##// END OF EJS Templates
reviewers: added observers as another way to define reviewers....
marcink -
r4500:bfede169 stable
parent child Browse files
Show More
@@ -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,5740 +1,5794 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 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import string
28 import string
29 import hashlib
29 import hashlib
30 import logging
30 import logging
31 import datetime
31 import datetime
32 import uuid
32 import uuid
33 import warnings
33 import warnings
34 import ipaddress
34 import ipaddress
35 import functools
35 import functools
36 import traceback
36 import traceback
37 import collections
37 import collections
38
38
39 from sqlalchemy import (
39 from sqlalchemy import (
40 or_, and_, not_, func, cast, TypeDecorator, event,
40 or_, and_, not_, func, cast, TypeDecorator, event,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 Text, Float, PickleType, BigInteger)
43 Text, Float, PickleType, BigInteger)
44 from sqlalchemy.sql.expression import true, false, case
44 from sqlalchemy.sql.expression import true, false, case
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 from sqlalchemy.orm import (
46 from sqlalchemy.orm import (
47 relationship, joinedload, class_mapper, validates, aliased)
47 relationship, joinedload, class_mapper, validates, aliased)
48 from sqlalchemy.ext.declarative import declared_attr
48 from sqlalchemy.ext.declarative import declared_attr
49 from sqlalchemy.ext.hybrid import hybrid_property
49 from sqlalchemy.ext.hybrid import hybrid_property
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 from sqlalchemy.dialects.mysql import LONGTEXT
51 from sqlalchemy.dialects.mysql import LONGTEXT
52 from zope.cachedescriptors.property import Lazy as LazyProperty
52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 from pyramid import compat
53 from pyramid import compat
54 from pyramid.threadlocal import get_current_request
54 from pyramid.threadlocal import get_current_request
55 from webhelpers2.text import remove_formatting
55 from webhelpers2.text import remove_formatting
56
56
57 from rhodecode.translation import _
57 from rhodecode.translation import _
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
60 from rhodecode.lib.utils2 import (
60 from rhodecode.lib.utils2 import (
61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 JsonRaw
65 JsonRaw
66 from rhodecode.lib.ext_json import json
66 from rhodecode.lib.ext_json import json
67 from rhodecode.lib.caching_query import FromCache
67 from rhodecode.lib.caching_query import FromCache
68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 from rhodecode.lib.encrypt2 import Encryptor
69 from rhodecode.lib.encrypt2 import Encryptor
70 from rhodecode.lib.exceptions import (
70 from rhodecode.lib.exceptions import (
71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 from rhodecode.model.meta import Base, Session
72 from rhodecode.model.meta import Base, Session
73
73
74 URL_SEP = '/'
74 URL_SEP = '/'
75 log = logging.getLogger(__name__)
75 log = logging.getLogger(__name__)
76
76
77 # =============================================================================
77 # =============================================================================
78 # BASE CLASSES
78 # BASE CLASSES
79 # =============================================================================
79 # =============================================================================
80
80
81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 # beaker.session.secret if first is not set.
82 # beaker.session.secret if first is not set.
83 # and initialized at environment.py
83 # and initialized at environment.py
84 ENCRYPTION_KEY = None
84 ENCRYPTION_KEY = None
85
85
86 # used to sort permissions by types, '#' used here is not allowed to be in
86 # used to sort permissions by types, '#' used here is not allowed to be in
87 # usernames, and it's very early in sorted string.printable table.
87 # usernames, and it's very early in sorted string.printable table.
88 PERMISSION_TYPE_SORT = {
88 PERMISSION_TYPE_SORT = {
89 'admin': '####',
89 'admin': '####',
90 'write': '###',
90 'write': '###',
91 'read': '##',
91 'read': '##',
92 'none': '#',
92 'none': '#',
93 }
93 }
94
94
95
95
96 def display_user_sort(obj):
96 def display_user_sort(obj):
97 """
97 """
98 Sort function used to sort permissions in .permissions() function of
98 Sort function used to sort permissions in .permissions() function of
99 Repository, RepoGroup, UserGroup. Also it put the default user in front
99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 of all other resources
100 of all other resources
101 """
101 """
102
102
103 if obj.username == User.DEFAULT_USER:
103 if obj.username == User.DEFAULT_USER:
104 return '#####'
104 return '#####'
105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 extra_sort_num = '1' # default
106 extra_sort_num = '1' # default
107
107
108 # NOTE(dan): inactive duplicates goes last
108 # NOTE(dan): inactive duplicates goes last
109 if getattr(obj, 'duplicate_perm', None):
109 if getattr(obj, 'duplicate_perm', None):
110 extra_sort_num = '9'
110 extra_sort_num = '9'
111 return prefix + extra_sort_num + obj.username
111 return prefix + extra_sort_num + obj.username
112
112
113
113
114 def display_user_group_sort(obj):
114 def display_user_group_sort(obj):
115 """
115 """
116 Sort function used to sort permissions in .permissions() function of
116 Sort function used to sort permissions in .permissions() function of
117 Repository, RepoGroup, UserGroup. Also it put the default user in front
117 Repository, RepoGroup, UserGroup. Also it put the default user in front
118 of all other resources
118 of all other resources
119 """
119 """
120
120
121 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
121 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
122 return prefix + obj.users_group_name
122 return prefix + obj.users_group_name
123
123
124
124
125 def _hash_key(k):
125 def _hash_key(k):
126 return sha1_safe(k)
126 return sha1_safe(k)
127
127
128
128
129 def in_filter_generator(qry, items, limit=500):
129 def in_filter_generator(qry, items, limit=500):
130 """
130 """
131 Splits IN() into multiple with OR
131 Splits IN() into multiple with OR
132 e.g.::
132 e.g.::
133 cnt = Repository.query().filter(
133 cnt = Repository.query().filter(
134 or_(
134 or_(
135 *in_filter_generator(Repository.repo_id, range(100000))
135 *in_filter_generator(Repository.repo_id, range(100000))
136 )).count()
136 )).count()
137 """
137 """
138 if not items:
138 if not items:
139 # empty list will cause empty query which might cause security issues
139 # empty list will cause empty query which might cause security issues
140 # this can lead to hidden unpleasant results
140 # this can lead to hidden unpleasant results
141 items = [-1]
141 items = [-1]
142
142
143 parts = []
143 parts = []
144 for chunk in xrange(0, len(items), limit):
144 for chunk in xrange(0, len(items), limit):
145 parts.append(
145 parts.append(
146 qry.in_(items[chunk: chunk + limit])
146 qry.in_(items[chunk: chunk + limit])
147 )
147 )
148
148
149 return parts
149 return parts
150
150
151
151
152 base_table_args = {
152 base_table_args = {
153 'extend_existing': True,
153 'extend_existing': True,
154 'mysql_engine': 'InnoDB',
154 'mysql_engine': 'InnoDB',
155 'mysql_charset': 'utf8',
155 'mysql_charset': 'utf8',
156 'sqlite_autoincrement': True
156 'sqlite_autoincrement': True
157 }
157 }
158
158
159
159
160 class EncryptedTextValue(TypeDecorator):
160 class EncryptedTextValue(TypeDecorator):
161 """
161 """
162 Special column for encrypted long text data, use like::
162 Special column for encrypted long text data, use like::
163
163
164 value = Column("encrypted_value", EncryptedValue(), nullable=False)
164 value = Column("encrypted_value", EncryptedValue(), nullable=False)
165
165
166 This column is intelligent so if value is in unencrypted form it return
166 This column is intelligent so if value is in unencrypted form it return
167 unencrypted form, but on save it always encrypts
167 unencrypted form, but on save it always encrypts
168 """
168 """
169 impl = Text
169 impl = Text
170
170
171 def process_bind_param(self, value, dialect):
171 def process_bind_param(self, value, dialect):
172 """
172 """
173 Setter for storing value
173 Setter for storing value
174 """
174 """
175 import rhodecode
175 import rhodecode
176 if not value:
176 if not value:
177 return value
177 return value
178
178
179 # protect against double encrypting if values is already encrypted
179 # protect against double encrypting if values is already encrypted
180 if value.startswith('enc$aes$') \
180 if value.startswith('enc$aes$') \
181 or value.startswith('enc$aes_hmac$') \
181 or value.startswith('enc$aes_hmac$') \
182 or value.startswith('enc2$'):
182 or value.startswith('enc2$'):
183 raise ValueError('value needs to be in unencrypted format, '
183 raise ValueError('value needs to be in unencrypted format, '
184 'ie. not starting with enc$ or enc2$')
184 'ie. not starting with enc$ or enc2$')
185
185
186 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
186 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
187 if algo == 'aes':
187 if algo == 'aes':
188 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
188 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
189 elif algo == 'fernet':
189 elif algo == 'fernet':
190 return Encryptor(ENCRYPTION_KEY).encrypt(value)
190 return Encryptor(ENCRYPTION_KEY).encrypt(value)
191 else:
191 else:
192 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
192 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
193
193
194 def process_result_value(self, value, dialect):
194 def process_result_value(self, value, dialect):
195 """
195 """
196 Getter for retrieving value
196 Getter for retrieving value
197 """
197 """
198
198
199 import rhodecode
199 import rhodecode
200 if not value:
200 if not value:
201 return value
201 return value
202
202
203 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
203 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
204 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
204 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
205 if algo == 'aes':
205 if algo == 'aes':
206 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
206 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
207 elif algo == 'fernet':
207 elif algo == 'fernet':
208 return Encryptor(ENCRYPTION_KEY).decrypt(value)
208 return Encryptor(ENCRYPTION_KEY).decrypt(value)
209 else:
209 else:
210 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
210 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
211 return decrypted_data
211 return decrypted_data
212
212
213
213
214 class BaseModel(object):
214 class BaseModel(object):
215 """
215 """
216 Base Model for all classes
216 Base Model for all classes
217 """
217 """
218
218
219 @classmethod
219 @classmethod
220 def _get_keys(cls):
220 def _get_keys(cls):
221 """return column names for this model """
221 """return column names for this model """
222 return class_mapper(cls).c.keys()
222 return class_mapper(cls).c.keys()
223
223
224 def get_dict(self):
224 def get_dict(self):
225 """
225 """
226 return dict with keys and values corresponding
226 return dict with keys and values corresponding
227 to this model data """
227 to this model data """
228
228
229 d = {}
229 d = {}
230 for k in self._get_keys():
230 for k in self._get_keys():
231 d[k] = getattr(self, k)
231 d[k] = getattr(self, k)
232
232
233 # also use __json__() if present to get additional fields
233 # also use __json__() if present to get additional fields
234 _json_attr = getattr(self, '__json__', None)
234 _json_attr = getattr(self, '__json__', None)
235 if _json_attr:
235 if _json_attr:
236 # update with attributes from __json__
236 # update with attributes from __json__
237 if callable(_json_attr):
237 if callable(_json_attr):
238 _json_attr = _json_attr()
238 _json_attr = _json_attr()
239 for k, val in _json_attr.iteritems():
239 for k, val in _json_attr.iteritems():
240 d[k] = val
240 d[k] = val
241 return d
241 return d
242
242
243 def get_appstruct(self):
243 def get_appstruct(self):
244 """return list with keys and values tuples corresponding
244 """return list with keys and values tuples corresponding
245 to this model data """
245 to this model data """
246
246
247 lst = []
247 lst = []
248 for k in self._get_keys():
248 for k in self._get_keys():
249 lst.append((k, getattr(self, k),))
249 lst.append((k, getattr(self, k),))
250 return lst
250 return lst
251
251
252 def populate_obj(self, populate_dict):
252 def populate_obj(self, populate_dict):
253 """populate model with data from given populate_dict"""
253 """populate model with data from given populate_dict"""
254
254
255 for k in self._get_keys():
255 for k in self._get_keys():
256 if k in populate_dict:
256 if k in populate_dict:
257 setattr(self, k, populate_dict[k])
257 setattr(self, k, populate_dict[k])
258
258
259 @classmethod
259 @classmethod
260 def query(cls):
260 def query(cls):
261 return Session().query(cls)
261 return Session().query(cls)
262
262
263 @classmethod
263 @classmethod
264 def get(cls, id_):
264 def get(cls, id_):
265 if id_:
265 if id_:
266 return cls.query().get(id_)
266 return cls.query().get(id_)
267
267
268 @classmethod
268 @classmethod
269 def get_or_404(cls, id_):
269 def get_or_404(cls, id_):
270 from pyramid.httpexceptions import HTTPNotFound
270 from pyramid.httpexceptions import HTTPNotFound
271
271
272 try:
272 try:
273 id_ = int(id_)
273 id_ = int(id_)
274 except (TypeError, ValueError):
274 except (TypeError, ValueError):
275 raise HTTPNotFound()
275 raise HTTPNotFound()
276
276
277 res = cls.query().get(id_)
277 res = cls.query().get(id_)
278 if not res:
278 if not res:
279 raise HTTPNotFound()
279 raise HTTPNotFound()
280 return res
280 return res
281
281
282 @classmethod
282 @classmethod
283 def getAll(cls):
283 def getAll(cls):
284 # deprecated and left for backward compatibility
284 # deprecated and left for backward compatibility
285 return cls.get_all()
285 return cls.get_all()
286
286
287 @classmethod
287 @classmethod
288 def get_all(cls):
288 def get_all(cls):
289 return cls.query().all()
289 return cls.query().all()
290
290
291 @classmethod
291 @classmethod
292 def delete(cls, id_):
292 def delete(cls, id_):
293 obj = cls.query().get(id_)
293 obj = cls.query().get(id_)
294 Session().delete(obj)
294 Session().delete(obj)
295
295
296 @classmethod
296 @classmethod
297 def identity_cache(cls, session, attr_name, value):
297 def identity_cache(cls, session, attr_name, value):
298 exist_in_session = []
298 exist_in_session = []
299 for (item_cls, pkey), instance in session.identity_map.items():
299 for (item_cls, pkey), instance in session.identity_map.items():
300 if cls == item_cls and getattr(instance, attr_name) == value:
300 if cls == item_cls and getattr(instance, attr_name) == value:
301 exist_in_session.append(instance)
301 exist_in_session.append(instance)
302 if exist_in_session:
302 if exist_in_session:
303 if len(exist_in_session) == 1:
303 if len(exist_in_session) == 1:
304 return exist_in_session[0]
304 return exist_in_session[0]
305 log.exception(
305 log.exception(
306 'multiple objects with attr %s and '
306 'multiple objects with attr %s and '
307 'value %s found with same name: %r',
307 'value %s found with same name: %r',
308 attr_name, value, exist_in_session)
308 attr_name, value, exist_in_session)
309
309
310 def __repr__(self):
310 def __repr__(self):
311 if hasattr(self, '__unicode__'):
311 if hasattr(self, '__unicode__'):
312 # python repr needs to return str
312 # python repr needs to return str
313 try:
313 try:
314 return safe_str(self.__unicode__())
314 return safe_str(self.__unicode__())
315 except UnicodeDecodeError:
315 except UnicodeDecodeError:
316 pass
316 pass
317 return '<DB:%s>' % (self.__class__.__name__)
317 return '<DB:%s>' % (self.__class__.__name__)
318
318
319
319
320 class RhodeCodeSetting(Base, BaseModel):
320 class RhodeCodeSetting(Base, BaseModel):
321 __tablename__ = 'rhodecode_settings'
321 __tablename__ = 'rhodecode_settings'
322 __table_args__ = (
322 __table_args__ = (
323 UniqueConstraint('app_settings_name'),
323 UniqueConstraint('app_settings_name'),
324 base_table_args
324 base_table_args
325 )
325 )
326
326
327 SETTINGS_TYPES = {
327 SETTINGS_TYPES = {
328 'str': safe_str,
328 'str': safe_str,
329 'int': safe_int,
329 'int': safe_int,
330 'unicode': safe_unicode,
330 'unicode': safe_unicode,
331 'bool': str2bool,
331 'bool': str2bool,
332 'list': functools.partial(aslist, sep=',')
332 'list': functools.partial(aslist, sep=',')
333 }
333 }
334 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
334 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
335 GLOBAL_CONF_KEY = 'app_settings'
335 GLOBAL_CONF_KEY = 'app_settings'
336
336
337 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
337 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
338 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
338 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
339 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
339 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
340 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
340 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
341
341
342 def __init__(self, key='', val='', type='unicode'):
342 def __init__(self, key='', val='', type='unicode'):
343 self.app_settings_name = key
343 self.app_settings_name = key
344 self.app_settings_type = type
344 self.app_settings_type = type
345 self.app_settings_value = val
345 self.app_settings_value = val
346
346
347 @validates('_app_settings_value')
347 @validates('_app_settings_value')
348 def validate_settings_value(self, key, val):
348 def validate_settings_value(self, key, val):
349 assert type(val) == unicode
349 assert type(val) == unicode
350 return val
350 return val
351
351
352 @hybrid_property
352 @hybrid_property
353 def app_settings_value(self):
353 def app_settings_value(self):
354 v = self._app_settings_value
354 v = self._app_settings_value
355 _type = self.app_settings_type
355 _type = self.app_settings_type
356 if _type:
356 if _type:
357 _type = self.app_settings_type.split('.')[0]
357 _type = self.app_settings_type.split('.')[0]
358 # decode the encrypted value
358 # decode the encrypted value
359 if 'encrypted' in self.app_settings_type:
359 if 'encrypted' in self.app_settings_type:
360 cipher = EncryptedTextValue()
360 cipher = EncryptedTextValue()
361 v = safe_unicode(cipher.process_result_value(v, None))
361 v = safe_unicode(cipher.process_result_value(v, None))
362
362
363 converter = self.SETTINGS_TYPES.get(_type) or \
363 converter = self.SETTINGS_TYPES.get(_type) or \
364 self.SETTINGS_TYPES['unicode']
364 self.SETTINGS_TYPES['unicode']
365 return converter(v)
365 return converter(v)
366
366
367 @app_settings_value.setter
367 @app_settings_value.setter
368 def app_settings_value(self, val):
368 def app_settings_value(self, val):
369 """
369 """
370 Setter that will always make sure we use unicode in app_settings_value
370 Setter that will always make sure we use unicode in app_settings_value
371
371
372 :param val:
372 :param val:
373 """
373 """
374 val = safe_unicode(val)
374 val = safe_unicode(val)
375 # encode the encrypted value
375 # encode the encrypted value
376 if 'encrypted' in self.app_settings_type:
376 if 'encrypted' in self.app_settings_type:
377 cipher = EncryptedTextValue()
377 cipher = EncryptedTextValue()
378 val = safe_unicode(cipher.process_bind_param(val, None))
378 val = safe_unicode(cipher.process_bind_param(val, None))
379 self._app_settings_value = val
379 self._app_settings_value = val
380
380
381 @hybrid_property
381 @hybrid_property
382 def app_settings_type(self):
382 def app_settings_type(self):
383 return self._app_settings_type
383 return self._app_settings_type
384
384
385 @app_settings_type.setter
385 @app_settings_type.setter
386 def app_settings_type(self, val):
386 def app_settings_type(self, val):
387 if val.split('.')[0] not in self.SETTINGS_TYPES:
387 if val.split('.')[0] not in self.SETTINGS_TYPES:
388 raise Exception('type must be one of %s got %s'
388 raise Exception('type must be one of %s got %s'
389 % (self.SETTINGS_TYPES.keys(), val))
389 % (self.SETTINGS_TYPES.keys(), val))
390 self._app_settings_type = val
390 self._app_settings_type = val
391
391
392 @classmethod
392 @classmethod
393 def get_by_prefix(cls, prefix):
393 def get_by_prefix(cls, prefix):
394 return RhodeCodeSetting.query()\
394 return RhodeCodeSetting.query()\
395 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
395 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
396 .all()
396 .all()
397
397
398 def __unicode__(self):
398 def __unicode__(self):
399 return u"<%s('%s:%s[%s]')>" % (
399 return u"<%s('%s:%s[%s]')>" % (
400 self.__class__.__name__,
400 self.__class__.__name__,
401 self.app_settings_name, self.app_settings_value,
401 self.app_settings_name, self.app_settings_value,
402 self.app_settings_type
402 self.app_settings_type
403 )
403 )
404
404
405
405
406 class RhodeCodeUi(Base, BaseModel):
406 class RhodeCodeUi(Base, BaseModel):
407 __tablename__ = 'rhodecode_ui'
407 __tablename__ = 'rhodecode_ui'
408 __table_args__ = (
408 __table_args__ = (
409 UniqueConstraint('ui_key'),
409 UniqueConstraint('ui_key'),
410 base_table_args
410 base_table_args
411 )
411 )
412
412
413 HOOK_REPO_SIZE = 'changegroup.repo_size'
413 HOOK_REPO_SIZE = 'changegroup.repo_size'
414 # HG
414 # HG
415 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
415 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
416 HOOK_PULL = 'outgoing.pull_logger'
416 HOOK_PULL = 'outgoing.pull_logger'
417 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
417 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
418 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
418 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
419 HOOK_PUSH = 'changegroup.push_logger'
419 HOOK_PUSH = 'changegroup.push_logger'
420 HOOK_PUSH_KEY = 'pushkey.key_push'
420 HOOK_PUSH_KEY = 'pushkey.key_push'
421
421
422 HOOKS_BUILTIN = [
422 HOOKS_BUILTIN = [
423 HOOK_PRE_PULL,
423 HOOK_PRE_PULL,
424 HOOK_PULL,
424 HOOK_PULL,
425 HOOK_PRE_PUSH,
425 HOOK_PRE_PUSH,
426 HOOK_PRETX_PUSH,
426 HOOK_PRETX_PUSH,
427 HOOK_PUSH,
427 HOOK_PUSH,
428 HOOK_PUSH_KEY,
428 HOOK_PUSH_KEY,
429 ]
429 ]
430
430
431 # TODO: johbo: Unify way how hooks are configured for git and hg,
431 # TODO: johbo: Unify way how hooks are configured for git and hg,
432 # git part is currently hardcoded.
432 # git part is currently hardcoded.
433
433
434 # SVN PATTERNS
434 # SVN PATTERNS
435 SVN_BRANCH_ID = 'vcs_svn_branch'
435 SVN_BRANCH_ID = 'vcs_svn_branch'
436 SVN_TAG_ID = 'vcs_svn_tag'
436 SVN_TAG_ID = 'vcs_svn_tag'
437
437
438 ui_id = Column(
438 ui_id = Column(
439 "ui_id", Integer(), nullable=False, unique=True, default=None,
439 "ui_id", Integer(), nullable=False, unique=True, default=None,
440 primary_key=True)
440 primary_key=True)
441 ui_section = Column(
441 ui_section = Column(
442 "ui_section", String(255), nullable=True, unique=None, default=None)
442 "ui_section", String(255), nullable=True, unique=None, default=None)
443 ui_key = Column(
443 ui_key = Column(
444 "ui_key", String(255), nullable=True, unique=None, default=None)
444 "ui_key", String(255), nullable=True, unique=None, default=None)
445 ui_value = Column(
445 ui_value = Column(
446 "ui_value", String(255), nullable=True, unique=None, default=None)
446 "ui_value", String(255), nullable=True, unique=None, default=None)
447 ui_active = Column(
447 ui_active = Column(
448 "ui_active", Boolean(), nullable=True, unique=None, default=True)
448 "ui_active", Boolean(), nullable=True, unique=None, default=True)
449
449
450 def __repr__(self):
450 def __repr__(self):
451 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
451 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
452 self.ui_key, self.ui_value)
452 self.ui_key, self.ui_value)
453
453
454
454
455 class RepoRhodeCodeSetting(Base, BaseModel):
455 class RepoRhodeCodeSetting(Base, BaseModel):
456 __tablename__ = 'repo_rhodecode_settings'
456 __tablename__ = 'repo_rhodecode_settings'
457 __table_args__ = (
457 __table_args__ = (
458 UniqueConstraint(
458 UniqueConstraint(
459 'app_settings_name', 'repository_id',
459 'app_settings_name', 'repository_id',
460 name='uq_repo_rhodecode_setting_name_repo_id'),
460 name='uq_repo_rhodecode_setting_name_repo_id'),
461 base_table_args
461 base_table_args
462 )
462 )
463
463
464 repository_id = Column(
464 repository_id = Column(
465 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
465 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
466 nullable=False)
466 nullable=False)
467 app_settings_id = Column(
467 app_settings_id = Column(
468 "app_settings_id", Integer(), nullable=False, unique=True,
468 "app_settings_id", Integer(), nullable=False, unique=True,
469 default=None, primary_key=True)
469 default=None, primary_key=True)
470 app_settings_name = Column(
470 app_settings_name = Column(
471 "app_settings_name", String(255), nullable=True, unique=None,
471 "app_settings_name", String(255), nullable=True, unique=None,
472 default=None)
472 default=None)
473 _app_settings_value = Column(
473 _app_settings_value = Column(
474 "app_settings_value", String(4096), nullable=True, unique=None,
474 "app_settings_value", String(4096), nullable=True, unique=None,
475 default=None)
475 default=None)
476 _app_settings_type = Column(
476 _app_settings_type = Column(
477 "app_settings_type", String(255), nullable=True, unique=None,
477 "app_settings_type", String(255), nullable=True, unique=None,
478 default=None)
478 default=None)
479
479
480 repository = relationship('Repository')
480 repository = relationship('Repository')
481
481
482 def __init__(self, repository_id, key='', val='', type='unicode'):
482 def __init__(self, repository_id, key='', val='', type='unicode'):
483 self.repository_id = repository_id
483 self.repository_id = repository_id
484 self.app_settings_name = key
484 self.app_settings_name = key
485 self.app_settings_type = type
485 self.app_settings_type = type
486 self.app_settings_value = val
486 self.app_settings_value = val
487
487
488 @validates('_app_settings_value')
488 @validates('_app_settings_value')
489 def validate_settings_value(self, key, val):
489 def validate_settings_value(self, key, val):
490 assert type(val) == unicode
490 assert type(val) == unicode
491 return val
491 return val
492
492
493 @hybrid_property
493 @hybrid_property
494 def app_settings_value(self):
494 def app_settings_value(self):
495 v = self._app_settings_value
495 v = self._app_settings_value
496 type_ = self.app_settings_type
496 type_ = self.app_settings_type
497 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
497 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
498 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
498 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
499 return converter(v)
499 return converter(v)
500
500
501 @app_settings_value.setter
501 @app_settings_value.setter
502 def app_settings_value(self, val):
502 def app_settings_value(self, val):
503 """
503 """
504 Setter that will always make sure we use unicode in app_settings_value
504 Setter that will always make sure we use unicode in app_settings_value
505
505
506 :param val:
506 :param val:
507 """
507 """
508 self._app_settings_value = safe_unicode(val)
508 self._app_settings_value = safe_unicode(val)
509
509
510 @hybrid_property
510 @hybrid_property
511 def app_settings_type(self):
511 def app_settings_type(self):
512 return self._app_settings_type
512 return self._app_settings_type
513
513
514 @app_settings_type.setter
514 @app_settings_type.setter
515 def app_settings_type(self, val):
515 def app_settings_type(self, val):
516 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
516 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
517 if val not in SETTINGS_TYPES:
517 if val not in SETTINGS_TYPES:
518 raise Exception('type must be one of %s got %s'
518 raise Exception('type must be one of %s got %s'
519 % (SETTINGS_TYPES.keys(), val))
519 % (SETTINGS_TYPES.keys(), val))
520 self._app_settings_type = val
520 self._app_settings_type = val
521
521
522 def __unicode__(self):
522 def __unicode__(self):
523 return u"<%s('%s:%s:%s[%s]')>" % (
523 return u"<%s('%s:%s:%s[%s]')>" % (
524 self.__class__.__name__, self.repository.repo_name,
524 self.__class__.__name__, self.repository.repo_name,
525 self.app_settings_name, self.app_settings_value,
525 self.app_settings_name, self.app_settings_value,
526 self.app_settings_type
526 self.app_settings_type
527 )
527 )
528
528
529
529
530 class RepoRhodeCodeUi(Base, BaseModel):
530 class RepoRhodeCodeUi(Base, BaseModel):
531 __tablename__ = 'repo_rhodecode_ui'
531 __tablename__ = 'repo_rhodecode_ui'
532 __table_args__ = (
532 __table_args__ = (
533 UniqueConstraint(
533 UniqueConstraint(
534 'repository_id', 'ui_section', 'ui_key',
534 'repository_id', 'ui_section', 'ui_key',
535 name='uq_repo_rhodecode_ui_repository_id_section_key'),
535 name='uq_repo_rhodecode_ui_repository_id_section_key'),
536 base_table_args
536 base_table_args
537 )
537 )
538
538
539 repository_id = Column(
539 repository_id = Column(
540 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
540 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
541 nullable=False)
541 nullable=False)
542 ui_id = Column(
542 ui_id = Column(
543 "ui_id", Integer(), nullable=False, unique=True, default=None,
543 "ui_id", Integer(), nullable=False, unique=True, default=None,
544 primary_key=True)
544 primary_key=True)
545 ui_section = Column(
545 ui_section = Column(
546 "ui_section", String(255), nullable=True, unique=None, default=None)
546 "ui_section", String(255), nullable=True, unique=None, default=None)
547 ui_key = Column(
547 ui_key = Column(
548 "ui_key", String(255), nullable=True, unique=None, default=None)
548 "ui_key", String(255), nullable=True, unique=None, default=None)
549 ui_value = Column(
549 ui_value = Column(
550 "ui_value", String(255), nullable=True, unique=None, default=None)
550 "ui_value", String(255), nullable=True, unique=None, default=None)
551 ui_active = Column(
551 ui_active = Column(
552 "ui_active", Boolean(), nullable=True, unique=None, default=True)
552 "ui_active", Boolean(), nullable=True, unique=None, default=True)
553
553
554 repository = relationship('Repository')
554 repository = relationship('Repository')
555
555
556 def __repr__(self):
556 def __repr__(self):
557 return '<%s[%s:%s]%s=>%s]>' % (
557 return '<%s[%s:%s]%s=>%s]>' % (
558 self.__class__.__name__, self.repository.repo_name,
558 self.__class__.__name__, self.repository.repo_name,
559 self.ui_section, self.ui_key, self.ui_value)
559 self.ui_section, self.ui_key, self.ui_value)
560
560
561
561
562 class User(Base, BaseModel):
562 class User(Base, BaseModel):
563 __tablename__ = 'users'
563 __tablename__ = 'users'
564 __table_args__ = (
564 __table_args__ = (
565 UniqueConstraint('username'), UniqueConstraint('email'),
565 UniqueConstraint('username'), UniqueConstraint('email'),
566 Index('u_username_idx', 'username'),
566 Index('u_username_idx', 'username'),
567 Index('u_email_idx', 'email'),
567 Index('u_email_idx', 'email'),
568 base_table_args
568 base_table_args
569 )
569 )
570
570
571 DEFAULT_USER = 'default'
571 DEFAULT_USER = 'default'
572 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
572 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
573 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
573 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
574
574
575 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
575 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
576 username = Column("username", String(255), nullable=True, unique=None, default=None)
576 username = Column("username", String(255), nullable=True, unique=None, default=None)
577 password = Column("password", String(255), nullable=True, unique=None, default=None)
577 password = Column("password", String(255), nullable=True, unique=None, default=None)
578 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
578 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
579 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
579 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
580 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
580 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
581 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
581 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
582 _email = Column("email", String(255), nullable=True, unique=None, default=None)
582 _email = Column("email", String(255), nullable=True, unique=None, default=None)
583 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
583 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
584 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
584 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
585 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
585 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
586
586
587 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
587 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
588 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
588 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
589 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
589 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
590 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
590 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
591 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
591 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
592 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
592 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
593
593
594 user_log = relationship('UserLog')
594 user_log = relationship('UserLog')
595 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
595 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
596
596
597 repositories = relationship('Repository')
597 repositories = relationship('Repository')
598 repository_groups = relationship('RepoGroup')
598 repository_groups = relationship('RepoGroup')
599 user_groups = relationship('UserGroup')
599 user_groups = relationship('UserGroup')
600
600
601 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
601 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
602 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
602 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
603
603
604 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
604 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
605 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
605 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
606 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
606 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
607
607
608 group_member = relationship('UserGroupMember', cascade='all')
608 group_member = relationship('UserGroupMember', cascade='all')
609
609
610 notifications = relationship('UserNotification', cascade='all')
610 notifications = relationship('UserNotification', cascade='all')
611 # notifications assigned to this user
611 # notifications assigned to this user
612 user_created_notifications = relationship('Notification', cascade='all')
612 user_created_notifications = relationship('Notification', cascade='all')
613 # comments created by this user
613 # comments created by this user
614 user_comments = relationship('ChangesetComment', cascade='all')
614 user_comments = relationship('ChangesetComment', cascade='all')
615 # user profile extra info
615 # user profile extra info
616 user_emails = relationship('UserEmailMap', cascade='all')
616 user_emails = relationship('UserEmailMap', cascade='all')
617 user_ip_map = relationship('UserIpMap', cascade='all')
617 user_ip_map = relationship('UserIpMap', cascade='all')
618 user_auth_tokens = relationship('UserApiKeys', cascade='all')
618 user_auth_tokens = relationship('UserApiKeys', cascade='all')
619 user_ssh_keys = relationship('UserSshKeys', cascade='all')
619 user_ssh_keys = relationship('UserSshKeys', cascade='all')
620
620
621 # gists
621 # gists
622 user_gists = relationship('Gist', cascade='all')
622 user_gists = relationship('Gist', cascade='all')
623 # user pull requests
623 # user pull requests
624 user_pull_requests = relationship('PullRequest', cascade='all')
624 user_pull_requests = relationship('PullRequest', cascade='all')
625
625
626 # external identities
626 # external identities
627 external_identities = relationship(
627 external_identities = relationship(
628 'ExternalIdentity',
628 'ExternalIdentity',
629 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
629 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
630 cascade='all')
630 cascade='all')
631 # review rules
631 # review rules
632 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
632 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
633
633
634 # artifacts owned
634 # artifacts owned
635 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
635 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
636
636
637 # no cascade, set NULL
637 # no cascade, set NULL
638 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
638 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
639
639
640 def __unicode__(self):
640 def __unicode__(self):
641 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
641 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
642 self.user_id, self.username)
642 self.user_id, self.username)
643
643
644 @hybrid_property
644 @hybrid_property
645 def email(self):
645 def email(self):
646 return self._email
646 return self._email
647
647
648 @email.setter
648 @email.setter
649 def email(self, val):
649 def email(self, val):
650 self._email = val.lower() if val else None
650 self._email = val.lower() if val else None
651
651
652 @hybrid_property
652 @hybrid_property
653 def first_name(self):
653 def first_name(self):
654 from rhodecode.lib import helpers as h
654 from rhodecode.lib import helpers as h
655 if self.name:
655 if self.name:
656 return h.escape(self.name)
656 return h.escape(self.name)
657 return self.name
657 return self.name
658
658
659 @hybrid_property
659 @hybrid_property
660 def last_name(self):
660 def last_name(self):
661 from rhodecode.lib import helpers as h
661 from rhodecode.lib import helpers as h
662 if self.lastname:
662 if self.lastname:
663 return h.escape(self.lastname)
663 return h.escape(self.lastname)
664 return self.lastname
664 return self.lastname
665
665
666 @hybrid_property
666 @hybrid_property
667 def api_key(self):
667 def api_key(self):
668 """
668 """
669 Fetch if exist an auth-token with role ALL connected to this user
669 Fetch if exist an auth-token with role ALL connected to this user
670 """
670 """
671 user_auth_token = UserApiKeys.query()\
671 user_auth_token = UserApiKeys.query()\
672 .filter(UserApiKeys.user_id == self.user_id)\
672 .filter(UserApiKeys.user_id == self.user_id)\
673 .filter(or_(UserApiKeys.expires == -1,
673 .filter(or_(UserApiKeys.expires == -1,
674 UserApiKeys.expires >= time.time()))\
674 UserApiKeys.expires >= time.time()))\
675 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
675 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
676 if user_auth_token:
676 if user_auth_token:
677 user_auth_token = user_auth_token.api_key
677 user_auth_token = user_auth_token.api_key
678
678
679 return user_auth_token
679 return user_auth_token
680
680
681 @api_key.setter
681 @api_key.setter
682 def api_key(self, val):
682 def api_key(self, val):
683 # don't allow to set API key this is deprecated for now
683 # don't allow to set API key this is deprecated for now
684 self._api_key = None
684 self._api_key = None
685
685
686 @property
686 @property
687 def reviewer_pull_requests(self):
687 def reviewer_pull_requests(self):
688 return PullRequestReviewers.query() \
688 return PullRequestReviewers.query() \
689 .options(joinedload(PullRequestReviewers.pull_request)) \
689 .options(joinedload(PullRequestReviewers.pull_request)) \
690 .filter(PullRequestReviewers.user_id == self.user_id) \
690 .filter(PullRequestReviewers.user_id == self.user_id) \
691 .all()
691 .all()
692
692
693 @property
693 @property
694 def firstname(self):
694 def firstname(self):
695 # alias for future
695 # alias for future
696 return self.name
696 return self.name
697
697
698 @property
698 @property
699 def emails(self):
699 def emails(self):
700 other = UserEmailMap.query()\
700 other = UserEmailMap.query()\
701 .filter(UserEmailMap.user == self) \
701 .filter(UserEmailMap.user == self) \
702 .order_by(UserEmailMap.email_id.asc()) \
702 .order_by(UserEmailMap.email_id.asc()) \
703 .all()
703 .all()
704 return [self.email] + [x.email for x in other]
704 return [self.email] + [x.email for x in other]
705
705
706 def emails_cached(self):
706 def emails_cached(self):
707 emails = UserEmailMap.query()\
707 emails = UserEmailMap.query()\
708 .filter(UserEmailMap.user == self) \
708 .filter(UserEmailMap.user == self) \
709 .order_by(UserEmailMap.email_id.asc())
709 .order_by(UserEmailMap.email_id.asc())
710
710
711 emails = emails.options(
711 emails = emails.options(
712 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
712 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
713 )
713 )
714
714
715 return [self.email] + [x.email for x in emails]
715 return [self.email] + [x.email for x in emails]
716
716
717 @property
717 @property
718 def auth_tokens(self):
718 def auth_tokens(self):
719 auth_tokens = self.get_auth_tokens()
719 auth_tokens = self.get_auth_tokens()
720 return [x.api_key for x in auth_tokens]
720 return [x.api_key for x in auth_tokens]
721
721
722 def get_auth_tokens(self):
722 def get_auth_tokens(self):
723 return UserApiKeys.query()\
723 return UserApiKeys.query()\
724 .filter(UserApiKeys.user == self)\
724 .filter(UserApiKeys.user == self)\
725 .order_by(UserApiKeys.user_api_key_id.asc())\
725 .order_by(UserApiKeys.user_api_key_id.asc())\
726 .all()
726 .all()
727
727
728 @LazyProperty
728 @LazyProperty
729 def feed_token(self):
729 def feed_token(self):
730 return self.get_feed_token()
730 return self.get_feed_token()
731
731
732 def get_feed_token(self, cache=True):
732 def get_feed_token(self, cache=True):
733 feed_tokens = UserApiKeys.query()\
733 feed_tokens = UserApiKeys.query()\
734 .filter(UserApiKeys.user == self)\
734 .filter(UserApiKeys.user == self)\
735 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
735 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
736 if cache:
736 if cache:
737 feed_tokens = feed_tokens.options(
737 feed_tokens = feed_tokens.options(
738 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
738 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
739
739
740 feed_tokens = feed_tokens.all()
740 feed_tokens = feed_tokens.all()
741 if feed_tokens:
741 if feed_tokens:
742 return feed_tokens[0].api_key
742 return feed_tokens[0].api_key
743 return 'NO_FEED_TOKEN_AVAILABLE'
743 return 'NO_FEED_TOKEN_AVAILABLE'
744
744
745 @LazyProperty
745 @LazyProperty
746 def artifact_token(self):
746 def artifact_token(self):
747 return self.get_artifact_token()
747 return self.get_artifact_token()
748
748
749 def get_artifact_token(self, cache=True):
749 def get_artifact_token(self, cache=True):
750 artifacts_tokens = UserApiKeys.query()\
750 artifacts_tokens = UserApiKeys.query()\
751 .filter(UserApiKeys.user == self)\
751 .filter(UserApiKeys.user == self)\
752 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
752 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
753 if cache:
753 if cache:
754 artifacts_tokens = artifacts_tokens.options(
754 artifacts_tokens = artifacts_tokens.options(
755 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
755 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
756
756
757 artifacts_tokens = artifacts_tokens.all()
757 artifacts_tokens = artifacts_tokens.all()
758 if artifacts_tokens:
758 if artifacts_tokens:
759 return artifacts_tokens[0].api_key
759 return artifacts_tokens[0].api_key
760 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
760 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
761
761
762 @classmethod
762 @classmethod
763 def get(cls, user_id, cache=False):
763 def get(cls, user_id, cache=False):
764 if not user_id:
764 if not user_id:
765 return
765 return
766
766
767 user = cls.query()
767 user = cls.query()
768 if cache:
768 if cache:
769 user = user.options(
769 user = user.options(
770 FromCache("sql_cache_short", "get_users_%s" % user_id))
770 FromCache("sql_cache_short", "get_users_%s" % user_id))
771 return user.get(user_id)
771 return user.get(user_id)
772
772
773 @classmethod
773 @classmethod
774 def extra_valid_auth_tokens(cls, user, role=None):
774 def extra_valid_auth_tokens(cls, user, role=None):
775 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
775 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
776 .filter(or_(UserApiKeys.expires == -1,
776 .filter(or_(UserApiKeys.expires == -1,
777 UserApiKeys.expires >= time.time()))
777 UserApiKeys.expires >= time.time()))
778 if role:
778 if role:
779 tokens = tokens.filter(or_(UserApiKeys.role == role,
779 tokens = tokens.filter(or_(UserApiKeys.role == role,
780 UserApiKeys.role == UserApiKeys.ROLE_ALL))
780 UserApiKeys.role == UserApiKeys.ROLE_ALL))
781 return tokens.all()
781 return tokens.all()
782
782
783 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
783 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
784 from rhodecode.lib import auth
784 from rhodecode.lib import auth
785
785
786 log.debug('Trying to authenticate user: %s via auth-token, '
786 log.debug('Trying to authenticate user: %s via auth-token, '
787 'and roles: %s', self, roles)
787 'and roles: %s', self, roles)
788
788
789 if not auth_token:
789 if not auth_token:
790 return False
790 return False
791
791
792 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
792 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
793 tokens_q = UserApiKeys.query()\
793 tokens_q = UserApiKeys.query()\
794 .filter(UserApiKeys.user_id == self.user_id)\
794 .filter(UserApiKeys.user_id == self.user_id)\
795 .filter(or_(UserApiKeys.expires == -1,
795 .filter(or_(UserApiKeys.expires == -1,
796 UserApiKeys.expires >= time.time()))
796 UserApiKeys.expires >= time.time()))
797
797
798 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
798 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
799
799
800 crypto_backend = auth.crypto_backend()
800 crypto_backend = auth.crypto_backend()
801 enc_token_map = {}
801 enc_token_map = {}
802 plain_token_map = {}
802 plain_token_map = {}
803 for token in tokens_q:
803 for token in tokens_q:
804 if token.api_key.startswith(crypto_backend.ENC_PREF):
804 if token.api_key.startswith(crypto_backend.ENC_PREF):
805 enc_token_map[token.api_key] = token
805 enc_token_map[token.api_key] = token
806 else:
806 else:
807 plain_token_map[token.api_key] = token
807 plain_token_map[token.api_key] = token
808 log.debug(
808 log.debug(
809 'Found %s plain and %s encrypted tokens to check for authentication for this user',
809 'Found %s plain and %s encrypted tokens to check for authentication for this user',
810 len(plain_token_map), len(enc_token_map))
810 len(plain_token_map), len(enc_token_map))
811
811
812 # plain token match comes first
812 # plain token match comes first
813 match = plain_token_map.get(auth_token)
813 match = plain_token_map.get(auth_token)
814
814
815 # check encrypted tokens now
815 # check encrypted tokens now
816 if not match:
816 if not match:
817 for token_hash, token in enc_token_map.items():
817 for token_hash, token in enc_token_map.items():
818 # NOTE(marcink): this is expensive to calculate, but most secure
818 # NOTE(marcink): this is expensive to calculate, but most secure
819 if crypto_backend.hash_check(auth_token, token_hash):
819 if crypto_backend.hash_check(auth_token, token_hash):
820 match = token
820 match = token
821 break
821 break
822
822
823 if match:
823 if match:
824 log.debug('Found matching token %s', match)
824 log.debug('Found matching token %s', match)
825 if match.repo_id:
825 if match.repo_id:
826 log.debug('Found scope, checking for scope match of token %s', match)
826 log.debug('Found scope, checking for scope match of token %s', match)
827 if match.repo_id == scope_repo_id:
827 if match.repo_id == scope_repo_id:
828 return True
828 return True
829 else:
829 else:
830 log.debug(
830 log.debug(
831 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
831 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
832 'and calling scope is:%s, skipping further checks',
832 'and calling scope is:%s, skipping further checks',
833 match.repo, scope_repo_id)
833 match.repo, scope_repo_id)
834 return False
834 return False
835 else:
835 else:
836 return True
836 return True
837
837
838 return False
838 return False
839
839
840 @property
840 @property
841 def ip_addresses(self):
841 def ip_addresses(self):
842 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
842 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
843 return [x.ip_addr for x in ret]
843 return [x.ip_addr for x in ret]
844
844
845 @property
845 @property
846 def username_and_name(self):
846 def username_and_name(self):
847 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
847 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
848
848
849 @property
849 @property
850 def username_or_name_or_email(self):
850 def username_or_name_or_email(self):
851 full_name = self.full_name if self.full_name is not ' ' else None
851 full_name = self.full_name if self.full_name is not ' ' else None
852 return self.username or full_name or self.email
852 return self.username or full_name or self.email
853
853
854 @property
854 @property
855 def full_name(self):
855 def full_name(self):
856 return '%s %s' % (self.first_name, self.last_name)
856 return '%s %s' % (self.first_name, self.last_name)
857
857
858 @property
858 @property
859 def full_name_or_username(self):
859 def full_name_or_username(self):
860 return ('%s %s' % (self.first_name, self.last_name)
860 return ('%s %s' % (self.first_name, self.last_name)
861 if (self.first_name and self.last_name) else self.username)
861 if (self.first_name and self.last_name) else self.username)
862
862
863 @property
863 @property
864 def full_contact(self):
864 def full_contact(self):
865 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
865 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
866
866
867 @property
867 @property
868 def short_contact(self):
868 def short_contact(self):
869 return '%s %s' % (self.first_name, self.last_name)
869 return '%s %s' % (self.first_name, self.last_name)
870
870
871 @property
871 @property
872 def is_admin(self):
872 def is_admin(self):
873 return self.admin
873 return self.admin
874
874
875 @property
875 @property
876 def language(self):
876 def language(self):
877 return self.user_data.get('language')
877 return self.user_data.get('language')
878
878
879 def AuthUser(self, **kwargs):
879 def AuthUser(self, **kwargs):
880 """
880 """
881 Returns instance of AuthUser for this user
881 Returns instance of AuthUser for this user
882 """
882 """
883 from rhodecode.lib.auth import AuthUser
883 from rhodecode.lib.auth import AuthUser
884 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
884 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
885
885
886 @hybrid_property
886 @hybrid_property
887 def user_data(self):
887 def user_data(self):
888 if not self._user_data:
888 if not self._user_data:
889 return {}
889 return {}
890
890
891 try:
891 try:
892 return json.loads(self._user_data)
892 return json.loads(self._user_data)
893 except TypeError:
893 except TypeError:
894 return {}
894 return {}
895
895
896 @user_data.setter
896 @user_data.setter
897 def user_data(self, val):
897 def user_data(self, val):
898 if not isinstance(val, dict):
898 if not isinstance(val, dict):
899 raise Exception('user_data must be dict, got %s' % type(val))
899 raise Exception('user_data must be dict, got %s' % type(val))
900 try:
900 try:
901 self._user_data = json.dumps(val)
901 self._user_data = json.dumps(val)
902 except Exception:
902 except Exception:
903 log.error(traceback.format_exc())
903 log.error(traceback.format_exc())
904
904
905 @classmethod
905 @classmethod
906 def get_by_username(cls, username, case_insensitive=False,
906 def get_by_username(cls, username, case_insensitive=False,
907 cache=False, identity_cache=False):
907 cache=False, identity_cache=False):
908 session = Session()
908 session = Session()
909
909
910 if case_insensitive:
910 if case_insensitive:
911 q = cls.query().filter(
911 q = cls.query().filter(
912 func.lower(cls.username) == func.lower(username))
912 func.lower(cls.username) == func.lower(username))
913 else:
913 else:
914 q = cls.query().filter(cls.username == username)
914 q = cls.query().filter(cls.username == username)
915
915
916 if cache:
916 if cache:
917 if identity_cache:
917 if identity_cache:
918 val = cls.identity_cache(session, 'username', username)
918 val = cls.identity_cache(session, 'username', username)
919 if val:
919 if val:
920 return val
920 return val
921 else:
921 else:
922 cache_key = "get_user_by_name_%s" % _hash_key(username)
922 cache_key = "get_user_by_name_%s" % _hash_key(username)
923 q = q.options(
923 q = q.options(
924 FromCache("sql_cache_short", cache_key))
924 FromCache("sql_cache_short", cache_key))
925
925
926 return q.scalar()
926 return q.scalar()
927
927
928 @classmethod
928 @classmethod
929 def get_by_auth_token(cls, auth_token, cache=False):
929 def get_by_auth_token(cls, auth_token, cache=False):
930 q = UserApiKeys.query()\
930 q = UserApiKeys.query()\
931 .filter(UserApiKeys.api_key == auth_token)\
931 .filter(UserApiKeys.api_key == auth_token)\
932 .filter(or_(UserApiKeys.expires == -1,
932 .filter(or_(UserApiKeys.expires == -1,
933 UserApiKeys.expires >= time.time()))
933 UserApiKeys.expires >= time.time()))
934 if cache:
934 if cache:
935 q = q.options(
935 q = q.options(
936 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
936 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
937
937
938 match = q.first()
938 match = q.first()
939 if match:
939 if match:
940 return match.user
940 return match.user
941
941
942 @classmethod
942 @classmethod
943 def get_by_email(cls, email, case_insensitive=False, cache=False):
943 def get_by_email(cls, email, case_insensitive=False, cache=False):
944
944
945 if case_insensitive:
945 if case_insensitive:
946 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
946 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
947
947
948 else:
948 else:
949 q = cls.query().filter(cls.email == email)
949 q = cls.query().filter(cls.email == email)
950
950
951 email_key = _hash_key(email)
951 email_key = _hash_key(email)
952 if cache:
952 if cache:
953 q = q.options(
953 q = q.options(
954 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
954 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
955
955
956 ret = q.scalar()
956 ret = q.scalar()
957 if ret is None:
957 if ret is None:
958 q = UserEmailMap.query()
958 q = UserEmailMap.query()
959 # try fetching in alternate email map
959 # try fetching in alternate email map
960 if case_insensitive:
960 if case_insensitive:
961 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
961 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
962 else:
962 else:
963 q = q.filter(UserEmailMap.email == email)
963 q = q.filter(UserEmailMap.email == email)
964 q = q.options(joinedload(UserEmailMap.user))
964 q = q.options(joinedload(UserEmailMap.user))
965 if cache:
965 if cache:
966 q = q.options(
966 q = q.options(
967 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
967 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
968 ret = getattr(q.scalar(), 'user', None)
968 ret = getattr(q.scalar(), 'user', None)
969
969
970 return ret
970 return ret
971
971
972 @classmethod
972 @classmethod
973 def get_from_cs_author(cls, author):
973 def get_from_cs_author(cls, author):
974 """
974 """
975 Tries to get User objects out of commit author string
975 Tries to get User objects out of commit author string
976
976
977 :param author:
977 :param author:
978 """
978 """
979 from rhodecode.lib.helpers import email, author_name
979 from rhodecode.lib.helpers import email, author_name
980 # Valid email in the attribute passed, see if they're in the system
980 # Valid email in the attribute passed, see if they're in the system
981 _email = email(author)
981 _email = email(author)
982 if _email:
982 if _email:
983 user = cls.get_by_email(_email, case_insensitive=True)
983 user = cls.get_by_email(_email, case_insensitive=True)
984 if user:
984 if user:
985 return user
985 return user
986 # Maybe we can match by username?
986 # Maybe we can match by username?
987 _author = author_name(author)
987 _author = author_name(author)
988 user = cls.get_by_username(_author, case_insensitive=True)
988 user = cls.get_by_username(_author, case_insensitive=True)
989 if user:
989 if user:
990 return user
990 return user
991
991
992 def update_userdata(self, **kwargs):
992 def update_userdata(self, **kwargs):
993 usr = self
993 usr = self
994 old = usr.user_data
994 old = usr.user_data
995 old.update(**kwargs)
995 old.update(**kwargs)
996 usr.user_data = old
996 usr.user_data = old
997 Session().add(usr)
997 Session().add(usr)
998 log.debug('updated userdata with %s', kwargs)
998 log.debug('updated userdata with %s', kwargs)
999
999
1000 def update_lastlogin(self):
1000 def update_lastlogin(self):
1001 """Update user lastlogin"""
1001 """Update user lastlogin"""
1002 self.last_login = datetime.datetime.now()
1002 self.last_login = datetime.datetime.now()
1003 Session().add(self)
1003 Session().add(self)
1004 log.debug('updated user %s lastlogin', self.username)
1004 log.debug('updated user %s lastlogin', self.username)
1005
1005
1006 def update_password(self, new_password):
1006 def update_password(self, new_password):
1007 from rhodecode.lib.auth import get_crypt_password
1007 from rhodecode.lib.auth import get_crypt_password
1008
1008
1009 self.password = get_crypt_password(new_password)
1009 self.password = get_crypt_password(new_password)
1010 Session().add(self)
1010 Session().add(self)
1011
1011
1012 @classmethod
1012 @classmethod
1013 def get_first_super_admin(cls):
1013 def get_first_super_admin(cls):
1014 user = User.query()\
1014 user = User.query()\
1015 .filter(User.admin == true()) \
1015 .filter(User.admin == true()) \
1016 .order_by(User.user_id.asc()) \
1016 .order_by(User.user_id.asc()) \
1017 .first()
1017 .first()
1018
1018
1019 if user is None:
1019 if user is None:
1020 raise Exception('FATAL: Missing administrative account!')
1020 raise Exception('FATAL: Missing administrative account!')
1021 return user
1021 return user
1022
1022
1023 @classmethod
1023 @classmethod
1024 def get_all_super_admins(cls, only_active=False):
1024 def get_all_super_admins(cls, only_active=False):
1025 """
1025 """
1026 Returns all admin accounts sorted by username
1026 Returns all admin accounts sorted by username
1027 """
1027 """
1028 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1028 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1029 if only_active:
1029 if only_active:
1030 qry = qry.filter(User.active == true())
1030 qry = qry.filter(User.active == true())
1031 return qry.all()
1031 return qry.all()
1032
1032
1033 @classmethod
1033 @classmethod
1034 def get_all_user_ids(cls, only_active=True):
1034 def get_all_user_ids(cls, only_active=True):
1035 """
1035 """
1036 Returns all users IDs
1036 Returns all users IDs
1037 """
1037 """
1038 qry = Session().query(User.user_id)
1038 qry = Session().query(User.user_id)
1039
1039
1040 if only_active:
1040 if only_active:
1041 qry = qry.filter(User.active == true())
1041 qry = qry.filter(User.active == true())
1042 return [x.user_id for x in qry]
1042 return [x.user_id for x in qry]
1043
1043
1044 @classmethod
1044 @classmethod
1045 def get_default_user(cls, cache=False, refresh=False):
1045 def get_default_user(cls, cache=False, refresh=False):
1046 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1046 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1047 if user is None:
1047 if user is None:
1048 raise Exception('FATAL: Missing default account!')
1048 raise Exception('FATAL: Missing default account!')
1049 if refresh:
1049 if refresh:
1050 # The default user might be based on outdated state which
1050 # The default user might be based on outdated state which
1051 # has been loaded from the cache.
1051 # has been loaded from the cache.
1052 # A call to refresh() ensures that the
1052 # A call to refresh() ensures that the
1053 # latest state from the database is used.
1053 # latest state from the database is used.
1054 Session().refresh(user)
1054 Session().refresh(user)
1055 return user
1055 return user
1056
1056
1057 @classmethod
1057 @classmethod
1058 def get_default_user_id(cls):
1058 def get_default_user_id(cls):
1059 import rhodecode
1059 import rhodecode
1060 return rhodecode.CONFIG['default_user_id']
1060 return rhodecode.CONFIG['default_user_id']
1061
1061
1062 def _get_default_perms(self, user, suffix=''):
1062 def _get_default_perms(self, user, suffix=''):
1063 from rhodecode.model.permission import PermissionModel
1063 from rhodecode.model.permission import PermissionModel
1064 return PermissionModel().get_default_perms(user.user_perms, suffix)
1064 return PermissionModel().get_default_perms(user.user_perms, suffix)
1065
1065
1066 def get_default_perms(self, suffix=''):
1066 def get_default_perms(self, suffix=''):
1067 return self._get_default_perms(self, suffix)
1067 return self._get_default_perms(self, suffix)
1068
1068
1069 def get_api_data(self, include_secrets=False, details='full'):
1069 def get_api_data(self, include_secrets=False, details='full'):
1070 """
1070 """
1071 Common function for generating user related data for API
1071 Common function for generating user related data for API
1072
1072
1073 :param include_secrets: By default secrets in the API data will be replaced
1073 :param include_secrets: By default secrets in the API data will be replaced
1074 by a placeholder value to prevent exposing this data by accident. In case
1074 by a placeholder value to prevent exposing this data by accident. In case
1075 this data shall be exposed, set this flag to ``True``.
1075 this data shall be exposed, set this flag to ``True``.
1076
1076
1077 :param details: details can be 'basic|full' basic gives only a subset of
1077 :param details: details can be 'basic|full' basic gives only a subset of
1078 the available user information that includes user_id, name and emails.
1078 the available user information that includes user_id, name and emails.
1079 """
1079 """
1080 user = self
1080 user = self
1081 user_data = self.user_data
1081 user_data = self.user_data
1082 data = {
1082 data = {
1083 'user_id': user.user_id,
1083 'user_id': user.user_id,
1084 'username': user.username,
1084 'username': user.username,
1085 'firstname': user.name,
1085 'firstname': user.name,
1086 'lastname': user.lastname,
1086 'lastname': user.lastname,
1087 'description': user.description,
1087 'description': user.description,
1088 'email': user.email,
1088 'email': user.email,
1089 'emails': user.emails,
1089 'emails': user.emails,
1090 }
1090 }
1091 if details == 'basic':
1091 if details == 'basic':
1092 return data
1092 return data
1093
1093
1094 auth_token_length = 40
1094 auth_token_length = 40
1095 auth_token_replacement = '*' * auth_token_length
1095 auth_token_replacement = '*' * auth_token_length
1096
1096
1097 extras = {
1097 extras = {
1098 'auth_tokens': [auth_token_replacement],
1098 'auth_tokens': [auth_token_replacement],
1099 'active': user.active,
1099 'active': user.active,
1100 'admin': user.admin,
1100 'admin': user.admin,
1101 'extern_type': user.extern_type,
1101 'extern_type': user.extern_type,
1102 'extern_name': user.extern_name,
1102 'extern_name': user.extern_name,
1103 'last_login': user.last_login,
1103 'last_login': user.last_login,
1104 'last_activity': user.last_activity,
1104 'last_activity': user.last_activity,
1105 'ip_addresses': user.ip_addresses,
1105 'ip_addresses': user.ip_addresses,
1106 'language': user_data.get('language')
1106 'language': user_data.get('language')
1107 }
1107 }
1108 data.update(extras)
1108 data.update(extras)
1109
1109
1110 if include_secrets:
1110 if include_secrets:
1111 data['auth_tokens'] = user.auth_tokens
1111 data['auth_tokens'] = user.auth_tokens
1112 return data
1112 return data
1113
1113
1114 def __json__(self):
1114 def __json__(self):
1115 data = {
1115 data = {
1116 'full_name': self.full_name,
1116 'full_name': self.full_name,
1117 'full_name_or_username': self.full_name_or_username,
1117 'full_name_or_username': self.full_name_or_username,
1118 'short_contact': self.short_contact,
1118 'short_contact': self.short_contact,
1119 'full_contact': self.full_contact,
1119 'full_contact': self.full_contact,
1120 }
1120 }
1121 data.update(self.get_api_data())
1121 data.update(self.get_api_data())
1122 return data
1122 return data
1123
1123
1124
1124
1125 class UserApiKeys(Base, BaseModel):
1125 class UserApiKeys(Base, BaseModel):
1126 __tablename__ = 'user_api_keys'
1126 __tablename__ = 'user_api_keys'
1127 __table_args__ = (
1127 __table_args__ = (
1128 Index('uak_api_key_idx', 'api_key'),
1128 Index('uak_api_key_idx', 'api_key'),
1129 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1129 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1130 base_table_args
1130 base_table_args
1131 )
1131 )
1132 __mapper_args__ = {}
1132 __mapper_args__ = {}
1133
1133
1134 # ApiKey role
1134 # ApiKey role
1135 ROLE_ALL = 'token_role_all'
1135 ROLE_ALL = 'token_role_all'
1136 ROLE_VCS = 'token_role_vcs'
1136 ROLE_VCS = 'token_role_vcs'
1137 ROLE_API = 'token_role_api'
1137 ROLE_API = 'token_role_api'
1138 ROLE_HTTP = 'token_role_http'
1138 ROLE_HTTP = 'token_role_http'
1139 ROLE_FEED = 'token_role_feed'
1139 ROLE_FEED = 'token_role_feed'
1140 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1140 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1141 # The last one is ignored in the list as we only
1141 # The last one is ignored in the list as we only
1142 # use it for one action, and cannot be created by users
1142 # use it for one action, and cannot be created by users
1143 ROLE_PASSWORD_RESET = 'token_password_reset'
1143 ROLE_PASSWORD_RESET = 'token_password_reset'
1144
1144
1145 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1145 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1146
1146
1147 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1147 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1148 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1148 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1149 api_key = Column("api_key", String(255), nullable=False, unique=True)
1149 api_key = Column("api_key", String(255), nullable=False, unique=True)
1150 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1150 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1151 expires = Column('expires', Float(53), nullable=False)
1151 expires = Column('expires', Float(53), nullable=False)
1152 role = Column('role', String(255), nullable=True)
1152 role = Column('role', String(255), nullable=True)
1153 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1153 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1154
1154
1155 # scope columns
1155 # scope columns
1156 repo_id = Column(
1156 repo_id = Column(
1157 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1157 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1158 nullable=True, unique=None, default=None)
1158 nullable=True, unique=None, default=None)
1159 repo = relationship('Repository', lazy='joined')
1159 repo = relationship('Repository', lazy='joined')
1160
1160
1161 repo_group_id = Column(
1161 repo_group_id = Column(
1162 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1162 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1163 nullable=True, unique=None, default=None)
1163 nullable=True, unique=None, default=None)
1164 repo_group = relationship('RepoGroup', lazy='joined')
1164 repo_group = relationship('RepoGroup', lazy='joined')
1165
1165
1166 user = relationship('User', lazy='joined')
1166 user = relationship('User', lazy='joined')
1167
1167
1168 def __unicode__(self):
1168 def __unicode__(self):
1169 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1169 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1170
1170
1171 def __json__(self):
1171 def __json__(self):
1172 data = {
1172 data = {
1173 'auth_token': self.api_key,
1173 'auth_token': self.api_key,
1174 'role': self.role,
1174 'role': self.role,
1175 'scope': self.scope_humanized,
1175 'scope': self.scope_humanized,
1176 'expired': self.expired
1176 'expired': self.expired
1177 }
1177 }
1178 return data
1178 return data
1179
1179
1180 def get_api_data(self, include_secrets=False):
1180 def get_api_data(self, include_secrets=False):
1181 data = self.__json__()
1181 data = self.__json__()
1182 if include_secrets:
1182 if include_secrets:
1183 return data
1183 return data
1184 else:
1184 else:
1185 data['auth_token'] = self.token_obfuscated
1185 data['auth_token'] = self.token_obfuscated
1186 return data
1186 return data
1187
1187
1188 @hybrid_property
1188 @hybrid_property
1189 def description_safe(self):
1189 def description_safe(self):
1190 from rhodecode.lib import helpers as h
1190 from rhodecode.lib import helpers as h
1191 return h.escape(self.description)
1191 return h.escape(self.description)
1192
1192
1193 @property
1193 @property
1194 def expired(self):
1194 def expired(self):
1195 if self.expires == -1:
1195 if self.expires == -1:
1196 return False
1196 return False
1197 return time.time() > self.expires
1197 return time.time() > self.expires
1198
1198
1199 @classmethod
1199 @classmethod
1200 def _get_role_name(cls, role):
1200 def _get_role_name(cls, role):
1201 return {
1201 return {
1202 cls.ROLE_ALL: _('all'),
1202 cls.ROLE_ALL: _('all'),
1203 cls.ROLE_HTTP: _('http/web interface'),
1203 cls.ROLE_HTTP: _('http/web interface'),
1204 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1204 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1205 cls.ROLE_API: _('api calls'),
1205 cls.ROLE_API: _('api calls'),
1206 cls.ROLE_FEED: _('feed access'),
1206 cls.ROLE_FEED: _('feed access'),
1207 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1207 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1208 }.get(role, role)
1208 }.get(role, role)
1209
1209
1210 @classmethod
1210 @classmethod
1211 def _get_role_description(cls, role):
1211 def _get_role_description(cls, role):
1212 return {
1212 return {
1213 cls.ROLE_ALL: _('Token for all actions.'),
1213 cls.ROLE_ALL: _('Token for all actions.'),
1214 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1214 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1215 'login using `api_access_controllers_whitelist` functionality.'),
1215 'login using `api_access_controllers_whitelist` functionality.'),
1216 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1216 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1217 'Requires auth_token authentication plugin to be active. <br/>'
1217 'Requires auth_token authentication plugin to be active. <br/>'
1218 'Such Token should be used then instead of a password to '
1218 'Such Token should be used then instead of a password to '
1219 'interact with a repository, and additionally can be '
1219 'interact with a repository, and additionally can be '
1220 'limited to single repository using repo scope.'),
1220 'limited to single repository using repo scope.'),
1221 cls.ROLE_API: _('Token limited to api calls.'),
1221 cls.ROLE_API: _('Token limited to api calls.'),
1222 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1222 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1223 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1223 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1224 }.get(role, role)
1224 }.get(role, role)
1225
1225
1226 @property
1226 @property
1227 def role_humanized(self):
1227 def role_humanized(self):
1228 return self._get_role_name(self.role)
1228 return self._get_role_name(self.role)
1229
1229
1230 def _get_scope(self):
1230 def _get_scope(self):
1231 if self.repo:
1231 if self.repo:
1232 return 'Repository: {}'.format(self.repo.repo_name)
1232 return 'Repository: {}'.format(self.repo.repo_name)
1233 if self.repo_group:
1233 if self.repo_group:
1234 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1234 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1235 return 'Global'
1235 return 'Global'
1236
1236
1237 @property
1237 @property
1238 def scope_humanized(self):
1238 def scope_humanized(self):
1239 return self._get_scope()
1239 return self._get_scope()
1240
1240
1241 @property
1241 @property
1242 def token_obfuscated(self):
1242 def token_obfuscated(self):
1243 if self.api_key:
1243 if self.api_key:
1244 return self.api_key[:4] + "****"
1244 return self.api_key[:4] + "****"
1245
1245
1246
1246
1247 class UserEmailMap(Base, BaseModel):
1247 class UserEmailMap(Base, BaseModel):
1248 __tablename__ = 'user_email_map'
1248 __tablename__ = 'user_email_map'
1249 __table_args__ = (
1249 __table_args__ = (
1250 Index('uem_email_idx', 'email'),
1250 Index('uem_email_idx', 'email'),
1251 UniqueConstraint('email'),
1251 UniqueConstraint('email'),
1252 base_table_args
1252 base_table_args
1253 )
1253 )
1254 __mapper_args__ = {}
1254 __mapper_args__ = {}
1255
1255
1256 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1256 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1257 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1257 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1258 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1258 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1259 user = relationship('User', lazy='joined')
1259 user = relationship('User', lazy='joined')
1260
1260
1261 @validates('_email')
1261 @validates('_email')
1262 def validate_email(self, key, email):
1262 def validate_email(self, key, email):
1263 # check if this email is not main one
1263 # check if this email is not main one
1264 main_email = Session().query(User).filter(User.email == email).scalar()
1264 main_email = Session().query(User).filter(User.email == email).scalar()
1265 if main_email is not None:
1265 if main_email is not None:
1266 raise AttributeError('email %s is present is user table' % email)
1266 raise AttributeError('email %s is present is user table' % email)
1267 return email
1267 return email
1268
1268
1269 @hybrid_property
1269 @hybrid_property
1270 def email(self):
1270 def email(self):
1271 return self._email
1271 return self._email
1272
1272
1273 @email.setter
1273 @email.setter
1274 def email(self, val):
1274 def email(self, val):
1275 self._email = val.lower() if val else None
1275 self._email = val.lower() if val else None
1276
1276
1277
1277
1278 class UserIpMap(Base, BaseModel):
1278 class UserIpMap(Base, BaseModel):
1279 __tablename__ = 'user_ip_map'
1279 __tablename__ = 'user_ip_map'
1280 __table_args__ = (
1280 __table_args__ = (
1281 UniqueConstraint('user_id', 'ip_addr'),
1281 UniqueConstraint('user_id', 'ip_addr'),
1282 base_table_args
1282 base_table_args
1283 )
1283 )
1284 __mapper_args__ = {}
1284 __mapper_args__ = {}
1285
1285
1286 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1286 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1287 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1287 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1288 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1288 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1289 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1289 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1290 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1290 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1291 user = relationship('User', lazy='joined')
1291 user = relationship('User', lazy='joined')
1292
1292
1293 @hybrid_property
1293 @hybrid_property
1294 def description_safe(self):
1294 def description_safe(self):
1295 from rhodecode.lib import helpers as h
1295 from rhodecode.lib import helpers as h
1296 return h.escape(self.description)
1296 return h.escape(self.description)
1297
1297
1298 @classmethod
1298 @classmethod
1299 def _get_ip_range(cls, ip_addr):
1299 def _get_ip_range(cls, ip_addr):
1300 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1300 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1301 return [str(net.network_address), str(net.broadcast_address)]
1301 return [str(net.network_address), str(net.broadcast_address)]
1302
1302
1303 def __json__(self):
1303 def __json__(self):
1304 return {
1304 return {
1305 'ip_addr': self.ip_addr,
1305 'ip_addr': self.ip_addr,
1306 'ip_range': self._get_ip_range(self.ip_addr),
1306 'ip_range': self._get_ip_range(self.ip_addr),
1307 }
1307 }
1308
1308
1309 def __unicode__(self):
1309 def __unicode__(self):
1310 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1310 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1311 self.user_id, self.ip_addr)
1311 self.user_id, self.ip_addr)
1312
1312
1313
1313
1314 class UserSshKeys(Base, BaseModel):
1314 class UserSshKeys(Base, BaseModel):
1315 __tablename__ = 'user_ssh_keys'
1315 __tablename__ = 'user_ssh_keys'
1316 __table_args__ = (
1316 __table_args__ = (
1317 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1317 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1318
1318
1319 UniqueConstraint('ssh_key_fingerprint'),
1319 UniqueConstraint('ssh_key_fingerprint'),
1320
1320
1321 base_table_args
1321 base_table_args
1322 )
1322 )
1323 __mapper_args__ = {}
1323 __mapper_args__ = {}
1324
1324
1325 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1325 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1326 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1326 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1327 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1327 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1328
1328
1329 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1329 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1330
1330
1331 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1331 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1332 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1332 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1333 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1333 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1334
1334
1335 user = relationship('User', lazy='joined')
1335 user = relationship('User', lazy='joined')
1336
1336
1337 def __json__(self):
1337 def __json__(self):
1338 data = {
1338 data = {
1339 'ssh_fingerprint': self.ssh_key_fingerprint,
1339 'ssh_fingerprint': self.ssh_key_fingerprint,
1340 'description': self.description,
1340 'description': self.description,
1341 'created_on': self.created_on
1341 'created_on': self.created_on
1342 }
1342 }
1343 return data
1343 return data
1344
1344
1345 def get_api_data(self):
1345 def get_api_data(self):
1346 data = self.__json__()
1346 data = self.__json__()
1347 return data
1347 return data
1348
1348
1349
1349
1350 class UserLog(Base, BaseModel):
1350 class UserLog(Base, BaseModel):
1351 __tablename__ = 'user_logs'
1351 __tablename__ = 'user_logs'
1352 __table_args__ = (
1352 __table_args__ = (
1353 base_table_args,
1353 base_table_args,
1354 )
1354 )
1355
1355
1356 VERSION_1 = 'v1'
1356 VERSION_1 = 'v1'
1357 VERSION_2 = 'v2'
1357 VERSION_2 = 'v2'
1358 VERSIONS = [VERSION_1, VERSION_2]
1358 VERSIONS = [VERSION_1, VERSION_2]
1359
1359
1360 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1360 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1361 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1361 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1362 username = Column("username", String(255), nullable=True, unique=None, default=None)
1362 username = Column("username", String(255), nullable=True, unique=None, default=None)
1363 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1363 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1364 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1364 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1365 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1365 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1366 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1366 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1367 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1367 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1368
1368
1369 version = Column("version", String(255), nullable=True, default=VERSION_1)
1369 version = Column("version", String(255), nullable=True, default=VERSION_1)
1370 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1370 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1371 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1371 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1372
1372
1373 def __unicode__(self):
1373 def __unicode__(self):
1374 return u"<%s('id:%s:%s')>" % (
1374 return u"<%s('id:%s:%s')>" % (
1375 self.__class__.__name__, self.repository_name, self.action)
1375 self.__class__.__name__, self.repository_name, self.action)
1376
1376
1377 def __json__(self):
1377 def __json__(self):
1378 return {
1378 return {
1379 'user_id': self.user_id,
1379 'user_id': self.user_id,
1380 'username': self.username,
1380 'username': self.username,
1381 'repository_id': self.repository_id,
1381 'repository_id': self.repository_id,
1382 'repository_name': self.repository_name,
1382 'repository_name': self.repository_name,
1383 'user_ip': self.user_ip,
1383 'user_ip': self.user_ip,
1384 'action_date': self.action_date,
1384 'action_date': self.action_date,
1385 'action': self.action,
1385 'action': self.action,
1386 }
1386 }
1387
1387
1388 @hybrid_property
1388 @hybrid_property
1389 def entry_id(self):
1389 def entry_id(self):
1390 return self.user_log_id
1390 return self.user_log_id
1391
1391
1392 @property
1392 @property
1393 def action_as_day(self):
1393 def action_as_day(self):
1394 return datetime.date(*self.action_date.timetuple()[:3])
1394 return datetime.date(*self.action_date.timetuple()[:3])
1395
1395
1396 user = relationship('User')
1396 user = relationship('User')
1397 repository = relationship('Repository', cascade='')
1397 repository = relationship('Repository', cascade='')
1398
1398
1399
1399
1400 class UserGroup(Base, BaseModel):
1400 class UserGroup(Base, BaseModel):
1401 __tablename__ = 'users_groups'
1401 __tablename__ = 'users_groups'
1402 __table_args__ = (
1402 __table_args__ = (
1403 base_table_args,
1403 base_table_args,
1404 )
1404 )
1405
1405
1406 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1406 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1407 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1407 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1408 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1408 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1409 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1409 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1410 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1410 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1411 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1411 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1412 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1412 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1413 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1413 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1414
1414
1415 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1415 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1416 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1416 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1417 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1417 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1418 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1418 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1419 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1419 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1420 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1420 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1421
1421
1422 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1422 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1423 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1423 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1424
1424
1425 @classmethod
1425 @classmethod
1426 def _load_group_data(cls, column):
1426 def _load_group_data(cls, column):
1427 if not column:
1427 if not column:
1428 return {}
1428 return {}
1429
1429
1430 try:
1430 try:
1431 return json.loads(column) or {}
1431 return json.loads(column) or {}
1432 except TypeError:
1432 except TypeError:
1433 return {}
1433 return {}
1434
1434
1435 @hybrid_property
1435 @hybrid_property
1436 def description_safe(self):
1436 def description_safe(self):
1437 from rhodecode.lib import helpers as h
1437 from rhodecode.lib import helpers as h
1438 return h.escape(self.user_group_description)
1438 return h.escape(self.user_group_description)
1439
1439
1440 @hybrid_property
1440 @hybrid_property
1441 def group_data(self):
1441 def group_data(self):
1442 return self._load_group_data(self._group_data)
1442 return self._load_group_data(self._group_data)
1443
1443
1444 @group_data.expression
1444 @group_data.expression
1445 def group_data(self, **kwargs):
1445 def group_data(self, **kwargs):
1446 return self._group_data
1446 return self._group_data
1447
1447
1448 @group_data.setter
1448 @group_data.setter
1449 def group_data(self, val):
1449 def group_data(self, val):
1450 try:
1450 try:
1451 self._group_data = json.dumps(val)
1451 self._group_data = json.dumps(val)
1452 except Exception:
1452 except Exception:
1453 log.error(traceback.format_exc())
1453 log.error(traceback.format_exc())
1454
1454
1455 @classmethod
1455 @classmethod
1456 def _load_sync(cls, group_data):
1456 def _load_sync(cls, group_data):
1457 if group_data:
1457 if group_data:
1458 return group_data.get('extern_type')
1458 return group_data.get('extern_type')
1459
1459
1460 @property
1460 @property
1461 def sync(self):
1461 def sync(self):
1462 return self._load_sync(self.group_data)
1462 return self._load_sync(self.group_data)
1463
1463
1464 def __unicode__(self):
1464 def __unicode__(self):
1465 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1465 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1466 self.users_group_id,
1466 self.users_group_id,
1467 self.users_group_name)
1467 self.users_group_name)
1468
1468
1469 @classmethod
1469 @classmethod
1470 def get_by_group_name(cls, group_name, cache=False,
1470 def get_by_group_name(cls, group_name, cache=False,
1471 case_insensitive=False):
1471 case_insensitive=False):
1472 if case_insensitive:
1472 if case_insensitive:
1473 q = cls.query().filter(func.lower(cls.users_group_name) ==
1473 q = cls.query().filter(func.lower(cls.users_group_name) ==
1474 func.lower(group_name))
1474 func.lower(group_name))
1475
1475
1476 else:
1476 else:
1477 q = cls.query().filter(cls.users_group_name == group_name)
1477 q = cls.query().filter(cls.users_group_name == group_name)
1478 if cache:
1478 if cache:
1479 q = q.options(
1479 q = q.options(
1480 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1480 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1481 return q.scalar()
1481 return q.scalar()
1482
1482
1483 @classmethod
1483 @classmethod
1484 def get(cls, user_group_id, cache=False):
1484 def get(cls, user_group_id, cache=False):
1485 if not user_group_id:
1485 if not user_group_id:
1486 return
1486 return
1487
1487
1488 user_group = cls.query()
1488 user_group = cls.query()
1489 if cache:
1489 if cache:
1490 user_group = user_group.options(
1490 user_group = user_group.options(
1491 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1491 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1492 return user_group.get(user_group_id)
1492 return user_group.get(user_group_id)
1493
1493
1494 def permissions(self, with_admins=True, with_owner=True,
1494 def permissions(self, with_admins=True, with_owner=True,
1495 expand_from_user_groups=False):
1495 expand_from_user_groups=False):
1496 """
1496 """
1497 Permissions for user groups
1497 Permissions for user groups
1498 """
1498 """
1499 _admin_perm = 'usergroup.admin'
1499 _admin_perm = 'usergroup.admin'
1500
1500
1501 owner_row = []
1501 owner_row = []
1502 if with_owner:
1502 if with_owner:
1503 usr = AttributeDict(self.user.get_dict())
1503 usr = AttributeDict(self.user.get_dict())
1504 usr.owner_row = True
1504 usr.owner_row = True
1505 usr.permission = _admin_perm
1505 usr.permission = _admin_perm
1506 owner_row.append(usr)
1506 owner_row.append(usr)
1507
1507
1508 super_admin_ids = []
1508 super_admin_ids = []
1509 super_admin_rows = []
1509 super_admin_rows = []
1510 if with_admins:
1510 if with_admins:
1511 for usr in User.get_all_super_admins():
1511 for usr in User.get_all_super_admins():
1512 super_admin_ids.append(usr.user_id)
1512 super_admin_ids.append(usr.user_id)
1513 # if this admin is also owner, don't double the record
1513 # if this admin is also owner, don't double the record
1514 if usr.user_id == owner_row[0].user_id:
1514 if usr.user_id == owner_row[0].user_id:
1515 owner_row[0].admin_row = True
1515 owner_row[0].admin_row = True
1516 else:
1516 else:
1517 usr = AttributeDict(usr.get_dict())
1517 usr = AttributeDict(usr.get_dict())
1518 usr.admin_row = True
1518 usr.admin_row = True
1519 usr.permission = _admin_perm
1519 usr.permission = _admin_perm
1520 super_admin_rows.append(usr)
1520 super_admin_rows.append(usr)
1521
1521
1522 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1522 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1523 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1523 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1524 joinedload(UserUserGroupToPerm.user),
1524 joinedload(UserUserGroupToPerm.user),
1525 joinedload(UserUserGroupToPerm.permission),)
1525 joinedload(UserUserGroupToPerm.permission),)
1526
1526
1527 # get owners and admins and permissions. We do a trick of re-writing
1527 # get owners and admins and permissions. We do a trick of re-writing
1528 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1528 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1529 # has a global reference and changing one object propagates to all
1529 # has a global reference and changing one object propagates to all
1530 # others. This means if admin is also an owner admin_row that change
1530 # others. This means if admin is also an owner admin_row that change
1531 # would propagate to both objects
1531 # would propagate to both objects
1532 perm_rows = []
1532 perm_rows = []
1533 for _usr in q.all():
1533 for _usr in q.all():
1534 usr = AttributeDict(_usr.user.get_dict())
1534 usr = AttributeDict(_usr.user.get_dict())
1535 # if this user is also owner/admin, mark as duplicate record
1535 # if this user is also owner/admin, mark as duplicate record
1536 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1536 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1537 usr.duplicate_perm = True
1537 usr.duplicate_perm = True
1538 usr.permission = _usr.permission.permission_name
1538 usr.permission = _usr.permission.permission_name
1539 perm_rows.append(usr)
1539 perm_rows.append(usr)
1540
1540
1541 # filter the perm rows by 'default' first and then sort them by
1541 # filter the perm rows by 'default' first and then sort them by
1542 # admin,write,read,none permissions sorted again alphabetically in
1542 # admin,write,read,none permissions sorted again alphabetically in
1543 # each group
1543 # each group
1544 perm_rows = sorted(perm_rows, key=display_user_sort)
1544 perm_rows = sorted(perm_rows, key=display_user_sort)
1545
1545
1546 user_groups_rows = []
1546 user_groups_rows = []
1547 if expand_from_user_groups:
1547 if expand_from_user_groups:
1548 for ug in self.permission_user_groups(with_members=True):
1548 for ug in self.permission_user_groups(with_members=True):
1549 for user_data in ug.members:
1549 for user_data in ug.members:
1550 user_groups_rows.append(user_data)
1550 user_groups_rows.append(user_data)
1551
1551
1552 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1552 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1553
1553
1554 def permission_user_groups(self, with_members=False):
1554 def permission_user_groups(self, with_members=False):
1555 q = UserGroupUserGroupToPerm.query()\
1555 q = UserGroupUserGroupToPerm.query()\
1556 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1556 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1557 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1557 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1558 joinedload(UserGroupUserGroupToPerm.target_user_group),
1558 joinedload(UserGroupUserGroupToPerm.target_user_group),
1559 joinedload(UserGroupUserGroupToPerm.permission),)
1559 joinedload(UserGroupUserGroupToPerm.permission),)
1560
1560
1561 perm_rows = []
1561 perm_rows = []
1562 for _user_group in q.all():
1562 for _user_group in q.all():
1563 entry = AttributeDict(_user_group.user_group.get_dict())
1563 entry = AttributeDict(_user_group.user_group.get_dict())
1564 entry.permission = _user_group.permission.permission_name
1564 entry.permission = _user_group.permission.permission_name
1565 if with_members:
1565 if with_members:
1566 entry.members = [x.user.get_dict()
1566 entry.members = [x.user.get_dict()
1567 for x in _user_group.user_group.members]
1567 for x in _user_group.user_group.members]
1568 perm_rows.append(entry)
1568 perm_rows.append(entry)
1569
1569
1570 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1570 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1571 return perm_rows
1571 return perm_rows
1572
1572
1573 def _get_default_perms(self, user_group, suffix=''):
1573 def _get_default_perms(self, user_group, suffix=''):
1574 from rhodecode.model.permission import PermissionModel
1574 from rhodecode.model.permission import PermissionModel
1575 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1575 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1576
1576
1577 def get_default_perms(self, suffix=''):
1577 def get_default_perms(self, suffix=''):
1578 return self._get_default_perms(self, suffix)
1578 return self._get_default_perms(self, suffix)
1579
1579
1580 def get_api_data(self, with_group_members=True, include_secrets=False):
1580 def get_api_data(self, with_group_members=True, include_secrets=False):
1581 """
1581 """
1582 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1582 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1583 basically forwarded.
1583 basically forwarded.
1584
1584
1585 """
1585 """
1586 user_group = self
1586 user_group = self
1587 data = {
1587 data = {
1588 'users_group_id': user_group.users_group_id,
1588 'users_group_id': user_group.users_group_id,
1589 'group_name': user_group.users_group_name,
1589 'group_name': user_group.users_group_name,
1590 'group_description': user_group.user_group_description,
1590 'group_description': user_group.user_group_description,
1591 'active': user_group.users_group_active,
1591 'active': user_group.users_group_active,
1592 'owner': user_group.user.username,
1592 'owner': user_group.user.username,
1593 'sync': user_group.sync,
1593 'sync': user_group.sync,
1594 'owner_email': user_group.user.email,
1594 'owner_email': user_group.user.email,
1595 }
1595 }
1596
1596
1597 if with_group_members:
1597 if with_group_members:
1598 users = []
1598 users = []
1599 for user in user_group.members:
1599 for user in user_group.members:
1600 user = user.user
1600 user = user.user
1601 users.append(user.get_api_data(include_secrets=include_secrets))
1601 users.append(user.get_api_data(include_secrets=include_secrets))
1602 data['users'] = users
1602 data['users'] = users
1603
1603
1604 return data
1604 return data
1605
1605
1606
1606
1607 class UserGroupMember(Base, BaseModel):
1607 class UserGroupMember(Base, BaseModel):
1608 __tablename__ = 'users_groups_members'
1608 __tablename__ = 'users_groups_members'
1609 __table_args__ = (
1609 __table_args__ = (
1610 base_table_args,
1610 base_table_args,
1611 )
1611 )
1612
1612
1613 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1613 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1614 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1614 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1615 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1615 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1616
1616
1617 user = relationship('User', lazy='joined')
1617 user = relationship('User', lazy='joined')
1618 users_group = relationship('UserGroup')
1618 users_group = relationship('UserGroup')
1619
1619
1620 def __init__(self, gr_id='', u_id=''):
1620 def __init__(self, gr_id='', u_id=''):
1621 self.users_group_id = gr_id
1621 self.users_group_id = gr_id
1622 self.user_id = u_id
1622 self.user_id = u_id
1623
1623
1624
1624
1625 class RepositoryField(Base, BaseModel):
1625 class RepositoryField(Base, BaseModel):
1626 __tablename__ = 'repositories_fields'
1626 __tablename__ = 'repositories_fields'
1627 __table_args__ = (
1627 __table_args__ = (
1628 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1628 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1629 base_table_args,
1629 base_table_args,
1630 )
1630 )
1631
1631
1632 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1632 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1633
1633
1634 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1634 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1635 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1635 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1636 field_key = Column("field_key", String(250))
1636 field_key = Column("field_key", String(250))
1637 field_label = Column("field_label", String(1024), nullable=False)
1637 field_label = Column("field_label", String(1024), nullable=False)
1638 field_value = Column("field_value", String(10000), nullable=False)
1638 field_value = Column("field_value", String(10000), nullable=False)
1639 field_desc = Column("field_desc", String(1024), nullable=False)
1639 field_desc = Column("field_desc", String(1024), nullable=False)
1640 field_type = Column("field_type", String(255), nullable=False, unique=None)
1640 field_type = Column("field_type", String(255), nullable=False, unique=None)
1641 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1641 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1642
1642
1643 repository = relationship('Repository')
1643 repository = relationship('Repository')
1644
1644
1645 @property
1645 @property
1646 def field_key_prefixed(self):
1646 def field_key_prefixed(self):
1647 return 'ex_%s' % self.field_key
1647 return 'ex_%s' % self.field_key
1648
1648
1649 @classmethod
1649 @classmethod
1650 def un_prefix_key(cls, key):
1650 def un_prefix_key(cls, key):
1651 if key.startswith(cls.PREFIX):
1651 if key.startswith(cls.PREFIX):
1652 return key[len(cls.PREFIX):]
1652 return key[len(cls.PREFIX):]
1653 return key
1653 return key
1654
1654
1655 @classmethod
1655 @classmethod
1656 def get_by_key_name(cls, key, repo):
1656 def get_by_key_name(cls, key, repo):
1657 row = cls.query()\
1657 row = cls.query()\
1658 .filter(cls.repository == repo)\
1658 .filter(cls.repository == repo)\
1659 .filter(cls.field_key == key).scalar()
1659 .filter(cls.field_key == key).scalar()
1660 return row
1660 return row
1661
1661
1662
1662
1663 class Repository(Base, BaseModel):
1663 class Repository(Base, BaseModel):
1664 __tablename__ = 'repositories'
1664 __tablename__ = 'repositories'
1665 __table_args__ = (
1665 __table_args__ = (
1666 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1666 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1667 base_table_args,
1667 base_table_args,
1668 )
1668 )
1669 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1669 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1670 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1670 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1671 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1671 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1672
1672
1673 STATE_CREATED = 'repo_state_created'
1673 STATE_CREATED = 'repo_state_created'
1674 STATE_PENDING = 'repo_state_pending'
1674 STATE_PENDING = 'repo_state_pending'
1675 STATE_ERROR = 'repo_state_error'
1675 STATE_ERROR = 'repo_state_error'
1676
1676
1677 LOCK_AUTOMATIC = 'lock_auto'
1677 LOCK_AUTOMATIC = 'lock_auto'
1678 LOCK_API = 'lock_api'
1678 LOCK_API = 'lock_api'
1679 LOCK_WEB = 'lock_web'
1679 LOCK_WEB = 'lock_web'
1680 LOCK_PULL = 'lock_pull'
1680 LOCK_PULL = 'lock_pull'
1681
1681
1682 NAME_SEP = URL_SEP
1682 NAME_SEP = URL_SEP
1683
1683
1684 repo_id = Column(
1684 repo_id = Column(
1685 "repo_id", Integer(), nullable=False, unique=True, default=None,
1685 "repo_id", Integer(), nullable=False, unique=True, default=None,
1686 primary_key=True)
1686 primary_key=True)
1687 _repo_name = Column(
1687 _repo_name = Column(
1688 "repo_name", Text(), nullable=False, default=None)
1688 "repo_name", Text(), nullable=False, default=None)
1689 repo_name_hash = Column(
1689 repo_name_hash = Column(
1690 "repo_name_hash", String(255), nullable=False, unique=True)
1690 "repo_name_hash", String(255), nullable=False, unique=True)
1691 repo_state = Column("repo_state", String(255), nullable=True)
1691 repo_state = Column("repo_state", String(255), nullable=True)
1692
1692
1693 clone_uri = Column(
1693 clone_uri = Column(
1694 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1694 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1695 default=None)
1695 default=None)
1696 push_uri = Column(
1696 push_uri = Column(
1697 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1697 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1698 default=None)
1698 default=None)
1699 repo_type = Column(
1699 repo_type = Column(
1700 "repo_type", String(255), nullable=False, unique=False, default=None)
1700 "repo_type", String(255), nullable=False, unique=False, default=None)
1701 user_id = Column(
1701 user_id = Column(
1702 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1702 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1703 unique=False, default=None)
1703 unique=False, default=None)
1704 private = Column(
1704 private = Column(
1705 "private", Boolean(), nullable=True, unique=None, default=None)
1705 "private", Boolean(), nullable=True, unique=None, default=None)
1706 archived = Column(
1706 archived = Column(
1707 "archived", Boolean(), nullable=True, unique=None, default=None)
1707 "archived", Boolean(), nullable=True, unique=None, default=None)
1708 enable_statistics = Column(
1708 enable_statistics = Column(
1709 "statistics", Boolean(), nullable=True, unique=None, default=True)
1709 "statistics", Boolean(), nullable=True, unique=None, default=True)
1710 enable_downloads = Column(
1710 enable_downloads = Column(
1711 "downloads", Boolean(), nullable=True, unique=None, default=True)
1711 "downloads", Boolean(), nullable=True, unique=None, default=True)
1712 description = Column(
1712 description = Column(
1713 "description", String(10000), nullable=True, unique=None, default=None)
1713 "description", String(10000), nullable=True, unique=None, default=None)
1714 created_on = Column(
1714 created_on = Column(
1715 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1715 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1716 default=datetime.datetime.now)
1716 default=datetime.datetime.now)
1717 updated_on = Column(
1717 updated_on = Column(
1718 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1718 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1719 default=datetime.datetime.now)
1719 default=datetime.datetime.now)
1720 _landing_revision = Column(
1720 _landing_revision = Column(
1721 "landing_revision", String(255), nullable=False, unique=False,
1721 "landing_revision", String(255), nullable=False, unique=False,
1722 default=None)
1722 default=None)
1723 enable_locking = Column(
1723 enable_locking = Column(
1724 "enable_locking", Boolean(), nullable=False, unique=None,
1724 "enable_locking", Boolean(), nullable=False, unique=None,
1725 default=False)
1725 default=False)
1726 _locked = Column(
1726 _locked = Column(
1727 "locked", String(255), nullable=True, unique=False, default=None)
1727 "locked", String(255), nullable=True, unique=False, default=None)
1728 _changeset_cache = Column(
1728 _changeset_cache = Column(
1729 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1729 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1730
1730
1731 fork_id = Column(
1731 fork_id = Column(
1732 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1732 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1733 nullable=True, unique=False, default=None)
1733 nullable=True, unique=False, default=None)
1734 group_id = Column(
1734 group_id = Column(
1735 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1735 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1736 unique=False, default=None)
1736 unique=False, default=None)
1737
1737
1738 user = relationship('User', lazy='joined')
1738 user = relationship('User', lazy='joined')
1739 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1739 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1740 group = relationship('RepoGroup', lazy='joined')
1740 group = relationship('RepoGroup', lazy='joined')
1741 repo_to_perm = relationship(
1741 repo_to_perm = relationship(
1742 'UserRepoToPerm', cascade='all',
1742 'UserRepoToPerm', cascade='all',
1743 order_by='UserRepoToPerm.repo_to_perm_id')
1743 order_by='UserRepoToPerm.repo_to_perm_id')
1744 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1744 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1745 stats = relationship('Statistics', cascade='all', uselist=False)
1745 stats = relationship('Statistics', cascade='all', uselist=False)
1746
1746
1747 followers = relationship(
1747 followers = relationship(
1748 'UserFollowing',
1748 'UserFollowing',
1749 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1749 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1750 cascade='all')
1750 cascade='all')
1751 extra_fields = relationship(
1751 extra_fields = relationship(
1752 'RepositoryField', cascade="all, delete-orphan")
1752 'RepositoryField', cascade="all, delete-orphan")
1753 logs = relationship('UserLog')
1753 logs = relationship('UserLog')
1754 comments = relationship(
1754 comments = relationship(
1755 'ChangesetComment', cascade="all, delete-orphan")
1755 'ChangesetComment', cascade="all, delete-orphan")
1756 pull_requests_source = relationship(
1756 pull_requests_source = relationship(
1757 'PullRequest',
1757 'PullRequest',
1758 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1758 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1759 cascade="all, delete-orphan")
1759 cascade="all, delete-orphan")
1760 pull_requests_target = relationship(
1760 pull_requests_target = relationship(
1761 'PullRequest',
1761 'PullRequest',
1762 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1762 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1763 cascade="all, delete-orphan")
1763 cascade="all, delete-orphan")
1764 ui = relationship('RepoRhodeCodeUi', cascade="all")
1764 ui = relationship('RepoRhodeCodeUi', cascade="all")
1765 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1765 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1766 integrations = relationship('Integration', cascade="all, delete-orphan")
1766 integrations = relationship('Integration', cascade="all, delete-orphan")
1767
1767
1768 scoped_tokens = relationship('UserApiKeys', cascade="all")
1768 scoped_tokens = relationship('UserApiKeys', cascade="all")
1769
1769
1770 # no cascade, set NULL
1770 # no cascade, set NULL
1771 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1771 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1772
1772
1773 def __unicode__(self):
1773 def __unicode__(self):
1774 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1774 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1775 safe_unicode(self.repo_name))
1775 safe_unicode(self.repo_name))
1776
1776
1777 @hybrid_property
1777 @hybrid_property
1778 def description_safe(self):
1778 def description_safe(self):
1779 from rhodecode.lib import helpers as h
1779 from rhodecode.lib import helpers as h
1780 return h.escape(self.description)
1780 return h.escape(self.description)
1781
1781
1782 @hybrid_property
1782 @hybrid_property
1783 def landing_rev(self):
1783 def landing_rev(self):
1784 # always should return [rev_type, rev], e.g ['branch', 'master']
1784 # always should return [rev_type, rev], e.g ['branch', 'master']
1785 if self._landing_revision:
1785 if self._landing_revision:
1786 _rev_info = self._landing_revision.split(':')
1786 _rev_info = self._landing_revision.split(':')
1787 if len(_rev_info) < 2:
1787 if len(_rev_info) < 2:
1788 _rev_info.insert(0, 'rev')
1788 _rev_info.insert(0, 'rev')
1789 return [_rev_info[0], _rev_info[1]]
1789 return [_rev_info[0], _rev_info[1]]
1790 return [None, None]
1790 return [None, None]
1791
1791
1792 @property
1792 @property
1793 def landing_ref_type(self):
1793 def landing_ref_type(self):
1794 return self.landing_rev[0]
1794 return self.landing_rev[0]
1795
1795
1796 @property
1796 @property
1797 def landing_ref_name(self):
1797 def landing_ref_name(self):
1798 return self.landing_rev[1]
1798 return self.landing_rev[1]
1799
1799
1800 @landing_rev.setter
1800 @landing_rev.setter
1801 def landing_rev(self, val):
1801 def landing_rev(self, val):
1802 if ':' not in val:
1802 if ':' not in val:
1803 raise ValueError('value must be delimited with `:` and consist '
1803 raise ValueError('value must be delimited with `:` and consist '
1804 'of <rev_type>:<rev>, got %s instead' % val)
1804 'of <rev_type>:<rev>, got %s instead' % val)
1805 self._landing_revision = val
1805 self._landing_revision = val
1806
1806
1807 @hybrid_property
1807 @hybrid_property
1808 def locked(self):
1808 def locked(self):
1809 if self._locked:
1809 if self._locked:
1810 user_id, timelocked, reason = self._locked.split(':')
1810 user_id, timelocked, reason = self._locked.split(':')
1811 lock_values = int(user_id), timelocked, reason
1811 lock_values = int(user_id), timelocked, reason
1812 else:
1812 else:
1813 lock_values = [None, None, None]
1813 lock_values = [None, None, None]
1814 return lock_values
1814 return lock_values
1815
1815
1816 @locked.setter
1816 @locked.setter
1817 def locked(self, val):
1817 def locked(self, val):
1818 if val and isinstance(val, (list, tuple)):
1818 if val and isinstance(val, (list, tuple)):
1819 self._locked = ':'.join(map(str, val))
1819 self._locked = ':'.join(map(str, val))
1820 else:
1820 else:
1821 self._locked = None
1821 self._locked = None
1822
1822
1823 @classmethod
1823 @classmethod
1824 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1824 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1825 from rhodecode.lib.vcs.backends.base import EmptyCommit
1825 from rhodecode.lib.vcs.backends.base import EmptyCommit
1826 dummy = EmptyCommit().__json__()
1826 dummy = EmptyCommit().__json__()
1827 if not changeset_cache_raw:
1827 if not changeset_cache_raw:
1828 dummy['source_repo_id'] = repo_id
1828 dummy['source_repo_id'] = repo_id
1829 return json.loads(json.dumps(dummy))
1829 return json.loads(json.dumps(dummy))
1830
1830
1831 try:
1831 try:
1832 return json.loads(changeset_cache_raw)
1832 return json.loads(changeset_cache_raw)
1833 except TypeError:
1833 except TypeError:
1834 return dummy
1834 return dummy
1835 except Exception:
1835 except Exception:
1836 log.error(traceback.format_exc())
1836 log.error(traceback.format_exc())
1837 return dummy
1837 return dummy
1838
1838
1839 @hybrid_property
1839 @hybrid_property
1840 def changeset_cache(self):
1840 def changeset_cache(self):
1841 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1841 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1842
1842
1843 @changeset_cache.setter
1843 @changeset_cache.setter
1844 def changeset_cache(self, val):
1844 def changeset_cache(self, val):
1845 try:
1845 try:
1846 self._changeset_cache = json.dumps(val)
1846 self._changeset_cache = json.dumps(val)
1847 except Exception:
1847 except Exception:
1848 log.error(traceback.format_exc())
1848 log.error(traceback.format_exc())
1849
1849
1850 @hybrid_property
1850 @hybrid_property
1851 def repo_name(self):
1851 def repo_name(self):
1852 return self._repo_name
1852 return self._repo_name
1853
1853
1854 @repo_name.setter
1854 @repo_name.setter
1855 def repo_name(self, value):
1855 def repo_name(self, value):
1856 self._repo_name = value
1856 self._repo_name = value
1857 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1857 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1858
1858
1859 @classmethod
1859 @classmethod
1860 def normalize_repo_name(cls, repo_name):
1860 def normalize_repo_name(cls, repo_name):
1861 """
1861 """
1862 Normalizes os specific repo_name to the format internally stored inside
1862 Normalizes os specific repo_name to the format internally stored inside
1863 database using URL_SEP
1863 database using URL_SEP
1864
1864
1865 :param cls:
1865 :param cls:
1866 :param repo_name:
1866 :param repo_name:
1867 """
1867 """
1868 return cls.NAME_SEP.join(repo_name.split(os.sep))
1868 return cls.NAME_SEP.join(repo_name.split(os.sep))
1869
1869
1870 @classmethod
1870 @classmethod
1871 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1871 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1872 session = Session()
1872 session = Session()
1873 q = session.query(cls).filter(cls.repo_name == repo_name)
1873 q = session.query(cls).filter(cls.repo_name == repo_name)
1874
1874
1875 if cache:
1875 if cache:
1876 if identity_cache:
1876 if identity_cache:
1877 val = cls.identity_cache(session, 'repo_name', repo_name)
1877 val = cls.identity_cache(session, 'repo_name', repo_name)
1878 if val:
1878 if val:
1879 return val
1879 return val
1880 else:
1880 else:
1881 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1881 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1882 q = q.options(
1882 q = q.options(
1883 FromCache("sql_cache_short", cache_key))
1883 FromCache("sql_cache_short", cache_key))
1884
1884
1885 return q.scalar()
1885 return q.scalar()
1886
1886
1887 @classmethod
1887 @classmethod
1888 def get_by_id_or_repo_name(cls, repoid):
1888 def get_by_id_or_repo_name(cls, repoid):
1889 if isinstance(repoid, (int, long)):
1889 if isinstance(repoid, (int, long)):
1890 try:
1890 try:
1891 repo = cls.get(repoid)
1891 repo = cls.get(repoid)
1892 except ValueError:
1892 except ValueError:
1893 repo = None
1893 repo = None
1894 else:
1894 else:
1895 repo = cls.get_by_repo_name(repoid)
1895 repo = cls.get_by_repo_name(repoid)
1896 return repo
1896 return repo
1897
1897
1898 @classmethod
1898 @classmethod
1899 def get_by_full_path(cls, repo_full_path):
1899 def get_by_full_path(cls, repo_full_path):
1900 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1900 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1901 repo_name = cls.normalize_repo_name(repo_name)
1901 repo_name = cls.normalize_repo_name(repo_name)
1902 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1902 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1903
1903
1904 @classmethod
1904 @classmethod
1905 def get_repo_forks(cls, repo_id):
1905 def get_repo_forks(cls, repo_id):
1906 return cls.query().filter(Repository.fork_id == repo_id)
1906 return cls.query().filter(Repository.fork_id == repo_id)
1907
1907
1908 @classmethod
1908 @classmethod
1909 def base_path(cls):
1909 def base_path(cls):
1910 """
1910 """
1911 Returns base path when all repos are stored
1911 Returns base path when all repos are stored
1912
1912
1913 :param cls:
1913 :param cls:
1914 """
1914 """
1915 q = Session().query(RhodeCodeUi)\
1915 q = Session().query(RhodeCodeUi)\
1916 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1916 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1917 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1917 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1918 return q.one().ui_value
1918 return q.one().ui_value
1919
1919
1920 @classmethod
1920 @classmethod
1921 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1921 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1922 case_insensitive=True, archived=False):
1922 case_insensitive=True, archived=False):
1923 q = Repository.query()
1923 q = Repository.query()
1924
1924
1925 if not archived:
1925 if not archived:
1926 q = q.filter(Repository.archived.isnot(true()))
1926 q = q.filter(Repository.archived.isnot(true()))
1927
1927
1928 if not isinstance(user_id, Optional):
1928 if not isinstance(user_id, Optional):
1929 q = q.filter(Repository.user_id == user_id)
1929 q = q.filter(Repository.user_id == user_id)
1930
1930
1931 if not isinstance(group_id, Optional):
1931 if not isinstance(group_id, Optional):
1932 q = q.filter(Repository.group_id == group_id)
1932 q = q.filter(Repository.group_id == group_id)
1933
1933
1934 if case_insensitive:
1934 if case_insensitive:
1935 q = q.order_by(func.lower(Repository.repo_name))
1935 q = q.order_by(func.lower(Repository.repo_name))
1936 else:
1936 else:
1937 q = q.order_by(Repository.repo_name)
1937 q = q.order_by(Repository.repo_name)
1938
1938
1939 return q.all()
1939 return q.all()
1940
1940
1941 @property
1941 @property
1942 def repo_uid(self):
1942 def repo_uid(self):
1943 return '_{}'.format(self.repo_id)
1943 return '_{}'.format(self.repo_id)
1944
1944
1945 @property
1945 @property
1946 def forks(self):
1946 def forks(self):
1947 """
1947 """
1948 Return forks of this repo
1948 Return forks of this repo
1949 """
1949 """
1950 return Repository.get_repo_forks(self.repo_id)
1950 return Repository.get_repo_forks(self.repo_id)
1951
1951
1952 @property
1952 @property
1953 def parent(self):
1953 def parent(self):
1954 """
1954 """
1955 Returns fork parent
1955 Returns fork parent
1956 """
1956 """
1957 return self.fork
1957 return self.fork
1958
1958
1959 @property
1959 @property
1960 def just_name(self):
1960 def just_name(self):
1961 return self.repo_name.split(self.NAME_SEP)[-1]
1961 return self.repo_name.split(self.NAME_SEP)[-1]
1962
1962
1963 @property
1963 @property
1964 def groups_with_parents(self):
1964 def groups_with_parents(self):
1965 groups = []
1965 groups = []
1966 if self.group is None:
1966 if self.group is None:
1967 return groups
1967 return groups
1968
1968
1969 cur_gr = self.group
1969 cur_gr = self.group
1970 groups.insert(0, cur_gr)
1970 groups.insert(0, cur_gr)
1971 while 1:
1971 while 1:
1972 gr = getattr(cur_gr, 'parent_group', None)
1972 gr = getattr(cur_gr, 'parent_group', None)
1973 cur_gr = cur_gr.parent_group
1973 cur_gr = cur_gr.parent_group
1974 if gr is None:
1974 if gr is None:
1975 break
1975 break
1976 groups.insert(0, gr)
1976 groups.insert(0, gr)
1977
1977
1978 return groups
1978 return groups
1979
1979
1980 @property
1980 @property
1981 def groups_and_repo(self):
1981 def groups_and_repo(self):
1982 return self.groups_with_parents, self
1982 return self.groups_with_parents, self
1983
1983
1984 @LazyProperty
1984 @LazyProperty
1985 def repo_path(self):
1985 def repo_path(self):
1986 """
1986 """
1987 Returns base full path for that repository means where it actually
1987 Returns base full path for that repository means where it actually
1988 exists on a filesystem
1988 exists on a filesystem
1989 """
1989 """
1990 q = Session().query(RhodeCodeUi).filter(
1990 q = Session().query(RhodeCodeUi).filter(
1991 RhodeCodeUi.ui_key == self.NAME_SEP)
1991 RhodeCodeUi.ui_key == self.NAME_SEP)
1992 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1992 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1993 return q.one().ui_value
1993 return q.one().ui_value
1994
1994
1995 @property
1995 @property
1996 def repo_full_path(self):
1996 def repo_full_path(self):
1997 p = [self.repo_path]
1997 p = [self.repo_path]
1998 # we need to split the name by / since this is how we store the
1998 # we need to split the name by / since this is how we store the
1999 # names in the database, but that eventually needs to be converted
1999 # names in the database, but that eventually needs to be converted
2000 # into a valid system path
2000 # into a valid system path
2001 p += self.repo_name.split(self.NAME_SEP)
2001 p += self.repo_name.split(self.NAME_SEP)
2002 return os.path.join(*map(safe_unicode, p))
2002 return os.path.join(*map(safe_unicode, p))
2003
2003
2004 @property
2004 @property
2005 def cache_keys(self):
2005 def cache_keys(self):
2006 """
2006 """
2007 Returns associated cache keys for that repo
2007 Returns associated cache keys for that repo
2008 """
2008 """
2009 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2009 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2010 repo_id=self.repo_id)
2010 repo_id=self.repo_id)
2011 return CacheKey.query()\
2011 return CacheKey.query()\
2012 .filter(CacheKey.cache_args == invalidation_namespace)\
2012 .filter(CacheKey.cache_args == invalidation_namespace)\
2013 .order_by(CacheKey.cache_key)\
2013 .order_by(CacheKey.cache_key)\
2014 .all()
2014 .all()
2015
2015
2016 @property
2016 @property
2017 def cached_diffs_relative_dir(self):
2017 def cached_diffs_relative_dir(self):
2018 """
2018 """
2019 Return a relative to the repository store path of cached diffs
2019 Return a relative to the repository store path of cached diffs
2020 used for safe display for users, who shouldn't know the absolute store
2020 used for safe display for users, who shouldn't know the absolute store
2021 path
2021 path
2022 """
2022 """
2023 return os.path.join(
2023 return os.path.join(
2024 os.path.dirname(self.repo_name),
2024 os.path.dirname(self.repo_name),
2025 self.cached_diffs_dir.split(os.path.sep)[-1])
2025 self.cached_diffs_dir.split(os.path.sep)[-1])
2026
2026
2027 @property
2027 @property
2028 def cached_diffs_dir(self):
2028 def cached_diffs_dir(self):
2029 path = self.repo_full_path
2029 path = self.repo_full_path
2030 return os.path.join(
2030 return os.path.join(
2031 os.path.dirname(path),
2031 os.path.dirname(path),
2032 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2032 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2033
2033
2034 def cached_diffs(self):
2034 def cached_diffs(self):
2035 diff_cache_dir = self.cached_diffs_dir
2035 diff_cache_dir = self.cached_diffs_dir
2036 if os.path.isdir(diff_cache_dir):
2036 if os.path.isdir(diff_cache_dir):
2037 return os.listdir(diff_cache_dir)
2037 return os.listdir(diff_cache_dir)
2038 return []
2038 return []
2039
2039
2040 def shadow_repos(self):
2040 def shadow_repos(self):
2041 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2041 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2042 return [
2042 return [
2043 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2043 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2044 if x.startswith(shadow_repos_pattern)]
2044 if x.startswith(shadow_repos_pattern)]
2045
2045
2046 def get_new_name(self, repo_name):
2046 def get_new_name(self, repo_name):
2047 """
2047 """
2048 returns new full repository name based on assigned group and new new
2048 returns new full repository name based on assigned group and new new
2049
2049
2050 :param group_name:
2050 :param group_name:
2051 """
2051 """
2052 path_prefix = self.group.full_path_splitted if self.group else []
2052 path_prefix = self.group.full_path_splitted if self.group else []
2053 return self.NAME_SEP.join(path_prefix + [repo_name])
2053 return self.NAME_SEP.join(path_prefix + [repo_name])
2054
2054
2055 @property
2055 @property
2056 def _config(self):
2056 def _config(self):
2057 """
2057 """
2058 Returns db based config object.
2058 Returns db based config object.
2059 """
2059 """
2060 from rhodecode.lib.utils import make_db_config
2060 from rhodecode.lib.utils import make_db_config
2061 return make_db_config(clear_session=False, repo=self)
2061 return make_db_config(clear_session=False, repo=self)
2062
2062
2063 def permissions(self, with_admins=True, with_owner=True,
2063 def permissions(self, with_admins=True, with_owner=True,
2064 expand_from_user_groups=False):
2064 expand_from_user_groups=False):
2065 """
2065 """
2066 Permissions for repositories
2066 Permissions for repositories
2067 """
2067 """
2068 _admin_perm = 'repository.admin'
2068 _admin_perm = 'repository.admin'
2069
2069
2070 owner_row = []
2070 owner_row = []
2071 if with_owner:
2071 if with_owner:
2072 usr = AttributeDict(self.user.get_dict())
2072 usr = AttributeDict(self.user.get_dict())
2073 usr.owner_row = True
2073 usr.owner_row = True
2074 usr.permission = _admin_perm
2074 usr.permission = _admin_perm
2075 usr.permission_id = None
2075 usr.permission_id = None
2076 owner_row.append(usr)
2076 owner_row.append(usr)
2077
2077
2078 super_admin_ids = []
2078 super_admin_ids = []
2079 super_admin_rows = []
2079 super_admin_rows = []
2080 if with_admins:
2080 if with_admins:
2081 for usr in User.get_all_super_admins():
2081 for usr in User.get_all_super_admins():
2082 super_admin_ids.append(usr.user_id)
2082 super_admin_ids.append(usr.user_id)
2083 # if this admin is also owner, don't double the record
2083 # if this admin is also owner, don't double the record
2084 if usr.user_id == owner_row[0].user_id:
2084 if usr.user_id == owner_row[0].user_id:
2085 owner_row[0].admin_row = True
2085 owner_row[0].admin_row = True
2086 else:
2086 else:
2087 usr = AttributeDict(usr.get_dict())
2087 usr = AttributeDict(usr.get_dict())
2088 usr.admin_row = True
2088 usr.admin_row = True
2089 usr.permission = _admin_perm
2089 usr.permission = _admin_perm
2090 usr.permission_id = None
2090 usr.permission_id = None
2091 super_admin_rows.append(usr)
2091 super_admin_rows.append(usr)
2092
2092
2093 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2093 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2094 q = q.options(joinedload(UserRepoToPerm.repository),
2094 q = q.options(joinedload(UserRepoToPerm.repository),
2095 joinedload(UserRepoToPerm.user),
2095 joinedload(UserRepoToPerm.user),
2096 joinedload(UserRepoToPerm.permission),)
2096 joinedload(UserRepoToPerm.permission),)
2097
2097
2098 # get owners and admins and permissions. We do a trick of re-writing
2098 # get owners and admins and permissions. We do a trick of re-writing
2099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2100 # has a global reference and changing one object propagates to all
2100 # has a global reference and changing one object propagates to all
2101 # others. This means if admin is also an owner admin_row that change
2101 # others. This means if admin is also an owner admin_row that change
2102 # would propagate to both objects
2102 # would propagate to both objects
2103 perm_rows = []
2103 perm_rows = []
2104 for _usr in q.all():
2104 for _usr in q.all():
2105 usr = AttributeDict(_usr.user.get_dict())
2105 usr = AttributeDict(_usr.user.get_dict())
2106 # if this user is also owner/admin, mark as duplicate record
2106 # if this user is also owner/admin, mark as duplicate record
2107 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2107 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2108 usr.duplicate_perm = True
2108 usr.duplicate_perm = True
2109 # also check if this permission is maybe used by branch_permissions
2109 # also check if this permission is maybe used by branch_permissions
2110 if _usr.branch_perm_entry:
2110 if _usr.branch_perm_entry:
2111 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2111 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2112
2112
2113 usr.permission = _usr.permission.permission_name
2113 usr.permission = _usr.permission.permission_name
2114 usr.permission_id = _usr.repo_to_perm_id
2114 usr.permission_id = _usr.repo_to_perm_id
2115 perm_rows.append(usr)
2115 perm_rows.append(usr)
2116
2116
2117 # filter the perm rows by 'default' first and then sort them by
2117 # filter the perm rows by 'default' first and then sort them by
2118 # admin,write,read,none permissions sorted again alphabetically in
2118 # admin,write,read,none permissions sorted again alphabetically in
2119 # each group
2119 # each group
2120 perm_rows = sorted(perm_rows, key=display_user_sort)
2120 perm_rows = sorted(perm_rows, key=display_user_sort)
2121
2121
2122 user_groups_rows = []
2122 user_groups_rows = []
2123 if expand_from_user_groups:
2123 if expand_from_user_groups:
2124 for ug in self.permission_user_groups(with_members=True):
2124 for ug in self.permission_user_groups(with_members=True):
2125 for user_data in ug.members:
2125 for user_data in ug.members:
2126 user_groups_rows.append(user_data)
2126 user_groups_rows.append(user_data)
2127
2127
2128 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2128 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2129
2129
2130 def permission_user_groups(self, with_members=True):
2130 def permission_user_groups(self, with_members=True):
2131 q = UserGroupRepoToPerm.query()\
2131 q = UserGroupRepoToPerm.query()\
2132 .filter(UserGroupRepoToPerm.repository == self)
2132 .filter(UserGroupRepoToPerm.repository == self)
2133 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2133 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2134 joinedload(UserGroupRepoToPerm.users_group),
2134 joinedload(UserGroupRepoToPerm.users_group),
2135 joinedload(UserGroupRepoToPerm.permission),)
2135 joinedload(UserGroupRepoToPerm.permission),)
2136
2136
2137 perm_rows = []
2137 perm_rows = []
2138 for _user_group in q.all():
2138 for _user_group in q.all():
2139 entry = AttributeDict(_user_group.users_group.get_dict())
2139 entry = AttributeDict(_user_group.users_group.get_dict())
2140 entry.permission = _user_group.permission.permission_name
2140 entry.permission = _user_group.permission.permission_name
2141 if with_members:
2141 if with_members:
2142 entry.members = [x.user.get_dict()
2142 entry.members = [x.user.get_dict()
2143 for x in _user_group.users_group.members]
2143 for x in _user_group.users_group.members]
2144 perm_rows.append(entry)
2144 perm_rows.append(entry)
2145
2145
2146 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2146 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2147 return perm_rows
2147 return perm_rows
2148
2148
2149 def get_api_data(self, include_secrets=False):
2149 def get_api_data(self, include_secrets=False):
2150 """
2150 """
2151 Common function for generating repo api data
2151 Common function for generating repo api data
2152
2152
2153 :param include_secrets: See :meth:`User.get_api_data`.
2153 :param include_secrets: See :meth:`User.get_api_data`.
2154
2154
2155 """
2155 """
2156 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2156 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2157 # move this methods on models level.
2157 # move this methods on models level.
2158 from rhodecode.model.settings import SettingsModel
2158 from rhodecode.model.settings import SettingsModel
2159 from rhodecode.model.repo import RepoModel
2159 from rhodecode.model.repo import RepoModel
2160
2160
2161 repo = self
2161 repo = self
2162 _user_id, _time, _reason = self.locked
2162 _user_id, _time, _reason = self.locked
2163
2163
2164 data = {
2164 data = {
2165 'repo_id': repo.repo_id,
2165 'repo_id': repo.repo_id,
2166 'repo_name': repo.repo_name,
2166 'repo_name': repo.repo_name,
2167 'repo_type': repo.repo_type,
2167 'repo_type': repo.repo_type,
2168 'clone_uri': repo.clone_uri or '',
2168 'clone_uri': repo.clone_uri or '',
2169 'push_uri': repo.push_uri or '',
2169 'push_uri': repo.push_uri or '',
2170 'url': RepoModel().get_url(self),
2170 'url': RepoModel().get_url(self),
2171 'private': repo.private,
2171 'private': repo.private,
2172 'created_on': repo.created_on,
2172 'created_on': repo.created_on,
2173 'description': repo.description_safe,
2173 'description': repo.description_safe,
2174 'landing_rev': repo.landing_rev,
2174 'landing_rev': repo.landing_rev,
2175 'owner': repo.user.username,
2175 'owner': repo.user.username,
2176 'fork_of': repo.fork.repo_name if repo.fork else None,
2176 'fork_of': repo.fork.repo_name if repo.fork else None,
2177 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2177 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2178 'enable_statistics': repo.enable_statistics,
2178 'enable_statistics': repo.enable_statistics,
2179 'enable_locking': repo.enable_locking,
2179 'enable_locking': repo.enable_locking,
2180 'enable_downloads': repo.enable_downloads,
2180 'enable_downloads': repo.enable_downloads,
2181 'last_changeset': repo.changeset_cache,
2181 'last_changeset': repo.changeset_cache,
2182 'locked_by': User.get(_user_id).get_api_data(
2182 'locked_by': User.get(_user_id).get_api_data(
2183 include_secrets=include_secrets) if _user_id else None,
2183 include_secrets=include_secrets) if _user_id else None,
2184 'locked_date': time_to_datetime(_time) if _time else None,
2184 'locked_date': time_to_datetime(_time) if _time else None,
2185 'lock_reason': _reason if _reason else None,
2185 'lock_reason': _reason if _reason else None,
2186 }
2186 }
2187
2187
2188 # TODO: mikhail: should be per-repo settings here
2188 # TODO: mikhail: should be per-repo settings here
2189 rc_config = SettingsModel().get_all_settings()
2189 rc_config = SettingsModel().get_all_settings()
2190 repository_fields = str2bool(
2190 repository_fields = str2bool(
2191 rc_config.get('rhodecode_repository_fields'))
2191 rc_config.get('rhodecode_repository_fields'))
2192 if repository_fields:
2192 if repository_fields:
2193 for f in self.extra_fields:
2193 for f in self.extra_fields:
2194 data[f.field_key_prefixed] = f.field_value
2194 data[f.field_key_prefixed] = f.field_value
2195
2195
2196 return data
2196 return data
2197
2197
2198 @classmethod
2198 @classmethod
2199 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2199 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2200 if not lock_time:
2200 if not lock_time:
2201 lock_time = time.time()
2201 lock_time = time.time()
2202 if not lock_reason:
2202 if not lock_reason:
2203 lock_reason = cls.LOCK_AUTOMATIC
2203 lock_reason = cls.LOCK_AUTOMATIC
2204 repo.locked = [user_id, lock_time, lock_reason]
2204 repo.locked = [user_id, lock_time, lock_reason]
2205 Session().add(repo)
2205 Session().add(repo)
2206 Session().commit()
2206 Session().commit()
2207
2207
2208 @classmethod
2208 @classmethod
2209 def unlock(cls, repo):
2209 def unlock(cls, repo):
2210 repo.locked = None
2210 repo.locked = None
2211 Session().add(repo)
2211 Session().add(repo)
2212 Session().commit()
2212 Session().commit()
2213
2213
2214 @classmethod
2214 @classmethod
2215 def getlock(cls, repo):
2215 def getlock(cls, repo):
2216 return repo.locked
2216 return repo.locked
2217
2217
2218 def is_user_lock(self, user_id):
2218 def is_user_lock(self, user_id):
2219 if self.lock[0]:
2219 if self.lock[0]:
2220 lock_user_id = safe_int(self.lock[0])
2220 lock_user_id = safe_int(self.lock[0])
2221 user_id = safe_int(user_id)
2221 user_id = safe_int(user_id)
2222 # both are ints, and they are equal
2222 # both are ints, and they are equal
2223 return all([lock_user_id, user_id]) and lock_user_id == user_id
2223 return all([lock_user_id, user_id]) and lock_user_id == user_id
2224
2224
2225 return False
2225 return False
2226
2226
2227 def get_locking_state(self, action, user_id, only_when_enabled=True):
2227 def get_locking_state(self, action, user_id, only_when_enabled=True):
2228 """
2228 """
2229 Checks locking on this repository, if locking is enabled and lock is
2229 Checks locking on this repository, if locking is enabled and lock is
2230 present returns a tuple of make_lock, locked, locked_by.
2230 present returns a tuple of make_lock, locked, locked_by.
2231 make_lock can have 3 states None (do nothing) True, make lock
2231 make_lock can have 3 states None (do nothing) True, make lock
2232 False release lock, This value is later propagated to hooks, which
2232 False release lock, This value is later propagated to hooks, which
2233 do the locking. Think about this as signals passed to hooks what to do.
2233 do the locking. Think about this as signals passed to hooks what to do.
2234
2234
2235 """
2235 """
2236 # TODO: johbo: This is part of the business logic and should be moved
2236 # TODO: johbo: This is part of the business logic and should be moved
2237 # into the RepositoryModel.
2237 # into the RepositoryModel.
2238
2238
2239 if action not in ('push', 'pull'):
2239 if action not in ('push', 'pull'):
2240 raise ValueError("Invalid action value: %s" % repr(action))
2240 raise ValueError("Invalid action value: %s" % repr(action))
2241
2241
2242 # defines if locked error should be thrown to user
2242 # defines if locked error should be thrown to user
2243 currently_locked = False
2243 currently_locked = False
2244 # defines if new lock should be made, tri-state
2244 # defines if new lock should be made, tri-state
2245 make_lock = None
2245 make_lock = None
2246 repo = self
2246 repo = self
2247 user = User.get(user_id)
2247 user = User.get(user_id)
2248
2248
2249 lock_info = repo.locked
2249 lock_info = repo.locked
2250
2250
2251 if repo and (repo.enable_locking or not only_when_enabled):
2251 if repo and (repo.enable_locking or not only_when_enabled):
2252 if action == 'push':
2252 if action == 'push':
2253 # check if it's already locked !, if it is compare users
2253 # check if it's already locked !, if it is compare users
2254 locked_by_user_id = lock_info[0]
2254 locked_by_user_id = lock_info[0]
2255 if user.user_id == locked_by_user_id:
2255 if user.user_id == locked_by_user_id:
2256 log.debug(
2256 log.debug(
2257 'Got `push` action from user %s, now unlocking', user)
2257 'Got `push` action from user %s, now unlocking', user)
2258 # unlock if we have push from user who locked
2258 # unlock if we have push from user who locked
2259 make_lock = False
2259 make_lock = False
2260 else:
2260 else:
2261 # we're not the same user who locked, ban with
2261 # we're not the same user who locked, ban with
2262 # code defined in settings (default is 423 HTTP Locked) !
2262 # code defined in settings (default is 423 HTTP Locked) !
2263 log.debug('Repo %s is currently locked by %s', repo, user)
2263 log.debug('Repo %s is currently locked by %s', repo, user)
2264 currently_locked = True
2264 currently_locked = True
2265 elif action == 'pull':
2265 elif action == 'pull':
2266 # [0] user [1] date
2266 # [0] user [1] date
2267 if lock_info[0] and lock_info[1]:
2267 if lock_info[0] and lock_info[1]:
2268 log.debug('Repo %s is currently locked by %s', repo, user)
2268 log.debug('Repo %s is currently locked by %s', repo, user)
2269 currently_locked = True
2269 currently_locked = True
2270 else:
2270 else:
2271 log.debug('Setting lock on repo %s by %s', repo, user)
2271 log.debug('Setting lock on repo %s by %s', repo, user)
2272 make_lock = True
2272 make_lock = True
2273
2273
2274 else:
2274 else:
2275 log.debug('Repository %s do not have locking enabled', repo)
2275 log.debug('Repository %s do not have locking enabled', repo)
2276
2276
2277 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2277 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2278 make_lock, currently_locked, lock_info)
2278 make_lock, currently_locked, lock_info)
2279
2279
2280 from rhodecode.lib.auth import HasRepoPermissionAny
2280 from rhodecode.lib.auth import HasRepoPermissionAny
2281 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2281 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2282 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2282 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2283 # if we don't have at least write permission we cannot make a lock
2283 # if we don't have at least write permission we cannot make a lock
2284 log.debug('lock state reset back to FALSE due to lack '
2284 log.debug('lock state reset back to FALSE due to lack '
2285 'of at least read permission')
2285 'of at least read permission')
2286 make_lock = False
2286 make_lock = False
2287
2287
2288 return make_lock, currently_locked, lock_info
2288 return make_lock, currently_locked, lock_info
2289
2289
2290 @property
2290 @property
2291 def last_commit_cache_update_diff(self):
2291 def last_commit_cache_update_diff(self):
2292 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2292 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2293
2293
2294 @classmethod
2294 @classmethod
2295 def _load_commit_change(cls, last_commit_cache):
2295 def _load_commit_change(cls, last_commit_cache):
2296 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2296 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2297 empty_date = datetime.datetime.fromtimestamp(0)
2297 empty_date = datetime.datetime.fromtimestamp(0)
2298 date_latest = last_commit_cache.get('date', empty_date)
2298 date_latest = last_commit_cache.get('date', empty_date)
2299 try:
2299 try:
2300 return parse_datetime(date_latest)
2300 return parse_datetime(date_latest)
2301 except Exception:
2301 except Exception:
2302 return empty_date
2302 return empty_date
2303
2303
2304 @property
2304 @property
2305 def last_commit_change(self):
2305 def last_commit_change(self):
2306 return self._load_commit_change(self.changeset_cache)
2306 return self._load_commit_change(self.changeset_cache)
2307
2307
2308 @property
2308 @property
2309 def last_db_change(self):
2309 def last_db_change(self):
2310 return self.updated_on
2310 return self.updated_on
2311
2311
2312 @property
2312 @property
2313 def clone_uri_hidden(self):
2313 def clone_uri_hidden(self):
2314 clone_uri = self.clone_uri
2314 clone_uri = self.clone_uri
2315 if clone_uri:
2315 if clone_uri:
2316 import urlobject
2316 import urlobject
2317 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2317 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2318 if url_obj.password:
2318 if url_obj.password:
2319 clone_uri = url_obj.with_password('*****')
2319 clone_uri = url_obj.with_password('*****')
2320 return clone_uri
2320 return clone_uri
2321
2321
2322 @property
2322 @property
2323 def push_uri_hidden(self):
2323 def push_uri_hidden(self):
2324 push_uri = self.push_uri
2324 push_uri = self.push_uri
2325 if push_uri:
2325 if push_uri:
2326 import urlobject
2326 import urlobject
2327 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2327 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2328 if url_obj.password:
2328 if url_obj.password:
2329 push_uri = url_obj.with_password('*****')
2329 push_uri = url_obj.with_password('*****')
2330 return push_uri
2330 return push_uri
2331
2331
2332 def clone_url(self, **override):
2332 def clone_url(self, **override):
2333 from rhodecode.model.settings import SettingsModel
2333 from rhodecode.model.settings import SettingsModel
2334
2334
2335 uri_tmpl = None
2335 uri_tmpl = None
2336 if 'with_id' in override:
2336 if 'with_id' in override:
2337 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2337 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2338 del override['with_id']
2338 del override['with_id']
2339
2339
2340 if 'uri_tmpl' in override:
2340 if 'uri_tmpl' in override:
2341 uri_tmpl = override['uri_tmpl']
2341 uri_tmpl = override['uri_tmpl']
2342 del override['uri_tmpl']
2342 del override['uri_tmpl']
2343
2343
2344 ssh = False
2344 ssh = False
2345 if 'ssh' in override:
2345 if 'ssh' in override:
2346 ssh = True
2346 ssh = True
2347 del override['ssh']
2347 del override['ssh']
2348
2348
2349 # we didn't override our tmpl from **overrides
2349 # we didn't override our tmpl from **overrides
2350 request = get_current_request()
2350 request = get_current_request()
2351 if not uri_tmpl:
2351 if not uri_tmpl:
2352 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2352 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2353 rc_config = request.call_context.rc_config
2353 rc_config = request.call_context.rc_config
2354 else:
2354 else:
2355 rc_config = SettingsModel().get_all_settings(cache=True)
2355 rc_config = SettingsModel().get_all_settings(cache=True)
2356
2356
2357 if ssh:
2357 if ssh:
2358 uri_tmpl = rc_config.get(
2358 uri_tmpl = rc_config.get(
2359 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2359 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2360
2360
2361 else:
2361 else:
2362 uri_tmpl = rc_config.get(
2362 uri_tmpl = rc_config.get(
2363 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2363 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2364
2364
2365 return get_clone_url(request=request,
2365 return get_clone_url(request=request,
2366 uri_tmpl=uri_tmpl,
2366 uri_tmpl=uri_tmpl,
2367 repo_name=self.repo_name,
2367 repo_name=self.repo_name,
2368 repo_id=self.repo_id,
2368 repo_id=self.repo_id,
2369 repo_type=self.repo_type,
2369 repo_type=self.repo_type,
2370 **override)
2370 **override)
2371
2371
2372 def set_state(self, state):
2372 def set_state(self, state):
2373 self.repo_state = state
2373 self.repo_state = state
2374 Session().add(self)
2374 Session().add(self)
2375 #==========================================================================
2375 #==========================================================================
2376 # SCM PROPERTIES
2376 # SCM PROPERTIES
2377 #==========================================================================
2377 #==========================================================================
2378
2378
2379 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2379 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2380 return get_commit_safe(
2380 return get_commit_safe(
2381 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2381 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2382 maybe_unreachable=maybe_unreachable)
2382 maybe_unreachable=maybe_unreachable)
2383
2383
2384 def get_changeset(self, rev=None, pre_load=None):
2384 def get_changeset(self, rev=None, pre_load=None):
2385 warnings.warn("Use get_commit", DeprecationWarning)
2385 warnings.warn("Use get_commit", DeprecationWarning)
2386 commit_id = None
2386 commit_id = None
2387 commit_idx = None
2387 commit_idx = None
2388 if isinstance(rev, compat.string_types):
2388 if isinstance(rev, compat.string_types):
2389 commit_id = rev
2389 commit_id = rev
2390 else:
2390 else:
2391 commit_idx = rev
2391 commit_idx = rev
2392 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2392 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2393 pre_load=pre_load)
2393 pre_load=pre_load)
2394
2394
2395 def get_landing_commit(self):
2395 def get_landing_commit(self):
2396 """
2396 """
2397 Returns landing commit, or if that doesn't exist returns the tip
2397 Returns landing commit, or if that doesn't exist returns the tip
2398 """
2398 """
2399 _rev_type, _rev = self.landing_rev
2399 _rev_type, _rev = self.landing_rev
2400 commit = self.get_commit(_rev)
2400 commit = self.get_commit(_rev)
2401 if isinstance(commit, EmptyCommit):
2401 if isinstance(commit, EmptyCommit):
2402 return self.get_commit()
2402 return self.get_commit()
2403 return commit
2403 return commit
2404
2404
2405 def flush_commit_cache(self):
2405 def flush_commit_cache(self):
2406 self.update_commit_cache(cs_cache={'raw_id':'0'})
2406 self.update_commit_cache(cs_cache={'raw_id':'0'})
2407 self.update_commit_cache()
2407 self.update_commit_cache()
2408
2408
2409 def update_commit_cache(self, cs_cache=None, config=None):
2409 def update_commit_cache(self, cs_cache=None, config=None):
2410 """
2410 """
2411 Update cache of last commit for repository
2411 Update cache of last commit for repository
2412 cache_keys should be::
2412 cache_keys should be::
2413
2413
2414 source_repo_id
2414 source_repo_id
2415 short_id
2415 short_id
2416 raw_id
2416 raw_id
2417 revision
2417 revision
2418 parents
2418 parents
2419 message
2419 message
2420 date
2420 date
2421 author
2421 author
2422 updated_on
2422 updated_on
2423
2423
2424 """
2424 """
2425 from rhodecode.lib.vcs.backends.base import BaseChangeset
2425 from rhodecode.lib.vcs.backends.base import BaseChangeset
2426 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2426 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2427 empty_date = datetime.datetime.fromtimestamp(0)
2427 empty_date = datetime.datetime.fromtimestamp(0)
2428
2428
2429 if cs_cache is None:
2429 if cs_cache is None:
2430 # use no-cache version here
2430 # use no-cache version here
2431 try:
2431 try:
2432 scm_repo = self.scm_instance(cache=False, config=config)
2432 scm_repo = self.scm_instance(cache=False, config=config)
2433 except VCSError:
2433 except VCSError:
2434 scm_repo = None
2434 scm_repo = None
2435 empty = scm_repo is None or scm_repo.is_empty()
2435 empty = scm_repo is None or scm_repo.is_empty()
2436
2436
2437 if not empty:
2437 if not empty:
2438 cs_cache = scm_repo.get_commit(
2438 cs_cache = scm_repo.get_commit(
2439 pre_load=["author", "date", "message", "parents", "branch"])
2439 pre_load=["author", "date", "message", "parents", "branch"])
2440 else:
2440 else:
2441 cs_cache = EmptyCommit()
2441 cs_cache = EmptyCommit()
2442
2442
2443 if isinstance(cs_cache, BaseChangeset):
2443 if isinstance(cs_cache, BaseChangeset):
2444 cs_cache = cs_cache.__json__()
2444 cs_cache = cs_cache.__json__()
2445
2445
2446 def is_outdated(new_cs_cache):
2446 def is_outdated(new_cs_cache):
2447 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2447 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2448 new_cs_cache['revision'] != self.changeset_cache['revision']):
2448 new_cs_cache['revision'] != self.changeset_cache['revision']):
2449 return True
2449 return True
2450 return False
2450 return False
2451
2451
2452 # check if we have maybe already latest cached revision
2452 # check if we have maybe already latest cached revision
2453 if is_outdated(cs_cache) or not self.changeset_cache:
2453 if is_outdated(cs_cache) or not self.changeset_cache:
2454 _current_datetime = datetime.datetime.utcnow()
2454 _current_datetime = datetime.datetime.utcnow()
2455 last_change = cs_cache.get('date') or _current_datetime
2455 last_change = cs_cache.get('date') or _current_datetime
2456 # we check if last update is newer than the new value
2456 # we check if last update is newer than the new value
2457 # if yes, we use the current timestamp instead. Imagine you get
2457 # if yes, we use the current timestamp instead. Imagine you get
2458 # old commit pushed 1y ago, we'd set last update 1y to ago.
2458 # old commit pushed 1y ago, we'd set last update 1y to ago.
2459 last_change_timestamp = datetime_to_time(last_change)
2459 last_change_timestamp = datetime_to_time(last_change)
2460 current_timestamp = datetime_to_time(last_change)
2460 current_timestamp = datetime_to_time(last_change)
2461 if last_change_timestamp > current_timestamp and not empty:
2461 if last_change_timestamp > current_timestamp and not empty:
2462 cs_cache['date'] = _current_datetime
2462 cs_cache['date'] = _current_datetime
2463
2463
2464 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2464 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2465 cs_cache['updated_on'] = time.time()
2465 cs_cache['updated_on'] = time.time()
2466 self.changeset_cache = cs_cache
2466 self.changeset_cache = cs_cache
2467 self.updated_on = last_change
2467 self.updated_on = last_change
2468 Session().add(self)
2468 Session().add(self)
2469 Session().commit()
2469 Session().commit()
2470
2470
2471 else:
2471 else:
2472 if empty:
2472 if empty:
2473 cs_cache = EmptyCommit().__json__()
2473 cs_cache = EmptyCommit().__json__()
2474 else:
2474 else:
2475 cs_cache = self.changeset_cache
2475 cs_cache = self.changeset_cache
2476
2476
2477 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2477 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2478
2478
2479 cs_cache['updated_on'] = time.time()
2479 cs_cache['updated_on'] = time.time()
2480 self.changeset_cache = cs_cache
2480 self.changeset_cache = cs_cache
2481 self.updated_on = _date_latest
2481 self.updated_on = _date_latest
2482 Session().add(self)
2482 Session().add(self)
2483 Session().commit()
2483 Session().commit()
2484
2484
2485 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2485 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2486 self.repo_name, cs_cache, _date_latest)
2486 self.repo_name, cs_cache, _date_latest)
2487
2487
2488 @property
2488 @property
2489 def tip(self):
2489 def tip(self):
2490 return self.get_commit('tip')
2490 return self.get_commit('tip')
2491
2491
2492 @property
2492 @property
2493 def author(self):
2493 def author(self):
2494 return self.tip.author
2494 return self.tip.author
2495
2495
2496 @property
2496 @property
2497 def last_change(self):
2497 def last_change(self):
2498 return self.scm_instance().last_change
2498 return self.scm_instance().last_change
2499
2499
2500 def get_comments(self, revisions=None):
2500 def get_comments(self, revisions=None):
2501 """
2501 """
2502 Returns comments for this repository grouped by revisions
2502 Returns comments for this repository grouped by revisions
2503
2503
2504 :param revisions: filter query by revisions only
2504 :param revisions: filter query by revisions only
2505 """
2505 """
2506 cmts = ChangesetComment.query()\
2506 cmts = ChangesetComment.query()\
2507 .filter(ChangesetComment.repo == self)
2507 .filter(ChangesetComment.repo == self)
2508 if revisions:
2508 if revisions:
2509 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2509 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2510 grouped = collections.defaultdict(list)
2510 grouped = collections.defaultdict(list)
2511 for cmt in cmts.all():
2511 for cmt in cmts.all():
2512 grouped[cmt.revision].append(cmt)
2512 grouped[cmt.revision].append(cmt)
2513 return grouped
2513 return grouped
2514
2514
2515 def statuses(self, revisions=None):
2515 def statuses(self, revisions=None):
2516 """
2516 """
2517 Returns statuses for this repository
2517 Returns statuses for this repository
2518
2518
2519 :param revisions: list of revisions to get statuses for
2519 :param revisions: list of revisions to get statuses for
2520 """
2520 """
2521 statuses = ChangesetStatus.query()\
2521 statuses = ChangesetStatus.query()\
2522 .filter(ChangesetStatus.repo == self)\
2522 .filter(ChangesetStatus.repo == self)\
2523 .filter(ChangesetStatus.version == 0)
2523 .filter(ChangesetStatus.version == 0)
2524
2524
2525 if revisions:
2525 if revisions:
2526 # Try doing the filtering in chunks to avoid hitting limits
2526 # Try doing the filtering in chunks to avoid hitting limits
2527 size = 500
2527 size = 500
2528 status_results = []
2528 status_results = []
2529 for chunk in xrange(0, len(revisions), size):
2529 for chunk in xrange(0, len(revisions), size):
2530 status_results += statuses.filter(
2530 status_results += statuses.filter(
2531 ChangesetStatus.revision.in_(
2531 ChangesetStatus.revision.in_(
2532 revisions[chunk: chunk+size])
2532 revisions[chunk: chunk+size])
2533 ).all()
2533 ).all()
2534 else:
2534 else:
2535 status_results = statuses.all()
2535 status_results = statuses.all()
2536
2536
2537 grouped = {}
2537 grouped = {}
2538
2538
2539 # maybe we have open new pullrequest without a status?
2539 # maybe we have open new pullrequest without a status?
2540 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2540 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2541 status_lbl = ChangesetStatus.get_status_lbl(stat)
2541 status_lbl = ChangesetStatus.get_status_lbl(stat)
2542 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2542 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2543 for rev in pr.revisions:
2543 for rev in pr.revisions:
2544 pr_id = pr.pull_request_id
2544 pr_id = pr.pull_request_id
2545 pr_repo = pr.target_repo.repo_name
2545 pr_repo = pr.target_repo.repo_name
2546 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2546 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2547
2547
2548 for stat in status_results:
2548 for stat in status_results:
2549 pr_id = pr_repo = None
2549 pr_id = pr_repo = None
2550 if stat.pull_request:
2550 if stat.pull_request:
2551 pr_id = stat.pull_request.pull_request_id
2551 pr_id = stat.pull_request.pull_request_id
2552 pr_repo = stat.pull_request.target_repo.repo_name
2552 pr_repo = stat.pull_request.target_repo.repo_name
2553 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2553 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2554 pr_id, pr_repo]
2554 pr_id, pr_repo]
2555 return grouped
2555 return grouped
2556
2556
2557 # ==========================================================================
2557 # ==========================================================================
2558 # SCM CACHE INSTANCE
2558 # SCM CACHE INSTANCE
2559 # ==========================================================================
2559 # ==========================================================================
2560
2560
2561 def scm_instance(self, **kwargs):
2561 def scm_instance(self, **kwargs):
2562 import rhodecode
2562 import rhodecode
2563
2563
2564 # Passing a config will not hit the cache currently only used
2564 # Passing a config will not hit the cache currently only used
2565 # for repo2dbmapper
2565 # for repo2dbmapper
2566 config = kwargs.pop('config', None)
2566 config = kwargs.pop('config', None)
2567 cache = kwargs.pop('cache', None)
2567 cache = kwargs.pop('cache', None)
2568 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2568 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2569 if vcs_full_cache is not None:
2569 if vcs_full_cache is not None:
2570 # allows override global config
2570 # allows override global config
2571 full_cache = vcs_full_cache
2571 full_cache = vcs_full_cache
2572 else:
2572 else:
2573 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2573 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2574 # if cache is NOT defined use default global, else we have a full
2574 # if cache is NOT defined use default global, else we have a full
2575 # control over cache behaviour
2575 # control over cache behaviour
2576 if cache is None and full_cache and not config:
2576 if cache is None and full_cache and not config:
2577 log.debug('Initializing pure cached instance for %s', self.repo_path)
2577 log.debug('Initializing pure cached instance for %s', self.repo_path)
2578 return self._get_instance_cached()
2578 return self._get_instance_cached()
2579
2579
2580 # cache here is sent to the "vcs server"
2580 # cache here is sent to the "vcs server"
2581 return self._get_instance(cache=bool(cache), config=config)
2581 return self._get_instance(cache=bool(cache), config=config)
2582
2582
2583 def _get_instance_cached(self):
2583 def _get_instance_cached(self):
2584 from rhodecode.lib import rc_cache
2584 from rhodecode.lib import rc_cache
2585
2585
2586 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2586 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2587 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2587 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2588 repo_id=self.repo_id)
2588 repo_id=self.repo_id)
2589 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2589 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2590
2590
2591 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2591 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2592 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2592 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2593 return self._get_instance(repo_state_uid=_cache_state_uid)
2593 return self._get_instance(repo_state_uid=_cache_state_uid)
2594
2594
2595 # we must use thread scoped cache here,
2595 # we must use thread scoped cache here,
2596 # because each thread of gevent needs it's own not shared connection and cache
2596 # because each thread of gevent needs it's own not shared connection and cache
2597 # we also alter `args` so the cache key is individual for every green thread.
2597 # we also alter `args` so the cache key is individual for every green thread.
2598 inv_context_manager = rc_cache.InvalidationContext(
2598 inv_context_manager = rc_cache.InvalidationContext(
2599 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2599 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2600 thread_scoped=True)
2600 thread_scoped=True)
2601 with inv_context_manager as invalidation_context:
2601 with inv_context_manager as invalidation_context:
2602 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2602 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2603 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2603 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2604
2604
2605 # re-compute and store cache if we get invalidate signal
2605 # re-compute and store cache if we get invalidate signal
2606 if invalidation_context.should_invalidate():
2606 if invalidation_context.should_invalidate():
2607 instance = get_instance_cached.refresh(*args)
2607 instance = get_instance_cached.refresh(*args)
2608 else:
2608 else:
2609 instance = get_instance_cached(*args)
2609 instance = get_instance_cached(*args)
2610
2610
2611 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2611 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2612 return instance
2612 return instance
2613
2613
2614 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2614 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2615 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2615 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2616 self.repo_type, self.repo_path, cache)
2616 self.repo_type, self.repo_path, cache)
2617 config = config or self._config
2617 config = config or self._config
2618 custom_wire = {
2618 custom_wire = {
2619 'cache': cache, # controls the vcs.remote cache
2619 'cache': cache, # controls the vcs.remote cache
2620 'repo_state_uid': repo_state_uid
2620 'repo_state_uid': repo_state_uid
2621 }
2621 }
2622 repo = get_vcs_instance(
2622 repo = get_vcs_instance(
2623 repo_path=safe_str(self.repo_full_path),
2623 repo_path=safe_str(self.repo_full_path),
2624 config=config,
2624 config=config,
2625 with_wire=custom_wire,
2625 with_wire=custom_wire,
2626 create=False,
2626 create=False,
2627 _vcs_alias=self.repo_type)
2627 _vcs_alias=self.repo_type)
2628 if repo is not None:
2628 if repo is not None:
2629 repo.count() # cache rebuild
2629 repo.count() # cache rebuild
2630 return repo
2630 return repo
2631
2631
2632 def get_shadow_repository_path(self, workspace_id):
2632 def get_shadow_repository_path(self, workspace_id):
2633 from rhodecode.lib.vcs.backends.base import BaseRepository
2633 from rhodecode.lib.vcs.backends.base import BaseRepository
2634 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2634 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2635 self.repo_full_path, self.repo_id, workspace_id)
2635 self.repo_full_path, self.repo_id, workspace_id)
2636 return shadow_repo_path
2636 return shadow_repo_path
2637
2637
2638 def __json__(self):
2638 def __json__(self):
2639 return {'landing_rev': self.landing_rev}
2639 return {'landing_rev': self.landing_rev}
2640
2640
2641 def get_dict(self):
2641 def get_dict(self):
2642
2642
2643 # Since we transformed `repo_name` to a hybrid property, we need to
2643 # Since we transformed `repo_name` to a hybrid property, we need to
2644 # keep compatibility with the code which uses `repo_name` field.
2644 # keep compatibility with the code which uses `repo_name` field.
2645
2645
2646 result = super(Repository, self).get_dict()
2646 result = super(Repository, self).get_dict()
2647 result['repo_name'] = result.pop('_repo_name', None)
2647 result['repo_name'] = result.pop('_repo_name', None)
2648 return result
2648 return result
2649
2649
2650
2650
2651 class RepoGroup(Base, BaseModel):
2651 class RepoGroup(Base, BaseModel):
2652 __tablename__ = 'groups'
2652 __tablename__ = 'groups'
2653 __table_args__ = (
2653 __table_args__ = (
2654 UniqueConstraint('group_name', 'group_parent_id'),
2654 UniqueConstraint('group_name', 'group_parent_id'),
2655 base_table_args,
2655 base_table_args,
2656 )
2656 )
2657 __mapper_args__ = {'order_by': 'group_name'}
2657 __mapper_args__ = {'order_by': 'group_name'}
2658
2658
2659 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2659 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2660
2660
2661 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2661 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2662 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2662 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2663 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2663 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2664 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2664 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2665 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2665 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2666 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2666 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2667 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2667 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2668 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2668 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2669 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2669 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2670 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2670 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2671 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2671 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2672
2672
2673 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2673 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2674 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2674 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2675 parent_group = relationship('RepoGroup', remote_side=group_id)
2675 parent_group = relationship('RepoGroup', remote_side=group_id)
2676 user = relationship('User')
2676 user = relationship('User')
2677 integrations = relationship('Integration', cascade="all, delete-orphan")
2677 integrations = relationship('Integration', cascade="all, delete-orphan")
2678
2678
2679 # no cascade, set NULL
2679 # no cascade, set NULL
2680 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2680 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2681
2681
2682 def __init__(self, group_name='', parent_group=None):
2682 def __init__(self, group_name='', parent_group=None):
2683 self.group_name = group_name
2683 self.group_name = group_name
2684 self.parent_group = parent_group
2684 self.parent_group = parent_group
2685
2685
2686 def __unicode__(self):
2686 def __unicode__(self):
2687 return u"<%s('id:%s:%s')>" % (
2687 return u"<%s('id:%s:%s')>" % (
2688 self.__class__.__name__, self.group_id, self.group_name)
2688 self.__class__.__name__, self.group_id, self.group_name)
2689
2689
2690 @hybrid_property
2690 @hybrid_property
2691 def group_name(self):
2691 def group_name(self):
2692 return self._group_name
2692 return self._group_name
2693
2693
2694 @group_name.setter
2694 @group_name.setter
2695 def group_name(self, value):
2695 def group_name(self, value):
2696 self._group_name = value
2696 self._group_name = value
2697 self.group_name_hash = self.hash_repo_group_name(value)
2697 self.group_name_hash = self.hash_repo_group_name(value)
2698
2698
2699 @classmethod
2699 @classmethod
2700 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2700 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2701 from rhodecode.lib.vcs.backends.base import EmptyCommit
2701 from rhodecode.lib.vcs.backends.base import EmptyCommit
2702 dummy = EmptyCommit().__json__()
2702 dummy = EmptyCommit().__json__()
2703 if not changeset_cache_raw:
2703 if not changeset_cache_raw:
2704 dummy['source_repo_id'] = repo_id
2704 dummy['source_repo_id'] = repo_id
2705 return json.loads(json.dumps(dummy))
2705 return json.loads(json.dumps(dummy))
2706
2706
2707 try:
2707 try:
2708 return json.loads(changeset_cache_raw)
2708 return json.loads(changeset_cache_raw)
2709 except TypeError:
2709 except TypeError:
2710 return dummy
2710 return dummy
2711 except Exception:
2711 except Exception:
2712 log.error(traceback.format_exc())
2712 log.error(traceback.format_exc())
2713 return dummy
2713 return dummy
2714
2714
2715 @hybrid_property
2715 @hybrid_property
2716 def changeset_cache(self):
2716 def changeset_cache(self):
2717 return self._load_changeset_cache('', self._changeset_cache)
2717 return self._load_changeset_cache('', self._changeset_cache)
2718
2718
2719 @changeset_cache.setter
2719 @changeset_cache.setter
2720 def changeset_cache(self, val):
2720 def changeset_cache(self, val):
2721 try:
2721 try:
2722 self._changeset_cache = json.dumps(val)
2722 self._changeset_cache = json.dumps(val)
2723 except Exception:
2723 except Exception:
2724 log.error(traceback.format_exc())
2724 log.error(traceback.format_exc())
2725
2725
2726 @validates('group_parent_id')
2726 @validates('group_parent_id')
2727 def validate_group_parent_id(self, key, val):
2727 def validate_group_parent_id(self, key, val):
2728 """
2728 """
2729 Check cycle references for a parent group to self
2729 Check cycle references for a parent group to self
2730 """
2730 """
2731 if self.group_id and val:
2731 if self.group_id and val:
2732 assert val != self.group_id
2732 assert val != self.group_id
2733
2733
2734 return val
2734 return val
2735
2735
2736 @hybrid_property
2736 @hybrid_property
2737 def description_safe(self):
2737 def description_safe(self):
2738 from rhodecode.lib import helpers as h
2738 from rhodecode.lib import helpers as h
2739 return h.escape(self.group_description)
2739 return h.escape(self.group_description)
2740
2740
2741 @classmethod
2741 @classmethod
2742 def hash_repo_group_name(cls, repo_group_name):
2742 def hash_repo_group_name(cls, repo_group_name):
2743 val = remove_formatting(repo_group_name)
2743 val = remove_formatting(repo_group_name)
2744 val = safe_str(val).lower()
2744 val = safe_str(val).lower()
2745 chars = []
2745 chars = []
2746 for c in val:
2746 for c in val:
2747 if c not in string.ascii_letters:
2747 if c not in string.ascii_letters:
2748 c = str(ord(c))
2748 c = str(ord(c))
2749 chars.append(c)
2749 chars.append(c)
2750
2750
2751 return ''.join(chars)
2751 return ''.join(chars)
2752
2752
2753 @classmethod
2753 @classmethod
2754 def _generate_choice(cls, repo_group):
2754 def _generate_choice(cls, repo_group):
2755 from webhelpers2.html import literal as _literal
2755 from webhelpers2.html import literal as _literal
2756 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2756 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2757 return repo_group.group_id, _name(repo_group.full_path_splitted)
2757 return repo_group.group_id, _name(repo_group.full_path_splitted)
2758
2758
2759 @classmethod
2759 @classmethod
2760 def groups_choices(cls, groups=None, show_empty_group=True):
2760 def groups_choices(cls, groups=None, show_empty_group=True):
2761 if not groups:
2761 if not groups:
2762 groups = cls.query().all()
2762 groups = cls.query().all()
2763
2763
2764 repo_groups = []
2764 repo_groups = []
2765 if show_empty_group:
2765 if show_empty_group:
2766 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2766 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2767
2767
2768 repo_groups.extend([cls._generate_choice(x) for x in groups])
2768 repo_groups.extend([cls._generate_choice(x) for x in groups])
2769
2769
2770 repo_groups = sorted(
2770 repo_groups = sorted(
2771 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2771 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2772 return repo_groups
2772 return repo_groups
2773
2773
2774 @classmethod
2774 @classmethod
2775 def url_sep(cls):
2775 def url_sep(cls):
2776 return URL_SEP
2776 return URL_SEP
2777
2777
2778 @classmethod
2778 @classmethod
2779 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2779 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2780 if case_insensitive:
2780 if case_insensitive:
2781 gr = cls.query().filter(func.lower(cls.group_name)
2781 gr = cls.query().filter(func.lower(cls.group_name)
2782 == func.lower(group_name))
2782 == func.lower(group_name))
2783 else:
2783 else:
2784 gr = cls.query().filter(cls.group_name == group_name)
2784 gr = cls.query().filter(cls.group_name == group_name)
2785 if cache:
2785 if cache:
2786 name_key = _hash_key(group_name)
2786 name_key = _hash_key(group_name)
2787 gr = gr.options(
2787 gr = gr.options(
2788 FromCache("sql_cache_short", "get_group_%s" % name_key))
2788 FromCache("sql_cache_short", "get_group_%s" % name_key))
2789 return gr.scalar()
2789 return gr.scalar()
2790
2790
2791 @classmethod
2791 @classmethod
2792 def get_user_personal_repo_group(cls, user_id):
2792 def get_user_personal_repo_group(cls, user_id):
2793 user = User.get(user_id)
2793 user = User.get(user_id)
2794 if user.username == User.DEFAULT_USER:
2794 if user.username == User.DEFAULT_USER:
2795 return None
2795 return None
2796
2796
2797 return cls.query()\
2797 return cls.query()\
2798 .filter(cls.personal == true()) \
2798 .filter(cls.personal == true()) \
2799 .filter(cls.user == user) \
2799 .filter(cls.user == user) \
2800 .order_by(cls.group_id.asc()) \
2800 .order_by(cls.group_id.asc()) \
2801 .first()
2801 .first()
2802
2802
2803 @classmethod
2803 @classmethod
2804 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2804 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2805 case_insensitive=True):
2805 case_insensitive=True):
2806 q = RepoGroup.query()
2806 q = RepoGroup.query()
2807
2807
2808 if not isinstance(user_id, Optional):
2808 if not isinstance(user_id, Optional):
2809 q = q.filter(RepoGroup.user_id == user_id)
2809 q = q.filter(RepoGroup.user_id == user_id)
2810
2810
2811 if not isinstance(group_id, Optional):
2811 if not isinstance(group_id, Optional):
2812 q = q.filter(RepoGroup.group_parent_id == group_id)
2812 q = q.filter(RepoGroup.group_parent_id == group_id)
2813
2813
2814 if case_insensitive:
2814 if case_insensitive:
2815 q = q.order_by(func.lower(RepoGroup.group_name))
2815 q = q.order_by(func.lower(RepoGroup.group_name))
2816 else:
2816 else:
2817 q = q.order_by(RepoGroup.group_name)
2817 q = q.order_by(RepoGroup.group_name)
2818 return q.all()
2818 return q.all()
2819
2819
2820 @property
2820 @property
2821 def parents(self, parents_recursion_limit=10):
2821 def parents(self, parents_recursion_limit=10):
2822 groups = []
2822 groups = []
2823 if self.parent_group is None:
2823 if self.parent_group is None:
2824 return groups
2824 return groups
2825 cur_gr = self.parent_group
2825 cur_gr = self.parent_group
2826 groups.insert(0, cur_gr)
2826 groups.insert(0, cur_gr)
2827 cnt = 0
2827 cnt = 0
2828 while 1:
2828 while 1:
2829 cnt += 1
2829 cnt += 1
2830 gr = getattr(cur_gr, 'parent_group', None)
2830 gr = getattr(cur_gr, 'parent_group', None)
2831 cur_gr = cur_gr.parent_group
2831 cur_gr = cur_gr.parent_group
2832 if gr is None:
2832 if gr is None:
2833 break
2833 break
2834 if cnt == parents_recursion_limit:
2834 if cnt == parents_recursion_limit:
2835 # this will prevent accidental infinit loops
2835 # this will prevent accidental infinit loops
2836 log.error('more than %s parents found for group %s, stopping '
2836 log.error('more than %s parents found for group %s, stopping '
2837 'recursive parent fetching', parents_recursion_limit, self)
2837 'recursive parent fetching', parents_recursion_limit, self)
2838 break
2838 break
2839
2839
2840 groups.insert(0, gr)
2840 groups.insert(0, gr)
2841 return groups
2841 return groups
2842
2842
2843 @property
2843 @property
2844 def last_commit_cache_update_diff(self):
2844 def last_commit_cache_update_diff(self):
2845 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2845 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2846
2846
2847 @classmethod
2847 @classmethod
2848 def _load_commit_change(cls, last_commit_cache):
2848 def _load_commit_change(cls, last_commit_cache):
2849 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2849 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2850 empty_date = datetime.datetime.fromtimestamp(0)
2850 empty_date = datetime.datetime.fromtimestamp(0)
2851 date_latest = last_commit_cache.get('date', empty_date)
2851 date_latest = last_commit_cache.get('date', empty_date)
2852 try:
2852 try:
2853 return parse_datetime(date_latest)
2853 return parse_datetime(date_latest)
2854 except Exception:
2854 except Exception:
2855 return empty_date
2855 return empty_date
2856
2856
2857 @property
2857 @property
2858 def last_commit_change(self):
2858 def last_commit_change(self):
2859 return self._load_commit_change(self.changeset_cache)
2859 return self._load_commit_change(self.changeset_cache)
2860
2860
2861 @property
2861 @property
2862 def last_db_change(self):
2862 def last_db_change(self):
2863 return self.updated_on
2863 return self.updated_on
2864
2864
2865 @property
2865 @property
2866 def children(self):
2866 def children(self):
2867 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2867 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2868
2868
2869 @property
2869 @property
2870 def name(self):
2870 def name(self):
2871 return self.group_name.split(RepoGroup.url_sep())[-1]
2871 return self.group_name.split(RepoGroup.url_sep())[-1]
2872
2872
2873 @property
2873 @property
2874 def full_path(self):
2874 def full_path(self):
2875 return self.group_name
2875 return self.group_name
2876
2876
2877 @property
2877 @property
2878 def full_path_splitted(self):
2878 def full_path_splitted(self):
2879 return self.group_name.split(RepoGroup.url_sep())
2879 return self.group_name.split(RepoGroup.url_sep())
2880
2880
2881 @property
2881 @property
2882 def repositories(self):
2882 def repositories(self):
2883 return Repository.query()\
2883 return Repository.query()\
2884 .filter(Repository.group == self)\
2884 .filter(Repository.group == self)\
2885 .order_by(Repository.repo_name)
2885 .order_by(Repository.repo_name)
2886
2886
2887 @property
2887 @property
2888 def repositories_recursive_count(self):
2888 def repositories_recursive_count(self):
2889 cnt = self.repositories.count()
2889 cnt = self.repositories.count()
2890
2890
2891 def children_count(group):
2891 def children_count(group):
2892 cnt = 0
2892 cnt = 0
2893 for child in group.children:
2893 for child in group.children:
2894 cnt += child.repositories.count()
2894 cnt += child.repositories.count()
2895 cnt += children_count(child)
2895 cnt += children_count(child)
2896 return cnt
2896 return cnt
2897
2897
2898 return cnt + children_count(self)
2898 return cnt + children_count(self)
2899
2899
2900 def _recursive_objects(self, include_repos=True, include_groups=True):
2900 def _recursive_objects(self, include_repos=True, include_groups=True):
2901 all_ = []
2901 all_ = []
2902
2902
2903 def _get_members(root_gr):
2903 def _get_members(root_gr):
2904 if include_repos:
2904 if include_repos:
2905 for r in root_gr.repositories:
2905 for r in root_gr.repositories:
2906 all_.append(r)
2906 all_.append(r)
2907 childs = root_gr.children.all()
2907 childs = root_gr.children.all()
2908 if childs:
2908 if childs:
2909 for gr in childs:
2909 for gr in childs:
2910 if include_groups:
2910 if include_groups:
2911 all_.append(gr)
2911 all_.append(gr)
2912 _get_members(gr)
2912 _get_members(gr)
2913
2913
2914 root_group = []
2914 root_group = []
2915 if include_groups:
2915 if include_groups:
2916 root_group = [self]
2916 root_group = [self]
2917
2917
2918 _get_members(self)
2918 _get_members(self)
2919 return root_group + all_
2919 return root_group + all_
2920
2920
2921 def recursive_groups_and_repos(self):
2921 def recursive_groups_and_repos(self):
2922 """
2922 """
2923 Recursive return all groups, with repositories in those groups
2923 Recursive return all groups, with repositories in those groups
2924 """
2924 """
2925 return self._recursive_objects()
2925 return self._recursive_objects()
2926
2926
2927 def recursive_groups(self):
2927 def recursive_groups(self):
2928 """
2928 """
2929 Returns all children groups for this group including children of children
2929 Returns all children groups for this group including children of children
2930 """
2930 """
2931 return self._recursive_objects(include_repos=False)
2931 return self._recursive_objects(include_repos=False)
2932
2932
2933 def recursive_repos(self):
2933 def recursive_repos(self):
2934 """
2934 """
2935 Returns all children repositories for this group
2935 Returns all children repositories for this group
2936 """
2936 """
2937 return self._recursive_objects(include_groups=False)
2937 return self._recursive_objects(include_groups=False)
2938
2938
2939 def get_new_name(self, group_name):
2939 def get_new_name(self, group_name):
2940 """
2940 """
2941 returns new full group name based on parent and new name
2941 returns new full group name based on parent and new name
2942
2942
2943 :param group_name:
2943 :param group_name:
2944 """
2944 """
2945 path_prefix = (self.parent_group.full_path_splitted if
2945 path_prefix = (self.parent_group.full_path_splitted if
2946 self.parent_group else [])
2946 self.parent_group else [])
2947 return RepoGroup.url_sep().join(path_prefix + [group_name])
2947 return RepoGroup.url_sep().join(path_prefix + [group_name])
2948
2948
2949 def update_commit_cache(self, config=None):
2949 def update_commit_cache(self, config=None):
2950 """
2950 """
2951 Update cache of last commit for newest repository inside this repository group.
2951 Update cache of last commit for newest repository inside this repository group.
2952 cache_keys should be::
2952 cache_keys should be::
2953
2953
2954 source_repo_id
2954 source_repo_id
2955 short_id
2955 short_id
2956 raw_id
2956 raw_id
2957 revision
2957 revision
2958 parents
2958 parents
2959 message
2959 message
2960 date
2960 date
2961 author
2961 author
2962
2962
2963 """
2963 """
2964 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2964 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2965 empty_date = datetime.datetime.fromtimestamp(0)
2965 empty_date = datetime.datetime.fromtimestamp(0)
2966
2966
2967 def repo_groups_and_repos(root_gr):
2967 def repo_groups_and_repos(root_gr):
2968 for _repo in root_gr.repositories:
2968 for _repo in root_gr.repositories:
2969 yield _repo
2969 yield _repo
2970 for child_group in root_gr.children.all():
2970 for child_group in root_gr.children.all():
2971 yield child_group
2971 yield child_group
2972
2972
2973 latest_repo_cs_cache = {}
2973 latest_repo_cs_cache = {}
2974 for obj in repo_groups_and_repos(self):
2974 for obj in repo_groups_and_repos(self):
2975 repo_cs_cache = obj.changeset_cache
2975 repo_cs_cache = obj.changeset_cache
2976 date_latest = latest_repo_cs_cache.get('date', empty_date)
2976 date_latest = latest_repo_cs_cache.get('date', empty_date)
2977 date_current = repo_cs_cache.get('date', empty_date)
2977 date_current = repo_cs_cache.get('date', empty_date)
2978 current_timestamp = datetime_to_time(parse_datetime(date_latest))
2978 current_timestamp = datetime_to_time(parse_datetime(date_latest))
2979 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
2979 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
2980 latest_repo_cs_cache = repo_cs_cache
2980 latest_repo_cs_cache = repo_cs_cache
2981 if hasattr(obj, 'repo_id'):
2981 if hasattr(obj, 'repo_id'):
2982 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
2982 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
2983 else:
2983 else:
2984 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
2984 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
2985
2985
2986 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
2986 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
2987
2987
2988 latest_repo_cs_cache['updated_on'] = time.time()
2988 latest_repo_cs_cache['updated_on'] = time.time()
2989 self.changeset_cache = latest_repo_cs_cache
2989 self.changeset_cache = latest_repo_cs_cache
2990 self.updated_on = _date_latest
2990 self.updated_on = _date_latest
2991 Session().add(self)
2991 Session().add(self)
2992 Session().commit()
2992 Session().commit()
2993
2993
2994 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
2994 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
2995 self.group_name, latest_repo_cs_cache, _date_latest)
2995 self.group_name, latest_repo_cs_cache, _date_latest)
2996
2996
2997 def permissions(self, with_admins=True, with_owner=True,
2997 def permissions(self, with_admins=True, with_owner=True,
2998 expand_from_user_groups=False):
2998 expand_from_user_groups=False):
2999 """
2999 """
3000 Permissions for repository groups
3000 Permissions for repository groups
3001 """
3001 """
3002 _admin_perm = 'group.admin'
3002 _admin_perm = 'group.admin'
3003
3003
3004 owner_row = []
3004 owner_row = []
3005 if with_owner:
3005 if with_owner:
3006 usr = AttributeDict(self.user.get_dict())
3006 usr = AttributeDict(self.user.get_dict())
3007 usr.owner_row = True
3007 usr.owner_row = True
3008 usr.permission = _admin_perm
3008 usr.permission = _admin_perm
3009 owner_row.append(usr)
3009 owner_row.append(usr)
3010
3010
3011 super_admin_ids = []
3011 super_admin_ids = []
3012 super_admin_rows = []
3012 super_admin_rows = []
3013 if with_admins:
3013 if with_admins:
3014 for usr in User.get_all_super_admins():
3014 for usr in User.get_all_super_admins():
3015 super_admin_ids.append(usr.user_id)
3015 super_admin_ids.append(usr.user_id)
3016 # if this admin is also owner, don't double the record
3016 # if this admin is also owner, don't double the record
3017 if usr.user_id == owner_row[0].user_id:
3017 if usr.user_id == owner_row[0].user_id:
3018 owner_row[0].admin_row = True
3018 owner_row[0].admin_row = True
3019 else:
3019 else:
3020 usr = AttributeDict(usr.get_dict())
3020 usr = AttributeDict(usr.get_dict())
3021 usr.admin_row = True
3021 usr.admin_row = True
3022 usr.permission = _admin_perm
3022 usr.permission = _admin_perm
3023 super_admin_rows.append(usr)
3023 super_admin_rows.append(usr)
3024
3024
3025 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3025 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3026 q = q.options(joinedload(UserRepoGroupToPerm.group),
3026 q = q.options(joinedload(UserRepoGroupToPerm.group),
3027 joinedload(UserRepoGroupToPerm.user),
3027 joinedload(UserRepoGroupToPerm.user),
3028 joinedload(UserRepoGroupToPerm.permission),)
3028 joinedload(UserRepoGroupToPerm.permission),)
3029
3029
3030 # get owners and admins and permissions. We do a trick of re-writing
3030 # get owners and admins and permissions. We do a trick of re-writing
3031 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3031 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3032 # has a global reference and changing one object propagates to all
3032 # has a global reference and changing one object propagates to all
3033 # others. This means if admin is also an owner admin_row that change
3033 # others. This means if admin is also an owner admin_row that change
3034 # would propagate to both objects
3034 # would propagate to both objects
3035 perm_rows = []
3035 perm_rows = []
3036 for _usr in q.all():
3036 for _usr in q.all():
3037 usr = AttributeDict(_usr.user.get_dict())
3037 usr = AttributeDict(_usr.user.get_dict())
3038 # if this user is also owner/admin, mark as duplicate record
3038 # if this user is also owner/admin, mark as duplicate record
3039 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3039 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3040 usr.duplicate_perm = True
3040 usr.duplicate_perm = True
3041 usr.permission = _usr.permission.permission_name
3041 usr.permission = _usr.permission.permission_name
3042 perm_rows.append(usr)
3042 perm_rows.append(usr)
3043
3043
3044 # filter the perm rows by 'default' first and then sort them by
3044 # filter the perm rows by 'default' first and then sort them by
3045 # admin,write,read,none permissions sorted again alphabetically in
3045 # admin,write,read,none permissions sorted again alphabetically in
3046 # each group
3046 # each group
3047 perm_rows = sorted(perm_rows, key=display_user_sort)
3047 perm_rows = sorted(perm_rows, key=display_user_sort)
3048
3048
3049 user_groups_rows = []
3049 user_groups_rows = []
3050 if expand_from_user_groups:
3050 if expand_from_user_groups:
3051 for ug in self.permission_user_groups(with_members=True):
3051 for ug in self.permission_user_groups(with_members=True):
3052 for user_data in ug.members:
3052 for user_data in ug.members:
3053 user_groups_rows.append(user_data)
3053 user_groups_rows.append(user_data)
3054
3054
3055 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3055 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3056
3056
3057 def permission_user_groups(self, with_members=False):
3057 def permission_user_groups(self, with_members=False):
3058 q = UserGroupRepoGroupToPerm.query()\
3058 q = UserGroupRepoGroupToPerm.query()\
3059 .filter(UserGroupRepoGroupToPerm.group == self)
3059 .filter(UserGroupRepoGroupToPerm.group == self)
3060 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3060 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3061 joinedload(UserGroupRepoGroupToPerm.users_group),
3061 joinedload(UserGroupRepoGroupToPerm.users_group),
3062 joinedload(UserGroupRepoGroupToPerm.permission),)
3062 joinedload(UserGroupRepoGroupToPerm.permission),)
3063
3063
3064 perm_rows = []
3064 perm_rows = []
3065 for _user_group in q.all():
3065 for _user_group in q.all():
3066 entry = AttributeDict(_user_group.users_group.get_dict())
3066 entry = AttributeDict(_user_group.users_group.get_dict())
3067 entry.permission = _user_group.permission.permission_name
3067 entry.permission = _user_group.permission.permission_name
3068 if with_members:
3068 if with_members:
3069 entry.members = [x.user.get_dict()
3069 entry.members = [x.user.get_dict()
3070 for x in _user_group.users_group.members]
3070 for x in _user_group.users_group.members]
3071 perm_rows.append(entry)
3071 perm_rows.append(entry)
3072
3072
3073 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3073 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3074 return perm_rows
3074 return perm_rows
3075
3075
3076 def get_api_data(self):
3076 def get_api_data(self):
3077 """
3077 """
3078 Common function for generating api data
3078 Common function for generating api data
3079
3079
3080 """
3080 """
3081 group = self
3081 group = self
3082 data = {
3082 data = {
3083 'group_id': group.group_id,
3083 'group_id': group.group_id,
3084 'group_name': group.group_name,
3084 'group_name': group.group_name,
3085 'group_description': group.description_safe,
3085 'group_description': group.description_safe,
3086 'parent_group': group.parent_group.group_name if group.parent_group else None,
3086 'parent_group': group.parent_group.group_name if group.parent_group else None,
3087 'repositories': [x.repo_name for x in group.repositories],
3087 'repositories': [x.repo_name for x in group.repositories],
3088 'owner': group.user.username,
3088 'owner': group.user.username,
3089 }
3089 }
3090 return data
3090 return data
3091
3091
3092 def get_dict(self):
3092 def get_dict(self):
3093 # Since we transformed `group_name` to a hybrid property, we need to
3093 # Since we transformed `group_name` to a hybrid property, we need to
3094 # keep compatibility with the code which uses `group_name` field.
3094 # keep compatibility with the code which uses `group_name` field.
3095 result = super(RepoGroup, self).get_dict()
3095 result = super(RepoGroup, self).get_dict()
3096 result['group_name'] = result.pop('_group_name', None)
3096 result['group_name'] = result.pop('_group_name', None)
3097 return result
3097 return result
3098
3098
3099
3099
3100 class Permission(Base, BaseModel):
3100 class Permission(Base, BaseModel):
3101 __tablename__ = 'permissions'
3101 __tablename__ = 'permissions'
3102 __table_args__ = (
3102 __table_args__ = (
3103 Index('p_perm_name_idx', 'permission_name'),
3103 Index('p_perm_name_idx', 'permission_name'),
3104 base_table_args,
3104 base_table_args,
3105 )
3105 )
3106
3106
3107 PERMS = [
3107 PERMS = [
3108 ('hg.admin', _('RhodeCode Super Administrator')),
3108 ('hg.admin', _('RhodeCode Super Administrator')),
3109
3109
3110 ('repository.none', _('Repository no access')),
3110 ('repository.none', _('Repository no access')),
3111 ('repository.read', _('Repository read access')),
3111 ('repository.read', _('Repository read access')),
3112 ('repository.write', _('Repository write access')),
3112 ('repository.write', _('Repository write access')),
3113 ('repository.admin', _('Repository admin access')),
3113 ('repository.admin', _('Repository admin access')),
3114
3114
3115 ('group.none', _('Repository group no access')),
3115 ('group.none', _('Repository group no access')),
3116 ('group.read', _('Repository group read access')),
3116 ('group.read', _('Repository group read access')),
3117 ('group.write', _('Repository group write access')),
3117 ('group.write', _('Repository group write access')),
3118 ('group.admin', _('Repository group admin access')),
3118 ('group.admin', _('Repository group admin access')),
3119
3119
3120 ('usergroup.none', _('User group no access')),
3120 ('usergroup.none', _('User group no access')),
3121 ('usergroup.read', _('User group read access')),
3121 ('usergroup.read', _('User group read access')),
3122 ('usergroup.write', _('User group write access')),
3122 ('usergroup.write', _('User group write access')),
3123 ('usergroup.admin', _('User group admin access')),
3123 ('usergroup.admin', _('User group admin access')),
3124
3124
3125 ('branch.none', _('Branch no permissions')),
3125 ('branch.none', _('Branch no permissions')),
3126 ('branch.merge', _('Branch access by web merge')),
3126 ('branch.merge', _('Branch access by web merge')),
3127 ('branch.push', _('Branch access by push')),
3127 ('branch.push', _('Branch access by push')),
3128 ('branch.push_force', _('Branch access by push with force')),
3128 ('branch.push_force', _('Branch access by push with force')),
3129
3129
3130 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3130 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3131 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3131 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3132
3132
3133 ('hg.usergroup.create.false', _('User Group creation disabled')),
3133 ('hg.usergroup.create.false', _('User Group creation disabled')),
3134 ('hg.usergroup.create.true', _('User Group creation enabled')),
3134 ('hg.usergroup.create.true', _('User Group creation enabled')),
3135
3135
3136 ('hg.create.none', _('Repository creation disabled')),
3136 ('hg.create.none', _('Repository creation disabled')),
3137 ('hg.create.repository', _('Repository creation enabled')),
3137 ('hg.create.repository', _('Repository creation enabled')),
3138 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3138 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3139 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3139 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3140
3140
3141 ('hg.fork.none', _('Repository forking disabled')),
3141 ('hg.fork.none', _('Repository forking disabled')),
3142 ('hg.fork.repository', _('Repository forking enabled')),
3142 ('hg.fork.repository', _('Repository forking enabled')),
3143
3143
3144 ('hg.register.none', _('Registration disabled')),
3144 ('hg.register.none', _('Registration disabled')),
3145 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3145 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3146 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3146 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3147
3147
3148 ('hg.password_reset.enabled', _('Password reset enabled')),
3148 ('hg.password_reset.enabled', _('Password reset enabled')),
3149 ('hg.password_reset.hidden', _('Password reset hidden')),
3149 ('hg.password_reset.hidden', _('Password reset hidden')),
3150 ('hg.password_reset.disabled', _('Password reset disabled')),
3150 ('hg.password_reset.disabled', _('Password reset disabled')),
3151
3151
3152 ('hg.extern_activate.manual', _('Manual activation of external account')),
3152 ('hg.extern_activate.manual', _('Manual activation of external account')),
3153 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3153 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3154
3154
3155 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3155 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3156 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3156 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3157 ]
3157 ]
3158
3158
3159 # definition of system default permissions for DEFAULT user, created on
3159 # definition of system default permissions for DEFAULT user, created on
3160 # system setup
3160 # system setup
3161 DEFAULT_USER_PERMISSIONS = [
3161 DEFAULT_USER_PERMISSIONS = [
3162 # object perms
3162 # object perms
3163 'repository.read',
3163 'repository.read',
3164 'group.read',
3164 'group.read',
3165 'usergroup.read',
3165 'usergroup.read',
3166 # branch, for backward compat we need same value as before so forced pushed
3166 # branch, for backward compat we need same value as before so forced pushed
3167 'branch.push_force',
3167 'branch.push_force',
3168 # global
3168 # global
3169 'hg.create.repository',
3169 'hg.create.repository',
3170 'hg.repogroup.create.false',
3170 'hg.repogroup.create.false',
3171 'hg.usergroup.create.false',
3171 'hg.usergroup.create.false',
3172 'hg.create.write_on_repogroup.true',
3172 'hg.create.write_on_repogroup.true',
3173 'hg.fork.repository',
3173 'hg.fork.repository',
3174 'hg.register.manual_activate',
3174 'hg.register.manual_activate',
3175 'hg.password_reset.enabled',
3175 'hg.password_reset.enabled',
3176 'hg.extern_activate.auto',
3176 'hg.extern_activate.auto',
3177 'hg.inherit_default_perms.true',
3177 'hg.inherit_default_perms.true',
3178 ]
3178 ]
3179
3179
3180 # defines which permissions are more important higher the more important
3180 # defines which permissions are more important higher the more important
3181 # Weight defines which permissions are more important.
3181 # Weight defines which permissions are more important.
3182 # The higher number the more important.
3182 # The higher number the more important.
3183 PERM_WEIGHTS = {
3183 PERM_WEIGHTS = {
3184 'repository.none': 0,
3184 'repository.none': 0,
3185 'repository.read': 1,
3185 'repository.read': 1,
3186 'repository.write': 3,
3186 'repository.write': 3,
3187 'repository.admin': 4,
3187 'repository.admin': 4,
3188
3188
3189 'group.none': 0,
3189 'group.none': 0,
3190 'group.read': 1,
3190 'group.read': 1,
3191 'group.write': 3,
3191 'group.write': 3,
3192 'group.admin': 4,
3192 'group.admin': 4,
3193
3193
3194 'usergroup.none': 0,
3194 'usergroup.none': 0,
3195 'usergroup.read': 1,
3195 'usergroup.read': 1,
3196 'usergroup.write': 3,
3196 'usergroup.write': 3,
3197 'usergroup.admin': 4,
3197 'usergroup.admin': 4,
3198
3198
3199 'branch.none': 0,
3199 'branch.none': 0,
3200 'branch.merge': 1,
3200 'branch.merge': 1,
3201 'branch.push': 3,
3201 'branch.push': 3,
3202 'branch.push_force': 4,
3202 'branch.push_force': 4,
3203
3203
3204 'hg.repogroup.create.false': 0,
3204 'hg.repogroup.create.false': 0,
3205 'hg.repogroup.create.true': 1,
3205 'hg.repogroup.create.true': 1,
3206
3206
3207 'hg.usergroup.create.false': 0,
3207 'hg.usergroup.create.false': 0,
3208 'hg.usergroup.create.true': 1,
3208 'hg.usergroup.create.true': 1,
3209
3209
3210 'hg.fork.none': 0,
3210 'hg.fork.none': 0,
3211 'hg.fork.repository': 1,
3211 'hg.fork.repository': 1,
3212 'hg.create.none': 0,
3212 'hg.create.none': 0,
3213 'hg.create.repository': 1
3213 'hg.create.repository': 1
3214 }
3214 }
3215
3215
3216 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3216 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3217 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3217 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3218 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3218 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3219
3219
3220 def __unicode__(self):
3220 def __unicode__(self):
3221 return u"<%s('%s:%s')>" % (
3221 return u"<%s('%s:%s')>" % (
3222 self.__class__.__name__, self.permission_id, self.permission_name
3222 self.__class__.__name__, self.permission_id, self.permission_name
3223 )
3223 )
3224
3224
3225 @classmethod
3225 @classmethod
3226 def get_by_key(cls, key):
3226 def get_by_key(cls, key):
3227 return cls.query().filter(cls.permission_name == key).scalar()
3227 return cls.query().filter(cls.permission_name == key).scalar()
3228
3228
3229 @classmethod
3229 @classmethod
3230 def get_default_repo_perms(cls, user_id, repo_id=None):
3230 def get_default_repo_perms(cls, user_id, repo_id=None):
3231 q = Session().query(UserRepoToPerm, Repository, Permission)\
3231 q = Session().query(UserRepoToPerm, Repository, Permission)\
3232 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3232 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3233 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3233 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3234 .filter(UserRepoToPerm.user_id == user_id)
3234 .filter(UserRepoToPerm.user_id == user_id)
3235 if repo_id:
3235 if repo_id:
3236 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3236 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3237 return q.all()
3237 return q.all()
3238
3238
3239 @classmethod
3239 @classmethod
3240 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3240 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3241 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3241 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3242 .join(
3242 .join(
3243 Permission,
3243 Permission,
3244 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3244 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3245 .join(
3245 .join(
3246 UserRepoToPerm,
3246 UserRepoToPerm,
3247 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3247 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3248 .filter(UserRepoToPerm.user_id == user_id)
3248 .filter(UserRepoToPerm.user_id == user_id)
3249
3249
3250 if repo_id:
3250 if repo_id:
3251 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3251 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3252 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3252 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3253
3253
3254 @classmethod
3254 @classmethod
3255 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3255 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3256 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3256 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3257 .join(
3257 .join(
3258 Permission,
3258 Permission,
3259 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3259 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3260 .join(
3260 .join(
3261 Repository,
3261 Repository,
3262 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3262 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3263 .join(
3263 .join(
3264 UserGroup,
3264 UserGroup,
3265 UserGroupRepoToPerm.users_group_id ==
3265 UserGroupRepoToPerm.users_group_id ==
3266 UserGroup.users_group_id)\
3266 UserGroup.users_group_id)\
3267 .join(
3267 .join(
3268 UserGroupMember,
3268 UserGroupMember,
3269 UserGroupRepoToPerm.users_group_id ==
3269 UserGroupRepoToPerm.users_group_id ==
3270 UserGroupMember.users_group_id)\
3270 UserGroupMember.users_group_id)\
3271 .filter(
3271 .filter(
3272 UserGroupMember.user_id == user_id,
3272 UserGroupMember.user_id == user_id,
3273 UserGroup.users_group_active == true())
3273 UserGroup.users_group_active == true())
3274 if repo_id:
3274 if repo_id:
3275 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3275 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3276 return q.all()
3276 return q.all()
3277
3277
3278 @classmethod
3278 @classmethod
3279 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3279 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3280 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3280 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3281 .join(
3281 .join(
3282 Permission,
3282 Permission,
3283 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3283 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3284 .join(
3284 .join(
3285 UserGroupRepoToPerm,
3285 UserGroupRepoToPerm,
3286 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3286 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3287 .join(
3287 .join(
3288 UserGroup,
3288 UserGroup,
3289 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3289 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3290 .join(
3290 .join(
3291 UserGroupMember,
3291 UserGroupMember,
3292 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3292 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3293 .filter(
3293 .filter(
3294 UserGroupMember.user_id == user_id,
3294 UserGroupMember.user_id == user_id,
3295 UserGroup.users_group_active == true())
3295 UserGroup.users_group_active == true())
3296
3296
3297 if repo_id:
3297 if repo_id:
3298 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3298 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3299 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3299 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3300
3300
3301 @classmethod
3301 @classmethod
3302 def get_default_group_perms(cls, user_id, repo_group_id=None):
3302 def get_default_group_perms(cls, user_id, repo_group_id=None):
3303 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3303 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3304 .join(
3304 .join(
3305 Permission,
3305 Permission,
3306 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3306 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3307 .join(
3307 .join(
3308 RepoGroup,
3308 RepoGroup,
3309 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3309 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3310 .filter(UserRepoGroupToPerm.user_id == user_id)
3310 .filter(UserRepoGroupToPerm.user_id == user_id)
3311 if repo_group_id:
3311 if repo_group_id:
3312 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3312 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3313 return q.all()
3313 return q.all()
3314
3314
3315 @classmethod
3315 @classmethod
3316 def get_default_group_perms_from_user_group(
3316 def get_default_group_perms_from_user_group(
3317 cls, user_id, repo_group_id=None):
3317 cls, user_id, repo_group_id=None):
3318 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3318 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3319 .join(
3319 .join(
3320 Permission,
3320 Permission,
3321 UserGroupRepoGroupToPerm.permission_id ==
3321 UserGroupRepoGroupToPerm.permission_id ==
3322 Permission.permission_id)\
3322 Permission.permission_id)\
3323 .join(
3323 .join(
3324 RepoGroup,
3324 RepoGroup,
3325 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3325 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3326 .join(
3326 .join(
3327 UserGroup,
3327 UserGroup,
3328 UserGroupRepoGroupToPerm.users_group_id ==
3328 UserGroupRepoGroupToPerm.users_group_id ==
3329 UserGroup.users_group_id)\
3329 UserGroup.users_group_id)\
3330 .join(
3330 .join(
3331 UserGroupMember,
3331 UserGroupMember,
3332 UserGroupRepoGroupToPerm.users_group_id ==
3332 UserGroupRepoGroupToPerm.users_group_id ==
3333 UserGroupMember.users_group_id)\
3333 UserGroupMember.users_group_id)\
3334 .filter(
3334 .filter(
3335 UserGroupMember.user_id == user_id,
3335 UserGroupMember.user_id == user_id,
3336 UserGroup.users_group_active == true())
3336 UserGroup.users_group_active == true())
3337 if repo_group_id:
3337 if repo_group_id:
3338 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3338 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3339 return q.all()
3339 return q.all()
3340
3340
3341 @classmethod
3341 @classmethod
3342 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3342 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3343 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3343 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3344 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3344 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3345 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3345 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3346 .filter(UserUserGroupToPerm.user_id == user_id)
3346 .filter(UserUserGroupToPerm.user_id == user_id)
3347 if user_group_id:
3347 if user_group_id:
3348 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3348 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3349 return q.all()
3349 return q.all()
3350
3350
3351 @classmethod
3351 @classmethod
3352 def get_default_user_group_perms_from_user_group(
3352 def get_default_user_group_perms_from_user_group(
3353 cls, user_id, user_group_id=None):
3353 cls, user_id, user_group_id=None):
3354 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3354 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3355 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3355 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3356 .join(
3356 .join(
3357 Permission,
3357 Permission,
3358 UserGroupUserGroupToPerm.permission_id ==
3358 UserGroupUserGroupToPerm.permission_id ==
3359 Permission.permission_id)\
3359 Permission.permission_id)\
3360 .join(
3360 .join(
3361 TargetUserGroup,
3361 TargetUserGroup,
3362 UserGroupUserGroupToPerm.target_user_group_id ==
3362 UserGroupUserGroupToPerm.target_user_group_id ==
3363 TargetUserGroup.users_group_id)\
3363 TargetUserGroup.users_group_id)\
3364 .join(
3364 .join(
3365 UserGroup,
3365 UserGroup,
3366 UserGroupUserGroupToPerm.user_group_id ==
3366 UserGroupUserGroupToPerm.user_group_id ==
3367 UserGroup.users_group_id)\
3367 UserGroup.users_group_id)\
3368 .join(
3368 .join(
3369 UserGroupMember,
3369 UserGroupMember,
3370 UserGroupUserGroupToPerm.user_group_id ==
3370 UserGroupUserGroupToPerm.user_group_id ==
3371 UserGroupMember.users_group_id)\
3371 UserGroupMember.users_group_id)\
3372 .filter(
3372 .filter(
3373 UserGroupMember.user_id == user_id,
3373 UserGroupMember.user_id == user_id,
3374 UserGroup.users_group_active == true())
3374 UserGroup.users_group_active == true())
3375 if user_group_id:
3375 if user_group_id:
3376 q = q.filter(
3376 q = q.filter(
3377 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3377 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3378
3378
3379 return q.all()
3379 return q.all()
3380
3380
3381
3381
3382 class UserRepoToPerm(Base, BaseModel):
3382 class UserRepoToPerm(Base, BaseModel):
3383 __tablename__ = 'repo_to_perm'
3383 __tablename__ = 'repo_to_perm'
3384 __table_args__ = (
3384 __table_args__ = (
3385 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3385 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3386 base_table_args
3386 base_table_args
3387 )
3387 )
3388
3388
3389 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3389 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3390 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3390 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3391 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3391 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3392 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3392 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3393
3393
3394 user = relationship('User')
3394 user = relationship('User')
3395 repository = relationship('Repository')
3395 repository = relationship('Repository')
3396 permission = relationship('Permission')
3396 permission = relationship('Permission')
3397
3397
3398 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3398 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3399
3399
3400 @classmethod
3400 @classmethod
3401 def create(cls, user, repository, permission):
3401 def create(cls, user, repository, permission):
3402 n = cls()
3402 n = cls()
3403 n.user = user
3403 n.user = user
3404 n.repository = repository
3404 n.repository = repository
3405 n.permission = permission
3405 n.permission = permission
3406 Session().add(n)
3406 Session().add(n)
3407 return n
3407 return n
3408
3408
3409 def __unicode__(self):
3409 def __unicode__(self):
3410 return u'<%s => %s >' % (self.user, self.repository)
3410 return u'<%s => %s >' % (self.user, self.repository)
3411
3411
3412
3412
3413 class UserUserGroupToPerm(Base, BaseModel):
3413 class UserUserGroupToPerm(Base, BaseModel):
3414 __tablename__ = 'user_user_group_to_perm'
3414 __tablename__ = 'user_user_group_to_perm'
3415 __table_args__ = (
3415 __table_args__ = (
3416 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3416 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3417 base_table_args
3417 base_table_args
3418 )
3418 )
3419
3419
3420 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3420 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3421 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3421 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3422 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3422 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3423 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3423 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3424
3424
3425 user = relationship('User')
3425 user = relationship('User')
3426 user_group = relationship('UserGroup')
3426 user_group = relationship('UserGroup')
3427 permission = relationship('Permission')
3427 permission = relationship('Permission')
3428
3428
3429 @classmethod
3429 @classmethod
3430 def create(cls, user, user_group, permission):
3430 def create(cls, user, user_group, permission):
3431 n = cls()
3431 n = cls()
3432 n.user = user
3432 n.user = user
3433 n.user_group = user_group
3433 n.user_group = user_group
3434 n.permission = permission
3434 n.permission = permission
3435 Session().add(n)
3435 Session().add(n)
3436 return n
3436 return n
3437
3437
3438 def __unicode__(self):
3438 def __unicode__(self):
3439 return u'<%s => %s >' % (self.user, self.user_group)
3439 return u'<%s => %s >' % (self.user, self.user_group)
3440
3440
3441
3441
3442 class UserToPerm(Base, BaseModel):
3442 class UserToPerm(Base, BaseModel):
3443 __tablename__ = 'user_to_perm'
3443 __tablename__ = 'user_to_perm'
3444 __table_args__ = (
3444 __table_args__ = (
3445 UniqueConstraint('user_id', 'permission_id'),
3445 UniqueConstraint('user_id', 'permission_id'),
3446 base_table_args
3446 base_table_args
3447 )
3447 )
3448
3448
3449 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3449 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3450 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3450 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3451 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3451 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3452
3452
3453 user = relationship('User')
3453 user = relationship('User')
3454 permission = relationship('Permission', lazy='joined')
3454 permission = relationship('Permission', lazy='joined')
3455
3455
3456 def __unicode__(self):
3456 def __unicode__(self):
3457 return u'<%s => %s >' % (self.user, self.permission)
3457 return u'<%s => %s >' % (self.user, self.permission)
3458
3458
3459
3459
3460 class UserGroupRepoToPerm(Base, BaseModel):
3460 class UserGroupRepoToPerm(Base, BaseModel):
3461 __tablename__ = 'users_group_repo_to_perm'
3461 __tablename__ = 'users_group_repo_to_perm'
3462 __table_args__ = (
3462 __table_args__ = (
3463 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3463 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3464 base_table_args
3464 base_table_args
3465 )
3465 )
3466
3466
3467 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3467 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3468 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3468 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3469 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3469 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3470 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3470 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3471
3471
3472 users_group = relationship('UserGroup')
3472 users_group = relationship('UserGroup')
3473 permission = relationship('Permission')
3473 permission = relationship('Permission')
3474 repository = relationship('Repository')
3474 repository = relationship('Repository')
3475 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3475 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3476
3476
3477 @classmethod
3477 @classmethod
3478 def create(cls, users_group, repository, permission):
3478 def create(cls, users_group, repository, permission):
3479 n = cls()
3479 n = cls()
3480 n.users_group = users_group
3480 n.users_group = users_group
3481 n.repository = repository
3481 n.repository = repository
3482 n.permission = permission
3482 n.permission = permission
3483 Session().add(n)
3483 Session().add(n)
3484 return n
3484 return n
3485
3485
3486 def __unicode__(self):
3486 def __unicode__(self):
3487 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3487 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3488
3488
3489
3489
3490 class UserGroupUserGroupToPerm(Base, BaseModel):
3490 class UserGroupUserGroupToPerm(Base, BaseModel):
3491 __tablename__ = 'user_group_user_group_to_perm'
3491 __tablename__ = 'user_group_user_group_to_perm'
3492 __table_args__ = (
3492 __table_args__ = (
3493 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3493 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3494 CheckConstraint('target_user_group_id != user_group_id'),
3494 CheckConstraint('target_user_group_id != user_group_id'),
3495 base_table_args
3495 base_table_args
3496 )
3496 )
3497
3497
3498 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3498 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3499 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3499 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3500 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3500 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3501 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3501 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3502
3502
3503 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3503 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3504 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3504 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3505 permission = relationship('Permission')
3505 permission = relationship('Permission')
3506
3506
3507 @classmethod
3507 @classmethod
3508 def create(cls, target_user_group, user_group, permission):
3508 def create(cls, target_user_group, user_group, permission):
3509 n = cls()
3509 n = cls()
3510 n.target_user_group = target_user_group
3510 n.target_user_group = target_user_group
3511 n.user_group = user_group
3511 n.user_group = user_group
3512 n.permission = permission
3512 n.permission = permission
3513 Session().add(n)
3513 Session().add(n)
3514 return n
3514 return n
3515
3515
3516 def __unicode__(self):
3516 def __unicode__(self):
3517 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3517 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3518
3518
3519
3519
3520 class UserGroupToPerm(Base, BaseModel):
3520 class UserGroupToPerm(Base, BaseModel):
3521 __tablename__ = 'users_group_to_perm'
3521 __tablename__ = 'users_group_to_perm'
3522 __table_args__ = (
3522 __table_args__ = (
3523 UniqueConstraint('users_group_id', 'permission_id',),
3523 UniqueConstraint('users_group_id', 'permission_id',),
3524 base_table_args
3524 base_table_args
3525 )
3525 )
3526
3526
3527 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3527 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3528 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3528 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3529 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3529 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3530
3530
3531 users_group = relationship('UserGroup')
3531 users_group = relationship('UserGroup')
3532 permission = relationship('Permission')
3532 permission = relationship('Permission')
3533
3533
3534
3534
3535 class UserRepoGroupToPerm(Base, BaseModel):
3535 class UserRepoGroupToPerm(Base, BaseModel):
3536 __tablename__ = 'user_repo_group_to_perm'
3536 __tablename__ = 'user_repo_group_to_perm'
3537 __table_args__ = (
3537 __table_args__ = (
3538 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3538 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3539 base_table_args
3539 base_table_args
3540 )
3540 )
3541
3541
3542 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3542 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3543 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3543 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3544 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3544 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3545 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3545 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3546
3546
3547 user = relationship('User')
3547 user = relationship('User')
3548 group = relationship('RepoGroup')
3548 group = relationship('RepoGroup')
3549 permission = relationship('Permission')
3549 permission = relationship('Permission')
3550
3550
3551 @classmethod
3551 @classmethod
3552 def create(cls, user, repository_group, permission):
3552 def create(cls, user, repository_group, permission):
3553 n = cls()
3553 n = cls()
3554 n.user = user
3554 n.user = user
3555 n.group = repository_group
3555 n.group = repository_group
3556 n.permission = permission
3556 n.permission = permission
3557 Session().add(n)
3557 Session().add(n)
3558 return n
3558 return n
3559
3559
3560
3560
3561 class UserGroupRepoGroupToPerm(Base, BaseModel):
3561 class UserGroupRepoGroupToPerm(Base, BaseModel):
3562 __tablename__ = 'users_group_repo_group_to_perm'
3562 __tablename__ = 'users_group_repo_group_to_perm'
3563 __table_args__ = (
3563 __table_args__ = (
3564 UniqueConstraint('users_group_id', 'group_id'),
3564 UniqueConstraint('users_group_id', 'group_id'),
3565 base_table_args
3565 base_table_args
3566 )
3566 )
3567
3567
3568 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3568 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3569 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3569 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3570 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3570 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3571 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3571 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3572
3572
3573 users_group = relationship('UserGroup')
3573 users_group = relationship('UserGroup')
3574 permission = relationship('Permission')
3574 permission = relationship('Permission')
3575 group = relationship('RepoGroup')
3575 group = relationship('RepoGroup')
3576
3576
3577 @classmethod
3577 @classmethod
3578 def create(cls, user_group, repository_group, permission):
3578 def create(cls, user_group, repository_group, permission):
3579 n = cls()
3579 n = cls()
3580 n.users_group = user_group
3580 n.users_group = user_group
3581 n.group = repository_group
3581 n.group = repository_group
3582 n.permission = permission
3582 n.permission = permission
3583 Session().add(n)
3583 Session().add(n)
3584 return n
3584 return n
3585
3585
3586 def __unicode__(self):
3586 def __unicode__(self):
3587 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3587 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3588
3588
3589
3589
3590 class Statistics(Base, BaseModel):
3590 class Statistics(Base, BaseModel):
3591 __tablename__ = 'statistics'
3591 __tablename__ = 'statistics'
3592 __table_args__ = (
3592 __table_args__ = (
3593 base_table_args
3593 base_table_args
3594 )
3594 )
3595
3595
3596 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3596 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3597 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3597 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3598 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3598 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3599 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3599 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3600 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3600 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3601 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3601 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3602
3602
3603 repository = relationship('Repository', single_parent=True)
3603 repository = relationship('Repository', single_parent=True)
3604
3604
3605
3605
3606 class UserFollowing(Base, BaseModel):
3606 class UserFollowing(Base, BaseModel):
3607 __tablename__ = 'user_followings'
3607 __tablename__ = 'user_followings'
3608 __table_args__ = (
3608 __table_args__ = (
3609 UniqueConstraint('user_id', 'follows_repository_id'),
3609 UniqueConstraint('user_id', 'follows_repository_id'),
3610 UniqueConstraint('user_id', 'follows_user_id'),
3610 UniqueConstraint('user_id', 'follows_user_id'),
3611 base_table_args
3611 base_table_args
3612 )
3612 )
3613
3613
3614 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3614 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3615 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3615 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3616 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3616 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3617 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3617 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3618 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3618 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3619
3619
3620 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3620 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3621
3621
3622 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3622 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3623 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3623 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3624
3624
3625 @classmethod
3625 @classmethod
3626 def get_repo_followers(cls, repo_id):
3626 def get_repo_followers(cls, repo_id):
3627 return cls.query().filter(cls.follows_repo_id == repo_id)
3627 return cls.query().filter(cls.follows_repo_id == repo_id)
3628
3628
3629
3629
3630 class CacheKey(Base, BaseModel):
3630 class CacheKey(Base, BaseModel):
3631 __tablename__ = 'cache_invalidation'
3631 __tablename__ = 'cache_invalidation'
3632 __table_args__ = (
3632 __table_args__ = (
3633 UniqueConstraint('cache_key'),
3633 UniqueConstraint('cache_key'),
3634 Index('key_idx', 'cache_key'),
3634 Index('key_idx', 'cache_key'),
3635 base_table_args,
3635 base_table_args,
3636 )
3636 )
3637
3637
3638 CACHE_TYPE_FEED = 'FEED'
3638 CACHE_TYPE_FEED = 'FEED'
3639
3639
3640 # namespaces used to register process/thread aware caches
3640 # namespaces used to register process/thread aware caches
3641 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3641 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3642 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3642 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3643
3643
3644 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3644 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3645 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3645 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3646 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3646 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3647 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3647 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3648 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3648 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3649
3649
3650 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3650 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3651 self.cache_key = cache_key
3651 self.cache_key = cache_key
3652 self.cache_args = cache_args
3652 self.cache_args = cache_args
3653 self.cache_active = False
3653 self.cache_active = False
3654 # first key should be same for all entries, since all workers should share it
3654 # first key should be same for all entries, since all workers should share it
3655 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3655 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3656
3656
3657 def __unicode__(self):
3657 def __unicode__(self):
3658 return u"<%s('%s:%s[%s]')>" % (
3658 return u"<%s('%s:%s[%s]')>" % (
3659 self.__class__.__name__,
3659 self.__class__.__name__,
3660 self.cache_id, self.cache_key, self.cache_active)
3660 self.cache_id, self.cache_key, self.cache_active)
3661
3661
3662 def _cache_key_partition(self):
3662 def _cache_key_partition(self):
3663 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3663 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3664 return prefix, repo_name, suffix
3664 return prefix, repo_name, suffix
3665
3665
3666 def get_prefix(self):
3666 def get_prefix(self):
3667 """
3667 """
3668 Try to extract prefix from existing cache key. The key could consist
3668 Try to extract prefix from existing cache key. The key could consist
3669 of prefix, repo_name, suffix
3669 of prefix, repo_name, suffix
3670 """
3670 """
3671 # this returns prefix, repo_name, suffix
3671 # this returns prefix, repo_name, suffix
3672 return self._cache_key_partition()[0]
3672 return self._cache_key_partition()[0]
3673
3673
3674 def get_suffix(self):
3674 def get_suffix(self):
3675 """
3675 """
3676 get suffix that might have been used in _get_cache_key to
3676 get suffix that might have been used in _get_cache_key to
3677 generate self.cache_key. Only used for informational purposes
3677 generate self.cache_key. Only used for informational purposes
3678 in repo_edit.mako.
3678 in repo_edit.mako.
3679 """
3679 """
3680 # prefix, repo_name, suffix
3680 # prefix, repo_name, suffix
3681 return self._cache_key_partition()[2]
3681 return self._cache_key_partition()[2]
3682
3682
3683 @classmethod
3683 @classmethod
3684 def generate_new_state_uid(cls, based_on=None):
3684 def generate_new_state_uid(cls, based_on=None):
3685 if based_on:
3685 if based_on:
3686 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3686 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3687 else:
3687 else:
3688 return str(uuid.uuid4())
3688 return str(uuid.uuid4())
3689
3689
3690 @classmethod
3690 @classmethod
3691 def delete_all_cache(cls):
3691 def delete_all_cache(cls):
3692 """
3692 """
3693 Delete all cache keys from database.
3693 Delete all cache keys from database.
3694 Should only be run when all instances are down and all entries
3694 Should only be run when all instances are down and all entries
3695 thus stale.
3695 thus stale.
3696 """
3696 """
3697 cls.query().delete()
3697 cls.query().delete()
3698 Session().commit()
3698 Session().commit()
3699
3699
3700 @classmethod
3700 @classmethod
3701 def set_invalidate(cls, cache_uid, delete=False):
3701 def set_invalidate(cls, cache_uid, delete=False):
3702 """
3702 """
3703 Mark all caches of a repo as invalid in the database.
3703 Mark all caches of a repo as invalid in the database.
3704 """
3704 """
3705
3705
3706 try:
3706 try:
3707 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3707 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3708 if delete:
3708 if delete:
3709 qry.delete()
3709 qry.delete()
3710 log.debug('cache objects deleted for cache args %s',
3710 log.debug('cache objects deleted for cache args %s',
3711 safe_str(cache_uid))
3711 safe_str(cache_uid))
3712 else:
3712 else:
3713 qry.update({"cache_active": False,
3713 qry.update({"cache_active": False,
3714 "cache_state_uid": cls.generate_new_state_uid()})
3714 "cache_state_uid": cls.generate_new_state_uid()})
3715 log.debug('cache objects marked as invalid for cache args %s',
3715 log.debug('cache objects marked as invalid for cache args %s',
3716 safe_str(cache_uid))
3716 safe_str(cache_uid))
3717
3717
3718 Session().commit()
3718 Session().commit()
3719 except Exception:
3719 except Exception:
3720 log.exception(
3720 log.exception(
3721 'Cache key invalidation failed for cache args %s',
3721 'Cache key invalidation failed for cache args %s',
3722 safe_str(cache_uid))
3722 safe_str(cache_uid))
3723 Session().rollback()
3723 Session().rollback()
3724
3724
3725 @classmethod
3725 @classmethod
3726 def get_active_cache(cls, cache_key):
3726 def get_active_cache(cls, cache_key):
3727 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3727 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3728 if inv_obj:
3728 if inv_obj:
3729 return inv_obj
3729 return inv_obj
3730 return None
3730 return None
3731
3731
3732 @classmethod
3732 @classmethod
3733 def get_namespace_map(cls, namespace):
3733 def get_namespace_map(cls, namespace):
3734 return {
3734 return {
3735 x.cache_key: x
3735 x.cache_key: x
3736 for x in cls.query().filter(cls.cache_args == namespace)}
3736 for x in cls.query().filter(cls.cache_args == namespace)}
3737
3737
3738
3738
3739 class ChangesetComment(Base, BaseModel):
3739 class ChangesetComment(Base, BaseModel):
3740 __tablename__ = 'changeset_comments'
3740 __tablename__ = 'changeset_comments'
3741 __table_args__ = (
3741 __table_args__ = (
3742 Index('cc_revision_idx', 'revision'),
3742 Index('cc_revision_idx', 'revision'),
3743 base_table_args,
3743 base_table_args,
3744 )
3744 )
3745
3745
3746 COMMENT_OUTDATED = u'comment_outdated'
3746 COMMENT_OUTDATED = u'comment_outdated'
3747 COMMENT_TYPE_NOTE = u'note'
3747 COMMENT_TYPE_NOTE = u'note'
3748 COMMENT_TYPE_TODO = u'todo'
3748 COMMENT_TYPE_TODO = u'todo'
3749 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3749 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3750
3750
3751 OP_IMMUTABLE = u'immutable'
3751 OP_IMMUTABLE = u'immutable'
3752 OP_CHANGEABLE = u'changeable'
3752 OP_CHANGEABLE = u'changeable'
3753
3753
3754 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3754 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3755 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3755 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3756 revision = Column('revision', String(40), nullable=True)
3756 revision = Column('revision', String(40), nullable=True)
3757 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3757 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3758 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3758 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3759 line_no = Column('line_no', Unicode(10), nullable=True)
3759 line_no = Column('line_no', Unicode(10), nullable=True)
3760 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3760 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3761 f_path = Column('f_path', Unicode(1000), nullable=True)
3761 f_path = Column('f_path', Unicode(1000), nullable=True)
3762 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3762 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3763 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3763 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3764 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3764 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3765 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3765 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3766 renderer = Column('renderer', Unicode(64), nullable=True)
3766 renderer = Column('renderer', Unicode(64), nullable=True)
3767 display_state = Column('display_state', Unicode(128), nullable=True)
3767 display_state = Column('display_state', Unicode(128), nullable=True)
3768 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3768 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3769
3769
3770 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3770 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3771 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3771 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3772
3772
3773 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3773 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3774 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3774 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3775
3775
3776 author = relationship('User', lazy='joined')
3776 author = relationship('User', lazy='joined')
3777 repo = relationship('Repository')
3777 repo = relationship('Repository')
3778 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3778 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3779 pull_request = relationship('PullRequest', lazy='joined')
3779 pull_request = relationship('PullRequest', lazy='joined')
3780 pull_request_version = relationship('PullRequestVersion')
3780 pull_request_version = relationship('PullRequestVersion')
3781 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='joined', order_by='ChangesetCommentHistory.version')
3781 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='joined', order_by='ChangesetCommentHistory.version')
3782
3782
3783 @classmethod
3783 @classmethod
3784 def get_users(cls, revision=None, pull_request_id=None):
3784 def get_users(cls, revision=None, pull_request_id=None):
3785 """
3785 """
3786 Returns user associated with this ChangesetComment. ie those
3786 Returns user associated with this ChangesetComment. ie those
3787 who actually commented
3787 who actually commented
3788
3788
3789 :param cls:
3789 :param cls:
3790 :param revision:
3790 :param revision:
3791 """
3791 """
3792 q = Session().query(User)\
3792 q = Session().query(User)\
3793 .join(ChangesetComment.author)
3793 .join(ChangesetComment.author)
3794 if revision:
3794 if revision:
3795 q = q.filter(cls.revision == revision)
3795 q = q.filter(cls.revision == revision)
3796 elif pull_request_id:
3796 elif pull_request_id:
3797 q = q.filter(cls.pull_request_id == pull_request_id)
3797 q = q.filter(cls.pull_request_id == pull_request_id)
3798 return q.all()
3798 return q.all()
3799
3799
3800 @classmethod
3800 @classmethod
3801 def get_index_from_version(cls, pr_version, versions):
3801 def get_index_from_version(cls, pr_version, versions):
3802 num_versions = [x.pull_request_version_id for x in versions]
3802 num_versions = [x.pull_request_version_id for x in versions]
3803 try:
3803 try:
3804 return num_versions.index(pr_version) + 1
3804 return num_versions.index(pr_version) + 1
3805 except (IndexError, ValueError):
3805 except (IndexError, ValueError):
3806 return
3806 return
3807
3807
3808 @property
3808 @property
3809 def outdated(self):
3809 def outdated(self):
3810 return self.display_state == self.COMMENT_OUTDATED
3810 return self.display_state == self.COMMENT_OUTDATED
3811
3811
3812 @property
3812 @property
3813 def outdated_js(self):
3813 def outdated_js(self):
3814 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3814 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3815
3815
3816 @property
3816 @property
3817 def immutable(self):
3817 def immutable(self):
3818 return self.immutable_state == self.OP_IMMUTABLE
3818 return self.immutable_state == self.OP_IMMUTABLE
3819
3819
3820 def outdated_at_version(self, version):
3820 def outdated_at_version(self, version):
3821 """
3821 """
3822 Checks if comment is outdated for given pull request version
3822 Checks if comment is outdated for given pull request version
3823 """
3823 """
3824 def version_check():
3824 def version_check():
3825 return self.pull_request_version_id and self.pull_request_version_id != version
3825 return self.pull_request_version_id and self.pull_request_version_id != version
3826
3826
3827 if self.is_inline:
3827 if self.is_inline:
3828 return self.outdated and version_check()
3828 return self.outdated and version_check()
3829 else:
3829 else:
3830 # general comments don't have .outdated set, also latest don't have a version
3830 # general comments don't have .outdated set, also latest don't have a version
3831 return version_check()
3831 return version_check()
3832
3832
3833 def outdated_at_version_js(self, version):
3833 def outdated_at_version_js(self, version):
3834 """
3834 """
3835 Checks if comment is outdated for given pull request version
3835 Checks if comment is outdated for given pull request version
3836 """
3836 """
3837 return json.dumps(self.outdated_at_version(version))
3837 return json.dumps(self.outdated_at_version(version))
3838
3838
3839 def older_than_version(self, version):
3839 def older_than_version(self, version):
3840 """
3840 """
3841 Checks if comment is made from previous version than given
3841 Checks if comment is made from previous version than given
3842 """
3842 """
3843 if version is None:
3843 if version is None:
3844 return self.pull_request_version != version
3844 return self.pull_request_version != version
3845
3845
3846 return self.pull_request_version < version
3846 return self.pull_request_version < version
3847
3847
3848 def older_than_version_js(self, version):
3848 def older_than_version_js(self, version):
3849 """
3849 """
3850 Checks if comment is made from previous version than given
3850 Checks if comment is made from previous version than given
3851 """
3851 """
3852 return json.dumps(self.older_than_version(version))
3852 return json.dumps(self.older_than_version(version))
3853
3853
3854 @property
3854 @property
3855 def commit_id(self):
3855 def commit_id(self):
3856 """New style naming to stop using .revision"""
3856 """New style naming to stop using .revision"""
3857 return self.revision
3857 return self.revision
3858
3858
3859 @property
3859 @property
3860 def resolved(self):
3860 def resolved(self):
3861 return self.resolved_by[0] if self.resolved_by else None
3861 return self.resolved_by[0] if self.resolved_by else None
3862
3862
3863 @property
3863 @property
3864 def is_todo(self):
3864 def is_todo(self):
3865 return self.comment_type == self.COMMENT_TYPE_TODO
3865 return self.comment_type == self.COMMENT_TYPE_TODO
3866
3866
3867 @property
3867 @property
3868 def is_inline(self):
3868 def is_inline(self):
3869 if self.line_no and self.f_path:
3869 if self.line_no and self.f_path:
3870 return True
3870 return True
3871 return False
3871 return False
3872
3872
3873 @property
3873 @property
3874 def last_version(self):
3874 def last_version(self):
3875 version = 0
3875 version = 0
3876 if self.history:
3876 if self.history:
3877 version = self.history[-1].version
3877 version = self.history[-1].version
3878 return version
3878 return version
3879
3879
3880 def get_index_version(self, versions):
3880 def get_index_version(self, versions):
3881 return self.get_index_from_version(
3881 return self.get_index_from_version(
3882 self.pull_request_version_id, versions)
3882 self.pull_request_version_id, versions)
3883
3883
3884 @property
3884 @property
3885 def review_status(self):
3885 def review_status(self):
3886 if self.status_change:
3886 if self.status_change:
3887 return self.status_change[0].status
3887 return self.status_change[0].status
3888
3888
3889 @property
3889 @property
3890 def review_status_lbl(self):
3890 def review_status_lbl(self):
3891 if self.status_change:
3891 if self.status_change:
3892 return self.status_change[0].status_lbl
3892 return self.status_change[0].status_lbl
3893
3893
3894 def __repr__(self):
3894 def __repr__(self):
3895 if self.comment_id:
3895 if self.comment_id:
3896 return '<DB:Comment #%s>' % self.comment_id
3896 return '<DB:Comment #%s>' % self.comment_id
3897 else:
3897 else:
3898 return '<DB:Comment at %#x>' % id(self)
3898 return '<DB:Comment at %#x>' % id(self)
3899
3899
3900 def get_api_data(self):
3900 def get_api_data(self):
3901 comment = self
3901 comment = self
3902
3902
3903 data = {
3903 data = {
3904 'comment_id': comment.comment_id,
3904 'comment_id': comment.comment_id,
3905 'comment_type': comment.comment_type,
3905 'comment_type': comment.comment_type,
3906 'comment_text': comment.text,
3906 'comment_text': comment.text,
3907 'comment_status': comment.status_change,
3907 'comment_status': comment.status_change,
3908 'comment_f_path': comment.f_path,
3908 'comment_f_path': comment.f_path,
3909 'comment_lineno': comment.line_no,
3909 'comment_lineno': comment.line_no,
3910 'comment_author': comment.author,
3910 'comment_author': comment.author,
3911 'comment_created_on': comment.created_on,
3911 'comment_created_on': comment.created_on,
3912 'comment_resolved_by': self.resolved,
3912 'comment_resolved_by': self.resolved,
3913 'comment_commit_id': comment.revision,
3913 'comment_commit_id': comment.revision,
3914 'comment_pull_request_id': comment.pull_request_id,
3914 'comment_pull_request_id': comment.pull_request_id,
3915 'comment_last_version': self.last_version
3915 'comment_last_version': self.last_version
3916 }
3916 }
3917 return data
3917 return data
3918
3918
3919 def __json__(self):
3919 def __json__(self):
3920 data = dict()
3920 data = dict()
3921 data.update(self.get_api_data())
3921 data.update(self.get_api_data())
3922 return data
3922 return data
3923
3923
3924
3924
3925 class ChangesetCommentHistory(Base, BaseModel):
3925 class ChangesetCommentHistory(Base, BaseModel):
3926 __tablename__ = 'changeset_comments_history'
3926 __tablename__ = 'changeset_comments_history'
3927 __table_args__ = (
3927 __table_args__ = (
3928 Index('cch_comment_id_idx', 'comment_id'),
3928 Index('cch_comment_id_idx', 'comment_id'),
3929 base_table_args,
3929 base_table_args,
3930 )
3930 )
3931
3931
3932 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3932 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3933 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3933 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3934 version = Column("version", Integer(), nullable=False, default=0)
3934 version = Column("version", Integer(), nullable=False, default=0)
3935 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3935 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3936 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3936 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3937 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3937 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3938 deleted = Column('deleted', Boolean(), default=False)
3938 deleted = Column('deleted', Boolean(), default=False)
3939
3939
3940 author = relationship('User', lazy='joined')
3940 author = relationship('User', lazy='joined')
3941 comment = relationship('ChangesetComment', cascade="all, delete")
3941 comment = relationship('ChangesetComment', cascade="all, delete")
3942
3942
3943 @classmethod
3943 @classmethod
3944 def get_version(cls, comment_id):
3944 def get_version(cls, comment_id):
3945 q = Session().query(ChangesetCommentHistory).filter(
3945 q = Session().query(ChangesetCommentHistory).filter(
3946 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3946 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3947 if q.count() == 0:
3947 if q.count() == 0:
3948 return 1
3948 return 1
3949 elif q.count() >= q[0].version:
3949 elif q.count() >= q[0].version:
3950 return q.count() + 1
3950 return q.count() + 1
3951 else:
3951 else:
3952 return q[0].version + 1
3952 return q[0].version + 1
3953
3953
3954
3954
3955 class ChangesetStatus(Base, BaseModel):
3955 class ChangesetStatus(Base, BaseModel):
3956 __tablename__ = 'changeset_statuses'
3956 __tablename__ = 'changeset_statuses'
3957 __table_args__ = (
3957 __table_args__ = (
3958 Index('cs_revision_idx', 'revision'),
3958 Index('cs_revision_idx', 'revision'),
3959 Index('cs_version_idx', 'version'),
3959 Index('cs_version_idx', 'version'),
3960 UniqueConstraint('repo_id', 'revision', 'version'),
3960 UniqueConstraint('repo_id', 'revision', 'version'),
3961 base_table_args
3961 base_table_args
3962 )
3962 )
3963
3963
3964 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3964 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3965 STATUS_APPROVED = 'approved'
3965 STATUS_APPROVED = 'approved'
3966 STATUS_REJECTED = 'rejected'
3966 STATUS_REJECTED = 'rejected'
3967 STATUS_UNDER_REVIEW = 'under_review'
3967 STATUS_UNDER_REVIEW = 'under_review'
3968
3968
3969 STATUSES = [
3969 STATUSES = [
3970 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3970 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3971 (STATUS_APPROVED, _("Approved")),
3971 (STATUS_APPROVED, _("Approved")),
3972 (STATUS_REJECTED, _("Rejected")),
3972 (STATUS_REJECTED, _("Rejected")),
3973 (STATUS_UNDER_REVIEW, _("Under Review")),
3973 (STATUS_UNDER_REVIEW, _("Under Review")),
3974 ]
3974 ]
3975
3975
3976 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3976 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3977 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3977 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3978 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3978 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3979 revision = Column('revision', String(40), nullable=False)
3979 revision = Column('revision', String(40), nullable=False)
3980 status = Column('status', String(128), nullable=False, default=DEFAULT)
3980 status = Column('status', String(128), nullable=False, default=DEFAULT)
3981 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3981 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3982 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3982 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3983 version = Column('version', Integer(), nullable=False, default=0)
3983 version = Column('version', Integer(), nullable=False, default=0)
3984 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3984 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3985
3985
3986 author = relationship('User', lazy='joined')
3986 author = relationship('User', lazy='joined')
3987 repo = relationship('Repository')
3987 repo = relationship('Repository')
3988 comment = relationship('ChangesetComment', lazy='joined')
3988 comment = relationship('ChangesetComment', lazy='joined')
3989 pull_request = relationship('PullRequest', lazy='joined')
3989 pull_request = relationship('PullRequest', lazy='joined')
3990
3990
3991 def __unicode__(self):
3991 def __unicode__(self):
3992 return u"<%s('%s[v%s]:%s')>" % (
3992 return u"<%s('%s[v%s]:%s')>" % (
3993 self.__class__.__name__,
3993 self.__class__.__name__,
3994 self.status, self.version, self.author
3994 self.status, self.version, self.author
3995 )
3995 )
3996
3996
3997 @classmethod
3997 @classmethod
3998 def get_status_lbl(cls, value):
3998 def get_status_lbl(cls, value):
3999 return dict(cls.STATUSES).get(value)
3999 return dict(cls.STATUSES).get(value)
4000
4000
4001 @property
4001 @property
4002 def status_lbl(self):
4002 def status_lbl(self):
4003 return ChangesetStatus.get_status_lbl(self.status)
4003 return ChangesetStatus.get_status_lbl(self.status)
4004
4004
4005 def get_api_data(self):
4005 def get_api_data(self):
4006 status = self
4006 status = self
4007 data = {
4007 data = {
4008 'status_id': status.changeset_status_id,
4008 'status_id': status.changeset_status_id,
4009 'status': status.status,
4009 'status': status.status,
4010 }
4010 }
4011 return data
4011 return data
4012
4012
4013 def __json__(self):
4013 def __json__(self):
4014 data = dict()
4014 data = dict()
4015 data.update(self.get_api_data())
4015 data.update(self.get_api_data())
4016 return data
4016 return data
4017
4017
4018
4018
4019 class _SetState(object):
4019 class _SetState(object):
4020 """
4020 """
4021 Context processor allowing changing state for sensitive operation such as
4021 Context processor allowing changing state for sensitive operation such as
4022 pull request update or merge
4022 pull request update or merge
4023 """
4023 """
4024
4024
4025 def __init__(self, pull_request, pr_state, back_state=None):
4025 def __init__(self, pull_request, pr_state, back_state=None):
4026 self._pr = pull_request
4026 self._pr = pull_request
4027 self._org_state = back_state or pull_request.pull_request_state
4027 self._org_state = back_state or pull_request.pull_request_state
4028 self._pr_state = pr_state
4028 self._pr_state = pr_state
4029 self._current_state = None
4029 self._current_state = None
4030
4030
4031 def __enter__(self):
4031 def __enter__(self):
4032 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4032 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4033 self._pr, self._pr_state)
4033 self._pr, self._pr_state)
4034 self.set_pr_state(self._pr_state)
4034 self.set_pr_state(self._pr_state)
4035 return self
4035 return self
4036
4036
4037 def __exit__(self, exc_type, exc_val, exc_tb):
4037 def __exit__(self, exc_type, exc_val, exc_tb):
4038 if exc_val is not None:
4038 if exc_val is not None:
4039 log.error(traceback.format_exc(exc_tb))
4039 log.error(traceback.format_exc(exc_tb))
4040 return None
4040 return None
4041
4041
4042 self.set_pr_state(self._org_state)
4042 self.set_pr_state(self._org_state)
4043 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4043 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4044 self._pr, self._org_state)
4044 self._pr, self._org_state)
4045
4045
4046 @property
4046 @property
4047 def state(self):
4047 def state(self):
4048 return self._current_state
4048 return self._current_state
4049
4049
4050 def set_pr_state(self, pr_state):
4050 def set_pr_state(self, pr_state):
4051 try:
4051 try:
4052 self._pr.pull_request_state = pr_state
4052 self._pr.pull_request_state = pr_state
4053 Session().add(self._pr)
4053 Session().add(self._pr)
4054 Session().commit()
4054 Session().commit()
4055 self._current_state = pr_state
4055 self._current_state = pr_state
4056 except Exception:
4056 except Exception:
4057 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4057 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4058 raise
4058 raise
4059
4059
4060
4060
4061 class _PullRequestBase(BaseModel):
4061 class _PullRequestBase(BaseModel):
4062 """
4062 """
4063 Common attributes of pull request and version entries.
4063 Common attributes of pull request and version entries.
4064 """
4064 """
4065
4065
4066 # .status values
4066 # .status values
4067 STATUS_NEW = u'new'
4067 STATUS_NEW = u'new'
4068 STATUS_OPEN = u'open'
4068 STATUS_OPEN = u'open'
4069 STATUS_CLOSED = u'closed'
4069 STATUS_CLOSED = u'closed'
4070
4070
4071 # available states
4071 # available states
4072 STATE_CREATING = u'creating'
4072 STATE_CREATING = u'creating'
4073 STATE_UPDATING = u'updating'
4073 STATE_UPDATING = u'updating'
4074 STATE_MERGING = u'merging'
4074 STATE_MERGING = u'merging'
4075 STATE_CREATED = u'created'
4075 STATE_CREATED = u'created'
4076
4076
4077 title = Column('title', Unicode(255), nullable=True)
4077 title = Column('title', Unicode(255), nullable=True)
4078 description = Column(
4078 description = Column(
4079 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4079 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4080 nullable=True)
4080 nullable=True)
4081 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4081 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4082
4082
4083 # new/open/closed status of pull request (not approve/reject/etc)
4083 # new/open/closed status of pull request (not approve/reject/etc)
4084 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4084 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4085 created_on = Column(
4085 created_on = Column(
4086 'created_on', DateTime(timezone=False), nullable=False,
4086 'created_on', DateTime(timezone=False), nullable=False,
4087 default=datetime.datetime.now)
4087 default=datetime.datetime.now)
4088 updated_on = Column(
4088 updated_on = Column(
4089 'updated_on', DateTime(timezone=False), nullable=False,
4089 'updated_on', DateTime(timezone=False), nullable=False,
4090 default=datetime.datetime.now)
4090 default=datetime.datetime.now)
4091
4091
4092 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4092 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4093
4093
4094 @declared_attr
4094 @declared_attr
4095 def user_id(cls):
4095 def user_id(cls):
4096 return Column(
4096 return Column(
4097 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4097 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4098 unique=None)
4098 unique=None)
4099
4099
4100 # 500 revisions max
4100 # 500 revisions max
4101 _revisions = Column(
4101 _revisions = Column(
4102 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4102 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4103
4103
4104 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4104 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4105
4105
4106 @declared_attr
4106 @declared_attr
4107 def source_repo_id(cls):
4107 def source_repo_id(cls):
4108 # TODO: dan: rename column to source_repo_id
4108 # TODO: dan: rename column to source_repo_id
4109 return Column(
4109 return Column(
4110 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4110 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4111 nullable=False)
4111 nullable=False)
4112
4112
4113 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4113 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4114
4114
4115 @hybrid_property
4115 @hybrid_property
4116 def source_ref(self):
4116 def source_ref(self):
4117 return self._source_ref
4117 return self._source_ref
4118
4118
4119 @source_ref.setter
4119 @source_ref.setter
4120 def source_ref(self, val):
4120 def source_ref(self, val):
4121 parts = (val or '').split(':')
4121 parts = (val or '').split(':')
4122 if len(parts) != 3:
4122 if len(parts) != 3:
4123 raise ValueError(
4123 raise ValueError(
4124 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4124 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4125 self._source_ref = safe_unicode(val)
4125 self._source_ref = safe_unicode(val)
4126
4126
4127 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4127 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4128
4128
4129 @hybrid_property
4129 @hybrid_property
4130 def target_ref(self):
4130 def target_ref(self):
4131 return self._target_ref
4131 return self._target_ref
4132
4132
4133 @target_ref.setter
4133 @target_ref.setter
4134 def target_ref(self, val):
4134 def target_ref(self, val):
4135 parts = (val or '').split(':')
4135 parts = (val or '').split(':')
4136 if len(parts) != 3:
4136 if len(parts) != 3:
4137 raise ValueError(
4137 raise ValueError(
4138 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4138 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4139 self._target_ref = safe_unicode(val)
4139 self._target_ref = safe_unicode(val)
4140
4140
4141 @declared_attr
4141 @declared_attr
4142 def target_repo_id(cls):
4142 def target_repo_id(cls):
4143 # TODO: dan: rename column to target_repo_id
4143 # TODO: dan: rename column to target_repo_id
4144 return Column(
4144 return Column(
4145 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4145 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4146 nullable=False)
4146 nullable=False)
4147
4147
4148 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4148 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4149
4149
4150 # TODO: dan: rename column to last_merge_source_rev
4150 # TODO: dan: rename column to last_merge_source_rev
4151 _last_merge_source_rev = Column(
4151 _last_merge_source_rev = Column(
4152 'last_merge_org_rev', String(40), nullable=True)
4152 'last_merge_org_rev', String(40), nullable=True)
4153 # TODO: dan: rename column to last_merge_target_rev
4153 # TODO: dan: rename column to last_merge_target_rev
4154 _last_merge_target_rev = Column(
4154 _last_merge_target_rev = Column(
4155 'last_merge_other_rev', String(40), nullable=True)
4155 'last_merge_other_rev', String(40), nullable=True)
4156 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4156 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4157 last_merge_metadata = Column(
4157 last_merge_metadata = Column(
4158 'last_merge_metadata', MutationObj.as_mutable(
4158 'last_merge_metadata', MutationObj.as_mutable(
4159 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4159 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4160
4160
4161 merge_rev = Column('merge_rev', String(40), nullable=True)
4161 merge_rev = Column('merge_rev', String(40), nullable=True)
4162
4162
4163 reviewer_data = Column(
4163 reviewer_data = Column(
4164 'reviewer_data_json', MutationObj.as_mutable(
4164 'reviewer_data_json', MutationObj.as_mutable(
4165 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4165 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4166
4166
4167 @property
4167 @property
4168 def reviewer_data_json(self):
4168 def reviewer_data_json(self):
4169 return json.dumps(self.reviewer_data)
4169 return json.dumps(self.reviewer_data)
4170
4170
4171 @property
4171 @property
4172 def last_merge_metadata_parsed(self):
4172 def last_merge_metadata_parsed(self):
4173 metadata = {}
4173 metadata = {}
4174 if not self.last_merge_metadata:
4174 if not self.last_merge_metadata:
4175 return metadata
4175 return metadata
4176
4176
4177 if hasattr(self.last_merge_metadata, 'de_coerce'):
4177 if hasattr(self.last_merge_metadata, 'de_coerce'):
4178 for k, v in self.last_merge_metadata.de_coerce().items():
4178 for k, v in self.last_merge_metadata.de_coerce().items():
4179 if k in ['target_ref', 'source_ref']:
4179 if k in ['target_ref', 'source_ref']:
4180 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4180 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4181 else:
4181 else:
4182 if hasattr(v, 'de_coerce'):
4182 if hasattr(v, 'de_coerce'):
4183 metadata[k] = v.de_coerce()
4183 metadata[k] = v.de_coerce()
4184 else:
4184 else:
4185 metadata[k] = v
4185 metadata[k] = v
4186 return metadata
4186 return metadata
4187
4187
4188 @property
4188 @property
4189 def work_in_progress(self):
4189 def work_in_progress(self):
4190 """checks if pull request is work in progress by checking the title"""
4190 """checks if pull request is work in progress by checking the title"""
4191 title = self.title.upper()
4191 title = self.title.upper()
4192 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4192 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4193 return True
4193 return True
4194 return False
4194 return False
4195
4195
4196 @hybrid_property
4196 @hybrid_property
4197 def description_safe(self):
4197 def description_safe(self):
4198 from rhodecode.lib import helpers as h
4198 from rhodecode.lib import helpers as h
4199 return h.escape(self.description)
4199 return h.escape(self.description)
4200
4200
4201 @hybrid_property
4201 @hybrid_property
4202 def revisions(self):
4202 def revisions(self):
4203 return self._revisions.split(':') if self._revisions else []
4203 return self._revisions.split(':') if self._revisions else []
4204
4204
4205 @revisions.setter
4205 @revisions.setter
4206 def revisions(self, val):
4206 def revisions(self, val):
4207 self._revisions = u':'.join(val)
4207 self._revisions = u':'.join(val)
4208
4208
4209 @hybrid_property
4209 @hybrid_property
4210 def last_merge_status(self):
4210 def last_merge_status(self):
4211 return safe_int(self._last_merge_status)
4211 return safe_int(self._last_merge_status)
4212
4212
4213 @last_merge_status.setter
4213 @last_merge_status.setter
4214 def last_merge_status(self, val):
4214 def last_merge_status(self, val):
4215 self._last_merge_status = val
4215 self._last_merge_status = val
4216
4216
4217 @declared_attr
4217 @declared_attr
4218 def author(cls):
4218 def author(cls):
4219 return relationship('User', lazy='joined')
4219 return relationship('User', lazy='joined')
4220
4220
4221 @declared_attr
4221 @declared_attr
4222 def source_repo(cls):
4222 def source_repo(cls):
4223 return relationship(
4223 return relationship(
4224 'Repository',
4224 'Repository',
4225 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4225 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4226
4226
4227 @property
4227 @property
4228 def source_ref_parts(self):
4228 def source_ref_parts(self):
4229 return self.unicode_to_reference(self.source_ref)
4229 return self.unicode_to_reference(self.source_ref)
4230
4230
4231 @declared_attr
4231 @declared_attr
4232 def target_repo(cls):
4232 def target_repo(cls):
4233 return relationship(
4233 return relationship(
4234 'Repository',
4234 'Repository',
4235 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4235 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4236
4236
4237 @property
4237 @property
4238 def target_ref_parts(self):
4238 def target_ref_parts(self):
4239 return self.unicode_to_reference(self.target_ref)
4239 return self.unicode_to_reference(self.target_ref)
4240
4240
4241 @property
4241 @property
4242 def shadow_merge_ref(self):
4242 def shadow_merge_ref(self):
4243 return self.unicode_to_reference(self._shadow_merge_ref)
4243 return self.unicode_to_reference(self._shadow_merge_ref)
4244
4244
4245 @shadow_merge_ref.setter
4245 @shadow_merge_ref.setter
4246 def shadow_merge_ref(self, ref):
4246 def shadow_merge_ref(self, ref):
4247 self._shadow_merge_ref = self.reference_to_unicode(ref)
4247 self._shadow_merge_ref = self.reference_to_unicode(ref)
4248
4248
4249 @staticmethod
4249 @staticmethod
4250 def unicode_to_reference(raw):
4250 def unicode_to_reference(raw):
4251 """
4251 """
4252 Convert a unicode (or string) to a reference object.
4252 Convert a unicode (or string) to a reference object.
4253 If unicode evaluates to False it returns None.
4253 If unicode evaluates to False it returns None.
4254 """
4254 """
4255 if raw:
4255 if raw:
4256 refs = raw.split(':')
4256 refs = raw.split(':')
4257 return Reference(*refs)
4257 return Reference(*refs)
4258 else:
4258 else:
4259 return None
4259 return None
4260
4260
4261 @staticmethod
4261 @staticmethod
4262 def reference_to_unicode(ref):
4262 def reference_to_unicode(ref):
4263 """
4263 """
4264 Convert a reference object to unicode.
4264 Convert a reference object to unicode.
4265 If reference is None it returns None.
4265 If reference is None it returns None.
4266 """
4266 """
4267 if ref:
4267 if ref:
4268 return u':'.join(ref)
4268 return u':'.join(ref)
4269 else:
4269 else:
4270 return None
4270 return None
4271
4271
4272 def get_api_data(self, with_merge_state=True):
4272 def get_api_data(self, with_merge_state=True):
4273 from rhodecode.model.pull_request import PullRequestModel
4273 from rhodecode.model.pull_request import PullRequestModel
4274
4274
4275 pull_request = self
4275 pull_request = self
4276 if with_merge_state:
4276 if with_merge_state:
4277 merge_response, merge_status, msg = \
4277 merge_response, merge_status, msg = \
4278 PullRequestModel().merge_status(pull_request)
4278 PullRequestModel().merge_status(pull_request)
4279 merge_state = {
4279 merge_state = {
4280 'status': merge_status,
4280 'status': merge_status,
4281 'message': safe_unicode(msg),
4281 'message': safe_unicode(msg),
4282 }
4282 }
4283 else:
4283 else:
4284 merge_state = {'status': 'not_available',
4284 merge_state = {'status': 'not_available',
4285 'message': 'not_available'}
4285 'message': 'not_available'}
4286
4286
4287 merge_data = {
4287 merge_data = {
4288 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4288 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4289 'reference': (
4289 'reference': (
4290 pull_request.shadow_merge_ref._asdict()
4290 pull_request.shadow_merge_ref._asdict()
4291 if pull_request.shadow_merge_ref else None),
4291 if pull_request.shadow_merge_ref else None),
4292 }
4292 }
4293
4293
4294 data = {
4294 data = {
4295 'pull_request_id': pull_request.pull_request_id,
4295 'pull_request_id': pull_request.pull_request_id,
4296 'url': PullRequestModel().get_url(pull_request),
4296 'url': PullRequestModel().get_url(pull_request),
4297 'title': pull_request.title,
4297 'title': pull_request.title,
4298 'description': pull_request.description,
4298 'description': pull_request.description,
4299 'status': pull_request.status,
4299 'status': pull_request.status,
4300 'state': pull_request.pull_request_state,
4300 'state': pull_request.pull_request_state,
4301 'created_on': pull_request.created_on,
4301 'created_on': pull_request.created_on,
4302 'updated_on': pull_request.updated_on,
4302 'updated_on': pull_request.updated_on,
4303 'commit_ids': pull_request.revisions,
4303 'commit_ids': pull_request.revisions,
4304 'review_status': pull_request.calculated_review_status(),
4304 'review_status': pull_request.calculated_review_status(),
4305 'mergeable': merge_state,
4305 'mergeable': merge_state,
4306 'source': {
4306 'source': {
4307 'clone_url': pull_request.source_repo.clone_url(),
4307 'clone_url': pull_request.source_repo.clone_url(),
4308 'repository': pull_request.source_repo.repo_name,
4308 'repository': pull_request.source_repo.repo_name,
4309 'reference': {
4309 'reference': {
4310 'name': pull_request.source_ref_parts.name,
4310 'name': pull_request.source_ref_parts.name,
4311 'type': pull_request.source_ref_parts.type,
4311 'type': pull_request.source_ref_parts.type,
4312 'commit_id': pull_request.source_ref_parts.commit_id,
4312 'commit_id': pull_request.source_ref_parts.commit_id,
4313 },
4313 },
4314 },
4314 },
4315 'target': {
4315 'target': {
4316 'clone_url': pull_request.target_repo.clone_url(),
4316 'clone_url': pull_request.target_repo.clone_url(),
4317 'repository': pull_request.target_repo.repo_name,
4317 'repository': pull_request.target_repo.repo_name,
4318 'reference': {
4318 'reference': {
4319 'name': pull_request.target_ref_parts.name,
4319 'name': pull_request.target_ref_parts.name,
4320 'type': pull_request.target_ref_parts.type,
4320 'type': pull_request.target_ref_parts.type,
4321 'commit_id': pull_request.target_ref_parts.commit_id,
4321 'commit_id': pull_request.target_ref_parts.commit_id,
4322 },
4322 },
4323 },
4323 },
4324 'merge': merge_data,
4324 'merge': merge_data,
4325 'author': pull_request.author.get_api_data(include_secrets=False,
4325 'author': pull_request.author.get_api_data(include_secrets=False,
4326 details='basic'),
4326 details='basic'),
4327 'reviewers': [
4327 'reviewers': [
4328 {
4328 {
4329 'user': reviewer.get_api_data(include_secrets=False,
4329 'user': reviewer.get_api_data(include_secrets=False,
4330 details='basic'),
4330 details='basic'),
4331 'reasons': reasons,
4331 'reasons': reasons,
4332 'review_status': st[0][1].status if st else 'not_reviewed',
4332 'review_status': st[0][1].status if st else 'not_reviewed',
4333 }
4333 }
4334 for obj, reviewer, reasons, mandatory, st in
4334 for obj, reviewer, reasons, mandatory, st in
4335 pull_request.reviewers_statuses()
4335 pull_request.reviewers_statuses()
4336 ]
4336 ]
4337 }
4337 }
4338
4338
4339 return data
4339 return data
4340
4340
4341 def set_state(self, pull_request_state, final_state=None):
4341 def set_state(self, pull_request_state, final_state=None):
4342 """
4342 """
4343 # goes from initial state to updating to initial state.
4343 # goes from initial state to updating to initial state.
4344 # initial state can be changed by specifying back_state=
4344 # initial state can be changed by specifying back_state=
4345 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4345 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4346 pull_request.merge()
4346 pull_request.merge()
4347
4347
4348 :param pull_request_state:
4348 :param pull_request_state:
4349 :param final_state:
4349 :param final_state:
4350
4350
4351 """
4351 """
4352
4352
4353 return _SetState(self, pull_request_state, back_state=final_state)
4353 return _SetState(self, pull_request_state, back_state=final_state)
4354
4354
4355
4355
4356 class PullRequest(Base, _PullRequestBase):
4356 class PullRequest(Base, _PullRequestBase):
4357 __tablename__ = 'pull_requests'
4357 __tablename__ = 'pull_requests'
4358 __table_args__ = (
4358 __table_args__ = (
4359 base_table_args,
4359 base_table_args,
4360 )
4360 )
4361 LATEST_VER = 'latest'
4361 LATEST_VER = 'latest'
4362
4362
4363 pull_request_id = Column(
4363 pull_request_id = Column(
4364 'pull_request_id', Integer(), nullable=False, primary_key=True)
4364 'pull_request_id', Integer(), nullable=False, primary_key=True)
4365
4365
4366 def __repr__(self):
4366 def __repr__(self):
4367 if self.pull_request_id:
4367 if self.pull_request_id:
4368 return '<DB:PullRequest #%s>' % self.pull_request_id
4368 return '<DB:PullRequest #%s>' % self.pull_request_id
4369 else:
4369 else:
4370 return '<DB:PullRequest at %#x>' % id(self)
4370 return '<DB:PullRequest at %#x>' % id(self)
4371
4371
4372 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4372 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4373 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4373 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4374 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4374 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4375 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4375 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4376 lazy='dynamic')
4376 lazy='dynamic')
4377
4377
4378 @classmethod
4378 @classmethod
4379 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4379 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4380 internal_methods=None):
4380 internal_methods=None):
4381
4381
4382 class PullRequestDisplay(object):
4382 class PullRequestDisplay(object):
4383 """
4383 """
4384 Special object wrapper for showing PullRequest data via Versions
4384 Special object wrapper for showing PullRequest data via Versions
4385 It mimics PR object as close as possible. This is read only object
4385 It mimics PR object as close as possible. This is read only object
4386 just for display
4386 just for display
4387 """
4387 """
4388
4388
4389 def __init__(self, attrs, internal=None):
4389 def __init__(self, attrs, internal=None):
4390 self.attrs = attrs
4390 self.attrs = attrs
4391 # internal have priority over the given ones via attrs
4391 # internal have priority over the given ones via attrs
4392 self.internal = internal or ['versions']
4392 self.internal = internal or ['versions']
4393
4393
4394 def __getattr__(self, item):
4394 def __getattr__(self, item):
4395 if item in self.internal:
4395 if item in self.internal:
4396 return getattr(self, item)
4396 return getattr(self, item)
4397 try:
4397 try:
4398 return self.attrs[item]
4398 return self.attrs[item]
4399 except KeyError:
4399 except KeyError:
4400 raise AttributeError(
4400 raise AttributeError(
4401 '%s object has no attribute %s' % (self, item))
4401 '%s object has no attribute %s' % (self, item))
4402
4402
4403 def __repr__(self):
4403 def __repr__(self):
4404 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4404 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4405
4405
4406 def versions(self):
4406 def versions(self):
4407 return pull_request_obj.versions.order_by(
4407 return pull_request_obj.versions.order_by(
4408 PullRequestVersion.pull_request_version_id).all()
4408 PullRequestVersion.pull_request_version_id).all()
4409
4409
4410 def is_closed(self):
4410 def is_closed(self):
4411 return pull_request_obj.is_closed()
4411 return pull_request_obj.is_closed()
4412
4412
4413 def is_state_changing(self):
4413 def is_state_changing(self):
4414 return pull_request_obj.is_state_changing()
4414 return pull_request_obj.is_state_changing()
4415
4415
4416 @property
4416 @property
4417 def pull_request_version_id(self):
4417 def pull_request_version_id(self):
4418 return getattr(pull_request_obj, 'pull_request_version_id', None)
4418 return getattr(pull_request_obj, 'pull_request_version_id', None)
4419
4419
4420 @property
4420 @property
4421 def pull_request_last_version(self):
4421 def pull_request_last_version(self):
4422 return pull_request_obj.pull_request_last_version
4422 return pull_request_obj.pull_request_last_version
4423
4423
4424 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4424 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4425
4425
4426 attrs.author = StrictAttributeDict(
4426 attrs.author = StrictAttributeDict(
4427 pull_request_obj.author.get_api_data())
4427 pull_request_obj.author.get_api_data())
4428 if pull_request_obj.target_repo:
4428 if pull_request_obj.target_repo:
4429 attrs.target_repo = StrictAttributeDict(
4429 attrs.target_repo = StrictAttributeDict(
4430 pull_request_obj.target_repo.get_api_data())
4430 pull_request_obj.target_repo.get_api_data())
4431 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4431 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4432
4432
4433 if pull_request_obj.source_repo:
4433 if pull_request_obj.source_repo:
4434 attrs.source_repo = StrictAttributeDict(
4434 attrs.source_repo = StrictAttributeDict(
4435 pull_request_obj.source_repo.get_api_data())
4435 pull_request_obj.source_repo.get_api_data())
4436 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4436 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4437
4437
4438 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4438 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4439 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4439 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4440 attrs.revisions = pull_request_obj.revisions
4440 attrs.revisions = pull_request_obj.revisions
4441 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4441 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4442 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4442 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4443 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4443 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4444 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4444 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4445
4445
4446 return PullRequestDisplay(attrs, internal=internal_methods)
4446 return PullRequestDisplay(attrs, internal=internal_methods)
4447
4447
4448 def is_closed(self):
4448 def is_closed(self):
4449 return self.status == self.STATUS_CLOSED
4449 return self.status == self.STATUS_CLOSED
4450
4450
4451 def is_state_changing(self):
4451 def is_state_changing(self):
4452 return self.pull_request_state != PullRequest.STATE_CREATED
4452 return self.pull_request_state != PullRequest.STATE_CREATED
4453
4453
4454 def __json__(self):
4454 def __json__(self):
4455 return {
4455 return {
4456 'revisions': self.revisions,
4456 'revisions': self.revisions,
4457 'versions': self.versions_count
4457 'versions': self.versions_count
4458 }
4458 }
4459
4459
4460 def calculated_review_status(self):
4460 def calculated_review_status(self):
4461 from rhodecode.model.changeset_status import ChangesetStatusModel
4461 from rhodecode.model.changeset_status import ChangesetStatusModel
4462 return ChangesetStatusModel().calculated_review_status(self)
4462 return ChangesetStatusModel().calculated_review_status(self)
4463
4463
4464 def reviewers_statuses(self):
4464 def reviewers_statuses(self):
4465 from rhodecode.model.changeset_status import ChangesetStatusModel
4465 from rhodecode.model.changeset_status import ChangesetStatusModel
4466 return ChangesetStatusModel().reviewers_statuses(self)
4466 return ChangesetStatusModel().reviewers_statuses(self)
4467
4467
4468 def get_pull_request_reviewers(self, role=None):
4469 qry = PullRequestReviewers.query()\
4470 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4471 if role:
4472 qry = qry.filter(PullRequestReviewers.role == role)
4473
4474 return qry.all()
4475
4476 @property
4477 def reviewers_count(self):
4478 qry = PullRequestReviewers.query()\
4479 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4480 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4481 return qry.count()
4482
4483 @property
4484 def observers_count(self):
4485 qry = PullRequestReviewers.query()\
4486 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4487 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4488 return qry.count()
4489
4490 def observers(self):
4491 qry = PullRequestReviewers.query()\
4492 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4493 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4494 .all()
4495
4496 for entry in qry:
4497 yield entry, entry.user
4498
4468 @property
4499 @property
4469 def workspace_id(self):
4500 def workspace_id(self):
4470 from rhodecode.model.pull_request import PullRequestModel
4501 from rhodecode.model.pull_request import PullRequestModel
4471 return PullRequestModel()._workspace_id(self)
4502 return PullRequestModel()._workspace_id(self)
4472
4503
4473 def get_shadow_repo(self):
4504 def get_shadow_repo(self):
4474 workspace_id = self.workspace_id
4505 workspace_id = self.workspace_id
4475 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4506 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4476 if os.path.isdir(shadow_repository_path):
4507 if os.path.isdir(shadow_repository_path):
4477 vcs_obj = self.target_repo.scm_instance()
4508 vcs_obj = self.target_repo.scm_instance()
4478 return vcs_obj.get_shadow_instance(shadow_repository_path)
4509 return vcs_obj.get_shadow_instance(shadow_repository_path)
4479
4510
4480 @property
4511 @property
4481 def versions_count(self):
4512 def versions_count(self):
4482 """
4513 """
4483 return number of versions this PR have, e.g a PR that once been
4514 return number of versions this PR have, e.g a PR that once been
4484 updated will have 2 versions
4515 updated will have 2 versions
4485 """
4516 """
4486 return self.versions.count() + 1
4517 return self.versions.count() + 1
4487
4518
4488 @property
4519 @property
4489 def pull_request_last_version(self):
4520 def pull_request_last_version(self):
4490 return self.versions_count
4521 return self.versions_count
4491
4522
4492
4523
4493 class PullRequestVersion(Base, _PullRequestBase):
4524 class PullRequestVersion(Base, _PullRequestBase):
4494 __tablename__ = 'pull_request_versions'
4525 __tablename__ = 'pull_request_versions'
4495 __table_args__ = (
4526 __table_args__ = (
4496 base_table_args,
4527 base_table_args,
4497 )
4528 )
4498
4529
4499 pull_request_version_id = Column(
4530 pull_request_version_id = Column(
4500 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4531 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4501 pull_request_id = Column(
4532 pull_request_id = Column(
4502 'pull_request_id', Integer(),
4533 'pull_request_id', Integer(),
4503 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4534 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4504 pull_request = relationship('PullRequest')
4535 pull_request = relationship('PullRequest')
4505
4536
4506 def __repr__(self):
4537 def __repr__(self):
4507 if self.pull_request_version_id:
4538 if self.pull_request_version_id:
4508 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4539 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4509 else:
4540 else:
4510 return '<DB:PullRequestVersion at %#x>' % id(self)
4541 return '<DB:PullRequestVersion at %#x>' % id(self)
4511
4542
4512 @property
4543 @property
4513 def reviewers(self):
4544 def reviewers(self):
4514 return self.pull_request.reviewers
4545 return self.pull_request.reviewers
4515
4546
4516 @property
4547 @property
4517 def versions(self):
4548 def versions(self):
4518 return self.pull_request.versions
4549 return self.pull_request.versions
4519
4550
4520 def is_closed(self):
4551 def is_closed(self):
4521 # calculate from original
4552 # calculate from original
4522 return self.pull_request.status == self.STATUS_CLOSED
4553 return self.pull_request.status == self.STATUS_CLOSED
4523
4554
4524 def is_state_changing(self):
4555 def is_state_changing(self):
4525 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4556 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4526
4557
4527 def calculated_review_status(self):
4558 def calculated_review_status(self):
4528 return self.pull_request.calculated_review_status()
4559 return self.pull_request.calculated_review_status()
4529
4560
4530 def reviewers_statuses(self):
4561 def reviewers_statuses(self):
4531 return self.pull_request.reviewers_statuses()
4562 return self.pull_request.reviewers_statuses()
4532
4563
4564 def observer(self):
4565 return self.pull_request.observers()
4566
4533
4567
4534 class PullRequestReviewers(Base, BaseModel):
4568 class PullRequestReviewers(Base, BaseModel):
4535 __tablename__ = 'pull_request_reviewers'
4569 __tablename__ = 'pull_request_reviewers'
4536 __table_args__ = (
4570 __table_args__ = (
4537 base_table_args,
4571 base_table_args,
4538 )
4572 )
4539 ROLE_REVIEWER = u'reviewer'
4573 ROLE_REVIEWER = u'reviewer'
4540 ROLE_OBSERVER = u'observer'
4574 ROLE_OBSERVER = u'observer'
4575 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4541
4576
4542 @hybrid_property
4577 @hybrid_property
4543 def reasons(self):
4578 def reasons(self):
4544 if not self._reasons:
4579 if not self._reasons:
4545 return []
4580 return []
4546 return self._reasons
4581 return self._reasons
4547
4582
4548 @reasons.setter
4583 @reasons.setter
4549 def reasons(self, val):
4584 def reasons(self, val):
4550 val = val or []
4585 val = val or []
4551 if any(not isinstance(x, compat.string_types) for x in val):
4586 if any(not isinstance(x, compat.string_types) for x in val):
4552 raise Exception('invalid reasons type, must be list of strings')
4587 raise Exception('invalid reasons type, must be list of strings')
4553 self._reasons = val
4588 self._reasons = val
4554
4589
4555 pull_requests_reviewers_id = Column(
4590 pull_requests_reviewers_id = Column(
4556 'pull_requests_reviewers_id', Integer(), nullable=False,
4591 'pull_requests_reviewers_id', Integer(), nullable=False,
4557 primary_key=True)
4592 primary_key=True)
4558 pull_request_id = Column(
4593 pull_request_id = Column(
4559 "pull_request_id", Integer(),
4594 "pull_request_id", Integer(),
4560 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4595 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4561 user_id = Column(
4596 user_id = Column(
4562 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4597 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4563 _reasons = Column(
4598 _reasons = Column(
4564 'reason', MutationList.as_mutable(
4599 'reason', MutationList.as_mutable(
4565 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4600 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4566
4601
4567 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4602 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4568 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4603 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4569
4604
4570 user = relationship('User')
4605 user = relationship('User')
4571 pull_request = relationship('PullRequest')
4606 pull_request = relationship('PullRequest')
4572
4607
4573 rule_data = Column(
4608 rule_data = Column(
4574 'rule_data_json',
4609 'rule_data_json',
4575 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4610 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4576
4611
4577 def rule_user_group_data(self):
4612 def rule_user_group_data(self):
4578 """
4613 """
4579 Returns the voting user group rule data for this reviewer
4614 Returns the voting user group rule data for this reviewer
4580 """
4615 """
4581
4616
4582 if self.rule_data and 'vote_rule' in self.rule_data:
4617 if self.rule_data and 'vote_rule' in self.rule_data:
4583 user_group_data = {}
4618 user_group_data = {}
4584 if 'rule_user_group_entry_id' in self.rule_data:
4619 if 'rule_user_group_entry_id' in self.rule_data:
4585 # means a group with voting rules !
4620 # means a group with voting rules !
4586 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4621 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4587 user_group_data['name'] = self.rule_data['rule_name']
4622 user_group_data['name'] = self.rule_data['rule_name']
4588 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4623 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4589
4624
4590 return user_group_data
4625 return user_group_data
4591
4626
4627 @classmethod
4628 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4629 qry = PullRequestReviewers.query()\
4630 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4631 if role:
4632 qry = qry.filter(PullRequestReviewers.role == role)
4633
4634 return qry.all()
4635
4592 def __unicode__(self):
4636 def __unicode__(self):
4593 return u"<%s('id:%s')>" % (self.__class__.__name__,
4637 return u"<%s('id:%s')>" % (self.__class__.__name__,
4594 self.pull_requests_reviewers_id)
4638 self.pull_requests_reviewers_id)
4595
4639
4596
4640
4597 class Notification(Base, BaseModel):
4641 class Notification(Base, BaseModel):
4598 __tablename__ = 'notifications'
4642 __tablename__ = 'notifications'
4599 __table_args__ = (
4643 __table_args__ = (
4600 Index('notification_type_idx', 'type'),
4644 Index('notification_type_idx', 'type'),
4601 base_table_args,
4645 base_table_args,
4602 )
4646 )
4603
4647
4604 TYPE_CHANGESET_COMMENT = u'cs_comment'
4648 TYPE_CHANGESET_COMMENT = u'cs_comment'
4605 TYPE_MESSAGE = u'message'
4649 TYPE_MESSAGE = u'message'
4606 TYPE_MENTION = u'mention'
4650 TYPE_MENTION = u'mention'
4607 TYPE_REGISTRATION = u'registration'
4651 TYPE_REGISTRATION = u'registration'
4608 TYPE_PULL_REQUEST = u'pull_request'
4652 TYPE_PULL_REQUEST = u'pull_request'
4609 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4653 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4610 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4654 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4611
4655
4612 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4656 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4613 subject = Column('subject', Unicode(512), nullable=True)
4657 subject = Column('subject', Unicode(512), nullable=True)
4614 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4658 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4615 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4659 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4616 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4660 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4617 type_ = Column('type', Unicode(255))
4661 type_ = Column('type', Unicode(255))
4618
4662
4619 created_by_user = relationship('User')
4663 created_by_user = relationship('User')
4620 notifications_to_users = relationship('UserNotification', lazy='joined',
4664 notifications_to_users = relationship('UserNotification', lazy='joined',
4621 cascade="all, delete-orphan")
4665 cascade="all, delete-orphan")
4622
4666
4623 @property
4667 @property
4624 def recipients(self):
4668 def recipients(self):
4625 return [x.user for x in UserNotification.query()\
4669 return [x.user for x in UserNotification.query()\
4626 .filter(UserNotification.notification == self)\
4670 .filter(UserNotification.notification == self)\
4627 .order_by(UserNotification.user_id.asc()).all()]
4671 .order_by(UserNotification.user_id.asc()).all()]
4628
4672
4629 @classmethod
4673 @classmethod
4630 def create(cls, created_by, subject, body, recipients, type_=None):
4674 def create(cls, created_by, subject, body, recipients, type_=None):
4631 if type_ is None:
4675 if type_ is None:
4632 type_ = Notification.TYPE_MESSAGE
4676 type_ = Notification.TYPE_MESSAGE
4633
4677
4634 notification = cls()
4678 notification = cls()
4635 notification.created_by_user = created_by
4679 notification.created_by_user = created_by
4636 notification.subject = subject
4680 notification.subject = subject
4637 notification.body = body
4681 notification.body = body
4638 notification.type_ = type_
4682 notification.type_ = type_
4639 notification.created_on = datetime.datetime.now()
4683 notification.created_on = datetime.datetime.now()
4640
4684
4641 # For each recipient link the created notification to his account
4685 # For each recipient link the created notification to his account
4642 for u in recipients:
4686 for u in recipients:
4643 assoc = UserNotification()
4687 assoc = UserNotification()
4644 assoc.user_id = u.user_id
4688 assoc.user_id = u.user_id
4645 assoc.notification = notification
4689 assoc.notification = notification
4646
4690
4647 # if created_by is inside recipients mark his notification
4691 # if created_by is inside recipients mark his notification
4648 # as read
4692 # as read
4649 if u.user_id == created_by.user_id:
4693 if u.user_id == created_by.user_id:
4650 assoc.read = True
4694 assoc.read = True
4651 Session().add(assoc)
4695 Session().add(assoc)
4652
4696
4653 Session().add(notification)
4697 Session().add(notification)
4654
4698
4655 return notification
4699 return notification
4656
4700
4657
4701
4658 class UserNotification(Base, BaseModel):
4702 class UserNotification(Base, BaseModel):
4659 __tablename__ = 'user_to_notification'
4703 __tablename__ = 'user_to_notification'
4660 __table_args__ = (
4704 __table_args__ = (
4661 UniqueConstraint('user_id', 'notification_id'),
4705 UniqueConstraint('user_id', 'notification_id'),
4662 base_table_args
4706 base_table_args
4663 )
4707 )
4664
4708
4665 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4709 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4666 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4710 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4667 read = Column('read', Boolean, default=False)
4711 read = Column('read', Boolean, default=False)
4668 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4712 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4669
4713
4670 user = relationship('User', lazy="joined")
4714 user = relationship('User', lazy="joined")
4671 notification = relationship('Notification', lazy="joined",
4715 notification = relationship('Notification', lazy="joined",
4672 order_by=lambda: Notification.created_on.desc(),)
4716 order_by=lambda: Notification.created_on.desc(),)
4673
4717
4674 def mark_as_read(self):
4718 def mark_as_read(self):
4675 self.read = True
4719 self.read = True
4676 Session().add(self)
4720 Session().add(self)
4677
4721
4678
4722
4679 class UserNotice(Base, BaseModel):
4723 class UserNotice(Base, BaseModel):
4680 __tablename__ = 'user_notices'
4724 __tablename__ = 'user_notices'
4681 __table_args__ = (
4725 __table_args__ = (
4682 base_table_args
4726 base_table_args
4683 )
4727 )
4684
4728
4685 NOTIFICATION_TYPE_MESSAGE = 'message'
4729 NOTIFICATION_TYPE_MESSAGE = 'message'
4686 NOTIFICATION_TYPE_NOTICE = 'notice'
4730 NOTIFICATION_TYPE_NOTICE = 'notice'
4687
4731
4688 NOTIFICATION_LEVEL_INFO = 'info'
4732 NOTIFICATION_LEVEL_INFO = 'info'
4689 NOTIFICATION_LEVEL_WARNING = 'warning'
4733 NOTIFICATION_LEVEL_WARNING = 'warning'
4690 NOTIFICATION_LEVEL_ERROR = 'error'
4734 NOTIFICATION_LEVEL_ERROR = 'error'
4691
4735
4692 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4736 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4693
4737
4694 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4738 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4695 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4739 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4696
4740
4697 notice_read = Column('notice_read', Boolean, default=False)
4741 notice_read = Column('notice_read', Boolean, default=False)
4698
4742
4699 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4743 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4700 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4744 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4701
4745
4702 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4746 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4703 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4747 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4704
4748
4705 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4749 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4706 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4750 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4707
4751
4708 @classmethod
4752 @classmethod
4709 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4753 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4710
4754
4711 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4755 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4712 cls.NOTIFICATION_LEVEL_WARNING,
4756 cls.NOTIFICATION_LEVEL_WARNING,
4713 cls.NOTIFICATION_LEVEL_INFO]:
4757 cls.NOTIFICATION_LEVEL_INFO]:
4714 return
4758 return
4715
4759
4716 from rhodecode.model.user import UserModel
4760 from rhodecode.model.user import UserModel
4717 user = UserModel().get_user(user)
4761 user = UserModel().get_user(user)
4718
4762
4719 new_notice = UserNotice()
4763 new_notice = UserNotice()
4720 if not allow_duplicate:
4764 if not allow_duplicate:
4721 existing_msg = UserNotice().query() \
4765 existing_msg = UserNotice().query() \
4722 .filter(UserNotice.user == user) \
4766 .filter(UserNotice.user == user) \
4723 .filter(UserNotice.notice_body == body) \
4767 .filter(UserNotice.notice_body == body) \
4724 .filter(UserNotice.notice_read == false()) \
4768 .filter(UserNotice.notice_read == false()) \
4725 .scalar()
4769 .scalar()
4726 if existing_msg:
4770 if existing_msg:
4727 log.warning('Ignoring duplicate notice for user %s', user)
4771 log.warning('Ignoring duplicate notice for user %s', user)
4728 return
4772 return
4729
4773
4730 new_notice.user = user
4774 new_notice.user = user
4731 new_notice.notice_subject = subject
4775 new_notice.notice_subject = subject
4732 new_notice.notice_body = body
4776 new_notice.notice_body = body
4733 new_notice.notification_level = notice_level
4777 new_notice.notification_level = notice_level
4734 Session().add(new_notice)
4778 Session().add(new_notice)
4735 Session().commit()
4779 Session().commit()
4736
4780
4737
4781
4738 class Gist(Base, BaseModel):
4782 class Gist(Base, BaseModel):
4739 __tablename__ = 'gists'
4783 __tablename__ = 'gists'
4740 __table_args__ = (
4784 __table_args__ = (
4741 Index('g_gist_access_id_idx', 'gist_access_id'),
4785 Index('g_gist_access_id_idx', 'gist_access_id'),
4742 Index('g_created_on_idx', 'created_on'),
4786 Index('g_created_on_idx', 'created_on'),
4743 base_table_args
4787 base_table_args
4744 )
4788 )
4745
4789
4746 GIST_PUBLIC = u'public'
4790 GIST_PUBLIC = u'public'
4747 GIST_PRIVATE = u'private'
4791 GIST_PRIVATE = u'private'
4748 DEFAULT_FILENAME = u'gistfile1.txt'
4792 DEFAULT_FILENAME = u'gistfile1.txt'
4749
4793
4750 ACL_LEVEL_PUBLIC = u'acl_public'
4794 ACL_LEVEL_PUBLIC = u'acl_public'
4751 ACL_LEVEL_PRIVATE = u'acl_private'
4795 ACL_LEVEL_PRIVATE = u'acl_private'
4752
4796
4753 gist_id = Column('gist_id', Integer(), primary_key=True)
4797 gist_id = Column('gist_id', Integer(), primary_key=True)
4754 gist_access_id = Column('gist_access_id', Unicode(250))
4798 gist_access_id = Column('gist_access_id', Unicode(250))
4755 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4799 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4756 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4800 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4757 gist_expires = Column('gist_expires', Float(53), nullable=False)
4801 gist_expires = Column('gist_expires', Float(53), nullable=False)
4758 gist_type = Column('gist_type', Unicode(128), nullable=False)
4802 gist_type = Column('gist_type', Unicode(128), nullable=False)
4759 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4803 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4760 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4804 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4761 acl_level = Column('acl_level', Unicode(128), nullable=True)
4805 acl_level = Column('acl_level', Unicode(128), nullable=True)
4762
4806
4763 owner = relationship('User')
4807 owner = relationship('User')
4764
4808
4765 def __repr__(self):
4809 def __repr__(self):
4766 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4810 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4767
4811
4768 @hybrid_property
4812 @hybrid_property
4769 def description_safe(self):
4813 def description_safe(self):
4770 from rhodecode.lib import helpers as h
4814 from rhodecode.lib import helpers as h
4771 return h.escape(self.gist_description)
4815 return h.escape(self.gist_description)
4772
4816
4773 @classmethod
4817 @classmethod
4774 def get_or_404(cls, id_):
4818 def get_or_404(cls, id_):
4775 from pyramid.httpexceptions import HTTPNotFound
4819 from pyramid.httpexceptions import HTTPNotFound
4776
4820
4777 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4821 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4778 if not res:
4822 if not res:
4779 raise HTTPNotFound()
4823 raise HTTPNotFound()
4780 return res
4824 return res
4781
4825
4782 @classmethod
4826 @classmethod
4783 def get_by_access_id(cls, gist_access_id):
4827 def get_by_access_id(cls, gist_access_id):
4784 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4828 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4785
4829
4786 def gist_url(self):
4830 def gist_url(self):
4787 from rhodecode.model.gist import GistModel
4831 from rhodecode.model.gist import GistModel
4788 return GistModel().get_url(self)
4832 return GistModel().get_url(self)
4789
4833
4790 @classmethod
4834 @classmethod
4791 def base_path(cls):
4835 def base_path(cls):
4792 """
4836 """
4793 Returns base path when all gists are stored
4837 Returns base path when all gists are stored
4794
4838
4795 :param cls:
4839 :param cls:
4796 """
4840 """
4797 from rhodecode.model.gist import GIST_STORE_LOC
4841 from rhodecode.model.gist import GIST_STORE_LOC
4798 q = Session().query(RhodeCodeUi)\
4842 q = Session().query(RhodeCodeUi)\
4799 .filter(RhodeCodeUi.ui_key == URL_SEP)
4843 .filter(RhodeCodeUi.ui_key == URL_SEP)
4800 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4844 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4801 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4845 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4802
4846
4803 def get_api_data(self):
4847 def get_api_data(self):
4804 """
4848 """
4805 Common function for generating gist related data for API
4849 Common function for generating gist related data for API
4806 """
4850 """
4807 gist = self
4851 gist = self
4808 data = {
4852 data = {
4809 'gist_id': gist.gist_id,
4853 'gist_id': gist.gist_id,
4810 'type': gist.gist_type,
4854 'type': gist.gist_type,
4811 'access_id': gist.gist_access_id,
4855 'access_id': gist.gist_access_id,
4812 'description': gist.gist_description,
4856 'description': gist.gist_description,
4813 'url': gist.gist_url(),
4857 'url': gist.gist_url(),
4814 'expires': gist.gist_expires,
4858 'expires': gist.gist_expires,
4815 'created_on': gist.created_on,
4859 'created_on': gist.created_on,
4816 'modified_at': gist.modified_at,
4860 'modified_at': gist.modified_at,
4817 'content': None,
4861 'content': None,
4818 'acl_level': gist.acl_level,
4862 'acl_level': gist.acl_level,
4819 }
4863 }
4820 return data
4864 return data
4821
4865
4822 def __json__(self):
4866 def __json__(self):
4823 data = dict(
4867 data = dict(
4824 )
4868 )
4825 data.update(self.get_api_data())
4869 data.update(self.get_api_data())
4826 return data
4870 return data
4827 # SCM functions
4871 # SCM functions
4828
4872
4829 def scm_instance(self, **kwargs):
4873 def scm_instance(self, **kwargs):
4830 """
4874 """
4831 Get an instance of VCS Repository
4875 Get an instance of VCS Repository
4832
4876
4833 :param kwargs:
4877 :param kwargs:
4834 """
4878 """
4835 from rhodecode.model.gist import GistModel
4879 from rhodecode.model.gist import GistModel
4836 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4880 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4837 return get_vcs_instance(
4881 return get_vcs_instance(
4838 repo_path=safe_str(full_repo_path), create=False,
4882 repo_path=safe_str(full_repo_path), create=False,
4839 _vcs_alias=GistModel.vcs_backend)
4883 _vcs_alias=GistModel.vcs_backend)
4840
4884
4841
4885
4842 class ExternalIdentity(Base, BaseModel):
4886 class ExternalIdentity(Base, BaseModel):
4843 __tablename__ = 'external_identities'
4887 __tablename__ = 'external_identities'
4844 __table_args__ = (
4888 __table_args__ = (
4845 Index('local_user_id_idx', 'local_user_id'),
4889 Index('local_user_id_idx', 'local_user_id'),
4846 Index('external_id_idx', 'external_id'),
4890 Index('external_id_idx', 'external_id'),
4847 base_table_args
4891 base_table_args
4848 )
4892 )
4849
4893
4850 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4894 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4851 external_username = Column('external_username', Unicode(1024), default=u'')
4895 external_username = Column('external_username', Unicode(1024), default=u'')
4852 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4896 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4853 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4897 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4854 access_token = Column('access_token', String(1024), default=u'')
4898 access_token = Column('access_token', String(1024), default=u'')
4855 alt_token = Column('alt_token', String(1024), default=u'')
4899 alt_token = Column('alt_token', String(1024), default=u'')
4856 token_secret = Column('token_secret', String(1024), default=u'')
4900 token_secret = Column('token_secret', String(1024), default=u'')
4857
4901
4858 @classmethod
4902 @classmethod
4859 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4903 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4860 """
4904 """
4861 Returns ExternalIdentity instance based on search params
4905 Returns ExternalIdentity instance based on search params
4862
4906
4863 :param external_id:
4907 :param external_id:
4864 :param provider_name:
4908 :param provider_name:
4865 :return: ExternalIdentity
4909 :return: ExternalIdentity
4866 """
4910 """
4867 query = cls.query()
4911 query = cls.query()
4868 query = query.filter(cls.external_id == external_id)
4912 query = query.filter(cls.external_id == external_id)
4869 query = query.filter(cls.provider_name == provider_name)
4913 query = query.filter(cls.provider_name == provider_name)
4870 if local_user_id:
4914 if local_user_id:
4871 query = query.filter(cls.local_user_id == local_user_id)
4915 query = query.filter(cls.local_user_id == local_user_id)
4872 return query.first()
4916 return query.first()
4873
4917
4874 @classmethod
4918 @classmethod
4875 def user_by_external_id_and_provider(cls, external_id, provider_name):
4919 def user_by_external_id_and_provider(cls, external_id, provider_name):
4876 """
4920 """
4877 Returns User instance based on search params
4921 Returns User instance based on search params
4878
4922
4879 :param external_id:
4923 :param external_id:
4880 :param provider_name:
4924 :param provider_name:
4881 :return: User
4925 :return: User
4882 """
4926 """
4883 query = User.query()
4927 query = User.query()
4884 query = query.filter(cls.external_id == external_id)
4928 query = query.filter(cls.external_id == external_id)
4885 query = query.filter(cls.provider_name == provider_name)
4929 query = query.filter(cls.provider_name == provider_name)
4886 query = query.filter(User.user_id == cls.local_user_id)
4930 query = query.filter(User.user_id == cls.local_user_id)
4887 return query.first()
4931 return query.first()
4888
4932
4889 @classmethod
4933 @classmethod
4890 def by_local_user_id(cls, local_user_id):
4934 def by_local_user_id(cls, local_user_id):
4891 """
4935 """
4892 Returns all tokens for user
4936 Returns all tokens for user
4893
4937
4894 :param local_user_id:
4938 :param local_user_id:
4895 :return: ExternalIdentity
4939 :return: ExternalIdentity
4896 """
4940 """
4897 query = cls.query()
4941 query = cls.query()
4898 query = query.filter(cls.local_user_id == local_user_id)
4942 query = query.filter(cls.local_user_id == local_user_id)
4899 return query
4943 return query
4900
4944
4901 @classmethod
4945 @classmethod
4902 def load_provider_plugin(cls, plugin_id):
4946 def load_provider_plugin(cls, plugin_id):
4903 from rhodecode.authentication.base import loadplugin
4947 from rhodecode.authentication.base import loadplugin
4904 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4948 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4905 auth_plugin = loadplugin(_plugin_id)
4949 auth_plugin = loadplugin(_plugin_id)
4906 return auth_plugin
4950 return auth_plugin
4907
4951
4908
4952
4909 class Integration(Base, BaseModel):
4953 class Integration(Base, BaseModel):
4910 __tablename__ = 'integrations'
4954 __tablename__ = 'integrations'
4911 __table_args__ = (
4955 __table_args__ = (
4912 base_table_args
4956 base_table_args
4913 )
4957 )
4914
4958
4915 integration_id = Column('integration_id', Integer(), primary_key=True)
4959 integration_id = Column('integration_id', Integer(), primary_key=True)
4916 integration_type = Column('integration_type', String(255))
4960 integration_type = Column('integration_type', String(255))
4917 enabled = Column('enabled', Boolean(), nullable=False)
4961 enabled = Column('enabled', Boolean(), nullable=False)
4918 name = Column('name', String(255), nullable=False)
4962 name = Column('name', String(255), nullable=False)
4919 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4963 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4920 default=False)
4964 default=False)
4921
4965
4922 settings = Column(
4966 settings = Column(
4923 'settings_json', MutationObj.as_mutable(
4967 'settings_json', MutationObj.as_mutable(
4924 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4968 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4925 repo_id = Column(
4969 repo_id = Column(
4926 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4970 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4927 nullable=True, unique=None, default=None)
4971 nullable=True, unique=None, default=None)
4928 repo = relationship('Repository', lazy='joined')
4972 repo = relationship('Repository', lazy='joined')
4929
4973
4930 repo_group_id = Column(
4974 repo_group_id = Column(
4931 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4975 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4932 nullable=True, unique=None, default=None)
4976 nullable=True, unique=None, default=None)
4933 repo_group = relationship('RepoGroup', lazy='joined')
4977 repo_group = relationship('RepoGroup', lazy='joined')
4934
4978
4935 @property
4979 @property
4936 def scope(self):
4980 def scope(self):
4937 if self.repo:
4981 if self.repo:
4938 return repr(self.repo)
4982 return repr(self.repo)
4939 if self.repo_group:
4983 if self.repo_group:
4940 if self.child_repos_only:
4984 if self.child_repos_only:
4941 return repr(self.repo_group) + ' (child repos only)'
4985 return repr(self.repo_group) + ' (child repos only)'
4942 else:
4986 else:
4943 return repr(self.repo_group) + ' (recursive)'
4987 return repr(self.repo_group) + ' (recursive)'
4944 if self.child_repos_only:
4988 if self.child_repos_only:
4945 return 'root_repos'
4989 return 'root_repos'
4946 return 'global'
4990 return 'global'
4947
4991
4948 def __repr__(self):
4992 def __repr__(self):
4949 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4993 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4950
4994
4951
4995
4952 class RepoReviewRuleUser(Base, BaseModel):
4996 class RepoReviewRuleUser(Base, BaseModel):
4953 __tablename__ = 'repo_review_rules_users'
4997 __tablename__ = 'repo_review_rules_users'
4954 __table_args__ = (
4998 __table_args__ = (
4955 base_table_args
4999 base_table_args
4956 )
5000 )
5001 ROLE_REVIEWER = u'reviewer'
5002 ROLE_OBSERVER = u'observer'
5003 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4957
5004
4958 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5005 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4959 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5006 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4960 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5007 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4961 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5008 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5009 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4962 user = relationship('User')
5010 user = relationship('User')
4963
5011
4964 def rule_data(self):
5012 def rule_data(self):
4965 return {
5013 return {
4966 'mandatory': self.mandatory
5014 'mandatory': self.mandatory,
5015 'role': self.role,
4967 }
5016 }
4968
5017
4969
5018
4970 class RepoReviewRuleUserGroup(Base, BaseModel):
5019 class RepoReviewRuleUserGroup(Base, BaseModel):
4971 __tablename__ = 'repo_review_rules_users_groups'
5020 __tablename__ = 'repo_review_rules_users_groups'
4972 __table_args__ = (
5021 __table_args__ = (
4973 base_table_args
5022 base_table_args
4974 )
5023 )
4975
5024
4976 VOTE_RULE_ALL = -1
5025 VOTE_RULE_ALL = -1
5026 ROLE_REVIEWER = u'reviewer'
5027 ROLE_OBSERVER = u'observer'
5028 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4977
5029
4978 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5030 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4979 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5031 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4980 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
5032 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
4981 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5033 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5034 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4982 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5035 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4983 users_group = relationship('UserGroup')
5036 users_group = relationship('UserGroup')
4984
5037
4985 def rule_data(self):
5038 def rule_data(self):
4986 return {
5039 return {
4987 'mandatory': self.mandatory,
5040 'mandatory': self.mandatory,
5041 'role': self.role,
4988 'vote_rule': self.vote_rule
5042 'vote_rule': self.vote_rule
4989 }
5043 }
4990
5044
4991 @property
5045 @property
4992 def vote_rule_label(self):
5046 def vote_rule_label(self):
4993 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5047 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4994 return 'all must vote'
5048 return 'all must vote'
4995 else:
5049 else:
4996 return 'min. vote {}'.format(self.vote_rule)
5050 return 'min. vote {}'.format(self.vote_rule)
4997
5051
4998
5052
4999 class RepoReviewRule(Base, BaseModel):
5053 class RepoReviewRule(Base, BaseModel):
5000 __tablename__ = 'repo_review_rules'
5054 __tablename__ = 'repo_review_rules'
5001 __table_args__ = (
5055 __table_args__ = (
5002 base_table_args
5056 base_table_args
5003 )
5057 )
5004
5058
5005 repo_review_rule_id = Column(
5059 repo_review_rule_id = Column(
5006 'repo_review_rule_id', Integer(), primary_key=True)
5060 'repo_review_rule_id', Integer(), primary_key=True)
5007 repo_id = Column(
5061 repo_id = Column(
5008 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5062 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5009 repo = relationship('Repository', backref='review_rules')
5063 repo = relationship('Repository', backref='review_rules')
5010
5064
5011 review_rule_name = Column('review_rule_name', String(255))
5065 review_rule_name = Column('review_rule_name', String(255))
5012 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5066 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5013 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5067 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5014 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5068 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5015
5069
5016 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5070 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5017 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5071 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5018 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5072 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5019 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5073 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5020
5074
5021 rule_users = relationship('RepoReviewRuleUser')
5075 rule_users = relationship('RepoReviewRuleUser')
5022 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5076 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5023
5077
5024 def _validate_pattern(self, value):
5078 def _validate_pattern(self, value):
5025 re.compile('^' + glob2re(value) + '$')
5079 re.compile('^' + glob2re(value) + '$')
5026
5080
5027 @hybrid_property
5081 @hybrid_property
5028 def source_branch_pattern(self):
5082 def source_branch_pattern(self):
5029 return self._branch_pattern or '*'
5083 return self._branch_pattern or '*'
5030
5084
5031 @source_branch_pattern.setter
5085 @source_branch_pattern.setter
5032 def source_branch_pattern(self, value):
5086 def source_branch_pattern(self, value):
5033 self._validate_pattern(value)
5087 self._validate_pattern(value)
5034 self._branch_pattern = value or '*'
5088 self._branch_pattern = value or '*'
5035
5089
5036 @hybrid_property
5090 @hybrid_property
5037 def target_branch_pattern(self):
5091 def target_branch_pattern(self):
5038 return self._target_branch_pattern or '*'
5092 return self._target_branch_pattern or '*'
5039
5093
5040 @target_branch_pattern.setter
5094 @target_branch_pattern.setter
5041 def target_branch_pattern(self, value):
5095 def target_branch_pattern(self, value):
5042 self._validate_pattern(value)
5096 self._validate_pattern(value)
5043 self._target_branch_pattern = value or '*'
5097 self._target_branch_pattern = value or '*'
5044
5098
5045 @hybrid_property
5099 @hybrid_property
5046 def file_pattern(self):
5100 def file_pattern(self):
5047 return self._file_pattern or '*'
5101 return self._file_pattern or '*'
5048
5102
5049 @file_pattern.setter
5103 @file_pattern.setter
5050 def file_pattern(self, value):
5104 def file_pattern(self, value):
5051 self._validate_pattern(value)
5105 self._validate_pattern(value)
5052 self._file_pattern = value or '*'
5106 self._file_pattern = value or '*'
5053
5107
5054 def matches(self, source_branch, target_branch, files_changed):
5108 def matches(self, source_branch, target_branch, files_changed):
5055 """
5109 """
5056 Check if this review rule matches a branch/files in a pull request
5110 Check if this review rule matches a branch/files in a pull request
5057
5111
5058 :param source_branch: source branch name for the commit
5112 :param source_branch: source branch name for the commit
5059 :param target_branch: target branch name for the commit
5113 :param target_branch: target branch name for the commit
5060 :param files_changed: list of file paths changed in the pull request
5114 :param files_changed: list of file paths changed in the pull request
5061 """
5115 """
5062
5116
5063 source_branch = source_branch or ''
5117 source_branch = source_branch or ''
5064 target_branch = target_branch or ''
5118 target_branch = target_branch or ''
5065 files_changed = files_changed or []
5119 files_changed = files_changed or []
5066
5120
5067 branch_matches = True
5121 branch_matches = True
5068 if source_branch or target_branch:
5122 if source_branch or target_branch:
5069 if self.source_branch_pattern == '*':
5123 if self.source_branch_pattern == '*':
5070 source_branch_match = True
5124 source_branch_match = True
5071 else:
5125 else:
5072 if self.source_branch_pattern.startswith('re:'):
5126 if self.source_branch_pattern.startswith('re:'):
5073 source_pattern = self.source_branch_pattern[3:]
5127 source_pattern = self.source_branch_pattern[3:]
5074 else:
5128 else:
5075 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5129 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5076 source_branch_regex = re.compile(source_pattern)
5130 source_branch_regex = re.compile(source_pattern)
5077 source_branch_match = bool(source_branch_regex.search(source_branch))
5131 source_branch_match = bool(source_branch_regex.search(source_branch))
5078 if self.target_branch_pattern == '*':
5132 if self.target_branch_pattern == '*':
5079 target_branch_match = True
5133 target_branch_match = True
5080 else:
5134 else:
5081 if self.target_branch_pattern.startswith('re:'):
5135 if self.target_branch_pattern.startswith('re:'):
5082 target_pattern = self.target_branch_pattern[3:]
5136 target_pattern = self.target_branch_pattern[3:]
5083 else:
5137 else:
5084 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5138 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5085 target_branch_regex = re.compile(target_pattern)
5139 target_branch_regex = re.compile(target_pattern)
5086 target_branch_match = bool(target_branch_regex.search(target_branch))
5140 target_branch_match = bool(target_branch_regex.search(target_branch))
5087
5141
5088 branch_matches = source_branch_match and target_branch_match
5142 branch_matches = source_branch_match and target_branch_match
5089
5143
5090 files_matches = True
5144 files_matches = True
5091 if self.file_pattern != '*':
5145 if self.file_pattern != '*':
5092 files_matches = False
5146 files_matches = False
5093 if self.file_pattern.startswith('re:'):
5147 if self.file_pattern.startswith('re:'):
5094 file_pattern = self.file_pattern[3:]
5148 file_pattern = self.file_pattern[3:]
5095 else:
5149 else:
5096 file_pattern = glob2re(self.file_pattern)
5150 file_pattern = glob2re(self.file_pattern)
5097 file_regex = re.compile(file_pattern)
5151 file_regex = re.compile(file_pattern)
5098 for file_data in files_changed:
5152 for file_data in files_changed:
5099 filename = file_data.get('filename')
5153 filename = file_data.get('filename')
5100
5154
5101 if file_regex.search(filename):
5155 if file_regex.search(filename):
5102 files_matches = True
5156 files_matches = True
5103 break
5157 break
5104
5158
5105 return branch_matches and files_matches
5159 return branch_matches and files_matches
5106
5160
5107 @property
5161 @property
5108 def review_users(self):
5162 def review_users(self):
5109 """ Returns the users which this rule applies to """
5163 """ Returns the users which this rule applies to """
5110
5164
5111 users = collections.OrderedDict()
5165 users = collections.OrderedDict()
5112
5166
5113 for rule_user in self.rule_users:
5167 for rule_user in self.rule_users:
5114 if rule_user.user.active:
5168 if rule_user.user.active:
5115 if rule_user.user not in users:
5169 if rule_user.user not in users:
5116 users[rule_user.user.username] = {
5170 users[rule_user.user.username] = {
5117 'user': rule_user.user,
5171 'user': rule_user.user,
5118 'source': 'user',
5172 'source': 'user',
5119 'source_data': {},
5173 'source_data': {},
5120 'data': rule_user.rule_data()
5174 'data': rule_user.rule_data()
5121 }
5175 }
5122
5176
5123 for rule_user_group in self.rule_user_groups:
5177 for rule_user_group in self.rule_user_groups:
5124 source_data = {
5178 source_data = {
5125 'user_group_id': rule_user_group.users_group.users_group_id,
5179 'user_group_id': rule_user_group.users_group.users_group_id,
5126 'name': rule_user_group.users_group.users_group_name,
5180 'name': rule_user_group.users_group.users_group_name,
5127 'members': len(rule_user_group.users_group.members)
5181 'members': len(rule_user_group.users_group.members)
5128 }
5182 }
5129 for member in rule_user_group.users_group.members:
5183 for member in rule_user_group.users_group.members:
5130 if member.user.active:
5184 if member.user.active:
5131 key = member.user.username
5185 key = member.user.username
5132 if key in users:
5186 if key in users:
5133 # skip this member as we have him already
5187 # skip this member as we have him already
5134 # this prevents from override the "first" matched
5188 # this prevents from override the "first" matched
5135 # users with duplicates in multiple groups
5189 # users with duplicates in multiple groups
5136 continue
5190 continue
5137
5191
5138 users[key] = {
5192 users[key] = {
5139 'user': member.user,
5193 'user': member.user,
5140 'source': 'user_group',
5194 'source': 'user_group',
5141 'source_data': source_data,
5195 'source_data': source_data,
5142 'data': rule_user_group.rule_data()
5196 'data': rule_user_group.rule_data()
5143 }
5197 }
5144
5198
5145 return users
5199 return users
5146
5200
5147 def user_group_vote_rule(self, user_id):
5201 def user_group_vote_rule(self, user_id):
5148
5202
5149 rules = []
5203 rules = []
5150 if not self.rule_user_groups:
5204 if not self.rule_user_groups:
5151 return rules
5205 return rules
5152
5206
5153 for user_group in self.rule_user_groups:
5207 for user_group in self.rule_user_groups:
5154 user_group_members = [x.user_id for x in user_group.users_group.members]
5208 user_group_members = [x.user_id for x in user_group.users_group.members]
5155 if user_id in user_group_members:
5209 if user_id in user_group_members:
5156 rules.append(user_group)
5210 rules.append(user_group)
5157 return rules
5211 return rules
5158
5212
5159 def __repr__(self):
5213 def __repr__(self):
5160 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5214 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5161 self.repo_review_rule_id, self.repo)
5215 self.repo_review_rule_id, self.repo)
5162
5216
5163
5217
5164 class ScheduleEntry(Base, BaseModel):
5218 class ScheduleEntry(Base, BaseModel):
5165 __tablename__ = 'schedule_entries'
5219 __tablename__ = 'schedule_entries'
5166 __table_args__ = (
5220 __table_args__ = (
5167 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5221 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5168 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5222 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5169 base_table_args,
5223 base_table_args,
5170 )
5224 )
5171
5225
5172 schedule_types = ['crontab', 'timedelta', 'integer']
5226 schedule_types = ['crontab', 'timedelta', 'integer']
5173 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5227 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5174
5228
5175 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5229 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5176 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5230 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5177 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5231 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5178
5232
5179 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5233 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5180 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5234 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5181
5235
5182 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5236 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5183 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5237 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5184
5238
5185 # task
5239 # task
5186 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5240 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5187 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5241 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5188 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5242 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5189 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5243 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5190
5244
5191 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5245 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5192 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5246 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5193
5247
5194 @hybrid_property
5248 @hybrid_property
5195 def schedule_type(self):
5249 def schedule_type(self):
5196 return self._schedule_type
5250 return self._schedule_type
5197
5251
5198 @schedule_type.setter
5252 @schedule_type.setter
5199 def schedule_type(self, val):
5253 def schedule_type(self, val):
5200 if val not in self.schedule_types:
5254 if val not in self.schedule_types:
5201 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5255 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5202 val, self.schedule_type))
5256 val, self.schedule_type))
5203
5257
5204 self._schedule_type = val
5258 self._schedule_type = val
5205
5259
5206 @classmethod
5260 @classmethod
5207 def get_uid(cls, obj):
5261 def get_uid(cls, obj):
5208 args = obj.task_args
5262 args = obj.task_args
5209 kwargs = obj.task_kwargs
5263 kwargs = obj.task_kwargs
5210 if isinstance(args, JsonRaw):
5264 if isinstance(args, JsonRaw):
5211 try:
5265 try:
5212 args = json.loads(args)
5266 args = json.loads(args)
5213 except ValueError:
5267 except ValueError:
5214 args = tuple()
5268 args = tuple()
5215
5269
5216 if isinstance(kwargs, JsonRaw):
5270 if isinstance(kwargs, JsonRaw):
5217 try:
5271 try:
5218 kwargs = json.loads(kwargs)
5272 kwargs = json.loads(kwargs)
5219 except ValueError:
5273 except ValueError:
5220 kwargs = dict()
5274 kwargs = dict()
5221
5275
5222 dot_notation = obj.task_dot_notation
5276 dot_notation = obj.task_dot_notation
5223 val = '.'.join(map(safe_str, [
5277 val = '.'.join(map(safe_str, [
5224 sorted(dot_notation), args, sorted(kwargs.items())]))
5278 sorted(dot_notation), args, sorted(kwargs.items())]))
5225 return hashlib.sha1(val).hexdigest()
5279 return hashlib.sha1(val).hexdigest()
5226
5280
5227 @classmethod
5281 @classmethod
5228 def get_by_schedule_name(cls, schedule_name):
5282 def get_by_schedule_name(cls, schedule_name):
5229 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5283 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5230
5284
5231 @classmethod
5285 @classmethod
5232 def get_by_schedule_id(cls, schedule_id):
5286 def get_by_schedule_id(cls, schedule_id):
5233 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5287 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5234
5288
5235 @property
5289 @property
5236 def task(self):
5290 def task(self):
5237 return self.task_dot_notation
5291 return self.task_dot_notation
5238
5292
5239 @property
5293 @property
5240 def schedule(self):
5294 def schedule(self):
5241 from rhodecode.lib.celerylib.utils import raw_2_schedule
5295 from rhodecode.lib.celerylib.utils import raw_2_schedule
5242 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5296 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5243 return schedule
5297 return schedule
5244
5298
5245 @property
5299 @property
5246 def args(self):
5300 def args(self):
5247 try:
5301 try:
5248 return list(self.task_args or [])
5302 return list(self.task_args or [])
5249 except ValueError:
5303 except ValueError:
5250 return list()
5304 return list()
5251
5305
5252 @property
5306 @property
5253 def kwargs(self):
5307 def kwargs(self):
5254 try:
5308 try:
5255 return dict(self.task_kwargs or {})
5309 return dict(self.task_kwargs or {})
5256 except ValueError:
5310 except ValueError:
5257 return dict()
5311 return dict()
5258
5312
5259 def _as_raw(self, val):
5313 def _as_raw(self, val):
5260 if hasattr(val, 'de_coerce'):
5314 if hasattr(val, 'de_coerce'):
5261 val = val.de_coerce()
5315 val = val.de_coerce()
5262 if val:
5316 if val:
5263 val = json.dumps(val)
5317 val = json.dumps(val)
5264
5318
5265 return val
5319 return val
5266
5320
5267 @property
5321 @property
5268 def schedule_definition_raw(self):
5322 def schedule_definition_raw(self):
5269 return self._as_raw(self.schedule_definition)
5323 return self._as_raw(self.schedule_definition)
5270
5324
5271 @property
5325 @property
5272 def args_raw(self):
5326 def args_raw(self):
5273 return self._as_raw(self.task_args)
5327 return self._as_raw(self.task_args)
5274
5328
5275 @property
5329 @property
5276 def kwargs_raw(self):
5330 def kwargs_raw(self):
5277 return self._as_raw(self.task_kwargs)
5331 return self._as_raw(self.task_kwargs)
5278
5332
5279 def __repr__(self):
5333 def __repr__(self):
5280 return '<DB:ScheduleEntry({}:{})>'.format(
5334 return '<DB:ScheduleEntry({}:{})>'.format(
5281 self.schedule_entry_id, self.schedule_name)
5335 self.schedule_entry_id, self.schedule_name)
5282
5336
5283
5337
5284 @event.listens_for(ScheduleEntry, 'before_update')
5338 @event.listens_for(ScheduleEntry, 'before_update')
5285 def update_task_uid(mapper, connection, target):
5339 def update_task_uid(mapper, connection, target):
5286 target.task_uid = ScheduleEntry.get_uid(target)
5340 target.task_uid = ScheduleEntry.get_uid(target)
5287
5341
5288
5342
5289 @event.listens_for(ScheduleEntry, 'before_insert')
5343 @event.listens_for(ScheduleEntry, 'before_insert')
5290 def set_task_uid(mapper, connection, target):
5344 def set_task_uid(mapper, connection, target):
5291 target.task_uid = ScheduleEntry.get_uid(target)
5345 target.task_uid = ScheduleEntry.get_uid(target)
5292
5346
5293
5347
5294 class _BaseBranchPerms(BaseModel):
5348 class _BaseBranchPerms(BaseModel):
5295 @classmethod
5349 @classmethod
5296 def compute_hash(cls, value):
5350 def compute_hash(cls, value):
5297 return sha1_safe(value)
5351 return sha1_safe(value)
5298
5352
5299 @hybrid_property
5353 @hybrid_property
5300 def branch_pattern(self):
5354 def branch_pattern(self):
5301 return self._branch_pattern or '*'
5355 return self._branch_pattern or '*'
5302
5356
5303 @hybrid_property
5357 @hybrid_property
5304 def branch_hash(self):
5358 def branch_hash(self):
5305 return self._branch_hash
5359 return self._branch_hash
5306
5360
5307 def _validate_glob(self, value):
5361 def _validate_glob(self, value):
5308 re.compile('^' + glob2re(value) + '$')
5362 re.compile('^' + glob2re(value) + '$')
5309
5363
5310 @branch_pattern.setter
5364 @branch_pattern.setter
5311 def branch_pattern(self, value):
5365 def branch_pattern(self, value):
5312 self._validate_glob(value)
5366 self._validate_glob(value)
5313 self._branch_pattern = value or '*'
5367 self._branch_pattern = value or '*'
5314 # set the Hash when setting the branch pattern
5368 # set the Hash when setting the branch pattern
5315 self._branch_hash = self.compute_hash(self._branch_pattern)
5369 self._branch_hash = self.compute_hash(self._branch_pattern)
5316
5370
5317 def matches(self, branch):
5371 def matches(self, branch):
5318 """
5372 """
5319 Check if this the branch matches entry
5373 Check if this the branch matches entry
5320
5374
5321 :param branch: branch name for the commit
5375 :param branch: branch name for the commit
5322 """
5376 """
5323
5377
5324 branch = branch or ''
5378 branch = branch or ''
5325
5379
5326 branch_matches = True
5380 branch_matches = True
5327 if branch:
5381 if branch:
5328 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5382 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5329 branch_matches = bool(branch_regex.search(branch))
5383 branch_matches = bool(branch_regex.search(branch))
5330
5384
5331 return branch_matches
5385 return branch_matches
5332
5386
5333
5387
5334 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5388 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5335 __tablename__ = 'user_to_repo_branch_permissions'
5389 __tablename__ = 'user_to_repo_branch_permissions'
5336 __table_args__ = (
5390 __table_args__ = (
5337 base_table_args
5391 base_table_args
5338 )
5392 )
5339
5393
5340 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5394 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5341
5395
5342 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5396 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5343 repo = relationship('Repository', backref='user_branch_perms')
5397 repo = relationship('Repository', backref='user_branch_perms')
5344
5398
5345 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5399 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5346 permission = relationship('Permission')
5400 permission = relationship('Permission')
5347
5401
5348 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5402 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5349 user_repo_to_perm = relationship('UserRepoToPerm')
5403 user_repo_to_perm = relationship('UserRepoToPerm')
5350
5404
5351 rule_order = Column('rule_order', Integer(), nullable=False)
5405 rule_order = Column('rule_order', Integer(), nullable=False)
5352 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5406 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5353 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5407 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5354
5408
5355 def __unicode__(self):
5409 def __unicode__(self):
5356 return u'<UserBranchPermission(%s => %r)>' % (
5410 return u'<UserBranchPermission(%s => %r)>' % (
5357 self.user_repo_to_perm, self.branch_pattern)
5411 self.user_repo_to_perm, self.branch_pattern)
5358
5412
5359
5413
5360 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5414 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5361 __tablename__ = 'user_group_to_repo_branch_permissions'
5415 __tablename__ = 'user_group_to_repo_branch_permissions'
5362 __table_args__ = (
5416 __table_args__ = (
5363 base_table_args
5417 base_table_args
5364 )
5418 )
5365
5419
5366 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5420 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5367
5421
5368 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5422 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5369 repo = relationship('Repository', backref='user_group_branch_perms')
5423 repo = relationship('Repository', backref='user_group_branch_perms')
5370
5424
5371 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5425 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5372 permission = relationship('Permission')
5426 permission = relationship('Permission')
5373
5427
5374 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5428 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5375 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5429 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5376
5430
5377 rule_order = Column('rule_order', Integer(), nullable=False)
5431 rule_order = Column('rule_order', Integer(), nullable=False)
5378 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5432 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5379 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5433 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5380
5434
5381 def __unicode__(self):
5435 def __unicode__(self):
5382 return u'<UserBranchPermission(%s => %r)>' % (
5436 return u'<UserBranchPermission(%s => %r)>' % (
5383 self.user_group_repo_to_perm, self.branch_pattern)
5437 self.user_group_repo_to_perm, self.branch_pattern)
5384
5438
5385
5439
5386 class UserBookmark(Base, BaseModel):
5440 class UserBookmark(Base, BaseModel):
5387 __tablename__ = 'user_bookmarks'
5441 __tablename__ = 'user_bookmarks'
5388 __table_args__ = (
5442 __table_args__ = (
5389 UniqueConstraint('user_id', 'bookmark_repo_id'),
5443 UniqueConstraint('user_id', 'bookmark_repo_id'),
5390 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5444 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5391 UniqueConstraint('user_id', 'bookmark_position'),
5445 UniqueConstraint('user_id', 'bookmark_position'),
5392 base_table_args
5446 base_table_args
5393 )
5447 )
5394
5448
5395 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5449 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5396 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5450 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5397 position = Column("bookmark_position", Integer(), nullable=False)
5451 position = Column("bookmark_position", Integer(), nullable=False)
5398 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5452 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5399 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5453 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5400 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5454 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5401
5455
5402 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5456 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5403 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5457 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5404
5458
5405 user = relationship("User")
5459 user = relationship("User")
5406
5460
5407 repository = relationship("Repository")
5461 repository = relationship("Repository")
5408 repository_group = relationship("RepoGroup")
5462 repository_group = relationship("RepoGroup")
5409
5463
5410 @classmethod
5464 @classmethod
5411 def get_by_position_for_user(cls, position, user_id):
5465 def get_by_position_for_user(cls, position, user_id):
5412 return cls.query() \
5466 return cls.query() \
5413 .filter(UserBookmark.user_id == user_id) \
5467 .filter(UserBookmark.user_id == user_id) \
5414 .filter(UserBookmark.position == position).scalar()
5468 .filter(UserBookmark.position == position).scalar()
5415
5469
5416 @classmethod
5470 @classmethod
5417 def get_bookmarks_for_user(cls, user_id, cache=True):
5471 def get_bookmarks_for_user(cls, user_id, cache=True):
5418 bookmarks = cls.query() \
5472 bookmarks = cls.query() \
5419 .filter(UserBookmark.user_id == user_id) \
5473 .filter(UserBookmark.user_id == user_id) \
5420 .options(joinedload(UserBookmark.repository)) \
5474 .options(joinedload(UserBookmark.repository)) \
5421 .options(joinedload(UserBookmark.repository_group)) \
5475 .options(joinedload(UserBookmark.repository_group)) \
5422 .order_by(UserBookmark.position.asc())
5476 .order_by(UserBookmark.position.asc())
5423
5477
5424 if cache:
5478 if cache:
5425 bookmarks = bookmarks.options(
5479 bookmarks = bookmarks.options(
5426 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5480 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5427 )
5481 )
5428
5482
5429 return bookmarks.all()
5483 return bookmarks.all()
5430
5484
5431 def __unicode__(self):
5485 def __unicode__(self):
5432 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5486 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5433
5487
5434
5488
5435 class FileStore(Base, BaseModel):
5489 class FileStore(Base, BaseModel):
5436 __tablename__ = 'file_store'
5490 __tablename__ = 'file_store'
5437 __table_args__ = (
5491 __table_args__ = (
5438 base_table_args
5492 base_table_args
5439 )
5493 )
5440
5494
5441 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5495 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5442 file_uid = Column('file_uid', String(1024), nullable=False)
5496 file_uid = Column('file_uid', String(1024), nullable=False)
5443 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5497 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5444 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5498 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5445 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5499 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5446
5500
5447 # sha256 hash
5501 # sha256 hash
5448 file_hash = Column('file_hash', String(512), nullable=False)
5502 file_hash = Column('file_hash', String(512), nullable=False)
5449 file_size = Column('file_size', BigInteger(), nullable=False)
5503 file_size = Column('file_size', BigInteger(), nullable=False)
5450
5504
5451 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5505 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5452 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5506 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5453 accessed_count = Column('accessed_count', Integer(), default=0)
5507 accessed_count = Column('accessed_count', Integer(), default=0)
5454
5508
5455 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5509 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5456
5510
5457 # if repo/repo_group reference is set, check for permissions
5511 # if repo/repo_group reference is set, check for permissions
5458 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5512 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5459
5513
5460 # hidden defines an attachment that should be hidden from showing in artifact listing
5514 # hidden defines an attachment that should be hidden from showing in artifact listing
5461 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5515 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5462
5516
5463 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5517 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5464 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5518 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5465
5519
5466 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5520 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5467
5521
5468 # scope limited to user, which requester have access to
5522 # scope limited to user, which requester have access to
5469 scope_user_id = Column(
5523 scope_user_id = Column(
5470 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5524 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5471 nullable=True, unique=None, default=None)
5525 nullable=True, unique=None, default=None)
5472 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5526 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5473
5527
5474 # scope limited to user group, which requester have access to
5528 # scope limited to user group, which requester have access to
5475 scope_user_group_id = Column(
5529 scope_user_group_id = Column(
5476 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5530 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5477 nullable=True, unique=None, default=None)
5531 nullable=True, unique=None, default=None)
5478 user_group = relationship('UserGroup', lazy='joined')
5532 user_group = relationship('UserGroup', lazy='joined')
5479
5533
5480 # scope limited to repo, which requester have access to
5534 # scope limited to repo, which requester have access to
5481 scope_repo_id = Column(
5535 scope_repo_id = Column(
5482 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5536 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5483 nullable=True, unique=None, default=None)
5537 nullable=True, unique=None, default=None)
5484 repo = relationship('Repository', lazy='joined')
5538 repo = relationship('Repository', lazy='joined')
5485
5539
5486 # scope limited to repo group, which requester have access to
5540 # scope limited to repo group, which requester have access to
5487 scope_repo_group_id = Column(
5541 scope_repo_group_id = Column(
5488 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5542 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5489 nullable=True, unique=None, default=None)
5543 nullable=True, unique=None, default=None)
5490 repo_group = relationship('RepoGroup', lazy='joined')
5544 repo_group = relationship('RepoGroup', lazy='joined')
5491
5545
5492 @classmethod
5546 @classmethod
5493 def get_by_store_uid(cls, file_store_uid, safe=False):
5547 def get_by_store_uid(cls, file_store_uid, safe=False):
5494 if safe:
5548 if safe:
5495 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5549 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5496 else:
5550 else:
5497 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5551 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5498
5552
5499 @classmethod
5553 @classmethod
5500 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5554 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5501 file_description='', enabled=True, hidden=False, check_acl=True,
5555 file_description='', enabled=True, hidden=False, check_acl=True,
5502 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5556 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5503
5557
5504 store_entry = FileStore()
5558 store_entry = FileStore()
5505 store_entry.file_uid = file_uid
5559 store_entry.file_uid = file_uid
5506 store_entry.file_display_name = file_display_name
5560 store_entry.file_display_name = file_display_name
5507 store_entry.file_org_name = filename
5561 store_entry.file_org_name = filename
5508 store_entry.file_size = file_size
5562 store_entry.file_size = file_size
5509 store_entry.file_hash = file_hash
5563 store_entry.file_hash = file_hash
5510 store_entry.file_description = file_description
5564 store_entry.file_description = file_description
5511
5565
5512 store_entry.check_acl = check_acl
5566 store_entry.check_acl = check_acl
5513 store_entry.enabled = enabled
5567 store_entry.enabled = enabled
5514 store_entry.hidden = hidden
5568 store_entry.hidden = hidden
5515
5569
5516 store_entry.user_id = user_id
5570 store_entry.user_id = user_id
5517 store_entry.scope_user_id = scope_user_id
5571 store_entry.scope_user_id = scope_user_id
5518 store_entry.scope_repo_id = scope_repo_id
5572 store_entry.scope_repo_id = scope_repo_id
5519 store_entry.scope_repo_group_id = scope_repo_group_id
5573 store_entry.scope_repo_group_id = scope_repo_group_id
5520
5574
5521 return store_entry
5575 return store_entry
5522
5576
5523 @classmethod
5577 @classmethod
5524 def store_metadata(cls, file_store_id, args, commit=True):
5578 def store_metadata(cls, file_store_id, args, commit=True):
5525 file_store = FileStore.get(file_store_id)
5579 file_store = FileStore.get(file_store_id)
5526 if file_store is None:
5580 if file_store is None:
5527 return
5581 return
5528
5582
5529 for section, key, value, value_type in args:
5583 for section, key, value, value_type in args:
5530 has_key = FileStoreMetadata().query() \
5584 has_key = FileStoreMetadata().query() \
5531 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5585 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5532 .filter(FileStoreMetadata.file_store_meta_section == section) \
5586 .filter(FileStoreMetadata.file_store_meta_section == section) \
5533 .filter(FileStoreMetadata.file_store_meta_key == key) \
5587 .filter(FileStoreMetadata.file_store_meta_key == key) \
5534 .scalar()
5588 .scalar()
5535 if has_key:
5589 if has_key:
5536 msg = 'key `{}` already defined under section `{}` for this file.'\
5590 msg = 'key `{}` already defined under section `{}` for this file.'\
5537 .format(key, section)
5591 .format(key, section)
5538 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5592 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5539
5593
5540 # NOTE(marcink): raises ArtifactMetadataBadValueType
5594 # NOTE(marcink): raises ArtifactMetadataBadValueType
5541 FileStoreMetadata.valid_value_type(value_type)
5595 FileStoreMetadata.valid_value_type(value_type)
5542
5596
5543 meta_entry = FileStoreMetadata()
5597 meta_entry = FileStoreMetadata()
5544 meta_entry.file_store = file_store
5598 meta_entry.file_store = file_store
5545 meta_entry.file_store_meta_section = section
5599 meta_entry.file_store_meta_section = section
5546 meta_entry.file_store_meta_key = key
5600 meta_entry.file_store_meta_key = key
5547 meta_entry.file_store_meta_value_type = value_type
5601 meta_entry.file_store_meta_value_type = value_type
5548 meta_entry.file_store_meta_value = value
5602 meta_entry.file_store_meta_value = value
5549
5603
5550 Session().add(meta_entry)
5604 Session().add(meta_entry)
5551
5605
5552 try:
5606 try:
5553 if commit:
5607 if commit:
5554 Session().commit()
5608 Session().commit()
5555 except IntegrityError:
5609 except IntegrityError:
5556 Session().rollback()
5610 Session().rollback()
5557 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5611 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5558
5612
5559 @classmethod
5613 @classmethod
5560 def bump_access_counter(cls, file_uid, commit=True):
5614 def bump_access_counter(cls, file_uid, commit=True):
5561 FileStore().query()\
5615 FileStore().query()\
5562 .filter(FileStore.file_uid == file_uid)\
5616 .filter(FileStore.file_uid == file_uid)\
5563 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5617 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5564 FileStore.accessed_on: datetime.datetime.now()})
5618 FileStore.accessed_on: datetime.datetime.now()})
5565 if commit:
5619 if commit:
5566 Session().commit()
5620 Session().commit()
5567
5621
5568 def __json__(self):
5622 def __json__(self):
5569 data = {
5623 data = {
5570 'filename': self.file_display_name,
5624 'filename': self.file_display_name,
5571 'filename_org': self.file_org_name,
5625 'filename_org': self.file_org_name,
5572 'file_uid': self.file_uid,
5626 'file_uid': self.file_uid,
5573 'description': self.file_description,
5627 'description': self.file_description,
5574 'hidden': self.hidden,
5628 'hidden': self.hidden,
5575 'size': self.file_size,
5629 'size': self.file_size,
5576 'created_on': self.created_on,
5630 'created_on': self.created_on,
5577 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5631 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5578 'downloaded_times': self.accessed_count,
5632 'downloaded_times': self.accessed_count,
5579 'sha256': self.file_hash,
5633 'sha256': self.file_hash,
5580 'metadata': self.file_metadata,
5634 'metadata': self.file_metadata,
5581 }
5635 }
5582
5636
5583 return data
5637 return data
5584
5638
5585 def __repr__(self):
5639 def __repr__(self):
5586 return '<FileStore({})>'.format(self.file_store_id)
5640 return '<FileStore({})>'.format(self.file_store_id)
5587
5641
5588
5642
5589 class FileStoreMetadata(Base, BaseModel):
5643 class FileStoreMetadata(Base, BaseModel):
5590 __tablename__ = 'file_store_metadata'
5644 __tablename__ = 'file_store_metadata'
5591 __table_args__ = (
5645 __table_args__ = (
5592 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5646 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5593 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5647 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5594 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5648 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5595 base_table_args
5649 base_table_args
5596 )
5650 )
5597 SETTINGS_TYPES = {
5651 SETTINGS_TYPES = {
5598 'str': safe_str,
5652 'str': safe_str,
5599 'int': safe_int,
5653 'int': safe_int,
5600 'unicode': safe_unicode,
5654 'unicode': safe_unicode,
5601 'bool': str2bool,
5655 'bool': str2bool,
5602 'list': functools.partial(aslist, sep=',')
5656 'list': functools.partial(aslist, sep=',')
5603 }
5657 }
5604
5658
5605 file_store_meta_id = Column(
5659 file_store_meta_id = Column(
5606 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5660 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5607 primary_key=True)
5661 primary_key=True)
5608 _file_store_meta_section = Column(
5662 _file_store_meta_section = Column(
5609 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5663 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5610 nullable=True, unique=None, default=None)
5664 nullable=True, unique=None, default=None)
5611 _file_store_meta_section_hash = Column(
5665 _file_store_meta_section_hash = Column(
5612 "file_store_meta_section_hash", String(255),
5666 "file_store_meta_section_hash", String(255),
5613 nullable=True, unique=None, default=None)
5667 nullable=True, unique=None, default=None)
5614 _file_store_meta_key = Column(
5668 _file_store_meta_key = Column(
5615 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5669 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5616 nullable=True, unique=None, default=None)
5670 nullable=True, unique=None, default=None)
5617 _file_store_meta_key_hash = Column(
5671 _file_store_meta_key_hash = Column(
5618 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5672 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5619 _file_store_meta_value = Column(
5673 _file_store_meta_value = Column(
5620 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5674 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5621 nullable=True, unique=None, default=None)
5675 nullable=True, unique=None, default=None)
5622 _file_store_meta_value_type = Column(
5676 _file_store_meta_value_type = Column(
5623 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5677 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5624 default='unicode')
5678 default='unicode')
5625
5679
5626 file_store_id = Column(
5680 file_store_id = Column(
5627 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5681 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5628 nullable=True, unique=None, default=None)
5682 nullable=True, unique=None, default=None)
5629
5683
5630 file_store = relationship('FileStore', lazy='joined')
5684 file_store = relationship('FileStore', lazy='joined')
5631
5685
5632 @classmethod
5686 @classmethod
5633 def valid_value_type(cls, value):
5687 def valid_value_type(cls, value):
5634 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5688 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5635 raise ArtifactMetadataBadValueType(
5689 raise ArtifactMetadataBadValueType(
5636 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5690 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5637
5691
5638 @hybrid_property
5692 @hybrid_property
5639 def file_store_meta_section(self):
5693 def file_store_meta_section(self):
5640 return self._file_store_meta_section
5694 return self._file_store_meta_section
5641
5695
5642 @file_store_meta_section.setter
5696 @file_store_meta_section.setter
5643 def file_store_meta_section(self, value):
5697 def file_store_meta_section(self, value):
5644 self._file_store_meta_section = value
5698 self._file_store_meta_section = value
5645 self._file_store_meta_section_hash = _hash_key(value)
5699 self._file_store_meta_section_hash = _hash_key(value)
5646
5700
5647 @hybrid_property
5701 @hybrid_property
5648 def file_store_meta_key(self):
5702 def file_store_meta_key(self):
5649 return self._file_store_meta_key
5703 return self._file_store_meta_key
5650
5704
5651 @file_store_meta_key.setter
5705 @file_store_meta_key.setter
5652 def file_store_meta_key(self, value):
5706 def file_store_meta_key(self, value):
5653 self._file_store_meta_key = value
5707 self._file_store_meta_key = value
5654 self._file_store_meta_key_hash = _hash_key(value)
5708 self._file_store_meta_key_hash = _hash_key(value)
5655
5709
5656 @hybrid_property
5710 @hybrid_property
5657 def file_store_meta_value(self):
5711 def file_store_meta_value(self):
5658 val = self._file_store_meta_value
5712 val = self._file_store_meta_value
5659
5713
5660 if self._file_store_meta_value_type:
5714 if self._file_store_meta_value_type:
5661 # e.g unicode.encrypted == unicode
5715 # e.g unicode.encrypted == unicode
5662 _type = self._file_store_meta_value_type.split('.')[0]
5716 _type = self._file_store_meta_value_type.split('.')[0]
5663 # decode the encrypted value if it's encrypted field type
5717 # decode the encrypted value if it's encrypted field type
5664 if '.encrypted' in self._file_store_meta_value_type:
5718 if '.encrypted' in self._file_store_meta_value_type:
5665 cipher = EncryptedTextValue()
5719 cipher = EncryptedTextValue()
5666 val = safe_unicode(cipher.process_result_value(val, None))
5720 val = safe_unicode(cipher.process_result_value(val, None))
5667 # do final type conversion
5721 # do final type conversion
5668 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5722 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5669 val = converter(val)
5723 val = converter(val)
5670
5724
5671 return val
5725 return val
5672
5726
5673 @file_store_meta_value.setter
5727 @file_store_meta_value.setter
5674 def file_store_meta_value(self, val):
5728 def file_store_meta_value(self, val):
5675 val = safe_unicode(val)
5729 val = safe_unicode(val)
5676 # encode the encrypted value
5730 # encode the encrypted value
5677 if '.encrypted' in self.file_store_meta_value_type:
5731 if '.encrypted' in self.file_store_meta_value_type:
5678 cipher = EncryptedTextValue()
5732 cipher = EncryptedTextValue()
5679 val = safe_unicode(cipher.process_bind_param(val, None))
5733 val = safe_unicode(cipher.process_bind_param(val, None))
5680 self._file_store_meta_value = val
5734 self._file_store_meta_value = val
5681
5735
5682 @hybrid_property
5736 @hybrid_property
5683 def file_store_meta_value_type(self):
5737 def file_store_meta_value_type(self):
5684 return self._file_store_meta_value_type
5738 return self._file_store_meta_value_type
5685
5739
5686 @file_store_meta_value_type.setter
5740 @file_store_meta_value_type.setter
5687 def file_store_meta_value_type(self, val):
5741 def file_store_meta_value_type(self, val):
5688 # e.g unicode.encrypted
5742 # e.g unicode.encrypted
5689 self.valid_value_type(val)
5743 self.valid_value_type(val)
5690 self._file_store_meta_value_type = val
5744 self._file_store_meta_value_type = val
5691
5745
5692 def __json__(self):
5746 def __json__(self):
5693 data = {
5747 data = {
5694 'artifact': self.file_store.file_uid,
5748 'artifact': self.file_store.file_uid,
5695 'section': self.file_store_meta_section,
5749 'section': self.file_store_meta_section,
5696 'key': self.file_store_meta_key,
5750 'key': self.file_store_meta_key,
5697 'value': self.file_store_meta_value,
5751 'value': self.file_store_meta_value,
5698 }
5752 }
5699
5753
5700 return data
5754 return data
5701
5755
5702 def __repr__(self):
5756 def __repr__(self):
5703 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5757 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5704 self.file_store_meta_key, self.file_store_meta_value)
5758 self.file_store_meta_key, self.file_store_meta_value)
5705
5759
5706
5760
5707 class DbMigrateVersion(Base, BaseModel):
5761 class DbMigrateVersion(Base, BaseModel):
5708 __tablename__ = 'db_migrate_version'
5762 __tablename__ = 'db_migrate_version'
5709 __table_args__ = (
5763 __table_args__ = (
5710 base_table_args,
5764 base_table_args,
5711 )
5765 )
5712
5766
5713 repository_id = Column('repository_id', String(250), primary_key=True)
5767 repository_id = Column('repository_id', String(250), primary_key=True)
5714 repository_path = Column('repository_path', Text)
5768 repository_path = Column('repository_path', Text)
5715 version = Column('version', Integer)
5769 version = Column('version', Integer)
5716
5770
5717 @classmethod
5771 @classmethod
5718 def set_version(cls, version):
5772 def set_version(cls, version):
5719 """
5773 """
5720 Helper for forcing a different version, usually for debugging purposes via ishell.
5774 Helper for forcing a different version, usually for debugging purposes via ishell.
5721 """
5775 """
5722 ver = DbMigrateVersion.query().first()
5776 ver = DbMigrateVersion.query().first()
5723 ver.version = version
5777 ver.version = version
5724 Session().commit()
5778 Session().commit()
5725
5779
5726
5780
5727 class DbSession(Base, BaseModel):
5781 class DbSession(Base, BaseModel):
5728 __tablename__ = 'db_session'
5782 __tablename__ = 'db_session'
5729 __table_args__ = (
5783 __table_args__ = (
5730 base_table_args,
5784 base_table_args,
5731 )
5785 )
5732
5786
5733 def __repr__(self):
5787 def __repr__(self):
5734 return '<DB:DbSession({})>'.format(self.id)
5788 return '<DB:DbSession({})>'.format(self.id)
5735
5789
5736 id = Column('id', Integer())
5790 id = Column('id', Integer())
5737 namespace = Column('namespace', String(255), primary_key=True)
5791 namespace = Column('namespace', String(255), primary_key=True)
5738 accessed = Column('accessed', DateTime, nullable=False)
5792 accessed = Column('accessed', DateTime, nullable=False)
5739 created = Column('created', DateTime, nullable=False)
5793 created = Column('created', DateTime, nullable=False)
5740 data = Column('data', PickleType, nullable=False)
5794 data = Column('data', PickleType, nullable=False)
@@ -1,631 +1,640 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 this is forms validation classes
22 this is forms validation classes
23 http://formencode.org/module-formencode.validators.html
23 http://formencode.org/module-formencode.validators.html
24 for list off all availible validators
24 for list off all availible validators
25
25
26 we can create our own validators
26 we can create our own validators
27
27
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 pre_validators [] These validators will be applied before the schema
29 pre_validators [] These validators will be applied before the schema
30 chained_validators [] These validators will be applied after the schema
30 chained_validators [] These validators will be applied after the schema
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35
35
36
36
37 <name> = formencode.validators.<name of validator>
37 <name> = formencode.validators.<name of validator>
38 <name> must equal form name
38 <name> must equal form name
39 list=[1,2,3,4,5]
39 list=[1,2,3,4,5]
40 for SELECT use formencode.All(OneOf(list), Int())
40 for SELECT use formencode.All(OneOf(list), Int())
41
41
42 """
42 """
43
43
44 import deform
44 import deform
45 import logging
45 import logging
46 import formencode
46 import formencode
47
47
48 from pkg_resources import resource_filename
48 from pkg_resources import resource_filename
49 from formencode import All, Pipe
49 from formencode import All, Pipe
50
50
51 from pyramid.threadlocal import get_current_request
51 from pyramid.threadlocal import get_current_request
52
52
53 from rhodecode import BACKENDS
53 from rhodecode import BACKENDS
54 from rhodecode.lib import helpers
54 from rhodecode.lib import helpers
55 from rhodecode.model import validators as v
55 from rhodecode.model import validators as v
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 deform_templates = resource_filename('deform', 'templates')
60 deform_templates = resource_filename('deform', 'templates')
61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 search_path = (rhodecode_templates, deform_templates)
62 search_path = (rhodecode_templates, deform_templates)
63
63
64
64
65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 def __call__(self, template_name, **kw):
67 def __call__(self, template_name, **kw):
68 kw['h'] = helpers
68 kw['h'] = helpers
69 kw['request'] = get_current_request()
69 kw['request'] = get_current_request()
70 return self.load(template_name)(**kw)
70 return self.load(template_name)(**kw)
71
71
72
72
73 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
73 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
74 deform.Form.set_default_renderer(form_renderer)
74 deform.Form.set_default_renderer(form_renderer)
75
75
76
76
77 def LoginForm(localizer):
77 def LoginForm(localizer):
78 _ = localizer
78 _ = localizer
79
79
80 class _LoginForm(formencode.Schema):
80 class _LoginForm(formencode.Schema):
81 allow_extra_fields = True
81 allow_extra_fields = True
82 filter_extra_fields = True
82 filter_extra_fields = True
83 username = v.UnicodeString(
83 username = v.UnicodeString(
84 strip=True,
84 strip=True,
85 min=1,
85 min=1,
86 not_empty=True,
86 not_empty=True,
87 messages={
87 messages={
88 'empty': _(u'Please enter a login'),
88 'empty': _(u'Please enter a login'),
89 'tooShort': _(u'Enter a value %(min)i characters long or more')
89 'tooShort': _(u'Enter a value %(min)i characters long or more')
90 }
90 }
91 )
91 )
92
92
93 password = v.UnicodeString(
93 password = v.UnicodeString(
94 strip=False,
94 strip=False,
95 min=3,
95 min=3,
96 max=72,
96 max=72,
97 not_empty=True,
97 not_empty=True,
98 messages={
98 messages={
99 'empty': _(u'Please enter a password'),
99 'empty': _(u'Please enter a password'),
100 'tooShort': _(u'Enter %(min)i characters or more')}
100 'tooShort': _(u'Enter %(min)i characters or more')}
101 )
101 )
102
102
103 remember = v.StringBoolean(if_missing=False)
103 remember = v.StringBoolean(if_missing=False)
104
104
105 chained_validators = [v.ValidAuth(localizer)]
105 chained_validators = [v.ValidAuth(localizer)]
106 return _LoginForm
106 return _LoginForm
107
107
108
108
109 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
109 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
110 old_data = old_data or {}
110 old_data = old_data or {}
111 available_languages = available_languages or []
111 available_languages = available_languages or []
112 _ = localizer
112 _ = localizer
113
113
114 class _UserForm(formencode.Schema):
114 class _UserForm(formencode.Schema):
115 allow_extra_fields = True
115 allow_extra_fields = True
116 filter_extra_fields = True
116 filter_extra_fields = True
117 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
117 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
118 v.ValidUsername(localizer, edit, old_data))
118 v.ValidUsername(localizer, edit, old_data))
119 if edit:
119 if edit:
120 new_password = All(
120 new_password = All(
121 v.ValidPassword(localizer),
121 v.ValidPassword(localizer),
122 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
122 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
123 )
123 )
124 password_confirmation = All(
124 password_confirmation = All(
125 v.ValidPassword(localizer),
125 v.ValidPassword(localizer),
126 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
126 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
127 )
127 )
128 admin = v.StringBoolean(if_missing=False)
128 admin = v.StringBoolean(if_missing=False)
129 else:
129 else:
130 password = All(
130 password = All(
131 v.ValidPassword(localizer),
131 v.ValidPassword(localizer),
132 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
132 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
133 )
133 )
134 password_confirmation = All(
134 password_confirmation = All(
135 v.ValidPassword(localizer),
135 v.ValidPassword(localizer),
136 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
136 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
137 )
137 )
138
138
139 password_change = v.StringBoolean(if_missing=False)
139 password_change = v.StringBoolean(if_missing=False)
140 create_repo_group = v.StringBoolean(if_missing=False)
140 create_repo_group = v.StringBoolean(if_missing=False)
141
141
142 active = v.StringBoolean(if_missing=False)
142 active = v.StringBoolean(if_missing=False)
143 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
143 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
144 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
144 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
145 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
145 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
146 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
146 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
147 if_missing='')
147 if_missing='')
148 extern_name = v.UnicodeString(strip=True)
148 extern_name = v.UnicodeString(strip=True)
149 extern_type = v.UnicodeString(strip=True)
149 extern_type = v.UnicodeString(strip=True)
150 language = v.OneOf(available_languages, hideList=False,
150 language = v.OneOf(available_languages, hideList=False,
151 testValueList=True, if_missing=None)
151 testValueList=True, if_missing=None)
152 chained_validators = [v.ValidPasswordsMatch(localizer)]
152 chained_validators = [v.ValidPasswordsMatch(localizer)]
153 return _UserForm
153 return _UserForm
154
154
155
155
156 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
156 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
157 old_data = old_data or {}
157 old_data = old_data or {}
158 _ = localizer
158 _ = localizer
159
159
160 class _UserGroupForm(formencode.Schema):
160 class _UserGroupForm(formencode.Schema):
161 allow_extra_fields = True
161 allow_extra_fields = True
162 filter_extra_fields = True
162 filter_extra_fields = True
163
163
164 users_group_name = All(
164 users_group_name = All(
165 v.UnicodeString(strip=True, min=1, not_empty=True),
165 v.UnicodeString(strip=True, min=1, not_empty=True),
166 v.ValidUserGroup(localizer, edit, old_data)
166 v.ValidUserGroup(localizer, edit, old_data)
167 )
167 )
168 user_group_description = v.UnicodeString(strip=True, min=1,
168 user_group_description = v.UnicodeString(strip=True, min=1,
169 not_empty=False)
169 not_empty=False)
170
170
171 users_group_active = v.StringBoolean(if_missing=False)
171 users_group_active = v.StringBoolean(if_missing=False)
172
172
173 if edit:
173 if edit:
174 # this is user group owner
174 # this is user group owner
175 user = All(
175 user = All(
176 v.UnicodeString(not_empty=True),
176 v.UnicodeString(not_empty=True),
177 v.ValidRepoUser(localizer, allow_disabled))
177 v.ValidRepoUser(localizer, allow_disabled))
178 return _UserGroupForm
178 return _UserGroupForm
179
179
180
180
181 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
181 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
182 can_create_in_root=False, allow_disabled=False):
182 can_create_in_root=False, allow_disabled=False):
183 _ = localizer
183 _ = localizer
184 old_data = old_data or {}
184 old_data = old_data or {}
185 available_groups = available_groups or []
185 available_groups = available_groups or []
186
186
187 class _RepoGroupForm(formencode.Schema):
187 class _RepoGroupForm(formencode.Schema):
188 allow_extra_fields = True
188 allow_extra_fields = True
189 filter_extra_fields = False
189 filter_extra_fields = False
190
190
191 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
191 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
192 v.SlugifyName(localizer),)
192 v.SlugifyName(localizer),)
193 group_description = v.UnicodeString(strip=True, min=1,
193 group_description = v.UnicodeString(strip=True, min=1,
194 not_empty=False)
194 not_empty=False)
195 group_copy_permissions = v.StringBoolean(if_missing=False)
195 group_copy_permissions = v.StringBoolean(if_missing=False)
196
196
197 group_parent_id = v.OneOf(available_groups, hideList=False,
197 group_parent_id = v.OneOf(available_groups, hideList=False,
198 testValueList=True, not_empty=True)
198 testValueList=True, not_empty=True)
199 enable_locking = v.StringBoolean(if_missing=False)
199 enable_locking = v.StringBoolean(if_missing=False)
200 chained_validators = [
200 chained_validators = [
201 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
201 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
202
202
203 if edit:
203 if edit:
204 # this is repo group owner
204 # this is repo group owner
205 user = All(
205 user = All(
206 v.UnicodeString(not_empty=True),
206 v.UnicodeString(not_empty=True),
207 v.ValidRepoUser(localizer, allow_disabled))
207 v.ValidRepoUser(localizer, allow_disabled))
208 return _RepoGroupForm
208 return _RepoGroupForm
209
209
210
210
211 def RegisterForm(localizer, edit=False, old_data=None):
211 def RegisterForm(localizer, edit=False, old_data=None):
212 _ = localizer
212 _ = localizer
213 old_data = old_data or {}
213 old_data = old_data or {}
214
214
215 class _RegisterForm(formencode.Schema):
215 class _RegisterForm(formencode.Schema):
216 allow_extra_fields = True
216 allow_extra_fields = True
217 filter_extra_fields = True
217 filter_extra_fields = True
218 username = All(
218 username = All(
219 v.ValidUsername(localizer, edit, old_data),
219 v.ValidUsername(localizer, edit, old_data),
220 v.UnicodeString(strip=True, min=1, not_empty=True)
220 v.UnicodeString(strip=True, min=1, not_empty=True)
221 )
221 )
222 password = All(
222 password = All(
223 v.ValidPassword(localizer),
223 v.ValidPassword(localizer),
224 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
224 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
225 )
225 )
226 password_confirmation = All(
226 password_confirmation = All(
227 v.ValidPassword(localizer),
227 v.ValidPassword(localizer),
228 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
228 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
229 )
229 )
230 active = v.StringBoolean(if_missing=False)
230 active = v.StringBoolean(if_missing=False)
231 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
231 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
232 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
232 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
233 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
233 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
234
234
235 chained_validators = [v.ValidPasswordsMatch(localizer)]
235 chained_validators = [v.ValidPasswordsMatch(localizer)]
236 return _RegisterForm
236 return _RegisterForm
237
237
238
238
239 def PasswordResetForm(localizer):
239 def PasswordResetForm(localizer):
240 _ = localizer
240 _ = localizer
241
241
242 class _PasswordResetForm(formencode.Schema):
242 class _PasswordResetForm(formencode.Schema):
243 allow_extra_fields = True
243 allow_extra_fields = True
244 filter_extra_fields = True
244 filter_extra_fields = True
245 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
245 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
246 return _PasswordResetForm
246 return _PasswordResetForm
247
247
248
248
249 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
249 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
250 _ = localizer
250 _ = localizer
251 old_data = old_data or {}
251 old_data = old_data or {}
252 repo_groups = repo_groups or []
252 repo_groups = repo_groups or []
253 supported_backends = BACKENDS.keys()
253 supported_backends = BACKENDS.keys()
254
254
255 class _RepoForm(formencode.Schema):
255 class _RepoForm(formencode.Schema):
256 allow_extra_fields = True
256 allow_extra_fields = True
257 filter_extra_fields = False
257 filter_extra_fields = False
258 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
258 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
259 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
259 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
260 repo_group = All(v.CanWriteGroup(localizer, old_data),
260 repo_group = All(v.CanWriteGroup(localizer, old_data),
261 v.OneOf(repo_groups, hideList=True))
261 v.OneOf(repo_groups, hideList=True))
262 repo_type = v.OneOf(supported_backends, required=False,
262 repo_type = v.OneOf(supported_backends, required=False,
263 if_missing=old_data.get('repo_type'))
263 if_missing=old_data.get('repo_type'))
264 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
264 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
265 repo_private = v.StringBoolean(if_missing=False)
265 repo_private = v.StringBoolean(if_missing=False)
266 repo_copy_permissions = v.StringBoolean(if_missing=False)
266 repo_copy_permissions = v.StringBoolean(if_missing=False)
267 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
267 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
268
268
269 repo_enable_statistics = v.StringBoolean(if_missing=False)
269 repo_enable_statistics = v.StringBoolean(if_missing=False)
270 repo_enable_downloads = v.StringBoolean(if_missing=False)
270 repo_enable_downloads = v.StringBoolean(if_missing=False)
271 repo_enable_locking = v.StringBoolean(if_missing=False)
271 repo_enable_locking = v.StringBoolean(if_missing=False)
272
272
273 if edit:
273 if edit:
274 # this is repo owner
274 # this is repo owner
275 user = All(
275 user = All(
276 v.UnicodeString(not_empty=True),
276 v.UnicodeString(not_empty=True),
277 v.ValidRepoUser(localizer, allow_disabled))
277 v.ValidRepoUser(localizer, allow_disabled))
278 clone_uri_change = v.UnicodeString(
278 clone_uri_change = v.UnicodeString(
279 not_empty=False, if_missing=v.Missing)
279 not_empty=False, if_missing=v.Missing)
280
280
281 chained_validators = [v.ValidCloneUri(localizer),
281 chained_validators = [v.ValidCloneUri(localizer),
282 v.ValidRepoName(localizer, edit, old_data)]
282 v.ValidRepoName(localizer, edit, old_data)]
283 return _RepoForm
283 return _RepoForm
284
284
285
285
286 def RepoPermsForm(localizer):
286 def RepoPermsForm(localizer):
287 _ = localizer
287 _ = localizer
288
288
289 class _RepoPermsForm(formencode.Schema):
289 class _RepoPermsForm(formencode.Schema):
290 allow_extra_fields = True
290 allow_extra_fields = True
291 filter_extra_fields = False
291 filter_extra_fields = False
292 chained_validators = [v.ValidPerms(localizer, type_='repo')]
292 chained_validators = [v.ValidPerms(localizer, type_='repo')]
293 return _RepoPermsForm
293 return _RepoPermsForm
294
294
295
295
296 def RepoGroupPermsForm(localizer, valid_recursive_choices):
296 def RepoGroupPermsForm(localizer, valid_recursive_choices):
297 _ = localizer
297 _ = localizer
298
298
299 class _RepoGroupPermsForm(formencode.Schema):
299 class _RepoGroupPermsForm(formencode.Schema):
300 allow_extra_fields = True
300 allow_extra_fields = True
301 filter_extra_fields = False
301 filter_extra_fields = False
302 recursive = v.OneOf(valid_recursive_choices)
302 recursive = v.OneOf(valid_recursive_choices)
303 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
303 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
304 return _RepoGroupPermsForm
304 return _RepoGroupPermsForm
305
305
306
306
307 def UserGroupPermsForm(localizer):
307 def UserGroupPermsForm(localizer):
308 _ = localizer
308 _ = localizer
309
309
310 class _UserPermsForm(formencode.Schema):
310 class _UserPermsForm(formencode.Schema):
311 allow_extra_fields = True
311 allow_extra_fields = True
312 filter_extra_fields = False
312 filter_extra_fields = False
313 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
313 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
314 return _UserPermsForm
314 return _UserPermsForm
315
315
316
316
317 def RepoFieldForm(localizer):
317 def RepoFieldForm(localizer):
318 _ = localizer
318 _ = localizer
319
319
320 class _RepoFieldForm(formencode.Schema):
320 class _RepoFieldForm(formencode.Schema):
321 filter_extra_fields = True
321 filter_extra_fields = True
322 allow_extra_fields = True
322 allow_extra_fields = True
323
323
324 new_field_key = All(v.FieldKey(localizer),
324 new_field_key = All(v.FieldKey(localizer),
325 v.UnicodeString(strip=True, min=3, not_empty=True))
325 v.UnicodeString(strip=True, min=3, not_empty=True))
326 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
326 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
327 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
327 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
328 if_missing='str')
328 if_missing='str')
329 new_field_label = v.UnicodeString(not_empty=False)
329 new_field_label = v.UnicodeString(not_empty=False)
330 new_field_desc = v.UnicodeString(not_empty=False)
330 new_field_desc = v.UnicodeString(not_empty=False)
331 return _RepoFieldForm
331 return _RepoFieldForm
332
332
333
333
334 def RepoForkForm(localizer, edit=False, old_data=None,
334 def RepoForkForm(localizer, edit=False, old_data=None,
335 supported_backends=BACKENDS.keys(), repo_groups=None):
335 supported_backends=BACKENDS.keys(), repo_groups=None):
336 _ = localizer
336 _ = localizer
337 old_data = old_data or {}
337 old_data = old_data or {}
338 repo_groups = repo_groups or []
338 repo_groups = repo_groups or []
339
339
340 class _RepoForkForm(formencode.Schema):
340 class _RepoForkForm(formencode.Schema):
341 allow_extra_fields = True
341 allow_extra_fields = True
342 filter_extra_fields = False
342 filter_extra_fields = False
343 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
343 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
344 v.SlugifyName(localizer))
344 v.SlugifyName(localizer))
345 repo_group = All(v.CanWriteGroup(localizer, ),
345 repo_group = All(v.CanWriteGroup(localizer, ),
346 v.OneOf(repo_groups, hideList=True))
346 v.OneOf(repo_groups, hideList=True))
347 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
347 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
348 description = v.UnicodeString(strip=True, min=1, not_empty=True)
348 description = v.UnicodeString(strip=True, min=1, not_empty=True)
349 private = v.StringBoolean(if_missing=False)
349 private = v.StringBoolean(if_missing=False)
350 copy_permissions = v.StringBoolean(if_missing=False)
350 copy_permissions = v.StringBoolean(if_missing=False)
351 fork_parent_id = v.UnicodeString()
351 fork_parent_id = v.UnicodeString()
352 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
352 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
353 return _RepoForkForm
353 return _RepoForkForm
354
354
355
355
356 def ApplicationSettingsForm(localizer):
356 def ApplicationSettingsForm(localizer):
357 _ = localizer
357 _ = localizer
358
358
359 class _ApplicationSettingsForm(formencode.Schema):
359 class _ApplicationSettingsForm(formencode.Schema):
360 allow_extra_fields = True
360 allow_extra_fields = True
361 filter_extra_fields = False
361 filter_extra_fields = False
362 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
362 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
363 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
363 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
364 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
364 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
365 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
365 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
366 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
366 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
367 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
367 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
368 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
368 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
369 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
369 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
370 return _ApplicationSettingsForm
370 return _ApplicationSettingsForm
371
371
372
372
373 def ApplicationVisualisationForm(localizer):
373 def ApplicationVisualisationForm(localizer):
374 from rhodecode.model.db import Repository
374 from rhodecode.model.db import Repository
375 _ = localizer
375 _ = localizer
376
376
377 class _ApplicationVisualisationForm(formencode.Schema):
377 class _ApplicationVisualisationForm(formencode.Schema):
378 allow_extra_fields = True
378 allow_extra_fields = True
379 filter_extra_fields = False
379 filter_extra_fields = False
380 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
380 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
381 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
381 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
382 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
382 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
383
383
384 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
384 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
385 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
385 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
386 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
386 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
387 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
387 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
388 rhodecode_show_version = v.StringBoolean(if_missing=False)
388 rhodecode_show_version = v.StringBoolean(if_missing=False)
389 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
389 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
390 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
390 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
391 rhodecode_gravatar_url = v.UnicodeString(min=3)
391 rhodecode_gravatar_url = v.UnicodeString(min=3)
392 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
392 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
393 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
393 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
394 rhodecode_support_url = v.UnicodeString()
394 rhodecode_support_url = v.UnicodeString()
395 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
395 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
396 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
396 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
397 return _ApplicationVisualisationForm
397 return _ApplicationVisualisationForm
398
398
399
399
400 class _BaseVcsSettingsForm(formencode.Schema):
400 class _BaseVcsSettingsForm(formencode.Schema):
401
401
402 allow_extra_fields = True
402 allow_extra_fields = True
403 filter_extra_fields = False
403 filter_extra_fields = False
404 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
404 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
405 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
405 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
406 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
406 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
407
407
408 # PR/Code-review
408 # PR/Code-review
409 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
409 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
410 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
410 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
411
411
412 # hg
412 # hg
413 extensions_largefiles = v.StringBoolean(if_missing=False)
413 extensions_largefiles = v.StringBoolean(if_missing=False)
414 extensions_evolve = v.StringBoolean(if_missing=False)
414 extensions_evolve = v.StringBoolean(if_missing=False)
415 phases_publish = v.StringBoolean(if_missing=False)
415 phases_publish = v.StringBoolean(if_missing=False)
416
416
417 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
417 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
418 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
418 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
419
419
420 # git
420 # git
421 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
421 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
422 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
422 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
423 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
423 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
424
424
425 # svn
425 # svn
426 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
426 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
427 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
427 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
428
428
429 # cache
429 # cache
430 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
430 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
431
431
432
432
433 def ApplicationUiSettingsForm(localizer):
433 def ApplicationUiSettingsForm(localizer):
434 _ = localizer
434 _ = localizer
435
435
436 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
436 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
437 web_push_ssl = v.StringBoolean(if_missing=False)
437 web_push_ssl = v.StringBoolean(if_missing=False)
438 paths_root_path = All(
438 paths_root_path = All(
439 v.ValidPath(localizer),
439 v.ValidPath(localizer),
440 v.UnicodeString(strip=True, min=1, not_empty=True)
440 v.UnicodeString(strip=True, min=1, not_empty=True)
441 )
441 )
442 largefiles_usercache = All(
442 largefiles_usercache = All(
443 v.ValidPath(localizer),
443 v.ValidPath(localizer),
444 v.UnicodeString(strip=True, min=2, not_empty=True))
444 v.UnicodeString(strip=True, min=2, not_empty=True))
445 vcs_git_lfs_store_location = All(
445 vcs_git_lfs_store_location = All(
446 v.ValidPath(localizer),
446 v.ValidPath(localizer),
447 v.UnicodeString(strip=True, min=2, not_empty=True))
447 v.UnicodeString(strip=True, min=2, not_empty=True))
448 extensions_hgsubversion = v.StringBoolean(if_missing=False)
448 extensions_hgsubversion = v.StringBoolean(if_missing=False)
449 extensions_hggit = v.StringBoolean(if_missing=False)
449 extensions_hggit = v.StringBoolean(if_missing=False)
450 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
450 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
451 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
451 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
452 return _ApplicationUiSettingsForm
452 return _ApplicationUiSettingsForm
453
453
454
454
455 def RepoVcsSettingsForm(localizer, repo_name):
455 def RepoVcsSettingsForm(localizer, repo_name):
456 _ = localizer
456 _ = localizer
457
457
458 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
458 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
459 inherit_global_settings = v.StringBoolean(if_missing=False)
459 inherit_global_settings = v.StringBoolean(if_missing=False)
460 new_svn_branch = v.ValidSvnPattern(localizer,
460 new_svn_branch = v.ValidSvnPattern(localizer,
461 section='vcs_svn_branch', repo_name=repo_name)
461 section='vcs_svn_branch', repo_name=repo_name)
462 new_svn_tag = v.ValidSvnPattern(localizer,
462 new_svn_tag = v.ValidSvnPattern(localizer,
463 section='vcs_svn_tag', repo_name=repo_name)
463 section='vcs_svn_tag', repo_name=repo_name)
464 return _RepoVcsSettingsForm
464 return _RepoVcsSettingsForm
465
465
466
466
467 def LabsSettingsForm(localizer):
467 def LabsSettingsForm(localizer):
468 _ = localizer
468 _ = localizer
469
469
470 class _LabSettingsForm(formencode.Schema):
470 class _LabSettingsForm(formencode.Schema):
471 allow_extra_fields = True
471 allow_extra_fields = True
472 filter_extra_fields = False
472 filter_extra_fields = False
473 return _LabSettingsForm
473 return _LabSettingsForm
474
474
475
475
476 def ApplicationPermissionsForm(
476 def ApplicationPermissionsForm(
477 localizer, register_choices, password_reset_choices,
477 localizer, register_choices, password_reset_choices,
478 extern_activate_choices):
478 extern_activate_choices):
479 _ = localizer
479 _ = localizer
480
480
481 class _DefaultPermissionsForm(formencode.Schema):
481 class _DefaultPermissionsForm(formencode.Schema):
482 allow_extra_fields = True
482 allow_extra_fields = True
483 filter_extra_fields = True
483 filter_extra_fields = True
484
484
485 anonymous = v.StringBoolean(if_missing=False)
485 anonymous = v.StringBoolean(if_missing=False)
486 default_register = v.OneOf(register_choices)
486 default_register = v.OneOf(register_choices)
487 default_register_message = v.UnicodeString()
487 default_register_message = v.UnicodeString()
488 default_password_reset = v.OneOf(password_reset_choices)
488 default_password_reset = v.OneOf(password_reset_choices)
489 default_extern_activate = v.OneOf(extern_activate_choices)
489 default_extern_activate = v.OneOf(extern_activate_choices)
490 return _DefaultPermissionsForm
490 return _DefaultPermissionsForm
491
491
492
492
493 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
493 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
494 user_group_perms_choices):
494 user_group_perms_choices):
495 _ = localizer
495 _ = localizer
496
496
497 class _ObjectPermissionsForm(formencode.Schema):
497 class _ObjectPermissionsForm(formencode.Schema):
498 allow_extra_fields = True
498 allow_extra_fields = True
499 filter_extra_fields = True
499 filter_extra_fields = True
500 overwrite_default_repo = v.StringBoolean(if_missing=False)
500 overwrite_default_repo = v.StringBoolean(if_missing=False)
501 overwrite_default_group = v.StringBoolean(if_missing=False)
501 overwrite_default_group = v.StringBoolean(if_missing=False)
502 overwrite_default_user_group = v.StringBoolean(if_missing=False)
502 overwrite_default_user_group = v.StringBoolean(if_missing=False)
503
503
504 default_repo_perm = v.OneOf(repo_perms_choices)
504 default_repo_perm = v.OneOf(repo_perms_choices)
505 default_group_perm = v.OneOf(group_perms_choices)
505 default_group_perm = v.OneOf(group_perms_choices)
506 default_user_group_perm = v.OneOf(user_group_perms_choices)
506 default_user_group_perm = v.OneOf(user_group_perms_choices)
507
507
508 return _ObjectPermissionsForm
508 return _ObjectPermissionsForm
509
509
510
510
511 def BranchPermissionsForm(localizer, branch_perms_choices):
511 def BranchPermissionsForm(localizer, branch_perms_choices):
512 _ = localizer
512 _ = localizer
513
513
514 class _BranchPermissionsForm(formencode.Schema):
514 class _BranchPermissionsForm(formencode.Schema):
515 allow_extra_fields = True
515 allow_extra_fields = True
516 filter_extra_fields = True
516 filter_extra_fields = True
517 overwrite_default_branch = v.StringBoolean(if_missing=False)
517 overwrite_default_branch = v.StringBoolean(if_missing=False)
518 default_branch_perm = v.OneOf(branch_perms_choices)
518 default_branch_perm = v.OneOf(branch_perms_choices)
519
519
520 return _BranchPermissionsForm
520 return _BranchPermissionsForm
521
521
522
522
523 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
523 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
524 repo_group_create_choices, user_group_create_choices,
524 repo_group_create_choices, user_group_create_choices,
525 fork_choices, inherit_default_permissions_choices):
525 fork_choices, inherit_default_permissions_choices):
526 _ = localizer
526 _ = localizer
527
527
528 class _DefaultPermissionsForm(formencode.Schema):
528 class _DefaultPermissionsForm(formencode.Schema):
529 allow_extra_fields = True
529 allow_extra_fields = True
530 filter_extra_fields = True
530 filter_extra_fields = True
531
531
532 anonymous = v.StringBoolean(if_missing=False)
532 anonymous = v.StringBoolean(if_missing=False)
533
533
534 default_repo_create = v.OneOf(create_choices)
534 default_repo_create = v.OneOf(create_choices)
535 default_repo_create_on_write = v.OneOf(create_on_write_choices)
535 default_repo_create_on_write = v.OneOf(create_on_write_choices)
536 default_user_group_create = v.OneOf(user_group_create_choices)
536 default_user_group_create = v.OneOf(user_group_create_choices)
537 default_repo_group_create = v.OneOf(repo_group_create_choices)
537 default_repo_group_create = v.OneOf(repo_group_create_choices)
538 default_fork_create = v.OneOf(fork_choices)
538 default_fork_create = v.OneOf(fork_choices)
539 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
539 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
540 return _DefaultPermissionsForm
540 return _DefaultPermissionsForm
541
541
542
542
543 def UserIndividualPermissionsForm(localizer):
543 def UserIndividualPermissionsForm(localizer):
544 _ = localizer
544 _ = localizer
545
545
546 class _DefaultPermissionsForm(formencode.Schema):
546 class _DefaultPermissionsForm(formencode.Schema):
547 allow_extra_fields = True
547 allow_extra_fields = True
548 filter_extra_fields = True
548 filter_extra_fields = True
549
549
550 inherit_default_permissions = v.StringBoolean(if_missing=False)
550 inherit_default_permissions = v.StringBoolean(if_missing=False)
551 return _DefaultPermissionsForm
551 return _DefaultPermissionsForm
552
552
553
553
554 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
554 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
555 _ = localizer
555 _ = localizer
556 old_data = old_data or {}
556 old_data = old_data or {}
557
557
558 class _DefaultsForm(formencode.Schema):
558 class _DefaultsForm(formencode.Schema):
559 allow_extra_fields = True
559 allow_extra_fields = True
560 filter_extra_fields = True
560 filter_extra_fields = True
561 default_repo_type = v.OneOf(supported_backends)
561 default_repo_type = v.OneOf(supported_backends)
562 default_repo_private = v.StringBoolean(if_missing=False)
562 default_repo_private = v.StringBoolean(if_missing=False)
563 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
563 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
564 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
564 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
565 default_repo_enable_locking = v.StringBoolean(if_missing=False)
565 default_repo_enable_locking = v.StringBoolean(if_missing=False)
566 return _DefaultsForm
566 return _DefaultsForm
567
567
568
568
569 def AuthSettingsForm(localizer):
569 def AuthSettingsForm(localizer):
570 _ = localizer
570 _ = localizer
571
571
572 class _AuthSettingsForm(formencode.Schema):
572 class _AuthSettingsForm(formencode.Schema):
573 allow_extra_fields = True
573 allow_extra_fields = True
574 filter_extra_fields = True
574 filter_extra_fields = True
575 auth_plugins = All(v.ValidAuthPlugins(localizer),
575 auth_plugins = All(v.ValidAuthPlugins(localizer),
576 v.UniqueListFromString(localizer)(not_empty=True))
576 v.UniqueListFromString(localizer)(not_empty=True))
577 return _AuthSettingsForm
577 return _AuthSettingsForm
578
578
579
579
580 def UserExtraEmailForm(localizer):
580 def UserExtraEmailForm(localizer):
581 _ = localizer
581 _ = localizer
582
582
583 class _UserExtraEmailForm(formencode.Schema):
583 class _UserExtraEmailForm(formencode.Schema):
584 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
584 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
585 return _UserExtraEmailForm
585 return _UserExtraEmailForm
586
586
587
587
588 def UserExtraIpForm(localizer):
588 def UserExtraIpForm(localizer):
589 _ = localizer
589 _ = localizer
590
590
591 class _UserExtraIpForm(formencode.Schema):
591 class _UserExtraIpForm(formencode.Schema):
592 ip = v.ValidIp(localizer)(not_empty=True)
592 ip = v.ValidIp(localizer)(not_empty=True)
593 return _UserExtraIpForm
593 return _UserExtraIpForm
594
594
595
595
596 def PullRequestForm(localizer, repo_id):
596 def PullRequestForm(localizer, repo_id):
597 _ = localizer
597 _ = localizer
598
598
599 class ReviewerForm(formencode.Schema):
599 class ReviewerForm(formencode.Schema):
600 user_id = v.Int(not_empty=True)
600 user_id = v.Int(not_empty=True)
601 reasons = All()
601 reasons = All()
602 rules = All(v.UniqueList(localizer, convert=int)())
602 rules = All(v.UniqueList(localizer, convert=int)())
603 mandatory = v.StringBoolean()
603 mandatory = v.StringBoolean()
604 role = v.String(if_missing='reviewer')
605
606 class ObserverForm(formencode.Schema):
607 user_id = v.Int(not_empty=True)
608 reasons = All()
609 rules = All(v.UniqueList(localizer, convert=int)())
610 mandatory = v.StringBoolean()
611 role = v.String(if_missing='observer')
604
612
605 class _PullRequestForm(formencode.Schema):
613 class _PullRequestForm(formencode.Schema):
606 allow_extra_fields = True
614 allow_extra_fields = True
607 filter_extra_fields = True
615 filter_extra_fields = True
608
616
609 common_ancestor = v.UnicodeString(strip=True, required=True)
617 common_ancestor = v.UnicodeString(strip=True, required=True)
610 source_repo = v.UnicodeString(strip=True, required=True)
618 source_repo = v.UnicodeString(strip=True, required=True)
611 source_ref = v.UnicodeString(strip=True, required=True)
619 source_ref = v.UnicodeString(strip=True, required=True)
612 target_repo = v.UnicodeString(strip=True, required=True)
620 target_repo = v.UnicodeString(strip=True, required=True)
613 target_ref = v.UnicodeString(strip=True, required=True)
621 target_ref = v.UnicodeString(strip=True, required=True)
614 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
622 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
615 v.UniqueList(localizer)(not_empty=True))
623 v.UniqueList(localizer)(not_empty=True))
616 review_members = formencode.ForEach(ReviewerForm())
624 review_members = formencode.ForEach(ReviewerForm())
625 observer_members = formencode.ForEach(ObserverForm())
617 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
626 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
618 pullrequest_desc = v.UnicodeString(strip=True, required=False)
627 pullrequest_desc = v.UnicodeString(strip=True, required=False)
619 description_renderer = v.UnicodeString(strip=True, required=False)
628 description_renderer = v.UnicodeString(strip=True, required=False)
620
629
621 return _PullRequestForm
630 return _PullRequestForm
622
631
623
632
624 def IssueTrackerPatternsForm(localizer):
633 def IssueTrackerPatternsForm(localizer):
625 _ = localizer
634 _ = localizer
626
635
627 class _IssueTrackerPatternsForm(formencode.Schema):
636 class _IssueTrackerPatternsForm(formencode.Schema):
628 allow_extra_fields = True
637 allow_extra_fields = True
629 filter_extra_fields = False
638 filter_extra_fields = False
630 chained_validators = [v.ValidPattern(localizer)]
639 chained_validators = [v.ValidPattern(localizer)]
631 return _IssueTrackerPatternsForm
640 return _IssueTrackerPatternsForm
@@ -1,2072 +1,2201 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30
30
31 import datetime
31 import datetime
32 import urllib
32 import urllib
33 import collections
33 import collections
34
34
35 from pyramid import compat
35 from pyramid import compat
36 from pyramid.threadlocal import get_current_request
36 from pyramid.threadlocal import get_current_request
37
37
38 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.lib.vcs.nodes import FileNode
39 from rhodecode.translation import lazy_ugettext
39 from rhodecode.translation import lazy_ugettext
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
41 from rhodecode.lib import audit_logger
41 from rhodecode.lib import audit_logger
42 from rhodecode.lib.compat import OrderedDict
42 from rhodecode.lib.compat import OrderedDict
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.markup_renderer import (
44 from rhodecode.lib.markup_renderer import (
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
46 from rhodecode.lib.utils2 import (
46 from rhodecode.lib.utils2 import (
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
48 get_current_rhodecode_user)
48 get_current_rhodecode_user)
49 from rhodecode.lib.vcs.backends.base import (
49 from rhodecode.lib.vcs.backends.base import (
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
51 TargetRefMissing, SourceRefMissing)
51 TargetRefMissing, SourceRefMissing)
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 CommitDoesNotExistError, EmptyRepositoryError)
54 CommitDoesNotExistError, EmptyRepositoryError)
55 from rhodecode.model import BaseModel
55 from rhodecode.model import BaseModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.comment import CommentsModel
57 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.db import (
58 from rhodecode.model.db import (
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62 from rhodecode.model.notification import NotificationModel, \
62 from rhodecode.model.notification import NotificationModel, \
63 EmailNotificationModel
63 EmailNotificationModel
64 from rhodecode.model.scm import ScmModel
64 from rhodecode.model.scm import ScmModel
65 from rhodecode.model.settings import VcsSettingsModel
65 from rhodecode.model.settings import VcsSettingsModel
66
66
67
67
68 log = logging.getLogger(__name__)
68 log = logging.getLogger(__name__)
69
69
70
70
71 # Data structure to hold the response data when updating commits during a pull
71 # Data structure to hold the response data when updating commits during a pull
72 # request update.
72 # request update.
73 class UpdateResponse(object):
73 class UpdateResponse(object):
74
74
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
76 commit_changes, source_changed, target_changed):
76 commit_changes, source_changed, target_changed):
77
77
78 self.executed = executed
78 self.executed = executed
79 self.reason = reason
79 self.reason = reason
80 self.new = new
80 self.new = new
81 self.old = old
81 self.old = old
82 self.common_ancestor_id = common_ancestor_id
82 self.common_ancestor_id = common_ancestor_id
83 self.changes = commit_changes
83 self.changes = commit_changes
84 self.source_changed = source_changed
84 self.source_changed = source_changed
85 self.target_changed = target_changed
85 self.target_changed = target_changed
86
86
87
87
88 def get_diff_info(
88 def get_diff_info(
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
90 get_commit_authors=True):
90 get_commit_authors=True):
91 """
91 """
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
93 This is also used for default reviewers logic
93 This is also used for default reviewers logic
94 """
94 """
95
95
96 source_scm = source_repo.scm_instance()
96 source_scm = source_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
98
98
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
100 if not ancestor_id:
100 if not ancestor_id:
101 raise ValueError(
101 raise ValueError(
102 'cannot calculate diff info without a common ancestor. '
102 'cannot calculate diff info without a common ancestor. '
103 'Make sure both repositories are related, and have a common forking commit.')
103 'Make sure both repositories are related, and have a common forking commit.')
104
104
105 # case here is that want a simple diff without incoming commits,
105 # case here is that want a simple diff without incoming commits,
106 # previewing what will be merged based only on commits in the source.
106 # previewing what will be merged based only on commits in the source.
107 log.debug('Using ancestor %s as source_ref instead of %s',
107 log.debug('Using ancestor %s as source_ref instead of %s',
108 ancestor_id, source_ref)
108 ancestor_id, source_ref)
109
109
110 # source of changes now is the common ancestor
110 # source of changes now is the common ancestor
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
112 # target commit becomes the source ref as it is the last commit
112 # target commit becomes the source ref as it is the last commit
113 # for diff generation this logic gives proper diff
113 # for diff generation this logic gives proper diff
114 target_commit = source_scm.get_commit(commit_id=source_ref)
114 target_commit = source_scm.get_commit(commit_id=source_ref)
115
115
116 vcs_diff = \
116 vcs_diff = \
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
118 ignore_whitespace=False, context=3)
118 ignore_whitespace=False, context=3)
119
119
120 diff_processor = diffs.DiffProcessor(
120 diff_processor = diffs.DiffProcessor(
121 vcs_diff, format='newdiff', diff_limit=None,
121 vcs_diff, format='newdiff', diff_limit=None,
122 file_limit=None, show_full_diff=True)
122 file_limit=None, show_full_diff=True)
123
123
124 _parsed = diff_processor.prepare()
124 _parsed = diff_processor.prepare()
125
125
126 all_files = []
126 all_files = []
127 all_files_changes = []
127 all_files_changes = []
128 changed_lines = {}
128 changed_lines = {}
129 stats = [0, 0]
129 stats = [0, 0]
130 for f in _parsed:
130 for f in _parsed:
131 all_files.append(f['filename'])
131 all_files.append(f['filename'])
132 all_files_changes.append({
132 all_files_changes.append({
133 'filename': f['filename'],
133 'filename': f['filename'],
134 'stats': f['stats']
134 'stats': f['stats']
135 })
135 })
136 stats[0] += f['stats']['added']
136 stats[0] += f['stats']['added']
137 stats[1] += f['stats']['deleted']
137 stats[1] += f['stats']['deleted']
138
138
139 changed_lines[f['filename']] = []
139 changed_lines[f['filename']] = []
140 if len(f['chunks']) < 2:
140 if len(f['chunks']) < 2:
141 continue
141 continue
142 # first line is "context" information
142 # first line is "context" information
143 for chunks in f['chunks'][1:]:
143 for chunks in f['chunks'][1:]:
144 for chunk in chunks['lines']:
144 for chunk in chunks['lines']:
145 if chunk['action'] not in ('del', 'mod'):
145 if chunk['action'] not in ('del', 'mod'):
146 continue
146 continue
147 changed_lines[f['filename']].append(chunk['old_lineno'])
147 changed_lines[f['filename']].append(chunk['old_lineno'])
148
148
149 commit_authors = []
149 commit_authors = []
150 user_counts = {}
150 user_counts = {}
151 email_counts = {}
151 email_counts = {}
152 author_counts = {}
152 author_counts = {}
153 _commit_cache = {}
153 _commit_cache = {}
154
154
155 commits = []
155 commits = []
156 if get_commit_authors:
156 if get_commit_authors:
157 commits = target_scm.compare(
157 commits = target_scm.compare(
158 target_ref, source_ref, source_scm, merge=True,
158 target_ref, source_ref, source_scm, merge=True,
159 pre_load=["author"])
159 pre_load=["author"])
160
160
161 for commit in commits:
161 for commit in commits:
162 user = User.get_from_cs_author(commit.author)
162 user = User.get_from_cs_author(commit.author)
163 if user and user not in commit_authors:
163 if user and user not in commit_authors:
164 commit_authors.append(user)
164 commit_authors.append(user)
165
165
166 # lines
166 # lines
167 if get_authors:
167 if get_authors:
168 target_commit = source_repo.get_commit(ancestor_id)
168 target_commit = source_repo.get_commit(ancestor_id)
169
169
170 for fname, lines in changed_lines.items():
170 for fname, lines in changed_lines.items():
171 try:
171 try:
172 node = target_commit.get_node(fname)
172 node = target_commit.get_node(fname)
173 except Exception:
173 except Exception:
174 continue
174 continue
175
175
176 if not isinstance(node, FileNode):
176 if not isinstance(node, FileNode):
177 continue
177 continue
178
178
179 for annotation in node.annotate:
179 for annotation in node.annotate:
180 line_no, commit_id, get_commit_func, line_text = annotation
180 line_no, commit_id, get_commit_func, line_text = annotation
181 if line_no in lines:
181 if line_no in lines:
182 if commit_id not in _commit_cache:
182 if commit_id not in _commit_cache:
183 _commit_cache[commit_id] = get_commit_func()
183 _commit_cache[commit_id] = get_commit_func()
184 commit = _commit_cache[commit_id]
184 commit = _commit_cache[commit_id]
185 author = commit.author
185 author = commit.author
186 email = commit.author_email
186 email = commit.author_email
187 user = User.get_from_cs_author(author)
187 user = User.get_from_cs_author(author)
188 if user:
188 if user:
189 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
189 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
190 author_counts[author] = author_counts.get(author, 0) + 1
190 author_counts[author] = author_counts.get(author, 0) + 1
191 email_counts[email] = email_counts.get(email, 0) + 1
191 email_counts[email] = email_counts.get(email, 0) + 1
192
192
193 return {
193 return {
194 'commits': commits,
194 'commits': commits,
195 'files': all_files_changes,
195 'files': all_files_changes,
196 'stats': stats,
196 'stats': stats,
197 'ancestor': ancestor_id,
197 'ancestor': ancestor_id,
198 # original authors of modified files
198 # original authors of modified files
199 'original_authors': {
199 'original_authors': {
200 'users': user_counts,
200 'users': user_counts,
201 'authors': author_counts,
201 'authors': author_counts,
202 'emails': email_counts,
202 'emails': email_counts,
203 },
203 },
204 'commit_authors': commit_authors
204 'commit_authors': commit_authors
205 }
205 }
206
206
207
207
208 class PullRequestModel(BaseModel):
208 class PullRequestModel(BaseModel):
209
209
210 cls = PullRequest
210 cls = PullRequest
211
211
212 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
212 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
213
213
214 UPDATE_STATUS_MESSAGES = {
214 UPDATE_STATUS_MESSAGES = {
215 UpdateFailureReason.NONE: lazy_ugettext(
215 UpdateFailureReason.NONE: lazy_ugettext(
216 'Pull request update successful.'),
216 'Pull request update successful.'),
217 UpdateFailureReason.UNKNOWN: lazy_ugettext(
217 UpdateFailureReason.UNKNOWN: lazy_ugettext(
218 'Pull request update failed because of an unknown error.'),
218 'Pull request update failed because of an unknown error.'),
219 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
219 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
220 'No update needed because the source and target have not changed.'),
220 'No update needed because the source and target have not changed.'),
221 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
221 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
222 'Pull request cannot be updated because the reference type is '
222 'Pull request cannot be updated because the reference type is '
223 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
223 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
224 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
224 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
225 'This pull request cannot be updated because the target '
225 'This pull request cannot be updated because the target '
226 'reference is missing.'),
226 'reference is missing.'),
227 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
227 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
228 'This pull request cannot be updated because the source '
228 'This pull request cannot be updated because the source '
229 'reference is missing.'),
229 'reference is missing.'),
230 }
230 }
231 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
231 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
232 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
232 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
233
233
234 def __get_pull_request(self, pull_request):
234 def __get_pull_request(self, pull_request):
235 return self._get_instance((
235 return self._get_instance((
236 PullRequest, PullRequestVersion), pull_request)
236 PullRequest, PullRequestVersion), pull_request)
237
237
238 def _check_perms(self, perms, pull_request, user, api=False):
238 def _check_perms(self, perms, pull_request, user, api=False):
239 if not api:
239 if not api:
240 return h.HasRepoPermissionAny(*perms)(
240 return h.HasRepoPermissionAny(*perms)(
241 user=user, repo_name=pull_request.target_repo.repo_name)
241 user=user, repo_name=pull_request.target_repo.repo_name)
242 else:
242 else:
243 return h.HasRepoPermissionAnyApi(*perms)(
243 return h.HasRepoPermissionAnyApi(*perms)(
244 user=user, repo_name=pull_request.target_repo.repo_name)
244 user=user, repo_name=pull_request.target_repo.repo_name)
245
245
246 def check_user_read(self, pull_request, user, api=False):
246 def check_user_read(self, pull_request, user, api=False):
247 _perms = ('repository.admin', 'repository.write', 'repository.read',)
247 _perms = ('repository.admin', 'repository.write', 'repository.read',)
248 return self._check_perms(_perms, pull_request, user, api)
248 return self._check_perms(_perms, pull_request, user, api)
249
249
250 def check_user_merge(self, pull_request, user, api=False):
250 def check_user_merge(self, pull_request, user, api=False):
251 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
251 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
252 return self._check_perms(_perms, pull_request, user, api)
252 return self._check_perms(_perms, pull_request, user, api)
253
253
254 def check_user_update(self, pull_request, user, api=False):
254 def check_user_update(self, pull_request, user, api=False):
255 owner = user.user_id == pull_request.user_id
255 owner = user.user_id == pull_request.user_id
256 return self.check_user_merge(pull_request, user, api) or owner
256 return self.check_user_merge(pull_request, user, api) or owner
257
257
258 def check_user_delete(self, pull_request, user):
258 def check_user_delete(self, pull_request, user):
259 owner = user.user_id == pull_request.user_id
259 owner = user.user_id == pull_request.user_id
260 _perms = ('repository.admin',)
260 _perms = ('repository.admin',)
261 return self._check_perms(_perms, pull_request, user) or owner
261 return self._check_perms(_perms, pull_request, user) or owner
262
262
263 def check_user_change_status(self, pull_request, user, api=False):
263 def check_user_change_status(self, pull_request, user, api=False):
264 reviewer = user.user_id in [x.user_id for x in
264 reviewer = user.user_id in [x.user_id for x in
265 pull_request.reviewers]
265 pull_request.reviewers]
266 return self.check_user_update(pull_request, user, api) or reviewer
266 return self.check_user_update(pull_request, user, api) or reviewer
267
267
268 def check_user_comment(self, pull_request, user):
268 def check_user_comment(self, pull_request, user):
269 owner = user.user_id == pull_request.user_id
269 owner = user.user_id == pull_request.user_id
270 return self.check_user_read(pull_request, user) or owner
270 return self.check_user_read(pull_request, user) or owner
271
271
272 def get(self, pull_request):
272 def get(self, pull_request):
273 return self.__get_pull_request(pull_request)
273 return self.__get_pull_request(pull_request)
274
274
275 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
275 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
276 statuses=None, opened_by=None, order_by=None,
276 statuses=None, opened_by=None, order_by=None,
277 order_dir='desc', only_created=False):
277 order_dir='desc', only_created=False):
278 repo = None
278 repo = None
279 if repo_name:
279 if repo_name:
280 repo = self._get_repo(repo_name)
280 repo = self._get_repo(repo_name)
281
281
282 q = PullRequest.query()
282 q = PullRequest.query()
283
283
284 if search_q:
284 if search_q:
285 like_expression = u'%{}%'.format(safe_unicode(search_q))
285 like_expression = u'%{}%'.format(safe_unicode(search_q))
286 q = q.join(User)
286 q = q.join(User)
287 q = q.filter(or_(
287 q = q.filter(or_(
288 cast(PullRequest.pull_request_id, String).ilike(like_expression),
288 cast(PullRequest.pull_request_id, String).ilike(like_expression),
289 User.username.ilike(like_expression),
289 User.username.ilike(like_expression),
290 PullRequest.title.ilike(like_expression),
290 PullRequest.title.ilike(like_expression),
291 PullRequest.description.ilike(like_expression),
291 PullRequest.description.ilike(like_expression),
292 ))
292 ))
293
293
294 # source or target
294 # source or target
295 if repo and source:
295 if repo and source:
296 q = q.filter(PullRequest.source_repo == repo)
296 q = q.filter(PullRequest.source_repo == repo)
297 elif repo:
297 elif repo:
298 q = q.filter(PullRequest.target_repo == repo)
298 q = q.filter(PullRequest.target_repo == repo)
299
299
300 # closed,opened
300 # closed,opened
301 if statuses:
301 if statuses:
302 q = q.filter(PullRequest.status.in_(statuses))
302 q = q.filter(PullRequest.status.in_(statuses))
303
303
304 # opened by filter
304 # opened by filter
305 if opened_by:
305 if opened_by:
306 q = q.filter(PullRequest.user_id.in_(opened_by))
306 q = q.filter(PullRequest.user_id.in_(opened_by))
307
307
308 # only get those that are in "created" state
308 # only get those that are in "created" state
309 if only_created:
309 if only_created:
310 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
310 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
311
311
312 if order_by:
312 if order_by:
313 order_map = {
313 order_map = {
314 'name_raw': PullRequest.pull_request_id,
314 'name_raw': PullRequest.pull_request_id,
315 'id': PullRequest.pull_request_id,
315 'id': PullRequest.pull_request_id,
316 'title': PullRequest.title,
316 'title': PullRequest.title,
317 'updated_on_raw': PullRequest.updated_on,
317 'updated_on_raw': PullRequest.updated_on,
318 'target_repo': PullRequest.target_repo_id
318 'target_repo': PullRequest.target_repo_id
319 }
319 }
320 if order_dir == 'asc':
320 if order_dir == 'asc':
321 q = q.order_by(order_map[order_by].asc())
321 q = q.order_by(order_map[order_by].asc())
322 else:
322 else:
323 q = q.order_by(order_map[order_by].desc())
323 q = q.order_by(order_map[order_by].desc())
324
324
325 return q
325 return q
326
326
327 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
327 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
328 opened_by=None):
328 opened_by=None):
329 """
329 """
330 Count the number of pull requests for a specific repository.
330 Count the number of pull requests for a specific repository.
331
331
332 :param repo_name: target or source repo
332 :param repo_name: target or source repo
333 :param search_q: filter by text
333 :param search_q: filter by text
334 :param source: boolean flag to specify if repo_name refers to source
334 :param source: boolean flag to specify if repo_name refers to source
335 :param statuses: list of pull request statuses
335 :param statuses: list of pull request statuses
336 :param opened_by: author user of the pull request
336 :param opened_by: author user of the pull request
337 :returns: int number of pull requests
337 :returns: int number of pull requests
338 """
338 """
339 q = self._prepare_get_all_query(
339 q = self._prepare_get_all_query(
340 repo_name, search_q=search_q, source=source, statuses=statuses,
340 repo_name, search_q=search_q, source=source, statuses=statuses,
341 opened_by=opened_by)
341 opened_by=opened_by)
342
342
343 return q.count()
343 return q.count()
344
344
345 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
345 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
346 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
346 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
347 """
347 """
348 Get all pull requests for a specific repository.
348 Get all pull requests for a specific repository.
349
349
350 :param repo_name: target or source repo
350 :param repo_name: target or source repo
351 :param search_q: filter by text
351 :param search_q: filter by text
352 :param source: boolean flag to specify if repo_name refers to source
352 :param source: boolean flag to specify if repo_name refers to source
353 :param statuses: list of pull request statuses
353 :param statuses: list of pull request statuses
354 :param opened_by: author user of the pull request
354 :param opened_by: author user of the pull request
355 :param offset: pagination offset
355 :param offset: pagination offset
356 :param length: length of returned list
356 :param length: length of returned list
357 :param order_by: order of the returned list
357 :param order_by: order of the returned list
358 :param order_dir: 'asc' or 'desc' ordering direction
358 :param order_dir: 'asc' or 'desc' ordering direction
359 :returns: list of pull requests
359 :returns: list of pull requests
360 """
360 """
361 q = self._prepare_get_all_query(
361 q = self._prepare_get_all_query(
362 repo_name, search_q=search_q, source=source, statuses=statuses,
362 repo_name, search_q=search_q, source=source, statuses=statuses,
363 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
363 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
364
364
365 if length:
365 if length:
366 pull_requests = q.limit(length).offset(offset).all()
366 pull_requests = q.limit(length).offset(offset).all()
367 else:
367 else:
368 pull_requests = q.all()
368 pull_requests = q.all()
369
369
370 return pull_requests
370 return pull_requests
371
371
372 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
372 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
373 opened_by=None):
373 opened_by=None):
374 """
374 """
375 Count the number of pull requests for a specific repository that are
375 Count the number of pull requests for a specific repository that are
376 awaiting review.
376 awaiting review.
377
377
378 :param repo_name: target or source repo
378 :param repo_name: target or source repo
379 :param search_q: filter by text
379 :param search_q: filter by text
380 :param source: boolean flag to specify if repo_name refers to source
380 :param source: boolean flag to specify if repo_name refers to source
381 :param statuses: list of pull request statuses
381 :param statuses: list of pull request statuses
382 :param opened_by: author user of the pull request
382 :param opened_by: author user of the pull request
383 :returns: int number of pull requests
383 :returns: int number of pull requests
384 """
384 """
385 pull_requests = self.get_awaiting_review(
385 pull_requests = self.get_awaiting_review(
386 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
386 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
387
387
388 return len(pull_requests)
388 return len(pull_requests)
389
389
390 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
390 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
391 opened_by=None, offset=0, length=None,
391 opened_by=None, offset=0, length=None,
392 order_by=None, order_dir='desc'):
392 order_by=None, order_dir='desc'):
393 """
393 """
394 Get all pull requests for a specific repository that are awaiting
394 Get all pull requests for a specific repository that are awaiting
395 review.
395 review.
396
396
397 :param repo_name: target or source repo
397 :param repo_name: target or source repo
398 :param search_q: filter by text
398 :param search_q: filter by text
399 :param source: boolean flag to specify if repo_name refers to source
399 :param source: boolean flag to specify if repo_name refers to source
400 :param statuses: list of pull request statuses
400 :param statuses: list of pull request statuses
401 :param opened_by: author user of the pull request
401 :param opened_by: author user of the pull request
402 :param offset: pagination offset
402 :param offset: pagination offset
403 :param length: length of returned list
403 :param length: length of returned list
404 :param order_by: order of the returned list
404 :param order_by: order of the returned list
405 :param order_dir: 'asc' or 'desc' ordering direction
405 :param order_dir: 'asc' or 'desc' ordering direction
406 :returns: list of pull requests
406 :returns: list of pull requests
407 """
407 """
408 pull_requests = self.get_all(
408 pull_requests = self.get_all(
409 repo_name, search_q=search_q, source=source, statuses=statuses,
409 repo_name, search_q=search_q, source=source, statuses=statuses,
410 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
410 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
411
411
412 _filtered_pull_requests = []
412 _filtered_pull_requests = []
413 for pr in pull_requests:
413 for pr in pull_requests:
414 status = pr.calculated_review_status()
414 status = pr.calculated_review_status()
415 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
415 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
416 ChangesetStatus.STATUS_UNDER_REVIEW]:
416 ChangesetStatus.STATUS_UNDER_REVIEW]:
417 _filtered_pull_requests.append(pr)
417 _filtered_pull_requests.append(pr)
418 if length:
418 if length:
419 return _filtered_pull_requests[offset:offset+length]
419 return _filtered_pull_requests[offset:offset+length]
420 else:
420 else:
421 return _filtered_pull_requests
421 return _filtered_pull_requests
422
422
423 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
423 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
424 opened_by=None, user_id=None):
424 opened_by=None, user_id=None):
425 """
425 """
426 Count the number of pull requests for a specific repository that are
426 Count the number of pull requests for a specific repository that are
427 awaiting review from a specific user.
427 awaiting review from a specific user.
428
428
429 :param repo_name: target or source repo
429 :param repo_name: target or source repo
430 :param search_q: filter by text
430 :param search_q: filter by text
431 :param source: boolean flag to specify if repo_name refers to source
431 :param source: boolean flag to specify if repo_name refers to source
432 :param statuses: list of pull request statuses
432 :param statuses: list of pull request statuses
433 :param opened_by: author user of the pull request
433 :param opened_by: author user of the pull request
434 :param user_id: reviewer user of the pull request
434 :param user_id: reviewer user of the pull request
435 :returns: int number of pull requests
435 :returns: int number of pull requests
436 """
436 """
437 pull_requests = self.get_awaiting_my_review(
437 pull_requests = self.get_awaiting_my_review(
438 repo_name, search_q=search_q, source=source, statuses=statuses,
438 repo_name, search_q=search_q, source=source, statuses=statuses,
439 opened_by=opened_by, user_id=user_id)
439 opened_by=opened_by, user_id=user_id)
440
440
441 return len(pull_requests)
441 return len(pull_requests)
442
442
443 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
443 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
444 opened_by=None, user_id=None, offset=0,
444 opened_by=None, user_id=None, offset=0,
445 length=None, order_by=None, order_dir='desc'):
445 length=None, order_by=None, order_dir='desc'):
446 """
446 """
447 Get all pull requests for a specific repository that are awaiting
447 Get all pull requests for a specific repository that are awaiting
448 review from a specific user.
448 review from a specific user.
449
449
450 :param repo_name: target or source repo
450 :param repo_name: target or source repo
451 :param search_q: filter by text
451 :param search_q: filter by text
452 :param source: boolean flag to specify if repo_name refers to source
452 :param source: boolean flag to specify if repo_name refers to source
453 :param statuses: list of pull request statuses
453 :param statuses: list of pull request statuses
454 :param opened_by: author user of the pull request
454 :param opened_by: author user of the pull request
455 :param user_id: reviewer user of the pull request
455 :param user_id: reviewer user of the pull request
456 :param offset: pagination offset
456 :param offset: pagination offset
457 :param length: length of returned list
457 :param length: length of returned list
458 :param order_by: order of the returned list
458 :param order_by: order of the returned list
459 :param order_dir: 'asc' or 'desc' ordering direction
459 :param order_dir: 'asc' or 'desc' ordering direction
460 :returns: list of pull requests
460 :returns: list of pull requests
461 """
461 """
462 pull_requests = self.get_all(
462 pull_requests = self.get_all(
463 repo_name, search_q=search_q, source=source, statuses=statuses,
463 repo_name, search_q=search_q, source=source, statuses=statuses,
464 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
464 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
465
465
466 _my = PullRequestModel().get_not_reviewed(user_id)
466 _my = PullRequestModel().get_not_reviewed(user_id)
467 my_participation = []
467 my_participation = []
468 for pr in pull_requests:
468 for pr in pull_requests:
469 if pr in _my:
469 if pr in _my:
470 my_participation.append(pr)
470 my_participation.append(pr)
471 _filtered_pull_requests = my_participation
471 _filtered_pull_requests = my_participation
472 if length:
472 if length:
473 return _filtered_pull_requests[offset:offset+length]
473 return _filtered_pull_requests[offset:offset+length]
474 else:
474 else:
475 return _filtered_pull_requests
475 return _filtered_pull_requests
476
476
477 def get_not_reviewed(self, user_id):
477 def get_not_reviewed(self, user_id):
478 return [
478 return [
479 x.pull_request for x in PullRequestReviewers.query().filter(
479 x.pull_request for x in PullRequestReviewers.query().filter(
480 PullRequestReviewers.user_id == user_id).all()
480 PullRequestReviewers.user_id == user_id).all()
481 ]
481 ]
482
482
483 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
483 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
484 order_by=None, order_dir='desc'):
484 order_by=None, order_dir='desc'):
485 q = PullRequest.query()
485 q = PullRequest.query()
486 if user_id:
486 if user_id:
487 reviewers_subquery = Session().query(
487 reviewers_subquery = Session().query(
488 PullRequestReviewers.pull_request_id).filter(
488 PullRequestReviewers.pull_request_id).filter(
489 PullRequestReviewers.user_id == user_id).subquery()
489 PullRequestReviewers.user_id == user_id).subquery()
490 user_filter = or_(
490 user_filter = or_(
491 PullRequest.user_id == user_id,
491 PullRequest.user_id == user_id,
492 PullRequest.pull_request_id.in_(reviewers_subquery)
492 PullRequest.pull_request_id.in_(reviewers_subquery)
493 )
493 )
494 q = PullRequest.query().filter(user_filter)
494 q = PullRequest.query().filter(user_filter)
495
495
496 # closed,opened
496 # closed,opened
497 if statuses:
497 if statuses:
498 q = q.filter(PullRequest.status.in_(statuses))
498 q = q.filter(PullRequest.status.in_(statuses))
499
499
500 if query:
500 if query:
501 like_expression = u'%{}%'.format(safe_unicode(query))
501 like_expression = u'%{}%'.format(safe_unicode(query))
502 q = q.join(User)
502 q = q.join(User)
503 q = q.filter(or_(
503 q = q.filter(or_(
504 cast(PullRequest.pull_request_id, String).ilike(like_expression),
504 cast(PullRequest.pull_request_id, String).ilike(like_expression),
505 User.username.ilike(like_expression),
505 User.username.ilike(like_expression),
506 PullRequest.title.ilike(like_expression),
506 PullRequest.title.ilike(like_expression),
507 PullRequest.description.ilike(like_expression),
507 PullRequest.description.ilike(like_expression),
508 ))
508 ))
509 if order_by:
509 if order_by:
510 order_map = {
510 order_map = {
511 'name_raw': PullRequest.pull_request_id,
511 'name_raw': PullRequest.pull_request_id,
512 'title': PullRequest.title,
512 'title': PullRequest.title,
513 'updated_on_raw': PullRequest.updated_on,
513 'updated_on_raw': PullRequest.updated_on,
514 'target_repo': PullRequest.target_repo_id
514 'target_repo': PullRequest.target_repo_id
515 }
515 }
516 if order_dir == 'asc':
516 if order_dir == 'asc':
517 q = q.order_by(order_map[order_by].asc())
517 q = q.order_by(order_map[order_by].asc())
518 else:
518 else:
519 q = q.order_by(order_map[order_by].desc())
519 q = q.order_by(order_map[order_by].desc())
520
520
521 return q
521 return q
522
522
523 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
523 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
524 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
524 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
525 return q.count()
525 return q.count()
526
526
527 def get_im_participating_in(
527 def get_im_participating_in(
528 self, user_id=None, statuses=None, query='', offset=0,
528 self, user_id=None, statuses=None, query='', offset=0,
529 length=None, order_by=None, order_dir='desc'):
529 length=None, order_by=None, order_dir='desc'):
530 """
530 """
531 Get all Pull requests that i'm participating in, or i have opened
531 Get all Pull requests that i'm participating in, or i have opened
532 """
532 """
533
533
534 q = self._prepare_participating_query(
534 q = self._prepare_participating_query(
535 user_id, statuses=statuses, query=query, order_by=order_by,
535 user_id, statuses=statuses, query=query, order_by=order_by,
536 order_dir=order_dir)
536 order_dir=order_dir)
537
537
538 if length:
538 if length:
539 pull_requests = q.limit(length).offset(offset).all()
539 pull_requests = q.limit(length).offset(offset).all()
540 else:
540 else:
541 pull_requests = q.all()
541 pull_requests = q.all()
542
542
543 return pull_requests
543 return pull_requests
544
544
545 def get_versions(self, pull_request):
545 def get_versions(self, pull_request):
546 """
546 """
547 returns version of pull request sorted by ID descending
547 returns version of pull request sorted by ID descending
548 """
548 """
549 return PullRequestVersion.query()\
549 return PullRequestVersion.query()\
550 .filter(PullRequestVersion.pull_request == pull_request)\
550 .filter(PullRequestVersion.pull_request == pull_request)\
551 .order_by(PullRequestVersion.pull_request_version_id.asc())\
551 .order_by(PullRequestVersion.pull_request_version_id.asc())\
552 .all()
552 .all()
553
553
554 def get_pr_version(self, pull_request_id, version=None):
554 def get_pr_version(self, pull_request_id, version=None):
555 at_version = None
555 at_version = None
556
556
557 if version and version == 'latest':
557 if version and version == 'latest':
558 pull_request_ver = PullRequest.get(pull_request_id)
558 pull_request_ver = PullRequest.get(pull_request_id)
559 pull_request_obj = pull_request_ver
559 pull_request_obj = pull_request_ver
560 _org_pull_request_obj = pull_request_obj
560 _org_pull_request_obj = pull_request_obj
561 at_version = 'latest'
561 at_version = 'latest'
562 elif version:
562 elif version:
563 pull_request_ver = PullRequestVersion.get_or_404(version)
563 pull_request_ver = PullRequestVersion.get_or_404(version)
564 pull_request_obj = pull_request_ver
564 pull_request_obj = pull_request_ver
565 _org_pull_request_obj = pull_request_ver.pull_request
565 _org_pull_request_obj = pull_request_ver.pull_request
566 at_version = pull_request_ver.pull_request_version_id
566 at_version = pull_request_ver.pull_request_version_id
567 else:
567 else:
568 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
568 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
569 pull_request_id)
569 pull_request_id)
570
570
571 pull_request_display_obj = PullRequest.get_pr_display_object(
571 pull_request_display_obj = PullRequest.get_pr_display_object(
572 pull_request_obj, _org_pull_request_obj)
572 pull_request_obj, _org_pull_request_obj)
573
573
574 return _org_pull_request_obj, pull_request_obj, \
574 return _org_pull_request_obj, pull_request_obj, \
575 pull_request_display_obj, at_version
575 pull_request_display_obj, at_version
576
576
577 def create(self, created_by, source_repo, source_ref, target_repo,
577 def create(self, created_by, source_repo, source_ref, target_repo,
578 target_ref, revisions, reviewers, title, description=None,
578 target_ref, revisions, reviewers, observers, title, description=None,
579 common_ancestor_id=None,
579 common_ancestor_id=None,
580 description_renderer=None,
580 description_renderer=None,
581 reviewer_data=None, translator=None, auth_user=None):
581 reviewer_data=None, translator=None, auth_user=None):
582 translator = translator or get_current_request().translate
582 translator = translator or get_current_request().translate
583
583
584 created_by_user = self._get_user(created_by)
584 created_by_user = self._get_user(created_by)
585 auth_user = auth_user or created_by_user.AuthUser()
585 auth_user = auth_user or created_by_user.AuthUser()
586 source_repo = self._get_repo(source_repo)
586 source_repo = self._get_repo(source_repo)
587 target_repo = self._get_repo(target_repo)
587 target_repo = self._get_repo(target_repo)
588
588
589 pull_request = PullRequest()
589 pull_request = PullRequest()
590 pull_request.source_repo = source_repo
590 pull_request.source_repo = source_repo
591 pull_request.source_ref = source_ref
591 pull_request.source_ref = source_ref
592 pull_request.target_repo = target_repo
592 pull_request.target_repo = target_repo
593 pull_request.target_ref = target_ref
593 pull_request.target_ref = target_ref
594 pull_request.revisions = revisions
594 pull_request.revisions = revisions
595 pull_request.title = title
595 pull_request.title = title
596 pull_request.description = description
596 pull_request.description = description
597 pull_request.description_renderer = description_renderer
597 pull_request.description_renderer = description_renderer
598 pull_request.author = created_by_user
598 pull_request.author = created_by_user
599 pull_request.reviewer_data = reviewer_data
599 pull_request.reviewer_data = reviewer_data
600 pull_request.pull_request_state = pull_request.STATE_CREATING
600 pull_request.pull_request_state = pull_request.STATE_CREATING
601 pull_request.common_ancestor_id = common_ancestor_id
601 pull_request.common_ancestor_id = common_ancestor_id
602
602
603 Session().add(pull_request)
603 Session().add(pull_request)
604 Session().flush()
604 Session().flush()
605
605
606 reviewer_ids = set()
606 reviewer_ids = set()
607 # members / reviewers
607 # members / reviewers
608 for reviewer_object in reviewers:
608 for reviewer_object in reviewers:
609 user_id, reasons, mandatory, rules = reviewer_object
609 user_id, reasons, mandatory, role, rules = reviewer_object
610 user = self._get_user(user_id)
610 user = self._get_user(user_id)
611
611
612 # skip duplicates
612 # skip duplicates
613 if user.user_id in reviewer_ids:
613 if user.user_id in reviewer_ids:
614 continue
614 continue
615
615
616 reviewer_ids.add(user.user_id)
616 reviewer_ids.add(user.user_id)
617
617
618 reviewer = PullRequestReviewers()
618 reviewer = PullRequestReviewers()
619 reviewer.user = user
619 reviewer.user = user
620 reviewer.pull_request = pull_request
620 reviewer.pull_request = pull_request
621 reviewer.reasons = reasons
621 reviewer.reasons = reasons
622 reviewer.mandatory = mandatory
622 reviewer.mandatory = mandatory
623 reviewer.role = role
623
624
624 # NOTE(marcink): pick only first rule for now
625 # NOTE(marcink): pick only first rule for now
625 rule_id = list(rules)[0] if rules else None
626 rule_id = list(rules)[0] if rules else None
626 rule = RepoReviewRule.get(rule_id) if rule_id else None
627 rule = RepoReviewRule.get(rule_id) if rule_id else None
627 if rule:
628 if rule:
628 review_group = rule.user_group_vote_rule(user_id)
629 review_group = rule.user_group_vote_rule(user_id)
629 # we check if this particular reviewer is member of a voting group
630 # we check if this particular reviewer is member of a voting group
630 if review_group:
631 if review_group:
631 # NOTE(marcink):
632 # NOTE(marcink):
632 # can be that user is member of more but we pick the first same,
633 # can be that user is member of more but we pick the first same,
633 # same as default reviewers algo
634 # same as default reviewers algo
634 review_group = review_group[0]
635 review_group = review_group[0]
635
636
636 rule_data = {
637 rule_data = {
637 'rule_name':
638 'rule_name':
638 rule.review_rule_name,
639 rule.review_rule_name,
639 'rule_user_group_entry_id':
640 'rule_user_group_entry_id':
640 review_group.repo_review_rule_users_group_id,
641 review_group.repo_review_rule_users_group_id,
641 'rule_user_group_name':
642 'rule_user_group_name':
642 review_group.users_group.users_group_name,
643 review_group.users_group.users_group_name,
643 'rule_user_group_members':
644 'rule_user_group_members':
644 [x.user.username for x in review_group.users_group.members],
645 [x.user.username for x in review_group.users_group.members],
645 'rule_user_group_members_id':
646 'rule_user_group_members_id':
646 [x.user.user_id for x in review_group.users_group.members],
647 [x.user.user_id for x in review_group.users_group.members],
647 }
648 }
648 # e.g {'vote_rule': -1, 'mandatory': True}
649 # e.g {'vote_rule': -1, 'mandatory': True}
649 rule_data.update(review_group.rule_data())
650 rule_data.update(review_group.rule_data())
650
651
651 reviewer.rule_data = rule_data
652 reviewer.rule_data = rule_data
652
653
653 Session().add(reviewer)
654 Session().add(reviewer)
654 Session().flush()
655 Session().flush()
655
656
657 for observer_object in observers:
658 user_id, reasons, mandatory, role, rules = observer_object
659 user = self._get_user(user_id)
660
661 # skip duplicates from reviewers
662 if user.user_id in reviewer_ids:
663 continue
664
665 #reviewer_ids.add(user.user_id)
666
667 observer = PullRequestReviewers()
668 observer.user = user
669 observer.pull_request = pull_request
670 observer.reasons = reasons
671 observer.mandatory = mandatory
672 observer.role = role
673
674 # NOTE(marcink): pick only first rule for now
675 rule_id = list(rules)[0] if rules else None
676 rule = RepoReviewRule.get(rule_id) if rule_id else None
677 if rule:
678 # TODO(marcink): do we need this for observers ??
679 pass
680
681 Session().add(observer)
682 Session().flush()
683
656 # Set approval status to "Under Review" for all commits which are
684 # Set approval status to "Under Review" for all commits which are
657 # part of this pull request.
685 # part of this pull request.
658 ChangesetStatusModel().set_status(
686 ChangesetStatusModel().set_status(
659 repo=target_repo,
687 repo=target_repo,
660 status=ChangesetStatus.STATUS_UNDER_REVIEW,
688 status=ChangesetStatus.STATUS_UNDER_REVIEW,
661 user=created_by_user,
689 user=created_by_user,
662 pull_request=pull_request
690 pull_request=pull_request
663 )
691 )
664 # we commit early at this point. This has to do with a fact
692 # we commit early at this point. This has to do with a fact
665 # that before queries do some row-locking. And because of that
693 # that before queries do some row-locking. And because of that
666 # we need to commit and finish transaction before below validate call
694 # we need to commit and finish transaction before below validate call
667 # that for large repos could be long resulting in long row locks
695 # that for large repos could be long resulting in long row locks
668 Session().commit()
696 Session().commit()
669
697
670 # prepare workspace, and run initial merge simulation. Set state during that
698 # prepare workspace, and run initial merge simulation. Set state during that
671 # operation
699 # operation
672 pull_request = PullRequest.get(pull_request.pull_request_id)
700 pull_request = PullRequest.get(pull_request.pull_request_id)
673
701
674 # set as merging, for merge simulation, and if finished to created so we mark
702 # set as merging, for merge simulation, and if finished to created so we mark
675 # simulation is working fine
703 # simulation is working fine
676 with pull_request.set_state(PullRequest.STATE_MERGING,
704 with pull_request.set_state(PullRequest.STATE_MERGING,
677 final_state=PullRequest.STATE_CREATED) as state_obj:
705 final_state=PullRequest.STATE_CREATED) as state_obj:
678 MergeCheck.validate(
706 MergeCheck.validate(
679 pull_request, auth_user=auth_user, translator=translator)
707 pull_request, auth_user=auth_user, translator=translator)
680
708
681 self.notify_reviewers(pull_request, reviewer_ids)
709 self.notify_reviewers(pull_request, reviewer_ids)
682 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
710 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
683
711
684 creation_data = pull_request.get_api_data(with_merge_state=False)
712 creation_data = pull_request.get_api_data(with_merge_state=False)
685 self._log_audit_action(
713 self._log_audit_action(
686 'repo.pull_request.create', {'data': creation_data},
714 'repo.pull_request.create', {'data': creation_data},
687 auth_user, pull_request)
715 auth_user, pull_request)
688
716
689 return pull_request
717 return pull_request
690
718
691 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
719 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
692 pull_request = self.__get_pull_request(pull_request)
720 pull_request = self.__get_pull_request(pull_request)
693 target_scm = pull_request.target_repo.scm_instance()
721 target_scm = pull_request.target_repo.scm_instance()
694 if action == 'create':
722 if action == 'create':
695 trigger_hook = hooks_utils.trigger_create_pull_request_hook
723 trigger_hook = hooks_utils.trigger_create_pull_request_hook
696 elif action == 'merge':
724 elif action == 'merge':
697 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
725 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
698 elif action == 'close':
726 elif action == 'close':
699 trigger_hook = hooks_utils.trigger_close_pull_request_hook
727 trigger_hook = hooks_utils.trigger_close_pull_request_hook
700 elif action == 'review_status_change':
728 elif action == 'review_status_change':
701 trigger_hook = hooks_utils.trigger_review_pull_request_hook
729 trigger_hook = hooks_utils.trigger_review_pull_request_hook
702 elif action == 'update':
730 elif action == 'update':
703 trigger_hook = hooks_utils.trigger_update_pull_request_hook
731 trigger_hook = hooks_utils.trigger_update_pull_request_hook
704 elif action == 'comment':
732 elif action == 'comment':
705 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
733 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
706 elif action == 'comment_edit':
734 elif action == 'comment_edit':
707 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
735 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
708 else:
736 else:
709 return
737 return
710
738
711 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
739 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
712 pull_request, action, trigger_hook)
740 pull_request, action, trigger_hook)
713 trigger_hook(
741 trigger_hook(
714 username=user.username,
742 username=user.username,
715 repo_name=pull_request.target_repo.repo_name,
743 repo_name=pull_request.target_repo.repo_name,
716 repo_type=target_scm.alias,
744 repo_type=target_scm.alias,
717 pull_request=pull_request,
745 pull_request=pull_request,
718 data=data)
746 data=data)
719
747
720 def _get_commit_ids(self, pull_request):
748 def _get_commit_ids(self, pull_request):
721 """
749 """
722 Return the commit ids of the merged pull request.
750 Return the commit ids of the merged pull request.
723
751
724 This method is not dealing correctly yet with the lack of autoupdates
752 This method is not dealing correctly yet with the lack of autoupdates
725 nor with the implicit target updates.
753 nor with the implicit target updates.
726 For example: if a commit in the source repo is already in the target it
754 For example: if a commit in the source repo is already in the target it
727 will be reported anyways.
755 will be reported anyways.
728 """
756 """
729 merge_rev = pull_request.merge_rev
757 merge_rev = pull_request.merge_rev
730 if merge_rev is None:
758 if merge_rev is None:
731 raise ValueError('This pull request was not merged yet')
759 raise ValueError('This pull request was not merged yet')
732
760
733 commit_ids = list(pull_request.revisions)
761 commit_ids = list(pull_request.revisions)
734 if merge_rev not in commit_ids:
762 if merge_rev not in commit_ids:
735 commit_ids.append(merge_rev)
763 commit_ids.append(merge_rev)
736
764
737 return commit_ids
765 return commit_ids
738
766
739 def merge_repo(self, pull_request, user, extras):
767 def merge_repo(self, pull_request, user, extras):
740 log.debug("Merging pull request %s", pull_request.pull_request_id)
768 log.debug("Merging pull request %s", pull_request.pull_request_id)
741 extras['user_agent'] = 'internal-merge'
769 extras['user_agent'] = 'internal-merge'
742 merge_state = self._merge_pull_request(pull_request, user, extras)
770 merge_state = self._merge_pull_request(pull_request, user, extras)
743 if merge_state.executed:
771 if merge_state.executed:
744 log.debug("Merge was successful, updating the pull request comments.")
772 log.debug("Merge was successful, updating the pull request comments.")
745 self._comment_and_close_pr(pull_request, user, merge_state)
773 self._comment_and_close_pr(pull_request, user, merge_state)
746
774
747 self._log_audit_action(
775 self._log_audit_action(
748 'repo.pull_request.merge',
776 'repo.pull_request.merge',
749 {'merge_state': merge_state.__dict__},
777 {'merge_state': merge_state.__dict__},
750 user, pull_request)
778 user, pull_request)
751
779
752 else:
780 else:
753 log.warn("Merge failed, not updating the pull request.")
781 log.warn("Merge failed, not updating the pull request.")
754 return merge_state
782 return merge_state
755
783
756 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
784 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
757 target_vcs = pull_request.target_repo.scm_instance()
785 target_vcs = pull_request.target_repo.scm_instance()
758 source_vcs = pull_request.source_repo.scm_instance()
786 source_vcs = pull_request.source_repo.scm_instance()
759
787
760 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
788 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
761 pr_id=pull_request.pull_request_id,
789 pr_id=pull_request.pull_request_id,
762 pr_title=pull_request.title,
790 pr_title=pull_request.title,
763 source_repo=source_vcs.name,
791 source_repo=source_vcs.name,
764 source_ref_name=pull_request.source_ref_parts.name,
792 source_ref_name=pull_request.source_ref_parts.name,
765 target_repo=target_vcs.name,
793 target_repo=target_vcs.name,
766 target_ref_name=pull_request.target_ref_parts.name,
794 target_ref_name=pull_request.target_ref_parts.name,
767 )
795 )
768
796
769 workspace_id = self._workspace_id(pull_request)
797 workspace_id = self._workspace_id(pull_request)
770 repo_id = pull_request.target_repo.repo_id
798 repo_id = pull_request.target_repo.repo_id
771 use_rebase = self._use_rebase_for_merging(pull_request)
799 use_rebase = self._use_rebase_for_merging(pull_request)
772 close_branch = self._close_branch_before_merging(pull_request)
800 close_branch = self._close_branch_before_merging(pull_request)
773 user_name = self._user_name_for_merging(pull_request, user)
801 user_name = self._user_name_for_merging(pull_request, user)
774
802
775 target_ref = self._refresh_reference(
803 target_ref = self._refresh_reference(
776 pull_request.target_ref_parts, target_vcs)
804 pull_request.target_ref_parts, target_vcs)
777
805
778 callback_daemon, extras = prepare_callback_daemon(
806 callback_daemon, extras = prepare_callback_daemon(
779 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
807 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
780 host=vcs_settings.HOOKS_HOST,
808 host=vcs_settings.HOOKS_HOST,
781 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
809 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
782
810
783 with callback_daemon:
811 with callback_daemon:
784 # TODO: johbo: Implement a clean way to run a config_override
812 # TODO: johbo: Implement a clean way to run a config_override
785 # for a single call.
813 # for a single call.
786 target_vcs.config.set(
814 target_vcs.config.set(
787 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
815 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
788
816
789 merge_state = target_vcs.merge(
817 merge_state = target_vcs.merge(
790 repo_id, workspace_id, target_ref, source_vcs,
818 repo_id, workspace_id, target_ref, source_vcs,
791 pull_request.source_ref_parts,
819 pull_request.source_ref_parts,
792 user_name=user_name, user_email=user.email,
820 user_name=user_name, user_email=user.email,
793 message=message, use_rebase=use_rebase,
821 message=message, use_rebase=use_rebase,
794 close_branch=close_branch)
822 close_branch=close_branch)
795 return merge_state
823 return merge_state
796
824
797 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
825 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
798 pull_request.merge_rev = merge_state.merge_ref.commit_id
826 pull_request.merge_rev = merge_state.merge_ref.commit_id
799 pull_request.updated_on = datetime.datetime.now()
827 pull_request.updated_on = datetime.datetime.now()
800 close_msg = close_msg or 'Pull request merged and closed'
828 close_msg = close_msg or 'Pull request merged and closed'
801
829
802 CommentsModel().create(
830 CommentsModel().create(
803 text=safe_unicode(close_msg),
831 text=safe_unicode(close_msg),
804 repo=pull_request.target_repo.repo_id,
832 repo=pull_request.target_repo.repo_id,
805 user=user.user_id,
833 user=user.user_id,
806 pull_request=pull_request.pull_request_id,
834 pull_request=pull_request.pull_request_id,
807 f_path=None,
835 f_path=None,
808 line_no=None,
836 line_no=None,
809 closing_pr=True
837 closing_pr=True
810 )
838 )
811
839
812 Session().add(pull_request)
840 Session().add(pull_request)
813 Session().flush()
841 Session().flush()
814 # TODO: paris: replace invalidation with less radical solution
842 # TODO: paris: replace invalidation with less radical solution
815 ScmModel().mark_for_invalidation(
843 ScmModel().mark_for_invalidation(
816 pull_request.target_repo.repo_name)
844 pull_request.target_repo.repo_name)
817 self.trigger_pull_request_hook(pull_request, user, 'merge')
845 self.trigger_pull_request_hook(pull_request, user, 'merge')
818
846
819 def has_valid_update_type(self, pull_request):
847 def has_valid_update_type(self, pull_request):
820 source_ref_type = pull_request.source_ref_parts.type
848 source_ref_type = pull_request.source_ref_parts.type
821 return source_ref_type in self.REF_TYPES
849 return source_ref_type in self.REF_TYPES
822
850
823 def get_flow_commits(self, pull_request):
851 def get_flow_commits(self, pull_request):
824
852
825 # source repo
853 # source repo
826 source_ref_name = pull_request.source_ref_parts.name
854 source_ref_name = pull_request.source_ref_parts.name
827 source_ref_type = pull_request.source_ref_parts.type
855 source_ref_type = pull_request.source_ref_parts.type
828 source_ref_id = pull_request.source_ref_parts.commit_id
856 source_ref_id = pull_request.source_ref_parts.commit_id
829 source_repo = pull_request.source_repo.scm_instance()
857 source_repo = pull_request.source_repo.scm_instance()
830
858
831 try:
859 try:
832 if source_ref_type in self.REF_TYPES:
860 if source_ref_type in self.REF_TYPES:
833 source_commit = source_repo.get_commit(source_ref_name)
861 source_commit = source_repo.get_commit(source_ref_name)
834 else:
862 else:
835 source_commit = source_repo.get_commit(source_ref_id)
863 source_commit = source_repo.get_commit(source_ref_id)
836 except CommitDoesNotExistError:
864 except CommitDoesNotExistError:
837 raise SourceRefMissing()
865 raise SourceRefMissing()
838
866
839 # target repo
867 # target repo
840 target_ref_name = pull_request.target_ref_parts.name
868 target_ref_name = pull_request.target_ref_parts.name
841 target_ref_type = pull_request.target_ref_parts.type
869 target_ref_type = pull_request.target_ref_parts.type
842 target_ref_id = pull_request.target_ref_parts.commit_id
870 target_ref_id = pull_request.target_ref_parts.commit_id
843 target_repo = pull_request.target_repo.scm_instance()
871 target_repo = pull_request.target_repo.scm_instance()
844
872
845 try:
873 try:
846 if target_ref_type in self.REF_TYPES:
874 if target_ref_type in self.REF_TYPES:
847 target_commit = target_repo.get_commit(target_ref_name)
875 target_commit = target_repo.get_commit(target_ref_name)
848 else:
876 else:
849 target_commit = target_repo.get_commit(target_ref_id)
877 target_commit = target_repo.get_commit(target_ref_id)
850 except CommitDoesNotExistError:
878 except CommitDoesNotExistError:
851 raise TargetRefMissing()
879 raise TargetRefMissing()
852
880
853 return source_commit, target_commit
881 return source_commit, target_commit
854
882
855 def update_commits(self, pull_request, updating_user):
883 def update_commits(self, pull_request, updating_user):
856 """
884 """
857 Get the updated list of commits for the pull request
885 Get the updated list of commits for the pull request
858 and return the new pull request version and the list
886 and return the new pull request version and the list
859 of commits processed by this update action
887 of commits processed by this update action
860
888
861 updating_user is the user_object who triggered the update
889 updating_user is the user_object who triggered the update
862 """
890 """
863 pull_request = self.__get_pull_request(pull_request)
891 pull_request = self.__get_pull_request(pull_request)
864 source_ref_type = pull_request.source_ref_parts.type
892 source_ref_type = pull_request.source_ref_parts.type
865 source_ref_name = pull_request.source_ref_parts.name
893 source_ref_name = pull_request.source_ref_parts.name
866 source_ref_id = pull_request.source_ref_parts.commit_id
894 source_ref_id = pull_request.source_ref_parts.commit_id
867
895
868 target_ref_type = pull_request.target_ref_parts.type
896 target_ref_type = pull_request.target_ref_parts.type
869 target_ref_name = pull_request.target_ref_parts.name
897 target_ref_name = pull_request.target_ref_parts.name
870 target_ref_id = pull_request.target_ref_parts.commit_id
898 target_ref_id = pull_request.target_ref_parts.commit_id
871
899
872 if not self.has_valid_update_type(pull_request):
900 if not self.has_valid_update_type(pull_request):
873 log.debug("Skipping update of pull request %s due to ref type: %s",
901 log.debug("Skipping update of pull request %s due to ref type: %s",
874 pull_request, source_ref_type)
902 pull_request, source_ref_type)
875 return UpdateResponse(
903 return UpdateResponse(
876 executed=False,
904 executed=False,
877 reason=UpdateFailureReason.WRONG_REF_TYPE,
905 reason=UpdateFailureReason.WRONG_REF_TYPE,
878 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
906 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
879 source_changed=False, target_changed=False)
907 source_changed=False, target_changed=False)
880
908
881 try:
909 try:
882 source_commit, target_commit = self.get_flow_commits(pull_request)
910 source_commit, target_commit = self.get_flow_commits(pull_request)
883 except SourceRefMissing:
911 except SourceRefMissing:
884 return UpdateResponse(
912 return UpdateResponse(
885 executed=False,
913 executed=False,
886 reason=UpdateFailureReason.MISSING_SOURCE_REF,
914 reason=UpdateFailureReason.MISSING_SOURCE_REF,
887 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
915 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
888 source_changed=False, target_changed=False)
916 source_changed=False, target_changed=False)
889 except TargetRefMissing:
917 except TargetRefMissing:
890 return UpdateResponse(
918 return UpdateResponse(
891 executed=False,
919 executed=False,
892 reason=UpdateFailureReason.MISSING_TARGET_REF,
920 reason=UpdateFailureReason.MISSING_TARGET_REF,
893 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
921 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
894 source_changed=False, target_changed=False)
922 source_changed=False, target_changed=False)
895
923
896 source_changed = source_ref_id != source_commit.raw_id
924 source_changed = source_ref_id != source_commit.raw_id
897 target_changed = target_ref_id != target_commit.raw_id
925 target_changed = target_ref_id != target_commit.raw_id
898
926
899 if not (source_changed or target_changed):
927 if not (source_changed or target_changed):
900 log.debug("Nothing changed in pull request %s", pull_request)
928 log.debug("Nothing changed in pull request %s", pull_request)
901 return UpdateResponse(
929 return UpdateResponse(
902 executed=False,
930 executed=False,
903 reason=UpdateFailureReason.NO_CHANGE,
931 reason=UpdateFailureReason.NO_CHANGE,
904 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
932 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
905 source_changed=target_changed, target_changed=source_changed)
933 source_changed=target_changed, target_changed=source_changed)
906
934
907 change_in_found = 'target repo' if target_changed else 'source repo'
935 change_in_found = 'target repo' if target_changed else 'source repo'
908 log.debug('Updating pull request because of change in %s detected',
936 log.debug('Updating pull request because of change in %s detected',
909 change_in_found)
937 change_in_found)
910
938
911 # Finally there is a need for an update, in case of source change
939 # Finally there is a need for an update, in case of source change
912 # we create a new version, else just an update
940 # we create a new version, else just an update
913 if source_changed:
941 if source_changed:
914 pull_request_version = self._create_version_from_snapshot(pull_request)
942 pull_request_version = self._create_version_from_snapshot(pull_request)
915 self._link_comments_to_version(pull_request_version)
943 self._link_comments_to_version(pull_request_version)
916 else:
944 else:
917 try:
945 try:
918 ver = pull_request.versions[-1]
946 ver = pull_request.versions[-1]
919 except IndexError:
947 except IndexError:
920 ver = None
948 ver = None
921
949
922 pull_request.pull_request_version_id = \
950 pull_request.pull_request_version_id = \
923 ver.pull_request_version_id if ver else None
951 ver.pull_request_version_id if ver else None
924 pull_request_version = pull_request
952 pull_request_version = pull_request
925
953
926 source_repo = pull_request.source_repo.scm_instance()
954 source_repo = pull_request.source_repo.scm_instance()
927 target_repo = pull_request.target_repo.scm_instance()
955 target_repo = pull_request.target_repo.scm_instance()
928
956
929 # re-compute commit ids
957 # re-compute commit ids
930 old_commit_ids = pull_request.revisions
958 old_commit_ids = pull_request.revisions
931 pre_load = ["author", "date", "message", "branch"]
959 pre_load = ["author", "date", "message", "branch"]
932 commit_ranges = target_repo.compare(
960 commit_ranges = target_repo.compare(
933 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
961 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
934 pre_load=pre_load)
962 pre_load=pre_load)
935
963
936 target_ref = target_commit.raw_id
964 target_ref = target_commit.raw_id
937 source_ref = source_commit.raw_id
965 source_ref = source_commit.raw_id
938 ancestor_commit_id = target_repo.get_common_ancestor(
966 ancestor_commit_id = target_repo.get_common_ancestor(
939 target_ref, source_ref, source_repo)
967 target_ref, source_ref, source_repo)
940
968
941 if not ancestor_commit_id:
969 if not ancestor_commit_id:
942 raise ValueError(
970 raise ValueError(
943 'cannot calculate diff info without a common ancestor. '
971 'cannot calculate diff info without a common ancestor. '
944 'Make sure both repositories are related, and have a common forking commit.')
972 'Make sure both repositories are related, and have a common forking commit.')
945
973
946 pull_request.common_ancestor_id = ancestor_commit_id
974 pull_request.common_ancestor_id = ancestor_commit_id
947
975
948 pull_request.source_ref = '%s:%s:%s' % (
976 pull_request.source_ref = '%s:%s:%s' % (
949 source_ref_type, source_ref_name, source_commit.raw_id)
977 source_ref_type, source_ref_name, source_commit.raw_id)
950 pull_request.target_ref = '%s:%s:%s' % (
978 pull_request.target_ref = '%s:%s:%s' % (
951 target_ref_type, target_ref_name, ancestor_commit_id)
979 target_ref_type, target_ref_name, ancestor_commit_id)
952
980
953 pull_request.revisions = [
981 pull_request.revisions = [
954 commit.raw_id for commit in reversed(commit_ranges)]
982 commit.raw_id for commit in reversed(commit_ranges)]
955 pull_request.updated_on = datetime.datetime.now()
983 pull_request.updated_on = datetime.datetime.now()
956 Session().add(pull_request)
984 Session().add(pull_request)
957 new_commit_ids = pull_request.revisions
985 new_commit_ids = pull_request.revisions
958
986
959 old_diff_data, new_diff_data = self._generate_update_diffs(
987 old_diff_data, new_diff_data = self._generate_update_diffs(
960 pull_request, pull_request_version)
988 pull_request, pull_request_version)
961
989
962 # calculate commit and file changes
990 # calculate commit and file changes
963 commit_changes = self._calculate_commit_id_changes(
991 commit_changes = self._calculate_commit_id_changes(
964 old_commit_ids, new_commit_ids)
992 old_commit_ids, new_commit_ids)
965 file_changes = self._calculate_file_changes(
993 file_changes = self._calculate_file_changes(
966 old_diff_data, new_diff_data)
994 old_diff_data, new_diff_data)
967
995
968 # set comments as outdated if DIFFS changed
996 # set comments as outdated if DIFFS changed
969 CommentsModel().outdate_comments(
997 CommentsModel().outdate_comments(
970 pull_request, old_diff_data=old_diff_data,
998 pull_request, old_diff_data=old_diff_data,
971 new_diff_data=new_diff_data)
999 new_diff_data=new_diff_data)
972
1000
973 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1001 valid_commit_changes = (commit_changes.added or commit_changes.removed)
974 file_node_changes = (
1002 file_node_changes = (
975 file_changes.added or file_changes.modified or file_changes.removed)
1003 file_changes.added or file_changes.modified or file_changes.removed)
976 pr_has_changes = valid_commit_changes or file_node_changes
1004 pr_has_changes = valid_commit_changes or file_node_changes
977
1005
978 # Add an automatic comment to the pull request, in case
1006 # Add an automatic comment to the pull request, in case
979 # anything has changed
1007 # anything has changed
980 if pr_has_changes:
1008 if pr_has_changes:
981 update_comment = CommentsModel().create(
1009 update_comment = CommentsModel().create(
982 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1010 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
983 repo=pull_request.target_repo,
1011 repo=pull_request.target_repo,
984 user=pull_request.author,
1012 user=pull_request.author,
985 pull_request=pull_request,
1013 pull_request=pull_request,
986 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1014 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
987
1015
988 # Update status to "Under Review" for added commits
1016 # Update status to "Under Review" for added commits
989 for commit_id in commit_changes.added:
1017 for commit_id in commit_changes.added:
990 ChangesetStatusModel().set_status(
1018 ChangesetStatusModel().set_status(
991 repo=pull_request.source_repo,
1019 repo=pull_request.source_repo,
992 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1020 status=ChangesetStatus.STATUS_UNDER_REVIEW,
993 comment=update_comment,
1021 comment=update_comment,
994 user=pull_request.author,
1022 user=pull_request.author,
995 pull_request=pull_request,
1023 pull_request=pull_request,
996 revision=commit_id)
1024 revision=commit_id)
997
1025
998 # send update email to users
1026 # send update email to users
999 try:
1027 try:
1000 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1028 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1001 ancestor_commit_id=ancestor_commit_id,
1029 ancestor_commit_id=ancestor_commit_id,
1002 commit_changes=commit_changes,
1030 commit_changes=commit_changes,
1003 file_changes=file_changes)
1031 file_changes=file_changes)
1004 except Exception:
1032 except Exception:
1005 log.exception('Failed to send email notification to users')
1033 log.exception('Failed to send email notification to users')
1006
1034
1007 log.debug(
1035 log.debug(
1008 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1036 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1009 'removed_ids: %s', pull_request.pull_request_id,
1037 'removed_ids: %s', pull_request.pull_request_id,
1010 commit_changes.added, commit_changes.common, commit_changes.removed)
1038 commit_changes.added, commit_changes.common, commit_changes.removed)
1011 log.debug(
1039 log.debug(
1012 'Updated pull request with the following file changes: %s',
1040 'Updated pull request with the following file changes: %s',
1013 file_changes)
1041 file_changes)
1014
1042
1015 log.info(
1043 log.info(
1016 "Updated pull request %s from commit %s to commit %s, "
1044 "Updated pull request %s from commit %s to commit %s, "
1017 "stored new version %s of this pull request.",
1045 "stored new version %s of this pull request.",
1018 pull_request.pull_request_id, source_ref_id,
1046 pull_request.pull_request_id, source_ref_id,
1019 pull_request.source_ref_parts.commit_id,
1047 pull_request.source_ref_parts.commit_id,
1020 pull_request_version.pull_request_version_id)
1048 pull_request_version.pull_request_version_id)
1021 Session().commit()
1049 Session().commit()
1022 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1050 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1023
1051
1024 return UpdateResponse(
1052 return UpdateResponse(
1025 executed=True, reason=UpdateFailureReason.NONE,
1053 executed=True, reason=UpdateFailureReason.NONE,
1026 old=pull_request, new=pull_request_version,
1054 old=pull_request, new=pull_request_version,
1027 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1055 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1028 source_changed=source_changed, target_changed=target_changed)
1056 source_changed=source_changed, target_changed=target_changed)
1029
1057
1030 def _create_version_from_snapshot(self, pull_request):
1058 def _create_version_from_snapshot(self, pull_request):
1031 version = PullRequestVersion()
1059 version = PullRequestVersion()
1032 version.title = pull_request.title
1060 version.title = pull_request.title
1033 version.description = pull_request.description
1061 version.description = pull_request.description
1034 version.status = pull_request.status
1062 version.status = pull_request.status
1035 version.pull_request_state = pull_request.pull_request_state
1063 version.pull_request_state = pull_request.pull_request_state
1036 version.created_on = datetime.datetime.now()
1064 version.created_on = datetime.datetime.now()
1037 version.updated_on = pull_request.updated_on
1065 version.updated_on = pull_request.updated_on
1038 version.user_id = pull_request.user_id
1066 version.user_id = pull_request.user_id
1039 version.source_repo = pull_request.source_repo
1067 version.source_repo = pull_request.source_repo
1040 version.source_ref = pull_request.source_ref
1068 version.source_ref = pull_request.source_ref
1041 version.target_repo = pull_request.target_repo
1069 version.target_repo = pull_request.target_repo
1042 version.target_ref = pull_request.target_ref
1070 version.target_ref = pull_request.target_ref
1043
1071
1044 version._last_merge_source_rev = pull_request._last_merge_source_rev
1072 version._last_merge_source_rev = pull_request._last_merge_source_rev
1045 version._last_merge_target_rev = pull_request._last_merge_target_rev
1073 version._last_merge_target_rev = pull_request._last_merge_target_rev
1046 version.last_merge_status = pull_request.last_merge_status
1074 version.last_merge_status = pull_request.last_merge_status
1047 version.last_merge_metadata = pull_request.last_merge_metadata
1075 version.last_merge_metadata = pull_request.last_merge_metadata
1048 version.shadow_merge_ref = pull_request.shadow_merge_ref
1076 version.shadow_merge_ref = pull_request.shadow_merge_ref
1049 version.merge_rev = pull_request.merge_rev
1077 version.merge_rev = pull_request.merge_rev
1050 version.reviewer_data = pull_request.reviewer_data
1078 version.reviewer_data = pull_request.reviewer_data
1051
1079
1052 version.revisions = pull_request.revisions
1080 version.revisions = pull_request.revisions
1053 version.common_ancestor_id = pull_request.common_ancestor_id
1081 version.common_ancestor_id = pull_request.common_ancestor_id
1054 version.pull_request = pull_request
1082 version.pull_request = pull_request
1055 Session().add(version)
1083 Session().add(version)
1056 Session().flush()
1084 Session().flush()
1057
1085
1058 return version
1086 return version
1059
1087
1060 def _generate_update_diffs(self, pull_request, pull_request_version):
1088 def _generate_update_diffs(self, pull_request, pull_request_version):
1061
1089
1062 diff_context = (
1090 diff_context = (
1063 self.DIFF_CONTEXT +
1091 self.DIFF_CONTEXT +
1064 CommentsModel.needed_extra_diff_context())
1092 CommentsModel.needed_extra_diff_context())
1065 hide_whitespace_changes = False
1093 hide_whitespace_changes = False
1066 source_repo = pull_request_version.source_repo
1094 source_repo = pull_request_version.source_repo
1067 source_ref_id = pull_request_version.source_ref_parts.commit_id
1095 source_ref_id = pull_request_version.source_ref_parts.commit_id
1068 target_ref_id = pull_request_version.target_ref_parts.commit_id
1096 target_ref_id = pull_request_version.target_ref_parts.commit_id
1069 old_diff = self._get_diff_from_pr_or_version(
1097 old_diff = self._get_diff_from_pr_or_version(
1070 source_repo, source_ref_id, target_ref_id,
1098 source_repo, source_ref_id, target_ref_id,
1071 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1099 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1072
1100
1073 source_repo = pull_request.source_repo
1101 source_repo = pull_request.source_repo
1074 source_ref_id = pull_request.source_ref_parts.commit_id
1102 source_ref_id = pull_request.source_ref_parts.commit_id
1075 target_ref_id = pull_request.target_ref_parts.commit_id
1103 target_ref_id = pull_request.target_ref_parts.commit_id
1076
1104
1077 new_diff = self._get_diff_from_pr_or_version(
1105 new_diff = self._get_diff_from_pr_or_version(
1078 source_repo, source_ref_id, target_ref_id,
1106 source_repo, source_ref_id, target_ref_id,
1079 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1107 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1080
1108
1081 old_diff_data = diffs.DiffProcessor(old_diff)
1109 old_diff_data = diffs.DiffProcessor(old_diff)
1082 old_diff_data.prepare()
1110 old_diff_data.prepare()
1083 new_diff_data = diffs.DiffProcessor(new_diff)
1111 new_diff_data = diffs.DiffProcessor(new_diff)
1084 new_diff_data.prepare()
1112 new_diff_data.prepare()
1085
1113
1086 return old_diff_data, new_diff_data
1114 return old_diff_data, new_diff_data
1087
1115
1088 def _link_comments_to_version(self, pull_request_version):
1116 def _link_comments_to_version(self, pull_request_version):
1089 """
1117 """
1090 Link all unlinked comments of this pull request to the given version.
1118 Link all unlinked comments of this pull request to the given version.
1091
1119
1092 :param pull_request_version: The `PullRequestVersion` to which
1120 :param pull_request_version: The `PullRequestVersion` to which
1093 the comments shall be linked.
1121 the comments shall be linked.
1094
1122
1095 """
1123 """
1096 pull_request = pull_request_version.pull_request
1124 pull_request = pull_request_version.pull_request
1097 comments = ChangesetComment.query()\
1125 comments = ChangesetComment.query()\
1098 .filter(
1126 .filter(
1099 # TODO: johbo: Should we query for the repo at all here?
1127 # TODO: johbo: Should we query for the repo at all here?
1100 # Pending decision on how comments of PRs are to be related
1128 # Pending decision on how comments of PRs are to be related
1101 # to either the source repo, the target repo or no repo at all.
1129 # to either the source repo, the target repo or no repo at all.
1102 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1130 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1103 ChangesetComment.pull_request == pull_request,
1131 ChangesetComment.pull_request == pull_request,
1104 ChangesetComment.pull_request_version == None)\
1132 ChangesetComment.pull_request_version == None)\
1105 .order_by(ChangesetComment.comment_id.asc())
1133 .order_by(ChangesetComment.comment_id.asc())
1106
1134
1107 # TODO: johbo: Find out why this breaks if it is done in a bulk
1135 # TODO: johbo: Find out why this breaks if it is done in a bulk
1108 # operation.
1136 # operation.
1109 for comment in comments:
1137 for comment in comments:
1110 comment.pull_request_version_id = (
1138 comment.pull_request_version_id = (
1111 pull_request_version.pull_request_version_id)
1139 pull_request_version.pull_request_version_id)
1112 Session().add(comment)
1140 Session().add(comment)
1113
1141
1114 def _calculate_commit_id_changes(self, old_ids, new_ids):
1142 def _calculate_commit_id_changes(self, old_ids, new_ids):
1115 added = [x for x in new_ids if x not in old_ids]
1143 added = [x for x in new_ids if x not in old_ids]
1116 common = [x for x in new_ids if x in old_ids]
1144 common = [x for x in new_ids if x in old_ids]
1117 removed = [x for x in old_ids if x not in new_ids]
1145 removed = [x for x in old_ids if x not in new_ids]
1118 total = new_ids
1146 total = new_ids
1119 return ChangeTuple(added, common, removed, total)
1147 return ChangeTuple(added, common, removed, total)
1120
1148
1121 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1149 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1122
1150
1123 old_files = OrderedDict()
1151 old_files = OrderedDict()
1124 for diff_data in old_diff_data.parsed_diff:
1152 for diff_data in old_diff_data.parsed_diff:
1125 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1153 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1126
1154
1127 added_files = []
1155 added_files = []
1128 modified_files = []
1156 modified_files = []
1129 removed_files = []
1157 removed_files = []
1130 for diff_data in new_diff_data.parsed_diff:
1158 for diff_data in new_diff_data.parsed_diff:
1131 new_filename = diff_data['filename']
1159 new_filename = diff_data['filename']
1132 new_hash = md5_safe(diff_data['raw_diff'])
1160 new_hash = md5_safe(diff_data['raw_diff'])
1133
1161
1134 old_hash = old_files.get(new_filename)
1162 old_hash = old_files.get(new_filename)
1135 if not old_hash:
1163 if not old_hash:
1136 # file is not present in old diff, we have to figure out from parsed diff
1164 # file is not present in old diff, we have to figure out from parsed diff
1137 # operation ADD/REMOVE
1165 # operation ADD/REMOVE
1138 operations_dict = diff_data['stats']['ops']
1166 operations_dict = diff_data['stats']['ops']
1139 if diffs.DEL_FILENODE in operations_dict:
1167 if diffs.DEL_FILENODE in operations_dict:
1140 removed_files.append(new_filename)
1168 removed_files.append(new_filename)
1141 else:
1169 else:
1142 added_files.append(new_filename)
1170 added_files.append(new_filename)
1143 else:
1171 else:
1144 if new_hash != old_hash:
1172 if new_hash != old_hash:
1145 modified_files.append(new_filename)
1173 modified_files.append(new_filename)
1146 # now remove a file from old, since we have seen it already
1174 # now remove a file from old, since we have seen it already
1147 del old_files[new_filename]
1175 del old_files[new_filename]
1148
1176
1149 # removed files is when there are present in old, but not in NEW,
1177 # removed files is when there are present in old, but not in NEW,
1150 # since we remove old files that are present in new diff, left-overs
1178 # since we remove old files that are present in new diff, left-overs
1151 # if any should be the removed files
1179 # if any should be the removed files
1152 removed_files.extend(old_files.keys())
1180 removed_files.extend(old_files.keys())
1153
1181
1154 return FileChangeTuple(added_files, modified_files, removed_files)
1182 return FileChangeTuple(added_files, modified_files, removed_files)
1155
1183
1156 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1184 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1157 """
1185 """
1158 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1186 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1159 so it's always looking the same disregarding on which default
1187 so it's always looking the same disregarding on which default
1160 renderer system is using.
1188 renderer system is using.
1161
1189
1162 :param ancestor_commit_id: ancestor raw_id
1190 :param ancestor_commit_id: ancestor raw_id
1163 :param changes: changes named tuple
1191 :param changes: changes named tuple
1164 :param file_changes: file changes named tuple
1192 :param file_changes: file changes named tuple
1165
1193
1166 """
1194 """
1167 new_status = ChangesetStatus.get_status_lbl(
1195 new_status = ChangesetStatus.get_status_lbl(
1168 ChangesetStatus.STATUS_UNDER_REVIEW)
1196 ChangesetStatus.STATUS_UNDER_REVIEW)
1169
1197
1170 changed_files = (
1198 changed_files = (
1171 file_changes.added + file_changes.modified + file_changes.removed)
1199 file_changes.added + file_changes.modified + file_changes.removed)
1172
1200
1173 params = {
1201 params = {
1174 'under_review_label': new_status,
1202 'under_review_label': new_status,
1175 'added_commits': changes.added,
1203 'added_commits': changes.added,
1176 'removed_commits': changes.removed,
1204 'removed_commits': changes.removed,
1177 'changed_files': changed_files,
1205 'changed_files': changed_files,
1178 'added_files': file_changes.added,
1206 'added_files': file_changes.added,
1179 'modified_files': file_changes.modified,
1207 'modified_files': file_changes.modified,
1180 'removed_files': file_changes.removed,
1208 'removed_files': file_changes.removed,
1181 'ancestor_commit_id': ancestor_commit_id
1209 'ancestor_commit_id': ancestor_commit_id
1182 }
1210 }
1183 renderer = RstTemplateRenderer()
1211 renderer = RstTemplateRenderer()
1184 return renderer.render('pull_request_update.mako', **params)
1212 return renderer.render('pull_request_update.mako', **params)
1185
1213
1186 def edit(self, pull_request, title, description, description_renderer, user):
1214 def edit(self, pull_request, title, description, description_renderer, user):
1187 pull_request = self.__get_pull_request(pull_request)
1215 pull_request = self.__get_pull_request(pull_request)
1188 old_data = pull_request.get_api_data(with_merge_state=False)
1216 old_data = pull_request.get_api_data(with_merge_state=False)
1189 if pull_request.is_closed():
1217 if pull_request.is_closed():
1190 raise ValueError('This pull request is closed')
1218 raise ValueError('This pull request is closed')
1191 if title:
1219 if title:
1192 pull_request.title = title
1220 pull_request.title = title
1193 pull_request.description = description
1221 pull_request.description = description
1194 pull_request.updated_on = datetime.datetime.now()
1222 pull_request.updated_on = datetime.datetime.now()
1195 pull_request.description_renderer = description_renderer
1223 pull_request.description_renderer = description_renderer
1196 Session().add(pull_request)
1224 Session().add(pull_request)
1197 self._log_audit_action(
1225 self._log_audit_action(
1198 'repo.pull_request.edit', {'old_data': old_data},
1226 'repo.pull_request.edit', {'old_data': old_data},
1199 user, pull_request)
1227 user, pull_request)
1200
1228
1201 def update_reviewers(self, pull_request, reviewer_data, user):
1229 def update_reviewers(self, pull_request, reviewer_data, user):
1202 """
1230 """
1203 Update the reviewers in the pull request
1231 Update the reviewers in the pull request
1204
1232
1205 :param pull_request: the pr to update
1233 :param pull_request: the pr to update
1206 :param reviewer_data: list of tuples
1234 :param reviewer_data: list of tuples
1207 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1235 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1236 :param user: current use who triggers this action
1208 """
1237 """
1238
1209 pull_request = self.__get_pull_request(pull_request)
1239 pull_request = self.__get_pull_request(pull_request)
1210 if pull_request.is_closed():
1240 if pull_request.is_closed():
1211 raise ValueError('This pull request is closed')
1241 raise ValueError('This pull request is closed')
1212
1242
1213 reviewers = {}
1243 reviewers = {}
1214 for user_id, reasons, mandatory, rules in reviewer_data:
1244 for user_id, reasons, mandatory, role, rules in reviewer_data:
1215 if isinstance(user_id, (int, compat.string_types)):
1245 if isinstance(user_id, (int, compat.string_types)):
1216 user_id = self._get_user(user_id).user_id
1246 user_id = self._get_user(user_id).user_id
1217 reviewers[user_id] = {
1247 reviewers[user_id] = {
1218 'reasons': reasons, 'mandatory': mandatory}
1248 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1219
1249
1220 reviewers_ids = set(reviewers.keys())
1250 reviewers_ids = set(reviewers.keys())
1221 current_reviewers = PullRequestReviewers.query()\
1251 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1222 .filter(PullRequestReviewers.pull_request ==
1252 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1223 pull_request).all()
1253
1224 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1254 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1225
1255
1226 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1256 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1227 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1257 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1228
1258
1229 log.debug("Adding %s reviewers", ids_to_add)
1259 log.debug("Adding %s reviewers", ids_to_add)
1230 log.debug("Removing %s reviewers", ids_to_remove)
1260 log.debug("Removing %s reviewers", ids_to_remove)
1231 changed = False
1261 changed = False
1232 added_audit_reviewers = []
1262 added_audit_reviewers = []
1233 removed_audit_reviewers = []
1263 removed_audit_reviewers = []
1234
1264
1235 for uid in ids_to_add:
1265 for uid in ids_to_add:
1236 changed = True
1266 changed = True
1237 _usr = self._get_user(uid)
1267 _usr = self._get_user(uid)
1238 reviewer = PullRequestReviewers()
1268 reviewer = PullRequestReviewers()
1239 reviewer.user = _usr
1269 reviewer.user = _usr
1240 reviewer.pull_request = pull_request
1270 reviewer.pull_request = pull_request
1241 reviewer.reasons = reviewers[uid]['reasons']
1271 reviewer.reasons = reviewers[uid]['reasons']
1242 # NOTE(marcink): mandatory shouldn't be changed now
1272 # NOTE(marcink): mandatory shouldn't be changed now
1243 # reviewer.mandatory = reviewers[uid]['reasons']
1273 # reviewer.mandatory = reviewers[uid]['reasons']
1274 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1275 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1244 Session().add(reviewer)
1276 Session().add(reviewer)
1245 added_audit_reviewers.append(reviewer.get_dict())
1277 added_audit_reviewers.append(reviewer.get_dict())
1246
1278
1247 for uid in ids_to_remove:
1279 for uid in ids_to_remove:
1248 changed = True
1280 changed = True
1249 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1281 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1250 # that prevents and fixes cases that we added the same reviewer twice.
1282 # This is an edge case that handles previous state of having the same reviewer twice.
1251 # this CAN happen due to the lack of DB checks
1283 # this CAN happen due to the lack of DB checks
1252 reviewers = PullRequestReviewers.query()\
1284 reviewers = PullRequestReviewers.query()\
1253 .filter(PullRequestReviewers.user_id == uid,
1285 .filter(PullRequestReviewers.user_id == uid,
1286 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1254 PullRequestReviewers.pull_request == pull_request)\
1287 PullRequestReviewers.pull_request == pull_request)\
1255 .all()
1288 .all()
1256
1289
1257 for obj in reviewers:
1290 for obj in reviewers:
1258 added_audit_reviewers.append(obj.get_dict())
1291 added_audit_reviewers.append(obj.get_dict())
1259 Session().delete(obj)
1292 Session().delete(obj)
1260
1293
1261 if changed:
1294 if changed:
1262 Session().expire_all()
1295 Session().expire_all()
1263 pull_request.updated_on = datetime.datetime.now()
1296 pull_request.updated_on = datetime.datetime.now()
1264 Session().add(pull_request)
1297 Session().add(pull_request)
1265
1298
1266 # finally store audit logs
1299 # finally store audit logs
1267 for user_data in added_audit_reviewers:
1300 for user_data in added_audit_reviewers:
1268 self._log_audit_action(
1301 self._log_audit_action(
1269 'repo.pull_request.reviewer.add', {'data': user_data},
1302 'repo.pull_request.reviewer.add', {'data': user_data},
1270 user, pull_request)
1303 user, pull_request)
1271 for user_data in removed_audit_reviewers:
1304 for user_data in removed_audit_reviewers:
1272 self._log_audit_action(
1305 self._log_audit_action(
1273 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1306 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1274 user, pull_request)
1307 user, pull_request)
1275
1308
1276 self.notify_reviewers(pull_request, ids_to_add)
1309 self.notify_reviewers(pull_request, ids_to_add, user.get_instance())
1310 return ids_to_add, ids_to_remove
1311
1312 def update_observers(self, pull_request, observer_data, user):
1313 """
1314 Update the observers in the pull request
1315
1316 :param pull_request: the pr to update
1317 :param observer_data: list of tuples
1318 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1319 :param user: current use who triggers this action
1320 """
1321 pull_request = self.__get_pull_request(pull_request)
1322 if pull_request.is_closed():
1323 raise ValueError('This pull request is closed')
1324
1325 observers = {}
1326 for user_id, reasons, mandatory, role, rules in observer_data:
1327 if isinstance(user_id, (int, compat.string_types)):
1328 user_id = self._get_user(user_id).user_id
1329 observers[user_id] = {
1330 'reasons': reasons, 'observers': mandatory, 'role': role}
1331
1332 observers_ids = set(observers.keys())
1333 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1334 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1335
1336 current_observers_ids = set([x.user.user_id for x in current_observers])
1337
1338 ids_to_add = observers_ids.difference(current_observers_ids)
1339 ids_to_remove = current_observers_ids.difference(observers_ids)
1340
1341 log.debug("Adding %s observer", ids_to_add)
1342 log.debug("Removing %s observer", ids_to_remove)
1343 changed = False
1344 added_audit_observers = []
1345 removed_audit_observers = []
1346
1347 for uid in ids_to_add:
1348 changed = True
1349 _usr = self._get_user(uid)
1350 observer = PullRequestReviewers()
1351 observer.user = _usr
1352 observer.pull_request = pull_request
1353 observer.reasons = observers[uid]['reasons']
1354 # NOTE(marcink): mandatory shouldn't be changed now
1355 # observer.mandatory = observer[uid]['reasons']
1356
1357 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1358 observer.role = PullRequestReviewers.ROLE_OBSERVER
1359 Session().add(observer)
1360 added_audit_observers.append(observer.get_dict())
1361
1362 for uid in ids_to_remove:
1363 changed = True
1364 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1365 # This is an edge case that handles previous state of having the same reviewer twice.
1366 # this CAN happen due to the lack of DB checks
1367 observers = PullRequestReviewers.query()\
1368 .filter(PullRequestReviewers.user_id == uid,
1369 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1370 PullRequestReviewers.pull_request == pull_request)\
1371 .all()
1372
1373 for obj in observers:
1374 added_audit_observers.append(obj.get_dict())
1375 Session().delete(obj)
1376
1377 if changed:
1378 Session().expire_all()
1379 pull_request.updated_on = datetime.datetime.now()
1380 Session().add(pull_request)
1381
1382 # finally store audit logs
1383 for user_data in added_audit_observers:
1384 self._log_audit_action(
1385 'repo.pull_request.observer.add', {'data': user_data},
1386 user, pull_request)
1387 for user_data in removed_audit_observers:
1388 self._log_audit_action(
1389 'repo.pull_request.observer.delete', {'old_data': user_data},
1390 user, pull_request)
1391
1392 self.notify_observers(pull_request, ids_to_add, user.get_instance())
1277 return ids_to_add, ids_to_remove
1393 return ids_to_add, ids_to_remove
1278
1394
1279 def get_url(self, pull_request, request=None, permalink=False):
1395 def get_url(self, pull_request, request=None, permalink=False):
1280 if not request:
1396 if not request:
1281 request = get_current_request()
1397 request = get_current_request()
1282
1398
1283 if permalink:
1399 if permalink:
1284 return request.route_url(
1400 return request.route_url(
1285 'pull_requests_global',
1401 'pull_requests_global',
1286 pull_request_id=pull_request.pull_request_id,)
1402 pull_request_id=pull_request.pull_request_id,)
1287 else:
1403 else:
1288 return request.route_url('pullrequest_show',
1404 return request.route_url('pullrequest_show',
1289 repo_name=safe_str(pull_request.target_repo.repo_name),
1405 repo_name=safe_str(pull_request.target_repo.repo_name),
1290 pull_request_id=pull_request.pull_request_id,)
1406 pull_request_id=pull_request.pull_request_id,)
1291
1407
1292 def get_shadow_clone_url(self, pull_request, request=None):
1408 def get_shadow_clone_url(self, pull_request, request=None):
1293 """
1409 """
1294 Returns qualified url pointing to the shadow repository. If this pull
1410 Returns qualified url pointing to the shadow repository. If this pull
1295 request is closed there is no shadow repository and ``None`` will be
1411 request is closed there is no shadow repository and ``None`` will be
1296 returned.
1412 returned.
1297 """
1413 """
1298 if pull_request.is_closed():
1414 if pull_request.is_closed():
1299 return None
1415 return None
1300 else:
1416 else:
1301 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1417 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1302 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1418 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1303
1419
1304 def notify_reviewers(self, pull_request, reviewers_ids):
1420 def _notify_reviewers(self, pull_request, user_ids, role, user):
1305 # notification to reviewers
1421 # notification to reviewers/observers
1306 if not reviewers_ids:
1422 if not user_ids:
1307 return
1423 return
1308
1424
1309 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1425 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1310
1426
1311 pull_request_obj = pull_request
1427 pull_request_obj = pull_request
1312 # get the current participants of this pull request
1428 # get the current participants of this pull request
1313 recipients = reviewers_ids
1429 recipients = user_ids
1314 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1430 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1315
1431
1316 pr_source_repo = pull_request_obj.source_repo
1432 pr_source_repo = pull_request_obj.source_repo
1317 pr_target_repo = pull_request_obj.target_repo
1433 pr_target_repo = pull_request_obj.target_repo
1318
1434
1319 pr_url = h.route_url('pullrequest_show',
1435 pr_url = h.route_url('pullrequest_show',
1320 repo_name=pr_target_repo.repo_name,
1436 repo_name=pr_target_repo.repo_name,
1321 pull_request_id=pull_request_obj.pull_request_id,)
1437 pull_request_id=pull_request_obj.pull_request_id,)
1322
1438
1323 # set some variables for email notification
1439 # set some variables for email notification
1324 pr_target_repo_url = h.route_url(
1440 pr_target_repo_url = h.route_url(
1325 'repo_summary', repo_name=pr_target_repo.repo_name)
1441 'repo_summary', repo_name=pr_target_repo.repo_name)
1326
1442
1327 pr_source_repo_url = h.route_url(
1443 pr_source_repo_url = h.route_url(
1328 'repo_summary', repo_name=pr_source_repo.repo_name)
1444 'repo_summary', repo_name=pr_source_repo.repo_name)
1329
1445
1330 # pull request specifics
1446 # pull request specifics
1331 pull_request_commits = [
1447 pull_request_commits = [
1332 (x.raw_id, x.message)
1448 (x.raw_id, x.message)
1333 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1449 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1334
1450
1451 current_rhodecode_user = user
1335 kwargs = {
1452 kwargs = {
1336 'user': pull_request.author,
1453 'user': current_rhodecode_user,
1454 'pull_request_author': pull_request.author,
1337 'pull_request': pull_request_obj,
1455 'pull_request': pull_request_obj,
1338 'pull_request_commits': pull_request_commits,
1456 'pull_request_commits': pull_request_commits,
1339
1457
1340 'pull_request_target_repo': pr_target_repo,
1458 'pull_request_target_repo': pr_target_repo,
1341 'pull_request_target_repo_url': pr_target_repo_url,
1459 'pull_request_target_repo_url': pr_target_repo_url,
1342
1460
1343 'pull_request_source_repo': pr_source_repo,
1461 'pull_request_source_repo': pr_source_repo,
1344 'pull_request_source_repo_url': pr_source_repo_url,
1462 'pull_request_source_repo_url': pr_source_repo_url,
1345
1463
1346 'pull_request_url': pr_url,
1464 'pull_request_url': pr_url,
1347 'thread_ids': [pr_url],
1465 'thread_ids': [pr_url],
1466 'user_role': role
1348 }
1467 }
1349
1468
1350 # pre-generate the subject for notification itself
1469 # pre-generate the subject for notification itself
1351 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1470 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1352 notification_type, **kwargs)
1471 notification_type, **kwargs)
1353
1472
1354 # create notification objects, and emails
1473 # create notification objects, and emails
1355 NotificationModel().create(
1474 NotificationModel().create(
1356 created_by=pull_request.author,
1475 created_by=current_rhodecode_user,
1357 notification_subject=subject,
1476 notification_subject=subject,
1358 notification_body=body_plaintext,
1477 notification_body=body_plaintext,
1359 notification_type=notification_type,
1478 notification_type=notification_type,
1360 recipients=recipients,
1479 recipients=recipients,
1361 email_kwargs=kwargs,
1480 email_kwargs=kwargs,
1362 )
1481 )
1363
1482
1483 def notify_reviewers(self, pull_request, reviewers_ids, user):
1484 return self._notify_reviewers(pull_request, reviewers_ids,
1485 PullRequestReviewers.ROLE_REVIEWER, user)
1486
1487 def notify_observers(self, pull_request, observers_ids, user):
1488 return self._notify_reviewers(pull_request, observers_ids,
1489 PullRequestReviewers.ROLE_OBSERVER, user)
1490
1364 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1491 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1365 commit_changes, file_changes):
1492 commit_changes, file_changes):
1366
1493
1367 updating_user_id = updating_user.user_id
1494 updating_user_id = updating_user.user_id
1368 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1495 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1369 # NOTE(marcink): send notification to all other users except to
1496 # NOTE(marcink): send notification to all other users except to
1370 # person who updated the PR
1497 # person who updated the PR
1371 recipients = reviewers.difference(set([updating_user_id]))
1498 recipients = reviewers.difference(set([updating_user_id]))
1372
1499
1373 log.debug('Notify following recipients about pull-request update %s', recipients)
1500 log.debug('Notify following recipients about pull-request update %s', recipients)
1374
1501
1375 pull_request_obj = pull_request
1502 pull_request_obj = pull_request
1376
1503
1377 # send email about the update
1504 # send email about the update
1378 changed_files = (
1505 changed_files = (
1379 file_changes.added + file_changes.modified + file_changes.removed)
1506 file_changes.added + file_changes.modified + file_changes.removed)
1380
1507
1381 pr_source_repo = pull_request_obj.source_repo
1508 pr_source_repo = pull_request_obj.source_repo
1382 pr_target_repo = pull_request_obj.target_repo
1509 pr_target_repo = pull_request_obj.target_repo
1383
1510
1384 pr_url = h.route_url('pullrequest_show',
1511 pr_url = h.route_url('pullrequest_show',
1385 repo_name=pr_target_repo.repo_name,
1512 repo_name=pr_target_repo.repo_name,
1386 pull_request_id=pull_request_obj.pull_request_id,)
1513 pull_request_id=pull_request_obj.pull_request_id,)
1387
1514
1388 # set some variables for email notification
1515 # set some variables for email notification
1389 pr_target_repo_url = h.route_url(
1516 pr_target_repo_url = h.route_url(
1390 'repo_summary', repo_name=pr_target_repo.repo_name)
1517 'repo_summary', repo_name=pr_target_repo.repo_name)
1391
1518
1392 pr_source_repo_url = h.route_url(
1519 pr_source_repo_url = h.route_url(
1393 'repo_summary', repo_name=pr_source_repo.repo_name)
1520 'repo_summary', repo_name=pr_source_repo.repo_name)
1394
1521
1395 email_kwargs = {
1522 email_kwargs = {
1396 'date': datetime.datetime.now(),
1523 'date': datetime.datetime.now(),
1397 'updating_user': updating_user,
1524 'updating_user': updating_user,
1398
1525
1399 'pull_request': pull_request_obj,
1526 'pull_request': pull_request_obj,
1400
1527
1401 'pull_request_target_repo': pr_target_repo,
1528 'pull_request_target_repo': pr_target_repo,
1402 'pull_request_target_repo_url': pr_target_repo_url,
1529 'pull_request_target_repo_url': pr_target_repo_url,
1403
1530
1404 'pull_request_source_repo': pr_source_repo,
1531 'pull_request_source_repo': pr_source_repo,
1405 'pull_request_source_repo_url': pr_source_repo_url,
1532 'pull_request_source_repo_url': pr_source_repo_url,
1406
1533
1407 'pull_request_url': pr_url,
1534 'pull_request_url': pr_url,
1408
1535
1409 'ancestor_commit_id': ancestor_commit_id,
1536 'ancestor_commit_id': ancestor_commit_id,
1410 'added_commits': commit_changes.added,
1537 'added_commits': commit_changes.added,
1411 'removed_commits': commit_changes.removed,
1538 'removed_commits': commit_changes.removed,
1412 'changed_files': changed_files,
1539 'changed_files': changed_files,
1413 'added_files': file_changes.added,
1540 'added_files': file_changes.added,
1414 'modified_files': file_changes.modified,
1541 'modified_files': file_changes.modified,
1415 'removed_files': file_changes.removed,
1542 'removed_files': file_changes.removed,
1416 'thread_ids': [pr_url],
1543 'thread_ids': [pr_url],
1417 }
1544 }
1418
1545
1419 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1546 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1420 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1547 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1421
1548
1422 # create notification objects, and emails
1549 # create notification objects, and emails
1423 NotificationModel().create(
1550 NotificationModel().create(
1424 created_by=updating_user,
1551 created_by=updating_user,
1425 notification_subject=subject,
1552 notification_subject=subject,
1426 notification_body=body_plaintext,
1553 notification_body=body_plaintext,
1427 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1554 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1428 recipients=recipients,
1555 recipients=recipients,
1429 email_kwargs=email_kwargs,
1556 email_kwargs=email_kwargs,
1430 )
1557 )
1431
1558
1432 def delete(self, pull_request, user=None):
1559 def delete(self, pull_request, user=None):
1433 if not user:
1560 if not user:
1434 user = getattr(get_current_rhodecode_user(), 'username', None)
1561 user = getattr(get_current_rhodecode_user(), 'username', None)
1435
1562
1436 pull_request = self.__get_pull_request(pull_request)
1563 pull_request = self.__get_pull_request(pull_request)
1437 old_data = pull_request.get_api_data(with_merge_state=False)
1564 old_data = pull_request.get_api_data(with_merge_state=False)
1438 self._cleanup_merge_workspace(pull_request)
1565 self._cleanup_merge_workspace(pull_request)
1439 self._log_audit_action(
1566 self._log_audit_action(
1440 'repo.pull_request.delete', {'old_data': old_data},
1567 'repo.pull_request.delete', {'old_data': old_data},
1441 user, pull_request)
1568 user, pull_request)
1442 Session().delete(pull_request)
1569 Session().delete(pull_request)
1443
1570
1444 def close_pull_request(self, pull_request, user):
1571 def close_pull_request(self, pull_request, user):
1445 pull_request = self.__get_pull_request(pull_request)
1572 pull_request = self.__get_pull_request(pull_request)
1446 self._cleanup_merge_workspace(pull_request)
1573 self._cleanup_merge_workspace(pull_request)
1447 pull_request.status = PullRequest.STATUS_CLOSED
1574 pull_request.status = PullRequest.STATUS_CLOSED
1448 pull_request.updated_on = datetime.datetime.now()
1575 pull_request.updated_on = datetime.datetime.now()
1449 Session().add(pull_request)
1576 Session().add(pull_request)
1450 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1577 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1451
1578
1452 pr_data = pull_request.get_api_data(with_merge_state=False)
1579 pr_data = pull_request.get_api_data(with_merge_state=False)
1453 self._log_audit_action(
1580 self._log_audit_action(
1454 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1581 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1455
1582
1456 def close_pull_request_with_comment(
1583 def close_pull_request_with_comment(
1457 self, pull_request, user, repo, message=None, auth_user=None):
1584 self, pull_request, user, repo, message=None, auth_user=None):
1458
1585
1459 pull_request_review_status = pull_request.calculated_review_status()
1586 pull_request_review_status = pull_request.calculated_review_status()
1460
1587
1461 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1588 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1462 # approved only if we have voting consent
1589 # approved only if we have voting consent
1463 status = ChangesetStatus.STATUS_APPROVED
1590 status = ChangesetStatus.STATUS_APPROVED
1464 else:
1591 else:
1465 status = ChangesetStatus.STATUS_REJECTED
1592 status = ChangesetStatus.STATUS_REJECTED
1466 status_lbl = ChangesetStatus.get_status_lbl(status)
1593 status_lbl = ChangesetStatus.get_status_lbl(status)
1467
1594
1468 default_message = (
1595 default_message = (
1469 'Closing with status change {transition_icon} {status}.'
1596 'Closing with status change {transition_icon} {status}.'
1470 ).format(transition_icon='>', status=status_lbl)
1597 ).format(transition_icon='>', status=status_lbl)
1471 text = message or default_message
1598 text = message or default_message
1472
1599
1473 # create a comment, and link it to new status
1600 # create a comment, and link it to new status
1474 comment = CommentsModel().create(
1601 comment = CommentsModel().create(
1475 text=text,
1602 text=text,
1476 repo=repo.repo_id,
1603 repo=repo.repo_id,
1477 user=user.user_id,
1604 user=user.user_id,
1478 pull_request=pull_request.pull_request_id,
1605 pull_request=pull_request.pull_request_id,
1479 status_change=status_lbl,
1606 status_change=status_lbl,
1480 status_change_type=status,
1607 status_change_type=status,
1481 closing_pr=True,
1608 closing_pr=True,
1482 auth_user=auth_user,
1609 auth_user=auth_user,
1483 )
1610 )
1484
1611
1485 # calculate old status before we change it
1612 # calculate old status before we change it
1486 old_calculated_status = pull_request.calculated_review_status()
1613 old_calculated_status = pull_request.calculated_review_status()
1487 ChangesetStatusModel().set_status(
1614 ChangesetStatusModel().set_status(
1488 repo.repo_id,
1615 repo.repo_id,
1489 status,
1616 status,
1490 user.user_id,
1617 user.user_id,
1491 comment=comment,
1618 comment=comment,
1492 pull_request=pull_request.pull_request_id
1619 pull_request=pull_request.pull_request_id
1493 )
1620 )
1494
1621
1495 Session().flush()
1622 Session().flush()
1496
1623
1497 self.trigger_pull_request_hook(pull_request, user, 'comment',
1624 self.trigger_pull_request_hook(pull_request, user, 'comment',
1498 data={'comment': comment})
1625 data={'comment': comment})
1499
1626
1500 # we now calculate the status of pull request again, and based on that
1627 # we now calculate the status of pull request again, and based on that
1501 # calculation trigger status change. This might happen in cases
1628 # calculation trigger status change. This might happen in cases
1502 # that non-reviewer admin closes a pr, which means his vote doesn't
1629 # that non-reviewer admin closes a pr, which means his vote doesn't
1503 # change the status, while if he's a reviewer this might change it.
1630 # change the status, while if he's a reviewer this might change it.
1504 calculated_status = pull_request.calculated_review_status()
1631 calculated_status = pull_request.calculated_review_status()
1505 if old_calculated_status != calculated_status:
1632 if old_calculated_status != calculated_status:
1506 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1633 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1507 data={'status': calculated_status})
1634 data={'status': calculated_status})
1508
1635
1509 # finally close the PR
1636 # finally close the PR
1510 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1637 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1511
1638
1512 return comment, status
1639 return comment, status
1513
1640
1514 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1641 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1515 _ = translator or get_current_request().translate
1642 _ = translator or get_current_request().translate
1516
1643
1517 if not self._is_merge_enabled(pull_request):
1644 if not self._is_merge_enabled(pull_request):
1518 return None, False, _('Server-side pull request merging is disabled.')
1645 return None, False, _('Server-side pull request merging is disabled.')
1519
1646
1520 if pull_request.is_closed():
1647 if pull_request.is_closed():
1521 return None, False, _('This pull request is closed.')
1648 return None, False, _('This pull request is closed.')
1522
1649
1523 merge_possible, msg = self._check_repo_requirements(
1650 merge_possible, msg = self._check_repo_requirements(
1524 target=pull_request.target_repo, source=pull_request.source_repo,
1651 target=pull_request.target_repo, source=pull_request.source_repo,
1525 translator=_)
1652 translator=_)
1526 if not merge_possible:
1653 if not merge_possible:
1527 return None, merge_possible, msg
1654 return None, merge_possible, msg
1528
1655
1529 try:
1656 try:
1530 merge_response = self._try_merge(
1657 merge_response = self._try_merge(
1531 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1658 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1532 log.debug("Merge response: %s", merge_response)
1659 log.debug("Merge response: %s", merge_response)
1533 return merge_response, merge_response.possible, merge_response.merge_status_message
1660 return merge_response, merge_response.possible, merge_response.merge_status_message
1534 except NotImplementedError:
1661 except NotImplementedError:
1535 return None, False, _('Pull request merging is not supported.')
1662 return None, False, _('Pull request merging is not supported.')
1536
1663
1537 def _check_repo_requirements(self, target, source, translator):
1664 def _check_repo_requirements(self, target, source, translator):
1538 """
1665 """
1539 Check if `target` and `source` have compatible requirements.
1666 Check if `target` and `source` have compatible requirements.
1540
1667
1541 Currently this is just checking for largefiles.
1668 Currently this is just checking for largefiles.
1542 """
1669 """
1543 _ = translator
1670 _ = translator
1544 target_has_largefiles = self._has_largefiles(target)
1671 target_has_largefiles = self._has_largefiles(target)
1545 source_has_largefiles = self._has_largefiles(source)
1672 source_has_largefiles = self._has_largefiles(source)
1546 merge_possible = True
1673 merge_possible = True
1547 message = u''
1674 message = u''
1548
1675
1549 if target_has_largefiles != source_has_largefiles:
1676 if target_has_largefiles != source_has_largefiles:
1550 merge_possible = False
1677 merge_possible = False
1551 if source_has_largefiles:
1678 if source_has_largefiles:
1552 message = _(
1679 message = _(
1553 'Target repository large files support is disabled.')
1680 'Target repository large files support is disabled.')
1554 else:
1681 else:
1555 message = _(
1682 message = _(
1556 'Source repository large files support is disabled.')
1683 'Source repository large files support is disabled.')
1557
1684
1558 return merge_possible, message
1685 return merge_possible, message
1559
1686
1560 def _has_largefiles(self, repo):
1687 def _has_largefiles(self, repo):
1561 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1688 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1562 'extensions', 'largefiles')
1689 'extensions', 'largefiles')
1563 return largefiles_ui and largefiles_ui[0].active
1690 return largefiles_ui and largefiles_ui[0].active
1564
1691
1565 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1692 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1566 """
1693 """
1567 Try to merge the pull request and return the merge status.
1694 Try to merge the pull request and return the merge status.
1568 """
1695 """
1569 log.debug(
1696 log.debug(
1570 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1697 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1571 pull_request.pull_request_id, force_shadow_repo_refresh)
1698 pull_request.pull_request_id, force_shadow_repo_refresh)
1572 target_vcs = pull_request.target_repo.scm_instance()
1699 target_vcs = pull_request.target_repo.scm_instance()
1573 # Refresh the target reference.
1700 # Refresh the target reference.
1574 try:
1701 try:
1575 target_ref = self._refresh_reference(
1702 target_ref = self._refresh_reference(
1576 pull_request.target_ref_parts, target_vcs)
1703 pull_request.target_ref_parts, target_vcs)
1577 except CommitDoesNotExistError:
1704 except CommitDoesNotExistError:
1578 merge_state = MergeResponse(
1705 merge_state = MergeResponse(
1579 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1706 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1580 metadata={'target_ref': pull_request.target_ref_parts})
1707 metadata={'target_ref': pull_request.target_ref_parts})
1581 return merge_state
1708 return merge_state
1582
1709
1583 target_locked = pull_request.target_repo.locked
1710 target_locked = pull_request.target_repo.locked
1584 if target_locked and target_locked[0]:
1711 if target_locked and target_locked[0]:
1585 locked_by = 'user:{}'.format(target_locked[0])
1712 locked_by = 'user:{}'.format(target_locked[0])
1586 log.debug("The target repository is locked by %s.", locked_by)
1713 log.debug("The target repository is locked by %s.", locked_by)
1587 merge_state = MergeResponse(
1714 merge_state = MergeResponse(
1588 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1715 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1589 metadata={'locked_by': locked_by})
1716 metadata={'locked_by': locked_by})
1590 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1717 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1591 pull_request, target_ref):
1718 pull_request, target_ref):
1592 log.debug("Refreshing the merge status of the repository.")
1719 log.debug("Refreshing the merge status of the repository.")
1593 merge_state = self._refresh_merge_state(
1720 merge_state = self._refresh_merge_state(
1594 pull_request, target_vcs, target_ref)
1721 pull_request, target_vcs, target_ref)
1595 else:
1722 else:
1596 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1723 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1597 metadata = {
1724 metadata = {
1598 'unresolved_files': '',
1725 'unresolved_files': '',
1599 'target_ref': pull_request.target_ref_parts,
1726 'target_ref': pull_request.target_ref_parts,
1600 'source_ref': pull_request.source_ref_parts,
1727 'source_ref': pull_request.source_ref_parts,
1601 }
1728 }
1602 if pull_request.last_merge_metadata:
1729 if pull_request.last_merge_metadata:
1603 metadata.update(pull_request.last_merge_metadata_parsed)
1730 metadata.update(pull_request.last_merge_metadata_parsed)
1604
1731
1605 if not possible and target_ref.type == 'branch':
1732 if not possible and target_ref.type == 'branch':
1606 # NOTE(marcink): case for mercurial multiple heads on branch
1733 # NOTE(marcink): case for mercurial multiple heads on branch
1607 heads = target_vcs._heads(target_ref.name)
1734 heads = target_vcs._heads(target_ref.name)
1608 if len(heads) != 1:
1735 if len(heads) != 1:
1609 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1736 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1610 metadata.update({
1737 metadata.update({
1611 'heads': heads
1738 'heads': heads
1612 })
1739 })
1613
1740
1614 merge_state = MergeResponse(
1741 merge_state = MergeResponse(
1615 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1742 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1616
1743
1617 return merge_state
1744 return merge_state
1618
1745
1619 def _refresh_reference(self, reference, vcs_repository):
1746 def _refresh_reference(self, reference, vcs_repository):
1620 if reference.type in self.UPDATABLE_REF_TYPES:
1747 if reference.type in self.UPDATABLE_REF_TYPES:
1621 name_or_id = reference.name
1748 name_or_id = reference.name
1622 else:
1749 else:
1623 name_or_id = reference.commit_id
1750 name_or_id = reference.commit_id
1624
1751
1625 refreshed_commit = vcs_repository.get_commit(name_or_id)
1752 refreshed_commit = vcs_repository.get_commit(name_or_id)
1626 refreshed_reference = Reference(
1753 refreshed_reference = Reference(
1627 reference.type, reference.name, refreshed_commit.raw_id)
1754 reference.type, reference.name, refreshed_commit.raw_id)
1628 return refreshed_reference
1755 return refreshed_reference
1629
1756
1630 def _needs_merge_state_refresh(self, pull_request, target_reference):
1757 def _needs_merge_state_refresh(self, pull_request, target_reference):
1631 return not(
1758 return not(
1632 pull_request.revisions and
1759 pull_request.revisions and
1633 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1760 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1634 target_reference.commit_id == pull_request._last_merge_target_rev)
1761 target_reference.commit_id == pull_request._last_merge_target_rev)
1635
1762
1636 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1763 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1637 workspace_id = self._workspace_id(pull_request)
1764 workspace_id = self._workspace_id(pull_request)
1638 source_vcs = pull_request.source_repo.scm_instance()
1765 source_vcs = pull_request.source_repo.scm_instance()
1639 repo_id = pull_request.target_repo.repo_id
1766 repo_id = pull_request.target_repo.repo_id
1640 use_rebase = self._use_rebase_for_merging(pull_request)
1767 use_rebase = self._use_rebase_for_merging(pull_request)
1641 close_branch = self._close_branch_before_merging(pull_request)
1768 close_branch = self._close_branch_before_merging(pull_request)
1642 merge_state = target_vcs.merge(
1769 merge_state = target_vcs.merge(
1643 repo_id, workspace_id,
1770 repo_id, workspace_id,
1644 target_reference, source_vcs, pull_request.source_ref_parts,
1771 target_reference, source_vcs, pull_request.source_ref_parts,
1645 dry_run=True, use_rebase=use_rebase,
1772 dry_run=True, use_rebase=use_rebase,
1646 close_branch=close_branch)
1773 close_branch=close_branch)
1647
1774
1648 # Do not store the response if there was an unknown error.
1775 # Do not store the response if there was an unknown error.
1649 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1776 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1650 pull_request._last_merge_source_rev = \
1777 pull_request._last_merge_source_rev = \
1651 pull_request.source_ref_parts.commit_id
1778 pull_request.source_ref_parts.commit_id
1652 pull_request._last_merge_target_rev = target_reference.commit_id
1779 pull_request._last_merge_target_rev = target_reference.commit_id
1653 pull_request.last_merge_status = merge_state.failure_reason
1780 pull_request.last_merge_status = merge_state.failure_reason
1654 pull_request.last_merge_metadata = merge_state.metadata
1781 pull_request.last_merge_metadata = merge_state.metadata
1655
1782
1656 pull_request.shadow_merge_ref = merge_state.merge_ref
1783 pull_request.shadow_merge_ref = merge_state.merge_ref
1657 Session().add(pull_request)
1784 Session().add(pull_request)
1658 Session().commit()
1785 Session().commit()
1659
1786
1660 return merge_state
1787 return merge_state
1661
1788
1662 def _workspace_id(self, pull_request):
1789 def _workspace_id(self, pull_request):
1663 workspace_id = 'pr-%s' % pull_request.pull_request_id
1790 workspace_id = 'pr-%s' % pull_request.pull_request_id
1664 return workspace_id
1791 return workspace_id
1665
1792
1666 def generate_repo_data(self, repo, commit_id=None, branch=None,
1793 def generate_repo_data(self, repo, commit_id=None, branch=None,
1667 bookmark=None, translator=None):
1794 bookmark=None, translator=None):
1668 from rhodecode.model.repo import RepoModel
1795 from rhodecode.model.repo import RepoModel
1669
1796
1670 all_refs, selected_ref = \
1797 all_refs, selected_ref = \
1671 self._get_repo_pullrequest_sources(
1798 self._get_repo_pullrequest_sources(
1672 repo.scm_instance(), commit_id=commit_id,
1799 repo.scm_instance(), commit_id=commit_id,
1673 branch=branch, bookmark=bookmark, translator=translator)
1800 branch=branch, bookmark=bookmark, translator=translator)
1674
1801
1675 refs_select2 = []
1802 refs_select2 = []
1676 for element in all_refs:
1803 for element in all_refs:
1677 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1804 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1678 refs_select2.append({'text': element[1], 'children': children})
1805 refs_select2.append({'text': element[1], 'children': children})
1679
1806
1680 return {
1807 return {
1681 'user': {
1808 'user': {
1682 'user_id': repo.user.user_id,
1809 'user_id': repo.user.user_id,
1683 'username': repo.user.username,
1810 'username': repo.user.username,
1684 'firstname': repo.user.first_name,
1811 'firstname': repo.user.first_name,
1685 'lastname': repo.user.last_name,
1812 'lastname': repo.user.last_name,
1686 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1813 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1687 },
1814 },
1688 'name': repo.repo_name,
1815 'name': repo.repo_name,
1689 'link': RepoModel().get_url(repo),
1816 'link': RepoModel().get_url(repo),
1690 'description': h.chop_at_smart(repo.description_safe, '\n'),
1817 'description': h.chop_at_smart(repo.description_safe, '\n'),
1691 'refs': {
1818 'refs': {
1692 'all_refs': all_refs,
1819 'all_refs': all_refs,
1693 'selected_ref': selected_ref,
1820 'selected_ref': selected_ref,
1694 'select2_refs': refs_select2
1821 'select2_refs': refs_select2
1695 }
1822 }
1696 }
1823 }
1697
1824
1698 def generate_pullrequest_title(self, source, source_ref, target):
1825 def generate_pullrequest_title(self, source, source_ref, target):
1699 return u'{source}#{at_ref} to {target}'.format(
1826 return u'{source}#{at_ref} to {target}'.format(
1700 source=source,
1827 source=source,
1701 at_ref=source_ref,
1828 at_ref=source_ref,
1702 target=target,
1829 target=target,
1703 )
1830 )
1704
1831
1705 def _cleanup_merge_workspace(self, pull_request):
1832 def _cleanup_merge_workspace(self, pull_request):
1706 # Merging related cleanup
1833 # Merging related cleanup
1707 repo_id = pull_request.target_repo.repo_id
1834 repo_id = pull_request.target_repo.repo_id
1708 target_scm = pull_request.target_repo.scm_instance()
1835 target_scm = pull_request.target_repo.scm_instance()
1709 workspace_id = self._workspace_id(pull_request)
1836 workspace_id = self._workspace_id(pull_request)
1710
1837
1711 try:
1838 try:
1712 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1839 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1713 except NotImplementedError:
1840 except NotImplementedError:
1714 pass
1841 pass
1715
1842
1716 def _get_repo_pullrequest_sources(
1843 def _get_repo_pullrequest_sources(
1717 self, repo, commit_id=None, branch=None, bookmark=None,
1844 self, repo, commit_id=None, branch=None, bookmark=None,
1718 translator=None):
1845 translator=None):
1719 """
1846 """
1720 Return a structure with repo's interesting commits, suitable for
1847 Return a structure with repo's interesting commits, suitable for
1721 the selectors in pullrequest controller
1848 the selectors in pullrequest controller
1722
1849
1723 :param commit_id: a commit that must be in the list somehow
1850 :param commit_id: a commit that must be in the list somehow
1724 and selected by default
1851 and selected by default
1725 :param branch: a branch that must be in the list and selected
1852 :param branch: a branch that must be in the list and selected
1726 by default - even if closed
1853 by default - even if closed
1727 :param bookmark: a bookmark that must be in the list and selected
1854 :param bookmark: a bookmark that must be in the list and selected
1728 """
1855 """
1729 _ = translator or get_current_request().translate
1856 _ = translator or get_current_request().translate
1730
1857
1731 commit_id = safe_str(commit_id) if commit_id else None
1858 commit_id = safe_str(commit_id) if commit_id else None
1732 branch = safe_unicode(branch) if branch else None
1859 branch = safe_unicode(branch) if branch else None
1733 bookmark = safe_unicode(bookmark) if bookmark else None
1860 bookmark = safe_unicode(bookmark) if bookmark else None
1734
1861
1735 selected = None
1862 selected = None
1736
1863
1737 # order matters: first source that has commit_id in it will be selected
1864 # order matters: first source that has commit_id in it will be selected
1738 sources = []
1865 sources = []
1739 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1866 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1740 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1867 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1741
1868
1742 if commit_id:
1869 if commit_id:
1743 ref_commit = (h.short_id(commit_id), commit_id)
1870 ref_commit = (h.short_id(commit_id), commit_id)
1744 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1871 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1745
1872
1746 sources.append(
1873 sources.append(
1747 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1874 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1748 )
1875 )
1749
1876
1750 groups = []
1877 groups = []
1751
1878
1752 for group_key, ref_list, group_name, match in sources:
1879 for group_key, ref_list, group_name, match in sources:
1753 group_refs = []
1880 group_refs = []
1754 for ref_name, ref_id in ref_list:
1881 for ref_name, ref_id in ref_list:
1755 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1882 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1756 group_refs.append((ref_key, ref_name))
1883 group_refs.append((ref_key, ref_name))
1757
1884
1758 if not selected:
1885 if not selected:
1759 if set([commit_id, match]) & set([ref_id, ref_name]):
1886 if set([commit_id, match]) & set([ref_id, ref_name]):
1760 selected = ref_key
1887 selected = ref_key
1761
1888
1762 if group_refs:
1889 if group_refs:
1763 groups.append((group_refs, group_name))
1890 groups.append((group_refs, group_name))
1764
1891
1765 if not selected:
1892 if not selected:
1766 ref = commit_id or branch or bookmark
1893 ref = commit_id or branch or bookmark
1767 if ref:
1894 if ref:
1768 raise CommitDoesNotExistError(
1895 raise CommitDoesNotExistError(
1769 u'No commit refs could be found matching: {}'.format(ref))
1896 u'No commit refs could be found matching: {}'.format(ref))
1770 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1897 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1771 selected = u'branch:{}:{}'.format(
1898 selected = u'branch:{}:{}'.format(
1772 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1899 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1773 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1900 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1774 )
1901 )
1775 elif repo.commit_ids:
1902 elif repo.commit_ids:
1776 # make the user select in this case
1903 # make the user select in this case
1777 selected = None
1904 selected = None
1778 else:
1905 else:
1779 raise EmptyRepositoryError()
1906 raise EmptyRepositoryError()
1780 return groups, selected
1907 return groups, selected
1781
1908
1782 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1909 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1783 hide_whitespace_changes, diff_context):
1910 hide_whitespace_changes, diff_context):
1784
1911
1785 return self._get_diff_from_pr_or_version(
1912 return self._get_diff_from_pr_or_version(
1786 source_repo, source_ref_id, target_ref_id,
1913 source_repo, source_ref_id, target_ref_id,
1787 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1914 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1788
1915
1789 def _get_diff_from_pr_or_version(
1916 def _get_diff_from_pr_or_version(
1790 self, source_repo, source_ref_id, target_ref_id,
1917 self, source_repo, source_ref_id, target_ref_id,
1791 hide_whitespace_changes, diff_context):
1918 hide_whitespace_changes, diff_context):
1792
1919
1793 target_commit = source_repo.get_commit(
1920 target_commit = source_repo.get_commit(
1794 commit_id=safe_str(target_ref_id))
1921 commit_id=safe_str(target_ref_id))
1795 source_commit = source_repo.get_commit(
1922 source_commit = source_repo.get_commit(
1796 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1923 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1797 if isinstance(source_repo, Repository):
1924 if isinstance(source_repo, Repository):
1798 vcs_repo = source_repo.scm_instance()
1925 vcs_repo = source_repo.scm_instance()
1799 else:
1926 else:
1800 vcs_repo = source_repo
1927 vcs_repo = source_repo
1801
1928
1802 # TODO: johbo: In the context of an update, we cannot reach
1929 # TODO: johbo: In the context of an update, we cannot reach
1803 # the old commit anymore with our normal mechanisms. It needs
1930 # the old commit anymore with our normal mechanisms. It needs
1804 # some sort of special support in the vcs layer to avoid this
1931 # some sort of special support in the vcs layer to avoid this
1805 # workaround.
1932 # workaround.
1806 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1933 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1807 vcs_repo.alias == 'git'):
1934 vcs_repo.alias == 'git'):
1808 source_commit.raw_id = safe_str(source_ref_id)
1935 source_commit.raw_id = safe_str(source_ref_id)
1809
1936
1810 log.debug('calculating diff between '
1937 log.debug('calculating diff between '
1811 'source_ref:%s and target_ref:%s for repo `%s`',
1938 'source_ref:%s and target_ref:%s for repo `%s`',
1812 target_ref_id, source_ref_id,
1939 target_ref_id, source_ref_id,
1813 safe_unicode(vcs_repo.path))
1940 safe_unicode(vcs_repo.path))
1814
1941
1815 vcs_diff = vcs_repo.get_diff(
1942 vcs_diff = vcs_repo.get_diff(
1816 commit1=target_commit, commit2=source_commit,
1943 commit1=target_commit, commit2=source_commit,
1817 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1944 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1818 return vcs_diff
1945 return vcs_diff
1819
1946
1820 def _is_merge_enabled(self, pull_request):
1947 def _is_merge_enabled(self, pull_request):
1821 return self._get_general_setting(
1948 return self._get_general_setting(
1822 pull_request, 'rhodecode_pr_merge_enabled')
1949 pull_request, 'rhodecode_pr_merge_enabled')
1823
1950
1824 def _use_rebase_for_merging(self, pull_request):
1951 def _use_rebase_for_merging(self, pull_request):
1825 repo_type = pull_request.target_repo.repo_type
1952 repo_type = pull_request.target_repo.repo_type
1826 if repo_type == 'hg':
1953 if repo_type == 'hg':
1827 return self._get_general_setting(
1954 return self._get_general_setting(
1828 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1955 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1829 elif repo_type == 'git':
1956 elif repo_type == 'git':
1830 return self._get_general_setting(
1957 return self._get_general_setting(
1831 pull_request, 'rhodecode_git_use_rebase_for_merging')
1958 pull_request, 'rhodecode_git_use_rebase_for_merging')
1832
1959
1833 return False
1960 return False
1834
1961
1835 def _user_name_for_merging(self, pull_request, user):
1962 def _user_name_for_merging(self, pull_request, user):
1836 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1963 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1837 if env_user_name_attr and hasattr(user, env_user_name_attr):
1964 if env_user_name_attr and hasattr(user, env_user_name_attr):
1838 user_name_attr = env_user_name_attr
1965 user_name_attr = env_user_name_attr
1839 else:
1966 else:
1840 user_name_attr = 'short_contact'
1967 user_name_attr = 'short_contact'
1841
1968
1842 user_name = getattr(user, user_name_attr)
1969 user_name = getattr(user, user_name_attr)
1843 return user_name
1970 return user_name
1844
1971
1845 def _close_branch_before_merging(self, pull_request):
1972 def _close_branch_before_merging(self, pull_request):
1846 repo_type = pull_request.target_repo.repo_type
1973 repo_type = pull_request.target_repo.repo_type
1847 if repo_type == 'hg':
1974 if repo_type == 'hg':
1848 return self._get_general_setting(
1975 return self._get_general_setting(
1849 pull_request, 'rhodecode_hg_close_branch_before_merging')
1976 pull_request, 'rhodecode_hg_close_branch_before_merging')
1850 elif repo_type == 'git':
1977 elif repo_type == 'git':
1851 return self._get_general_setting(
1978 return self._get_general_setting(
1852 pull_request, 'rhodecode_git_close_branch_before_merging')
1979 pull_request, 'rhodecode_git_close_branch_before_merging')
1853
1980
1854 return False
1981 return False
1855
1982
1856 def _get_general_setting(self, pull_request, settings_key, default=False):
1983 def _get_general_setting(self, pull_request, settings_key, default=False):
1857 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1984 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1858 settings = settings_model.get_general_settings()
1985 settings = settings_model.get_general_settings()
1859 return settings.get(settings_key, default)
1986 return settings.get(settings_key, default)
1860
1987
1861 def _log_audit_action(self, action, action_data, user, pull_request):
1988 def _log_audit_action(self, action, action_data, user, pull_request):
1862 audit_logger.store(
1989 audit_logger.store(
1863 action=action,
1990 action=action,
1864 action_data=action_data,
1991 action_data=action_data,
1865 user=user,
1992 user=user,
1866 repo=pull_request.target_repo)
1993 repo=pull_request.target_repo)
1867
1994
1868 def get_reviewer_functions(self):
1995 def get_reviewer_functions(self):
1869 """
1996 """
1870 Fetches functions for validation and fetching default reviewers.
1997 Fetches functions for validation and fetching default reviewers.
1871 If available we use the EE package, else we fallback to CE
1998 If available we use the EE package, else we fallback to CE
1872 package functions
1999 package functions
1873 """
2000 """
1874 try:
2001 try:
1875 from rc_reviewers.utils import get_default_reviewers_data
2002 from rc_reviewers.utils import get_default_reviewers_data
1876 from rc_reviewers.utils import validate_default_reviewers
2003 from rc_reviewers.utils import validate_default_reviewers
2004 from rc_reviewers.utils import validate_observers
1877 except ImportError:
2005 except ImportError:
1878 from rhodecode.apps.repository.utils import get_default_reviewers_data
2006 from rhodecode.apps.repository.utils import get_default_reviewers_data
1879 from rhodecode.apps.repository.utils import validate_default_reviewers
2007 from rhodecode.apps.repository.utils import validate_default_reviewers
2008 from rhodecode.apps.repository.utils import validate_observers
1880
2009
1881 return get_default_reviewers_data, validate_default_reviewers
2010 return get_default_reviewers_data, validate_default_reviewers, validate_observers
1882
2011
1883
2012
1884 class MergeCheck(object):
2013 class MergeCheck(object):
1885 """
2014 """
1886 Perform Merge Checks and returns a check object which stores information
2015 Perform Merge Checks and returns a check object which stores information
1887 about merge errors, and merge conditions
2016 about merge errors, and merge conditions
1888 """
2017 """
1889 TODO_CHECK = 'todo'
2018 TODO_CHECK = 'todo'
1890 PERM_CHECK = 'perm'
2019 PERM_CHECK = 'perm'
1891 REVIEW_CHECK = 'review'
2020 REVIEW_CHECK = 'review'
1892 MERGE_CHECK = 'merge'
2021 MERGE_CHECK = 'merge'
1893 WIP_CHECK = 'wip'
2022 WIP_CHECK = 'wip'
1894
2023
1895 def __init__(self):
2024 def __init__(self):
1896 self.review_status = None
2025 self.review_status = None
1897 self.merge_possible = None
2026 self.merge_possible = None
1898 self.merge_msg = ''
2027 self.merge_msg = ''
1899 self.merge_response = None
2028 self.merge_response = None
1900 self.failed = None
2029 self.failed = None
1901 self.errors = []
2030 self.errors = []
1902 self.error_details = OrderedDict()
2031 self.error_details = OrderedDict()
1903 self.source_commit = AttributeDict()
2032 self.source_commit = AttributeDict()
1904 self.target_commit = AttributeDict()
2033 self.target_commit = AttributeDict()
1905
2034
1906 def __repr__(self):
2035 def __repr__(self):
1907 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2036 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
1908 self.merge_possible, self.failed, self.errors)
2037 self.merge_possible, self.failed, self.errors)
1909
2038
1910 def push_error(self, error_type, message, error_key, details):
2039 def push_error(self, error_type, message, error_key, details):
1911 self.failed = True
2040 self.failed = True
1912 self.errors.append([error_type, message])
2041 self.errors.append([error_type, message])
1913 self.error_details[error_key] = dict(
2042 self.error_details[error_key] = dict(
1914 details=details,
2043 details=details,
1915 error_type=error_type,
2044 error_type=error_type,
1916 message=message
2045 message=message
1917 )
2046 )
1918
2047
1919 @classmethod
2048 @classmethod
1920 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2049 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1921 force_shadow_repo_refresh=False):
2050 force_shadow_repo_refresh=False):
1922 _ = translator
2051 _ = translator
1923 merge_check = cls()
2052 merge_check = cls()
1924
2053
1925 # title has WIP:
2054 # title has WIP:
1926 if pull_request.work_in_progress:
2055 if pull_request.work_in_progress:
1927 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2056 log.debug("MergeCheck: cannot merge, title has wip: marker.")
1928
2057
1929 msg = _('WIP marker in title prevents from accidental merge.')
2058 msg = _('WIP marker in title prevents from accidental merge.')
1930 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2059 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
1931 if fail_early:
2060 if fail_early:
1932 return merge_check
2061 return merge_check
1933
2062
1934 # permissions to merge
2063 # permissions to merge
1935 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2064 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
1936 if not user_allowed_to_merge:
2065 if not user_allowed_to_merge:
1937 log.debug("MergeCheck: cannot merge, approval is pending.")
2066 log.debug("MergeCheck: cannot merge, approval is pending.")
1938
2067
1939 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2068 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1940 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2069 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1941 if fail_early:
2070 if fail_early:
1942 return merge_check
2071 return merge_check
1943
2072
1944 # permission to merge into the target branch
2073 # permission to merge into the target branch
1945 target_commit_id = pull_request.target_ref_parts.commit_id
2074 target_commit_id = pull_request.target_ref_parts.commit_id
1946 if pull_request.target_ref_parts.type == 'branch':
2075 if pull_request.target_ref_parts.type == 'branch':
1947 branch_name = pull_request.target_ref_parts.name
2076 branch_name = pull_request.target_ref_parts.name
1948 else:
2077 else:
1949 # for mercurial we can always figure out the branch from the commit
2078 # for mercurial we can always figure out the branch from the commit
1950 # in case of bookmark
2079 # in case of bookmark
1951 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2080 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1952 branch_name = target_commit.branch
2081 branch_name = target_commit.branch
1953
2082
1954 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2083 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1955 pull_request.target_repo.repo_name, branch_name)
2084 pull_request.target_repo.repo_name, branch_name)
1956 if branch_perm and branch_perm == 'branch.none':
2085 if branch_perm and branch_perm == 'branch.none':
1957 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2086 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1958 branch_name, rule)
2087 branch_name, rule)
1959 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2088 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1960 if fail_early:
2089 if fail_early:
1961 return merge_check
2090 return merge_check
1962
2091
1963 # review status, must be always present
2092 # review status, must be always present
1964 review_status = pull_request.calculated_review_status()
2093 review_status = pull_request.calculated_review_status()
1965 merge_check.review_status = review_status
2094 merge_check.review_status = review_status
1966
2095
1967 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2096 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1968 if not status_approved:
2097 if not status_approved:
1969 log.debug("MergeCheck: cannot merge, approval is pending.")
2098 log.debug("MergeCheck: cannot merge, approval is pending.")
1970
2099
1971 msg = _('Pull request reviewer approval is pending.')
2100 msg = _('Pull request reviewer approval is pending.')
1972
2101
1973 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2102 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1974
2103
1975 if fail_early:
2104 if fail_early:
1976 return merge_check
2105 return merge_check
1977
2106
1978 # left over TODOs
2107 # left over TODOs
1979 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2108 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1980 if todos:
2109 if todos:
1981 log.debug("MergeCheck: cannot merge, {} "
2110 log.debug("MergeCheck: cannot merge, {} "
1982 "unresolved TODOs left.".format(len(todos)))
2111 "unresolved TODOs left.".format(len(todos)))
1983
2112
1984 if len(todos) == 1:
2113 if len(todos) == 1:
1985 msg = _('Cannot merge, {} TODO still not resolved.').format(
2114 msg = _('Cannot merge, {} TODO still not resolved.').format(
1986 len(todos))
2115 len(todos))
1987 else:
2116 else:
1988 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2117 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1989 len(todos))
2118 len(todos))
1990
2119
1991 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2120 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1992
2121
1993 if fail_early:
2122 if fail_early:
1994 return merge_check
2123 return merge_check
1995
2124
1996 # merge possible, here is the filesystem simulation + shadow repo
2125 # merge possible, here is the filesystem simulation + shadow repo
1997 merge_response, merge_status, msg = PullRequestModel().merge_status(
2126 merge_response, merge_status, msg = PullRequestModel().merge_status(
1998 pull_request, translator=translator,
2127 pull_request, translator=translator,
1999 force_shadow_repo_refresh=force_shadow_repo_refresh)
2128 force_shadow_repo_refresh=force_shadow_repo_refresh)
2000
2129
2001 merge_check.merge_possible = merge_status
2130 merge_check.merge_possible = merge_status
2002 merge_check.merge_msg = msg
2131 merge_check.merge_msg = msg
2003 merge_check.merge_response = merge_response
2132 merge_check.merge_response = merge_response
2004
2133
2005 source_ref_id = pull_request.source_ref_parts.commit_id
2134 source_ref_id = pull_request.source_ref_parts.commit_id
2006 target_ref_id = pull_request.target_ref_parts.commit_id
2135 target_ref_id = pull_request.target_ref_parts.commit_id
2007
2136
2008 try:
2137 try:
2009 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2138 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2010 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2139 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2011 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2140 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2012 merge_check.source_commit.current_raw_id = source_commit.raw_id
2141 merge_check.source_commit.current_raw_id = source_commit.raw_id
2013 merge_check.source_commit.previous_raw_id = source_ref_id
2142 merge_check.source_commit.previous_raw_id = source_ref_id
2014
2143
2015 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2144 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2016 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2145 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2017 merge_check.target_commit.current_raw_id = target_commit.raw_id
2146 merge_check.target_commit.current_raw_id = target_commit.raw_id
2018 merge_check.target_commit.previous_raw_id = target_ref_id
2147 merge_check.target_commit.previous_raw_id = target_ref_id
2019 except (SourceRefMissing, TargetRefMissing):
2148 except (SourceRefMissing, TargetRefMissing):
2020 pass
2149 pass
2021
2150
2022 if not merge_status:
2151 if not merge_status:
2023 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2152 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2024 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2153 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2025
2154
2026 if fail_early:
2155 if fail_early:
2027 return merge_check
2156 return merge_check
2028
2157
2029 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2158 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2030 return merge_check
2159 return merge_check
2031
2160
2032 @classmethod
2161 @classmethod
2033 def get_merge_conditions(cls, pull_request, translator):
2162 def get_merge_conditions(cls, pull_request, translator):
2034 _ = translator
2163 _ = translator
2035 merge_details = {}
2164 merge_details = {}
2036
2165
2037 model = PullRequestModel()
2166 model = PullRequestModel()
2038 use_rebase = model._use_rebase_for_merging(pull_request)
2167 use_rebase = model._use_rebase_for_merging(pull_request)
2039
2168
2040 if use_rebase:
2169 if use_rebase:
2041 merge_details['merge_strategy'] = dict(
2170 merge_details['merge_strategy'] = dict(
2042 details={},
2171 details={},
2043 message=_('Merge strategy: rebase')
2172 message=_('Merge strategy: rebase')
2044 )
2173 )
2045 else:
2174 else:
2046 merge_details['merge_strategy'] = dict(
2175 merge_details['merge_strategy'] = dict(
2047 details={},
2176 details={},
2048 message=_('Merge strategy: explicit merge commit')
2177 message=_('Merge strategy: explicit merge commit')
2049 )
2178 )
2050
2179
2051 close_branch = model._close_branch_before_merging(pull_request)
2180 close_branch = model._close_branch_before_merging(pull_request)
2052 if close_branch:
2181 if close_branch:
2053 repo_type = pull_request.target_repo.repo_type
2182 repo_type = pull_request.target_repo.repo_type
2054 close_msg = ''
2183 close_msg = ''
2055 if repo_type == 'hg':
2184 if repo_type == 'hg':
2056 close_msg = _('Source branch will be closed before the merge.')
2185 close_msg = _('Source branch will be closed before the merge.')
2057 elif repo_type == 'git':
2186 elif repo_type == 'git':
2058 close_msg = _('Source branch will be deleted after the merge.')
2187 close_msg = _('Source branch will be deleted after the merge.')
2059
2188
2060 merge_details['close_branch'] = dict(
2189 merge_details['close_branch'] = dict(
2061 details={},
2190 details={},
2062 message=close_msg
2191 message=close_msg
2063 )
2192 )
2064
2193
2065 return merge_details
2194 return merge_details
2066
2195
2067
2196
2068 ChangeTuple = collections.namedtuple(
2197 ChangeTuple = collections.namedtuple(
2069 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2198 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2070
2199
2071 FileChangeTuple = collections.namedtuple(
2200 FileChangeTuple = collections.namedtuple(
2072 'FileChangeTuple', ['added', 'modified', 'removed'])
2201 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,3211 +1,3236 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'variables';
8 @import 'variables';
9 @import 'bootstrap-variables';
9 @import 'bootstrap-variables';
10 @import 'form-bootstrap';
10 @import 'form-bootstrap';
11 @import 'codemirror';
11 @import 'codemirror';
12 @import 'legacy_code_styles';
12 @import 'legacy_code_styles';
13 @import 'readme-box';
13 @import 'readme-box';
14 @import 'progress-bar';
14 @import 'progress-bar';
15
15
16 @import 'type';
16 @import 'type';
17 @import 'alerts';
17 @import 'alerts';
18 @import 'buttons';
18 @import 'buttons';
19 @import 'tags';
19 @import 'tags';
20 @import 'code-block';
20 @import 'code-block';
21 @import 'examples';
21 @import 'examples';
22 @import 'login';
22 @import 'login';
23 @import 'main-content';
23 @import 'main-content';
24 @import 'select2';
24 @import 'select2';
25 @import 'comments';
25 @import 'comments';
26 @import 'panels-bootstrap';
26 @import 'panels-bootstrap';
27 @import 'panels';
27 @import 'panels';
28 @import 'deform';
28 @import 'deform';
29 @import 'tooltips';
29 @import 'tooltips';
30 @import 'sweetalert2';
30 @import 'sweetalert2';
31
31
32
32
33 //--- BASE ------------------//
33 //--- BASE ------------------//
34 .noscript-error {
34 .noscript-error {
35 top: 0;
35 top: 0;
36 left: 0;
36 left: 0;
37 width: 100%;
37 width: 100%;
38 z-index: 101;
38 z-index: 101;
39 text-align: center;
39 text-align: center;
40 font-size: 120%;
40 font-size: 120%;
41 color: white;
41 color: white;
42 background-color: @alert2;
42 background-color: @alert2;
43 padding: 5px 0 5px 0;
43 padding: 5px 0 5px 0;
44 font-weight: @text-semibold-weight;
44 font-weight: @text-semibold-weight;
45 font-family: @text-semibold;
45 font-family: @text-semibold;
46 }
46 }
47
47
48 html {
48 html {
49 display: table;
49 display: table;
50 height: 100%;
50 height: 100%;
51 width: 100%;
51 width: 100%;
52 }
52 }
53
53
54 body {
54 body {
55 display: table-cell;
55 display: table-cell;
56 width: 100%;
56 width: 100%;
57 }
57 }
58
58
59 //--- LAYOUT ------------------//
59 //--- LAYOUT ------------------//
60
60
61 .hidden{
61 .hidden{
62 display: none !important;
62 display: none !important;
63 }
63 }
64
64
65 .box{
65 .box{
66 float: left;
66 float: left;
67 width: 100%;
67 width: 100%;
68 }
68 }
69
69
70 .browser-header {
70 .browser-header {
71 clear: both;
71 clear: both;
72 }
72 }
73 .main {
73 .main {
74 clear: both;
74 clear: both;
75 padding:0 0 @pagepadding;
75 padding:0 0 @pagepadding;
76 height: auto;
76 height: auto;
77
77
78 &:after { //clearfix
78 &:after { //clearfix
79 content:"";
79 content:"";
80 clear:both;
80 clear:both;
81 width:100%;
81 width:100%;
82 display:block;
82 display:block;
83 }
83 }
84 }
84 }
85
85
86 .flex-container {
86 .flex-container {
87 display: flex;
87 display: flex;
88 justify-content: space-between;
88 justify-content: space-between;
89 }
89 }
90
90
91 .action-link{
91 .action-link{
92 margin-left: @padding;
92 margin-left: @padding;
93 padding-left: @padding;
93 padding-left: @padding;
94 border-left: @border-thickness solid @border-default-color;
94 border-left: @border-thickness solid @border-default-color;
95 }
95 }
96
96
97 .cursor-pointer {
97 .cursor-pointer {
98 cursor: pointer;
98 cursor: pointer;
99 }
99 }
100
100
101 input + .action-link, .action-link.first{
101 input + .action-link, .action-link.first{
102 border-left: none;
102 border-left: none;
103 }
103 }
104
104
105 .link-disabled {
105 .link-disabled {
106 color: @grey4;
106 color: @grey4;
107 cursor: default;
107 cursor: default;
108 }
108 }
109
109
110 .action-link.last{
110 .action-link.last{
111 margin-right: @padding;
111 margin-right: @padding;
112 padding-right: @padding;
112 padding-right: @padding;
113 }
113 }
114
114
115 .action-link.active,
115 .action-link.active,
116 .action-link.active a{
116 .action-link.active a{
117 color: @grey4;
117 color: @grey4;
118 }
118 }
119
119
120 .action-link.disabled {
120 .action-link.disabled {
121 color: @grey4;
121 color: @grey4;
122 cursor: inherit;
122 cursor: inherit;
123 }
123 }
124
124
125 .grey-link-action {
125 .grey-link-action {
126 cursor: pointer;
126 cursor: pointer;
127 &:hover {
127 &:hover {
128 color: @grey2;
128 color: @grey2;
129 }
129 }
130 color: @grey4;
130 color: @grey4;
131 }
131 }
132
132
133 .clipboard-action {
133 .clipboard-action {
134 cursor: pointer;
134 cursor: pointer;
135 margin-left: 5px;
135 margin-left: 5px;
136
136
137 &:not(.no-grey) {
137 &:not(.no-grey) {
138
138
139 &:hover {
139 &:hover {
140 color: @grey2;
140 color: @grey2;
141 }
141 }
142 color: @grey4;
142 color: @grey4;
143 }
143 }
144 }
144 }
145
145
146 ul.simple-list{
146 ul.simple-list{
147 list-style: none;
147 list-style: none;
148 margin: 0;
148 margin: 0;
149 padding: 0;
149 padding: 0;
150 }
150 }
151
151
152 .main-content {
152 .main-content {
153 padding-bottom: @pagepadding;
153 padding-bottom: @pagepadding;
154 }
154 }
155
155
156 .wide-mode-wrapper {
156 .wide-mode-wrapper {
157 max-width:4000px !important;
157 max-width:4000px !important;
158 }
158 }
159
159
160 .wrapper {
160 .wrapper {
161 position: relative;
161 position: relative;
162 max-width: @wrapper-maxwidth;
162 max-width: @wrapper-maxwidth;
163 margin: 0 auto;
163 margin: 0 auto;
164 }
164 }
165
165
166 #content {
166 #content {
167 clear: both;
167 clear: both;
168 padding: 0 @contentpadding;
168 padding: 0 @contentpadding;
169 }
169 }
170
170
171 .advanced-settings-fields{
171 .advanced-settings-fields{
172 input{
172 input{
173 margin-left: @textmargin;
173 margin-left: @textmargin;
174 margin-right: @padding/2;
174 margin-right: @padding/2;
175 }
175 }
176 }
176 }
177
177
178 .cs_files_title {
178 .cs_files_title {
179 margin: @pagepadding 0 0;
179 margin: @pagepadding 0 0;
180 }
180 }
181
181
182 input.inline[type="file"] {
182 input.inline[type="file"] {
183 display: inline;
183 display: inline;
184 }
184 }
185
185
186 .error_page {
186 .error_page {
187 margin: 10% auto;
187 margin: 10% auto;
188
188
189 h1 {
189 h1 {
190 color: @grey2;
190 color: @grey2;
191 }
191 }
192
192
193 .alert {
193 .alert {
194 margin: @padding 0;
194 margin: @padding 0;
195 }
195 }
196
196
197 .error-branding {
197 .error-branding {
198 color: @grey4;
198 color: @grey4;
199 font-weight: @text-semibold-weight;
199 font-weight: @text-semibold-weight;
200 font-family: @text-semibold;
200 font-family: @text-semibold;
201 }
201 }
202
202
203 .error_message {
203 .error_message {
204 font-family: @text-regular;
204 font-family: @text-regular;
205 }
205 }
206
206
207 .sidebar {
207 .sidebar {
208 min-height: 275px;
208 min-height: 275px;
209 margin: 0;
209 margin: 0;
210 padding: 0 0 @sidebarpadding @sidebarpadding;
210 padding: 0 0 @sidebarpadding @sidebarpadding;
211 border: none;
211 border: none;
212 }
212 }
213
213
214 .main-content {
214 .main-content {
215 position: relative;
215 position: relative;
216 margin: 0 @sidebarpadding @sidebarpadding;
216 margin: 0 @sidebarpadding @sidebarpadding;
217 padding: 0 0 0 @sidebarpadding;
217 padding: 0 0 0 @sidebarpadding;
218 border-left: @border-thickness solid @grey5;
218 border-left: @border-thickness solid @grey5;
219
219
220 @media (max-width:767px) {
220 @media (max-width:767px) {
221 clear: both;
221 clear: both;
222 width: 100%;
222 width: 100%;
223 margin: 0;
223 margin: 0;
224 border: none;
224 border: none;
225 }
225 }
226 }
226 }
227
227
228 .inner-column {
228 .inner-column {
229 float: left;
229 float: left;
230 width: 29.75%;
230 width: 29.75%;
231 min-height: 150px;
231 min-height: 150px;
232 margin: @sidebarpadding 2% 0 0;
232 margin: @sidebarpadding 2% 0 0;
233 padding: 0 2% 0 0;
233 padding: 0 2% 0 0;
234 border-right: @border-thickness solid @grey5;
234 border-right: @border-thickness solid @grey5;
235
235
236 @media (max-width:767px) {
236 @media (max-width:767px) {
237 clear: both;
237 clear: both;
238 width: 100%;
238 width: 100%;
239 border: none;
239 border: none;
240 }
240 }
241
241
242 ul {
242 ul {
243 padding-left: 1.25em;
243 padding-left: 1.25em;
244 }
244 }
245
245
246 &:last-child {
246 &:last-child {
247 margin: @sidebarpadding 0 0;
247 margin: @sidebarpadding 0 0;
248 border: none;
248 border: none;
249 }
249 }
250
250
251 h4 {
251 h4 {
252 margin: 0 0 @padding;
252 margin: 0 0 @padding;
253 font-weight: @text-semibold-weight;
253 font-weight: @text-semibold-weight;
254 font-family: @text-semibold;
254 font-family: @text-semibold;
255 }
255 }
256 }
256 }
257 }
257 }
258 .error-page-logo {
258 .error-page-logo {
259 width: 130px;
259 width: 130px;
260 height: 160px;
260 height: 160px;
261 }
261 }
262
262
263 // HEADER
263 // HEADER
264 .header {
264 .header {
265
265
266 // TODO: johbo: Fix login pages, so that they work without a min-height
266 // TODO: johbo: Fix login pages, so that they work without a min-height
267 // for the header and then remove the min-height. I chose a smaller value
267 // for the header and then remove the min-height. I chose a smaller value
268 // intentionally here to avoid rendering issues in the main navigation.
268 // intentionally here to avoid rendering issues in the main navigation.
269 min-height: 49px;
269 min-height: 49px;
270 min-width: 1024px;
270 min-width: 1024px;
271
271
272 position: relative;
272 position: relative;
273 vertical-align: bottom;
273 vertical-align: bottom;
274 padding: 0 @header-padding;
274 padding: 0 @header-padding;
275 background-color: @grey1;
275 background-color: @grey1;
276 color: @grey5;
276 color: @grey5;
277
277
278 .title {
278 .title {
279 overflow: visible;
279 overflow: visible;
280 }
280 }
281
281
282 &:before,
282 &:before,
283 &:after {
283 &:after {
284 content: "";
284 content: "";
285 clear: both;
285 clear: both;
286 width: 100%;
286 width: 100%;
287 }
287 }
288
288
289 // TODO: johbo: Avoids breaking "Repositories" chooser
289 // TODO: johbo: Avoids breaking "Repositories" chooser
290 .select2-container .select2-choice .select2-arrow {
290 .select2-container .select2-choice .select2-arrow {
291 display: none;
291 display: none;
292 }
292 }
293 }
293 }
294
294
295 #header-inner {
295 #header-inner {
296 &.title {
296 &.title {
297 margin: 0;
297 margin: 0;
298 }
298 }
299 &:before,
299 &:before,
300 &:after {
300 &:after {
301 content: "";
301 content: "";
302 clear: both;
302 clear: both;
303 }
303 }
304 }
304 }
305
305
306 // Gists
306 // Gists
307 #files_data {
307 #files_data {
308 clear: both; //for firefox
308 clear: both; //for firefox
309 padding-top: 10px;
309 padding-top: 10px;
310 }
310 }
311
311
312 #gistid {
312 #gistid {
313 margin-right: @padding;
313 margin-right: @padding;
314 }
314 }
315
315
316 // Global Settings Editor
316 // Global Settings Editor
317 .textarea.editor {
317 .textarea.editor {
318 float: left;
318 float: left;
319 position: relative;
319 position: relative;
320 max-width: @texteditor-width;
320 max-width: @texteditor-width;
321
321
322 select {
322 select {
323 position: absolute;
323 position: absolute;
324 top:10px;
324 top:10px;
325 right:0;
325 right:0;
326 }
326 }
327
327
328 .CodeMirror {
328 .CodeMirror {
329 margin: 0;
329 margin: 0;
330 }
330 }
331
331
332 .help-block {
332 .help-block {
333 margin: 0 0 @padding;
333 margin: 0 0 @padding;
334 padding:.5em;
334 padding:.5em;
335 background-color: @grey6;
335 background-color: @grey6;
336 &.pre-formatting {
336 &.pre-formatting {
337 white-space: pre;
337 white-space: pre;
338 }
338 }
339 }
339 }
340 }
340 }
341
341
342 ul.auth_plugins {
342 ul.auth_plugins {
343 margin: @padding 0 @padding @legend-width;
343 margin: @padding 0 @padding @legend-width;
344 padding: 0;
344 padding: 0;
345
345
346 li {
346 li {
347 margin-bottom: @padding;
347 margin-bottom: @padding;
348 line-height: 1em;
348 line-height: 1em;
349 list-style-type: none;
349 list-style-type: none;
350
350
351 .auth_buttons .btn {
351 .auth_buttons .btn {
352 margin-right: @padding;
352 margin-right: @padding;
353 }
353 }
354
354
355 }
355 }
356 }
356 }
357
357
358
358
359 // My Account PR list
359 // My Account PR list
360
360
361 #show_closed {
361 #show_closed {
362 margin: 0 1em 0 0;
362 margin: 0 1em 0 0;
363 }
363 }
364
364
365 #pull_request_list_table {
365 #pull_request_list_table {
366 .closed {
366 .closed {
367 background-color: @grey6;
367 background-color: @grey6;
368 }
368 }
369
369
370 .state-creating,
370 .state-creating,
371 .state-updating,
371 .state-updating,
372 .state-merging
372 .state-merging
373 {
373 {
374 background-color: @grey6;
374 background-color: @grey6;
375 }
375 }
376
376
377 .td-status {
377 .td-status {
378 padding-left: .5em;
378 padding-left: .5em;
379 }
379 }
380 .log-container .truncate {
380 .log-container .truncate {
381 height: 2.75em;
381 height: 2.75em;
382 white-space: pre-line;
382 white-space: pre-line;
383 }
383 }
384 table.rctable .user {
384 table.rctable .user {
385 padding-left: 0;
385 padding-left: 0;
386 }
386 }
387 table.rctable {
387 table.rctable {
388 td.td-description,
388 td.td-description,
389 .rc-user {
389 .rc-user {
390 min-width: auto;
390 min-width: auto;
391 }
391 }
392 }
392 }
393 }
393 }
394
394
395 // Pull Requests
395 // Pull Requests
396
396
397 .pullrequests_section_head {
397 .pullrequests_section_head {
398 display: block;
398 display: block;
399 clear: both;
399 clear: both;
400 margin: @padding 0;
400 margin: @padding 0;
401 font-weight: @text-bold-weight;
401 font-weight: @text-bold-weight;
402 font-family: @text-bold;
402 font-family: @text-bold;
403 }
403 }
404
404
405 .pr-commit-flow {
405 .pr-commit-flow {
406 position: relative;
406 position: relative;
407 font-weight: 600;
407 font-weight: 600;
408
408
409 .tag {
409 .tag {
410 display: inline-block;
410 display: inline-block;
411 margin: 0 1em .5em 0;
411 margin: 0 1em .5em 0;
412 }
412 }
413
413
414 .clone-url {
414 .clone-url {
415 display: inline-block;
415 display: inline-block;
416 margin: 0 0 .5em 0;
416 margin: 0 0 .5em 0;
417 padding: 0;
417 padding: 0;
418 line-height: 1.2em;
418 line-height: 1.2em;
419 }
419 }
420 }
420 }
421
421
422 .pr-mergeinfo {
422 .pr-mergeinfo {
423 min-width: 95% !important;
423 min-width: 95% !important;
424 padding: 0 !important;
424 padding: 0 !important;
425 border: 0;
425 border: 0;
426 }
426 }
427 .pr-mergeinfo-copy {
427 .pr-mergeinfo-copy {
428 padding: 0 0;
428 padding: 0 0;
429 }
429 }
430
430
431 .pr-pullinfo {
431 .pr-pullinfo {
432 min-width: 95% !important;
432 min-width: 95% !important;
433 padding: 0 !important;
433 padding: 0 !important;
434 border: 0;
434 border: 0;
435 }
435 }
436 .pr-pullinfo-copy {
436 .pr-pullinfo-copy {
437 padding: 0 0;
437 padding: 0 0;
438 }
438 }
439
439
440 .pr-title-input {
440 .pr-title-input {
441 width: 100%;
441 width: 100%;
442 font-size: 18px;
442 font-size: 18px;
443 margin: 0 0 4px 0;
443 margin: 0 0 4px 0;
444 padding: 0;
444 padding: 0;
445 line-height: 1.7em;
445 line-height: 1.7em;
446 color: @text-color;
446 color: @text-color;
447 letter-spacing: .02em;
447 letter-spacing: .02em;
448 font-weight: @text-bold-weight;
448 font-weight: @text-bold-weight;
449 font-family: @text-bold;
449 font-family: @text-bold;
450
450
451 &:hover {
451 &:hover {
452 box-shadow: none;
452 box-shadow: none;
453 }
453 }
454 }
454 }
455
455
456 #pr-title {
456 #pr-title {
457 input {
457 input {
458 border: 1px transparent;
458 border: 1px transparent;
459 color: black;
459 color: black;
460 opacity: 1;
460 opacity: 1;
461 background: #fff;
461 background: #fff;
462 font-size: 18px;
462 font-size: 18px;
463 }
463 }
464 }
464 }
465
465
466 .pr-title-closed-tag {
466 .pr-title-closed-tag {
467 font-size: 16px;
467 font-size: 16px;
468 }
468 }
469
469
470 #pr-desc {
470 #pr-desc {
471 padding: 10px 0;
471 padding: 10px 0;
472
472
473 .markdown-block {
473 .markdown-block {
474 padding: 0;
474 padding: 0;
475 margin-bottom: -30px;
475 margin-bottom: -30px;
476 }
476 }
477 }
477 }
478
478
479 #pullrequest_title {
479 #pullrequest_title {
480 width: 100%;
480 width: 100%;
481 box-sizing: border-box;
481 box-sizing: border-box;
482 }
482 }
483
483
484 #pr_open_message {
484 #pr_open_message {
485 border: @border-thickness solid #fff;
485 border: @border-thickness solid #fff;
486 border-radius: @border-radius;
486 border-radius: @border-radius;
487 text-align: left;
487 text-align: left;
488 overflow: hidden;
488 overflow: hidden;
489 white-space: pre-line;
489 white-space: pre-line;
490 padding-top: 5px
490 padding-top: 5px
491 }
491 }
492
492
493 #add_reviewer {
493 #add_reviewer {
494 padding-top: 10px;
494 padding-top: 10px;
495 }
495 }
496
496
497 #add_reviewer_input {
497 #add_reviewer_input {
498 padding-top: 10px
498 padding-top: 10px
499 }
499 }
500
500
501 .pr-details-title-author-pref {
501 .pr-details-title-author-pref {
502 padding-right: 10px
502 padding-right: 10px
503 }
503 }
504
504
505 .label-pr-detail {
505 .label-pr-detail {
506 display: table-cell;
506 display: table-cell;
507 width: 120px;
507 width: 120px;
508 padding-top: 7.5px;
508 padding-top: 7.5px;
509 padding-bottom: 7.5px;
509 padding-bottom: 7.5px;
510 padding-right: 7.5px;
510 padding-right: 7.5px;
511 }
511 }
512
512
513 .source-details ul {
513 .source-details ul {
514 padding: 10px 16px;
514 padding: 10px 16px;
515 }
515 }
516
516
517 .source-details-action {
517 .source-details-action {
518 color: @grey4;
518 color: @grey4;
519 font-size: 11px
519 font-size: 11px
520 }
520 }
521
521
522 .pr-submit-button {
522 .pr-submit-button {
523 float: right;
523 float: right;
524 margin: 0 0 0 5px;
524 margin: 0 0 0 5px;
525 }
525 }
526
526
527 .pr-spacing-container {
527 .pr-spacing-container {
528 padding: 20px;
528 padding: 20px;
529 clear: both
529 clear: both
530 }
530 }
531
531
532 #pr-description-input {
532 #pr-description-input {
533 margin-bottom: 0;
533 margin-bottom: 0;
534 }
534 }
535
535
536 .pr-description-label {
536 .pr-description-label {
537 vertical-align: top;
537 vertical-align: top;
538 }
538 }
539
539
540 #open_edit_pullrequest {
540 #open_edit_pullrequest {
541 padding: 0;
541 padding: 0;
542 }
542 }
543
543
544 #close_edit_pullrequest {
544 #close_edit_pullrequest {
545
545
546 }
546 }
547
547
548 #delete_pullrequest {
548 #delete_pullrequest {
549 clear: inherit;
549 clear: inherit;
550
550
551 form {
551 form {
552 display: inline;
552 display: inline;
553 }
553 }
554
554
555 }
555 }
556
556
557 .perms_section_head {
557 .perms_section_head {
558 min-width: 625px;
558 min-width: 625px;
559
559
560 h2 {
560 h2 {
561 margin-bottom: 0;
561 margin-bottom: 0;
562 }
562 }
563
563
564 .label-checkbox {
564 .label-checkbox {
565 float: left;
565 float: left;
566 }
566 }
567
567
568 &.field {
568 &.field {
569 margin: @space 0 @padding;
569 margin: @space 0 @padding;
570 }
570 }
571
571
572 &:first-child.field {
572 &:first-child.field {
573 margin-top: 0;
573 margin-top: 0;
574
574
575 .label {
575 .label {
576 margin-top: 0;
576 margin-top: 0;
577 padding-top: 0;
577 padding-top: 0;
578 }
578 }
579
579
580 .radios {
580 .radios {
581 padding-top: 0;
581 padding-top: 0;
582 }
582 }
583 }
583 }
584
584
585 .radios {
585 .radios {
586 position: relative;
586 position: relative;
587 width: 505px;
587 width: 505px;
588 }
588 }
589 }
589 }
590
590
591 //--- MODULES ------------------//
591 //--- MODULES ------------------//
592
592
593
593
594 // Server Announcement
594 // Server Announcement
595 #server-announcement {
595 #server-announcement {
596 width: 95%;
596 width: 95%;
597 margin: @padding auto;
597 margin: @padding auto;
598 padding: @padding;
598 padding: @padding;
599 border-width: 2px;
599 border-width: 2px;
600 border-style: solid;
600 border-style: solid;
601 .border-radius(2px);
601 .border-radius(2px);
602 font-weight: @text-bold-weight;
602 font-weight: @text-bold-weight;
603 font-family: @text-bold;
603 font-family: @text-bold;
604
604
605 &.info { border-color: @alert4; background-color: @alert4-inner; }
605 &.info { border-color: @alert4; background-color: @alert4-inner; }
606 &.warning { border-color: @alert3; background-color: @alert3-inner; }
606 &.warning { border-color: @alert3; background-color: @alert3-inner; }
607 &.error { border-color: @alert2; background-color: @alert2-inner; }
607 &.error { border-color: @alert2; background-color: @alert2-inner; }
608 &.success { border-color: @alert1; background-color: @alert1-inner; }
608 &.success { border-color: @alert1; background-color: @alert1-inner; }
609 &.neutral { border-color: @grey3; background-color: @grey6; }
609 &.neutral { border-color: @grey3; background-color: @grey6; }
610 }
610 }
611
611
612 // Fixed Sidebar Column
612 // Fixed Sidebar Column
613 .sidebar-col-wrapper {
613 .sidebar-col-wrapper {
614 padding-left: @sidebar-all-width;
614 padding-left: @sidebar-all-width;
615
615
616 .sidebar {
616 .sidebar {
617 width: @sidebar-width;
617 width: @sidebar-width;
618 margin-left: -@sidebar-all-width;
618 margin-left: -@sidebar-all-width;
619 }
619 }
620 }
620 }
621
621
622 .sidebar-col-wrapper.scw-small {
622 .sidebar-col-wrapper.scw-small {
623 padding-left: @sidebar-small-all-width;
623 padding-left: @sidebar-small-all-width;
624
624
625 .sidebar {
625 .sidebar {
626 width: @sidebar-small-width;
626 width: @sidebar-small-width;
627 margin-left: -@sidebar-small-all-width;
627 margin-left: -@sidebar-small-all-width;
628 }
628 }
629 }
629 }
630
630
631
631
632 // FOOTER
632 // FOOTER
633 #footer {
633 #footer {
634 padding: 0;
634 padding: 0;
635 text-align: center;
635 text-align: center;
636 vertical-align: middle;
636 vertical-align: middle;
637 color: @grey2;
637 color: @grey2;
638 font-size: 11px;
638 font-size: 11px;
639
639
640 p {
640 p {
641 margin: 0;
641 margin: 0;
642 padding: 1em;
642 padding: 1em;
643 line-height: 1em;
643 line-height: 1em;
644 }
644 }
645
645
646 .server-instance { //server instance
646 .server-instance { //server instance
647 display: none;
647 display: none;
648 }
648 }
649
649
650 .title {
650 .title {
651 float: none;
651 float: none;
652 margin: 0 auto;
652 margin: 0 auto;
653 }
653 }
654 }
654 }
655
655
656 button.close {
656 button.close {
657 padding: 0;
657 padding: 0;
658 cursor: pointer;
658 cursor: pointer;
659 background: transparent;
659 background: transparent;
660 border: 0;
660 border: 0;
661 .box-shadow(none);
661 .box-shadow(none);
662 -webkit-appearance: none;
662 -webkit-appearance: none;
663 }
663 }
664
664
665 .close {
665 .close {
666 float: right;
666 float: right;
667 font-size: 21px;
667 font-size: 21px;
668 font-family: @text-bootstrap;
668 font-family: @text-bootstrap;
669 line-height: 1em;
669 line-height: 1em;
670 font-weight: bold;
670 font-weight: bold;
671 color: @grey2;
671 color: @grey2;
672
672
673 &:hover,
673 &:hover,
674 &:focus {
674 &:focus {
675 color: @grey1;
675 color: @grey1;
676 text-decoration: none;
676 text-decoration: none;
677 cursor: pointer;
677 cursor: pointer;
678 }
678 }
679 }
679 }
680
680
681 // GRID
681 // GRID
682 .sorting,
682 .sorting,
683 .sorting_desc,
683 .sorting_desc,
684 .sorting_asc {
684 .sorting_asc {
685 cursor: pointer;
685 cursor: pointer;
686 }
686 }
687 .sorting_desc:after {
687 .sorting_desc:after {
688 content: "\00A0\25B2";
688 content: "\00A0\25B2";
689 font-size: .75em;
689 font-size: .75em;
690 }
690 }
691 .sorting_asc:after {
691 .sorting_asc:after {
692 content: "\00A0\25BC";
692 content: "\00A0\25BC";
693 font-size: .68em;
693 font-size: .68em;
694 }
694 }
695
695
696
696
697 .user_auth_tokens {
697 .user_auth_tokens {
698
698
699 &.truncate {
699 &.truncate {
700 white-space: nowrap;
700 white-space: nowrap;
701 overflow: hidden;
701 overflow: hidden;
702 text-overflow: ellipsis;
702 text-overflow: ellipsis;
703 }
703 }
704
704
705 .fields .field .input {
705 .fields .field .input {
706 margin: 0;
706 margin: 0;
707 }
707 }
708
708
709 input#description {
709 input#description {
710 width: 100px;
710 width: 100px;
711 margin: 0;
711 margin: 0;
712 }
712 }
713
713
714 .drop-menu {
714 .drop-menu {
715 // TODO: johbo: Remove this, should work out of the box when
715 // TODO: johbo: Remove this, should work out of the box when
716 // having multiple inputs inline
716 // having multiple inputs inline
717 margin: 0 0 0 5px;
717 margin: 0 0 0 5px;
718 }
718 }
719 }
719 }
720 #user_list_table {
720 #user_list_table {
721 .closed {
721 .closed {
722 background-color: @grey6;
722 background-color: @grey6;
723 }
723 }
724 }
724 }
725
725
726
726
727 input, textarea {
727 input, textarea {
728 &.disabled {
728 &.disabled {
729 opacity: .5;
729 opacity: .5;
730 }
730 }
731
731
732 &:hover {
732 &:hover {
733 border-color: @grey3;
733 border-color: @grey3;
734 box-shadow: @button-shadow;
734 box-shadow: @button-shadow;
735 }
735 }
736
736
737 &:focus {
737 &:focus {
738 border-color: @rcblue;
738 border-color: @rcblue;
739 box-shadow: @button-shadow;
739 box-shadow: @button-shadow;
740 }
740 }
741 }
741 }
742
742
743 // remove extra padding in firefox
743 // remove extra padding in firefox
744 input::-moz-focus-inner { border:0; padding:0 }
744 input::-moz-focus-inner { border:0; padding:0 }
745
745
746 .adjacent input {
746 .adjacent input {
747 margin-bottom: @padding;
747 margin-bottom: @padding;
748 }
748 }
749
749
750 .permissions_boxes {
750 .permissions_boxes {
751 display: block;
751 display: block;
752 }
752 }
753
753
754 //FORMS
754 //FORMS
755
755
756 .medium-inline,
756 .medium-inline,
757 input#description.medium-inline {
757 input#description.medium-inline {
758 display: inline;
758 display: inline;
759 width: @medium-inline-input-width;
759 width: @medium-inline-input-width;
760 min-width: 100px;
760 min-width: 100px;
761 }
761 }
762
762
763 select {
763 select {
764 //reset
764 //reset
765 -webkit-appearance: none;
765 -webkit-appearance: none;
766 -moz-appearance: none;
766 -moz-appearance: none;
767
767
768 display: inline-block;
768 display: inline-block;
769 height: 28px;
769 height: 28px;
770 width: auto;
770 width: auto;
771 margin: 0 @padding @padding 0;
771 margin: 0 @padding @padding 0;
772 padding: 0 18px 0 8px;
772 padding: 0 18px 0 8px;
773 line-height:1em;
773 line-height:1em;
774 font-size: @basefontsize;
774 font-size: @basefontsize;
775 border: @border-thickness solid @grey5;
775 border: @border-thickness solid @grey5;
776 border-radius: @border-radius;
776 border-radius: @border-radius;
777 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
777 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
778 color: @grey4;
778 color: @grey4;
779 box-shadow: @button-shadow;
779 box-shadow: @button-shadow;
780
780
781 &:after {
781 &:after {
782 content: "\00A0\25BE";
782 content: "\00A0\25BE";
783 }
783 }
784
784
785 &:focus, &:hover {
785 &:focus, &:hover {
786 outline: none;
786 outline: none;
787 border-color: @grey4;
787 border-color: @grey4;
788 color: @rcdarkblue;
788 color: @rcdarkblue;
789 }
789 }
790 }
790 }
791
791
792 option {
792 option {
793 &:focus {
793 &:focus {
794 outline: none;
794 outline: none;
795 }
795 }
796 }
796 }
797
797
798 input,
798 input,
799 textarea {
799 textarea {
800 padding: @input-padding;
800 padding: @input-padding;
801 border: @input-border-thickness solid @border-highlight-color;
801 border: @input-border-thickness solid @border-highlight-color;
802 .border-radius (@border-radius);
802 .border-radius (@border-radius);
803 font-family: @text-light;
803 font-family: @text-light;
804 font-size: @basefontsize;
804 font-size: @basefontsize;
805
805
806 &.input-sm {
806 &.input-sm {
807 padding: 5px;
807 padding: 5px;
808 }
808 }
809
809
810 &#description {
810 &#description {
811 min-width: @input-description-minwidth;
811 min-width: @input-description-minwidth;
812 min-height: 1em;
812 min-height: 1em;
813 padding: 10px;
813 padding: 10px;
814 }
814 }
815 }
815 }
816
816
817 .field-sm {
817 .field-sm {
818 input,
818 input,
819 textarea {
819 textarea {
820 padding: 5px;
820 padding: 5px;
821 }
821 }
822 }
822 }
823
823
824 textarea {
824 textarea {
825 display: block;
825 display: block;
826 clear: both;
826 clear: both;
827 width: 100%;
827 width: 100%;
828 min-height: 100px;
828 min-height: 100px;
829 margin-bottom: @padding;
829 margin-bottom: @padding;
830 .box-sizing(border-box);
830 .box-sizing(border-box);
831 overflow: auto;
831 overflow: auto;
832 }
832 }
833
833
834 label {
834 label {
835 font-family: @text-light;
835 font-family: @text-light;
836 }
836 }
837
837
838 // GRAVATARS
838 // GRAVATARS
839 // centers gravatar on username to the right
839 // centers gravatar on username to the right
840
840
841 .gravatar {
841 .gravatar {
842 display: inline;
842 display: inline;
843 min-width: 16px;
843 min-width: 16px;
844 min-height: 16px;
844 min-height: 16px;
845 margin: -5px 0;
845 margin: -5px 0;
846 padding: 0;
846 padding: 0;
847 line-height: 1em;
847 line-height: 1em;
848 box-sizing: content-box;
848 box-sizing: content-box;
849 border-radius: 50%;
849 border-radius: 50%;
850
850
851 &.gravatar-large {
851 &.gravatar-large {
852 margin: -0.5em .25em -0.5em 0;
852 margin: -0.5em .25em -0.5em 0;
853 }
853 }
854
854
855 & + .user {
855 & + .user {
856 display: inline;
856 display: inline;
857 margin: 0;
857 margin: 0;
858 padding: 0 0 0 .17em;
858 padding: 0 0 0 .17em;
859 line-height: 1em;
859 line-height: 1em;
860 }
860 }
861
861
862 & + .no-margin {
862 & + .no-margin {
863 margin: 0
863 margin: 0
864 }
864 }
865
865
866 }
866 }
867
867
868 .user-inline-data {
868 .user-inline-data {
869 display: inline-block;
869 display: inline-block;
870 float: left;
870 float: left;
871 padding-left: .5em;
871 padding-left: .5em;
872 line-height: 1.3em;
872 line-height: 1.3em;
873 }
873 }
874
874
875 .rc-user { // gravatar + user wrapper
875 .rc-user { // gravatar + user wrapper
876 float: left;
876 float: left;
877 position: relative;
877 position: relative;
878 min-width: 100px;
878 min-width: 100px;
879 max-width: 200px;
879 max-width: 200px;
880 min-height: (@gravatar-size + @border-thickness * 2); // account for border
880 min-height: (@gravatar-size + @border-thickness * 2); // account for border
881 display: block;
881 display: block;
882 padding: 0 0 0 (@gravatar-size + @basefontsize/4);
882 padding: 0 0 0 (@gravatar-size + @basefontsize/4);
883
883
884
884
885 .gravatar {
885 .gravatar {
886 display: block;
886 display: block;
887 position: absolute;
887 position: absolute;
888 top: 0;
888 top: 0;
889 left: 0;
889 left: 0;
890 min-width: @gravatar-size;
890 min-width: @gravatar-size;
891 min-height: @gravatar-size;
891 min-height: @gravatar-size;
892 margin: 0;
892 margin: 0;
893 }
893 }
894
894
895 .user {
895 .user {
896 display: block;
896 display: block;
897 max-width: 175px;
897 max-width: 175px;
898 padding-top: 2px;
898 padding-top: 2px;
899 overflow: hidden;
899 overflow: hidden;
900 text-overflow: ellipsis;
900 text-overflow: ellipsis;
901 }
901 }
902 }
902 }
903
903
904 .gist-gravatar,
904 .gist-gravatar,
905 .journal_container {
905 .journal_container {
906 .gravatar-large {
906 .gravatar-large {
907 margin: 0 .5em -10px 0;
907 margin: 0 .5em -10px 0;
908 }
908 }
909 }
909 }
910
910
911 .gist-type-fields {
911 .gist-type-fields {
912 line-height: 30px;
912 line-height: 30px;
913 height: 30px;
913 height: 30px;
914
914
915 .gist-type-fields-wrapper {
915 .gist-type-fields-wrapper {
916 vertical-align: middle;
916 vertical-align: middle;
917 display: inline-block;
917 display: inline-block;
918 line-height: 25px;
918 line-height: 25px;
919 }
919 }
920 }
920 }
921
921
922 // ADMIN SETTINGS
922 // ADMIN SETTINGS
923
923
924 // Tag Patterns
924 // Tag Patterns
925 .tag_patterns {
925 .tag_patterns {
926 .tag_input {
926 .tag_input {
927 margin-bottom: @padding;
927 margin-bottom: @padding;
928 }
928 }
929 }
929 }
930
930
931 .locked_input {
931 .locked_input {
932 position: relative;
932 position: relative;
933
933
934 input {
934 input {
935 display: inline;
935 display: inline;
936 margin: 3px 5px 0px 0px;
936 margin: 3px 5px 0px 0px;
937 }
937 }
938
938
939 br {
939 br {
940 display: none;
940 display: none;
941 }
941 }
942
942
943 .error-message {
943 .error-message {
944 float: left;
944 float: left;
945 width: 100%;
945 width: 100%;
946 }
946 }
947
947
948 .lock_input_button {
948 .lock_input_button {
949 display: inline;
949 display: inline;
950 }
950 }
951
951
952 .help-block {
952 .help-block {
953 clear: both;
953 clear: both;
954 }
954 }
955 }
955 }
956
956
957 // Notifications
957 // Notifications
958
958
959 .notifications_buttons {
959 .notifications_buttons {
960 margin: 0 0 @space 0;
960 margin: 0 0 @space 0;
961 padding: 0;
961 padding: 0;
962
962
963 .btn {
963 .btn {
964 display: inline-block;
964 display: inline-block;
965 }
965 }
966 }
966 }
967
967
968 .notification-list {
968 .notification-list {
969
969
970 div {
970 div {
971 vertical-align: middle;
971 vertical-align: middle;
972 }
972 }
973
973
974 .container {
974 .container {
975 display: block;
975 display: block;
976 margin: 0 0 @padding 0;
976 margin: 0 0 @padding 0;
977 }
977 }
978
978
979 .delete-notifications {
979 .delete-notifications {
980 margin-left: @padding;
980 margin-left: @padding;
981 text-align: right;
981 text-align: right;
982 cursor: pointer;
982 cursor: pointer;
983 }
983 }
984
984
985 .read-notifications {
985 .read-notifications {
986 margin-left: @padding/2;
986 margin-left: @padding/2;
987 text-align: right;
987 text-align: right;
988 width: 35px;
988 width: 35px;
989 cursor: pointer;
989 cursor: pointer;
990 }
990 }
991
991
992 .icon-minus-sign {
992 .icon-minus-sign {
993 color: @alert2;
993 color: @alert2;
994 }
994 }
995
995
996 .icon-ok-sign {
996 .icon-ok-sign {
997 color: @alert1;
997 color: @alert1;
998 }
998 }
999 }
999 }
1000
1000
1001 .user_settings {
1001 .user_settings {
1002 float: left;
1002 float: left;
1003 clear: both;
1003 clear: both;
1004 display: block;
1004 display: block;
1005 width: 100%;
1005 width: 100%;
1006
1006
1007 .gravatar_box {
1007 .gravatar_box {
1008 margin-bottom: @padding;
1008 margin-bottom: @padding;
1009
1009
1010 &:after {
1010 &:after {
1011 content: " ";
1011 content: " ";
1012 clear: both;
1012 clear: both;
1013 width: 100%;
1013 width: 100%;
1014 }
1014 }
1015 }
1015 }
1016
1016
1017 .fields .field {
1017 .fields .field {
1018 clear: both;
1018 clear: both;
1019 }
1019 }
1020 }
1020 }
1021
1021
1022 .advanced_settings {
1022 .advanced_settings {
1023 margin-bottom: @space;
1023 margin-bottom: @space;
1024
1024
1025 .help-block {
1025 .help-block {
1026 margin-left: 0;
1026 margin-left: 0;
1027 }
1027 }
1028
1028
1029 button + .help-block {
1029 button + .help-block {
1030 margin-top: @padding;
1030 margin-top: @padding;
1031 }
1031 }
1032 }
1032 }
1033
1033
1034 // admin settings radio buttons and labels
1034 // admin settings radio buttons and labels
1035 .label-2 {
1035 .label-2 {
1036 float: left;
1036 float: left;
1037 width: @label2-width;
1037 width: @label2-width;
1038
1038
1039 label {
1039 label {
1040 color: @grey1;
1040 color: @grey1;
1041 }
1041 }
1042 }
1042 }
1043 .checkboxes {
1043 .checkboxes {
1044 float: left;
1044 float: left;
1045 width: @checkboxes-width;
1045 width: @checkboxes-width;
1046 margin-bottom: @padding;
1046 margin-bottom: @padding;
1047
1047
1048 .checkbox {
1048 .checkbox {
1049 width: 100%;
1049 width: 100%;
1050
1050
1051 label {
1051 label {
1052 margin: 0;
1052 margin: 0;
1053 padding: 0;
1053 padding: 0;
1054 }
1054 }
1055 }
1055 }
1056
1056
1057 .checkbox + .checkbox {
1057 .checkbox + .checkbox {
1058 display: inline-block;
1058 display: inline-block;
1059 }
1059 }
1060
1060
1061 label {
1061 label {
1062 margin-right: 1em;
1062 margin-right: 1em;
1063 }
1063 }
1064 }
1064 }
1065
1065
1066 // CHANGELOG
1066 // CHANGELOG
1067 .container_header {
1067 .container_header {
1068 float: left;
1068 float: left;
1069 display: block;
1069 display: block;
1070 width: 100%;
1070 width: 100%;
1071 margin: @padding 0 @padding;
1071 margin: @padding 0 @padding;
1072
1072
1073 #filter_changelog {
1073 #filter_changelog {
1074 float: left;
1074 float: left;
1075 margin-right: @padding;
1075 margin-right: @padding;
1076 }
1076 }
1077
1077
1078 .breadcrumbs_light {
1078 .breadcrumbs_light {
1079 display: inline-block;
1079 display: inline-block;
1080 }
1080 }
1081 }
1081 }
1082
1082
1083 .info_box {
1083 .info_box {
1084 float: right;
1084 float: right;
1085 }
1085 }
1086
1086
1087
1087
1088
1088
1089 #graph_content{
1089 #graph_content{
1090
1090
1091 // adjust for table headers so that graph renders properly
1091 // adjust for table headers so that graph renders properly
1092 // #graph_nodes padding - table cell padding
1092 // #graph_nodes padding - table cell padding
1093 padding-top: (@space - (@basefontsize * 2.4));
1093 padding-top: (@space - (@basefontsize * 2.4));
1094
1094
1095 &.graph_full_width {
1095 &.graph_full_width {
1096 width: 100%;
1096 width: 100%;
1097 max-width: 100%;
1097 max-width: 100%;
1098 }
1098 }
1099 }
1099 }
1100
1100
1101 #graph {
1101 #graph {
1102
1102
1103 .pagination-left {
1103 .pagination-left {
1104 float: left;
1104 float: left;
1105 clear: both;
1105 clear: both;
1106 }
1106 }
1107
1107
1108 .log-container {
1108 .log-container {
1109 max-width: 345px;
1109 max-width: 345px;
1110
1110
1111 .message{
1111 .message{
1112 max-width: 340px;
1112 max-width: 340px;
1113 }
1113 }
1114 }
1114 }
1115
1115
1116 .graph-col-wrapper {
1116 .graph-col-wrapper {
1117
1117
1118 #graph_nodes {
1118 #graph_nodes {
1119 width: 100px;
1119 width: 100px;
1120 position: absolute;
1120 position: absolute;
1121 left: 70px;
1121 left: 70px;
1122 z-index: -1;
1122 z-index: -1;
1123 }
1123 }
1124 }
1124 }
1125
1125
1126 .load-more-commits {
1126 .load-more-commits {
1127 text-align: center;
1127 text-align: center;
1128 }
1128 }
1129 .load-more-commits:hover {
1129 .load-more-commits:hover {
1130 background-color: @grey7;
1130 background-color: @grey7;
1131 }
1131 }
1132 .load-more-commits {
1132 .load-more-commits {
1133 a {
1133 a {
1134 display: block;
1134 display: block;
1135 }
1135 }
1136 }
1136 }
1137 }
1137 }
1138
1138
1139 .obsolete-toggle {
1139 .obsolete-toggle {
1140 line-height: 30px;
1140 line-height: 30px;
1141 margin-left: -15px;
1141 margin-left: -15px;
1142 }
1142 }
1143
1143
1144 #rev_range_container, #rev_range_clear, #rev_range_more {
1144 #rev_range_container, #rev_range_clear, #rev_range_more {
1145 margin-top: -5px;
1145 margin-top: -5px;
1146 margin-bottom: -5px;
1146 margin-bottom: -5px;
1147 }
1147 }
1148
1148
1149 #filter_changelog {
1149 #filter_changelog {
1150 float: left;
1150 float: left;
1151 }
1151 }
1152
1152
1153
1153
1154 //--- THEME ------------------//
1154 //--- THEME ------------------//
1155
1155
1156 #logo {
1156 #logo {
1157 float: left;
1157 float: left;
1158 margin: 9px 0 0 0;
1158 margin: 9px 0 0 0;
1159
1159
1160 .header {
1160 .header {
1161 background-color: transparent;
1161 background-color: transparent;
1162 }
1162 }
1163
1163
1164 a {
1164 a {
1165 display: inline-block;
1165 display: inline-block;
1166 }
1166 }
1167
1167
1168 img {
1168 img {
1169 height:30px;
1169 height:30px;
1170 }
1170 }
1171 }
1171 }
1172
1172
1173 .logo-wrapper {
1173 .logo-wrapper {
1174 float:left;
1174 float:left;
1175 }
1175 }
1176
1176
1177 .branding {
1177 .branding {
1178 float: left;
1178 float: left;
1179 padding: 9px 2px;
1179 padding: 9px 2px;
1180 line-height: 1em;
1180 line-height: 1em;
1181 font-size: @navigation-fontsize;
1181 font-size: @navigation-fontsize;
1182
1182
1183 a {
1183 a {
1184 color: @grey5
1184 color: @grey5
1185 }
1185 }
1186
1186
1187 // 1024px or smaller
1187 // 1024px or smaller
1188 @media screen and (max-width: 1180px) {
1188 @media screen and (max-width: 1180px) {
1189 display: none;
1189 display: none;
1190 }
1190 }
1191
1191
1192 }
1192 }
1193
1193
1194 img {
1194 img {
1195 border: none;
1195 border: none;
1196 outline: none;
1196 outline: none;
1197 }
1197 }
1198 user-profile-header
1198 user-profile-header
1199 label {
1199 label {
1200
1200
1201 input[type="checkbox"] {
1201 input[type="checkbox"] {
1202 margin-right: 1em;
1202 margin-right: 1em;
1203 }
1203 }
1204 input[type="radio"] {
1204 input[type="radio"] {
1205 margin-right: 1em;
1205 margin-right: 1em;
1206 }
1206 }
1207 }
1207 }
1208
1208
1209 .review-status {
1209 .review-status {
1210 &.under_review {
1210 &.under_review {
1211 color: @alert3;
1211 color: @alert3;
1212 }
1212 }
1213 &.approved {
1213 &.approved {
1214 color: @alert1;
1214 color: @alert1;
1215 }
1215 }
1216 &.rejected,
1216 &.rejected,
1217 &.forced_closed{
1217 &.forced_closed{
1218 color: @alert2;
1218 color: @alert2;
1219 }
1219 }
1220 &.not_reviewed {
1220 &.not_reviewed {
1221 color: @grey5;
1221 color: @grey5;
1222 }
1222 }
1223 }
1223 }
1224
1224
1225 .review-status-under_review {
1225 .review-status-under_review {
1226 color: @alert3;
1226 color: @alert3;
1227 }
1227 }
1228 .status-tag-under_review {
1228 .status-tag-under_review {
1229 border-color: @alert3;
1229 border-color: @alert3;
1230 }
1230 }
1231
1231
1232 .review-status-approved {
1232 .review-status-approved {
1233 color: @alert1;
1233 color: @alert1;
1234 }
1234 }
1235 .status-tag-approved {
1235 .status-tag-approved {
1236 border-color: @alert1;
1236 border-color: @alert1;
1237 }
1237 }
1238
1238
1239 .review-status-rejected,
1239 .review-status-rejected,
1240 .review-status-forced_closed {
1240 .review-status-forced_closed {
1241 color: @alert2;
1241 color: @alert2;
1242 }
1242 }
1243 .status-tag-rejected,
1243 .status-tag-rejected,
1244 .status-tag-forced_closed {
1244 .status-tag-forced_closed {
1245 border-color: @alert2;
1245 border-color: @alert2;
1246 }
1246 }
1247
1247
1248 .review-status-not_reviewed {
1248 .review-status-not_reviewed {
1249 color: @grey5;
1249 color: @grey5;
1250 }
1250 }
1251 .status-tag-not_reviewed {
1251 .status-tag-not_reviewed {
1252 border-color: @grey5;
1252 border-color: @grey5;
1253 }
1253 }
1254
1254
1255 .test_pattern_preview {
1255 .test_pattern_preview {
1256 margin: @space 0;
1256 margin: @space 0;
1257
1257
1258 p {
1258 p {
1259 margin-bottom: 0;
1259 margin-bottom: 0;
1260 border-bottom: @border-thickness solid @border-default-color;
1260 border-bottom: @border-thickness solid @border-default-color;
1261 color: @grey3;
1261 color: @grey3;
1262 }
1262 }
1263
1263
1264 .btn {
1264 .btn {
1265 margin-bottom: @padding;
1265 margin-bottom: @padding;
1266 }
1266 }
1267 }
1267 }
1268 #test_pattern_result {
1268 #test_pattern_result {
1269 display: none;
1269 display: none;
1270 &:extend(pre);
1270 &:extend(pre);
1271 padding: .9em;
1271 padding: .9em;
1272 color: @grey3;
1272 color: @grey3;
1273 background-color: @grey7;
1273 background-color: @grey7;
1274 border-right: @border-thickness solid @border-default-color;
1274 border-right: @border-thickness solid @border-default-color;
1275 border-bottom: @border-thickness solid @border-default-color;
1275 border-bottom: @border-thickness solid @border-default-color;
1276 border-left: @border-thickness solid @border-default-color;
1276 border-left: @border-thickness solid @border-default-color;
1277 }
1277 }
1278
1278
1279 #repo_vcs_settings {
1279 #repo_vcs_settings {
1280 #inherit_overlay_vcs_default {
1280 #inherit_overlay_vcs_default {
1281 display: none;
1281 display: none;
1282 }
1282 }
1283 #inherit_overlay_vcs_custom {
1283 #inherit_overlay_vcs_custom {
1284 display: custom;
1284 display: custom;
1285 }
1285 }
1286 &.inherited {
1286 &.inherited {
1287 #inherit_overlay_vcs_default {
1287 #inherit_overlay_vcs_default {
1288 display: block;
1288 display: block;
1289 }
1289 }
1290 #inherit_overlay_vcs_custom {
1290 #inherit_overlay_vcs_custom {
1291 display: none;
1291 display: none;
1292 }
1292 }
1293 }
1293 }
1294 }
1294 }
1295
1295
1296 .issue-tracker-link {
1296 .issue-tracker-link {
1297 color: @rcblue;
1297 color: @rcblue;
1298 }
1298 }
1299
1299
1300 // Issue Tracker Table Show/Hide
1300 // Issue Tracker Table Show/Hide
1301 #repo_issue_tracker {
1301 #repo_issue_tracker {
1302 #inherit_overlay {
1302 #inherit_overlay {
1303 display: none;
1303 display: none;
1304 }
1304 }
1305 #custom_overlay {
1305 #custom_overlay {
1306 display: custom;
1306 display: custom;
1307 }
1307 }
1308 &.inherited {
1308 &.inherited {
1309 #inherit_overlay {
1309 #inherit_overlay {
1310 display: block;
1310 display: block;
1311 }
1311 }
1312 #custom_overlay {
1312 #custom_overlay {
1313 display: none;
1313 display: none;
1314 }
1314 }
1315 }
1315 }
1316 }
1316 }
1317 table.issuetracker {
1317 table.issuetracker {
1318 &.readonly {
1318 &.readonly {
1319 tr, td {
1319 tr, td {
1320 color: @grey3;
1320 color: @grey3;
1321 }
1321 }
1322 }
1322 }
1323 .edit {
1323 .edit {
1324 display: none;
1324 display: none;
1325 }
1325 }
1326 .editopen {
1326 .editopen {
1327 .edit {
1327 .edit {
1328 display: inline;
1328 display: inline;
1329 }
1329 }
1330 .entry {
1330 .entry {
1331 display: none;
1331 display: none;
1332 }
1332 }
1333 }
1333 }
1334 tr td.td-action {
1334 tr td.td-action {
1335 min-width: 117px;
1335 min-width: 117px;
1336 }
1336 }
1337 td input {
1337 td input {
1338 max-width: none;
1338 max-width: none;
1339 min-width: 30px;
1339 min-width: 30px;
1340 width: 80%;
1340 width: 80%;
1341 }
1341 }
1342 .issuetracker_pref input {
1342 .issuetracker_pref input {
1343 width: 40%;
1343 width: 40%;
1344 }
1344 }
1345 input.edit_issuetracker_update {
1345 input.edit_issuetracker_update {
1346 margin-right: 0;
1346 margin-right: 0;
1347 width: auto;
1347 width: auto;
1348 }
1348 }
1349 }
1349 }
1350
1350
1351 table.integrations {
1351 table.integrations {
1352 .td-icon {
1352 .td-icon {
1353 width: 20px;
1353 width: 20px;
1354 .integration-icon {
1354 .integration-icon {
1355 height: 20px;
1355 height: 20px;
1356 width: 20px;
1356 width: 20px;
1357 }
1357 }
1358 }
1358 }
1359 }
1359 }
1360
1360
1361 .integrations {
1361 .integrations {
1362 a.integration-box {
1362 a.integration-box {
1363 color: @text-color;
1363 color: @text-color;
1364 &:hover {
1364 &:hover {
1365 .panel {
1365 .panel {
1366 background: #fbfbfb;
1366 background: #fbfbfb;
1367 }
1367 }
1368 }
1368 }
1369 .integration-icon {
1369 .integration-icon {
1370 width: 30px;
1370 width: 30px;
1371 height: 30px;
1371 height: 30px;
1372 margin-right: 20px;
1372 margin-right: 20px;
1373 float: left;
1373 float: left;
1374 }
1374 }
1375
1375
1376 .panel-body {
1376 .panel-body {
1377 padding: 10px;
1377 padding: 10px;
1378 }
1378 }
1379 .panel {
1379 .panel {
1380 margin-bottom: 10px;
1380 margin-bottom: 10px;
1381 }
1381 }
1382 h2 {
1382 h2 {
1383 display: inline-block;
1383 display: inline-block;
1384 margin: 0;
1384 margin: 0;
1385 min-width: 140px;
1385 min-width: 140px;
1386 }
1386 }
1387 }
1387 }
1388 a.integration-box.dummy-integration {
1388 a.integration-box.dummy-integration {
1389 color: @grey4
1389 color: @grey4
1390 }
1390 }
1391 }
1391 }
1392
1392
1393 //Permissions Settings
1393 //Permissions Settings
1394 #add_perm {
1394 #add_perm {
1395 margin: 0 0 @padding;
1395 margin: 0 0 @padding;
1396 cursor: pointer;
1396 cursor: pointer;
1397 }
1397 }
1398
1398
1399 .perm_ac {
1399 .perm_ac {
1400 input {
1400 input {
1401 width: 95%;
1401 width: 95%;
1402 }
1402 }
1403 }
1403 }
1404
1404
1405 .autocomplete-suggestions {
1405 .autocomplete-suggestions {
1406 width: auto !important; // overrides autocomplete.js
1406 width: auto !important; // overrides autocomplete.js
1407 min-width: 278px;
1407 min-width: 278px;
1408 margin: 0;
1408 margin: 0;
1409 border: @border-thickness solid @grey5;
1409 border: @border-thickness solid @grey5;
1410 border-radius: @border-radius;
1410 border-radius: @border-radius;
1411 color: @grey2;
1411 color: @grey2;
1412 background-color: white;
1412 background-color: white;
1413 }
1413 }
1414
1414
1415 .autocomplete-qfilter-suggestions {
1415 .autocomplete-qfilter-suggestions {
1416 width: auto !important; // overrides autocomplete.js
1416 width: auto !important; // overrides autocomplete.js
1417 max-height: 100% !important;
1417 max-height: 100% !important;
1418 min-width: 376px;
1418 min-width: 376px;
1419 margin: 0;
1419 margin: 0;
1420 border: @border-thickness solid @grey5;
1420 border: @border-thickness solid @grey5;
1421 color: @grey2;
1421 color: @grey2;
1422 background-color: white;
1422 background-color: white;
1423 }
1423 }
1424
1424
1425 .autocomplete-selected {
1425 .autocomplete-selected {
1426 background: #F0F0F0;
1426 background: #F0F0F0;
1427 }
1427 }
1428
1428
1429 .ac-container-wrap {
1429 .ac-container-wrap {
1430 margin: 0;
1430 margin: 0;
1431 padding: 8px;
1431 padding: 8px;
1432 border-bottom: @border-thickness solid @grey5;
1432 border-bottom: @border-thickness solid @grey5;
1433 list-style-type: none;
1433 list-style-type: none;
1434 cursor: pointer;
1434 cursor: pointer;
1435
1435
1436 &:hover {
1436 &:hover {
1437 background-color: @grey7;
1437 background-color: @grey7;
1438 }
1438 }
1439
1439
1440 img {
1440 img {
1441 height: @gravatar-size;
1441 height: @gravatar-size;
1442 width: @gravatar-size;
1442 width: @gravatar-size;
1443 margin-right: 1em;
1443 margin-right: 1em;
1444 }
1444 }
1445
1445
1446 strong {
1446 strong {
1447 font-weight: normal;
1447 font-weight: normal;
1448 }
1448 }
1449 }
1449 }
1450
1450
1451 // Settings Dropdown
1451 // Settings Dropdown
1452 .user-menu .container {
1452 .user-menu .container {
1453 padding: 0 4px;
1453 padding: 0 4px;
1454 margin: 0;
1454 margin: 0;
1455 }
1455 }
1456
1456
1457 .user-menu .gravatar {
1457 .user-menu .gravatar {
1458 cursor: pointer;
1458 cursor: pointer;
1459 }
1459 }
1460
1460
1461 .codeblock {
1461 .codeblock {
1462 margin-bottom: @padding;
1462 margin-bottom: @padding;
1463 clear: both;
1463 clear: both;
1464
1464
1465 .stats {
1465 .stats {
1466 overflow: hidden;
1466 overflow: hidden;
1467 }
1467 }
1468
1468
1469 .message{
1469 .message{
1470 textarea{
1470 textarea{
1471 margin: 0;
1471 margin: 0;
1472 }
1472 }
1473 }
1473 }
1474
1474
1475 .code-header {
1475 .code-header {
1476 .stats {
1476 .stats {
1477 line-height: 2em;
1477 line-height: 2em;
1478
1478
1479 .revision_id {
1479 .revision_id {
1480 margin-left: 0;
1480 margin-left: 0;
1481 }
1481 }
1482 .buttons {
1482 .buttons {
1483 padding-right: 0;
1483 padding-right: 0;
1484 }
1484 }
1485 }
1485 }
1486
1486
1487 .item{
1487 .item{
1488 margin-right: 0.5em;
1488 margin-right: 0.5em;
1489 }
1489 }
1490 }
1490 }
1491
1491
1492 #editor_container {
1492 #editor_container {
1493 position: relative;
1493 position: relative;
1494 margin: @padding 10px;
1494 margin: @padding 10px;
1495 }
1495 }
1496 }
1496 }
1497
1497
1498 #file_history_container {
1498 #file_history_container {
1499 display: none;
1499 display: none;
1500 }
1500 }
1501
1501
1502 .file-history-inner {
1502 .file-history-inner {
1503 margin-bottom: 10px;
1503 margin-bottom: 10px;
1504 }
1504 }
1505
1505
1506 // Pull Requests
1506 // Pull Requests
1507 .summary-details {
1507 .summary-details {
1508 width: 100%;
1508 width: 100%;
1509 }
1509 }
1510 .pr-summary {
1510 .pr-summary {
1511 border-bottom: @border-thickness solid @grey5;
1511 border-bottom: @border-thickness solid @grey5;
1512 margin-bottom: @space;
1512 margin-bottom: @space;
1513 }
1513 }
1514
1514
1515 .reviewers {
1515 .reviewers {
1516 width: 98%;
1516 width: 98%;
1517 }
1517 }
1518
1518
1519 .reviewers ul li {
1519 .reviewers ul li {
1520 position: relative;
1520 position: relative;
1521 width: 100%;
1521 width: 100%;
1522 padding-bottom: 8px;
1522 padding-bottom: 8px;
1523 list-style-type: none;
1523 list-style-type: none;
1524 }
1524 }
1525
1525
1526 .reviewer_entry {
1526 .reviewer_entry {
1527 min-height: 55px;
1527 min-height: 55px;
1528 }
1528 }
1529
1529
1530 .reviewer_reason {
1530 .reviewer_reason {
1531 padding-left: 20px;
1531 padding-left: 20px;
1532 line-height: 1.5em;
1532 line-height: 1.5em;
1533 }
1533 }
1534 .reviewer_status {
1534 .reviewer_status {
1535 display: inline-block;
1535 display: inline-block;
1536 width: 20px;
1536 width: 20px;
1537 min-width: 20px;
1537 min-width: 20px;
1538 height: 1.2em;
1538 height: 1.2em;
1539 line-height: 1em;
1539 line-height: 1em;
1540 }
1540 }
1541
1541
1542 .reviewer_name {
1542 .reviewer_name {
1543 display: inline-block;
1543 display: inline-block;
1544 max-width: 83%;
1544 max-width: 83%;
1545 padding-right: 20px;
1545 padding-right: 20px;
1546 vertical-align: middle;
1546 vertical-align: middle;
1547 line-height: 1;
1547 line-height: 1;
1548
1548
1549 .rc-user {
1549 .rc-user {
1550 min-width: 0;
1550 min-width: 0;
1551 margin: -2px 1em 0 0;
1551 margin: -2px 1em 0 0;
1552 }
1552 }
1553
1553
1554 .reviewer {
1554 .reviewer {
1555 float: left;
1555 float: left;
1556 }
1556 }
1557 }
1557 }
1558
1558
1559 .reviewer_member_mandatory {
1559 .reviewer_member_mandatory {
1560 width: 16px;
1560 width: 16px;
1561 font-size: 11px;
1561 font-size: 11px;
1562 margin: 0;
1562 margin: 0;
1563 padding: 0;
1563 padding: 0;
1564 color: black;
1564 color: black;
1565 opacity: 0.4;
1565 opacity: 0.4;
1566 }
1566 }
1567
1567
1568 .reviewer_member_mandatory_remove,
1568 .reviewer_member_mandatory_remove,
1569 .reviewer_member_remove {
1569 .reviewer_member_remove {
1570 width: 16px;
1570 width: 16px;
1571 padding: 0;
1571 padding: 0;
1572 color: black;
1572 color: black;
1573 cursor: pointer;
1573 cursor: pointer;
1574 }
1574 }
1575
1575
1576 .reviewer_member_mandatory_remove {
1576 .reviewer_member_mandatory_remove {
1577 color: @grey4;
1577 color: @grey4;
1578 }
1578 }
1579
1579
1580 .reviewer_member_status {
1580 .reviewer_member_status {
1581 margin-top: 5px;
1581 margin-top: 5px;
1582 }
1582 }
1583 .pr-summary #summary{
1583 .pr-summary #summary{
1584 width: 100%;
1584 width: 100%;
1585 }
1585 }
1586 .pr-summary .action_button:hover {
1586 .pr-summary .action_button:hover {
1587 border: 0;
1587 border: 0;
1588 cursor: pointer;
1588 cursor: pointer;
1589 }
1589 }
1590 .pr-details-title {
1590 .pr-details-title {
1591 height: 20px;
1591 height: 20px;
1592 line-height: 20px;
1592 line-height: 20px;
1593
1593
1594 padding-bottom: 8px;
1594 padding-bottom: 8px;
1595 border-bottom: @border-thickness solid @grey5;
1595 border-bottom: @border-thickness solid @grey5;
1596
1596
1597 .action_button.disabled {
1597 .action_button.disabled {
1598 color: @grey4;
1598 color: @grey4;
1599 cursor: inherit;
1599 cursor: inherit;
1600 }
1600 }
1601 .action_button {
1601 .action_button {
1602 color: @rcblue;
1602 color: @rcblue;
1603 }
1603 }
1604 }
1604 }
1605 .pr-details-content {
1605 .pr-details-content {
1606 margin-top: @textmargin - 5;
1606 margin-top: @textmargin - 5;
1607 margin-bottom: @textmargin - 5;
1607 margin-bottom: @textmargin - 5;
1608 }
1608 }
1609
1609
1610 .pr-reviewer-rules {
1610 .pr-reviewer-rules {
1611 padding: 10px 0px 20px 0px;
1611 padding: 10px 0px 20px 0px;
1612 }
1612 }
1613
1613
1614 .todo-resolved {
1614 .todo-resolved {
1615 text-decoration: line-through;
1615 text-decoration: line-through;
1616 }
1616 }
1617
1617
1618 .todo-table, .comments-table {
1618 .todo-table, .comments-table {
1619 width: 100%;
1619 width: 100%;
1620
1620
1621 td {
1621 td {
1622 padding: 5px 0px;
1622 padding: 5px 0px;
1623 }
1623 }
1624
1624
1625 .td-todo-number {
1625 .td-todo-number {
1626 text-align: left;
1626 text-align: left;
1627 white-space: nowrap;
1627 white-space: nowrap;
1628 width: 1%;
1628 width: 1%;
1629 padding-right: 2px;
1629 padding-right: 2px;
1630 }
1630 }
1631
1631
1632 .td-todo-gravatar {
1632 .td-todo-gravatar {
1633 width: 5%;
1633 width: 5%;
1634
1634
1635 img {
1635 img {
1636 margin: -3px 0;
1636 margin: -3px 0;
1637 }
1637 }
1638 }
1638 }
1639
1639
1640 }
1640 }
1641
1641
1642 .todo-comment-text-wrapper {
1642 .todo-comment-text-wrapper {
1643 display: inline-grid;
1643 display: inline-grid;
1644 }
1644 }
1645
1645
1646 .todo-comment-text {
1646 .todo-comment-text {
1647 margin-left: 5px;
1647 margin-left: 5px;
1648 white-space: nowrap;
1648 white-space: nowrap;
1649 overflow: hidden;
1649 overflow: hidden;
1650 text-overflow: ellipsis;
1650 text-overflow: ellipsis;
1651 }
1651 }
1652
1652
1653 table.group_members {
1653 table.group_members {
1654 width: 100%
1654 width: 100%
1655 }
1655 }
1656
1656
1657 .group_members {
1657 .group_members {
1658 margin-top: 0;
1658 margin-top: 0;
1659 padding: 0;
1659 padding: 0;
1660
1660
1661 img {
1661 img {
1662 height: @gravatar-size;
1662 height: @gravatar-size;
1663 width: @gravatar-size;
1663 width: @gravatar-size;
1664 margin-right: .5em;
1664 margin-right: .5em;
1665 margin-left: 3px;
1665 margin-left: 3px;
1666 }
1666 }
1667
1667
1668 .to-delete {
1668 .to-delete {
1669 .user {
1669 .user {
1670 text-decoration: line-through;
1670 text-decoration: line-through;
1671 }
1671 }
1672 }
1672 }
1673 }
1673 }
1674
1674
1675 .compare_view_commits_title {
1675 .compare_view_commits_title {
1676 .disabled {
1676 .disabled {
1677 cursor: inherit;
1677 cursor: inherit;
1678 &:hover{
1678 &:hover{
1679 background-color: inherit;
1679 background-color: inherit;
1680 color: inherit;
1680 color: inherit;
1681 }
1681 }
1682 }
1682 }
1683 }
1683 }
1684
1684
1685 .subtitle-compare {
1685 .subtitle-compare {
1686 margin: -15px 0px 0px 0px;
1686 margin: -15px 0px 0px 0px;
1687 }
1687 }
1688
1688
1689 // new entry in group_members
1689 // new entry in group_members
1690 .td-author-new-entry {
1690 .td-author-new-entry {
1691 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1691 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1692 }
1692 }
1693
1693
1694 .usergroup_member_remove {
1694 .usergroup_member_remove {
1695 width: 16px;
1695 width: 16px;
1696 margin-bottom: 10px;
1696 margin-bottom: 10px;
1697 padding: 0;
1697 padding: 0;
1698 color: black !important;
1698 color: black !important;
1699 cursor: pointer;
1699 cursor: pointer;
1700 }
1700 }
1701
1701
1702 .reviewer_ac .ac-input {
1702 .reviewer_ac .ac-input {
1703 width: 98%;
1704 margin-bottom: 1em;
1705 }
1706
1707 .observer_ac .ac-input {
1708 width: 98%;
1709 margin-bottom: 1em;
1710 }
1711
1712 .rule-table {
1703 width: 100%;
1713 width: 100%;
1704 margin-bottom: 1em;
1714 }
1715
1716 .rule-table td {
1717
1718 }
1719
1720 .rule-table .td-role {
1721 width: 100px
1722 }
1723
1724 .rule-table .td-mandatory {
1725 width: 100px
1726 }
1727
1728 .rule-table .td-group-votes {
1729 width: 150px
1705 }
1730 }
1706
1731
1707 .compare_view_commits tr{
1732 .compare_view_commits tr{
1708 height: 20px;
1733 height: 20px;
1709 }
1734 }
1710 .compare_view_commits td {
1735 .compare_view_commits td {
1711 vertical-align: top;
1736 vertical-align: top;
1712 padding-top: 10px;
1737 padding-top: 10px;
1713 }
1738 }
1714 .compare_view_commits .author {
1739 .compare_view_commits .author {
1715 margin-left: 5px;
1740 margin-left: 5px;
1716 }
1741 }
1717
1742
1718 .compare_view_commits {
1743 .compare_view_commits {
1719 .color-a {
1744 .color-a {
1720 color: @alert1;
1745 color: @alert1;
1721 }
1746 }
1722
1747
1723 .color-c {
1748 .color-c {
1724 color: @color3;
1749 color: @color3;
1725 }
1750 }
1726
1751
1727 .color-r {
1752 .color-r {
1728 color: @color5;
1753 color: @color5;
1729 }
1754 }
1730
1755
1731 .color-a-bg {
1756 .color-a-bg {
1732 background-color: @alert1;
1757 background-color: @alert1;
1733 }
1758 }
1734
1759
1735 .color-c-bg {
1760 .color-c-bg {
1736 background-color: @alert3;
1761 background-color: @alert3;
1737 }
1762 }
1738
1763
1739 .color-r-bg {
1764 .color-r-bg {
1740 background-color: @alert2;
1765 background-color: @alert2;
1741 }
1766 }
1742
1767
1743 .color-a-border {
1768 .color-a-border {
1744 border: 1px solid @alert1;
1769 border: 1px solid @alert1;
1745 }
1770 }
1746
1771
1747 .color-c-border {
1772 .color-c-border {
1748 border: 1px solid @alert3;
1773 border: 1px solid @alert3;
1749 }
1774 }
1750
1775
1751 .color-r-border {
1776 .color-r-border {
1752 border: 1px solid @alert2;
1777 border: 1px solid @alert2;
1753 }
1778 }
1754
1779
1755 .commit-change-indicator {
1780 .commit-change-indicator {
1756 width: 15px;
1781 width: 15px;
1757 height: 15px;
1782 height: 15px;
1758 position: relative;
1783 position: relative;
1759 left: 15px;
1784 left: 15px;
1760 }
1785 }
1761
1786
1762 .commit-change-content {
1787 .commit-change-content {
1763 text-align: center;
1788 text-align: center;
1764 vertical-align: middle;
1789 vertical-align: middle;
1765 line-height: 15px;
1790 line-height: 15px;
1766 }
1791 }
1767 }
1792 }
1768
1793
1769 .compare_view_filepath {
1794 .compare_view_filepath {
1770 color: @grey1;
1795 color: @grey1;
1771 }
1796 }
1772
1797
1773 .show_more {
1798 .show_more {
1774 display: inline-block;
1799 display: inline-block;
1775 width: 0;
1800 width: 0;
1776 height: 0;
1801 height: 0;
1777 vertical-align: middle;
1802 vertical-align: middle;
1778 content: "";
1803 content: "";
1779 border: 4px solid;
1804 border: 4px solid;
1780 border-right-color: transparent;
1805 border-right-color: transparent;
1781 border-bottom-color: transparent;
1806 border-bottom-color: transparent;
1782 border-left-color: transparent;
1807 border-left-color: transparent;
1783 font-size: 0;
1808 font-size: 0;
1784 }
1809 }
1785
1810
1786 .journal_more .show_more {
1811 .journal_more .show_more {
1787 display: inline;
1812 display: inline;
1788
1813
1789 &:after {
1814 &:after {
1790 content: none;
1815 content: none;
1791 }
1816 }
1792 }
1817 }
1793
1818
1794 .compare_view_commits .collapse_commit:after {
1819 .compare_view_commits .collapse_commit:after {
1795 cursor: pointer;
1820 cursor: pointer;
1796 content: "\00A0\25B4";
1821 content: "\00A0\25B4";
1797 margin-left: -3px;
1822 margin-left: -3px;
1798 font-size: 17px;
1823 font-size: 17px;
1799 color: @grey4;
1824 color: @grey4;
1800 }
1825 }
1801
1826
1802 .diff_links {
1827 .diff_links {
1803 margin-left: 8px;
1828 margin-left: 8px;
1804 }
1829 }
1805
1830
1806 #pull_request_overview {
1831 #pull_request_overview {
1807 div.ancestor {
1832 div.ancestor {
1808 margin: -33px 0;
1833 margin: -33px 0;
1809 }
1834 }
1810 }
1835 }
1811
1836
1812 div.ancestor {
1837 div.ancestor {
1813
1838
1814 }
1839 }
1815
1840
1816 .cs_icon_td input[type="checkbox"] {
1841 .cs_icon_td input[type="checkbox"] {
1817 display: none;
1842 display: none;
1818 }
1843 }
1819
1844
1820 .cs_icon_td .expand_file_icon:after {
1845 .cs_icon_td .expand_file_icon:after {
1821 cursor: pointer;
1846 cursor: pointer;
1822 content: "\00A0\25B6";
1847 content: "\00A0\25B6";
1823 font-size: 12px;
1848 font-size: 12px;
1824 color: @grey4;
1849 color: @grey4;
1825 }
1850 }
1826
1851
1827 .cs_icon_td .collapse_file_icon:after {
1852 .cs_icon_td .collapse_file_icon:after {
1828 cursor: pointer;
1853 cursor: pointer;
1829 content: "\00A0\25BC";
1854 content: "\00A0\25BC";
1830 font-size: 12px;
1855 font-size: 12px;
1831 color: @grey4;
1856 color: @grey4;
1832 }
1857 }
1833
1858
1834 /*new binary
1859 /*new binary
1835 NEW_FILENODE = 1
1860 NEW_FILENODE = 1
1836 DEL_FILENODE = 2
1861 DEL_FILENODE = 2
1837 MOD_FILENODE = 3
1862 MOD_FILENODE = 3
1838 RENAMED_FILENODE = 4
1863 RENAMED_FILENODE = 4
1839 COPIED_FILENODE = 5
1864 COPIED_FILENODE = 5
1840 CHMOD_FILENODE = 6
1865 CHMOD_FILENODE = 6
1841 BIN_FILENODE = 7
1866 BIN_FILENODE = 7
1842 */
1867 */
1843 .cs_files_expand {
1868 .cs_files_expand {
1844 font-size: @basefontsize + 5px;
1869 font-size: @basefontsize + 5px;
1845 line-height: 1.8em;
1870 line-height: 1.8em;
1846 float: right;
1871 float: right;
1847 }
1872 }
1848
1873
1849 .cs_files_expand span{
1874 .cs_files_expand span{
1850 color: @rcblue;
1875 color: @rcblue;
1851 cursor: pointer;
1876 cursor: pointer;
1852 }
1877 }
1853 .cs_files {
1878 .cs_files {
1854 clear: both;
1879 clear: both;
1855 padding-bottom: @padding;
1880 padding-bottom: @padding;
1856
1881
1857 .cur_cs {
1882 .cur_cs {
1858 margin: 10px 2px;
1883 margin: 10px 2px;
1859 font-weight: bold;
1884 font-weight: bold;
1860 }
1885 }
1861
1886
1862 .node {
1887 .node {
1863 float: left;
1888 float: left;
1864 }
1889 }
1865
1890
1866 .changes {
1891 .changes {
1867 float: right;
1892 float: right;
1868 color: white;
1893 color: white;
1869 font-size: @basefontsize - 4px;
1894 font-size: @basefontsize - 4px;
1870 margin-top: 4px;
1895 margin-top: 4px;
1871 opacity: 0.6;
1896 opacity: 0.6;
1872 filter: Alpha(opacity=60); /* IE8 and earlier */
1897 filter: Alpha(opacity=60); /* IE8 and earlier */
1873
1898
1874 .added {
1899 .added {
1875 background-color: @alert1;
1900 background-color: @alert1;
1876 float: left;
1901 float: left;
1877 text-align: center;
1902 text-align: center;
1878 }
1903 }
1879
1904
1880 .deleted {
1905 .deleted {
1881 background-color: @alert2;
1906 background-color: @alert2;
1882 float: left;
1907 float: left;
1883 text-align: center;
1908 text-align: center;
1884 }
1909 }
1885
1910
1886 .bin {
1911 .bin {
1887 background-color: @alert1;
1912 background-color: @alert1;
1888 text-align: center;
1913 text-align: center;
1889 }
1914 }
1890
1915
1891 /*new binary*/
1916 /*new binary*/
1892 .bin.bin1 {
1917 .bin.bin1 {
1893 background-color: @alert1;
1918 background-color: @alert1;
1894 text-align: center;
1919 text-align: center;
1895 }
1920 }
1896
1921
1897 /*deleted binary*/
1922 /*deleted binary*/
1898 .bin.bin2 {
1923 .bin.bin2 {
1899 background-color: @alert2;
1924 background-color: @alert2;
1900 text-align: center;
1925 text-align: center;
1901 }
1926 }
1902
1927
1903 /*mod binary*/
1928 /*mod binary*/
1904 .bin.bin3 {
1929 .bin.bin3 {
1905 background-color: @grey2;
1930 background-color: @grey2;
1906 text-align: center;
1931 text-align: center;
1907 }
1932 }
1908
1933
1909 /*rename file*/
1934 /*rename file*/
1910 .bin.bin4 {
1935 .bin.bin4 {
1911 background-color: @alert4;
1936 background-color: @alert4;
1912 text-align: center;
1937 text-align: center;
1913 }
1938 }
1914
1939
1915 /*copied file*/
1940 /*copied file*/
1916 .bin.bin5 {
1941 .bin.bin5 {
1917 background-color: @alert4;
1942 background-color: @alert4;
1918 text-align: center;
1943 text-align: center;
1919 }
1944 }
1920
1945
1921 /*chmod file*/
1946 /*chmod file*/
1922 .bin.bin6 {
1947 .bin.bin6 {
1923 background-color: @grey2;
1948 background-color: @grey2;
1924 text-align: center;
1949 text-align: center;
1925 }
1950 }
1926 }
1951 }
1927 }
1952 }
1928
1953
1929 .cs_files .cs_added, .cs_files .cs_A,
1954 .cs_files .cs_added, .cs_files .cs_A,
1930 .cs_files .cs_added, .cs_files .cs_M,
1955 .cs_files .cs_added, .cs_files .cs_M,
1931 .cs_files .cs_added, .cs_files .cs_D {
1956 .cs_files .cs_added, .cs_files .cs_D {
1932 height: 16px;
1957 height: 16px;
1933 padding-right: 10px;
1958 padding-right: 10px;
1934 margin-top: 7px;
1959 margin-top: 7px;
1935 text-align: left;
1960 text-align: left;
1936 }
1961 }
1937
1962
1938 .cs_icon_td {
1963 .cs_icon_td {
1939 min-width: 16px;
1964 min-width: 16px;
1940 width: 16px;
1965 width: 16px;
1941 }
1966 }
1942
1967
1943 .pull-request-merge {
1968 .pull-request-merge {
1944 border: 1px solid @grey5;
1969 border: 1px solid @grey5;
1945 padding: 10px 0px 20px;
1970 padding: 10px 0px 20px;
1946 margin-top: 10px;
1971 margin-top: 10px;
1947 margin-bottom: 20px;
1972 margin-bottom: 20px;
1948 }
1973 }
1949
1974
1950 .pull-request-merge-refresh {
1975 .pull-request-merge-refresh {
1951 margin: 2px 7px;
1976 margin: 2px 7px;
1952 a {
1977 a {
1953 color: @grey3;
1978 color: @grey3;
1954 }
1979 }
1955 }
1980 }
1956
1981
1957 .pull-request-merge ul {
1982 .pull-request-merge ul {
1958 padding: 0px 0px;
1983 padding: 0px 0px;
1959 }
1984 }
1960
1985
1961 .pull-request-merge li {
1986 .pull-request-merge li {
1962 list-style-type: none;
1987 list-style-type: none;
1963 }
1988 }
1964
1989
1965 .pull-request-merge .pull-request-wrap {
1990 .pull-request-merge .pull-request-wrap {
1966 height: auto;
1991 height: auto;
1967 padding: 0px 0px;
1992 padding: 0px 0px;
1968 text-align: right;
1993 text-align: right;
1969 }
1994 }
1970
1995
1971 .pull-request-merge span {
1996 .pull-request-merge span {
1972 margin-right: 5px;
1997 margin-right: 5px;
1973 }
1998 }
1974
1999
1975 .pull-request-merge-actions {
2000 .pull-request-merge-actions {
1976 min-height: 30px;
2001 min-height: 30px;
1977 padding: 0px 0px;
2002 padding: 0px 0px;
1978 }
2003 }
1979
2004
1980 .pull-request-merge-info {
2005 .pull-request-merge-info {
1981 padding: 0px 5px 5px 0px;
2006 padding: 0px 5px 5px 0px;
1982 }
2007 }
1983
2008
1984 .merge-status {
2009 .merge-status {
1985 margin-right: 5px;
2010 margin-right: 5px;
1986 }
2011 }
1987
2012
1988 .merge-message {
2013 .merge-message {
1989 font-size: 1.2em
2014 font-size: 1.2em
1990 }
2015 }
1991
2016
1992 .merge-message.success i,
2017 .merge-message.success i,
1993 .merge-icon.success i {
2018 .merge-icon.success i {
1994 color:@alert1;
2019 color:@alert1;
1995 }
2020 }
1996
2021
1997 .merge-message.warning i,
2022 .merge-message.warning i,
1998 .merge-icon.warning i {
2023 .merge-icon.warning i {
1999 color: @alert3;
2024 color: @alert3;
2000 }
2025 }
2001
2026
2002 .merge-message.error i,
2027 .merge-message.error i,
2003 .merge-icon.error i {
2028 .merge-icon.error i {
2004 color:@alert2;
2029 color:@alert2;
2005 }
2030 }
2006
2031
2007 .pr-versions {
2032 .pr-versions {
2008 font-size: 1.1em;
2033 font-size: 1.1em;
2009 padding: 7.5px;
2034 padding: 7.5px;
2010
2035
2011 table {
2036 table {
2012
2037
2013 }
2038 }
2014
2039
2015 td {
2040 td {
2016 line-height: 15px;
2041 line-height: 15px;
2017 }
2042 }
2018
2043
2019 .compare-radio-button {
2044 .compare-radio-button {
2020 position: relative;
2045 position: relative;
2021 top: -3px;
2046 top: -3px;
2022 }
2047 }
2023 }
2048 }
2024
2049
2025
2050
2026 #close_pull_request {
2051 #close_pull_request {
2027 margin-right: 0px;
2052 margin-right: 0px;
2028 }
2053 }
2029
2054
2030 .empty_data {
2055 .empty_data {
2031 color: @grey4;
2056 color: @grey4;
2032 }
2057 }
2033
2058
2034 #changeset_compare_view_content {
2059 #changeset_compare_view_content {
2035 clear: both;
2060 clear: both;
2036 width: 100%;
2061 width: 100%;
2037 box-sizing: border-box;
2062 box-sizing: border-box;
2038 .border-radius(@border-radius);
2063 .border-radius(@border-radius);
2039
2064
2040 .help-block {
2065 .help-block {
2041 margin: @padding 0;
2066 margin: @padding 0;
2042 color: @text-color;
2067 color: @text-color;
2043 &.pre-formatting {
2068 &.pre-formatting {
2044 white-space: pre;
2069 white-space: pre;
2045 }
2070 }
2046 }
2071 }
2047
2072
2048 .empty_data {
2073 .empty_data {
2049 margin: @padding 0;
2074 margin: @padding 0;
2050 }
2075 }
2051
2076
2052 .alert {
2077 .alert {
2053 margin-bottom: @space;
2078 margin-bottom: @space;
2054 }
2079 }
2055 }
2080 }
2056
2081
2057 .table_disp {
2082 .table_disp {
2058 .status {
2083 .status {
2059 width: auto;
2084 width: auto;
2060 }
2085 }
2061 }
2086 }
2062
2087
2063
2088
2064 .creation_in_progress {
2089 .creation_in_progress {
2065 color: @grey4
2090 color: @grey4
2066 }
2091 }
2067
2092
2068 .status_box_menu {
2093 .status_box_menu {
2069 margin: 0;
2094 margin: 0;
2070 }
2095 }
2071
2096
2072 .notification-table{
2097 .notification-table{
2073 margin-bottom: @space;
2098 margin-bottom: @space;
2074 display: table;
2099 display: table;
2075 width: 100%;
2100 width: 100%;
2076
2101
2077 .container{
2102 .container{
2078 display: table-row;
2103 display: table-row;
2079
2104
2080 .notification-header{
2105 .notification-header{
2081 border-bottom: @border-thickness solid @border-default-color;
2106 border-bottom: @border-thickness solid @border-default-color;
2082 }
2107 }
2083
2108
2084 .notification-subject{
2109 .notification-subject{
2085 display: table-cell;
2110 display: table-cell;
2086 }
2111 }
2087 }
2112 }
2088 }
2113 }
2089
2114
2090 // Notifications
2115 // Notifications
2091 .notification-header{
2116 .notification-header{
2092 display: table;
2117 display: table;
2093 width: 100%;
2118 width: 100%;
2094 padding: floor(@basefontsize/2) 0;
2119 padding: floor(@basefontsize/2) 0;
2095 line-height: 1em;
2120 line-height: 1em;
2096
2121
2097 .desc, .delete-notifications, .read-notifications{
2122 .desc, .delete-notifications, .read-notifications{
2098 display: table-cell;
2123 display: table-cell;
2099 text-align: left;
2124 text-align: left;
2100 }
2125 }
2101
2126
2102 .delete-notifications, .read-notifications{
2127 .delete-notifications, .read-notifications{
2103 width: 35px;
2128 width: 35px;
2104 min-width: 35px; //fixes when only one button is displayed
2129 min-width: 35px; //fixes when only one button is displayed
2105 }
2130 }
2106 }
2131 }
2107
2132
2108 .notification-body {
2133 .notification-body {
2109 .markdown-block,
2134 .markdown-block,
2110 .rst-block {
2135 .rst-block {
2111 padding: @padding 0;
2136 padding: @padding 0;
2112 }
2137 }
2113
2138
2114 .notification-subject {
2139 .notification-subject {
2115 padding: @textmargin 0;
2140 padding: @textmargin 0;
2116 border-bottom: @border-thickness solid @border-default-color;
2141 border-bottom: @border-thickness solid @border-default-color;
2117 }
2142 }
2118 }
2143 }
2119
2144
2120 .notice-messages {
2145 .notice-messages {
2121 .markdown-block,
2146 .markdown-block,
2122 .rst-block {
2147 .rst-block {
2123 padding: 0;
2148 padding: 0;
2124 }
2149 }
2125 }
2150 }
2126
2151
2127 .notifications_buttons{
2152 .notifications_buttons{
2128 float: right;
2153 float: right;
2129 }
2154 }
2130
2155
2131 #notification-status{
2156 #notification-status{
2132 display: inline;
2157 display: inline;
2133 }
2158 }
2134
2159
2135 // Repositories
2160 // Repositories
2136
2161
2137 #summary.fields{
2162 #summary.fields{
2138 display: table;
2163 display: table;
2139
2164
2140 .field{
2165 .field{
2141 display: table-row;
2166 display: table-row;
2142
2167
2143 .label-summary{
2168 .label-summary{
2144 display: table-cell;
2169 display: table-cell;
2145 min-width: @label-summary-minwidth;
2170 min-width: @label-summary-minwidth;
2146 padding-top: @padding/2;
2171 padding-top: @padding/2;
2147 padding-bottom: @padding/2;
2172 padding-bottom: @padding/2;
2148 padding-right: @padding/2;
2173 padding-right: @padding/2;
2149 }
2174 }
2150
2175
2151 .input{
2176 .input{
2152 display: table-cell;
2177 display: table-cell;
2153 padding: @padding/2;
2178 padding: @padding/2;
2154
2179
2155 input{
2180 input{
2156 min-width: 29em;
2181 min-width: 29em;
2157 padding: @padding/4;
2182 padding: @padding/4;
2158 }
2183 }
2159 }
2184 }
2160 .statistics, .downloads{
2185 .statistics, .downloads{
2161 .disabled{
2186 .disabled{
2162 color: @grey4;
2187 color: @grey4;
2163 }
2188 }
2164 }
2189 }
2165 }
2190 }
2166 }
2191 }
2167
2192
2168 #summary{
2193 #summary{
2169 width: 70%;
2194 width: 70%;
2170 }
2195 }
2171
2196
2172
2197
2173 // Journal
2198 // Journal
2174 .journal.title {
2199 .journal.title {
2175 h5 {
2200 h5 {
2176 float: left;
2201 float: left;
2177 margin: 0;
2202 margin: 0;
2178 width: 70%;
2203 width: 70%;
2179 }
2204 }
2180
2205
2181 ul {
2206 ul {
2182 float: right;
2207 float: right;
2183 display: inline-block;
2208 display: inline-block;
2184 margin: 0;
2209 margin: 0;
2185 width: 30%;
2210 width: 30%;
2186 text-align: right;
2211 text-align: right;
2187
2212
2188 li {
2213 li {
2189 display: inline;
2214 display: inline;
2190 font-size: @journal-fontsize;
2215 font-size: @journal-fontsize;
2191 line-height: 1em;
2216 line-height: 1em;
2192
2217
2193 list-style-type: none;
2218 list-style-type: none;
2194 }
2219 }
2195 }
2220 }
2196 }
2221 }
2197
2222
2198 .filterexample {
2223 .filterexample {
2199 position: absolute;
2224 position: absolute;
2200 top: 95px;
2225 top: 95px;
2201 left: @contentpadding;
2226 left: @contentpadding;
2202 color: @rcblue;
2227 color: @rcblue;
2203 font-size: 11px;
2228 font-size: 11px;
2204 font-family: @text-regular;
2229 font-family: @text-regular;
2205 cursor: help;
2230 cursor: help;
2206
2231
2207 &:hover {
2232 &:hover {
2208 color: @rcdarkblue;
2233 color: @rcdarkblue;
2209 }
2234 }
2210
2235
2211 @media (max-width:768px) {
2236 @media (max-width:768px) {
2212 position: relative;
2237 position: relative;
2213 top: auto;
2238 top: auto;
2214 left: auto;
2239 left: auto;
2215 display: block;
2240 display: block;
2216 }
2241 }
2217 }
2242 }
2218
2243
2219
2244
2220 #journal{
2245 #journal{
2221 margin-bottom: @space;
2246 margin-bottom: @space;
2222
2247
2223 .journal_day{
2248 .journal_day{
2224 margin-bottom: @textmargin/2;
2249 margin-bottom: @textmargin/2;
2225 padding-bottom: @textmargin/2;
2250 padding-bottom: @textmargin/2;
2226 font-size: @journal-fontsize;
2251 font-size: @journal-fontsize;
2227 border-bottom: @border-thickness solid @border-default-color;
2252 border-bottom: @border-thickness solid @border-default-color;
2228 }
2253 }
2229
2254
2230 .journal_container{
2255 .journal_container{
2231 margin-bottom: @space;
2256 margin-bottom: @space;
2232
2257
2233 .journal_user{
2258 .journal_user{
2234 display: inline-block;
2259 display: inline-block;
2235 }
2260 }
2236 .journal_action_container{
2261 .journal_action_container{
2237 display: block;
2262 display: block;
2238 margin-top: @textmargin;
2263 margin-top: @textmargin;
2239
2264
2240 div{
2265 div{
2241 display: inline;
2266 display: inline;
2242 }
2267 }
2243
2268
2244 div.journal_action_params{
2269 div.journal_action_params{
2245 display: block;
2270 display: block;
2246 }
2271 }
2247
2272
2248 div.journal_repo:after{
2273 div.journal_repo:after{
2249 content: "\A";
2274 content: "\A";
2250 white-space: pre;
2275 white-space: pre;
2251 }
2276 }
2252
2277
2253 div.date{
2278 div.date{
2254 display: block;
2279 display: block;
2255 margin-bottom: @textmargin;
2280 margin-bottom: @textmargin;
2256 }
2281 }
2257 }
2282 }
2258 }
2283 }
2259 }
2284 }
2260
2285
2261 // Files
2286 // Files
2262 .edit-file-title {
2287 .edit-file-title {
2263 font-size: 16px;
2288 font-size: 16px;
2264
2289
2265 .title-heading {
2290 .title-heading {
2266 padding: 2px;
2291 padding: 2px;
2267 }
2292 }
2268 }
2293 }
2269
2294
2270 .edit-file-fieldset {
2295 .edit-file-fieldset {
2271 margin: @sidebarpadding 0;
2296 margin: @sidebarpadding 0;
2272
2297
2273 .fieldset {
2298 .fieldset {
2274 .left-label {
2299 .left-label {
2275 width: 13%;
2300 width: 13%;
2276 }
2301 }
2277 .right-content {
2302 .right-content {
2278 width: 87%;
2303 width: 87%;
2279 max-width: 100%;
2304 max-width: 100%;
2280 }
2305 }
2281 .filename-label {
2306 .filename-label {
2282 margin-top: 13px;
2307 margin-top: 13px;
2283 }
2308 }
2284 .commit-message-label {
2309 .commit-message-label {
2285 margin-top: 4px;
2310 margin-top: 4px;
2286 }
2311 }
2287 .file-upload-input {
2312 .file-upload-input {
2288 input {
2313 input {
2289 display: none;
2314 display: none;
2290 }
2315 }
2291 margin-top: 10px;
2316 margin-top: 10px;
2292 }
2317 }
2293 .file-upload-label {
2318 .file-upload-label {
2294 margin-top: 10px;
2319 margin-top: 10px;
2295 }
2320 }
2296 p {
2321 p {
2297 margin-top: 5px;
2322 margin-top: 5px;
2298 }
2323 }
2299
2324
2300 }
2325 }
2301 .custom-path-link {
2326 .custom-path-link {
2302 margin-left: 5px;
2327 margin-left: 5px;
2303 }
2328 }
2304 #commit {
2329 #commit {
2305 resize: vertical;
2330 resize: vertical;
2306 }
2331 }
2307 }
2332 }
2308
2333
2309 .delete-file-preview {
2334 .delete-file-preview {
2310 max-height: 250px;
2335 max-height: 250px;
2311 }
2336 }
2312
2337
2313 .new-file,
2338 .new-file,
2314 #filter_activate,
2339 #filter_activate,
2315 #filter_deactivate {
2340 #filter_deactivate {
2316 float: right;
2341 float: right;
2317 margin: 0 0 0 10px;
2342 margin: 0 0 0 10px;
2318 }
2343 }
2319
2344
2320 .file-upload-transaction-wrapper {
2345 .file-upload-transaction-wrapper {
2321 margin-top: 57px;
2346 margin-top: 57px;
2322 clear: both;
2347 clear: both;
2323 }
2348 }
2324
2349
2325 .file-upload-transaction-wrapper .error {
2350 .file-upload-transaction-wrapper .error {
2326 color: @color5;
2351 color: @color5;
2327 }
2352 }
2328
2353
2329 .file-upload-transaction {
2354 .file-upload-transaction {
2330 min-height: 200px;
2355 min-height: 200px;
2331 padding: 54px;
2356 padding: 54px;
2332 border: 1px solid @grey5;
2357 border: 1px solid @grey5;
2333 text-align: center;
2358 text-align: center;
2334 clear: both;
2359 clear: both;
2335 }
2360 }
2336
2361
2337 .file-upload-transaction i {
2362 .file-upload-transaction i {
2338 font-size: 48px
2363 font-size: 48px
2339 }
2364 }
2340
2365
2341 h3.files_location{
2366 h3.files_location{
2342 line-height: 2.4em;
2367 line-height: 2.4em;
2343 }
2368 }
2344
2369
2345 .browser-nav {
2370 .browser-nav {
2346 width: 100%;
2371 width: 100%;
2347 display: table;
2372 display: table;
2348 margin-bottom: 20px;
2373 margin-bottom: 20px;
2349
2374
2350 .info_box {
2375 .info_box {
2351 float: left;
2376 float: left;
2352 display: inline-table;
2377 display: inline-table;
2353 height: 2.5em;
2378 height: 2.5em;
2354
2379
2355 .browser-cur-rev, .info_box_elem {
2380 .browser-cur-rev, .info_box_elem {
2356 display: table-cell;
2381 display: table-cell;
2357 vertical-align: middle;
2382 vertical-align: middle;
2358 }
2383 }
2359
2384
2360 .drop-menu {
2385 .drop-menu {
2361 margin: 0 10px;
2386 margin: 0 10px;
2362 }
2387 }
2363
2388
2364 .info_box_elem {
2389 .info_box_elem {
2365 border-top: @border-thickness solid @grey5;
2390 border-top: @border-thickness solid @grey5;
2366 border-bottom: @border-thickness solid @grey5;
2391 border-bottom: @border-thickness solid @grey5;
2367 box-shadow: @button-shadow;
2392 box-shadow: @button-shadow;
2368
2393
2369 #at_rev, a {
2394 #at_rev, a {
2370 padding: 0.6em 0.4em;
2395 padding: 0.6em 0.4em;
2371 margin: 0;
2396 margin: 0;
2372 .box-shadow(none);
2397 .box-shadow(none);
2373 border: 0;
2398 border: 0;
2374 height: 12px;
2399 height: 12px;
2375 color: @grey2;
2400 color: @grey2;
2376 }
2401 }
2377
2402
2378 input#at_rev {
2403 input#at_rev {
2379 max-width: 50px;
2404 max-width: 50px;
2380 text-align: center;
2405 text-align: center;
2381 }
2406 }
2382
2407
2383 &.previous {
2408 &.previous {
2384 border: @border-thickness solid @grey5;
2409 border: @border-thickness solid @grey5;
2385 border-top-left-radius: @border-radius;
2410 border-top-left-radius: @border-radius;
2386 border-bottom-left-radius: @border-radius;
2411 border-bottom-left-radius: @border-radius;
2387
2412
2388 &:hover {
2413 &:hover {
2389 border-color: @grey4;
2414 border-color: @grey4;
2390 }
2415 }
2391
2416
2392 .disabled {
2417 .disabled {
2393 color: @grey5;
2418 color: @grey5;
2394 cursor: not-allowed;
2419 cursor: not-allowed;
2395 opacity: 0.5;
2420 opacity: 0.5;
2396 }
2421 }
2397 }
2422 }
2398
2423
2399 &.next {
2424 &.next {
2400 border: @border-thickness solid @grey5;
2425 border: @border-thickness solid @grey5;
2401 border-top-right-radius: @border-radius;
2426 border-top-right-radius: @border-radius;
2402 border-bottom-right-radius: @border-radius;
2427 border-bottom-right-radius: @border-radius;
2403
2428
2404 &:hover {
2429 &:hover {
2405 border-color: @grey4;
2430 border-color: @grey4;
2406 }
2431 }
2407
2432
2408 .disabled {
2433 .disabled {
2409 color: @grey5;
2434 color: @grey5;
2410 cursor: not-allowed;
2435 cursor: not-allowed;
2411 opacity: 0.5;
2436 opacity: 0.5;
2412 }
2437 }
2413 }
2438 }
2414 }
2439 }
2415
2440
2416 .browser-cur-rev {
2441 .browser-cur-rev {
2417
2442
2418 span{
2443 span{
2419 margin: 0;
2444 margin: 0;
2420 color: @rcblue;
2445 color: @rcblue;
2421 height: 12px;
2446 height: 12px;
2422 display: inline-block;
2447 display: inline-block;
2423 padding: 0.7em 1em ;
2448 padding: 0.7em 1em ;
2424 border: @border-thickness solid @rcblue;
2449 border: @border-thickness solid @rcblue;
2425 margin-right: @padding;
2450 margin-right: @padding;
2426 }
2451 }
2427 }
2452 }
2428
2453
2429 }
2454 }
2430
2455
2431 .select-index-number {
2456 .select-index-number {
2432 margin: 0 0 0 20px;
2457 margin: 0 0 0 20px;
2433 color: @grey3;
2458 color: @grey3;
2434 }
2459 }
2435
2460
2436 .search_activate {
2461 .search_activate {
2437 display: table-cell;
2462 display: table-cell;
2438 vertical-align: middle;
2463 vertical-align: middle;
2439
2464
2440 input, label{
2465 input, label{
2441 margin: 0;
2466 margin: 0;
2442 padding: 0;
2467 padding: 0;
2443 }
2468 }
2444
2469
2445 input{
2470 input{
2446 margin-left: @textmargin;
2471 margin-left: @textmargin;
2447 }
2472 }
2448
2473
2449 }
2474 }
2450 }
2475 }
2451
2476
2452 .browser-cur-rev{
2477 .browser-cur-rev{
2453 margin-bottom: @textmargin;
2478 margin-bottom: @textmargin;
2454 }
2479 }
2455
2480
2456 #node_filter_box_loading{
2481 #node_filter_box_loading{
2457 .info_text;
2482 .info_text;
2458 }
2483 }
2459
2484
2460 .browser-search {
2485 .browser-search {
2461 margin: -25px 0px 5px 0px;
2486 margin: -25px 0px 5px 0px;
2462 }
2487 }
2463
2488
2464 .files-quick-filter {
2489 .files-quick-filter {
2465 float: right;
2490 float: right;
2466 width: 180px;
2491 width: 180px;
2467 position: relative;
2492 position: relative;
2468 }
2493 }
2469
2494
2470 .files-filter-box {
2495 .files-filter-box {
2471 display: flex;
2496 display: flex;
2472 padding: 0px;
2497 padding: 0px;
2473 border-radius: 3px;
2498 border-radius: 3px;
2474 margin-bottom: 0;
2499 margin-bottom: 0;
2475
2500
2476 a {
2501 a {
2477 border: none !important;
2502 border: none !important;
2478 }
2503 }
2479
2504
2480 li {
2505 li {
2481 list-style-type: none
2506 list-style-type: none
2482 }
2507 }
2483 }
2508 }
2484
2509
2485 .files-filter-box-path {
2510 .files-filter-box-path {
2486 line-height: 33px;
2511 line-height: 33px;
2487 padding: 0;
2512 padding: 0;
2488 width: 20px;
2513 width: 20px;
2489 position: absolute;
2514 position: absolute;
2490 z-index: 11;
2515 z-index: 11;
2491 left: 5px;
2516 left: 5px;
2492 }
2517 }
2493
2518
2494 .files-filter-box-input {
2519 .files-filter-box-input {
2495 margin-right: 0;
2520 margin-right: 0;
2496
2521
2497 input {
2522 input {
2498 border: 1px solid @white;
2523 border: 1px solid @white;
2499 padding-left: 25px;
2524 padding-left: 25px;
2500 width: 145px;
2525 width: 145px;
2501
2526
2502 &:hover {
2527 &:hover {
2503 border-color: @grey6;
2528 border-color: @grey6;
2504 }
2529 }
2505
2530
2506 &:focus {
2531 &:focus {
2507 border-color: @grey5;
2532 border-color: @grey5;
2508 }
2533 }
2509 }
2534 }
2510 }
2535 }
2511
2536
2512 .browser-result{
2537 .browser-result{
2513 td a{
2538 td a{
2514 margin-left: 0.5em;
2539 margin-left: 0.5em;
2515 display: inline-block;
2540 display: inline-block;
2516
2541
2517 em {
2542 em {
2518 font-weight: @text-bold-weight;
2543 font-weight: @text-bold-weight;
2519 font-family: @text-bold;
2544 font-family: @text-bold;
2520 }
2545 }
2521 }
2546 }
2522 }
2547 }
2523
2548
2524 .browser-highlight{
2549 .browser-highlight{
2525 background-color: @grey5-alpha;
2550 background-color: @grey5-alpha;
2526 }
2551 }
2527
2552
2528
2553
2529 .edit-file-fieldset #location,
2554 .edit-file-fieldset #location,
2530 .edit-file-fieldset #filename {
2555 .edit-file-fieldset #filename {
2531 display: flex;
2556 display: flex;
2532 width: -moz-available; /* WebKit-based browsers will ignore this. */
2557 width: -moz-available; /* WebKit-based browsers will ignore this. */
2533 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2558 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2534 width: fill-available;
2559 width: fill-available;
2535 border: 0;
2560 border: 0;
2536 }
2561 }
2537
2562
2538 .path-items {
2563 .path-items {
2539 display: flex;
2564 display: flex;
2540 padding: 0;
2565 padding: 0;
2541 border: 1px solid #eeeeee;
2566 border: 1px solid #eeeeee;
2542 width: 100%;
2567 width: 100%;
2543 float: left;
2568 float: left;
2544
2569
2545 .breadcrumb-path {
2570 .breadcrumb-path {
2546 line-height: 30px;
2571 line-height: 30px;
2547 padding: 0 4px;
2572 padding: 0 4px;
2548 white-space: nowrap;
2573 white-space: nowrap;
2549 }
2574 }
2550
2575
2551 .upload-form {
2576 .upload-form {
2552 margin-top: 46px;
2577 margin-top: 46px;
2553 }
2578 }
2554
2579
2555 .location-path {
2580 .location-path {
2556 width: -moz-available; /* WebKit-based browsers will ignore this. */
2581 width: -moz-available; /* WebKit-based browsers will ignore this. */
2557 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2582 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2558 width: fill-available;
2583 width: fill-available;
2559
2584
2560 .file-name-input {
2585 .file-name-input {
2561 padding: 0.5em 0;
2586 padding: 0.5em 0;
2562 }
2587 }
2563
2588
2564 }
2589 }
2565
2590
2566 ul {
2591 ul {
2567 display: flex;
2592 display: flex;
2568 margin: 0;
2593 margin: 0;
2569 padding: 0;
2594 padding: 0;
2570 width: 100%;
2595 width: 100%;
2571 }
2596 }
2572
2597
2573 li {
2598 li {
2574 list-style-type: none;
2599 list-style-type: none;
2575 }
2600 }
2576
2601
2577 }
2602 }
2578
2603
2579 .editor-items {
2604 .editor-items {
2580 height: 40px;
2605 height: 40px;
2581 margin: 10px 0 -17px 10px;
2606 margin: 10px 0 -17px 10px;
2582
2607
2583 .editor-action {
2608 .editor-action {
2584 cursor: pointer;
2609 cursor: pointer;
2585 }
2610 }
2586
2611
2587 .editor-action.active {
2612 .editor-action.active {
2588 border-bottom: 2px solid #5C5C5C;
2613 border-bottom: 2px solid #5C5C5C;
2589 }
2614 }
2590
2615
2591 li {
2616 li {
2592 list-style-type: none;
2617 list-style-type: none;
2593 }
2618 }
2594 }
2619 }
2595
2620
2596 .edit-file-fieldset .message textarea {
2621 .edit-file-fieldset .message textarea {
2597 border: 1px solid #eeeeee;
2622 border: 1px solid #eeeeee;
2598 }
2623 }
2599
2624
2600 #files_data .codeblock {
2625 #files_data .codeblock {
2601 background-color: #F5F5F5;
2626 background-color: #F5F5F5;
2602 }
2627 }
2603
2628
2604 #editor_preview {
2629 #editor_preview {
2605 background: white;
2630 background: white;
2606 }
2631 }
2607
2632
2608 .show-editor {
2633 .show-editor {
2609 padding: 10px;
2634 padding: 10px;
2610 background-color: white;
2635 background-color: white;
2611
2636
2612 }
2637 }
2613
2638
2614 .show-preview {
2639 .show-preview {
2615 padding: 10px;
2640 padding: 10px;
2616 background-color: white;
2641 background-color: white;
2617 border-left: 1px solid #eeeeee;
2642 border-left: 1px solid #eeeeee;
2618 }
2643 }
2619 // quick filter
2644 // quick filter
2620 .grid-quick-filter {
2645 .grid-quick-filter {
2621 float: right;
2646 float: right;
2622 position: relative;
2647 position: relative;
2623 }
2648 }
2624
2649
2625 .grid-filter-box {
2650 .grid-filter-box {
2626 display: flex;
2651 display: flex;
2627 padding: 0px;
2652 padding: 0px;
2628 border-radius: 3px;
2653 border-radius: 3px;
2629 margin-bottom: 0;
2654 margin-bottom: 0;
2630
2655
2631 a {
2656 a {
2632 border: none !important;
2657 border: none !important;
2633 }
2658 }
2634
2659
2635 li {
2660 li {
2636 list-style-type: none
2661 list-style-type: none
2637 }
2662 }
2638 }
2663 }
2639
2664
2640 .grid-filter-box-icon {
2665 .grid-filter-box-icon {
2641 line-height: 33px;
2666 line-height: 33px;
2642 padding: 0;
2667 padding: 0;
2643 width: 20px;
2668 width: 20px;
2644 position: absolute;
2669 position: absolute;
2645 z-index: 11;
2670 z-index: 11;
2646 left: 5px;
2671 left: 5px;
2647 }
2672 }
2648
2673
2649 .grid-filter-box-input {
2674 .grid-filter-box-input {
2650 margin-right: 0;
2675 margin-right: 0;
2651
2676
2652 input {
2677 input {
2653 border: 1px solid @white;
2678 border: 1px solid @white;
2654 padding-left: 25px;
2679 padding-left: 25px;
2655 width: 145px;
2680 width: 145px;
2656
2681
2657 &:hover {
2682 &:hover {
2658 border-color: @grey6;
2683 border-color: @grey6;
2659 }
2684 }
2660
2685
2661 &:focus {
2686 &:focus {
2662 border-color: @grey5;
2687 border-color: @grey5;
2663 }
2688 }
2664 }
2689 }
2665 }
2690 }
2666
2691
2667
2692
2668
2693
2669 // Search
2694 // Search
2670
2695
2671 .search-form{
2696 .search-form{
2672 #q {
2697 #q {
2673 width: @search-form-width;
2698 width: @search-form-width;
2674 }
2699 }
2675 .fields{
2700 .fields{
2676 margin: 0 0 @space;
2701 margin: 0 0 @space;
2677 }
2702 }
2678
2703
2679 label{
2704 label{
2680 display: inline-block;
2705 display: inline-block;
2681 margin-right: @textmargin;
2706 margin-right: @textmargin;
2682 padding-top: 0.25em;
2707 padding-top: 0.25em;
2683 }
2708 }
2684
2709
2685
2710
2686 .results{
2711 .results{
2687 clear: both;
2712 clear: both;
2688 margin: 0 0 @padding;
2713 margin: 0 0 @padding;
2689 }
2714 }
2690
2715
2691 .search-tags {
2716 .search-tags {
2692 padding: 5px 0;
2717 padding: 5px 0;
2693 }
2718 }
2694 }
2719 }
2695
2720
2696 div.search-feedback-items {
2721 div.search-feedback-items {
2697 display: inline-block;
2722 display: inline-block;
2698 }
2723 }
2699
2724
2700 div.search-code-body {
2725 div.search-code-body {
2701 background-color: #ffffff; padding: 5px 0 5px 10px;
2726 background-color: #ffffff; padding: 5px 0 5px 10px;
2702 pre {
2727 pre {
2703 .match { background-color: #faffa6;}
2728 .match { background-color: #faffa6;}
2704 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2729 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2705 }
2730 }
2706 }
2731 }
2707
2732
2708 .expand_commit.search {
2733 .expand_commit.search {
2709 .show_more.open {
2734 .show_more.open {
2710 height: auto;
2735 height: auto;
2711 max-height: none;
2736 max-height: none;
2712 }
2737 }
2713 }
2738 }
2714
2739
2715 .search-results {
2740 .search-results {
2716
2741
2717 h2 {
2742 h2 {
2718 margin-bottom: 0;
2743 margin-bottom: 0;
2719 }
2744 }
2720 .codeblock {
2745 .codeblock {
2721 border: none;
2746 border: none;
2722 background: transparent;
2747 background: transparent;
2723 }
2748 }
2724
2749
2725 .codeblock-header {
2750 .codeblock-header {
2726 border: none;
2751 border: none;
2727 background: transparent;
2752 background: transparent;
2728 }
2753 }
2729
2754
2730 .code-body {
2755 .code-body {
2731 border: @border-thickness solid @grey6;
2756 border: @border-thickness solid @grey6;
2732 .border-radius(@border-radius);
2757 .border-radius(@border-radius);
2733 }
2758 }
2734
2759
2735 .td-commit {
2760 .td-commit {
2736 &:extend(pre);
2761 &:extend(pre);
2737 border-bottom: @border-thickness solid @border-default-color;
2762 border-bottom: @border-thickness solid @border-default-color;
2738 }
2763 }
2739
2764
2740 .message {
2765 .message {
2741 height: auto;
2766 height: auto;
2742 max-width: 350px;
2767 max-width: 350px;
2743 white-space: normal;
2768 white-space: normal;
2744 text-overflow: initial;
2769 text-overflow: initial;
2745 overflow: visible;
2770 overflow: visible;
2746
2771
2747 .match { background-color: #faffa6;}
2772 .match { background-color: #faffa6;}
2748 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2773 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2749 }
2774 }
2750
2775
2751 .path {
2776 .path {
2752 border-bottom: none !important;
2777 border-bottom: none !important;
2753 border-left: 1px solid @grey6 !important;
2778 border-left: 1px solid @grey6 !important;
2754 border-right: 1px solid @grey6 !important;
2779 border-right: 1px solid @grey6 !important;
2755 }
2780 }
2756 }
2781 }
2757
2782
2758 table.rctable td.td-search-results div {
2783 table.rctable td.td-search-results div {
2759 max-width: 100%;
2784 max-width: 100%;
2760 }
2785 }
2761
2786
2762 #tip-box, .tip-box{
2787 #tip-box, .tip-box{
2763 padding: @menupadding/2;
2788 padding: @menupadding/2;
2764 display: block;
2789 display: block;
2765 border: @border-thickness solid @border-highlight-color;
2790 border: @border-thickness solid @border-highlight-color;
2766 .border-radius(@border-radius);
2791 .border-radius(@border-radius);
2767 background-color: white;
2792 background-color: white;
2768 z-index: 99;
2793 z-index: 99;
2769 white-space: pre-wrap;
2794 white-space: pre-wrap;
2770 }
2795 }
2771
2796
2772 #linktt {
2797 #linktt {
2773 width: 79px;
2798 width: 79px;
2774 }
2799 }
2775
2800
2776 #help_kb .modal-content{
2801 #help_kb .modal-content{
2777 max-width: 800px;
2802 max-width: 800px;
2778 margin: 10% auto;
2803 margin: 10% auto;
2779
2804
2780 table{
2805 table{
2781 td,th{
2806 td,th{
2782 border-bottom: none;
2807 border-bottom: none;
2783 line-height: 2.5em;
2808 line-height: 2.5em;
2784 }
2809 }
2785 th{
2810 th{
2786 padding-bottom: @textmargin/2;
2811 padding-bottom: @textmargin/2;
2787 }
2812 }
2788 td.keys{
2813 td.keys{
2789 text-align: center;
2814 text-align: center;
2790 }
2815 }
2791 }
2816 }
2792
2817
2793 .block-left{
2818 .block-left{
2794 width: 45%;
2819 width: 45%;
2795 margin-right: 5%;
2820 margin-right: 5%;
2796 }
2821 }
2797 .modal-footer{
2822 .modal-footer{
2798 clear: both;
2823 clear: both;
2799 }
2824 }
2800 .key.tag{
2825 .key.tag{
2801 padding: 0.5em;
2826 padding: 0.5em;
2802 background-color: @rcblue;
2827 background-color: @rcblue;
2803 color: white;
2828 color: white;
2804 border-color: @rcblue;
2829 border-color: @rcblue;
2805 .box-shadow(none);
2830 .box-shadow(none);
2806 }
2831 }
2807 }
2832 }
2808
2833
2809
2834
2810
2835
2811 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2836 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2812
2837
2813 @import 'statistics-graph';
2838 @import 'statistics-graph';
2814 @import 'tables';
2839 @import 'tables';
2815 @import 'forms';
2840 @import 'forms';
2816 @import 'diff';
2841 @import 'diff';
2817 @import 'summary';
2842 @import 'summary';
2818 @import 'navigation';
2843 @import 'navigation';
2819
2844
2820 //--- SHOW/HIDE SECTIONS --//
2845 //--- SHOW/HIDE SECTIONS --//
2821
2846
2822 .btn-collapse {
2847 .btn-collapse {
2823 float: right;
2848 float: right;
2824 text-align: right;
2849 text-align: right;
2825 font-family: @text-light;
2850 font-family: @text-light;
2826 font-size: @basefontsize;
2851 font-size: @basefontsize;
2827 cursor: pointer;
2852 cursor: pointer;
2828 border: none;
2853 border: none;
2829 color: @rcblue;
2854 color: @rcblue;
2830 }
2855 }
2831
2856
2832 table.rctable,
2857 table.rctable,
2833 table.dataTable {
2858 table.dataTable {
2834 .btn-collapse {
2859 .btn-collapse {
2835 float: right;
2860 float: right;
2836 text-align: right;
2861 text-align: right;
2837 }
2862 }
2838 }
2863 }
2839
2864
2840 table.rctable {
2865 table.rctable {
2841 &.permissions {
2866 &.permissions {
2842
2867
2843 th.td-owner {
2868 th.td-owner {
2844 padding: 0;
2869 padding: 0;
2845 }
2870 }
2846
2871
2847 th {
2872 th {
2848 font-weight: normal;
2873 font-weight: normal;
2849 padding: 0 5px;
2874 padding: 0 5px;
2850 }
2875 }
2851
2876
2852 }
2877 }
2853 }
2878 }
2854
2879
2855
2880
2856 // TODO: johbo: Fix for IE10, this avoids that we see a border
2881 // TODO: johbo: Fix for IE10, this avoids that we see a border
2857 // and padding around checkboxes and radio boxes. Move to the right place,
2882 // and padding around checkboxes and radio boxes. Move to the right place,
2858 // or better: Remove this once we did the form refactoring.
2883 // or better: Remove this once we did the form refactoring.
2859 input[type=checkbox],
2884 input[type=checkbox],
2860 input[type=radio] {
2885 input[type=radio] {
2861 padding: 0;
2886 padding: 0;
2862 border: none;
2887 border: none;
2863 }
2888 }
2864
2889
2865 .toggle-ajax-spinner{
2890 .toggle-ajax-spinner{
2866 height: 16px;
2891 height: 16px;
2867 width: 16px;
2892 width: 16px;
2868 }
2893 }
2869
2894
2870
2895
2871 .markup-form .clearfix {
2896 .markup-form .clearfix {
2872 .border-radius(@border-radius);
2897 .border-radius(@border-radius);
2873 margin: 0px;
2898 margin: 0px;
2874 }
2899 }
2875
2900
2876 .markup-form-area {
2901 .markup-form-area {
2877 padding: 8px 12px;
2902 padding: 8px 12px;
2878 border: 1px solid @grey4;
2903 border: 1px solid @grey4;
2879 .border-radius(@border-radius);
2904 .border-radius(@border-radius);
2880 }
2905 }
2881
2906
2882 .markup-form-area-header .nav-links {
2907 .markup-form-area-header .nav-links {
2883 display: flex;
2908 display: flex;
2884 flex-flow: row wrap;
2909 flex-flow: row wrap;
2885 -webkit-flex-flow: row wrap;
2910 -webkit-flex-flow: row wrap;
2886 width: 100%;
2911 width: 100%;
2887 }
2912 }
2888
2913
2889 .markup-form-area-footer {
2914 .markup-form-area-footer {
2890 display: flex;
2915 display: flex;
2891 }
2916 }
2892
2917
2893 .markup-form-area-footer .toolbar {
2918 .markup-form-area-footer .toolbar {
2894
2919
2895 }
2920 }
2896
2921
2897 // markup Form
2922 // markup Form
2898 div.markup-form {
2923 div.markup-form {
2899 margin-top: 20px;
2924 margin-top: 20px;
2900 }
2925 }
2901
2926
2902 .markup-form strong {
2927 .markup-form strong {
2903 display: block;
2928 display: block;
2904 margin-bottom: 15px;
2929 margin-bottom: 15px;
2905 }
2930 }
2906
2931
2907 .markup-form textarea {
2932 .markup-form textarea {
2908 width: 100%;
2933 width: 100%;
2909 height: 100px;
2934 height: 100px;
2910 font-family: @text-monospace;
2935 font-family: @text-monospace;
2911 }
2936 }
2912
2937
2913 form.markup-form {
2938 form.markup-form {
2914 margin-top: 10px;
2939 margin-top: 10px;
2915 margin-left: 10px;
2940 margin-left: 10px;
2916 }
2941 }
2917
2942
2918 .markup-form .comment-block-ta,
2943 .markup-form .comment-block-ta,
2919 .markup-form .preview-box {
2944 .markup-form .preview-box {
2920 .border-radius(@border-radius);
2945 .border-radius(@border-radius);
2921 .box-sizing(border-box);
2946 .box-sizing(border-box);
2922 background-color: white;
2947 background-color: white;
2923 }
2948 }
2924
2949
2925 .markup-form .preview-box.unloaded {
2950 .markup-form .preview-box.unloaded {
2926 height: 50px;
2951 height: 50px;
2927 text-align: center;
2952 text-align: center;
2928 padding: 20px;
2953 padding: 20px;
2929 background-color: white;
2954 background-color: white;
2930 }
2955 }
2931
2956
2932
2957
2933 .dropzone-wrapper {
2958 .dropzone-wrapper {
2934 border: 1px solid @grey5;
2959 border: 1px solid @grey5;
2935 padding: 20px;
2960 padding: 20px;
2936 }
2961 }
2937
2962
2938 .dropzone,
2963 .dropzone,
2939 .dropzone-pure {
2964 .dropzone-pure {
2940 border: 2px dashed @grey5;
2965 border: 2px dashed @grey5;
2941 border-radius: 5px;
2966 border-radius: 5px;
2942 background: white;
2967 background: white;
2943 min-height: 200px;
2968 min-height: 200px;
2944 padding: 54px;
2969 padding: 54px;
2945
2970
2946 .dz-message {
2971 .dz-message {
2947 font-weight: 700;
2972 font-weight: 700;
2948 text-align: center;
2973 text-align: center;
2949 margin: 2em 0;
2974 margin: 2em 0;
2950 }
2975 }
2951
2976
2952 }
2977 }
2953
2978
2954 .dz-preview {
2979 .dz-preview {
2955 margin: 10px 0 !important;
2980 margin: 10px 0 !important;
2956 position: relative;
2981 position: relative;
2957 vertical-align: top;
2982 vertical-align: top;
2958 padding: 10px;
2983 padding: 10px;
2959 border-bottom: 1px solid @grey5;
2984 border-bottom: 1px solid @grey5;
2960 }
2985 }
2961
2986
2962 .dz-filename {
2987 .dz-filename {
2963 font-weight: 700;
2988 font-weight: 700;
2964 float: left;
2989 float: left;
2965 }
2990 }
2966
2991
2967 .dz-sending {
2992 .dz-sending {
2968 float: right;
2993 float: right;
2969 }
2994 }
2970
2995
2971 .dz-response {
2996 .dz-response {
2972 clear: both
2997 clear: both
2973 }
2998 }
2974
2999
2975 .dz-filename-size {
3000 .dz-filename-size {
2976 float: right
3001 float: right
2977 }
3002 }
2978
3003
2979 .dz-error-message {
3004 .dz-error-message {
2980 color: @alert2;
3005 color: @alert2;
2981 padding-top: 10px;
3006 padding-top: 10px;
2982 clear: both;
3007 clear: both;
2983 }
3008 }
2984
3009
2985
3010
2986 .user-hovercard {
3011 .user-hovercard {
2987 padding: 5px;
3012 padding: 5px;
2988 }
3013 }
2989
3014
2990 .user-hovercard-icon {
3015 .user-hovercard-icon {
2991 display: inline;
3016 display: inline;
2992 padding: 0;
3017 padding: 0;
2993 box-sizing: content-box;
3018 box-sizing: content-box;
2994 border-radius: 50%;
3019 border-radius: 50%;
2995 float: left;
3020 float: left;
2996 }
3021 }
2997
3022
2998 .user-hovercard-name {
3023 .user-hovercard-name {
2999 float: right;
3024 float: right;
3000 vertical-align: top;
3025 vertical-align: top;
3001 padding-left: 10px;
3026 padding-left: 10px;
3002 min-width: 150px;
3027 min-width: 150px;
3003 }
3028 }
3004
3029
3005 .user-hovercard-bio {
3030 .user-hovercard-bio {
3006 clear: both;
3031 clear: both;
3007 padding-top: 10px;
3032 padding-top: 10px;
3008 }
3033 }
3009
3034
3010 .user-hovercard-header {
3035 .user-hovercard-header {
3011 clear: both;
3036 clear: both;
3012 min-height: 10px;
3037 min-height: 10px;
3013 }
3038 }
3014
3039
3015 .user-hovercard-footer {
3040 .user-hovercard-footer {
3016 clear: both;
3041 clear: both;
3017 min-height: 10px;
3042 min-height: 10px;
3018 }
3043 }
3019
3044
3020 .user-group-hovercard {
3045 .user-group-hovercard {
3021 padding: 5px;
3046 padding: 5px;
3022 }
3047 }
3023
3048
3024 .user-group-hovercard-icon {
3049 .user-group-hovercard-icon {
3025 display: inline;
3050 display: inline;
3026 padding: 0;
3051 padding: 0;
3027 box-sizing: content-box;
3052 box-sizing: content-box;
3028 border-radius: 50%;
3053 border-radius: 50%;
3029 float: left;
3054 float: left;
3030 }
3055 }
3031
3056
3032 .user-group-hovercard-name {
3057 .user-group-hovercard-name {
3033 float: left;
3058 float: left;
3034 vertical-align: top;
3059 vertical-align: top;
3035 padding-left: 10px;
3060 padding-left: 10px;
3036 min-width: 150px;
3061 min-width: 150px;
3037 }
3062 }
3038
3063
3039 .user-group-hovercard-icon i {
3064 .user-group-hovercard-icon i {
3040 border: 1px solid @grey4;
3065 border: 1px solid @grey4;
3041 border-radius: 4px;
3066 border-radius: 4px;
3042 }
3067 }
3043
3068
3044 .user-group-hovercard-bio {
3069 .user-group-hovercard-bio {
3045 clear: both;
3070 clear: both;
3046 padding-top: 10px;
3071 padding-top: 10px;
3047 line-height: 1.0em;
3072 line-height: 1.0em;
3048 }
3073 }
3049
3074
3050 .user-group-hovercard-header {
3075 .user-group-hovercard-header {
3051 clear: both;
3076 clear: both;
3052 min-height: 10px;
3077 min-height: 10px;
3053 }
3078 }
3054
3079
3055 .user-group-hovercard-footer {
3080 .user-group-hovercard-footer {
3056 clear: both;
3081 clear: both;
3057 min-height: 10px;
3082 min-height: 10px;
3058 }
3083 }
3059
3084
3060 .pr-hovercard-header {
3085 .pr-hovercard-header {
3061 clear: both;
3086 clear: both;
3062 display: block;
3087 display: block;
3063 line-height: 20px;
3088 line-height: 20px;
3064 }
3089 }
3065
3090
3066 .pr-hovercard-user {
3091 .pr-hovercard-user {
3067 display: flex;
3092 display: flex;
3068 align-items: center;
3093 align-items: center;
3069 padding-left: 5px;
3094 padding-left: 5px;
3070 }
3095 }
3071
3096
3072 .pr-hovercard-title {
3097 .pr-hovercard-title {
3073 padding-top: 5px;
3098 padding-top: 5px;
3074 }
3099 }
3075
3100
3076 .action-divider {
3101 .action-divider {
3077 opacity: 0.5;
3102 opacity: 0.5;
3078 }
3103 }
3079
3104
3080 .details-inline-block {
3105 .details-inline-block {
3081 display: inline-block;
3106 display: inline-block;
3082 position: relative;
3107 position: relative;
3083 }
3108 }
3084
3109
3085 .details-inline-block summary {
3110 .details-inline-block summary {
3086 list-style: none;
3111 list-style: none;
3087 }
3112 }
3088
3113
3089 details:not([open]) > :not(summary) {
3114 details:not([open]) > :not(summary) {
3090 display: none !important;
3115 display: none !important;
3091 }
3116 }
3092
3117
3093 .details-reset > summary {
3118 .details-reset > summary {
3094 list-style: none;
3119 list-style: none;
3095 }
3120 }
3096
3121
3097 .details-reset > summary::-webkit-details-marker {
3122 .details-reset > summary::-webkit-details-marker {
3098 display: none;
3123 display: none;
3099 }
3124 }
3100
3125
3101 .details-dropdown {
3126 .details-dropdown {
3102 position: absolute;
3127 position: absolute;
3103 top: 100%;
3128 top: 100%;
3104 width: 185px;
3129 width: 185px;
3105 list-style: none;
3130 list-style: none;
3106 background-color: #fff;
3131 background-color: #fff;
3107 background-clip: padding-box;
3132 background-clip: padding-box;
3108 border: 1px solid @grey5;
3133 border: 1px solid @grey5;
3109 box-shadow: 0 8px 24px rgba(149, 157, 165, .2);
3134 box-shadow: 0 8px 24px rgba(149, 157, 165, .2);
3110 left: -150px;
3135 left: -150px;
3111 text-align: left;
3136 text-align: left;
3112 z-index: 90;
3137 z-index: 90;
3113 }
3138 }
3114
3139
3115 .dropdown-divider {
3140 .dropdown-divider {
3116 display: block;
3141 display: block;
3117 height: 0;
3142 height: 0;
3118 margin: 8px 0;
3143 margin: 8px 0;
3119 border-top: 1px solid @grey5;
3144 border-top: 1px solid @grey5;
3120 }
3145 }
3121
3146
3122 .dropdown-item {
3147 .dropdown-item {
3123 display: block;
3148 display: block;
3124 padding: 4px 8px 4px 16px;
3149 padding: 4px 8px 4px 16px;
3125 overflow: hidden;
3150 overflow: hidden;
3126 text-overflow: ellipsis;
3151 text-overflow: ellipsis;
3127 white-space: nowrap;
3152 white-space: nowrap;
3128 font-weight: normal;
3153 font-weight: normal;
3129 }
3154 }
3130
3155
3131 .right-sidebar {
3156 .right-sidebar {
3132 position: fixed;
3157 position: fixed;
3133 top: 0px;
3158 top: 0px;
3134 bottom: 0;
3159 bottom: 0;
3135 right: 0;
3160 right: 0;
3136
3161
3137 background: #fafafa;
3162 background: #fafafa;
3138 z-index: 50;
3163 z-index: 50;
3139 }
3164 }
3140
3165
3141 .right-sidebar {
3166 .right-sidebar {
3142 border-left: 1px solid @grey5;
3167 border-left: 1px solid @grey5;
3143 }
3168 }
3144
3169
3145 .right-sidebar.right-sidebar-expanded {
3170 .right-sidebar.right-sidebar-expanded {
3146 width: 300px;
3171 width: 300px;
3147 overflow: scroll;
3172 overflow: scroll;
3148 }
3173 }
3149
3174
3150 .right-sidebar.right-sidebar-collapsed {
3175 .right-sidebar.right-sidebar-collapsed {
3151 width: 40px;
3176 width: 40px;
3152 padding: 0;
3177 padding: 0;
3153 display: block;
3178 display: block;
3154 overflow: hidden;
3179 overflow: hidden;
3155 }
3180 }
3156
3181
3157 .sidenav {
3182 .sidenav {
3158 float: right;
3183 float: right;
3159 will-change: min-height;
3184 will-change: min-height;
3160 background: #fafafa;
3185 background: #fafafa;
3161 width: 100%;
3186 width: 100%;
3162 }
3187 }
3163
3188
3164 .sidebar-toggle {
3189 .sidebar-toggle {
3165 height: 30px;
3190 height: 30px;
3166 text-align: center;
3191 text-align: center;
3167 margin: 15px 0px 0 0;
3192 margin: 15px 0px 0 0;
3168 }
3193 }
3169
3194
3170 .sidebar-toggle a {
3195 .sidebar-toggle a {
3171
3196
3172 }
3197 }
3173
3198
3174 .sidebar-content {
3199 .sidebar-content {
3175 margin-left: 15px;
3200 margin-left: 15px;
3176 margin-right: 15px;
3201 margin-right: 15px;
3177 }
3202 }
3178
3203
3179 .sidebar-heading {
3204 .sidebar-heading {
3180 font-size: 1.2em;
3205 font-size: 1.2em;
3181 font-weight: 700;
3206 font-weight: 700;
3182 margin-top: 10px;
3207 margin-top: 10px;
3183 }
3208 }
3184
3209
3185 .sidebar-element {
3210 .sidebar-element {
3186 margin-top: 20px;
3211 margin-top: 20px;
3187 }
3212 }
3188
3213
3189 .right-sidebar-collapsed-state {
3214 .right-sidebar-collapsed-state {
3190 display: flex;
3215 display: flex;
3191 flex-direction: column;
3216 flex-direction: column;
3192 justify-content: center;
3217 justify-content: center;
3193 align-items: center;
3218 align-items: center;
3194 padding: 0 10px;
3219 padding: 0 10px;
3195 cursor: pointer;
3220 cursor: pointer;
3196 font-size: 1.3em;
3221 font-size: 1.3em;
3197 margin: 0 -15px;
3222 margin: 0 -15px;
3198 }
3223 }
3199
3224
3200 .right-sidebar-collapsed-state:hover {
3225 .right-sidebar-collapsed-state:hover {
3201 background-color: @grey5;
3226 background-color: @grey5;
3202 }
3227 }
3203
3228
3204 .old-comments-marker {
3229 .old-comments-marker {
3205 text-align: left;
3230 text-align: left;
3206 }
3231 }
3207
3232
3208 .old-comments-marker td {
3233 .old-comments-marker td {
3209 padding-top: 15px;
3234 padding-top: 15px;
3210 border-bottom: 1px solid @grey5;
3235 border-bottom: 1px solid @grey5;
3211 }
3236 }
@@ -1,890 +1,1175 b''
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 var prButtonLockChecks = {
20 var prButtonLockChecks = {
21 'compare': false,
21 'compare': false,
22 'reviewers': false
22 'reviewers': false
23 };
23 };
24
24
25 /**
25 /**
26 * lock button until all checks and loads are made. E.g reviewer calculation
26 * lock button until all checks and loads are made. E.g reviewer calculation
27 * should prevent from submitting a PR
27 * should prevent from submitting a PR
28 * @param lockEnabled
28 * @param lockEnabled
29 * @param msg
29 * @param msg
30 * @param scope
30 * @param scope
31 */
31 */
32 var prButtonLock = function(lockEnabled, msg, scope) {
32 var prButtonLock = function(lockEnabled, msg, scope) {
33 scope = scope || 'all';
33 scope = scope || 'all';
34 if (scope == 'all'){
34 if (scope == 'all'){
35 prButtonLockChecks['compare'] = !lockEnabled;
35 prButtonLockChecks['compare'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 } else if (scope == 'compare') {
37 } else if (scope == 'compare') {
38 prButtonLockChecks['compare'] = !lockEnabled;
38 prButtonLockChecks['compare'] = !lockEnabled;
39 } else if (scope == 'reviewers'){
39 } else if (scope == 'reviewers'){
40 prButtonLockChecks['reviewers'] = !lockEnabled;
40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 }
41 }
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 if (lockEnabled) {
43 if (lockEnabled) {
44 $('#pr_submit').attr('disabled', 'disabled');
44 $('#pr_submit').attr('disabled', 'disabled');
45 }
45 }
46 else if (checksMeet) {
46 else if (checksMeet) {
47 $('#pr_submit').removeAttr('disabled');
47 $('#pr_submit').removeAttr('disabled');
48 }
48 }
49
49
50 if (msg) {
50 if (msg) {
51 $('#pr_open_message').html(msg);
51 $('#pr_open_message').html(msg);
52 }
52 }
53 };
53 };
54
54
55
55
56 /**
56 /**
57 Generate Title and Description for a PullRequest.
57 Generate Title and Description for a PullRequest.
58 In case of 1 commits, the title and description is that one commit
58 In case of 1 commits, the title and description is that one commit
59 in case of multiple commits, we iterate on them with max N number of commits,
59 in case of multiple commits, we iterate on them with max N number of commits,
60 and build description in a form
60 and build description in a form
61 - commitN
61 - commitN
62 - commitN+1
62 - commitN+1
63 ...
63 ...
64
64
65 Title is then constructed from branch names, or other references,
65 Title is then constructed from branch names, or other references,
66 replacing '-' and '_' into spaces
66 replacing '-' and '_' into spaces
67
67
68 * @param sourceRef
68 * @param sourceRef
69 * @param elements
69 * @param elements
70 * @param limit
70 * @param limit
71 * @returns {*[]}
71 * @returns {*[]}
72 */
72 */
73 var getTitleAndDescription = function(sourceRefType, sourceRef, elements, limit) {
73 var getTitleAndDescription = function(sourceRefType, sourceRef, elements, limit) {
74 var title = '';
74 var title = '';
75 var desc = '';
75 var desc = '';
76
76
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 var rawMessage = value['message'];
78 var rawMessage = value['message'];
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 });
80 });
81 // only 1 commit, use commit message as title
81 // only 1 commit, use commit message as title
82 if (elements.length === 1) {
82 if (elements.length === 1) {
83 var rawMessage = elements[0]['message'];
83 var rawMessage = elements[0]['message'];
84 title = rawMessage.split('\n')[0];
84 title = rawMessage.split('\n')[0];
85 }
85 }
86 else {
86 else {
87 // use reference name
87 // use reference name
88 var normalizedRef = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter()
88 var normalizedRef = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter()
89 var refType = sourceRefType;
89 var refType = sourceRefType;
90 title = 'Changes from {0}: {1}'.format(refType, normalizedRef);
90 title = 'Changes from {0}: {1}'.format(refType, normalizedRef);
91 }
91 }
92
92
93 return [title, desc]
93 return [title, desc]
94 };
94 };
95
95
96
96
97 ReviewersController = function () {
97 window.ReviewersController = function () {
98 var self = this;
98 var self = this;
99 this.$loadingIndicator = $('.calculate-reviewers');
99 this.$reviewRulesContainer = $('#review_rules');
100 this.$reviewRulesContainer = $('#review_rules');
100 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
101 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
101 this.$userRule = $('.pr-user-rule-container');
102 this.$userRule = $('.pr-user-rule-container');
102 this.forbidReviewUsers = undefined;
103 this.$reviewMembers = $('#review_members');
103 this.$reviewMembers = $('#review_members');
104 this.$observerMembers = $('#observer_members');
105
104 this.currentRequest = null;
106 this.currentRequest = null;
105 this.diffData = null;
107 this.diffData = null;
106 this.enabledRules = [];
108 this.enabledRules = [];
109 // sync with db.py entries
110 this.ROLE_REVIEWER = 'reviewer';
111 this.ROLE_OBSERVER = 'observer'
107
112
108 //dummy handler, we might register our own later
113 //dummy handler, we might register our own later
109 this.diffDataHandler = function(data){};
114 this.diffDataHandler = function (data) {};
110
115
111 this.defaultForbidReviewUsers = function () {
116 this.defaultForbidUsers = function () {
112 return [
117 return [
113 {
118 {
114 'username': 'default',
119 'username': 'default',
115 'user_id': templateContext.default_user.user_id
120 'user_id': templateContext.default_user.user_id
116 }
121 }
117 ];
122 ];
118 };
123 };
119
124
125 // init default forbidden users
126 this.forbidUsers = this.defaultForbidUsers();
127
120 this.hideReviewRules = function () {
128 this.hideReviewRules = function () {
121 self.$reviewRulesContainer.hide();
129 self.$reviewRulesContainer.hide();
122 $(self.$userRule.selector).hide();
130 $(self.$userRule.selector).hide();
123 };
131 };
124
132
125 this.showReviewRules = function () {
133 this.showReviewRules = function () {
126 self.$reviewRulesContainer.show();
134 self.$reviewRulesContainer.show();
127 $(self.$userRule.selector).show();
135 $(self.$userRule.selector).show();
128 };
136 };
129
137
130 this.addRule = function (ruleText) {
138 this.addRule = function (ruleText) {
131 self.showReviewRules();
139 self.showReviewRules();
132 self.enabledRules.push(ruleText);
140 self.enabledRules.push(ruleText);
133 return '<div>- {0}</div>'.format(ruleText)
141 return '<div>- {0}</div>'.format(ruleText)
134 };
142 };
135
143
144 this.increaseCounter = function(role) {
145 if (role === self.ROLE_REVIEWER) {
146 var $elem = $('#reviewers-cnt')
147 var cnt = parseInt($elem.data('count') || 0)
148 cnt +=1
149 $elem.html(cnt);
150 $elem.data('count', cnt);
151 }
152 else if (role === self.ROLE_OBSERVER) {
153 var $elem = $('#observers-cnt');
154 var cnt = parseInt($elem.data('count') || 0)
155 cnt +=1
156 $elem.html(cnt);
157 $elem.data('count', cnt);
158 }
159 }
160
161 this.resetCounter = function () {
162 var $elem = $('#reviewers-cnt');
163
164 $elem.data('count', 0);
165 $elem.html(0);
166
167 var $elem = $('#observers-cnt');
168
169 $elem.data('count', 0);
170 $elem.html(0);
171 }
172
136 this.loadReviewRules = function (data) {
173 this.loadReviewRules = function (data) {
137 self.diffData = data;
174 self.diffData = data;
138
175
139 // reset forbidden Users
176 // reset forbidden Users
140 this.forbidReviewUsers = self.defaultForbidReviewUsers();
177 this.forbidUsers = self.defaultForbidUsers();
141
178
142 // reset state of review rules
179 // reset state of review rules
143 self.$rulesList.html('');
180 self.$rulesList.html('');
144
181
145 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
182 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
146 // default rule, case for older repo that don't have any rules stored
183 // default rule, case for older repo that don't have any rules stored
147 self.$rulesList.append(
184 self.$rulesList.append(
148 self.addRule(
185 self.addRule(
149 _gettext('All reviewers must vote.'))
186 _gettext('All reviewers must vote.'))
150 );
187 );
151 return self.forbidReviewUsers
188 return self.forbidUsers
152 }
189 }
153
190
154 if (data.rules.voting !== undefined) {
191 if (data.rules.voting !== undefined) {
155 if (data.rules.voting < 0) {
192 if (data.rules.voting < 0) {
156 self.$rulesList.append(
193 self.$rulesList.append(
157 self.addRule(
194 self.addRule(
158 _gettext('All individual reviewers must vote.'))
195 _gettext('All individual reviewers must vote.'))
159 )
196 )
160 } else if (data.rules.voting === 1) {
197 } else if (data.rules.voting === 1) {
161 self.$rulesList.append(
198 self.$rulesList.append(
162 self.addRule(
199 self.addRule(
163 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
200 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
164 )
201 )
165
202
166 } else {
203 } else {
167 self.$rulesList.append(
204 self.$rulesList.append(
168 self.addRule(
205 self.addRule(
169 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
206 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
170 )
207 )
171 }
208 }
172 }
209 }
173
210
174 if (data.rules.voting_groups !== undefined) {
211 if (data.rules.voting_groups !== undefined) {
175 $.each(data.rules.voting_groups, function (index, rule_data) {
212 $.each(data.rules.voting_groups, function (index, rule_data) {
176 self.$rulesList.append(
213 self.$rulesList.append(
177 self.addRule(rule_data.text)
214 self.addRule(rule_data.text)
178 )
215 )
179 });
216 });
180 }
217 }
181
218
182 if (data.rules.use_code_authors_for_review) {
219 if (data.rules.use_code_authors_for_review) {
183 self.$rulesList.append(
220 self.$rulesList.append(
184 self.addRule(
221 self.addRule(
185 _gettext('Reviewers picked from source code changes.'))
222 _gettext('Reviewers picked from source code changes.'))
186 )
223 )
187 }
224 }
188
225
189 if (data.rules.forbid_adding_reviewers) {
226 if (data.rules.forbid_adding_reviewers) {
190 $('#add_reviewer_input').remove();
227 $('#add_reviewer_input').remove();
191 self.$rulesList.append(
228 self.$rulesList.append(
192 self.addRule(
229 self.addRule(
193 _gettext('Adding new reviewers is forbidden.'))
230 _gettext('Adding new reviewers is forbidden.'))
194 )
231 )
195 }
232 }
196
233
197 if (data.rules.forbid_author_to_review) {
234 if (data.rules.forbid_author_to_review) {
198 self.forbidReviewUsers.push(data.rules_data.pr_author);
235 self.forbidUsers.push(data.rules_data.pr_author);
199 self.$rulesList.append(
236 self.$rulesList.append(
200 self.addRule(
237 self.addRule(
201 _gettext('Author is not allowed to be a reviewer.'))
238 _gettext('Author is not allowed to be a reviewer.'))
202 )
239 )
203 }
240 }
204
241
205 if (data.rules.forbid_commit_author_to_review) {
242 if (data.rules.forbid_commit_author_to_review) {
206
243
207 if (data.rules_data.forbidden_users) {
244 if (data.rules_data.forbidden_users) {
208 $.each(data.rules_data.forbidden_users, function (index, member_data) {
245 $.each(data.rules_data.forbidden_users, function (index, member_data) {
209 self.forbidReviewUsers.push(member_data)
246 self.forbidUsers.push(member_data)
210 });
247 });
211
212 }
248 }
213
249
214 self.$rulesList.append(
250 self.$rulesList.append(
215 self.addRule(
251 self.addRule(
216 _gettext('Commit Authors are not allowed to be a reviewer.'))
252 _gettext('Commit Authors are not allowed to be a reviewer.'))
217 )
253 )
218 }
254 }
219
255
220 // we don't have any rules set, so we inform users about it
256 // we don't have any rules set, so we inform users about it
221 if (self.enabledRules.length === 0) {
257 if (self.enabledRules.length === 0) {
222 self.addRule(
258 self.addRule(
223 _gettext('No review rules set.'))
259 _gettext('No review rules set.'))
224 }
260 }
225
261
226 return self.forbidReviewUsers
262 return self.forbidUsers
227 };
263 };
228
264
265 this.emptyTables = function () {
266 self.emptyReviewersTable();
267 self.emptyObserversTable();
268
269 // Also reset counters.
270 self.resetCounter();
271 }
272
273 this.emptyReviewersTable = function (withText) {
274 self.$reviewMembers.empty();
275 if (withText !== undefined) {
276 self.$reviewMembers.html(withText)
277 }
278 };
279
280 this.emptyObserversTable = function (withText) {
281 self.$observerMembers.empty();
282 if (withText !== undefined) {
283 self.$observerMembers.html(withText)
284 }
285 }
286
229 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
287 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
230
288
231 if (self.currentRequest) {
289 if (self.currentRequest) {
232 // make sure we cleanup old running requests before triggering this again
290 // make sure we cleanup old running requests before triggering this again
233 self.currentRequest.abort();
291 self.currentRequest.abort();
234 }
292 }
235
293
236 $('.calculate-reviewers').show();
294 self.$loadingIndicator.show();
237 // reset reviewer members
295
238 self.$reviewMembers.empty();
296 // reset reviewer/observe members
297 self.emptyTables();
239
298
240 prButtonLock(true, null, 'reviewers');
299 prButtonLock(true, null, 'reviewers');
241 $('#user').hide(); // hide user autocomplete before load
300 $('#user').hide(); // hide user autocomplete before load
301 $('#observer').hide(); //hide observer autocomplete before load
242
302
243 // lock PR button, so we cannot send PR before it's calculated
303 // lock PR button, so we cannot send PR before it's calculated
244 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
304 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
245
305
246 if (sourceRef.length !== 3 || targetRef.length !== 3) {
306 if (sourceRef.length !== 3 || targetRef.length !== 3) {
247 // don't load defaults in case we're missing some refs...
307 // don't load defaults in case we're missing some refs...
248 $('.calculate-reviewers').hide();
308 self.$loadingIndicator.hide();
249 return
309 return
250 }
310 }
251
311
252 var url = pyroutes.url('repo_default_reviewers_data',
312 var url = pyroutes.url('repo_default_reviewers_data',
253 {
313 {
254 'repo_name': templateContext.repo_name,
314 'repo_name': templateContext.repo_name,
255 'source_repo': sourceRepo,
315 'source_repo': sourceRepo,
256 'source_ref': sourceRef[2],
316 'source_ref': sourceRef[2],
257 'target_repo': targetRepo,
317 'target_repo': targetRepo,
258 'target_ref': targetRef[2]
318 'target_ref': targetRef[2]
259 });
319 });
260
320
261 self.currentRequest = $.ajax({
321 self.currentRequest = $.ajax({
262 url: url,
322 url: url,
263 headers: {'X-PARTIAL-XHR': true},
323 headers: {'X-PARTIAL-XHR': true},
264 type: 'GET',
324 type: 'GET',
265 success: function (data) {
325 success: function (data) {
266
326
267 self.currentRequest = null;
327 self.currentRequest = null;
268
328
269 // review rules
329 // review rules
270 self.loadReviewRules(data);
330 self.loadReviewRules(data);
271 self.handleDiffData(data["diff_info"]);
331 self.handleDiffData(data["diff_info"]);
272
332
273 for (var i = 0; i < data.reviewers.length; i++) {
333 for (var i = 0; i < data.reviewers.length; i++) {
274 var reviewer = data.reviewers[i];
334 var reviewer = data.reviewers[i];
275 self.addReviewMember(reviewer, reviewer.reasons, reviewer.mandatory);
335 // load reviewer rules from the repo data
336 self.addMember(reviewer, reviewer.reasons, reviewer.mandatory, reviewer.role);
276 }
337 }
277 $('.calculate-reviewers').hide();
338
339
340 self.$loadingIndicator.hide();
278 prButtonLock(false, null, 'reviewers');
341 prButtonLock(false, null, 'reviewers');
279 $('#user').show(); // show user autocomplete after load
342
343 $('#user').show(); // show user autocomplete before load
344 $('#observer').show(); // show observer autocomplete before load
280
345
281 var commitElements = data["diff_info"]['commits'];
346 var commitElements = data["diff_info"]['commits'];
282
347
283 if (commitElements.length === 0) {
348 if (commitElements.length === 0) {
284 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
349 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
285 _gettext('There are no commits to merge.'));
350 _gettext('There are no commits to merge.'));
286 prButtonLock(true, noCommitsMsg, 'all');
351 prButtonLock(true, noCommitsMsg, 'all');
287
352
288 } else {
353 } else {
289 // un-lock PR button, so we cannot send PR before it's calculated
354 // un-lock PR button, so we cannot send PR before it's calculated
290 prButtonLock(false, null, 'compare');
355 prButtonLock(false, null, 'compare');
291 }
356 }
292
357
293 },
358 },
294 error: function (jqXHR, textStatus, errorThrown) {
359 error: function (jqXHR, textStatus, errorThrown) {
295 var prefix = "Loading diff and reviewers failed\n"
360 var prefix = "Loading diff and reviewers/observers failed\n"
296 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
361 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
297 ajaxErrorSwal(message);
362 ajaxErrorSwal(message);
298 }
363 }
299 });
364 });
300
365
301 };
366 };
302
367
303 // check those, refactor
368 // check those, refactor
304 this.removeReviewMember = function (reviewer_id, mark_delete) {
369 this.removeMember = function (reviewer_id, mark_delete) {
305 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
370 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
306
371
307 if (typeof (mark_delete) === undefined) {
372 if (typeof (mark_delete) === undefined) {
308 mark_delete = false;
373 mark_delete = false;
309 }
374 }
310
375
311 if (mark_delete === true) {
376 if (mark_delete === true) {
312 if (reviewer) {
377 if (reviewer) {
313 // now delete the input
378 // now delete the input
314 $('#reviewer_{0} input'.format(reviewer_id)).remove();
379 $('#reviewer_{0} input'.format(reviewer_id)).remove();
380 $('#reviewer_{0}_rules input'.format(reviewer_id)).remove();
315 // mark as to-delete
381 // mark as to-delete
316 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
382 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
317 obj.addClass('to-delete');
383 obj.addClass('to-delete');
318 obj.css({"text-decoration": "line-through", "opacity": 0.5});
384 obj.css({"text-decoration": "line-through", "opacity": 0.5});
319 }
385 }
320 } else {
386 } else {
321 $('#reviewer_{0}'.format(reviewer_id)).remove();
387 $('#reviewer_{0}'.format(reviewer_id)).remove();
322 }
388 }
323 };
389 };
324
390
325 this.reviewMemberEntry = function () {
391 this.addMember = function (reviewer_obj, reasons, mandatory, role) {
326
392
327 };
328
329 this.addReviewMember = function (reviewer_obj, reasons, mandatory) {
330 var id = reviewer_obj.user_id;
393 var id = reviewer_obj.user_id;
331 var username = reviewer_obj.username;
394 var username = reviewer_obj.username;
332
395
333 var reasons = reasons || [];
396 reasons = reasons || [];
334 var mandatory = mandatory || false;
397 mandatory = mandatory || false;
398 role = role || self.ROLE_REVIEWER
335
399
336 // register IDS to check if we don't have this ID already in
400 // register current set IDS to check if we don't have this ID already in
401 // and prevent duplicates
337 var currentIds = [];
402 var currentIds = [];
338
403
339 $.each(self.$reviewMembers.find('.reviewer_entry'), function (index, value) {
404 $.each($('.reviewer_entry'), function (index, value) {
340 currentIds.push($(value).data('reviewerUserId'))
405 currentIds.push($(value).data('reviewerUserId'))
341 })
406 })
342
407
343 var userAllowedReview = function (userId) {
408 var userAllowedReview = function (userId) {
344 var allowed = true;
409 var allowed = true;
345 $.each(self.forbidReviewUsers, function (index, member_data) {
410 $.each(self.forbidUsers, function (index, member_data) {
346 if (parseInt(userId) === member_data['user_id']) {
411 if (parseInt(userId) === member_data['user_id']) {
347 allowed = false;
412 allowed = false;
348 return false // breaks the loop
413 return false // breaks the loop
349 }
414 }
350 });
415 });
351 return allowed
416 return allowed
352 };
417 };
353
418
354 var userAllowed = userAllowedReview(id);
419 var userAllowed = userAllowedReview(id);
420
355 if (!userAllowed) {
421 if (!userAllowed) {
356 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
422 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
357 } else {
423 } else {
358 // only add if it's not there
424 // only add if it's not there
359 var alreadyReviewer = currentIds.indexOf(id) != -1;
425 var alreadyReviewer = currentIds.indexOf(id) != -1;
360
426
361 if (alreadyReviewer) {
427 if (alreadyReviewer) {
362 alert(_gettext('User `{0}` already in reviewers').format(username));
428 alert(_gettext('User `{0}` already in reviewers/observers').format(username));
363 } else {
429 } else {
430
364 var reviewerEntry = renderTemplate('reviewMemberEntry', {
431 var reviewerEntry = renderTemplate('reviewMemberEntry', {
365 'member': reviewer_obj,
432 'member': reviewer_obj,
366 'mandatory': mandatory,
433 'mandatory': mandatory,
434 'role': role,
367 'reasons': reasons,
435 'reasons': reasons,
368 'allowed_to_update': true,
436 'allowed_to_update': true,
369 'review_status': 'not_reviewed',
437 'review_status': 'not_reviewed',
370 'review_status_label': _gettext('Not Reviewed'),
438 'review_status_label': _gettext('Not Reviewed'),
371 'user_group': reviewer_obj.user_group,
439 'user_group': reviewer_obj.user_group,
372 'create': true,
440 'create': true,
373 'rule_show': true,
441 'rule_show': true,
374 })
442 })
375 $(self.$reviewMembers.selector).append(reviewerEntry);
443
444 if (role === self.ROLE_REVIEWER) {
445 $(self.$reviewMembers.selector).append(reviewerEntry);
446 self.increaseCounter(self.ROLE_REVIEWER);
447 $('#reviewer-empty-msg').remove()
448 }
449 else if (role === self.ROLE_OBSERVER) {
450 $(self.$observerMembers.selector).append(reviewerEntry);
451 self.increaseCounter(self.ROLE_OBSERVER);
452 $('#observer-empty-msg').remove();
453 }
454
376 tooltipActivate();
455 tooltipActivate();
377 }
456 }
378 }
457 }
379
458
380 };
459 };
381
460
382 this.updateReviewers = function (repo_name, pull_request_id) {
461 this.updateReviewers = function (repo_name, pull_request_id, role) {
383 var postData = $('#reviewers input').serialize();
462 if (role === 'reviewer') {
384 _updatePullRequest(repo_name, pull_request_id, postData);
463 var postData = $('#reviewers input').serialize();
464 _updatePullRequest(repo_name, pull_request_id, postData);
465 } else if (role === 'observer') {
466 var postData = $('#observers input').serialize();
467 _updatePullRequest(repo_name, pull_request_id, postData);
468 }
385 };
469 };
386
470
387 this.handleDiffData = function (data) {
471 this.handleDiffData = function (data) {
388 self.diffDataHandler(data)
472 self.diffDataHandler(data)
389 }
473 }
390 };
474 };
391
475
392
476
393 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
477 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
394 var url = pyroutes.url(
478 var url = pyroutes.url(
395 'pullrequest_update',
479 'pullrequest_update',
396 {"repo_name": repo_name, "pull_request_id": pull_request_id});
480 {"repo_name": repo_name, "pull_request_id": pull_request_id});
397 if (typeof postData === 'string' ) {
481 if (typeof postData === 'string' ) {
398 postData += '&csrf_token=' + CSRF_TOKEN;
482 postData += '&csrf_token=' + CSRF_TOKEN;
399 } else {
483 } else {
400 postData.csrf_token = CSRF_TOKEN;
484 postData.csrf_token = CSRF_TOKEN;
401 }
485 }
402
486
403 var success = function(o) {
487 var success = function(o) {
404 var redirectUrl = o['redirect_url'];
488 var redirectUrl = o['redirect_url'];
405 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
489 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
406 window.location = redirectUrl;
490 window.location = redirectUrl;
407 } else {
491 } else {
408 window.location.reload();
492 window.location.reload();
409 }
493 }
410 };
494 };
411
495
412 ajaxPOST(url, postData, success);
496 ajaxPOST(url, postData, success);
413 };
497 };
414
498
415 /**
499 /**
416 * PULL REQUEST update commits
500 * PULL REQUEST update commits
417 */
501 */
418 var updateCommits = function(repo_name, pull_request_id, force) {
502 var updateCommits = function(repo_name, pull_request_id, force) {
419 var postData = {
503 var postData = {
420 'update_commits': true
504 'update_commits': true
421 };
505 };
422 if (force !== undefined && force === true) {
506 if (force !== undefined && force === true) {
423 postData['force_refresh'] = true
507 postData['force_refresh'] = true
424 }
508 }
425 _updatePullRequest(repo_name, pull_request_id, postData);
509 _updatePullRequest(repo_name, pull_request_id, postData);
426 };
510 };
427
511
428
512
429 /**
513 /**
430 * PULL REQUEST edit info
514 * PULL REQUEST edit info
431 */
515 */
432 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
516 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
433 var url = pyroutes.url(
517 var url = pyroutes.url(
434 'pullrequest_update',
518 'pullrequest_update',
435 {"repo_name": repo_name, "pull_request_id": pull_request_id});
519 {"repo_name": repo_name, "pull_request_id": pull_request_id});
436
520
437 var postData = {
521 var postData = {
438 'title': title,
522 'title': title,
439 'description': description,
523 'description': description,
440 'description_renderer': renderer,
524 'description_renderer': renderer,
441 'edit_pull_request': true,
525 'edit_pull_request': true,
442 'csrf_token': CSRF_TOKEN
526 'csrf_token': CSRF_TOKEN
443 };
527 };
444 var success = function(o) {
528 var success = function(o) {
445 window.location.reload();
529 window.location.reload();
446 };
530 };
447 ajaxPOST(url, postData, success);
531 ajaxPOST(url, postData, success);
448 };
532 };
449
533
450
534
451 /**
535 /**
452 * Reviewer autocomplete
536 * autocomplete handler for reviewers/observers
453 */
537 */
454 var ReviewerAutoComplete = function(inputId) {
538 var autoCompleteHandler = function (inputId, controller, role) {
455 $(inputId).autocomplete({
539
456 serviceUrl: pyroutes.url('user_autocomplete_data'),
540 return function (element, data) {
457 minChars:2,
458 maxHeight:400,
459 deferRequestBy: 300, //miliseconds
460 showNoSuggestionNotice: true,
461 tabDisabled: true,
462 autoSelectFirst: true,
463 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
464 formatResult: autocompleteFormatResult,
465 lookupFilter: autocompleteFilterResult,
466 onSelect: function(element, data) {
467 var mandatory = false;
541 var mandatory = false;
468 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
542 var reasons = [_gettext('added manually by "{0}"').format(
543 templateContext.rhodecode_user.username)];
469
544
470 // add whole user groups
545 // add whole user groups
471 if (data.value_type == 'user_group') {
546 if (data.value_type == 'user_group') {
472 reasons.push(_gettext('member of "{0}"').format(data.value_display));
547 reasons.push(_gettext('member of "{0}"').format(data.value_display));
473
548
474 $.each(data.members, function(index, member_data) {
549 $.each(data.members, function (index, member_data) {
475 var reviewer = member_data;
550 var reviewer = member_data;
476 reviewer['user_id'] = member_data['id'];
551 reviewer['user_id'] = member_data['id'];
477 reviewer['gravatar_link'] = member_data['icon_link'];
552 reviewer['gravatar_link'] = member_data['icon_link'];
478 reviewer['user_link'] = member_data['profile_link'];
553 reviewer['user_link'] = member_data['profile_link'];
479 reviewer['rules'] = [];
554 reviewer['rules'] = [];
480 reviewersController.addReviewMember(reviewer, reasons, mandatory);
555 controller.addMember(reviewer, reasons, mandatory, role);
481 })
556 })
482 }
557 }
483 // add single user
558 // add single user
484 else {
559 else {
485 var reviewer = data;
560 var reviewer = data;
486 reviewer['user_id'] = data['id'];
561 reviewer['user_id'] = data['id'];
487 reviewer['gravatar_link'] = data['icon_link'];
562 reviewer['gravatar_link'] = data['icon_link'];
488 reviewer['user_link'] = data['profile_link'];
563 reviewer['user_link'] = data['profile_link'];
489 reviewer['rules'] = [];
564 reviewer['rules'] = [];
490 reviewersController.addReviewMember(reviewer, reasons, mandatory);
565 controller.addMember(reviewer, reasons, mandatory, role);
491 }
566 }
492
567
493 $(inputId).val('');
568 $(inputId).val('');
494 }
569 }
495 });
570 }
571
572 /**
573 * Reviewer autocomplete
574 */
575 var ReviewerAutoComplete = function (inputId, controller) {
576 var self = this;
577 self.controller = controller;
578 self.inputId = inputId;
579 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_REVIEWER);
580
581 $(inputId).autocomplete({
582 serviceUrl: pyroutes.url('user_autocomplete_data'),
583 minChars: 2,
584 maxHeight: 400,
585 deferRequestBy: 300, //miliseconds
586 showNoSuggestionNotice: true,
587 tabDisabled: true,
588 autoSelectFirst: true,
589 params: {
590 user_id: templateContext.rhodecode_user.user_id,
591 user_groups: true,
592 user_groups_expand: true,
593 skip_default_user: true
594 },
595 formatResult: autocompleteFormatResult,
596 lookupFilter: autocompleteFilterResult,
597 onSelect: handler
598 });
496 };
599 };
497
600
601 /**
602 * Observers autocomplete
603 */
604 var ObserverAutoComplete = function(inputId, controller) {
605 var self = this;
606 self.controller = controller;
607 self.inputId = inputId;
608 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_OBSERVER);
609
610 $(inputId).autocomplete({
611 serviceUrl: pyroutes.url('user_autocomplete_data'),
612 minChars: 2,
613 maxHeight: 400,
614 deferRequestBy: 300, //miliseconds
615 showNoSuggestionNotice: true,
616 tabDisabled: true,
617 autoSelectFirst: true,
618 params: {
619 user_id: templateContext.rhodecode_user.user_id,
620 user_groups: true,
621 user_groups_expand: true,
622 skip_default_user: true
623 },
624 formatResult: autocompleteFormatResult,
625 lookupFilter: autocompleteFilterResult,
626 onSelect: handler
627 });
628 }
629
498
630
499 window.VersionController = function () {
631 window.VersionController = function () {
500 var self = this;
632 var self = this;
501 this.$verSource = $('input[name=ver_source]');
633 this.$verSource = $('input[name=ver_source]');
502 this.$verTarget = $('input[name=ver_target]');
634 this.$verTarget = $('input[name=ver_target]');
503 this.$showVersionDiff = $('#show-version-diff');
635 this.$showVersionDiff = $('#show-version-diff');
504
636
505 this.adjustRadioSelectors = function (curNode) {
637 this.adjustRadioSelectors = function (curNode) {
506 var getVal = function (item) {
638 var getVal = function (item) {
507 if (item == 'latest') {
639 if (item === 'latest') {
508 return Number.MAX_SAFE_INTEGER
640 return Number.MAX_SAFE_INTEGER
509 }
641 }
510 else {
642 else {
511 return parseInt(item)
643 return parseInt(item)
512 }
644 }
513 };
645 };
514
646
515 var curVal = getVal($(curNode).val());
647 var curVal = getVal($(curNode).val());
516 var cleared = false;
648 var cleared = false;
517
649
518 $.each(self.$verSource, function (index, value) {
650 $.each(self.$verSource, function (index, value) {
519 var elVal = getVal($(value).val());
651 var elVal = getVal($(value).val());
520
652
521 if (elVal > curVal) {
653 if (elVal > curVal) {
522 if ($(value).is(':checked')) {
654 if ($(value).is(':checked')) {
523 cleared = true;
655 cleared = true;
524 }
656 }
525 $(value).attr('disabled', 'disabled');
657 $(value).attr('disabled', 'disabled');
526 $(value).removeAttr('checked');
658 $(value).removeAttr('checked');
527 $(value).css({'opacity': 0.1});
659 $(value).css({'opacity': 0.1});
528 }
660 }
529 else {
661 else {
530 $(value).css({'opacity': 1});
662 $(value).css({'opacity': 1});
531 $(value).removeAttr('disabled');
663 $(value).removeAttr('disabled');
532 }
664 }
533 });
665 });
534
666
535 if (cleared) {
667 if (cleared) {
536 // if we unchecked an active, set the next one to same loc.
668 // if we unchecked an active, set the next one to same loc.
537 $(this.$verSource).filter('[value={0}]'.format(
669 $(this.$verSource).filter('[value={0}]'.format(
538 curVal)).attr('checked', 'checked');
670 curVal)).attr('checked', 'checked');
539 }
671 }
540
672
541 self.setLockAction(false,
673 self.setLockAction(false,
542 $(curNode).data('verPos'),
674 $(curNode).data('verPos'),
543 $(this.$verSource).filter(':checked').data('verPos')
675 $(this.$verSource).filter(':checked').data('verPos')
544 );
676 );
545 };
677 };
546
678
547
679
548 this.attachVersionListener = function () {
680 this.attachVersionListener = function () {
549 self.$verTarget.change(function (e) {
681 self.$verTarget.change(function (e) {
550 self.adjustRadioSelectors(this)
682 self.adjustRadioSelectors(this)
551 });
683 });
552 self.$verSource.change(function (e) {
684 self.$verSource.change(function (e) {
553 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
685 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
554 });
686 });
555 };
687 };
556
688
557 this.init = function () {
689 this.init = function () {
558
690
559 var curNode = self.$verTarget.filter(':checked');
691 var curNode = self.$verTarget.filter(':checked');
560 self.adjustRadioSelectors(curNode);
692 self.adjustRadioSelectors(curNode);
561 self.setLockAction(true);
693 self.setLockAction(true);
562 self.attachVersionListener();
694 self.attachVersionListener();
563
695
564 };
696 };
565
697
566 this.setLockAction = function (state, selectedVersion, otherVersion) {
698 this.setLockAction = function (state, selectedVersion, otherVersion) {
567 var $showVersionDiff = this.$showVersionDiff;
699 var $showVersionDiff = this.$showVersionDiff;
568
700
569 if (state) {
701 if (state) {
570 $showVersionDiff.attr('disabled', 'disabled');
702 $showVersionDiff.attr('disabled', 'disabled');
571 $showVersionDiff.addClass('disabled');
703 $showVersionDiff.addClass('disabled');
572 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
704 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
573 }
705 }
574 else {
706 else {
575 $showVersionDiff.removeAttr('disabled');
707 $showVersionDiff.removeAttr('disabled');
576 $showVersionDiff.removeClass('disabled');
708 $showVersionDiff.removeClass('disabled');
577
709
578 if (selectedVersion == otherVersion) {
710 if (selectedVersion == otherVersion) {
579 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
711 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
580 } else {
712 } else {
581 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
713 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
582 }
714 }
583 }
715 }
584
716
585 };
717 };
586
718
587 this.showVersionDiff = function () {
719 this.showVersionDiff = function () {
588 var target = self.$verTarget.filter(':checked');
720 var target = self.$verTarget.filter(':checked');
589 var source = self.$verSource.filter(':checked');
721 var source = self.$verSource.filter(':checked');
590
722
591 if (target.val() && source.val()) {
723 if (target.val() && source.val()) {
592 var params = {
724 var params = {
593 'pull_request_id': templateContext.pull_request_data.pull_request_id,
725 'pull_request_id': templateContext.pull_request_data.pull_request_id,
594 'repo_name': templateContext.repo_name,
726 'repo_name': templateContext.repo_name,
595 'version': target.val(),
727 'version': target.val(),
596 'from_version': source.val()
728 'from_version': source.val()
597 };
729 };
598 window.location = pyroutes.url('pullrequest_show', params)
730 window.location = pyroutes.url('pullrequest_show', params)
599 }
731 }
600
732
601 return false;
733 return false;
602 };
734 };
603
735
604 this.toggleVersionView = function (elem) {
736 this.toggleVersionView = function (elem) {
605
737
606 if (this.$showVersionDiff.is(':visible')) {
738 if (this.$showVersionDiff.is(':visible')) {
607 $('.version-pr').hide();
739 $('.version-pr').hide();
608 this.$showVersionDiff.hide();
740 this.$showVersionDiff.hide();
609 $(elem).html($(elem).data('toggleOn'))
741 $(elem).html($(elem).data('toggleOn'))
610 } else {
742 } else {
611 $('.version-pr').show();
743 $('.version-pr').show();
612 this.$showVersionDiff.show();
744 this.$showVersionDiff.show();
613 $(elem).html($(elem).data('toggleOff'))
745 $(elem).html($(elem).data('toggleOff'))
614 }
746 }
615
747
616 return false
748 return false
617 };
749 };
618
750
619 };
751 };
620
752
621
753
622 window.UpdatePrController = function () {
754 window.UpdatePrController = function () {
623 var self = this;
755 var self = this;
624 this.$updateCommits = $('#update_commits');
756 this.$updateCommits = $('#update_commits');
625 this.$updateCommitsSwitcher = $('#update_commits_switcher');
757 this.$updateCommitsSwitcher = $('#update_commits_switcher');
626
758
627 this.lockUpdateButton = function (label) {
759 this.lockUpdateButton = function (label) {
628 self.$updateCommits.attr('disabled', 'disabled');
760 self.$updateCommits.attr('disabled', 'disabled');
629 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
761 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
630
762
631 self.$updateCommits.addClass('disabled');
763 self.$updateCommits.addClass('disabled');
632 self.$updateCommitsSwitcher.addClass('disabled');
764 self.$updateCommitsSwitcher.addClass('disabled');
633
765
634 self.$updateCommits.removeClass('btn-primary');
766 self.$updateCommits.removeClass('btn-primary');
635 self.$updateCommitsSwitcher.removeClass('btn-primary');
767 self.$updateCommitsSwitcher.removeClass('btn-primary');
636
768
637 self.$updateCommits.text(_gettext(label));
769 self.$updateCommits.text(_gettext(label));
638 };
770 };
639
771
640 this.isUpdateLocked = function () {
772 this.isUpdateLocked = function () {
641 return self.$updateCommits.attr('disabled') !== undefined;
773 return self.$updateCommits.attr('disabled') !== undefined;
642 };
774 };
643
775
644 this.updateCommits = function (curNode) {
776 this.updateCommits = function (curNode) {
645 if (self.isUpdateLocked()) {
777 if (self.isUpdateLocked()) {
646 return
778 return
647 }
779 }
648 self.lockUpdateButton(_gettext('Updating...'));
780 self.lockUpdateButton(_gettext('Updating...'));
649 updateCommits(
781 updateCommits(
650 templateContext.repo_name,
782 templateContext.repo_name,
651 templateContext.pull_request_data.pull_request_id);
783 templateContext.pull_request_data.pull_request_id);
652 };
784 };
653
785
654 this.forceUpdateCommits = function () {
786 this.forceUpdateCommits = function () {
655 if (self.isUpdateLocked()) {
787 if (self.isUpdateLocked()) {
656 return
788 return
657 }
789 }
658 self.lockUpdateButton(_gettext('Force updating...'));
790 self.lockUpdateButton(_gettext('Force updating...'));
659 var force = true;
791 var force = true;
660 updateCommits(
792 updateCommits(
661 templateContext.repo_name,
793 templateContext.repo_name,
662 templateContext.pull_request_data.pull_request_id, force);
794 templateContext.pull_request_data.pull_request_id, force);
663 };
795 };
664 };
796 };
665
797
798
666 /**
799 /**
667 * Reviewer display panel
800 * Reviewer display panel
668 */
801 */
669 window.ReviewersPanel = {
802 window.ReviewersPanel = {
670 editButton: null,
803 editButton: null,
671 closeButton: null,
804 closeButton: null,
672 addButton: null,
805 addButton: null,
673 removeButtons: null,
806 removeButtons: null,
674 reviewRules: null,
807 reviewRules: null,
675 setReviewers: null,
808 setReviewers: null,
676
809
677 setSelectors: function () {
810 setSelectors: function () {
678 var self = this;
811 var self = this;
679 self.editButton = $('#open_edit_reviewers');
812 self.editButton = $('#open_edit_reviewers');
680 self.closeButton =$('#close_edit_reviewers');
813 self.closeButton =$('#close_edit_reviewers');
681 self.addButton = $('#add_reviewer');
814 self.addButton = $('#add_reviewer');
682 self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
815 self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
683 },
816 },
684
817
685 init: function (reviewRules, setReviewers) {
818 init: function (reviewRules, setReviewers) {
686 var self = this;
819 var self = this;
687 self.setSelectors();
820 self.setSelectors();
688
821
689 this.reviewRules = reviewRules;
822 this.reviewRules = reviewRules;
690 this.setReviewers = setReviewers;
823 this.setReviewers = setReviewers;
691
824
692 this.editButton.on('click', function (e) {
825 this.editButton.on('click', function (e) {
693 self.edit();
826 self.edit();
694 });
827 });
695 this.closeButton.on('click', function (e) {
828 this.closeButton.on('click', function (e) {
696 self.close();
829 self.close();
697 self.renderReviewers();
830 self.renderReviewers();
698 });
831 });
699
832
700 self.renderReviewers();
833 self.renderReviewers();
701
834
702 },
835 },
703
836
704 renderReviewers: function () {
837 renderReviewers: function () {
838 if (this.setReviewers.reviewers === undefined) {
839 return
840 }
841 if (this.setReviewers.reviewers.length === 0) {
842 reviewersController.emptyReviewersTable('<tr id="reviewer-empty-msg"><td colspan="6">No reviewers</td></tr>');
843 return
844 }
705
845
706 $('#review_members').html('')
846 reviewersController.emptyReviewersTable();
847
707 $.each(this.setReviewers.reviewers, function (key, val) {
848 $.each(this.setReviewers.reviewers, function (key, val) {
708 var member = val;
709
849
710 var entry = renderTemplate('reviewMemberEntry', {
850 var member = val;
711 'member': member,
851 if (member.role === reviewersController.ROLE_REVIEWER) {
712 'mandatory': member.mandatory,
852 var entry = renderTemplate('reviewMemberEntry', {
713 'reasons': member.reasons,
853 'member': member,
714 'allowed_to_update': member.allowed_to_update,
854 'mandatory': member.mandatory,
715 'review_status': member.review_status,
855 'role': member.role,
716 'review_status_label': member.review_status_label,
856 'reasons': member.reasons,
717 'user_group': member.user_group,
857 'allowed_to_update': member.allowed_to_update,
718 'create': false
858 'review_status': member.review_status,
719 });
859 'review_status_label': member.review_status_label,
860 'user_group': member.user_group,
861 'create': false
862 });
720
863
721 $('#review_members').append(entry)
864 $(reviewersController.$reviewMembers.selector).append(entry)
865 }
722 });
866 });
867
723 tooltipActivate();
868 tooltipActivate();
724
725 },
869 },
726
870
727 edit: function (event) {
871 edit: function (event) {
728 this.editButton.hide();
872 this.editButton.hide();
729 this.closeButton.show();
873 this.closeButton.show();
730 this.addButton.show();
874 this.addButton.show();
731 $(this.removeButtons.selector).css('visibility', 'visible');
875 $(this.removeButtons.selector).css('visibility', 'visible');
732 // review rules
876 // review rules
733 reviewersController.loadReviewRules(this.reviewRules);
877 reviewersController.loadReviewRules(this.reviewRules);
734 },
878 },
735
879
736 close: function (event) {
880 close: function (event) {
737 this.editButton.show();
881 this.editButton.show();
738 this.closeButton.hide();
882 this.closeButton.hide();
739 this.addButton.hide();
883 this.addButton.hide();
740 $(this.removeButtons.selector).css('visibility', 'hidden');
884 $(this.removeButtons.selector).css('visibility', 'hidden');
741 // hide review rules
885 // hide review rules
742 reviewersController.hideReviewRules()
886 reviewersController.hideReviewRules();
743 }
887 }
744 };
888 };
745
889
890 /**
891 * Reviewer display panel
892 */
893 window.ObserversPanel = {
894 editButton: null,
895 closeButton: null,
896 addButton: null,
897 removeButtons: null,
898 reviewRules: null,
899 setReviewers: null,
900
901 setSelectors: function () {
902 var self = this;
903 self.editButton = $('#open_edit_observers');
904 self.closeButton =$('#close_edit_observers');
905 self.addButton = $('#add_observer');
906 self.removeButtons = $('.observer_member_remove,.observer_member_mandatory_remove');
907 },
908
909 init: function (reviewRules, setReviewers) {
910 var self = this;
911 self.setSelectors();
912
913 this.reviewRules = reviewRules;
914 this.setReviewers = setReviewers;
915
916 this.editButton.on('click', function (e) {
917 self.edit();
918 });
919 this.closeButton.on('click', function (e) {
920 self.close();
921 self.renderObservers();
922 });
923
924 self.renderObservers();
925
926 },
927
928 renderObservers: function () {
929 if (this.setReviewers.observers === undefined) {
930 return
931 }
932 if (this.setReviewers.observers.length === 0) {
933 reviewersController.emptyObserversTable('<tr id="observer-empty-msg"><td colspan="6">No observers</td></tr>');
934 return
935 }
936
937 reviewersController.emptyObserversTable();
938
939 $.each(this.setReviewers.observers, function (key, val) {
940 var member = val;
941 if (member.role === reviewersController.ROLE_OBSERVER) {
942 var entry = renderTemplate('reviewMemberEntry', {
943 'member': member,
944 'mandatory': member.mandatory,
945 'role': member.role,
946 'reasons': member.reasons,
947 'allowed_to_update': member.allowed_to_update,
948 'review_status': member.review_status,
949 'review_status_label': member.review_status_label,
950 'user_group': member.user_group,
951 'create': false
952 });
953
954 $(reviewersController.$observerMembers.selector).append(entry)
955 }
956 });
957
958 tooltipActivate();
959 },
960
961 edit: function (event) {
962 this.editButton.hide();
963 this.closeButton.show();
964 this.addButton.show();
965 $(this.removeButtons.selector).css('visibility', 'visible');
966 },
967
968 close: function (event) {
969 this.editButton.show();
970 this.closeButton.hide();
971 this.addButton.hide();
972 $(this.removeButtons.selector).css('visibility', 'hidden');
973 }
974
975 };
976
977 window.PRDetails = {
978 editButton: null,
979 closeButton: null,
980 deleteButton: null,
981 viewFields: null,
982 editFields: null,
983
984 setSelectors: function () {
985 var self = this;
986 self.editButton = $('#open_edit_pullrequest')
987 self.closeButton = $('#close_edit_pullrequest')
988 self.deleteButton = $('#delete_pullrequest')
989 self.viewFields = $('#pr-desc, #pr-title')
990 self.editFields = $('#pr-desc-edit, #pr-title-edit, .pr-save')
991 },
992
993 init: function () {
994 var self = this;
995 self.setSelectors();
996 self.editButton.on('click', function (e) {
997 self.edit();
998 });
999 self.closeButton.on('click', function (e) {
1000 self.view();
1001 });
1002 },
1003
1004 edit: function (event) {
1005 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
1006 this.viewFields.hide();
1007 this.editButton.hide();
1008 this.deleteButton.hide();
1009 this.closeButton.show();
1010 this.editFields.show();
1011 cmInstance.refresh();
1012 },
1013
1014 view: function (event) {
1015 this.editButton.show();
1016 this.deleteButton.show();
1017 this.editFields.hide();
1018 this.closeButton.hide();
1019 this.viewFields.show();
1020 }
1021 };
746
1022
747 /**
1023 /**
748 * OnLine presence using channelstream
1024 * OnLine presence using channelstream
749 */
1025 */
750 window.ReviewerPresenceController = function (channel) {
1026 window.ReviewerPresenceController = function (channel) {
751 var self = this;
1027 var self = this;
752 this.channel = channel;
1028 this.channel = channel;
753 this.users = {};
1029 this.users = {};
754
1030
755 this.storeUsers = function (users) {
1031 this.storeUsers = function (users) {
756 self.users = {}
1032 self.users = {}
757 $.each(users, function (index, value) {
1033 $.each(users, function (index, value) {
758 var userId = value.state.id;
1034 var userId = value.state.id;
759 self.users[userId] = value.state;
1035 self.users[userId] = value.state;
760 })
1036 })
761 }
1037 }
762
1038
763 this.render = function () {
1039 this.render = function () {
764 $.each($('.reviewer_entry'), function (index, value) {
1040 $.each($('.reviewer_entry'), function (index, value) {
765 var userData = $(value).data();
1041 var userData = $(value).data();
766 if (self.users[userData.reviewerUserId] !== undefined) {
1042 if (self.users[userData.reviewerUserId] !== undefined) {
767 $(value).find('.presence-state').show();
1043 $(value).find('.presence-state').show();
768 } else {
1044 } else {
769 $(value).find('.presence-state').hide();
1045 $(value).find('.presence-state').hide();
770 }
1046 }
771 })
1047 })
772 };
1048 };
773
1049
774 this.handlePresence = function (data) {
1050 this.handlePresence = function (data) {
775 if (data.type == 'presence' && data.channel === self.channel) {
1051 if (data.type == 'presence' && data.channel === self.channel) {
776 this.storeUsers(data.users);
1052 this.storeUsers(data.users);
777 this.render()
1053 this.render()
778 }
1054 }
779 };
1055 };
780
1056
781 this.handleChannelUpdate = function (data) {
1057 this.handleChannelUpdate = function (data) {
782 if (data.channel === this.channel) {
1058 if (data.channel === this.channel) {
783 this.storeUsers(data.state.users);
1059 this.storeUsers(data.state.users);
784 this.render()
1060 this.render()
785 }
1061 }
786
1062
787 };
1063 };
788
1064
789 /* subscribe to the current presence */
1065 /* subscribe to the current presence */
790 $.Topic('/connection_controller/presence').subscribe(this.handlePresence.bind(this));
1066 $.Topic('/connection_controller/presence').subscribe(this.handlePresence.bind(this));
791 /* subscribe to updates e.g connect/disconnect */
1067 /* subscribe to updates e.g connect/disconnect */
792 $.Topic('/connection_controller/channel_update').subscribe(this.handleChannelUpdate.bind(this));
1068 $.Topic('/connection_controller/channel_update').subscribe(this.handleChannelUpdate.bind(this));
793
1069
794 };
1070 };
795
1071
796 window.refreshComments = function (version) {
1072 window.refreshComments = function (version) {
797 version = version || templateContext.pull_request_data.pull_request_version || '';
1073 version = version || templateContext.pull_request_data.pull_request_version || '';
798
1074
799 // Pull request case
1075 // Pull request case
800 if (templateContext.pull_request_data.pull_request_id !== null) {
1076 if (templateContext.pull_request_data.pull_request_id !== null) {
801 var params = {
1077 var params = {
802 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1078 'pull_request_id': templateContext.pull_request_data.pull_request_id,
803 'repo_name': templateContext.repo_name,
1079 'repo_name': templateContext.repo_name,
804 'version': version,
1080 'version': version,
805 };
1081 };
806 var loadUrl = pyroutes.url('pullrequest_comments', params);
1082 var loadUrl = pyroutes.url('pullrequest_comments', params);
807 } // commit case
1083 } // commit case
808 else {
1084 else {
809 return
1085 return
810 }
1086 }
811
1087
812 var currentIDs = []
1088 var currentIDs = []
813 $.each($('.comment'), function (idx, element) {
1089 $.each($('.comment'), function (idx, element) {
814 currentIDs.push($(element).data('commentId'));
1090 currentIDs.push($(element).data('commentId'));
815 });
1091 });
816 var data = {"comments[]": currentIDs};
1092 var data = {"comments": currentIDs};
817
1093
818 var $targetElem = $('.comments-content-table');
1094 var $targetElem = $('.comments-content-table');
819 $targetElem.css('opacity', 0.3);
1095 $targetElem.css('opacity', 0.3);
820 $targetElem.load(
821 loadUrl, data, function (responseText, textStatus, jqXHR) {
822 if (jqXHR.status !== 200) {
823 return false;
824 }
825 var $counterElem = $('#comments-count');
826 var newCount = $(responseText).data('counter');
827 if (newCount !== undefined) {
828 var callback = function () {
829 $counterElem.animate({'opacity': 1.00}, 200)
830 $counterElem.html(newCount);
831 };
832 $counterElem.animate({'opacity': 0.15}, 200, callback);
833 }
834
1096
835 $targetElem.css('opacity', 1);
1097 var success = function (data) {
836 tooltipActivate();
1098 var $counterElem = $('#comments-count');
1099 var newCount = $(data).data('counter');
1100 if (newCount !== undefined) {
1101 var callback = function () {
1102 $counterElem.animate({'opacity': 1.00}, 200)
1103 $counterElem.html(newCount);
1104 };
1105 $counterElem.animate({'opacity': 0.15}, 200, callback);
837 }
1106 }
838 );
1107
1108 $targetElem.css('opacity', 1);
1109 $targetElem.html(data);
1110 tooltipActivate();
1111 }
1112
1113 ajaxPOST(loadUrl, data, success, null, {})
1114
839 }
1115 }
840
1116
841 window.refreshTODOs = function (version) {
1117 window.refreshTODOs = function (version) {
842 version = version || templateContext.pull_request_data.pull_request_version || '';
1118 version = version || templateContext.pull_request_data.pull_request_version || '';
843 // Pull request case
1119 // Pull request case
844 if (templateContext.pull_request_data.pull_request_id !== null) {
1120 if (templateContext.pull_request_data.pull_request_id !== null) {
845 var params = {
1121 var params = {
846 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1122 'pull_request_id': templateContext.pull_request_data.pull_request_id,
847 'repo_name': templateContext.repo_name,
1123 'repo_name': templateContext.repo_name,
848 'version': version,
1124 'version': version,
849 };
1125 };
850 var loadUrl = pyroutes.url('pullrequest_comments', params);
1126 var loadUrl = pyroutes.url('pullrequest_comments', params);
851 } // commit case
1127 } // commit case
852 else {
1128 else {
853 return
1129 return
854 }
1130 }
855
1131
856 var currentIDs = []
1132 var currentIDs = []
857 $.each($('.comment'), function (idx, element) {
1133 $.each($('.comment'), function (idx, element) {
858 currentIDs.push($(element).data('commentId'));
1134 currentIDs.push($(element).data('commentId'));
859 });
1135 });
860
1136
861 var data = {"comments[]": currentIDs};
1137 var data = {"comments": currentIDs};
862 var $targetElem = $('.todos-content-table');
1138 var $targetElem = $('.todos-content-table');
863 $targetElem.css('opacity', 0.3);
1139 $targetElem.css('opacity', 0.3);
864 $targetElem.load(
865 loadUrl, data, function (responseText, textStatus, jqXHR) {
866 if (jqXHR.status !== 200) {
867 return false;
868 }
869 var $counterElem = $('#todos-count')
870 var newCount = $(responseText).data('counter');
871 if (newCount !== undefined) {
872 var callback = function () {
873 $counterElem.animate({'opacity': 1.00}, 200)
874 $counterElem.html(newCount);
875 };
876 $counterElem.animate({'opacity': 0.15}, 200, callback);
877 }
878
1140
879 $targetElem.css('opacity', 1);
1141 var success = function (data) {
880 tooltipActivate();
1142 var $counterElem = $('#todos-count')
1143 var newCount = $(data).data('counter');
1144 if (newCount !== undefined) {
1145 var callback = function () {
1146 $counterElem.animate({'opacity': 1.00}, 200)
1147 $counterElem.html(newCount);
1148 };
1149 $counterElem.animate({'opacity': 0.15}, 200, callback);
881 }
1150 }
882 );
1151
1152 $targetElem.css('opacity', 1);
1153 $targetElem.html(data);
1154 tooltipActivate();
1155 }
1156
1157 ajaxPOST(loadUrl, data, success, null, {})
1158
883 }
1159 }
884
1160
885 window.refreshAllComments = function (version) {
1161 window.refreshAllComments = function (version) {
886 version = version || templateContext.pull_request_data.pull_request_version || '';
1162 version = version || templateContext.pull_request_data.pull_request_version || '';
887
1163
888 refreshComments(version);
1164 refreshComments(version);
889 refreshTODOs(version);
1165 refreshTODOs(version);
890 };
1166 };
1167
1168 window.sidebarComment = function (commentId) {
1169 var jsonData = $('#commentHovercard{0}'.format(commentId)).data('commentJsonB64');
1170 if (!jsonData) {
1171 return 'Failed to load comment {0}'.format(commentId)
1172 }
1173 var funcData = JSON.parse(atob(jsonData));
1174 return renderTemplate('sideBarCommentHovercard', funcData)
1175 };
@@ -1,178 +1,182 b''
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 /**
19 /**
20 * turns objects into GET query string
20 * turns objects into GET query string
21 */
21 */
22 var toQueryString = function(o) {
22 var toQueryString = function(o) {
23 if(typeof o === 'string') {
23 if(typeof o === 'string') {
24 return o;
24 return o;
25 }
25 }
26 if(typeof o !== 'object') {
26 if(typeof o !== 'object') {
27 return false;
27 return false;
28 }
28 }
29 var _p, _qs = [];
29 var _p, _qs = [];
30 for(_p in o) {
30 for(_p in o) {
31 _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
31 _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
32 }
32 }
33 return _qs.join('&');
33 return _qs.join('&');
34 };
34 };
35
35
36 /**
36 /**
37 * ajax call wrappers
37 * ajax call wrappers
38 */
38 */
39
39
40 var ajaxGET = function (url, success, failure) {
40 var ajaxGET = function (url, success, failure) {
41 var sUrl = url;
41 var sUrl = url;
42 var request = $.ajax({
42 var request = $.ajax({
43 url: sUrl,
43 url: sUrl,
44 headers: {'X-PARTIAL-XHR': true}
44 headers: {'X-PARTIAL-XHR': true}
45 })
45 })
46 .done(function (data) {
46 .done(function (data) {
47 success(data);
47 success(data);
48 })
48 })
49 .fail(function (jqXHR, textStatus, errorThrown) {
49 .fail(function (jqXHR, textStatus, errorThrown) {
50 if (failure) {
50 if (failure) {
51 failure(jqXHR, textStatus, errorThrown);
51 failure(jqXHR, textStatus, errorThrown);
52 } else {
52 } else {
53 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
53 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
54 ajaxErrorSwal(message);
54 ajaxErrorSwal(message);
55 }
55 }
56 });
56 });
57 return request;
57 return request;
58 };
58 };
59
59
60 var ajaxPOST = function (url, postData, success, failure) {
60 var ajaxPOST = function (url, postData, success, failure, options) {
61 var sUrl = url;
61
62 var postData = toQueryString(postData);
62 var ajaxSettings = $.extend({
63 var request = $.ajax({
64 type: 'POST',
63 type: 'POST',
65 url: sUrl,
64 url: url,
66 data: postData,
65 data: toQueryString(postData),
67 headers: {'X-PARTIAL-XHR': true}
66 headers: {'X-PARTIAL-XHR': true}
68 })
67 }, options);
68
69 var request = $.ajax(
70 ajaxSettings
71 )
69 .done(function (data) {
72 .done(function (data) {
70 success(data);
73 success(data);
71 })
74 })
72 .fail(function (jqXHR, textStatus, errorThrown) {
75 .fail(function (jqXHR, textStatus, errorThrown) {
73 if (failure) {
76 if (failure) {
74 failure(jqXHR, textStatus, errorThrown);
77 failure(jqXHR, textStatus, errorThrown);
75 } else {
78 } else {
76 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
79 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
77 ajaxErrorSwal(message);
80 ajaxErrorSwal(message);
78 }
81 }
79 });
82 });
80 return request;
83 return request;
81 };
84 };
82
85
83
86
84 SwalNoAnimation = Swal.mixin({
87 SwalNoAnimation = Swal.mixin({
85 confirmButtonColor: '#84a5d2',
88 confirmButtonColor: '#84a5d2',
86 cancelButtonColor: '#e85e4d',
89 cancelButtonColor: '#e85e4d',
87 showClass: {
90 showClass: {
88 popup: 'swal2-noanimation',
91 popup: 'swal2-noanimation',
89 backdrop: 'swal2-noanimation'
92 backdrop: 'swal2-noanimation'
90 },
93 },
91 hideClass: {
94 hideClass: {
92 popup: '',
95 popup: '',
93 backdrop: ''
96 backdrop: ''
94 },
97 },
95 })
98 })
96
99
97
100
98 /* Example usage:
101 /* Example usage:
99 *
102 *
100 error: function(jqXHR, textStatus, errorThrown) {
103 error: function(jqXHR, textStatus, errorThrown) {
101 var prefix = "Error while fetching entries.\n"
104 var prefix = "Error while fetching entries.\n"
102 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
105 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
103 ajaxErrorSwal(message);
106 ajaxErrorSwal(message);
104 }
107 }
105 *
108 *
106 * */
109 * */
107 function formatErrorMessage(jqXHR, textStatus, errorThrown, prefix) {
110 function formatErrorMessage(jqXHR, textStatus, errorThrown, prefix) {
108 if(typeof prefix === "undefined") {
111 if(typeof prefix === "undefined") {
109 prefix = ''
112 prefix = ''
110 }
113 }
111
114
112 if (jqXHR.status === 0) {
115 if (jqXHR.status === 0) {
113 return (prefix + 'Not connected.\nPlease verify your network connection.');
116 return (prefix + 'Not connected.\nPlease verify your network connection.');
114 } else if (jqXHR.status == 401) {
117 } else if (jqXHR.status == 401) {
115 return (prefix + 'Unauthorized access. [401]');
118 return (prefix + 'Unauthorized access. [401]');
116 } else if (jqXHR.status == 404) {
119 } else if (jqXHR.status == 404) {
117 return (prefix + 'The requested page not found. [404]');
120 return (prefix + 'The requested page not found. [404]');
118 } else if (jqXHR.status == 500) {
121 } else if (jqXHR.status == 500) {
119 return (prefix + 'Internal Server Error [500].');
122 return (prefix + 'Internal Server Error [500].');
120 } else if (jqXHR.status == 503) {
123 } else if (jqXHR.status == 503) {
121 return (prefix + 'Service unavailable [503].');
124 return (prefix + 'Service unavailable [503].');
122 } else if (errorThrown === 'parsererror') {
125 } else if (errorThrown === 'parsererror') {
123 return (prefix + 'Requested JSON parse failed.');
126 return (prefix + 'Requested JSON parse failed.');
124 } else if (errorThrown === 'timeout') {
127 } else if (errorThrown === 'timeout') {
125 return (prefix + 'Time out error.');
128 return (prefix + 'Time out error.');
126 } else if (errorThrown === 'abort') {
129 } else if (errorThrown === 'abort') {
127 return (prefix + 'Ajax request aborted.');
130 return (prefix + 'Ajax request aborted.');
128 } else {
131 } else {
129 return (prefix + 'Uncaught Error.\n' + jqXHR.responseText);
132 var errInfo = 'Uncaught Error. code: {0}\n'.format(jqXHR.status)
133 return (prefix + errInfo + jqXHR.responseText);
130 }
134 }
131 }
135 }
132
136
133 function ajaxErrorSwal(message, title) {
137 function ajaxErrorSwal(message, title) {
134
138
135 var title = (typeof title !== 'undefined') ? title : _gettext('Ajax Request Error');
139 var title = (typeof title !== 'undefined') ? title : _gettext('Ajax Request Error');
136
140
137 SwalNoAnimation.fire({
141 SwalNoAnimation.fire({
138 icon: 'error',
142 icon: 'error',
139 title: title,
143 title: title,
140 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
144 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
141 showClass: {
145 showClass: {
142 popup: 'swal2-noanimation',
146 popup: 'swal2-noanimation',
143 backdrop: 'swal2-noanimation'
147 backdrop: 'swal2-noanimation'
144 },
148 },
145 hideClass: {
149 hideClass: {
146 popup: '',
150 popup: '',
147 backdrop: ''
151 backdrop: ''
148 }
152 }
149 })
153 })
150 }
154 }
151
155
152 /*
156 /*
153 * use in onclick attributes e.g
157 * use in onclick attributes e.g
154 * onclick="submitConfirm(event, this, _gettext('Confirm to delete '), _gettext('Confirm Delete'), 'what we delete')">
158 * onclick="submitConfirm(event, this, _gettext('Confirm to delete '), _gettext('Confirm Delete'), 'what we delete')">
155 * */
159 * */
156 function submitConfirm(event, self, question, confirmText, htmlText) {
160 function submitConfirm(event, self, question, confirmText, htmlText) {
157 if (htmlText === "undefined") {
161 if (htmlText === "undefined") {
158 htmlText = null;
162 htmlText = null;
159 }
163 }
160 if (confirmText === "undefined") {
164 if (confirmText === "undefined") {
161 confirmText = _gettext('Delete')
165 confirmText = _gettext('Delete')
162 }
166 }
163 event.preventDefault();
167 event.preventDefault();
164
168
165 SwalNoAnimation.fire({
169 SwalNoAnimation.fire({
166 title: question,
170 title: question,
167 icon: 'warning',
171 icon: 'warning',
168 html: htmlText,
172 html: htmlText,
169
173
170 showCancelButton: true,
174 showCancelButton: true,
171
175
172 confirmButtonText: confirmText
176 confirmButtonText: confirmText
173 }).then(function(result) {
177 }).then(function(result) {
174 if (result.value) {
178 if (result.value) {
175 $(self).closest("form").submit();
179 $(self).closest("form").submit();
176 }
180 }
177 })
181 })
178 } No newline at end of file
182 }
@@ -1,142 +1,147 b''
1 ## snippet for sidebar elements
1 ## snippet for sidebar elements
2 ## usage:
2 ## usage:
3 ## <%namespace name="sidebar" file="/base/sidebar.mako"/>
3 ## <%namespace name="sidebar" file="/base/sidebar.mako"/>
4 ## ${sidebar.comments_table()}
4 ## ${sidebar.comments_table()}
5 <%namespace name="base" file="/base/base.mako"/>
5 <%namespace name="base" file="/base/base.mako"/>
6
6
7 <%def name="comments_table(comments, counter_num, todo_comments=False, existing_ids=None, is_pr=True)">
7 <%def name="comments_table(comments, counter_num, todo_comments=False, existing_ids=None, is_pr=True)">
8 <%
8 <%
9 if todo_comments:
9 if todo_comments:
10 cls_ = 'todos-content-table'
10 cls_ = 'todos-content-table'
11 def sorter(entry):
11 def sorter(entry):
12 user_id = entry.author.user_id
12 user_id = entry.author.user_id
13 resolved = '1' if entry.resolved else '0'
13 resolved = '1' if entry.resolved else '0'
14 if user_id == c.rhodecode_user.user_id:
14 if user_id == c.rhodecode_user.user_id:
15 # own comments first
15 # own comments first
16 user_id = 0
16 user_id = 0
17 return '{}'.format(str(entry.comment_id).zfill(10000))
17 return '{}'.format(str(entry.comment_id).zfill(10000))
18 else:
18 else:
19 cls_ = 'comments-content-table'
19 cls_ = 'comments-content-table'
20 def sorter(entry):
20 def sorter(entry):
21 user_id = entry.author.user_id
21 user_id = entry.author.user_id
22 return '{}'.format(str(entry.comment_id).zfill(10000))
22 return '{}'.format(str(entry.comment_id).zfill(10000))
23
23
24 existing_ids = existing_ids or []
24 existing_ids = existing_ids or []
25
25
26 %>
26 %>
27
27
28 <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
28 <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
29
29
30 % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
30 % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
31 <%
31 <%
32 display = ''
32 display = ''
33 _cls = ''
33 _cls = ''
34 %>
34 %>
35
35
36 <%
36 <%
37 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
37 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
38 prev_comment_ver_index = 0
38 prev_comment_ver_index = 0
39 if loop_obj.previous:
39 if loop_obj.previous:
40 prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
40 prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
41
41
42 ver_info = None
42 ver_info = None
43 if getattr(c, 'versions', []):
43 if getattr(c, 'versions', []):
44 ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None
44 ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None
45 %>
45 %>
46 <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
46 <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
47 <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
47 <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
48 <%
48 <%
49 if (prev_comment_ver_index > comment_ver_index):
49 if (prev_comment_ver_index > comment_ver_index):
50 comments_ver_divider = comment_ver_index
50 comments_ver_divider = comment_ver_index
51 else:
51 else:
52 comments_ver_divider = None
52 comments_ver_divider = None
53 %>
53 %>
54
54
55 % if todo_comments:
55 % if todo_comments:
56 % if comment_obj.resolved:
56 % if comment_obj.resolved:
57 <% _cls = 'resolved-todo' %>
57 <% _cls = 'resolved-todo' %>
58 <% display = 'none' %>
58 <% display = 'none' %>
59 % endif
59 % endif
60 % else:
60 % else:
61 ## SKIP TODOs we display them in other area
61 ## SKIP TODOs we display them in other area
62 % if comment_obj.is_todo:
62 % if comment_obj.is_todo:
63 <% display = 'none' %>
63 <% display = 'none' %>
64 % endif
64 % endif
65 ## Skip outdated comments
65 ## Skip outdated comments
66 % if comment_obj.outdated:
66 % if comment_obj.outdated:
67 <% display = 'none' %>
67 <% display = 'none' %>
68 <% _cls = 'hidden-comment' %>
68 <% _cls = 'hidden-comment' %>
69 % endif
69 % endif
70 % endif
70 % endif
71
71
72 % if not todo_comments and comments_ver_divider:
72 % if not todo_comments and comments_ver_divider:
73 <tr class="old-comments-marker">
73 <tr class="old-comments-marker">
74 <td colspan="3">
74 <td colspan="3">
75 % if ver_info:
75 % if ver_info:
76 <code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code>
76 <code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code>
77 % else:
77 % else:
78 <code>v${comments_ver_divider}</code>
78 <code>v${comments_ver_divider}</code>
79 % endif
79 % endif
80 </td>
80 </td>
81 </tr>
81 </tr>
82
82
83 % endif
83 % endif
84
84
85 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
85 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
86 <td class="td-todo-number">
86 <td class="td-todo-number">
87 <%
87 <%
88 version_info = ''
88 version_info = ''
89 if is_pr:
89 if is_pr:
90 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
90 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
91 %>
91 %>
92
92 ## NEW, since refresh
93 <script type="text/javascript">
93 % if existing_ids and comment_obj.comment_id not in existing_ids:
94 // closure function helper
94 <div class="tooltip" style="position: absolute; left: 8px" title="New comment">
95 var sidebarComment${comment_obj.comment_id} = function() {
95 !
96 return renderTemplate('sideBarCommentHovercard', {
96 </div>
97 version_info: "${version_info}",
98 file_name: "${comment_obj.f_path}",
99 line_no: "${comment_obj.line_no}",
100 outdated: ${h.json.dumps(comment_obj.outdated)},
101 inline: ${h.json.dumps(comment_obj.is_inline)},
102 is_todo: ${h.json.dumps(comment_obj.is_todo)},
103 created_on: "${h.format_date(comment_obj.created_on)}",
104 datetime: "${comment_obj.created_on}${h.get_timezone(comment_obj.created_on, time_is_local=True)}",
105 review_status: "${(comment_obj.review_status or '')}"
106 })
107 }
108 </script>
109
110 % if comment_obj.outdated:
111 <i class="icon-comment-toggle tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
112 % elif comment_obj.is_inline:
113 <i class="icon-code tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
114 % else:
115 <i class="icon-comment tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
116 % endif
97 % endif
117
98
118 ## NEW, since refresh
99 <%
119 % if existing_ids and comment_obj.comment_id not in existing_ids:
100 data = h.json.dumps({
120 <span class="tag">NEW</span>
101 'comment_id': comment_obj.comment_id,
121 % endif
102 'version_info': version_info,
103 'file_name': comment_obj.f_path,
104 'line_no': comment_obj.line_no,
105 'outdated': comment_obj.outdated,
106 'inline': comment_obj.is_inline,
107 'is_todo': comment_obj.is_todo,
108 'created_on': h.format_date(comment_obj.created_on),
109 'datetime': '{}{}'.format(comment_obj.created_on, h.get_timezone(comment_obj.created_on, time_is_local=True)),
110 'review_status': (comment_obj.review_status or '')
111 })
112
113 if comment_obj.outdated:
114 icon = 'icon-comment-toggle'
115 elif comment_obj.is_inline:
116 icon = 'icon-code'
117 else:
118 icon = 'icon-comment'
119 %>
120
121 <i id="commentHovercard${comment_obj.comment_id}"
122 class="${icon} tooltip-hovercard"
123 data-hovercard-url="javascript:sidebarComment(${comment_obj.comment_id})"
124 data-comment-json-b64='${h.b64(data)}'>
125 </i>
126
122 </td>
127 </td>
123
128
124 <td class="td-todo-gravatar">
129 <td class="td-todo-gravatar">
125 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
130 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
126 </td>
131 </td>
127 <td class="todo-comment-text-wrapper">
132 <td class="todo-comment-text-wrapper">
128 <div class="todo-comment-text ${('todo-resolved' if comment_obj.resolved else '')}">
133 <div class="todo-comment-text ${('todo-resolved' if comment_obj.resolved else '')}">
129 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
134 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
130 href="#comment-${comment_obj.comment_id}"
135 href="#comment-${comment_obj.comment_id}"
131 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
136 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
132
137
133 ${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}
138 ${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}
134 </a>
139 </a>
135 </div>
140 </div>
136 </td>
141 </td>
137 </tr>
142 </tr>
138 % endfor
143 % endfor
139
144
140 </table>
145 </table>
141
146
142 </%def> No newline at end of file
147 </%def>
@@ -1,431 +1,431 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%inherit file="/base/base.mako"/>
3 <%inherit file="/base/base.mako"/>
4 <%namespace name="base" file="/base/base.mako"/>
4 <%namespace name="base" file="/base/base.mako"/>
5 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
5 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
6 <%namespace name="file_base" file="/files/base.mako"/>
6 <%namespace name="file_base" file="/files/base.mako"/>
7 <%namespace name="sidebar" file="/base/sidebar.mako"/>
7 <%namespace name="sidebar" file="/base/sidebar.mako"/>
8
8
9
9
10 <%def name="title()">
10 <%def name="title()">
11 ${_('{} Commit').format(c.repo_name)} - ${h.show_id(c.commit)}
11 ${_('{} Commit').format(c.repo_name)} - ${h.show_id(c.commit)}
12 %if c.rhodecode_name:
12 %if c.rhodecode_name:
13 &middot; ${h.branding(c.rhodecode_name)}
13 &middot; ${h.branding(c.rhodecode_name)}
14 %endif
14 %endif
15 </%def>
15 </%def>
16
16
17 <%def name="menu_bar_nav()">
17 <%def name="menu_bar_nav()">
18 ${self.menu_items(active='repositories')}
18 ${self.menu_items(active='repositories')}
19 </%def>
19 </%def>
20
20
21 <%def name="menu_bar_subnav()">
21 <%def name="menu_bar_subnav()">
22 ${self.repo_menu(active='commits')}
22 ${self.repo_menu(active='commits')}
23 </%def>
23 </%def>
24
24
25 <%def name="main()">
25 <%def name="main()">
26 <script type="text/javascript">
26 <script type="text/javascript">
27 // TODO: marcink switch this to pyroutes
27 // TODO: marcink switch this to pyroutes
28 AJAX_COMMENT_DELETE_URL = "${h.route_path('repo_commit_comment_delete',repo_name=c.repo_name,commit_id=c.commit.raw_id,comment_id='__COMMENT_ID__')}";
28 AJAX_COMMENT_DELETE_URL = "${h.route_path('repo_commit_comment_delete',repo_name=c.repo_name,commit_id=c.commit.raw_id,comment_id='__COMMENT_ID__')}";
29 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
29 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
30 </script>
30 </script>
31
31
32 <div class="box">
32 <div class="box">
33
33
34 <div class="summary">
34 <div class="summary">
35
35
36 <div class="fieldset">
36 <div class="fieldset">
37 <div class="left-content">
37 <div class="left-content">
38 <%
38 <%
39 rc_user = h.discover_user(c.commit.author_email)
39 rc_user = h.discover_user(c.commit.author_email)
40 %>
40 %>
41 <div class="left-content-avatar">
41 <div class="left-content-avatar">
42 ${base.gravatar(c.commit.author_email, 30, tooltip=(True if rc_user else False), user=rc_user)}
42 ${base.gravatar(c.commit.author_email, 30, tooltip=(True if rc_user else False), user=rc_user)}
43 </div>
43 </div>
44
44
45 <div class="left-content-message">
45 <div class="left-content-message">
46 <div class="fieldset collapsable-content no-hide" data-toggle="summary-details">
46 <div class="fieldset collapsable-content no-hide" data-toggle="summary-details">
47 <div class="commit truncate-wrap">${h.urlify_commit_message(h.chop_at_smart(c.commit.message, '\n', suffix_if_chopped='...'), c.repo_name)}</div>
47 <div class="commit truncate-wrap">${h.urlify_commit_message(h.chop_at_smart(c.commit.message, '\n', suffix_if_chopped='...'), c.repo_name)}</div>
48 </div>
48 </div>
49
49
50 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none">
50 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none">
51 <div class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
51 <div class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
52 </div>
52 </div>
53
53
54 <div class="fieldset" data-toggle="summary-details">
54 <div class="fieldset" data-toggle="summary-details">
55 <div class="">
55 <div class="">
56 <table>
56 <table>
57 <tr class="file_author">
57 <tr class="file_author">
58
58
59 <td>
59 <td>
60 <span class="user commit-author">${h.link_to_user(rc_user or c.commit.author)}</span>
60 <span class="user commit-author">${h.link_to_user(rc_user or c.commit.author)}</span>
61 <span class="commit-date">- ${h.age_component(c.commit.date)}</span>
61 <span class="commit-date">- ${h.age_component(c.commit.date)}</span>
62 </td>
62 </td>
63
63
64 <td>
64 <td>
65 ## second cell for consistency with files
65 ## second cell for consistency with files
66 </td>
66 </td>
67 </tr>
67 </tr>
68 </table>
68 </table>
69 </div>
69 </div>
70 </div>
70 </div>
71
71
72 </div>
72 </div>
73 </div>
73 </div>
74
74
75 <div class="right-content">
75 <div class="right-content">
76
76
77 <div data-toggle="summary-details">
77 <div data-toggle="summary-details">
78 <div class="tags tags-main">
78 <div class="tags tags-main">
79 <code><a href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">${h.show_id(c.commit)}</a></code>
79 <code><a href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">${h.show_id(c.commit)}</a></code>
80 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.commit.raw_id}" title="${_('Copy the full commit id')}"></i>
80 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.commit.raw_id}" title="${_('Copy the full commit id')}"></i>
81 ${file_base.refs(c.commit)}
81 ${file_base.refs(c.commit)}
82
82
83 ## phase
83 ## phase
84 % if hasattr(c.commit, 'phase') and getattr(c.commit, 'phase') != 'public':
84 % if hasattr(c.commit, 'phase') and getattr(c.commit, 'phase') != 'public':
85 <span class="tag phase-${c.commit.phase} tooltip" title="${_('Commit phase')}">
85 <span class="tag phase-${c.commit.phase} tooltip" title="${_('Commit phase')}">
86 <i class="icon-info"></i>${c.commit.phase}
86 <i class="icon-info"></i>${c.commit.phase}
87 </span>
87 </span>
88 % endif
88 % endif
89
89
90 ## obsolete commits
90 ## obsolete commits
91 % if getattr(c.commit, 'obsolete', False):
91 % if getattr(c.commit, 'obsolete', False):
92 <span class="tag obsolete-${c.commit.obsolete} tooltip" title="${_('Evolve State')}">
92 <span class="tag obsolete-${c.commit.obsolete} tooltip" title="${_('Evolve State')}">
93 ${_('obsolete')}
93 ${_('obsolete')}
94 </span>
94 </span>
95 % endif
95 % endif
96
96
97 ## hidden commits
97 ## hidden commits
98 % if getattr(c.commit, 'hidden', False):
98 % if getattr(c.commit, 'hidden', False):
99 <span class="tag hidden-${c.commit.hidden} tooltip" title="${_('Evolve State')}">
99 <span class="tag hidden-${c.commit.hidden} tooltip" title="${_('Evolve State')}">
100 ${_('hidden')}
100 ${_('hidden')}
101 </span>
101 </span>
102 % endif
102 % endif
103 </div>
103 </div>
104
104
105 <span id="parent_link" class="tag tagtag">
105 <span id="parent_link" class="tag tagtag">
106 <a href="#parentCommit" title="${_('Parent Commit')}"><i class="icon-left icon-no-margin"></i>${_('parent')}</a>
106 <a href="#parentCommit" title="${_('Parent Commit')}"><i class="icon-left icon-no-margin"></i>${_('parent')}</a>
107 </span>
107 </span>
108
108
109 <span id="child_link" class="tag tagtag">
109 <span id="child_link" class="tag tagtag">
110 <a href="#childCommit" title="${_('Child Commit')}">${_('child')}<i class="icon-right icon-no-margin"></i></a>
110 <a href="#childCommit" title="${_('Child Commit')}">${_('child')}<i class="icon-right icon-no-margin"></i></a>
111 </span>
111 </span>
112
112
113 </div>
113 </div>
114
114
115 </div>
115 </div>
116 </div>
116 </div>
117
117
118 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
118 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
119 <div class="left-label-summary">
119 <div class="left-label-summary">
120 <p>${_('Diff options')}:</p>
120 <p>${_('Diff options')}:</p>
121 <div class="right-label-summary">
121 <div class="right-label-summary">
122 <div class="diff-actions">
122 <div class="diff-actions">
123 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
123 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
124 ${_('Raw Diff')}
124 ${_('Raw Diff')}
125 </a>
125 </a>
126 |
126 |
127 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
127 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
128 ${_('Patch Diff')}
128 ${_('Patch Diff')}
129 </a>
129 </a>
130 |
130 |
131 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(diff='download'))}">
131 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(diff='download'))}">
132 ${_('Download Diff')}
132 ${_('Download Diff')}
133 </a>
133 </a>
134 </div>
134 </div>
135 </div>
135 </div>
136 </div>
136 </div>
137 </div>
137 </div>
138
138
139 <div class="clear-fix"></div>
139 <div class="clear-fix"></div>
140
140
141 <div class="btn-collapse" data-toggle="summary-details">
141 <div class="btn-collapse" data-toggle="summary-details">
142 ${_('Show More')}
142 ${_('Show More')}
143 </div>
143 </div>
144
144
145 </div>
145 </div>
146
146
147 <div class="cs_files">
147 <div class="cs_files">
148 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
148 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
149 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id], commit=c.commit)}
149 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id], commit=c.commit)}
150 ${cbdiffs.render_diffset(
150 ${cbdiffs.render_diffset(
151 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,
151 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,
152 inline_comments=c.inline_comments,
152 inline_comments=c.inline_comments,
153 show_todos=False)}
153 show_todos=False)}
154 </div>
154 </div>
155
155
156 ## template for inline comment form
156 ## template for inline comment form
157 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
157 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
158
158
159 ## comments heading with count
159 ## comments heading with count
160 <div class="comments-heading">
160 <div class="comments-heading">
161 <i class="icon-comment"></i>
161 <i class="icon-comment"></i>
162 ${_('General Comments')} ${len(c.comments)}
162 ${_('General Comments')} ${len(c.comments)}
163 </div>
163 </div>
164
164
165 ## render comments
165 ## render comments
166 ${comment.generate_comments(c.comments)}
166 ${comment.generate_comments(c.comments)}
167
167
168 ## main comment form and it status
168 ## main comment form and it status
169 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id=c.commit.raw_id),
169 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id=c.commit.raw_id),
170 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
170 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
171 </div>
171 </div>
172
172
173 ### NAV SIDEBAR
173 ### NAV SIDEBAR
174 <aside class="right-sidebar right-sidebar-expanded" id="commit-nav-sticky" style="display: none">
174 <aside class="right-sidebar right-sidebar-expanded" id="commit-nav-sticky" style="display: none">
175 <div class="sidenav navbar__inner" >
175 <div class="sidenav navbar__inner" >
176 ## TOGGLE
176 ## TOGGLE
177 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
177 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
178 <a href="#toggleSidebar" class="grey-link-action">
178 <a href="#toggleSidebar" class="grey-link-action">
179
179
180 </a>
180 </a>
181 </div>
181 </div>
182
182
183 ## CONTENT
183 ## CONTENT
184 <div class="sidebar-content">
184 <div class="sidebar-content">
185
185
186 ## RULES SUMMARY/RULES
186 ## RULES SUMMARY/RULES
187 <div class="sidebar-element clear-both">
187 <div class="sidebar-element clear-both">
188 <% vote_title = _ungettext(
188 <% vote_title = _ungettext(
189 'Status calculated based on votes from {} reviewer',
189 'Status calculated based on votes from {} reviewer',
190 'Status calculated based on votes from {} reviewers', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))
190 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
191 %>
191 %>
192
192
193 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
193 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
194 <i class="icon-circle review-status-${c.commit_review_status}"></i>
194 <i class="icon-circle review-status-${c.commit_review_status}"></i>
195 ${len(c.allowed_reviewers)}
195 ${c.reviewers_count}
196 </div>
196 </div>
197 </div>
197 </div>
198
198
199 ## REVIEWERS
199 ## REVIEWERS
200 <div class="right-sidebar-expanded-state pr-details-title">
200 <div class="right-sidebar-expanded-state pr-details-title">
201 <span class="tooltip sidebar-heading" title="${vote_title}">
201 <span class="tooltip sidebar-heading" title="${vote_title}">
202 <i class="icon-circle review-status-${c.commit_review_status}"></i>
202 <i class="icon-circle review-status-${c.commit_review_status}"></i>
203 ${_('Reviewers')}
203 ${_('Reviewers')}
204 </span>
204 </span>
205 </div>
205 </div>
206
206
207 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
207 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
208
208
209 <table id="review_members" class="group_members">
209 <table id="review_members" class="group_members">
210 ## This content is loaded via JS and ReviewersPanel
210 ## This content is loaded via JS and ReviewersPanel
211 </table>
211 </table>
212
212
213 </div>
213 </div>
214
214
215 ## TODOs
215 ## TODOs
216 <div class="sidebar-element clear-both">
216 <div class="sidebar-element clear-both">
217 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
217 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
218 <i class="icon-flag-filled"></i>
218 <i class="icon-flag-filled"></i>
219 <span id="todos-count">${len(c.unresolved_comments)}</span>
219 <span id="todos-count">${len(c.unresolved_comments)}</span>
220 </div>
220 </div>
221
221
222 <div class="right-sidebar-expanded-state pr-details-title">
222 <div class="right-sidebar-expanded-state pr-details-title">
223 ## Only show unresolved, that is only what matters
223 ## Only show unresolved, that is only what matters
224 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
224 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
225 <i class="icon-flag-filled"></i>
225 <i class="icon-flag-filled"></i>
226 TODOs
226 TODOs
227 </span>
227 </span>
228
228
229 % if c.resolved_comments:
229 % if c.resolved_comments:
230 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
230 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
231 % else:
231 % else:
232 <span class="block-right last-item noselect">Show resolved</span>
232 <span class="block-right last-item noselect">Show resolved</span>
233 % endif
233 % endif
234
234
235 </div>
235 </div>
236
236
237 <div class="right-sidebar-expanded-state pr-details-content">
237 <div class="right-sidebar-expanded-state pr-details-content">
238 % if c.unresolved_comments + c.resolved_comments:
238 % if c.unresolved_comments + c.resolved_comments:
239 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True, is_pr=False)}
239 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True, is_pr=False)}
240 % else:
240 % else:
241 <table>
241 <table>
242 <tr>
242 <tr>
243 <td>
243 <td>
244 ${_('No TODOs yet')}
244 ${_('No TODOs yet')}
245 </td>
245 </td>
246 </tr>
246 </tr>
247 </table>
247 </table>
248 % endif
248 % endif
249 </div>
249 </div>
250 </div>
250 </div>
251
251
252 ## COMMENTS
252 ## COMMENTS
253 <div class="sidebar-element clear-both">
253 <div class="sidebar-element clear-both">
254 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
254 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
255 <i class="icon-comment" style="color: #949494"></i>
255 <i class="icon-comment" style="color: #949494"></i>
256 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
256 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
257 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
257 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
258 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
258 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
259 </div>
259 </div>
260
260
261 <div class="right-sidebar-expanded-state pr-details-title">
261 <div class="right-sidebar-expanded-state pr-details-title">
262 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
262 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
263 <i class="icon-comment" style="color: #949494"></i>
263 <i class="icon-comment" style="color: #949494"></i>
264 ${_('Comments')}
264 ${_('Comments')}
265 </span>
265 </span>
266
266
267 </div>
267 </div>
268
268
269 <div class="right-sidebar-expanded-state pr-details-content">
269 <div class="right-sidebar-expanded-state pr-details-content">
270 % if c.inline_comments_flat + c.comments:
270 % if c.inline_comments_flat + c.comments:
271 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments), is_pr=False)}
271 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments), is_pr=False)}
272 % else:
272 % else:
273 <table>
273 <table>
274 <tr>
274 <tr>
275 <td>
275 <td>
276 ${_('No Comments yet')}
276 ${_('No Comments yet')}
277 </td>
277 </td>
278 </tr>
278 </tr>
279 </table>
279 </table>
280 % endif
280 % endif
281 </div>
281 </div>
282
282
283 </div>
283 </div>
284
284
285 </div>
285 </div>
286
286
287 </div>
287 </div>
288 </aside>
288 </aside>
289
289
290 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
290 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
291 <script type="text/javascript">
291 <script type="text/javascript">
292 window.setReviewersData = ${c.commit_set_reviewers_data_json | n};
292 window.setReviewersData = ${c.commit_set_reviewers_data_json | n};
293
293
294 $(document).ready(function () {
294 $(document).ready(function () {
295 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
295 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
296
296
297 if ($('#trimmed_message_box').height() === boxmax) {
297 if ($('#trimmed_message_box').height() === boxmax) {
298 $('#message_expand').show();
298 $('#message_expand').show();
299 }
299 }
300
300
301 $('#message_expand').on('click', function (e) {
301 $('#message_expand').on('click', function (e) {
302 $('#trimmed_message_box').css('max-height', 'none');
302 $('#trimmed_message_box').css('max-height', 'none');
303 $(this).hide();
303 $(this).hide();
304 });
304 });
305
305
306 $('.show-inline-comments').on('click', function (e) {
306 $('.show-inline-comments').on('click', function (e) {
307 var boxid = $(this).attr('data-comment-id');
307 var boxid = $(this).attr('data-comment-id');
308 var button = $(this);
308 var button = $(this);
309
309
310 if (button.hasClass("comments-visible")) {
310 if (button.hasClass("comments-visible")) {
311 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
311 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
312 $(this).hide();
312 $(this).hide();
313 });
313 });
314 button.removeClass("comments-visible");
314 button.removeClass("comments-visible");
315 } else {
315 } else {
316 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
316 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
317 $(this).show();
317 $(this).show();
318 });
318 });
319 button.addClass("comments-visible");
319 button.addClass("comments-visible");
320 }
320 }
321 });
321 });
322
322
323 // next links
323 // next links
324 $('#child_link').on('click', function (e) {
324 $('#child_link').on('click', function (e) {
325 // fetch via ajax what is going to be the next link, if we have
325 // fetch via ajax what is going to be the next link, if we have
326 // >1 links show them to user to choose
326 // >1 links show them to user to choose
327 if (!$('#child_link').hasClass('disabled')) {
327 if (!$('#child_link').hasClass('disabled')) {
328 $.ajax({
328 $.ajax({
329 url: '${h.route_path('repo_commit_children',repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
329 url: '${h.route_path('repo_commit_children',repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
330 success: function (data) {
330 success: function (data) {
331 if (data.results.length === 0) {
331 if (data.results.length === 0) {
332 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
332 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
333 }
333 }
334 if (data.results.length === 1) {
334 if (data.results.length === 1) {
335 var commit = data.results[0];
335 var commit = data.results[0];
336 window.location = pyroutes.url('repo_commit', {
336 window.location = pyroutes.url('repo_commit', {
337 'repo_name': '${c.repo_name}',
337 'repo_name': '${c.repo_name}',
338 'commit_id': commit.raw_id
338 'commit_id': commit.raw_id
339 });
339 });
340 } else if (data.results.length === 2) {
340 } else if (data.results.length === 2) {
341 $('#child_link').addClass('disabled');
341 $('#child_link').addClass('disabled');
342 $('#child_link').addClass('double');
342 $('#child_link').addClass('double');
343
343
344 var _html = '';
344 var _html = '';
345 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
345 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
346 .replace('__branch__', data.results[0].branch)
346 .replace('__branch__', data.results[0].branch)
347 .replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
347 .replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
348 .replace('__title__', data.results[0].message)
348 .replace('__title__', data.results[0].message)
349 .replace('__url__', pyroutes.url('repo_commit', {
349 .replace('__url__', pyroutes.url('repo_commit', {
350 'repo_name': '${c.repo_name}',
350 'repo_name': '${c.repo_name}',
351 'commit_id': data.results[0].raw_id
351 'commit_id': data.results[0].raw_id
352 }));
352 }));
353 _html += ' | ';
353 _html += ' | ';
354 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
354 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
355 .replace('__branch__', data.results[1].branch)
355 .replace('__branch__', data.results[1].branch)
356 .replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
356 .replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
357 .replace('__title__', data.results[1].message)
357 .replace('__title__', data.results[1].message)
358 .replace('__url__', pyroutes.url('repo_commit', {
358 .replace('__url__', pyroutes.url('repo_commit', {
359 'repo_name': '${c.repo_name}',
359 'repo_name': '${c.repo_name}',
360 'commit_id': data.results[1].raw_id
360 'commit_id': data.results[1].raw_id
361 }));
361 }));
362 $('#child_link').html(_html);
362 $('#child_link').html(_html);
363 }
363 }
364 }
364 }
365 });
365 });
366 e.preventDefault();
366 e.preventDefault();
367 }
367 }
368 });
368 });
369
369
370 // prev links
370 // prev links
371 $('#parent_link').on('click', function (e) {
371 $('#parent_link').on('click', function (e) {
372 // fetch via ajax what is going to be the next link, if we have
372 // fetch via ajax what is going to be the next link, if we have
373 // >1 links show them to user to choose
373 // >1 links show them to user to choose
374 if (!$('#parent_link').hasClass('disabled')) {
374 if (!$('#parent_link').hasClass('disabled')) {
375 $.ajax({
375 $.ajax({
376 url: '${h.route_path("repo_commit_parents",repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
376 url: '${h.route_path("repo_commit_parents",repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
377 success: function (data) {
377 success: function (data) {
378 if (data.results.length === 0) {
378 if (data.results.length === 0) {
379 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
379 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
380 }
380 }
381 if (data.results.length === 1) {
381 if (data.results.length === 1) {
382 var commit = data.results[0];
382 var commit = data.results[0];
383 window.location = pyroutes.url('repo_commit', {
383 window.location = pyroutes.url('repo_commit', {
384 'repo_name': '${c.repo_name}',
384 'repo_name': '${c.repo_name}',
385 'commit_id': commit.raw_id
385 'commit_id': commit.raw_id
386 });
386 });
387 } else if (data.results.length === 2) {
387 } else if (data.results.length === 2) {
388 $('#parent_link').addClass('disabled');
388 $('#parent_link').addClass('disabled');
389 $('#parent_link').addClass('double');
389 $('#parent_link').addClass('double');
390
390
391 var _html = '';
391 var _html = '';
392 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
392 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
393 .replace('__branch__', data.results[0].branch)
393 .replace('__branch__', data.results[0].branch)
394 .replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
394 .replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
395 .replace('__title__', data.results[0].message)
395 .replace('__title__', data.results[0].message)
396 .replace('__url__', pyroutes.url('repo_commit', {
396 .replace('__url__', pyroutes.url('repo_commit', {
397 'repo_name': '${c.repo_name}',
397 'repo_name': '${c.repo_name}',
398 'commit_id': data.results[0].raw_id
398 'commit_id': data.results[0].raw_id
399 }));
399 }));
400 _html += ' | ';
400 _html += ' | ';
401 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
401 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
402 .replace('__branch__', data.results[1].branch)
402 .replace('__branch__', data.results[1].branch)
403 .replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
403 .replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
404 .replace('__title__', data.results[1].message)
404 .replace('__title__', data.results[1].message)
405 .replace('__url__', pyroutes.url('repo_commit', {
405 .replace('__url__', pyroutes.url('repo_commit', {
406 'repo_name': '${c.repo_name}',
406 'repo_name': '${c.repo_name}',
407 'commit_id': data.results[1].raw_id
407 'commit_id': data.results[1].raw_id
408 }));
408 }));
409 $('#parent_link').html(_html);
409 $('#parent_link').html(_html);
410 }
410 }
411 }
411 }
412 });
412 });
413 e.preventDefault();
413 e.preventDefault();
414 }
414 }
415 });
415 });
416
416
417 // browse tree @ revision
417 // browse tree @ revision
418 $('#files_link').on('click', function (e) {
418 $('#files_link').on('click', function (e) {
419 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
419 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
420 e.preventDefault();
420 e.preventDefault();
421 });
421 });
422
422
423 ReviewersPanel.init(null, setReviewersData);
423 ReviewersPanel.init(null, setReviewersData);
424
424
425 var channel = '${c.commit_broadcast_channel}';
425 var channel = '${c.commit_broadcast_channel}';
426 new ReviewerPresenceController(channel)
426 new ReviewerPresenceController(channel)
427
427
428 })
428 })
429 </script>
429 </script>
430
430
431 </%def>
431 </%def>
@@ -1,962 +1,962 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/debug_style/index.html"/>
2 <%inherit file="/debug_style/index.html"/>
3
3
4 <%def name="breadcrumbs_links()">
4 <%def name="breadcrumbs_links()">
5 ${h.link_to(_('Style'), h.route_path('debug_style_home'))}
5 ${h.link_to(_('Style'), h.route_path('debug_style_home'))}
6 &raquo;
6 &raquo;
7 ${c.active}
7 ${c.active}
8 </%def>
8 </%def>
9
9
10
10
11 <%def name="real_main()">
11 <%def name="real_main()">
12 <div class="box">
12 <div class="box">
13 <div class="title">
13 <div class="title">
14 ${self.breadcrumbs()}
14 ${self.breadcrumbs()}
15 </div>
15 </div>
16
16
17 <div class='sidebar-col-wrapper'>
17 <div class='sidebar-col-wrapper'>
18 ${self.sidebar()}
18 ${self.sidebar()}
19
19
20 <div class="main-content">
20 <div class="main-content">
21
21
22 <h2>Collapsable Content</h2>
22 <h2>Collapsable Content</h2>
23 <p>Where a section may have a very long list of information, it can be desirable to use collapsable content. There is a premade function for showing/hiding elements, though its use may or may not be practical, depending on the situation. Use it, or don't, on a case-by-case basis.</p>
23 <p>Where a section may have a very long list of information, it can be desirable to use collapsable content. There is a premade function for showing/hiding elements, though its use may or may not be practical, depending on the situation. Use it, or don't, on a case-by-case basis.</p>
24
24
25 <p><strong>To use the collapsable-content function:</strong> Create a toggle button using <code>&lt;div class="btn-collapse"&gt;Show More&lt;/div&gt;</code> and a data attribute using <code>data-toggle</code>. Clicking this button will toggle any sibling element(s) containing the class <code>collapsable-content</code> and an identical <code>data-toggle</code> attribute. It will also change the button to read "Show Less"; another click toggles it back to the previous state. Ideally, use pre-existing elements and add the class and attribute; creating a new div around the existing content may lead to unexpected results, as the toggle function will use <code>display:block</code> if no previous display specification was found.
25 <p><strong>To use the collapsable-content function:</strong> Create a toggle button using <code>&lt;div class="btn-collapse"&gt;Show More&lt;/div&gt;</code> and a data attribute using <code>data-toggle</code>. Clicking this button will toggle any sibling element(s) containing the class <code>collapsable-content</code> and an identical <code>data-toggle</code> attribute. It will also change the button to read "Show Less"; another click toggles it back to the previous state. Ideally, use pre-existing elements and add the class and attribute; creating a new div around the existing content may lead to unexpected results, as the toggle function will use <code>display:block</code> if no previous display specification was found.
26 </p>
26 </p>
27 <p>Notes:</p>
27 <p>Notes:</p>
28 <ul>
28 <ul>
29 <li>Changes made to the text of the button will require adjustment to the function, but for the sake of consistency and user experience, this is best avoided. </li>
29 <li>Changes made to the text of the button will require adjustment to the function, but for the sake of consistency and user experience, this is best avoided. </li>
30 <li>Collapsable content inside of a pjax loaded container will require <code>collapsableContent();</code> to be called from within the container. No variables are necessary.</li>
30 <li>Collapsable content inside of a pjax loaded container will require <code>collapsableContent();</code> to be called from within the container. No variables are necessary.</li>
31 </ul>
31 </ul>
32
32
33 </div> <!-- .main-content -->
33 </div> <!-- .main-content -->
34 </div> <!-- .sidebar-col-wrapper -->
34 </div> <!-- .sidebar-col-wrapper -->
35 </div> <!-- .box -->
35 </div> <!-- .box -->
36
36
37 <!-- CONTENT -->
37 <!-- CONTENT -->
38 <div id="content" class="wrapper">
38 <div id="content" class="wrapper">
39
39
40 <div class="main">
40 <div class="main">
41
41
42 <div class="box">
42 <div class="box">
43 <div class="title">
43 <div class="title">
44 <h1>
44 <h1>
45 Diff: enable filename with spaces on diffs
45 Diff: enable filename with spaces on diffs
46 </h1>
46 </h1>
47 <h1>
47 <h1>
48 <i class="icon-hg" ></i>
48 <i class="icon-hg" ></i>
49
49
50 <i class="icon-lock"></i>
50 <i class="icon-lock"></i>
51 <span><a href="/rhodecode-momentum">rhodecode-momentum</a></span>
51 <span><a href="/rhodecode-momentum">rhodecode-momentum</a></span>
52
52
53 </h1>
53 </h1>
54 </div>
54 </div>
55
55
56 <div class="box pr-summary">
56 <div class="box pr-summary">
57 <div class="summary-details block-left">
57 <div class="summary-details block-left">
58
58
59 <div class="pr-details-title">
59 <div class="pr-details-title">
60
60
61 Pull request #720 From Tue, 17 Feb 2015 16:21:38
61 Pull request #720 From Tue, 17 Feb 2015 16:21:38
62 <div class="btn-collapse" data-toggle="description">Show More</div>
62 <div class="btn-collapse" data-toggle="description">Show More</div>
63 </div>
63 </div>
64 <div id="summary" class="fields pr-details-content">
64 <div id="summary" class="fields pr-details-content">
65 <div class="field">
65 <div class="field">
66 <div class="label-summary">
66 <div class="label-summary">
67 <label>Origin:</label>
67 <label>Origin:</label>
68 </div>
68 </div>
69 <div class="input">
69 <div class="input">
70 <div>
70 <div>
71 <span class="tag">
71 <span class="tag">
72 <a href="/andersonsantos/rhodecode-momentum-fork#fix_574">book: fix_574</a>
72 <a href="/andersonsantos/rhodecode-momentum-fork#fix_574">book: fix_574</a>
73 </span>
73 </span>
74 <span class="clone-url">
74 <span class="clone-url">
75 <a href="/andersonsantos/rhodecode-momentum-fork">https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork</a>
75 <a href="/andersonsantos/rhodecode-momentum-fork">https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork</a>
76 </span>
76 </span>
77 </div>
77 </div>
78 <div>
78 <div>
79 <br>
79 <br>
80 <input type="text" value="hg pull -r 46b3d50315f0 https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork" readonly="readonly">
80 <input type="text" value="hg pull -r 46b3d50315f0 https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork" readonly="readonly">
81 </div>
81 </div>
82 </div>
82 </div>
83 </div>
83 </div>
84 <div class="field">
84 <div class="field">
85 <div class="label-summary">
85 <div class="label-summary">
86 <label>Review:</label>
86 <label>Review:</label>
87 </div>
87 </div>
88 <div class="input">
88 <div class="input">
89 <div class="flag_status under_review tooltip pull-left" title="Pull request status calculated from votes"></div>
89 <div class="flag_status under_review tooltip pull-left" title="Pull request status calculated from votes"></div>
90 <span class="changeset-status-lbl tooltip" title="Pull request status calculated from votes">
90 <span class="changeset-status-lbl tooltip" title="Pull request status calculated from votes">
91 Under Review
91 Under Review
92 </span>
92 </span>
93
93
94 </div>
94 </div>
95 </div>
95 </div>
96 <div class="field collapsable-content" data-toggle="description">
96 <div class="field collapsable-content" data-toggle="description">
97 <div class="label-summary">
97 <div class="label-summary">
98 <label>Description:</label>
98 <label>Description:</label>
99 </div>
99 </div>
100 <div class="input">
100 <div class="input">
101 <div class="pr-description">Fixing issue <a class="issue- tracker-link" href="http://bugs.rhodecode.com/issues/574"># 574</a>, changing regex for capturing filenames</div>
101 <div class="pr-description">Fixing issue <a class="issue- tracker-link" href="http://bugs.rhodecode.com/issues/574"># 574</a>, changing regex for capturing filenames</div>
102 </div>
102 </div>
103 </div>
103 </div>
104 <div class="field collapsable-content" data-toggle="description">
104 <div class="field collapsable-content" data-toggle="description">
105 <div class="label-summary">
105 <div class="label-summary">
106 <label>Comments:</label>
106 <label>Comments:</label>
107 </div>
107 </div>
108 <div class="input">
108 <div class="input">
109 <div>
109 <div>
110 <div class="comments-number">
110 <div class="comments-number">
111 <a href="#inline-comments-container">0 Pull request comments</a>,
111 <a href="#inline-comments-container">0 Pull request comments</a>,
112 0 Inline Comments
112 0 Inline Comments
113 </div>
113 </div>
114 </div>
114 </div>
115 </div>
115 </div>
116 </div>
116 </div>
117 </div>
117 </div>
118 </div>
118 </div>
119 <div>
119 <div>
120 <div class="reviewers-title block-right">
120 <div class="reviewers-title block-right">
121 <div class="pr-details-title">
121 <div class="pr-details-title">
122 Author
122 Author
123 </div>
123 </div>
124 </div>
124 </div>
125 <div class="block-right pr-details-content reviewers">
125 <div class="block-right pr-details-content reviewers">
126 <ul class="group_members">
126 <ul class="group_members">
127 <li>
127 <li>
128 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
128 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
129 <span class="user"> <a href="/_profiles/lolek">lolek (Lolek Santos)</a></span>
129 <span class="user"> <a href="/_profiles/lolek">lolek (Lolek Santos)</a></span>
130 </li>
130 </li>
131 </ul>
131 </ul>
132 </div>
132 </div>
133 <div class="reviewers-title block-right">
133 <div class="reviewers-title block-right">
134 <div class="pr-details-title">
134 <div class="pr-details-title">
135 Pull request reviewers
135 Pull request reviewers
136 <span class="btn-collapse" data-toggle="reviewers">Show More</span>
136 <span class="btn-collapse" data-toggle="reviewers">Show More</span>
137 </div>
137 </div>
138
138
139 </div>
139 </div>
140 <div id="reviewers" class="block-right pr-details-content reviewers">
140 <div id="reviewers" class="block-right pr-details-content reviewers">
141
141
142 <ul id="review_members" class="group_members">
142 <ul id="review_members" class="group_members">
143 <li id="reviewer_70">
143 <li id="reviewer_70">
144 <div class="reviewers_member">
144 <div class="reviewers_member">
145 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
145 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
146 <div class="flag_status rejected pull-left reviewer_member_status"></div>
146 <div class="flag_status rejected pull-left reviewer_member_status"></div>
147 </div>
147 </div>
148 <img class="gravatar" src="https://secure.gravatar.com/avatar/153a0fab13160b3e64a2cbc7c0373506?d=identicon&amp;s=32" height="16" width="16">
148 <img class="gravatar" src="https://secure.gravatar.com/avatar/153a0fab13160b3e64a2cbc7c0373506?d=identicon&amp;s=32" height="16" width="16">
149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
150 </div>
150 </div>
151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
152 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(70, true)" style="visibility: hidden;">
152 <div class="reviewer_member_remove action_button" onclick="removeMember(70, true)" style="visibility: hidden;">
153 <i class="icon-remove"></i>
153 <i class="icon-remove"></i>
154 </div>
154 </div>
155 </li>
155 </li>
156 <li id="reviewer_33" class="collapsable-content" data-toggle="reviewers">
156 <li id="reviewer_33" class="collapsable-content" data-toggle="reviewers">
157 <div class="reviewers_member">
157 <div class="reviewers_member">
158 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
158 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
159 <div class="flag_status approved pull-left reviewer_member_status"></div>
159 <div class="flag_status approved pull-left reviewer_member_status"></div>
160 </div>
160 </div>
161 <img class="gravatar" src="https://secure.gravatar.com/avatar/ffd6a317ec2b66be880143cd8459d0d9?d=identicon&amp;s=32" height="16" width="16">
161 <img class="gravatar" src="https://secure.gravatar.com/avatar/ffd6a317ec2b66be880143cd8459d0d9?d=identicon&amp;s=32" height="16" width="16">
162 <span class="user"> <a href="/_profiles/jenkins-tests">garbas (Rok Garbas)</a> (reviewer)</span>
162 <span class="user"> <a href="/_profiles/jenkins-tests">garbas (Rok Garbas)</a> (reviewer)</span>
163 </div>
163 </div>
164 </li>
164 </li>
165 <li id="reviewer_2" class="collapsable-content" data-toggle="reviewers">
165 <li id="reviewer_2" class="collapsable-content" data-toggle="reviewers">
166 <div class="reviewers_member">
166 <div class="reviewers_member">
167 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
167 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
168 <div class="flag_status not_reviewed pull-left reviewer_member_status"></div>
168 <div class="flag_status not_reviewed pull-left reviewer_member_status"></div>
169 </div>
169 </div>
170 <img class="gravatar" src="https://secure.gravatar.com/avatar/aad9d40cac1259ea39b5578554ad9d64?d=identicon&amp;s=32" height="16" width="16">
170 <img class="gravatar" src="https://secure.gravatar.com/avatar/aad9d40cac1259ea39b5578554ad9d64?d=identicon&amp;s=32" height="16" width="16">
171 <span class="user"> <a href="/_profiles/jenkins-tests">marcink (Marcin Kuzminski)</a> (reviewer)</span>
171 <span class="user"> <a href="/_profiles/jenkins-tests">marcink (Marcin Kuzminski)</a> (reviewer)</span>
172 </div>
172 </div>
173 </li>
173 </li>
174 <li id="reviewer_36" class="collapsable-content" data-toggle="reviewers">
174 <li id="reviewer_36" class="collapsable-content" data-toggle="reviewers">
175 <div class="reviewers_member">
175 <div class="reviewers_member">
176 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
176 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
177 <div class="flag_status approved pull-left reviewer_member_status"></div>
177 <div class="flag_status approved pull-left reviewer_member_status"></div>
178 </div>
178 </div>
179 <img class="gravatar" src="https://secure.gravatar.com/avatar/7a4da001a0af0016ed056ab523255db9?d=identicon&amp;s=32" height="16" width="16">
179 <img class="gravatar" src="https://secure.gravatar.com/avatar/7a4da001a0af0016ed056ab523255db9?d=identicon&amp;s=32" height="16" width="16">
180 <span class="user"> <a href="/_profiles/jenkins-tests">johbo (Johannes Bornhold)</a> (reviewer)</span>
180 <span class="user"> <a href="/_profiles/jenkins-tests">johbo (Johannes Bornhold)</a> (reviewer)</span>
181 </div>
181 </div>
182 </li>
182 </li>
183 <li id="reviewer_47" class="collapsable-content" data-toggle="reviewers">
183 <li id="reviewer_47" class="collapsable-content" data-toggle="reviewers">
184 <div class="reviewers_member">
184 <div class="reviewers_member">
185 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
185 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
186 <div class="flag_status under_review pull-left reviewer_member_status"></div>
186 <div class="flag_status under_review pull-left reviewer_member_status"></div>
187 </div>
187 </div>
188 <img class="gravatar" src="https://secure.gravatar.com/avatar/8f6dc00dce79d6bd7d415be5cea6a008?d=identicon&amp;s=32" height="16" width="16">
188 <img class="gravatar" src="https://secure.gravatar.com/avatar/8f6dc00dce79d6bd7d415be5cea6a008?d=identicon&amp;s=32" height="16" width="16">
189 <span class="user"> <a href="/_profiles/jenkins-tests">lisaq (Lisa Quatmann)</a> (reviewer)</span>
189 <span class="user"> <a href="/_profiles/jenkins-tests">lisaq (Lisa Quatmann)</a> (reviewer)</span>
190 </div>
190 </div>
191 </li>
191 </li>
192 <li id="reviewer_49" class="collapsable-content" data-toggle="reviewers">
192 <li id="reviewer_49" class="collapsable-content" data-toggle="reviewers">
193 <div class="reviewers_member">
193 <div class="reviewers_member">
194 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
194 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
195 <div class="flag_status approved pull-left reviewer_member_status"></div>
195 <div class="flag_status approved pull-left reviewer_member_status"></div>
196 </div>
196 </div>
197 <img class="gravatar" src="https://secure.gravatar.com/avatar/89f722927932a8f737a0feafb03a606e?d=identicon&amp;s=32" height="16" width="16">
197 <img class="gravatar" src="https://secure.gravatar.com/avatar/89f722927932a8f737a0feafb03a606e?d=identicon&amp;s=32" height="16" width="16">
198 <span class="user"> <a href="/_profiles/jenkins-tests">paris (Paris Kolios)</a> (reviewer)</span>
198 <span class="user"> <a href="/_profiles/jenkins-tests">paris (Paris Kolios)</a> (reviewer)</span>
199 </div>
199 </div>
200 </li>
200 </li>
201 <li id="reviewer_50" class="collapsable-content" data-toggle="reviewers">
201 <li id="reviewer_50" class="collapsable-content" data-toggle="reviewers">
202 <div class="reviewers_member">
202 <div class="reviewers_member">
203 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
203 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
204 <div class="flag_status approved pull-left reviewer_member_status"></div>
204 <div class="flag_status approved pull-left reviewer_member_status"></div>
205 </div>
205 </div>
206 <img class="gravatar" src="https://secure.gravatar.com/avatar/081322c975e8545ec269372405fbd016?d=identicon&amp;s=32" height="16" width="16">
206 <img class="gravatar" src="https://secure.gravatar.com/avatar/081322c975e8545ec269372405fbd016?d=identicon&amp;s=32" height="16" width="16">
207 <span class="user"> <a href="/_profiles/jenkins-tests">ergo (Marcin Lulek)</a> (reviewer)</span>
207 <span class="user"> <a href="/_profiles/jenkins-tests">ergo (Marcin Lulek)</a> (reviewer)</span>
208 </div>
208 </div>
209 </li>
209 </li>
210 <li id="reviewer_54" class="collapsable-content" data-toggle="reviewers">
210 <li id="reviewer_54" class="collapsable-content" data-toggle="reviewers">
211 <div class="reviewers_member">
211 <div class="reviewers_member">
212 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
212 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
213 <div class="flag_status under_review pull-left reviewer_member_status"></div>
213 <div class="flag_status under_review pull-left reviewer_member_status"></div>
214 </div>
214 </div>
215 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
215 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
216 <span class="user"> <a href="/_profiles/jenkins-tests">anderson (Anderson Santos)</a> (reviewer)</span>
216 <span class="user"> <a href="/_profiles/jenkins-tests">anderson (Anderson Santos)</a> (reviewer)</span>
217 </div>
217 </div>
218 </li>
218 </li>
219 <li id="reviewer_57" class="collapsable-content" data-toggle="reviewers">
219 <li id="reviewer_57" class="collapsable-content" data-toggle="reviewers">
220 <div class="reviewers_member">
220 <div class="reviewers_member">
221 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
221 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
222 <div class="flag_status approved pull-left reviewer_member_status"></div>
222 <div class="flag_status approved pull-left reviewer_member_status"></div>
223 </div>
223 </div>
224 <img class="gravatar" src="https://secure.gravatar.com/avatar/23e2ee8f5fd462cba8129a40cc1e896c?d=identicon&amp;s=32" height="16" width="16">
224 <img class="gravatar" src="https://secure.gravatar.com/avatar/23e2ee8f5fd462cba8129a40cc1e896c?d=identicon&amp;s=32" height="16" width="16">
225 <span class="user"> <a href="/_profiles/jenkins-tests">gmgauthier (Greg Gauthier)</a> (reviewer)</span>
225 <span class="user"> <a href="/_profiles/jenkins-tests">gmgauthier (Greg Gauthier)</a> (reviewer)</span>
226 </div>
226 </div>
227 </li>
227 </li>
228 <li id="reviewer_31" class="collapsable-content" data-toggle="reviewers">
228 <li id="reviewer_31" class="collapsable-content" data-toggle="reviewers">
229 <div class="reviewers_member">
229 <div class="reviewers_member">
230 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
230 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
231 <div class="flag_status under_review pull-left reviewer_member_status"></div>
231 <div class="flag_status under_review pull-left reviewer_member_status"></div>
232 </div>
232 </div>
233 <img class="gravatar" src="https://secure.gravatar.com/avatar/0c9a7e6674b6f0b35d98dbe073e3f0ab?d=identicon&amp;s=32" height="16" width="16">
233 <img class="gravatar" src="https://secure.gravatar.com/avatar/0c9a7e6674b6f0b35d98dbe073e3f0ab?d=identicon&amp;s=32" height="16" width="16">
234 <span class="user"> <a href="/_profiles/jenkins-tests">ostrobel (Oliver Strobel)</a> (reviewer)</span>
234 <span class="user"> <a href="/_profiles/jenkins-tests">ostrobel (Oliver Strobel)</a> (reviewer)</span>
235 </div>
235 </div>
236 </li>
236 </li>
237 </ul>
237 </ul>
238 <div id="add_reviewer_input" class="ac" style="display: none;">
238 <div id="add_reviewer_input" class="ac" style="display: none;">
239 </div>
239 </div>
240 </div>
240 </div>
241 </div>
241 </div>
242 </div>
242 </div>
243 </div>
243 </div>
244 <div class="box">
244 <div class="box">
245 <div class="table" >
245 <div class="table" >
246 <div id="changeset_compare_view_content">
246 <div id="changeset_compare_view_content">
247 <div class="compare_view_commits_title">
247 <div class="compare_view_commits_title">
248 <h2>Compare View: 6 commits<span class="btn-collapse" data-toggle="commits">Show More</span></h2>
248 <h2>Compare View: 6 commits<span class="btn-collapse" data-toggle="commits">Show More</span></h2>
249
249
250 </div>
250 </div>
251 <div class="container">
251 <div class="container">
252
252
253
253
254 <table class="rctable compare_view_commits">
254 <table class="rctable compare_view_commits">
255 <tr>
255 <tr>
256 <th>Time</th>
256 <th>Time</th>
257 <th>Author</th>
257 <th>Author</th>
258 <th>Commit</th>
258 <th>Commit</th>
259 <th></th>
259 <th></th>
260 <th>Title</th>
260 <th>Title</th>
261 </tr>
261 </tr>
262 <tr id="row-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" commit_id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="compare_select">
262 <tr id="row-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" commit_id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="compare_select">
263 <td class="td-time">
263 <td class="td-time">
264 <span class="tooltip" title="3 hours and 23 minutes ago" tt_title="3 hours and 23 minutes ago">2015-02-18 10:13:34</span>
264 <span class="tooltip" title="3 hours and 23 minutes ago" tt_title="3 hours and 23 minutes ago">2015-02-18 10:13:34</span>
265 </td>
265 </td>
266 <td class="td-user">
266 <td class="td-user">
267 <div class="gravatar_with_user">
267 <div class="gravatar_with_user">
268 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
268 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
269 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
269 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
270 </div>
270 </div>
271 </td>
271 </td>
272 <td class="td-hash">
272 <td class="td-hash">
273 <code>
273 <code>
274 <a href="/brian/documentation-rep/changeset/7e83e5cd7812dd9e055ce30e77c65cdc08154b43">r395:7e83e5cd7812</a>
274 <a href="/brian/documentation-rep/changeset/7e83e5cd7812dd9e055ce30e77c65cdc08154b43">r395:7e83e5cd7812</a>
275 </code>
275 </code>
276 </td>
276 </td>
277 <td class="expand_commit" data-commit-id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" title="Expand commit message">
277 <td class="expand_commit" data-commit-id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" title="Expand commit message">
278 <div class="show_more_col">
278 <div class="show_more_col">
279 <i class="show_more"></i>
279 <i class="show_more"></i>
280 </div>
280 </div>
281 </td>
281 </td>
282 <td class="mid td-description">
282 <td class="mid td-description">
283 <div class="log-container truncate-wrap">
283 <div class="log-container truncate-wrap">
284 <div id="c-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="message truncate">rep: added how we doc to guide</div>
284 <div id="c-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="message truncate">rep: added how we doc to guide</div>
285 </div>
285 </div>
286 </td>
286 </td>
287 </tr>
287 </tr>
288 <tr id="row-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" commit_id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="compare_select">
288 <tr id="row-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" commit_id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="compare_select">
289 <td class="td-time">
289 <td class="td-time">
290 <span class="tooltip" title="4 hours and 18 minutes ago">2015-02-18 09:18:31</span>
290 <span class="tooltip" title="4 hours and 18 minutes ago">2015-02-18 09:18:31</span>
291 </td>
291 </td>
292 <td class="td-user">
292 <td class="td-user">
293 <div class="gravatar_with_user">
293 <div class="gravatar_with_user">
294 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
294 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
295 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
295 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
296 </div>
296 </div>
297 </td>
297 </td>
298 <td class="td-hash">
298 <td class="td-hash">
299 <code>
299 <code>
300 <a href="/brian/documentation-rep/changeset/48ce1581bdb3aa7679c246cbdd3fb030623f5c87">r394:48ce1581bdb3</a>
300 <a href="/brian/documentation-rep/changeset/48ce1581bdb3aa7679c246cbdd3fb030623f5c87">r394:48ce1581bdb3</a>
301 </code>
301 </code>
302 </td>
302 </td>
303 <td class="expand_commit" data-commit-id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" title="Expand commit message">
303 <td class="expand_commit" data-commit-id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" title="Expand commit message">
304 <div class="show_more_col">
304 <div class="show_more_col">
305 <i class="show_more"></i>
305 <i class="show_more"></i>
306 </div>
306 </div>
307 </td>
307 </td>
308 <td class="mid td-description">
308 <td class="mid td-description">
309 <div class="log-container truncate-wrap">
309 <div class="log-container truncate-wrap">
310 <div id="c-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="message truncate">repo 0004 - typo</div>
310 <div id="c-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="message truncate">repo 0004 - typo</div>
311 </div>
311 </div>
312 </td>
312 </td>
313 </tr>
313 </tr>
314 <tr id="row-982d857aafb4c71e7686e419c32b71c9a837257d" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d" class="compare_select collapsable-content" data-toggle="commits">
314 <tr id="row-982d857aafb4c71e7686e419c32b71c9a837257d" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d" class="compare_select collapsable-content" data-toggle="commits">
315 <td class="td-time">
315 <td class="td-time">
316 <span class="tooltip" title="4 hours and 22 minutes ago">2015-02-18 09:14:45</span>
316 <span class="tooltip" title="4 hours and 22 minutes ago">2015-02-18 09:14:45</span>
317 </td>
317 </td>
318 <td class="td-user">
318 <td class="td-user">
319 <span class="gravatar" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d">
319 <span class="gravatar" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d">
320 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
320 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
321 </span>
321 </span>
322 <span class="author">brian (Brian Butler)</span>
322 <span class="author">brian (Brian Butler)</span>
323 </td>
323 </td>
324 <td class="td-hash">
324 <td class="td-hash">
325 <code>
325 <code>
326 <a href="/brian/documentation-rep/changeset/982d857aafb4c71e7686e419c32b71c9a837257d">r393:982d857aafb4</a>
326 <a href="/brian/documentation-rep/changeset/982d857aafb4c71e7686e419c32b71c9a837257d">r393:982d857aafb4</a>
327 </code>
327 </code>
328 </td>
328 </td>
329 <td class="expand_commit" data-commit-id="982d857aafb4c71e7686e419c32b71c9a837257d" title="Expand commit message">
329 <td class="expand_commit" data-commit-id="982d857aafb4c71e7686e419c32b71c9a837257d" title="Expand commit message">
330 <div class="show_more_col">
330 <div class="show_more_col">
331 <i class="show_more"></i>
331 <i class="show_more"></i>
332 </div>
332 </div>
333 </td>
333 </td>
334 <td class="mid td-description">
334 <td class="mid td-description">
335 <div class="log-container truncate-wrap">
335 <div class="log-container truncate-wrap">
336 <div id="c-982d857aafb4c71e7686e419c32b71c9a837257d" class="message truncate">internals: how to doc section added</div>
336 <div id="c-982d857aafb4c71e7686e419c32b71c9a837257d" class="message truncate">internals: how to doc section added</div>
337 </div>
337 </div>
338 </td>
338 </td>
339 </tr>
339 </tr>
340 <tr id="row-4c7258ad1af6dae91bbaf87a933e3597e676fab8" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="compare_select collapsable-content" data-toggle="commits">
340 <tr id="row-4c7258ad1af6dae91bbaf87a933e3597e676fab8" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="compare_select collapsable-content" data-toggle="commits">
341 <td class="td-time">
341 <td class="td-time">
342 <span class="tooltip" title="20 hours and 16 minutes ago">2015-02-17 17:20:44</span>
342 <span class="tooltip" title="20 hours and 16 minutes ago">2015-02-17 17:20:44</span>
343 </td>
343 </td>
344 <td class="td-user">
344 <td class="td-user">
345 <span class="gravatar" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8">
345 <span class="gravatar" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8">
346 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
346 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
347 </span>
347 </span>
348 <span class="author">brian (Brian Butler)</span>
348 <span class="author">brian (Brian Butler)</span>
349 </td>
349 </td>
350 <td class="td-hash">
350 <td class="td-hash">
351 <code>
351 <code>
352 <a href="/brian/documentation-rep/changeset/4c7258ad1af6dae91bbaf87a933e3597e676fab8">r392:4c7258ad1af6</a>
352 <a href="/brian/documentation-rep/changeset/4c7258ad1af6dae91bbaf87a933e3597e676fab8">r392:4c7258ad1af6</a>
353 </code>
353 </code>
354 </td>
354 </td>
355 <td class="expand_commit" data-commit-id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" title="Expand commit message">
355 <td class="expand_commit" data-commit-id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" title="Expand commit message">
356 <div class="show_more_col">
356 <div class="show_more_col">
357 <i class="show_more"></i>
357 <i class="show_more"></i>
358 </div>
358 </div>
359 </td>
359 </td>
360 <td class="mid td-description">
360 <td class="mid td-description">
361 <div class="log-container truncate-wrap">
361 <div class="log-container truncate-wrap">
362 <div id="c-4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="message truncate">REP: 0004 Documentation standards</div>
362 <div id="c-4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="message truncate">REP: 0004 Documentation standards</div>
363 </div>
363 </div>
364 </td>
364 </td>
365 </tr>
365 </tr>
366 <tr id="row-46b3d50315f0f2b1f64485ac95af4f384948f9cb" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="compare_select collapsable-content" data-toggle="commits">
366 <tr id="row-46b3d50315f0f2b1f64485ac95af4f384948f9cb" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="compare_select collapsable-content" data-toggle="commits">
367 <td class="td-time">
367 <td class="td-time">
368 <span class="tooltip" title="18 hours and 19 minutes ago">2015-02-17 16:18:49</span>
368 <span class="tooltip" title="18 hours and 19 minutes ago">2015-02-17 16:18:49</span>
369 </td>
369 </td>
370 <td class="td-user">
370 <td class="td-user">
371 <span class="gravatar" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb">
371 <span class="gravatar" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb">
372 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
372 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
373 </span>
373 </span>
374 <span class="author">anderson (Anderson Santos)</span>
374 <span class="author">anderson (Anderson Santos)</span>
375 </td>
375 </td>
376 <td class="td-hash">
376 <td class="td-hash">
377 <code>
377 <code>
378 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/46b3d50315f0f2b1f64485ac95af4f384948f9cb">r8743:46b3d50315f0</a>
378 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/46b3d50315f0f2b1f64485ac95af4f384948f9cb">r8743:46b3d50315f0</a>
379 </code>
379 </code>
380 </td>
380 </td>
381 <td class="expand_commit" data-commit-id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" title="Expand commit message">
381 <td class="expand_commit" data-commit-id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" title="Expand commit message">
382 <div class="show_more_col">
382 <div class="show_more_col">
383 <i class="show_more" ></i>
383 <i class="show_more" ></i>
384 </div>
384 </div>
385 </td>
385 </td>
386 <td class="mid td-description">
386 <td class="mid td-description">
387 <div class="log-container truncate-wrap">
387 <div class="log-container truncate-wrap">
388 <div id="c-46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="message truncate">Diff: created tests for the diff with filenames with spaces</div>
388 <div id="c-46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="message truncate">Diff: created tests for the diff with filenames with spaces</div>
389
389
390 </div>
390 </div>
391 </td>
391 </td>
392 </tr>
392 </tr>
393 <tr id="row-1e57d2549bd6c34798075bf05ac39f708bb33b90" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90" class="compare_select collapsable-content" data-toggle="commits">
393 <tr id="row-1e57d2549bd6c34798075bf05ac39f708bb33b90" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90" class="compare_select collapsable-content" data-toggle="commits">
394 <td class="td-time">
394 <td class="td-time">
395 <span class="tooltip" title="2 days ago">2015-02-16 10:06:08</span>
395 <span class="tooltip" title="2 days ago">2015-02-16 10:06:08</span>
396 </td>
396 </td>
397 <td class="td-user">
397 <td class="td-user">
398 <span class="gravatar" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90">
398 <span class="gravatar" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90">
399 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
399 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
400 </span>
400 </span>
401 <span class="author">anderson (Anderson Santos)</span>
401 <span class="author">anderson (Anderson Santos)</span>
402 </td>
402 </td>
403 <td class="td-hash">
403 <td class="td-hash">
404 <code>
404 <code>
405 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/1e57d2549bd6c34798075bf05ac39f708bb33b90">r8742:1e57d2549bd6</a>
405 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/1e57d2549bd6c34798075bf05ac39f708bb33b90">r8742:1e57d2549bd6</a>
406 </code>
406 </code>
407 </td>
407 </td>
408 <td class="expand_commit" data-commit-id="1e57d2549bd6c34798075bf05ac39f708bb33b90" title="Expand commit message">
408 <td class="expand_commit" data-commit-id="1e57d2549bd6c34798075bf05ac39f708bb33b90" title="Expand commit message">
409 <div class="show_more_col">
409 <div class="show_more_col">
410 <i class="show_more" ></i>
410 <i class="show_more" ></i>
411 </div>
411 </div>
412 </td>
412 </td>
413 <td class="mid td-description">
413 <td class="mid td-description">
414 <div class="log-container truncate-wrap">
414 <div class="log-container truncate-wrap">
415 <div id="c-1e57d2549bd6c34798075bf05ac39f708bb33b90" class="message truncate">Diff: fix renaming files with spaces <a class="issue-tracker-link" href="http://bugs.rhodecode.com/issues/574">#574</a></div>
415 <div id="c-1e57d2549bd6c34798075bf05ac39f708bb33b90" class="message truncate">Diff: fix renaming files with spaces <a class="issue-tracker-link" href="http://bugs.rhodecode.com/issues/574">#574</a></div>
416
416
417 </div>
417 </div>
418 </td>
418 </td>
419 </tr>
419 </tr>
420 </table>
420 </table>
421 </div>
421 </div>
422
422
423 <script>
423 <script>
424 $('.expand_commit').on('click',function(e){
424 $('.expand_commit').on('click',function(e){
425 $(this).children('i').hide();
425 $(this).children('i').hide();
426 var cid = $(this).data('commitId');
426 var cid = $(this).data('commitId');
427 $('#c-'+cid).css({'height': 'auto', 'margin': '.65em 1em .65em 0','white-space': 'pre-line', 'text-overflow': 'initial', 'overflow':'visible'})
427 $('#c-'+cid).css({'height': 'auto', 'margin': '.65em 1em .65em 0','white-space': 'pre-line', 'text-overflow': 'initial', 'overflow':'visible'})
428 $('#t-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible', 'white-space':'normal'})
428 $('#t-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible', 'white-space':'normal'})
429 });
429 });
430 $('.compare_select').on('click',function(e){
430 $('.compare_select').on('click',function(e){
431 var cid = $(this).attr('commit_id');
431 var cid = $(this).attr('commit_id');
432 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
432 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
433 });
433 });
434 </script>
434 </script>
435 <div class="cs_files_title">
435 <div class="cs_files_title">
436 <span class="cs_files_expand">
436 <span class="cs_files_expand">
437 <span id="expand_all_files">Expand All</span> | <span id="collapse_all_files">Collapse All</span>
437 <span id="expand_all_files">Expand All</span> | <span id="collapse_all_files">Collapse All</span>
438 </span>
438 </span>
439 <h2>
439 <h2>
440 7 files changed: 55 inserted, 9 deleted
440 7 files changed: 55 inserted, 9 deleted
441 </h2>
441 </h2>
442 </div>
442 </div>
443 <div class="cs_files">
443 <div class="cs_files">
444 <table class="compare_view_files">
444 <table class="compare_view_files">
445
445
446 <tr class="cs_A expand_file" fid="c--efbe5b7a3f13">
446 <tr class="cs_A expand_file" fid="c--efbe5b7a3f13">
447 <td class="cs_icon_td">
447 <td class="cs_icon_td">
448 <span class="expand_file_icon" fid="c--efbe5b7a3f13"></span>
448 <span class="expand_file_icon" fid="c--efbe5b7a3f13"></span>
449 </td>
449 </td>
450 <td class="cs_icon_td">
450 <td class="cs_icon_td">
451 <div class="flag_status not_reviewed hidden"></div>
451 <div class="flag_status not_reviewed hidden"></div>
452 </td>
452 </td>
453 <td id="a_c--efbe5b7a3f13">
453 <td id="a_c--efbe5b7a3f13">
454 <a class="compare_view_filepath" href="#a_c--efbe5b7a3f13">
454 <a class="compare_view_filepath" href="#a_c--efbe5b7a3f13">
455 rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff
455 rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff
456 </a>
456 </a>
457 <span id="diff_c--efbe5b7a3f13" class="diff_links" style="display: none;">
457 <span id="diff_c--efbe5b7a3f13" class="diff_links" style="display: none;">
458 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
458 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
459 Unified Diff
459 Unified Diff
460 </a>
460 </a>
461 |
461 |
462 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
462 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
463 Side-by-side Diff
463 Side-by-side Diff
464 </a>
464 </a>
465 </span>
465 </span>
466 </td>
466 </td>
467 <td>
467 <td>
468 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">4</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
468 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">4</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
469 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff">
469 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff">
470 <i class="icon-comment"></i>
470 <i class="icon-comment"></i>
471 </div>
471 </div>
472 </td>
472 </td>
473 </tr>
473 </tr>
474 <tr id="tr_c--efbe5b7a3f13">
474 <tr id="tr_c--efbe5b7a3f13">
475 <td></td>
475 <td></td>
476 <td></td>
476 <td></td>
477 <td class="injected_diff" colspan="2">
477 <td class="injected_diff" colspan="2">
478
478
479 <div class="diff-container" id="diff-container-140716195039928">
479 <div class="diff-container" id="diff-container-140716195039928">
480 <div id="c--efbe5b7a3f13_target" ></div>
480 <div id="c--efbe5b7a3f13_target" ></div>
481 <div id="c--efbe5b7a3f13" class="diffblock margined comm" >
481 <div id="c--efbe5b7a3f13" class="diffblock margined comm" >
482 <div class="code-body">
482 <div class="code-body">
483 <div class="full_f_path" path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff" style="display: none;"></div>
483 <div class="full_f_path" path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff" style="display: none;"></div>
484 <table class="code-difftable">
484 <table class="code-difftable">
485 <tr class="line context">
485 <tr class="line context">
486 <td class="add-comment-line"><span class="add-comment-content"></span></td>
486 <td class="add-comment-line"><span class="add-comment-content"></span></td>
487 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
487 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
488 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n"></a></td>
488 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n"></a></td>
489 <td class="code no-comment">
489 <td class="code no-comment">
490 <pre>new file 100644</pre>
490 <pre>new file 100644</pre>
491 </td>
491 </td>
492 </tr>
492 </tr>
493 <tr class="line add">
493 <tr class="line add">
494 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
494 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
495 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
495 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
496 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1">1</a></td>
496 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1">1</a></td>
497 <td class="code">
497 <td class="code">
498 <pre>diff --git a/file_with_ spaces.txt b/file_with_ two spaces.txt
498 <pre>diff --git a/file_with_ spaces.txt b/file_with_ two spaces.txt
499 </pre>
499 </pre>
500 </td>
500 </td>
501 </tr>
501 </tr>
502 <tr class="line add">
502 <tr class="line add">
503 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
503 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
504 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
504 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
505 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2">2</a></td>
505 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2">2</a></td>
506 <td class="code">
506 <td class="code">
507 <pre>similarity index 100%
507 <pre>similarity index 100%
508 </pre>
508 </pre>
509 </td>
509 </td>
510 </tr>
510 </tr>
511 <tr class="line add">
511 <tr class="line add">
512 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
512 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
513 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
513 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
514 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3">3</a></td>
514 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3">3</a></td>
515 <td class="code">
515 <td class="code">
516 <pre>rename from file_with_ spaces.txt
516 <pre>rename from file_with_ spaces.txt
517 </pre>
517 </pre>
518 </td>
518 </td>
519 </tr>
519 </tr>
520 <tr class="line add">
520 <tr class="line add">
521 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
521 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
522 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
522 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
523 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4">4</a></td>
523 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4">4</a></td>
524 <td class="code">
524 <td class="code">
525 <pre>rename to file_with_ two spaces.txt
525 <pre>rename to file_with_ two spaces.txt
526 </pre>
526 </pre>
527 </td>
527 </td>
528 </tr>
528 </tr>
529 <tr class="line context">
529 <tr class="line context">
530 <td class="add-comment-line"><span class="add-comment-content"></span></td>
530 <td class="add-comment-line"><span class="add-comment-content"></span></td>
531 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o...">...</a></td>
531 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o...">...</a></td>
532 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n...">...</a></td>
532 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n...">...</a></td>
533 <td class="code no-comment">
533 <td class="code no-comment">
534 <pre> No newline at end of file</pre>
534 <pre> No newline at end of file</pre>
535 </td>
535 </td>
536 </tr>
536 </tr>
537 </table>
537 </table>
538 </div>
538 </div>
539 </div>
539 </div>
540 </div>
540 </div>
541
541
542 </td>
542 </td>
543 </tr>
543 </tr>
544 <tr class="cs_A expand_file" fid="c--c21377f778f9">
544 <tr class="cs_A expand_file" fid="c--c21377f778f9">
545 <td class="cs_icon_td">
545 <td class="cs_icon_td">
546 <span class="expand_file_icon" fid="c--c21377f778f9"></span>
546 <span class="expand_file_icon" fid="c--c21377f778f9"></span>
547 </td>
547 </td>
548 <td class="cs_icon_td">
548 <td class="cs_icon_td">
549 <div class="flag_status not_reviewed hidden"></div>
549 <div class="flag_status not_reviewed hidden"></div>
550 </td>
550 </td>
551 <td id="a_c--c21377f778f9">
551 <td id="a_c--c21377f778f9">
552 <a class="compare_view_filepath" href="#a_c--c21377f778f9">
552 <a class="compare_view_filepath" href="#a_c--c21377f778f9">
553 rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff
553 rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff
554 </a>
554 </a>
555 <span id="diff_c--c21377f778f9" class="diff_links" style="display: none;">
555 <span id="diff_c--c21377f778f9" class="diff_links" style="display: none;">
556 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
556 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
557 Unified Diff
557 Unified Diff
558 </a>
558 </a>
559 |
559 |
560 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
560 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
561 Side-by-side Diff
561 Side-by-side Diff
562 </a>
562 </a>
563 </span>
563 </span>
564 </td>
564 </td>
565 <td>
565 <td>
566 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
566 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
567 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff">
567 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff">
568 <i class="icon-comment"></i>
568 <i class="icon-comment"></i>
569 </div>
569 </div>
570 </td>
570 </td>
571 </tr>
571 </tr>
572 <tr id="tr_c--c21377f778f9">
572 <tr id="tr_c--c21377f778f9">
573 <td></td>
573 <td></td>
574 <td></td>
574 <td></td>
575 <td class="injected_diff" colspan="2">
575 <td class="injected_diff" colspan="2">
576
576
577 <div class="diff-container" id="diff-container-140716195038344">
577 <div class="diff-container" id="diff-container-140716195038344">
578 <div id="c--c21377f778f9_target" ></div>
578 <div id="c--c21377f778f9_target" ></div>
579 <div id="c--c21377f778f9" class="diffblock margined comm" >
579 <div id="c--c21377f778f9" class="diffblock margined comm" >
580 <div class="code-body">
580 <div class="code-body">
581 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff" style="display: none;"></div>
581 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff" style="display: none;"></div>
582 <table class="code-difftable">
582 <table class="code-difftable">
583 <tr class="line context">
583 <tr class="line context">
584 <td class="add-comment-line"><span class="add-comment-content"></span></td>
584 <td class="add-comment-line"><span class="add-comment-content"></span></td>
585 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
585 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
586 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n"></a></td>
586 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n"></a></td>
587 <td class="code no-comment">
587 <td class="code no-comment">
588 <pre>new file 100644</pre>
588 <pre>new file 100644</pre>
589 </td>
589 </td>
590 </tr>
590 </tr>
591 <tr class="line add">
591 <tr class="line add">
592 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
592 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
593 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
593 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
594 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1">1</a></td>
594 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1">1</a></td>
595 <td class="code">
595 <td class="code">
596 <pre>diff --git a/file_changed_without_spaces.txt b/file_copied_ with spaces.txt
596 <pre>diff --git a/file_changed_without_spaces.txt b/file_copied_ with spaces.txt
597 </pre>
597 </pre>
598 </td>
598 </td>
599 </tr>
599 </tr>
600 <tr class="line add">
600 <tr class="line add">
601 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
601 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
602 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
602 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
603 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2">2</a></td>
603 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2">2</a></td>
604 <td class="code">
604 <td class="code">
605 <pre>copy from file_changed_without_spaces.txt
605 <pre>copy from file_changed_without_spaces.txt
606 </pre>
606 </pre>
607 </td>
607 </td>
608 </tr>
608 </tr>
609 <tr class="line add">
609 <tr class="line add">
610 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
610 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
611 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
611 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
612 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3">3</a></td>
612 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3">3</a></td>
613 <td class="code">
613 <td class="code">
614 <pre>copy to file_copied_ with spaces.txt
614 <pre>copy to file_copied_ with spaces.txt
615 </pre>
615 </pre>
616 </td>
616 </td>
617 </tr>
617 </tr>
618 <tr class="line context">
618 <tr class="line context">
619 <td class="add-comment-line"><span class="add-comment-content"></span></td>
619 <td class="add-comment-line"><span class="add-comment-content"></span></td>
620 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o...">...</a></td>
620 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o...">...</a></td>
621 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n...">...</a></td>
621 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n...">...</a></td>
622 <td class="code no-comment">
622 <td class="code no-comment">
623 <pre> No newline at end of file</pre>
623 <pre> No newline at end of file</pre>
624 </td>
624 </td>
625 </tr>
625 </tr>
626 </table>
626 </table>
627 </div>
627 </div>
628 </div>
628 </div>
629 </div>
629 </div>
630
630
631 </td>
631 </td>
632 </tr>
632 </tr>
633 <tr class="cs_A expand_file" fid="c--ee62085ad7a8">
633 <tr class="cs_A expand_file" fid="c--ee62085ad7a8">
634 <td class="cs_icon_td">
634 <td class="cs_icon_td">
635 <span class="expand_file_icon" fid="c--ee62085ad7a8"></span>
635 <span class="expand_file_icon" fid="c--ee62085ad7a8"></span>
636 </td>
636 </td>
637 <td class="cs_icon_td">
637 <td class="cs_icon_td">
638 <div class="flag_status not_reviewed hidden"></div>
638 <div class="flag_status not_reviewed hidden"></div>
639 </td>
639 </td>
640 <td id="a_c--ee62085ad7a8">
640 <td id="a_c--ee62085ad7a8">
641 <a class="compare_view_filepath" href="#a_c--ee62085ad7a8">
641 <a class="compare_view_filepath" href="#a_c--ee62085ad7a8">
642 rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff
642 rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff
643 </a>
643 </a>
644 <span id="diff_c--ee62085ad7a8" class="diff_links" style="display: none;">
644 <span id="diff_c--ee62085ad7a8" class="diff_links" style="display: none;">
645 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
645 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
646 Unified Diff
646 Unified Diff
647 </a>
647 </a>
648 |
648 |
649 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
649 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
650 Side-by-side Diff
650 Side-by-side Diff
651 </a>
651 </a>
652 </span>
652 </span>
653 </td>
653 </td>
654 <td>
654 <td>
655 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
655 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
656 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff">
656 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff">
657 <i class="icon-comment"></i>
657 <i class="icon-comment"></i>
658 </div>
658 </div>
659 </td>
659 </td>
660 </tr>
660 </tr>
661 <tr id="tr_c--ee62085ad7a8">
661 <tr id="tr_c--ee62085ad7a8">
662 <td></td>
662 <td></td>
663 <td></td>
663 <td></td>
664 <td class="injected_diff" colspan="2">
664 <td class="injected_diff" colspan="2">
665
665
666 <div class="diff-container" id="diff-container-140716195039496">
666 <div class="diff-container" id="diff-container-140716195039496">
667 <div id="c--ee62085ad7a8_target" ></div>
667 <div id="c--ee62085ad7a8_target" ></div>
668 <div id="c--ee62085ad7a8" class="diffblock margined comm" >
668 <div id="c--ee62085ad7a8" class="diffblock margined comm" >
669 <div class="code-body">
669 <div class="code-body">
670 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff" style="display: none;"></div>
670 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff" style="display: none;"></div>
671 <table class="code-difftable">
671 <table class="code-difftable">
672 <tr class="line context">
672 <tr class="line context">
673 <td class="add-comment-line"><span class="add-comment-content"></span></td>
673 <td class="add-comment-line"><span class="add-comment-content"></span></td>
674 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
674 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
675 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n"></a></td>
675 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n"></a></td>
676 <td class="code no-comment">
676 <td class="code no-comment">
677 <pre>new file 100644</pre>
677 <pre>new file 100644</pre>
678 </td>
678 </td>
679 </tr>
679 </tr>
680 <tr class="line add">
680 <tr class="line add">
681 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
681 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
682 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
682 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
683 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1">1</a></td>
683 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1">1</a></td>
684 <td class="code">
684 <td class="code">
685 <pre>diff --git a/file_ with update.txt b/file_changed _.txt
685 <pre>diff --git a/file_ with update.txt b/file_changed _.txt
686 </pre>
686 </pre>
687 </td>
687 </td>
688 </tr>
688 </tr>
689 <tr class="line add">
689 <tr class="line add">
690 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
690 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
691 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
691 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
692 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2">2</a></td>
692 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2">2</a></td>
693 <td class="code">
693 <td class="code">
694 <pre>rename from file_ with update.txt
694 <pre>rename from file_ with update.txt
695 </pre>
695 </pre>
696 </td>
696 </td>
697 </tr>
697 </tr>
698 <tr class="line add">
698 <tr class="line add">
699 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
699 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
700 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
700 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
701 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3">3</a></td>
701 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3">3</a></td>
702 <td class="code">
702 <td class="code">
703 <pre>rename to file_changed _.txt</pre>
703 <pre>rename to file_changed _.txt</pre>
704 </td>
704 </td>
705 </tr>
705 </tr>
706 </table>
706 </table>
707 </div>
707 </div>
708 </div>
708 </div>
709 </div>
709 </div>
710
710
711 </td>
711 </td>
712 </tr>
712 </tr>
713
713
714 </table>
714 </table>
715 </div>
715 </div>
716 </div>
716 </div>
717 </div>
717 </div>
718
718
719 </td>
719 </td>
720 </tr>
720 </tr>
721 </table>
721 </table>
722 </div>
722 </div>
723 </div>
723 </div>
724 </div>
724 </div>
725
725
726
726
727
727
728
728
729 <div id="comment-inline-form-template" style="display: none;">
729 <div id="comment-inline-form-template" style="display: none;">
730 <div class="comment-inline-form ac">
730 <div class="comment-inline-form ac">
731 <div class="overlay"><div class="overlay-text">Submitting...</div></div>
731 <div class="overlay"><div class="overlay-text">Submitting...</div></div>
732 <form action="#" class="inline-form" method="get">
732 <form action="#" class="inline-form" method="get">
733 <div id="edit-container_{1}" class="clearfix">
733 <div id="edit-container_{1}" class="clearfix">
734 <div class="comment-title pull-left">
734 <div class="comment-title pull-left">
735 Commenting on line {1}.
735 Commenting on line {1}.
736 </div>
736 </div>
737 <div class="comment-help pull-right">
737 <div class="comment-help pull-right">
738 Comments parsed using <a href="http://docutils.sourceforge.io/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
738 Comments parsed using <a href="http://docutils.sourceforge.io/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
739 </div>
739 </div>
740 <div style="clear: both"></div>
740 <div style="clear: both"></div>
741 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
741 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
742 </div>
742 </div>
743 <div id="preview-container_{1}" class="clearfix" style="display: none;">
743 <div id="preview-container_{1}" class="clearfix" style="display: none;">
744 <div class="comment-help">
744 <div class="comment-help">
745 Comment preview
745 Comment preview
746 </div>
746 </div>
747 <div id="preview-box_{1}" class="preview-box"></div>
747 <div id="preview-box_{1}" class="preview-box"></div>
748 </div>
748 </div>
749 <div class="comment-button pull-right">
749 <div class="comment-button pull-right">
750 <input type="hidden" name="f_path" value="{0}">
750 <input type="hidden" name="f_path" value="{0}">
751 <input type="hidden" name="line" value="{1}">
751 <input type="hidden" name="line" value="{1}">
752 <div id="preview-btn_{1}" class="btn btn-default">Preview</div>
752 <div id="preview-btn_{1}" class="btn btn-default">Preview</div>
753 <div id="edit-btn_{1}" class="btn" style="display: none;">Edit</div>
753 <div id="edit-btn_{1}" class="btn" style="display: none;">Edit</div>
754 <input class="btn btn-success save-inline-form" id="save" name="save" type="submit" value="Comment" />
754 <input class="btn btn-success save-inline-form" id="save" name="save" type="submit" value="Comment" />
755 </div>
755 </div>
756 <div class="comment-button hide-inline-form-button">
756 <div class="comment-button hide-inline-form-button">
757 <input class="btn hide-inline-form" id="hide-inline-form" name="hide-inline-form" type="reset" value="Cancel" />
757 <input class="btn hide-inline-form" id="hide-inline-form" name="hide-inline-form" type="reset" value="Cancel" />
758 </div>
758 </div>
759 </form>
759 </form>
760 </div>
760 </div>
761 </div>
761 </div>
762
762
763
763
764
764
765 <div class="comments">
765 <div class="comments">
766 <div id="inline-comments-container">
766 <div id="inline-comments-container">
767
767
768 <h2>0 Pull Request Comments</h2>
768 <h2>0 Pull Request Comments</h2>
769
769
770
770
771 </div>
771 </div>
772
772
773 </div>
773 </div>
774
774
775
775
776
776
777
777
778 <div class="pull-request-merge">
778 <div class="pull-request-merge">
779 </div>
779 </div>
780 <div class="comments">
780 <div class="comments">
781 <div class="comment-form ac">
781 <div class="comment-form ac">
782 <form action="/rhodecode-momentum/pull-request-comment/720" id="comments_form" method="POST">
782 <form action="/rhodecode-momentum/pull-request-comment/720" id="comments_form" method="POST">
783 <div style="display: none;"><input id="csrf_token" name="csrf_token" type="hidden" value="6dbc0b19ac65237df65d57202a3e1f2df4153e38" /></div>
783 <div style="display: none;"><input id="csrf_token" name="csrf_token" type="hidden" value="6dbc0b19ac65237df65d57202a3e1f2df4153e38" /></div>
784 <div id="edit-container" class="clearfix">
784 <div id="edit-container" class="clearfix">
785 <div class="comment-title pull-left">
785 <div class="comment-title pull-left">
786 Create a comment on this Pull Request.
786 Create a comment on this Pull Request.
787 </div>
787 </div>
788 <div class="comment-help pull-right">
788 <div class="comment-help pull-right">
789 Comments parsed using <a href="http://docutils.sourceforge.io/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
789 Comments parsed using <a href="http://docutils.sourceforge.io/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
790 </div>
790 </div>
791 <div style="clear: both"></div>
791 <div style="clear: both"></div>
792 <textarea class="comment-block-ta" id="text" name="text"></textarea>
792 <textarea class="comment-block-ta" id="text" name="text"></textarea>
793 </div>
793 </div>
794
794
795 <div id="preview-container" class="clearfix" style="display: none;">
795 <div id="preview-container" class="clearfix" style="display: none;">
796 <div class="comment-title">
796 <div class="comment-title">
797 Comment preview
797 Comment preview
798 </div>
798 </div>
799 <div id="preview-box" class="preview-box"></div>
799 <div id="preview-box" class="preview-box"></div>
800 </div>
800 </div>
801
801
802 <div id="comment_form_extras">
802 <div id="comment_form_extras">
803 </div>
803 </div>
804 <div class="action-button pull-right">
804 <div class="action-button pull-right">
805 <div id="preview-btn" class="btn">
805 <div id="preview-btn" class="btn">
806 Preview
806 Preview
807 </div>
807 </div>
808 <div id="edit-btn" class="btn" style="display: none;">
808 <div id="edit-btn" class="btn" style="display: none;">
809 Edit
809 Edit
810 </div>
810 </div>
811 <div class="comment-button">
811 <div class="comment-button">
812 <input class="btn btn-small btn-success comment-button-input" id="save" name="save" type="submit" value="Comment" />
812 <input class="btn btn-small btn-success comment-button-input" id="save" name="save" type="submit" value="Comment" />
813 </div>
813 </div>
814 </div>
814 </div>
815 </form>
815 </form>
816 </div>
816 </div>
817 </div>
817 </div>
818 <script>
818 <script>
819
819
820 $(document).ready(function() {
820 $(document).ready(function() {
821
821
822 var cm = initCommentBoxCodeMirror('#text');
822 var cm = initCommentBoxCodeMirror('#text');
823
823
824 // main form preview
824 // main form preview
825 $('#preview-btn').on('click', function(e) {
825 $('#preview-btn').on('click', function(e) {
826 $('#preview-btn').hide();
826 $('#preview-btn').hide();
827 $('#edit-btn').show();
827 $('#edit-btn').show();
828 var _text = cm.getValue();
828 var _text = cm.getValue();
829 if (!_text) {
829 if (!_text) {
830 return;
830 return;
831 }
831 }
832 var post_data = {
832 var post_data = {
833 'text': _text,
833 'text': _text,
834 'renderer': DEFAULT_RENDERER,
834 'renderer': DEFAULT_RENDERER,
835 'csrf_token': CSRF_TOKEN
835 'csrf_token': CSRF_TOKEN
836 };
836 };
837 var previewbox = $('#preview-box');
837 var previewbox = $('#preview-box');
838 previewbox.addClass('unloaded');
838 previewbox.addClass('unloaded');
839 previewbox.html(_gettext('Loading ...'));
839 previewbox.html(_gettext('Loading ...'));
840 $('#edit-container').hide();
840 $('#edit-container').hide();
841 $('#preview-container').show();
841 $('#preview-container').show();
842
842
843 var url = pyroutes.url('repo_commit_comment_preview',
843 var url = pyroutes.url('repo_commit_comment_preview',
844 {'repo_name': 'rhodecode-momentum', 'commit_id': '000000'});
844 {'repo_name': 'rhodecode-momentum', 'commit_id': '000000'});
845
845
846 ajaxPOST(url, post_data, function(o) {
846 ajaxPOST(url, post_data, function(o) {
847 previewbox.html(o);
847 previewbox.html(o);
848 previewbox.removeClass('unloaded');
848 previewbox.removeClass('unloaded');
849 });
849 });
850 });
850 });
851 $('#edit-btn').on('click', function(e) {
851 $('#edit-btn').on('click', function(e) {
852 $('#preview-btn').show();
852 $('#preview-btn').show();
853 $('#edit-btn').hide();
853 $('#edit-btn').hide();
854 $('#edit-container').show();
854 $('#edit-container').show();
855 $('#preview-container').hide();
855 $('#preview-container').hide();
856 });
856 });
857
857
858 var formatChangeStatus = function(state, escapeMarkup) {
858 var formatChangeStatus = function(state, escapeMarkup) {
859 var originalOption = state.element;
859 var originalOption = state.element;
860 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
860 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
861 '<span>' + escapeMarkup(state.text) + '</span>';
861 '<span>' + escapeMarkup(state.text) + '</span>';
862 };
862 };
863
863
864 var formatResult = function(result, container, query, escapeMarkup) {
864 var formatResult = function(result, container, query, escapeMarkup) {
865 return formatChangeStatus(result, escapeMarkup);
865 return formatChangeStatus(result, escapeMarkup);
866 };
866 };
867
867
868 var formatSelection = function(data, container, escapeMarkup) {
868 var formatSelection = function(data, container, escapeMarkup) {
869 return formatChangeStatus(data, escapeMarkup);
869 return formatChangeStatus(data, escapeMarkup);
870 };
870 };
871
871
872 $('#change_status_general').select2({
872 $('#change_status_general').select2({
873 placeholder: "Status Review",
873 placeholder: "Status Review",
874 formatResult: formatResult,
874 formatResult: formatResult,
875 formatSelection: formatSelection,
875 formatSelection: formatSelection,
876 containerCssClass: "drop-menu status_box_menu",
876 containerCssClass: "drop-menu status_box_menu",
877 dropdownCssClass: "drop-menu-dropdown",
877 dropdownCssClass: "drop-menu-dropdown",
878 dropdownAutoWidth: true,
878 dropdownAutoWidth: true,
879 minimumResultsForSearch: -1
879 minimumResultsForSearch: -1
880 });
880 });
881 });
881 });
882 </script>
882 </script>
883
883
884
884
885 <script type="text/javascript">
885 <script type="text/javascript">
886 // TODO: switch this to pyroutes
886 // TODO: switch this to pyroutes
887 AJAX_COMMENT_DELETE_URL = "/rhodecode-momentum/pull-request-comment/__COMMENT_ID__/delete";
887 AJAX_COMMENT_DELETE_URL = "/rhodecode-momentum/pull-request-comment/__COMMENT_ID__/delete";
888
888
889 $(function(){
889 $(function(){
890 ReviewerAutoComplete('#user');
890 ReviewerAutoComplete('#user');
891
891
892 $('#open_edit_reviewers').on('click', function(e){
892 $('#open_edit_reviewers').on('click', function(e){
893 $('#open_edit_reviewers').hide();
893 $('#open_edit_reviewers').hide();
894 $('#close_edit_reviewers').show();
894 $('#close_edit_reviewers').show();
895 $('#add_reviewer_input').show();
895 $('#add_reviewer_input').show();
896 $('.reviewer_member_remove').css('visibility', 'visible');
896 $('.reviewer_member_remove').css('visibility', 'visible');
897 });
897 });
898
898
899 $('#close_edit_reviewers').on('click', function(e){
899 $('#close_edit_reviewers').on('click', function(e){
900 $('#open_edit_reviewers').show();
900 $('#open_edit_reviewers').show();
901 $('#close_edit_reviewers').hide();
901 $('#close_edit_reviewers').hide();
902 $('#add_reviewer_input').hide();
902 $('#add_reviewer_input').hide();
903 $('.reviewer_member_remove').css('visibility', 'hidden');
903 $('.reviewer_member_remove').css('visibility', 'hidden');
904 });
904 });
905
905
906 $('.show-inline-comments').on('change', function(e){
906 $('.show-inline-comments').on('change', function(e){
907 var show = 'none';
907 var show = 'none';
908 var target = e.currentTarget;
908 var target = e.currentTarget;
909 if(target.checked){
909 if(target.checked){
910 show = ''
910 show = ''
911 }
911 }
912 var boxid = $(target).attr('id_for');
912 var boxid = $(target).attr('id_for');
913 var comments = $('#{0} .inline-comments'.format(boxid));
913 var comments = $('#{0} .inline-comments'.format(boxid));
914 var fn_display = function(idx){
914 var fn_display = function(idx){
915 $(this).css('display', show);
915 $(this).css('display', show);
916 };
916 };
917 $(comments).each(fn_display);
917 $(comments).each(fn_display);
918 var btns = $('#{0} .inline-comments-button'.format(boxid));
918 var btns = $('#{0} .inline-comments-button'.format(boxid));
919 $(btns).each(fn_display);
919 $(btns).each(fn_display);
920 });
920 });
921
921
922 var commentTotals = {};
922 var commentTotals = {};
923 $.each(file_comments, function(i, comment) {
923 $.each(file_comments, function(i, comment) {
924 var path = $(comment).attr('path');
924 var path = $(comment).attr('path');
925 var comms = $(comment).children().length;
925 var comms = $(comment).children().length;
926 if (path in commentTotals) {
926 if (path in commentTotals) {
927 commentTotals[path] += comms;
927 commentTotals[path] += comms;
928 } else {
928 } else {
929 commentTotals[path] = comms;
929 commentTotals[path] = comms;
930 }
930 }
931 });
931 });
932 $.each(commentTotals, function(path, total) {
932 $.each(commentTotals, function(path, total) {
933 var elem = $('.comment-bubble[data-path="'+ path +'"]')
933 var elem = $('.comment-bubble[data-path="'+ path +'"]')
934 elem.css('visibility', 'visible');
934 elem.css('visibility', 'visible');
935 elem.html(elem.html() + ' ' + total );
935 elem.html(elem.html() + ' ' + total );
936 });
936 });
937
937
938 $('#merge_pull_request_form').submit(function() {
938 $('#merge_pull_request_form').submit(function() {
939 if (!$('#merge_pull_request').attr('disabled')) {
939 if (!$('#merge_pull_request').attr('disabled')) {
940 $('#merge_pull_request').attr('disabled', 'disabled');
940 $('#merge_pull_request').attr('disabled', 'disabled');
941 }
941 }
942 return true;
942 return true;
943 });
943 });
944
944
945 $('#update_pull_request').on('click', function(e){
945 $('#update_pull_request').on('click', function(e){
946 updateReviewers(undefined, "rhodecode-momentum", "720");
946 updateReviewers(undefined, "rhodecode-momentum", "720");
947 });
947 });
948
948
949 $('#update_commits').on('click', function(e){
949 $('#update_commits').on('click', function(e){
950 updateCommits("rhodecode-momentum", "720");
950 updateCommits("rhodecode-momentum", "720");
951 });
951 });
952
952
953 })
953 })
954 </script>
954 </script>
955
955
956 </div>
956 </div>
957 </div></div>
957 </div></div>
958
958
959 </div>
959 </div>
960
960
961
961
962 </%def>
962 </%def>
@@ -1,242 +1,249 b''
1 <%text>
1 <%text>
2 <div style="display: none">
2 <div style="display: none">
3
3
4 <script>
4 <script>
5 var CG = new ColorGenerator();
5 var CG = new ColorGenerator();
6 </script>
6 </script>
7
7
8 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
8 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
9
9
10 <%
10 <%
11 if (size > 16) {
11 if (size > 16) {
12 var gravatar_class = 'gravatar gravatar-large';
12 var gravatar_class = 'gravatar gravatar-large';
13 } else {
13 } else {
14 var gravatar_class = 'gravatar';
14 var gravatar_class = 'gravatar';
15 }
15 }
16
16
17 if (tooltip) {
17 if (tooltip) {
18 var gravatar_class = gravatar_class + ' tooltip-hovercard';
18 var gravatar_class = gravatar_class + ' tooltip-hovercard';
19 }
19 }
20
20
21 var data_hovercard_alt = username;
21 var data_hovercard_alt = username;
22
22
23 %>
23 %>
24
24
25 <%
25 <%
26 if (show_disabled) {
26 if (show_disabled) {
27 var user_cls = 'user user-disabled';
27 var user_cls = 'user user-disabled';
28 } else {
28 } else {
29 var user_cls = 'user';
29 var user_cls = 'user';
30 }
30 }
31 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
31 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
32 %>
32 %>
33
33
34 <div class="rc-user">
34 <div class="rc-user">
35 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
35 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
36 <span class="<%= user_cls %>"> <%- user_link -%> </span>
36 <span class="<%= user_cls %>"> <%- user_link -%> </span>
37 </div>
37 </div>
38
38
39 </script>
39 </script>
40
40
41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
42 <%
42 <%
43 if (create) {
43 if (create) {
44 var edit_visibility = 'visible';
44 var edit_visibility = 'visible';
45 } else {
45 } else {
46 var edit_visibility = 'hidden';
46 var edit_visibility = 'hidden';
47 }
47 }
48
48
49 if (member.user_group && member.user_group.vote_rule) {
49 if (member.user_group && member.user_group.vote_rule) {
50 var reviewGroup = '<i class="icon-user-group"></i>';
50 var reviewGroup = '<i class="icon-user-group"></i>';
51 var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
51 var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
52 } else {
52 } else {
53 var reviewGroup = null;
53 var reviewGroup = null;
54 var reviewGroupColor = 'transparent';
54 var reviewGroupColor = 'transparent';
55 }
55 }
56 var rule_show = rule_show || false;
56 var rule_show = rule_show || false;
57
57
58 if (rule_show) {
58 if (rule_show) {
59 var rule_visibility = 'table-cell';
59 var rule_visibility = 'table-cell';
60 } else {
60 } else {
61 var rule_visibility = 'none';
61 var rule_visibility = 'none';
62 }
62 }
63
63
64 %>
64 %>
65
65
66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
67
67
68 <td style="width: 20px">
68 <td style="width: 20px">
69 <div class="tooltip presence-state" style="display: none; position: absolute; left: 2px" title="This users is currently at this page">
70 <i class="icon-eye" style="color: #0ac878"></i>
71 </div>
72 <% if (role === 'reviewer') { %>
69 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
73 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
70 <i class="icon-circle review-status-<%= review_status %>"></i>
74 <i class="icon-circle review-status-<%= review_status %>"></i>
71 </div>
75 </div>
76 <% } else if (role === 'observer') { %>
77 <div class="tooltip" title="Observer without voting right.">
78 <i class="icon-circle-thin"></i>
79 </div>
80 <% } %>
72 </td>
81 </td>
73
82
74 <td>
83 <td>
75 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
84 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
76 <%-
85 <%-
77 renderTemplate('gravatarWithUser', {
86 renderTemplate('gravatarWithUser', {
78 'size': 16,
87 'size': 16,
79 'show_disabled': false,
88 'show_disabled': false,
80 'tooltip': true,
89 'tooltip': true,
81 'username': member.username,
90 'username': member.username,
82 'user_id': member.user_id,
91 'user_id': member.user_id,
83 'user_link': member.user_link,
92 'user_link': member.user_link,
84 'gravatar_url': member.gravatar_link
93 'gravatar_url': member.gravatar_link
85 })
94 })
86 %>
95 %>
87 <span class="tooltip presence-state" style="display: none" title="This users is currently at this page">
88 <i class="icon-eye" style="color: #0ac878"></i>
89 </span>
90 </div>
96 </div>
91 </td>
97 </td>
92
98
93 <td style="width: 10px">
99 <td style="width: 10px">
94 <% if (reviewGroup !== null) { %>
100 <% if (reviewGroup !== null) { %>
95 <span class="tooltip" title="Member of review group from rule: `<%= member.user_group.name %>`" style="color: <%= reviewGroupColor %>">
101 <span class="tooltip" title="Member of review group from rule: `<%= member.user_group.name %>`" style="color: <%= reviewGroupColor %>">
96 <%- reviewGroup %>
102 <%- reviewGroup %>
97 </span>
103 </span>
98 <% } %>
104 <% } %>
99 </td>
105 </td>
100
106
101 <% if (mandatory) { %>
107 <% if (mandatory) { %>
102 <td style="text-align: right;width: 10px;">
108 <td style="text-align: right;width: 10px;">
103 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
109 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
104 <i class="icon-lock"></i>
110 <i class="icon-lock"></i>
105 </div>
111 </div>
106 </td>
112 </td>
107
113
108 <% } else { %>
114 <% } else { %>
109 <td style="text-align: right;width: 10px;">
115 <td style="text-align: right;width: 10px;">
110 <% if (allowed_to_update) { %>
116 <% if (allowed_to_update) { %>
111 <div class="reviewer_member_remove" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
117 <div class="<%=role %>_member_remove" onclick="reviewersController.removeMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
112 <i class="icon-remove"></i>
118 <i class="icon-remove"></i>
113 </div>
119 </div>
114 <% } %>
120 <% } %>
115 </td>
121 </td>
116 <% } %>
122 <% } %>
117
123
118 </tr>
124 </tr>
119
125
120 <tr>
126 <tr id="reviewer_<%= member.user_id %>_rules">
121 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
127 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
122 <input type="hidden" name="__start__" value="reviewer:mapping">
128 <input type="hidden" name="__start__" value="reviewer:mapping">
123
129
124 <%if (member.user_group && member.user_group.vote_rule) { %>
130 <%if (member.user_group && member.user_group.vote_rule) { %>
125 <div class="reviewer_reason">
131 <div class="reviewer_reason">
126
132
127 <%if (member.user_group.vote_rule == -1) {%>
133 <%if (member.user_group.vote_rule == -1) {%>
128 - group votes required: ALL
134 - group votes required: ALL
129 <%} else {%>
135 <%} else {%>
130 - group votes required: <%= member.user_group.vote_rule %>
136 - group votes required: <%= member.user_group.vote_rule %>
131 <%}%>
137 <%}%>
132 </div>
138 </div>
133 <%} %>
139 <%} %>
134
140
135 <input type="hidden" name="__start__" value="reasons:sequence">
141 <input type="hidden" name="__start__" value="reasons:sequence">
136 <% for (var i = 0; i < reasons.length; i++) { %>
142 <% for (var i = 0; i < reasons.length; i++) { %>
137 <% var reason = reasons[i] %>
143 <% var reason = reasons[i] %>
138 <div class="reviewer_reason">- <%= reason %></div>
144 <div class="reviewer_reason">- <%= reason %></div>
139 <input type="hidden" name="reason" value="<%= reason %>">
145 <input type="hidden" name="reason" value="<%= reason %>">
140 <% } %>
146 <% } %>
141 <input type="hidden" name="__end__" value="reasons:sequence">
147 <input type="hidden" name="__end__" value="reasons:sequence">
142
148
143 <input type="hidden" name="__start__" value="rules:sequence">
149 <input type="hidden" name="__start__" value="rules:sequence">
144 <% for (var i = 0; i < member.rules.length; i++) { %>
150 <% for (var i = 0; i < member.rules.length; i++) { %>
145 <% var rule = member.rules[i] %>
151 <% var rule = member.rules[i] %>
146 <input type="hidden" name="rule_id" value="<%= rule %>">
152 <input type="hidden" name="rule_id" value="<%= rule %>">
147 <% } %>
153 <% } %>
148 <input type="hidden" name="__end__" value="rules:sequence">
154 <input type="hidden" name="__end__" value="rules:sequence">
149
155
150 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
156 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
151 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
157 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
158 <input type="hidden" name="role" value="<%= role %>"/>
152
159
153 <input type="hidden" name="__end__" value="reviewer:mapping">
160 <input type="hidden" name="__end__" value="reviewer:mapping">
154 </td>
161 </td>
155 </tr>
162 </tr>
156
163
157 </script>
164 </script>
158
165
159 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
166 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
160
167
161 <%
168 <%
162 if (size > 16) {
169 if (size > 16) {
163 var gravatar_class = 'gravatar gravatar-large';
170 var gravatar_class = 'gravatar gravatar-large';
164 } else {
171 } else {
165 var gravatar_class = 'gravatar';
172 var gravatar_class = 'gravatar';
166 }
173 }
167
174
168 %>
175 %>
169
176
170 <%
177 <%
171 if (show_disabled) {
178 if (show_disabled) {
172 var user_cls = 'user user-disabled';
179 var user_cls = 'user user-disabled';
173 } else {
180 } else {
174 var user_cls = 'user';
181 var user_cls = 'user';
175 }
182 }
176
183
177 %>
184 %>
178
185
179 <div style='line-height: 20px'>
186 <div style='line-height: 20px'>
180 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
187 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
181 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
188 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
182 </div>
189 </div>
183
190
184 </script>
191 </script>
185
192
186
193
187 <script id="ejs_sideBarCommentHovercard" type="text/template" class="ejsTemplate">
194 <script id="ejs_sideBarCommentHovercard" type="text/template" class="ejsTemplate">
188
195
189 <div>
196 <div>
190 <% if (is_todo) { %>
197 <% if (is_todo) { %>
191 <% if (inline) { %>
198 <% if (inline) { %>
192 <strong>Inline</strong> TODO on line: <%= line_no %>
199 <strong>Inline</strong> TODO on line: <%= line_no %>
193 <% if (version_info) { %>
200 <% if (version_info) { %>
194 <%= version_info %>
201 <%= version_info %>
195 <% } %>
202 <% } %>
196 <br/>
203 <br/>
197 File: <code><%- file_name -%></code>
204 File: <code><%- file_name -%></code>
198 <% } else { %>
205 <% } else { %>
199 <% if (review_status) { %>
206 <% if (review_status) { %>
200 <i class="icon-circle review-status-<%= review_status %>"></i>
207 <i class="icon-circle review-status-<%= review_status %>"></i>
201 <% } %>
208 <% } %>
202 <strong>General</strong> TODO
209 <strong>General</strong> TODO
203 <% if (version_info) { %>
210 <% if (version_info) { %>
204 <%= version_info %>
211 <%= version_info %>
205 <% } %>
212 <% } %>
206 <% } %>
213 <% } %>
207 <% } else { %>
214 <% } else { %>
208 <% if (inline) { %>
215 <% if (inline) { %>
209 <strong>Inline</strong> comment on line: <%= line_no %>
216 <strong>Inline</strong> comment on line: <%= line_no %>
210 <% if (version_info) { %>
217 <% if (version_info) { %>
211 <%= version_info %>
218 <%= version_info %>
212 <% } %>
219 <% } %>
213 <br/>
220 <br/>
214 File: <code><%- file_name -%></code>
221 File: <code><%- file_name -%></code>
215 <% } else { %>
222 <% } else { %>
216 <% if (review_status) { %>
223 <% if (review_status) { %>
217 <i class="icon-circle review-status-<%= review_status %>"></i>
224 <i class="icon-circle review-status-<%= review_status %>"></i>
218 <% } %>
225 <% } %>
219 <strong>General</strong> comment
226 <strong>General</strong> comment
220 <% if (version_info) { %>
227 <% if (version_info) { %>
221 <%= version_info %>
228 <%= version_info %>
222 <% } %>
229 <% } %>
223 <% } %>
230 <% } %>
224 <% } %>
231 <% } %>
225 <br/>
232 <br/>
226 Created:
233 Created:
227 <time class="timeago" title="<%= created_on %>" datetime="<%= datetime %>"><%= $.timeago(datetime) %></time>
234 <time class="timeago" title="<%= created_on %>" datetime="<%= datetime %>"><%= $.timeago(datetime) %></time>
228
235
229 </div>
236 </div>
230
237
231 </script>
238 </script>
232
239
233 ##// END OF EJS Templates
240 ##// END OF EJS Templates
234 </div>
241 </div>
235
242
236
243
237 <script>
244 <script>
238 // registers the templates into global cache
245 // registers the templates into global cache
239 registerTemplates();
246 registerTemplates();
240 </script>
247 </script>
241
248
242 </%text>
249 </%text>
@@ -1,146 +1,154 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'user': '@'+h.person(user),
9 'user': '@'+h.person(user),
10 'pr_id': pull_request.pull_request_id,
10 'pr_id': pull_request.pull_request_id,
11 'pr_title': pull_request.title,
11 'pr_title': pull_request.title,
12 }
12 }
13
13
14 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
14 if user_role == 'observer':
15 subject_template = email_pr_review_subject_template or _('{user} added you as observer to pull request. !{pr_id}: "{pr_title}"')
16 else:
17 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
15 %>
18 %>
16
19
17 ${subject_template.format(**data) |n}
20 ${subject_template.format(**data) |n}
18 </%def>
21 </%def>
19
22
20 ## PLAINTEXT VERSION OF BODY
23 ## PLAINTEXT VERSION OF BODY
21 <%def name="body_plaintext()" filter="n,trim">
24 <%def name="body_plaintext()" filter="n,trim">
22 <%
25 <%
23 data = {
26 data = {
24 'user': h.person(user),
27 'user': h.person(user),
25 'pr_id': pull_request.pull_request_id,
28 'pr_id': pull_request.pull_request_id,
26 'pr_title': pull_request.title,
29 'pr_title': pull_request.title,
27 'source_ref_type': pull_request.source_ref_parts.type,
30 'source_ref_type': pull_request.source_ref_parts.type,
28 'source_ref_name': pull_request.source_ref_parts.name,
31 'source_ref_name': pull_request.source_ref_parts.name,
29 'target_ref_type': pull_request.target_ref_parts.type,
32 'target_ref_type': pull_request.target_ref_parts.type,
30 'target_ref_name': pull_request.target_ref_parts.name,
33 'target_ref_name': pull_request.target_ref_parts.name,
31 'repo_url': pull_request_source_repo_url,
34 'repo_url': pull_request_source_repo_url,
32 'source_repo': pull_request_source_repo.repo_name,
35 'source_repo': pull_request_source_repo.repo_name,
33 'target_repo': pull_request_target_repo.repo_name,
36 'target_repo': pull_request_target_repo.repo_name,
34 'source_repo_url': pull_request_source_repo_url,
37 'source_repo_url': pull_request_source_repo_url,
35 'target_repo_url': pull_request_target_repo_url,
38 'target_repo_url': pull_request_target_repo_url,
36 }
39 }
40
37 %>
41 %>
38
42
39 * ${_('Pull Request link')}: ${pull_request_url}
43 * ${_('Pull Request link')}: ${pull_request_url}
40
44
41 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
45 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
42
46
43 * ${_('Title')}: ${pull_request.title}
47 * ${_('Title')}: ${pull_request.title}
44
48
45 * ${_('Description')}:
49 * ${_('Description')}:
46
50
47 ${pull_request.description | trim}
51 ${pull_request.description | trim}
48
52
49
53
50 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
54 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
51
55
52 % for commit_id, message in pull_request_commits:
56 % for commit_id, message in pull_request_commits:
53 - ${h.short_id(commit_id)}
57 - ${h.short_id(commit_id)}
54 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
58 ${h.chop_at_smart(message.lstrip(), '\n', suffix_if_chopped='...')}
55
59
56 % endfor
60 % endfor
57
61
58 ---
62 ---
59 ${self.plaintext_footer()}
63 ${self.plaintext_footer()}
60 </%def>
64 </%def>
61 <%
65 <%
62 data = {
66 data = {
63 'user': h.person(user),
67 'user': h.person(user),
64 'pr_id': pull_request.pull_request_id,
68 'pr_id': pull_request.pull_request_id,
65 'pr_title': pull_request.title,
69 'pr_title': pull_request.title,
66 'source_ref_type': pull_request.source_ref_parts.type,
70 'source_ref_type': pull_request.source_ref_parts.type,
67 'source_ref_name': pull_request.source_ref_parts.name,
71 'source_ref_name': pull_request.source_ref_parts.name,
68 'target_ref_type': pull_request.target_ref_parts.type,
72 'target_ref_type': pull_request.target_ref_parts.type,
69 'target_ref_name': pull_request.target_ref_parts.name,
73 'target_ref_name': pull_request.target_ref_parts.name,
70 'repo_url': pull_request_source_repo_url,
74 'repo_url': pull_request_source_repo_url,
71 'source_repo': pull_request_source_repo.repo_name,
75 'source_repo': pull_request_source_repo.repo_name,
72 'target_repo': pull_request_target_repo.repo_name,
76 'target_repo': pull_request_target_repo.repo_name,
73 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
77 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
74 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
78 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
75 }
79 }
76 %>
80 %>
77 ## header
81 ## header
78 <table style="text-align:left;vertical-align:middle;width: 100%">
82 <table style="text-align:left;vertical-align:middle;width: 100%">
79 <tr>
83 <tr>
80 <td style="width:100%;border-bottom:1px solid #dbd9da;">
84 <td style="width:100%;border-bottom:1px solid #dbd9da;">
81
82 <div style="margin: 0; font-weight: bold">
85 <div style="margin: 0; font-weight: bold">
86 % if user_role == 'observer':
87 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
88 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
89 ${_('added you as observer to')}
90 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a>.
91 </div>
92 % else:
83 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
93 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
84 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
94 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
85 ${_('requested a')}
95 ${_('requested a')}
86 <a href="${pull_request_url}" style="${base.link_css()}">
96 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a> review.
87 ${_('pull request review.').format(**data) }
88 </a>
89 </div>
97 </div>
98 % endif
90 <div style="margin-top: 10px"></div>
99 <div style="margin-top: 10px"></div>
91 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
100 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
92 </div>
101 </div>
93
94 </td>
102 </td>
95 </tr>
103 </tr>
96
104
97 </table>
105 </table>
98 <div class="clear-both"></div>
106 <div class="clear-both"></div>
99 ## main body
107 ## main body
100 <table style="text-align:left;vertical-align:middle;width: 100%">
108 <table style="text-align:left;vertical-align:middle;width: 100%">
101 ## spacing def
109 ## spacing def
102 <tr>
110 <tr>
103 <td style="width: 130px"></td>
111 <td style="width: 130px"></td>
104 <td></td>
112 <td></td>
105 </tr>
113 </tr>
106
114
107 <tr>
115 <tr>
108 <td style="padding-right:20px;">${_('Pull request')}:</td>
116 <td style="padding-right:20px;">${_('Pull request')}:</td>
109 <td>
117 <td>
110 <a href="${pull_request_url}" style="${base.link_css()}">
118 <a href="${pull_request_url}" style="${base.link_css()}">
111 !${pull_request.pull_request_id}
119 !${pull_request.pull_request_id}
112 </a>
120 </a>
113 </td>
121 </td>
114 </tr>
122 </tr>
115
123
116 <tr>
124 <tr>
117 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
125 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
118 <td style="line-height:20px;">
126 <td style="line-height:20px;">
119 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
127 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
120 &rarr;
128 &rarr;
121 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
129 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
122 </td>
130 </td>
123 </tr>
131 </tr>
124
132
125 <tr>
133 <tr>
126 <td style="padding-right:20px;">${_('Description')}:</td>
134 <td style="padding-right:20px;">${_('Description')}:</td>
127 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
135 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
128 </tr>
136 </tr>
129 <tr>
137 <tr>
130 <td style="padding-right:20px;">${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits)) % {'num': len(pull_request_commits)}}:</td>
138 <td style="padding-right:20px;">${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits)) % {'num': len(pull_request_commits)}}:</td>
131 <td></td>
139 <td></td>
132 </tr>
140 </tr>
133
141
134 <tr>
142 <tr>
135 <td colspan="2">
143 <td colspan="2">
136 <ol style="margin:0 0 0 1em;padding:0;text-align:left;">
144 <ol style="margin:0 0 0 1em;padding:0;text-align:left;">
137 % for commit_id, message in pull_request_commits:
145 % for commit_id, message in pull_request_commits:
138 <li style="margin:0 0 1em;">
146 <li style="margin:0 0 1em;">
139 <pre style="margin:0 0 .5em"><a href="${h.route_path('repo_commit', repo_name=pull_request_source_repo.repo_name, commit_id=commit_id)}" style="${base.link_css()}">${h.short_id(commit_id)}</a></pre>
147 <pre style="margin:0 0 .5em"><a href="${h.route_path('repo_commit', repo_name=pull_request_source_repo.repo_name, commit_id=commit_id)}" style="${base.link_css()}">${h.short_id(commit_id)}</a></pre>
140 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
148 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
141 </li>
149 </li>
142 % endfor
150 % endfor
143 </ol>
151 </ol>
144 </td>
152 </td>
145 </tr>
153 </tr>
146 </table>
154 </table>
@@ -1,555 +1,635 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
2 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${c.repo_name} ${_('New pull request')}
5 ${c.repo_name} ${_('New pull request')}
6 </%def>
6 </%def>
7
7
8 <%def name="breadcrumbs_links()"></%def>
8 <%def name="breadcrumbs_links()"></%def>
9
9
10 <%def name="menu_bar_nav()">
10 <%def name="menu_bar_nav()">
11 ${self.menu_items(active='repositories')}
11 ${self.menu_items(active='repositories')}
12 </%def>
12 </%def>
13
13
14 <%def name="menu_bar_subnav()">
14 <%def name="menu_bar_subnav()">
15 ${self.repo_menu(active='showpullrequest')}
15 ${self.repo_menu(active='showpullrequest')}
16 </%def>
16 </%def>
17
17
18 <%def name="main()">
18 <%def name="main()">
19 <div class="box">
19 <div class="box">
20 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
20 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
21
21
22 <div class="box">
22 <div class="box">
23
23
24 <div class="summary-details block-left">
24 <div class="summary-details block-left">
25
25
26 <div class="form" style="padding-top: 10px">
26 <div class="form" style="padding-top: 10px">
27
27
28 <div class="fields" >
28 <div class="fields" >
29
29
30 ## COMMIT FLOW
30 ## COMMIT FLOW
31 <div class="field">
31 <div class="field">
32 <div class="label label-textarea">
32 <div class="label label-textarea">
33 <label for="commit_flow">${_('Commit flow')}:</label>
33 <label for="commit_flow">${_('Commit flow')}:</label>
34 </div>
34 </div>
35
35
36 <div class="content">
36 <div class="content">
37 <div class="flex-container">
37 <div class="flex-container">
38 <div style="width: 45%;">
38 <div style="width: 45%;">
39 <div class="panel panel-default source-panel">
39 <div class="panel panel-default source-panel">
40 <div class="panel-heading">
40 <div class="panel-heading">
41 <h3 class="panel-title">${_('Source repository')}</h3>
41 <h3 class="panel-title">${_('Source repository')}</h3>
42 </div>
42 </div>
43 <div class="panel-body">
43 <div class="panel-body">
44 <div style="display:none">${c.rhodecode_db_repo.description}</div>
44 <div style="display:none">${c.rhodecode_db_repo.description}</div>
45 ${h.hidden('source_repo')}
45 ${h.hidden('source_repo')}
46 ${h.hidden('source_ref')}
46 ${h.hidden('source_ref')}
47
47
48 <div id="pr_open_message"></div>
48 <div id="pr_open_message"></div>
49 </div>
49 </div>
50 </div>
50 </div>
51 </div>
51 </div>
52
52
53 <div style="width: 90px; text-align: center; padding-top: 30px">
53 <div style="width: 90px; text-align: center; padding-top: 30px">
54 <div>
54 <div>
55 <i class="icon-right" style="font-size: 2.2em"></i>
55 <i class="icon-right" style="font-size: 2.2em"></i>
56 </div>
56 </div>
57 <div style="position: relative; top: 10px">
57 <div style="position: relative; top: 10px">
58 <span class="tag tag">
58 <span class="tag tag">
59 <span id="switch_base"></span>
59 <span id="switch_base"></span>
60 </span>
60 </span>
61 </div>
61 </div>
62
62
63 </div>
63 </div>
64
64
65 <div style="width: 45%;">
65 <div style="width: 45%;">
66
66
67 <div class="panel panel-default target-panel">
67 <div class="panel panel-default target-panel">
68 <div class="panel-heading">
68 <div class="panel-heading">
69 <h3 class="panel-title">${_('Target repository')}</h3>
69 <h3 class="panel-title">${_('Target repository')}</h3>
70 </div>
70 </div>
71 <div class="panel-body">
71 <div class="panel-body">
72 <div style="display:none" id="target_repo_desc"></div>
72 <div style="display:none" id="target_repo_desc"></div>
73 ${h.hidden('target_repo')}
73 ${h.hidden('target_repo')}
74 ${h.hidden('target_ref')}
74 ${h.hidden('target_ref')}
75 <span id="target_ref_loading" style="display: none">
75 <span id="target_ref_loading" style="display: none">
76 ${_('Loading refs...')}
76 ${_('Loading refs...')}
77 </span>
77 </span>
78 </div>
78 </div>
79 </div>
79 </div>
80
80
81 </div>
81 </div>
82 </div>
82 </div>
83
83
84 </div>
84 </div>
85
85
86 </div>
86 </div>
87
87
88 ## TITLE
88 ## TITLE
89 <div class="field">
89 <div class="field">
90 <div class="label">
90 <div class="label">
91 <label for="pullrequest_title">${_('Title')}:</label>
91 <label for="pullrequest_title">${_('Title')}:</label>
92 </div>
92 </div>
93 <div class="input">
93 <div class="input">
94 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
94 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
95 </div>
95 </div>
96 <p class="help-block">
96 <p class="help-block">
97 Start the title with WIP: to prevent accidental merge of Work In Progress pull request before it's ready.
97 Start the title with WIP: to prevent accidental merge of Work In Progress pull request before it's ready.
98 </p>
98 </p>
99 </div>
99 </div>
100
100
101 ## DESC
101 ## DESC
102 <div class="field">
102 <div class="field">
103 <div class="label label-textarea">
103 <div class="label label-textarea">
104 <label for="pullrequest_desc">${_('Description')}:</label>
104 <label for="pullrequest_desc">${_('Description')}:</label>
105 </div>
105 </div>
106 <div class="textarea text-area">
106 <div class="textarea text-area">
107 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
107 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
108 ${dt.markup_form('pullrequest_desc')}
108 ${dt.markup_form('pullrequest_desc')}
109 </div>
109 </div>
110 </div>
110 </div>
111
111
112 ## REVIEWERS
112 ## REVIEWERS
113 <div class="field">
113 <div class="field">
114 <div class="label label-textarea">
114 <div class="label label-textarea">
115 <label for="pullrequest_reviewers">${_('Reviewers')}:</label>
115 <label for="pullrequest_reviewers">${_('Reviewers / Observers')}:</label>
116 </div>
116 </div>
117 <div class="content">
117 <div class="content">
118 ## REVIEW RULES
118 ## REVIEW RULES
119 <div id="review_rules" style="display: none" class="reviewers-title">
119 <div id="review_rules" style="display: none" class="reviewers-title">
120 <div class="pr-details-title">
120 <div class="pr-details-title">
121 ${_('Reviewer rules')}
121 ${_('Reviewer rules')}
122 </div>
122 </div>
123 <div class="pr-reviewer-rules">
123 <div class="pr-reviewer-rules">
124 ## review rules will be appended here, by default reviewers logic
124 ## review rules will be appended here, by default reviewers logic
125 </div>
125 </div>
126 </div>
126 </div>
127
127
128 ## REVIEWERS
128 ## REVIEWERS / OBSERVERS
129 <div class="reviewers-title">
129 <div class="reviewers-title">
130 <div class="pr-details-title">
130
131 ${_('Pull request reviewers')}
131 <ul class="nav-links clearfix">
132 <span class="calculate-reviewers"> - ${_('loading...')}</span>
132
133 </div>
133 ## TAB1 MANDATORY REVIEWERS
134 </div>
134 <li class="active">
135 <div id="reviewers" class="pr-details-content reviewers">
135 <a id="reviewers-btn" href="#showReviewers" tabindex="-1">
136 ## members goes here, filled via JS based on initial selection !
136 Reviewers
137 <input type="hidden" name="__start__" value="review_members:sequence">
137 <span id="reviewers-cnt" data-count="0" class="menulink-counter">0</span>
138 <table id="review_members" class="group_members">
138 </a>
139 ## This content is loaded via JS and ReviewersPanel
139 </li>
140 </table>
140
141 <input type="hidden" name="__end__" value="review_members:sequence">
141 ## TAB2 OBSERVERS
142 <li class="">
143 <a id="observers-btn" href="#showObservers" tabindex="-1">
144 Observers
145 <span id="observers-cnt" data-count="0" class="menulink-counter">0</span>
146 </a>
147 </li>
148
149 </ul>
142
150
143 <div id="add_reviewer_input" class='ac'>
151 ## TAB1 MANDATORY REVIEWERS
144 <div class="reviewer_ac">
152 <div id="reviewers-container">
145 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
153 <span class="calculate-reviewers">
146 <div id="reviewers_container"></div>
154 <h4>${_('loading...')}</h4>
155 </span>
156
157 <div id="reviewers" class="pr-details-content reviewers">
158 ## members goes here, filled via JS based on initial selection !
159 <input type="hidden" name="__start__" value="review_members:sequence">
160 <table id="review_members" class="group_members">
161 ## This content is loaded via JS and ReviewersPanel, an sets reviewer_entry class on each element
162 </table>
163 <input type="hidden" name="__end__" value="review_members:sequence">
164
165 <div id="add_reviewer_input" class='ac'>
166 <div class="reviewer_ac">
167 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
168 <div id="reviewers_container"></div>
169 </div>
170 </div>
171
147 </div>
172 </div>
148 </div>
173 </div>
149
174
175 ## TAB2 OBSERVERS
176 <div id="observers-container" style="display: none">
177 <span class="calculate-reviewers">
178 <h4>${_('loading...')}</h4>
179 </span>
180
181 <div id="observers" class="pr-details-content observers">
182 ## members goes here, filled via JS based on initial selection !
183 <input type="hidden" name="__start__" value="observer_members:sequence">
184 <table id="observer_members" class="group_members">
185 ## This content is loaded via JS and ReviewersPanel, an sets reviewer_entry class on each element
186 </table>
187 <input type="hidden" name="__end__" value="observer_members:sequence">
188
189 <div id="add_observer_input" class='ac'>
190 <div class="observer_ac">
191 ${h.text('observer', class_='ac-input', placeholder=_('Add observer or observer group'))}
192 <div id="observers_container"></div>
193 </div>
194 </div>
195 </div>
196
197 </div>
198
150 </div>
199 </div>
200
151 </div>
201 </div>
152 </div>
202 </div>
153
203
154 ## SUBMIT
204 ## SUBMIT
155 <div class="field">
205 <div class="field">
156 <div class="label label-textarea">
206 <div class="label label-textarea">
157 <label for="pullrequest_submit"></label>
207 <label for="pullrequest_submit"></label>
158 </div>
208 </div>
159 <div class="input">
209 <div class="input">
160 <div class="pr-submit-button">
210 <div class="pr-submit-button">
161 <input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}">
211 <input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}">
162 </div>
212 </div>
163 </div>
213 </div>
164 </div>
214 </div>
165 </div>
215 </div>
166 </div>
216 </div>
167 </div>
217 </div>
168
218
169 </div>
219 </div>
170
220
171 ${h.end_form()}
221 ${h.end_form()}
172 </div>
222 </div>
173
223
174 <script type="text/javascript">
224 <script type="text/javascript">
175 $(function(){
225 $(function(){
176 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
226 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
177 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
227 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
178 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
228 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
179 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
229 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
180
230
181 var $pullRequestForm = $('#pull_request_form');
231 var $pullRequestForm = $('#pull_request_form');
182 var $pullRequestSubmit = $('#pr_submit', $pullRequestForm);
232 var $pullRequestSubmit = $('#pr_submit', $pullRequestForm);
183 var $sourceRepo = $('#source_repo', $pullRequestForm);
233 var $sourceRepo = $('#source_repo', $pullRequestForm);
184 var $targetRepo = $('#target_repo', $pullRequestForm);
234 var $targetRepo = $('#target_repo', $pullRequestForm);
185 var $sourceRef = $('#source_ref', $pullRequestForm);
235 var $sourceRef = $('#source_ref', $pullRequestForm);
186 var $targetRef = $('#target_ref', $pullRequestForm);
236 var $targetRef = $('#target_ref', $pullRequestForm);
187
237
188 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
238 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
189 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
239 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
190
240
191 var targetRepo = function() { return $targetRepo.eq(0).val() };
241 var targetRepo = function() { return $targetRepo.eq(0).val() };
192 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
242 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
193
243
194 var calculateContainerWidth = function() {
244 var calculateContainerWidth = function() {
195 var maxWidth = 0;
245 var maxWidth = 0;
196 var repoSelect2Containers = ['#source_repo', '#target_repo'];
246 var repoSelect2Containers = ['#source_repo', '#target_repo'];
197 $.each(repoSelect2Containers, function(idx, value) {
247 $.each(repoSelect2Containers, function(idx, value) {
198 $(value).select2('container').width('auto');
248 $(value).select2('container').width('auto');
199 var curWidth = $(value).select2('container').width();
249 var curWidth = $(value).select2('container').width();
200 if (maxWidth <= curWidth) {
250 if (maxWidth <= curWidth) {
201 maxWidth = curWidth;
251 maxWidth = curWidth;
202 }
252 }
203 $.each(repoSelect2Containers, function(idx, value) {
253 $.each(repoSelect2Containers, function(idx, value) {
204 $(value).select2('container').width(maxWidth + 10);
254 $(value).select2('container').width(maxWidth + 10);
205 });
255 });
206 });
256 });
207 };
257 };
208
258
209 var initRefSelection = function(selectedRef) {
259 var initRefSelection = function(selectedRef) {
210 return function(element, callback) {
260 return function(element, callback) {
211 // translate our select2 id into a text, it's a mapping to show
261 // translate our select2 id into a text, it's a mapping to show
212 // simple label when selecting by internal ID.
262 // simple label when selecting by internal ID.
213 var id, refData;
263 var id, refData;
214 if (selectedRef === undefined || selectedRef === null) {
264 if (selectedRef === undefined || selectedRef === null) {
215 id = element.val();
265 id = element.val();
216 refData = element.val().split(':');
266 refData = element.val().split(':');
217
267
218 if (refData.length !== 3){
268 if (refData.length !== 3){
219 refData = ["", "", ""]
269 refData = ["", "", ""]
220 }
270 }
221 } else {
271 } else {
222 id = selectedRef;
272 id = selectedRef;
223 refData = selectedRef.split(':');
273 refData = selectedRef.split(':');
224 }
274 }
225
275
226 var text = refData[1];
276 var text = refData[1];
227 if (refData[0] === 'rev') {
277 if (refData[0] === 'rev') {
228 text = text.substring(0, 12);
278 text = text.substring(0, 12);
229 }
279 }
230
280
231 var data = {id: id, text: text};
281 var data = {id: id, text: text};
232 callback(data);
282 callback(data);
233 };
283 };
234 };
284 };
235
285
236 var formatRefSelection = function(data, container, escapeMarkup) {
286 var formatRefSelection = function(data, container, escapeMarkup) {
237 var prefix = '';
287 var prefix = '';
238 var refData = data.id.split(':');
288 var refData = data.id.split(':');
239 if (refData[0] === 'branch') {
289 if (refData[0] === 'branch') {
240 prefix = '<i class="icon-branch"></i>';
290 prefix = '<i class="icon-branch"></i>';
241 }
291 }
242 else if (refData[0] === 'book') {
292 else if (refData[0] === 'book') {
243 prefix = '<i class="icon-bookmark"></i>';
293 prefix = '<i class="icon-bookmark"></i>';
244 }
294 }
245 else if (refData[0] === 'tag') {
295 else if (refData[0] === 'tag') {
246 prefix = '<i class="icon-tag"></i>';
296 prefix = '<i class="icon-tag"></i>';
247 }
297 }
248
298
249 var originalOption = data.element;
299 var originalOption = data.element;
250 return prefix + escapeMarkup(data.text);
300 return prefix + escapeMarkup(data.text);
251 };formatSelection:
301 };formatSelection:
252
302
253 // custom code mirror
303 // custom code mirror
254 var codeMirrorInstance = $('#pullrequest_desc').get(0).MarkupForm.cm;
304 var codeMirrorInstance = $('#pullrequest_desc').get(0).MarkupForm.cm;
255
305
256 var diffDataHandler = function(data) {
306 var diffDataHandler = function(data) {
257
307
258 var commitElements = data['commits'];
308 var commitElements = data['commits'];
259 var files = data['files'];
309 var files = data['files'];
260 var added = data['stats'][0]
310 var added = data['stats'][0]
261 var deleted = data['stats'][1]
311 var deleted = data['stats'][1]
262 var commonAncestorId = data['ancestor'];
312 var commonAncestorId = data['ancestor'];
263 var _sourceRefType = sourceRef()[0];
313 var _sourceRefType = sourceRef()[0];
264 var _sourceRefName = sourceRef()[1];
314 var _sourceRefName = sourceRef()[1];
265 var prTitleAndDesc = getTitleAndDescription(_sourceRefType, _sourceRefName, commitElements, 5);
315 var prTitleAndDesc = getTitleAndDescription(_sourceRefType, _sourceRefName, commitElements, 5);
266
316
267 var title = prTitleAndDesc[0];
317 var title = prTitleAndDesc[0];
268 var proposedDescription = prTitleAndDesc[1];
318 var proposedDescription = prTitleAndDesc[1];
269
319
270 var useGeneratedTitle = (
320 var useGeneratedTitle = (
271 $('#pullrequest_title').hasClass('autogenerated-title') ||
321 $('#pullrequest_title').hasClass('autogenerated-title') ||
272 $('#pullrequest_title').val() === "");
322 $('#pullrequest_title').val() === "");
273
323
274 if (title && useGeneratedTitle) {
324 if (title && useGeneratedTitle) {
275 // use generated title if we haven't specified our own
325 // use generated title if we haven't specified our own
276 $('#pullrequest_title').val(title);
326 $('#pullrequest_title').val(title);
277 $('#pullrequest_title').addClass('autogenerated-title');
327 $('#pullrequest_title').addClass('autogenerated-title');
278
328
279 }
329 }
280
330
281 var useGeneratedDescription = (
331 var useGeneratedDescription = (
282 !codeMirrorInstance._userDefinedValue ||
332 !codeMirrorInstance._userDefinedValue ||
283 codeMirrorInstance.getValue() === "");
333 codeMirrorInstance.getValue() === "");
284
334
285 if (proposedDescription && useGeneratedDescription) {
335 if (proposedDescription && useGeneratedDescription) {
286 // set proposed content, if we haven't defined our own,
336 // set proposed content, if we haven't defined our own,
287 // or we don't have description written
337 // or we don't have description written
288 codeMirrorInstance._userDefinedValue = false; // reset state
338 codeMirrorInstance._userDefinedValue = false; // reset state
289 codeMirrorInstance.setValue(proposedDescription);
339 codeMirrorInstance.setValue(proposedDescription);
290 }
340 }
291
341
292 // refresh our codeMirror so events kicks in and it's change aware
342 // refresh our codeMirror so events kicks in and it's change aware
293 codeMirrorInstance.refresh();
343 codeMirrorInstance.refresh();
294
344
295 var url_data = {
345 var url_data = {
296 'repo_name': targetRepo(),
346 'repo_name': targetRepo(),
297 'target_repo': sourceRepo(),
347 'target_repo': sourceRepo(),
298 'source_ref': targetRef()[2],
348 'source_ref': targetRef()[2],
299 'source_ref_type': 'rev',
349 'source_ref_type': 'rev',
300 'target_ref': sourceRef()[2],
350 'target_ref': sourceRef()[2],
301 'target_ref_type': 'rev',
351 'target_ref_type': 'rev',
302 'merge': true,
352 'merge': true,
303 '_': Date.now() // bypass browser caching
353 '_': Date.now() // bypass browser caching
304 }; // gather the source/target ref and repo here
354 }; // gather the source/target ref and repo here
305 var url = pyroutes.url('repo_compare', url_data);
355 var url = pyroutes.url('repo_compare', url_data);
306
356
307 var msg = '<input id="common_ancestor" type="hidden" name="common_ancestor" value="{0}">'.format(commonAncestorId);
357 var msg = '<input id="common_ancestor" type="hidden" name="common_ancestor" value="{0}">'.format(commonAncestorId);
308 msg += '<input type="hidden" name="__start__" value="revisions:sequence">'
358 msg += '<input type="hidden" name="__start__" value="revisions:sequence">'
309
359
310 $.each(commitElements, function(idx, value) {
360 $.each(commitElements, function(idx, value) {
311 msg += '<input type="hidden" name="revisions" value="{0}">'.format(value["raw_id"]);
361 msg += '<input type="hidden" name="revisions" value="{0}">'.format(value["raw_id"]);
312 });
362 });
313
363
314 msg += '<input type="hidden" name="__end__" value="revisions:sequence">'
364 msg += '<input type="hidden" name="__end__" value="revisions:sequence">'
315 msg += _ngettext(
365 msg += _ngettext(
316 'Compare summary: <strong>{0} commit</strong>',
366 'Compare summary: <strong>{0} commit</strong>',
317 'Compare summary: <strong>{0} commits</strong>',
367 'Compare summary: <strong>{0} commits</strong>',
318 commitElements.length).format(commitElements.length)
368 commitElements.length).format(commitElements.length)
319
369
320 msg += '';
370 msg += '';
321 msg += _ngettext(
371 msg += _ngettext(
322 '<strong>, and {0} file</strong> changed.',
372 '<strong>, and {0} file</strong> changed.',
323 '<strong>, and {0} files</strong> changed.',
373 '<strong>, and {0} files</strong> changed.',
324 files.length).format(files.length)
374 files.length).format(files.length)
325
375
326 msg += '\n Diff: <span class="op-added">{0} lines inserted</span>, <span class="op-deleted">{1} lines deleted </span>.'.format(added, deleted)
376 msg += '\n Diff: <span class="op-added">{0} lines inserted</span>, <span class="op-deleted">{1} lines deleted </span>.'.format(added, deleted)
327
377
328 msg += '\n <a class="" id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
378 msg += '\n <a class="" id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
329
379
330 if (commitElements.length) {
380 if (commitElements.length) {
331 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
381 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
332 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
382 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
333 }
383 }
334 else {
384 else {
335 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
385 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
336 _gettext('There are no commits to merge.'));
386 _gettext('There are no commits to merge.'));
337 prButtonLock(true, noCommitsMsg, 'compare');
387 prButtonLock(true, noCommitsMsg, 'compare');
338 }
388 }
339
389
340 //make both panels equal
390 //make both panels equal
341 $('.target-panel').height($('.source-panel').height())
391 $('.target-panel').height($('.source-panel').height())
342
343 };
392 };
344
393
345 reviewersController = new ReviewersController();
394 reviewersController = new ReviewersController();
346 reviewersController.diffDataHandler = diffDataHandler;
395 reviewersController.diffDataHandler = diffDataHandler;
347
396
348 var queryTargetRepo = function(self, query) {
397 var queryTargetRepo = function(self, query) {
349 // cache ALL results if query is empty
398 // cache ALL results if query is empty
350 var cacheKey = query.term || '__';
399 var cacheKey = query.term || '__';
351 var cachedData = self.cachedDataSource[cacheKey];
400 var cachedData = self.cachedDataSource[cacheKey];
352
401
353 if (cachedData) {
402 if (cachedData) {
354 query.callback({results: cachedData.results});
403 query.callback({results: cachedData.results});
355 } else {
404 } else {
356 $.ajax({
405 $.ajax({
357 url: pyroutes.url('pullrequest_repo_targets', {'repo_name': templateContext.repo_name}),
406 url: pyroutes.url('pullrequest_repo_targets', {'repo_name': templateContext.repo_name}),
358 data: {query: query.term},
407 data: {query: query.term},
359 dataType: 'json',
408 dataType: 'json',
360 type: 'GET',
409 type: 'GET',
361 success: function(data) {
410 success: function(data) {
362 self.cachedDataSource[cacheKey] = data;
411 self.cachedDataSource[cacheKey] = data;
363 query.callback({results: data.results});
412 query.callback({results: data.results});
364 },
413 },
365 error: function(jqXHR, textStatus, errorThrown) {
414 error: function(jqXHR, textStatus, errorThrown) {
366 var prefix = "Error while fetching entries.\n"
415 var prefix = "Error while fetching entries.\n"
367 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
416 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
368 ajaxErrorSwal(message);
417 ajaxErrorSwal(message);
369 }
418 }
370 });
419 });
371 }
420 }
372 };
421 };
373
422
374 var queryTargetRefs = function(initialData, query) {
423 var queryTargetRefs = function(initialData, query) {
375 var data = {results: []};
424 var data = {results: []};
376 // filter initialData
425 // filter initialData
377 $.each(initialData, function() {
426 $.each(initialData, function() {
378 var section = this.text;
427 var section = this.text;
379 var children = [];
428 var children = [];
380 $.each(this.children, function() {
429 $.each(this.children, function() {
381 if (query.term.length === 0 ||
430 if (query.term.length === 0 ||
382 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
431 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
383 children.push({'id': this.id, 'text': this.text})
432 children.push({'id': this.id, 'text': this.text})
384 }
433 }
385 });
434 });
386 data.results.push({'text': section, 'children': children})
435 data.results.push({'text': section, 'children': children})
387 });
436 });
388 query.callback({results: data.results});
437 query.callback({results: data.results});
389 };
438 };
390
439
391 var Select2Box = function(element, overrides) {
440 var Select2Box = function(element, overrides) {
392 var globalDefaults = {
441 var globalDefaults = {
393 dropdownAutoWidth: true,
442 dropdownAutoWidth: true,
394 containerCssClass: "drop-menu",
443 containerCssClass: "drop-menu",
395 dropdownCssClass: "drop-menu-dropdown"
444 dropdownCssClass: "drop-menu-dropdown"
396 };
445 };
397
446
398 var initSelect2 = function(defaultOptions) {
447 var initSelect2 = function(defaultOptions) {
399 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
448 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
400 element.select2(options);
449 element.select2(options);
401 };
450 };
402
451
403 return {
452 return {
404 initRef: function() {
453 initRef: function() {
405 var defaultOptions = {
454 var defaultOptions = {
406 minimumResultsForSearch: 5,
455 minimumResultsForSearch: 5,
407 formatSelection: formatRefSelection
456 formatSelection: formatRefSelection
408 };
457 };
409
458
410 initSelect2(defaultOptions);
459 initSelect2(defaultOptions);
411 },
460 },
412
461
413 initRepo: function(defaultValue, readOnly) {
462 initRepo: function(defaultValue, readOnly) {
414 var defaultOptions = {
463 var defaultOptions = {
415 initSelection : function (element, callback) {
464 initSelection : function (element, callback) {
416 var data = {id: defaultValue, text: defaultValue};
465 var data = {id: defaultValue, text: defaultValue};
417 callback(data);
466 callback(data);
418 }
467 }
419 };
468 };
420
469
421 initSelect2(defaultOptions);
470 initSelect2(defaultOptions);
422
471
423 element.select2('val', defaultSourceRepo);
472 element.select2('val', defaultSourceRepo);
424 if (readOnly === true) {
473 if (readOnly === true) {
425 element.select2('readonly', true);
474 element.select2('readonly', true);
426 }
475 }
427 }
476 }
428 };
477 };
429 };
478 };
430
479
431 var initTargetRefs = function(refsData, selectedRef) {
480 var initTargetRefs = function(refsData, selectedRef) {
432
481
433 Select2Box($targetRef, {
482 Select2Box($targetRef, {
434 placeholder: "${_('Select commit reference')}",
483 placeholder: "${_('Select commit reference')}",
435 query: function(query) {
484 query: function(query) {
436 queryTargetRefs(refsData, query);
485 queryTargetRefs(refsData, query);
437 },
486 },
438 initSelection : initRefSelection(selectedRef)
487 initSelection : initRefSelection(selectedRef)
439 }).initRef();
488 }).initRef();
440
489
441 if (!(selectedRef === undefined)) {
490 if (!(selectedRef === undefined)) {
442 $targetRef.select2('val', selectedRef);
491 $targetRef.select2('val', selectedRef);
443 }
492 }
444 };
493 };
445
494
446 var targetRepoChanged = function(repoData) {
495 var targetRepoChanged = function(repoData) {
447 // generate new DESC of target repo displayed next to select
496 // generate new DESC of target repo displayed next to select
448
497
449 $('#target_repo_desc').html(repoData['description']);
498 $('#target_repo_desc').html(repoData['description']);
450
499
451 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
500 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
452 var title = _gettext('Switch target repository with the source.')
501 var title = _gettext('Switch target repository with the source.')
453 $('#switch_base').html("<a class=\"tooltip\" title=\"{0}\" href=\"{1}\">Switch sides</a>".format(title, prLink))
502 $('#switch_base').html("<a class=\"tooltip\" title=\"{0}\" href=\"{1}\">Switch sides</a>".format(title, prLink))
454
503
455 // generate dynamic select2 for refs.
504 // generate dynamic select2 for refs.
456 initTargetRefs(repoData['refs']['select2_refs'],
505 initTargetRefs(repoData['refs']['select2_refs'],
457 repoData['refs']['selected_ref']);
506 repoData['refs']['selected_ref']);
458
507
459 };
508 };
460
509
461 var sourceRefSelect2 = Select2Box($sourceRef, {
510 var sourceRefSelect2 = Select2Box($sourceRef, {
462 placeholder: "${_('Select commit reference')}",
511 placeholder: "${_('Select commit reference')}",
463 query: function(query) {
512 query: function(query) {
464 var initialData = defaultSourceRepoData['refs']['select2_refs'];
513 var initialData = defaultSourceRepoData['refs']['select2_refs'];
465 queryTargetRefs(initialData, query)
514 queryTargetRefs(initialData, query)
466 },
515 },
467 initSelection: initRefSelection()
516 initSelection: initRefSelection()
468 }
517 });
469 );
470
518
471 var sourceRepoSelect2 = Select2Box($sourceRepo, {
519 var sourceRepoSelect2 = Select2Box($sourceRepo, {
472 query: function(query) {}
520 query: function(query) {}
473 });
521 });
474
522
475 var targetRepoSelect2 = Select2Box($targetRepo, {
523 var targetRepoSelect2 = Select2Box($targetRepo, {
476 cachedDataSource: {},
524 cachedDataSource: {},
477 query: $.debounce(250, function(query) {
525 query: $.debounce(250, function(query) {
478 queryTargetRepo(this, query);
526 queryTargetRepo(this, query);
479 }),
527 }),
480 formatResult: formatRepoResult
528 formatResult: formatRepoResult
481 });
529 });
482
530
483 sourceRefSelect2.initRef();
531 sourceRefSelect2.initRef();
484
532
485 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
533 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
486
534
487 targetRepoSelect2.initRepo(defaultTargetRepo, false);
535 targetRepoSelect2.initRepo(defaultTargetRepo, false);
488
536
489 $sourceRef.on('change', function(e){
537 $sourceRef.on('change', function(e){
490 reviewersController.loadDefaultReviewers(
538 reviewersController.loadDefaultReviewers(
491 sourceRepo(), sourceRef(), targetRepo(), targetRef());
539 sourceRepo(), sourceRef(), targetRepo(), targetRef());
492 });
540 });
493
541
494 $targetRef.on('change', function(e){
542 $targetRef.on('change', function(e){
495 reviewersController.loadDefaultReviewers(
543 reviewersController.loadDefaultReviewers(
496 sourceRepo(), sourceRef(), targetRepo(), targetRef());
544 sourceRepo(), sourceRef(), targetRepo(), targetRef());
497 });
545 });
498
546
499 $targetRepo.on('change', function(e){
547 $targetRepo.on('change', function(e){
500 var repoName = $(this).val();
548 var repoName = $(this).val();
501 calculateContainerWidth();
549 calculateContainerWidth();
502 $targetRef.select2('destroy');
550 $targetRef.select2('destroy');
503 $('#target_ref_loading').show();
551 $('#target_ref_loading').show();
504
552
505 $.ajax({
553 $.ajax({
506 url: pyroutes.url('pullrequest_repo_refs',
554 url: pyroutes.url('pullrequest_repo_refs',
507 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
555 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
508 data: {},
556 data: {},
509 dataType: 'json',
557 dataType: 'json',
510 type: 'GET',
558 type: 'GET',
511 success: function(data) {
559 success: function(data) {
512 $('#target_ref_loading').hide();
560 $('#target_ref_loading').hide();
513 targetRepoChanged(data);
561 targetRepoChanged(data);
514 },
562 },
515 error: function(jqXHR, textStatus, errorThrown) {
563 error: function(jqXHR, textStatus, errorThrown) {
516 var prefix = "Error while fetching entries.\n"
564 var prefix = "Error while fetching entries.\n"
517 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
565 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
518 ajaxErrorSwal(message);
566 ajaxErrorSwal(message);
519 }
567 }
520 })
568 })
521
569
522 });
570 });
523
571
524 $pullRequestForm.on('submit', function(e){
572 $pullRequestForm.on('submit', function(e){
525 // Flush changes into textarea
573 // Flush changes into textarea
526 codeMirrorInstance.save();
574 codeMirrorInstance.save();
527 prButtonLock(true, null, 'all');
575 prButtonLock(true, null, 'all');
528 $pullRequestSubmit.val(_gettext('Please wait creating pull request...'));
576 $pullRequestSubmit.val(_gettext('Please wait creating pull request...'));
529 });
577 });
530
578
531 prButtonLock(true, "${_('Please select source and target')}", 'all');
579 prButtonLock(true, "${_('Please select source and target')}", 'all');
532
580
533 // auto-load on init, the target refs select2
581 // auto-load on init, the target refs select2
534 calculateContainerWidth();
582 calculateContainerWidth();
535 targetRepoChanged(defaultTargetRepoData);
583 targetRepoChanged(defaultTargetRepoData);
536
584
537 $('#pullrequest_title').on('keyup', function(e){
585 $('#pullrequest_title').on('keyup', function(e){
538 $(this).removeClass('autogenerated-title');
586 $(this).removeClass('autogenerated-title');
539 });
587 });
540
588
541 % if c.default_source_ref:
589 % if c.default_source_ref:
542 // in case we have a pre-selected value, use it now
590 // in case we have a pre-selected value, use it now
543 $sourceRef.select2('val', '${c.default_source_ref}');
591 $sourceRef.select2('val', '${c.default_source_ref}');
544
592
545
593
546 // default reviewers
594 // default reviewers / observers
547 reviewersController.loadDefaultReviewers(
595 reviewersController.loadDefaultReviewers(
548 sourceRepo(), sourceRef(), targetRepo(), targetRef());
596 sourceRepo(), sourceRef(), targetRepo(), targetRef());
549 % endif
597 % endif
550
598
551 ReviewerAutoComplete('#user');
599 ReviewerAutoComplete('#user', reviewersController);
600 ObserverAutoComplete('#observer', reviewersController);
601
602 // TODO, move this to another handler
603
604 var $reviewersBtn = $('#reviewers-btn');
605 var $reviewersContainer = $('#reviewers-container');
606
607 var $observersBtn = $('#observers-btn')
608 var $observersContainer = $('#observers-container');
609
610 $reviewersBtn.on('click', function (e) {
611
612 $observersContainer.hide();
613 $reviewersContainer.show();
614
615 $observersBtn.parent().removeClass('active');
616 $reviewersBtn.parent().addClass('active');
617 e.preventDefault();
618
619 })
620
621 $observersBtn.on('click', function (e) {
622
623 $reviewersContainer.hide();
624 $observersContainer.show();
625
626 $reviewersBtn.parent().removeClass('active');
627 $observersBtn.parent().addClass('active');
628 e.preventDefault();
629
630 })
631
552 });
632 });
553 </script>
633 </script>
554
634
555 </%def>
635 </%def>
@@ -1,993 +1,994 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 <%namespace name="sidebar" file="/base/sidebar.mako"/>
4 <%namespace name="sidebar" file="/base/sidebar.mako"/>
5
5
6
6
7 <%def name="title()">
7 <%def name="title()">
8 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
8 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
9 %if c.rhodecode_name:
9 %if c.rhodecode_name:
10 &middot; ${h.branding(c.rhodecode_name)}
10 &middot; ${h.branding(c.rhodecode_name)}
11 %endif
11 %endif
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()">
14 <%def name="breadcrumbs_links()">
15
15
16 </%def>
16 </%def>
17
17
18 <%def name="menu_bar_nav()">
18 <%def name="menu_bar_nav()">
19 ${self.menu_items(active='repositories')}
19 ${self.menu_items(active='repositories')}
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_subnav()">
22 <%def name="menu_bar_subnav()">
23 ${self.repo_menu(active='showpullrequest')}
23 ${self.repo_menu(active='showpullrequest')}
24 </%def>
24 </%def>
25
25
26
26
27 <%def name="main()">
27 <%def name="main()">
28 ## Container to gather extracted Tickets
28 ## Container to gather extracted Tickets
29 <%
29 <%
30 c.referenced_commit_issues = []
30 c.referenced_commit_issues = []
31 c.referenced_desc_issues = []
31 c.referenced_desc_issues = []
32 %>
32 %>
33
33
34 <script type="text/javascript">
34 <script type="text/javascript">
35 // TODO: marcink switch this to pyroutes
35 // TODO: marcink switch this to pyroutes
36 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
36 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
37 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
38 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
38 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
39 </script>
39 </script>
40
40
41 <div class="box">
41 <div class="box">
42
42
43 <div class="box pr-summary">
43 <div class="box pr-summary">
44
44
45 <div class="summary-details block-left">
45 <div class="summary-details block-left">
46 <div id="pr-title">
46 <div id="pr-title">
47 % if c.pull_request.is_closed():
47 % if c.pull_request.is_closed():
48 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
48 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
49 % endif
49 % endif
50 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
50 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
51 </div>
51 </div>
52 <div id="pr-title-edit" class="input" style="display: none;">
52 <div id="pr-title-edit" class="input" style="display: none;">
53 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
53 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
54 </div>
54 </div>
55
55
56 <% summary = lambda n:{False:'summary-short'}.get(n) %>
56 <% summary = lambda n:{False:'summary-short'}.get(n) %>
57 <div class="pr-details-title">
57 <div class="pr-details-title">
58 <div class="pull-left">
58 <div class="pull-left">
59 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a>
59 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a>
60 ${_('Created on')}
60 ${_('Created on')}
61 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
61 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
62 <span class="pr-details-title-author-pref">${_('by')}</span>
62 <span class="pr-details-title-author-pref">${_('by')}</span>
63 </div>
63 </div>
64
64
65 <div class="pull-left">
65 <div class="pull-left">
66 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
66 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
67 </div>
67 </div>
68
68
69 %if c.allowed_to_update:
69 %if c.allowed_to_update:
70 <div class="pull-right">
70 <div class="pull-right">
71 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
71 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
72 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
72 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
73 % if c.allowed_to_delete:
73 % if c.allowed_to_delete:
74 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
74 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
75 <input class="btn btn-link btn-danger no-margin" id="remove_${c.pull_request.pull_request_id}" name="remove_${c.pull_request.pull_request_id}"
75 <input class="btn btn-link btn-danger no-margin" id="remove_${c.pull_request.pull_request_id}" name="remove_${c.pull_request.pull_request_id}"
76 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
76 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
77 type="submit" value="${_('Delete pull request')}">
77 type="submit" value="${_('Delete pull request')}">
78 ${h.end_form()}
78 ${h.end_form()}
79 % else:
79 % else:
80 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
80 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
81 % endif
81 % endif
82 </div>
82 </div>
83 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
83 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
84 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
84 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
85 </div>
85 </div>
86
86
87 %endif
87 %endif
88 </div>
88 </div>
89
89
90 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
90 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
91 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container=c.referenced_desc_issues)}
91 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container=c.referenced_desc_issues)}
92 </div>
92 </div>
93
93
94 <div id="pr-desc-edit" class="input textarea" style="display: none;">
94 <div id="pr-desc-edit" class="input textarea" style="display: none;">
95 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
95 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
96 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
96 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
97 </div>
97 </div>
98
98
99 <div id="summary" class="fields pr-details-content">
99 <div id="summary" class="fields pr-details-content">
100
100
101 ## source
101 ## source
102 <div class="field">
102 <div class="field">
103 <div class="label-pr-detail">
103 <div class="label-pr-detail">
104 <label>${_('Commit flow')}:</label>
104 <label>${_('Commit flow')}:</label>
105 </div>
105 </div>
106 <div class="input">
106 <div class="input">
107 <div class="pr-commit-flow">
107 <div class="pr-commit-flow">
108 ## Source
108 ## Source
109 %if c.pull_request.source_ref_parts.type == 'branch':
109 %if c.pull_request.source_ref_parts.type == 'branch':
110 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a>
110 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a>
111 %else:
111 %else:
112 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
112 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
113 %endif
113 %endif
114 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a>
114 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a>
115 &rarr;
115 &rarr;
116 ## Target
116 ## Target
117 %if c.pull_request.target_ref_parts.type == 'branch':
117 %if c.pull_request.target_ref_parts.type == 'branch':
118 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}"><code class="pr-target-info">${c.pull_request.target_ref_parts.type}:${c.pull_request.target_ref_parts.name}</code></a>
118 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}"><code class="pr-target-info">${c.pull_request.target_ref_parts.type}:${c.pull_request.target_ref_parts.name}</code></a>
119 %else:
119 %else:
120 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
120 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
121 %endif
121 %endif
122
122
123 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
123 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
124
124
125 <a class="source-details-action" href="#expand-source-details" onclick="return toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
125 <a class="source-details-action" href="#expand-source-details" onclick="return toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
126 <i class="icon-angle-down">more details</i>
126 <i class="icon-angle-down">more details</i>
127 </a>
127 </a>
128
128
129 </div>
129 </div>
130
130
131 <div class="source-details" style="display: none">
131 <div class="source-details" style="display: none">
132
132
133 <ul>
133 <ul>
134
134
135 ## common ancestor
135 ## common ancestor
136 <li>
136 <li>
137 ${_('Common ancestor')}:
137 ${_('Common ancestor')}:
138 % if c.ancestor_commit:
138 % if c.ancestor_commit:
139 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a>
139 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a>
140 % else:
140 % else:
141 ${_('not available')}
141 ${_('not available')}
142 % endif
142 % endif
143 </li>
143 </li>
144
144
145 ## pull url
145 ## pull url
146 <li>
146 <li>
147 %if h.is_hg(c.pull_request.source_repo):
147 %if h.is_hg(c.pull_request.source_repo):
148 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
148 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
149 %elif h.is_git(c.pull_request.source_repo):
149 %elif h.is_git(c.pull_request.source_repo):
150 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
150 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
151 %endif
151 %endif
152
152
153 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
153 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
154 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
154 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
155 </li>
155 </li>
156
156
157 ## Shadow repo
157 ## Shadow repo
158 <li>
158 <li>
159 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
159 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
160 %if h.is_hg(c.pull_request.target_repo):
160 %if h.is_hg(c.pull_request.target_repo):
161 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
161 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
162 %elif h.is_git(c.pull_request.target_repo):
162 %elif h.is_git(c.pull_request.target_repo):
163 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
163 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
164 %endif
164 %endif
165
165
166 <span class="tooltip" title="${_('Clone repository in its merged state using shadow repository')}">${_('Clone from shadow repository')}</span>: <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
166 <span class="tooltip" title="${_('Clone repository in its merged state using shadow repository')}">${_('Clone from shadow repository')}</span>: <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
167 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
167 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
168
168
169 % else:
169 % else:
170 <div class="">
170 <div class="">
171 ${_('Shadow repository data not available')}.
171 ${_('Shadow repository data not available')}.
172 </div>
172 </div>
173 % endif
173 % endif
174 </li>
174 </li>
175
175
176 </ul>
176 </ul>
177
177
178 </div>
178 </div>
179
179
180 </div>
180 </div>
181
181
182 </div>
182 </div>
183
183
184 ## versions
184 ## versions
185 <div class="field">
185 <div class="field">
186 <div class="label-pr-detail">
186 <div class="label-pr-detail">
187 <label>${_('Versions')}:</label>
187 <label>${_('Versions')}:</label>
188 </div>
188 </div>
189
189
190 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
190 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
191 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
191 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
192
192
193 <div class="pr-versions">
193 <div class="pr-versions">
194 % if c.show_version_changes:
194 % if c.show_version_changes:
195 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
195 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
196 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
196 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
197 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
197 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
198 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
198 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
199 data-toggle-on="${_('show versions')}."
199 data-toggle-on="${_('show versions')}."
200 data-toggle-off="${_('hide versions')}.">
200 data-toggle-off="${_('hide versions')}.">
201 ${_('show versions')}.
201 ${_('show versions')}.
202 </a>
202 </a>
203 <table>
203 <table>
204 ## SHOW ALL VERSIONS OF PR
204 ## SHOW ALL VERSIONS OF PR
205 <% ver_pr = None %>
205 <% ver_pr = None %>
206
206
207 % for data in reversed(list(enumerate(c.versions, 1))):
207 % for data in reversed(list(enumerate(c.versions, 1))):
208 <% ver_pos = data[0] %>
208 <% ver_pos = data[0] %>
209 <% ver = data[1] %>
209 <% ver = data[1] %>
210 <% ver_pr = ver.pull_request_version_id %>
210 <% ver_pr = ver.pull_request_version_id %>
211 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
211 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
212
212
213 <tr class="version-pr" style="display: ${display_row}">
213 <tr class="version-pr" style="display: ${display_row}">
214 <td>
214 <td>
215 <code>
215 <code>
216 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
216 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
217 </code>
217 </code>
218 </td>
218 </td>
219 <td>
219 <td>
220 <input ${('checked="checked"' if c.from_version_index == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
220 <input ${('checked="checked"' if c.from_version_index == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
221 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
221 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
222 </td>
222 </td>
223 <td>
223 <td>
224 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
224 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
225 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
225 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
226
226
227 </td>
227 </td>
228 <td>
228 <td>
229 % if c.at_version_num != ver_pr:
229 % if c.at_version_num != ver_pr:
230 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
230 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
231 <code>
231 <code>
232 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
232 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
233 </code>
233 </code>
234 % endif
234 % endif
235 </td>
235 </td>
236 <td>
236 <td>
237 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
237 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
238 </td>
238 </td>
239 <td>
239 <td>
240 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
240 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
241 </td>
241 </td>
242 </tr>
242 </tr>
243 % endfor
243 % endfor
244
244
245 <tr>
245 <tr>
246 <td colspan="6">
246 <td colspan="6">
247 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
247 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
248 data-label-text-locked="${_('select versions to show changes')}"
248 data-label-text-locked="${_('select versions to show changes')}"
249 data-label-text-diff="${_('show changes between versions')}"
249 data-label-text-diff="${_('show changes between versions')}"
250 data-label-text-show="${_('show pull request for this version')}"
250 data-label-text-show="${_('show pull request for this version')}"
251 >
251 >
252 ${_('select versions to show changes')}
252 ${_('select versions to show changes')}
253 </button>
253 </button>
254 </td>
254 </td>
255 </tr>
255 </tr>
256 </table>
256 </table>
257 % else:
257 % else:
258 <div>
258 <div>
259 ${_('Pull request versions not available')}.
259 ${_('Pull request versions not available')}.
260 </div>
260 </div>
261 % endif
261 % endif
262 </div>
262 </div>
263 </div>
263 </div>
264
264
265 </div>
265 </div>
266
266
267 </div>
267 </div>
268
268
269
269
270 </div>
270 </div>
271
271
272 </div>
272 </div>
273
273
274 <div class="box">
274 <div class="box">
275
275
276 % if c.state_progressing:
276 % if c.state_progressing:
277
277
278 <h2 style="text-align: center">
278 <h2 style="text-align: center">
279 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
279 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
280
280
281 % if c.is_super_admin:
281 % if c.is_super_admin:
282 <br/>
282 <br/>
283 If you think this is an error try <a href="${h.current_route_path(request, force_state='created')}">forced state reset</a> to <span class="tag tag-merge-state-created">created</span> state.
283 If you think this is an error try <a href="${h.current_route_path(request, force_state='created')}">forced state reset</a> to <span class="tag tag-merge-state-created">created</span> state.
284 % endif
284 % endif
285 </h2>
285 </h2>
286
286
287 % else:
287 % else:
288
288
289 ## Diffs rendered here
289 ## Diffs rendered here
290 <div class="table" >
290 <div class="table" >
291 <div id="changeset_compare_view_content">
291 <div id="changeset_compare_view_content">
292 ##CS
292 ##CS
293 % if c.missing_requirements:
293 % if c.missing_requirements:
294 <div class="box">
294 <div class="box">
295 <div class="alert alert-warning">
295 <div class="alert alert-warning">
296 <div>
296 <div>
297 <strong>${_('Missing requirements:')}</strong>
297 <strong>${_('Missing requirements:')}</strong>
298 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
298 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
299 </div>
299 </div>
300 </div>
300 </div>
301 </div>
301 </div>
302 % elif c.missing_commits:
302 % elif c.missing_commits:
303 <div class="box">
303 <div class="box">
304 <div class="alert alert-warning">
304 <div class="alert alert-warning">
305 <div>
305 <div>
306 <strong>${_('Missing commits')}:</strong>
306 <strong>${_('Missing commits')}:</strong>
307 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
307 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
308 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
308 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
309 ${_('Consider doing a `force update commits` in case you think this is an error.')}
309 ${_('Consider doing a `force update commits` in case you think this is an error.')}
310 </div>
310 </div>
311 </div>
311 </div>
312 </div>
312 </div>
313 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
313 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
314 <div class="box">
314 <div class="box">
315 <div class="alert alert-info">
315 <div class="alert alert-info">
316 <div>
316 <div>
317 <strong>${_('There are new changes for `{}:{}` in source repository, please consider updating this pull request.').format(c.pr_merge_source_commit.ref_spec.type, c.pr_merge_source_commit.ref_spec.name)}</strong>
317 <strong>${_('There are new changes for `{}:{}` in source repository, please consider updating this pull request.').format(c.pr_merge_source_commit.ref_spec.type, c.pr_merge_source_commit.ref_spec.name)}</strong>
318 </div>
318 </div>
319 </div>
319 </div>
320 </div>
320 </div>
321 % endif
321 % endif
322
322
323 <div class="compare_view_commits_title">
323 <div class="compare_view_commits_title">
324 % if not c.compare_mode:
324 % if not c.compare_mode:
325
325
326 % if c.at_version_index:
326 % if c.at_version_index:
327 <h4>
327 <h4>
328 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
328 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
329 </h4>
329 </h4>
330 % endif
330 % endif
331
331
332 <div class="pull-left">
332 <div class="pull-left">
333 <div class="btn-group">
333 <div class="btn-group">
334 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
334 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
335 % if c.collapse_all_commits:
335 % if c.collapse_all_commits:
336 <i class="icon-plus-squared-alt icon-no-margin"></i>
336 <i class="icon-plus-squared-alt icon-no-margin"></i>
337 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
337 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
338 % else:
338 % else:
339 <i class="icon-minus-squared-alt icon-no-margin"></i>
339 <i class="icon-minus-squared-alt icon-no-margin"></i>
340 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
340 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
341 % endif
341 % endif
342 </a>
342 </a>
343 </div>
343 </div>
344 </div>
344 </div>
345
345
346 <div class="pull-right">
346 <div class="pull-right">
347 % if c.allowed_to_update and not c.pull_request.is_closed():
347 % if c.allowed_to_update and not c.pull_request.is_closed():
348
348
349 <div class="btn-group btn-group-actions">
349 <div class="btn-group btn-group-actions">
350 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
350 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
351 ${_('Update commits')}
351 ${_('Update commits')}
352 </a>
352 </a>
353
353
354 <a id="update_commits_switcher" class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
354 <a id="update_commits_switcher" class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
355 <i class="icon-down"></i>
355 <i class="icon-down"></i>
356 </a>
356 </a>
357
357
358 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
358 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
359 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
359 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
360 <li>
360 <li>
361 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
361 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
362 ${_('Force update commits')}
362 ${_('Force update commits')}
363 </a>
363 </a>
364 <div class="action-help-block">
364 <div class="action-help-block">
365 ${_('Update commits and force refresh this pull request.')}
365 ${_('Update commits and force refresh this pull request.')}
366 </div>
366 </div>
367 </li>
367 </li>
368 </ul>
368 </ul>
369 </div>
369 </div>
370 </div>
370 </div>
371
371
372 % else:
372 % else:
373 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
373 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
374 % endif
374 % endif
375
375
376 </div>
376 </div>
377 % endif
377 % endif
378 </div>
378 </div>
379
379
380 % if not c.missing_commits:
380 % if not c.missing_commits:
381 ## COMPARE RANGE DIFF MODE
381 ## COMPARE RANGE DIFF MODE
382 % if c.compare_mode:
382 % if c.compare_mode:
383 % if c.at_version:
383 % if c.at_version:
384 <h4>
384 <h4>
385 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_index, ver_to=c.at_version_index if c.at_version_index else 'latest')}:
385 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_index, ver_to=c.at_version_index if c.at_version_index else 'latest')}:
386 </h4>
386 </h4>
387
387
388 <div class="subtitle-compare">
388 <div class="subtitle-compare">
389 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
389 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
390 </div>
390 </div>
391
391
392 <div class="container">
392 <div class="container">
393 <table class="rctable compare_view_commits">
393 <table class="rctable compare_view_commits">
394 <tr>
394 <tr>
395 <th></th>
395 <th></th>
396 <th>${_('Time')}</th>
396 <th>${_('Time')}</th>
397 <th>${_('Author')}</th>
397 <th>${_('Author')}</th>
398 <th>${_('Commit')}</th>
398 <th>${_('Commit')}</th>
399 <th></th>
399 <th></th>
400 <th>${_('Description')}</th>
400 <th>${_('Description')}</th>
401 </tr>
401 </tr>
402
402
403 % for c_type, commit in c.commit_changes:
403 % for c_type, commit in c.commit_changes:
404 % if c_type in ['a', 'r']:
404 % if c_type in ['a', 'r']:
405 <%
405 <%
406 if c_type == 'a':
406 if c_type == 'a':
407 cc_title = _('Commit added in displayed changes')
407 cc_title = _('Commit added in displayed changes')
408 elif c_type == 'r':
408 elif c_type == 'r':
409 cc_title = _('Commit removed in displayed changes')
409 cc_title = _('Commit removed in displayed changes')
410 else:
410 else:
411 cc_title = ''
411 cc_title = ''
412 %>
412 %>
413 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
413 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
414 <td>
414 <td>
415 <div class="commit-change-indicator color-${c_type}-border">
415 <div class="commit-change-indicator color-${c_type}-border">
416 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
416 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
417 ${c_type.upper()}
417 ${c_type.upper()}
418 </div>
418 </div>
419 </div>
419 </div>
420 </td>
420 </td>
421 <td class="td-time">
421 <td class="td-time">
422 ${h.age_component(commit.date)}
422 ${h.age_component(commit.date)}
423 </td>
423 </td>
424 <td class="td-user">
424 <td class="td-user">
425 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
425 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
426 </td>
426 </td>
427 <td class="td-hash">
427 <td class="td-hash">
428 <code>
428 <code>
429 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
429 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
430 r${commit.idx}:${h.short_id(commit.raw_id)}
430 r${commit.idx}:${h.short_id(commit.raw_id)}
431 </a>
431 </a>
432 ${h.hidden('revisions', commit.raw_id)}
432 ${h.hidden('revisions', commit.raw_id)}
433 </code>
433 </code>
434 </td>
434 </td>
435 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
435 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
436 <i class="icon-expand-linked"></i>
436 <i class="icon-expand-linked"></i>
437 </td>
437 </td>
438 <td class="mid td-description">
438 <td class="mid td-description">
439 <div class="log-container truncate-wrap">
439 <div class="log-container truncate-wrap">
440 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name, issues_container=c.referenced_commit_issues)}</div>
440 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name, issues_container=c.referenced_commit_issues)}</div>
441 </div>
441 </div>
442 </td>
442 </td>
443 </tr>
443 </tr>
444 % endif
444 % endif
445 % endfor
445 % endfor
446 </table>
446 </table>
447 </div>
447 </div>
448
448
449 % endif
449 % endif
450
450
451 ## Regular DIFF
451 ## Regular DIFF
452 % else:
452 % else:
453 <%include file="/compare/compare_commits.mako" />
453 <%include file="/compare/compare_commits.mako" />
454 % endif
454 % endif
455
455
456 <div class="cs_files">
456 <div class="cs_files">
457 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
457 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
458
458
459 <%
459 <%
460 pr_menu_data = {
460 pr_menu_data = {
461 'outdated_comm_count_ver': outdated_comm_count_ver,
461 'outdated_comm_count_ver': outdated_comm_count_ver,
462 'pull_request': c.pull_request
462 'pull_request': c.pull_request
463 }
463 }
464 %>
464 %>
465
465
466 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
466 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
467
467
468 % if c.range_diff_on:
468 % if c.range_diff_on:
469 % for commit in c.commit_ranges:
469 % for commit in c.commit_ranges:
470 ${cbdiffs.render_diffset(
470 ${cbdiffs.render_diffset(
471 c.changes[commit.raw_id],
471 c.changes[commit.raw_id],
472 commit=commit, use_comments=True,
472 commit=commit, use_comments=True,
473 collapse_when_files_over=5,
473 collapse_when_files_over=5,
474 disable_new_comments=True,
474 disable_new_comments=True,
475 deleted_files_comments=c.deleted_files_comments,
475 deleted_files_comments=c.deleted_files_comments,
476 inline_comments=c.inline_comments,
476 inline_comments=c.inline_comments,
477 pull_request_menu=pr_menu_data, show_todos=False)}
477 pull_request_menu=pr_menu_data, show_todos=False)}
478 % endfor
478 % endfor
479 % else:
479 % else:
480 ${cbdiffs.render_diffset(
480 ${cbdiffs.render_diffset(
481 c.diffset, use_comments=True,
481 c.diffset, use_comments=True,
482 collapse_when_files_over=30,
482 collapse_when_files_over=30,
483 disable_new_comments=not c.allowed_to_comment,
483 disable_new_comments=not c.allowed_to_comment,
484 deleted_files_comments=c.deleted_files_comments,
484 deleted_files_comments=c.deleted_files_comments,
485 inline_comments=c.inline_comments,
485 inline_comments=c.inline_comments,
486 pull_request_menu=pr_menu_data, show_todos=False)}
486 pull_request_menu=pr_menu_data, show_todos=False)}
487 % endif
487 % endif
488
488
489 </div>
489 </div>
490 % else:
490 % else:
491 ## skipping commits we need to clear the view for missing commits
491 ## skipping commits we need to clear the view for missing commits
492 <div style="clear:both;"></div>
492 <div style="clear:both;"></div>
493 % endif
493 % endif
494
494
495 </div>
495 </div>
496 </div>
496 </div>
497
497
498 ## template for inline comment form
498 ## template for inline comment form
499 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
499 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
500
500
501 ## comments heading with count
501 ## comments heading with count
502 <div class="comments-heading">
502 <div class="comments-heading">
503 <i class="icon-comment"></i>
503 <i class="icon-comment"></i>
504 ${_('General Comments')} ${len(c.comments)}
504 ${_('General Comments')} ${len(c.comments)}
505 </div>
505 </div>
506
506
507 ## render general comments
507 ## render general comments
508 <div id="comment-tr-show">
508 <div id="comment-tr-show">
509 % if general_outdated_comm_count_ver:
509 % if general_outdated_comm_count_ver:
510 <div class="info-box">
510 <div class="info-box">
511 % if general_outdated_comm_count_ver == 1:
511 % if general_outdated_comm_count_ver == 1:
512 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
512 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
513 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
513 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
514 % else:
514 % else:
515 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
515 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
516 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
516 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
517 % endif
517 % endif
518 </div>
518 </div>
519 % endif
519 % endif
520 </div>
520 </div>
521
521
522 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
522 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
523
523
524 % if not c.pull_request.is_closed():
524 % if not c.pull_request.is_closed():
525 ## main comment form and it status
525 ## main comment form and it status
526 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
526 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
527 pull_request_id=c.pull_request.pull_request_id),
527 pull_request_id=c.pull_request.pull_request_id),
528 c.pull_request_review_status,
528 c.pull_request_review_status,
529 is_pull_request=True, change_status=c.allowed_to_change_status)}
529 is_pull_request=True, change_status=c.allowed_to_change_status)}
530
530
531 ## merge status, and merge action
531 ## merge status, and merge action
532 <div class="pull-request-merge">
532 <div class="pull-request-merge">
533 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
533 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
534 </div>
534 </div>
535
535
536 %endif
536 %endif
537
537
538 % endif
538 % endif
539 </div>
539 </div>
540
540
541
541
542 ### NAV SIDEBAR
542 ### NAV SIDEBAR
543 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
543 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
544 <div class="sidenav navbar__inner" >
544 <div class="sidenav navbar__inner" >
545 ## TOGGLE
545 ## TOGGLE
546 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
546 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
547 <a href="#toggleSidebar" class="grey-link-action">
547 <a href="#toggleSidebar" class="grey-link-action">
548
548
549 </a>
549 </a>
550 </div>
550 </div>
551
551
552 ## CONTENT
552 ## CONTENT
553 <div class="sidebar-content">
553 <div class="sidebar-content">
554
554
555 ## RULES SUMMARY/RULES
555 ## RULES SUMMARY/RULES
556 <div class="sidebar-element clear-both">
556 <div class="sidebar-element clear-both">
557 <% vote_title = _ungettext(
557 <% vote_title = _ungettext(
558 'Status calculated based on votes from {} reviewer',
558 'Status calculated based on votes from {} reviewer',
559 'Status calculated based on votes from {} reviewers', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))
559 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
560 %>
560 %>
561
561
562 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
562 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
563 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
563 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
564 ${len(c.allowed_reviewers)}
564 ${c.reviewers_count}
565 </div>
565 </div>
566
566
567 ## REVIEW RULES
567 ## REVIEW RULES
568 <div id="review_rules" style="display: none" class="">
568 <div id="review_rules" style="display: none" class="">
569 <div class="right-sidebar-expanded-state pr-details-title">
569 <div class="right-sidebar-expanded-state pr-details-title">
570 <span class="sidebar-heading">
570 <span class="sidebar-heading">
571 ${_('Reviewer rules')}
571 ${_('Reviewer rules')}
572 </span>
572 </span>
573
573
574 </div>
574 </div>
575 <div class="pr-reviewer-rules">
575 <div class="pr-reviewer-rules">
576 ## review rules will be appended here, by default reviewers logic
576 ## review rules will be appended here, by default reviewers logic
577 </div>
577 </div>
578 <input id="review_data" type="hidden" name="review_data" value="">
578 <input id="review_data" type="hidden" name="review_data" value="">
579 </div>
579 </div>
580
580
581 ## REVIEWERS
581 ## REVIEWERS
582 <div class="right-sidebar-expanded-state pr-details-title">
582 <div class="right-sidebar-expanded-state pr-details-title">
583 <span class="tooltip sidebar-heading" title="${vote_title}">
583 <span class="tooltip sidebar-heading" title="${vote_title}">
584 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
584 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
585 ${_('Reviewers')}
585 ${_('Reviewers')}
586 </span>
586 </span>
587 %if c.allowed_to_update:
587 %if c.allowed_to_update:
588 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
588 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
589 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
589 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
590 %else:
590 %else:
591 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
591 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
592 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
592 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
593 %endif
593 %endif
594 </div>
594 </div>
595
595
596 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
596 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
597
597
598 ## members redering block
598 ## members redering block
599 <input type="hidden" name="__start__" value="review_members:sequence">
599 <input type="hidden" name="__start__" value="review_members:sequence">
600
600
601 <table id="review_members" class="group_members">
601 <table id="review_members" class="group_members">
602 ## This content is loaded via JS and ReviewersPanel
602 ## This content is loaded via JS and ReviewersPanel
603 </table>
603 </table>
604
604
605 <input type="hidden" name="__end__" value="review_members:sequence">
605 <input type="hidden" name="__end__" value="review_members:sequence">
606 ## end members redering block
606 ## end members redering block
607
607
608 %if not c.pull_request.is_closed():
608 %if not c.pull_request.is_closed():
609 <div id="add_reviewer" class="ac" style="display: none;">
609 <div id="add_reviewer" class="ac" style="display: none;">
610 %if c.allowed_to_update:
610 %if c.allowed_to_update:
611 % if not c.forbid_adding_reviewers:
611 % if not c.forbid_adding_reviewers:
612 <div id="add_reviewer_input" class="reviewer_ac">
612 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
613 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
613 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
614 <div id="reviewers_container"></div>
614 <div id="reviewers_container"></div>
615 </div>
615 </div>
616 % endif
616 % endif
617 <div class="pull-right">
617 <div class="pull-right" style="margin-bottom: 15px">
618 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
618 <button data-role="reviewer" id="update_reviewers" class="btn btn-small no-margin">${_('Save Changes')}</button>
619 </div>
619 </div>
620 %endif
620 %endif
621 </div>
621 </div>
622 %endif
622 %endif
623 </div>
623 </div>
624 </div>
624 </div>
625
625
626 ## ## OBSERVERS
626 ## OBSERVERS
627 ## <div class="sidebar-element clear-both">
627 <div class="sidebar-element clear-both">
628 ## <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
628 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
629 ## <i class="icon-eye"></i>
629 <i class="icon-circle-thin"></i>
630 ## 0
630 ${c.observers_count}
631 ## </div>
631 </div>
632 ##
632
633 ## <div class="right-sidebar-expanded-state pr-details-title">
633 <div class="right-sidebar-expanded-state pr-details-title">
634 ## <span class="sidebar-heading">
634 <span class="sidebar-heading">
635 ## <i class="icon-eye"></i>
635 <i class="icon-circle-thin"></i>
636 ## ${_('Observers')}
636 ${_('Observers')}
637 ## </span>
637 </span>
638 ## </div>
638 %if c.allowed_to_update:
639 ## <div class="right-sidebar-expanded-state pr-details-content">
639 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
640 ## No observers
640 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
641 ## </div>
641 %endif
642 ## </div>
642 </div>
643
644 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
645 ## members redering block
646 <input type="hidden" name="__start__" value="observer_members:sequence">
647
648 <table id="observer_members" class="group_members">
649 ## This content is loaded via JS and ReviewersPanel
650 </table>
651
652 <input type="hidden" name="__end__" value="observer_members:sequence">
653 ## end members redering block
654
655 %if not c.pull_request.is_closed():
656 <div id="add_observer" class="ac" style="display: none;">
657 %if c.allowed_to_update:
658 % if not c.forbid_adding_reviewers or 1:
659 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
660 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
661 <div id="observers_container"></div>
662 </div>
663 % endif
664 <div class="pull-right" style="margin-bottom: 15px">
665 <button data-role="observer" id="update_observers" class="btn btn-small no-margin">${_('Save Changes')}</button>
666 </div>
667 %endif
668 </div>
669 %endif
670 </div>
671 </div>
643
672
644 ## TODOs
673 ## TODOs
645 <div class="sidebar-element clear-both">
674 <div class="sidebar-element clear-both">
646 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
675 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
647 <i class="icon-flag-filled"></i>
676 <i class="icon-flag-filled"></i>
648 <span id="todos-count">${len(c.unresolved_comments)}</span>
677 <span id="todos-count">${len(c.unresolved_comments)}</span>
649 </div>
678 </div>
650
679
651 <div class="right-sidebar-expanded-state pr-details-title">
680 <div class="right-sidebar-expanded-state pr-details-title">
652 ## Only show unresolved, that is only what matters
681 ## Only show unresolved, that is only what matters
653 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
682 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
654 <i class="icon-flag-filled"></i>
683 <i class="icon-flag-filled"></i>
655 TODOs
684 TODOs
656 </span>
685 </span>
657
686
658 % if not c.at_version:
687 % if not c.at_version:
659 % if c.resolved_comments:
688 % if c.resolved_comments:
660 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
689 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
661 % else:
690 % else:
662 <span class="block-right last-item noselect">Show resolved</span>
691 <span class="block-right last-item noselect">Show resolved</span>
663 % endif
692 % endif
664 % endif
693 % endif
665 </div>
694 </div>
666
695
667 <div class="right-sidebar-expanded-state pr-details-content">
696 <div class="right-sidebar-expanded-state pr-details-content">
668
697
669 % if c.at_version:
698 % if c.at_version:
670 <table>
699 <table>
671 <tr>
700 <tr>
672 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
701 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
673 </tr>
702 </tr>
674 </table>
703 </table>
675 % else:
704 % else:
676 % if c.unresolved_comments + c.resolved_comments:
705 % if c.unresolved_comments + c.resolved_comments:
677 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
706 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
678 % else:
707 % else:
679 <table>
708 <table>
680 <tr>
709 <tr>
681 <td>
710 <td>
682 ${_('No TODOs yet')}
711 ${_('No TODOs yet')}
683 </td>
712 </td>
684 </tr>
713 </tr>
685 </table>
714 </table>
686 % endif
715 % endif
687 % endif
716 % endif
688 </div>
717 </div>
689 </div>
718 </div>
690
719
691 ## COMMENTS
720 ## COMMENTS
692 <div class="sidebar-element clear-both">
721 <div class="sidebar-element clear-both">
693 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
722 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
694 <i class="icon-comment" style="color: #949494"></i>
723 <i class="icon-comment" style="color: #949494"></i>
695 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
724 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
696 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
725 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
697 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
726 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
698 </div>
727 </div>
699
728
700 <div class="right-sidebar-expanded-state pr-details-title">
729 <div class="right-sidebar-expanded-state pr-details-title">
701 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
730 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
702 <i class="icon-comment" style="color: #949494"></i>
731 <i class="icon-comment" style="color: #949494"></i>
703 ${_('Comments')}
732 ${_('Comments')}
704
733
705 ## % if outdated_comm_count_ver:
734 ## % if outdated_comm_count_ver:
706 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
735 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
707 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
736 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
708 ## </a>
737 ## </a>
709 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
738 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
710 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
739 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
711
740
712 ## % else:
741 ## % else:
713 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
742 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
714 ## % endif
743 ## % endif
715
744
716 </span>
745 </span>
717
746
718 % if outdated_comm_count_ver:
747 % if outdated_comm_count_ver:
719 <span class="block-right action_button last-item noselect" onclick="return toggleElement(this, '.hidden-comment');" data-toggle-on="Show outdated" data-toggle-off="Hide outdated">Show outdated</span>
748 <span class="block-right action_button last-item noselect" onclick="return toggleElement(this, '.hidden-comment');" data-toggle-on="Show outdated" data-toggle-off="Hide outdated">Show outdated</span>
720 % else:
749 % else:
721 <span class="block-right last-item noselect">Show hidden</span>
750 <span class="block-right last-item noselect">Show hidden</span>
722 % endif
751 % endif
723
752
724 </div>
753 </div>
725
754
726 <div class="right-sidebar-expanded-state pr-details-content">
755 <div class="right-sidebar-expanded-state pr-details-content">
727 % if c.inline_comments_flat + c.comments:
756 % if c.inline_comments_flat + c.comments:
728 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
757 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
729 % else:
758 % else:
730 <table>
759 <table>
731 <tr>
760 <tr>
732 <td>
761 <td>
733 ${_('No Comments yet')}
762 ${_('No Comments yet')}
734 </td>
763 </td>
735 </tr>
764 </tr>
736 </table>
765 </table>
737 % endif
766 % endif
738 </div>
767 </div>
739
768
740 </div>
769 </div>
741
770
742 ## Referenced Tickets
771 ## Referenced Tickets
743 <div class="sidebar-element clear-both">
772 <div class="sidebar-element clear-both">
744 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
773 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
745 <i class="icon-info-circled"></i>
774 <i class="icon-info-circled"></i>
746 ${(len(c.referenced_desc_issues) + len(c.referenced_commit_issues))}
775 ${(len(c.referenced_desc_issues) + len(c.referenced_commit_issues))}
747 </div>
776 </div>
748
777
749 <div class="right-sidebar-expanded-state pr-details-title">
778 <div class="right-sidebar-expanded-state pr-details-title">
750 <span class="sidebar-heading">
779 <span class="sidebar-heading">
751 <i class="icon-info-circled"></i>
780 <i class="icon-info-circled"></i>
752 ${_('Referenced Tickets')}
781 ${_('Referenced Tickets')}
753 </span>
782 </span>
754 </div>
783 </div>
755 <div class="right-sidebar-expanded-state pr-details-content">
784 <div class="right-sidebar-expanded-state pr-details-content">
756 <table>
785 <table>
757
786
758 <tr><td><code>${_('In pull request description')}:</code></td></tr>
787 <tr><td><code>${_('In pull request description')}:</code></td></tr>
759 % if c.referenced_desc_issues:
788 % if c.referenced_desc_issues:
760 % for ticket_dict in c.referenced_desc_issues:
789 % for ticket_dict in c.referenced_desc_issues:
761 <tr>
790 <tr>
762 <td>
791 <td>
763 <a href="${ticket_dict.get('url')}">
792 <a href="${ticket_dict.get('url')}">
764 ${ticket_dict.get('id')}
793 ${ticket_dict.get('id')}
765 </a>
794 </a>
766 </td>
795 </td>
767 </tr>
796 </tr>
768 % endfor
797 % endfor
769 % else:
798 % else:
770 <tr>
799 <tr>
771 <td>
800 <td>
772 ${_('No Ticket data found.')}
801 ${_('No Ticket data found.')}
773 </td>
802 </td>
774 </tr>
803 </tr>
775 % endif
804 % endif
776
805
777 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
806 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
778 % if c.referenced_commit_issues:
807 % if c.referenced_commit_issues:
779 % for ticket_dict in c.referenced_commit_issues:
808 % for ticket_dict in c.referenced_commit_issues:
780 <tr>
809 <tr>
781 <td>
810 <td>
782 <a href="${ticket_dict.get('url')}">
811 <a href="${ticket_dict.get('url')}">
783 ${ticket_dict.get('id')}
812 ${ticket_dict.get('id')}
784 </a>
813 </a>
785 </td>
814 </td>
786 </tr>
815 </tr>
787 % endfor
816 % endfor
788 % else:
817 % else:
789 <tr>
818 <tr>
790 <td>
819 <td>
791 ${_('No Ticket data found.')}
820 ${_('No Ticket data found.')}
792 </td>
821 </td>
793 </tr>
822 </tr>
794 % endif
823 % endif
795 </table>
824 </table>
796
825
797 </div>
826 </div>
798 </div>
827 </div>
799
828
800 </div>
829 </div>
801
830
802 </div>
831 </div>
803 </aside>
832 </aside>
804
833
805 ## This JS needs to be at the end
834 ## This JS needs to be at the end
806 <script type="text/javascript">
835 <script type="text/javascript">
807
836
808 versionController = new VersionController();
837 versionController = new VersionController();
809 versionController.init();
838 versionController.init();
810
839
811 reviewersController = new ReviewersController();
840 reviewersController = new ReviewersController();
812 commitsController = new CommitsController();
841 commitsController = new CommitsController();
813
842
814 updateController = new UpdatePrController();
843 updateController = new UpdatePrController();
815
844
816 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
845 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
817 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
846 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
847 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
818
848
819 (function () {
849 (function () {
820 "use strict";
850 "use strict";
821
851
822 // custom code mirror
852 // custom code mirror
823 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
853 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
824
854
825 var PRDetails = {
826 editButton: $('#open_edit_pullrequest'),
827 closeButton: $('#close_edit_pullrequest'),
828 deleteButton: $('#delete_pullrequest'),
829 viewFields: $('#pr-desc, #pr-title'),
830 editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
831
832 init: function () {
833 var that = this;
834 this.editButton.on('click', function (e) {
835 that.edit();
836 });
837 this.closeButton.on('click', function (e) {
838 that.view();
839 });
840 },
841
842 edit: function (event) {
843 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
844 this.viewFields.hide();
845 this.editButton.hide();
846 this.deleteButton.hide();
847 this.closeButton.show();
848 this.editFields.show();
849 cmInstance.refresh();
850 },
851
852 view: function (event) {
853 this.editButton.show();
854 this.deleteButton.show();
855 this.editFields.hide();
856 this.closeButton.hide();
857 this.viewFields.show();
858 }
859 };
860
861 PRDetails.init();
855 PRDetails.init();
862 ReviewersPanel.init(reviewerRulesData, setReviewersData);
856 ReviewersPanel.init(reviewerRulesData, setReviewersData);
857 ObserversPanel.init(reviewerRulesData, setObserversData);
863
858
864 window.showOutdated = function (self) {
859 window.showOutdated = function (self) {
865 $('.comment-inline.comment-outdated').show();
860 $('.comment-inline.comment-outdated').show();
866 $('.filediff-outdated').show();
861 $('.filediff-outdated').show();
867 $('.showOutdatedComments').hide();
862 $('.showOutdatedComments').hide();
868 $('.hideOutdatedComments').show();
863 $('.hideOutdatedComments').show();
869 };
864 };
870
865
871 window.hideOutdated = function (self) {
866 window.hideOutdated = function (self) {
872 $('.comment-inline.comment-outdated').hide();
867 $('.comment-inline.comment-outdated').hide();
873 $('.filediff-outdated').hide();
868 $('.filediff-outdated').hide();
874 $('.hideOutdatedComments').hide();
869 $('.hideOutdatedComments').hide();
875 $('.showOutdatedComments').show();
870 $('.showOutdatedComments').show();
876 };
871 };
877
872
878 window.refreshMergeChecks = function () {
873 window.refreshMergeChecks = function () {
879 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
874 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
880 $('.pull-request-merge').css('opacity', 0.3);
875 $('.pull-request-merge').css('opacity', 0.3);
881 $('.action-buttons-extra').css('opacity', 0.3);
876 $('.action-buttons-extra').css('opacity', 0.3);
882
877
883 $('.pull-request-merge').load(
878 $('.pull-request-merge').load(
884 loadUrl, function () {
879 loadUrl, function () {
885 $('.pull-request-merge').css('opacity', 1);
880 $('.pull-request-merge').css('opacity', 1);
886
881
887 $('.action-buttons-extra').css('opacity', 1);
882 $('.action-buttons-extra').css('opacity', 1);
888 }
883 }
889 );
884 );
890 };
885 };
891
886
892 window.closePullRequest = function (status) {
887 window.closePullRequest = function (status) {
893 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
888 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
894 return false;
889 return false;
895 }
890 }
896 // inject closing flag
891 // inject closing flag
897 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
892 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
898 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
893 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
899 $(generalCommentForm.submitForm).submit();
894 $(generalCommentForm.submitForm).submit();
900 };
895 };
901
896
902 //TODO this functionality is now missing
897 //TODO this functionality is now missing
903 $('#show-outdated-comments').on('click', function (e) {
898 $('#show-outdated-comments').on('click', function (e) {
904 var button = $(this);
899 var button = $(this);
905 var outdated = $('.comment-outdated');
900 var outdated = $('.comment-outdated');
906
901
907 if (button.html() === "(Show)") {
902 if (button.html() === "(Show)") {
908 button.html("(Hide)");
903 button.html("(Hide)");
909 outdated.show();
904 outdated.show();
910 } else {
905 } else {
911 button.html("(Show)");
906 button.html("(Show)");
912 outdated.hide();
907 outdated.hide();
913 }
908 }
914 });
909 });
915
910
916 $('#merge_pull_request_form').submit(function () {
911 $('#merge_pull_request_form').submit(function () {
917 if (!$('#merge_pull_request').attr('disabled')) {
912 if (!$('#merge_pull_request').attr('disabled')) {
918 $('#merge_pull_request').attr('disabled', 'disabled');
913 $('#merge_pull_request').attr('disabled', 'disabled');
919 }
914 }
920 return true;
915 return true;
921 });
916 });
922
917
923 $('#edit_pull_request').on('click', function (e) {
918 $('#edit_pull_request').on('click', function (e) {
924 var title = $('#pr-title-input').val();
919 var title = $('#pr-title-input').val();
925 var description = codeMirrorInstance.getValue();
920 var description = codeMirrorInstance.getValue();
926 var renderer = $('#pr-renderer-input').val();
921 var renderer = $('#pr-renderer-input').val();
927 editPullRequest(
922 editPullRequest(
928 "${c.repo_name}", "${c.pull_request.pull_request_id}",
923 "${c.repo_name}", "${c.pull_request.pull_request_id}",
929 title, description, renderer);
924 title, description, renderer);
930 });
925 });
931
926
932 $('#update_pull_request').on('click', function (e) {
927 var $updateButtons = $('#update_reviewers,#update_observers');
933 $(this).attr('disabled', 'disabled');
928 $updateButtons.on('click', function (e) {
934 $(this).addClass('disabled');
929 var role = $(this).data('role');
935 $(this).html(_gettext('Saving...'));
930 $updateButtons.attr('disabled', 'disabled');
931 $updateButtons.addClass('disabled');
932 $updateButtons.html(_gettext('Saving...'));
936 reviewersController.updateReviewers(
933 reviewersController.updateReviewers(
937 "${c.repo_name}", "${c.pull_request.pull_request_id}");
934 templateContext.repo_name,
935 templateContext.pull_request_data.pull_request_id,
936 role
937 );
938 });
938 });
939
939
940 // fixing issue with caches on firefox
940 // fixing issue with caches on firefox
941 $('#update_commits').removeAttr("disabled");
941 $('#update_commits').removeAttr("disabled");
942
942
943 $('.show-inline-comments').on('click', function (e) {
943 $('.show-inline-comments').on('click', function (e) {
944 var boxid = $(this).attr('data-comment-id');
944 var boxid = $(this).attr('data-comment-id');
945 var button = $(this);
945 var button = $(this);
946
946
947 if (button.hasClass("comments-visible")) {
947 if (button.hasClass("comments-visible")) {
948 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
948 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
949 $(this).hide();
949 $(this).hide();
950 });
950 });
951 button.removeClass("comments-visible");
951 button.removeClass("comments-visible");
952 } else {
952 } else {
953 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
953 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
954 $(this).show();
954 $(this).show();
955 });
955 });
956 button.addClass("comments-visible");
956 button.addClass("comments-visible");
957 }
957 }
958 });
958 });
959
959
960 $('.show-inline-comments').on('change', function (e) {
960 $('.show-inline-comments').on('change', function (e) {
961 var show = 'none';
961 var show = 'none';
962 var target = e.currentTarget;
962 var target = e.currentTarget;
963 if (target.checked) {
963 if (target.checked) {
964 show = ''
964 show = ''
965 }
965 }
966 var boxid = $(target).attr('id_for');
966 var boxid = $(target).attr('id_for');
967 var comments = $('#{0} .inline-comments'.format(boxid));
967 var comments = $('#{0} .inline-comments'.format(boxid));
968 var fn_display = function (idx) {
968 var fn_display = function (idx) {
969 $(this).css('display', show);
969 $(this).css('display', show);
970 };
970 };
971 $(comments).each(fn_display);
971 $(comments).each(fn_display);
972 var btns = $('#{0} .inline-comments-button'.format(boxid));
972 var btns = $('#{0} .inline-comments-button'.format(boxid));
973 $(btns).each(fn_display);
973 $(btns).each(fn_display);
974 });
974 });
975
975
976 // register submit callback on commentForm form to track TODOs
976 // register submit callback on commentForm form to track TODOs
977 window.commentFormGlobalSubmitSuccessCallback = function () {
977 window.commentFormGlobalSubmitSuccessCallback = function () {
978 refreshMergeChecks();
978 refreshMergeChecks();
979 };
979 };
980
980
981 ReviewerAutoComplete('#user');
981 ReviewerAutoComplete('#user', reviewersController);
982 ObserverAutoComplete('#observer', reviewersController);
982
983
983 })();
984 })();
984
985
985 $(document).ready(function () {
986 $(document).ready(function () {
986
987
987 var channel = '${c.pr_broadcast_channel}';
988 var channel = '${c.pr_broadcast_channel}';
988 new ReviewerPresenceController(channel)
989 new ReviewerPresenceController(channel)
989
990
990 })
991 })
991 </script>
992 </script>
992
993
993 </%def>
994 </%def>
@@ -1,190 +1,195 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 pytest
21 import pytest
22 import collections
22 import collections
23
23
24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
25 from rhodecode.lib.utils2 import AttributeDict
25 from rhodecode.lib.utils2 import AttributeDict
26 from rhodecode.model.db import User
26 from rhodecode.model.db import User, PullRequestReviewers
27 from rhodecode.model.notification import EmailNotificationModel
27 from rhodecode.model.notification import EmailNotificationModel
28
28
29
29
30 def test_get_template_obj(app, request_stub):
30 def test_get_template_obj(app, request_stub):
31 template = EmailNotificationModel().get_renderer(
31 template = EmailNotificationModel().get_renderer(
32 EmailNotificationModel.TYPE_TEST, request_stub)
32 EmailNotificationModel.TYPE_TEST, request_stub)
33 assert isinstance(template, PyramidPartialRenderer)
33 assert isinstance(template, PyramidPartialRenderer)
34
34
35
35
36 def test_render_email(app, http_host_only_stub):
36 def test_render_email(app, http_host_only_stub):
37 kwargs = {}
37 kwargs = {}
38 subject, body, body_plaintext = EmailNotificationModel().render_email(
38 subject, body, body_plaintext = EmailNotificationModel().render_email(
39 EmailNotificationModel.TYPE_TEST, **kwargs)
39 EmailNotificationModel.TYPE_TEST, **kwargs)
40
40
41 # subject
41 # subject
42 assert subject == 'Test "Subject" hello "world"'
42 assert subject == 'Test "Subject" hello "world"'
43
43
44 # body plaintext
44 # body plaintext
45 assert body_plaintext == 'Email Plaintext Body'
45 assert body_plaintext == 'Email Plaintext Body'
46
46
47 # body
47 # body
48 notification_footer1 = 'This is a notification from RhodeCode.'
48 notification_footer1 = 'This is a notification from RhodeCode.'
49 notification_footer2 = 'http://{}/'.format(http_host_only_stub)
49 notification_footer2 = 'http://{}/'.format(http_host_only_stub)
50 assert notification_footer1 in body
50 assert notification_footer1 in body
51 assert notification_footer2 in body
51 assert notification_footer2 in body
52 assert 'Email Body' in body
52 assert 'Email Body' in body
53
53
54
54
55 def test_render_pr_email(app, user_admin):
55 @pytest.mark.parametrize('role', PullRequestReviewers.ROLES)
56 def test_render_pr_email(app, user_admin, role):
56 ref = collections.namedtuple(
57 ref = collections.namedtuple(
57 'Ref', 'name, type')('fxies123', 'book')
58 'Ref', 'name, type')('fxies123', 'book')
58
59
59 pr = collections.namedtuple('PullRequest',
60 pr = collections.namedtuple('PullRequest',
60 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
61 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
61 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
62 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
62
63
63 source_repo = target_repo = collections.namedtuple(
64 source_repo = target_repo = collections.namedtuple(
64 'Repo', 'type, repo_name')('hg', 'pull_request_1')
65 'Repo', 'type, repo_name')('hg', 'pull_request_1')
65
66
66 kwargs = {
67 kwargs = {
67 'user': User.get_first_super_admin(),
68 'user': User.get_first_super_admin(),
68 'pull_request': pr,
69 'pull_request': pr,
69 'pull_request_commits': [],
70 'pull_request_commits': [],
70
71
71 'pull_request_target_repo': target_repo,
72 'pull_request_target_repo': target_repo,
72 'pull_request_target_repo_url': 'x',
73 'pull_request_target_repo_url': 'x',
73
74
74 'pull_request_source_repo': source_repo,
75 'pull_request_source_repo': source_repo,
75 'pull_request_source_repo_url': 'x',
76 'pull_request_source_repo_url': 'x',
76
77
77 'pull_request_url': 'http://localhost/pr1',
78 'pull_request_url': 'http://localhost/pr1',
79 'user_role': role,
78 }
80 }
79
81
80 subject, body, body_plaintext = EmailNotificationModel().render_email(
82 subject, body, body_plaintext = EmailNotificationModel().render_email(
81 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
83 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
82
84
83 # subject
85 # subject
84 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
86 if role == PullRequestReviewers.ROLE_REVIEWER:
87 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
88 elif role == PullRequestReviewers.ROLE_OBSERVER:
89 assert subject == '@test_admin (RhodeCode Admin) added you as observer to pull request. !200: "Example Pull Request"'
85
90
86
91
87 def test_render_pr_update_email(app, user_admin):
92 def test_render_pr_update_email(app, user_admin):
88 ref = collections.namedtuple(
93 ref = collections.namedtuple(
89 'Ref', 'name, type')('fxies123', 'book')
94 'Ref', 'name, type')('fxies123', 'book')
90
95
91 pr = collections.namedtuple('PullRequest',
96 pr = collections.namedtuple('PullRequest',
92 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
97 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
93 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
98 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
94
99
95 source_repo = target_repo = collections.namedtuple(
100 source_repo = target_repo = collections.namedtuple(
96 'Repo', 'type, repo_name')('hg', 'pull_request_1')
101 'Repo', 'type, repo_name')('hg', 'pull_request_1')
97
102
98 commit_changes = AttributeDict({
103 commit_changes = AttributeDict({
99 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
104 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
100 'removed': ['eeeeeeeeeee'],
105 'removed': ['eeeeeeeeeee'],
101 })
106 })
102 file_changes = AttributeDict({
107 file_changes = AttributeDict({
103 'added': ['a/file1.md', 'file2.py'],
108 'added': ['a/file1.md', 'file2.py'],
104 'modified': ['b/modified_file.rst'],
109 'modified': ['b/modified_file.rst'],
105 'removed': ['.idea'],
110 'removed': ['.idea'],
106 })
111 })
107
112
108 kwargs = {
113 kwargs = {
109 'updating_user': User.get_first_super_admin(),
114 'updating_user': User.get_first_super_admin(),
110
115
111 'pull_request': pr,
116 'pull_request': pr,
112 'pull_request_commits': [],
117 'pull_request_commits': [],
113
118
114 'pull_request_target_repo': target_repo,
119 'pull_request_target_repo': target_repo,
115 'pull_request_target_repo_url': 'x',
120 'pull_request_target_repo_url': 'x',
116
121
117 'pull_request_source_repo': source_repo,
122 'pull_request_source_repo': source_repo,
118 'pull_request_source_repo_url': 'x',
123 'pull_request_source_repo_url': 'x',
119
124
120 'pull_request_url': 'http://localhost/pr1',
125 'pull_request_url': 'http://localhost/pr1',
121
126
122 'pr_comment_url': 'http://comment-url',
127 'pr_comment_url': 'http://comment-url',
123 'pr_comment_reply_url': 'http://comment-url#reply',
128 'pr_comment_reply_url': 'http://comment-url#reply',
124 'ancestor_commit_id': 'f39bd443',
129 'ancestor_commit_id': 'f39bd443',
125 'added_commits': commit_changes.added,
130 'added_commits': commit_changes.added,
126 'removed_commits': commit_changes.removed,
131 'removed_commits': commit_changes.removed,
127 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
132 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
128 'added_files': file_changes.added,
133 'added_files': file_changes.added,
129 'modified_files': file_changes.modified,
134 'modified_files': file_changes.modified,
130 'removed_files': file_changes.removed,
135 'removed_files': file_changes.removed,
131 }
136 }
132
137
133 subject, body, body_plaintext = EmailNotificationModel().render_email(
138 subject, body, body_plaintext = EmailNotificationModel().render_email(
134 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **kwargs)
139 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **kwargs)
135
140
136 # subject
141 # subject
137 assert subject == '@test_admin (RhodeCode Admin) updated pull request. !200: "Example Pull Request"'
142 assert subject == '@test_admin (RhodeCode Admin) updated pull request. !200: "Example Pull Request"'
138
143
139
144
140 @pytest.mark.parametrize('mention', [
145 @pytest.mark.parametrize('mention', [
141 True,
146 True,
142 False
147 False
143 ])
148 ])
144 @pytest.mark.parametrize('email_type', [
149 @pytest.mark.parametrize('email_type', [
145 EmailNotificationModel.TYPE_COMMIT_COMMENT,
150 EmailNotificationModel.TYPE_COMMIT_COMMENT,
146 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
151 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
147 ])
152 ])
148 def test_render_comment_subject_no_newlines(app, mention, email_type):
153 def test_render_comment_subject_no_newlines(app, mention, email_type):
149 ref = collections.namedtuple(
154 ref = collections.namedtuple(
150 'Ref', 'name, type')('fxies123', 'book')
155 'Ref', 'name, type')('fxies123', 'book')
151
156
152 pr = collections.namedtuple('PullRequest',
157 pr = collections.namedtuple('PullRequest',
153 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
158 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
154 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
159 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
155
160
156 source_repo = target_repo = collections.namedtuple(
161 source_repo = target_repo = collections.namedtuple(
157 'Repo', 'type, repo_name')('hg', 'pull_request_1')
162 'Repo', 'type, repo_name')('hg', 'pull_request_1')
158
163
159 kwargs = {
164 kwargs = {
160 'user': User.get_first_super_admin(),
165 'user': User.get_first_super_admin(),
161 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
166 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
162 'status_change': 'approved',
167 'status_change': 'approved',
163 'commit_target_repo_url': 'http://foo.example.com/#comment1',
168 'commit_target_repo_url': 'http://foo.example.com/#comment1',
164 'repo_name': 'test-repo',
169 'repo_name': 'test-repo',
165 'comment_file': 'test-file.py',
170 'comment_file': 'test-file.py',
166 'comment_line': 'n100',
171 'comment_line': 'n100',
167 'comment_type': 'note',
172 'comment_type': 'note',
168 'comment_id': 2048,
173 'comment_id': 2048,
169 'commit_comment_url': 'http://comment-url',
174 'commit_comment_url': 'http://comment-url',
170 'commit_comment_reply_url': 'http://comment-url/#Reply',
175 'commit_comment_reply_url': 'http://comment-url/#Reply',
171 'instance_url': 'http://rc-instance',
176 'instance_url': 'http://rc-instance',
172 'comment_body': 'hello world',
177 'comment_body': 'hello world',
173 'mention': mention,
178 'mention': mention,
174
179
175 'pr_comment_url': 'http://comment-url',
180 'pr_comment_url': 'http://comment-url',
176 'pr_comment_reply_url': 'http://comment-url/#Reply',
181 'pr_comment_reply_url': 'http://comment-url/#Reply',
177 'pull_request': pr,
182 'pull_request': pr,
178 'pull_request_commits': [],
183 'pull_request_commits': [],
179
184
180 'pull_request_target_repo': target_repo,
185 'pull_request_target_repo': target_repo,
181 'pull_request_target_repo_url': 'x',
186 'pull_request_target_repo_url': 'x',
182
187
183 'pull_request_source_repo': source_repo,
188 'pull_request_source_repo': source_repo,
184 'pull_request_source_repo_url': 'x',
189 'pull_request_source_repo_url': 'x',
185
190
186 'pull_request_url': 'http://code.rc.com/_pr/123'
191 'pull_request_url': 'http://code.rc.com/_pr/123'
187 }
192 }
188 subject, body, body_plaintext = EmailNotificationModel().render_email(email_type, **kwargs)
193 subject, body, body_plaintext = EmailNotificationModel().render_email(email_type, **kwargs)
189
194
190 assert '\n' not in subject
195 assert '\n' not in subject
General Comments 0
You need to be logged in to leave comments. Login now