##// END OF EJS Templates
pr: Display link to shadow repository on pull request page.
Martin Bornhold -
r896:a4f1049a default
parent child Browse files
Show More
@@ -1,891 +1,893 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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
24
25 import peppercorn
25 import peppercorn
26 import formencode
26 import formencode
27 import logging
27 import logging
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 from pylons import request, tmpl_context as c, url
30 from pylons import request, tmpl_context as c, url
31 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
34 from sqlalchemy.sql import func
34 from sqlalchemy.sql import func
35 from sqlalchemy.sql.expression import or_
35 from sqlalchemy.sql.expression import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import auth, diffs, helpers as h
38 from rhodecode.lib import auth, diffs, helpers as h
39 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.base import (
40 from rhodecode.lib.base import (
41 BaseRepoController, render, vcs_operation_context)
41 BaseRepoController, render, vcs_operation_context)
42 from rhodecode.lib.auth import (
42 from rhodecode.lib.auth import (
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
44 HasAcceptedRepoType, XHRRequired)
44 HasAcceptedRepoType, XHRRequired)
45 from rhodecode.lib.channelstream import channelstream_request
45 from rhodecode.lib.channelstream import channelstream_request
46 from rhodecode.lib.utils import jsonify
46 from rhodecode.lib.utils import jsonify
47 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
47 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
48 from rhodecode.lib.vcs.backends.base import EmptyCommit
48 from rhodecode.lib.vcs.backends.base import EmptyCommit
49 from rhodecode.lib.vcs.exceptions import (
49 from rhodecode.lib.vcs.exceptions import (
50 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
50 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
51 from rhodecode.lib.diffs import LimitedDiffContainer
51 from rhodecode.lib.diffs import LimitedDiffContainer
52 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.changeset_status import ChangesetStatusModel
53 from rhodecode.model.comment import ChangesetCommentsModel
53 from rhodecode.model.comment import ChangesetCommentsModel
54 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
54 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
55 Repository
55 Repository
56 from rhodecode.model.forms import PullRequestForm
56 from rhodecode.model.forms import PullRequestForm
57 from rhodecode.model.meta import Session
57 from rhodecode.model.meta import Session
58 from rhodecode.model.pull_request import PullRequestModel
58 from rhodecode.model.pull_request import PullRequestModel
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 class PullrequestsController(BaseRepoController):
63 class PullrequestsController(BaseRepoController):
64 def __before__(self):
64 def __before__(self):
65 super(PullrequestsController, self).__before__()
65 super(PullrequestsController, self).__before__()
66
66
67 def _load_compare_data(self, pull_request, enable_comments=True):
67 def _load_compare_data(self, pull_request, enable_comments=True):
68 """
68 """
69 Load context data needed for generating compare diff
69 Load context data needed for generating compare diff
70
70
71 :param pull_request: object related to the request
71 :param pull_request: object related to the request
72 :param enable_comments: flag to determine if comments are included
72 :param enable_comments: flag to determine if comments are included
73 """
73 """
74 source_repo = pull_request.source_repo
74 source_repo = pull_request.source_repo
75 source_ref_id = pull_request.source_ref_parts.commit_id
75 source_ref_id = pull_request.source_ref_parts.commit_id
76
76
77 target_repo = pull_request.target_repo
77 target_repo = pull_request.target_repo
78 target_ref_id = pull_request.target_ref_parts.commit_id
78 target_ref_id = pull_request.target_ref_parts.commit_id
79
79
80 # despite opening commits for bookmarks/branches/tags, we always
80 # despite opening commits for bookmarks/branches/tags, we always
81 # convert this to rev to prevent changes after bookmark or branch change
81 # convert this to rev to prevent changes after bookmark or branch change
82 c.source_ref_type = 'rev'
82 c.source_ref_type = 'rev'
83 c.source_ref = source_ref_id
83 c.source_ref = source_ref_id
84
84
85 c.target_ref_type = 'rev'
85 c.target_ref_type = 'rev'
86 c.target_ref = target_ref_id
86 c.target_ref = target_ref_id
87
87
88 c.source_repo = source_repo
88 c.source_repo = source_repo
89 c.target_repo = target_repo
89 c.target_repo = target_repo
90
90
91 c.fulldiff = bool(request.GET.get('fulldiff'))
91 c.fulldiff = bool(request.GET.get('fulldiff'))
92
92
93 # diff_limit is the old behavior, will cut off the whole diff
93 # diff_limit is the old behavior, will cut off the whole diff
94 # if the limit is applied otherwise will just hide the
94 # if the limit is applied otherwise will just hide the
95 # big files from the front-end
95 # big files from the front-end
96 diff_limit = self.cut_off_limit_diff
96 diff_limit = self.cut_off_limit_diff
97 file_limit = self.cut_off_limit_file
97 file_limit = self.cut_off_limit_file
98
98
99 pre_load = ["author", "branch", "date", "message"]
99 pre_load = ["author", "branch", "date", "message"]
100
100
101 c.commit_ranges = []
101 c.commit_ranges = []
102 source_commit = EmptyCommit()
102 source_commit = EmptyCommit()
103 target_commit = EmptyCommit()
103 target_commit = EmptyCommit()
104 c.missing_requirements = False
104 c.missing_requirements = False
105 try:
105 try:
106 c.commit_ranges = [
106 c.commit_ranges = [
107 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
107 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
108 for rev in pull_request.revisions]
108 for rev in pull_request.revisions]
109
109
110 c.statuses = source_repo.statuses(
110 c.statuses = source_repo.statuses(
111 [x.raw_id for x in c.commit_ranges])
111 [x.raw_id for x in c.commit_ranges])
112
112
113 target_commit = source_repo.get_commit(
113 target_commit = source_repo.get_commit(
114 commit_id=safe_str(target_ref_id))
114 commit_id=safe_str(target_ref_id))
115 source_commit = source_repo.get_commit(
115 source_commit = source_repo.get_commit(
116 commit_id=safe_str(source_ref_id))
116 commit_id=safe_str(source_ref_id))
117 except RepositoryRequirementError:
117 except RepositoryRequirementError:
118 c.missing_requirements = True
118 c.missing_requirements = True
119
119
120 c.missing_commits = False
120 c.missing_commits = False
121 if (c.missing_requirements or
121 if (c.missing_requirements or
122 isinstance(source_commit, EmptyCommit) or
122 isinstance(source_commit, EmptyCommit) or
123 source_commit == target_commit):
123 source_commit == target_commit):
124 _parsed = []
124 _parsed = []
125 c.missing_commits = True
125 c.missing_commits = True
126 else:
126 else:
127 vcs_diff = PullRequestModel().get_diff(pull_request)
127 vcs_diff = PullRequestModel().get_diff(pull_request)
128 diff_processor = diffs.DiffProcessor(
128 diff_processor = diffs.DiffProcessor(
129 vcs_diff, format='gitdiff', diff_limit=diff_limit,
129 vcs_diff, format='gitdiff', diff_limit=diff_limit,
130 file_limit=file_limit, show_full_diff=c.fulldiff)
130 file_limit=file_limit, show_full_diff=c.fulldiff)
131 _parsed = diff_processor.prepare()
131 _parsed = diff_processor.prepare()
132
132
133 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
133 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
134
134
135 c.files = []
135 c.files = []
136 c.changes = {}
136 c.changes = {}
137 c.lines_added = 0
137 c.lines_added = 0
138 c.lines_deleted = 0
138 c.lines_deleted = 0
139 c.included_files = []
139 c.included_files = []
140 c.deleted_files = []
140 c.deleted_files = []
141
141
142 for f in _parsed:
142 for f in _parsed:
143 st = f['stats']
143 st = f['stats']
144 c.lines_added += st['added']
144 c.lines_added += st['added']
145 c.lines_deleted += st['deleted']
145 c.lines_deleted += st['deleted']
146
146
147 fid = h.FID('', f['filename'])
147 fid = h.FID('', f['filename'])
148 c.files.append([fid, f['operation'], f['filename'], f['stats']])
148 c.files.append([fid, f['operation'], f['filename'], f['stats']])
149 c.included_files.append(f['filename'])
149 c.included_files.append(f['filename'])
150 html_diff = diff_processor.as_html(enable_comments=enable_comments,
150 html_diff = diff_processor.as_html(enable_comments=enable_comments,
151 parsed_lines=[f])
151 parsed_lines=[f])
152 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
152 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
153
153
154 def _extract_ordering(self, request):
154 def _extract_ordering(self, request):
155 column_index = safe_int(request.GET.get('order[0][column]'))
155 column_index = safe_int(request.GET.get('order[0][column]'))
156 order_dir = request.GET.get('order[0][dir]', 'desc')
156 order_dir = request.GET.get('order[0][dir]', 'desc')
157 order_by = request.GET.get(
157 order_by = request.GET.get(
158 'columns[%s][data][sort]' % column_index, 'name_raw')
158 'columns[%s][data][sort]' % column_index, 'name_raw')
159 return order_by, order_dir
159 return order_by, order_dir
160
160
161 @LoginRequired()
161 @LoginRequired()
162 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
162 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
163 'repository.admin')
163 'repository.admin')
164 @HasAcceptedRepoType('git', 'hg')
164 @HasAcceptedRepoType('git', 'hg')
165 def show_all(self, repo_name):
165 def show_all(self, repo_name):
166 # filter types
166 # filter types
167 c.active = 'open'
167 c.active = 'open'
168 c.source = str2bool(request.GET.get('source'))
168 c.source = str2bool(request.GET.get('source'))
169 c.closed = str2bool(request.GET.get('closed'))
169 c.closed = str2bool(request.GET.get('closed'))
170 c.my = str2bool(request.GET.get('my'))
170 c.my = str2bool(request.GET.get('my'))
171 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
171 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
172 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
172 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
173 c.repo_name = repo_name
173 c.repo_name = repo_name
174
174
175 opened_by = None
175 opened_by = None
176 if c.my:
176 if c.my:
177 c.active = 'my'
177 c.active = 'my'
178 opened_by = [c.rhodecode_user.user_id]
178 opened_by = [c.rhodecode_user.user_id]
179
179
180 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
180 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
181 if c.closed:
181 if c.closed:
182 c.active = 'closed'
182 c.active = 'closed'
183 statuses = [PullRequest.STATUS_CLOSED]
183 statuses = [PullRequest.STATUS_CLOSED]
184
184
185 if c.awaiting_review and not c.source:
185 if c.awaiting_review and not c.source:
186 c.active = 'awaiting'
186 c.active = 'awaiting'
187 if c.source and not c.awaiting_review:
187 if c.source and not c.awaiting_review:
188 c.active = 'source'
188 c.active = 'source'
189 if c.awaiting_my_review:
189 if c.awaiting_my_review:
190 c.active = 'awaiting_my'
190 c.active = 'awaiting_my'
191
191
192 data = self._get_pull_requests_list(
192 data = self._get_pull_requests_list(
193 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
193 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
194 if not request.is_xhr:
194 if not request.is_xhr:
195 c.data = json.dumps(data['data'])
195 c.data = json.dumps(data['data'])
196 c.records_total = data['recordsTotal']
196 c.records_total = data['recordsTotal']
197 return render('/pullrequests/pullrequests.html')
197 return render('/pullrequests/pullrequests.html')
198 else:
198 else:
199 return json.dumps(data)
199 return json.dumps(data)
200
200
201 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
201 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
202 # pagination
202 # pagination
203 start = safe_int(request.GET.get('start'), 0)
203 start = safe_int(request.GET.get('start'), 0)
204 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
204 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
205 order_by, order_dir = self._extract_ordering(request)
205 order_by, order_dir = self._extract_ordering(request)
206
206
207 if c.awaiting_review:
207 if c.awaiting_review:
208 pull_requests = PullRequestModel().get_awaiting_review(
208 pull_requests = PullRequestModel().get_awaiting_review(
209 repo_name, source=c.source, opened_by=opened_by,
209 repo_name, source=c.source, opened_by=opened_by,
210 statuses=statuses, offset=start, length=length,
210 statuses=statuses, offset=start, length=length,
211 order_by=order_by, order_dir=order_dir)
211 order_by=order_by, order_dir=order_dir)
212 pull_requests_total_count = PullRequestModel(
212 pull_requests_total_count = PullRequestModel(
213 ).count_awaiting_review(
213 ).count_awaiting_review(
214 repo_name, source=c.source, statuses=statuses,
214 repo_name, source=c.source, statuses=statuses,
215 opened_by=opened_by)
215 opened_by=opened_by)
216 elif c.awaiting_my_review:
216 elif c.awaiting_my_review:
217 pull_requests = PullRequestModel().get_awaiting_my_review(
217 pull_requests = PullRequestModel().get_awaiting_my_review(
218 repo_name, source=c.source, opened_by=opened_by,
218 repo_name, source=c.source, opened_by=opened_by,
219 user_id=c.rhodecode_user.user_id, statuses=statuses,
219 user_id=c.rhodecode_user.user_id, statuses=statuses,
220 offset=start, length=length, order_by=order_by,
220 offset=start, length=length, order_by=order_by,
221 order_dir=order_dir)
221 order_dir=order_dir)
222 pull_requests_total_count = PullRequestModel(
222 pull_requests_total_count = PullRequestModel(
223 ).count_awaiting_my_review(
223 ).count_awaiting_my_review(
224 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
224 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
225 statuses=statuses, opened_by=opened_by)
225 statuses=statuses, opened_by=opened_by)
226 else:
226 else:
227 pull_requests = PullRequestModel().get_all(
227 pull_requests = PullRequestModel().get_all(
228 repo_name, source=c.source, opened_by=opened_by,
228 repo_name, source=c.source, opened_by=opened_by,
229 statuses=statuses, offset=start, length=length,
229 statuses=statuses, offset=start, length=length,
230 order_by=order_by, order_dir=order_dir)
230 order_by=order_by, order_dir=order_dir)
231 pull_requests_total_count = PullRequestModel().count_all(
231 pull_requests_total_count = PullRequestModel().count_all(
232 repo_name, source=c.source, statuses=statuses,
232 repo_name, source=c.source, statuses=statuses,
233 opened_by=opened_by)
233 opened_by=opened_by)
234
234
235 from rhodecode.lib.utils import PartialRenderer
235 from rhodecode.lib.utils import PartialRenderer
236 _render = PartialRenderer('data_table/_dt_elements.html')
236 _render = PartialRenderer('data_table/_dt_elements.html')
237 data = []
237 data = []
238 for pr in pull_requests:
238 for pr in pull_requests:
239 comments = ChangesetCommentsModel().get_all_comments(
239 comments = ChangesetCommentsModel().get_all_comments(
240 c.rhodecode_db_repo.repo_id, pull_request=pr)
240 c.rhodecode_db_repo.repo_id, pull_request=pr)
241
241
242 data.append({
242 data.append({
243 'name': _render('pullrequest_name',
243 'name': _render('pullrequest_name',
244 pr.pull_request_id, pr.target_repo.repo_name),
244 pr.pull_request_id, pr.target_repo.repo_name),
245 'name_raw': pr.pull_request_id,
245 'name_raw': pr.pull_request_id,
246 'status': _render('pullrequest_status',
246 'status': _render('pullrequest_status',
247 pr.calculated_review_status()),
247 pr.calculated_review_status()),
248 'title': _render(
248 'title': _render(
249 'pullrequest_title', pr.title, pr.description),
249 'pullrequest_title', pr.title, pr.description),
250 'description': h.escape(pr.description),
250 'description': h.escape(pr.description),
251 'updated_on': _render('pullrequest_updated_on',
251 'updated_on': _render('pullrequest_updated_on',
252 h.datetime_to_time(pr.updated_on)),
252 h.datetime_to_time(pr.updated_on)),
253 'updated_on_raw': h.datetime_to_time(pr.updated_on),
253 'updated_on_raw': h.datetime_to_time(pr.updated_on),
254 'created_on': _render('pullrequest_updated_on',
254 'created_on': _render('pullrequest_updated_on',
255 h.datetime_to_time(pr.created_on)),
255 h.datetime_to_time(pr.created_on)),
256 'created_on_raw': h.datetime_to_time(pr.created_on),
256 'created_on_raw': h.datetime_to_time(pr.created_on),
257 'author': _render('pullrequest_author',
257 'author': _render('pullrequest_author',
258 pr.author.full_contact, ),
258 pr.author.full_contact, ),
259 'author_raw': pr.author.full_name,
259 'author_raw': pr.author.full_name,
260 'comments': _render('pullrequest_comments', len(comments)),
260 'comments': _render('pullrequest_comments', len(comments)),
261 'comments_raw': len(comments),
261 'comments_raw': len(comments),
262 'closed': pr.is_closed(),
262 'closed': pr.is_closed(),
263 })
263 })
264 # json used to render the grid
264 # json used to render the grid
265 data = ({
265 data = ({
266 'data': data,
266 'data': data,
267 'recordsTotal': pull_requests_total_count,
267 'recordsTotal': pull_requests_total_count,
268 'recordsFiltered': pull_requests_total_count,
268 'recordsFiltered': pull_requests_total_count,
269 })
269 })
270 return data
270 return data
271
271
272 @LoginRequired()
272 @LoginRequired()
273 @NotAnonymous()
273 @NotAnonymous()
274 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
274 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
275 'repository.admin')
275 'repository.admin')
276 @HasAcceptedRepoType('git', 'hg')
276 @HasAcceptedRepoType('git', 'hg')
277 def index(self):
277 def index(self):
278 source_repo = c.rhodecode_db_repo
278 source_repo = c.rhodecode_db_repo
279
279
280 try:
280 try:
281 source_repo.scm_instance().get_commit()
281 source_repo.scm_instance().get_commit()
282 except EmptyRepositoryError:
282 except EmptyRepositoryError:
283 h.flash(h.literal(_('There are no commits yet')),
283 h.flash(h.literal(_('There are no commits yet')),
284 category='warning')
284 category='warning')
285 redirect(url('summary_home', repo_name=source_repo.repo_name))
285 redirect(url('summary_home', repo_name=source_repo.repo_name))
286
286
287 commit_id = request.GET.get('commit')
287 commit_id = request.GET.get('commit')
288 branch_ref = request.GET.get('branch')
288 branch_ref = request.GET.get('branch')
289 bookmark_ref = request.GET.get('bookmark')
289 bookmark_ref = request.GET.get('bookmark')
290
290
291 try:
291 try:
292 source_repo_data = PullRequestModel().generate_repo_data(
292 source_repo_data = PullRequestModel().generate_repo_data(
293 source_repo, commit_id=commit_id,
293 source_repo, commit_id=commit_id,
294 branch=branch_ref, bookmark=bookmark_ref)
294 branch=branch_ref, bookmark=bookmark_ref)
295 except CommitDoesNotExistError as e:
295 except CommitDoesNotExistError as e:
296 log.exception(e)
296 log.exception(e)
297 h.flash(_('Commit does not exist'), 'error')
297 h.flash(_('Commit does not exist'), 'error')
298 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
298 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
299
299
300 default_target_repo = source_repo
300 default_target_repo = source_repo
301 if (source_repo.parent and
301 if (source_repo.parent and
302 not source_repo.parent.scm_instance().is_empty()):
302 not source_repo.parent.scm_instance().is_empty()):
303 # change default if we have a parent repo
303 # change default if we have a parent repo
304 default_target_repo = source_repo.parent
304 default_target_repo = source_repo.parent
305
305
306 target_repo_data = PullRequestModel().generate_repo_data(
306 target_repo_data = PullRequestModel().generate_repo_data(
307 default_target_repo)
307 default_target_repo)
308
308
309 selected_source_ref = source_repo_data['refs']['selected_ref']
309 selected_source_ref = source_repo_data['refs']['selected_ref']
310
310
311 title_source_ref = selected_source_ref.split(':', 2)[1]
311 title_source_ref = selected_source_ref.split(':', 2)[1]
312 c.default_title = PullRequestModel().generate_pullrequest_title(
312 c.default_title = PullRequestModel().generate_pullrequest_title(
313 source=source_repo.repo_name,
313 source=source_repo.repo_name,
314 source_ref=title_source_ref,
314 source_ref=title_source_ref,
315 target=default_target_repo.repo_name
315 target=default_target_repo.repo_name
316 )
316 )
317
317
318 c.default_repo_data = {
318 c.default_repo_data = {
319 'source_repo_name': source_repo.repo_name,
319 'source_repo_name': source_repo.repo_name,
320 'source_refs_json': json.dumps(source_repo_data),
320 'source_refs_json': json.dumps(source_repo_data),
321 'target_repo_name': default_target_repo.repo_name,
321 'target_repo_name': default_target_repo.repo_name,
322 'target_refs_json': json.dumps(target_repo_data),
322 'target_refs_json': json.dumps(target_repo_data),
323 }
323 }
324 c.default_source_ref = selected_source_ref
324 c.default_source_ref = selected_source_ref
325
325
326 return render('/pullrequests/pullrequest.html')
326 return render('/pullrequests/pullrequest.html')
327
327
328 @LoginRequired()
328 @LoginRequired()
329 @NotAnonymous()
329 @NotAnonymous()
330 @XHRRequired()
330 @XHRRequired()
331 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
331 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
332 'repository.admin')
332 'repository.admin')
333 @jsonify
333 @jsonify
334 def get_repo_refs(self, repo_name, target_repo_name):
334 def get_repo_refs(self, repo_name, target_repo_name):
335 repo = Repository.get_by_repo_name(target_repo_name)
335 repo = Repository.get_by_repo_name(target_repo_name)
336 if not repo:
336 if not repo:
337 raise HTTPNotFound
337 raise HTTPNotFound
338 return PullRequestModel().generate_repo_data(repo)
338 return PullRequestModel().generate_repo_data(repo)
339
339
340 @LoginRequired()
340 @LoginRequired()
341 @NotAnonymous()
341 @NotAnonymous()
342 @XHRRequired()
342 @XHRRequired()
343 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
343 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
344 'repository.admin')
344 'repository.admin')
345 @jsonify
345 @jsonify
346 def get_repo_destinations(self, repo_name):
346 def get_repo_destinations(self, repo_name):
347 repo = Repository.get_by_repo_name(repo_name)
347 repo = Repository.get_by_repo_name(repo_name)
348 if not repo:
348 if not repo:
349 raise HTTPNotFound
349 raise HTTPNotFound
350 filter_query = request.GET.get('query')
350 filter_query = request.GET.get('query')
351
351
352 query = Repository.query() \
352 query = Repository.query() \
353 .order_by(func.length(Repository.repo_name)) \
353 .order_by(func.length(Repository.repo_name)) \
354 .filter(or_(
354 .filter(or_(
355 Repository.repo_name == repo.repo_name,
355 Repository.repo_name == repo.repo_name,
356 Repository.fork_id == repo.repo_id))
356 Repository.fork_id == repo.repo_id))
357
357
358 if filter_query:
358 if filter_query:
359 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
359 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
360 query = query.filter(
360 query = query.filter(
361 Repository.repo_name.ilike(ilike_expression))
361 Repository.repo_name.ilike(ilike_expression))
362
362
363 add_parent = False
363 add_parent = False
364 if repo.parent:
364 if repo.parent:
365 if filter_query in repo.parent.repo_name:
365 if filter_query in repo.parent.repo_name:
366 if not repo.parent.scm_instance().is_empty():
366 if not repo.parent.scm_instance().is_empty():
367 add_parent = True
367 add_parent = True
368
368
369 limit = 20 - 1 if add_parent else 20
369 limit = 20 - 1 if add_parent else 20
370 all_repos = query.limit(limit).all()
370 all_repos = query.limit(limit).all()
371 if add_parent:
371 if add_parent:
372 all_repos += [repo.parent]
372 all_repos += [repo.parent]
373
373
374 repos = []
374 repos = []
375 for obj in self.scm_model.get_repos(all_repos):
375 for obj in self.scm_model.get_repos(all_repos):
376 repos.append({
376 repos.append({
377 'id': obj['name'],
377 'id': obj['name'],
378 'text': obj['name'],
378 'text': obj['name'],
379 'type': 'repo',
379 'type': 'repo',
380 'obj': obj['dbrepo']
380 'obj': obj['dbrepo']
381 })
381 })
382
382
383 data = {
383 data = {
384 'more': False,
384 'more': False,
385 'results': [{
385 'results': [{
386 'text': _('Repositories'),
386 'text': _('Repositories'),
387 'children': repos
387 'children': repos
388 }] if repos else []
388 }] if repos else []
389 }
389 }
390 return data
390 return data
391
391
392 @LoginRequired()
392 @LoginRequired()
393 @NotAnonymous()
393 @NotAnonymous()
394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
395 'repository.admin')
395 'repository.admin')
396 @HasAcceptedRepoType('git', 'hg')
396 @HasAcceptedRepoType('git', 'hg')
397 @auth.CSRFRequired()
397 @auth.CSRFRequired()
398 def create(self, repo_name):
398 def create(self, repo_name):
399 repo = Repository.get_by_repo_name(repo_name)
399 repo = Repository.get_by_repo_name(repo_name)
400 if not repo:
400 if not repo:
401 raise HTTPNotFound
401 raise HTTPNotFound
402
402
403 controls = peppercorn.parse(request.POST.items())
403 controls = peppercorn.parse(request.POST.items())
404
404
405 try:
405 try:
406 _form = PullRequestForm(repo.repo_id)().to_python(controls)
406 _form = PullRequestForm(repo.repo_id)().to_python(controls)
407 except formencode.Invalid as errors:
407 except formencode.Invalid as errors:
408 if errors.error_dict.get('revisions'):
408 if errors.error_dict.get('revisions'):
409 msg = 'Revisions: %s' % errors.error_dict['revisions']
409 msg = 'Revisions: %s' % errors.error_dict['revisions']
410 elif errors.error_dict.get('pullrequest_title'):
410 elif errors.error_dict.get('pullrequest_title'):
411 msg = _('Pull request requires a title with min. 3 chars')
411 msg = _('Pull request requires a title with min. 3 chars')
412 else:
412 else:
413 msg = _('Error creating pull request: {}').format(errors)
413 msg = _('Error creating pull request: {}').format(errors)
414 log.exception(msg)
414 log.exception(msg)
415 h.flash(msg, 'error')
415 h.flash(msg, 'error')
416
416
417 # would rather just go back to form ...
417 # would rather just go back to form ...
418 return redirect(url('pullrequest_home', repo_name=repo_name))
418 return redirect(url('pullrequest_home', repo_name=repo_name))
419
419
420 source_repo = _form['source_repo']
420 source_repo = _form['source_repo']
421 source_ref = _form['source_ref']
421 source_ref = _form['source_ref']
422 target_repo = _form['target_repo']
422 target_repo = _form['target_repo']
423 target_ref = _form['target_ref']
423 target_ref = _form['target_ref']
424 commit_ids = _form['revisions'][::-1]
424 commit_ids = _form['revisions'][::-1]
425 reviewers = [
425 reviewers = [
426 (r['user_id'], r['reasons']) for r in _form['review_members']]
426 (r['user_id'], r['reasons']) for r in _form['review_members']]
427
427
428 # find the ancestor for this pr
428 # find the ancestor for this pr
429 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
429 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
430 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
430 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
431
431
432 source_scm = source_db_repo.scm_instance()
432 source_scm = source_db_repo.scm_instance()
433 target_scm = target_db_repo.scm_instance()
433 target_scm = target_db_repo.scm_instance()
434
434
435 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
435 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
436 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
436 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
437
437
438 ancestor = source_scm.get_common_ancestor(
438 ancestor = source_scm.get_common_ancestor(
439 source_commit.raw_id, target_commit.raw_id, target_scm)
439 source_commit.raw_id, target_commit.raw_id, target_scm)
440
440
441 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
441 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
442 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
442 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
443
443
444 pullrequest_title = _form['pullrequest_title']
444 pullrequest_title = _form['pullrequest_title']
445 title_source_ref = source_ref.split(':', 2)[1]
445 title_source_ref = source_ref.split(':', 2)[1]
446 if not pullrequest_title:
446 if not pullrequest_title:
447 pullrequest_title = PullRequestModel().generate_pullrequest_title(
447 pullrequest_title = PullRequestModel().generate_pullrequest_title(
448 source=source_repo,
448 source=source_repo,
449 source_ref=title_source_ref,
449 source_ref=title_source_ref,
450 target=target_repo
450 target=target_repo
451 )
451 )
452
452
453 description = _form['pullrequest_desc']
453 description = _form['pullrequest_desc']
454 try:
454 try:
455 pull_request = PullRequestModel().create(
455 pull_request = PullRequestModel().create(
456 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
456 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
457 target_ref, commit_ids, reviewers, pullrequest_title,
457 target_ref, commit_ids, reviewers, pullrequest_title,
458 description
458 description
459 )
459 )
460 Session().commit()
460 Session().commit()
461 h.flash(_('Successfully opened new pull request'),
461 h.flash(_('Successfully opened new pull request'),
462 category='success')
462 category='success')
463 except Exception as e:
463 except Exception as e:
464 msg = _('Error occurred during sending pull request')
464 msg = _('Error occurred during sending pull request')
465 log.exception(msg)
465 log.exception(msg)
466 h.flash(msg, category='error')
466 h.flash(msg, category='error')
467 return redirect(url('pullrequest_home', repo_name=repo_name))
467 return redirect(url('pullrequest_home', repo_name=repo_name))
468
468
469 return redirect(url('pullrequest_show', repo_name=target_repo,
469 return redirect(url('pullrequest_show', repo_name=target_repo,
470 pull_request_id=pull_request.pull_request_id))
470 pull_request_id=pull_request.pull_request_id))
471
471
472 @LoginRequired()
472 @LoginRequired()
473 @NotAnonymous()
473 @NotAnonymous()
474 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
474 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
475 'repository.admin')
475 'repository.admin')
476 @auth.CSRFRequired()
476 @auth.CSRFRequired()
477 @jsonify
477 @jsonify
478 def update(self, repo_name, pull_request_id):
478 def update(self, repo_name, pull_request_id):
479 pull_request_id = safe_int(pull_request_id)
479 pull_request_id = safe_int(pull_request_id)
480 pull_request = PullRequest.get_or_404(pull_request_id)
480 pull_request = PullRequest.get_or_404(pull_request_id)
481 # only owner or admin can update it
481 # only owner or admin can update it
482 allowed_to_update = PullRequestModel().check_user_update(
482 allowed_to_update = PullRequestModel().check_user_update(
483 pull_request, c.rhodecode_user)
483 pull_request, c.rhodecode_user)
484 if allowed_to_update:
484 if allowed_to_update:
485 controls = peppercorn.parse(request.POST.items())
485 controls = peppercorn.parse(request.POST.items())
486
486
487 if 'review_members' in controls:
487 if 'review_members' in controls:
488 self._update_reviewers(
488 self._update_reviewers(
489 pull_request_id, controls['review_members'])
489 pull_request_id, controls['review_members'])
490 elif str2bool(request.POST.get('update_commits', 'false')):
490 elif str2bool(request.POST.get('update_commits', 'false')):
491 self._update_commits(pull_request)
491 self._update_commits(pull_request)
492 elif str2bool(request.POST.get('close_pull_request', 'false')):
492 elif str2bool(request.POST.get('close_pull_request', 'false')):
493 self._reject_close(pull_request)
493 self._reject_close(pull_request)
494 elif str2bool(request.POST.get('edit_pull_request', 'false')):
494 elif str2bool(request.POST.get('edit_pull_request', 'false')):
495 self._edit_pull_request(pull_request)
495 self._edit_pull_request(pull_request)
496 else:
496 else:
497 raise HTTPBadRequest()
497 raise HTTPBadRequest()
498 return True
498 return True
499 raise HTTPForbidden()
499 raise HTTPForbidden()
500
500
501 def _edit_pull_request(self, pull_request):
501 def _edit_pull_request(self, pull_request):
502 try:
502 try:
503 PullRequestModel().edit(
503 PullRequestModel().edit(
504 pull_request, request.POST.get('title'),
504 pull_request, request.POST.get('title'),
505 request.POST.get('description'))
505 request.POST.get('description'))
506 except ValueError:
506 except ValueError:
507 msg = _(u'Cannot update closed pull requests.')
507 msg = _(u'Cannot update closed pull requests.')
508 h.flash(msg, category='error')
508 h.flash(msg, category='error')
509 return
509 return
510 else:
510 else:
511 Session().commit()
511 Session().commit()
512
512
513 msg = _(u'Pull request title & description updated.')
513 msg = _(u'Pull request title & description updated.')
514 h.flash(msg, category='success')
514 h.flash(msg, category='success')
515 return
515 return
516
516
517 def _update_commits(self, pull_request):
517 def _update_commits(self, pull_request):
518 try:
518 try:
519 if PullRequestModel().has_valid_update_type(pull_request):
519 if PullRequestModel().has_valid_update_type(pull_request):
520 updated_version, changes = PullRequestModel().update_commits(
520 updated_version, changes = PullRequestModel().update_commits(
521 pull_request)
521 pull_request)
522 if updated_version:
522 if updated_version:
523 msg = _(
523 msg = _(
524 u'Pull request updated to "{source_commit_id}" with '
524 u'Pull request updated to "{source_commit_id}" with '
525 u'{count_added} added, {count_removed} removed '
525 u'{count_added} added, {count_removed} removed '
526 u'commits.'
526 u'commits.'
527 ).format(
527 ).format(
528 source_commit_id=pull_request.source_ref_parts.commit_id,
528 source_commit_id=pull_request.source_ref_parts.commit_id,
529 count_added=len(changes.added),
529 count_added=len(changes.added),
530 count_removed=len(changes.removed))
530 count_removed=len(changes.removed))
531 h.flash(msg, category='success')
531 h.flash(msg, category='success')
532 registry = get_current_registry()
532 registry = get_current_registry()
533 rhodecode_plugins = getattr(registry,
533 rhodecode_plugins = getattr(registry,
534 'rhodecode_plugins', {})
534 'rhodecode_plugins', {})
535 channelstream_config = rhodecode_plugins.get(
535 channelstream_config = rhodecode_plugins.get(
536 'channelstream', {})
536 'channelstream', {})
537 if channelstream_config.get('enabled'):
537 if channelstream_config.get('enabled'):
538 message = msg + ' - <a onclick="' \
538 message = msg + ' - <a onclick="' \
539 'window.location.reload()">' \
539 'window.location.reload()">' \
540 '<strong>{}</strong></a>'.format(
540 '<strong>{}</strong></a>'.format(
541 _('Reload page')
541 _('Reload page')
542 )
542 )
543 channel = '/repo${}$/pr/{}'.format(
543 channel = '/repo${}$/pr/{}'.format(
544 pull_request.target_repo.repo_name,
544 pull_request.target_repo.repo_name,
545 pull_request.pull_request_id
545 pull_request.pull_request_id
546 )
546 )
547 payload = {
547 payload = {
548 'type': 'message',
548 'type': 'message',
549 'user': 'system',
549 'user': 'system',
550 'exclude_users': [request.user.username],
550 'exclude_users': [request.user.username],
551 'channel': channel,
551 'channel': channel,
552 'message': {
552 'message': {
553 'message': message,
553 'message': message,
554 'level': 'success',
554 'level': 'success',
555 'topic': '/notifications'
555 'topic': '/notifications'
556 }
556 }
557 }
557 }
558 channelstream_request(channelstream_config, [payload],
558 channelstream_request(channelstream_config, [payload],
559 '/message', raise_exc=False)
559 '/message', raise_exc=False)
560 else:
560 else:
561 h.flash(_("Nothing changed in pull request."),
561 h.flash(_("Nothing changed in pull request."),
562 category='warning')
562 category='warning')
563 else:
563 else:
564 msg = _(
564 msg = _(
565 u"Skipping update of pull request due to reference "
565 u"Skipping update of pull request due to reference "
566 u"type: {reference_type}"
566 u"type: {reference_type}"
567 ).format(reference_type=pull_request.source_ref_parts.type)
567 ).format(reference_type=pull_request.source_ref_parts.type)
568 h.flash(msg, category='warning')
568 h.flash(msg, category='warning')
569 except CommitDoesNotExistError:
569 except CommitDoesNotExistError:
570 h.flash(
570 h.flash(
571 _(u'Update failed due to missing commits.'), category='error')
571 _(u'Update failed due to missing commits.'), category='error')
572
572
573 @auth.CSRFRequired()
573 @auth.CSRFRequired()
574 @LoginRequired()
574 @LoginRequired()
575 @NotAnonymous()
575 @NotAnonymous()
576 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
576 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
577 'repository.admin')
577 'repository.admin')
578 def merge(self, repo_name, pull_request_id):
578 def merge(self, repo_name, pull_request_id):
579 """
579 """
580 POST /{repo_name}/pull-request/{pull_request_id}
580 POST /{repo_name}/pull-request/{pull_request_id}
581
581
582 Merge will perform a server-side merge of the specified
582 Merge will perform a server-side merge of the specified
583 pull request, if the pull request is approved and mergeable.
583 pull request, if the pull request is approved and mergeable.
584 After succesfull merging, the pull request is automatically
584 After succesfull merging, the pull request is automatically
585 closed, with a relevant comment.
585 closed, with a relevant comment.
586 """
586 """
587 pull_request_id = safe_int(pull_request_id)
587 pull_request_id = safe_int(pull_request_id)
588 pull_request = PullRequest.get_or_404(pull_request_id)
588 pull_request = PullRequest.get_or_404(pull_request_id)
589 user = c.rhodecode_user
589 user = c.rhodecode_user
590
590
591 if self._meets_merge_pre_conditions(pull_request, user):
591 if self._meets_merge_pre_conditions(pull_request, user):
592 log.debug("Pre-conditions checked, trying to merge.")
592 log.debug("Pre-conditions checked, trying to merge.")
593 extras = vcs_operation_context(
593 extras = vcs_operation_context(
594 request.environ, repo_name=pull_request.target_repo.repo_name,
594 request.environ, repo_name=pull_request.target_repo.repo_name,
595 username=user.username, action='push',
595 username=user.username, action='push',
596 scm=pull_request.target_repo.repo_type)
596 scm=pull_request.target_repo.repo_type)
597 self._merge_pull_request(pull_request, user, extras)
597 self._merge_pull_request(pull_request, user, extras)
598
598
599 return redirect(url(
599 return redirect(url(
600 'pullrequest_show',
600 'pullrequest_show',
601 repo_name=pull_request.target_repo.repo_name,
601 repo_name=pull_request.target_repo.repo_name,
602 pull_request_id=pull_request.pull_request_id))
602 pull_request_id=pull_request.pull_request_id))
603
603
604 def _meets_merge_pre_conditions(self, pull_request, user):
604 def _meets_merge_pre_conditions(self, pull_request, user):
605 if not PullRequestModel().check_user_merge(pull_request, user):
605 if not PullRequestModel().check_user_merge(pull_request, user):
606 raise HTTPForbidden()
606 raise HTTPForbidden()
607
607
608 merge_status, msg = PullRequestModel().merge_status(pull_request)
608 merge_status, msg = PullRequestModel().merge_status(pull_request)
609 if not merge_status:
609 if not merge_status:
610 log.debug("Cannot merge, not mergeable.")
610 log.debug("Cannot merge, not mergeable.")
611 h.flash(msg, category='error')
611 h.flash(msg, category='error')
612 return False
612 return False
613
613
614 if (pull_request.calculated_review_status()
614 if (pull_request.calculated_review_status()
615 is not ChangesetStatus.STATUS_APPROVED):
615 is not ChangesetStatus.STATUS_APPROVED):
616 log.debug("Cannot merge, approval is pending.")
616 log.debug("Cannot merge, approval is pending.")
617 msg = _('Pull request reviewer approval is pending.')
617 msg = _('Pull request reviewer approval is pending.')
618 h.flash(msg, category='error')
618 h.flash(msg, category='error')
619 return False
619 return False
620 return True
620 return True
621
621
622 def _merge_pull_request(self, pull_request, user, extras):
622 def _merge_pull_request(self, pull_request, user, extras):
623 merge_resp = PullRequestModel().merge(
623 merge_resp = PullRequestModel().merge(
624 pull_request, user, extras=extras)
624 pull_request, user, extras=extras)
625
625
626 if merge_resp.executed:
626 if merge_resp.executed:
627 log.debug("The merge was successful, closing the pull request.")
627 log.debug("The merge was successful, closing the pull request.")
628 PullRequestModel().close_pull_request(
628 PullRequestModel().close_pull_request(
629 pull_request.pull_request_id, user)
629 pull_request.pull_request_id, user)
630 Session().commit()
630 Session().commit()
631 msg = _('Pull request was successfully merged and closed.')
631 msg = _('Pull request was successfully merged and closed.')
632 h.flash(msg, category='success')
632 h.flash(msg, category='success')
633 else:
633 else:
634 log.debug(
634 log.debug(
635 "The merge was not successful. Merge response: %s",
635 "The merge was not successful. Merge response: %s",
636 merge_resp)
636 merge_resp)
637 msg = PullRequestModel().merge_status_message(
637 msg = PullRequestModel().merge_status_message(
638 merge_resp.failure_reason)
638 merge_resp.failure_reason)
639 h.flash(msg, category='error')
639 h.flash(msg, category='error')
640
640
641 def _update_reviewers(self, pull_request_id, review_members):
641 def _update_reviewers(self, pull_request_id, review_members):
642 reviewers = [
642 reviewers = [
643 (int(r['user_id']), r['reasons']) for r in review_members]
643 (int(r['user_id']), r['reasons']) for r in review_members]
644 PullRequestModel().update_reviewers(pull_request_id, reviewers)
644 PullRequestModel().update_reviewers(pull_request_id, reviewers)
645 Session().commit()
645 Session().commit()
646
646
647 def _reject_close(self, pull_request):
647 def _reject_close(self, pull_request):
648 if pull_request.is_closed():
648 if pull_request.is_closed():
649 raise HTTPForbidden()
649 raise HTTPForbidden()
650
650
651 PullRequestModel().close_pull_request_with_comment(
651 PullRequestModel().close_pull_request_with_comment(
652 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
652 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
653 Session().commit()
653 Session().commit()
654
654
655 @LoginRequired()
655 @LoginRequired()
656 @NotAnonymous()
656 @NotAnonymous()
657 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
657 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
658 'repository.admin')
658 'repository.admin')
659 @auth.CSRFRequired()
659 @auth.CSRFRequired()
660 @jsonify
660 @jsonify
661 def delete(self, repo_name, pull_request_id):
661 def delete(self, repo_name, pull_request_id):
662 pull_request_id = safe_int(pull_request_id)
662 pull_request_id = safe_int(pull_request_id)
663 pull_request = PullRequest.get_or_404(pull_request_id)
663 pull_request = PullRequest.get_or_404(pull_request_id)
664 # only owner can delete it !
664 # only owner can delete it !
665 if pull_request.author.user_id == c.rhodecode_user.user_id:
665 if pull_request.author.user_id == c.rhodecode_user.user_id:
666 PullRequestModel().delete(pull_request)
666 PullRequestModel().delete(pull_request)
667 Session().commit()
667 Session().commit()
668 h.flash(_('Successfully deleted pull request'),
668 h.flash(_('Successfully deleted pull request'),
669 category='success')
669 category='success')
670 return redirect(url('my_account_pullrequests'))
670 return redirect(url('my_account_pullrequests'))
671 raise HTTPForbidden()
671 raise HTTPForbidden()
672
672
673 @LoginRequired()
673 @LoginRequired()
674 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
675 'repository.admin')
675 'repository.admin')
676 def show(self, repo_name, pull_request_id):
676 def show(self, repo_name, pull_request_id):
677 pull_request_id = safe_int(pull_request_id)
677 pull_request_id = safe_int(pull_request_id)
678 c.pull_request = PullRequest.get_or_404(pull_request_id)
678 c.pull_request = PullRequest.get_or_404(pull_request_id)
679
679
680 c.template_context['pull_request_data']['pull_request_id'] = \
680 c.template_context['pull_request_data']['pull_request_id'] = \
681 pull_request_id
681 pull_request_id
682
682
683 # pull_requests repo_name we opened it against
683 # pull_requests repo_name we opened it against
684 # ie. target_repo must match
684 # ie. target_repo must match
685 if repo_name != c.pull_request.target_repo.repo_name:
685 if repo_name != c.pull_request.target_repo.repo_name:
686 raise HTTPNotFound
686 raise HTTPNotFound
687
687
688 c.allowed_to_change_status = PullRequestModel(). \
688 c.allowed_to_change_status = PullRequestModel(). \
689 check_user_change_status(c.pull_request, c.rhodecode_user)
689 check_user_change_status(c.pull_request, c.rhodecode_user)
690 c.allowed_to_update = PullRequestModel().check_user_update(
690 c.allowed_to_update = PullRequestModel().check_user_update(
691 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
691 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
692 c.allowed_to_merge = PullRequestModel().check_user_merge(
692 c.allowed_to_merge = PullRequestModel().check_user_merge(
693 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
693 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
694 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
695 c.pull_request)
694
696
695 cc_model = ChangesetCommentsModel()
697 cc_model = ChangesetCommentsModel()
696
698
697 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
699 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
698
700
699 c.pull_request_review_status = c.pull_request.calculated_review_status()
701 c.pull_request_review_status = c.pull_request.calculated_review_status()
700 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
702 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
701 c.pull_request)
703 c.pull_request)
702 c.approval_msg = None
704 c.approval_msg = None
703 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
705 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
704 c.approval_msg = _('Reviewer approval is pending.')
706 c.approval_msg = _('Reviewer approval is pending.')
705 c.pr_merge_status = False
707 c.pr_merge_status = False
706 # load compare data into template context
708 # load compare data into template context
707 enable_comments = not c.pull_request.is_closed()
709 enable_comments = not c.pull_request.is_closed()
708 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
710 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
709
711
710 # this is a hack to properly display links, when creating PR, the
712 # this is a hack to properly display links, when creating PR, the
711 # compare view and others uses different notation, and
713 # compare view and others uses different notation, and
712 # compare_commits.html renders links based on the target_repo.
714 # compare_commits.html renders links based on the target_repo.
713 # We need to swap that here to generate it properly on the html side
715 # We need to swap that here to generate it properly on the html side
714 c.target_repo = c.source_repo
716 c.target_repo = c.source_repo
715
717
716 # inline comments
718 # inline comments
717 c.inline_cnt = 0
719 c.inline_cnt = 0
718 c.inline_comments = cc_model.get_inline_comments(
720 c.inline_comments = cc_model.get_inline_comments(
719 c.rhodecode_db_repo.repo_id,
721 c.rhodecode_db_repo.repo_id,
720 pull_request=pull_request_id).items()
722 pull_request=pull_request_id).items()
721 # count inline comments
723 # count inline comments
722 for __, lines in c.inline_comments:
724 for __, lines in c.inline_comments:
723 for comments in lines.values():
725 for comments in lines.values():
724 c.inline_cnt += len(comments)
726 c.inline_cnt += len(comments)
725
727
726 # outdated comments
728 # outdated comments
727 c.outdated_cnt = 0
729 c.outdated_cnt = 0
728 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
730 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
729 c.outdated_comments = cc_model.get_outdated_comments(
731 c.outdated_comments = cc_model.get_outdated_comments(
730 c.rhodecode_db_repo.repo_id,
732 c.rhodecode_db_repo.repo_id,
731 pull_request=c.pull_request)
733 pull_request=c.pull_request)
732 # Count outdated comments and check for deleted files
734 # Count outdated comments and check for deleted files
733 for file_name, lines in c.outdated_comments.iteritems():
735 for file_name, lines in c.outdated_comments.iteritems():
734 for comments in lines.values():
736 for comments in lines.values():
735 c.outdated_cnt += len(comments)
737 c.outdated_cnt += len(comments)
736 if file_name not in c.included_files:
738 if file_name not in c.included_files:
737 c.deleted_files.append(file_name)
739 c.deleted_files.append(file_name)
738 else:
740 else:
739 c.outdated_comments = {}
741 c.outdated_comments = {}
740
742
741 # comments
743 # comments
742 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
744 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
743 pull_request=pull_request_id)
745 pull_request=pull_request_id)
744
746
745 if c.allowed_to_update:
747 if c.allowed_to_update:
746 force_close = ('forced_closed', _('Close Pull Request'))
748 force_close = ('forced_closed', _('Close Pull Request'))
747 statuses = ChangesetStatus.STATUSES + [force_close]
749 statuses = ChangesetStatus.STATUSES + [force_close]
748 else:
750 else:
749 statuses = ChangesetStatus.STATUSES
751 statuses = ChangesetStatus.STATUSES
750 c.commit_statuses = statuses
752 c.commit_statuses = statuses
751
753
752 c.ancestor = None # TODO: add ancestor here
754 c.ancestor = None # TODO: add ancestor here
753
755
754 return render('/pullrequests/pullrequest_show.html')
756 return render('/pullrequests/pullrequest_show.html')
755
757
756 @LoginRequired()
758 @LoginRequired()
757 @NotAnonymous()
759 @NotAnonymous()
758 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
759 'repository.admin')
761 'repository.admin')
760 @auth.CSRFRequired()
762 @auth.CSRFRequired()
761 @jsonify
763 @jsonify
762 def comment(self, repo_name, pull_request_id):
764 def comment(self, repo_name, pull_request_id):
763 pull_request_id = safe_int(pull_request_id)
765 pull_request_id = safe_int(pull_request_id)
764 pull_request = PullRequest.get_or_404(pull_request_id)
766 pull_request = PullRequest.get_or_404(pull_request_id)
765 if pull_request.is_closed():
767 if pull_request.is_closed():
766 raise HTTPForbidden()
768 raise HTTPForbidden()
767
769
768 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
770 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
769 # as a changeset status, still we want to send it in one value.
771 # as a changeset status, still we want to send it in one value.
770 status = request.POST.get('changeset_status', None)
772 status = request.POST.get('changeset_status', None)
771 text = request.POST.get('text')
773 text = request.POST.get('text')
772 if status and '_closed' in status:
774 if status and '_closed' in status:
773 close_pr = True
775 close_pr = True
774 status = status.replace('_closed', '')
776 status = status.replace('_closed', '')
775 else:
777 else:
776 close_pr = False
778 close_pr = False
777
779
778 forced = (status == 'forced')
780 forced = (status == 'forced')
779 if forced:
781 if forced:
780 status = 'rejected'
782 status = 'rejected'
781
783
782 allowed_to_change_status = PullRequestModel().check_user_change_status(
784 allowed_to_change_status = PullRequestModel().check_user_change_status(
783 pull_request, c.rhodecode_user)
785 pull_request, c.rhodecode_user)
784
786
785 if status and allowed_to_change_status:
787 if status and allowed_to_change_status:
786 message = (_('Status change %(transition_icon)s %(status)s')
788 message = (_('Status change %(transition_icon)s %(status)s')
787 % {'transition_icon': '>',
789 % {'transition_icon': '>',
788 'status': ChangesetStatus.get_status_lbl(status)})
790 'status': ChangesetStatus.get_status_lbl(status)})
789 if close_pr:
791 if close_pr:
790 message = _('Closing with') + ' ' + message
792 message = _('Closing with') + ' ' + message
791 text = text or message
793 text = text or message
792 comm = ChangesetCommentsModel().create(
794 comm = ChangesetCommentsModel().create(
793 text=text,
795 text=text,
794 repo=c.rhodecode_db_repo.repo_id,
796 repo=c.rhodecode_db_repo.repo_id,
795 user=c.rhodecode_user.user_id,
797 user=c.rhodecode_user.user_id,
796 pull_request=pull_request_id,
798 pull_request=pull_request_id,
797 f_path=request.POST.get('f_path'),
799 f_path=request.POST.get('f_path'),
798 line_no=request.POST.get('line'),
800 line_no=request.POST.get('line'),
799 status_change=(ChangesetStatus.get_status_lbl(status)
801 status_change=(ChangesetStatus.get_status_lbl(status)
800 if status and allowed_to_change_status else None),
802 if status and allowed_to_change_status else None),
801 status_change_type=(status
803 status_change_type=(status
802 if status and allowed_to_change_status else None),
804 if status and allowed_to_change_status else None),
803 closing_pr=close_pr
805 closing_pr=close_pr
804 )
806 )
805
807
806
808
807
809
808 if allowed_to_change_status:
810 if allowed_to_change_status:
809 old_calculated_status = pull_request.calculated_review_status()
811 old_calculated_status = pull_request.calculated_review_status()
810 # get status if set !
812 # get status if set !
811 if status:
813 if status:
812 ChangesetStatusModel().set_status(
814 ChangesetStatusModel().set_status(
813 c.rhodecode_db_repo.repo_id,
815 c.rhodecode_db_repo.repo_id,
814 status,
816 status,
815 c.rhodecode_user.user_id,
817 c.rhodecode_user.user_id,
816 comm,
818 comm,
817 pull_request=pull_request_id
819 pull_request=pull_request_id
818 )
820 )
819
821
820 Session().flush()
822 Session().flush()
821 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
823 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
822 # we now calculate the status of pull request, and based on that
824 # we now calculate the status of pull request, and based on that
823 # calculation we set the commits status
825 # calculation we set the commits status
824 calculated_status = pull_request.calculated_review_status()
826 calculated_status = pull_request.calculated_review_status()
825 if old_calculated_status != calculated_status:
827 if old_calculated_status != calculated_status:
826 PullRequestModel()._trigger_pull_request_hook(
828 PullRequestModel()._trigger_pull_request_hook(
827 pull_request, c.rhodecode_user, 'review_status_change')
829 pull_request, c.rhodecode_user, 'review_status_change')
828
830
829 calculated_status_lbl = ChangesetStatus.get_status_lbl(
831 calculated_status_lbl = ChangesetStatus.get_status_lbl(
830 calculated_status)
832 calculated_status)
831
833
832 if close_pr:
834 if close_pr:
833 status_completed = (
835 status_completed = (
834 calculated_status in [ChangesetStatus.STATUS_APPROVED,
836 calculated_status in [ChangesetStatus.STATUS_APPROVED,
835 ChangesetStatus.STATUS_REJECTED])
837 ChangesetStatus.STATUS_REJECTED])
836 if forced or status_completed:
838 if forced or status_completed:
837 PullRequestModel().close_pull_request(
839 PullRequestModel().close_pull_request(
838 pull_request_id, c.rhodecode_user)
840 pull_request_id, c.rhodecode_user)
839 else:
841 else:
840 h.flash(_('Closing pull request on other statuses than '
842 h.flash(_('Closing pull request on other statuses than '
841 'rejected or approved is forbidden. '
843 'rejected or approved is forbidden. '
842 'Calculated status from all reviewers '
844 'Calculated status from all reviewers '
843 'is currently: %s') % calculated_status_lbl,
845 'is currently: %s') % calculated_status_lbl,
844 category='warning')
846 category='warning')
845
847
846 Session().commit()
848 Session().commit()
847
849
848 if not request.is_xhr:
850 if not request.is_xhr:
849 return redirect(h.url('pullrequest_show', repo_name=repo_name,
851 return redirect(h.url('pullrequest_show', repo_name=repo_name,
850 pull_request_id=pull_request_id))
852 pull_request_id=pull_request_id))
851
853
852 data = {
854 data = {
853 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
855 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
854 }
856 }
855 if comm:
857 if comm:
856 c.co = comm
858 c.co = comm
857 data.update(comm.get_dict())
859 data.update(comm.get_dict())
858 data.update({'rendered_text':
860 data.update({'rendered_text':
859 render('changeset/changeset_comment_block.html')})
861 render('changeset/changeset_comment_block.html')})
860
862
861 return data
863 return data
862
864
863 @LoginRequired()
865 @LoginRequired()
864 @NotAnonymous()
866 @NotAnonymous()
865 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
867 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
866 'repository.admin')
868 'repository.admin')
867 @auth.CSRFRequired()
869 @auth.CSRFRequired()
868 @jsonify
870 @jsonify
869 def delete_comment(self, repo_name, comment_id):
871 def delete_comment(self, repo_name, comment_id):
870 return self._delete_comment(comment_id)
872 return self._delete_comment(comment_id)
871
873
872 def _delete_comment(self, comment_id):
874 def _delete_comment(self, comment_id):
873 comment_id = safe_int(comment_id)
875 comment_id = safe_int(comment_id)
874 co = ChangesetComment.get_or_404(comment_id)
876 co = ChangesetComment.get_or_404(comment_id)
875 if co.pull_request.is_closed():
877 if co.pull_request.is_closed():
876 # don't allow deleting comments on closed pull request
878 # don't allow deleting comments on closed pull request
877 raise HTTPForbidden()
879 raise HTTPForbidden()
878
880
879 is_owner = co.author.user_id == c.rhodecode_user.user_id
881 is_owner = co.author.user_id == c.rhodecode_user.user_id
880 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
882 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
881 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
883 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
882 old_calculated_status = co.pull_request.calculated_review_status()
884 old_calculated_status = co.pull_request.calculated_review_status()
883 ChangesetCommentsModel().delete(comment=co)
885 ChangesetCommentsModel().delete(comment=co)
884 Session().commit()
886 Session().commit()
885 calculated_status = co.pull_request.calculated_review_status()
887 calculated_status = co.pull_request.calculated_review_status()
886 if old_calculated_status != calculated_status:
888 if old_calculated_status != calculated_status:
887 PullRequestModel()._trigger_pull_request_hook(
889 PullRequestModel()._trigger_pull_request_hook(
888 co.pull_request, c.rhodecode_user, 'review_status_change')
890 co.pull_request, c.rhodecode_user, 'review_status_change')
889 return True
891 return True
890 else:
892 else:
891 raise HTTPForbidden()
893 raise HTTPForbidden()
@@ -1,1177 +1,1180 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.i18n.translation import lazy_ugettext
32 from pylons.i18n.translation import lazy_ugettext
33
33
34 from rhodecode.lib import helpers as h, hooks_utils, diffs
34 from rhodecode.lib import helpers as h, hooks_utils, diffs
35 from rhodecode.lib.compat import OrderedDict
35 from rhodecode.lib.compat import OrderedDict
36 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
36 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
37 from rhodecode.lib.markup_renderer import (
37 from rhodecode.lib.markup_renderer import (
38 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
38 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
39 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils import action_logger
40 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
40 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
41 from rhodecode.lib.vcs.backends.base import (
41 from rhodecode.lib.vcs.backends.base import (
42 Reference, MergeResponse, MergeFailureReason)
42 Reference, MergeResponse, MergeFailureReason)
43 from rhodecode.lib.vcs.conf import settings as vcs_settings
43 from rhodecode.lib.vcs.conf import settings as vcs_settings
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, EmptyRepositoryError)
45 CommitDoesNotExistError, EmptyRepositoryError)
46 from rhodecode.model import BaseModel
46 from rhodecode.model import BaseModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import ChangesetCommentsModel
48 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 PullRequest, PullRequestReviewers, ChangesetStatus,
50 PullRequest, PullRequestReviewers, ChangesetStatus,
51 PullRequestVersion, ChangesetComment)
51 PullRequestVersion, ChangesetComment)
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.notification import NotificationModel, \
53 from rhodecode.model.notification import NotificationModel, \
54 EmailNotificationModel
54 EmailNotificationModel
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56 from rhodecode.model.settings import VcsSettingsModel
56 from rhodecode.model.settings import VcsSettingsModel
57
57
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 class PullRequestModel(BaseModel):
62 class PullRequestModel(BaseModel):
63
63
64 cls = PullRequest
64 cls = PullRequest
65
65
66 DIFF_CONTEXT = 3
66 DIFF_CONTEXT = 3
67
67
68 MERGE_STATUS_MESSAGES = {
68 MERGE_STATUS_MESSAGES = {
69 MergeFailureReason.NONE: lazy_ugettext(
69 MergeFailureReason.NONE: lazy_ugettext(
70 'This pull request can be automatically merged.'),
70 'This pull request can be automatically merged.'),
71 MergeFailureReason.UNKNOWN: lazy_ugettext(
71 MergeFailureReason.UNKNOWN: lazy_ugettext(
72 'This pull request cannot be merged because of an unhandled'
72 'This pull request cannot be merged because of an unhandled'
73 ' exception.'),
73 ' exception.'),
74 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
74 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
75 'This pull request cannot be merged because of conflicts.'),
75 'This pull request cannot be merged because of conflicts.'),
76 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
76 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
77 'This pull request could not be merged because push to target'
77 'This pull request could not be merged because push to target'
78 ' failed.'),
78 ' failed.'),
79 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
79 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
80 'This pull request cannot be merged because the target is not a'
80 'This pull request cannot be merged because the target is not a'
81 ' head.'),
81 ' head.'),
82 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
82 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
83 'This pull request cannot be merged because the source contains'
83 'This pull request cannot be merged because the source contains'
84 ' more branches than the target.'),
84 ' more branches than the target.'),
85 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
85 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
86 'This pull request cannot be merged because the target has'
86 'This pull request cannot be merged because the target has'
87 ' multiple heads.'),
87 ' multiple heads.'),
88 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
88 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
89 'This pull request cannot be merged because the target repository'
89 'This pull request cannot be merged because the target repository'
90 ' is locked.'),
90 ' is locked.'),
91 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
91 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
92 'This pull request cannot be merged because the target or the '
92 'This pull request cannot be merged because the target or the '
93 'source reference is missing.'),
93 'source reference is missing.'),
94 }
94 }
95
95
96 def __get_pull_request(self, pull_request):
96 def __get_pull_request(self, pull_request):
97 return self._get_instance(PullRequest, pull_request)
97 return self._get_instance(PullRequest, pull_request)
98
98
99 def _check_perms(self, perms, pull_request, user, api=False):
99 def _check_perms(self, perms, pull_request, user, api=False):
100 if not api:
100 if not api:
101 return h.HasRepoPermissionAny(*perms)(
101 return h.HasRepoPermissionAny(*perms)(
102 user=user, repo_name=pull_request.target_repo.repo_name)
102 user=user, repo_name=pull_request.target_repo.repo_name)
103 else:
103 else:
104 return h.HasRepoPermissionAnyApi(*perms)(
104 return h.HasRepoPermissionAnyApi(*perms)(
105 user=user, repo_name=pull_request.target_repo.repo_name)
105 user=user, repo_name=pull_request.target_repo.repo_name)
106
106
107 def check_user_read(self, pull_request, user, api=False):
107 def check_user_read(self, pull_request, user, api=False):
108 _perms = ('repository.admin', 'repository.write', 'repository.read',)
108 _perms = ('repository.admin', 'repository.write', 'repository.read',)
109 return self._check_perms(_perms, pull_request, user, api)
109 return self._check_perms(_perms, pull_request, user, api)
110
110
111 def check_user_merge(self, pull_request, user, api=False):
111 def check_user_merge(self, pull_request, user, api=False):
112 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
112 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
113 return self._check_perms(_perms, pull_request, user, api)
113 return self._check_perms(_perms, pull_request, user, api)
114
114
115 def check_user_update(self, pull_request, user, api=False):
115 def check_user_update(self, pull_request, user, api=False):
116 owner = user.user_id == pull_request.user_id
116 owner = user.user_id == pull_request.user_id
117 return self.check_user_merge(pull_request, user, api) or owner
117 return self.check_user_merge(pull_request, user, api) or owner
118
118
119 def check_user_change_status(self, pull_request, user, api=False):
119 def check_user_change_status(self, pull_request, user, api=False):
120 reviewer = user.user_id in [x.user_id for x in
120 reviewer = user.user_id in [x.user_id for x in
121 pull_request.reviewers]
121 pull_request.reviewers]
122 return self.check_user_update(pull_request, user, api) or reviewer
122 return self.check_user_update(pull_request, user, api) or reviewer
123
123
124 def get(self, pull_request):
124 def get(self, pull_request):
125 return self.__get_pull_request(pull_request)
125 return self.__get_pull_request(pull_request)
126
126
127 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
127 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
128 opened_by=None, order_by=None,
128 opened_by=None, order_by=None,
129 order_dir='desc'):
129 order_dir='desc'):
130 repo = self._get_repo(repo_name)
130 repo = self._get_repo(repo_name)
131 q = PullRequest.query()
131 q = PullRequest.query()
132 # source or target
132 # source or target
133 if source:
133 if source:
134 q = q.filter(PullRequest.source_repo == repo)
134 q = q.filter(PullRequest.source_repo == repo)
135 else:
135 else:
136 q = q.filter(PullRequest.target_repo == repo)
136 q = q.filter(PullRequest.target_repo == repo)
137
137
138 # closed,opened
138 # closed,opened
139 if statuses:
139 if statuses:
140 q = q.filter(PullRequest.status.in_(statuses))
140 q = q.filter(PullRequest.status.in_(statuses))
141
141
142 # opened by filter
142 # opened by filter
143 if opened_by:
143 if opened_by:
144 q = q.filter(PullRequest.user_id.in_(opened_by))
144 q = q.filter(PullRequest.user_id.in_(opened_by))
145
145
146 if order_by:
146 if order_by:
147 order_map = {
147 order_map = {
148 'name_raw': PullRequest.pull_request_id,
148 'name_raw': PullRequest.pull_request_id,
149 'title': PullRequest.title,
149 'title': PullRequest.title,
150 'updated_on_raw': PullRequest.updated_on
150 'updated_on_raw': PullRequest.updated_on
151 }
151 }
152 if order_dir == 'asc':
152 if order_dir == 'asc':
153 q = q.order_by(order_map[order_by].asc())
153 q = q.order_by(order_map[order_by].asc())
154 else:
154 else:
155 q = q.order_by(order_map[order_by].desc())
155 q = q.order_by(order_map[order_by].desc())
156
156
157 return q
157 return q
158
158
159 def count_all(self, repo_name, source=False, statuses=None,
159 def count_all(self, repo_name, source=False, statuses=None,
160 opened_by=None):
160 opened_by=None):
161 """
161 """
162 Count the number of pull requests for a specific repository.
162 Count the number of pull requests for a specific repository.
163
163
164 :param repo_name: target or source repo
164 :param repo_name: target or source repo
165 :param source: boolean flag to specify if repo_name refers to source
165 :param source: boolean flag to specify if repo_name refers to source
166 :param statuses: list of pull request statuses
166 :param statuses: list of pull request statuses
167 :param opened_by: author user of the pull request
167 :param opened_by: author user of the pull request
168 :returns: int number of pull requests
168 :returns: int number of pull requests
169 """
169 """
170 q = self._prepare_get_all_query(
170 q = self._prepare_get_all_query(
171 repo_name, source=source, statuses=statuses, opened_by=opened_by)
171 repo_name, source=source, statuses=statuses, opened_by=opened_by)
172
172
173 return q.count()
173 return q.count()
174
174
175 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
175 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
176 offset=0, length=None, order_by=None, order_dir='desc'):
176 offset=0, length=None, order_by=None, order_dir='desc'):
177 """
177 """
178 Get all pull requests for a specific repository.
178 Get all pull requests for a specific repository.
179
179
180 :param repo_name: target or source repo
180 :param repo_name: target or source repo
181 :param source: boolean flag to specify if repo_name refers to source
181 :param source: boolean flag to specify if repo_name refers to source
182 :param statuses: list of pull request statuses
182 :param statuses: list of pull request statuses
183 :param opened_by: author user of the pull request
183 :param opened_by: author user of the pull request
184 :param offset: pagination offset
184 :param offset: pagination offset
185 :param length: length of returned list
185 :param length: length of returned list
186 :param order_by: order of the returned list
186 :param order_by: order of the returned list
187 :param order_dir: 'asc' or 'desc' ordering direction
187 :param order_dir: 'asc' or 'desc' ordering direction
188 :returns: list of pull requests
188 :returns: list of pull requests
189 """
189 """
190 q = self._prepare_get_all_query(
190 q = self._prepare_get_all_query(
191 repo_name, source=source, statuses=statuses, opened_by=opened_by,
191 repo_name, source=source, statuses=statuses, opened_by=opened_by,
192 order_by=order_by, order_dir=order_dir)
192 order_by=order_by, order_dir=order_dir)
193
193
194 if length:
194 if length:
195 pull_requests = q.limit(length).offset(offset).all()
195 pull_requests = q.limit(length).offset(offset).all()
196 else:
196 else:
197 pull_requests = q.all()
197 pull_requests = q.all()
198
198
199 return pull_requests
199 return pull_requests
200
200
201 def count_awaiting_review(self, repo_name, source=False, statuses=None,
201 def count_awaiting_review(self, repo_name, source=False, statuses=None,
202 opened_by=None):
202 opened_by=None):
203 """
203 """
204 Count the number of pull requests for a specific repository that are
204 Count the number of pull requests for a specific repository that are
205 awaiting review.
205 awaiting review.
206
206
207 :param repo_name: target or source repo
207 :param repo_name: target or source repo
208 :param source: boolean flag to specify if repo_name refers to source
208 :param source: boolean flag to specify if repo_name refers to source
209 :param statuses: list of pull request statuses
209 :param statuses: list of pull request statuses
210 :param opened_by: author user of the pull request
210 :param opened_by: author user of the pull request
211 :returns: int number of pull requests
211 :returns: int number of pull requests
212 """
212 """
213 pull_requests = self.get_awaiting_review(
213 pull_requests = self.get_awaiting_review(
214 repo_name, source=source, statuses=statuses, opened_by=opened_by)
214 repo_name, source=source, statuses=statuses, opened_by=opened_by)
215
215
216 return len(pull_requests)
216 return len(pull_requests)
217
217
218 def get_awaiting_review(self, repo_name, source=False, statuses=None,
218 def get_awaiting_review(self, repo_name, source=False, statuses=None,
219 opened_by=None, offset=0, length=None,
219 opened_by=None, offset=0, length=None,
220 order_by=None, order_dir='desc'):
220 order_by=None, order_dir='desc'):
221 """
221 """
222 Get all pull requests for a specific repository that are awaiting
222 Get all pull requests for a specific repository that are awaiting
223 review.
223 review.
224
224
225 :param repo_name: target or source repo
225 :param repo_name: target or source repo
226 :param source: boolean flag to specify if repo_name refers to source
226 :param source: boolean flag to specify if repo_name refers to source
227 :param statuses: list of pull request statuses
227 :param statuses: list of pull request statuses
228 :param opened_by: author user of the pull request
228 :param opened_by: author user of the pull request
229 :param offset: pagination offset
229 :param offset: pagination offset
230 :param length: length of returned list
230 :param length: length of returned list
231 :param order_by: order of the returned list
231 :param order_by: order of the returned list
232 :param order_dir: 'asc' or 'desc' ordering direction
232 :param order_dir: 'asc' or 'desc' ordering direction
233 :returns: list of pull requests
233 :returns: list of pull requests
234 """
234 """
235 pull_requests = self.get_all(
235 pull_requests = self.get_all(
236 repo_name, source=source, statuses=statuses, opened_by=opened_by,
236 repo_name, source=source, statuses=statuses, opened_by=opened_by,
237 order_by=order_by, order_dir=order_dir)
237 order_by=order_by, order_dir=order_dir)
238
238
239 _filtered_pull_requests = []
239 _filtered_pull_requests = []
240 for pr in pull_requests:
240 for pr in pull_requests:
241 status = pr.calculated_review_status()
241 status = pr.calculated_review_status()
242 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
242 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
243 ChangesetStatus.STATUS_UNDER_REVIEW]:
243 ChangesetStatus.STATUS_UNDER_REVIEW]:
244 _filtered_pull_requests.append(pr)
244 _filtered_pull_requests.append(pr)
245 if length:
245 if length:
246 return _filtered_pull_requests[offset:offset+length]
246 return _filtered_pull_requests[offset:offset+length]
247 else:
247 else:
248 return _filtered_pull_requests
248 return _filtered_pull_requests
249
249
250 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
250 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
251 opened_by=None, user_id=None):
251 opened_by=None, user_id=None):
252 """
252 """
253 Count the number of pull requests for a specific repository that are
253 Count the number of pull requests for a specific repository that are
254 awaiting review from a specific user.
254 awaiting review from a specific user.
255
255
256 :param repo_name: target or source repo
256 :param repo_name: target or source repo
257 :param source: boolean flag to specify if repo_name refers to source
257 :param source: boolean flag to specify if repo_name refers to source
258 :param statuses: list of pull request statuses
258 :param statuses: list of pull request statuses
259 :param opened_by: author user of the pull request
259 :param opened_by: author user of the pull request
260 :param user_id: reviewer user of the pull request
260 :param user_id: reviewer user of the pull request
261 :returns: int number of pull requests
261 :returns: int number of pull requests
262 """
262 """
263 pull_requests = self.get_awaiting_my_review(
263 pull_requests = self.get_awaiting_my_review(
264 repo_name, source=source, statuses=statuses, opened_by=opened_by,
264 repo_name, source=source, statuses=statuses, opened_by=opened_by,
265 user_id=user_id)
265 user_id=user_id)
266
266
267 return len(pull_requests)
267 return len(pull_requests)
268
268
269 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
269 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
270 opened_by=None, user_id=None, offset=0,
270 opened_by=None, user_id=None, offset=0,
271 length=None, order_by=None, order_dir='desc'):
271 length=None, order_by=None, order_dir='desc'):
272 """
272 """
273 Get all pull requests for a specific repository that are awaiting
273 Get all pull requests for a specific repository that are awaiting
274 review from a specific user.
274 review from a specific user.
275
275
276 :param repo_name: target or source repo
276 :param repo_name: target or source repo
277 :param source: boolean flag to specify if repo_name refers to source
277 :param source: boolean flag to specify if repo_name refers to source
278 :param statuses: list of pull request statuses
278 :param statuses: list of pull request statuses
279 :param opened_by: author user of the pull request
279 :param opened_by: author user of the pull request
280 :param user_id: reviewer user of the pull request
280 :param user_id: reviewer user of the pull request
281 :param offset: pagination offset
281 :param offset: pagination offset
282 :param length: length of returned list
282 :param length: length of returned list
283 :param order_by: order of the returned list
283 :param order_by: order of the returned list
284 :param order_dir: 'asc' or 'desc' ordering direction
284 :param order_dir: 'asc' or 'desc' ordering direction
285 :returns: list of pull requests
285 :returns: list of pull requests
286 """
286 """
287 pull_requests = self.get_all(
287 pull_requests = self.get_all(
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 order_by=order_by, order_dir=order_dir)
289 order_by=order_by, order_dir=order_dir)
290
290
291 _my = PullRequestModel().get_not_reviewed(user_id)
291 _my = PullRequestModel().get_not_reviewed(user_id)
292 my_participation = []
292 my_participation = []
293 for pr in pull_requests:
293 for pr in pull_requests:
294 if pr in _my:
294 if pr in _my:
295 my_participation.append(pr)
295 my_participation.append(pr)
296 _filtered_pull_requests = my_participation
296 _filtered_pull_requests = my_participation
297 if length:
297 if length:
298 return _filtered_pull_requests[offset:offset+length]
298 return _filtered_pull_requests[offset:offset+length]
299 else:
299 else:
300 return _filtered_pull_requests
300 return _filtered_pull_requests
301
301
302 def get_not_reviewed(self, user_id):
302 def get_not_reviewed(self, user_id):
303 return [
303 return [
304 x.pull_request for x in PullRequestReviewers.query().filter(
304 x.pull_request for x in PullRequestReviewers.query().filter(
305 PullRequestReviewers.user_id == user_id).all()
305 PullRequestReviewers.user_id == user_id).all()
306 ]
306 ]
307
307
308 def get_versions(self, pull_request):
308 def get_versions(self, pull_request):
309 """
309 """
310 returns version of pull request sorted by ID descending
310 returns version of pull request sorted by ID descending
311 """
311 """
312 return PullRequestVersion.query()\
312 return PullRequestVersion.query()\
313 .filter(PullRequestVersion.pull_request == pull_request)\
313 .filter(PullRequestVersion.pull_request == pull_request)\
314 .order_by(PullRequestVersion.pull_request_version_id.asc())\
314 .order_by(PullRequestVersion.pull_request_version_id.asc())\
315 .all()
315 .all()
316
316
317 def create(self, created_by, source_repo, source_ref, target_repo,
317 def create(self, created_by, source_repo, source_ref, target_repo,
318 target_ref, revisions, reviewers, title, description=None):
318 target_ref, revisions, reviewers, title, description=None):
319 created_by_user = self._get_user(created_by)
319 created_by_user = self._get_user(created_by)
320 source_repo = self._get_repo(source_repo)
320 source_repo = self._get_repo(source_repo)
321 target_repo = self._get_repo(target_repo)
321 target_repo = self._get_repo(target_repo)
322
322
323 pull_request = PullRequest()
323 pull_request = PullRequest()
324 pull_request.source_repo = source_repo
324 pull_request.source_repo = source_repo
325 pull_request.source_ref = source_ref
325 pull_request.source_ref = source_ref
326 pull_request.target_repo = target_repo
326 pull_request.target_repo = target_repo
327 pull_request.target_ref = target_ref
327 pull_request.target_ref = target_ref
328 pull_request.revisions = revisions
328 pull_request.revisions = revisions
329 pull_request.title = title
329 pull_request.title = title
330 pull_request.description = description
330 pull_request.description = description
331 pull_request.author = created_by_user
331 pull_request.author = created_by_user
332
332
333 Session().add(pull_request)
333 Session().add(pull_request)
334 Session().flush()
334 Session().flush()
335
335
336 reviewer_ids = set()
336 reviewer_ids = set()
337 # members / reviewers
337 # members / reviewers
338 for reviewer_object in reviewers:
338 for reviewer_object in reviewers:
339 if isinstance(reviewer_object, tuple):
339 if isinstance(reviewer_object, tuple):
340 user_id, reasons = reviewer_object
340 user_id, reasons = reviewer_object
341 else:
341 else:
342 user_id, reasons = reviewer_object, []
342 user_id, reasons = reviewer_object, []
343
343
344 user = self._get_user(user_id)
344 user = self._get_user(user_id)
345 reviewer_ids.add(user.user_id)
345 reviewer_ids.add(user.user_id)
346
346
347 reviewer = PullRequestReviewers(user, pull_request, reasons)
347 reviewer = PullRequestReviewers(user, pull_request, reasons)
348 Session().add(reviewer)
348 Session().add(reviewer)
349
349
350 # Set approval status to "Under Review" for all commits which are
350 # Set approval status to "Under Review" for all commits which are
351 # part of this pull request.
351 # part of this pull request.
352 ChangesetStatusModel().set_status(
352 ChangesetStatusModel().set_status(
353 repo=target_repo,
353 repo=target_repo,
354 status=ChangesetStatus.STATUS_UNDER_REVIEW,
354 status=ChangesetStatus.STATUS_UNDER_REVIEW,
355 user=created_by_user,
355 user=created_by_user,
356 pull_request=pull_request
356 pull_request=pull_request
357 )
357 )
358
358
359 self.notify_reviewers(pull_request, reviewer_ids)
359 self.notify_reviewers(pull_request, reviewer_ids)
360 self._trigger_pull_request_hook(
360 self._trigger_pull_request_hook(
361 pull_request, created_by_user, 'create')
361 pull_request, created_by_user, 'create')
362
362
363 return pull_request
363 return pull_request
364
364
365 def _trigger_pull_request_hook(self, pull_request, user, action):
365 def _trigger_pull_request_hook(self, pull_request, user, action):
366 pull_request = self.__get_pull_request(pull_request)
366 pull_request = self.__get_pull_request(pull_request)
367 target_scm = pull_request.target_repo.scm_instance()
367 target_scm = pull_request.target_repo.scm_instance()
368 if action == 'create':
368 if action == 'create':
369 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
369 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
370 elif action == 'merge':
370 elif action == 'merge':
371 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
371 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
372 elif action == 'close':
372 elif action == 'close':
373 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
373 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
374 elif action == 'review_status_change':
374 elif action == 'review_status_change':
375 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
375 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
376 elif action == 'update':
376 elif action == 'update':
377 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
377 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
378 else:
378 else:
379 return
379 return
380
380
381 trigger_hook(
381 trigger_hook(
382 username=user.username,
382 username=user.username,
383 repo_name=pull_request.target_repo.repo_name,
383 repo_name=pull_request.target_repo.repo_name,
384 repo_alias=target_scm.alias,
384 repo_alias=target_scm.alias,
385 pull_request=pull_request)
385 pull_request=pull_request)
386
386
387 def _get_commit_ids(self, pull_request):
387 def _get_commit_ids(self, pull_request):
388 """
388 """
389 Return the commit ids of the merged pull request.
389 Return the commit ids of the merged pull request.
390
390
391 This method is not dealing correctly yet with the lack of autoupdates
391 This method is not dealing correctly yet with the lack of autoupdates
392 nor with the implicit target updates.
392 nor with the implicit target updates.
393 For example: if a commit in the source repo is already in the target it
393 For example: if a commit in the source repo is already in the target it
394 will be reported anyways.
394 will be reported anyways.
395 """
395 """
396 merge_rev = pull_request.merge_rev
396 merge_rev = pull_request.merge_rev
397 if merge_rev is None:
397 if merge_rev is None:
398 raise ValueError('This pull request was not merged yet')
398 raise ValueError('This pull request was not merged yet')
399
399
400 commit_ids = list(pull_request.revisions)
400 commit_ids = list(pull_request.revisions)
401 if merge_rev not in commit_ids:
401 if merge_rev not in commit_ids:
402 commit_ids.append(merge_rev)
402 commit_ids.append(merge_rev)
403
403
404 return commit_ids
404 return commit_ids
405
405
406 def merge(self, pull_request, user, extras):
406 def merge(self, pull_request, user, extras):
407 log.debug("Merging pull request %s", pull_request.pull_request_id)
407 log.debug("Merging pull request %s", pull_request.pull_request_id)
408 merge_state = self._merge_pull_request(pull_request, user, extras)
408 merge_state = self._merge_pull_request(pull_request, user, extras)
409 if merge_state.executed:
409 if merge_state.executed:
410 log.debug(
410 log.debug(
411 "Merge was successful, updating the pull request comments.")
411 "Merge was successful, updating the pull request comments.")
412 self._comment_and_close_pr(pull_request, user, merge_state)
412 self._comment_and_close_pr(pull_request, user, merge_state)
413 self._log_action('user_merged_pull_request', user, pull_request)
413 self._log_action('user_merged_pull_request', user, pull_request)
414 else:
414 else:
415 log.warn("Merge failed, not updating the pull request.")
415 log.warn("Merge failed, not updating the pull request.")
416 return merge_state
416 return merge_state
417
417
418 def _merge_pull_request(self, pull_request, user, extras):
418 def _merge_pull_request(self, pull_request, user, extras):
419 target_vcs = pull_request.target_repo.scm_instance()
419 target_vcs = pull_request.target_repo.scm_instance()
420 source_vcs = pull_request.source_repo.scm_instance()
420 source_vcs = pull_request.source_repo.scm_instance()
421 target_ref = self._refresh_reference(
421 target_ref = self._refresh_reference(
422 pull_request.target_ref_parts, target_vcs)
422 pull_request.target_ref_parts, target_vcs)
423
423
424 message = _(
424 message = _(
425 'Merge pull request #%(pr_id)s from '
425 'Merge pull request #%(pr_id)s from '
426 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
426 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
427 'pr_id': pull_request.pull_request_id,
427 'pr_id': pull_request.pull_request_id,
428 'source_repo': source_vcs.name,
428 'source_repo': source_vcs.name,
429 'source_ref_name': pull_request.source_ref_parts.name,
429 'source_ref_name': pull_request.source_ref_parts.name,
430 'pr_title': pull_request.title
430 'pr_title': pull_request.title
431 }
431 }
432
432
433 workspace_id = self._workspace_id(pull_request)
433 workspace_id = self._workspace_id(pull_request)
434 use_rebase = self._use_rebase_for_merging(pull_request)
434 use_rebase = self._use_rebase_for_merging(pull_request)
435
435
436 callback_daemon, extras = prepare_callback_daemon(
436 callback_daemon, extras = prepare_callback_daemon(
437 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
437 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
438 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
438 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
439
439
440 with callback_daemon:
440 with callback_daemon:
441 # TODO: johbo: Implement a clean way to run a config_override
441 # TODO: johbo: Implement a clean way to run a config_override
442 # for a single call.
442 # for a single call.
443 target_vcs.config.set(
443 target_vcs.config.set(
444 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
444 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
445 merge_state = target_vcs.merge(
445 merge_state = target_vcs.merge(
446 target_ref, source_vcs, pull_request.source_ref_parts,
446 target_ref, source_vcs, pull_request.source_ref_parts,
447 workspace_id, user_name=user.username,
447 workspace_id, user_name=user.username,
448 user_email=user.email, message=message, use_rebase=use_rebase)
448 user_email=user.email, message=message, use_rebase=use_rebase)
449 return merge_state
449 return merge_state
450
450
451 def _comment_and_close_pr(self, pull_request, user, merge_state):
451 def _comment_and_close_pr(self, pull_request, user, merge_state):
452 pull_request.merge_rev = merge_state.merge_commit_id
452 pull_request.merge_rev = merge_state.merge_commit_id
453 pull_request.updated_on = datetime.datetime.now()
453 pull_request.updated_on = datetime.datetime.now()
454
454
455 ChangesetCommentsModel().create(
455 ChangesetCommentsModel().create(
456 text=unicode(_('Pull request merged and closed')),
456 text=unicode(_('Pull request merged and closed')),
457 repo=pull_request.target_repo.repo_id,
457 repo=pull_request.target_repo.repo_id,
458 user=user.user_id,
458 user=user.user_id,
459 pull_request=pull_request.pull_request_id,
459 pull_request=pull_request.pull_request_id,
460 f_path=None,
460 f_path=None,
461 line_no=None,
461 line_no=None,
462 closing_pr=True
462 closing_pr=True
463 )
463 )
464
464
465 Session().add(pull_request)
465 Session().add(pull_request)
466 Session().flush()
466 Session().flush()
467 # TODO: paris: replace invalidation with less radical solution
467 # TODO: paris: replace invalidation with less radical solution
468 ScmModel().mark_for_invalidation(
468 ScmModel().mark_for_invalidation(
469 pull_request.target_repo.repo_name)
469 pull_request.target_repo.repo_name)
470 self._trigger_pull_request_hook(pull_request, user, 'merge')
470 self._trigger_pull_request_hook(pull_request, user, 'merge')
471
471
472 def has_valid_update_type(self, pull_request):
472 def has_valid_update_type(self, pull_request):
473 source_ref_type = pull_request.source_ref_parts.type
473 source_ref_type = pull_request.source_ref_parts.type
474 return source_ref_type in ['book', 'branch', 'tag']
474 return source_ref_type in ['book', 'branch', 'tag']
475
475
476 def update_commits(self, pull_request):
476 def update_commits(self, pull_request):
477 """
477 """
478 Get the updated list of commits for the pull request
478 Get the updated list of commits for the pull request
479 and return the new pull request version and the list
479 and return the new pull request version and the list
480 of commits processed by this update action
480 of commits processed by this update action
481 """
481 """
482
482
483 pull_request = self.__get_pull_request(pull_request)
483 pull_request = self.__get_pull_request(pull_request)
484 source_ref_type = pull_request.source_ref_parts.type
484 source_ref_type = pull_request.source_ref_parts.type
485 source_ref_name = pull_request.source_ref_parts.name
485 source_ref_name = pull_request.source_ref_parts.name
486 source_ref_id = pull_request.source_ref_parts.commit_id
486 source_ref_id = pull_request.source_ref_parts.commit_id
487
487
488 if not self.has_valid_update_type(pull_request):
488 if not self.has_valid_update_type(pull_request):
489 log.debug(
489 log.debug(
490 "Skipping update of pull request %s due to ref type: %s",
490 "Skipping update of pull request %s due to ref type: %s",
491 pull_request, source_ref_type)
491 pull_request, source_ref_type)
492 return (None, None)
492 return (None, None)
493
493
494 source_repo = pull_request.source_repo.scm_instance()
494 source_repo = pull_request.source_repo.scm_instance()
495 source_commit = source_repo.get_commit(commit_id=source_ref_name)
495 source_commit = source_repo.get_commit(commit_id=source_ref_name)
496 if source_ref_id == source_commit.raw_id:
496 if source_ref_id == source_commit.raw_id:
497 log.debug("Nothing changed in pull request %s", pull_request)
497 log.debug("Nothing changed in pull request %s", pull_request)
498 return (None, None)
498 return (None, None)
499
499
500 # Finally there is a need for an update
500 # Finally there is a need for an update
501 pull_request_version = self._create_version_from_snapshot(pull_request)
501 pull_request_version = self._create_version_from_snapshot(pull_request)
502 self._link_comments_to_version(pull_request_version)
502 self._link_comments_to_version(pull_request_version)
503
503
504 target_ref_type = pull_request.target_ref_parts.type
504 target_ref_type = pull_request.target_ref_parts.type
505 target_ref_name = pull_request.target_ref_parts.name
505 target_ref_name = pull_request.target_ref_parts.name
506 target_ref_id = pull_request.target_ref_parts.commit_id
506 target_ref_id = pull_request.target_ref_parts.commit_id
507 target_repo = pull_request.target_repo.scm_instance()
507 target_repo = pull_request.target_repo.scm_instance()
508
508
509 if target_ref_type in ('tag', 'branch', 'book'):
509 if target_ref_type in ('tag', 'branch', 'book'):
510 target_commit = target_repo.get_commit(target_ref_name)
510 target_commit = target_repo.get_commit(target_ref_name)
511 else:
511 else:
512 target_commit = target_repo.get_commit(target_ref_id)
512 target_commit = target_repo.get_commit(target_ref_id)
513
513
514 # re-compute commit ids
514 # re-compute commit ids
515 old_commit_ids = set(pull_request.revisions)
515 old_commit_ids = set(pull_request.revisions)
516 pre_load = ["author", "branch", "date", "message"]
516 pre_load = ["author", "branch", "date", "message"]
517 commit_ranges = target_repo.compare(
517 commit_ranges = target_repo.compare(
518 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
518 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
519 pre_load=pre_load)
519 pre_load=pre_load)
520
520
521 ancestor = target_repo.get_common_ancestor(
521 ancestor = target_repo.get_common_ancestor(
522 target_commit.raw_id, source_commit.raw_id, source_repo)
522 target_commit.raw_id, source_commit.raw_id, source_repo)
523
523
524 pull_request.source_ref = '%s:%s:%s' % (
524 pull_request.source_ref = '%s:%s:%s' % (
525 source_ref_type, source_ref_name, source_commit.raw_id)
525 source_ref_type, source_ref_name, source_commit.raw_id)
526 pull_request.target_ref = '%s:%s:%s' % (
526 pull_request.target_ref = '%s:%s:%s' % (
527 target_ref_type, target_ref_name, ancestor)
527 target_ref_type, target_ref_name, ancestor)
528 pull_request.revisions = [
528 pull_request.revisions = [
529 commit.raw_id for commit in reversed(commit_ranges)]
529 commit.raw_id for commit in reversed(commit_ranges)]
530 pull_request.updated_on = datetime.datetime.now()
530 pull_request.updated_on = datetime.datetime.now()
531 Session().add(pull_request)
531 Session().add(pull_request)
532 new_commit_ids = set(pull_request.revisions)
532 new_commit_ids = set(pull_request.revisions)
533
533
534 changes = self._calculate_commit_id_changes(
534 changes = self._calculate_commit_id_changes(
535 old_commit_ids, new_commit_ids)
535 old_commit_ids, new_commit_ids)
536
536
537 old_diff_data, new_diff_data = self._generate_update_diffs(
537 old_diff_data, new_diff_data = self._generate_update_diffs(
538 pull_request, pull_request_version)
538 pull_request, pull_request_version)
539
539
540 ChangesetCommentsModel().outdate_comments(
540 ChangesetCommentsModel().outdate_comments(
541 pull_request, old_diff_data=old_diff_data,
541 pull_request, old_diff_data=old_diff_data,
542 new_diff_data=new_diff_data)
542 new_diff_data=new_diff_data)
543
543
544 file_changes = self._calculate_file_changes(
544 file_changes = self._calculate_file_changes(
545 old_diff_data, new_diff_data)
545 old_diff_data, new_diff_data)
546
546
547 # Add an automatic comment to the pull request
547 # Add an automatic comment to the pull request
548 update_comment = ChangesetCommentsModel().create(
548 update_comment = ChangesetCommentsModel().create(
549 text=self._render_update_message(changes, file_changes),
549 text=self._render_update_message(changes, file_changes),
550 repo=pull_request.target_repo,
550 repo=pull_request.target_repo,
551 user=pull_request.author,
551 user=pull_request.author,
552 pull_request=pull_request,
552 pull_request=pull_request,
553 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
553 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
554
554
555 # Update status to "Under Review" for added commits
555 # Update status to "Under Review" for added commits
556 for commit_id in changes.added:
556 for commit_id in changes.added:
557 ChangesetStatusModel().set_status(
557 ChangesetStatusModel().set_status(
558 repo=pull_request.source_repo,
558 repo=pull_request.source_repo,
559 status=ChangesetStatus.STATUS_UNDER_REVIEW,
559 status=ChangesetStatus.STATUS_UNDER_REVIEW,
560 comment=update_comment,
560 comment=update_comment,
561 user=pull_request.author,
561 user=pull_request.author,
562 pull_request=pull_request,
562 pull_request=pull_request,
563 revision=commit_id)
563 revision=commit_id)
564
564
565 log.debug(
565 log.debug(
566 'Updated pull request %s, added_ids: %s, common_ids: %s, '
566 'Updated pull request %s, added_ids: %s, common_ids: %s, '
567 'removed_ids: %s', pull_request.pull_request_id,
567 'removed_ids: %s', pull_request.pull_request_id,
568 changes.added, changes.common, changes.removed)
568 changes.added, changes.common, changes.removed)
569 log.debug('Updated pull request with the following file changes: %s',
569 log.debug('Updated pull request with the following file changes: %s',
570 file_changes)
570 file_changes)
571
571
572 log.info(
572 log.info(
573 "Updated pull request %s from commit %s to commit %s, "
573 "Updated pull request %s from commit %s to commit %s, "
574 "stored new version %s of this pull request.",
574 "stored new version %s of this pull request.",
575 pull_request.pull_request_id, source_ref_id,
575 pull_request.pull_request_id, source_ref_id,
576 pull_request.source_ref_parts.commit_id,
576 pull_request.source_ref_parts.commit_id,
577 pull_request_version.pull_request_version_id)
577 pull_request_version.pull_request_version_id)
578 Session().commit()
578 Session().commit()
579 self._trigger_pull_request_hook(pull_request, pull_request.author,
579 self._trigger_pull_request_hook(pull_request, pull_request.author,
580 'update')
580 'update')
581
581
582 return (pull_request_version, changes)
582 return (pull_request_version, changes)
583
583
584 def _create_version_from_snapshot(self, pull_request):
584 def _create_version_from_snapshot(self, pull_request):
585 version = PullRequestVersion()
585 version = PullRequestVersion()
586 version.title = pull_request.title
586 version.title = pull_request.title
587 version.description = pull_request.description
587 version.description = pull_request.description
588 version.status = pull_request.status
588 version.status = pull_request.status
589 version.created_on = pull_request.created_on
589 version.created_on = pull_request.created_on
590 version.updated_on = pull_request.updated_on
590 version.updated_on = pull_request.updated_on
591 version.user_id = pull_request.user_id
591 version.user_id = pull_request.user_id
592 version.source_repo = pull_request.source_repo
592 version.source_repo = pull_request.source_repo
593 version.source_ref = pull_request.source_ref
593 version.source_ref = pull_request.source_ref
594 version.target_repo = pull_request.target_repo
594 version.target_repo = pull_request.target_repo
595 version.target_ref = pull_request.target_ref
595 version.target_ref = pull_request.target_ref
596
596
597 version._last_merge_source_rev = pull_request._last_merge_source_rev
597 version._last_merge_source_rev = pull_request._last_merge_source_rev
598 version._last_merge_target_rev = pull_request._last_merge_target_rev
598 version._last_merge_target_rev = pull_request._last_merge_target_rev
599 version._last_merge_status = pull_request._last_merge_status
599 version._last_merge_status = pull_request._last_merge_status
600 version.merge_rev = pull_request.merge_rev
600 version.merge_rev = pull_request.merge_rev
601
601
602 version.revisions = pull_request.revisions
602 version.revisions = pull_request.revisions
603 version.pull_request = pull_request
603 version.pull_request = pull_request
604 Session().add(version)
604 Session().add(version)
605 Session().flush()
605 Session().flush()
606
606
607 return version
607 return version
608
608
609 def _generate_update_diffs(self, pull_request, pull_request_version):
609 def _generate_update_diffs(self, pull_request, pull_request_version):
610 diff_context = (
610 diff_context = (
611 self.DIFF_CONTEXT +
611 self.DIFF_CONTEXT +
612 ChangesetCommentsModel.needed_extra_diff_context())
612 ChangesetCommentsModel.needed_extra_diff_context())
613 old_diff = self._get_diff_from_pr_or_version(
613 old_diff = self._get_diff_from_pr_or_version(
614 pull_request_version, context=diff_context)
614 pull_request_version, context=diff_context)
615 new_diff = self._get_diff_from_pr_or_version(
615 new_diff = self._get_diff_from_pr_or_version(
616 pull_request, context=diff_context)
616 pull_request, context=diff_context)
617
617
618 old_diff_data = diffs.DiffProcessor(old_diff)
618 old_diff_data = diffs.DiffProcessor(old_diff)
619 old_diff_data.prepare()
619 old_diff_data.prepare()
620 new_diff_data = diffs.DiffProcessor(new_diff)
620 new_diff_data = diffs.DiffProcessor(new_diff)
621 new_diff_data.prepare()
621 new_diff_data.prepare()
622
622
623 return old_diff_data, new_diff_data
623 return old_diff_data, new_diff_data
624
624
625 def _link_comments_to_version(self, pull_request_version):
625 def _link_comments_to_version(self, pull_request_version):
626 """
626 """
627 Link all unlinked comments of this pull request to the given version.
627 Link all unlinked comments of this pull request to the given version.
628
628
629 :param pull_request_version: The `PullRequestVersion` to which
629 :param pull_request_version: The `PullRequestVersion` to which
630 the comments shall be linked.
630 the comments shall be linked.
631
631
632 """
632 """
633 pull_request = pull_request_version.pull_request
633 pull_request = pull_request_version.pull_request
634 comments = ChangesetComment.query().filter(
634 comments = ChangesetComment.query().filter(
635 # TODO: johbo: Should we query for the repo at all here?
635 # TODO: johbo: Should we query for the repo at all here?
636 # Pending decision on how comments of PRs are to be related
636 # Pending decision on how comments of PRs are to be related
637 # to either the source repo, the target repo or no repo at all.
637 # to either the source repo, the target repo or no repo at all.
638 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
638 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
639 ChangesetComment.pull_request == pull_request,
639 ChangesetComment.pull_request == pull_request,
640 ChangesetComment.pull_request_version == None)
640 ChangesetComment.pull_request_version == None)
641
641
642 # TODO: johbo: Find out why this breaks if it is done in a bulk
642 # TODO: johbo: Find out why this breaks if it is done in a bulk
643 # operation.
643 # operation.
644 for comment in comments:
644 for comment in comments:
645 comment.pull_request_version_id = (
645 comment.pull_request_version_id = (
646 pull_request_version.pull_request_version_id)
646 pull_request_version.pull_request_version_id)
647 Session().add(comment)
647 Session().add(comment)
648
648
649 def _calculate_commit_id_changes(self, old_ids, new_ids):
649 def _calculate_commit_id_changes(self, old_ids, new_ids):
650 added = new_ids.difference(old_ids)
650 added = new_ids.difference(old_ids)
651 common = old_ids.intersection(new_ids)
651 common = old_ids.intersection(new_ids)
652 removed = old_ids.difference(new_ids)
652 removed = old_ids.difference(new_ids)
653 return ChangeTuple(added, common, removed)
653 return ChangeTuple(added, common, removed)
654
654
655 def _calculate_file_changes(self, old_diff_data, new_diff_data):
655 def _calculate_file_changes(self, old_diff_data, new_diff_data):
656
656
657 old_files = OrderedDict()
657 old_files = OrderedDict()
658 for diff_data in old_diff_data.parsed_diff:
658 for diff_data in old_diff_data.parsed_diff:
659 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
659 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
660
660
661 added_files = []
661 added_files = []
662 modified_files = []
662 modified_files = []
663 removed_files = []
663 removed_files = []
664 for diff_data in new_diff_data.parsed_diff:
664 for diff_data in new_diff_data.parsed_diff:
665 new_filename = diff_data['filename']
665 new_filename = diff_data['filename']
666 new_hash = md5_safe(diff_data['raw_diff'])
666 new_hash = md5_safe(diff_data['raw_diff'])
667
667
668 old_hash = old_files.get(new_filename)
668 old_hash = old_files.get(new_filename)
669 if not old_hash:
669 if not old_hash:
670 # file is not present in old diff, means it's added
670 # file is not present in old diff, means it's added
671 added_files.append(new_filename)
671 added_files.append(new_filename)
672 else:
672 else:
673 if new_hash != old_hash:
673 if new_hash != old_hash:
674 modified_files.append(new_filename)
674 modified_files.append(new_filename)
675 # now remove a file from old, since we have seen it already
675 # now remove a file from old, since we have seen it already
676 del old_files[new_filename]
676 del old_files[new_filename]
677
677
678 # removed files is when there are present in old, but not in NEW,
678 # removed files is when there are present in old, but not in NEW,
679 # since we remove old files that are present in new diff, left-overs
679 # since we remove old files that are present in new diff, left-overs
680 # if any should be the removed files
680 # if any should be the removed files
681 removed_files.extend(old_files.keys())
681 removed_files.extend(old_files.keys())
682
682
683 return FileChangeTuple(added_files, modified_files, removed_files)
683 return FileChangeTuple(added_files, modified_files, removed_files)
684
684
685 def _render_update_message(self, changes, file_changes):
685 def _render_update_message(self, changes, file_changes):
686 """
686 """
687 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
687 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
688 so it's always looking the same disregarding on which default
688 so it's always looking the same disregarding on which default
689 renderer system is using.
689 renderer system is using.
690
690
691 :param changes: changes named tuple
691 :param changes: changes named tuple
692 :param file_changes: file changes named tuple
692 :param file_changes: file changes named tuple
693
693
694 """
694 """
695 new_status = ChangesetStatus.get_status_lbl(
695 new_status = ChangesetStatus.get_status_lbl(
696 ChangesetStatus.STATUS_UNDER_REVIEW)
696 ChangesetStatus.STATUS_UNDER_REVIEW)
697
697
698 changed_files = (
698 changed_files = (
699 file_changes.added + file_changes.modified + file_changes.removed)
699 file_changes.added + file_changes.modified + file_changes.removed)
700
700
701 params = {
701 params = {
702 'under_review_label': new_status,
702 'under_review_label': new_status,
703 'added_commits': changes.added,
703 'added_commits': changes.added,
704 'removed_commits': changes.removed,
704 'removed_commits': changes.removed,
705 'changed_files': changed_files,
705 'changed_files': changed_files,
706 'added_files': file_changes.added,
706 'added_files': file_changes.added,
707 'modified_files': file_changes.modified,
707 'modified_files': file_changes.modified,
708 'removed_files': file_changes.removed,
708 'removed_files': file_changes.removed,
709 }
709 }
710 renderer = RstTemplateRenderer()
710 renderer = RstTemplateRenderer()
711 return renderer.render('pull_request_update.mako', **params)
711 return renderer.render('pull_request_update.mako', **params)
712
712
713 def edit(self, pull_request, title, description):
713 def edit(self, pull_request, title, description):
714 pull_request = self.__get_pull_request(pull_request)
714 pull_request = self.__get_pull_request(pull_request)
715 if pull_request.is_closed():
715 if pull_request.is_closed():
716 raise ValueError('This pull request is closed')
716 raise ValueError('This pull request is closed')
717 if title:
717 if title:
718 pull_request.title = title
718 pull_request.title = title
719 pull_request.description = description
719 pull_request.description = description
720 pull_request.updated_on = datetime.datetime.now()
720 pull_request.updated_on = datetime.datetime.now()
721 Session().add(pull_request)
721 Session().add(pull_request)
722
722
723 def update_reviewers(self, pull_request, reviewer_data):
723 def update_reviewers(self, pull_request, reviewer_data):
724 """
724 """
725 Update the reviewers in the pull request
725 Update the reviewers in the pull request
726
726
727 :param pull_request: the pr to update
727 :param pull_request: the pr to update
728 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
728 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
729 """
729 """
730
730
731 reviewers_reasons = {}
731 reviewers_reasons = {}
732 for user_id, reasons in reviewer_data:
732 for user_id, reasons in reviewer_data:
733 if isinstance(user_id, (int, basestring)):
733 if isinstance(user_id, (int, basestring)):
734 user_id = self._get_user(user_id).user_id
734 user_id = self._get_user(user_id).user_id
735 reviewers_reasons[user_id] = reasons
735 reviewers_reasons[user_id] = reasons
736
736
737 reviewers_ids = set(reviewers_reasons.keys())
737 reviewers_ids = set(reviewers_reasons.keys())
738 pull_request = self.__get_pull_request(pull_request)
738 pull_request = self.__get_pull_request(pull_request)
739 current_reviewers = PullRequestReviewers.query()\
739 current_reviewers = PullRequestReviewers.query()\
740 .filter(PullRequestReviewers.pull_request ==
740 .filter(PullRequestReviewers.pull_request ==
741 pull_request).all()
741 pull_request).all()
742 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
742 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
743
743
744 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
744 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
745 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
745 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
746
746
747 log.debug("Adding %s reviewers", ids_to_add)
747 log.debug("Adding %s reviewers", ids_to_add)
748 log.debug("Removing %s reviewers", ids_to_remove)
748 log.debug("Removing %s reviewers", ids_to_remove)
749 changed = False
749 changed = False
750 for uid in ids_to_add:
750 for uid in ids_to_add:
751 changed = True
751 changed = True
752 _usr = self._get_user(uid)
752 _usr = self._get_user(uid)
753 reasons = reviewers_reasons[uid]
753 reasons = reviewers_reasons[uid]
754 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
754 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
755 Session().add(reviewer)
755 Session().add(reviewer)
756
756
757 self.notify_reviewers(pull_request, ids_to_add)
757 self.notify_reviewers(pull_request, ids_to_add)
758
758
759 for uid in ids_to_remove:
759 for uid in ids_to_remove:
760 changed = True
760 changed = True
761 reviewer = PullRequestReviewers.query()\
761 reviewer = PullRequestReviewers.query()\
762 .filter(PullRequestReviewers.user_id == uid,
762 .filter(PullRequestReviewers.user_id == uid,
763 PullRequestReviewers.pull_request == pull_request)\
763 PullRequestReviewers.pull_request == pull_request)\
764 .scalar()
764 .scalar()
765 if reviewer:
765 if reviewer:
766 Session().delete(reviewer)
766 Session().delete(reviewer)
767 if changed:
767 if changed:
768 pull_request.updated_on = datetime.datetime.now()
768 pull_request.updated_on = datetime.datetime.now()
769 Session().add(pull_request)
769 Session().add(pull_request)
770
770
771 return ids_to_add, ids_to_remove
771 return ids_to_add, ids_to_remove
772
772
773 def get_url(self, pull_request):
773 def get_url(self, pull_request):
774 return h.url('pullrequest_show',
774 return h.url('pullrequest_show',
775 repo_name=safe_str(pull_request.target_repo.repo_name),
775 repo_name=safe_str(pull_request.target_repo.repo_name),
776 pull_request_id=pull_request.pull_request_id,
776 pull_request_id=pull_request.pull_request_id,
777 qualified=True)
777 qualified=True)
778
778
779 def get_shadow_clone_url(self, pull_request):
780 return u'{url}/repository'.format(url=self.get_url(pull_request))
781
779 def notify_reviewers(self, pull_request, reviewers_ids):
782 def notify_reviewers(self, pull_request, reviewers_ids):
780 # notification to reviewers
783 # notification to reviewers
781 if not reviewers_ids:
784 if not reviewers_ids:
782 return
785 return
783
786
784 pull_request_obj = pull_request
787 pull_request_obj = pull_request
785 # get the current participants of this pull request
788 # get the current participants of this pull request
786 recipients = reviewers_ids
789 recipients = reviewers_ids
787 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
790 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
788
791
789 pr_source_repo = pull_request_obj.source_repo
792 pr_source_repo = pull_request_obj.source_repo
790 pr_target_repo = pull_request_obj.target_repo
793 pr_target_repo = pull_request_obj.target_repo
791
794
792 pr_url = h.url(
795 pr_url = h.url(
793 'pullrequest_show',
796 'pullrequest_show',
794 repo_name=pr_target_repo.repo_name,
797 repo_name=pr_target_repo.repo_name,
795 pull_request_id=pull_request_obj.pull_request_id,
798 pull_request_id=pull_request_obj.pull_request_id,
796 qualified=True,)
799 qualified=True,)
797
800
798 # set some variables for email notification
801 # set some variables for email notification
799 pr_target_repo_url = h.url(
802 pr_target_repo_url = h.url(
800 'summary_home',
803 'summary_home',
801 repo_name=pr_target_repo.repo_name,
804 repo_name=pr_target_repo.repo_name,
802 qualified=True)
805 qualified=True)
803
806
804 pr_source_repo_url = h.url(
807 pr_source_repo_url = h.url(
805 'summary_home',
808 'summary_home',
806 repo_name=pr_source_repo.repo_name,
809 repo_name=pr_source_repo.repo_name,
807 qualified=True)
810 qualified=True)
808
811
809 # pull request specifics
812 # pull request specifics
810 pull_request_commits = [
813 pull_request_commits = [
811 (x.raw_id, x.message)
814 (x.raw_id, x.message)
812 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
815 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
813
816
814 kwargs = {
817 kwargs = {
815 'user': pull_request.author,
818 'user': pull_request.author,
816 'pull_request': pull_request_obj,
819 'pull_request': pull_request_obj,
817 'pull_request_commits': pull_request_commits,
820 'pull_request_commits': pull_request_commits,
818
821
819 'pull_request_target_repo': pr_target_repo,
822 'pull_request_target_repo': pr_target_repo,
820 'pull_request_target_repo_url': pr_target_repo_url,
823 'pull_request_target_repo_url': pr_target_repo_url,
821
824
822 'pull_request_source_repo': pr_source_repo,
825 'pull_request_source_repo': pr_source_repo,
823 'pull_request_source_repo_url': pr_source_repo_url,
826 'pull_request_source_repo_url': pr_source_repo_url,
824
827
825 'pull_request_url': pr_url,
828 'pull_request_url': pr_url,
826 }
829 }
827
830
828 # pre-generate the subject for notification itself
831 # pre-generate the subject for notification itself
829 (subject,
832 (subject,
830 _h, _e, # we don't care about those
833 _h, _e, # we don't care about those
831 body_plaintext) = EmailNotificationModel().render_email(
834 body_plaintext) = EmailNotificationModel().render_email(
832 notification_type, **kwargs)
835 notification_type, **kwargs)
833
836
834 # create notification objects, and emails
837 # create notification objects, and emails
835 NotificationModel().create(
838 NotificationModel().create(
836 created_by=pull_request.author,
839 created_by=pull_request.author,
837 notification_subject=subject,
840 notification_subject=subject,
838 notification_body=body_plaintext,
841 notification_body=body_plaintext,
839 notification_type=notification_type,
842 notification_type=notification_type,
840 recipients=recipients,
843 recipients=recipients,
841 email_kwargs=kwargs,
844 email_kwargs=kwargs,
842 )
845 )
843
846
844 def delete(self, pull_request):
847 def delete(self, pull_request):
845 pull_request = self.__get_pull_request(pull_request)
848 pull_request = self.__get_pull_request(pull_request)
846 self._cleanup_merge_workspace(pull_request)
849 self._cleanup_merge_workspace(pull_request)
847 Session().delete(pull_request)
850 Session().delete(pull_request)
848
851
849 def close_pull_request(self, pull_request, user):
852 def close_pull_request(self, pull_request, user):
850 pull_request = self.__get_pull_request(pull_request)
853 pull_request = self.__get_pull_request(pull_request)
851 self._cleanup_merge_workspace(pull_request)
854 self._cleanup_merge_workspace(pull_request)
852 pull_request.status = PullRequest.STATUS_CLOSED
855 pull_request.status = PullRequest.STATUS_CLOSED
853 pull_request.updated_on = datetime.datetime.now()
856 pull_request.updated_on = datetime.datetime.now()
854 Session().add(pull_request)
857 Session().add(pull_request)
855 self._trigger_pull_request_hook(
858 self._trigger_pull_request_hook(
856 pull_request, pull_request.author, 'close')
859 pull_request, pull_request.author, 'close')
857 self._log_action('user_closed_pull_request', user, pull_request)
860 self._log_action('user_closed_pull_request', user, pull_request)
858
861
859 def close_pull_request_with_comment(self, pull_request, user, repo,
862 def close_pull_request_with_comment(self, pull_request, user, repo,
860 message=None):
863 message=None):
861 status = ChangesetStatus.STATUS_REJECTED
864 status = ChangesetStatus.STATUS_REJECTED
862
865
863 if not message:
866 if not message:
864 message = (
867 message = (
865 _('Status change %(transition_icon)s %(status)s') % {
868 _('Status change %(transition_icon)s %(status)s') % {
866 'transition_icon': '>',
869 'transition_icon': '>',
867 'status': ChangesetStatus.get_status_lbl(status)})
870 'status': ChangesetStatus.get_status_lbl(status)})
868
871
869 internal_message = _('Closing with') + ' ' + message
872 internal_message = _('Closing with') + ' ' + message
870
873
871 comm = ChangesetCommentsModel().create(
874 comm = ChangesetCommentsModel().create(
872 text=internal_message,
875 text=internal_message,
873 repo=repo.repo_id,
876 repo=repo.repo_id,
874 user=user.user_id,
877 user=user.user_id,
875 pull_request=pull_request.pull_request_id,
878 pull_request=pull_request.pull_request_id,
876 f_path=None,
879 f_path=None,
877 line_no=None,
880 line_no=None,
878 status_change=ChangesetStatus.get_status_lbl(status),
881 status_change=ChangesetStatus.get_status_lbl(status),
879 status_change_type=status,
882 status_change_type=status,
880 closing_pr=True
883 closing_pr=True
881 )
884 )
882
885
883 ChangesetStatusModel().set_status(
886 ChangesetStatusModel().set_status(
884 repo.repo_id,
887 repo.repo_id,
885 status,
888 status,
886 user.user_id,
889 user.user_id,
887 comm,
890 comm,
888 pull_request=pull_request.pull_request_id
891 pull_request=pull_request.pull_request_id
889 )
892 )
890 Session().flush()
893 Session().flush()
891
894
892 PullRequestModel().close_pull_request(
895 PullRequestModel().close_pull_request(
893 pull_request.pull_request_id, user)
896 pull_request.pull_request_id, user)
894
897
895 def merge_status(self, pull_request):
898 def merge_status(self, pull_request):
896 if not self._is_merge_enabled(pull_request):
899 if not self._is_merge_enabled(pull_request):
897 return False, _('Server-side pull request merging is disabled.')
900 return False, _('Server-side pull request merging is disabled.')
898 if pull_request.is_closed():
901 if pull_request.is_closed():
899 return False, _('This pull request is closed.')
902 return False, _('This pull request is closed.')
900 merge_possible, msg = self._check_repo_requirements(
903 merge_possible, msg = self._check_repo_requirements(
901 target=pull_request.target_repo, source=pull_request.source_repo)
904 target=pull_request.target_repo, source=pull_request.source_repo)
902 if not merge_possible:
905 if not merge_possible:
903 return merge_possible, msg
906 return merge_possible, msg
904
907
905 try:
908 try:
906 resp = self._try_merge(pull_request)
909 resp = self._try_merge(pull_request)
907 status = resp.possible, self.merge_status_message(
910 status = resp.possible, self.merge_status_message(
908 resp.failure_reason)
911 resp.failure_reason)
909 except NotImplementedError:
912 except NotImplementedError:
910 status = False, _('Pull request merging is not supported.')
913 status = False, _('Pull request merging is not supported.')
911
914
912 return status
915 return status
913
916
914 def _check_repo_requirements(self, target, source):
917 def _check_repo_requirements(self, target, source):
915 """
918 """
916 Check if `target` and `source` have compatible requirements.
919 Check if `target` and `source` have compatible requirements.
917
920
918 Currently this is just checking for largefiles.
921 Currently this is just checking for largefiles.
919 """
922 """
920 target_has_largefiles = self._has_largefiles(target)
923 target_has_largefiles = self._has_largefiles(target)
921 source_has_largefiles = self._has_largefiles(source)
924 source_has_largefiles = self._has_largefiles(source)
922 merge_possible = True
925 merge_possible = True
923 message = u''
926 message = u''
924
927
925 if target_has_largefiles != source_has_largefiles:
928 if target_has_largefiles != source_has_largefiles:
926 merge_possible = False
929 merge_possible = False
927 if source_has_largefiles:
930 if source_has_largefiles:
928 message = _(
931 message = _(
929 'Target repository large files support is disabled.')
932 'Target repository large files support is disabled.')
930 else:
933 else:
931 message = _(
934 message = _(
932 'Source repository large files support is disabled.')
935 'Source repository large files support is disabled.')
933
936
934 return merge_possible, message
937 return merge_possible, message
935
938
936 def _has_largefiles(self, repo):
939 def _has_largefiles(self, repo):
937 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
940 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
938 'extensions', 'largefiles')
941 'extensions', 'largefiles')
939 return largefiles_ui and largefiles_ui[0].active
942 return largefiles_ui and largefiles_ui[0].active
940
943
941 def _try_merge(self, pull_request):
944 def _try_merge(self, pull_request):
942 """
945 """
943 Try to merge the pull request and return the merge status.
946 Try to merge the pull request and return the merge status.
944 """
947 """
945 log.debug(
948 log.debug(
946 "Trying out if the pull request %s can be merged.",
949 "Trying out if the pull request %s can be merged.",
947 pull_request.pull_request_id)
950 pull_request.pull_request_id)
948 target_vcs = pull_request.target_repo.scm_instance()
951 target_vcs = pull_request.target_repo.scm_instance()
949 target_ref = self._refresh_reference(
952 target_ref = self._refresh_reference(
950 pull_request.target_ref_parts, target_vcs)
953 pull_request.target_ref_parts, target_vcs)
951
954
952 target_locked = pull_request.target_repo.locked
955 target_locked = pull_request.target_repo.locked
953 if target_locked and target_locked[0]:
956 if target_locked and target_locked[0]:
954 log.debug("The target repository is locked.")
957 log.debug("The target repository is locked.")
955 merge_state = MergeResponse(
958 merge_state = MergeResponse(
956 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
959 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
957 elif self._needs_merge_state_refresh(pull_request, target_ref):
960 elif self._needs_merge_state_refresh(pull_request, target_ref):
958 log.debug("Refreshing the merge status of the repository.")
961 log.debug("Refreshing the merge status of the repository.")
959 merge_state = self._refresh_merge_state(
962 merge_state = self._refresh_merge_state(
960 pull_request, target_vcs, target_ref)
963 pull_request, target_vcs, target_ref)
961 else:
964 else:
962 possible = pull_request.\
965 possible = pull_request.\
963 _last_merge_status == MergeFailureReason.NONE
966 _last_merge_status == MergeFailureReason.NONE
964 merge_state = MergeResponse(
967 merge_state = MergeResponse(
965 possible, False, None, pull_request._last_merge_status)
968 possible, False, None, pull_request._last_merge_status)
966 log.debug("Merge response: %s", merge_state)
969 log.debug("Merge response: %s", merge_state)
967 return merge_state
970 return merge_state
968
971
969 def _refresh_reference(self, reference, vcs_repository):
972 def _refresh_reference(self, reference, vcs_repository):
970 if reference.type in ('branch', 'book'):
973 if reference.type in ('branch', 'book'):
971 name_or_id = reference.name
974 name_or_id = reference.name
972 else:
975 else:
973 name_or_id = reference.commit_id
976 name_or_id = reference.commit_id
974 refreshed_commit = vcs_repository.get_commit(name_or_id)
977 refreshed_commit = vcs_repository.get_commit(name_or_id)
975 refreshed_reference = Reference(
978 refreshed_reference = Reference(
976 reference.type, reference.name, refreshed_commit.raw_id)
979 reference.type, reference.name, refreshed_commit.raw_id)
977 return refreshed_reference
980 return refreshed_reference
978
981
979 def _needs_merge_state_refresh(self, pull_request, target_reference):
982 def _needs_merge_state_refresh(self, pull_request, target_reference):
980 return not(
983 return not(
981 pull_request.revisions and
984 pull_request.revisions and
982 pull_request.revisions[0] == pull_request._last_merge_source_rev and
985 pull_request.revisions[0] == pull_request._last_merge_source_rev and
983 target_reference.commit_id == pull_request._last_merge_target_rev)
986 target_reference.commit_id == pull_request._last_merge_target_rev)
984
987
985 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
988 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
986 workspace_id = self._workspace_id(pull_request)
989 workspace_id = self._workspace_id(pull_request)
987 source_vcs = pull_request.source_repo.scm_instance()
990 source_vcs = pull_request.source_repo.scm_instance()
988 use_rebase = self._use_rebase_for_merging(pull_request)
991 use_rebase = self._use_rebase_for_merging(pull_request)
989 merge_state = target_vcs.merge(
992 merge_state = target_vcs.merge(
990 target_reference, source_vcs, pull_request.source_ref_parts,
993 target_reference, source_vcs, pull_request.source_ref_parts,
991 workspace_id, dry_run=True, use_rebase=use_rebase)
994 workspace_id, dry_run=True, use_rebase=use_rebase)
992
995
993 # Do not store the response if there was an unknown error.
996 # Do not store the response if there was an unknown error.
994 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
997 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
995 pull_request._last_merge_source_rev = pull_request.\
998 pull_request._last_merge_source_rev = pull_request.\
996 source_ref_parts.commit_id
999 source_ref_parts.commit_id
997 pull_request._last_merge_target_rev = target_reference.commit_id
1000 pull_request._last_merge_target_rev = target_reference.commit_id
998 pull_request._last_merge_status = (
1001 pull_request._last_merge_status = (
999 merge_state.failure_reason)
1002 merge_state.failure_reason)
1000 Session().add(pull_request)
1003 Session().add(pull_request)
1001 Session().flush()
1004 Session().flush()
1002
1005
1003 return merge_state
1006 return merge_state
1004
1007
1005 def _workspace_id(self, pull_request):
1008 def _workspace_id(self, pull_request):
1006 workspace_id = 'pr-%s' % pull_request.pull_request_id
1009 workspace_id = 'pr-%s' % pull_request.pull_request_id
1007 return workspace_id
1010 return workspace_id
1008
1011
1009 def merge_status_message(self, status_code):
1012 def merge_status_message(self, status_code):
1010 """
1013 """
1011 Return a human friendly error message for the given merge status code.
1014 Return a human friendly error message for the given merge status code.
1012 """
1015 """
1013 return self.MERGE_STATUS_MESSAGES[status_code]
1016 return self.MERGE_STATUS_MESSAGES[status_code]
1014
1017
1015 def generate_repo_data(self, repo, commit_id=None, branch=None,
1018 def generate_repo_data(self, repo, commit_id=None, branch=None,
1016 bookmark=None):
1019 bookmark=None):
1017 all_refs, selected_ref = \
1020 all_refs, selected_ref = \
1018 self._get_repo_pullrequest_sources(
1021 self._get_repo_pullrequest_sources(
1019 repo.scm_instance(), commit_id=commit_id,
1022 repo.scm_instance(), commit_id=commit_id,
1020 branch=branch, bookmark=bookmark)
1023 branch=branch, bookmark=bookmark)
1021
1024
1022 refs_select2 = []
1025 refs_select2 = []
1023 for element in all_refs:
1026 for element in all_refs:
1024 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1027 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1025 refs_select2.append({'text': element[1], 'children': children})
1028 refs_select2.append({'text': element[1], 'children': children})
1026
1029
1027 return {
1030 return {
1028 'user': {
1031 'user': {
1029 'user_id': repo.user.user_id,
1032 'user_id': repo.user.user_id,
1030 'username': repo.user.username,
1033 'username': repo.user.username,
1031 'firstname': repo.user.firstname,
1034 'firstname': repo.user.firstname,
1032 'lastname': repo.user.lastname,
1035 'lastname': repo.user.lastname,
1033 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1036 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1034 },
1037 },
1035 'description': h.chop_at_smart(repo.description, '\n'),
1038 'description': h.chop_at_smart(repo.description, '\n'),
1036 'refs': {
1039 'refs': {
1037 'all_refs': all_refs,
1040 'all_refs': all_refs,
1038 'selected_ref': selected_ref,
1041 'selected_ref': selected_ref,
1039 'select2_refs': refs_select2
1042 'select2_refs': refs_select2
1040 }
1043 }
1041 }
1044 }
1042
1045
1043 def generate_pullrequest_title(self, source, source_ref, target):
1046 def generate_pullrequest_title(self, source, source_ref, target):
1044 return u'{source}#{at_ref} to {target}'.format(
1047 return u'{source}#{at_ref} to {target}'.format(
1045 source=source,
1048 source=source,
1046 at_ref=source_ref,
1049 at_ref=source_ref,
1047 target=target,
1050 target=target,
1048 )
1051 )
1049
1052
1050 def _cleanup_merge_workspace(self, pull_request):
1053 def _cleanup_merge_workspace(self, pull_request):
1051 # Merging related cleanup
1054 # Merging related cleanup
1052 target_scm = pull_request.target_repo.scm_instance()
1055 target_scm = pull_request.target_repo.scm_instance()
1053 workspace_id = 'pr-%s' % pull_request.pull_request_id
1056 workspace_id = 'pr-%s' % pull_request.pull_request_id
1054
1057
1055 try:
1058 try:
1056 target_scm.cleanup_merge_workspace(workspace_id)
1059 target_scm.cleanup_merge_workspace(workspace_id)
1057 except NotImplementedError:
1060 except NotImplementedError:
1058 pass
1061 pass
1059
1062
1060 def _get_repo_pullrequest_sources(
1063 def _get_repo_pullrequest_sources(
1061 self, repo, commit_id=None, branch=None, bookmark=None):
1064 self, repo, commit_id=None, branch=None, bookmark=None):
1062 """
1065 """
1063 Return a structure with repo's interesting commits, suitable for
1066 Return a structure with repo's interesting commits, suitable for
1064 the selectors in pullrequest controller
1067 the selectors in pullrequest controller
1065
1068
1066 :param commit_id: a commit that must be in the list somehow
1069 :param commit_id: a commit that must be in the list somehow
1067 and selected by default
1070 and selected by default
1068 :param branch: a branch that must be in the list and selected
1071 :param branch: a branch that must be in the list and selected
1069 by default - even if closed
1072 by default - even if closed
1070 :param bookmark: a bookmark that must be in the list and selected
1073 :param bookmark: a bookmark that must be in the list and selected
1071 """
1074 """
1072
1075
1073 commit_id = safe_str(commit_id) if commit_id else None
1076 commit_id = safe_str(commit_id) if commit_id else None
1074 branch = safe_str(branch) if branch else None
1077 branch = safe_str(branch) if branch else None
1075 bookmark = safe_str(bookmark) if bookmark else None
1078 bookmark = safe_str(bookmark) if bookmark else None
1076
1079
1077 selected = None
1080 selected = None
1078
1081
1079 # order matters: first source that has commit_id in it will be selected
1082 # order matters: first source that has commit_id in it will be selected
1080 sources = []
1083 sources = []
1081 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1084 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1082 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1085 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1083
1086
1084 if commit_id:
1087 if commit_id:
1085 ref_commit = (h.short_id(commit_id), commit_id)
1088 ref_commit = (h.short_id(commit_id), commit_id)
1086 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1089 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1087
1090
1088 sources.append(
1091 sources.append(
1089 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1092 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1090 )
1093 )
1091
1094
1092 groups = []
1095 groups = []
1093 for group_key, ref_list, group_name, match in sources:
1096 for group_key, ref_list, group_name, match in sources:
1094 group_refs = []
1097 group_refs = []
1095 for ref_name, ref_id in ref_list:
1098 for ref_name, ref_id in ref_list:
1096 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1099 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1097 group_refs.append((ref_key, ref_name))
1100 group_refs.append((ref_key, ref_name))
1098
1101
1099 if not selected:
1102 if not selected:
1100 if set([commit_id, match]) & set([ref_id, ref_name]):
1103 if set([commit_id, match]) & set([ref_id, ref_name]):
1101 selected = ref_key
1104 selected = ref_key
1102
1105
1103 if group_refs:
1106 if group_refs:
1104 groups.append((group_refs, group_name))
1107 groups.append((group_refs, group_name))
1105
1108
1106 if not selected:
1109 if not selected:
1107 ref = commit_id or branch or bookmark
1110 ref = commit_id or branch or bookmark
1108 if ref:
1111 if ref:
1109 raise CommitDoesNotExistError(
1112 raise CommitDoesNotExistError(
1110 'No commit refs could be found matching: %s' % ref)
1113 'No commit refs could be found matching: %s' % ref)
1111 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1114 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1112 selected = 'branch:%s:%s' % (
1115 selected = 'branch:%s:%s' % (
1113 repo.DEFAULT_BRANCH_NAME,
1116 repo.DEFAULT_BRANCH_NAME,
1114 repo.branches[repo.DEFAULT_BRANCH_NAME]
1117 repo.branches[repo.DEFAULT_BRANCH_NAME]
1115 )
1118 )
1116 elif repo.commit_ids:
1119 elif repo.commit_ids:
1117 rev = repo.commit_ids[0]
1120 rev = repo.commit_ids[0]
1118 selected = 'rev:%s:%s' % (rev, rev)
1121 selected = 'rev:%s:%s' % (rev, rev)
1119 else:
1122 else:
1120 raise EmptyRepositoryError()
1123 raise EmptyRepositoryError()
1121 return groups, selected
1124 return groups, selected
1122
1125
1123 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1126 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1124 pull_request = self.__get_pull_request(pull_request)
1127 pull_request = self.__get_pull_request(pull_request)
1125 return self._get_diff_from_pr_or_version(pull_request, context=context)
1128 return self._get_diff_from_pr_or_version(pull_request, context=context)
1126
1129
1127 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1130 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1128 source_repo = pr_or_version.source_repo
1131 source_repo = pr_or_version.source_repo
1129
1132
1130 # we swap org/other ref since we run a simple diff on one repo
1133 # we swap org/other ref since we run a simple diff on one repo
1131 target_ref_id = pr_or_version.target_ref_parts.commit_id
1134 target_ref_id = pr_or_version.target_ref_parts.commit_id
1132 source_ref_id = pr_or_version.source_ref_parts.commit_id
1135 source_ref_id = pr_or_version.source_ref_parts.commit_id
1133 target_commit = source_repo.get_commit(
1136 target_commit = source_repo.get_commit(
1134 commit_id=safe_str(target_ref_id))
1137 commit_id=safe_str(target_ref_id))
1135 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1138 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1136 vcs_repo = source_repo.scm_instance()
1139 vcs_repo = source_repo.scm_instance()
1137
1140
1138 # TODO: johbo: In the context of an update, we cannot reach
1141 # TODO: johbo: In the context of an update, we cannot reach
1139 # the old commit anymore with our normal mechanisms. It needs
1142 # the old commit anymore with our normal mechanisms. It needs
1140 # some sort of special support in the vcs layer to avoid this
1143 # some sort of special support in the vcs layer to avoid this
1141 # workaround.
1144 # workaround.
1142 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1145 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1143 vcs_repo.alias == 'git'):
1146 vcs_repo.alias == 'git'):
1144 source_commit.raw_id = safe_str(source_ref_id)
1147 source_commit.raw_id = safe_str(source_ref_id)
1145
1148
1146 log.debug('calculating diff between '
1149 log.debug('calculating diff between '
1147 'source_ref:%s and target_ref:%s for repo `%s`',
1150 'source_ref:%s and target_ref:%s for repo `%s`',
1148 target_ref_id, source_ref_id,
1151 target_ref_id, source_ref_id,
1149 safe_unicode(vcs_repo.path))
1152 safe_unicode(vcs_repo.path))
1150
1153
1151 vcs_diff = vcs_repo.get_diff(
1154 vcs_diff = vcs_repo.get_diff(
1152 commit1=target_commit, commit2=source_commit, context=context)
1155 commit1=target_commit, commit2=source_commit, context=context)
1153 return vcs_diff
1156 return vcs_diff
1154
1157
1155 def _is_merge_enabled(self, pull_request):
1158 def _is_merge_enabled(self, pull_request):
1156 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1159 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1157 settings = settings_model.get_general_settings()
1160 settings = settings_model.get_general_settings()
1158 return settings.get('rhodecode_pr_merge_enabled', False)
1161 return settings.get('rhodecode_pr_merge_enabled', False)
1159
1162
1160 def _use_rebase_for_merging(self, pull_request):
1163 def _use_rebase_for_merging(self, pull_request):
1161 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1164 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1162 settings = settings_model.get_general_settings()
1165 settings = settings_model.get_general_settings()
1163 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1166 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1164
1167
1165 def _log_action(self, action, user, pull_request):
1168 def _log_action(self, action, user, pull_request):
1166 action_logger(
1169 action_logger(
1167 user,
1170 user,
1168 '{action}:{pr_id}'.format(
1171 '{action}:{pr_id}'.format(
1169 action=action, pr_id=pull_request.pull_request_id),
1172 action=action, pr_id=pull_request.pull_request_id),
1170 pull_request.target_repo)
1173 pull_request.target_repo)
1171
1174
1172
1175
1173 ChangeTuple = namedtuple('ChangeTuple',
1176 ChangeTuple = namedtuple('ChangeTuple',
1174 ['added', 'common', 'removed'])
1177 ['added', 'common', 'removed'])
1175
1178
1176 FileChangeTuple = namedtuple('FileChangeTuple',
1179 FileChangeTuple = namedtuple('FileChangeTuple',
1177 ['added', 'modified', 'removed'])
1180 ['added', 'modified', 'removed'])
@@ -1,602 +1,621 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
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 <script type="text/javascript">
31 <script type="text/javascript">
32 // TODO: marcink switch this to pyroutes
32 // TODO: marcink switch this to pyroutes
33 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
33 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
34 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
34 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 </script>
35 </script>
36 <div class="box">
36 <div class="box">
37 <div class="title">
37 <div class="title">
38 ${self.repo_page_title(c.rhodecode_db_repo)}
38 ${self.repo_page_title(c.rhodecode_db_repo)}
39 </div>
39 </div>
40
40
41 ${self.breadcrumbs()}
41 ${self.breadcrumbs()}
42
42
43
43
44 <div class="box pr-summary">
44 <div class="box pr-summary">
45 <div class="summary-details block-left">
45 <div class="summary-details block-left">
46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
47 <div class="pr-details-title">
47 <div class="pr-details-title">
48 ${_('Pull request #%s') % c.pull_request.pull_request_id} ${_('From')} ${h.format_date(c.pull_request.created_on)}
48 ${_('Pull request #%s') % c.pull_request.pull_request_id} ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 %if c.allowed_to_update:
49 %if c.allowed_to_update:
50 <span id="open_edit_pullrequest" class="block-right action_button">${_('Edit')}</span>
50 <span id="open_edit_pullrequest" class="block-right action_button">${_('Edit')}</span>
51 <span id="close_edit_pullrequest" class="block-right action_button" style="display: none;">${_('Close')}</span>
51 <span id="close_edit_pullrequest" class="block-right action_button" style="display: none;">${_('Close')}</span>
52 %endif
52 %endif
53 </div>
53 </div>
54
54
55 <div id="summary" class="fields pr-details-content">
55 <div id="summary" class="fields pr-details-content">
56 <div class="field">
56 <div class="field">
57 <div class="label-summary">
57 <div class="label-summary">
58 <label>${_('Origin')}:</label>
58 <label>${_('Origin')}:</label>
59 </div>
59 </div>
60 <div class="input">
60 <div class="input">
61 <div class="pr-origininfo">
61 <div class="pr-origininfo">
62 ## branch link is only valid if it is a branch
62 ## branch link is only valid if it is a branch
63 <span class="tag">
63 <span class="tag">
64 %if c.pull_request.source_ref_parts.type == 'branch':
64 %if c.pull_request.source_ref_parts.type == 'branch':
65 <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>
65 <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>
66 %else:
66 %else:
67 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
67 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
68 %endif
68 %endif
69 </span>
69 </span>
70 <span class="clone-url">
70 <span class="clone-url">
71 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
71 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
72 </span>
72 </span>
73 </div>
73 </div>
74 <div class="pr-pullinfo">
74 <div class="pr-pullinfo">
75 %if h.is_hg(c.pull_request.source_repo):
75 %if h.is_hg(c.pull_request.source_repo):
76 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
76 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
77 %elif h.is_git(c.pull_request.source_repo):
77 %elif h.is_git(c.pull_request.source_repo):
78 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
78 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
79 %endif
79 %endif
80 </div>
80 </div>
81 </div>
81 </div>
82 </div>
82 </div>
83 <div class="field">
83 <div class="field">
84 <div class="label-summary">
84 <div class="label-summary">
85 <label>${_('Target')}:</label>
85 <label>${_('Target')}:</label>
86 </div>
86 </div>
87 <div class="input">
87 <div class="input">
88 <div class="pr-targetinfo">
88 <div class="pr-targetinfo">
89 ## branch link is only valid if it is a branch
89 ## branch link is only valid if it is a branch
90 <span class="tag">
90 <span class="tag">
91 %if c.pull_request.target_ref_parts.type == 'branch':
91 %if c.pull_request.target_ref_parts.type == 'branch':
92 <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>
92 <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>
93 %else:
93 %else:
94 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
94 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
95 %endif
95 %endif
96 </span>
96 </span>
97 <span class="clone-url">
97 <span class="clone-url">
98 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
98 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
99 </span>
99 </span>
100 </div>
100 </div>
101 </div>
101 </div>
102 </div>
102 </div>
103
104 ## Clone link of the shadow repository.
105 %if not c.pull_request.is_closed():
106 <div class="field">
107 <div class="label-summary">
108 <label>${_('Shadow')}:</label>
109 </div>
110 <div class="input">
111 <div class="pr-shadowinfo">
112 %if h.is_hg(c.pull_request.target_repo):
113 <input type="text" value="hg clone ${c.shadow_clone_url}" readonly="readonly">
114 %elif h.is_git(c.pull_request.target_repo):
115 <input type="text" value="git clone ${c.shadow_clone_url}" readonly="readonly">
116 %endif
117 </div>
118 </div>
119 </div>
120 %endif
121
103 <div class="field">
122 <div class="field">
104 <div class="label-summary">
123 <div class="label-summary">
105 <label>${_('Review')}:</label>
124 <label>${_('Review')}:</label>
106 </div>
125 </div>
107 <div class="input">
126 <div class="input">
108 %if c.pull_request_review_status:
127 %if c.pull_request_review_status:
109 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
128 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
110 <span class="changeset-status-lbl tooltip">
129 <span class="changeset-status-lbl tooltip">
111 %if c.pull_request.is_closed():
130 %if c.pull_request.is_closed():
112 ${_('Closed')},
131 ${_('Closed')},
113 %endif
132 %endif
114 ${h.commit_status_lbl(c.pull_request_review_status)}
133 ${h.commit_status_lbl(c.pull_request_review_status)}
115 </span>
134 </span>
116 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
135 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
117 %endif
136 %endif
118 </div>
137 </div>
119 </div>
138 </div>
120 <div class="field">
139 <div class="field">
121 <div class="pr-description-label label-summary">
140 <div class="pr-description-label label-summary">
122 <label>${_('Description')}:</label>
141 <label>${_('Description')}:</label>
123 </div>
142 </div>
124 <div id="pr-desc" class="input">
143 <div id="pr-desc" class="input">
125 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
144 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
126 </div>
145 </div>
127 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
146 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
128 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
147 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
129 </div>
148 </div>
130 </div>
149 </div>
131 <div class="field">
150 <div class="field">
132 <div class="label-summary">
151 <div class="label-summary">
133 <label>${_('Comments')}:</label>
152 <label>${_('Comments')}:</label>
134 </div>
153 </div>
135 <div class="input">
154 <div class="input">
136 <div>
155 <div>
137 <div class="comments-number">
156 <div class="comments-number">
138 %if c.comments:
157 %if c.comments:
139 <a href="#comments">${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}</a>,
158 <a href="#comments">${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}</a>,
140 %else:
159 %else:
141 ${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}
160 ${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}
142 %endif
161 %endif
143 %if c.inline_cnt:
162 %if c.inline_cnt:
144 ## this is replaced with a proper link to first comment via JS linkifyComments() func
163 ## this is replaced with a proper link to first comment via JS linkifyComments() func
145 <a href="#inline-comments" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
164 <a href="#inline-comments" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
146 %else:
165 %else:
147 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
166 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
148 %endif
167 %endif
149
168
150 % if c.outdated_cnt:
169 % if c.outdated_cnt:
151 ,${ungettext("%d Outdated Comment", "%d Outdated Comments", c.outdated_cnt) % c.outdated_cnt} <span id="show-outdated-comments" class="btn btn-link">${_('(Show)')}</span>
170 ,${ungettext("%d Outdated Comment", "%d Outdated Comments", c.outdated_cnt) % c.outdated_cnt} <span id="show-outdated-comments" class="btn btn-link">${_('(Show)')}</span>
152 % endif
171 % endif
153 </div>
172 </div>
154 </div>
173 </div>
155 </div>
174 </div>
156 </div>
175 </div>
157 <div id="pr-save" class="field" style="display: none;">
176 <div id="pr-save" class="field" style="display: none;">
158 <div class="label-summary"></div>
177 <div class="label-summary"></div>
159 <div class="input">
178 <div class="input">
160 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
179 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
161 </div>
180 </div>
162 </div>
181 </div>
163 </div>
182 </div>
164 </div>
183 </div>
165 <div>
184 <div>
166 ## AUTHOR
185 ## AUTHOR
167 <div class="reviewers-title block-right">
186 <div class="reviewers-title block-right">
168 <div class="pr-details-title">
187 <div class="pr-details-title">
169 ${_('Author')}
188 ${_('Author')}
170 </div>
189 </div>
171 </div>
190 </div>
172 <div class="block-right pr-details-content reviewers">
191 <div class="block-right pr-details-content reviewers">
173 <ul class="group_members">
192 <ul class="group_members">
174 <li>
193 <li>
175 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
194 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
176 </li>
195 </li>
177 </ul>
196 </ul>
178 </div>
197 </div>
179 ## REVIEWERS
198 ## REVIEWERS
180 <div class="reviewers-title block-right">
199 <div class="reviewers-title block-right">
181 <div class="pr-details-title">
200 <div class="pr-details-title">
182 ${_('Pull request reviewers')}
201 ${_('Pull request reviewers')}
183 %if c.allowed_to_update:
202 %if c.allowed_to_update:
184 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
203 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
185 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
204 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
186 %endif
205 %endif
187 </div>
206 </div>
188 </div>
207 </div>
189 <div id="reviewers" class="block-right pr-details-content reviewers">
208 <div id="reviewers" class="block-right pr-details-content reviewers">
190 ## members goes here !
209 ## members goes here !
191 <input type="hidden" name="__start__" value="review_members:sequence">
210 <input type="hidden" name="__start__" value="review_members:sequence">
192 <ul id="review_members" class="group_members">
211 <ul id="review_members" class="group_members">
193 %for member,reasons,status in c.pull_request_reviewers:
212 %for member,reasons,status in c.pull_request_reviewers:
194 <li id="reviewer_${member.user_id}">
213 <li id="reviewer_${member.user_id}">
195 <div class="reviewers_member">
214 <div class="reviewers_member">
196 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
215 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
197 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
216 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
198 </div>
217 </div>
199 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
218 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
200 ${self.gravatar_with_user(member.email, 16)}
219 ${self.gravatar_with_user(member.email, 16)}
201 </div>
220 </div>
202 <input type="hidden" name="__start__" value="reviewer:mapping">
221 <input type="hidden" name="__start__" value="reviewer:mapping">
203 <input type="hidden" name="__start__" value="reasons:sequence">
222 <input type="hidden" name="__start__" value="reasons:sequence">
204 %for reason in reasons:
223 %for reason in reasons:
205 <div class="reviewer_reason">- ${reason}</div>
224 <div class="reviewer_reason">- ${reason}</div>
206 <input type="hidden" name="reason" value="${reason}">
225 <input type="hidden" name="reason" value="${reason}">
207
226
208 %endfor
227 %endfor
209 <input type="hidden" name="__end__" value="reasons:sequence">
228 <input type="hidden" name="__end__" value="reasons:sequence">
210 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
229 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
211 <input type="hidden" name="__end__" value="reviewer:mapping">
230 <input type="hidden" name="__end__" value="reviewer:mapping">
212 %if c.allowed_to_update:
231 %if c.allowed_to_update:
213 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
232 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
214 <i class="icon-remove-sign" ></i>
233 <i class="icon-remove-sign" ></i>
215 </div>
234 </div>
216 %endif
235 %endif
217 </div>
236 </div>
218 </li>
237 </li>
219 %endfor
238 %endfor
220 </ul>
239 </ul>
221 <input type="hidden" name="__end__" value="review_members:sequence">
240 <input type="hidden" name="__end__" value="review_members:sequence">
222 %if not c.pull_request.is_closed():
241 %if not c.pull_request.is_closed():
223 <div id="add_reviewer_input" class='ac' style="display: none;">
242 <div id="add_reviewer_input" class='ac' style="display: none;">
224 %if c.allowed_to_update:
243 %if c.allowed_to_update:
225 <div class="reviewer_ac">
244 <div class="reviewer_ac">
226 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
245 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
227 <div id="reviewers_container"></div>
246 <div id="reviewers_container"></div>
228 </div>
247 </div>
229 <div>
248 <div>
230 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
249 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
231 </div>
250 </div>
232 %endif
251 %endif
233 </div>
252 </div>
234 %endif
253 %endif
235 </div>
254 </div>
236 </div>
255 </div>
237 </div>
256 </div>
238 <div class="box">
257 <div class="box">
239 ##DIFF
258 ##DIFF
240 <div class="table" >
259 <div class="table" >
241 <div id="changeset_compare_view_content">
260 <div id="changeset_compare_view_content">
242 ##CS
261 ##CS
243 % if c.missing_requirements:
262 % if c.missing_requirements:
244 <div class="box">
263 <div class="box">
245 <div class="alert alert-warning">
264 <div class="alert alert-warning">
246 <div>
265 <div>
247 <strong>${_('Missing requirements:')}</strong>
266 <strong>${_('Missing requirements:')}</strong>
248 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
267 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
249 </div>
268 </div>
250 </div>
269 </div>
251 </div>
270 </div>
252 % elif c.missing_commits:
271 % elif c.missing_commits:
253 <div class="box">
272 <div class="box">
254 <div class="alert alert-warning">
273 <div class="alert alert-warning">
255 <div>
274 <div>
256 <strong>${_('Missing commits')}:</strong>
275 <strong>${_('Missing commits')}:</strong>
257 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
276 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
258 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
277 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
259 </div>
278 </div>
260 </div>
279 </div>
261 </div>
280 </div>
262 % endif
281 % endif
263 <div class="compare_view_commits_title">
282 <div class="compare_view_commits_title">
264 % if c.allowed_to_update and not c.pull_request.is_closed():
283 % if c.allowed_to_update and not c.pull_request.is_closed():
265 <button id="update_commits" class="btn btn-small">${_('Update commits')}</button>
284 <button id="update_commits" class="btn btn-small">${_('Update commits')}</button>
266 % endif
285 % endif
267 % if len(c.commit_ranges):
286 % if len(c.commit_ranges):
268 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
287 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
269 % endif
288 % endif
270 </div>
289 </div>
271 % if not c.missing_commits:
290 % if not c.missing_commits:
272 <%include file="/compare/compare_commits.html" />
291 <%include file="/compare/compare_commits.html" />
273 ## FILES
292 ## FILES
274 <div class="cs_files_title">
293 <div class="cs_files_title">
275 <span class="cs_files_expand">
294 <span class="cs_files_expand">
276 <span id="expand_all_files">${_('Expand All')}</span> | <span id="collapse_all_files">${_('Collapse All')}</span>
295 <span id="expand_all_files">${_('Expand All')}</span> | <span id="collapse_all_files">${_('Collapse All')}</span>
277 </span>
296 </span>
278 <h2>
297 <h2>
279 ${diff_block.diff_summary_text(len(c.files), c.lines_added, c.lines_deleted, c.limited_diff)}
298 ${diff_block.diff_summary_text(len(c.files), c.lines_added, c.lines_deleted, c.limited_diff)}
280 </h2>
299 </h2>
281 </div>
300 </div>
282 % endif
301 % endif
283 <div class="cs_files">
302 <div class="cs_files">
284 %if not c.files and not c.missing_commits:
303 %if not c.files and not c.missing_commits:
285 <span class="empty_data">${_('No files')}</span>
304 <span class="empty_data">${_('No files')}</span>
286 %endif
305 %endif
287 <table class="compare_view_files">
306 <table class="compare_view_files">
288 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
307 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
289 %for FID, change, path, stats in c.files:
308 %for FID, change, path, stats in c.files:
290 <tr class="cs_${change} collapse_file" fid="${FID}">
309 <tr class="cs_${change} collapse_file" fid="${FID}">
291 <td class="cs_icon_td">
310 <td class="cs_icon_td">
292 <span class="collapse_file_icon" fid="${FID}"></span>
311 <span class="collapse_file_icon" fid="${FID}"></span>
293 </td>
312 </td>
294 <td class="cs_icon_td">
313 <td class="cs_icon_td">
295 <div class="flag_status not_reviewed hidden"></div>
314 <div class="flag_status not_reviewed hidden"></div>
296 </td>
315 </td>
297 <td class="cs_${change}" id="a_${FID}">
316 <td class="cs_${change}" id="a_${FID}">
298 <div class="node">
317 <div class="node">
299 <a href="#a_${FID}">
318 <a href="#a_${FID}">
300 <i class="icon-file-${change.lower()}"></i>
319 <i class="icon-file-${change.lower()}"></i>
301 ${h.safe_unicode(path)}
320 ${h.safe_unicode(path)}
302 </a>
321 </a>
303 </div>
322 </div>
304 </td>
323 </td>
305 <td>
324 <td>
306 <div class="changes pull-right">${h.fancy_file_stats(stats)}</div>
325 <div class="changes pull-right">${h.fancy_file_stats(stats)}</div>
307 <div class="comment-bubble pull-right" data-path="${path}">
326 <div class="comment-bubble pull-right" data-path="${path}">
308 <i class="icon-comment"></i>
327 <i class="icon-comment"></i>
309 </div>
328 </div>
310 </td>
329 </td>
311 </tr>
330 </tr>
312 <tr fid="${FID}" id="diff_${FID}" class="diff_links">
331 <tr fid="${FID}" id="diff_${FID}" class="diff_links">
313 <td></td>
332 <td></td>
314 <td></td>
333 <td></td>
315 <td class="cs_${change}">
334 <td class="cs_${change}">
316 %if c.target_repo.repo_name == c.repo_name:
335 %if c.target_repo.repo_name == c.repo_name:
317 ${diff_block.diff_menu(c.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
336 ${diff_block.diff_menu(c.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
318 %else:
337 %else:
319 ## this is slightly different case later, since the other repo can have this
338 ## this is slightly different case later, since the other repo can have this
320 ## file in other state than the origin repo
339 ## file in other state than the origin repo
321 ${diff_block.diff_menu(c.target_repo.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
340 ${diff_block.diff_menu(c.target_repo.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
322 %endif
341 %endif
323 </td>
342 </td>
324 <td class="td-actions rc-form">
343 <td class="td-actions rc-form">
325 <div data-comment-id="${FID}" class="btn-link show-inline-comments comments-visible">
344 <div data-comment-id="${FID}" class="btn-link show-inline-comments comments-visible">
326 <span class="comments-show">${_('Show comments')}</span>
345 <span class="comments-show">${_('Show comments')}</span>
327 <span class="comments-hide">${_('Hide comments')}</span>
346 <span class="comments-hide">${_('Hide comments')}</span>
328 </div>
347 </div>
329 </td>
348 </td>
330 </tr>
349 </tr>
331 <tr id="tr_${FID}">
350 <tr id="tr_${FID}">
332 <td></td>
351 <td></td>
333 <td></td>
352 <td></td>
334 <td class="injected_diff" colspan="2">
353 <td class="injected_diff" colspan="2">
335 ${diff_block.diff_block_simple([c.changes[FID]])}
354 ${diff_block.diff_block_simple([c.changes[FID]])}
336 </td>
355 </td>
337 </tr>
356 </tr>
338
357
339 ## Loop through inline comments
358 ## Loop through inline comments
340 % if c.outdated_comments.get(path,False):
359 % if c.outdated_comments.get(path,False):
341 <tr class="outdated">
360 <tr class="outdated">
342 <td></td>
361 <td></td>
343 <td></td>
362 <td></td>
344 <td colspan="2">
363 <td colspan="2">
345 <p>${_('Outdated Inline Comments')}:</p>
364 <p>${_('Outdated Inline Comments')}:</p>
346 </td>
365 </td>
347 </tr>
366 </tr>
348 <tr class="outdated">
367 <tr class="outdated">
349 <td></td>
368 <td></td>
350 <td></td>
369 <td></td>
351 <td colspan="2" class="outdated_comment_block">
370 <td colspan="2" class="outdated_comment_block">
352 % for line, comments in c.outdated_comments[path].iteritems():
371 % for line, comments in c.outdated_comments[path].iteritems():
353 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
372 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
354 % for co in comments:
373 % for co in comments:
355 ${comment.comment_block_outdated(co)}
374 ${comment.comment_block_outdated(co)}
356 % endfor
375 % endfor
357 </div>
376 </div>
358 % endfor
377 % endfor
359 </td>
378 </td>
360 </tr>
379 </tr>
361 % endif
380 % endif
362 %endfor
381 %endfor
363 ## Loop through inline comments for deleted files
382 ## Loop through inline comments for deleted files
364 %for path in c.deleted_files:
383 %for path in c.deleted_files:
365 <tr class="outdated deleted">
384 <tr class="outdated deleted">
366 <td></td>
385 <td></td>
367 <td></td>
386 <td></td>
368 <td>${path}</td>
387 <td>${path}</td>
369 </tr>
388 </tr>
370 <tr class="outdated deleted">
389 <tr class="outdated deleted">
371 <td></td>
390 <td></td>
372 <td></td>
391 <td></td>
373 <td>(${_('Removed')})</td>
392 <td>(${_('Removed')})</td>
374 </tr>
393 </tr>
375 % if path in c.outdated_comments:
394 % if path in c.outdated_comments:
376 <tr class="outdated deleted">
395 <tr class="outdated deleted">
377 <td></td>
396 <td></td>
378 <td></td>
397 <td></td>
379 <td colspan="2">
398 <td colspan="2">
380 <p>${_('Outdated Inline Comments')}:</p>
399 <p>${_('Outdated Inline Comments')}:</p>
381 </td>
400 </td>
382 </tr>
401 </tr>
383 <tr class="outdated">
402 <tr class="outdated">
384 <td></td>
403 <td></td>
385 <td></td>
404 <td></td>
386 <td colspan="2" class="outdated_comment_block">
405 <td colspan="2" class="outdated_comment_block">
387 % for line, comments in c.outdated_comments[path].iteritems():
406 % for line, comments in c.outdated_comments[path].iteritems():
388 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
407 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
389 % for co in comments:
408 % for co in comments:
390 ${comment.comment_block_outdated(co)}
409 ${comment.comment_block_outdated(co)}
391 % endfor
410 % endfor
392 </div>
411 </div>
393 % endfor
412 % endfor
394 </td>
413 </td>
395 </tr>
414 </tr>
396 % endif
415 % endif
397 %endfor
416 %endfor
398 </table>
417 </table>
399 </div>
418 </div>
400 % if c.limited_diff:
419 % if c.limited_diff:
401 <h5>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
420 <h5>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
402 % endif
421 % endif
403 </div>
422 </div>
404 </div>
423 </div>
405
424
406 % if c.limited_diff:
425 % if c.limited_diff:
407 <p>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></p>
426 <p>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></p>
408 % endif
427 % endif
409
428
410 ## template for inline comment form
429 ## template for inline comment form
411 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
430 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
412 ${comment.comment_inline_form()}
431 ${comment.comment_inline_form()}
413
432
414 ## render comments and inlines
433 ## render comments and inlines
415 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
434 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
416
435
417 % if not c.pull_request.is_closed():
436 % if not c.pull_request.is_closed():
418 ## main comment form and it status
437 ## main comment form and it status
419 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
438 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
420 pull_request_id=c.pull_request.pull_request_id),
439 pull_request_id=c.pull_request.pull_request_id),
421 c.pull_request_review_status,
440 c.pull_request_review_status,
422 is_pull_request=True, change_status=c.allowed_to_change_status)}
441 is_pull_request=True, change_status=c.allowed_to_change_status)}
423 %endif
442 %endif
424
443
425 <script type="text/javascript">
444 <script type="text/javascript">
426 if (location.hash) {
445 if (location.hash) {
427 var result = splitDelimitedHash(location.hash);
446 var result = splitDelimitedHash(location.hash);
428 var line = $('html').find(result.loc);
447 var line = $('html').find(result.loc);
429 if (line.length > 0){
448 if (line.length > 0){
430 offsetScroll(line, 70);
449 offsetScroll(line, 70);
431 }
450 }
432 }
451 }
433 $(function(){
452 $(function(){
434 ReviewerAutoComplete('user');
453 ReviewerAutoComplete('user');
435 // custom code mirror
454 // custom code mirror
436 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
455 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
437
456
438 var PRDetails = {
457 var PRDetails = {
439 editButton: $('#open_edit_pullrequest'),
458 editButton: $('#open_edit_pullrequest'),
440 closeButton: $('#close_edit_pullrequest'),
459 closeButton: $('#close_edit_pullrequest'),
441 viewFields: $('#pr-desc, #pr-title'),
460 viewFields: $('#pr-desc, #pr-title'),
442 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
461 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
443
462
444 init: function() {
463 init: function() {
445 var that = this;
464 var that = this;
446 this.editButton.on('click', function(e) { that.edit(); });
465 this.editButton.on('click', function(e) { that.edit(); });
447 this.closeButton.on('click', function(e) { that.view(); });
466 this.closeButton.on('click', function(e) { that.view(); });
448 },
467 },
449
468
450 edit: function(event) {
469 edit: function(event) {
451 this.viewFields.hide();
470 this.viewFields.hide();
452 this.editButton.hide();
471 this.editButton.hide();
453 this.editFields.show();
472 this.editFields.show();
454 codeMirrorInstance.refresh();
473 codeMirrorInstance.refresh();
455 },
474 },
456
475
457 view: function(event) {
476 view: function(event) {
458 this.editFields.hide();
477 this.editFields.hide();
459 this.closeButton.hide();
478 this.closeButton.hide();
460 this.viewFields.show();
479 this.viewFields.show();
461 }
480 }
462 };
481 };
463
482
464 var ReviewersPanel = {
483 var ReviewersPanel = {
465 editButton: $('#open_edit_reviewers'),
484 editButton: $('#open_edit_reviewers'),
466 closeButton: $('#close_edit_reviewers'),
485 closeButton: $('#close_edit_reviewers'),
467 addButton: $('#add_reviewer_input'),
486 addButton: $('#add_reviewer_input'),
468 removeButtons: $('.reviewer_member_remove'),
487 removeButtons: $('.reviewer_member_remove'),
469
488
470 init: function() {
489 init: function() {
471 var that = this;
490 var that = this;
472 this.editButton.on('click', function(e) { that.edit(); });
491 this.editButton.on('click', function(e) { that.edit(); });
473 this.closeButton.on('click', function(e) { that.close(); });
492 this.closeButton.on('click', function(e) { that.close(); });
474 },
493 },
475
494
476 edit: function(event) {
495 edit: function(event) {
477 this.editButton.hide();
496 this.editButton.hide();
478 this.closeButton.show();
497 this.closeButton.show();
479 this.addButton.show();
498 this.addButton.show();
480 this.removeButtons.css('visibility', 'visible');
499 this.removeButtons.css('visibility', 'visible');
481 },
500 },
482
501
483 close: function(event) {
502 close: function(event) {
484 this.editButton.show();
503 this.editButton.show();
485 this.closeButton.hide();
504 this.closeButton.hide();
486 this.addButton.hide();
505 this.addButton.hide();
487 this.removeButtons.css('visibility', 'hidden');
506 this.removeButtons.css('visibility', 'hidden');
488 }
507 }
489 };
508 };
490
509
491 PRDetails.init();
510 PRDetails.init();
492 ReviewersPanel.init();
511 ReviewersPanel.init();
493
512
494 $('#show-outdated-comments').on('click', function(e){
513 $('#show-outdated-comments').on('click', function(e){
495 var button = $(this);
514 var button = $(this);
496 var outdated = $('.outdated');
515 var outdated = $('.outdated');
497 if (button.html() === "(Show)") {
516 if (button.html() === "(Show)") {
498 button.html("(Hide)");
517 button.html("(Hide)");
499 outdated.show();
518 outdated.show();
500 } else {
519 } else {
501 button.html("(Show)");
520 button.html("(Show)");
502 outdated.hide();
521 outdated.hide();
503 }
522 }
504 });
523 });
505
524
506 $('.show-inline-comments').on('change', function(e){
525 $('.show-inline-comments').on('change', function(e){
507 var show = 'none';
526 var show = 'none';
508 var target = e.currentTarget;
527 var target = e.currentTarget;
509 if(target.checked){
528 if(target.checked){
510 show = ''
529 show = ''
511 }
530 }
512 var boxid = $(target).attr('id_for');
531 var boxid = $(target).attr('id_for');
513 var comments = $('#{0} .inline-comments'.format(boxid));
532 var comments = $('#{0} .inline-comments'.format(boxid));
514 var fn_display = function(idx){
533 var fn_display = function(idx){
515 $(this).css('display', show);
534 $(this).css('display', show);
516 };
535 };
517 $(comments).each(fn_display);
536 $(comments).each(fn_display);
518 var btns = $('#{0} .inline-comments-button'.format(boxid));
537 var btns = $('#{0} .inline-comments-button'.format(boxid));
519 $(btns).each(fn_display);
538 $(btns).each(fn_display);
520 });
539 });
521
540
522 // inject comments into their proper positions
541 // inject comments into their proper positions
523 var file_comments = $('.inline-comment-placeholder');
542 var file_comments = $('.inline-comment-placeholder');
524 %if c.pull_request.is_closed():
543 %if c.pull_request.is_closed():
525 renderInlineComments(file_comments, false);
544 renderInlineComments(file_comments, false);
526 %else:
545 %else:
527 renderInlineComments(file_comments, true);
546 renderInlineComments(file_comments, true);
528 %endif
547 %endif
529 var commentTotals = {};
548 var commentTotals = {};
530 $.each(file_comments, function(i, comment) {
549 $.each(file_comments, function(i, comment) {
531 var path = $(comment).attr('path');
550 var path = $(comment).attr('path');
532 var comms = $(comment).children().length;
551 var comms = $(comment).children().length;
533 if (path in commentTotals) {
552 if (path in commentTotals) {
534 commentTotals[path] += comms;
553 commentTotals[path] += comms;
535 } else {
554 } else {
536 commentTotals[path] = comms;
555 commentTotals[path] = comms;
537 }
556 }
538 });
557 });
539 $.each(commentTotals, function(path, total) {
558 $.each(commentTotals, function(path, total) {
540 var elem = $('.comment-bubble[data-path="'+ path +'"]');
559 var elem = $('.comment-bubble[data-path="'+ path +'"]');
541 elem.css('visibility', 'visible');
560 elem.css('visibility', 'visible');
542 elem.html(elem.html() + ' ' + total );
561 elem.html(elem.html() + ' ' + total );
543 });
562 });
544
563
545 $('#merge_pull_request_form').submit(function() {
564 $('#merge_pull_request_form').submit(function() {
546 if (!$('#merge_pull_request').attr('disabled')) {
565 if (!$('#merge_pull_request').attr('disabled')) {
547 $('#merge_pull_request').attr('disabled', 'disabled');
566 $('#merge_pull_request').attr('disabled', 'disabled');
548 }
567 }
549 return true;
568 return true;
550 });
569 });
551
570
552 $('#edit_pull_request').on('click', function(e){
571 $('#edit_pull_request').on('click', function(e){
553 var title = $('#pr-title-input').val();
572 var title = $('#pr-title-input').val();
554 var description = codeMirrorInstance.getValue();
573 var description = codeMirrorInstance.getValue();
555 editPullRequest(
574 editPullRequest(
556 "${c.repo_name}", "${c.pull_request.pull_request_id}",
575 "${c.repo_name}", "${c.pull_request.pull_request_id}",
557 title, description);
576 title, description);
558 });
577 });
559
578
560 $('#update_pull_request').on('click', function(e){
579 $('#update_pull_request').on('click', function(e){
561 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
580 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
562 });
581 });
563
582
564 $('#update_commits').on('click', function(e){
583 $('#update_commits').on('click', function(e){
565 var isDisabled = !$(e.currentTarget).attr('disabled');
584 var isDisabled = !$(e.currentTarget).attr('disabled');
566 $(e.currentTarget).text(_gettext('Updating...'));
585 $(e.currentTarget).text(_gettext('Updating...'));
567 $(e.currentTarget).attr('disabled', 'disabled');
586 $(e.currentTarget).attr('disabled', 'disabled');
568 if(isDisabled){
587 if(isDisabled){
569 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
588 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
570 }
589 }
571
590
572 });
591 });
573 // fixing issue with caches on firefox
592 // fixing issue with caches on firefox
574 $('#update_commits').removeAttr("disabled");
593 $('#update_commits').removeAttr("disabled");
575
594
576 $('#close_pull_request').on('click', function(e){
595 $('#close_pull_request').on('click', function(e){
577 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
596 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
578 });
597 });
579
598
580 $('.show-inline-comments').on('click', function(e){
599 $('.show-inline-comments').on('click', function(e){
581 var boxid = $(this).attr('data-comment-id');
600 var boxid = $(this).attr('data-comment-id');
582 var button = $(this);
601 var button = $(this);
583
602
584 if(button.hasClass("comments-visible")) {
603 if(button.hasClass("comments-visible")) {
585 $('#{0} .inline-comments'.format(boxid)).each(function(index){
604 $('#{0} .inline-comments'.format(boxid)).each(function(index){
586 $(this).hide();
605 $(this).hide();
587 })
606 })
588 button.removeClass("comments-visible");
607 button.removeClass("comments-visible");
589 } else {
608 } else {
590 $('#{0} .inline-comments'.format(boxid)).each(function(index){
609 $('#{0} .inline-comments'.format(boxid)).each(function(index){
591 $(this).show();
610 $(this).show();
592 })
611 })
593 button.addClass("comments-visible");
612 button.addClass("comments-visible");
594 }
613 }
595 });
614 });
596 })
615 })
597 </script>
616 </script>
598
617
599 </div>
618 </div>
600 </div>
619 </div>
601
620
602 </%def>
621 </%def>
General Comments 0
You need to be logged in to leave comments. Login now