##// END OF EJS Templates
pull-requests: added retry mechanism for updating pull requests.
super-admin -
r4696:7a5e2fc4 stable
parent child Browse files
Show More

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

@@ -1,1868 +1,1872 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
29
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, aslist
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist, retry
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
51 PullRequestReviewers)
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 # backward compat., we use for OLD PRs a plain renderer
66 # backward compat., we use for OLD PRs a plain renderer
67 c.renderer = 'plain'
67 c.renderer = 'plain'
68 return c
68 return c
69
69
70 def _get_pull_requests_list(
70 def _get_pull_requests_list(
71 self, repo_name, source, filter_type, opened_by, statuses):
71 self, repo_name, source, filter_type, opened_by, statuses):
72
72
73 draw, start, limit = self._extract_chunk(self.request)
73 draw, start, limit = self._extract_chunk(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 _render = self.request.get_partial_renderer(
75 _render = self.request.get_partial_renderer(
76 'rhodecode:templates/data_table/_dt_elements.mako')
76 'rhodecode:templates/data_table/_dt_elements.mako')
77
77
78 # pagination
78 # pagination
79
79
80 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name,
82 repo_name,
83 search_q=search_q, statuses=statuses,
83 search_q=search_q, statuses=statuses,
84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name,
86 repo_name,
87 search_q=search_q, statuses=statuses)
87 search_q=search_q, statuses=statuses)
88 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, self._rhodecode_user.user_id,
90 repo_name, self._rhodecode_user.user_id,
91 search_q=search_q, statuses=statuses,
91 search_q=search_q, statuses=statuses,
92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 repo_name, self._rhodecode_user.user_id,
94 repo_name, self._rhodecode_user.user_id,
95 search_q=search_q, statuses=statuses)
95 search_q=search_q, statuses=statuses)
96 else:
96 else:
97 pull_requests = PullRequestModel().get_all(
97 pull_requests = PullRequestModel().get_all(
98 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,
99 statuses=statuses, offset=start, length=limit,
99 statuses=statuses, offset=start, length=limit,
100 order_by=order_by, order_dir=order_dir)
100 order_by=order_by, order_dir=order_dir)
101 pull_requests_total_count = PullRequestModel().count_all(
101 pull_requests_total_count = PullRequestModel().count_all(
102 repo_name, search_q=search_q, source=source, statuses=statuses,
102 repo_name, search_q=search_q, source=source, statuses=statuses,
103 opened_by=opened_by)
103 opened_by=opened_by)
104
104
105 data = []
105 data = []
106 comments_model = CommentsModel()
106 comments_model = CommentsModel()
107 for pr in pull_requests:
107 for pr in pull_requests:
108 comments_count = comments_model.get_all_comments(
108 comments_count = comments_model.get_all_comments(
109 self.db_repo.repo_id, pull_request=pr,
109 self.db_repo.repo_id, pull_request=pr,
110 include_drafts=False, count_only=True)
110 include_drafts=False, count_only=True)
111
111
112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 if review_statuses and review_statuses[4]:
114 if review_statuses and review_statuses[4]:
115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 my_review_status = statuses[0][1].status
116 my_review_status = statuses[0][1].status
117
117
118 data.append({
118 data.append({
119 'name': _render('pullrequest_name',
119 'name': _render('pullrequest_name',
120 pr.pull_request_id, pr.pull_request_state,
120 pr.pull_request_id, pr.pull_request_state,
121 pr.work_in_progress, pr.target_repo.repo_name,
121 pr.work_in_progress, pr.target_repo.repo_name,
122 short=True),
122 short=True),
123 'name_raw': pr.pull_request_id,
123 'name_raw': pr.pull_request_id,
124 'status': _render('pullrequest_status',
124 'status': _render('pullrequest_status',
125 pr.calculated_review_status()),
125 pr.calculated_review_status()),
126 'my_status': _render('pullrequest_status',
126 'my_status': _render('pullrequest_status',
127 my_review_status),
127 my_review_status),
128 'title': _render('pullrequest_title', pr.title, pr.description),
128 'title': _render('pullrequest_title', pr.title, pr.description),
129 'description': h.escape(pr.description),
129 'description': h.escape(pr.description),
130 'updated_on': _render('pullrequest_updated_on',
130 'updated_on': _render('pullrequest_updated_on',
131 h.datetime_to_time(pr.updated_on),
131 h.datetime_to_time(pr.updated_on),
132 pr.versions_count),
132 pr.versions_count),
133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
134 'created_on': _render('pullrequest_updated_on',
134 'created_on': _render('pullrequest_updated_on',
135 h.datetime_to_time(pr.created_on)),
135 h.datetime_to_time(pr.created_on)),
136 'created_on_raw': h.datetime_to_time(pr.created_on),
136 'created_on_raw': h.datetime_to_time(pr.created_on),
137 'state': pr.pull_request_state,
137 'state': pr.pull_request_state,
138 'author': _render('pullrequest_author',
138 'author': _render('pullrequest_author',
139 pr.author.full_contact, ),
139 pr.author.full_contact, ),
140 'author_raw': pr.author.full_name,
140 'author_raw': pr.author.full_name,
141 'comments': _render('pullrequest_comments', comments_count),
141 'comments': _render('pullrequest_comments', comments_count),
142 'comments_raw': comments_count,
142 'comments_raw': comments_count,
143 'closed': pr.is_closed(),
143 'closed': pr.is_closed(),
144 })
144 })
145
145
146 data = ({
146 data = ({
147 'draw': draw,
147 'draw': draw,
148 'data': data,
148 'data': data,
149 'recordsTotal': pull_requests_total_count,
149 'recordsTotal': pull_requests_total_count,
150 'recordsFiltered': pull_requests_total_count,
150 'recordsFiltered': pull_requests_total_count,
151 })
151 })
152 return data
152 return data
153
153
154 @LoginRequired()
154 @LoginRequired()
155 @HasRepoPermissionAnyDecorator(
155 @HasRepoPermissionAnyDecorator(
156 'repository.read', 'repository.write', 'repository.admin')
156 'repository.read', 'repository.write', 'repository.admin')
157 def pull_request_list(self):
157 def pull_request_list(self):
158 c = self.load_default_context()
158 c = self.load_default_context()
159
159
160 req_get = self.request.GET
160 req_get = self.request.GET
161 c.source = str2bool(req_get.get('source'))
161 c.source = str2bool(req_get.get('source'))
162 c.closed = str2bool(req_get.get('closed'))
162 c.closed = str2bool(req_get.get('closed'))
163 c.my = str2bool(req_get.get('my'))
163 c.my = str2bool(req_get.get('my'))
164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
166
166
167 c.active = 'open'
167 c.active = 'open'
168 if c.my:
168 if c.my:
169 c.active = 'my'
169 c.active = 'my'
170 if c.closed:
170 if c.closed:
171 c.active = 'closed'
171 c.active = 'closed'
172 if c.awaiting_review and not c.source:
172 if c.awaiting_review and not c.source:
173 c.active = 'awaiting'
173 c.active = 'awaiting'
174 if c.source and not c.awaiting_review:
174 if c.source and not c.awaiting_review:
175 c.active = 'source'
175 c.active = 'source'
176 if c.awaiting_my_review:
176 if c.awaiting_my_review:
177 c.active = 'awaiting_my'
177 c.active = 'awaiting_my'
178
178
179 return self._get_template_context(c)
179 return self._get_template_context(c)
180
180
181 @LoginRequired()
181 @LoginRequired()
182 @HasRepoPermissionAnyDecorator(
182 @HasRepoPermissionAnyDecorator(
183 'repository.read', 'repository.write', 'repository.admin')
183 'repository.read', 'repository.write', 'repository.admin')
184 def pull_request_list_data(self):
184 def pull_request_list_data(self):
185 self.load_default_context()
185 self.load_default_context()
186
186
187 # additional filters
187 # additional filters
188 req_get = self.request.GET
188 req_get = self.request.GET
189 source = str2bool(req_get.get('source'))
189 source = str2bool(req_get.get('source'))
190 closed = str2bool(req_get.get('closed'))
190 closed = str2bool(req_get.get('closed'))
191 my = str2bool(req_get.get('my'))
191 my = str2bool(req_get.get('my'))
192 awaiting_review = str2bool(req_get.get('awaiting_review'))
192 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194
194
195 filter_type = 'awaiting_review' if awaiting_review \
195 filter_type = 'awaiting_review' if awaiting_review \
196 else 'awaiting_my_review' if awaiting_my_review \
196 else 'awaiting_my_review' if awaiting_my_review \
197 else None
197 else None
198
198
199 opened_by = None
199 opened_by = None
200 if my:
200 if my:
201 opened_by = [self._rhodecode_user.user_id]
201 opened_by = [self._rhodecode_user.user_id]
202
202
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 if closed:
204 if closed:
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 data = self._get_pull_requests_list(
207 data = self._get_pull_requests_list(
208 repo_name=self.db_repo_name, source=source,
208 repo_name=self.db_repo_name, source=source,
209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210
210
211 return data
211 return data
212
212
213 def _is_diff_cache_enabled(self, target_repo):
213 def _is_diff_cache_enabled(self, target_repo):
214 caching_enabled = self._get_general_setting(
214 caching_enabled = self._get_general_setting(
215 target_repo, 'rhodecode_diff_cache')
215 target_repo, 'rhodecode_diff_cache')
216 log.debug('Diff caching enabled: %s', caching_enabled)
216 log.debug('Diff caching enabled: %s', caching_enabled)
217 return caching_enabled
217 return caching_enabled
218
218
219 def _get_diffset(self, source_repo_name, source_repo,
219 def _get_diffset(self, source_repo_name, source_repo,
220 ancestor_commit,
220 ancestor_commit,
221 source_ref_id, target_ref_id,
221 source_ref_id, target_ref_id,
222 target_commit, source_commit, diff_limit, file_limit,
222 target_commit, source_commit, diff_limit, file_limit,
223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
224
224
225 target_commit_final = target_commit
225 target_commit_final = target_commit
226 source_commit_final = source_commit
226 source_commit_final = source_commit
227
227
228 if use_ancestor:
228 if use_ancestor:
229 # we might want to not use it for versions
229 # we might want to not use it for versions
230 target_ref_id = ancestor_commit.raw_id
230 target_ref_id = ancestor_commit.raw_id
231 target_commit_final = ancestor_commit
231 target_commit_final = ancestor_commit
232
232
233 vcs_diff = PullRequestModel().get_diff(
233 vcs_diff = PullRequestModel().get_diff(
234 source_repo, source_ref_id, target_ref_id,
234 source_repo, source_ref_id, target_ref_id,
235 hide_whitespace_changes, diff_context)
235 hide_whitespace_changes, diff_context)
236
236
237 diff_processor = diffs.DiffProcessor(
237 diff_processor = diffs.DiffProcessor(
238 vcs_diff, format='newdiff', diff_limit=diff_limit,
238 vcs_diff, format='newdiff', diff_limit=diff_limit,
239 file_limit=file_limit, show_full_diff=fulldiff)
239 file_limit=file_limit, show_full_diff=fulldiff)
240
240
241 _parsed = diff_processor.prepare()
241 _parsed = diff_processor.prepare()
242
242
243 diffset = codeblocks.DiffSet(
243 diffset = codeblocks.DiffSet(
244 repo_name=self.db_repo_name,
244 repo_name=self.db_repo_name,
245 source_repo_name=source_repo_name,
245 source_repo_name=source_repo_name,
246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
248 )
248 )
249 diffset = self.path_filter.render_patchset_filtered(
249 diffset = self.path_filter.render_patchset_filtered(
250 diffset, _parsed, target_ref_id, source_ref_id)
250 diffset, _parsed, target_ref_id, source_ref_id)
251
251
252 return diffset
252 return diffset
253
253
254 def _get_range_diffset(self, source_scm, source_repo,
254 def _get_range_diffset(self, source_scm, source_repo,
255 commit1, commit2, diff_limit, file_limit,
255 commit1, commit2, diff_limit, file_limit,
256 fulldiff, hide_whitespace_changes, diff_context):
256 fulldiff, hide_whitespace_changes, diff_context):
257 vcs_diff = source_scm.get_diff(
257 vcs_diff = source_scm.get_diff(
258 commit1, commit2,
258 commit1, commit2,
259 ignore_whitespace=hide_whitespace_changes,
259 ignore_whitespace=hide_whitespace_changes,
260 context=diff_context)
260 context=diff_context)
261
261
262 diff_processor = diffs.DiffProcessor(
262 diff_processor = diffs.DiffProcessor(
263 vcs_diff, format='newdiff', diff_limit=diff_limit,
263 vcs_diff, format='newdiff', diff_limit=diff_limit,
264 file_limit=file_limit, show_full_diff=fulldiff)
264 file_limit=file_limit, show_full_diff=fulldiff)
265
265
266 _parsed = diff_processor.prepare()
266 _parsed = diff_processor.prepare()
267
267
268 diffset = codeblocks.DiffSet(
268 diffset = codeblocks.DiffSet(
269 repo_name=source_repo.repo_name,
269 repo_name=source_repo.repo_name,
270 source_node_getter=codeblocks.diffset_node_getter(commit1),
270 source_node_getter=codeblocks.diffset_node_getter(commit1),
271 target_node_getter=codeblocks.diffset_node_getter(commit2))
271 target_node_getter=codeblocks.diffset_node_getter(commit2))
272
272
273 diffset = self.path_filter.render_patchset_filtered(
273 diffset = self.path_filter.render_patchset_filtered(
274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
275
275
276 return diffset
276 return diffset
277
277
278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
279 comments_model = CommentsModel()
279 comments_model = CommentsModel()
280
280
281 # GENERAL COMMENTS with versions #
281 # GENERAL COMMENTS with versions #
282 q = comments_model._all_general_comments_of_pull_request(pull_request)
282 q = comments_model._all_general_comments_of_pull_request(pull_request)
283 q = q.order_by(ChangesetComment.comment_id.asc())
283 q = q.order_by(ChangesetComment.comment_id.asc())
284 if not include_drafts:
284 if not include_drafts:
285 q = q.filter(ChangesetComment.draft == false())
285 q = q.filter(ChangesetComment.draft == false())
286 general_comments = q
286 general_comments = q
287
287
288 # pick comments we want to render at current version
288 # pick comments we want to render at current version
289 c.comment_versions = comments_model.aggregate_comments(
289 c.comment_versions = comments_model.aggregate_comments(
290 general_comments, versions, c.at_version_num)
290 general_comments, versions, c.at_version_num)
291
291
292 # INLINE COMMENTS with versions #
292 # INLINE COMMENTS with versions #
293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
294 q = q.order_by(ChangesetComment.comment_id.asc())
294 q = q.order_by(ChangesetComment.comment_id.asc())
295 if not include_drafts:
295 if not include_drafts:
296 q = q.filter(ChangesetComment.draft == false())
296 q = q.filter(ChangesetComment.draft == false())
297 inline_comments = q
297 inline_comments = q
298
298
299 c.inline_versions = comments_model.aggregate_comments(
299 c.inline_versions = comments_model.aggregate_comments(
300 inline_comments, versions, c.at_version_num, inline=True)
300 inline_comments, versions, c.at_version_num, inline=True)
301
301
302 # Comments inline+general
302 # Comments inline+general
303 if c.at_version:
303 if c.at_version:
304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
305 c.comments = c.comment_versions[c.at_version_num]['display']
305 c.comments = c.comment_versions[c.at_version_num]['display']
306 else:
306 else:
307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
308 c.comments = c.comment_versions[c.at_version_num]['until']
308 c.comments = c.comment_versions[c.at_version_num]['until']
309
309
310 return general_comments, inline_comments
310 return general_comments, inline_comments
311
311
312 @LoginRequired()
312 @LoginRequired()
313 @HasRepoPermissionAnyDecorator(
313 @HasRepoPermissionAnyDecorator(
314 'repository.read', 'repository.write', 'repository.admin')
314 'repository.read', 'repository.write', 'repository.admin')
315 def pull_request_show(self):
315 def pull_request_show(self):
316 _ = self.request.translate
316 _ = self.request.translate
317 c = self.load_default_context()
317 c = self.load_default_context()
318
318
319 pull_request = PullRequest.get_or_404(
319 pull_request = PullRequest.get_or_404(
320 self.request.matchdict['pull_request_id'])
320 self.request.matchdict['pull_request_id'])
321 pull_request_id = pull_request.pull_request_id
321 pull_request_id = pull_request.pull_request_id
322
322
323 c.state_progressing = pull_request.is_state_changing()
323 c.state_progressing = pull_request.is_state_changing()
324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
325
325
326 _new_state = {
326 _new_state = {
327 'created': PullRequest.STATE_CREATED,
327 'created': PullRequest.STATE_CREATED,
328 }.get(self.request.GET.get('force_state'))
328 }.get(self.request.GET.get('force_state'))
329
329
330 if c.is_super_admin and _new_state:
330 if c.is_super_admin and _new_state:
331 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
331 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
332 h.flash(
332 h.flash(
333 _('Pull Request state was force changed to `{}`').format(_new_state),
333 _('Pull Request state was force changed to `{}`').format(_new_state),
334 category='success')
334 category='success')
335 Session().commit()
335 Session().commit()
336
336
337 raise HTTPFound(h.route_path(
337 raise HTTPFound(h.route_path(
338 'pullrequest_show', repo_name=self.db_repo_name,
338 'pullrequest_show', repo_name=self.db_repo_name,
339 pull_request_id=pull_request_id))
339 pull_request_id=pull_request_id))
340
340
341 version = self.request.GET.get('version')
341 version = self.request.GET.get('version')
342 from_version = self.request.GET.get('from_version') or version
342 from_version = self.request.GET.get('from_version') or version
343 merge_checks = self.request.GET.get('merge_checks')
343 merge_checks = self.request.GET.get('merge_checks')
344 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
344 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
345 force_refresh = str2bool(self.request.GET.get('force_refresh'))
345 force_refresh = str2bool(self.request.GET.get('force_refresh'))
346 c.range_diff_on = self.request.GET.get('range-diff') == "1"
346 c.range_diff_on = self.request.GET.get('range-diff') == "1"
347
347
348 # fetch global flags of ignore ws or context lines
348 # fetch global flags of ignore ws or context lines
349 diff_context = diffs.get_diff_context(self.request)
349 diff_context = diffs.get_diff_context(self.request)
350 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
350 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
351
351
352 (pull_request_latest,
352 (pull_request_latest,
353 pull_request_at_ver,
353 pull_request_at_ver,
354 pull_request_display_obj,
354 pull_request_display_obj,
355 at_version) = PullRequestModel().get_pr_version(
355 at_version) = PullRequestModel().get_pr_version(
356 pull_request_id, version=version)
356 pull_request_id, version=version)
357
357
358 pr_closed = pull_request_latest.is_closed()
358 pr_closed = pull_request_latest.is_closed()
359
359
360 if pr_closed and (version or from_version):
360 if pr_closed and (version or from_version):
361 # not allow to browse versions for closed PR
361 # not allow to browse versions for closed PR
362 raise HTTPFound(h.route_path(
362 raise HTTPFound(h.route_path(
363 'pullrequest_show', repo_name=self.db_repo_name,
363 'pullrequest_show', repo_name=self.db_repo_name,
364 pull_request_id=pull_request_id))
364 pull_request_id=pull_request_id))
365
365
366 versions = pull_request_display_obj.versions()
366 versions = pull_request_display_obj.versions()
367
367
368 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
368 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
369
369
370 # used to store per-commit range diffs
370 # used to store per-commit range diffs
371 c.changes = collections.OrderedDict()
371 c.changes = collections.OrderedDict()
372
372
373 c.at_version = at_version
373 c.at_version = at_version
374 c.at_version_num = (at_version
374 c.at_version_num = (at_version
375 if at_version and at_version != PullRequest.LATEST_VER
375 if at_version and at_version != PullRequest.LATEST_VER
376 else None)
376 else None)
377
377
378 c.at_version_index = ChangesetComment.get_index_from_version(
378 c.at_version_index = ChangesetComment.get_index_from_version(
379 c.at_version_num, versions)
379 c.at_version_num, versions)
380
380
381 (prev_pull_request_latest,
381 (prev_pull_request_latest,
382 prev_pull_request_at_ver,
382 prev_pull_request_at_ver,
383 prev_pull_request_display_obj,
383 prev_pull_request_display_obj,
384 prev_at_version) = PullRequestModel().get_pr_version(
384 prev_at_version) = PullRequestModel().get_pr_version(
385 pull_request_id, version=from_version)
385 pull_request_id, version=from_version)
386
386
387 c.from_version = prev_at_version
387 c.from_version = prev_at_version
388 c.from_version_num = (prev_at_version
388 c.from_version_num = (prev_at_version
389 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
389 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
390 else None)
390 else None)
391 c.from_version_index = ChangesetComment.get_index_from_version(
391 c.from_version_index = ChangesetComment.get_index_from_version(
392 c.from_version_num, versions)
392 c.from_version_num, versions)
393
393
394 # define if we're in COMPARE mode or VIEW at version mode
394 # define if we're in COMPARE mode or VIEW at version mode
395 compare = at_version != prev_at_version
395 compare = at_version != prev_at_version
396
396
397 # pull_requests repo_name we opened it against
397 # pull_requests repo_name we opened it against
398 # ie. target_repo must match
398 # ie. target_repo must match
399 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
399 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
400 log.warning('Mismatch between the current repo: %s, and target %s',
400 log.warning('Mismatch between the current repo: %s, and target %s',
401 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
401 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
402 raise HTTPNotFound()
402 raise HTTPNotFound()
403
403
404 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
404 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
405
405
406 c.pull_request = pull_request_display_obj
406 c.pull_request = pull_request_display_obj
407 c.renderer = pull_request_at_ver.description_renderer or c.renderer
407 c.renderer = pull_request_at_ver.description_renderer or c.renderer
408 c.pull_request_latest = pull_request_latest
408 c.pull_request_latest = pull_request_latest
409
409
410 # inject latest version
410 # inject latest version
411 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
411 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
412 c.versions = versions + [latest_ver]
412 c.versions = versions + [latest_ver]
413
413
414 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
414 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
415 c.allowed_to_change_status = False
415 c.allowed_to_change_status = False
416 c.allowed_to_update = False
416 c.allowed_to_update = False
417 c.allowed_to_merge = False
417 c.allowed_to_merge = False
418 c.allowed_to_delete = False
418 c.allowed_to_delete = False
419 c.allowed_to_comment = False
419 c.allowed_to_comment = False
420 c.allowed_to_close = False
420 c.allowed_to_close = False
421 else:
421 else:
422 can_change_status = PullRequestModel().check_user_change_status(
422 can_change_status = PullRequestModel().check_user_change_status(
423 pull_request_at_ver, self._rhodecode_user)
423 pull_request_at_ver, self._rhodecode_user)
424 c.allowed_to_change_status = can_change_status and not pr_closed
424 c.allowed_to_change_status = can_change_status and not pr_closed
425
425
426 c.allowed_to_update = PullRequestModel().check_user_update(
426 c.allowed_to_update = PullRequestModel().check_user_update(
427 pull_request_latest, self._rhodecode_user) and not pr_closed
427 pull_request_latest, self._rhodecode_user) and not pr_closed
428 c.allowed_to_merge = PullRequestModel().check_user_merge(
428 c.allowed_to_merge = PullRequestModel().check_user_merge(
429 pull_request_latest, self._rhodecode_user) and not pr_closed
429 pull_request_latest, self._rhodecode_user) and not pr_closed
430 c.allowed_to_delete = PullRequestModel().check_user_delete(
430 c.allowed_to_delete = PullRequestModel().check_user_delete(
431 pull_request_latest, self._rhodecode_user) and not pr_closed
431 pull_request_latest, self._rhodecode_user) and not pr_closed
432 c.allowed_to_comment = not pr_closed
432 c.allowed_to_comment = not pr_closed
433 c.allowed_to_close = c.allowed_to_merge and not pr_closed
433 c.allowed_to_close = c.allowed_to_merge and not pr_closed
434
434
435 c.forbid_adding_reviewers = False
435 c.forbid_adding_reviewers = False
436
436
437 if pull_request_latest.reviewer_data and \
437 if pull_request_latest.reviewer_data and \
438 'rules' in pull_request_latest.reviewer_data:
438 'rules' in pull_request_latest.reviewer_data:
439 rules = pull_request_latest.reviewer_data['rules'] or {}
439 rules = pull_request_latest.reviewer_data['rules'] or {}
440 try:
440 try:
441 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
441 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
442 except Exception:
442 except Exception:
443 pass
443 pass
444
444
445 # check merge capabilities
445 # check merge capabilities
446 _merge_check = MergeCheck.validate(
446 _merge_check = MergeCheck.validate(
447 pull_request_latest, auth_user=self._rhodecode_user,
447 pull_request_latest, auth_user=self._rhodecode_user,
448 translator=self.request.translate,
448 translator=self.request.translate,
449 force_shadow_repo_refresh=force_refresh)
449 force_shadow_repo_refresh=force_refresh)
450
450
451 c.pr_merge_errors = _merge_check.error_details
451 c.pr_merge_errors = _merge_check.error_details
452 c.pr_merge_possible = not _merge_check.failed
452 c.pr_merge_possible = not _merge_check.failed
453 c.pr_merge_message = _merge_check.merge_msg
453 c.pr_merge_message = _merge_check.merge_msg
454 c.pr_merge_source_commit = _merge_check.source_commit
454 c.pr_merge_source_commit = _merge_check.source_commit
455 c.pr_merge_target_commit = _merge_check.target_commit
455 c.pr_merge_target_commit = _merge_check.target_commit
456
456
457 c.pr_merge_info = MergeCheck.get_merge_conditions(
457 c.pr_merge_info = MergeCheck.get_merge_conditions(
458 pull_request_latest, translator=self.request.translate)
458 pull_request_latest, translator=self.request.translate)
459
459
460 c.pull_request_review_status = _merge_check.review_status
460 c.pull_request_review_status = _merge_check.review_status
461 if merge_checks:
461 if merge_checks:
462 self.request.override_renderer = \
462 self.request.override_renderer = \
463 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
463 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
464 return self._get_template_context(c)
464 return self._get_template_context(c)
465
465
466 c.reviewers_count = pull_request.reviewers_count
466 c.reviewers_count = pull_request.reviewers_count
467 c.observers_count = pull_request.observers_count
467 c.observers_count = pull_request.observers_count
468
468
469 # reviewers and statuses
469 # reviewers and statuses
470 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
470 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
471 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
471 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
472 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
472 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
473
473
474 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
474 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
475 member_reviewer = h.reviewer_as_json(
475 member_reviewer = h.reviewer_as_json(
476 member, reasons=reasons, mandatory=mandatory,
476 member, reasons=reasons, mandatory=mandatory,
477 role=review_obj.role,
477 role=review_obj.role,
478 user_group=review_obj.rule_user_group_data()
478 user_group=review_obj.rule_user_group_data()
479 )
479 )
480
480
481 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
481 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
482 member_reviewer['review_status'] = current_review_status
482 member_reviewer['review_status'] = current_review_status
483 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
483 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
484 member_reviewer['allowed_to_update'] = c.allowed_to_update
484 member_reviewer['allowed_to_update'] = c.allowed_to_update
485 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
485 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
486
486
487 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
487 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
488
488
489 for observer_obj, member in pull_request_at_ver.observers():
489 for observer_obj, member in pull_request_at_ver.observers():
490 member_observer = h.reviewer_as_json(
490 member_observer = h.reviewer_as_json(
491 member, reasons=[], mandatory=False,
491 member, reasons=[], mandatory=False,
492 role=observer_obj.role,
492 role=observer_obj.role,
493 user_group=observer_obj.rule_user_group_data()
493 user_group=observer_obj.rule_user_group_data()
494 )
494 )
495 member_observer['allowed_to_update'] = c.allowed_to_update
495 member_observer['allowed_to_update'] = c.allowed_to_update
496 c.pull_request_set_observers_data_json['observers'].append(member_observer)
496 c.pull_request_set_observers_data_json['observers'].append(member_observer)
497
497
498 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
498 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
499
499
500 general_comments, inline_comments = \
500 general_comments, inline_comments = \
501 self.register_comments_vars(c, pull_request_latest, versions)
501 self.register_comments_vars(c, pull_request_latest, versions)
502
502
503 # TODOs
503 # TODOs
504 c.unresolved_comments = CommentsModel() \
504 c.unresolved_comments = CommentsModel() \
505 .get_pull_request_unresolved_todos(pull_request_latest)
505 .get_pull_request_unresolved_todos(pull_request_latest)
506 c.resolved_comments = CommentsModel() \
506 c.resolved_comments = CommentsModel() \
507 .get_pull_request_resolved_todos(pull_request_latest)
507 .get_pull_request_resolved_todos(pull_request_latest)
508
508
509 # Drafts
509 # Drafts
510 c.draft_comments = CommentsModel().get_pull_request_drafts(
510 c.draft_comments = CommentsModel().get_pull_request_drafts(
511 self._rhodecode_db_user.user_id,
511 self._rhodecode_db_user.user_id,
512 pull_request_latest)
512 pull_request_latest)
513
513
514 # if we use version, then do not show later comments
514 # if we use version, then do not show later comments
515 # than current version
515 # than current version
516 display_inline_comments = collections.defaultdict(
516 display_inline_comments = collections.defaultdict(
517 lambda: collections.defaultdict(list))
517 lambda: collections.defaultdict(list))
518 for co in inline_comments:
518 for co in inline_comments:
519 if c.at_version_num:
519 if c.at_version_num:
520 # pick comments that are at least UPTO given version, so we
520 # pick comments that are at least UPTO given version, so we
521 # don't render comments for higher version
521 # don't render comments for higher version
522 should_render = co.pull_request_version_id and \
522 should_render = co.pull_request_version_id and \
523 co.pull_request_version_id <= c.at_version_num
523 co.pull_request_version_id <= c.at_version_num
524 else:
524 else:
525 # showing all, for 'latest'
525 # showing all, for 'latest'
526 should_render = True
526 should_render = True
527
527
528 if should_render:
528 if should_render:
529 display_inline_comments[co.f_path][co.line_no].append(co)
529 display_inline_comments[co.f_path][co.line_no].append(co)
530
530
531 # load diff data into template context, if we use compare mode then
531 # load diff data into template context, if we use compare mode then
532 # diff is calculated based on changes between versions of PR
532 # diff is calculated based on changes between versions of PR
533
533
534 source_repo = pull_request_at_ver.source_repo
534 source_repo = pull_request_at_ver.source_repo
535 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
535 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
536
536
537 target_repo = pull_request_at_ver.target_repo
537 target_repo = pull_request_at_ver.target_repo
538 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
538 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
539
539
540 if compare:
540 if compare:
541 # in compare switch the diff base to latest commit from prev version
541 # in compare switch the diff base to latest commit from prev version
542 target_ref_id = prev_pull_request_display_obj.revisions[0]
542 target_ref_id = prev_pull_request_display_obj.revisions[0]
543
543
544 # despite opening commits for bookmarks/branches/tags, we always
544 # despite opening commits for bookmarks/branches/tags, we always
545 # convert this to rev to prevent changes after bookmark or branch change
545 # convert this to rev to prevent changes after bookmark or branch change
546 c.source_ref_type = 'rev'
546 c.source_ref_type = 'rev'
547 c.source_ref = source_ref_id
547 c.source_ref = source_ref_id
548
548
549 c.target_ref_type = 'rev'
549 c.target_ref_type = 'rev'
550 c.target_ref = target_ref_id
550 c.target_ref = target_ref_id
551
551
552 c.source_repo = source_repo
552 c.source_repo = source_repo
553 c.target_repo = target_repo
553 c.target_repo = target_repo
554
554
555 c.commit_ranges = []
555 c.commit_ranges = []
556 source_commit = EmptyCommit()
556 source_commit = EmptyCommit()
557 target_commit = EmptyCommit()
557 target_commit = EmptyCommit()
558 c.missing_requirements = False
558 c.missing_requirements = False
559
559
560 source_scm = source_repo.scm_instance()
560 source_scm = source_repo.scm_instance()
561 target_scm = target_repo.scm_instance()
561 target_scm = target_repo.scm_instance()
562
562
563 shadow_scm = None
563 shadow_scm = None
564 try:
564 try:
565 shadow_scm = pull_request_latest.get_shadow_repo()
565 shadow_scm = pull_request_latest.get_shadow_repo()
566 except Exception:
566 except Exception:
567 log.debug('Failed to get shadow repo', exc_info=True)
567 log.debug('Failed to get shadow repo', exc_info=True)
568 # try first the existing source_repo, and then shadow
568 # try first the existing source_repo, and then shadow
569 # repo if we can obtain one
569 # repo if we can obtain one
570 commits_source_repo = source_scm
570 commits_source_repo = source_scm
571 if shadow_scm:
571 if shadow_scm:
572 commits_source_repo = shadow_scm
572 commits_source_repo = shadow_scm
573
573
574 c.commits_source_repo = commits_source_repo
574 c.commits_source_repo = commits_source_repo
575 c.ancestor = None # set it to None, to hide it from PR view
575 c.ancestor = None # set it to None, to hide it from PR view
576
576
577 # empty version means latest, so we keep this to prevent
577 # empty version means latest, so we keep this to prevent
578 # double caching
578 # double caching
579 version_normalized = version or PullRequest.LATEST_VER
579 version_normalized = version or PullRequest.LATEST_VER
580 from_version_normalized = from_version or PullRequest.LATEST_VER
580 from_version_normalized = from_version or PullRequest.LATEST_VER
581
581
582 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
582 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
583 cache_file_path = diff_cache_exist(
583 cache_file_path = diff_cache_exist(
584 cache_path, 'pull_request', pull_request_id, version_normalized,
584 cache_path, 'pull_request', pull_request_id, version_normalized,
585 from_version_normalized, source_ref_id, target_ref_id,
585 from_version_normalized, source_ref_id, target_ref_id,
586 hide_whitespace_changes, diff_context, c.fulldiff)
586 hide_whitespace_changes, diff_context, c.fulldiff)
587
587
588 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
588 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
589 force_recache = self.get_recache_flag()
589 force_recache = self.get_recache_flag()
590
590
591 cached_diff = None
591 cached_diff = None
592 if caching_enabled:
592 if caching_enabled:
593 cached_diff = load_cached_diff(cache_file_path)
593 cached_diff = load_cached_diff(cache_file_path)
594
594
595 has_proper_commit_cache = (
595 has_proper_commit_cache = (
596 cached_diff and cached_diff.get('commits')
596 cached_diff and cached_diff.get('commits')
597 and len(cached_diff.get('commits', [])) == 5
597 and len(cached_diff.get('commits', [])) == 5
598 and cached_diff.get('commits')[0]
598 and cached_diff.get('commits')[0]
599 and cached_diff.get('commits')[3])
599 and cached_diff.get('commits')[3])
600
600
601 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
601 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
602 diff_commit_cache = \
602 diff_commit_cache = \
603 (ancestor_commit, commit_cache, missing_requirements,
603 (ancestor_commit, commit_cache, missing_requirements,
604 source_commit, target_commit) = cached_diff['commits']
604 source_commit, target_commit) = cached_diff['commits']
605 else:
605 else:
606 # NOTE(marcink): we reach potentially unreachable errors when a PR has
606 # NOTE(marcink): we reach potentially unreachable errors when a PR has
607 # merge errors resulting in potentially hidden commits in the shadow repo.
607 # merge errors resulting in potentially hidden commits in the shadow repo.
608 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
608 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
609 and _merge_check.merge_response
609 and _merge_check.merge_response
610 maybe_unreachable = maybe_unreachable \
610 maybe_unreachable = maybe_unreachable \
611 and _merge_check.merge_response.metadata.get('unresolved_files')
611 and _merge_check.merge_response.metadata.get('unresolved_files')
612 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
612 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
613 diff_commit_cache = \
613 diff_commit_cache = \
614 (ancestor_commit, commit_cache, missing_requirements,
614 (ancestor_commit, commit_cache, missing_requirements,
615 source_commit, target_commit) = self.get_commits(
615 source_commit, target_commit) = self.get_commits(
616 commits_source_repo,
616 commits_source_repo,
617 pull_request_at_ver,
617 pull_request_at_ver,
618 source_commit,
618 source_commit,
619 source_ref_id,
619 source_ref_id,
620 source_scm,
620 source_scm,
621 target_commit,
621 target_commit,
622 target_ref_id,
622 target_ref_id,
623 target_scm,
623 target_scm,
624 maybe_unreachable=maybe_unreachable)
624 maybe_unreachable=maybe_unreachable)
625
625
626 # register our commit range
626 # register our commit range
627 for comm in commit_cache.values():
627 for comm in commit_cache.values():
628 c.commit_ranges.append(comm)
628 c.commit_ranges.append(comm)
629
629
630 c.missing_requirements = missing_requirements
630 c.missing_requirements = missing_requirements
631 c.ancestor_commit = ancestor_commit
631 c.ancestor_commit = ancestor_commit
632 c.statuses = source_repo.statuses(
632 c.statuses = source_repo.statuses(
633 [x.raw_id for x in c.commit_ranges])
633 [x.raw_id for x in c.commit_ranges])
634
634
635 # auto collapse if we have more than limit
635 # auto collapse if we have more than limit
636 collapse_limit = diffs.DiffProcessor._collapse_commits_over
636 collapse_limit = diffs.DiffProcessor._collapse_commits_over
637 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
637 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
638 c.compare_mode = compare
638 c.compare_mode = compare
639
639
640 # diff_limit is the old behavior, will cut off the whole diff
640 # diff_limit is the old behavior, will cut off the whole diff
641 # if the limit is applied otherwise will just hide the
641 # if the limit is applied otherwise will just hide the
642 # big files from the front-end
642 # big files from the front-end
643 diff_limit = c.visual.cut_off_limit_diff
643 diff_limit = c.visual.cut_off_limit_diff
644 file_limit = c.visual.cut_off_limit_file
644 file_limit = c.visual.cut_off_limit_file
645
645
646 c.missing_commits = False
646 c.missing_commits = False
647 if (c.missing_requirements
647 if (c.missing_requirements
648 or isinstance(source_commit, EmptyCommit)
648 or isinstance(source_commit, EmptyCommit)
649 or source_commit == target_commit):
649 or source_commit == target_commit):
650
650
651 c.missing_commits = True
651 c.missing_commits = True
652 else:
652 else:
653 c.inline_comments = display_inline_comments
653 c.inline_comments = display_inline_comments
654
654
655 use_ancestor = True
655 use_ancestor = True
656 if from_version_normalized != version_normalized:
656 if from_version_normalized != version_normalized:
657 use_ancestor = False
657 use_ancestor = False
658
658
659 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
659 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
660 if not force_recache and has_proper_diff_cache:
660 if not force_recache and has_proper_diff_cache:
661 c.diffset = cached_diff['diff']
661 c.diffset = cached_diff['diff']
662 else:
662 else:
663 try:
663 try:
664 c.diffset = self._get_diffset(
664 c.diffset = self._get_diffset(
665 c.source_repo.repo_name, commits_source_repo,
665 c.source_repo.repo_name, commits_source_repo,
666 c.ancestor_commit,
666 c.ancestor_commit,
667 source_ref_id, target_ref_id,
667 source_ref_id, target_ref_id,
668 target_commit, source_commit,
668 target_commit, source_commit,
669 diff_limit, file_limit, c.fulldiff,
669 diff_limit, file_limit, c.fulldiff,
670 hide_whitespace_changes, diff_context,
670 hide_whitespace_changes, diff_context,
671 use_ancestor=use_ancestor
671 use_ancestor=use_ancestor
672 )
672 )
673
673
674 # save cached diff
674 # save cached diff
675 if caching_enabled:
675 if caching_enabled:
676 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
676 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
677 except CommitDoesNotExistError:
677 except CommitDoesNotExistError:
678 log.exception('Failed to generate diffset')
678 log.exception('Failed to generate diffset')
679 c.missing_commits = True
679 c.missing_commits = True
680
680
681 if not c.missing_commits:
681 if not c.missing_commits:
682
682
683 c.limited_diff = c.diffset.limited_diff
683 c.limited_diff = c.diffset.limited_diff
684
684
685 # calculate removed files that are bound to comments
685 # calculate removed files that are bound to comments
686 comment_deleted_files = [
686 comment_deleted_files = [
687 fname for fname in display_inline_comments
687 fname for fname in display_inline_comments
688 if fname not in c.diffset.file_stats]
688 if fname not in c.diffset.file_stats]
689
689
690 c.deleted_files_comments = collections.defaultdict(dict)
690 c.deleted_files_comments = collections.defaultdict(dict)
691 for fname, per_line_comments in display_inline_comments.items():
691 for fname, per_line_comments in display_inline_comments.items():
692 if fname in comment_deleted_files:
692 if fname in comment_deleted_files:
693 c.deleted_files_comments[fname]['stats'] = 0
693 c.deleted_files_comments[fname]['stats'] = 0
694 c.deleted_files_comments[fname]['comments'] = list()
694 c.deleted_files_comments[fname]['comments'] = list()
695 for lno, comments in per_line_comments.items():
695 for lno, comments in per_line_comments.items():
696 c.deleted_files_comments[fname]['comments'].extend(comments)
696 c.deleted_files_comments[fname]['comments'].extend(comments)
697
697
698 # maybe calculate the range diff
698 # maybe calculate the range diff
699 if c.range_diff_on:
699 if c.range_diff_on:
700 # TODO(marcink): set whitespace/context
700 # TODO(marcink): set whitespace/context
701 context_lcl = 3
701 context_lcl = 3
702 ign_whitespace_lcl = False
702 ign_whitespace_lcl = False
703
703
704 for commit in c.commit_ranges:
704 for commit in c.commit_ranges:
705 commit2 = commit
705 commit2 = commit
706 commit1 = commit.first_parent
706 commit1 = commit.first_parent
707
707
708 range_diff_cache_file_path = diff_cache_exist(
708 range_diff_cache_file_path = diff_cache_exist(
709 cache_path, 'diff', commit.raw_id,
709 cache_path, 'diff', commit.raw_id,
710 ign_whitespace_lcl, context_lcl, c.fulldiff)
710 ign_whitespace_lcl, context_lcl, c.fulldiff)
711
711
712 cached_diff = None
712 cached_diff = None
713 if caching_enabled:
713 if caching_enabled:
714 cached_diff = load_cached_diff(range_diff_cache_file_path)
714 cached_diff = load_cached_diff(range_diff_cache_file_path)
715
715
716 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
716 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
717 if not force_recache and has_proper_diff_cache:
717 if not force_recache and has_proper_diff_cache:
718 diffset = cached_diff['diff']
718 diffset = cached_diff['diff']
719 else:
719 else:
720 diffset = self._get_range_diffset(
720 diffset = self._get_range_diffset(
721 commits_source_repo, source_repo,
721 commits_source_repo, source_repo,
722 commit1, commit2, diff_limit, file_limit,
722 commit1, commit2, diff_limit, file_limit,
723 c.fulldiff, ign_whitespace_lcl, context_lcl
723 c.fulldiff, ign_whitespace_lcl, context_lcl
724 )
724 )
725
725
726 # save cached diff
726 # save cached diff
727 if caching_enabled:
727 if caching_enabled:
728 cache_diff(range_diff_cache_file_path, diffset, None)
728 cache_diff(range_diff_cache_file_path, diffset, None)
729
729
730 c.changes[commit.raw_id] = diffset
730 c.changes[commit.raw_id] = diffset
731
731
732 # this is a hack to properly display links, when creating PR, the
732 # this is a hack to properly display links, when creating PR, the
733 # compare view and others uses different notation, and
733 # compare view and others uses different notation, and
734 # compare_commits.mako renders links based on the target_repo.
734 # compare_commits.mako renders links based on the target_repo.
735 # We need to swap that here to generate it properly on the html side
735 # We need to swap that here to generate it properly on the html side
736 c.target_repo = c.source_repo
736 c.target_repo = c.source_repo
737
737
738 c.commit_statuses = ChangesetStatus.STATUSES
738 c.commit_statuses = ChangesetStatus.STATUSES
739
739
740 c.show_version_changes = not pr_closed
740 c.show_version_changes = not pr_closed
741 if c.show_version_changes:
741 if c.show_version_changes:
742 cur_obj = pull_request_at_ver
742 cur_obj = pull_request_at_ver
743 prev_obj = prev_pull_request_at_ver
743 prev_obj = prev_pull_request_at_ver
744
744
745 old_commit_ids = prev_obj.revisions
745 old_commit_ids = prev_obj.revisions
746 new_commit_ids = cur_obj.revisions
746 new_commit_ids = cur_obj.revisions
747 commit_changes = PullRequestModel()._calculate_commit_id_changes(
747 commit_changes = PullRequestModel()._calculate_commit_id_changes(
748 old_commit_ids, new_commit_ids)
748 old_commit_ids, new_commit_ids)
749 c.commit_changes_summary = commit_changes
749 c.commit_changes_summary = commit_changes
750
750
751 # calculate the diff for commits between versions
751 # calculate the diff for commits between versions
752 c.commit_changes = []
752 c.commit_changes = []
753
753
754 def mark(cs, fw):
754 def mark(cs, fw):
755 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
755 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
756
756
757 for c_type, raw_id in mark(commit_changes.added, 'a') \
757 for c_type, raw_id in mark(commit_changes.added, 'a') \
758 + mark(commit_changes.removed, 'r') \
758 + mark(commit_changes.removed, 'r') \
759 + mark(commit_changes.common, 'c'):
759 + mark(commit_changes.common, 'c'):
760
760
761 if raw_id in commit_cache:
761 if raw_id in commit_cache:
762 commit = commit_cache[raw_id]
762 commit = commit_cache[raw_id]
763 else:
763 else:
764 try:
764 try:
765 commit = commits_source_repo.get_commit(raw_id)
765 commit = commits_source_repo.get_commit(raw_id)
766 except CommitDoesNotExistError:
766 except CommitDoesNotExistError:
767 # in case we fail extracting still use "dummy" commit
767 # in case we fail extracting still use "dummy" commit
768 # for display in commit diff
768 # for display in commit diff
769 commit = h.AttributeDict(
769 commit = h.AttributeDict(
770 {'raw_id': raw_id,
770 {'raw_id': raw_id,
771 'message': 'EMPTY or MISSING COMMIT'})
771 'message': 'EMPTY or MISSING COMMIT'})
772 c.commit_changes.append([c_type, commit])
772 c.commit_changes.append([c_type, commit])
773
773
774 # current user review statuses for each version
774 # current user review statuses for each version
775 c.review_versions = {}
775 c.review_versions = {}
776 is_reviewer = PullRequestModel().is_user_reviewer(
776 is_reviewer = PullRequestModel().is_user_reviewer(
777 pull_request, self._rhodecode_user)
777 pull_request, self._rhodecode_user)
778 if is_reviewer:
778 if is_reviewer:
779 for co in general_comments:
779 for co in general_comments:
780 if co.author.user_id == self._rhodecode_user.user_id:
780 if co.author.user_id == self._rhodecode_user.user_id:
781 status = co.status_change
781 status = co.status_change
782 if status:
782 if status:
783 _ver_pr = status[0].comment.pull_request_version_id
783 _ver_pr = status[0].comment.pull_request_version_id
784 c.review_versions[_ver_pr] = status[0]
784 c.review_versions[_ver_pr] = status[0]
785
785
786 return self._get_template_context(c)
786 return self._get_template_context(c)
787
787
788 def get_commits(
788 def get_commits(
789 self, commits_source_repo, pull_request_at_ver, source_commit,
789 self, commits_source_repo, pull_request_at_ver, source_commit,
790 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
790 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
791 maybe_unreachable=False):
791 maybe_unreachable=False):
792
792
793 commit_cache = collections.OrderedDict()
793 commit_cache = collections.OrderedDict()
794 missing_requirements = False
794 missing_requirements = False
795
795
796 try:
796 try:
797 pre_load = ["author", "date", "message", "branch", "parents"]
797 pre_load = ["author", "date", "message", "branch", "parents"]
798
798
799 pull_request_commits = pull_request_at_ver.revisions
799 pull_request_commits = pull_request_at_ver.revisions
800 log.debug('Loading %s commits from %s',
800 log.debug('Loading %s commits from %s',
801 len(pull_request_commits), commits_source_repo)
801 len(pull_request_commits), commits_source_repo)
802
802
803 for rev in pull_request_commits:
803 for rev in pull_request_commits:
804 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
804 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
805 maybe_unreachable=maybe_unreachable)
805 maybe_unreachable=maybe_unreachable)
806 commit_cache[comm.raw_id] = comm
806 commit_cache[comm.raw_id] = comm
807
807
808 # Order here matters, we first need to get target, and then
808 # Order here matters, we first need to get target, and then
809 # the source
809 # the source
810 target_commit = commits_source_repo.get_commit(
810 target_commit = commits_source_repo.get_commit(
811 commit_id=safe_str(target_ref_id))
811 commit_id=safe_str(target_ref_id))
812
812
813 source_commit = commits_source_repo.get_commit(
813 source_commit = commits_source_repo.get_commit(
814 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
814 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
815 except CommitDoesNotExistError:
815 except CommitDoesNotExistError:
816 log.warning('Failed to get commit from `{}` repo'.format(
816 log.warning('Failed to get commit from `{}` repo'.format(
817 commits_source_repo), exc_info=True)
817 commits_source_repo), exc_info=True)
818 except RepositoryRequirementError:
818 except RepositoryRequirementError:
819 log.warning('Failed to get all required data from repo', exc_info=True)
819 log.warning('Failed to get all required data from repo', exc_info=True)
820 missing_requirements = True
820 missing_requirements = True
821
821
822 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
822 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
823
823
824 try:
824 try:
825 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
825 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
826 except Exception:
826 except Exception:
827 ancestor_commit = None
827 ancestor_commit = None
828
828
829 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
829 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
830
830
831 def assure_not_empty_repo(self):
831 def assure_not_empty_repo(self):
832 _ = self.request.translate
832 _ = self.request.translate
833
833
834 try:
834 try:
835 self.db_repo.scm_instance().get_commit()
835 self.db_repo.scm_instance().get_commit()
836 except EmptyRepositoryError:
836 except EmptyRepositoryError:
837 h.flash(h.literal(_('There are no commits yet')),
837 h.flash(h.literal(_('There are no commits yet')),
838 category='warning')
838 category='warning')
839 raise HTTPFound(
839 raise HTTPFound(
840 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
840 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
841
841
842 @LoginRequired()
842 @LoginRequired()
843 @NotAnonymous()
843 @NotAnonymous()
844 @HasRepoPermissionAnyDecorator(
844 @HasRepoPermissionAnyDecorator(
845 'repository.read', 'repository.write', 'repository.admin')
845 'repository.read', 'repository.write', 'repository.admin')
846 def pull_request_new(self):
846 def pull_request_new(self):
847 _ = self.request.translate
847 _ = self.request.translate
848 c = self.load_default_context()
848 c = self.load_default_context()
849
849
850 self.assure_not_empty_repo()
850 self.assure_not_empty_repo()
851 source_repo = self.db_repo
851 source_repo = self.db_repo
852
852
853 commit_id = self.request.GET.get('commit')
853 commit_id = self.request.GET.get('commit')
854 branch_ref = self.request.GET.get('branch')
854 branch_ref = self.request.GET.get('branch')
855 bookmark_ref = self.request.GET.get('bookmark')
855 bookmark_ref = self.request.GET.get('bookmark')
856
856
857 try:
857 try:
858 source_repo_data = PullRequestModel().generate_repo_data(
858 source_repo_data = PullRequestModel().generate_repo_data(
859 source_repo, commit_id=commit_id,
859 source_repo, commit_id=commit_id,
860 branch=branch_ref, bookmark=bookmark_ref,
860 branch=branch_ref, bookmark=bookmark_ref,
861 translator=self.request.translate)
861 translator=self.request.translate)
862 except CommitDoesNotExistError as e:
862 except CommitDoesNotExistError as e:
863 log.exception(e)
863 log.exception(e)
864 h.flash(_('Commit does not exist'), 'error')
864 h.flash(_('Commit does not exist'), 'error')
865 raise HTTPFound(
865 raise HTTPFound(
866 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
866 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
867
867
868 default_target_repo = source_repo
868 default_target_repo = source_repo
869
869
870 if source_repo.parent and c.has_origin_repo_read_perm:
870 if source_repo.parent and c.has_origin_repo_read_perm:
871 parent_vcs_obj = source_repo.parent.scm_instance()
871 parent_vcs_obj = source_repo.parent.scm_instance()
872 if parent_vcs_obj and not parent_vcs_obj.is_empty():
872 if parent_vcs_obj and not parent_vcs_obj.is_empty():
873 # change default if we have a parent repo
873 # change default if we have a parent repo
874 default_target_repo = source_repo.parent
874 default_target_repo = source_repo.parent
875
875
876 target_repo_data = PullRequestModel().generate_repo_data(
876 target_repo_data = PullRequestModel().generate_repo_data(
877 default_target_repo, translator=self.request.translate)
877 default_target_repo, translator=self.request.translate)
878
878
879 selected_source_ref = source_repo_data['refs']['selected_ref']
879 selected_source_ref = source_repo_data['refs']['selected_ref']
880 title_source_ref = ''
880 title_source_ref = ''
881 if selected_source_ref:
881 if selected_source_ref:
882 title_source_ref = selected_source_ref.split(':', 2)[1]
882 title_source_ref = selected_source_ref.split(':', 2)[1]
883 c.default_title = PullRequestModel().generate_pullrequest_title(
883 c.default_title = PullRequestModel().generate_pullrequest_title(
884 source=source_repo.repo_name,
884 source=source_repo.repo_name,
885 source_ref=title_source_ref,
885 source_ref=title_source_ref,
886 target=default_target_repo.repo_name
886 target=default_target_repo.repo_name
887 )
887 )
888
888
889 c.default_repo_data = {
889 c.default_repo_data = {
890 'source_repo_name': source_repo.repo_name,
890 'source_repo_name': source_repo.repo_name,
891 'source_refs_json': json.dumps(source_repo_data),
891 'source_refs_json': json.dumps(source_repo_data),
892 'target_repo_name': default_target_repo.repo_name,
892 'target_repo_name': default_target_repo.repo_name,
893 'target_refs_json': json.dumps(target_repo_data),
893 'target_refs_json': json.dumps(target_repo_data),
894 }
894 }
895 c.default_source_ref = selected_source_ref
895 c.default_source_ref = selected_source_ref
896
896
897 return self._get_template_context(c)
897 return self._get_template_context(c)
898
898
899 @LoginRequired()
899 @LoginRequired()
900 @NotAnonymous()
900 @NotAnonymous()
901 @HasRepoPermissionAnyDecorator(
901 @HasRepoPermissionAnyDecorator(
902 'repository.read', 'repository.write', 'repository.admin')
902 'repository.read', 'repository.write', 'repository.admin')
903 def pull_request_repo_refs(self):
903 def pull_request_repo_refs(self):
904 self.load_default_context()
904 self.load_default_context()
905 target_repo_name = self.request.matchdict['target_repo_name']
905 target_repo_name = self.request.matchdict['target_repo_name']
906 repo = Repository.get_by_repo_name(target_repo_name)
906 repo = Repository.get_by_repo_name(target_repo_name)
907 if not repo:
907 if not repo:
908 raise HTTPNotFound()
908 raise HTTPNotFound()
909
909
910 target_perm = HasRepoPermissionAny(
910 target_perm = HasRepoPermissionAny(
911 'repository.read', 'repository.write', 'repository.admin')(
911 'repository.read', 'repository.write', 'repository.admin')(
912 target_repo_name)
912 target_repo_name)
913 if not target_perm:
913 if not target_perm:
914 raise HTTPNotFound()
914 raise HTTPNotFound()
915
915
916 return PullRequestModel().generate_repo_data(
916 return PullRequestModel().generate_repo_data(
917 repo, translator=self.request.translate)
917 repo, translator=self.request.translate)
918
918
919 @LoginRequired()
919 @LoginRequired()
920 @NotAnonymous()
920 @NotAnonymous()
921 @HasRepoPermissionAnyDecorator(
921 @HasRepoPermissionAnyDecorator(
922 'repository.read', 'repository.write', 'repository.admin')
922 'repository.read', 'repository.write', 'repository.admin')
923 def pullrequest_repo_targets(self):
923 def pullrequest_repo_targets(self):
924 _ = self.request.translate
924 _ = self.request.translate
925 filter_query = self.request.GET.get('query')
925 filter_query = self.request.GET.get('query')
926
926
927 # get the parents
927 # get the parents
928 parent_target_repos = []
928 parent_target_repos = []
929 if self.db_repo.parent:
929 if self.db_repo.parent:
930 parents_query = Repository.query() \
930 parents_query = Repository.query() \
931 .order_by(func.length(Repository.repo_name)) \
931 .order_by(func.length(Repository.repo_name)) \
932 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
932 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
933
933
934 if filter_query:
934 if filter_query:
935 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
935 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
936 parents_query = parents_query.filter(
936 parents_query = parents_query.filter(
937 Repository.repo_name.ilike(ilike_expression))
937 Repository.repo_name.ilike(ilike_expression))
938 parents = parents_query.limit(20).all()
938 parents = parents_query.limit(20).all()
939
939
940 for parent in parents:
940 for parent in parents:
941 parent_vcs_obj = parent.scm_instance()
941 parent_vcs_obj = parent.scm_instance()
942 if parent_vcs_obj and not parent_vcs_obj.is_empty():
942 if parent_vcs_obj and not parent_vcs_obj.is_empty():
943 parent_target_repos.append(parent)
943 parent_target_repos.append(parent)
944
944
945 # get other forks, and repo itself
945 # get other forks, and repo itself
946 query = Repository.query() \
946 query = Repository.query() \
947 .order_by(func.length(Repository.repo_name)) \
947 .order_by(func.length(Repository.repo_name)) \
948 .filter(
948 .filter(
949 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
949 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
950 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
950 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
951 ) \
951 ) \
952 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
952 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
953
953
954 if filter_query:
954 if filter_query:
955 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
955 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
956 query = query.filter(Repository.repo_name.ilike(ilike_expression))
956 query = query.filter(Repository.repo_name.ilike(ilike_expression))
957
957
958 limit = max(20 - len(parent_target_repos), 5) # not less then 5
958 limit = max(20 - len(parent_target_repos), 5) # not less then 5
959 target_repos = query.limit(limit).all()
959 target_repos = query.limit(limit).all()
960
960
961 all_target_repos = target_repos + parent_target_repos
961 all_target_repos = target_repos + parent_target_repos
962
962
963 repos = []
963 repos = []
964 # This checks permissions to the repositories
964 # This checks permissions to the repositories
965 for obj in ScmModel().get_repos(all_target_repos):
965 for obj in ScmModel().get_repos(all_target_repos):
966 repos.append({
966 repos.append({
967 'id': obj['name'],
967 'id': obj['name'],
968 'text': obj['name'],
968 'text': obj['name'],
969 'type': 'repo',
969 'type': 'repo',
970 'repo_id': obj['dbrepo']['repo_id'],
970 'repo_id': obj['dbrepo']['repo_id'],
971 'repo_type': obj['dbrepo']['repo_type'],
971 'repo_type': obj['dbrepo']['repo_type'],
972 'private': obj['dbrepo']['private'],
972 'private': obj['dbrepo']['private'],
973
973
974 })
974 })
975
975
976 data = {
976 data = {
977 'more': False,
977 'more': False,
978 'results': [{
978 'results': [{
979 'text': _('Repositories'),
979 'text': _('Repositories'),
980 'children': repos
980 'children': repos
981 }] if repos else []
981 }] if repos else []
982 }
982 }
983 return data
983 return data
984
984
985 @classmethod
985 @classmethod
986 def get_comment_ids(cls, post_data):
986 def get_comment_ids(cls, post_data):
987 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
987 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
988
988
989 @LoginRequired()
989 @LoginRequired()
990 @NotAnonymous()
990 @NotAnonymous()
991 @HasRepoPermissionAnyDecorator(
991 @HasRepoPermissionAnyDecorator(
992 'repository.read', 'repository.write', 'repository.admin')
992 'repository.read', 'repository.write', 'repository.admin')
993 def pullrequest_comments(self):
993 def pullrequest_comments(self):
994 self.load_default_context()
994 self.load_default_context()
995
995
996 pull_request = PullRequest.get_or_404(
996 pull_request = PullRequest.get_or_404(
997 self.request.matchdict['pull_request_id'])
997 self.request.matchdict['pull_request_id'])
998 pull_request_id = pull_request.pull_request_id
998 pull_request_id = pull_request.pull_request_id
999 version = self.request.GET.get('version')
999 version = self.request.GET.get('version')
1000
1000
1001 _render = self.request.get_partial_renderer(
1001 _render = self.request.get_partial_renderer(
1002 'rhodecode:templates/base/sidebar.mako')
1002 'rhodecode:templates/base/sidebar.mako')
1003 c = _render.get_call_context()
1003 c = _render.get_call_context()
1004
1004
1005 (pull_request_latest,
1005 (pull_request_latest,
1006 pull_request_at_ver,
1006 pull_request_at_ver,
1007 pull_request_display_obj,
1007 pull_request_display_obj,
1008 at_version) = PullRequestModel().get_pr_version(
1008 at_version) = PullRequestModel().get_pr_version(
1009 pull_request_id, version=version)
1009 pull_request_id, version=version)
1010 versions = pull_request_display_obj.versions()
1010 versions = pull_request_display_obj.versions()
1011 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1011 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1012 c.versions = versions + [latest_ver]
1012 c.versions = versions + [latest_ver]
1013
1013
1014 c.at_version = at_version
1014 c.at_version = at_version
1015 c.at_version_num = (at_version
1015 c.at_version_num = (at_version
1016 if at_version and at_version != PullRequest.LATEST_VER
1016 if at_version and at_version != PullRequest.LATEST_VER
1017 else None)
1017 else None)
1018
1018
1019 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1019 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1020 all_comments = c.inline_comments_flat + c.comments
1020 all_comments = c.inline_comments_flat + c.comments
1021
1021
1022 existing_ids = self.get_comment_ids(self.request.POST)
1022 existing_ids = self.get_comment_ids(self.request.POST)
1023 return _render('comments_table', all_comments, len(all_comments),
1023 return _render('comments_table', all_comments, len(all_comments),
1024 existing_ids=existing_ids)
1024 existing_ids=existing_ids)
1025
1025
1026 @LoginRequired()
1026 @LoginRequired()
1027 @NotAnonymous()
1027 @NotAnonymous()
1028 @HasRepoPermissionAnyDecorator(
1028 @HasRepoPermissionAnyDecorator(
1029 'repository.read', 'repository.write', 'repository.admin')
1029 'repository.read', 'repository.write', 'repository.admin')
1030 def pullrequest_todos(self):
1030 def pullrequest_todos(self):
1031 self.load_default_context()
1031 self.load_default_context()
1032
1032
1033 pull_request = PullRequest.get_or_404(
1033 pull_request = PullRequest.get_or_404(
1034 self.request.matchdict['pull_request_id'])
1034 self.request.matchdict['pull_request_id'])
1035 pull_request_id = pull_request.pull_request_id
1035 pull_request_id = pull_request.pull_request_id
1036 version = self.request.GET.get('version')
1036 version = self.request.GET.get('version')
1037
1037
1038 _render = self.request.get_partial_renderer(
1038 _render = self.request.get_partial_renderer(
1039 'rhodecode:templates/base/sidebar.mako')
1039 'rhodecode:templates/base/sidebar.mako')
1040 c = _render.get_call_context()
1040 c = _render.get_call_context()
1041 (pull_request_latest,
1041 (pull_request_latest,
1042 pull_request_at_ver,
1042 pull_request_at_ver,
1043 pull_request_display_obj,
1043 pull_request_display_obj,
1044 at_version) = PullRequestModel().get_pr_version(
1044 at_version) = PullRequestModel().get_pr_version(
1045 pull_request_id, version=version)
1045 pull_request_id, version=version)
1046 versions = pull_request_display_obj.versions()
1046 versions = pull_request_display_obj.versions()
1047 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1047 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1048 c.versions = versions + [latest_ver]
1048 c.versions = versions + [latest_ver]
1049
1049
1050 c.at_version = at_version
1050 c.at_version = at_version
1051 c.at_version_num = (at_version
1051 c.at_version_num = (at_version
1052 if at_version and at_version != PullRequest.LATEST_VER
1052 if at_version and at_version != PullRequest.LATEST_VER
1053 else None)
1053 else None)
1054
1054
1055 c.unresolved_comments = CommentsModel() \
1055 c.unresolved_comments = CommentsModel() \
1056 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1056 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1057 c.resolved_comments = CommentsModel() \
1057 c.resolved_comments = CommentsModel() \
1058 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1058 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1059
1059
1060 all_comments = c.unresolved_comments + c.resolved_comments
1060 all_comments = c.unresolved_comments + c.resolved_comments
1061 existing_ids = self.get_comment_ids(self.request.POST)
1061 existing_ids = self.get_comment_ids(self.request.POST)
1062 return _render('comments_table', all_comments, len(c.unresolved_comments),
1062 return _render('comments_table', all_comments, len(c.unresolved_comments),
1063 todo_comments=True, existing_ids=existing_ids)
1063 todo_comments=True, existing_ids=existing_ids)
1064
1064
1065 @LoginRequired()
1065 @LoginRequired()
1066 @NotAnonymous()
1066 @NotAnonymous()
1067 @HasRepoPermissionAnyDecorator(
1067 @HasRepoPermissionAnyDecorator(
1068 'repository.read', 'repository.write', 'repository.admin')
1068 'repository.read', 'repository.write', 'repository.admin')
1069 def pullrequest_drafts(self):
1069 def pullrequest_drafts(self):
1070 self.load_default_context()
1070 self.load_default_context()
1071
1071
1072 pull_request = PullRequest.get_or_404(
1072 pull_request = PullRequest.get_or_404(
1073 self.request.matchdict['pull_request_id'])
1073 self.request.matchdict['pull_request_id'])
1074 pull_request_id = pull_request.pull_request_id
1074 pull_request_id = pull_request.pull_request_id
1075 version = self.request.GET.get('version')
1075 version = self.request.GET.get('version')
1076
1076
1077 _render = self.request.get_partial_renderer(
1077 _render = self.request.get_partial_renderer(
1078 'rhodecode:templates/base/sidebar.mako')
1078 'rhodecode:templates/base/sidebar.mako')
1079 c = _render.get_call_context()
1079 c = _render.get_call_context()
1080
1080
1081 (pull_request_latest,
1081 (pull_request_latest,
1082 pull_request_at_ver,
1082 pull_request_at_ver,
1083 pull_request_display_obj,
1083 pull_request_display_obj,
1084 at_version) = PullRequestModel().get_pr_version(
1084 at_version) = PullRequestModel().get_pr_version(
1085 pull_request_id, version=version)
1085 pull_request_id, version=version)
1086 versions = pull_request_display_obj.versions()
1086 versions = pull_request_display_obj.versions()
1087 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1087 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1088 c.versions = versions + [latest_ver]
1088 c.versions = versions + [latest_ver]
1089
1089
1090 c.at_version = at_version
1090 c.at_version = at_version
1091 c.at_version_num = (at_version
1091 c.at_version_num = (at_version
1092 if at_version and at_version != PullRequest.LATEST_VER
1092 if at_version and at_version != PullRequest.LATEST_VER
1093 else None)
1093 else None)
1094
1094
1095 c.draft_comments = CommentsModel() \
1095 c.draft_comments = CommentsModel() \
1096 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1096 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1097
1097
1098 all_comments = c.draft_comments
1098 all_comments = c.draft_comments
1099
1099
1100 existing_ids = self.get_comment_ids(self.request.POST)
1100 existing_ids = self.get_comment_ids(self.request.POST)
1101 return _render('comments_table', all_comments, len(all_comments),
1101 return _render('comments_table', all_comments, len(all_comments),
1102 existing_ids=existing_ids, draft_comments=True)
1102 existing_ids=existing_ids, draft_comments=True)
1103
1103
1104 @LoginRequired()
1104 @LoginRequired()
1105 @NotAnonymous()
1105 @NotAnonymous()
1106 @HasRepoPermissionAnyDecorator(
1106 @HasRepoPermissionAnyDecorator(
1107 'repository.read', 'repository.write', 'repository.admin')
1107 'repository.read', 'repository.write', 'repository.admin')
1108 @CSRFRequired()
1108 @CSRFRequired()
1109 def pull_request_create(self):
1109 def pull_request_create(self):
1110 _ = self.request.translate
1110 _ = self.request.translate
1111 self.assure_not_empty_repo()
1111 self.assure_not_empty_repo()
1112 self.load_default_context()
1112 self.load_default_context()
1113
1113
1114 controls = peppercorn.parse(self.request.POST.items())
1114 controls = peppercorn.parse(self.request.POST.items())
1115
1115
1116 try:
1116 try:
1117 form = PullRequestForm(
1117 form = PullRequestForm(
1118 self.request.translate, self.db_repo.repo_id)()
1118 self.request.translate, self.db_repo.repo_id)()
1119 _form = form.to_python(controls)
1119 _form = form.to_python(controls)
1120 except formencode.Invalid as errors:
1120 except formencode.Invalid as errors:
1121 if errors.error_dict.get('revisions'):
1121 if errors.error_dict.get('revisions'):
1122 msg = 'Revisions: %s' % errors.error_dict['revisions']
1122 msg = 'Revisions: %s' % errors.error_dict['revisions']
1123 elif errors.error_dict.get('pullrequest_title'):
1123 elif errors.error_dict.get('pullrequest_title'):
1124 msg = errors.error_dict.get('pullrequest_title')
1124 msg = errors.error_dict.get('pullrequest_title')
1125 else:
1125 else:
1126 msg = _('Error creating pull request: {}').format(errors)
1126 msg = _('Error creating pull request: {}').format(errors)
1127 log.exception(msg)
1127 log.exception(msg)
1128 h.flash(msg, 'error')
1128 h.flash(msg, 'error')
1129
1129
1130 # would rather just go back to form ...
1130 # would rather just go back to form ...
1131 raise HTTPFound(
1131 raise HTTPFound(
1132 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1132 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1133
1133
1134 source_repo = _form['source_repo']
1134 source_repo = _form['source_repo']
1135 source_ref = _form['source_ref']
1135 source_ref = _form['source_ref']
1136 target_repo = _form['target_repo']
1136 target_repo = _form['target_repo']
1137 target_ref = _form['target_ref']
1137 target_ref = _form['target_ref']
1138 commit_ids = _form['revisions'][::-1]
1138 commit_ids = _form['revisions'][::-1]
1139 common_ancestor_id = _form['common_ancestor']
1139 common_ancestor_id = _form['common_ancestor']
1140
1140
1141 # find the ancestor for this pr
1141 # find the ancestor for this pr
1142 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1142 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1143 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1143 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1144
1144
1145 if not (source_db_repo or target_db_repo):
1145 if not (source_db_repo or target_db_repo):
1146 h.flash(_('source_repo or target repo not found'), category='error')
1146 h.flash(_('source_repo or target repo not found'), category='error')
1147 raise HTTPFound(
1147 raise HTTPFound(
1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1149
1149
1150 # re-check permissions again here
1150 # re-check permissions again here
1151 # source_repo we must have read permissions
1151 # source_repo we must have read permissions
1152
1152
1153 source_perm = HasRepoPermissionAny(
1153 source_perm = HasRepoPermissionAny(
1154 'repository.read', 'repository.write', 'repository.admin')(
1154 'repository.read', 'repository.write', 'repository.admin')(
1155 source_db_repo.repo_name)
1155 source_db_repo.repo_name)
1156 if not source_perm:
1156 if not source_perm:
1157 msg = _('Not Enough permissions to source repo `{}`.'.format(
1157 msg = _('Not Enough permissions to source repo `{}`.'.format(
1158 source_db_repo.repo_name))
1158 source_db_repo.repo_name))
1159 h.flash(msg, category='error')
1159 h.flash(msg, category='error')
1160 # copy the args back to redirect
1160 # copy the args back to redirect
1161 org_query = self.request.GET.mixed()
1161 org_query = self.request.GET.mixed()
1162 raise HTTPFound(
1162 raise HTTPFound(
1163 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1163 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1164 _query=org_query))
1164 _query=org_query))
1165
1165
1166 # target repo we must have read permissions, and also later on
1166 # target repo we must have read permissions, and also later on
1167 # we want to check branch permissions here
1167 # we want to check branch permissions here
1168 target_perm = HasRepoPermissionAny(
1168 target_perm = HasRepoPermissionAny(
1169 'repository.read', 'repository.write', 'repository.admin')(
1169 'repository.read', 'repository.write', 'repository.admin')(
1170 target_db_repo.repo_name)
1170 target_db_repo.repo_name)
1171 if not target_perm:
1171 if not target_perm:
1172 msg = _('Not Enough permissions to target repo `{}`.'.format(
1172 msg = _('Not Enough permissions to target repo `{}`.'.format(
1173 target_db_repo.repo_name))
1173 target_db_repo.repo_name))
1174 h.flash(msg, category='error')
1174 h.flash(msg, category='error')
1175 # copy the args back to redirect
1175 # copy the args back to redirect
1176 org_query = self.request.GET.mixed()
1176 org_query = self.request.GET.mixed()
1177 raise HTTPFound(
1177 raise HTTPFound(
1178 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1178 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1179 _query=org_query))
1179 _query=org_query))
1180
1180
1181 source_scm = source_db_repo.scm_instance()
1181 source_scm = source_db_repo.scm_instance()
1182 target_scm = target_db_repo.scm_instance()
1182 target_scm = target_db_repo.scm_instance()
1183
1183
1184 source_ref_obj = unicode_to_reference(source_ref)
1184 source_ref_obj = unicode_to_reference(source_ref)
1185 target_ref_obj = unicode_to_reference(target_ref)
1185 target_ref_obj = unicode_to_reference(target_ref)
1186
1186
1187 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1187 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1188 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1188 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1189
1189
1190 ancestor = source_scm.get_common_ancestor(
1190 ancestor = source_scm.get_common_ancestor(
1191 source_commit.raw_id, target_commit.raw_id, target_scm)
1191 source_commit.raw_id, target_commit.raw_id, target_scm)
1192
1192
1193 # recalculate target ref based on ancestor
1193 # recalculate target ref based on ancestor
1194 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1194 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1195
1195
1196 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1196 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1197 PullRequestModel().get_reviewer_functions()
1197 PullRequestModel().get_reviewer_functions()
1198
1198
1199 # recalculate reviewers logic, to make sure we can validate this
1199 # recalculate reviewers logic, to make sure we can validate this
1200 reviewer_rules = get_default_reviewers_data(
1200 reviewer_rules = get_default_reviewers_data(
1201 self._rhodecode_db_user,
1201 self._rhodecode_db_user,
1202 source_db_repo,
1202 source_db_repo,
1203 source_ref_obj,
1203 source_ref_obj,
1204 target_db_repo,
1204 target_db_repo,
1205 target_ref_obj,
1205 target_ref_obj,
1206 include_diff_info=False)
1206 include_diff_info=False)
1207
1207
1208 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1208 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1209 observers = validate_observers(_form['observer_members'], reviewer_rules)
1209 observers = validate_observers(_form['observer_members'], reviewer_rules)
1210
1210
1211 pullrequest_title = _form['pullrequest_title']
1211 pullrequest_title = _form['pullrequest_title']
1212 title_source_ref = source_ref_obj.name
1212 title_source_ref = source_ref_obj.name
1213 if not pullrequest_title:
1213 if not pullrequest_title:
1214 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1214 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1215 source=source_repo,
1215 source=source_repo,
1216 source_ref=title_source_ref,
1216 source_ref=title_source_ref,
1217 target=target_repo
1217 target=target_repo
1218 )
1218 )
1219
1219
1220 description = _form['pullrequest_desc']
1220 description = _form['pullrequest_desc']
1221 description_renderer = _form['description_renderer']
1221 description_renderer = _form['description_renderer']
1222
1222
1223 try:
1223 try:
1224 pull_request = PullRequestModel().create(
1224 pull_request = PullRequestModel().create(
1225 created_by=self._rhodecode_user.user_id,
1225 created_by=self._rhodecode_user.user_id,
1226 source_repo=source_repo,
1226 source_repo=source_repo,
1227 source_ref=source_ref,
1227 source_ref=source_ref,
1228 target_repo=target_repo,
1228 target_repo=target_repo,
1229 target_ref=target_ref,
1229 target_ref=target_ref,
1230 revisions=commit_ids,
1230 revisions=commit_ids,
1231 common_ancestor_id=common_ancestor_id,
1231 common_ancestor_id=common_ancestor_id,
1232 reviewers=reviewers,
1232 reviewers=reviewers,
1233 observers=observers,
1233 observers=observers,
1234 title=pullrequest_title,
1234 title=pullrequest_title,
1235 description=description,
1235 description=description,
1236 description_renderer=description_renderer,
1236 description_renderer=description_renderer,
1237 reviewer_data=reviewer_rules,
1237 reviewer_data=reviewer_rules,
1238 auth_user=self._rhodecode_user
1238 auth_user=self._rhodecode_user
1239 )
1239 )
1240 Session().commit()
1240 Session().commit()
1241
1241
1242 h.flash(_('Successfully opened new pull request'),
1242 h.flash(_('Successfully opened new pull request'),
1243 category='success')
1243 category='success')
1244 except Exception:
1244 except Exception:
1245 msg = _('Error occurred during creation of this pull request.')
1245 msg = _('Error occurred during creation of this pull request.')
1246 log.exception(msg)
1246 log.exception(msg)
1247 h.flash(msg, category='error')
1247 h.flash(msg, category='error')
1248
1248
1249 # copy the args back to redirect
1249 # copy the args back to redirect
1250 org_query = self.request.GET.mixed()
1250 org_query = self.request.GET.mixed()
1251 raise HTTPFound(
1251 raise HTTPFound(
1252 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1252 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1253 _query=org_query))
1253 _query=org_query))
1254
1254
1255 raise HTTPFound(
1255 raise HTTPFound(
1256 h.route_path('pullrequest_show', repo_name=target_repo,
1256 h.route_path('pullrequest_show', repo_name=target_repo,
1257 pull_request_id=pull_request.pull_request_id))
1257 pull_request_id=pull_request.pull_request_id))
1258
1258
1259 @LoginRequired()
1259 @LoginRequired()
1260 @NotAnonymous()
1260 @NotAnonymous()
1261 @HasRepoPermissionAnyDecorator(
1261 @HasRepoPermissionAnyDecorator(
1262 'repository.read', 'repository.write', 'repository.admin')
1262 'repository.read', 'repository.write', 'repository.admin')
1263 @CSRFRequired()
1263 @CSRFRequired()
1264 def pull_request_update(self):
1264 def pull_request_update(self):
1265 pull_request = PullRequest.get_or_404(
1265 pull_request = PullRequest.get_or_404(
1266 self.request.matchdict['pull_request_id'])
1266 self.request.matchdict['pull_request_id'])
1267 _ = self.request.translate
1267 _ = self.request.translate
1268
1268
1269 c = self.load_default_context()
1269 c = self.load_default_context()
1270 redirect_url = None
1270 redirect_url = None
1271
1271
1272 if pull_request.is_closed():
1272 if pull_request.is_closed():
1273 log.debug('update: forbidden because pull request is closed')
1273 log.debug('update: forbidden because pull request is closed')
1274 msg = _(u'Cannot update closed pull requests.')
1274 msg = _(u'Cannot update closed pull requests.')
1275 h.flash(msg, category='error')
1275 h.flash(msg, category='error')
1276 return {'response': True,
1276 return {'response': True,
1277 'redirect_url': redirect_url}
1277 'redirect_url': redirect_url}
1278
1278
1279 is_state_changing = pull_request.is_state_changing()
1279 is_state_changing = pull_request.is_state_changing()
1280 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1280 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1281
1281
1282 # only owner or admin can update it
1282 # only owner or admin can update it
1283 allowed_to_update = PullRequestModel().check_user_update(
1283 allowed_to_update = PullRequestModel().check_user_update(
1284 pull_request, self._rhodecode_user)
1284 pull_request, self._rhodecode_user)
1285
1285
1286 if allowed_to_update:
1286 if allowed_to_update:
1287 controls = peppercorn.parse(self.request.POST.items())
1287 controls = peppercorn.parse(self.request.POST.items())
1288 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1288 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1289
1289
1290 if 'review_members' in controls:
1290 if 'review_members' in controls:
1291 self._update_reviewers(
1291 self._update_reviewers(
1292 c,
1292 c,
1293 pull_request, controls['review_members'],
1293 pull_request, controls['review_members'],
1294 pull_request.reviewer_data,
1294 pull_request.reviewer_data,
1295 PullRequestReviewers.ROLE_REVIEWER)
1295 PullRequestReviewers.ROLE_REVIEWER)
1296 elif 'observer_members' in controls:
1296 elif 'observer_members' in controls:
1297 self._update_reviewers(
1297 self._update_reviewers(
1298 c,
1298 c,
1299 pull_request, controls['observer_members'],
1299 pull_request, controls['observer_members'],
1300 pull_request.reviewer_data,
1300 pull_request.reviewer_data,
1301 PullRequestReviewers.ROLE_OBSERVER)
1301 PullRequestReviewers.ROLE_OBSERVER)
1302 elif str2bool(self.request.POST.get('update_commits', 'false')):
1302 elif str2bool(self.request.POST.get('update_commits', 'false')):
1303 if is_state_changing:
1303 if is_state_changing:
1304 log.debug('commits update: forbidden because pull request is in state %s',
1304 log.debug('commits update: forbidden because pull request is in state %s',
1305 pull_request.pull_request_state)
1305 pull_request.pull_request_state)
1306 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1306 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1307 u'Current state is: `{}`').format(
1307 u'Current state is: `{}`').format(
1308 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1308 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1309 h.flash(msg, category='error')
1309 h.flash(msg, category='error')
1310 return {'response': True,
1310 return {'response': True,
1311 'redirect_url': redirect_url}
1311 'redirect_url': redirect_url}
1312
1312
1313 self._update_commits(c, pull_request)
1313 self._update_commits(c, pull_request)
1314 if force_refresh:
1314 if force_refresh:
1315 redirect_url = h.route_path(
1315 redirect_url = h.route_path(
1316 'pullrequest_show', repo_name=self.db_repo_name,
1316 'pullrequest_show', repo_name=self.db_repo_name,
1317 pull_request_id=pull_request.pull_request_id,
1317 pull_request_id=pull_request.pull_request_id,
1318 _query={"force_refresh": 1})
1318 _query={"force_refresh": 1})
1319 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1319 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1320 self._edit_pull_request(pull_request)
1320 self._edit_pull_request(pull_request)
1321 else:
1321 else:
1322 log.error('Unhandled update data.')
1322 log.error('Unhandled update data.')
1323 raise HTTPBadRequest()
1323 raise HTTPBadRequest()
1324
1324
1325 return {'response': True,
1325 return {'response': True,
1326 'redirect_url': redirect_url}
1326 'redirect_url': redirect_url}
1327 raise HTTPForbidden()
1327 raise HTTPForbidden()
1328
1328
1329 def _edit_pull_request(self, pull_request):
1329 def _edit_pull_request(self, pull_request):
1330 """
1330 """
1331 Edit title and description
1331 Edit title and description
1332 """
1332 """
1333 _ = self.request.translate
1333 _ = self.request.translate
1334
1334
1335 try:
1335 try:
1336 PullRequestModel().edit(
1336 PullRequestModel().edit(
1337 pull_request,
1337 pull_request,
1338 self.request.POST.get('title'),
1338 self.request.POST.get('title'),
1339 self.request.POST.get('description'),
1339 self.request.POST.get('description'),
1340 self.request.POST.get('description_renderer'),
1340 self.request.POST.get('description_renderer'),
1341 self._rhodecode_user)
1341 self._rhodecode_user)
1342 except ValueError:
1342 except ValueError:
1343 msg = _(u'Cannot update closed pull requests.')
1343 msg = _(u'Cannot update closed pull requests.')
1344 h.flash(msg, category='error')
1344 h.flash(msg, category='error')
1345 return
1345 return
1346 else:
1346 else:
1347 Session().commit()
1347 Session().commit()
1348
1348
1349 msg = _(u'Pull request title & description updated.')
1349 msg = _(u'Pull request title & description updated.')
1350 h.flash(msg, category='success')
1350 h.flash(msg, category='success')
1351 return
1351 return
1352
1352
1353 def _update_commits(self, c, pull_request):
1353 def _update_commits(self, c, pull_request):
1354 _ = self.request.translate
1354 _ = self.request.translate
1355
1355
1356 @retry(exception=Exception, n_tries=3)
1357 def commits_update():
1358 return PullRequestModel().update_commits(
1359 pull_request, self._rhodecode_db_user)
1360
1356 with pull_request.set_state(PullRequest.STATE_UPDATING):
1361 with pull_request.set_state(PullRequest.STATE_UPDATING):
1357 resp = PullRequestModel().update_commits(
1362 resp = commits_update() # retry x3
1358 pull_request, self._rhodecode_db_user)
1359
1363
1360 if resp.executed:
1364 if resp.executed:
1361
1365
1362 if resp.target_changed and resp.source_changed:
1366 if resp.target_changed and resp.source_changed:
1363 changed = 'target and source repositories'
1367 changed = 'target and source repositories'
1364 elif resp.target_changed and not resp.source_changed:
1368 elif resp.target_changed and not resp.source_changed:
1365 changed = 'target repository'
1369 changed = 'target repository'
1366 elif not resp.target_changed and resp.source_changed:
1370 elif not resp.target_changed and resp.source_changed:
1367 changed = 'source repository'
1371 changed = 'source repository'
1368 else:
1372 else:
1369 changed = 'nothing'
1373 changed = 'nothing'
1370
1374
1371 msg = _(u'Pull request updated to "{source_commit_id}" with '
1375 msg = _(u'Pull request updated to "{source_commit_id}" with '
1372 u'{count_added} added, {count_removed} removed commits. '
1376 u'{count_added} added, {count_removed} removed commits. '
1373 u'Source of changes: {change_source}.')
1377 u'Source of changes: {change_source}.')
1374 msg = msg.format(
1378 msg = msg.format(
1375 source_commit_id=pull_request.source_ref_parts.commit_id,
1379 source_commit_id=pull_request.source_ref_parts.commit_id,
1376 count_added=len(resp.changes.added),
1380 count_added=len(resp.changes.added),
1377 count_removed=len(resp.changes.removed),
1381 count_removed=len(resp.changes.removed),
1378 change_source=changed)
1382 change_source=changed)
1379 h.flash(msg, category='success')
1383 h.flash(msg, category='success')
1380 channelstream.pr_update_channelstream_push(
1384 channelstream.pr_update_channelstream_push(
1381 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1385 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1382 else:
1386 else:
1383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1387 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1384 warning_reasons = [
1388 warning_reasons = [
1385 UpdateFailureReason.NO_CHANGE,
1389 UpdateFailureReason.NO_CHANGE,
1386 UpdateFailureReason.WRONG_REF_TYPE,
1390 UpdateFailureReason.WRONG_REF_TYPE,
1387 ]
1391 ]
1388 category = 'warning' if resp.reason in warning_reasons else 'error'
1392 category = 'warning' if resp.reason in warning_reasons else 'error'
1389 h.flash(msg, category=category)
1393 h.flash(msg, category=category)
1390
1394
1391 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1395 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1392 _ = self.request.translate
1396 _ = self.request.translate
1393
1397
1394 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1398 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1395 PullRequestModel().get_reviewer_functions()
1399 PullRequestModel().get_reviewer_functions()
1396
1400
1397 if role == PullRequestReviewers.ROLE_REVIEWER:
1401 if role == PullRequestReviewers.ROLE_REVIEWER:
1398 try:
1402 try:
1399 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1403 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1400 except ValueError as e:
1404 except ValueError as e:
1401 log.error('Reviewers Validation: {}'.format(e))
1405 log.error('Reviewers Validation: {}'.format(e))
1402 h.flash(e, category='error')
1406 h.flash(e, category='error')
1403 return
1407 return
1404
1408
1405 old_calculated_status = pull_request.calculated_review_status()
1409 old_calculated_status = pull_request.calculated_review_status()
1406 PullRequestModel().update_reviewers(
1410 PullRequestModel().update_reviewers(
1407 pull_request, reviewers, self._rhodecode_db_user)
1411 pull_request, reviewers, self._rhodecode_db_user)
1408
1412
1409 Session().commit()
1413 Session().commit()
1410
1414
1411 msg = _('Pull request reviewers updated.')
1415 msg = _('Pull request reviewers updated.')
1412 h.flash(msg, category='success')
1416 h.flash(msg, category='success')
1413 channelstream.pr_update_channelstream_push(
1417 channelstream.pr_update_channelstream_push(
1414 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1418 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1415
1419
1416 # trigger status changed if change in reviewers changes the status
1420 # trigger status changed if change in reviewers changes the status
1417 calculated_status = pull_request.calculated_review_status()
1421 calculated_status = pull_request.calculated_review_status()
1418 if old_calculated_status != calculated_status:
1422 if old_calculated_status != calculated_status:
1419 PullRequestModel().trigger_pull_request_hook(
1423 PullRequestModel().trigger_pull_request_hook(
1420 pull_request, self._rhodecode_user, 'review_status_change',
1424 pull_request, self._rhodecode_user, 'review_status_change',
1421 data={'status': calculated_status})
1425 data={'status': calculated_status})
1422
1426
1423 elif role == PullRequestReviewers.ROLE_OBSERVER:
1427 elif role == PullRequestReviewers.ROLE_OBSERVER:
1424 try:
1428 try:
1425 observers = validate_observers(review_members, reviewer_rules)
1429 observers = validate_observers(review_members, reviewer_rules)
1426 except ValueError as e:
1430 except ValueError as e:
1427 log.error('Observers Validation: {}'.format(e))
1431 log.error('Observers Validation: {}'.format(e))
1428 h.flash(e, category='error')
1432 h.flash(e, category='error')
1429 return
1433 return
1430
1434
1431 PullRequestModel().update_observers(
1435 PullRequestModel().update_observers(
1432 pull_request, observers, self._rhodecode_db_user)
1436 pull_request, observers, self._rhodecode_db_user)
1433
1437
1434 Session().commit()
1438 Session().commit()
1435 msg = _('Pull request observers updated.')
1439 msg = _('Pull request observers updated.')
1436 h.flash(msg, category='success')
1440 h.flash(msg, category='success')
1437 channelstream.pr_update_channelstream_push(
1441 channelstream.pr_update_channelstream_push(
1438 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1442 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1439
1443
1440 @LoginRequired()
1444 @LoginRequired()
1441 @NotAnonymous()
1445 @NotAnonymous()
1442 @HasRepoPermissionAnyDecorator(
1446 @HasRepoPermissionAnyDecorator(
1443 'repository.read', 'repository.write', 'repository.admin')
1447 'repository.read', 'repository.write', 'repository.admin')
1444 @CSRFRequired()
1448 @CSRFRequired()
1445 def pull_request_merge(self):
1449 def pull_request_merge(self):
1446 """
1450 """
1447 Merge will perform a server-side merge of the specified
1451 Merge will perform a server-side merge of the specified
1448 pull request, if the pull request is approved and mergeable.
1452 pull request, if the pull request is approved and mergeable.
1449 After successful merging, the pull request is automatically
1453 After successful merging, the pull request is automatically
1450 closed, with a relevant comment.
1454 closed, with a relevant comment.
1451 """
1455 """
1452 pull_request = PullRequest.get_or_404(
1456 pull_request = PullRequest.get_or_404(
1453 self.request.matchdict['pull_request_id'])
1457 self.request.matchdict['pull_request_id'])
1454 _ = self.request.translate
1458 _ = self.request.translate
1455
1459
1456 if pull_request.is_state_changing():
1460 if pull_request.is_state_changing():
1457 log.debug('show: forbidden because pull request is in state %s',
1461 log.debug('show: forbidden because pull request is in state %s',
1458 pull_request.pull_request_state)
1462 pull_request.pull_request_state)
1459 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1463 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1460 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1464 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1461 pull_request.pull_request_state)
1465 pull_request.pull_request_state)
1462 h.flash(msg, category='error')
1466 h.flash(msg, category='error')
1463 raise HTTPFound(
1467 raise HTTPFound(
1464 h.route_path('pullrequest_show',
1468 h.route_path('pullrequest_show',
1465 repo_name=pull_request.target_repo.repo_name,
1469 repo_name=pull_request.target_repo.repo_name,
1466 pull_request_id=pull_request.pull_request_id))
1470 pull_request_id=pull_request.pull_request_id))
1467
1471
1468 self.load_default_context()
1472 self.load_default_context()
1469
1473
1470 with pull_request.set_state(PullRequest.STATE_UPDATING):
1474 with pull_request.set_state(PullRequest.STATE_UPDATING):
1471 check = MergeCheck.validate(
1475 check = MergeCheck.validate(
1472 pull_request, auth_user=self._rhodecode_user,
1476 pull_request, auth_user=self._rhodecode_user,
1473 translator=self.request.translate)
1477 translator=self.request.translate)
1474 merge_possible = not check.failed
1478 merge_possible = not check.failed
1475
1479
1476 for err_type, error_msg in check.errors:
1480 for err_type, error_msg in check.errors:
1477 h.flash(error_msg, category=err_type)
1481 h.flash(error_msg, category=err_type)
1478
1482
1479 if merge_possible:
1483 if merge_possible:
1480 log.debug("Pre-conditions checked, trying to merge.")
1484 log.debug("Pre-conditions checked, trying to merge.")
1481 extras = vcs_operation_context(
1485 extras = vcs_operation_context(
1482 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1486 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1483 username=self._rhodecode_db_user.username, action='push',
1487 username=self._rhodecode_db_user.username, action='push',
1484 scm=pull_request.target_repo.repo_type)
1488 scm=pull_request.target_repo.repo_type)
1485 with pull_request.set_state(PullRequest.STATE_UPDATING):
1489 with pull_request.set_state(PullRequest.STATE_UPDATING):
1486 self._merge_pull_request(
1490 self._merge_pull_request(
1487 pull_request, self._rhodecode_db_user, extras)
1491 pull_request, self._rhodecode_db_user, extras)
1488 else:
1492 else:
1489 log.debug("Pre-conditions failed, NOT merging.")
1493 log.debug("Pre-conditions failed, NOT merging.")
1490
1494
1491 raise HTTPFound(
1495 raise HTTPFound(
1492 h.route_path('pullrequest_show',
1496 h.route_path('pullrequest_show',
1493 repo_name=pull_request.target_repo.repo_name,
1497 repo_name=pull_request.target_repo.repo_name,
1494 pull_request_id=pull_request.pull_request_id))
1498 pull_request_id=pull_request.pull_request_id))
1495
1499
1496 def _merge_pull_request(self, pull_request, user, extras):
1500 def _merge_pull_request(self, pull_request, user, extras):
1497 _ = self.request.translate
1501 _ = self.request.translate
1498 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1502 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1499
1503
1500 if merge_resp.executed:
1504 if merge_resp.executed:
1501 log.debug("The merge was successful, closing the pull request.")
1505 log.debug("The merge was successful, closing the pull request.")
1502 PullRequestModel().close_pull_request(
1506 PullRequestModel().close_pull_request(
1503 pull_request.pull_request_id, user)
1507 pull_request.pull_request_id, user)
1504 Session().commit()
1508 Session().commit()
1505 msg = _('Pull request was successfully merged and closed.')
1509 msg = _('Pull request was successfully merged and closed.')
1506 h.flash(msg, category='success')
1510 h.flash(msg, category='success')
1507 else:
1511 else:
1508 log.debug(
1512 log.debug(
1509 "The merge was not successful. Merge response: %s", merge_resp)
1513 "The merge was not successful. Merge response: %s", merge_resp)
1510 msg = merge_resp.merge_status_message
1514 msg = merge_resp.merge_status_message
1511 h.flash(msg, category='error')
1515 h.flash(msg, category='error')
1512
1516
1513 @LoginRequired()
1517 @LoginRequired()
1514 @NotAnonymous()
1518 @NotAnonymous()
1515 @HasRepoPermissionAnyDecorator(
1519 @HasRepoPermissionAnyDecorator(
1516 'repository.read', 'repository.write', 'repository.admin')
1520 'repository.read', 'repository.write', 'repository.admin')
1517 @CSRFRequired()
1521 @CSRFRequired()
1518 def pull_request_delete(self):
1522 def pull_request_delete(self):
1519 _ = self.request.translate
1523 _ = self.request.translate
1520
1524
1521 pull_request = PullRequest.get_or_404(
1525 pull_request = PullRequest.get_or_404(
1522 self.request.matchdict['pull_request_id'])
1526 self.request.matchdict['pull_request_id'])
1523 self.load_default_context()
1527 self.load_default_context()
1524
1528
1525 pr_closed = pull_request.is_closed()
1529 pr_closed = pull_request.is_closed()
1526 allowed_to_delete = PullRequestModel().check_user_delete(
1530 allowed_to_delete = PullRequestModel().check_user_delete(
1527 pull_request, self._rhodecode_user) and not pr_closed
1531 pull_request, self._rhodecode_user) and not pr_closed
1528
1532
1529 # only owner can delete it !
1533 # only owner can delete it !
1530 if allowed_to_delete:
1534 if allowed_to_delete:
1531 PullRequestModel().delete(pull_request, self._rhodecode_user)
1535 PullRequestModel().delete(pull_request, self._rhodecode_user)
1532 Session().commit()
1536 Session().commit()
1533 h.flash(_('Successfully deleted pull request'),
1537 h.flash(_('Successfully deleted pull request'),
1534 category='success')
1538 category='success')
1535 raise HTTPFound(h.route_path('pullrequest_show_all',
1539 raise HTTPFound(h.route_path('pullrequest_show_all',
1536 repo_name=self.db_repo_name))
1540 repo_name=self.db_repo_name))
1537
1541
1538 log.warning('user %s tried to delete pull request without access',
1542 log.warning('user %s tried to delete pull request without access',
1539 self._rhodecode_user)
1543 self._rhodecode_user)
1540 raise HTTPNotFound()
1544 raise HTTPNotFound()
1541
1545
1542 def _pull_request_comments_create(self, pull_request, comments):
1546 def _pull_request_comments_create(self, pull_request, comments):
1543 _ = self.request.translate
1547 _ = self.request.translate
1544 data = {}
1548 data = {}
1545 if not comments:
1549 if not comments:
1546 return
1550 return
1547 pull_request_id = pull_request.pull_request_id
1551 pull_request_id = pull_request.pull_request_id
1548
1552
1549 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1553 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1550
1554
1551 for entry in comments:
1555 for entry in comments:
1552 c = self.load_default_context()
1556 c = self.load_default_context()
1553 comment_type = entry['comment_type']
1557 comment_type = entry['comment_type']
1554 text = entry['text']
1558 text = entry['text']
1555 status = entry['status']
1559 status = entry['status']
1556 is_draft = str2bool(entry['is_draft'])
1560 is_draft = str2bool(entry['is_draft'])
1557 resolves_comment_id = entry['resolves_comment_id']
1561 resolves_comment_id = entry['resolves_comment_id']
1558 close_pull_request = entry['close_pull_request']
1562 close_pull_request = entry['close_pull_request']
1559 f_path = entry['f_path']
1563 f_path = entry['f_path']
1560 line_no = entry['line']
1564 line_no = entry['line']
1561 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1565 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1562
1566
1563 # the logic here should work like following, if we submit close
1567 # the logic here should work like following, if we submit close
1564 # pr comment, use `close_pull_request_with_comment` function
1568 # pr comment, use `close_pull_request_with_comment` function
1565 # else handle regular comment logic
1569 # else handle regular comment logic
1566
1570
1567 if close_pull_request:
1571 if close_pull_request:
1568 # only owner or admin or person with write permissions
1572 # only owner or admin or person with write permissions
1569 allowed_to_close = PullRequestModel().check_user_update(
1573 allowed_to_close = PullRequestModel().check_user_update(
1570 pull_request, self._rhodecode_user)
1574 pull_request, self._rhodecode_user)
1571 if not allowed_to_close:
1575 if not allowed_to_close:
1572 log.debug('comment: forbidden because not allowed to close '
1576 log.debug('comment: forbidden because not allowed to close '
1573 'pull request %s', pull_request_id)
1577 'pull request %s', pull_request_id)
1574 raise HTTPForbidden()
1578 raise HTTPForbidden()
1575
1579
1576 # This also triggers `review_status_change`
1580 # This also triggers `review_status_change`
1577 comment, status = PullRequestModel().close_pull_request_with_comment(
1581 comment, status = PullRequestModel().close_pull_request_with_comment(
1578 pull_request, self._rhodecode_user, self.db_repo, message=text,
1582 pull_request, self._rhodecode_user, self.db_repo, message=text,
1579 auth_user=self._rhodecode_user)
1583 auth_user=self._rhodecode_user)
1580 Session().flush()
1584 Session().flush()
1581 is_inline = comment.is_inline
1585 is_inline = comment.is_inline
1582
1586
1583 PullRequestModel().trigger_pull_request_hook(
1587 PullRequestModel().trigger_pull_request_hook(
1584 pull_request, self._rhodecode_user, 'comment',
1588 pull_request, self._rhodecode_user, 'comment',
1585 data={'comment': comment})
1589 data={'comment': comment})
1586
1590
1587 else:
1591 else:
1588 # regular comment case, could be inline, or one with status.
1592 # regular comment case, could be inline, or one with status.
1589 # for that one we check also permissions
1593 # for that one we check also permissions
1590 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1594 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1591 allowed_to_change_status = PullRequestModel().check_user_change_status(
1595 allowed_to_change_status = PullRequestModel().check_user_change_status(
1592 pull_request, self._rhodecode_user) and not is_draft
1596 pull_request, self._rhodecode_user) and not is_draft
1593
1597
1594 if status and allowed_to_change_status:
1598 if status and allowed_to_change_status:
1595 message = (_('Status change %(transition_icon)s %(status)s')
1599 message = (_('Status change %(transition_icon)s %(status)s')
1596 % {'transition_icon': '>',
1600 % {'transition_icon': '>',
1597 'status': ChangesetStatus.get_status_lbl(status)})
1601 'status': ChangesetStatus.get_status_lbl(status)})
1598 text = text or message
1602 text = text or message
1599
1603
1600 comment = CommentsModel().create(
1604 comment = CommentsModel().create(
1601 text=text,
1605 text=text,
1602 repo=self.db_repo.repo_id,
1606 repo=self.db_repo.repo_id,
1603 user=self._rhodecode_user.user_id,
1607 user=self._rhodecode_user.user_id,
1604 pull_request=pull_request,
1608 pull_request=pull_request,
1605 f_path=f_path,
1609 f_path=f_path,
1606 line_no=line_no,
1610 line_no=line_no,
1607 status_change=(ChangesetStatus.get_status_lbl(status)
1611 status_change=(ChangesetStatus.get_status_lbl(status)
1608 if status and allowed_to_change_status else None),
1612 if status and allowed_to_change_status else None),
1609 status_change_type=(status
1613 status_change_type=(status
1610 if status and allowed_to_change_status else None),
1614 if status and allowed_to_change_status else None),
1611 comment_type=comment_type,
1615 comment_type=comment_type,
1612 is_draft=is_draft,
1616 is_draft=is_draft,
1613 resolves_comment_id=resolves_comment_id,
1617 resolves_comment_id=resolves_comment_id,
1614 auth_user=self._rhodecode_user,
1618 auth_user=self._rhodecode_user,
1615 send_email=not is_draft, # skip notification for draft comments
1619 send_email=not is_draft, # skip notification for draft comments
1616 )
1620 )
1617 is_inline = comment.is_inline
1621 is_inline = comment.is_inline
1618
1622
1619 if allowed_to_change_status:
1623 if allowed_to_change_status:
1620 # calculate old status before we change it
1624 # calculate old status before we change it
1621 old_calculated_status = pull_request.calculated_review_status()
1625 old_calculated_status = pull_request.calculated_review_status()
1622
1626
1623 # get status if set !
1627 # get status if set !
1624 if status:
1628 if status:
1625 ChangesetStatusModel().set_status(
1629 ChangesetStatusModel().set_status(
1626 self.db_repo.repo_id,
1630 self.db_repo.repo_id,
1627 status,
1631 status,
1628 self._rhodecode_user.user_id,
1632 self._rhodecode_user.user_id,
1629 comment,
1633 comment,
1630 pull_request=pull_request
1634 pull_request=pull_request
1631 )
1635 )
1632
1636
1633 Session().flush()
1637 Session().flush()
1634 # this is somehow required to get access to some relationship
1638 # this is somehow required to get access to some relationship
1635 # loaded on comment
1639 # loaded on comment
1636 Session().refresh(comment)
1640 Session().refresh(comment)
1637
1641
1638 # skip notifications for drafts
1642 # skip notifications for drafts
1639 if not is_draft:
1643 if not is_draft:
1640 PullRequestModel().trigger_pull_request_hook(
1644 PullRequestModel().trigger_pull_request_hook(
1641 pull_request, self._rhodecode_user, 'comment',
1645 pull_request, self._rhodecode_user, 'comment',
1642 data={'comment': comment})
1646 data={'comment': comment})
1643
1647
1644 # we now calculate the status of pull request, and based on that
1648 # we now calculate the status of pull request, and based on that
1645 # calculation we set the commits status
1649 # calculation we set the commits status
1646 calculated_status = pull_request.calculated_review_status()
1650 calculated_status = pull_request.calculated_review_status()
1647 if old_calculated_status != calculated_status:
1651 if old_calculated_status != calculated_status:
1648 PullRequestModel().trigger_pull_request_hook(
1652 PullRequestModel().trigger_pull_request_hook(
1649 pull_request, self._rhodecode_user, 'review_status_change',
1653 pull_request, self._rhodecode_user, 'review_status_change',
1650 data={'status': calculated_status})
1654 data={'status': calculated_status})
1651
1655
1652 comment_id = comment.comment_id
1656 comment_id = comment.comment_id
1653 data[comment_id] = {
1657 data[comment_id] = {
1654 'target_id': target_elem_id
1658 'target_id': target_elem_id
1655 }
1659 }
1656 Session().flush()
1660 Session().flush()
1657
1661
1658 c.co = comment
1662 c.co = comment
1659 c.at_version_num = None
1663 c.at_version_num = None
1660 c.is_new = True
1664 c.is_new = True
1661 rendered_comment = render(
1665 rendered_comment = render(
1662 'rhodecode:templates/changeset/changeset_comment_block.mako',
1666 'rhodecode:templates/changeset/changeset_comment_block.mako',
1663 self._get_template_context(c), self.request)
1667 self._get_template_context(c), self.request)
1664
1668
1665 data[comment_id].update(comment.get_dict())
1669 data[comment_id].update(comment.get_dict())
1666 data[comment_id].update({'rendered_text': rendered_comment})
1670 data[comment_id].update({'rendered_text': rendered_comment})
1667
1671
1668 Session().commit()
1672 Session().commit()
1669
1673
1670 # skip channelstream for draft comments
1674 # skip channelstream for draft comments
1671 if not all_drafts:
1675 if not all_drafts:
1672 comment_broadcast_channel = channelstream.comment_channel(
1676 comment_broadcast_channel = channelstream.comment_channel(
1673 self.db_repo_name, pull_request_obj=pull_request)
1677 self.db_repo_name, pull_request_obj=pull_request)
1674
1678
1675 comment_data = data
1679 comment_data = data
1676 posted_comment_type = 'inline' if is_inline else 'general'
1680 posted_comment_type = 'inline' if is_inline else 'general'
1677 if len(data) == 1:
1681 if len(data) == 1:
1678 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1682 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1679 else:
1683 else:
1680 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1684 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1681
1685
1682 channelstream.comment_channelstream_push(
1686 channelstream.comment_channelstream_push(
1683 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1687 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1684 comment_data=comment_data)
1688 comment_data=comment_data)
1685
1689
1686 return data
1690 return data
1687
1691
1688 @LoginRequired()
1692 @LoginRequired()
1689 @NotAnonymous()
1693 @NotAnonymous()
1690 @HasRepoPermissionAnyDecorator(
1694 @HasRepoPermissionAnyDecorator(
1691 'repository.read', 'repository.write', 'repository.admin')
1695 'repository.read', 'repository.write', 'repository.admin')
1692 @CSRFRequired()
1696 @CSRFRequired()
1693 def pull_request_comment_create(self):
1697 def pull_request_comment_create(self):
1694 _ = self.request.translate
1698 _ = self.request.translate
1695
1699
1696 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1700 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1697
1701
1698 if pull_request.is_closed():
1702 if pull_request.is_closed():
1699 log.debug('comment: forbidden because pull request is closed')
1703 log.debug('comment: forbidden because pull request is closed')
1700 raise HTTPForbidden()
1704 raise HTTPForbidden()
1701
1705
1702 allowed_to_comment = PullRequestModel().check_user_comment(
1706 allowed_to_comment = PullRequestModel().check_user_comment(
1703 pull_request, self._rhodecode_user)
1707 pull_request, self._rhodecode_user)
1704 if not allowed_to_comment:
1708 if not allowed_to_comment:
1705 log.debug('comment: forbidden because pull request is from forbidden repo')
1709 log.debug('comment: forbidden because pull request is from forbidden repo')
1706 raise HTTPForbidden()
1710 raise HTTPForbidden()
1707
1711
1708 comment_data = {
1712 comment_data = {
1709 'comment_type': self.request.POST.get('comment_type'),
1713 'comment_type': self.request.POST.get('comment_type'),
1710 'text': self.request.POST.get('text'),
1714 'text': self.request.POST.get('text'),
1711 'status': self.request.POST.get('changeset_status', None),
1715 'status': self.request.POST.get('changeset_status', None),
1712 'is_draft': self.request.POST.get('draft'),
1716 'is_draft': self.request.POST.get('draft'),
1713 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1717 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1714 'close_pull_request': self.request.POST.get('close_pull_request'),
1718 'close_pull_request': self.request.POST.get('close_pull_request'),
1715 'f_path': self.request.POST.get('f_path'),
1719 'f_path': self.request.POST.get('f_path'),
1716 'line': self.request.POST.get('line'),
1720 'line': self.request.POST.get('line'),
1717 }
1721 }
1718 data = self._pull_request_comments_create(pull_request, [comment_data])
1722 data = self._pull_request_comments_create(pull_request, [comment_data])
1719
1723
1720 return data
1724 return data
1721
1725
1722 @LoginRequired()
1726 @LoginRequired()
1723 @NotAnonymous()
1727 @NotAnonymous()
1724 @HasRepoPermissionAnyDecorator(
1728 @HasRepoPermissionAnyDecorator(
1725 'repository.read', 'repository.write', 'repository.admin')
1729 'repository.read', 'repository.write', 'repository.admin')
1726 @CSRFRequired()
1730 @CSRFRequired()
1727 def pull_request_comment_delete(self):
1731 def pull_request_comment_delete(self):
1728 pull_request = PullRequest.get_or_404(
1732 pull_request = PullRequest.get_or_404(
1729 self.request.matchdict['pull_request_id'])
1733 self.request.matchdict['pull_request_id'])
1730
1734
1731 comment = ChangesetComment.get_or_404(
1735 comment = ChangesetComment.get_or_404(
1732 self.request.matchdict['comment_id'])
1736 self.request.matchdict['comment_id'])
1733 comment_id = comment.comment_id
1737 comment_id = comment.comment_id
1734
1738
1735 if comment.immutable:
1739 if comment.immutable:
1736 # don't allow deleting comments that are immutable
1740 # don't allow deleting comments that are immutable
1737 raise HTTPForbidden()
1741 raise HTTPForbidden()
1738
1742
1739 if pull_request.is_closed():
1743 if pull_request.is_closed():
1740 log.debug('comment: forbidden because pull request is closed')
1744 log.debug('comment: forbidden because pull request is closed')
1741 raise HTTPForbidden()
1745 raise HTTPForbidden()
1742
1746
1743 if not comment:
1747 if not comment:
1744 log.debug('Comment with id:%s not found, skipping', comment_id)
1748 log.debug('Comment with id:%s not found, skipping', comment_id)
1745 # comment already deleted in another call probably
1749 # comment already deleted in another call probably
1746 return True
1750 return True
1747
1751
1748 if comment.pull_request.is_closed():
1752 if comment.pull_request.is_closed():
1749 # don't allow deleting comments on closed pull request
1753 # don't allow deleting comments on closed pull request
1750 raise HTTPForbidden()
1754 raise HTTPForbidden()
1751
1755
1752 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1756 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1753 super_admin = h.HasPermissionAny('hg.admin')()
1757 super_admin = h.HasPermissionAny('hg.admin')()
1754 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1758 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1755 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1759 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1756 comment_repo_admin = is_repo_admin and is_repo_comment
1760 comment_repo_admin = is_repo_admin and is_repo_comment
1757
1761
1758 if comment.draft and not comment_owner:
1762 if comment.draft and not comment_owner:
1759 # We never allow to delete draft comments for other than owners
1763 # We never allow to delete draft comments for other than owners
1760 raise HTTPNotFound()
1764 raise HTTPNotFound()
1761
1765
1762 if super_admin or comment_owner or comment_repo_admin:
1766 if super_admin or comment_owner or comment_repo_admin:
1763 old_calculated_status = comment.pull_request.calculated_review_status()
1767 old_calculated_status = comment.pull_request.calculated_review_status()
1764 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1768 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1765 Session().commit()
1769 Session().commit()
1766 calculated_status = comment.pull_request.calculated_review_status()
1770 calculated_status = comment.pull_request.calculated_review_status()
1767 if old_calculated_status != calculated_status:
1771 if old_calculated_status != calculated_status:
1768 PullRequestModel().trigger_pull_request_hook(
1772 PullRequestModel().trigger_pull_request_hook(
1769 comment.pull_request, self._rhodecode_user, 'review_status_change',
1773 comment.pull_request, self._rhodecode_user, 'review_status_change',
1770 data={'status': calculated_status})
1774 data={'status': calculated_status})
1771 return True
1775 return True
1772 else:
1776 else:
1773 log.warning('No permissions for user %s to delete comment_id: %s',
1777 log.warning('No permissions for user %s to delete comment_id: %s',
1774 self._rhodecode_db_user, comment_id)
1778 self._rhodecode_db_user, comment_id)
1775 raise HTTPNotFound()
1779 raise HTTPNotFound()
1776
1780
1777 @LoginRequired()
1781 @LoginRequired()
1778 @NotAnonymous()
1782 @NotAnonymous()
1779 @HasRepoPermissionAnyDecorator(
1783 @HasRepoPermissionAnyDecorator(
1780 'repository.read', 'repository.write', 'repository.admin')
1784 'repository.read', 'repository.write', 'repository.admin')
1781 @CSRFRequired()
1785 @CSRFRequired()
1782 def pull_request_comment_edit(self):
1786 def pull_request_comment_edit(self):
1783 self.load_default_context()
1787 self.load_default_context()
1784
1788
1785 pull_request = PullRequest.get_or_404(
1789 pull_request = PullRequest.get_or_404(
1786 self.request.matchdict['pull_request_id']
1790 self.request.matchdict['pull_request_id']
1787 )
1791 )
1788 comment = ChangesetComment.get_or_404(
1792 comment = ChangesetComment.get_or_404(
1789 self.request.matchdict['comment_id']
1793 self.request.matchdict['comment_id']
1790 )
1794 )
1791 comment_id = comment.comment_id
1795 comment_id = comment.comment_id
1792
1796
1793 if comment.immutable:
1797 if comment.immutable:
1794 # don't allow deleting comments that are immutable
1798 # don't allow deleting comments that are immutable
1795 raise HTTPForbidden()
1799 raise HTTPForbidden()
1796
1800
1797 if pull_request.is_closed():
1801 if pull_request.is_closed():
1798 log.debug('comment: forbidden because pull request is closed')
1802 log.debug('comment: forbidden because pull request is closed')
1799 raise HTTPForbidden()
1803 raise HTTPForbidden()
1800
1804
1801 if comment.pull_request.is_closed():
1805 if comment.pull_request.is_closed():
1802 # don't allow deleting comments on closed pull request
1806 # don't allow deleting comments on closed pull request
1803 raise HTTPForbidden()
1807 raise HTTPForbidden()
1804
1808
1805 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1809 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1806 super_admin = h.HasPermissionAny('hg.admin')()
1810 super_admin = h.HasPermissionAny('hg.admin')()
1807 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1811 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1808 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1812 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1809 comment_repo_admin = is_repo_admin and is_repo_comment
1813 comment_repo_admin = is_repo_admin and is_repo_comment
1810
1814
1811 if super_admin or comment_owner or comment_repo_admin:
1815 if super_admin or comment_owner or comment_repo_admin:
1812 text = self.request.POST.get('text')
1816 text = self.request.POST.get('text')
1813 version = self.request.POST.get('version')
1817 version = self.request.POST.get('version')
1814 if text == comment.text:
1818 if text == comment.text:
1815 log.warning(
1819 log.warning(
1816 'Comment(PR): '
1820 'Comment(PR): '
1817 'Trying to create new version '
1821 'Trying to create new version '
1818 'with the same comment body {}'.format(
1822 'with the same comment body {}'.format(
1819 comment_id,
1823 comment_id,
1820 )
1824 )
1821 )
1825 )
1822 raise HTTPNotFound()
1826 raise HTTPNotFound()
1823
1827
1824 if version.isdigit():
1828 if version.isdigit():
1825 version = int(version)
1829 version = int(version)
1826 else:
1830 else:
1827 log.warning(
1831 log.warning(
1828 'Comment(PR): Wrong version type {} {} '
1832 'Comment(PR): Wrong version type {} {} '
1829 'for comment {}'.format(
1833 'for comment {}'.format(
1830 version,
1834 version,
1831 type(version),
1835 type(version),
1832 comment_id,
1836 comment_id,
1833 )
1837 )
1834 )
1838 )
1835 raise HTTPNotFound()
1839 raise HTTPNotFound()
1836
1840
1837 try:
1841 try:
1838 comment_history = CommentsModel().edit(
1842 comment_history = CommentsModel().edit(
1839 comment_id=comment_id,
1843 comment_id=comment_id,
1840 text=text,
1844 text=text,
1841 auth_user=self._rhodecode_user,
1845 auth_user=self._rhodecode_user,
1842 version=version,
1846 version=version,
1843 )
1847 )
1844 except CommentVersionMismatch:
1848 except CommentVersionMismatch:
1845 raise HTTPConflict()
1849 raise HTTPConflict()
1846
1850
1847 if not comment_history:
1851 if not comment_history:
1848 raise HTTPNotFound()
1852 raise HTTPNotFound()
1849
1853
1850 Session().commit()
1854 Session().commit()
1851 if not comment.draft:
1855 if not comment.draft:
1852 PullRequestModel().trigger_pull_request_hook(
1856 PullRequestModel().trigger_pull_request_hook(
1853 pull_request, self._rhodecode_user, 'comment_edit',
1857 pull_request, self._rhodecode_user, 'comment_edit',
1854 data={'comment': comment})
1858 data={'comment': comment})
1855
1859
1856 return {
1860 return {
1857 'comment_history_id': comment_history.comment_history_id,
1861 'comment_history_id': comment_history.comment_history_id,
1858 'comment_id': comment.comment_id,
1862 'comment_id': comment.comment_id,
1859 'comment_version': comment_history.version,
1863 'comment_version': comment_history.version,
1860 'comment_author_username': comment_history.author.username,
1864 'comment_author_username': comment_history.author.username,
1861 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1865 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1862 'comment_created_on': h.age_component(comment_history.created_on,
1866 'comment_created_on': h.age_component(comment_history.created_on,
1863 time_is_local=True),
1867 time_is_local=True),
1864 }
1868 }
1865 else:
1869 else:
1866 log.warning('No permissions for user %s to edit comment_id: %s',
1870 log.warning('No permissions for user %s to edit comment_id: %s',
1867 self._rhodecode_db_user, comment_id)
1871 self._rhodecode_db_user, comment_id)
1868 raise HTTPNotFound()
1872 raise HTTPNotFound()
@@ -1,1074 +1,1148 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 """
22 """
23 Some simple helper functions
23 Some simple helper functions
24 """
24 """
25
25
26 import collections
26 import collections
27 import datetime
27 import datetime
28 import dateutil.relativedelta
28 import dateutil.relativedelta
29 import hashlib
29 import hashlib
30 import logging
30 import logging
31 import re
31 import re
32 import sys
32 import sys
33 import time
33 import time
34 import urllib
34 import urllib
35 import urlobject
35 import urlobject
36 import uuid
36 import uuid
37 import getpass
37 import getpass
38 from functools import update_wrapper, partial
38 from functools import update_wrapper, partial, wraps
39
39
40 import pygments.lexers
40 import pygments.lexers
41 import sqlalchemy
41 import sqlalchemy
42 import sqlalchemy.engine.url
42 import sqlalchemy.engine.url
43 import sqlalchemy.exc
43 import sqlalchemy.exc
44 import sqlalchemy.sql
44 import sqlalchemy.sql
45 import webob
45 import webob
46 import pyramid.threadlocal
46 import pyramid.threadlocal
47 from pyramid import compat
47 from pyramid import compat
48 from pyramid.settings import asbool
48 from pyramid.settings import asbool
49
49
50 import rhodecode
50 import rhodecode
51 from rhodecode.translation import _, _pluralize
51 from rhodecode.translation import _, _pluralize
52
52
53
53
54 def md5(s):
54 def md5(s):
55 return hashlib.md5(s).hexdigest()
55 return hashlib.md5(s).hexdigest()
56
56
57
57
58 def md5_safe(s):
58 def md5_safe(s):
59 return md5(safe_str(s))
59 return md5(safe_str(s))
60
60
61
61
62 def sha1(s):
62 def sha1(s):
63 return hashlib.sha1(s).hexdigest()
63 return hashlib.sha1(s).hexdigest()
64
64
65
65
66 def sha1_safe(s):
66 def sha1_safe(s):
67 return sha1(safe_str(s))
67 return sha1(safe_str(s))
68
68
69
69
70 def __get_lem(extra_mapping=None):
70 def __get_lem(extra_mapping=None):
71 """
71 """
72 Get language extension map based on what's inside pygments lexers
72 Get language extension map based on what's inside pygments lexers
73 """
73 """
74 d = collections.defaultdict(lambda: [])
74 d = collections.defaultdict(lambda: [])
75
75
76 def __clean(s):
76 def __clean(s):
77 s = s.lstrip('*')
77 s = s.lstrip('*')
78 s = s.lstrip('.')
78 s = s.lstrip('.')
79
79
80 if s.find('[') != -1:
80 if s.find('[') != -1:
81 exts = []
81 exts = []
82 start, stop = s.find('['), s.find(']')
82 start, stop = s.find('['), s.find(']')
83
83
84 for suffix in s[start + 1:stop]:
84 for suffix in s[start + 1:stop]:
85 exts.append(s[:s.find('[')] + suffix)
85 exts.append(s[:s.find('[')] + suffix)
86 return [e.lower() for e in exts]
86 return [e.lower() for e in exts]
87 else:
87 else:
88 return [s.lower()]
88 return [s.lower()]
89
89
90 for lx, t in sorted(pygments.lexers.LEXERS.items()):
90 for lx, t in sorted(pygments.lexers.LEXERS.items()):
91 m = map(__clean, t[-2])
91 m = map(__clean, t[-2])
92 if m:
92 if m:
93 m = reduce(lambda x, y: x + y, m)
93 m = reduce(lambda x, y: x + y, m)
94 for ext in m:
94 for ext in m:
95 desc = lx.replace('Lexer', '')
95 desc = lx.replace('Lexer', '')
96 d[ext].append(desc)
96 d[ext].append(desc)
97
97
98 data = dict(d)
98 data = dict(d)
99
99
100 extra_mapping = extra_mapping or {}
100 extra_mapping = extra_mapping or {}
101 if extra_mapping:
101 if extra_mapping:
102 for k, v in extra_mapping.items():
102 for k, v in extra_mapping.items():
103 if k not in data:
103 if k not in data:
104 # register new mapping2lexer
104 # register new mapping2lexer
105 data[k] = [v]
105 data[k] = [v]
106
106
107 return data
107 return data
108
108
109
109
110 def str2bool(_str):
110 def str2bool(_str):
111 """
111 """
112 returns True/False value from given string, it tries to translate the
112 returns True/False value from given string, it tries to translate the
113 string into boolean
113 string into boolean
114
114
115 :param _str: string value to translate into boolean
115 :param _str: string value to translate into boolean
116 :rtype: boolean
116 :rtype: boolean
117 :returns: boolean from given string
117 :returns: boolean from given string
118 """
118 """
119 if _str is None:
119 if _str is None:
120 return False
120 return False
121 if _str in (True, False):
121 if _str in (True, False):
122 return _str
122 return _str
123 _str = str(_str).strip().lower()
123 _str = str(_str).strip().lower()
124 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
124 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
125
125
126
126
127 def aslist(obj, sep=None, strip=True):
127 def aslist(obj, sep=None, strip=True):
128 """
128 """
129 Returns given string separated by sep as list
129 Returns given string separated by sep as list
130
130
131 :param obj:
131 :param obj:
132 :param sep:
132 :param sep:
133 :param strip:
133 :param strip:
134 """
134 """
135 if isinstance(obj, (basestring,)):
135 if isinstance(obj, (basestring,)):
136 lst = obj.split(sep)
136 lst = obj.split(sep)
137 if strip:
137 if strip:
138 lst = [v.strip() for v in lst]
138 lst = [v.strip() for v in lst]
139 return lst
139 return lst
140 elif isinstance(obj, (list, tuple)):
140 elif isinstance(obj, (list, tuple)):
141 return obj
141 return obj
142 elif obj is None:
142 elif obj is None:
143 return []
143 return []
144 else:
144 else:
145 return [obj]
145 return [obj]
146
146
147
147
148 def convert_line_endings(line, mode):
148 def convert_line_endings(line, mode):
149 """
149 """
150 Converts a given line "line end" accordingly to given mode
150 Converts a given line "line end" accordingly to given mode
151
151
152 Available modes are::
152 Available modes are::
153 0 - Unix
153 0 - Unix
154 1 - Mac
154 1 - Mac
155 2 - DOS
155 2 - DOS
156
156
157 :param line: given line to convert
157 :param line: given line to convert
158 :param mode: mode to convert to
158 :param mode: mode to convert to
159 :rtype: str
159 :rtype: str
160 :return: converted line according to mode
160 :return: converted line according to mode
161 """
161 """
162 if mode == 0:
162 if mode == 0:
163 line = line.replace('\r\n', '\n')
163 line = line.replace('\r\n', '\n')
164 line = line.replace('\r', '\n')
164 line = line.replace('\r', '\n')
165 elif mode == 1:
165 elif mode == 1:
166 line = line.replace('\r\n', '\r')
166 line = line.replace('\r\n', '\r')
167 line = line.replace('\n', '\r')
167 line = line.replace('\n', '\r')
168 elif mode == 2:
168 elif mode == 2:
169 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
169 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
170 return line
170 return line
171
171
172
172
173 def detect_mode(line, default):
173 def detect_mode(line, default):
174 """
174 """
175 Detects line break for given line, if line break couldn't be found
175 Detects line break for given line, if line break couldn't be found
176 given default value is returned
176 given default value is returned
177
177
178 :param line: str line
178 :param line: str line
179 :param default: default
179 :param default: default
180 :rtype: int
180 :rtype: int
181 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
181 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
182 """
182 """
183 if line.endswith('\r\n'):
183 if line.endswith('\r\n'):
184 return 2
184 return 2
185 elif line.endswith('\n'):
185 elif line.endswith('\n'):
186 return 0
186 return 0
187 elif line.endswith('\r'):
187 elif line.endswith('\r'):
188 return 1
188 return 1
189 else:
189 else:
190 return default
190 return default
191
191
192
192
193 def safe_int(val, default=None):
193 def safe_int(val, default=None):
194 """
194 """
195 Returns int() of val if val is not convertable to int use default
195 Returns int() of val if val is not convertable to int use default
196 instead
196 instead
197
197
198 :param val:
198 :param val:
199 :param default:
199 :param default:
200 """
200 """
201
201
202 try:
202 try:
203 val = int(val)
203 val = int(val)
204 except (ValueError, TypeError):
204 except (ValueError, TypeError):
205 val = default
205 val = default
206
206
207 return val
207 return val
208
208
209
209
210 def safe_unicode(str_, from_encoding=None, use_chardet=False):
210 def safe_unicode(str_, from_encoding=None, use_chardet=False):
211 """
211 """
212 safe unicode function. Does few trick to turn str_ into unicode
212 safe unicode function. Does few trick to turn str_ into unicode
213
213
214 In case of UnicodeDecode error, we try to return it with encoding detected
214 In case of UnicodeDecode error, we try to return it with encoding detected
215 by chardet library if it fails fallback to unicode with errors replaced
215 by chardet library if it fails fallback to unicode with errors replaced
216
216
217 :param str_: string to decode
217 :param str_: string to decode
218 :rtype: unicode
218 :rtype: unicode
219 :returns: unicode object
219 :returns: unicode object
220 """
220 """
221 if isinstance(str_, unicode):
221 if isinstance(str_, unicode):
222 return str_
222 return str_
223
223
224 if not from_encoding:
224 if not from_encoding:
225 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
225 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
226 'utf8'), sep=',')
226 'utf8'), sep=',')
227 from_encoding = DEFAULT_ENCODINGS
227 from_encoding = DEFAULT_ENCODINGS
228
228
229 if not isinstance(from_encoding, (list, tuple)):
229 if not isinstance(from_encoding, (list, tuple)):
230 from_encoding = [from_encoding]
230 from_encoding = [from_encoding]
231
231
232 try:
232 try:
233 return unicode(str_)
233 return unicode(str_)
234 except UnicodeDecodeError:
234 except UnicodeDecodeError:
235 pass
235 pass
236
236
237 for enc in from_encoding:
237 for enc in from_encoding:
238 try:
238 try:
239 return unicode(str_, enc)
239 return unicode(str_, enc)
240 except UnicodeDecodeError:
240 except UnicodeDecodeError:
241 pass
241 pass
242
242
243 if use_chardet:
243 if use_chardet:
244 try:
244 try:
245 import chardet
245 import chardet
246 encoding = chardet.detect(str_)['encoding']
246 encoding = chardet.detect(str_)['encoding']
247 if encoding is None:
247 if encoding is None:
248 raise Exception()
248 raise Exception()
249 return str_.decode(encoding)
249 return str_.decode(encoding)
250 except (ImportError, UnicodeDecodeError, Exception):
250 except (ImportError, UnicodeDecodeError, Exception):
251 return unicode(str_, from_encoding[0], 'replace')
251 return unicode(str_, from_encoding[0], 'replace')
252 else:
252 else:
253 return unicode(str_, from_encoding[0], 'replace')
253 return unicode(str_, from_encoding[0], 'replace')
254
254
255 def safe_str(unicode_, to_encoding=None, use_chardet=False):
255 def safe_str(unicode_, to_encoding=None, use_chardet=False):
256 """
256 """
257 safe str function. Does few trick to turn unicode_ into string
257 safe str function. Does few trick to turn unicode_ into string
258
258
259 In case of UnicodeEncodeError, we try to return it with encoding detected
259 In case of UnicodeEncodeError, we try to return it with encoding detected
260 by chardet library if it fails fallback to string with errors replaced
260 by chardet library if it fails fallback to string with errors replaced
261
261
262 :param unicode_: unicode to encode
262 :param unicode_: unicode to encode
263 :rtype: str
263 :rtype: str
264 :returns: str object
264 :returns: str object
265 """
265 """
266
266
267 # if it's not basestr cast to str
267 # if it's not basestr cast to str
268 if not isinstance(unicode_, compat.string_types):
268 if not isinstance(unicode_, compat.string_types):
269 return str(unicode_)
269 return str(unicode_)
270
270
271 if isinstance(unicode_, str):
271 if isinstance(unicode_, str):
272 return unicode_
272 return unicode_
273
273
274 if not to_encoding:
274 if not to_encoding:
275 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
275 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
276 'utf8'), sep=',')
276 'utf8'), sep=',')
277 to_encoding = DEFAULT_ENCODINGS
277 to_encoding = DEFAULT_ENCODINGS
278
278
279 if not isinstance(to_encoding, (list, tuple)):
279 if not isinstance(to_encoding, (list, tuple)):
280 to_encoding = [to_encoding]
280 to_encoding = [to_encoding]
281
281
282 for enc in to_encoding:
282 for enc in to_encoding:
283 try:
283 try:
284 return unicode_.encode(enc)
284 return unicode_.encode(enc)
285 except UnicodeEncodeError:
285 except UnicodeEncodeError:
286 pass
286 pass
287
287
288 if use_chardet:
288 if use_chardet:
289 try:
289 try:
290 import chardet
290 import chardet
291 encoding = chardet.detect(unicode_)['encoding']
291 encoding = chardet.detect(unicode_)['encoding']
292 if encoding is None:
292 if encoding is None:
293 raise UnicodeEncodeError()
293 raise UnicodeEncodeError()
294
294
295 return unicode_.encode(encoding)
295 return unicode_.encode(encoding)
296 except (ImportError, UnicodeEncodeError):
296 except (ImportError, UnicodeEncodeError):
297 return unicode_.encode(to_encoding[0], 'replace')
297 return unicode_.encode(to_encoding[0], 'replace')
298 else:
298 else:
299 return unicode_.encode(to_encoding[0], 'replace')
299 return unicode_.encode(to_encoding[0], 'replace')
300
300
301
301
302 def remove_suffix(s, suffix):
302 def remove_suffix(s, suffix):
303 if s.endswith(suffix):
303 if s.endswith(suffix):
304 s = s[:-1 * len(suffix)]
304 s = s[:-1 * len(suffix)]
305 return s
305 return s
306
306
307
307
308 def remove_prefix(s, prefix):
308 def remove_prefix(s, prefix):
309 if s.startswith(prefix):
309 if s.startswith(prefix):
310 s = s[len(prefix):]
310 s = s[len(prefix):]
311 return s
311 return s
312
312
313
313
314 def find_calling_context(ignore_modules=None):
314 def find_calling_context(ignore_modules=None):
315 """
315 """
316 Look through the calling stack and return the frame which called
316 Look through the calling stack and return the frame which called
317 this function and is part of core module ( ie. rhodecode.* )
317 this function and is part of core module ( ie. rhodecode.* )
318
318
319 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
319 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
320 """
320 """
321
321
322 ignore_modules = ignore_modules or []
322 ignore_modules = ignore_modules or []
323
323
324 f = sys._getframe(2)
324 f = sys._getframe(2)
325 while f.f_back is not None:
325 while f.f_back is not None:
326 name = f.f_globals.get('__name__')
326 name = f.f_globals.get('__name__')
327 if name and name.startswith(__name__.split('.')[0]):
327 if name and name.startswith(__name__.split('.')[0]):
328 if name not in ignore_modules:
328 if name not in ignore_modules:
329 return f
329 return f
330 f = f.f_back
330 f = f.f_back
331 return None
331 return None
332
332
333
333
334 def ping_connection(connection, branch):
334 def ping_connection(connection, branch):
335 if branch:
335 if branch:
336 # "branch" refers to a sub-connection of a connection,
336 # "branch" refers to a sub-connection of a connection,
337 # we don't want to bother pinging on these.
337 # we don't want to bother pinging on these.
338 return
338 return
339
339
340 # turn off "close with result". This flag is only used with
340 # turn off "close with result". This flag is only used with
341 # "connectionless" execution, otherwise will be False in any case
341 # "connectionless" execution, otherwise will be False in any case
342 save_should_close_with_result = connection.should_close_with_result
342 save_should_close_with_result = connection.should_close_with_result
343 connection.should_close_with_result = False
343 connection.should_close_with_result = False
344
344
345 try:
345 try:
346 # run a SELECT 1. use a core select() so that
346 # run a SELECT 1. use a core select() so that
347 # the SELECT of a scalar value without a table is
347 # the SELECT of a scalar value without a table is
348 # appropriately formatted for the backend
348 # appropriately formatted for the backend
349 connection.scalar(sqlalchemy.sql.select([1]))
349 connection.scalar(sqlalchemy.sql.select([1]))
350 except sqlalchemy.exc.DBAPIError as err:
350 except sqlalchemy.exc.DBAPIError as err:
351 # catch SQLAlchemy's DBAPIError, which is a wrapper
351 # catch SQLAlchemy's DBAPIError, which is a wrapper
352 # for the DBAPI's exception. It includes a .connection_invalidated
352 # for the DBAPI's exception. It includes a .connection_invalidated
353 # attribute which specifies if this connection is a "disconnect"
353 # attribute which specifies if this connection is a "disconnect"
354 # condition, which is based on inspection of the original exception
354 # condition, which is based on inspection of the original exception
355 # by the dialect in use.
355 # by the dialect in use.
356 if err.connection_invalidated:
356 if err.connection_invalidated:
357 # run the same SELECT again - the connection will re-validate
357 # run the same SELECT again - the connection will re-validate
358 # itself and establish a new connection. The disconnect detection
358 # itself and establish a new connection. The disconnect detection
359 # here also causes the whole connection pool to be invalidated
359 # here also causes the whole connection pool to be invalidated
360 # so that all stale connections are discarded.
360 # so that all stale connections are discarded.
361 connection.scalar(sqlalchemy.sql.select([1]))
361 connection.scalar(sqlalchemy.sql.select([1]))
362 else:
362 else:
363 raise
363 raise
364 finally:
364 finally:
365 # restore "close with result"
365 # restore "close with result"
366 connection.should_close_with_result = save_should_close_with_result
366 connection.should_close_with_result = save_should_close_with_result
367
367
368
368
369 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
369 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
370 """Custom engine_from_config functions."""
370 """Custom engine_from_config functions."""
371 log = logging.getLogger('sqlalchemy.engine')
371 log = logging.getLogger('sqlalchemy.engine')
372 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
372 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
373 debug = asbool(configuration.pop('sqlalchemy.db1.debug_query', None))
373 debug = asbool(configuration.pop('sqlalchemy.db1.debug_query', None))
374
374
375 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
375 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
376
376
377 def color_sql(sql):
377 def color_sql(sql):
378 color_seq = '\033[1;33m' # This is yellow: code 33
378 color_seq = '\033[1;33m' # This is yellow: code 33
379 normal = '\x1b[0m'
379 normal = '\x1b[0m'
380 return ''.join([color_seq, sql, normal])
380 return ''.join([color_seq, sql, normal])
381
381
382 if use_ping_connection:
382 if use_ping_connection:
383 log.debug('Adding ping_connection on the engine config.')
383 log.debug('Adding ping_connection on the engine config.')
384 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
384 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
385
385
386 if debug:
386 if debug:
387 # attach events only for debug configuration
387 # attach events only for debug configuration
388 def before_cursor_execute(conn, cursor, statement,
388 def before_cursor_execute(conn, cursor, statement,
389 parameters, context, executemany):
389 parameters, context, executemany):
390 setattr(conn, 'query_start_time', time.time())
390 setattr(conn, 'query_start_time', time.time())
391 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
391 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
392 calling_context = find_calling_context(ignore_modules=[
392 calling_context = find_calling_context(ignore_modules=[
393 'rhodecode.lib.caching_query',
393 'rhodecode.lib.caching_query',
394 'rhodecode.model.settings',
394 'rhodecode.model.settings',
395 ])
395 ])
396 if calling_context:
396 if calling_context:
397 log.info(color_sql('call context %s:%s' % (
397 log.info(color_sql('call context %s:%s' % (
398 calling_context.f_code.co_filename,
398 calling_context.f_code.co_filename,
399 calling_context.f_lineno,
399 calling_context.f_lineno,
400 )))
400 )))
401
401
402 def after_cursor_execute(conn, cursor, statement,
402 def after_cursor_execute(conn, cursor, statement,
403 parameters, context, executemany):
403 parameters, context, executemany):
404 delattr(conn, 'query_start_time')
404 delattr(conn, 'query_start_time')
405
405
406 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
406 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
407 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
407 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
408
408
409 return engine
409 return engine
410
410
411
411
412 def get_encryption_key(config):
412 def get_encryption_key(config):
413 secret = config.get('rhodecode.encrypted_values.secret')
413 secret = config.get('rhodecode.encrypted_values.secret')
414 default = config['beaker.session.secret']
414 default = config['beaker.session.secret']
415 return secret or default
415 return secret or default
416
416
417
417
418 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
418 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
419 short_format=False):
419 short_format=False):
420 """
420 """
421 Turns a datetime into an age string.
421 Turns a datetime into an age string.
422 If show_short_version is True, this generates a shorter string with
422 If show_short_version is True, this generates a shorter string with
423 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
423 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
424
424
425 * IMPORTANT*
425 * IMPORTANT*
426 Code of this function is written in special way so it's easier to
426 Code of this function is written in special way so it's easier to
427 backport it to javascript. If you mean to update it, please also update
427 backport it to javascript. If you mean to update it, please also update
428 `jquery.timeago-extension.js` file
428 `jquery.timeago-extension.js` file
429
429
430 :param prevdate: datetime object
430 :param prevdate: datetime object
431 :param now: get current time, if not define we use
431 :param now: get current time, if not define we use
432 `datetime.datetime.now()`
432 `datetime.datetime.now()`
433 :param show_short_version: if it should approximate the date and
433 :param show_short_version: if it should approximate the date and
434 return a shorter string
434 return a shorter string
435 :param show_suffix:
435 :param show_suffix:
436 :param short_format: show short format, eg 2D instead of 2 days
436 :param short_format: show short format, eg 2D instead of 2 days
437 :rtype: unicode
437 :rtype: unicode
438 :returns: unicode words describing age
438 :returns: unicode words describing age
439 """
439 """
440
440
441 def _get_relative_delta(now, prevdate):
441 def _get_relative_delta(now, prevdate):
442 base = dateutil.relativedelta.relativedelta(now, prevdate)
442 base = dateutil.relativedelta.relativedelta(now, prevdate)
443 return {
443 return {
444 'year': base.years,
444 'year': base.years,
445 'month': base.months,
445 'month': base.months,
446 'day': base.days,
446 'day': base.days,
447 'hour': base.hours,
447 'hour': base.hours,
448 'minute': base.minutes,
448 'minute': base.minutes,
449 'second': base.seconds,
449 'second': base.seconds,
450 }
450 }
451
451
452 def _is_leap_year(year):
452 def _is_leap_year(year):
453 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
453 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
454
454
455 def get_month(prevdate):
455 def get_month(prevdate):
456 return prevdate.month
456 return prevdate.month
457
457
458 def get_year(prevdate):
458 def get_year(prevdate):
459 return prevdate.year
459 return prevdate.year
460
460
461 now = now or datetime.datetime.now()
461 now = now or datetime.datetime.now()
462 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
462 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
463 deltas = {}
463 deltas = {}
464 future = False
464 future = False
465
465
466 if prevdate > now:
466 if prevdate > now:
467 now_old = now
467 now_old = now
468 now = prevdate
468 now = prevdate
469 prevdate = now_old
469 prevdate = now_old
470 future = True
470 future = True
471 if future:
471 if future:
472 prevdate = prevdate.replace(microsecond=0)
472 prevdate = prevdate.replace(microsecond=0)
473 # Get date parts deltas
473 # Get date parts deltas
474 for part in order:
474 for part in order:
475 rel_delta = _get_relative_delta(now, prevdate)
475 rel_delta = _get_relative_delta(now, prevdate)
476 deltas[part] = rel_delta[part]
476 deltas[part] = rel_delta[part]
477
477
478 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
478 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
479 # not 1 hour, -59 minutes and -59 seconds)
479 # not 1 hour, -59 minutes and -59 seconds)
480 offsets = [[5, 60], [4, 60], [3, 24]]
480 offsets = [[5, 60], [4, 60], [3, 24]]
481 for element in offsets: # seconds, minutes, hours
481 for element in offsets: # seconds, minutes, hours
482 num = element[0]
482 num = element[0]
483 length = element[1]
483 length = element[1]
484
484
485 part = order[num]
485 part = order[num]
486 carry_part = order[num - 1]
486 carry_part = order[num - 1]
487
487
488 if deltas[part] < 0:
488 if deltas[part] < 0:
489 deltas[part] += length
489 deltas[part] += length
490 deltas[carry_part] -= 1
490 deltas[carry_part] -= 1
491
491
492 # Same thing for days except that the increment depends on the (variable)
492 # Same thing for days except that the increment depends on the (variable)
493 # number of days in the month
493 # number of days in the month
494 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
494 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
495 if deltas['day'] < 0:
495 if deltas['day'] < 0:
496 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
496 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
497 deltas['day'] += 29
497 deltas['day'] += 29
498 else:
498 else:
499 deltas['day'] += month_lengths[get_month(prevdate) - 1]
499 deltas['day'] += month_lengths[get_month(prevdate) - 1]
500
500
501 deltas['month'] -= 1
501 deltas['month'] -= 1
502
502
503 if deltas['month'] < 0:
503 if deltas['month'] < 0:
504 deltas['month'] += 12
504 deltas['month'] += 12
505 deltas['year'] -= 1
505 deltas['year'] -= 1
506
506
507 # Format the result
507 # Format the result
508 if short_format:
508 if short_format:
509 fmt_funcs = {
509 fmt_funcs = {
510 'year': lambda d: u'%dy' % d,
510 'year': lambda d: u'%dy' % d,
511 'month': lambda d: u'%dm' % d,
511 'month': lambda d: u'%dm' % d,
512 'day': lambda d: u'%dd' % d,
512 'day': lambda d: u'%dd' % d,
513 'hour': lambda d: u'%dh' % d,
513 'hour': lambda d: u'%dh' % d,
514 'minute': lambda d: u'%dmin' % d,
514 'minute': lambda d: u'%dmin' % d,
515 'second': lambda d: u'%dsec' % d,
515 'second': lambda d: u'%dsec' % d,
516 }
516 }
517 else:
517 else:
518 fmt_funcs = {
518 fmt_funcs = {
519 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
519 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
520 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
520 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
521 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
521 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
522 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
522 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
523 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
523 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
524 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
524 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
525 }
525 }
526
526
527 i = 0
527 i = 0
528 for part in order:
528 for part in order:
529 value = deltas[part]
529 value = deltas[part]
530 if value != 0:
530 if value != 0:
531
531
532 if i < 5:
532 if i < 5:
533 sub_part = order[i + 1]
533 sub_part = order[i + 1]
534 sub_value = deltas[sub_part]
534 sub_value = deltas[sub_part]
535 else:
535 else:
536 sub_value = 0
536 sub_value = 0
537
537
538 if sub_value == 0 or show_short_version:
538 if sub_value == 0 or show_short_version:
539 _val = fmt_funcs[part](value)
539 _val = fmt_funcs[part](value)
540 if future:
540 if future:
541 if show_suffix:
541 if show_suffix:
542 return _(u'in ${ago}', mapping={'ago': _val})
542 return _(u'in ${ago}', mapping={'ago': _val})
543 else:
543 else:
544 return _(_val)
544 return _(_val)
545
545
546 else:
546 else:
547 if show_suffix:
547 if show_suffix:
548 return _(u'${ago} ago', mapping={'ago': _val})
548 return _(u'${ago} ago', mapping={'ago': _val})
549 else:
549 else:
550 return _(_val)
550 return _(_val)
551
551
552 val = fmt_funcs[part](value)
552 val = fmt_funcs[part](value)
553 val_detail = fmt_funcs[sub_part](sub_value)
553 val_detail = fmt_funcs[sub_part](sub_value)
554 mapping = {'val': val, 'detail': val_detail}
554 mapping = {'val': val, 'detail': val_detail}
555
555
556 if short_format:
556 if short_format:
557 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
557 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
558 if show_suffix:
558 if show_suffix:
559 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
559 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
560 if future:
560 if future:
561 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
561 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
562 else:
562 else:
563 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
563 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
564 if show_suffix:
564 if show_suffix:
565 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
565 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
566 if future:
566 if future:
567 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
567 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
568
568
569 return datetime_tmpl
569 return datetime_tmpl
570 i += 1
570 i += 1
571 return _(u'just now')
571 return _(u'just now')
572
572
573
573
574 def age_from_seconds(seconds):
574 def age_from_seconds(seconds):
575 seconds = safe_int(seconds) or 0
575 seconds = safe_int(seconds) or 0
576 prevdate = time_to_datetime(time.time() + seconds)
576 prevdate = time_to_datetime(time.time() + seconds)
577 return age(prevdate, show_suffix=False, show_short_version=True)
577 return age(prevdate, show_suffix=False, show_short_version=True)
578
578
579
579
580 def cleaned_uri(uri):
580 def cleaned_uri(uri):
581 """
581 """
582 Quotes '[' and ']' from uri if there is only one of them.
582 Quotes '[' and ']' from uri if there is only one of them.
583 according to RFC3986 we cannot use such chars in uri
583 according to RFC3986 we cannot use such chars in uri
584 :param uri:
584 :param uri:
585 :return: uri without this chars
585 :return: uri without this chars
586 """
586 """
587 return urllib.quote(uri, safe='@$:/')
587 return urllib.quote(uri, safe='@$:/')
588
588
589
589
590 def credentials_filter(uri):
590 def credentials_filter(uri):
591 """
591 """
592 Returns a url with removed credentials
592 Returns a url with removed credentials
593
593
594 :param uri:
594 :param uri:
595 """
595 """
596 import urlobject
596 import urlobject
597 if isinstance(uri, rhodecode.lib.encrypt.InvalidDecryptedValue):
597 if isinstance(uri, rhodecode.lib.encrypt.InvalidDecryptedValue):
598 return 'InvalidDecryptionKey'
598 return 'InvalidDecryptionKey'
599
599
600 url_obj = urlobject.URLObject(cleaned_uri(uri))
600 url_obj = urlobject.URLObject(cleaned_uri(uri))
601 url_obj = url_obj.without_password().without_username()
601 url_obj = url_obj.without_password().without_username()
602
602
603 return url_obj
603 return url_obj
604
604
605
605
606 def get_host_info(request):
606 def get_host_info(request):
607 """
607 """
608 Generate host info, to obtain full url e.g https://server.com
608 Generate host info, to obtain full url e.g https://server.com
609 use this
609 use this
610 `{scheme}://{netloc}`
610 `{scheme}://{netloc}`
611 """
611 """
612 if not request:
612 if not request:
613 return {}
613 return {}
614
614
615 qualified_home_url = request.route_url('home')
615 qualified_home_url = request.route_url('home')
616 parsed_url = urlobject.URLObject(qualified_home_url)
616 parsed_url = urlobject.URLObject(qualified_home_url)
617 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
617 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
618
618
619 return {
619 return {
620 'scheme': parsed_url.scheme,
620 'scheme': parsed_url.scheme,
621 'netloc': parsed_url.netloc+decoded_path,
621 'netloc': parsed_url.netloc+decoded_path,
622 'hostname': parsed_url.hostname,
622 'hostname': parsed_url.hostname,
623 }
623 }
624
624
625
625
626 def get_clone_url(request, uri_tmpl, repo_name, repo_id, repo_type, **override):
626 def get_clone_url(request, uri_tmpl, repo_name, repo_id, repo_type, **override):
627 qualified_home_url = request.route_url('home')
627 qualified_home_url = request.route_url('home')
628 parsed_url = urlobject.URLObject(qualified_home_url)
628 parsed_url = urlobject.URLObject(qualified_home_url)
629 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
629 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
630
630
631 args = {
631 args = {
632 'scheme': parsed_url.scheme,
632 'scheme': parsed_url.scheme,
633 'user': '',
633 'user': '',
634 'sys_user': getpass.getuser(),
634 'sys_user': getpass.getuser(),
635 # path if we use proxy-prefix
635 # path if we use proxy-prefix
636 'netloc': parsed_url.netloc+decoded_path,
636 'netloc': parsed_url.netloc+decoded_path,
637 'hostname': parsed_url.hostname,
637 'hostname': parsed_url.hostname,
638 'prefix': decoded_path,
638 'prefix': decoded_path,
639 'repo': repo_name,
639 'repo': repo_name,
640 'repoid': str(repo_id),
640 'repoid': str(repo_id),
641 'repo_type': repo_type
641 'repo_type': repo_type
642 }
642 }
643 args.update(override)
643 args.update(override)
644 args['user'] = urllib.quote(safe_str(args['user']))
644 args['user'] = urllib.quote(safe_str(args['user']))
645
645
646 for k, v in args.items():
646 for k, v in args.items():
647 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
647 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
648
648
649 # special case for SVN clone url
649 # special case for SVN clone url
650 if repo_type == 'svn':
650 if repo_type == 'svn':
651 uri_tmpl = uri_tmpl.replace('ssh://', 'svn+ssh://')
651 uri_tmpl = uri_tmpl.replace('ssh://', 'svn+ssh://')
652
652
653 # remove leading @ sign if it's present. Case of empty user
653 # remove leading @ sign if it's present. Case of empty user
654 url_obj = urlobject.URLObject(uri_tmpl)
654 url_obj = urlobject.URLObject(uri_tmpl)
655 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
655 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
656
656
657 return safe_unicode(url)
657 return safe_unicode(url)
658
658
659
659
660 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
660 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
661 maybe_unreachable=False, reference_obj=None):
661 maybe_unreachable=False, reference_obj=None):
662 """
662 """
663 Safe version of get_commit if this commit doesn't exists for a
663 Safe version of get_commit if this commit doesn't exists for a
664 repository it returns a Dummy one instead
664 repository it returns a Dummy one instead
665
665
666 :param repo: repository instance
666 :param repo: repository instance
667 :param commit_id: commit id as str
667 :param commit_id: commit id as str
668 :param commit_idx: numeric commit index
668 :param commit_idx: numeric commit index
669 :param pre_load: optional list of commit attributes to load
669 :param pre_load: optional list of commit attributes to load
670 :param maybe_unreachable: translate unreachable commits on git repos
670 :param maybe_unreachable: translate unreachable commits on git repos
671 :param reference_obj: explicitly search via a reference obj in git. E.g "branch:123" would mean branch "123"
671 :param reference_obj: explicitly search via a reference obj in git. E.g "branch:123" would mean branch "123"
672 """
672 """
673 # TODO(skreft): remove these circular imports
673 # TODO(skreft): remove these circular imports
674 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
674 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
675 from rhodecode.lib.vcs.exceptions import RepositoryError
675 from rhodecode.lib.vcs.exceptions import RepositoryError
676 if not isinstance(repo, BaseRepository):
676 if not isinstance(repo, BaseRepository):
677 raise Exception('You must pass an Repository '
677 raise Exception('You must pass an Repository '
678 'object as first argument got %s', type(repo))
678 'object as first argument got %s', type(repo))
679
679
680 try:
680 try:
681 commit = repo.get_commit(
681 commit = repo.get_commit(
682 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load,
682 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load,
683 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
683 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
684 except (RepositoryError, LookupError):
684 except (RepositoryError, LookupError):
685 commit = EmptyCommit()
685 commit = EmptyCommit()
686 return commit
686 return commit
687
687
688
688
689 def datetime_to_time(dt):
689 def datetime_to_time(dt):
690 if dt:
690 if dt:
691 return time.mktime(dt.timetuple())
691 return time.mktime(dt.timetuple())
692
692
693
693
694 def time_to_datetime(tm):
694 def time_to_datetime(tm):
695 if tm:
695 if tm:
696 if isinstance(tm, compat.string_types):
696 if isinstance(tm, compat.string_types):
697 try:
697 try:
698 tm = float(tm)
698 tm = float(tm)
699 except ValueError:
699 except ValueError:
700 return
700 return
701 return datetime.datetime.fromtimestamp(tm)
701 return datetime.datetime.fromtimestamp(tm)
702
702
703
703
704 def time_to_utcdatetime(tm):
704 def time_to_utcdatetime(tm):
705 if tm:
705 if tm:
706 if isinstance(tm, compat.string_types):
706 if isinstance(tm, compat.string_types):
707 try:
707 try:
708 tm = float(tm)
708 tm = float(tm)
709 except ValueError:
709 except ValueError:
710 return
710 return
711 return datetime.datetime.utcfromtimestamp(tm)
711 return datetime.datetime.utcfromtimestamp(tm)
712
712
713
713
714 MENTIONS_REGEX = re.compile(
714 MENTIONS_REGEX = re.compile(
715 # ^@ or @ without any special chars in front
715 # ^@ or @ without any special chars in front
716 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
716 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
717 # main body starts with letter, then can be . - _
717 # main body starts with letter, then can be . - _
718 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
718 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
719 re.VERBOSE | re.MULTILINE)
719 re.VERBOSE | re.MULTILINE)
720
720
721
721
722 def extract_mentioned_users(s):
722 def extract_mentioned_users(s):
723 """
723 """
724 Returns unique usernames from given string s that have @mention
724 Returns unique usernames from given string s that have @mention
725
725
726 :param s: string to get mentions
726 :param s: string to get mentions
727 """
727 """
728 usrs = set()
728 usrs = set()
729 for username in MENTIONS_REGEX.findall(s):
729 for username in MENTIONS_REGEX.findall(s):
730 usrs.add(username)
730 usrs.add(username)
731
731
732 return sorted(list(usrs), key=lambda k: k.lower())
732 return sorted(list(usrs), key=lambda k: k.lower())
733
733
734
734
735 class AttributeDictBase(dict):
735 class AttributeDictBase(dict):
736 def __getstate__(self):
736 def __getstate__(self):
737 odict = self.__dict__ # get attribute dictionary
737 odict = self.__dict__ # get attribute dictionary
738 return odict
738 return odict
739
739
740 def __setstate__(self, dict):
740 def __setstate__(self, dict):
741 self.__dict__ = dict
741 self.__dict__ = dict
742
742
743 __setattr__ = dict.__setitem__
743 __setattr__ = dict.__setitem__
744 __delattr__ = dict.__delitem__
744 __delattr__ = dict.__delitem__
745
745
746
746
747 class StrictAttributeDict(AttributeDictBase):
747 class StrictAttributeDict(AttributeDictBase):
748 """
748 """
749 Strict Version of Attribute dict which raises an Attribute error when
749 Strict Version of Attribute dict which raises an Attribute error when
750 requested attribute is not set
750 requested attribute is not set
751 """
751 """
752 def __getattr__(self, attr):
752 def __getattr__(self, attr):
753 try:
753 try:
754 return self[attr]
754 return self[attr]
755 except KeyError:
755 except KeyError:
756 raise AttributeError('%s object has no attribute %s' % (
756 raise AttributeError('%s object has no attribute %s' % (
757 self.__class__, attr))
757 self.__class__, attr))
758
758
759
759
760 class AttributeDict(AttributeDictBase):
760 class AttributeDict(AttributeDictBase):
761 def __getattr__(self, attr):
761 def __getattr__(self, attr):
762 return self.get(attr, None)
762 return self.get(attr, None)
763
763
764
764
765
765
766 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
766 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
767 def __init__(self, default_factory=None, *args, **kwargs):
767 def __init__(self, default_factory=None, *args, **kwargs):
768 # in python3 you can omit the args to super
768 # in python3 you can omit the args to super
769 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
769 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
770 self.default_factory = default_factory
770 self.default_factory = default_factory
771
771
772
772
773 def fix_PATH(os_=None):
773 def fix_PATH(os_=None):
774 """
774 """
775 Get current active python path, and append it to PATH variable to fix
775 Get current active python path, and append it to PATH variable to fix
776 issues of subprocess calls and different python versions
776 issues of subprocess calls and different python versions
777 """
777 """
778 if os_ is None:
778 if os_ is None:
779 import os
779 import os
780 else:
780 else:
781 os = os_
781 os = os_
782
782
783 cur_path = os.path.split(sys.executable)[0]
783 cur_path = os.path.split(sys.executable)[0]
784 if not os.environ['PATH'].startswith(cur_path):
784 if not os.environ['PATH'].startswith(cur_path):
785 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
785 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
786
786
787
787
788 def obfuscate_url_pw(engine):
788 def obfuscate_url_pw(engine):
789 _url = engine or ''
789 _url = engine or ''
790 try:
790 try:
791 _url = sqlalchemy.engine.url.make_url(engine)
791 _url = sqlalchemy.engine.url.make_url(engine)
792 if _url.password:
792 if _url.password:
793 _url.password = 'XXXXX'
793 _url.password = 'XXXXX'
794 except Exception:
794 except Exception:
795 pass
795 pass
796 return unicode(_url)
796 return unicode(_url)
797
797
798
798
799 def get_server_url(environ):
799 def get_server_url(environ):
800 req = webob.Request(environ)
800 req = webob.Request(environ)
801 return req.host_url + req.script_name
801 return req.host_url + req.script_name
802
802
803
803
804 def unique_id(hexlen=32):
804 def unique_id(hexlen=32):
805 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
805 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
806 return suuid(truncate_to=hexlen, alphabet=alphabet)
806 return suuid(truncate_to=hexlen, alphabet=alphabet)
807
807
808
808
809 def suuid(url=None, truncate_to=22, alphabet=None):
809 def suuid(url=None, truncate_to=22, alphabet=None):
810 """
810 """
811 Generate and return a short URL safe UUID.
811 Generate and return a short URL safe UUID.
812
812
813 If the url parameter is provided, set the namespace to the provided
813 If the url parameter is provided, set the namespace to the provided
814 URL and generate a UUID.
814 URL and generate a UUID.
815
815
816 :param url to get the uuid for
816 :param url to get the uuid for
817 :truncate_to: truncate the basic 22 UUID to shorter version
817 :truncate_to: truncate the basic 22 UUID to shorter version
818
818
819 The IDs won't be universally unique any longer, but the probability of
819 The IDs won't be universally unique any longer, but the probability of
820 a collision will still be very low.
820 a collision will still be very low.
821 """
821 """
822 # Define our alphabet.
822 # Define our alphabet.
823 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
823 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
824
824
825 # If no URL is given, generate a random UUID.
825 # If no URL is given, generate a random UUID.
826 if url is None:
826 if url is None:
827 unique_id = uuid.uuid4().int
827 unique_id = uuid.uuid4().int
828 else:
828 else:
829 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
829 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
830
830
831 alphabet_length = len(_ALPHABET)
831 alphabet_length = len(_ALPHABET)
832 output = []
832 output = []
833 while unique_id > 0:
833 while unique_id > 0:
834 digit = unique_id % alphabet_length
834 digit = unique_id % alphabet_length
835 output.append(_ALPHABET[digit])
835 output.append(_ALPHABET[digit])
836 unique_id = int(unique_id / alphabet_length)
836 unique_id = int(unique_id / alphabet_length)
837 return "".join(output)[:truncate_to]
837 return "".join(output)[:truncate_to]
838
838
839
839
840 def get_current_rhodecode_user(request=None):
840 def get_current_rhodecode_user(request=None):
841 """
841 """
842 Gets rhodecode user from request
842 Gets rhodecode user from request
843 """
843 """
844 pyramid_request = request or pyramid.threadlocal.get_current_request()
844 pyramid_request = request or pyramid.threadlocal.get_current_request()
845
845
846 # web case
846 # web case
847 if pyramid_request and hasattr(pyramid_request, 'user'):
847 if pyramid_request and hasattr(pyramid_request, 'user'):
848 return pyramid_request.user
848 return pyramid_request.user
849
849
850 # api case
850 # api case
851 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
851 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
852 return pyramid_request.rpc_user
852 return pyramid_request.rpc_user
853
853
854 return None
854 return None
855
855
856
856
857 def action_logger_generic(action, namespace=''):
857 def action_logger_generic(action, namespace=''):
858 """
858 """
859 A generic logger for actions useful to the system overview, tries to find
859 A generic logger for actions useful to the system overview, tries to find
860 an acting user for the context of the call otherwise reports unknown user
860 an acting user for the context of the call otherwise reports unknown user
861
861
862 :param action: logging message eg 'comment 5 deleted'
862 :param action: logging message eg 'comment 5 deleted'
863 :param type: string
863 :param type: string
864
864
865 :param namespace: namespace of the logging message eg. 'repo.comments'
865 :param namespace: namespace of the logging message eg. 'repo.comments'
866 :param type: string
866 :param type: string
867
867
868 """
868 """
869
869
870 logger_name = 'rhodecode.actions'
870 logger_name = 'rhodecode.actions'
871
871
872 if namespace:
872 if namespace:
873 logger_name += '.' + namespace
873 logger_name += '.' + namespace
874
874
875 log = logging.getLogger(logger_name)
875 log = logging.getLogger(logger_name)
876
876
877 # get a user if we can
877 # get a user if we can
878 user = get_current_rhodecode_user()
878 user = get_current_rhodecode_user()
879
879
880 logfunc = log.info
880 logfunc = log.info
881
881
882 if not user:
882 if not user:
883 user = '<unknown user>'
883 user = '<unknown user>'
884 logfunc = log.warning
884 logfunc = log.warning
885
885
886 logfunc('Logging action by {}: {}'.format(user, action))
886 logfunc('Logging action by {}: {}'.format(user, action))
887
887
888
888
889 def escape_split(text, sep=',', maxsplit=-1):
889 def escape_split(text, sep=',', maxsplit=-1):
890 r"""
890 r"""
891 Allows for escaping of the separator: e.g. arg='foo\, bar'
891 Allows for escaping of the separator: e.g. arg='foo\, bar'
892
892
893 It should be noted that the way bash et. al. do command line parsing, those
893 It should be noted that the way bash et. al. do command line parsing, those
894 single quotes are required.
894 single quotes are required.
895 """
895 """
896 escaped_sep = r'\%s' % sep
896 escaped_sep = r'\%s' % sep
897
897
898 if escaped_sep not in text:
898 if escaped_sep not in text:
899 return text.split(sep, maxsplit)
899 return text.split(sep, maxsplit)
900
900
901 before, _mid, after = text.partition(escaped_sep)
901 before, _mid, after = text.partition(escaped_sep)
902 startlist = before.split(sep, maxsplit) # a regular split is fine here
902 startlist = before.split(sep, maxsplit) # a regular split is fine here
903 unfinished = startlist[-1]
903 unfinished = startlist[-1]
904 startlist = startlist[:-1]
904 startlist = startlist[:-1]
905
905
906 # recurse because there may be more escaped separators
906 # recurse because there may be more escaped separators
907 endlist = escape_split(after, sep, maxsplit)
907 endlist = escape_split(after, sep, maxsplit)
908
908
909 # finish building the escaped value. we use endlist[0] becaue the first
909 # finish building the escaped value. we use endlist[0] becaue the first
910 # part of the string sent in recursion is the rest of the escaped value.
910 # part of the string sent in recursion is the rest of the escaped value.
911 unfinished += sep + endlist[0]
911 unfinished += sep + endlist[0]
912
912
913 return startlist + [unfinished] + endlist[1:] # put together all the parts
913 return startlist + [unfinished] + endlist[1:] # put together all the parts
914
914
915
915
916 class OptionalAttr(object):
916 class OptionalAttr(object):
917 """
917 """
918 Special Optional Option that defines other attribute. Example::
918 Special Optional Option that defines other attribute. Example::
919
919
920 def test(apiuser, userid=Optional(OAttr('apiuser')):
920 def test(apiuser, userid=Optional(OAttr('apiuser')):
921 user = Optional.extract(userid)
921 user = Optional.extract(userid)
922 # calls
922 # calls
923
923
924 """
924 """
925
925
926 def __init__(self, attr_name):
926 def __init__(self, attr_name):
927 self.attr_name = attr_name
927 self.attr_name = attr_name
928
928
929 def __repr__(self):
929 def __repr__(self):
930 return '<OptionalAttr:%s>' % self.attr_name
930 return '<OptionalAttr:%s>' % self.attr_name
931
931
932 def __call__(self):
932 def __call__(self):
933 return self
933 return self
934
934
935
935
936 # alias
936 # alias
937 OAttr = OptionalAttr
937 OAttr = OptionalAttr
938
938
939
939
940 class Optional(object):
940 class Optional(object):
941 """
941 """
942 Defines an optional parameter::
942 Defines an optional parameter::
943
943
944 param = param.getval() if isinstance(param, Optional) else param
944 param = param.getval() if isinstance(param, Optional) else param
945 param = param() if isinstance(param, Optional) else param
945 param = param() if isinstance(param, Optional) else param
946
946
947 is equivalent of::
947 is equivalent of::
948
948
949 param = Optional.extract(param)
949 param = Optional.extract(param)
950
950
951 """
951 """
952
952
953 def __init__(self, type_):
953 def __init__(self, type_):
954 self.type_ = type_
954 self.type_ = type_
955
955
956 def __repr__(self):
956 def __repr__(self):
957 return '<Optional:%s>' % self.type_.__repr__()
957 return '<Optional:%s>' % self.type_.__repr__()
958
958
959 def __call__(self):
959 def __call__(self):
960 return self.getval()
960 return self.getval()
961
961
962 def getval(self):
962 def getval(self):
963 """
963 """
964 returns value from this Optional instance
964 returns value from this Optional instance
965 """
965 """
966 if isinstance(self.type_, OAttr):
966 if isinstance(self.type_, OAttr):
967 # use params name
967 # use params name
968 return self.type_.attr_name
968 return self.type_.attr_name
969 return self.type_
969 return self.type_
970
970
971 @classmethod
971 @classmethod
972 def extract(cls, val):
972 def extract(cls, val):
973 """
973 """
974 Extracts value from Optional() instance
974 Extracts value from Optional() instance
975
975
976 :param val:
976 :param val:
977 :return: original value if it's not Optional instance else
977 :return: original value if it's not Optional instance else
978 value of instance
978 value of instance
979 """
979 """
980 if isinstance(val, cls):
980 if isinstance(val, cls):
981 return val.getval()
981 return val.getval()
982 return val
982 return val
983
983
984
984
985 def glob2re(pat):
985 def glob2re(pat):
986 """
986 """
987 Translate a shell PATTERN to a regular expression.
987 Translate a shell PATTERN to a regular expression.
988
988
989 There is no way to quote meta-characters.
989 There is no way to quote meta-characters.
990 """
990 """
991
991
992 i, n = 0, len(pat)
992 i, n = 0, len(pat)
993 res = ''
993 res = ''
994 while i < n:
994 while i < n:
995 c = pat[i]
995 c = pat[i]
996 i = i+1
996 i = i+1
997 if c == '*':
997 if c == '*':
998 #res = res + '.*'
998 #res = res + '.*'
999 res = res + '[^/]*'
999 res = res + '[^/]*'
1000 elif c == '?':
1000 elif c == '?':
1001 #res = res + '.'
1001 #res = res + '.'
1002 res = res + '[^/]'
1002 res = res + '[^/]'
1003 elif c == '[':
1003 elif c == '[':
1004 j = i
1004 j = i
1005 if j < n and pat[j] == '!':
1005 if j < n and pat[j] == '!':
1006 j = j+1
1006 j = j+1
1007 if j < n and pat[j] == ']':
1007 if j < n and pat[j] == ']':
1008 j = j+1
1008 j = j+1
1009 while j < n and pat[j] != ']':
1009 while j < n and pat[j] != ']':
1010 j = j+1
1010 j = j+1
1011 if j >= n:
1011 if j >= n:
1012 res = res + '\\['
1012 res = res + '\\['
1013 else:
1013 else:
1014 stuff = pat[i:j].replace('\\','\\\\')
1014 stuff = pat[i:j].replace('\\','\\\\')
1015 i = j+1
1015 i = j+1
1016 if stuff[0] == '!':
1016 if stuff[0] == '!':
1017 stuff = '^' + stuff[1:]
1017 stuff = '^' + stuff[1:]
1018 elif stuff[0] == '^':
1018 elif stuff[0] == '^':
1019 stuff = '\\' + stuff
1019 stuff = '\\' + stuff
1020 res = '%s[%s]' % (res, stuff)
1020 res = '%s[%s]' % (res, stuff)
1021 else:
1021 else:
1022 res = res + re.escape(c)
1022 res = res + re.escape(c)
1023 return res + '\Z(?ms)'
1023 return res + '\Z(?ms)'
1024
1024
1025
1025
1026 def parse_byte_string(size_str):
1026 def parse_byte_string(size_str):
1027 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1027 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1028 if not match:
1028 if not match:
1029 raise ValueError('Given size:%s is invalid, please make sure '
1029 raise ValueError('Given size:%s is invalid, please make sure '
1030 'to use format of <num>(MB|KB)' % size_str)
1030 'to use format of <num>(MB|KB)' % size_str)
1031
1031
1032 _parts = match.groups()
1032 _parts = match.groups()
1033 num, type_ = _parts
1033 num, type_ = _parts
1034 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
1034 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
1035
1035
1036
1036
1037 class CachedProperty(object):
1037 class CachedProperty(object):
1038 """
1038 """
1039 Lazy Attributes. With option to invalidate the cache by running a method
1039 Lazy Attributes. With option to invalidate the cache by running a method
1040
1040
1041 class Foo():
1041 >>> class Foo(object):
1042 ...
1043 ... @CachedProperty
1044 ... def heavy_func(self):
1045 ... return 'super-calculation'
1046 ...
1047 ... foo = Foo()
1048 ... foo.heavy_func() # first computation
1049 ... foo.heavy_func() # fetch from cache
1050 ... foo._invalidate_prop_cache('heavy_func')
1042
1051
1043 @CachedProperty
1044 def heavy_func():
1045 return 'super-calculation'
1046
1047 foo = Foo()
1048 foo.heavy_func() # first computions
1049 foo.heavy_func() # fetch from cache
1050 foo._invalidate_prop_cache('heavy_func')
1051 # at this point calling foo.heavy_func() will be re-computed
1052 # at this point calling foo.heavy_func() will be re-computed
1052 """
1053 """
1053
1054
1054 def __init__(self, func, func_name=None):
1055 def __init__(self, func, func_name=None):
1055
1056
1056 if func_name is None:
1057 if func_name is None:
1057 func_name = func.__name__
1058 func_name = func.__name__
1058 self.data = (func, func_name)
1059 self.data = (func, func_name)
1059 update_wrapper(self, func)
1060 update_wrapper(self, func)
1060
1061
1061 def __get__(self, inst, class_):
1062 def __get__(self, inst, class_):
1062 if inst is None:
1063 if inst is None:
1063 return self
1064 return self
1064
1065
1065 func, func_name = self.data
1066 func, func_name = self.data
1066 value = func(inst)
1067 value = func(inst)
1067 inst.__dict__[func_name] = value
1068 inst.__dict__[func_name] = value
1068 if '_invalidate_prop_cache' not in inst.__dict__:
1069 if '_invalidate_prop_cache' not in inst.__dict__:
1069 inst.__dict__['_invalidate_prop_cache'] = partial(
1070 inst.__dict__['_invalidate_prop_cache'] = partial(
1070 self._invalidate_prop_cache, inst)
1071 self._invalidate_prop_cache, inst)
1071 return value
1072 return value
1072
1073
1073 def _invalidate_prop_cache(self, inst, name):
1074 def _invalidate_prop_cache(self, inst, name):
1074 inst.__dict__.pop(name, None)
1075 inst.__dict__.pop(name, None)
1076
1077
1078 def retry(func=None, exception=Exception, n_tries=5, delay=5, backoff=1, logger=True):
1079 """
1080 Retry decorator with exponential backoff.
1081
1082 Parameters
1083 ----------
1084 func : typing.Callable, optional
1085 Callable on which the decorator is applied, by default None
1086 exception : Exception or tuple of Exceptions, optional
1087 Exception(s) that invoke retry, by default Exception
1088 n_tries : int, optional
1089 Number of tries before giving up, by default 5
1090 delay : int, optional
1091 Initial delay between retries in seconds, by default 5
1092 backoff : int, optional
1093 Backoff multiplier e.g. value of 2 will double the delay, by default 1
1094 logger : bool, optional
1095 Option to log or print, by default False
1096
1097 Returns
1098 -------
1099 typing.Callable
1100 Decorated callable that calls itself when exception(s) occur.
1101
1102 Examples
1103 --------
1104 >>> import random
1105 >>> @retry(exception=Exception, n_tries=3)
1106 ... def test_random(text):
1107 ... x = random.random()
1108 ... if x < 0.5:
1109 ... raise Exception("Fail")
1110 ... else:
1111 ... print("Success: ", text)
1112 >>> test_random("It works!")
1113 """
1114
1115 if func is None:
1116 return partial(
1117 retry,
1118 exception=exception,
1119 n_tries=n_tries,
1120 delay=delay,
1121 backoff=backoff,
1122 logger=logger,
1123 )
1124
1125 @wraps(func)
1126 def wrapper(*args, **kwargs):
1127 _n_tries, n_delay = n_tries, delay
1128 log = logging.getLogger('rhodecode.retry')
1129
1130 while _n_tries > 1:
1131 try:
1132 return func(*args, **kwargs)
1133 except exception as e:
1134 e_details = repr(e)
1135 msg = "Exception on calling func {func}: {e}, " \
1136 "Retrying in {n_delay} seconds..."\
1137 .format(func=func, e=e_details, n_delay=n_delay)
1138 if logger:
1139 log.warning(msg)
1140 else:
1141 print(msg)
1142 time.sleep(n_delay)
1143 _n_tries -= 1
1144 n_delay *= backoff
1145
1146 return func(*args, **kwargs)
1147
1148 return wrapper
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now