##// END OF EJS Templates
comments: use unified aggregation of comments counters....
marcink -
r1332:f4e615fc default
parent child Browse files
Show More

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

@@ -1,1029 +1,1018 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel
63 from rhodecode.model.pull_request import PullRequestModel
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69 def __before__(self):
69 def __before__(self):
70 super(PullrequestsController, self).__before__()
70 super(PullrequestsController, self).__before__()
71
71
72 def _load_compare_data(self, pull_request, inline_comments):
72 def _load_compare_data(self, pull_request, inline_comments):
73 """
73 """
74 Load context data needed for generating compare diff
74 Load context data needed for generating compare diff
75
75
76 :param pull_request: object related to the request
76 :param pull_request: object related to the request
77 :param enable_comments: flag to determine if comments are included
77 :param enable_comments: flag to determine if comments are included
78 """
78 """
79 source_repo = pull_request.source_repo
79 source_repo = pull_request.source_repo
80 source_ref_id = pull_request.source_ref_parts.commit_id
80 source_ref_id = pull_request.source_ref_parts.commit_id
81
81
82 target_repo = pull_request.target_repo
82 target_repo = pull_request.target_repo
83 target_ref_id = pull_request.target_ref_parts.commit_id
83 target_ref_id = pull_request.target_ref_parts.commit_id
84
84
85 # despite opening commits for bookmarks/branches/tags, we always
85 # despite opening commits for bookmarks/branches/tags, we always
86 # convert this to rev to prevent changes after bookmark or branch change
86 # convert this to rev to prevent changes after bookmark or branch change
87 c.source_ref_type = 'rev'
87 c.source_ref_type = 'rev'
88 c.source_ref = source_ref_id
88 c.source_ref = source_ref_id
89
89
90 c.target_ref_type = 'rev'
90 c.target_ref_type = 'rev'
91 c.target_ref = target_ref_id
91 c.target_ref = target_ref_id
92
92
93 c.source_repo = source_repo
93 c.source_repo = source_repo
94 c.target_repo = target_repo
94 c.target_repo = target_repo
95
95
96 c.fulldiff = bool(request.GET.get('fulldiff'))
96 c.fulldiff = bool(request.GET.get('fulldiff'))
97
97
98 # diff_limit is the old behavior, will cut off the whole diff
98 # diff_limit is the old behavior, will cut off the whole diff
99 # if the limit is applied otherwise will just hide the
99 # if the limit is applied otherwise will just hide the
100 # big files from the front-end
100 # big files from the front-end
101 diff_limit = self.cut_off_limit_diff
101 diff_limit = self.cut_off_limit_diff
102 file_limit = self.cut_off_limit_file
102 file_limit = self.cut_off_limit_file
103
103
104 pre_load = ["author", "branch", "date", "message"]
104 pre_load = ["author", "branch", "date", "message"]
105
105
106 c.commit_ranges = []
106 c.commit_ranges = []
107 source_commit = EmptyCommit()
107 source_commit = EmptyCommit()
108 target_commit = EmptyCommit()
108 target_commit = EmptyCommit()
109 c.missing_requirements = False
109 c.missing_requirements = False
110 try:
110 try:
111 c.commit_ranges = [
111 c.commit_ranges = [
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 for rev in pull_request.revisions]
113 for rev in pull_request.revisions]
114
114
115 c.statuses = source_repo.statuses(
115 c.statuses = source_repo.statuses(
116 [x.raw_id for x in c.commit_ranges])
116 [x.raw_id for x in c.commit_ranges])
117
117
118 target_commit = source_repo.get_commit(
118 target_commit = source_repo.get_commit(
119 commit_id=safe_str(target_ref_id))
119 commit_id=safe_str(target_ref_id))
120 source_commit = source_repo.get_commit(
120 source_commit = source_repo.get_commit(
121 commit_id=safe_str(source_ref_id))
121 commit_id=safe_str(source_ref_id))
122 except RepositoryRequirementError:
122 except RepositoryRequirementError:
123 c.missing_requirements = True
123 c.missing_requirements = True
124
124
125 # auto collapse if we have more than limit
125 # auto collapse if we have more than limit
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128
128
129 c.changes = {}
129 c.changes = {}
130 c.missing_commits = False
130 c.missing_commits = False
131 if (c.missing_requirements or
131 if (c.missing_requirements or
132 isinstance(source_commit, EmptyCommit) or
132 isinstance(source_commit, EmptyCommit) or
133 source_commit == target_commit):
133 source_commit == target_commit):
134 _parsed = []
134 _parsed = []
135 c.missing_commits = True
135 c.missing_commits = True
136 else:
136 else:
137 vcs_diff = PullRequestModel().get_diff(pull_request)
137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 diff_processor = diffs.DiffProcessor(
138 diff_processor = diffs.DiffProcessor(
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 file_limit=file_limit, show_full_diff=c.fulldiff)
140 file_limit=file_limit, show_full_diff=c.fulldiff)
141
141
142 _parsed = diff_processor.prepare()
142 _parsed = diff_processor.prepare()
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144
144
145 included_files = {}
145 included_files = {}
146 for f in _parsed:
146 for f in _parsed:
147 included_files[f['filename']] = f['stats']
147 included_files[f['filename']] = f['stats']
148
148
149 c.deleted_files = [fname for fname in inline_comments if
149 c.deleted_files = [fname for fname in inline_comments if
150 fname not in included_files]
150 fname not in included_files]
151
151
152 c.deleted_files_comments = collections.defaultdict(dict)
152 c.deleted_files_comments = collections.defaultdict(dict)
153 for fname, per_line_comments in inline_comments.items():
153 for fname, per_line_comments in inline_comments.items():
154 if fname in c.deleted_files:
154 if fname in c.deleted_files:
155 c.deleted_files_comments[fname]['stats'] = 0
155 c.deleted_files_comments[fname]['stats'] = 0
156 c.deleted_files_comments[fname]['comments'] = list()
156 c.deleted_files_comments[fname]['comments'] = list()
157 for lno, comments in per_line_comments.items():
157 for lno, comments in per_line_comments.items():
158 c.deleted_files_comments[fname]['comments'].extend(comments)
158 c.deleted_files_comments[fname]['comments'].extend(comments)
159
159
160 def _node_getter(commit):
160 def _node_getter(commit):
161 def get_node(fname):
161 def get_node(fname):
162 try:
162 try:
163 return commit.get_node(fname)
163 return commit.get_node(fname)
164 except NodeDoesNotExistError:
164 except NodeDoesNotExistError:
165 return None
165 return None
166 return get_node
166 return get_node
167
167
168 c.diffset = codeblocks.DiffSet(
168 c.diffset = codeblocks.DiffSet(
169 repo_name=c.repo_name,
169 repo_name=c.repo_name,
170 source_repo_name=c.source_repo.repo_name,
170 source_repo_name=c.source_repo.repo_name,
171 source_node_getter=_node_getter(target_commit),
171 source_node_getter=_node_getter(target_commit),
172 target_node_getter=_node_getter(source_commit),
172 target_node_getter=_node_getter(source_commit),
173 comments=inline_comments
173 comments=inline_comments
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175
175
176 def _extract_ordering(self, request):
176 def _extract_ordering(self, request):
177 column_index = safe_int(request.GET.get('order[0][column]'))
177 column_index = safe_int(request.GET.get('order[0][column]'))
178 order_dir = request.GET.get('order[0][dir]', 'desc')
178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 order_by = request.GET.get(
179 order_by = request.GET.get(
180 'columns[%s][data][sort]' % column_index, 'name_raw')
180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 return order_by, order_dir
181 return order_by, order_dir
182
182
183 @LoginRequired()
183 @LoginRequired()
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 'repository.admin')
185 'repository.admin')
186 @HasAcceptedRepoType('git', 'hg')
186 @HasAcceptedRepoType('git', 'hg')
187 def show_all(self, repo_name):
187 def show_all(self, repo_name):
188 # filter types
188 # filter types
189 c.active = 'open'
189 c.active = 'open'
190 c.source = str2bool(request.GET.get('source'))
190 c.source = str2bool(request.GET.get('source'))
191 c.closed = str2bool(request.GET.get('closed'))
191 c.closed = str2bool(request.GET.get('closed'))
192 c.my = str2bool(request.GET.get('my'))
192 c.my = str2bool(request.GET.get('my'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 c.repo_name = repo_name
195 c.repo_name = repo_name
196
196
197 opened_by = None
197 opened_by = None
198 if c.my:
198 if c.my:
199 c.active = 'my'
199 c.active = 'my'
200 opened_by = [c.rhodecode_user.user_id]
200 opened_by = [c.rhodecode_user.user_id]
201
201
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 if c.closed:
203 if c.closed:
204 c.active = 'closed'
204 c.active = 'closed'
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 if c.awaiting_review and not c.source:
207 if c.awaiting_review and not c.source:
208 c.active = 'awaiting'
208 c.active = 'awaiting'
209 if c.source and not c.awaiting_review:
209 if c.source and not c.awaiting_review:
210 c.active = 'source'
210 c.active = 'source'
211 if c.awaiting_my_review:
211 if c.awaiting_my_review:
212 c.active = 'awaiting_my'
212 c.active = 'awaiting_my'
213
213
214 data = self._get_pull_requests_list(
214 data = self._get_pull_requests_list(
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 if not request.is_xhr:
216 if not request.is_xhr:
217 c.data = json.dumps(data['data'])
217 c.data = json.dumps(data['data'])
218 c.records_total = data['recordsTotal']
218 c.records_total = data['recordsTotal']
219 return render('/pullrequests/pullrequests.mako')
219 return render('/pullrequests/pullrequests.mako')
220 else:
220 else:
221 return json.dumps(data)
221 return json.dumps(data)
222
222
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 # pagination
224 # pagination
225 start = safe_int(request.GET.get('start'), 0)
225 start = safe_int(request.GET.get('start'), 0)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 order_by, order_dir = self._extract_ordering(request)
227 order_by, order_dir = self._extract_ordering(request)
228
228
229 if c.awaiting_review:
229 if c.awaiting_review:
230 pull_requests = PullRequestModel().get_awaiting_review(
230 pull_requests = PullRequestModel().get_awaiting_review(
231 repo_name, source=c.source, opened_by=opened_by,
231 repo_name, source=c.source, opened_by=opened_by,
232 statuses=statuses, offset=start, length=length,
232 statuses=statuses, offset=start, length=length,
233 order_by=order_by, order_dir=order_dir)
233 order_by=order_by, order_dir=order_dir)
234 pull_requests_total_count = PullRequestModel(
234 pull_requests_total_count = PullRequestModel(
235 ).count_awaiting_review(
235 ).count_awaiting_review(
236 repo_name, source=c.source, statuses=statuses,
236 repo_name, source=c.source, statuses=statuses,
237 opened_by=opened_by)
237 opened_by=opened_by)
238 elif c.awaiting_my_review:
238 elif c.awaiting_my_review:
239 pull_requests = PullRequestModel().get_awaiting_my_review(
239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 repo_name, source=c.source, opened_by=opened_by,
240 repo_name, source=c.source, opened_by=opened_by,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 offset=start, length=length, order_by=order_by,
242 offset=start, length=length, order_by=order_by,
243 order_dir=order_dir)
243 order_dir=order_dir)
244 pull_requests_total_count = PullRequestModel(
244 pull_requests_total_count = PullRequestModel(
245 ).count_awaiting_my_review(
245 ).count_awaiting_my_review(
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 statuses=statuses, opened_by=opened_by)
247 statuses=statuses, opened_by=opened_by)
248 else:
248 else:
249 pull_requests = PullRequestModel().get_all(
249 pull_requests = PullRequestModel().get_all(
250 repo_name, source=c.source, opened_by=opened_by,
250 repo_name, source=c.source, opened_by=opened_by,
251 statuses=statuses, offset=start, length=length,
251 statuses=statuses, offset=start, length=length,
252 order_by=order_by, order_dir=order_dir)
252 order_by=order_by, order_dir=order_dir)
253 pull_requests_total_count = PullRequestModel().count_all(
253 pull_requests_total_count = PullRequestModel().count_all(
254 repo_name, source=c.source, statuses=statuses,
254 repo_name, source=c.source, statuses=statuses,
255 opened_by=opened_by)
255 opened_by=opened_by)
256
256
257 from rhodecode.lib.utils import PartialRenderer
257 from rhodecode.lib.utils import PartialRenderer
258 _render = PartialRenderer('data_table/_dt_elements.mako')
258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 data = []
259 data = []
260 for pr in pull_requests:
260 for pr in pull_requests:
261 comments = CommentsModel().get_all_comments(
261 comments = CommentsModel().get_all_comments(
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263
263
264 data.append({
264 data.append({
265 'name': _render('pullrequest_name',
265 'name': _render('pullrequest_name',
266 pr.pull_request_id, pr.target_repo.repo_name),
266 pr.pull_request_id, pr.target_repo.repo_name),
267 'name_raw': pr.pull_request_id,
267 'name_raw': pr.pull_request_id,
268 'status': _render('pullrequest_status',
268 'status': _render('pullrequest_status',
269 pr.calculated_review_status()),
269 pr.calculated_review_status()),
270 'title': _render(
270 'title': _render(
271 'pullrequest_title', pr.title, pr.description),
271 'pullrequest_title', pr.title, pr.description),
272 'description': h.escape(pr.description),
272 'description': h.escape(pr.description),
273 'updated_on': _render('pullrequest_updated_on',
273 'updated_on': _render('pullrequest_updated_on',
274 h.datetime_to_time(pr.updated_on)),
274 h.datetime_to_time(pr.updated_on)),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 'created_on': _render('pullrequest_updated_on',
276 'created_on': _render('pullrequest_updated_on',
277 h.datetime_to_time(pr.created_on)),
277 h.datetime_to_time(pr.created_on)),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 'author': _render('pullrequest_author',
279 'author': _render('pullrequest_author',
280 pr.author.full_contact, ),
280 pr.author.full_contact, ),
281 'author_raw': pr.author.full_name,
281 'author_raw': pr.author.full_name,
282 'comments': _render('pullrequest_comments', len(comments)),
282 'comments': _render('pullrequest_comments', len(comments)),
283 'comments_raw': len(comments),
283 'comments_raw': len(comments),
284 'closed': pr.is_closed(),
284 'closed': pr.is_closed(),
285 })
285 })
286 # json used to render the grid
286 # json used to render the grid
287 data = ({
287 data = ({
288 'data': data,
288 'data': data,
289 'recordsTotal': pull_requests_total_count,
289 'recordsTotal': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
291 })
291 })
292 return data
292 return data
293
293
294 @LoginRequired()
294 @LoginRequired()
295 @NotAnonymous()
295 @NotAnonymous()
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 'repository.admin')
297 'repository.admin')
298 @HasAcceptedRepoType('git', 'hg')
298 @HasAcceptedRepoType('git', 'hg')
299 def index(self):
299 def index(self):
300 source_repo = c.rhodecode_db_repo
300 source_repo = c.rhodecode_db_repo
301
301
302 try:
302 try:
303 source_repo.scm_instance().get_commit()
303 source_repo.scm_instance().get_commit()
304 except EmptyRepositoryError:
304 except EmptyRepositoryError:
305 h.flash(h.literal(_('There are no commits yet')),
305 h.flash(h.literal(_('There are no commits yet')),
306 category='warning')
306 category='warning')
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308
308
309 commit_id = request.GET.get('commit')
309 commit_id = request.GET.get('commit')
310 branch_ref = request.GET.get('branch')
310 branch_ref = request.GET.get('branch')
311 bookmark_ref = request.GET.get('bookmark')
311 bookmark_ref = request.GET.get('bookmark')
312
312
313 try:
313 try:
314 source_repo_data = PullRequestModel().generate_repo_data(
314 source_repo_data = PullRequestModel().generate_repo_data(
315 source_repo, commit_id=commit_id,
315 source_repo, commit_id=commit_id,
316 branch=branch_ref, bookmark=bookmark_ref)
316 branch=branch_ref, bookmark=bookmark_ref)
317 except CommitDoesNotExistError as e:
317 except CommitDoesNotExistError as e:
318 log.exception(e)
318 log.exception(e)
319 h.flash(_('Commit does not exist'), 'error')
319 h.flash(_('Commit does not exist'), 'error')
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321
321
322 default_target_repo = source_repo
322 default_target_repo = source_repo
323
323
324 if source_repo.parent:
324 if source_repo.parent:
325 parent_vcs_obj = source_repo.parent.scm_instance()
325 parent_vcs_obj = source_repo.parent.scm_instance()
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 # change default if we have a parent repo
327 # change default if we have a parent repo
328 default_target_repo = source_repo.parent
328 default_target_repo = source_repo.parent
329
329
330 target_repo_data = PullRequestModel().generate_repo_data(
330 target_repo_data = PullRequestModel().generate_repo_data(
331 default_target_repo)
331 default_target_repo)
332
332
333 selected_source_ref = source_repo_data['refs']['selected_ref']
333 selected_source_ref = source_repo_data['refs']['selected_ref']
334
334
335 title_source_ref = selected_source_ref.split(':', 2)[1]
335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 c.default_title = PullRequestModel().generate_pullrequest_title(
336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 source=source_repo.repo_name,
337 source=source_repo.repo_name,
338 source_ref=title_source_ref,
338 source_ref=title_source_ref,
339 target=default_target_repo.repo_name
339 target=default_target_repo.repo_name
340 )
340 )
341
341
342 c.default_repo_data = {
342 c.default_repo_data = {
343 'source_repo_name': source_repo.repo_name,
343 'source_repo_name': source_repo.repo_name,
344 'source_refs_json': json.dumps(source_repo_data),
344 'source_refs_json': json.dumps(source_repo_data),
345 'target_repo_name': default_target_repo.repo_name,
345 'target_repo_name': default_target_repo.repo_name,
346 'target_refs_json': json.dumps(target_repo_data),
346 'target_refs_json': json.dumps(target_repo_data),
347 }
347 }
348 c.default_source_ref = selected_source_ref
348 c.default_source_ref = selected_source_ref
349
349
350 return render('/pullrequests/pullrequest.mako')
350 return render('/pullrequests/pullrequest.mako')
351
351
352 @LoginRequired()
352 @LoginRequired()
353 @NotAnonymous()
353 @NotAnonymous()
354 @XHRRequired()
354 @XHRRequired()
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 'repository.admin')
356 'repository.admin')
357 @jsonify
357 @jsonify
358 def get_repo_refs(self, repo_name, target_repo_name):
358 def get_repo_refs(self, repo_name, target_repo_name):
359 repo = Repository.get_by_repo_name(target_repo_name)
359 repo = Repository.get_by_repo_name(target_repo_name)
360 if not repo:
360 if not repo:
361 raise HTTPNotFound
361 raise HTTPNotFound
362 return PullRequestModel().generate_repo_data(repo)
362 return PullRequestModel().generate_repo_data(repo)
363
363
364 @LoginRequired()
364 @LoginRequired()
365 @NotAnonymous()
365 @NotAnonymous()
366 @XHRRequired()
366 @XHRRequired()
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 'repository.admin')
368 'repository.admin')
369 @jsonify
369 @jsonify
370 def get_repo_destinations(self, repo_name):
370 def get_repo_destinations(self, repo_name):
371 repo = Repository.get_by_repo_name(repo_name)
371 repo = Repository.get_by_repo_name(repo_name)
372 if not repo:
372 if not repo:
373 raise HTTPNotFound
373 raise HTTPNotFound
374 filter_query = request.GET.get('query')
374 filter_query = request.GET.get('query')
375
375
376 query = Repository.query() \
376 query = Repository.query() \
377 .order_by(func.length(Repository.repo_name)) \
377 .order_by(func.length(Repository.repo_name)) \
378 .filter(or_(
378 .filter(or_(
379 Repository.repo_name == repo.repo_name,
379 Repository.repo_name == repo.repo_name,
380 Repository.fork_id == repo.repo_id))
380 Repository.fork_id == repo.repo_id))
381
381
382 if filter_query:
382 if filter_query:
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 query = query.filter(
384 query = query.filter(
385 Repository.repo_name.ilike(ilike_expression))
385 Repository.repo_name.ilike(ilike_expression))
386
386
387 add_parent = False
387 add_parent = False
388 if repo.parent:
388 if repo.parent:
389 if filter_query in repo.parent.repo_name:
389 if filter_query in repo.parent.repo_name:
390 parent_vcs_obj = repo.parent.scm_instance()
390 parent_vcs_obj = repo.parent.scm_instance()
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 add_parent = True
392 add_parent = True
393
393
394 limit = 20 - 1 if add_parent else 20
394 limit = 20 - 1 if add_parent else 20
395 all_repos = query.limit(limit).all()
395 all_repos = query.limit(limit).all()
396 if add_parent:
396 if add_parent:
397 all_repos += [repo.parent]
397 all_repos += [repo.parent]
398
398
399 repos = []
399 repos = []
400 for obj in self.scm_model.get_repos(all_repos):
400 for obj in self.scm_model.get_repos(all_repos):
401 repos.append({
401 repos.append({
402 'id': obj['name'],
402 'id': obj['name'],
403 'text': obj['name'],
403 'text': obj['name'],
404 'type': 'repo',
404 'type': 'repo',
405 'obj': obj['dbrepo']
405 'obj': obj['dbrepo']
406 })
406 })
407
407
408 data = {
408 data = {
409 'more': False,
409 'more': False,
410 'results': [{
410 'results': [{
411 'text': _('Repositories'),
411 'text': _('Repositories'),
412 'children': repos
412 'children': repos
413 }] if repos else []
413 }] if repos else []
414 }
414 }
415 return data
415 return data
416
416
417 @LoginRequired()
417 @LoginRequired()
418 @NotAnonymous()
418 @NotAnonymous()
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 'repository.admin')
420 'repository.admin')
421 @HasAcceptedRepoType('git', 'hg')
421 @HasAcceptedRepoType('git', 'hg')
422 @auth.CSRFRequired()
422 @auth.CSRFRequired()
423 def create(self, repo_name):
423 def create(self, repo_name):
424 repo = Repository.get_by_repo_name(repo_name)
424 repo = Repository.get_by_repo_name(repo_name)
425 if not repo:
425 if not repo:
426 raise HTTPNotFound
426 raise HTTPNotFound
427
427
428 controls = peppercorn.parse(request.POST.items())
428 controls = peppercorn.parse(request.POST.items())
429
429
430 try:
430 try:
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 except formencode.Invalid as errors:
432 except formencode.Invalid as errors:
433 if errors.error_dict.get('revisions'):
433 if errors.error_dict.get('revisions'):
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 elif errors.error_dict.get('pullrequest_title'):
435 elif errors.error_dict.get('pullrequest_title'):
436 msg = _('Pull request requires a title with min. 3 chars')
436 msg = _('Pull request requires a title with min. 3 chars')
437 else:
437 else:
438 msg = _('Error creating pull request: {}').format(errors)
438 msg = _('Error creating pull request: {}').format(errors)
439 log.exception(msg)
439 log.exception(msg)
440 h.flash(msg, 'error')
440 h.flash(msg, 'error')
441
441
442 # would rather just go back to form ...
442 # would rather just go back to form ...
443 return redirect(url('pullrequest_home', repo_name=repo_name))
443 return redirect(url('pullrequest_home', repo_name=repo_name))
444
444
445 source_repo = _form['source_repo']
445 source_repo = _form['source_repo']
446 source_ref = _form['source_ref']
446 source_ref = _form['source_ref']
447 target_repo = _form['target_repo']
447 target_repo = _form['target_repo']
448 target_ref = _form['target_ref']
448 target_ref = _form['target_ref']
449 commit_ids = _form['revisions'][::-1]
449 commit_ids = _form['revisions'][::-1]
450 reviewers = [
450 reviewers = [
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452
452
453 # find the ancestor for this pr
453 # find the ancestor for this pr
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456
456
457 source_scm = source_db_repo.scm_instance()
457 source_scm = source_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
459
459
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462
462
463 ancestor = source_scm.get_common_ancestor(
463 ancestor = source_scm.get_common_ancestor(
464 source_commit.raw_id, target_commit.raw_id, target_scm)
464 source_commit.raw_id, target_commit.raw_id, target_scm)
465
465
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468
468
469 pullrequest_title = _form['pullrequest_title']
469 pullrequest_title = _form['pullrequest_title']
470 title_source_ref = source_ref.split(':', 2)[1]
470 title_source_ref = source_ref.split(':', 2)[1]
471 if not pullrequest_title:
471 if not pullrequest_title:
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 source=source_repo,
473 source=source_repo,
474 source_ref=title_source_ref,
474 source_ref=title_source_ref,
475 target=target_repo
475 target=target_repo
476 )
476 )
477
477
478 description = _form['pullrequest_desc']
478 description = _form['pullrequest_desc']
479 try:
479 try:
480 pull_request = PullRequestModel().create(
480 pull_request = PullRequestModel().create(
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 target_ref, commit_ids, reviewers, pullrequest_title,
482 target_ref, commit_ids, reviewers, pullrequest_title,
483 description
483 description
484 )
484 )
485 Session().commit()
485 Session().commit()
486 h.flash(_('Successfully opened new pull request'),
486 h.flash(_('Successfully opened new pull request'),
487 category='success')
487 category='success')
488 except Exception as e:
488 except Exception as e:
489 msg = _('Error occurred during sending pull request')
489 msg = _('Error occurred during sending pull request')
490 log.exception(msg)
490 log.exception(msg)
491 h.flash(msg, category='error')
491 h.flash(msg, category='error')
492 return redirect(url('pullrequest_home', repo_name=repo_name))
492 return redirect(url('pullrequest_home', repo_name=repo_name))
493
493
494 return redirect(url('pullrequest_show', repo_name=target_repo,
494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 pull_request_id=pull_request.pull_request_id))
495 pull_request_id=pull_request.pull_request_id))
496
496
497 @LoginRequired()
497 @LoginRequired()
498 @NotAnonymous()
498 @NotAnonymous()
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 'repository.admin')
500 'repository.admin')
501 @auth.CSRFRequired()
501 @auth.CSRFRequired()
502 @jsonify
502 @jsonify
503 def update(self, repo_name, pull_request_id):
503 def update(self, repo_name, pull_request_id):
504 pull_request_id = safe_int(pull_request_id)
504 pull_request_id = safe_int(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
506 # only owner or admin can update it
506 # only owner or admin can update it
507 allowed_to_update = PullRequestModel().check_user_update(
507 allowed_to_update = PullRequestModel().check_user_update(
508 pull_request, c.rhodecode_user)
508 pull_request, c.rhodecode_user)
509 if allowed_to_update:
509 if allowed_to_update:
510 controls = peppercorn.parse(request.POST.items())
510 controls = peppercorn.parse(request.POST.items())
511
511
512 if 'review_members' in controls:
512 if 'review_members' in controls:
513 self._update_reviewers(
513 self._update_reviewers(
514 pull_request_id, controls['review_members'])
514 pull_request_id, controls['review_members'])
515 elif str2bool(request.POST.get('update_commits', 'false')):
515 elif str2bool(request.POST.get('update_commits', 'false')):
516 self._update_commits(pull_request)
516 self._update_commits(pull_request)
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 self._reject_close(pull_request)
518 self._reject_close(pull_request)
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 self._edit_pull_request(pull_request)
520 self._edit_pull_request(pull_request)
521 else:
521 else:
522 raise HTTPBadRequest()
522 raise HTTPBadRequest()
523 return True
523 return True
524 raise HTTPForbidden()
524 raise HTTPForbidden()
525
525
526 def _edit_pull_request(self, pull_request):
526 def _edit_pull_request(self, pull_request):
527 try:
527 try:
528 PullRequestModel().edit(
528 PullRequestModel().edit(
529 pull_request, request.POST.get('title'),
529 pull_request, request.POST.get('title'),
530 request.POST.get('description'))
530 request.POST.get('description'))
531 except ValueError:
531 except ValueError:
532 msg = _(u'Cannot update closed pull requests.')
532 msg = _(u'Cannot update closed pull requests.')
533 h.flash(msg, category='error')
533 h.flash(msg, category='error')
534 return
534 return
535 else:
535 else:
536 Session().commit()
536 Session().commit()
537
537
538 msg = _(u'Pull request title & description updated.')
538 msg = _(u'Pull request title & description updated.')
539 h.flash(msg, category='success')
539 h.flash(msg, category='success')
540 return
540 return
541
541
542 def _update_commits(self, pull_request):
542 def _update_commits(self, pull_request):
543 resp = PullRequestModel().update_commits(pull_request)
543 resp = PullRequestModel().update_commits(pull_request)
544
544
545 if resp.executed:
545 if resp.executed:
546 msg = _(
546 msg = _(
547 u'Pull request updated to "{source_commit_id}" with '
547 u'Pull request updated to "{source_commit_id}" with '
548 u'{count_added} added, {count_removed} removed commits.')
548 u'{count_added} added, {count_removed} removed commits.')
549 msg = msg.format(
549 msg = msg.format(
550 source_commit_id=pull_request.source_ref_parts.commit_id,
550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 count_added=len(resp.changes.added),
551 count_added=len(resp.changes.added),
552 count_removed=len(resp.changes.removed))
552 count_removed=len(resp.changes.removed))
553 h.flash(msg, category='success')
553 h.flash(msg, category='success')
554
554
555 registry = get_current_registry()
555 registry = get_current_registry()
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 if channelstream_config.get('enabled'):
558 if channelstream_config.get('enabled'):
559 message = msg + (
559 message = msg + (
560 ' - <a onclick="window.location.reload()">'
560 ' - <a onclick="window.location.reload()">'
561 '<strong>{}</strong></a>'.format(_('Reload page')))
561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 channel = '/repo${}$/pr/{}'.format(
562 channel = '/repo${}$/pr/{}'.format(
563 pull_request.target_repo.repo_name,
563 pull_request.target_repo.repo_name,
564 pull_request.pull_request_id
564 pull_request.pull_request_id
565 )
565 )
566 payload = {
566 payload = {
567 'type': 'message',
567 'type': 'message',
568 'user': 'system',
568 'user': 'system',
569 'exclude_users': [request.user.username],
569 'exclude_users': [request.user.username],
570 'channel': channel,
570 'channel': channel,
571 'message': {
571 'message': {
572 'message': message,
572 'message': message,
573 'level': 'success',
573 'level': 'success',
574 'topic': '/notifications'
574 'topic': '/notifications'
575 }
575 }
576 }
576 }
577 channelstream_request(
577 channelstream_request(
578 channelstream_config, [payload], '/message',
578 channelstream_config, [payload], '/message',
579 raise_exc=False)
579 raise_exc=False)
580 else:
580 else:
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 warning_reasons = [
582 warning_reasons = [
583 UpdateFailureReason.NO_CHANGE,
583 UpdateFailureReason.NO_CHANGE,
584 UpdateFailureReason.WRONG_REF_TPYE,
584 UpdateFailureReason.WRONG_REF_TPYE,
585 ]
585 ]
586 category = 'warning' if resp.reason in warning_reasons else 'error'
586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 h.flash(msg, category=category)
587 h.flash(msg, category=category)
588
588
589 @auth.CSRFRequired()
589 @auth.CSRFRequired()
590 @LoginRequired()
590 @LoginRequired()
591 @NotAnonymous()
591 @NotAnonymous()
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 'repository.admin')
593 'repository.admin')
594 def merge(self, repo_name, pull_request_id):
594 def merge(self, repo_name, pull_request_id):
595 """
595 """
596 POST /{repo_name}/pull-request/{pull_request_id}
596 POST /{repo_name}/pull-request/{pull_request_id}
597
597
598 Merge will perform a server-side merge of the specified
598 Merge will perform a server-side merge of the specified
599 pull request, if the pull request is approved and mergeable.
599 pull request, if the pull request is approved and mergeable.
600 After succesfull merging, the pull request is automatically
600 After succesfull merging, the pull request is automatically
601 closed, with a relevant comment.
601 closed, with a relevant comment.
602 """
602 """
603 pull_request_id = safe_int(pull_request_id)
603 pull_request_id = safe_int(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
605 user = c.rhodecode_user
605 user = c.rhodecode_user
606
606
607 if self._meets_merge_pre_conditions(pull_request, user):
607 if self._meets_merge_pre_conditions(pull_request, user):
608 log.debug("Pre-conditions checked, trying to merge.")
608 log.debug("Pre-conditions checked, trying to merge.")
609 extras = vcs_operation_context(
609 extras = vcs_operation_context(
610 request.environ, repo_name=pull_request.target_repo.repo_name,
610 request.environ, repo_name=pull_request.target_repo.repo_name,
611 username=user.username, action='push',
611 username=user.username, action='push',
612 scm=pull_request.target_repo.repo_type)
612 scm=pull_request.target_repo.repo_type)
613 self._merge_pull_request(pull_request, user, extras)
613 self._merge_pull_request(pull_request, user, extras)
614
614
615 return redirect(url(
615 return redirect(url(
616 'pullrequest_show',
616 'pullrequest_show',
617 repo_name=pull_request.target_repo.repo_name,
617 repo_name=pull_request.target_repo.repo_name,
618 pull_request_id=pull_request.pull_request_id))
618 pull_request_id=pull_request.pull_request_id))
619
619
620 def _meets_merge_pre_conditions(self, pull_request, user):
620 def _meets_merge_pre_conditions(self, pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
622 raise HTTPForbidden()
622 raise HTTPForbidden()
623
623
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 if not merge_status:
625 if not merge_status:
626 log.debug("Cannot merge, not mergeable.")
626 log.debug("Cannot merge, not mergeable.")
627 h.flash(msg, category='error')
627 h.flash(msg, category='error')
628 return False
628 return False
629
629
630 if (pull_request.calculated_review_status()
630 if (pull_request.calculated_review_status()
631 is not ChangesetStatus.STATUS_APPROVED):
631 is not ChangesetStatus.STATUS_APPROVED):
632 log.debug("Cannot merge, approval is pending.")
632 log.debug("Cannot merge, approval is pending.")
633 msg = _('Pull request reviewer approval is pending.')
633 msg = _('Pull request reviewer approval is pending.')
634 h.flash(msg, category='error')
634 h.flash(msg, category='error')
635 return False
635 return False
636 return True
636 return True
637
637
638 def _merge_pull_request(self, pull_request, user, extras):
638 def _merge_pull_request(self, pull_request, user, extras):
639 merge_resp = PullRequestModel().merge(
639 merge_resp = PullRequestModel().merge(
640 pull_request, user, extras=extras)
640 pull_request, user, extras=extras)
641
641
642 if merge_resp.executed:
642 if merge_resp.executed:
643 log.debug("The merge was successful, closing the pull request.")
643 log.debug("The merge was successful, closing the pull request.")
644 PullRequestModel().close_pull_request(
644 PullRequestModel().close_pull_request(
645 pull_request.pull_request_id, user)
645 pull_request.pull_request_id, user)
646 Session().commit()
646 Session().commit()
647 msg = _('Pull request was successfully merged and closed.')
647 msg = _('Pull request was successfully merged and closed.')
648 h.flash(msg, category='success')
648 h.flash(msg, category='success')
649 else:
649 else:
650 log.debug(
650 log.debug(
651 "The merge was not successful. Merge response: %s",
651 "The merge was not successful. Merge response: %s",
652 merge_resp)
652 merge_resp)
653 msg = PullRequestModel().merge_status_message(
653 msg = PullRequestModel().merge_status_message(
654 merge_resp.failure_reason)
654 merge_resp.failure_reason)
655 h.flash(msg, category='error')
655 h.flash(msg, category='error')
656
656
657 def _update_reviewers(self, pull_request_id, review_members):
657 def _update_reviewers(self, pull_request_id, review_members):
658 reviewers = [
658 reviewers = [
659 (int(r['user_id']), r['reasons']) for r in review_members]
659 (int(r['user_id']), r['reasons']) for r in review_members]
660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 Session().commit()
661 Session().commit()
662
662
663 def _reject_close(self, pull_request):
663 def _reject_close(self, pull_request):
664 if pull_request.is_closed():
664 if pull_request.is_closed():
665 raise HTTPForbidden()
665 raise HTTPForbidden()
666
666
667 PullRequestModel().close_pull_request_with_comment(
667 PullRequestModel().close_pull_request_with_comment(
668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 Session().commit()
669 Session().commit()
670
670
671 @LoginRequired()
671 @LoginRequired()
672 @NotAnonymous()
672 @NotAnonymous()
673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 'repository.admin')
674 'repository.admin')
675 @auth.CSRFRequired()
675 @auth.CSRFRequired()
676 @jsonify
676 @jsonify
677 def delete(self, repo_name, pull_request_id):
677 def delete(self, repo_name, pull_request_id):
678 pull_request_id = safe_int(pull_request_id)
678 pull_request_id = safe_int(pull_request_id)
679 pull_request = PullRequest.get_or_404(pull_request_id)
679 pull_request = PullRequest.get_or_404(pull_request_id)
680 # only owner can delete it !
680 # only owner can delete it !
681 if pull_request.author.user_id == c.rhodecode_user.user_id:
681 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 PullRequestModel().delete(pull_request)
682 PullRequestModel().delete(pull_request)
683 Session().commit()
683 Session().commit()
684 h.flash(_('Successfully deleted pull request'),
684 h.flash(_('Successfully deleted pull request'),
685 category='success')
685 category='success')
686 return redirect(url('my_account_pullrequests'))
686 return redirect(url('my_account_pullrequests'))
687 raise HTTPForbidden()
687 raise HTTPForbidden()
688
688
689 def _get_pr_version(self, pull_request_id, version=None):
689 def _get_pr_version(self, pull_request_id, version=None):
690 pull_request_id = safe_int(pull_request_id)
690 pull_request_id = safe_int(pull_request_id)
691 at_version = None
691 at_version = None
692
692
693 if version and version == 'latest':
693 if version and version == 'latest':
694 pull_request_ver = PullRequest.get(pull_request_id)
694 pull_request_ver = PullRequest.get(pull_request_id)
695 pull_request_obj = pull_request_ver
695 pull_request_obj = pull_request_ver
696 _org_pull_request_obj = pull_request_obj
696 _org_pull_request_obj = pull_request_obj
697 at_version = 'latest'
697 at_version = 'latest'
698 elif version:
698 elif version:
699 pull_request_ver = PullRequestVersion.get_or_404(version)
699 pull_request_ver = PullRequestVersion.get_or_404(version)
700 pull_request_obj = pull_request_ver
700 pull_request_obj = pull_request_ver
701 _org_pull_request_obj = pull_request_ver.pull_request
701 _org_pull_request_obj = pull_request_ver.pull_request
702 at_version = pull_request_ver.pull_request_version_id
702 at_version = pull_request_ver.pull_request_version_id
703 else:
703 else:
704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
705
705
706 pull_request_display_obj = PullRequest.get_pr_display_object(
706 pull_request_display_obj = PullRequest.get_pr_display_object(
707 pull_request_obj, _org_pull_request_obj)
707 pull_request_obj, _org_pull_request_obj)
708 return _org_pull_request_obj, pull_request_obj, \
708 return _org_pull_request_obj, pull_request_obj, \
709 pull_request_display_obj, at_version
709 pull_request_display_obj, at_version
710
710
711 def _get_pr_version_changes(self, version, pull_request_latest):
711 def _get_pr_version_changes(self, version, pull_request_latest):
712 """
712 """
713 Generate changes commits, and diff data based on the current pr version
713 Generate changes commits, and diff data based on the current pr version
714 """
714 """
715
715
716 #TODO(marcink): save those changes as JSON metadata for chaching later.
716 #TODO(marcink): save those changes as JSON metadata for chaching later.
717
717
718 # fake the version to add the "initial" state object
718 # fake the version to add the "initial" state object
719 pull_request_initial = PullRequest.get_pr_display_object(
719 pull_request_initial = PullRequest.get_pr_display_object(
720 pull_request_latest, pull_request_latest,
720 pull_request_latest, pull_request_latest,
721 internal_methods=['get_commit', 'versions'])
721 internal_methods=['get_commit', 'versions'])
722 pull_request_initial.revisions = []
722 pull_request_initial.revisions = []
723 pull_request_initial.source_repo.get_commit = types.MethodType(
723 pull_request_initial.source_repo.get_commit = types.MethodType(
724 lambda *a, **k: EmptyCommit(), pull_request_initial)
724 lambda *a, **k: EmptyCommit(), pull_request_initial)
725 pull_request_initial.source_repo.scm_instance = types.MethodType(
725 pull_request_initial.source_repo.scm_instance = types.MethodType(
726 lambda *a, **k: EmptyRepository(), pull_request_initial)
726 lambda *a, **k: EmptyRepository(), pull_request_initial)
727
727
728 _changes_versions = [pull_request_latest] + \
728 _changes_versions = [pull_request_latest] + \
729 list(reversed(c.versions)) + \
729 list(reversed(c.versions)) + \
730 [pull_request_initial]
730 [pull_request_initial]
731
731
732 if version == 'latest':
732 if version == 'latest':
733 index = 0
733 index = 0
734 else:
734 else:
735 for pos, prver in enumerate(_changes_versions):
735 for pos, prver in enumerate(_changes_versions):
736 ver = getattr(prver, 'pull_request_version_id', -1)
736 ver = getattr(prver, 'pull_request_version_id', -1)
737 if ver == safe_int(version):
737 if ver == safe_int(version):
738 index = pos
738 index = pos
739 break
739 break
740 else:
740 else:
741 index = 0
741 index = 0
742
742
743 cur_obj = _changes_versions[index]
743 cur_obj = _changes_versions[index]
744 prev_obj = _changes_versions[index + 1]
744 prev_obj = _changes_versions[index + 1]
745
745
746 old_commit_ids = set(prev_obj.revisions)
746 old_commit_ids = set(prev_obj.revisions)
747 new_commit_ids = set(cur_obj.revisions)
747 new_commit_ids = set(cur_obj.revisions)
748
748
749 changes = PullRequestModel()._calculate_commit_id_changes(
749 changes = PullRequestModel()._calculate_commit_id_changes(
750 old_commit_ids, new_commit_ids)
750 old_commit_ids, new_commit_ids)
751
751
752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
753 cur_obj, prev_obj)
753 cur_obj, prev_obj)
754 file_changes = PullRequestModel()._calculate_file_changes(
754 file_changes = PullRequestModel()._calculate_file_changes(
755 old_diff_data, new_diff_data)
755 old_diff_data, new_diff_data)
756 return changes, file_changes
756 return changes, file_changes
757
757
758 @LoginRequired()
758 @LoginRequired()
759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 'repository.admin')
760 'repository.admin')
761 def show(self, repo_name, pull_request_id):
761 def show(self, repo_name, pull_request_id):
762 pull_request_id = safe_int(pull_request_id)
762 pull_request_id = safe_int(pull_request_id)
763 version = request.GET.get('version')
763 version = request.GET.get('version')
764
764
765 (pull_request_latest,
765 (pull_request_latest,
766 pull_request_at_ver,
766 pull_request_at_ver,
767 pull_request_display_obj,
767 pull_request_display_obj,
768 at_version) = self._get_pr_version(pull_request_id, version=version)
768 at_version) = self._get_pr_version(pull_request_id, version=version)
769
769
770 c.template_context['pull_request_data']['pull_request_id'] = \
770 c.template_context['pull_request_data']['pull_request_id'] = \
771 pull_request_id
771 pull_request_id
772
772
773 # pull_requests repo_name we opened it against
773 # pull_requests repo_name we opened it against
774 # ie. target_repo must match
774 # ie. target_repo must match
775 if repo_name != pull_request_at_ver.target_repo.repo_name:
775 if repo_name != pull_request_at_ver.target_repo.repo_name:
776 raise HTTPNotFound
776 raise HTTPNotFound
777
777
778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
779 pull_request_at_ver)
779 pull_request_at_ver)
780
780
781 pr_closed = pull_request_latest.is_closed()
781 pr_closed = pull_request_latest.is_closed()
782 if at_version and not at_version == 'latest':
782 if at_version and not at_version == 'latest':
783 c.allowed_to_change_status = False
783 c.allowed_to_change_status = False
784 c.allowed_to_update = False
784 c.allowed_to_update = False
785 c.allowed_to_merge = False
785 c.allowed_to_merge = False
786 c.allowed_to_delete = False
786 c.allowed_to_delete = False
787 c.allowed_to_comment = False
787 c.allowed_to_comment = False
788 else:
788 else:
789 c.allowed_to_change_status = PullRequestModel(). \
789 c.allowed_to_change_status = PullRequestModel(). \
790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
791 c.allowed_to_update = PullRequestModel().check_user_update(
791 c.allowed_to_update = PullRequestModel().check_user_update(
792 pull_request_latest, c.rhodecode_user) and not pr_closed
792 pull_request_latest, c.rhodecode_user) and not pr_closed
793 c.allowed_to_merge = PullRequestModel().check_user_merge(
793 c.allowed_to_merge = PullRequestModel().check_user_merge(
794 pull_request_latest, c.rhodecode_user) and not pr_closed
794 pull_request_latest, c.rhodecode_user) and not pr_closed
795 c.allowed_to_delete = PullRequestModel().check_user_delete(
795 c.allowed_to_delete = PullRequestModel().check_user_delete(
796 pull_request_latest, c.rhodecode_user) and not pr_closed
796 pull_request_latest, c.rhodecode_user) and not pr_closed
797 c.allowed_to_comment = not pr_closed
797 c.allowed_to_comment = not pr_closed
798
798
799 cc_model = CommentsModel()
799 cc_model = CommentsModel()
800
800
801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
804 pull_request_at_ver)
804 pull_request_at_ver)
805 c.approval_msg = None
805 c.approval_msg = None
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
807 c.approval_msg = _('Reviewer approval is pending.')
807 c.approval_msg = _('Reviewer approval is pending.')
808 c.pr_merge_status = False
808 c.pr_merge_status = False
809
809
810 # inline comments
811 inline_comments = cc_model.get_inline_comments(
812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
813
814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
815 inline_comments, version=at_version, include_aggregates=True)
816
817 c.versions = pull_request_display_obj.versions()
810 c.versions = pull_request_display_obj.versions()
811 c.at_version = at_version
818 c.at_version_num = at_version if at_version and at_version != 'latest' else None
812 c.at_version_num = at_version if at_version and at_version != 'latest' else None
819 c.at_version_pos = ChangesetComment.get_index_from_version(
813 c.at_version_pos = ChangesetComment.get_index_from_version(
820 c.at_version_num, c.versions)
814 c.at_version_num, c.versions)
821
815
822 is_outdated = lambda co: \
816 # GENERAL COMMENTS with versions #
823 not c.at_version_num \
817 q = cc_model._all_general_comments_of_pull_request(pull_request_latest)
824 or co.pull_request_version_id <= c.at_version_num
818 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
825
819
826 # inline_comments_until_version
820 # pick comments we want to render at current version
827 if c.at_version_num:
821 c.comment_versions = cc_model.aggregate_comments(
828 # if we use version, then do not show later comments
822 general_comments, c.versions, c.at_version_num)
829 # than current version
823 c.comments = c.comment_versions[c.at_version_num]['until']
830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
824
831 for fname, per_line_comments in inline_comments.iteritems():
825 # INLINE COMMENTS with versions #
832 for lno, comments in per_line_comments.iteritems():
826 q = cc_model._all_inline_comments_of_pull_request(pull_request_latest)
833 for co in comments:
827 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
834 if co.pull_request_version_id and is_outdated(co):
828 c.inline_versions = cc_model.aggregate_comments(
835 paths[co.f_path][co.line_no].append(co)
829 inline_comments, c.versions, c.at_version_num, inline=True)
836 inline_comments = paths
837
830
838 # outdated comments
831 # if we use version, then do not show later comments
839 c.outdated_cnt = 0
832 # than current version
840 if CommentsModel.use_outdated_comments(pull_request_latest):
833 paths = collections.defaultdict(lambda: collections.defaultdict(list))
841 outdated_comments = cc_model.get_outdated_comments(
834 for co in inline_comments:
842 c.rhodecode_db_repo.repo_id,
835 if c.at_version_num:
843 pull_request=pull_request_at_ver)
836 # pick comments that are at least UPTO given version, so we
837 # don't render comments for higher version
838 should_render = co.pull_request_version_id and \
839 co.pull_request_version_id <= c.at_version_num
840 else:
841 # showing all, for 'latest'
842 should_render = True
844
843
845 # Count outdated comments and check for deleted files
844 if should_render:
846 is_outdated = lambda co: \
845 paths[co.f_path][co.line_no].append(co)
847 not c.at_version_num \
846 inline_comments = paths
848 or co.pull_request_version_id < c.at_version_num
849 for file_name, lines in outdated_comments.iteritems():
850 for comments in lines.values():
851 comments = [comm for comm in comments if is_outdated(comm)]
852 c.outdated_cnt += len(comments)
853
847
854 # load compare data into template context
848 # load compare data into template context
855 self._load_compare_data(pull_request_at_ver, inline_comments)
849 self._load_compare_data(pull_request_at_ver, inline_comments)
856
850
857 # this is a hack to properly display links, when creating PR, the
851 # this is a hack to properly display links, when creating PR, the
858 # compare view and others uses different notation, and
852 # compare view and others uses different notation, and
859 # compare_commits.mako renders links based on the target_repo.
853 # compare_commits.mako renders links based on the target_repo.
860 # We need to swap that here to generate it properly on the html side
854 # We need to swap that here to generate it properly on the html side
861 c.target_repo = c.source_repo
855 c.target_repo = c.source_repo
862
856
863 # general comments
864 c.comments = cc_model.get_comments(
865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
866
867 if c.allowed_to_update:
857 if c.allowed_to_update:
868 force_close = ('forced_closed', _('Close Pull Request'))
858 force_close = ('forced_closed', _('Close Pull Request'))
869 statuses = ChangesetStatus.STATUSES + [force_close]
859 statuses = ChangesetStatus.STATUSES + [force_close]
870 else:
860 else:
871 statuses = ChangesetStatus.STATUSES
861 statuses = ChangesetStatus.STATUSES
872 c.commit_statuses = statuses
862 c.commit_statuses = statuses
873
863
874 c.ancestor = None # TODO: add ancestor here
864 c.ancestor = None # TODO: add ancestor here
875 c.pull_request = pull_request_display_obj
865 c.pull_request = pull_request_display_obj
876 c.pull_request_latest = pull_request_latest
866 c.pull_request_latest = pull_request_latest
877 c.at_version = at_version
878
867
879 c.changes = None
868 c.changes = None
880 c.file_changes = None
869 c.file_changes = None
881
870
882 c.show_version_changes = 1 # control flag, not used yet
871 c.show_version_changes = 1 # control flag, not used yet
883
872
884 if at_version and c.show_version_changes:
873 if at_version and c.show_version_changes:
885 c.changes, c.file_changes = self._get_pr_version_changes(
874 c.changes, c.file_changes = self._get_pr_version_changes(
886 version, pull_request_latest)
875 version, pull_request_latest)
887
876
888 return render('/pullrequests/pullrequest_show.mako')
877 return render('/pullrequests/pullrequest_show.mako')
889
878
890 @LoginRequired()
879 @LoginRequired()
891 @NotAnonymous()
880 @NotAnonymous()
892 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
881 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
893 'repository.admin')
882 'repository.admin')
894 @auth.CSRFRequired()
883 @auth.CSRFRequired()
895 @jsonify
884 @jsonify
896 def comment(self, repo_name, pull_request_id):
885 def comment(self, repo_name, pull_request_id):
897 pull_request_id = safe_int(pull_request_id)
886 pull_request_id = safe_int(pull_request_id)
898 pull_request = PullRequest.get_or_404(pull_request_id)
887 pull_request = PullRequest.get_or_404(pull_request_id)
899 if pull_request.is_closed():
888 if pull_request.is_closed():
900 raise HTTPForbidden()
889 raise HTTPForbidden()
901
890
902 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
891 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
903 # as a changeset status, still we want to send it in one value.
892 # as a changeset status, still we want to send it in one value.
904 status = request.POST.get('changeset_status', None)
893 status = request.POST.get('changeset_status', None)
905 text = request.POST.get('text')
894 text = request.POST.get('text')
906 comment_type = request.POST.get('comment_type')
895 comment_type = request.POST.get('comment_type')
907 resolves_comment_id = request.POST.get('resolves_comment_id', None)
896 resolves_comment_id = request.POST.get('resolves_comment_id', None)
908
897
909 if status and '_closed' in status:
898 if status and '_closed' in status:
910 close_pr = True
899 close_pr = True
911 status = status.replace('_closed', '')
900 status = status.replace('_closed', '')
912 else:
901 else:
913 close_pr = False
902 close_pr = False
914
903
915 forced = (status == 'forced')
904 forced = (status == 'forced')
916 if forced:
905 if forced:
917 status = 'rejected'
906 status = 'rejected'
918
907
919 allowed_to_change_status = PullRequestModel().check_user_change_status(
908 allowed_to_change_status = PullRequestModel().check_user_change_status(
920 pull_request, c.rhodecode_user)
909 pull_request, c.rhodecode_user)
921
910
922 if status and allowed_to_change_status:
911 if status and allowed_to_change_status:
923 message = (_('Status change %(transition_icon)s %(status)s')
912 message = (_('Status change %(transition_icon)s %(status)s')
924 % {'transition_icon': '>',
913 % {'transition_icon': '>',
925 'status': ChangesetStatus.get_status_lbl(status)})
914 'status': ChangesetStatus.get_status_lbl(status)})
926 if close_pr:
915 if close_pr:
927 message = _('Closing with') + ' ' + message
916 message = _('Closing with') + ' ' + message
928 text = text or message
917 text = text or message
929 comm = CommentsModel().create(
918 comm = CommentsModel().create(
930 text=text,
919 text=text,
931 repo=c.rhodecode_db_repo.repo_id,
920 repo=c.rhodecode_db_repo.repo_id,
932 user=c.rhodecode_user.user_id,
921 user=c.rhodecode_user.user_id,
933 pull_request=pull_request_id,
922 pull_request=pull_request_id,
934 f_path=request.POST.get('f_path'),
923 f_path=request.POST.get('f_path'),
935 line_no=request.POST.get('line'),
924 line_no=request.POST.get('line'),
936 status_change=(ChangesetStatus.get_status_lbl(status)
925 status_change=(ChangesetStatus.get_status_lbl(status)
937 if status and allowed_to_change_status else None),
926 if status and allowed_to_change_status else None),
938 status_change_type=(status
927 status_change_type=(status
939 if status and allowed_to_change_status else None),
928 if status and allowed_to_change_status else None),
940 closing_pr=close_pr,
929 closing_pr=close_pr,
941 comment_type=comment_type,
930 comment_type=comment_type,
942 resolves_comment_id=resolves_comment_id
931 resolves_comment_id=resolves_comment_id
943 )
932 )
944
933
945 if allowed_to_change_status:
934 if allowed_to_change_status:
946 old_calculated_status = pull_request.calculated_review_status()
935 old_calculated_status = pull_request.calculated_review_status()
947 # get status if set !
936 # get status if set !
948 if status:
937 if status:
949 ChangesetStatusModel().set_status(
938 ChangesetStatusModel().set_status(
950 c.rhodecode_db_repo.repo_id,
939 c.rhodecode_db_repo.repo_id,
951 status,
940 status,
952 c.rhodecode_user.user_id,
941 c.rhodecode_user.user_id,
953 comm,
942 comm,
954 pull_request=pull_request_id
943 pull_request=pull_request_id
955 )
944 )
956
945
957 Session().flush()
946 Session().flush()
958 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
947 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
959 # we now calculate the status of pull request, and based on that
948 # we now calculate the status of pull request, and based on that
960 # calculation we set the commits status
949 # calculation we set the commits status
961 calculated_status = pull_request.calculated_review_status()
950 calculated_status = pull_request.calculated_review_status()
962 if old_calculated_status != calculated_status:
951 if old_calculated_status != calculated_status:
963 PullRequestModel()._trigger_pull_request_hook(
952 PullRequestModel()._trigger_pull_request_hook(
964 pull_request, c.rhodecode_user, 'review_status_change')
953 pull_request, c.rhodecode_user, 'review_status_change')
965
954
966 calculated_status_lbl = ChangesetStatus.get_status_lbl(
955 calculated_status_lbl = ChangesetStatus.get_status_lbl(
967 calculated_status)
956 calculated_status)
968
957
969 if close_pr:
958 if close_pr:
970 status_completed = (
959 status_completed = (
971 calculated_status in [ChangesetStatus.STATUS_APPROVED,
960 calculated_status in [ChangesetStatus.STATUS_APPROVED,
972 ChangesetStatus.STATUS_REJECTED])
961 ChangesetStatus.STATUS_REJECTED])
973 if forced or status_completed:
962 if forced or status_completed:
974 PullRequestModel().close_pull_request(
963 PullRequestModel().close_pull_request(
975 pull_request_id, c.rhodecode_user)
964 pull_request_id, c.rhodecode_user)
976 else:
965 else:
977 h.flash(_('Closing pull request on other statuses than '
966 h.flash(_('Closing pull request on other statuses than '
978 'rejected or approved is forbidden. '
967 'rejected or approved is forbidden. '
979 'Calculated status from all reviewers '
968 'Calculated status from all reviewers '
980 'is currently: %s') % calculated_status_lbl,
969 'is currently: %s') % calculated_status_lbl,
981 category='warning')
970 category='warning')
982
971
983 Session().commit()
972 Session().commit()
984
973
985 if not request.is_xhr:
974 if not request.is_xhr:
986 return redirect(h.url('pullrequest_show', repo_name=repo_name,
975 return redirect(h.url('pullrequest_show', repo_name=repo_name,
987 pull_request_id=pull_request_id))
976 pull_request_id=pull_request_id))
988
977
989 data = {
978 data = {
990 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
979 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
991 }
980 }
992 if comm:
981 if comm:
993 c.co = comm
982 c.co = comm
994 c.inline_comment = True if comm.line_no else False
983 c.inline_comment = True if comm.line_no else False
995 data.update(comm.get_dict())
984 data.update(comm.get_dict())
996 data.update({'rendered_text':
985 data.update({'rendered_text':
997 render('changeset/changeset_comment_block.mako')})
986 render('changeset/changeset_comment_block.mako')})
998
987
999 return data
988 return data
1000
989
1001 @LoginRequired()
990 @LoginRequired()
1002 @NotAnonymous()
991 @NotAnonymous()
1003 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
992 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1004 'repository.admin')
993 'repository.admin')
1005 @auth.CSRFRequired()
994 @auth.CSRFRequired()
1006 @jsonify
995 @jsonify
1007 def delete_comment(self, repo_name, comment_id):
996 def delete_comment(self, repo_name, comment_id):
1008 return self._delete_comment(comment_id)
997 return self._delete_comment(comment_id)
1009
998
1010 def _delete_comment(self, comment_id):
999 def _delete_comment(self, comment_id):
1011 comment_id = safe_int(comment_id)
1000 comment_id = safe_int(comment_id)
1012 co = ChangesetComment.get_or_404(comment_id)
1001 co = ChangesetComment.get_or_404(comment_id)
1013 if co.pull_request.is_closed():
1002 if co.pull_request.is_closed():
1014 # don't allow deleting comments on closed pull request
1003 # don't allow deleting comments on closed pull request
1015 raise HTTPForbidden()
1004 raise HTTPForbidden()
1016
1005
1017 is_owner = co.author.user_id == c.rhodecode_user.user_id
1006 is_owner = co.author.user_id == c.rhodecode_user.user_id
1018 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1007 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1019 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1008 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1020 old_calculated_status = co.pull_request.calculated_review_status()
1009 old_calculated_status = co.pull_request.calculated_review_status()
1021 CommentsModel().delete(comment=co)
1010 CommentsModel().delete(comment=co)
1022 Session().commit()
1011 Session().commit()
1023 calculated_status = co.pull_request.calculated_review_status()
1012 calculated_status = co.pull_request.calculated_review_status()
1024 if old_calculated_status != calculated_status:
1013 if old_calculated_status != calculated_status:
1025 PullRequestModel()._trigger_pull_request_hook(
1014 PullRequestModel()._trigger_pull_request_hook(
1026 co.pull_request, c.rhodecode_user, 'review_status_change')
1015 co.pull_request, c.rhodecode_user, 'review_status_change')
1027 return True
1016 return True
1028 else:
1017 else:
1029 raise HTTPForbidden()
1018 raise HTTPForbidden()
@@ -1,551 +1,600 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from datetime import datetime
29 from datetime import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry
32 from pyramid.threadlocal import get_current_registry
33 from sqlalchemy.sql.expression import null
33 from sqlalchemy.sql.expression import null
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs
36 from rhodecode.lib import helpers as h, diffs
37 from rhodecode.lib.channelstream import channelstream_request
37 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.utils import action_logger
38 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils2 import extract_mentioned_users
39 from rhodecode.lib.utils2 import extract_mentioned_users
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 ChangesetComment, User, Notification, PullRequest)
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
43 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.notification import NotificationModel
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import VcsSettingsModel
45 from rhodecode.model.settings import VcsSettingsModel
46 from rhodecode.model.notification import EmailNotificationModel
46 from rhodecode.model.notification import EmailNotificationModel
47 from rhodecode.model.validation_schema.schemas import comment_schema
47 from rhodecode.model.validation_schema.schemas import comment_schema
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class CommentsModel(BaseModel):
53 class CommentsModel(BaseModel):
54
54
55 cls = ChangesetComment
55 cls = ChangesetComment
56
56
57 DIFF_CONTEXT_BEFORE = 3
57 DIFF_CONTEXT_BEFORE = 3
58 DIFF_CONTEXT_AFTER = 3
58 DIFF_CONTEXT_AFTER = 3
59
59
60 def __get_commit_comment(self, changeset_comment):
60 def __get_commit_comment(self, changeset_comment):
61 return self._get_instance(ChangesetComment, changeset_comment)
61 return self._get_instance(ChangesetComment, changeset_comment)
62
62
63 def __get_pull_request(self, pull_request):
63 def __get_pull_request(self, pull_request):
64 return self._get_instance(PullRequest, pull_request)
64 return self._get_instance(PullRequest, pull_request)
65
65
66 def _extract_mentions(self, s):
66 def _extract_mentions(self, s):
67 user_objects = []
67 user_objects = []
68 for username in extract_mentioned_users(s):
68 for username in extract_mentioned_users(s):
69 user_obj = User.get_by_username(username, case_insensitive=True)
69 user_obj = User.get_by_username(username, case_insensitive=True)
70 if user_obj:
70 if user_obj:
71 user_objects.append(user_obj)
71 user_objects.append(user_obj)
72 return user_objects
72 return user_objects
73
73
74 def _get_renderer(self, global_renderer='rst'):
74 def _get_renderer(self, global_renderer='rst'):
75 try:
75 try:
76 # try reading from visual context
76 # try reading from visual context
77 from pylons import tmpl_context
77 from pylons import tmpl_context
78 global_renderer = tmpl_context.visual.default_renderer
78 global_renderer = tmpl_context.visual.default_renderer
79 except AttributeError:
79 except AttributeError:
80 log.debug("Renderer not set, falling back "
80 log.debug("Renderer not set, falling back "
81 "to default renderer '%s'", global_renderer)
81 "to default renderer '%s'", global_renderer)
82 except Exception:
82 except Exception:
83 log.error(traceback.format_exc())
83 log.error(traceback.format_exc())
84 return global_renderer
84 return global_renderer
85
85
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
87 # group by versions, and count until, and display objects
88
89 comment_groups = collections.defaultdict(list)
90 [comment_groups[
91 _co.pull_request_version_id].append(_co) for _co in comments]
92
93 def yield_comments(pos):
94 for co in comment_groups[pos]:
95 yield co
96
97 comment_versions = collections.defaultdict(
98 lambda: collections.defaultdict(list))
99 prev_prvid = -1
100 # fake last entry with None, to aggregate on "latest" version which
101 # doesn't have an pull_request_version_id
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
103 prvid = ver.pull_request_version_id
104 if prev_prvid == -1:
105 prev_prvid = prvid
106
107 for co in yield_comments(prvid):
108 comment_versions[prvid]['at'].append(co)
109
110 # save until
111 current = comment_versions[prvid]['at']
112 prev_until = comment_versions[prev_prvid]['until']
113 cur_until = prev_until + current
114 comment_versions[prvid]['until'].extend(cur_until)
115
116 # save outdated
117 if inline:
118 outdated = [x for x in cur_until
119 if x.outdated_at_version(show_version)]
120 else:
121 outdated = [x for x in cur_until
122 if x.older_than_version(show_version)]
123 display = [x for x in cur_until if x not in outdated]
124
125 comment_versions[prvid]['outdated'] = outdated
126 comment_versions[prvid]['display'] = display
127
128 prev_prvid = prvid
129
130 return comment_versions
131
86 def create(self, text, repo, user, commit_id=None, pull_request=None,
132 def create(self, text, repo, user, commit_id=None, pull_request=None,
87 f_path=None, line_no=None, status_change=None,
133 f_path=None, line_no=None, status_change=None,
88 status_change_type=None, comment_type=None,
134 status_change_type=None, comment_type=None,
89 resolves_comment_id=None, closing_pr=False, send_email=True,
135 resolves_comment_id=None, closing_pr=False, send_email=True,
90 renderer=None):
136 renderer=None):
91 """
137 """
92 Creates new comment for commit or pull request.
138 Creates new comment for commit or pull request.
93 IF status_change is not none this comment is associated with a
139 IF status_change is not none this comment is associated with a
94 status change of commit or commit associated with pull request
140 status change of commit or commit associated with pull request
95
141
96 :param text:
142 :param text:
97 :param repo:
143 :param repo:
98 :param user:
144 :param user:
99 :param commit_id:
145 :param commit_id:
100 :param pull_request:
146 :param pull_request:
101 :param f_path:
147 :param f_path:
102 :param line_no:
148 :param line_no:
103 :param status_change: Label for status change
149 :param status_change: Label for status change
104 :param comment_type: Type of comment
150 :param comment_type: Type of comment
105 :param status_change_type: type of status change
151 :param status_change_type: type of status change
106 :param closing_pr:
152 :param closing_pr:
107 :param send_email:
153 :param send_email:
108 :param renderer: pick renderer for this comment
154 :param renderer: pick renderer for this comment
109 """
155 """
110 if not text:
156 if not text:
111 log.warning('Missing text for comment, skipping...')
157 log.warning('Missing text for comment, skipping...')
112 return
158 return
113
159
114 if not renderer:
160 if not renderer:
115 renderer = self._get_renderer()
161 renderer = self._get_renderer()
116
162
117 repo = self._get_repo(repo)
163 repo = self._get_repo(repo)
118 user = self._get_user(user)
164 user = self._get_user(user)
119
165
120 schema = comment_schema.CommentSchema()
166 schema = comment_schema.CommentSchema()
121 validated_kwargs = schema.deserialize(dict(
167 validated_kwargs = schema.deserialize(dict(
122 comment_body=text,
168 comment_body=text,
123 comment_type=comment_type,
169 comment_type=comment_type,
124 comment_file=f_path,
170 comment_file=f_path,
125 comment_line=line_no,
171 comment_line=line_no,
126 renderer_type=renderer,
172 renderer_type=renderer,
127 status_change=status_change_type,
173 status_change=status_change_type,
128 resolves_comment_id=resolves_comment_id,
174 resolves_comment_id=resolves_comment_id,
129 repo=repo.repo_id,
175 repo=repo.repo_id,
130 user=user.user_id,
176 user=user.user_id,
131 ))
177 ))
132
178
133 comment = ChangesetComment()
179 comment = ChangesetComment()
134 comment.renderer = validated_kwargs['renderer_type']
180 comment.renderer = validated_kwargs['renderer_type']
135 comment.text = validated_kwargs['comment_body']
181 comment.text = validated_kwargs['comment_body']
136 comment.f_path = validated_kwargs['comment_file']
182 comment.f_path = validated_kwargs['comment_file']
137 comment.line_no = validated_kwargs['comment_line']
183 comment.line_no = validated_kwargs['comment_line']
138 comment.comment_type = validated_kwargs['comment_type']
184 comment.comment_type = validated_kwargs['comment_type']
139
185
140 comment.repo = repo
186 comment.repo = repo
141 comment.author = user
187 comment.author = user
142 comment.resolved_comment = self.__get_commit_comment(
188 comment.resolved_comment = self.__get_commit_comment(
143 validated_kwargs['resolves_comment_id'])
189 validated_kwargs['resolves_comment_id'])
144
190
145 pull_request_id = pull_request
191 pull_request_id = pull_request
146
192
147 commit_obj = None
193 commit_obj = None
148 pull_request_obj = None
194 pull_request_obj = None
149
195
150 if commit_id:
196 if commit_id:
151 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
197 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
152 # do a lookup, so we don't pass something bad here
198 # do a lookup, so we don't pass something bad here
153 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
199 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
154 comment.revision = commit_obj.raw_id
200 comment.revision = commit_obj.raw_id
155
201
156 elif pull_request_id:
202 elif pull_request_id:
157 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
203 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
158 pull_request_obj = self.__get_pull_request(pull_request_id)
204 pull_request_obj = self.__get_pull_request(pull_request_id)
159 comment.pull_request = pull_request_obj
205 comment.pull_request = pull_request_obj
160 else:
206 else:
161 raise Exception('Please specify commit or pull_request_id')
207 raise Exception('Please specify commit or pull_request_id')
162
208
163 Session().add(comment)
209 Session().add(comment)
164 Session().flush()
210 Session().flush()
165 kwargs = {
211 kwargs = {
166 'user': user,
212 'user': user,
167 'renderer_type': renderer,
213 'renderer_type': renderer,
168 'repo_name': repo.repo_name,
214 'repo_name': repo.repo_name,
169 'status_change': status_change,
215 'status_change': status_change,
170 'status_change_type': status_change_type,
216 'status_change_type': status_change_type,
171 'comment_body': text,
217 'comment_body': text,
172 'comment_file': f_path,
218 'comment_file': f_path,
173 'comment_line': line_no,
219 'comment_line': line_no,
174 }
220 }
175
221
176 if commit_obj:
222 if commit_obj:
177 recipients = ChangesetComment.get_users(
223 recipients = ChangesetComment.get_users(
178 revision=commit_obj.raw_id)
224 revision=commit_obj.raw_id)
179 # add commit author if it's in RhodeCode system
225 # add commit author if it's in RhodeCode system
180 cs_author = User.get_from_cs_author(commit_obj.author)
226 cs_author = User.get_from_cs_author(commit_obj.author)
181 if not cs_author:
227 if not cs_author:
182 # use repo owner if we cannot extract the author correctly
228 # use repo owner if we cannot extract the author correctly
183 cs_author = repo.user
229 cs_author = repo.user
184 recipients += [cs_author]
230 recipients += [cs_author]
185
231
186 commit_comment_url = self.get_url(comment)
232 commit_comment_url = self.get_url(comment)
187
233
188 target_repo_url = h.link_to(
234 target_repo_url = h.link_to(
189 repo.repo_name,
235 repo.repo_name,
190 h.url('summary_home',
236 h.url('summary_home',
191 repo_name=repo.repo_name, qualified=True))
237 repo_name=repo.repo_name, qualified=True))
192
238
193 # commit specifics
239 # commit specifics
194 kwargs.update({
240 kwargs.update({
195 'commit': commit_obj,
241 'commit': commit_obj,
196 'commit_message': commit_obj.message,
242 'commit_message': commit_obj.message,
197 'commit_target_repo': target_repo_url,
243 'commit_target_repo': target_repo_url,
198 'commit_comment_url': commit_comment_url,
244 'commit_comment_url': commit_comment_url,
199 })
245 })
200
246
201 elif pull_request_obj:
247 elif pull_request_obj:
202 # get the current participants of this pull request
248 # get the current participants of this pull request
203 recipients = ChangesetComment.get_users(
249 recipients = ChangesetComment.get_users(
204 pull_request_id=pull_request_obj.pull_request_id)
250 pull_request_id=pull_request_obj.pull_request_id)
205 # add pull request author
251 # add pull request author
206 recipients += [pull_request_obj.author]
252 recipients += [pull_request_obj.author]
207
253
208 # add the reviewers to notification
254 # add the reviewers to notification
209 recipients += [x.user for x in pull_request_obj.reviewers]
255 recipients += [x.user for x in pull_request_obj.reviewers]
210
256
211 pr_target_repo = pull_request_obj.target_repo
257 pr_target_repo = pull_request_obj.target_repo
212 pr_source_repo = pull_request_obj.source_repo
258 pr_source_repo = pull_request_obj.source_repo
213
259
214 pr_comment_url = h.url(
260 pr_comment_url = h.url(
215 'pullrequest_show',
261 'pullrequest_show',
216 repo_name=pr_target_repo.repo_name,
262 repo_name=pr_target_repo.repo_name,
217 pull_request_id=pull_request_obj.pull_request_id,
263 pull_request_id=pull_request_obj.pull_request_id,
218 anchor='comment-%s' % comment.comment_id,
264 anchor='comment-%s' % comment.comment_id,
219 qualified=True,)
265 qualified=True,)
220
266
221 # set some variables for email notification
267 # set some variables for email notification
222 pr_target_repo_url = h.url(
268 pr_target_repo_url = h.url(
223 'summary_home', repo_name=pr_target_repo.repo_name,
269 'summary_home', repo_name=pr_target_repo.repo_name,
224 qualified=True)
270 qualified=True)
225
271
226 pr_source_repo_url = h.url(
272 pr_source_repo_url = h.url(
227 'summary_home', repo_name=pr_source_repo.repo_name,
273 'summary_home', repo_name=pr_source_repo.repo_name,
228 qualified=True)
274 qualified=True)
229
275
230 # pull request specifics
276 # pull request specifics
231 kwargs.update({
277 kwargs.update({
232 'pull_request': pull_request_obj,
278 'pull_request': pull_request_obj,
233 'pr_id': pull_request_obj.pull_request_id,
279 'pr_id': pull_request_obj.pull_request_id,
234 'pr_target_repo': pr_target_repo,
280 'pr_target_repo': pr_target_repo,
235 'pr_target_repo_url': pr_target_repo_url,
281 'pr_target_repo_url': pr_target_repo_url,
236 'pr_source_repo': pr_source_repo,
282 'pr_source_repo': pr_source_repo,
237 'pr_source_repo_url': pr_source_repo_url,
283 'pr_source_repo_url': pr_source_repo_url,
238 'pr_comment_url': pr_comment_url,
284 'pr_comment_url': pr_comment_url,
239 'pr_closing': closing_pr,
285 'pr_closing': closing_pr,
240 })
286 })
241 if send_email:
287 if send_email:
242 # pre-generate the subject for notification itself
288 # pre-generate the subject for notification itself
243 (subject,
289 (subject,
244 _h, _e, # we don't care about those
290 _h, _e, # we don't care about those
245 body_plaintext) = EmailNotificationModel().render_email(
291 body_plaintext) = EmailNotificationModel().render_email(
246 notification_type, **kwargs)
292 notification_type, **kwargs)
247
293
248 mention_recipients = set(
294 mention_recipients = set(
249 self._extract_mentions(text)).difference(recipients)
295 self._extract_mentions(text)).difference(recipients)
250
296
251 # create notification objects, and emails
297 # create notification objects, and emails
252 NotificationModel().create(
298 NotificationModel().create(
253 created_by=user,
299 created_by=user,
254 notification_subject=subject,
300 notification_subject=subject,
255 notification_body=body_plaintext,
301 notification_body=body_plaintext,
256 notification_type=notification_type,
302 notification_type=notification_type,
257 recipients=recipients,
303 recipients=recipients,
258 mention_recipients=mention_recipients,
304 mention_recipients=mention_recipients,
259 email_kwargs=kwargs,
305 email_kwargs=kwargs,
260 )
306 )
261
307
262 action = (
308 action = (
263 'user_commented_pull_request:{}'.format(
309 'user_commented_pull_request:{}'.format(
264 comment.pull_request.pull_request_id)
310 comment.pull_request.pull_request_id)
265 if comment.pull_request
311 if comment.pull_request
266 else 'user_commented_revision:{}'.format(comment.revision)
312 else 'user_commented_revision:{}'.format(comment.revision)
267 )
313 )
268 action_logger(user, action, comment.repo)
314 action_logger(user, action, comment.repo)
269
315
270 registry = get_current_registry()
316 registry = get_current_registry()
271 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
317 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
272 channelstream_config = rhodecode_plugins.get('channelstream', {})
318 channelstream_config = rhodecode_plugins.get('channelstream', {})
273 msg_url = ''
319 msg_url = ''
274 if commit_obj:
320 if commit_obj:
275 msg_url = commit_comment_url
321 msg_url = commit_comment_url
276 repo_name = repo.repo_name
322 repo_name = repo.repo_name
277 elif pull_request_obj:
323 elif pull_request_obj:
278 msg_url = pr_comment_url
324 msg_url = pr_comment_url
279 repo_name = pr_target_repo.repo_name
325 repo_name = pr_target_repo.repo_name
280
326
281 if channelstream_config.get('enabled'):
327 if channelstream_config.get('enabled'):
282 message = '<strong>{}</strong> {} - ' \
328 message = '<strong>{}</strong> {} - ' \
283 '<a onclick="window.location=\'{}\';' \
329 '<a onclick="window.location=\'{}\';' \
284 'window.location.reload()">' \
330 'window.location.reload()">' \
285 '<strong>{}</strong></a>'
331 '<strong>{}</strong></a>'
286 message = message.format(
332 message = message.format(
287 user.username, _('made a comment'), msg_url,
333 user.username, _('made a comment'), msg_url,
288 _('Show it now'))
334 _('Show it now'))
289 channel = '/repo${}$/pr/{}'.format(
335 channel = '/repo${}$/pr/{}'.format(
290 repo_name,
336 repo_name,
291 pull_request_id
337 pull_request_id
292 )
338 )
293 payload = {
339 payload = {
294 'type': 'message',
340 'type': 'message',
295 'timestamp': datetime.utcnow(),
341 'timestamp': datetime.utcnow(),
296 'user': 'system',
342 'user': 'system',
297 'exclude_users': [user.username],
343 'exclude_users': [user.username],
298 'channel': channel,
344 'channel': channel,
299 'message': {
345 'message': {
300 'message': message,
346 'message': message,
301 'level': 'info',
347 'level': 'info',
302 'topic': '/notifications'
348 'topic': '/notifications'
303 }
349 }
304 }
350 }
305 channelstream_request(channelstream_config, [payload],
351 channelstream_request(channelstream_config, [payload],
306 '/message', raise_exc=False)
352 '/message', raise_exc=False)
307
353
308 return comment
354 return comment
309
355
310 def delete(self, comment):
356 def delete(self, comment):
311 """
357 """
312 Deletes given comment
358 Deletes given comment
313
359
314 :param comment_id:
360 :param comment_id:
315 """
361 """
316 comment = self.__get_commit_comment(comment)
362 comment = self.__get_commit_comment(comment)
317 Session().delete(comment)
363 Session().delete(comment)
318
364
319 return comment
365 return comment
320
366
321 def get_all_comments(self, repo_id, revision=None, pull_request=None):
367 def get_all_comments(self, repo_id, revision=None, pull_request=None):
322 q = ChangesetComment.query()\
368 q = ChangesetComment.query()\
323 .filter(ChangesetComment.repo_id == repo_id)
369 .filter(ChangesetComment.repo_id == repo_id)
324 if revision:
370 if revision:
325 q = q.filter(ChangesetComment.revision == revision)
371 q = q.filter(ChangesetComment.revision == revision)
326 elif pull_request:
372 elif pull_request:
327 pull_request = self.__get_pull_request(pull_request)
373 pull_request = self.__get_pull_request(pull_request)
328 q = q.filter(ChangesetComment.pull_request == pull_request)
374 q = q.filter(ChangesetComment.pull_request == pull_request)
329 else:
375 else:
330 raise Exception('Please specify commit or pull_request')
376 raise Exception('Please specify commit or pull_request')
331 q = q.order_by(ChangesetComment.created_on)
377 q = q.order_by(ChangesetComment.created_on)
332 return q.all()
378 return q.all()
333
379
334 def get_url(self, comment):
380 def get_url(self, comment):
335 comment = self.__get_commit_comment(comment)
381 comment = self.__get_commit_comment(comment)
336 if comment.pull_request:
382 if comment.pull_request:
337 return h.url(
383 return h.url(
338 'pullrequest_show',
384 'pullrequest_show',
339 repo_name=comment.pull_request.target_repo.repo_name,
385 repo_name=comment.pull_request.target_repo.repo_name,
340 pull_request_id=comment.pull_request.pull_request_id,
386 pull_request_id=comment.pull_request.pull_request_id,
341 anchor='comment-%s' % comment.comment_id,
387 anchor='comment-%s' % comment.comment_id,
342 qualified=True,)
388 qualified=True,)
343 else:
389 else:
344 return h.url(
390 return h.url(
345 'changeset_home',
391 'changeset_home',
346 repo_name=comment.repo.repo_name,
392 repo_name=comment.repo.repo_name,
347 revision=comment.revision,
393 revision=comment.revision,
348 anchor='comment-%s' % comment.comment_id,
394 anchor='comment-%s' % comment.comment_id,
349 qualified=True,)
395 qualified=True,)
350
396
351 def get_comments(self, repo_id, revision=None, pull_request=None):
397 def get_comments(self, repo_id, revision=None, pull_request=None):
352 """
398 """
353 Gets main comments based on revision or pull_request_id
399 Gets main comments based on revision or pull_request_id
354
400
355 :param repo_id:
401 :param repo_id:
356 :param revision:
402 :param revision:
357 :param pull_request:
403 :param pull_request:
358 """
404 """
359
405
360 q = ChangesetComment.query()\
406 q = ChangesetComment.query()\
361 .filter(ChangesetComment.repo_id == repo_id)\
407 .filter(ChangesetComment.repo_id == repo_id)\
362 .filter(ChangesetComment.line_no == None)\
408 .filter(ChangesetComment.line_no == None)\
363 .filter(ChangesetComment.f_path == None)
409 .filter(ChangesetComment.f_path == None)
364 if revision:
410 if revision:
365 q = q.filter(ChangesetComment.revision == revision)
411 q = q.filter(ChangesetComment.revision == revision)
366 elif pull_request:
412 elif pull_request:
367 pull_request = self.__get_pull_request(pull_request)
413 pull_request = self.__get_pull_request(pull_request)
368 q = q.filter(ChangesetComment.pull_request == pull_request)
414 q = q.filter(ChangesetComment.pull_request == pull_request)
369 else:
415 else:
370 raise Exception('Please specify commit or pull_request')
416 raise Exception('Please specify commit or pull_request')
371 q = q.order_by(ChangesetComment.created_on)
417 q = q.order_by(ChangesetComment.created_on)
372 return q.all()
418 return q.all()
373
419
374 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
420 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
375 q = self._get_inline_comments_query(repo_id, revision, pull_request)
421 q = self._get_inline_comments_query(repo_id, revision, pull_request)
376 return self._group_comments_by_path_and_line_number(q)
422 return self._group_comments_by_path_and_line_number(q)
377
423
378 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
424 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
379 version=None, include_aggregates=False):
425 version=None):
380 version_aggregates = collections.defaultdict(list)
381 inline_cnt = 0
426 inline_cnt = 0
382 for fname, per_line_comments in inline_comments.iteritems():
427 for fname, per_line_comments in inline_comments.iteritems():
383 for lno, comments in per_line_comments.iteritems():
428 for lno, comments in per_line_comments.iteritems():
384 for comm in comments:
429 for comm in comments:
385 version_aggregates[comm.pull_request_version_id].append(comm)
386 if not comm.outdated_at_version(version) and skip_outdated:
430 if not comm.outdated_at_version(version) and skip_outdated:
387 inline_cnt += 1
431 inline_cnt += 1
388
432
389 if include_aggregates:
390 return inline_cnt, version_aggregates
391 return inline_cnt
433 return inline_cnt
392
434
393 def get_outdated_comments(self, repo_id, pull_request):
435 def get_outdated_comments(self, repo_id, pull_request):
394 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
436 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
395 # of a pull request.
437 # of a pull request.
396 q = self._all_inline_comments_of_pull_request(pull_request)
438 q = self._all_inline_comments_of_pull_request(pull_request)
397 q = q.filter(
439 q = q.filter(
398 ChangesetComment.display_state ==
440 ChangesetComment.display_state ==
399 ChangesetComment.COMMENT_OUTDATED
441 ChangesetComment.COMMENT_OUTDATED
400 ).order_by(ChangesetComment.comment_id.asc())
442 ).order_by(ChangesetComment.comment_id.asc())
401
443
402 return self._group_comments_by_path_and_line_number(q)
444 return self._group_comments_by_path_and_line_number(q)
403
445
404 def _get_inline_comments_query(self, repo_id, revision, pull_request):
446 def _get_inline_comments_query(self, repo_id, revision, pull_request):
405 # TODO: johbo: Split this into two methods: One for PR and one for
447 # TODO: johbo: Split this into two methods: One for PR and one for
406 # commit.
448 # commit.
407 if revision:
449 if revision:
408 q = Session().query(ChangesetComment).filter(
450 q = Session().query(ChangesetComment).filter(
409 ChangesetComment.repo_id == repo_id,
451 ChangesetComment.repo_id == repo_id,
410 ChangesetComment.line_no != null(),
452 ChangesetComment.line_no != null(),
411 ChangesetComment.f_path != null(),
453 ChangesetComment.f_path != null(),
412 ChangesetComment.revision == revision)
454 ChangesetComment.revision == revision)
413
455
414 elif pull_request:
456 elif pull_request:
415 pull_request = self.__get_pull_request(pull_request)
457 pull_request = self.__get_pull_request(pull_request)
416 if not CommentsModel.use_outdated_comments(pull_request):
458 if not CommentsModel.use_outdated_comments(pull_request):
417 q = self._visible_inline_comments_of_pull_request(pull_request)
459 q = self._visible_inline_comments_of_pull_request(pull_request)
418 else:
460 else:
419 q = self._all_inline_comments_of_pull_request(pull_request)
461 q = self._all_inline_comments_of_pull_request(pull_request)
420
462
421 else:
463 else:
422 raise Exception('Please specify commit or pull_request_id')
464 raise Exception('Please specify commit or pull_request_id')
423 q = q.order_by(ChangesetComment.comment_id.asc())
465 q = q.order_by(ChangesetComment.comment_id.asc())
424 return q
466 return q
425
467
426 def _group_comments_by_path_and_line_number(self, q):
468 def _group_comments_by_path_and_line_number(self, q):
427 comments = q.all()
469 comments = q.all()
428 paths = collections.defaultdict(lambda: collections.defaultdict(list))
470 paths = collections.defaultdict(lambda: collections.defaultdict(list))
429 for co in comments:
471 for co in comments:
430 paths[co.f_path][co.line_no].append(co)
472 paths[co.f_path][co.line_no].append(co)
431 return paths
473 return paths
432
474
433 @classmethod
475 @classmethod
434 def needed_extra_diff_context(cls):
476 def needed_extra_diff_context(cls):
435 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
477 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
436
478
437 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
479 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
438 if not CommentsModel.use_outdated_comments(pull_request):
480 if not CommentsModel.use_outdated_comments(pull_request):
439 return
481 return
440
482
441 comments = self._visible_inline_comments_of_pull_request(pull_request)
483 comments = self._visible_inline_comments_of_pull_request(pull_request)
442 comments_to_outdate = comments.all()
484 comments_to_outdate = comments.all()
443
485
444 for comment in comments_to_outdate:
486 for comment in comments_to_outdate:
445 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
487 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
446
488
447 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
489 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
448 diff_line = _parse_comment_line_number(comment.line_no)
490 diff_line = _parse_comment_line_number(comment.line_no)
449
491
450 try:
492 try:
451 old_context = old_diff_proc.get_context_of_line(
493 old_context = old_diff_proc.get_context_of_line(
452 path=comment.f_path, diff_line=diff_line)
494 path=comment.f_path, diff_line=diff_line)
453 new_context = new_diff_proc.get_context_of_line(
495 new_context = new_diff_proc.get_context_of_line(
454 path=comment.f_path, diff_line=diff_line)
496 path=comment.f_path, diff_line=diff_line)
455 except (diffs.LineNotInDiffException,
497 except (diffs.LineNotInDiffException,
456 diffs.FileNotInDiffException):
498 diffs.FileNotInDiffException):
457 comment.display_state = ChangesetComment.COMMENT_OUTDATED
499 comment.display_state = ChangesetComment.COMMENT_OUTDATED
458 return
500 return
459
501
460 if old_context == new_context:
502 if old_context == new_context:
461 return
503 return
462
504
463 if self._should_relocate_diff_line(diff_line):
505 if self._should_relocate_diff_line(diff_line):
464 new_diff_lines = new_diff_proc.find_context(
506 new_diff_lines = new_diff_proc.find_context(
465 path=comment.f_path, context=old_context,
507 path=comment.f_path, context=old_context,
466 offset=self.DIFF_CONTEXT_BEFORE)
508 offset=self.DIFF_CONTEXT_BEFORE)
467 if not new_diff_lines:
509 if not new_diff_lines:
468 comment.display_state = ChangesetComment.COMMENT_OUTDATED
510 comment.display_state = ChangesetComment.COMMENT_OUTDATED
469 else:
511 else:
470 new_diff_line = self._choose_closest_diff_line(
512 new_diff_line = self._choose_closest_diff_line(
471 diff_line, new_diff_lines)
513 diff_line, new_diff_lines)
472 comment.line_no = _diff_to_comment_line_number(new_diff_line)
514 comment.line_no = _diff_to_comment_line_number(new_diff_line)
473 else:
515 else:
474 comment.display_state = ChangesetComment.COMMENT_OUTDATED
516 comment.display_state = ChangesetComment.COMMENT_OUTDATED
475
517
476 def _should_relocate_diff_line(self, diff_line):
518 def _should_relocate_diff_line(self, diff_line):
477 """
519 """
478 Checks if relocation shall be tried for the given `diff_line`.
520 Checks if relocation shall be tried for the given `diff_line`.
479
521
480 If a comment points into the first lines, then we can have a situation
522 If a comment points into the first lines, then we can have a situation
481 that after an update another line has been added on top. In this case
523 that after an update another line has been added on top. In this case
482 we would find the context still and move the comment around. This
524 we would find the context still and move the comment around. This
483 would be wrong.
525 would be wrong.
484 """
526 """
485 should_relocate = (
527 should_relocate = (
486 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
528 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
487 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
529 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
488 return should_relocate
530 return should_relocate
489
531
490 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
532 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
491 candidate = new_diff_lines[0]
533 candidate = new_diff_lines[0]
492 best_delta = _diff_line_delta(diff_line, candidate)
534 best_delta = _diff_line_delta(diff_line, candidate)
493 for new_diff_line in new_diff_lines[1:]:
535 for new_diff_line in new_diff_lines[1:]:
494 delta = _diff_line_delta(diff_line, new_diff_line)
536 delta = _diff_line_delta(diff_line, new_diff_line)
495 if delta < best_delta:
537 if delta < best_delta:
496 candidate = new_diff_line
538 candidate = new_diff_line
497 best_delta = delta
539 best_delta = delta
498 return candidate
540 return candidate
499
541
500 def _visible_inline_comments_of_pull_request(self, pull_request):
542 def _visible_inline_comments_of_pull_request(self, pull_request):
501 comments = self._all_inline_comments_of_pull_request(pull_request)
543 comments = self._all_inline_comments_of_pull_request(pull_request)
502 comments = comments.filter(
544 comments = comments.filter(
503 coalesce(ChangesetComment.display_state, '') !=
545 coalesce(ChangesetComment.display_state, '') !=
504 ChangesetComment.COMMENT_OUTDATED)
546 ChangesetComment.COMMENT_OUTDATED)
505 return comments
547 return comments
506
548
507 def _all_inline_comments_of_pull_request(self, pull_request):
549 def _all_inline_comments_of_pull_request(self, pull_request):
508 comments = Session().query(ChangesetComment)\
550 comments = Session().query(ChangesetComment)\
509 .filter(ChangesetComment.line_no != None)\
551 .filter(ChangesetComment.line_no != None)\
510 .filter(ChangesetComment.f_path != None)\
552 .filter(ChangesetComment.f_path != None)\
511 .filter(ChangesetComment.pull_request == pull_request)
553 .filter(ChangesetComment.pull_request == pull_request)
512 return comments
554 return comments
513
555
556 def _all_general_comments_of_pull_request(self, pull_request):
557 comments = Session().query(ChangesetComment)\
558 .filter(ChangesetComment.line_no == None)\
559 .filter(ChangesetComment.f_path == None)\
560 .filter(ChangesetComment.pull_request == pull_request)
561 return comments
562
514 @staticmethod
563 @staticmethod
515 def use_outdated_comments(pull_request):
564 def use_outdated_comments(pull_request):
516 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
565 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
517 settings = settings_model.get_general_settings()
566 settings = settings_model.get_general_settings()
518 return settings.get('rhodecode_use_outdated_comments', False)
567 return settings.get('rhodecode_use_outdated_comments', False)
519
568
520
569
521 def _parse_comment_line_number(line_no):
570 def _parse_comment_line_number(line_no):
522 """
571 """
523 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
572 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
524 """
573 """
525 old_line = None
574 old_line = None
526 new_line = None
575 new_line = None
527 if line_no.startswith('o'):
576 if line_no.startswith('o'):
528 old_line = int(line_no[1:])
577 old_line = int(line_no[1:])
529 elif line_no.startswith('n'):
578 elif line_no.startswith('n'):
530 new_line = int(line_no[1:])
579 new_line = int(line_no[1:])
531 else:
580 else:
532 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
581 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
533 return diffs.DiffLineNumber(old_line, new_line)
582 return diffs.DiffLineNumber(old_line, new_line)
534
583
535
584
536 def _diff_to_comment_line_number(diff_line):
585 def _diff_to_comment_line_number(diff_line):
537 if diff_line.new is not None:
586 if diff_line.new is not None:
538 return u'n{}'.format(diff_line.new)
587 return u'n{}'.format(diff_line.new)
539 elif diff_line.old is not None:
588 elif diff_line.old is not None:
540 return u'o{}'.format(diff_line.old)
589 return u'o{}'.format(diff_line.old)
541 return u''
590 return u''
542
591
543
592
544 def _diff_line_delta(a, b):
593 def _diff_line_delta(a, b):
545 if None not in (a.new, b.new):
594 if None not in (a.new, b.new):
546 return abs(a.new - b.new)
595 return abs(a.new - b.new)
547 elif None not in (a.old, b.old):
596 elif None not in (a.old, b.old):
548 return abs(a.old - b.old)
597 return abs(a.old - b.old)
549 else:
598 else:
550 raise ValueError(
599 raise ValueError(
551 "Cannot compute delta between {} and {}".format(a, b))
600 "Cannot compute delta between {} and {}".format(a, b))
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,547 +1,547 b''
1 // comments.less
1 // comments.less
2 // For use in RhodeCode applications;
2 // For use in RhodeCode applications;
3 // see style guide documentation for guidelines.
3 // see style guide documentation for guidelines.
4
4
5
5
6 // Comments
6 // Comments
7 .comments {
7 .comments {
8 width: 100%;
8 width: 100%;
9 }
9 }
10
10
11 tr.inline-comments div {
11 tr.inline-comments div {
12 max-width: 100%;
12 max-width: 100%;
13
13
14 p {
14 p {
15 white-space: normal;
15 white-space: normal;
16 }
16 }
17
17
18 code, pre, .code, dd {
18 code, pre, .code, dd {
19 overflow-x: auto;
19 overflow-x: auto;
20 width: 1062px;
20 width: 1062px;
21 }
21 }
22
22
23 dd {
23 dd {
24 width: auto;
24 width: auto;
25 }
25 }
26 }
26 }
27
27
28 #injected_page_comments {
28 #injected_page_comments {
29 .comment-previous-link,
29 .comment-previous-link,
30 .comment-next-link,
30 .comment-next-link,
31 .comment-links-divider {
31 .comment-links-divider {
32 display: none;
32 display: none;
33 }
33 }
34 }
34 }
35
35
36 .add-comment {
36 .add-comment {
37 margin-bottom: 10px;
37 margin-bottom: 10px;
38 }
38 }
39 .hide-comment-button .add-comment {
39 .hide-comment-button .add-comment {
40 display: none;
40 display: none;
41 }
41 }
42
42
43 .comment-bubble {
43 .comment-bubble {
44 color: @grey4;
44 color: @grey4;
45 margin-top: 4px;
45 margin-top: 4px;
46 margin-right: 30px;
46 margin-right: 30px;
47 visibility: hidden;
47 visibility: hidden;
48 }
48 }
49
49
50 .comment-label {
50 .comment-label {
51 float: left;
51 float: left;
52
52
53 padding: 0.4em 0.4em;
53 padding: 0.4em 0.4em;
54 margin: 2px 5px 0px -10px;
54 margin: 3px 5px 0px -10px;
55 display: inline-block;
55 display: inline-block;
56 min-height: 0;
56 min-height: 0;
57
57
58 text-align: center;
58 text-align: center;
59 font-size: 10px;
59 font-size: 10px;
60 line-height: .8em;
60 line-height: .8em;
61
61
62 font-family: @text-italic;
62 font-family: @text-italic;
63 background: #fff none;
63 background: #fff none;
64 color: @grey4;
64 color: @grey4;
65 border: 1px solid @grey4;
65 border: 1px solid @grey4;
66 white-space: nowrap;
66 white-space: nowrap;
67
67
68 text-transform: uppercase;
68 text-transform: uppercase;
69 min-width: 40px;
69 min-width: 40px;
70
70
71 &.todo {
71 &.todo {
72 color: @color5;
72 color: @color5;
73 font-family: @text-bold-italic;
73 font-family: @text-bold-italic;
74 }
74 }
75
75
76 .resolve {
76 .resolve {
77 cursor: pointer;
77 cursor: pointer;
78 text-decoration: underline;
78 text-decoration: underline;
79 }
79 }
80
80
81 .resolved {
81 .resolved {
82 text-decoration: line-through;
82 text-decoration: line-through;
83 color: @color1;
83 color: @color1;
84 }
84 }
85 .resolved a {
85 .resolved a {
86 text-decoration: line-through;
86 text-decoration: line-through;
87 color: @color1;
87 color: @color1;
88 }
88 }
89 .resolve-text {
89 .resolve-text {
90 color: @color1;
90 color: @color1;
91 margin: 2px 8px;
91 margin: 2px 8px;
92 font-family: @text-italic;
92 font-family: @text-italic;
93 }
93 }
94
94
95 }
95 }
96
96
97
97
98 .comment {
98 .comment {
99
99
100 &.comment-general {
100 &.comment-general {
101 border: 1px solid @grey5;
101 border: 1px solid @grey5;
102 padding: 5px 5px 5px 5px;
102 padding: 5px 5px 5px 5px;
103 }
103 }
104
104
105 margin: @padding 0;
105 margin: @padding 0;
106 padding: 4px 0 0 0;
106 padding: 4px 0 0 0;
107 line-height: 1em;
107 line-height: 1em;
108
108
109 .rc-user {
109 .rc-user {
110 min-width: 0;
110 min-width: 0;
111 margin: 0px .5em 0 0;
111 margin: 0px .5em 0 0;
112
112
113 .user {
113 .user {
114 display: inline;
114 display: inline;
115 }
115 }
116 }
116 }
117
117
118 .meta {
118 .meta {
119 position: relative;
119 position: relative;
120 width: 100%;
120 width: 100%;
121 border-bottom: 1px solid @grey5;
121 border-bottom: 1px solid @grey5;
122 margin: -5px 0px;
122 margin: -5px 0px;
123 line-height: 24px;
123 line-height: 24px;
124
124
125 &:hover .permalink {
125 &:hover .permalink {
126 visibility: visible;
126 visibility: visible;
127 color: @rcblue;
127 color: @rcblue;
128 }
128 }
129 }
129 }
130
130
131 .author,
131 .author,
132 .date {
132 .date {
133 display: inline;
133 display: inline;
134
134
135 &:after {
135 &:after {
136 content: ' | ';
136 content: ' | ';
137 color: @grey5;
137 color: @grey5;
138 }
138 }
139 }
139 }
140
140
141 .author-general img {
141 .author-general img {
142 top: 3px;
142 top: 3px;
143 }
143 }
144 .author-inline img {
144 .author-inline img {
145 top: 3px;
145 top: 3px;
146 }
146 }
147
147
148 .status-change,
148 .status-change,
149 .permalink,
149 .permalink,
150 .changeset-status-lbl {
150 .changeset-status-lbl {
151 display: inline;
151 display: inline;
152 }
152 }
153
153
154 .permalink {
154 .permalink {
155 visibility: hidden;
155 visibility: hidden;
156 }
156 }
157
157
158 .comment-links-divider {
158 .comment-links-divider {
159 display: inline;
159 display: inline;
160 }
160 }
161
161
162 .comment-links-block {
162 .comment-links-block {
163 float:right;
163 float:right;
164 text-align: right;
164 text-align: right;
165 min-width: 85px;
165 min-width: 85px;
166
166
167 [class^="icon-"]:before,
167 [class^="icon-"]:before,
168 [class*=" icon-"]:before {
168 [class*=" icon-"]:before {
169 margin-left: 0;
169 margin-left: 0;
170 margin-right: 0;
170 margin-right: 0;
171 }
171 }
172 }
172 }
173
173
174 .comment-previous-link {
174 .comment-previous-link {
175 display: inline-block;
175 display: inline-block;
176
176
177 .arrow_comment_link{
177 .arrow_comment_link{
178 cursor: pointer;
178 cursor: pointer;
179 i {
179 i {
180 font-size:10px;
180 font-size:10px;
181 }
181 }
182 }
182 }
183 .arrow_comment_link.disabled {
183 .arrow_comment_link.disabled {
184 cursor: default;
184 cursor: default;
185 color: @grey5;
185 color: @grey5;
186 }
186 }
187 }
187 }
188
188
189 .comment-next-link {
189 .comment-next-link {
190 display: inline-block;
190 display: inline-block;
191
191
192 .arrow_comment_link{
192 .arrow_comment_link{
193 cursor: pointer;
193 cursor: pointer;
194 i {
194 i {
195 font-size:10px;
195 font-size:10px;
196 }
196 }
197 }
197 }
198 .arrow_comment_link.disabled {
198 .arrow_comment_link.disabled {
199 cursor: default;
199 cursor: default;
200 color: @grey5;
200 color: @grey5;
201 }
201 }
202 }
202 }
203
203
204 .flag_status {
204 .flag_status {
205 display: inline-block;
205 display: inline-block;
206 margin: -2px .5em 0 .25em
206 margin: -2px .5em 0 .25em
207 }
207 }
208
208
209 .delete-comment {
209 .delete-comment {
210 display: inline-block;
210 display: inline-block;
211 color: @rcblue;
211 color: @rcblue;
212
212
213 &:hover {
213 &:hover {
214 cursor: pointer;
214 cursor: pointer;
215 }
215 }
216 }
216 }
217
217
218 .text {
218 .text {
219 clear: both;
219 clear: both;
220 .border-radius(@border-radius);
220 .border-radius(@border-radius);
221 .box-sizing(border-box);
221 .box-sizing(border-box);
222
222
223 .markdown-block p,
223 .markdown-block p,
224 .rst-block p {
224 .rst-block p {
225 margin: .5em 0 !important;
225 margin: .5em 0 !important;
226 // TODO: lisa: This is needed because of other rst !important rules :[
226 // TODO: lisa: This is needed because of other rst !important rules :[
227 }
227 }
228 }
228 }
229
229
230 .pr-version {
230 .pr-version {
231 float: left;
231 float: left;
232 margin: 0px 4px;
232 margin: 0px 4px;
233 }
233 }
234 .pr-version-inline {
234 .pr-version-inline {
235 float: left;
235 float: left;
236 margin: 0px 4px;
236 margin: 0px 4px;
237 }
237 }
238 .pr-version-num {
238 .pr-version-num {
239 font-size: 10px;
239 font-size: 10px;
240 }
240 }
241
241
242 }
242 }
243
243
244 @comment-padding: 5px;
244 @comment-padding: 5px;
245
245
246 .inline-comments {
246 .inline-comments {
247 border-radius: @border-radius;
247 border-radius: @border-radius;
248 .comment {
248 .comment {
249 margin: 0;
249 margin: 0;
250 border-radius: @border-radius;
250 border-radius: @border-radius;
251 }
251 }
252 .comment-outdated {
252 .comment-outdated {
253 opacity: 0.5;
253 opacity: 0.5;
254 }
254 }
255
255
256 .comment-inline {
256 .comment-inline {
257 background: white;
257 background: white;
258 padding: @comment-padding @comment-padding;
258 padding: @comment-padding @comment-padding;
259 border: @comment-padding solid @grey6;
259 border: @comment-padding solid @grey6;
260
260
261 .text {
261 .text {
262 border: none;
262 border: none;
263 }
263 }
264 .meta {
264 .meta {
265 border-bottom: 1px solid @grey6;
265 border-bottom: 1px solid @grey6;
266 margin: -5px 0px;
266 margin: -5px 0px;
267 line-height: 24px;
267 line-height: 24px;
268 }
268 }
269 }
269 }
270 .comment-selected {
270 .comment-selected {
271 border-left: 6px solid @comment-highlight-color;
271 border-left: 6px solid @comment-highlight-color;
272 }
272 }
273 .comment-inline-form {
273 .comment-inline-form {
274 padding: @comment-padding;
274 padding: @comment-padding;
275 display: none;
275 display: none;
276 }
276 }
277 .cb-comment-add-button {
277 .cb-comment-add-button {
278 margin: @comment-padding;
278 margin: @comment-padding;
279 }
279 }
280 /* hide add comment button when form is open */
280 /* hide add comment button when form is open */
281 .comment-inline-form-open ~ .cb-comment-add-button {
281 .comment-inline-form-open ~ .cb-comment-add-button {
282 display: none;
282 display: none;
283 }
283 }
284 .comment-inline-form-open {
284 .comment-inline-form-open {
285 display: block;
285 display: block;
286 }
286 }
287 /* hide add comment button when form but no comments */
287 /* hide add comment button when form but no comments */
288 .comment-inline-form:first-child + .cb-comment-add-button {
288 .comment-inline-form:first-child + .cb-comment-add-button {
289 display: none;
289 display: none;
290 }
290 }
291 /* hide add comment button when no comments or form */
291 /* hide add comment button when no comments or form */
292 .cb-comment-add-button:first-child {
292 .cb-comment-add-button:first-child {
293 display: none;
293 display: none;
294 }
294 }
295 /* hide add comment button when only comment is being deleted */
295 /* hide add comment button when only comment is being deleted */
296 .comment-deleting:first-child + .cb-comment-add-button {
296 .comment-deleting:first-child + .cb-comment-add-button {
297 display: none;
297 display: none;
298 }
298 }
299 }
299 }
300
300
301
301
302 .show-outdated-comments {
302 .show-outdated-comments {
303 display: inline;
303 display: inline;
304 color: @rcblue;
304 color: @rcblue;
305 }
305 }
306
306
307 // Comment Form
307 // Comment Form
308 div.comment-form {
308 div.comment-form {
309 margin-top: 20px;
309 margin-top: 20px;
310 }
310 }
311
311
312 .comment-form strong {
312 .comment-form strong {
313 display: block;
313 display: block;
314 margin-bottom: 15px;
314 margin-bottom: 15px;
315 }
315 }
316
316
317 .comment-form textarea {
317 .comment-form textarea {
318 width: 100%;
318 width: 100%;
319 height: 100px;
319 height: 100px;
320 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
320 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
321 }
321 }
322
322
323 form.comment-form {
323 form.comment-form {
324 margin-top: 10px;
324 margin-top: 10px;
325 margin-left: 10px;
325 margin-left: 10px;
326 }
326 }
327
327
328 .comment-inline-form .comment-block-ta,
328 .comment-inline-form .comment-block-ta,
329 .comment-form .comment-block-ta,
329 .comment-form .comment-block-ta,
330 .comment-form .preview-box {
330 .comment-form .preview-box {
331 .border-radius(@border-radius);
331 .border-radius(@border-radius);
332 .box-sizing(border-box);
332 .box-sizing(border-box);
333 background-color: white;
333 background-color: white;
334 }
334 }
335
335
336 .comment-form-submit {
336 .comment-form-submit {
337 margin-top: 5px;
337 margin-top: 5px;
338 margin-left: 525px;
338 margin-left: 525px;
339 }
339 }
340
340
341 .file-comments {
341 .file-comments {
342 display: none;
342 display: none;
343 }
343 }
344
344
345 .comment-form .preview-box.unloaded,
345 .comment-form .preview-box.unloaded,
346 .comment-inline-form .preview-box.unloaded {
346 .comment-inline-form .preview-box.unloaded {
347 height: 50px;
347 height: 50px;
348 text-align: center;
348 text-align: center;
349 padding: 20px;
349 padding: 20px;
350 background-color: white;
350 background-color: white;
351 }
351 }
352
352
353 .comment-footer {
353 .comment-footer {
354 position: relative;
354 position: relative;
355 width: 100%;
355 width: 100%;
356 min-height: 42px;
356 min-height: 42px;
357
357
358 .status_box,
358 .status_box,
359 .cancel-button {
359 .cancel-button {
360 float: left;
360 float: left;
361 display: inline-block;
361 display: inline-block;
362 }
362 }
363
363
364 .action-buttons {
364 .action-buttons {
365 float: right;
365 float: right;
366 display: inline-block;
366 display: inline-block;
367 }
367 }
368 }
368 }
369
369
370 .comment-form {
370 .comment-form {
371
371
372 .comment {
372 .comment {
373 margin-left: 10px;
373 margin-left: 10px;
374 }
374 }
375
375
376 .comment-help {
376 .comment-help {
377 color: @grey4;
377 color: @grey4;
378 padding: 5px 0 5px 0;
378 padding: 5px 0 5px 0;
379 }
379 }
380
380
381 .comment-title {
381 .comment-title {
382 padding: 5px 0 5px 0;
382 padding: 5px 0 5px 0;
383 }
383 }
384
384
385 .comment-button {
385 .comment-button {
386 display: inline-block;
386 display: inline-block;
387 }
387 }
388
388
389 .comment-button-input {
389 .comment-button-input {
390 margin-right: 0;
390 margin-right: 0;
391 }
391 }
392
392
393 .comment-footer {
393 .comment-footer {
394 margin-bottom: 110px;
394 margin-bottom: 110px;
395 margin-top: 10px;
395 margin-top: 10px;
396 }
396 }
397 }
397 }
398
398
399
399
400 .comment-form-login {
400 .comment-form-login {
401 .comment-help {
401 .comment-help {
402 padding: 0.9em; //same as the button
402 padding: 0.9em; //same as the button
403 }
403 }
404
404
405 div.clearfix {
405 div.clearfix {
406 clear: both;
406 clear: both;
407 width: 100%;
407 width: 100%;
408 display: block;
408 display: block;
409 }
409 }
410 }
410 }
411
411
412 .comment-type {
412 .comment-type {
413 margin: 0px;
413 margin: 0px;
414 border-radius: inherit;
414 border-radius: inherit;
415 border-color: @grey6;
415 border-color: @grey6;
416 }
416 }
417
417
418 .preview-box {
418 .preview-box {
419 min-height: 105px;
419 min-height: 105px;
420 margin-bottom: 15px;
420 margin-bottom: 15px;
421 background-color: white;
421 background-color: white;
422 .border-radius(@border-radius);
422 .border-radius(@border-radius);
423 .box-sizing(border-box);
423 .box-sizing(border-box);
424 }
424 }
425
425
426 .add-another-button {
426 .add-another-button {
427 margin-left: 10px;
427 margin-left: 10px;
428 margin-top: 10px;
428 margin-top: 10px;
429 margin-bottom: 10px;
429 margin-bottom: 10px;
430 }
430 }
431
431
432 .comment .buttons {
432 .comment .buttons {
433 float: right;
433 float: right;
434 margin: -1px 0px 0px 0px;
434 margin: -1px 0px 0px 0px;
435 }
435 }
436
436
437 // Inline Comment Form
437 // Inline Comment Form
438 .injected_diff .comment-inline-form,
438 .injected_diff .comment-inline-form,
439 .comment-inline-form {
439 .comment-inline-form {
440 background-color: white;
440 background-color: white;
441 margin-top: 10px;
441 margin-top: 10px;
442 margin-bottom: 20px;
442 margin-bottom: 20px;
443 }
443 }
444
444
445 .inline-form {
445 .inline-form {
446 padding: 10px 7px;
446 padding: 10px 7px;
447 }
447 }
448
448
449 .inline-form div {
449 .inline-form div {
450 max-width: 100%;
450 max-width: 100%;
451 }
451 }
452
452
453 .overlay {
453 .overlay {
454 display: none;
454 display: none;
455 position: absolute;
455 position: absolute;
456 width: 100%;
456 width: 100%;
457 text-align: center;
457 text-align: center;
458 vertical-align: middle;
458 vertical-align: middle;
459 font-size: 16px;
459 font-size: 16px;
460 background: none repeat scroll 0 0 white;
460 background: none repeat scroll 0 0 white;
461
461
462 &.submitting {
462 &.submitting {
463 display: block;
463 display: block;
464 opacity: 0.5;
464 opacity: 0.5;
465 z-index: 100;
465 z-index: 100;
466 }
466 }
467 }
467 }
468 .comment-inline-form .overlay.submitting .overlay-text {
468 .comment-inline-form .overlay.submitting .overlay-text {
469 margin-top: 5%;
469 margin-top: 5%;
470 }
470 }
471
471
472 .comment-inline-form .clearfix,
472 .comment-inline-form .clearfix,
473 .comment-form .clearfix {
473 .comment-form .clearfix {
474 .border-radius(@border-radius);
474 .border-radius(@border-radius);
475 margin: 0px;
475 margin: 0px;
476 }
476 }
477
477
478 .comment-inline-form .comment-footer {
478 .comment-inline-form .comment-footer {
479 margin: 10px 0px 0px 0px;
479 margin: 10px 0px 0px 0px;
480 }
480 }
481
481
482 .hide-inline-form-button {
482 .hide-inline-form-button {
483 margin-left: 5px;
483 margin-left: 5px;
484 }
484 }
485 .comment-button .hide-inline-form {
485 .comment-button .hide-inline-form {
486 background: white;
486 background: white;
487 }
487 }
488
488
489 .comment-area {
489 .comment-area {
490 padding: 8px 12px;
490 padding: 8px 12px;
491 border: 1px solid @grey5;
491 border: 1px solid @grey5;
492 .border-radius(@border-radius);
492 .border-radius(@border-radius);
493 }
493 }
494
494
495 .comment-area-header .nav-links {
495 .comment-area-header .nav-links {
496 display: flex;
496 display: flex;
497 flex-flow: row wrap;
497 flex-flow: row wrap;
498 -webkit-flex-flow: row wrap;
498 -webkit-flex-flow: row wrap;
499 width: 100%;
499 width: 100%;
500 }
500 }
501
501
502 .comment-area-footer {
502 .comment-area-footer {
503 display: flex;
503 display: flex;
504 }
504 }
505
505
506 .comment-footer .toolbar {
506 .comment-footer .toolbar {
507
507
508 }
508 }
509
509
510 .nav-links {
510 .nav-links {
511 padding: 0;
511 padding: 0;
512 margin: 0;
512 margin: 0;
513 list-style: none;
513 list-style: none;
514 height: auto;
514 height: auto;
515 border-bottom: 1px solid @grey5;
515 border-bottom: 1px solid @grey5;
516 }
516 }
517 .nav-links li {
517 .nav-links li {
518 display: inline-block;
518 display: inline-block;
519 }
519 }
520 .nav-links li:before {
520 .nav-links li:before {
521 content: "";
521 content: "";
522 }
522 }
523 .nav-links li a.disabled {
523 .nav-links li a.disabled {
524 cursor: not-allowed;
524 cursor: not-allowed;
525 }
525 }
526
526
527 .nav-links li.active a {
527 .nav-links li.active a {
528 border-bottom: 2px solid @rcblue;
528 border-bottom: 2px solid @rcblue;
529 color: #000;
529 color: #000;
530 font-weight: 600;
530 font-weight: 600;
531 }
531 }
532 .nav-links li a {
532 .nav-links li a {
533 display: inline-block;
533 display: inline-block;
534 padding: 0px 10px 5px 10px;
534 padding: 0px 10px 5px 10px;
535 margin-bottom: -1px;
535 margin-bottom: -1px;
536 font-size: 14px;
536 font-size: 14px;
537 line-height: 28px;
537 line-height: 28px;
538 color: #8f8f8f;
538 color: #8f8f8f;
539 border-bottom: 2px solid transparent;
539 border-bottom: 2px solid transparent;
540 }
540 }
541
541
542 .toolbar-text {
542 .toolbar-text {
543 float: left;
543 float: left;
544 margin: -5px 0px 0px 0px;
544 margin: -5px 0px 0px 0px;
545 font-size: 12px;
545 font-size: 12px;
546 }
546 }
547
547
@@ -1,314 +1,314 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%inherit file="/base/base.mako"/>
3 <%inherit file="/base/base.mako"/>
4 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
4 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
5
5
6 <%def name="title()">
6 <%def name="title()">
7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
8 %if c.rhodecode_name:
8 %if c.rhodecode_name:
9 &middot; ${h.branding(c.rhodecode_name)}
9 &middot; ${h.branding(c.rhodecode_name)}
10 %endif
10 %endif
11 </%def>
11 </%def>
12
12
13 <%def name="menu_bar_nav()">
13 <%def name="menu_bar_nav()">
14 ${self.menu_items(active='repositories')}
14 ${self.menu_items(active='repositories')}
15 </%def>
15 </%def>
16
16
17 <%def name="menu_bar_subnav()">
17 <%def name="menu_bar_subnav()">
18 ${self.repo_menu(active='changelog')}
18 ${self.repo_menu(active='changelog')}
19 </%def>
19 </%def>
20
20
21 <%def name="main()">
21 <%def name="main()">
22 <script>
22 <script>
23 // TODO: marcink switch this to pyroutes
23 // TODO: marcink switch this to pyroutes
24 AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
24 AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
26 </script>
26 </script>
27 <div class="box">
27 <div class="box">
28 <div class="title">
28 <div class="title">
29 ${self.repo_page_title(c.rhodecode_db_repo)}
29 ${self.repo_page_title(c.rhodecode_db_repo)}
30 </div>
30 </div>
31
31
32 <div id="changeset_compare_view_content" class="summary changeset">
32 <div id="changeset_compare_view_content" class="summary changeset">
33 <div class="summary-detail">
33 <div class="summary-detail">
34 <div class="summary-detail-header">
34 <div class="summary-detail-header">
35 <span class="breadcrumbs files_location">
35 <span class="breadcrumbs files_location">
36 <h4>${_('Commit')}
36 <h4>${_('Commit')}
37 <code>
37 <code>
38 ${h.show_id(c.commit)}
38 ${h.show_id(c.commit)}
39 </code>
39 </code>
40 </h4>
40 </h4>
41 </span>
41 </span>
42 <span id="parent_link">
42 <span id="parent_link">
43 <a href="#" title="${_('Parent Commit')}">${_('Parent')}</a>
43 <a href="#" title="${_('Parent Commit')}">${_('Parent')}</a>
44 </span>
44 </span>
45 |
45 |
46 <span id="child_link">
46 <span id="child_link">
47 <a href="#" title="${_('Child Commit')}">${_('Child')}</a>
47 <a href="#" title="${_('Child Commit')}">${_('Child')}</a>
48 </span>
48 </span>
49 </div>
49 </div>
50
50
51 <div class="fieldset">
51 <div class="fieldset">
52 <div class="left-label">
52 <div class="left-label">
53 ${_('Description')}:
53 ${_('Description')}:
54 </div>
54 </div>
55 <div class="right-content">
55 <div class="right-content">
56 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
56 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
57 <div id="message_expand" style="display:none;">
57 <div id="message_expand" style="display:none;">
58 ${_('Expand')}
58 ${_('Expand')}
59 </div>
59 </div>
60 </div>
60 </div>
61 </div>
61 </div>
62
62
63 %if c.statuses:
63 %if c.statuses:
64 <div class="fieldset">
64 <div class="fieldset">
65 <div class="left-label">
65 <div class="left-label">
66 ${_('Commit status')}:
66 ${_('Commit status')}:
67 </div>
67 </div>
68 <div class="right-content">
68 <div class="right-content">
69 <div class="changeset-status-ico">
69 <div class="changeset-status-ico">
70 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
70 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
71 </div>
71 </div>
72 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
72 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
73 </div>
73 </div>
74 </div>
74 </div>
75 %endif
75 %endif
76
76
77 <div class="fieldset">
77 <div class="fieldset">
78 <div class="left-label">
78 <div class="left-label">
79 ${_('References')}:
79 ${_('References')}:
80 </div>
80 </div>
81 <div class="right-content">
81 <div class="right-content">
82 <div class="tags">
82 <div class="tags">
83
83
84 %if c.commit.merge:
84 %if c.commit.merge:
85 <span class="mergetag tag">
85 <span class="mergetag tag">
86 <i class="icon-merge"></i>${_('merge')}
86 <i class="icon-merge"></i>${_('merge')}
87 </span>
87 </span>
88 %endif
88 %endif
89
89
90 %if h.is_hg(c.rhodecode_repo):
90 %if h.is_hg(c.rhodecode_repo):
91 %for book in c.commit.bookmarks:
91 %for book in c.commit.bookmarks:
92 <span class="booktag tag" title="${_('Bookmark %s') % book}">
92 <span class="booktag tag" title="${_('Bookmark %s') % book}">
93 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
93 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
94 </span>
94 </span>
95 %endfor
95 %endfor
96 %endif
96 %endif
97
97
98 %for tag in c.commit.tags:
98 %for tag in c.commit.tags:
99 <span class="tagtag tag" title="${_('Tag %s') % tag}">
99 <span class="tagtag tag" title="${_('Tag %s') % tag}">
100 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-tag"></i>${tag}</a>
100 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-tag"></i>${tag}</a>
101 </span>
101 </span>
102 %endfor
102 %endfor
103
103
104 %if c.commit.branch:
104 %if c.commit.branch:
105 <span class="branchtag tag" title="${_('Branch %s') % c.commit.branch}">
105 <span class="branchtag tag" title="${_('Branch %s') % c.commit.branch}">
106 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
106 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
107 </span>
107 </span>
108 %endif
108 %endif
109 </div>
109 </div>
110 </div>
110 </div>
111 </div>
111 </div>
112
112
113 <div class="fieldset">
113 <div class="fieldset">
114 <div class="left-label">
114 <div class="left-label">
115 ${_('Diff options')}:
115 ${_('Diff options')}:
116 </div>
116 </div>
117 <div class="right-content">
117 <div class="right-content">
118 <div class="diff-actions">
118 <div class="diff-actions">
119 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
119 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
120 ${_('Raw Diff')}
120 ${_('Raw Diff')}
121 </a>
121 </a>
122 |
122 |
123 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
123 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
124 ${_('Patch Diff')}
124 ${_('Patch Diff')}
125 </a>
125 </a>
126 |
126 |
127 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.commit.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
127 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.commit.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
128 ${_('Download Diff')}
128 ${_('Download Diff')}
129 </a>
129 </a>
130 |
130 |
131 ${c.ignorews_url(request.GET)}
131 ${c.ignorews_url(request.GET)}
132 |
132 |
133 ${c.context_url(request.GET)}
133 ${c.context_url(request.GET)}
134 </div>
134 </div>
135 </div>
135 </div>
136 </div>
136 </div>
137
137
138 <div class="fieldset">
138 <div class="fieldset">
139 <div class="left-label">
139 <div class="left-label">
140 ${_('Comments')}:
140 ${_('Comments')}:
141 </div>
141 </div>
142 <div class="right-content">
142 <div class="right-content">
143 <div class="comments-number">
143 <div class="comments-number">
144 %if c.comments:
144 %if c.comments:
145 <a href="#comments">${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
145 <a href="#comments">${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
146 %else:
146 %else:
147 ${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
147 ${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
148 %endif
148 %endif
149 %if c.inline_cnt:
149 %if c.inline_cnt:
150 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
150 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
151 %else:
151 %else:
152 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
152 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
153 %endif
153 %endif
154 </div>
154 </div>
155 </div>
155 </div>
156 </div>
156 </div>
157
157
158 </div> <!-- end summary-detail -->
158 </div> <!-- end summary-detail -->
159
159
160 <div id="commit-stats" class="sidebar-right">
160 <div id="commit-stats" class="sidebar-right">
161 <div class="summary-detail-header">
161 <div class="summary-detail-header">
162 <h4 class="item">
162 <h4 class="item">
163 ${_('Author')}
163 ${_('Author')}
164 </h4>
164 </h4>
165 </div>
165 </div>
166 <div class="sidebar-right-content">
166 <div class="sidebar-right-content">
167 ${self.gravatar_with_user(c.commit.author)}
167 ${self.gravatar_with_user(c.commit.author)}
168 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
168 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
169 </div>
169 </div>
170 </div><!-- end sidebar -->
170 </div><!-- end sidebar -->
171 </div> <!-- end summary -->
171 </div> <!-- end summary -->
172 <div class="cs_files">
172 <div class="cs_files">
173 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
173 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
174 ${cbdiffs.render_diffset_menu()}
174 ${cbdiffs.render_diffset_menu()}
175 ${cbdiffs.render_diffset(
175 ${cbdiffs.render_diffset(
176 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True)}
176 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True)}
177 </div>
177 </div>
178
178
179 ## template for inline comment form
179 ## template for inline comment form
180 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
180 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
181
181
182 ## render comments
182 ## render comments
183 ${comment.generate_comments()}
183 ${comment.generate_comments(c.comments)}
184
184
185 ## main comment form and it status
185 ## main comment form and it status
186 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.commit.raw_id),
186 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.commit.raw_id),
187 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
187 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
188 </div>
188 </div>
189
189
190 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
190 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
191 <script type="text/javascript">
191 <script type="text/javascript">
192
192
193 $(document).ready(function() {
193 $(document).ready(function() {
194
194
195 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
195 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
196 if($('#trimmed_message_box').height() === boxmax){
196 if($('#trimmed_message_box').height() === boxmax){
197 $('#message_expand').show();
197 $('#message_expand').show();
198 }
198 }
199
199
200 $('#message_expand').on('click', function(e){
200 $('#message_expand').on('click', function(e){
201 $('#trimmed_message_box').css('max-height', 'none');
201 $('#trimmed_message_box').css('max-height', 'none');
202 $(this).hide();
202 $(this).hide();
203 });
203 });
204
204
205 $('.show-inline-comments').on('click', function(e){
205 $('.show-inline-comments').on('click', function(e){
206 var boxid = $(this).attr('data-comment-id');
206 var boxid = $(this).attr('data-comment-id');
207 var button = $(this);
207 var button = $(this);
208
208
209 if(button.hasClass("comments-visible")) {
209 if(button.hasClass("comments-visible")) {
210 $('#{0} .inline-comments'.format(boxid)).each(function(index){
210 $('#{0} .inline-comments'.format(boxid)).each(function(index){
211 $(this).hide();
211 $(this).hide();
212 });
212 });
213 button.removeClass("comments-visible");
213 button.removeClass("comments-visible");
214 } else {
214 } else {
215 $('#{0} .inline-comments'.format(boxid)).each(function(index){
215 $('#{0} .inline-comments'.format(boxid)).each(function(index){
216 $(this).show();
216 $(this).show();
217 });
217 });
218 button.addClass("comments-visible");
218 button.addClass("comments-visible");
219 }
219 }
220 });
220 });
221
221
222
222
223 // next links
223 // next links
224 $('#child_link').on('click', function(e){
224 $('#child_link').on('click', function(e){
225 // fetch via ajax what is going to be the next link, if we have
225 // fetch via ajax what is going to be the next link, if we have
226 // >1 links show them to user to choose
226 // >1 links show them to user to choose
227 if(!$('#child_link').hasClass('disabled')){
227 if(!$('#child_link').hasClass('disabled')){
228 $.ajax({
228 $.ajax({
229 url: '${h.url('changeset_children',repo_name=c.repo_name, revision=c.commit.raw_id)}',
229 url: '${h.url('changeset_children',repo_name=c.repo_name, revision=c.commit.raw_id)}',
230 success: function(data) {
230 success: function(data) {
231 if(data.results.length === 0){
231 if(data.results.length === 0){
232 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
232 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
233 }
233 }
234 if(data.results.length === 1){
234 if(data.results.length === 1){
235 var commit = data.results[0];
235 var commit = data.results[0];
236 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
236 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
237 }
237 }
238 else if(data.results.length === 2){
238 else if(data.results.length === 2){
239 $('#child_link').addClass('disabled');
239 $('#child_link').addClass('disabled');
240 $('#child_link').addClass('double');
240 $('#child_link').addClass('double');
241 var _html = '';
241 var _html = '';
242 _html +='<a title="__title__" href="__url__">__rev__</a> '
242 _html +='<a title="__title__" href="__url__">__rev__</a> '
243 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
243 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
244 .replace('__title__', data.results[0].message)
244 .replace('__title__', data.results[0].message)
245 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
245 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
246 _html +=' | ';
246 _html +=' | ';
247 _html +='<a title="__title__" href="__url__">__rev__</a> '
247 _html +='<a title="__title__" href="__url__">__rev__</a> '
248 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
248 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
249 .replace('__title__', data.results[1].message)
249 .replace('__title__', data.results[1].message)
250 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
250 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
251 $('#child_link').html(_html);
251 $('#child_link').html(_html);
252 }
252 }
253 }
253 }
254 });
254 });
255 e.preventDefault();
255 e.preventDefault();
256 }
256 }
257 });
257 });
258
258
259 // prev links
259 // prev links
260 $('#parent_link').on('click', function(e){
260 $('#parent_link').on('click', function(e){
261 // fetch via ajax what is going to be the next link, if we have
261 // fetch via ajax what is going to be the next link, if we have
262 // >1 links show them to user to choose
262 // >1 links show them to user to choose
263 if(!$('#parent_link').hasClass('disabled')){
263 if(!$('#parent_link').hasClass('disabled')){
264 $.ajax({
264 $.ajax({
265 url: '${h.url("changeset_parents",repo_name=c.repo_name, revision=c.commit.raw_id)}',
265 url: '${h.url("changeset_parents",repo_name=c.repo_name, revision=c.commit.raw_id)}',
266 success: function(data) {
266 success: function(data) {
267 if(data.results.length === 0){
267 if(data.results.length === 0){
268 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
268 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
269 }
269 }
270 if(data.results.length === 1){
270 if(data.results.length === 1){
271 var commit = data.results[0];
271 var commit = data.results[0];
272 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
272 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
273 }
273 }
274 else if(data.results.length === 2){
274 else if(data.results.length === 2){
275 $('#parent_link').addClass('disabled');
275 $('#parent_link').addClass('disabled');
276 $('#parent_link').addClass('double');
276 $('#parent_link').addClass('double');
277 var _html = '';
277 var _html = '';
278 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
278 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
279 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
279 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
280 .replace('__title__', data.results[0].message)
280 .replace('__title__', data.results[0].message)
281 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
281 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
282 _html +=' | ';
282 _html +=' | ';
283 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
283 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
284 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
284 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
285 .replace('__title__', data.results[1].message)
285 .replace('__title__', data.results[1].message)
286 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
286 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
287 $('#parent_link').html(_html);
287 $('#parent_link').html(_html);
288 }
288 }
289 }
289 }
290 });
290 });
291 e.preventDefault();
291 e.preventDefault();
292 }
292 }
293 });
293 });
294
294
295 if (location.hash) {
295 if (location.hash) {
296 var result = splitDelimitedHash(location.hash);
296 var result = splitDelimitedHash(location.hash);
297 var line = $('html').find(result.loc);
297 var line = $('html').find(result.loc);
298 if (line.length > 0){
298 if (line.length > 0){
299 offsetScroll(line, 70);
299 offsetScroll(line, 70);
300 }
300 }
301 }
301 }
302
302
303 // browse tree @ revision
303 // browse tree @ revision
304 $('#files_link').on('click', function(e){
304 $('#files_link').on('click', function(e){
305 window.location = '${h.url('files_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path='')}';
305 window.location = '${h.url('files_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path='')}';
306 e.preventDefault();
306 e.preventDefault();
307 });
307 });
308
308
309 // inject comments into their proper positions
309 // inject comments into their proper positions
310 var file_comments = $('.inline-comment-placeholder');
310 var file_comments = $('.inline-comment-placeholder');
311 })
311 })
312 </script>
312 </script>
313
313
314 </%def>
314 </%def>
@@ -1,400 +1,408 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 <%namespace name="base" file="/base/base.mako"/>
7
7
8 <%def name="comment_block(comment, inline=False)">
8 <%def name="comment_block(comment, inline=False)">
9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
10 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 % if inline:
11 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
12 % else:
13 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
14 % endif
15
11
16
12 <div class="comment
17 <div class="comment
13 ${'comment-inline' if inline else 'comment-general'}
18 ${'comment-inline' if inline else 'comment-general'}
14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
19 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
15 id="comment-${comment.comment_id}"
20 id="comment-${comment.comment_id}"
16 line="${comment.line_no}"
21 line="${comment.line_no}"
17 data-comment-id="${comment.comment_id}"
22 data-comment-id="${comment.comment_id}"
18 data-comment-type="${comment.comment_type}"
23 data-comment-type="${comment.comment_type}"
19 data-comment-inline=${h.json.dumps(inline)}
24 data-comment-inline=${h.json.dumps(inline)}
20 style="${'display: none;' if outdated_at_ver else ''}">
25 style="${'display: none;' if outdated_at_ver else ''}">
21
26
22 <div class="meta">
27 <div class="meta">
23 <div class="comment-type-label">
28 <div class="comment-type-label">
24 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
29 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
25 % if comment.comment_type == 'todo':
30 % if comment.comment_type == 'todo':
26 % if comment.resolved:
31 % if comment.resolved:
27 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
32 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
28 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
33 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
29 </div>
34 </div>
30 % else:
35 % else:
31 <div class="resolved tooltip" style="display: none">
36 <div class="resolved tooltip" style="display: none">
32 <span>${comment.comment_type}</span>
37 <span>${comment.comment_type}</span>
33 </div>
38 </div>
34 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
39 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
35 ${comment.comment_type}
40 ${comment.comment_type}
36 </div>
41 </div>
37 % endif
42 % endif
38 % else:
43 % else:
39 % if comment.resolved_comment:
44 % if comment.resolved_comment:
40 fix
45 fix
41 % else:
46 % else:
42 ${comment.comment_type or 'note'}
47 ${comment.comment_type or 'note'}
43 % endif
48 % endif
44 % endif
49 % endif
45 </div>
50 </div>
46 </div>
51 </div>
47
52
48 <div class="author ${'author-inline' if inline else 'author-general'}">
53 <div class="author ${'author-inline' if inline else 'author-general'}">
49 ${base.gravatar_with_user(comment.author.email, 16)}
54 ${base.gravatar_with_user(comment.author.email, 16)}
50 </div>
55 </div>
51 <div class="date">
56 <div class="date">
52 ${h.age_component(comment.modified_at, time_is_local=True)}
57 ${h.age_component(comment.modified_at, time_is_local=True)}
53 </div>
58 </div>
54 % if inline:
59 % if inline:
55 <span></span>
60 <span></span>
56 % else:
61 % else:
57 <div class="status-change">
62 <div class="status-change">
58 % if comment.pull_request:
63 % if comment.pull_request:
59 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
64 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
60 % if comment.status_change:
65 % if comment.status_change:
61 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
66 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
62 % else:
67 % else:
63 ${_('pull request #%s') % comment.pull_request.pull_request_id}
68 ${_('pull request #%s') % comment.pull_request.pull_request_id}
64 % endif
69 % endif
65 </a>
70 </a>
66 % else:
71 % else:
67 % if comment.status_change:
72 % if comment.status_change:
68 ${_('Status change on commit')}:
73 ${_('Status change on commit')}:
69 % endif
74 % endif
70 % endif
75 % endif
71 </div>
76 </div>
72 % endif
77 % endif
73
78
74 % if comment.status_change:
79 % if comment.status_change:
75 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
80 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
76 <div title="${_('Commit status')}" class="changeset-status-lbl">
81 <div title="${_('Commit status')}" class="changeset-status-lbl">
77 ${comment.status_change[0].status_lbl}
82 ${comment.status_change[0].status_lbl}
78 </div>
83 </div>
79 % endif
84 % endif
80
85
81 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
86 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
82
87
83 <div class="comment-links-block">
88 <div class="comment-links-block">
84
89
85 % if inline:
90 % if inline:
86 % if outdated_at_ver:
87 <div class="pr-version-inline">
91 <div class="pr-version-inline">
88 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
92 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
89 <code class="pr-version-num">
93 % if outdated_at_ver:
90 outdated ${'v{}'.format(pr_index_ver)}
94 <code class="pr-version-num" title="${_('Outdated comment from pull request version {0}').format(pr_index_ver)}">
91 </code>
95 outdated ${'v{}'.format(pr_index_ver)} |
96 </code>
97 % elif pr_index_ver:
98 <code class="pr-version-num" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
99 ${'v{}'.format(pr_index_ver)} |
100 </code>
101 % endif
92 </a>
102 </a>
93 </div>
103 </div>
94 |
95 % endif
96 % else:
104 % else:
97 % if comment.pull_request_version_id and pr_index_ver:
105 % if comment.pull_request_version_id and pr_index_ver:
98 |
106 |
99 <div class="pr-version">
107 <div class="pr-version">
100 % if comment.outdated:
108 % if comment.outdated:
101 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
109 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
102 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
110 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
103 </a>
111 </a>
104 % else:
112 % else:
105 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
113 <div title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
106 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
114 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
107 <code class="pr-version-num">
115 <code class="pr-version-num">
108 ${'v{}'.format(pr_index_ver)}
116 ${'v{}'.format(pr_index_ver)}
109 </code>
117 </code>
110 </a>
118 </a>
111 </div>
119 </div>
112 % endif
120 % endif
113 </div>
121 </div>
114 % endif
122 % endif
115 % endif
123 % endif
116
124
117 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
125 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
118 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
126 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
119 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
127 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
120 ## permissions to delete
128 ## permissions to delete
121 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
129 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
122 ## TODO: dan: add edit comment here
130 ## TODO: dan: add edit comment here
123 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
131 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
124 %else:
132 %else:
125 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
133 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
126 %endif
134 %endif
127 %else:
135 %else:
128 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
136 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
129 %endif
137 %endif
130
138
131 %if not outdated_at_ver:
139 %if not outdated_at_ver:
132 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
140 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
133 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
141 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
134 %endif
142 %endif
135
143
136 </div>
144 </div>
137 </div>
145 </div>
138 <div class="text">
146 <div class="text">
139 ${comment.render(mentions=True)|n}
147 ${comment.render(mentions=True)|n}
140 </div>
148 </div>
141
149
142 </div>
150 </div>
143 </%def>
151 </%def>
144
152
145 ## generate main comments
153 ## generate main comments
146 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
154 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
147 <div id="comments">
155 <div id="comments">
148 %for comment in c.comments:
156 %for comment in comments:
149 <div id="comment-tr-${comment.comment_id}">
157 <div id="comment-tr-${comment.comment_id}">
150 ## only render comments that are not from pull request, or from
158 ## only render comments that are not from pull request, or from
151 ## pull request and a status change
159 ## pull request and a status change
152 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
160 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
153 ${comment_block(comment)}
161 ${comment_block(comment)}
154 %endif
162 %endif
155 </div>
163 </div>
156 %endfor
164 %endfor
157 ## to anchor ajax comments
165 ## to anchor ajax comments
158 <div id="injected_page_comments"></div>
166 <div id="injected_page_comments"></div>
159 </div>
167 </div>
160 </%def>
168 </%def>
161
169
162
170
163 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
171 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
164
172
165 ## merge status, and merge action
173 ## merge status, and merge action
166 %if is_pull_request:
174 %if is_pull_request:
167 <div class="pull-request-merge">
175 <div class="pull-request-merge">
168 %if c.allowed_to_merge:
176 %if c.allowed_to_merge:
169 <div class="pull-request-wrap">
177 <div class="pull-request-wrap">
170 <div class="pull-right">
178 <div class="pull-right">
171 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
179 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
172 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
180 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
173 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
181 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
174 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
182 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
175 ${h.end_form()}
183 ${h.end_form()}
176 </div>
184 </div>
177 </div>
185 </div>
178 %else:
186 %else:
179 <div class="pull-request-wrap">
187 <div class="pull-request-wrap">
180 <div class="pull-right">
188 <div class="pull-right">
181 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
189 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
182 </div>
190 </div>
183 </div>
191 </div>
184 %endif
192 %endif
185 </div>
193 </div>
186 %endif
194 %endif
187
195
188 <div class="comments">
196 <div class="comments">
189 <%
197 <%
190 if is_pull_request:
198 if is_pull_request:
191 placeholder = _('Leave a comment on this Pull Request.')
199 placeholder = _('Leave a comment on this Pull Request.')
192 elif is_compare:
200 elif is_compare:
193 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
201 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
194 else:
202 else:
195 placeholder = _('Leave a comment on this Commit.')
203 placeholder = _('Leave a comment on this Commit.')
196 %>
204 %>
197
205
198 % if c.rhodecode_user.username != h.DEFAULT_USER:
206 % if c.rhodecode_user.username != h.DEFAULT_USER:
199 <div class="js-template" id="cb-comment-general-form-template">
207 <div class="js-template" id="cb-comment-general-form-template">
200 ## template generated for injection
208 ## template generated for injection
201 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
209 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
202 </div>
210 </div>
203
211
204 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
212 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
205 ## inject form here
213 ## inject form here
206 </div>
214 </div>
207 <script type="text/javascript">
215 <script type="text/javascript">
208 var lineNo = 'general';
216 var lineNo = 'general';
209 var resolvesCommentId = null;
217 var resolvesCommentId = null;
210 var generalCommentForm = Rhodecode.comments.createGeneralComment(
218 var generalCommentForm = Rhodecode.comments.createGeneralComment(
211 lineNo, "${placeholder}", resolvesCommentId);
219 lineNo, "${placeholder}", resolvesCommentId);
212
220
213 // set custom success callback on rangeCommit
221 // set custom success callback on rangeCommit
214 % if is_compare:
222 % if is_compare:
215 generalCommentForm.setHandleFormSubmit(function(o) {
223 generalCommentForm.setHandleFormSubmit(function(o) {
216 var self = generalCommentForm;
224 var self = generalCommentForm;
217
225
218 var text = self.cm.getValue();
226 var text = self.cm.getValue();
219 var status = self.getCommentStatus();
227 var status = self.getCommentStatus();
220 var commentType = self.getCommentType();
228 var commentType = self.getCommentType();
221
229
222 if (text === "" && !status) {
230 if (text === "" && !status) {
223 return;
231 return;
224 }
232 }
225
233
226 // we can pick which commits we want to make the comment by
234 // we can pick which commits we want to make the comment by
227 // selecting them via click on preview pane, this will alter the hidden inputs
235 // selecting them via click on preview pane, this will alter the hidden inputs
228 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
236 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
229
237
230 var commitIds = [];
238 var commitIds = [];
231 $('#changeset_compare_view_content .compare_select').each(function(el) {
239 $('#changeset_compare_view_content .compare_select').each(function(el) {
232 var commitId = this.id.replace('row-', '');
240 var commitId = this.id.replace('row-', '');
233 if ($(this).hasClass('hl') || !cherryPicked) {
241 if ($(this).hasClass('hl') || !cherryPicked) {
234 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
242 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
235 commitIds.push(commitId);
243 commitIds.push(commitId);
236 } else {
244 } else {
237 $("input[data-commit-id='{0}']".format(commitId)).val('')
245 $("input[data-commit-id='{0}']".format(commitId)).val('')
238 }
246 }
239 });
247 });
240
248
241 self.setActionButtonsDisabled(true);
249 self.setActionButtonsDisabled(true);
242 self.cm.setOption("readOnly", true);
250 self.cm.setOption("readOnly", true);
243 var postData = {
251 var postData = {
244 'text': text,
252 'text': text,
245 'changeset_status': status,
253 'changeset_status': status,
246 'comment_type': commentType,
254 'comment_type': commentType,
247 'commit_ids': commitIds,
255 'commit_ids': commitIds,
248 'csrf_token': CSRF_TOKEN
256 'csrf_token': CSRF_TOKEN
249 };
257 };
250
258
251 var submitSuccessCallback = function(o) {
259 var submitSuccessCallback = function(o) {
252 location.reload(true);
260 location.reload(true);
253 };
261 };
254 var submitFailCallback = function(){
262 var submitFailCallback = function(){
255 self.resetCommentFormState(text)
263 self.resetCommentFormState(text)
256 };
264 };
257 self.submitAjaxPOST(
265 self.submitAjaxPOST(
258 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
266 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
259 });
267 });
260 % endif
268 % endif
261
269
262
270
263 </script>
271 </script>
264 % else:
272 % else:
265 ## form state when not logged in
273 ## form state when not logged in
266 <div class="comment-form ac">
274 <div class="comment-form ac">
267
275
268 <div class="comment-area">
276 <div class="comment-area">
269 <div class="comment-area-header">
277 <div class="comment-area-header">
270 <ul class="nav-links clearfix">
278 <ul class="nav-links clearfix">
271 <li class="active">
279 <li class="active">
272 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
280 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
273 </li>
281 </li>
274 <li class="">
282 <li class="">
275 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
283 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
276 </li>
284 </li>
277 </ul>
285 </ul>
278 </div>
286 </div>
279
287
280 <div class="comment-area-write" style="display: block;">
288 <div class="comment-area-write" style="display: block;">
281 <div id="edit-container">
289 <div id="edit-container">
282 <div style="padding: 40px 0">
290 <div style="padding: 40px 0">
283 ${_('You need to be logged in to leave comments.')}
291 ${_('You need to be logged in to leave comments.')}
284 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
292 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
285 </div>
293 </div>
286 </div>
294 </div>
287 <div id="preview-container" class="clearfix" style="display: none;">
295 <div id="preview-container" class="clearfix" style="display: none;">
288 <div id="preview-box" class="preview-box"></div>
296 <div id="preview-box" class="preview-box"></div>
289 </div>
297 </div>
290 </div>
298 </div>
291
299
292 <div class="comment-area-footer">
300 <div class="comment-area-footer">
293 <div class="toolbar">
301 <div class="toolbar">
294 <div class="toolbar-text">
302 <div class="toolbar-text">
295 </div>
303 </div>
296 </div>
304 </div>
297 </div>
305 </div>
298 </div>
306 </div>
299
307
300 <div class="comment-footer">
308 <div class="comment-footer">
301 </div>
309 </div>
302
310
303 </div>
311 </div>
304 % endif
312 % endif
305
313
306 <script type="text/javascript">
314 <script type="text/javascript">
307 bindToggleButtons();
315 bindToggleButtons();
308 </script>
316 </script>
309 </div>
317 </div>
310 </%def>
318 </%def>
311
319
312
320
313 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
321 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
314 ## comment injected based on assumption that user is logged in
322 ## comment injected based on assumption that user is logged in
315
323
316 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
324 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
317
325
318 <div class="comment-area">
326 <div class="comment-area">
319 <div class="comment-area-header">
327 <div class="comment-area-header">
320 <ul class="nav-links clearfix">
328 <ul class="nav-links clearfix">
321 <li class="active">
329 <li class="active">
322 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
330 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
323 </li>
331 </li>
324 <li class="">
332 <li class="">
325 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
333 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
326 </li>
334 </li>
327 <li class="pull-right">
335 <li class="pull-right">
328 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
336 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
329 % for val in c.visual.comment_types:
337 % for val in c.visual.comment_types:
330 <option value="${val}">${val.upper()}</option>
338 <option value="${val}">${val.upper()}</option>
331 % endfor
339 % endfor
332 </select>
340 </select>
333 </li>
341 </li>
334 </ul>
342 </ul>
335 </div>
343 </div>
336
344
337 <div class="comment-area-write" style="display: block;">
345 <div class="comment-area-write" style="display: block;">
338 <div id="edit-container_${lineno_id}">
346 <div id="edit-container_${lineno_id}">
339 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
347 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
340 </div>
348 </div>
341 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
349 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
342 <div id="preview-box_${lineno_id}" class="preview-box"></div>
350 <div id="preview-box_${lineno_id}" class="preview-box"></div>
343 </div>
351 </div>
344 </div>
352 </div>
345
353
346 <div class="comment-area-footer">
354 <div class="comment-area-footer">
347 <div class="toolbar">
355 <div class="toolbar">
348 <div class="toolbar-text">
356 <div class="toolbar-text">
349 ${(_('Comments parsed using %s syntax with %s support.') % (
357 ${(_('Comments parsed using %s syntax with %s support.') % (
350 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
358 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
351 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
359 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
352 )
360 )
353 )|n}
361 )|n}
354 </div>
362 </div>
355 </div>
363 </div>
356 </div>
364 </div>
357 </div>
365 </div>
358
366
359 <div class="comment-footer">
367 <div class="comment-footer">
360
368
361 % if review_statuses:
369 % if review_statuses:
362 <div class="status_box">
370 <div class="status_box">
363 <select id="change_status_${lineno_id}" name="changeset_status">
371 <select id="change_status_${lineno_id}" name="changeset_status">
364 <option></option> ## Placeholder
372 <option></option> ## Placeholder
365 % for status, lbl in review_statuses:
373 % for status, lbl in review_statuses:
366 <option value="${status}" data-status="${status}">${lbl}</option>
374 <option value="${status}" data-status="${status}">${lbl}</option>
367 %if is_pull_request and change_status and status in ('approved', 'rejected'):
375 %if is_pull_request and change_status and status in ('approved', 'rejected'):
368 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
376 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
369 %endif
377 %endif
370 % endfor
378 % endfor
371 </select>
379 </select>
372 </div>
380 </div>
373 % endif
381 % endif
374
382
375 ## inject extra inputs into the form
383 ## inject extra inputs into the form
376 % if form_extras and isinstance(form_extras, (list, tuple)):
384 % if form_extras and isinstance(form_extras, (list, tuple)):
377 <div id="comment_form_extras">
385 <div id="comment_form_extras">
378 % for form_ex_el in form_extras:
386 % for form_ex_el in form_extras:
379 ${form_ex_el|n}
387 ${form_ex_el|n}
380 % endfor
388 % endfor
381 </div>
389 </div>
382 % endif
390 % endif
383
391
384 <div class="action-buttons">
392 <div class="action-buttons">
385 ## inline for has a file, and line-number together with cancel hide button.
393 ## inline for has a file, and line-number together with cancel hide button.
386 % if form_type == 'inline':
394 % if form_type == 'inline':
387 <input type="hidden" name="f_path" value="{0}">
395 <input type="hidden" name="f_path" value="{0}">
388 <input type="hidden" name="line" value="${lineno_id}">
396 <input type="hidden" name="line" value="${lineno_id}">
389 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
397 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
390 ${_('Cancel')}
398 ${_('Cancel')}
391 </button>
399 </button>
392 % endif
400 % endif
393 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
401 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
394
402
395 </div>
403 </div>
396 </div>
404 </div>
397
405
398 </form>
406 </form>
399
407
400 </%def> No newline at end of file
408 </%def>
@@ -1,647 +1,692 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()">
10 <%def name="breadcrumbs_links()">
11 <span id="pr-title">
11 <span id="pr-title">
12 ${c.pull_request.title}
12 ${c.pull_request.title}
13 %if c.pull_request.is_closed():
13 %if c.pull_request.is_closed():
14 (${_('Closed')})
14 (${_('Closed')})
15 %endif
15 %endif
16 </span>
16 </span>
17 <div id="pr-title-edit" class="input" style="display: none;">
17 <div id="pr-title-edit" class="input" style="display: none;">
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 </div>
19 </div>
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_nav()">
22 <%def name="menu_bar_nav()">
23 ${self.menu_items(active='repositories')}
23 ${self.menu_items(active='repositories')}
24 </%def>
24 </%def>
25
25
26 <%def name="menu_bar_subnav()">
26 <%def name="menu_bar_subnav()">
27 ${self.repo_menu(active='showpullrequest')}
27 ${self.repo_menu(active='showpullrequest')}
28 </%def>
28 </%def>
29
29
30 <%def name="main()">
30 <%def name="main()">
31
31
32 <script type="text/javascript">
32 <script type="text/javascript">
33 // TODO: marcink switch this to pyroutes
33 // TODO: marcink switch this to pyroutes
34 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
34 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 </script>
36 </script>
37 <div class="box">
37 <div class="box">
38
38 <div class="title">
39 <div class="title">
39 ${self.repo_page_title(c.rhodecode_db_repo)}
40 ${self.repo_page_title(c.rhodecode_db_repo)}
40 </div>
41 </div>
41
42
42 ${self.breadcrumbs()}
43 ${self.breadcrumbs()}
43
44
44 <div class="box pr-summary">
45 <div class="box pr-summary">
46
45 <div class="summary-details block-left">
47 <div class="summary-details block-left">
46 <% summary = lambda n:{False:'summary-short'}.get(n) %>
48 <% summary = lambda n:{False:'summary-short'}.get(n) %>
47 <div class="pr-details-title">
49 <div class="pr-details-title">
48 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
50 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 %if c.allowed_to_update:
51 %if c.allowed_to_update:
50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
52 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 % if c.allowed_to_delete:
53 % if c.allowed_to_delete:
52 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
54 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
55 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
54 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
56 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
55 ${h.end_form()}
57 ${h.end_form()}
56 % else:
58 % else:
57 ${_('Delete')}
59 ${_('Delete')}
58 % endif
60 % endif
59 </div>
61 </div>
60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
62 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
63 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
62 %endif
64 %endif
63 </div>
65 </div>
64
66
65 <div id="summary" class="fields pr-details-content">
67 <div id="summary" class="fields pr-details-content">
66 <div class="field">
68 <div class="field">
67 <div class="label-summary">
69 <div class="label-summary">
68 <label>${_('Origin')}:</label>
70 <label>${_('Origin')}:</label>
69 </div>
71 </div>
70 <div class="input">
72 <div class="input">
71 <div class="pr-origininfo">
73 <div class="pr-origininfo">
72 ## branch link is only valid if it is a branch
74 ## branch link is only valid if it is a branch
73 <span class="tag">
75 <span class="tag">
74 %if c.pull_request.source_ref_parts.type == 'branch':
76 %if c.pull_request.source_ref_parts.type == 'branch':
75 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
77 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
76 %else:
78 %else:
77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
79 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
78 %endif
80 %endif
79 </span>
81 </span>
80 <span class="clone-url">
82 <span class="clone-url">
81 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
83 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
82 </span>
84 </span>
83 </div>
85 </div>
84 <div class="pr-pullinfo">
86 <div class="pr-pullinfo">
85 %if h.is_hg(c.pull_request.source_repo):
87 %if h.is_hg(c.pull_request.source_repo):
86 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
88 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
87 %elif h.is_git(c.pull_request.source_repo):
89 %elif h.is_git(c.pull_request.source_repo):
88 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
90 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
89 %endif
91 %endif
90 </div>
92 </div>
91 </div>
93 </div>
92 </div>
94 </div>
93 <div class="field">
95 <div class="field">
94 <div class="label-summary">
96 <div class="label-summary">
95 <label>${_('Target')}:</label>
97 <label>${_('Target')}:</label>
96 </div>
98 </div>
97 <div class="input">
99 <div class="input">
98 <div class="pr-targetinfo">
100 <div class="pr-targetinfo">
99 ## branch link is only valid if it is a branch
101 ## branch link is only valid if it is a branch
100 <span class="tag">
102 <span class="tag">
101 %if c.pull_request.target_ref_parts.type == 'branch':
103 %if c.pull_request.target_ref_parts.type == 'branch':
102 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
104 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
103 %else:
105 %else:
104 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
106 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
105 %endif
107 %endif
106 </span>
108 </span>
107 <span class="clone-url">
109 <span class="clone-url">
108 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
110 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
109 </span>
111 </span>
110 </div>
112 </div>
111 </div>
113 </div>
112 </div>
114 </div>
113
115
114 ## Link to the shadow repository.
116 ## Link to the shadow repository.
115 <div class="field">
117 <div class="field">
116 <div class="label-summary">
118 <div class="label-summary">
117 <label>${_('Merge')}:</label>
119 <label>${_('Merge')}:</label>
118 </div>
120 </div>
119 <div class="input">
121 <div class="input">
120 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
122 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
121 <div class="pr-mergeinfo">
123 <div class="pr-mergeinfo">
122 %if h.is_hg(c.pull_request.target_repo):
124 %if h.is_hg(c.pull_request.target_repo):
123 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
125 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
124 %elif h.is_git(c.pull_request.target_repo):
126 %elif h.is_git(c.pull_request.target_repo):
125 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
127 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
126 %endif
128 %endif
127 </div>
129 </div>
128 % else:
130 % else:
129 <div class="">
131 <div class="">
130 ${_('Shadow repository data not available')}.
132 ${_('Shadow repository data not available')}.
131 </div>
133 </div>
132 % endif
134 % endif
133 </div>
135 </div>
134 </div>
136 </div>
135
137
136 <div class="field">
138 <div class="field">
137 <div class="label-summary">
139 <div class="label-summary">
138 <label>${_('Review')}:</label>
140 <label>${_('Review')}:</label>
139 </div>
141 </div>
140 <div class="input">
142 <div class="input">
141 %if c.pull_request_review_status:
143 %if c.pull_request_review_status:
142 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
144 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
143 <span class="changeset-status-lbl tooltip">
145 <span class="changeset-status-lbl tooltip">
144 %if c.pull_request.is_closed():
146 %if c.pull_request.is_closed():
145 ${_('Closed')},
147 ${_('Closed')},
146 %endif
148 %endif
147 ${h.commit_status_lbl(c.pull_request_review_status)}
149 ${h.commit_status_lbl(c.pull_request_review_status)}
148 </span>
150 </span>
149 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
151 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
150 %endif
152 %endif
151 </div>
153 </div>
152 </div>
154 </div>
153 <div class="field">
155 <div class="field">
154 <div class="pr-description-label label-summary">
156 <div class="pr-description-label label-summary">
155 <label>${_('Description')}:</label>
157 <label>${_('Description')}:</label>
156 </div>
158 </div>
157 <div id="pr-desc" class="input">
159 <div id="pr-desc" class="input">
158 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
160 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
159 </div>
161 </div>
160 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
162 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
161 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
163 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
162 </div>
164 </div>
163 </div>
165 </div>
164
166
165 <div class="field">
167 <div class="field">
166 <div class="label-summary">
168 <div class="label-summary">
167 <label>${_('Versions')} (${len(c.versions)+1}):</label>
169 <label>${_('Versions')} (${len(c.versions)+1}):</label>
168 </div>
170 </div>
169
171
170 <div class="pr-versions">
172 <div class="pr-versions">
171 % if c.show_version_changes:
173 % if c.show_version_changes:
172 <table>
174 <table>
173 ## CURRENTLY SELECT PR VERSION
175 ## CURRENTLY SELECT PR VERSION
174 <tr class="version-pr" style="display: ${'' if c.at_version_num is None else 'none'}">
176 <tr class="version-pr" style="display: ${'' if c.at_version_num is None else 'none'}">
175 <td>
177 <td>
176 % if c.at_version in [None, 'latest']:
178 % if c.at_version_num is None:
177 <i class="icon-ok link"></i>
179 <i class="icon-ok link"></i>
178 % else:
180 % else:
179 <i class="icon-comment"></i> <code>${len(c.inline_versions[None])}</code>
181 <i class="icon-comment"></i>
182 <code>
183 ${len(c.comment_versions[None]['at'])}/${len(c.inline_versions[None]['at'])}
184 </code>
180 % endif
185 % endif
181 </td>
186 </td>
182 <td>
187 <td>
183 <code>
188 <code>
184 % if c.versions:
189 % if c.versions:
185 <a href="${h.url.current(version='latest')}">${_('latest')}</a>
190 <a href="${h.url.current(version='latest')}">${_('latest')}</a>
186 % else:
191 % else:
187 ${_('initial')}
192 ${_('initial')}
188 % endif
193 % endif
189 </code>
194 </code>
190 </td>
195 </td>
191 <td>
196 <td>
192 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
197 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
193 </td>
198 </td>
194 <td>
199 <td>
195 ${_('created')} ${h.age_component(c.pull_request_latest.updated_on)}
200 ${_('created')} ${h.age_component(c.pull_request_latest.updated_on)}
196 </td>
201 </td>
197 <td align="right">
202 <td align="right">
198 % if c.versions and c.at_version_num in [None, 'latest']:
203 % if c.versions and c.at_version_num in [None, 'latest']:
199 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
204 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
200 % endif
205 % endif
201 </td>
206 </td>
202 </tr>
207 </tr>
203
208
204 ## SHOW ALL VERSIONS OF PR
209 ## SHOW ALL VERSIONS OF PR
205 <% ver_pr = None %>
210 <% ver_pr = None %>
211
206 % for data in reversed(list(enumerate(c.versions, 1))):
212 % for data in reversed(list(enumerate(c.versions, 1))):
207 <% ver_pos = data[0] %>
213 <% ver_pos = data[0] %>
208 <% ver = data[1] %>
214 <% ver = data[1] %>
209 <% ver_pr = ver.pull_request_version_id %>
215 <% ver_pr = ver.pull_request_version_id %>
210
216
211 <tr class="version-pr" style="display: ${'' if c.at_version == ver_pr else 'none'}">
217 <tr class="version-pr" style="display: ${'' if c.at_version_num == ver_pr else 'none'}">
212 <td>
218 <td>
213 % if c.at_version == ver_pr:
219 % if c.at_version_num == ver_pr:
214 <i class="icon-ok link"></i>
220 <i class="icon-ok link"></i>
215 % else:
221 % else:
216 <i class="icon-comment"></i> <code>${len(c.inline_versions[ver_pr])}</code>
222 <i class="icon-comment"></i>
217 % endif
223 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
218 </td>
224 ${len(c.comment_versions[ver_pr]['at'])}/${len(c.inline_versions[ver_pr]['at'])}
219 <td>
225 </code>
220 <code class="tooltip" title="${_('Comment from pull request version {0}').format(ver_pos)}">
226 % endif
221 <a href="${h.url.current(version=ver_pr)}">v${ver_pos}</a>
227 </td>
222 </code>
228 <td>
223 </td>
229 <code>
224 <td>
230 <a href="${h.url.current(version=ver_pr)}">v${ver_pos}</a>
225 <code>${ver.source_ref_parts.commit_id[:6]}</code>
231 </code>
226 </td>
232 </td>
227 <td>
233 <td>
228 ${_('created')} ${h.age_component(ver.updated_on)}
234 <code>${ver.source_ref_parts.commit_id[:6]}</code>
229 </td>
235 </td>
230 <td align="right">
236 <td>
231 % if c.at_version == ver_pr:
237 ${_('created')} ${h.age_component(ver.updated_on)}
232 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
238 </td>
233 % endif
239 <td align="right">
234 </td>
240 % if c.at_version_num == ver_pr:
235 </tr>
241 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
242 % endif
243 </td>
244 </tr>
236 % endfor
245 % endfor
237
246
238 ## show comment/inline comments summary
247 ## show comment/inline comments summary
239 <tr>
248 <tr>
240 <td>
249 <td>
241 </td>
250 </td>
242
251
243 <% inline_comm_count_ver = len(c.inline_versions[ver_pr])%>
244 <td colspan="4" style="border-top: 1px dashed #dbd9da">
252 <td colspan="4" style="border-top: 1px dashed #dbd9da">
245 ${_('Comments for this version')}:
253 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
246 %if c.comments:
254 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
247 <a href="#comments">${_("%d General ") % len(c.comments)}</a>
255
256
257 % if c.at_version:
258 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
259 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
260 ${_('Comments at this version')}:
261 % else:
262 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
263 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
264 ${_('Comments for this pull request')}:
265 % endif
266
267 %if general_comm_count_ver:
268 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
248 %else:
269 %else:
249 ${_("%d General ") % len(c.comments)}
270 ${_("%d General ") % general_comm_count_ver}
250 %endif
271 %endif
251
272
252 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num])%>
253 %if inline_comm_count_ver:
273 %if inline_comm_count_ver:
254 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
274 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
255 %else:
275 %else:
256 , ${_("%d Inline") % inline_comm_count_ver}
276 , ${_("%d Inline") % inline_comm_count_ver}
257 %endif
277 %endif
258
278
259 %if c.outdated_cnt:
279 %if outdated_comm_count_ver:
260 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % c.outdated_cnt}</a>
280 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
261 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
281 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
262 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
282 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
263 %else:
283 %else:
264 , ${_("%d Outdated") % c.outdated_cnt}
284 , ${_("%d Outdated") % outdated_comm_count_ver}
265 %endif
285 %endif
266 </td>
286 </td>
267 </tr>
287 </tr>
268
288
269 <tr>
289 <tr>
270 <td></td>
290 <td></td>
271 <td colspan="4">
291 <td colspan="4">
272 % if c.at_version:
292 % if c.at_version:
273 <pre>
293 <pre>
274 Changed commits:
294 Changed commits:
275 * added: ${len(c.changes.added)}
295 * added: ${len(c.changes.added)}
276 * removed: ${len(c.changes.removed)}
296 * removed: ${len(c.changes.removed)}
277
297
278 % if not (c.file_changes.added+c.file_changes.modified+c.file_changes.removed):
298 % if not (c.file_changes.added+c.file_changes.modified+c.file_changes.removed):
279 No file changes found
299 No file changes found
280 % else:
300 % else:
281 Changed files:
301 Changed files:
282 %for file_name in c.file_changes.added:
302 %for file_name in c.file_changes.added:
283 * A <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
303 * A <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
284 %endfor
304 %endfor
285 %for file_name in c.file_changes.modified:
305 %for file_name in c.file_changes.modified:
286 * M <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
306 * M <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
287 %endfor
307 %endfor
288 %for file_name in c.file_changes.removed:
308 %for file_name in c.file_changes.removed:
289 * R ${file_name}
309 * R ${file_name}
290 %endfor
310 %endfor
291 % endif
311 % endif
292 </pre>
312 </pre>
293 % endif
313 % endif
294 </td>
314 </td>
295 </tr>
315 </tr>
296 </table>
316 </table>
297 % else:
317 % else:
298 ${_('Pull request versions not available')}.
318 ${_('Pull request versions not available')}.
299 % endif
319 % endif
300 </div>
320 </div>
301 </div>
321 </div>
302
322
303 <div id="pr-save" class="field" style="display: none;">
323 <div id="pr-save" class="field" style="display: none;">
304 <div class="label-summary"></div>
324 <div class="label-summary"></div>
305 <div class="input">
325 <div class="input">
306 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
326 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
307 </div>
327 </div>
308 </div>
328 </div>
309 </div>
329 </div>
310 </div>
330 </div>
311 <div>
331 <div>
312 ## AUTHOR
332 ## AUTHOR
313 <div class="reviewers-title block-right">
333 <div class="reviewers-title block-right">
314 <div class="pr-details-title">
334 <div class="pr-details-title">
315 ${_('Author')}
335 ${_('Author')}
316 </div>
336 </div>
317 </div>
337 </div>
318 <div class="block-right pr-details-content reviewers">
338 <div class="block-right pr-details-content reviewers">
319 <ul class="group_members">
339 <ul class="group_members">
320 <li>
340 <li>
321 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
341 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
322 </li>
342 </li>
323 </ul>
343 </ul>
324 </div>
344 </div>
325 ## REVIEWERS
345 ## REVIEWERS
326 <div class="reviewers-title block-right">
346 <div class="reviewers-title block-right">
327 <div class="pr-details-title">
347 <div class="pr-details-title">
328 ${_('Pull request reviewers')}
348 ${_('Pull request reviewers')}
329 %if c.allowed_to_update:
349 %if c.allowed_to_update:
330 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
350 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
331 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
351 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
332 %endif
352 %endif
333 </div>
353 </div>
334 </div>
354 </div>
335 <div id="reviewers" class="block-right pr-details-content reviewers">
355 <div id="reviewers" class="block-right pr-details-content reviewers">
336 ## members goes here !
356 ## members goes here !
337 <input type="hidden" name="__start__" value="review_members:sequence">
357 <input type="hidden" name="__start__" value="review_members:sequence">
338 <ul id="review_members" class="group_members">
358 <ul id="review_members" class="group_members">
339 %for member,reasons,status in c.pull_request_reviewers:
359 %for member,reasons,status in c.pull_request_reviewers:
340 <li id="reviewer_${member.user_id}">
360 <li id="reviewer_${member.user_id}">
341 <div class="reviewers_member">
361 <div class="reviewers_member">
342 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
362 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
343 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
363 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
344 </div>
364 </div>
345 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
365 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
346 ${self.gravatar_with_user(member.email, 16)}
366 ${self.gravatar_with_user(member.email, 16)}
347 </div>
367 </div>
348 <input type="hidden" name="__start__" value="reviewer:mapping">
368 <input type="hidden" name="__start__" value="reviewer:mapping">
349 <input type="hidden" name="__start__" value="reasons:sequence">
369 <input type="hidden" name="__start__" value="reasons:sequence">
350 %for reason in reasons:
370 %for reason in reasons:
351 <div class="reviewer_reason">- ${reason}</div>
371 <div class="reviewer_reason">- ${reason}</div>
352 <input type="hidden" name="reason" value="${reason}">
372 <input type="hidden" name="reason" value="${reason}">
353
373
354 %endfor
374 %endfor
355 <input type="hidden" name="__end__" value="reasons:sequence">
375 <input type="hidden" name="__end__" value="reasons:sequence">
356 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
376 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
357 <input type="hidden" name="__end__" value="reviewer:mapping">
377 <input type="hidden" name="__end__" value="reviewer:mapping">
358 %if c.allowed_to_update:
378 %if c.allowed_to_update:
359 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
379 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
360 <i class="icon-remove-sign" ></i>
380 <i class="icon-remove-sign" ></i>
361 </div>
381 </div>
362 %endif
382 %endif
363 </div>
383 </div>
364 </li>
384 </li>
365 %endfor
385 %endfor
366 </ul>
386 </ul>
367 <input type="hidden" name="__end__" value="review_members:sequence">
387 <input type="hidden" name="__end__" value="review_members:sequence">
368 %if not c.pull_request.is_closed():
388 %if not c.pull_request.is_closed():
369 <div id="add_reviewer_input" class='ac' style="display: none;">
389 <div id="add_reviewer_input" class='ac' style="display: none;">
370 %if c.allowed_to_update:
390 %if c.allowed_to_update:
371 <div class="reviewer_ac">
391 <div class="reviewer_ac">
372 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
373 <div id="reviewers_container"></div>
393 <div id="reviewers_container"></div>
374 </div>
394 </div>
375 <div>
395 <div>
376 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
396 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
377 </div>
397 </div>
378 %endif
398 %endif
379 </div>
399 </div>
380 %endif
400 %endif
381 </div>
401 </div>
382 </div>
402 </div>
383 </div>
403 </div>
384 <div class="box">
404 <div class="box">
385 ##DIFF
405 ##DIFF
386 <div class="table" >
406 <div class="table" >
387 <div id="changeset_compare_view_content">
407 <div id="changeset_compare_view_content">
388 ##CS
408 ##CS
389 % if c.missing_requirements:
409 % if c.missing_requirements:
390 <div class="box">
410 <div class="box">
391 <div class="alert alert-warning">
411 <div class="alert alert-warning">
392 <div>
412 <div>
393 <strong>${_('Missing requirements:')}</strong>
413 <strong>${_('Missing requirements:')}</strong>
394 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
414 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
395 </div>
415 </div>
396 </div>
416 </div>
397 </div>
417 </div>
398 % elif c.missing_commits:
418 % elif c.missing_commits:
399 <div class="box">
419 <div class="box">
400 <div class="alert alert-warning">
420 <div class="alert alert-warning">
401 <div>
421 <div>
402 <strong>${_('Missing commits')}:</strong>
422 <strong>${_('Missing commits')}:</strong>
403 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
423 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
404 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
424 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
405 </div>
425 </div>
406 </div>
426 </div>
407 </div>
427 </div>
408 % endif
428 % endif
409 <div class="compare_view_commits_title">
429 <div class="compare_view_commits_title">
410
430
411 <div class="pull-left">
431 <div class="pull-left">
412 <div class="btn-group">
432 <div class="btn-group">
413 <a
433 <a
414 class="btn"
434 class="btn"
415 href="#"
435 href="#"
416 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
436 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
417 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
437 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
418 </a>
438 </a>
419 <a
439 <a
420 class="btn"
440 class="btn"
421 href="#"
441 href="#"
422 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
442 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
423 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
443 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
424 </a>
444 </a>
425 </div>
445 </div>
426 </div>
446 </div>
427
447
428 <div class="pull-right">
448 <div class="pull-right">
429 % if c.allowed_to_update and not c.pull_request.is_closed():
449 % if c.allowed_to_update and not c.pull_request.is_closed():
430 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
450 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
431 % else:
451 % else:
432 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
452 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
433 % endif
453 % endif
434
454
435 </div>
455 </div>
436
456
437 </div>
457 </div>
438
458
439 % if not c.missing_commits:
459 % if not c.missing_commits:
440 <%include file="/compare/compare_commits.mako" />
460 <%include file="/compare/compare_commits.mako" />
441 <div class="cs_files">
461 <div class="cs_files">
442 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
462 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
443 ${cbdiffs.render_diffset_menu()}
463 ${cbdiffs.render_diffset_menu()}
444 ${cbdiffs.render_diffset(
464 ${cbdiffs.render_diffset(
445 c.diffset, use_comments=True,
465 c.diffset, use_comments=True,
446 collapse_when_files_over=30,
466 collapse_when_files_over=30,
447 disable_new_comments=not c.allowed_to_comment,
467 disable_new_comments=not c.allowed_to_comment,
448 deleted_files_comments=c.deleted_files_comments)}
468 deleted_files_comments=c.deleted_files_comments)}
449 </div>
469 </div>
450 % else:
470 % else:
451 ## skipping commits we need to clear the view for missing commits
471 ## skipping commits we need to clear the view for missing commits
452 <div style="clear:both;"></div>
472 <div style="clear:both;"></div>
453 % endif
473 % endif
454
474
455 </div>
475 </div>
456 </div>
476 </div>
457
477
458 ## template for inline comment form
478 ## template for inline comment form
459 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
479 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
460
480
461 ## render general comments
481 ## render general comments
462 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
482
483 <div id="comment-tr-show">
484 <div class="comment">
485 <div class="meta">
486 % if general_outdated_comm_count_ver:
487 % if general_outdated_comm_count_ver == 1:
488 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
489 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
490 % else:
491 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
492 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
493 % endif
494 % endif
495 </div>
496 </div>
497 </div>
498
499 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
463
500
464 % if not c.pull_request.is_closed():
501 % if not c.pull_request.is_closed():
465 ## main comment form and it status
502 ## main comment form and it status
466 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
503 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
467 pull_request_id=c.pull_request.pull_request_id),
504 pull_request_id=c.pull_request.pull_request_id),
468 c.pull_request_review_status,
505 c.pull_request_review_status,
469 is_pull_request=True, change_status=c.allowed_to_change_status)}
506 is_pull_request=True, change_status=c.allowed_to_change_status)}
470 %endif
507 %endif
471
508
472 <script type="text/javascript">
509 <script type="text/javascript">
473 if (location.hash) {
510 if (location.hash) {
474 var result = splitDelimitedHash(location.hash);
511 var result = splitDelimitedHash(location.hash);
475 var line = $('html').find(result.loc);
512 var line = $('html').find(result.loc);
513 // show hidden comments if we use location.hash
514 if (line.hasClass('comment-general')) {
515 $(line).show();
516 } else if (line.hasClass('comment-inline')) {
517 $(line).show();
518 var $cb = $(line).closest('.cb');
519 $cb.removeClass('cb-collapsed')
520 }
476 if (line.length > 0){
521 if (line.length > 0){
477 offsetScroll(line, 70);
522 offsetScroll(line, 70);
478 }
523 }
479 }
524 }
525
480 $(function(){
526 $(function(){
481 ReviewerAutoComplete('user');
527 ReviewerAutoComplete('user');
482 // custom code mirror
528 // custom code mirror
483 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
529 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
484
530
485 var PRDetails = {
531 var PRDetails = {
486 editButton: $('#open_edit_pullrequest'),
532 editButton: $('#open_edit_pullrequest'),
487 closeButton: $('#close_edit_pullrequest'),
533 closeButton: $('#close_edit_pullrequest'),
488 deleteButton: $('#delete_pullrequest'),
534 deleteButton: $('#delete_pullrequest'),
489 viewFields: $('#pr-desc, #pr-title'),
535 viewFields: $('#pr-desc, #pr-title'),
490 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
536 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
491
537
492 init: function() {
538 init: function() {
493 var that = this;
539 var that = this;
494 this.editButton.on('click', function(e) { that.edit(); });
540 this.editButton.on('click', function(e) { that.edit(); });
495 this.closeButton.on('click', function(e) { that.view(); });
541 this.closeButton.on('click', function(e) { that.view(); });
496 },
542 },
497
543
498 edit: function(event) {
544 edit: function(event) {
499 this.viewFields.hide();
545 this.viewFields.hide();
500 this.editButton.hide();
546 this.editButton.hide();
501 this.deleteButton.hide();
547 this.deleteButton.hide();
502 this.closeButton.show();
548 this.closeButton.show();
503 this.editFields.show();
549 this.editFields.show();
504 codeMirrorInstance.refresh();
550 codeMirrorInstance.refresh();
505 },
551 },
506
552
507 view: function(event) {
553 view: function(event) {
508 this.editButton.show();
554 this.editButton.show();
509 this.deleteButton.show();
555 this.deleteButton.show();
510 this.editFields.hide();
556 this.editFields.hide();
511 this.closeButton.hide();
557 this.closeButton.hide();
512 this.viewFields.show();
558 this.viewFields.show();
513 }
559 }
514 };
560 };
515
561
516 var ReviewersPanel = {
562 var ReviewersPanel = {
517 editButton: $('#open_edit_reviewers'),
563 editButton: $('#open_edit_reviewers'),
518 closeButton: $('#close_edit_reviewers'),
564 closeButton: $('#close_edit_reviewers'),
519 addButton: $('#add_reviewer_input'),
565 addButton: $('#add_reviewer_input'),
520 removeButtons: $('.reviewer_member_remove'),
566 removeButtons: $('.reviewer_member_remove'),
521
567
522 init: function() {
568 init: function() {
523 var that = this;
569 var that = this;
524 this.editButton.on('click', function(e) { that.edit(); });
570 this.editButton.on('click', function(e) { that.edit(); });
525 this.closeButton.on('click', function(e) { that.close(); });
571 this.closeButton.on('click', function(e) { that.close(); });
526 },
572 },
527
573
528 edit: function(event) {
574 edit: function(event) {
529 this.editButton.hide();
575 this.editButton.hide();
530 this.closeButton.show();
576 this.closeButton.show();
531 this.addButton.show();
577 this.addButton.show();
532 this.removeButtons.css('visibility', 'visible');
578 this.removeButtons.css('visibility', 'visible');
533 },
579 },
534
580
535 close: function(event) {
581 close: function(event) {
536 this.editButton.show();
582 this.editButton.show();
537 this.closeButton.hide();
583 this.closeButton.hide();
538 this.addButton.hide();
584 this.addButton.hide();
539 this.removeButtons.css('visibility', 'hidden');
585 this.removeButtons.css('visibility', 'hidden');
540 }
586 }
541 };
587 };
542
588
543 PRDetails.init();
589 PRDetails.init();
544 ReviewersPanel.init();
590 ReviewersPanel.init();
545
591
546 showOutdated = function(self){
592 showOutdated = function(self){
547 $('.comment-outdated').show();
593 $('.comment-inline.comment-outdated').show();
548 $('.filediff-outdated').show();
594 $('.filediff-outdated').show();
549 $('.showOutdatedComments').hide();
595 $('.showOutdatedComments').hide();
550 $('.hideOutdatedComments').show();
596 $('.hideOutdatedComments').show();
551
552 };
597 };
553
598
554 hideOutdated = function(self){
599 hideOutdated = function(self){
555 $('.comment-outdated').hide();
600 $('.comment-inline.comment-outdated').hide();
556 $('.filediff-outdated').hide();
601 $('.filediff-outdated').hide();
557 $('.hideOutdatedComments').hide();
602 $('.hideOutdatedComments').hide();
558 $('.showOutdatedComments').show();
603 $('.showOutdatedComments').show();
559 };
604 };
560
605
561 $('#show-outdated-comments').on('click', function(e){
606 $('#show-outdated-comments').on('click', function(e){
562 var button = $(this);
607 var button = $(this);
563 var outdated = $('.comment-outdated');
608 var outdated = $('.comment-outdated');
564
609
565 if (button.html() === "(Show)") {
610 if (button.html() === "(Show)") {
566 button.html("(Hide)");
611 button.html("(Hide)");
567 outdated.show();
612 outdated.show();
568 } else {
613 } else {
569 button.html("(Show)");
614 button.html("(Show)");
570 outdated.hide();
615 outdated.hide();
571 }
616 }
572 });
617 });
573
618
574 $('.show-inline-comments').on('change', function(e){
619 $('.show-inline-comments').on('change', function(e){
575 var show = 'none';
620 var show = 'none';
576 var target = e.currentTarget;
621 var target = e.currentTarget;
577 if(target.checked){
622 if(target.checked){
578 show = ''
623 show = ''
579 }
624 }
580 var boxid = $(target).attr('id_for');
625 var boxid = $(target).attr('id_for');
581 var comments = $('#{0} .inline-comments'.format(boxid));
626 var comments = $('#{0} .inline-comments'.format(boxid));
582 var fn_display = function(idx){
627 var fn_display = function(idx){
583 $(this).css('display', show);
628 $(this).css('display', show);
584 };
629 };
585 $(comments).each(fn_display);
630 $(comments).each(fn_display);
586 var btns = $('#{0} .inline-comments-button'.format(boxid));
631 var btns = $('#{0} .inline-comments-button'.format(boxid));
587 $(btns).each(fn_display);
632 $(btns).each(fn_display);
588 });
633 });
589
634
590 $('#merge_pull_request_form').submit(function() {
635 $('#merge_pull_request_form').submit(function() {
591 if (!$('#merge_pull_request').attr('disabled')) {
636 if (!$('#merge_pull_request').attr('disabled')) {
592 $('#merge_pull_request').attr('disabled', 'disabled');
637 $('#merge_pull_request').attr('disabled', 'disabled');
593 }
638 }
594 return true;
639 return true;
595 });
640 });
596
641
597 $('#edit_pull_request').on('click', function(e){
642 $('#edit_pull_request').on('click', function(e){
598 var title = $('#pr-title-input').val();
643 var title = $('#pr-title-input').val();
599 var description = codeMirrorInstance.getValue();
644 var description = codeMirrorInstance.getValue();
600 editPullRequest(
645 editPullRequest(
601 "${c.repo_name}", "${c.pull_request.pull_request_id}",
646 "${c.repo_name}", "${c.pull_request.pull_request_id}",
602 title, description);
647 title, description);
603 });
648 });
604
649
605 $('#update_pull_request').on('click', function(e){
650 $('#update_pull_request').on('click', function(e){
606 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
651 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
607 });
652 });
608
653
609 $('#update_commits').on('click', function(e){
654 $('#update_commits').on('click', function(e){
610 var isDisabled = !$(e.currentTarget).attr('disabled');
655 var isDisabled = !$(e.currentTarget).attr('disabled');
611 $(e.currentTarget).text(_gettext('Updating...'));
656 $(e.currentTarget).text(_gettext('Updating...'));
612 $(e.currentTarget).attr('disabled', 'disabled');
657 $(e.currentTarget).attr('disabled', 'disabled');
613 if(isDisabled){
658 if(isDisabled){
614 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
659 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
615 }
660 }
616
661
617 });
662 });
618 // fixing issue with caches on firefox
663 // fixing issue with caches on firefox
619 $('#update_commits').removeAttr("disabled");
664 $('#update_commits').removeAttr("disabled");
620
665
621 $('#close_pull_request').on('click', function(e){
666 $('#close_pull_request').on('click', function(e){
622 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
667 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
623 });
668 });
624
669
625 $('.show-inline-comments').on('click', function(e){
670 $('.show-inline-comments').on('click', function(e){
626 var boxid = $(this).attr('data-comment-id');
671 var boxid = $(this).attr('data-comment-id');
627 var button = $(this);
672 var button = $(this);
628
673
629 if(button.hasClass("comments-visible")) {
674 if(button.hasClass("comments-visible")) {
630 $('#{0} .inline-comments'.format(boxid)).each(function(index){
675 $('#{0} .inline-comments'.format(boxid)).each(function(index){
631 $(this).hide();
676 $(this).hide();
632 });
677 });
633 button.removeClass("comments-visible");
678 button.removeClass("comments-visible");
634 } else {
679 } else {
635 $('#{0} .inline-comments'.format(boxid)).each(function(index){
680 $('#{0} .inline-comments'.format(boxid)).each(function(index){
636 $(this).show();
681 $(this).show();
637 });
682 });
638 button.addClass("comments-visible");
683 button.addClass("comments-visible");
639 }
684 }
640 });
685 });
641 })
686 })
642 </script>
687 </script>
643
688
644 </div>
689 </div>
645 </div>
690 </div>
646
691
647 </%def>
692 </%def>
General Comments 0
You need to be logged in to leave comments. Login now