##// END OF EJS Templates
review-rules: extend code to support the forbid commit authors.
marcink -
r1787:bb077306 default
parent child Browse files
Show More

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

@@ -0,0 +1,33 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 repo_review_rule_table = db.RepoReviewRule.__table__
19
20 forbid_commit_author_to_review = Column(
21 "forbid_commit_author_to_review", Boolean(), nullable=True, default=False)
22 forbid_commit_author_to_review.create(table=repo_review_rule_table)
23
24 fixups(db, meta.Session)
25
26
27 def downgrade(migrate_engine):
28 meta = MetaData()
29 meta.bind = migrate_engine
30
31
32 def fixups(models, _SESSION):
33 pass
@@ -1,63 +1,63 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22
22
23 RhodeCode, a web based repository management software
23 RhodeCode, a web based repository management software
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 """
25 """
26
26
27 import os
27 import os
28 import sys
28 import sys
29 import platform
29 import platform
30
30
31 VERSION = tuple(open(os.path.join(
31 VERSION = tuple(open(os.path.join(
32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33
33
34 BACKENDS = {
34 BACKENDS = {
35 'hg': 'Mercurial repository',
35 'hg': 'Mercurial repository',
36 'git': 'Git repository',
36 'git': 'Git repository',
37 'svn': 'Subversion repository',
37 'svn': 'Subversion repository',
38 }
38 }
39
39
40 CELERY_ENABLED = False
40 CELERY_ENABLED = False
41 CELERY_EAGER = False
41 CELERY_EAGER = False
42
42
43 # link to config for pylons
43 # link to config for pylons
44 CONFIG = {}
44 CONFIG = {}
45
45
46 # Populated with the settings dictionary from application init in
46 # Populated with the settings dictionary from application init in
47 # rhodecode.conf.environment.load_pyramid_environment
47 # rhodecode.conf.environment.load_pyramid_environment
48 PYRAMID_SETTINGS = {}
48 PYRAMID_SETTINGS = {}
49
49
50 # Linked module for extensions
50 # Linked module for extensions
51 EXTENSIONS = {}
51 EXTENSIONS = {}
52
52
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 77 # defines current db version for migrations
54 __dbversion__ = 78 # defines current db version for migrations
55 __platform__ = platform.system()
55 __platform__ = platform.system()
56 __license__ = 'AGPLv3, and Commercial License'
56 __license__ = 'AGPLv3, and Commercial License'
57 __author__ = 'RhodeCode GmbH'
57 __author__ = 'RhodeCode GmbH'
58 __url__ = 'https://code.rhodecode.com'
58 __url__ = 'https://code.rhodecode.com'
59
59
60 is_windows = __platform__ in ['Windows']
60 is_windows = __platform__ in ['Windows']
61 is_unix = not is_windows
61 is_unix = not is_windows
62 is_test = False
62 is_test = False
63 disable_error_handler = False
63 disable_error_handler = False
@@ -1,1018 +1,1023 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69
69
70 def __before__(self):
70 def __before__(self):
71 super(PullrequestsController, self).__before__()
71 super(PullrequestsController, self).__before__()
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
74
74
75 @LoginRequired()
75 @LoginRequired()
76 @NotAnonymous()
76 @NotAnonymous()
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
78 'repository.admin')
78 'repository.admin')
79 @HasAcceptedRepoType('git', 'hg')
79 @HasAcceptedRepoType('git', 'hg')
80 def index(self):
80 def index(self):
81 source_repo = c.rhodecode_db_repo
81 source_repo = c.rhodecode_db_repo
82
82
83 try:
83 try:
84 source_repo.scm_instance().get_commit()
84 source_repo.scm_instance().get_commit()
85 except EmptyRepositoryError:
85 except EmptyRepositoryError:
86 h.flash(h.literal(_('There are no commits yet')),
86 h.flash(h.literal(_('There are no commits yet')),
87 category='warning')
87 category='warning')
88 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
88 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
89
89
90 commit_id = request.GET.get('commit')
90 commit_id = request.GET.get('commit')
91 branch_ref = request.GET.get('branch')
91 branch_ref = request.GET.get('branch')
92 bookmark_ref = request.GET.get('bookmark')
92 bookmark_ref = request.GET.get('bookmark')
93
93
94 try:
94 try:
95 source_repo_data = PullRequestModel().generate_repo_data(
95 source_repo_data = PullRequestModel().generate_repo_data(
96 source_repo, commit_id=commit_id,
96 source_repo, commit_id=commit_id,
97 branch=branch_ref, bookmark=bookmark_ref)
97 branch=branch_ref, bookmark=bookmark_ref)
98 except CommitDoesNotExistError as e:
98 except CommitDoesNotExistError as e:
99 log.exception(e)
99 log.exception(e)
100 h.flash(_('Commit does not exist'), 'error')
100 h.flash(_('Commit does not exist'), 'error')
101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
102
102
103 default_target_repo = source_repo
103 default_target_repo = source_repo
104
104
105 if source_repo.parent:
105 if source_repo.parent:
106 parent_vcs_obj = source_repo.parent.scm_instance()
106 parent_vcs_obj = source_repo.parent.scm_instance()
107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
108 # change default if we have a parent repo
108 # change default if we have a parent repo
109 default_target_repo = source_repo.parent
109 default_target_repo = source_repo.parent
110
110
111 target_repo_data = PullRequestModel().generate_repo_data(
111 target_repo_data = PullRequestModel().generate_repo_data(
112 default_target_repo)
112 default_target_repo)
113
113
114 selected_source_ref = source_repo_data['refs']['selected_ref']
114 selected_source_ref = source_repo_data['refs']['selected_ref']
115
115
116 title_source_ref = selected_source_ref.split(':', 2)[1]
116 title_source_ref = selected_source_ref.split(':', 2)[1]
117 c.default_title = PullRequestModel().generate_pullrequest_title(
117 c.default_title = PullRequestModel().generate_pullrequest_title(
118 source=source_repo.repo_name,
118 source=source_repo.repo_name,
119 source_ref=title_source_ref,
119 source_ref=title_source_ref,
120 target=default_target_repo.repo_name
120 target=default_target_repo.repo_name
121 )
121 )
122
122
123 c.default_repo_data = {
123 c.default_repo_data = {
124 'source_repo_name': source_repo.repo_name,
124 'source_repo_name': source_repo.repo_name,
125 'source_refs_json': json.dumps(source_repo_data),
125 'source_refs_json': json.dumps(source_repo_data),
126 'target_repo_name': default_target_repo.repo_name,
126 'target_repo_name': default_target_repo.repo_name,
127 'target_refs_json': json.dumps(target_repo_data),
127 'target_refs_json': json.dumps(target_repo_data),
128 }
128 }
129 c.default_source_ref = selected_source_ref
129 c.default_source_ref = selected_source_ref
130
130
131 return render('/pullrequests/pullrequest.mako')
131 return render('/pullrequests/pullrequest.mako')
132
132
133 @LoginRequired()
133 @LoginRequired()
134 @NotAnonymous()
134 @NotAnonymous()
135 @XHRRequired()
135 @XHRRequired()
136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
137 'repository.admin')
137 'repository.admin')
138 @jsonify
138 @jsonify
139 def get_repo_refs(self, repo_name, target_repo_name):
139 def get_repo_refs(self, repo_name, target_repo_name):
140 repo = Repository.get_by_repo_name(target_repo_name)
140 repo = Repository.get_by_repo_name(target_repo_name)
141 if not repo:
141 if not repo:
142 raise HTTPNotFound
142 raise HTTPNotFound
143 return PullRequestModel().generate_repo_data(repo)
143 return PullRequestModel().generate_repo_data(repo)
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @NotAnonymous()
146 @NotAnonymous()
147 @XHRRequired()
147 @XHRRequired()
148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
149 'repository.admin')
149 'repository.admin')
150 @jsonify
150 @jsonify
151 def get_repo_destinations(self, repo_name):
151 def get_repo_destinations(self, repo_name):
152 repo = Repository.get_by_repo_name(repo_name)
152 repo = Repository.get_by_repo_name(repo_name)
153 if not repo:
153 if not repo:
154 raise HTTPNotFound
154 raise HTTPNotFound
155 filter_query = request.GET.get('query')
155 filter_query = request.GET.get('query')
156
156
157 query = Repository.query() \
157 query = Repository.query() \
158 .order_by(func.length(Repository.repo_name)) \
158 .order_by(func.length(Repository.repo_name)) \
159 .filter(or_(
159 .filter(or_(
160 Repository.repo_name == repo.repo_name,
160 Repository.repo_name == repo.repo_name,
161 Repository.fork_id == repo.repo_id))
161 Repository.fork_id == repo.repo_id))
162
162
163 if filter_query:
163 if filter_query:
164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
165 query = query.filter(
165 query = query.filter(
166 Repository.repo_name.ilike(ilike_expression))
166 Repository.repo_name.ilike(ilike_expression))
167
167
168 add_parent = False
168 add_parent = False
169 if repo.parent:
169 if repo.parent:
170 if filter_query in repo.parent.repo_name:
170 if filter_query in repo.parent.repo_name:
171 parent_vcs_obj = repo.parent.scm_instance()
171 parent_vcs_obj = repo.parent.scm_instance()
172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
173 add_parent = True
173 add_parent = True
174
174
175 limit = 20 - 1 if add_parent else 20
175 limit = 20 - 1 if add_parent else 20
176 all_repos = query.limit(limit).all()
176 all_repos = query.limit(limit).all()
177 if add_parent:
177 if add_parent:
178 all_repos += [repo.parent]
178 all_repos += [repo.parent]
179
179
180 repos = []
180 repos = []
181 for obj in self.scm_model.get_repos(all_repos):
181 for obj in self.scm_model.get_repos(all_repos):
182 repos.append({
182 repos.append({
183 'id': obj['name'],
183 'id': obj['name'],
184 'text': obj['name'],
184 'text': obj['name'],
185 'type': 'repo',
185 'type': 'repo',
186 'obj': obj['dbrepo']
186 'obj': obj['dbrepo']
187 })
187 })
188
188
189 data = {
189 data = {
190 'more': False,
190 'more': False,
191 'results': [{
191 'results': [{
192 'text': _('Repositories'),
192 'text': _('Repositories'),
193 'children': repos
193 'children': repos
194 }] if repos else []
194 }] if repos else []
195 }
195 }
196 return data
196 return data
197
197
198 @LoginRequired()
198 @LoginRequired()
199 @NotAnonymous()
199 @NotAnonymous()
200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
201 'repository.admin')
201 'repository.admin')
202 @HasAcceptedRepoType('git', 'hg')
202 @HasAcceptedRepoType('git', 'hg')
203 @auth.CSRFRequired()
203 @auth.CSRFRequired()
204 def create(self, repo_name):
204 def create(self, repo_name):
205 repo = Repository.get_by_repo_name(repo_name)
205 repo = Repository.get_by_repo_name(repo_name)
206 if not repo:
206 if not repo:
207 raise HTTPNotFound
207 raise HTTPNotFound
208
208
209 controls = peppercorn.parse(request.POST.items())
209 controls = peppercorn.parse(request.POST.items())
210
210
211 try:
211 try:
212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
213 except formencode.Invalid as errors:
213 except formencode.Invalid as errors:
214 if errors.error_dict.get('revisions'):
214 if errors.error_dict.get('revisions'):
215 msg = 'Revisions: %s' % errors.error_dict['revisions']
215 msg = 'Revisions: %s' % errors.error_dict['revisions']
216 elif errors.error_dict.get('pullrequest_title'):
216 elif errors.error_dict.get('pullrequest_title'):
217 msg = _('Pull request requires a title with min. 3 chars')
217 msg = _('Pull request requires a title with min. 3 chars')
218 else:
218 else:
219 msg = _('Error creating pull request: {}').format(errors)
219 msg = _('Error creating pull request: {}').format(errors)
220 log.exception(msg)
220 log.exception(msg)
221 h.flash(msg, 'error')
221 h.flash(msg, 'error')
222
222
223 # would rather just go back to form ...
223 # would rather just go back to form ...
224 return redirect(url('pullrequest_home', repo_name=repo_name))
224 return redirect(url('pullrequest_home', repo_name=repo_name))
225
225
226 source_repo = _form['source_repo']
226 source_repo = _form['source_repo']
227 source_ref = _form['source_ref']
227 source_ref = _form['source_ref']
228 target_repo = _form['target_repo']
228 target_repo = _form['target_repo']
229 target_ref = _form['target_ref']
229 target_ref = _form['target_ref']
230 commit_ids = _form['revisions'][::-1]
230 commit_ids = _form['revisions'][::-1]
231
231
232 # find the ancestor for this pr
232 # find the ancestor for this pr
233 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
233 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
234 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
234 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
235
235
236 source_scm = source_db_repo.scm_instance()
236 source_scm = source_db_repo.scm_instance()
237 target_scm = target_db_repo.scm_instance()
237 target_scm = target_db_repo.scm_instance()
238
238
239 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
239 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
240 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
240 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
241
241
242 ancestor = source_scm.get_common_ancestor(
242 ancestor = source_scm.get_common_ancestor(
243 source_commit.raw_id, target_commit.raw_id, target_scm)
243 source_commit.raw_id, target_commit.raw_id, target_scm)
244
244
245 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
245 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
246 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
246 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
247
247
248 pullrequest_title = _form['pullrequest_title']
248 pullrequest_title = _form['pullrequest_title']
249 title_source_ref = source_ref.split(':', 2)[1]
249 title_source_ref = source_ref.split(':', 2)[1]
250 if not pullrequest_title:
250 if not pullrequest_title:
251 pullrequest_title = PullRequestModel().generate_pullrequest_title(
251 pullrequest_title = PullRequestModel().generate_pullrequest_title(
252 source=source_repo,
252 source=source_repo,
253 source_ref=title_source_ref,
253 source_ref=title_source_ref,
254 target=target_repo
254 target=target_repo
255 )
255 )
256
256
257 description = _form['pullrequest_desc']
257 description = _form['pullrequest_desc']
258
258
259 get_default_reviewers_data, validate_default_reviewers = \
259 get_default_reviewers_data, validate_default_reviewers = \
260 PullRequestModel().get_reviewer_functions()
260 PullRequestModel().get_reviewer_functions()
261
261
262 # recalculate reviewers logic, to make sure we can validate this
262 # recalculate reviewers logic, to make sure we can validate this
263 reviewer_rules = get_default_reviewers_data(
263 reviewer_rules = get_default_reviewers_data(
264 c.rhodecode_user, source_db_repo, source_commit, target_db_repo,
264 c.rhodecode_user.get_instance(), source_db_repo,
265 target_commit)
265 source_commit, target_db_repo, target_commit)
266
266
267 reviewers = validate_default_reviewers(
267 reviewers = validate_default_reviewers(
268 _form['review_members'], reviewer_rules)
268 _form['review_members'], reviewer_rules)
269
269
270 try:
270 try:
271 pull_request = PullRequestModel().create(
271 pull_request = PullRequestModel().create(
272 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
272 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
273 target_ref, commit_ids, reviewers, pullrequest_title,
273 target_ref, commit_ids, reviewers, pullrequest_title,
274 description, reviewer_rules
274 description, reviewer_rules
275 )
275 )
276 Session().commit()
276 Session().commit()
277 h.flash(_('Successfully opened new pull request'),
277 h.flash(_('Successfully opened new pull request'),
278 category='success')
278 category='success')
279 except Exception as e:
279 except Exception as e:
280 msg = _('Error occurred during creation of this pull request.')
280 msg = _('Error occurred during creation of this pull request.')
281 log.exception(msg)
281 log.exception(msg)
282 h.flash(msg, category='error')
282 h.flash(msg, category='error')
283 return redirect(url('pullrequest_home', repo_name=repo_name))
283 return redirect(url('pullrequest_home', repo_name=repo_name))
284
284
285 return redirect(url('pullrequest_show', repo_name=target_repo,
285 return redirect(url('pullrequest_show', repo_name=target_repo,
286 pull_request_id=pull_request.pull_request_id))
286 pull_request_id=pull_request.pull_request_id))
287
287
288 @LoginRequired()
288 @LoginRequired()
289 @NotAnonymous()
289 @NotAnonymous()
290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
291 'repository.admin')
291 'repository.admin')
292 @auth.CSRFRequired()
292 @auth.CSRFRequired()
293 @jsonify
293 @jsonify
294 def update(self, repo_name, pull_request_id):
294 def update(self, repo_name, pull_request_id):
295 pull_request_id = safe_int(pull_request_id)
295 pull_request_id = safe_int(pull_request_id)
296 pull_request = PullRequest.get_or_404(pull_request_id)
296 pull_request = PullRequest.get_or_404(pull_request_id)
297 # only owner or admin can update it
297 # only owner or admin can update it
298 allowed_to_update = PullRequestModel().check_user_update(
298 allowed_to_update = PullRequestModel().check_user_update(
299 pull_request, c.rhodecode_user)
299 pull_request, c.rhodecode_user)
300 if allowed_to_update:
300 if allowed_to_update:
301 controls = peppercorn.parse(request.POST.items())
301 controls = peppercorn.parse(request.POST.items())
302
302
303 if 'review_members' in controls:
303 if 'review_members' in controls:
304 self._update_reviewers(
304 self._update_reviewers(
305 pull_request_id, controls['review_members'],
305 pull_request_id, controls['review_members'],
306 pull_request.reviewer_data)
306 pull_request.reviewer_data)
307 elif str2bool(request.POST.get('update_commits', 'false')):
307 elif str2bool(request.POST.get('update_commits', 'false')):
308 self._update_commits(pull_request)
308 self._update_commits(pull_request)
309 elif str2bool(request.POST.get('close_pull_request', 'false')):
309 elif str2bool(request.POST.get('close_pull_request', 'false')):
310 self._reject_close(pull_request)
310 self._reject_close(pull_request)
311 elif str2bool(request.POST.get('edit_pull_request', 'false')):
311 elif str2bool(request.POST.get('edit_pull_request', 'false')):
312 self._edit_pull_request(pull_request)
312 self._edit_pull_request(pull_request)
313 else:
313 else:
314 raise HTTPBadRequest()
314 raise HTTPBadRequest()
315 return True
315 return True
316 raise HTTPForbidden()
316 raise HTTPForbidden()
317
317
318 def _edit_pull_request(self, pull_request):
318 def _edit_pull_request(self, pull_request):
319 try:
319 try:
320 PullRequestModel().edit(
320 PullRequestModel().edit(
321 pull_request, request.POST.get('title'),
321 pull_request, request.POST.get('title'),
322 request.POST.get('description'))
322 request.POST.get('description'))
323 except ValueError:
323 except ValueError:
324 msg = _(u'Cannot update closed pull requests.')
324 msg = _(u'Cannot update closed pull requests.')
325 h.flash(msg, category='error')
325 h.flash(msg, category='error')
326 return
326 return
327 else:
327 else:
328 Session().commit()
328 Session().commit()
329
329
330 msg = _(u'Pull request title & description updated.')
330 msg = _(u'Pull request title & description updated.')
331 h.flash(msg, category='success')
331 h.flash(msg, category='success')
332 return
332 return
333
333
334 def _update_commits(self, pull_request):
334 def _update_commits(self, pull_request):
335 resp = PullRequestModel().update_commits(pull_request)
335 resp = PullRequestModel().update_commits(pull_request)
336
336
337 if resp.executed:
337 if resp.executed:
338
338
339 if resp.target_changed and resp.source_changed:
339 if resp.target_changed and resp.source_changed:
340 changed = 'target and source repositories'
340 changed = 'target and source repositories'
341 elif resp.target_changed and not resp.source_changed:
341 elif resp.target_changed and not resp.source_changed:
342 changed = 'target repository'
342 changed = 'target repository'
343 elif not resp.target_changed and resp.source_changed:
343 elif not resp.target_changed and resp.source_changed:
344 changed = 'source repository'
344 changed = 'source repository'
345 else:
345 else:
346 changed = 'nothing'
346 changed = 'nothing'
347
347
348 msg = _(
348 msg = _(
349 u'Pull request updated to "{source_commit_id}" with '
349 u'Pull request updated to "{source_commit_id}" with '
350 u'{count_added} added, {count_removed} removed commits. '
350 u'{count_added} added, {count_removed} removed commits. '
351 u'Source of changes: {change_source}')
351 u'Source of changes: {change_source}')
352 msg = msg.format(
352 msg = msg.format(
353 source_commit_id=pull_request.source_ref_parts.commit_id,
353 source_commit_id=pull_request.source_ref_parts.commit_id,
354 count_added=len(resp.changes.added),
354 count_added=len(resp.changes.added),
355 count_removed=len(resp.changes.removed),
355 count_removed=len(resp.changes.removed),
356 change_source=changed)
356 change_source=changed)
357 h.flash(msg, category='success')
357 h.flash(msg, category='success')
358
358
359 registry = get_current_registry()
359 registry = get_current_registry()
360 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
360 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
361 channelstream_config = rhodecode_plugins.get('channelstream', {})
361 channelstream_config = rhodecode_plugins.get('channelstream', {})
362 if channelstream_config.get('enabled'):
362 if channelstream_config.get('enabled'):
363 message = msg + (
363 message = msg + (
364 ' - <a onclick="window.location.reload()">'
364 ' - <a onclick="window.location.reload()">'
365 '<strong>{}</strong></a>'.format(_('Reload page')))
365 '<strong>{}</strong></a>'.format(_('Reload page')))
366 channel = '/repo${}$/pr/{}'.format(
366 channel = '/repo${}$/pr/{}'.format(
367 pull_request.target_repo.repo_name,
367 pull_request.target_repo.repo_name,
368 pull_request.pull_request_id
368 pull_request.pull_request_id
369 )
369 )
370 payload = {
370 payload = {
371 'type': 'message',
371 'type': 'message',
372 'user': 'system',
372 'user': 'system',
373 'exclude_users': [request.user.username],
373 'exclude_users': [request.user.username],
374 'channel': channel,
374 'channel': channel,
375 'message': {
375 'message': {
376 'message': message,
376 'message': message,
377 'level': 'success',
377 'level': 'success',
378 'topic': '/notifications'
378 'topic': '/notifications'
379 }
379 }
380 }
380 }
381 channelstream_request(
381 channelstream_request(
382 channelstream_config, [payload], '/message',
382 channelstream_config, [payload], '/message',
383 raise_exc=False)
383 raise_exc=False)
384 else:
384 else:
385 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
385 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
386 warning_reasons = [
386 warning_reasons = [
387 UpdateFailureReason.NO_CHANGE,
387 UpdateFailureReason.NO_CHANGE,
388 UpdateFailureReason.WRONG_REF_TYPE,
388 UpdateFailureReason.WRONG_REF_TYPE,
389 ]
389 ]
390 category = 'warning' if resp.reason in warning_reasons else 'error'
390 category = 'warning' if resp.reason in warning_reasons else 'error'
391 h.flash(msg, category=category)
391 h.flash(msg, category=category)
392
392
393 @auth.CSRFRequired()
393 @auth.CSRFRequired()
394 @LoginRequired()
394 @LoginRequired()
395 @NotAnonymous()
395 @NotAnonymous()
396 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
396 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
397 'repository.admin')
397 'repository.admin')
398 def merge(self, repo_name, pull_request_id):
398 def merge(self, repo_name, pull_request_id):
399 """
399 """
400 POST /{repo_name}/pull-request/{pull_request_id}
400 POST /{repo_name}/pull-request/{pull_request_id}
401
401
402 Merge will perform a server-side merge of the specified
402 Merge will perform a server-side merge of the specified
403 pull request, if the pull request is approved and mergeable.
403 pull request, if the pull request is approved and mergeable.
404 After successful merging, the pull request is automatically
404 After successful merging, the pull request is automatically
405 closed, with a relevant comment.
405 closed, with a relevant comment.
406 """
406 """
407 pull_request_id = safe_int(pull_request_id)
407 pull_request_id = safe_int(pull_request_id)
408 pull_request = PullRequest.get_or_404(pull_request_id)
408 pull_request = PullRequest.get_or_404(pull_request_id)
409 user = c.rhodecode_user
409 user = c.rhodecode_user
410
410
411 check = MergeCheck.validate(pull_request, user)
411 check = MergeCheck.validate(pull_request, user)
412 merge_possible = not check.failed
412 merge_possible = not check.failed
413
413
414 for err_type, error_msg in check.errors:
414 for err_type, error_msg in check.errors:
415 h.flash(error_msg, category=err_type)
415 h.flash(error_msg, category=err_type)
416
416
417 if merge_possible:
417 if merge_possible:
418 log.debug("Pre-conditions checked, trying to merge.")
418 log.debug("Pre-conditions checked, trying to merge.")
419 extras = vcs_operation_context(
419 extras = vcs_operation_context(
420 request.environ, repo_name=pull_request.target_repo.repo_name,
420 request.environ, repo_name=pull_request.target_repo.repo_name,
421 username=user.username, action='push',
421 username=user.username, action='push',
422 scm=pull_request.target_repo.repo_type)
422 scm=pull_request.target_repo.repo_type)
423 self._merge_pull_request(pull_request, user, extras)
423 self._merge_pull_request(pull_request, user, extras)
424
424
425 return redirect(url(
425 return redirect(url(
426 'pullrequest_show',
426 'pullrequest_show',
427 repo_name=pull_request.target_repo.repo_name,
427 repo_name=pull_request.target_repo.repo_name,
428 pull_request_id=pull_request.pull_request_id))
428 pull_request_id=pull_request.pull_request_id))
429
429
430 def _merge_pull_request(self, pull_request, user, extras):
430 def _merge_pull_request(self, pull_request, user, extras):
431 merge_resp = PullRequestModel().merge(
431 merge_resp = PullRequestModel().merge(
432 pull_request, user, extras=extras)
432 pull_request, user, extras=extras)
433
433
434 if merge_resp.executed:
434 if merge_resp.executed:
435 log.debug("The merge was successful, closing the pull request.")
435 log.debug("The merge was successful, closing the pull request.")
436 PullRequestModel().close_pull_request(
436 PullRequestModel().close_pull_request(
437 pull_request.pull_request_id, user)
437 pull_request.pull_request_id, user)
438 Session().commit()
438 Session().commit()
439 msg = _('Pull request was successfully merged and closed.')
439 msg = _('Pull request was successfully merged and closed.')
440 h.flash(msg, category='success')
440 h.flash(msg, category='success')
441 else:
441 else:
442 log.debug(
442 log.debug(
443 "The merge was not successful. Merge response: %s",
443 "The merge was not successful. Merge response: %s",
444 merge_resp)
444 merge_resp)
445 msg = PullRequestModel().merge_status_message(
445 msg = PullRequestModel().merge_status_message(
446 merge_resp.failure_reason)
446 merge_resp.failure_reason)
447 h.flash(msg, category='error')
447 h.flash(msg, category='error')
448
448
449 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
449 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
450
450
451 get_default_reviewers_data, validate_default_reviewers = \
451 get_default_reviewers_data, validate_default_reviewers = \
452 PullRequestModel().get_reviewer_functions()
452 PullRequestModel().get_reviewer_functions()
453
453
454 try:
454 try:
455 reviewers = validate_default_reviewers(review_members, reviewer_rules)
455 reviewers = validate_default_reviewers(review_members, reviewer_rules)
456 except ValueError as e:
456 except ValueError as e:
457 log.error('Reviewers Validation:{}'.format(e))
457 log.error('Reviewers Validation:{}'.format(e))
458 h.flash(e, category='error')
458 h.flash(e, category='error')
459 return
459 return
460
460
461 PullRequestModel().update_reviewers(pull_request_id, reviewers)
461 PullRequestModel().update_reviewers(pull_request_id, reviewers)
462 h.flash(_('Pull request reviewers updated.'), category='success')
462 h.flash(_('Pull request reviewers updated.'), category='success')
463 Session().commit()
463 Session().commit()
464
464
465 def _reject_close(self, pull_request):
465 def _reject_close(self, pull_request):
466 if pull_request.is_closed():
466 if pull_request.is_closed():
467 raise HTTPForbidden()
467 raise HTTPForbidden()
468
468
469 PullRequestModel().close_pull_request_with_comment(
469 PullRequestModel().close_pull_request_with_comment(
470 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
470 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
471 Session().commit()
471 Session().commit()
472
472
473 @LoginRequired()
473 @LoginRequired()
474 @NotAnonymous()
474 @NotAnonymous()
475 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
475 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
476 'repository.admin')
476 'repository.admin')
477 @auth.CSRFRequired()
477 @auth.CSRFRequired()
478 @jsonify
478 @jsonify
479 def delete(self, repo_name, pull_request_id):
479 def delete(self, repo_name, pull_request_id):
480 pull_request_id = safe_int(pull_request_id)
480 pull_request_id = safe_int(pull_request_id)
481 pull_request = PullRequest.get_or_404(pull_request_id)
481 pull_request = PullRequest.get_or_404(pull_request_id)
482
482
483 pr_closed = pull_request.is_closed()
483 pr_closed = pull_request.is_closed()
484 allowed_to_delete = PullRequestModel().check_user_delete(
484 allowed_to_delete = PullRequestModel().check_user_delete(
485 pull_request, c.rhodecode_user) and not pr_closed
485 pull_request, c.rhodecode_user) and not pr_closed
486
486
487 # only owner can delete it !
487 # only owner can delete it !
488 if allowed_to_delete:
488 if allowed_to_delete:
489 PullRequestModel().delete(pull_request)
489 PullRequestModel().delete(pull_request)
490 Session().commit()
490 Session().commit()
491 h.flash(_('Successfully deleted pull request'),
491 h.flash(_('Successfully deleted pull request'),
492 category='success')
492 category='success')
493 return redirect(url('my_account_pullrequests'))
493 return redirect(url('my_account_pullrequests'))
494
494
495 h.flash(_('Your are not allowed to delete this pull request'),
495 h.flash(_('Your are not allowed to delete this pull request'),
496 category='error')
496 category='error')
497 raise HTTPForbidden()
497 raise HTTPForbidden()
498
498
499 def _get_pr_version(self, pull_request_id, version=None):
499 def _get_pr_version(self, pull_request_id, version=None):
500 pull_request_id = safe_int(pull_request_id)
500 pull_request_id = safe_int(pull_request_id)
501 at_version = None
501 at_version = None
502
502
503 if version and version == 'latest':
503 if version and version == 'latest':
504 pull_request_ver = PullRequest.get(pull_request_id)
504 pull_request_ver = PullRequest.get(pull_request_id)
505 pull_request_obj = pull_request_ver
505 pull_request_obj = pull_request_ver
506 _org_pull_request_obj = pull_request_obj
506 _org_pull_request_obj = pull_request_obj
507 at_version = 'latest'
507 at_version = 'latest'
508 elif version:
508 elif version:
509 pull_request_ver = PullRequestVersion.get_or_404(version)
509 pull_request_ver = PullRequestVersion.get_or_404(version)
510 pull_request_obj = pull_request_ver
510 pull_request_obj = pull_request_ver
511 _org_pull_request_obj = pull_request_ver.pull_request
511 _org_pull_request_obj = pull_request_ver.pull_request
512 at_version = pull_request_ver.pull_request_version_id
512 at_version = pull_request_ver.pull_request_version_id
513 else:
513 else:
514 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
514 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
515 pull_request_id)
515 pull_request_id)
516
516
517 pull_request_display_obj = PullRequest.get_pr_display_object(
517 pull_request_display_obj = PullRequest.get_pr_display_object(
518 pull_request_obj, _org_pull_request_obj)
518 pull_request_obj, _org_pull_request_obj)
519
519
520 return _org_pull_request_obj, pull_request_obj, \
520 return _org_pull_request_obj, pull_request_obj, \
521 pull_request_display_obj, at_version
521 pull_request_display_obj, at_version
522
522
523 def _get_diffset(
523 def _get_diffset(
524 self, source_repo, source_ref_id, target_ref_id, target_commit,
524 self, source_repo, source_ref_id, target_ref_id, target_commit,
525 source_commit, diff_limit, file_limit, display_inline_comments):
525 source_commit, diff_limit, file_limit, display_inline_comments):
526 vcs_diff = PullRequestModel().get_diff(
526 vcs_diff = PullRequestModel().get_diff(
527 source_repo, source_ref_id, target_ref_id)
527 source_repo, source_ref_id, target_ref_id)
528
528
529 diff_processor = diffs.DiffProcessor(
529 diff_processor = diffs.DiffProcessor(
530 vcs_diff, format='newdiff', diff_limit=diff_limit,
530 vcs_diff, format='newdiff', diff_limit=diff_limit,
531 file_limit=file_limit, show_full_diff=c.fulldiff)
531 file_limit=file_limit, show_full_diff=c.fulldiff)
532
532
533 _parsed = diff_processor.prepare()
533 _parsed = diff_processor.prepare()
534
534
535 def _node_getter(commit):
535 def _node_getter(commit):
536 def get_node(fname):
536 def get_node(fname):
537 try:
537 try:
538 return commit.get_node(fname)
538 return commit.get_node(fname)
539 except NodeDoesNotExistError:
539 except NodeDoesNotExistError:
540 return None
540 return None
541
541
542 return get_node
542 return get_node
543
543
544 diffset = codeblocks.DiffSet(
544 diffset = codeblocks.DiffSet(
545 repo_name=c.repo_name,
545 repo_name=c.repo_name,
546 source_repo_name=c.source_repo.repo_name,
546 source_repo_name=c.source_repo.repo_name,
547 source_node_getter=_node_getter(target_commit),
547 source_node_getter=_node_getter(target_commit),
548 target_node_getter=_node_getter(source_commit),
548 target_node_getter=_node_getter(source_commit),
549 comments=display_inline_comments
549 comments=display_inline_comments
550 )
550 )
551 diffset = diffset.render_patchset(
551 diffset = diffset.render_patchset(
552 _parsed, target_commit.raw_id, source_commit.raw_id)
552 _parsed, target_commit.raw_id, source_commit.raw_id)
553
553
554 return diffset
554 return diffset
555
555
556 @LoginRequired()
556 @LoginRequired()
557 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
557 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
558 'repository.admin')
558 'repository.admin')
559 def show(self, repo_name, pull_request_id):
559 def show(self, repo_name, pull_request_id):
560 pull_request_id = safe_int(pull_request_id)
560 pull_request_id = safe_int(pull_request_id)
561 version = request.GET.get('version')
561 version = request.GET.get('version')
562 from_version = request.GET.get('from_version') or version
562 from_version = request.GET.get('from_version') or version
563 merge_checks = request.GET.get('merge_checks')
563 merge_checks = request.GET.get('merge_checks')
564 c.fulldiff = str2bool(request.GET.get('fulldiff'))
564 c.fulldiff = str2bool(request.GET.get('fulldiff'))
565
565
566 (pull_request_latest,
566 (pull_request_latest,
567 pull_request_at_ver,
567 pull_request_at_ver,
568 pull_request_display_obj,
568 pull_request_display_obj,
569 at_version) = self._get_pr_version(
569 at_version) = self._get_pr_version(
570 pull_request_id, version=version)
570 pull_request_id, version=version)
571 pr_closed = pull_request_latest.is_closed()
571 pr_closed = pull_request_latest.is_closed()
572
572
573 if pr_closed and (version or from_version):
573 if pr_closed and (version or from_version):
574 # not allow to browse versions
574 # not allow to browse versions
575 return redirect(h.url('pullrequest_show', repo_name=repo_name,
575 return redirect(h.url('pullrequest_show', repo_name=repo_name,
576 pull_request_id=pull_request_id))
576 pull_request_id=pull_request_id))
577
577
578 versions = pull_request_display_obj.versions()
578 versions = pull_request_display_obj.versions()
579
579
580 c.at_version = at_version
580 c.at_version = at_version
581 c.at_version_num = (at_version
581 c.at_version_num = (at_version
582 if at_version and at_version != 'latest'
582 if at_version and at_version != 'latest'
583 else None)
583 else None)
584 c.at_version_pos = ChangesetComment.get_index_from_version(
584 c.at_version_pos = ChangesetComment.get_index_from_version(
585 c.at_version_num, versions)
585 c.at_version_num, versions)
586
586
587 (prev_pull_request_latest,
587 (prev_pull_request_latest,
588 prev_pull_request_at_ver,
588 prev_pull_request_at_ver,
589 prev_pull_request_display_obj,
589 prev_pull_request_display_obj,
590 prev_at_version) = self._get_pr_version(
590 prev_at_version) = self._get_pr_version(
591 pull_request_id, version=from_version)
591 pull_request_id, version=from_version)
592
592
593 c.from_version = prev_at_version
593 c.from_version = prev_at_version
594 c.from_version_num = (prev_at_version
594 c.from_version_num = (prev_at_version
595 if prev_at_version and prev_at_version != 'latest'
595 if prev_at_version and prev_at_version != 'latest'
596 else None)
596 else None)
597 c.from_version_pos = ChangesetComment.get_index_from_version(
597 c.from_version_pos = ChangesetComment.get_index_from_version(
598 c.from_version_num, versions)
598 c.from_version_num, versions)
599
599
600 # define if we're in COMPARE mode or VIEW at version mode
600 # define if we're in COMPARE mode or VIEW at version mode
601 compare = at_version != prev_at_version
601 compare = at_version != prev_at_version
602
602
603 # pull_requests repo_name we opened it against
603 # pull_requests repo_name we opened it against
604 # ie. target_repo must match
604 # ie. target_repo must match
605 if repo_name != pull_request_at_ver.target_repo.repo_name:
605 if repo_name != pull_request_at_ver.target_repo.repo_name:
606 raise HTTPNotFound
606 raise HTTPNotFound
607
607
608 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
608 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
609 pull_request_at_ver)
609 pull_request_at_ver)
610
610
611 c.pull_request = pull_request_display_obj
611 c.pull_request = pull_request_display_obj
612 c.pull_request_latest = pull_request_latest
612 c.pull_request_latest = pull_request_latest
613
613
614 if compare or (at_version and not at_version == 'latest'):
614 if compare or (at_version and not at_version == 'latest'):
615 c.allowed_to_change_status = False
615 c.allowed_to_change_status = False
616 c.allowed_to_update = False
616 c.allowed_to_update = False
617 c.allowed_to_merge = False
617 c.allowed_to_merge = False
618 c.allowed_to_delete = False
618 c.allowed_to_delete = False
619 c.allowed_to_comment = False
619 c.allowed_to_comment = False
620 c.allowed_to_close = False
620 c.allowed_to_close = False
621 else:
621 else:
622 can_change_status = PullRequestModel().check_user_change_status(
622 can_change_status = PullRequestModel().check_user_change_status(
623 pull_request_at_ver, c.rhodecode_user)
623 pull_request_at_ver, c.rhodecode_user)
624 c.allowed_to_change_status = can_change_status and not pr_closed
624 c.allowed_to_change_status = can_change_status and not pr_closed
625
625
626 c.allowed_to_update = PullRequestModel().check_user_update(
626 c.allowed_to_update = PullRequestModel().check_user_update(
627 pull_request_latest, c.rhodecode_user) and not pr_closed
627 pull_request_latest, c.rhodecode_user) and not pr_closed
628 c.allowed_to_merge = PullRequestModel().check_user_merge(
628 c.allowed_to_merge = PullRequestModel().check_user_merge(
629 pull_request_latest, c.rhodecode_user) and not pr_closed
629 pull_request_latest, c.rhodecode_user) and not pr_closed
630 c.allowed_to_delete = PullRequestModel().check_user_delete(
630 c.allowed_to_delete = PullRequestModel().check_user_delete(
631 pull_request_latest, c.rhodecode_user) and not pr_closed
631 pull_request_latest, c.rhodecode_user) and not pr_closed
632 c.allowed_to_comment = not pr_closed
632 c.allowed_to_comment = not pr_closed
633 c.allowed_to_close = c.allowed_to_merge and not pr_closed
633 c.allowed_to_close = c.allowed_to_merge and not pr_closed
634
634
635 c.forbid_adding_reviewers = False
635 c.forbid_adding_reviewers = False
636 c.forbid_author_to_review = False
636 c.forbid_author_to_review = False
637 c.forbid_commit_author_to_review = False
637
638
638 if pull_request_latest.reviewer_data and \
639 if pull_request_latest.reviewer_data and \
639 'rules' in pull_request_latest.reviewer_data:
640 'rules' in pull_request_latest.reviewer_data:
640 rules = pull_request_latest.reviewer_data['rules'] or {}
641 rules = pull_request_latest.reviewer_data['rules'] or {}
641 try:
642 try:
642 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
643 c.forbid_adding_reviewers = rules.get(
643 c.forbid_author_to_review = rules.get('forbid_author_to_review')
644 'forbid_adding_reviewers')
645 c.forbid_author_to_review = rules.get(
646 'forbid_author_to_review')
647 c.forbid_commit_author_to_review = rules.get(
648 'forbid_commit_author_to_review')
644 except Exception:
649 except Exception:
645 pass
650 pass
646
651
647 # check merge capabilities
652 # check merge capabilities
648 _merge_check = MergeCheck.validate(
653 _merge_check = MergeCheck.validate(
649 pull_request_latest, user=c.rhodecode_user)
654 pull_request_latest, user=c.rhodecode_user)
650 c.pr_merge_errors = _merge_check.error_details
655 c.pr_merge_errors = _merge_check.error_details
651 c.pr_merge_possible = not _merge_check.failed
656 c.pr_merge_possible = not _merge_check.failed
652 c.pr_merge_message = _merge_check.merge_msg
657 c.pr_merge_message = _merge_check.merge_msg
653
658
654 c.pull_request_review_status = _merge_check.review_status
659 c.pull_request_review_status = _merge_check.review_status
655 if merge_checks:
660 if merge_checks:
656 return render('/pullrequests/pullrequest_merge_checks.mako')
661 return render('/pullrequests/pullrequest_merge_checks.mako')
657
662
658 comments_model = CommentsModel()
663 comments_model = CommentsModel()
659
664
660 # reviewers and statuses
665 # reviewers and statuses
661 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
666 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
662 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
667 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
663
668
664 # GENERAL COMMENTS with versions #
669 # GENERAL COMMENTS with versions #
665 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
670 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
666 q = q.order_by(ChangesetComment.comment_id.asc())
671 q = q.order_by(ChangesetComment.comment_id.asc())
667 general_comments = q
672 general_comments = q
668
673
669 # pick comments we want to render at current version
674 # pick comments we want to render at current version
670 c.comment_versions = comments_model.aggregate_comments(
675 c.comment_versions = comments_model.aggregate_comments(
671 general_comments, versions, c.at_version_num)
676 general_comments, versions, c.at_version_num)
672 c.comments = c.comment_versions[c.at_version_num]['until']
677 c.comments = c.comment_versions[c.at_version_num]['until']
673
678
674 # INLINE COMMENTS with versions #
679 # INLINE COMMENTS with versions #
675 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
680 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
676 q = q.order_by(ChangesetComment.comment_id.asc())
681 q = q.order_by(ChangesetComment.comment_id.asc())
677 inline_comments = q
682 inline_comments = q
678
683
679 c.inline_versions = comments_model.aggregate_comments(
684 c.inline_versions = comments_model.aggregate_comments(
680 inline_comments, versions, c.at_version_num, inline=True)
685 inline_comments, versions, c.at_version_num, inline=True)
681
686
682 # inject latest version
687 # inject latest version
683 latest_ver = PullRequest.get_pr_display_object(
688 latest_ver = PullRequest.get_pr_display_object(
684 pull_request_latest, pull_request_latest)
689 pull_request_latest, pull_request_latest)
685
690
686 c.versions = versions + [latest_ver]
691 c.versions = versions + [latest_ver]
687
692
688 # if we use version, then do not show later comments
693 # if we use version, then do not show later comments
689 # than current version
694 # than current version
690 display_inline_comments = collections.defaultdict(
695 display_inline_comments = collections.defaultdict(
691 lambda: collections.defaultdict(list))
696 lambda: collections.defaultdict(list))
692 for co in inline_comments:
697 for co in inline_comments:
693 if c.at_version_num:
698 if c.at_version_num:
694 # pick comments that are at least UPTO given version, so we
699 # pick comments that are at least UPTO given version, so we
695 # don't render comments for higher version
700 # don't render comments for higher version
696 should_render = co.pull_request_version_id and \
701 should_render = co.pull_request_version_id and \
697 co.pull_request_version_id <= c.at_version_num
702 co.pull_request_version_id <= c.at_version_num
698 else:
703 else:
699 # showing all, for 'latest'
704 # showing all, for 'latest'
700 should_render = True
705 should_render = True
701
706
702 if should_render:
707 if should_render:
703 display_inline_comments[co.f_path][co.line_no].append(co)
708 display_inline_comments[co.f_path][co.line_no].append(co)
704
709
705 # load diff data into template context, if we use compare mode then
710 # load diff data into template context, if we use compare mode then
706 # diff is calculated based on changes between versions of PR
711 # diff is calculated based on changes between versions of PR
707
712
708 source_repo = pull_request_at_ver.source_repo
713 source_repo = pull_request_at_ver.source_repo
709 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
714 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
710
715
711 target_repo = pull_request_at_ver.target_repo
716 target_repo = pull_request_at_ver.target_repo
712 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
717 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
713
718
714 if compare:
719 if compare:
715 # in compare switch the diff base to latest commit from prev version
720 # in compare switch the diff base to latest commit from prev version
716 target_ref_id = prev_pull_request_display_obj.revisions[0]
721 target_ref_id = prev_pull_request_display_obj.revisions[0]
717
722
718 # despite opening commits for bookmarks/branches/tags, we always
723 # despite opening commits for bookmarks/branches/tags, we always
719 # convert this to rev to prevent changes after bookmark or branch change
724 # convert this to rev to prevent changes after bookmark or branch change
720 c.source_ref_type = 'rev'
725 c.source_ref_type = 'rev'
721 c.source_ref = source_ref_id
726 c.source_ref = source_ref_id
722
727
723 c.target_ref_type = 'rev'
728 c.target_ref_type = 'rev'
724 c.target_ref = target_ref_id
729 c.target_ref = target_ref_id
725
730
726 c.source_repo = source_repo
731 c.source_repo = source_repo
727 c.target_repo = target_repo
732 c.target_repo = target_repo
728
733
729 # diff_limit is the old behavior, will cut off the whole diff
734 # diff_limit is the old behavior, will cut off the whole diff
730 # if the limit is applied otherwise will just hide the
735 # if the limit is applied otherwise will just hide the
731 # big files from the front-end
736 # big files from the front-end
732 diff_limit = self.cut_off_limit_diff
737 diff_limit = self.cut_off_limit_diff
733 file_limit = self.cut_off_limit_file
738 file_limit = self.cut_off_limit_file
734
739
735 c.commit_ranges = []
740 c.commit_ranges = []
736 source_commit = EmptyCommit()
741 source_commit = EmptyCommit()
737 target_commit = EmptyCommit()
742 target_commit = EmptyCommit()
738 c.missing_requirements = False
743 c.missing_requirements = False
739
744
740 source_scm = source_repo.scm_instance()
745 source_scm = source_repo.scm_instance()
741 target_scm = target_repo.scm_instance()
746 target_scm = target_repo.scm_instance()
742
747
743 # try first shadow repo, fallback to regular repo
748 # try first shadow repo, fallback to regular repo
744 try:
749 try:
745 commits_source_repo = pull_request_latest.get_shadow_repo()
750 commits_source_repo = pull_request_latest.get_shadow_repo()
746 except Exception:
751 except Exception:
747 log.debug('Failed to get shadow repo', exc_info=True)
752 log.debug('Failed to get shadow repo', exc_info=True)
748 commits_source_repo = source_scm
753 commits_source_repo = source_scm
749
754
750 c.commits_source_repo = commits_source_repo
755 c.commits_source_repo = commits_source_repo
751 commit_cache = {}
756 commit_cache = {}
752 try:
757 try:
753 pre_load = ["author", "branch", "date", "message"]
758 pre_load = ["author", "branch", "date", "message"]
754 show_revs = pull_request_at_ver.revisions
759 show_revs = pull_request_at_ver.revisions
755 for rev in show_revs:
760 for rev in show_revs:
756 comm = commits_source_repo.get_commit(
761 comm = commits_source_repo.get_commit(
757 commit_id=rev, pre_load=pre_load)
762 commit_id=rev, pre_load=pre_load)
758 c.commit_ranges.append(comm)
763 c.commit_ranges.append(comm)
759 commit_cache[comm.raw_id] = comm
764 commit_cache[comm.raw_id] = comm
760
765
761 # Order here matters, we first need to get target, and then
766 # Order here matters, we first need to get target, and then
762 # the source
767 # the source
763 target_commit = commits_source_repo.get_commit(
768 target_commit = commits_source_repo.get_commit(
764 commit_id=safe_str(target_ref_id))
769 commit_id=safe_str(target_ref_id))
765
770
766 source_commit = commits_source_repo.get_commit(
771 source_commit = commits_source_repo.get_commit(
767 commit_id=safe_str(source_ref_id))
772 commit_id=safe_str(source_ref_id))
768
773
769 except CommitDoesNotExistError:
774 except CommitDoesNotExistError:
770 log.warning(
775 log.warning(
771 'Failed to get commit from `{}` repo'.format(
776 'Failed to get commit from `{}` repo'.format(
772 commits_source_repo), exc_info=True)
777 commits_source_repo), exc_info=True)
773 except RepositoryRequirementError:
778 except RepositoryRequirementError:
774 log.warning(
779 log.warning(
775 'Failed to get all required data from repo', exc_info=True)
780 'Failed to get all required data from repo', exc_info=True)
776 c.missing_requirements = True
781 c.missing_requirements = True
777
782
778 c.ancestor = None # set it to None, to hide it from PR view
783 c.ancestor = None # set it to None, to hide it from PR view
779
784
780 try:
785 try:
781 ancestor_id = source_scm.get_common_ancestor(
786 ancestor_id = source_scm.get_common_ancestor(
782 source_commit.raw_id, target_commit.raw_id, target_scm)
787 source_commit.raw_id, target_commit.raw_id, target_scm)
783 c.ancestor_commit = source_scm.get_commit(ancestor_id)
788 c.ancestor_commit = source_scm.get_commit(ancestor_id)
784 except Exception:
789 except Exception:
785 c.ancestor_commit = None
790 c.ancestor_commit = None
786
791
787 c.statuses = source_repo.statuses(
792 c.statuses = source_repo.statuses(
788 [x.raw_id for x in c.commit_ranges])
793 [x.raw_id for x in c.commit_ranges])
789
794
790 # auto collapse if we have more than limit
795 # auto collapse if we have more than limit
791 collapse_limit = diffs.DiffProcessor._collapse_commits_over
796 collapse_limit = diffs.DiffProcessor._collapse_commits_over
792 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
797 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
793 c.compare_mode = compare
798 c.compare_mode = compare
794
799
795 c.missing_commits = False
800 c.missing_commits = False
796 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
801 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
797 or source_commit == target_commit):
802 or source_commit == target_commit):
798
803
799 c.missing_commits = True
804 c.missing_commits = True
800 else:
805 else:
801
806
802 c.diffset = self._get_diffset(
807 c.diffset = self._get_diffset(
803 commits_source_repo, source_ref_id, target_ref_id,
808 commits_source_repo, source_ref_id, target_ref_id,
804 target_commit, source_commit,
809 target_commit, source_commit,
805 diff_limit, file_limit, display_inline_comments)
810 diff_limit, file_limit, display_inline_comments)
806
811
807 c.limited_diff = c.diffset.limited_diff
812 c.limited_diff = c.diffset.limited_diff
808
813
809 # calculate removed files that are bound to comments
814 # calculate removed files that are bound to comments
810 comment_deleted_files = [
815 comment_deleted_files = [
811 fname for fname in display_inline_comments
816 fname for fname in display_inline_comments
812 if fname not in c.diffset.file_stats]
817 if fname not in c.diffset.file_stats]
813
818
814 c.deleted_files_comments = collections.defaultdict(dict)
819 c.deleted_files_comments = collections.defaultdict(dict)
815 for fname, per_line_comments in display_inline_comments.items():
820 for fname, per_line_comments in display_inline_comments.items():
816 if fname in comment_deleted_files:
821 if fname in comment_deleted_files:
817 c.deleted_files_comments[fname]['stats'] = 0
822 c.deleted_files_comments[fname]['stats'] = 0
818 c.deleted_files_comments[fname]['comments'] = list()
823 c.deleted_files_comments[fname]['comments'] = list()
819 for lno, comments in per_line_comments.items():
824 for lno, comments in per_line_comments.items():
820 c.deleted_files_comments[fname]['comments'].extend(
825 c.deleted_files_comments[fname]['comments'].extend(
821 comments)
826 comments)
822
827
823 # this is a hack to properly display links, when creating PR, the
828 # this is a hack to properly display links, when creating PR, the
824 # compare view and others uses different notation, and
829 # compare view and others uses different notation, and
825 # compare_commits.mako renders links based on the target_repo.
830 # compare_commits.mako renders links based on the target_repo.
826 # We need to swap that here to generate it properly on the html side
831 # We need to swap that here to generate it properly on the html side
827 c.target_repo = c.source_repo
832 c.target_repo = c.source_repo
828
833
829 c.commit_statuses = ChangesetStatus.STATUSES
834 c.commit_statuses = ChangesetStatus.STATUSES
830
835
831 c.show_version_changes = not pr_closed
836 c.show_version_changes = not pr_closed
832 if c.show_version_changes:
837 if c.show_version_changes:
833 cur_obj = pull_request_at_ver
838 cur_obj = pull_request_at_ver
834 prev_obj = prev_pull_request_at_ver
839 prev_obj = prev_pull_request_at_ver
835
840
836 old_commit_ids = prev_obj.revisions
841 old_commit_ids = prev_obj.revisions
837 new_commit_ids = cur_obj.revisions
842 new_commit_ids = cur_obj.revisions
838 commit_changes = PullRequestModel()._calculate_commit_id_changes(
843 commit_changes = PullRequestModel()._calculate_commit_id_changes(
839 old_commit_ids, new_commit_ids)
844 old_commit_ids, new_commit_ids)
840 c.commit_changes_summary = commit_changes
845 c.commit_changes_summary = commit_changes
841
846
842 # calculate the diff for commits between versions
847 # calculate the diff for commits between versions
843 c.commit_changes = []
848 c.commit_changes = []
844 mark = lambda cs, fw: list(
849 mark = lambda cs, fw: list(
845 h.itertools.izip_longest([], cs, fillvalue=fw))
850 h.itertools.izip_longest([], cs, fillvalue=fw))
846 for c_type, raw_id in mark(commit_changes.added, 'a') \
851 for c_type, raw_id in mark(commit_changes.added, 'a') \
847 + mark(commit_changes.removed, 'r') \
852 + mark(commit_changes.removed, 'r') \
848 + mark(commit_changes.common, 'c'):
853 + mark(commit_changes.common, 'c'):
849
854
850 if raw_id in commit_cache:
855 if raw_id in commit_cache:
851 commit = commit_cache[raw_id]
856 commit = commit_cache[raw_id]
852 else:
857 else:
853 try:
858 try:
854 commit = commits_source_repo.get_commit(raw_id)
859 commit = commits_source_repo.get_commit(raw_id)
855 except CommitDoesNotExistError:
860 except CommitDoesNotExistError:
856 # in case we fail extracting still use "dummy" commit
861 # in case we fail extracting still use "dummy" commit
857 # for display in commit diff
862 # for display in commit diff
858 commit = h.AttributeDict(
863 commit = h.AttributeDict(
859 {'raw_id': raw_id,
864 {'raw_id': raw_id,
860 'message': 'EMPTY or MISSING COMMIT'})
865 'message': 'EMPTY or MISSING COMMIT'})
861 c.commit_changes.append([c_type, commit])
866 c.commit_changes.append([c_type, commit])
862
867
863 # current user review statuses for each version
868 # current user review statuses for each version
864 c.review_versions = {}
869 c.review_versions = {}
865 if c.rhodecode_user.user_id in allowed_reviewers:
870 if c.rhodecode_user.user_id in allowed_reviewers:
866 for co in general_comments:
871 for co in general_comments:
867 if co.author.user_id == c.rhodecode_user.user_id:
872 if co.author.user_id == c.rhodecode_user.user_id:
868 # each comment has a status change
873 # each comment has a status change
869 status = co.status_change
874 status = co.status_change
870 if status:
875 if status:
871 _ver_pr = status[0].comment.pull_request_version_id
876 _ver_pr = status[0].comment.pull_request_version_id
872 c.review_versions[_ver_pr] = status[0]
877 c.review_versions[_ver_pr] = status[0]
873
878
874 return render('/pullrequests/pullrequest_show.mako')
879 return render('/pullrequests/pullrequest_show.mako')
875
880
876 @LoginRequired()
881 @LoginRequired()
877 @NotAnonymous()
882 @NotAnonymous()
878 @HasRepoPermissionAnyDecorator(
883 @HasRepoPermissionAnyDecorator(
879 'repository.read', 'repository.write', 'repository.admin')
884 'repository.read', 'repository.write', 'repository.admin')
880 @auth.CSRFRequired()
885 @auth.CSRFRequired()
881 @jsonify
886 @jsonify
882 def comment(self, repo_name, pull_request_id):
887 def comment(self, repo_name, pull_request_id):
883 pull_request_id = safe_int(pull_request_id)
888 pull_request_id = safe_int(pull_request_id)
884 pull_request = PullRequest.get_or_404(pull_request_id)
889 pull_request = PullRequest.get_or_404(pull_request_id)
885 if pull_request.is_closed():
890 if pull_request.is_closed():
886 raise HTTPForbidden()
891 raise HTTPForbidden()
887
892
888 status = request.POST.get('changeset_status', None)
893 status = request.POST.get('changeset_status', None)
889 text = request.POST.get('text')
894 text = request.POST.get('text')
890 comment_type = request.POST.get('comment_type')
895 comment_type = request.POST.get('comment_type')
891 resolves_comment_id = request.POST.get('resolves_comment_id', None)
896 resolves_comment_id = request.POST.get('resolves_comment_id', None)
892 close_pull_request = request.POST.get('close_pull_request')
897 close_pull_request = request.POST.get('close_pull_request')
893
898
894 close_pr = False
899 close_pr = False
895 # only owner or admin or person with write permissions
900 # only owner or admin or person with write permissions
896 allowed_to_close = PullRequestModel().check_user_update(
901 allowed_to_close = PullRequestModel().check_user_update(
897 pull_request, c.rhodecode_user)
902 pull_request, c.rhodecode_user)
898
903
899 if close_pull_request and allowed_to_close:
904 if close_pull_request and allowed_to_close:
900 close_pr = True
905 close_pr = True
901 pull_request_review_status = pull_request.calculated_review_status()
906 pull_request_review_status = pull_request.calculated_review_status()
902 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
907 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
903 # approved only if we have voting consent
908 # approved only if we have voting consent
904 status = ChangesetStatus.STATUS_APPROVED
909 status = ChangesetStatus.STATUS_APPROVED
905 else:
910 else:
906 status = ChangesetStatus.STATUS_REJECTED
911 status = ChangesetStatus.STATUS_REJECTED
907
912
908 allowed_to_change_status = PullRequestModel().check_user_change_status(
913 allowed_to_change_status = PullRequestModel().check_user_change_status(
909 pull_request, c.rhodecode_user)
914 pull_request, c.rhodecode_user)
910
915
911 if status and allowed_to_change_status:
916 if status and allowed_to_change_status:
912 message = (_('Status change %(transition_icon)s %(status)s')
917 message = (_('Status change %(transition_icon)s %(status)s')
913 % {'transition_icon': '>',
918 % {'transition_icon': '>',
914 'status': ChangesetStatus.get_status_lbl(status)})
919 'status': ChangesetStatus.get_status_lbl(status)})
915 if close_pr:
920 if close_pr:
916 message = _('Closing with') + ' ' + message
921 message = _('Closing with') + ' ' + message
917 text = text or message
922 text = text or message
918 comm = CommentsModel().create(
923 comm = CommentsModel().create(
919 text=text,
924 text=text,
920 repo=c.rhodecode_db_repo.repo_id,
925 repo=c.rhodecode_db_repo.repo_id,
921 user=c.rhodecode_user.user_id,
926 user=c.rhodecode_user.user_id,
922 pull_request=pull_request_id,
927 pull_request=pull_request_id,
923 f_path=request.POST.get('f_path'),
928 f_path=request.POST.get('f_path'),
924 line_no=request.POST.get('line'),
929 line_no=request.POST.get('line'),
925 status_change=(ChangesetStatus.get_status_lbl(status)
930 status_change=(ChangesetStatus.get_status_lbl(status)
926 if status and allowed_to_change_status else None),
931 if status and allowed_to_change_status else None),
927 status_change_type=(status
932 status_change_type=(status
928 if status and allowed_to_change_status else None),
933 if status and allowed_to_change_status else None),
929 closing_pr=close_pr,
934 closing_pr=close_pr,
930 comment_type=comment_type,
935 comment_type=comment_type,
931 resolves_comment_id=resolves_comment_id
936 resolves_comment_id=resolves_comment_id
932 )
937 )
933
938
934 if allowed_to_change_status:
939 if allowed_to_change_status:
935 old_calculated_status = pull_request.calculated_review_status()
940 old_calculated_status = pull_request.calculated_review_status()
936 # get status if set !
941 # get status if set !
937 if status:
942 if status:
938 ChangesetStatusModel().set_status(
943 ChangesetStatusModel().set_status(
939 c.rhodecode_db_repo.repo_id,
944 c.rhodecode_db_repo.repo_id,
940 status,
945 status,
941 c.rhodecode_user.user_id,
946 c.rhodecode_user.user_id,
942 comm,
947 comm,
943 pull_request=pull_request_id
948 pull_request=pull_request_id
944 )
949 )
945
950
946 Session().flush()
951 Session().flush()
947 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
952 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
948 # we now calculate the status of pull request, and based on that
953 # we now calculate the status of pull request, and based on that
949 # calculation we set the commits status
954 # calculation we set the commits status
950 calculated_status = pull_request.calculated_review_status()
955 calculated_status = pull_request.calculated_review_status()
951 if old_calculated_status != calculated_status:
956 if old_calculated_status != calculated_status:
952 PullRequestModel()._trigger_pull_request_hook(
957 PullRequestModel()._trigger_pull_request_hook(
953 pull_request, c.rhodecode_user, 'review_status_change')
958 pull_request, c.rhodecode_user, 'review_status_change')
954
959
955 calculated_status_lbl = ChangesetStatus.get_status_lbl(
960 calculated_status_lbl = ChangesetStatus.get_status_lbl(
956 calculated_status)
961 calculated_status)
957
962
958 if close_pr:
963 if close_pr:
959 status_completed = (
964 status_completed = (
960 calculated_status in [ChangesetStatus.STATUS_APPROVED,
965 calculated_status in [ChangesetStatus.STATUS_APPROVED,
961 ChangesetStatus.STATUS_REJECTED])
966 ChangesetStatus.STATUS_REJECTED])
962 if close_pull_request or status_completed:
967 if close_pull_request or status_completed:
963 PullRequestModel().close_pull_request(
968 PullRequestModel().close_pull_request(
964 pull_request_id, c.rhodecode_user)
969 pull_request_id, c.rhodecode_user)
965 else:
970 else:
966 h.flash(_('Closing pull request on other statuses than '
971 h.flash(_('Closing pull request on other statuses than '
967 'rejected or approved is forbidden. '
972 'rejected or approved is forbidden. '
968 'Calculated status from all reviewers '
973 'Calculated status from all reviewers '
969 'is currently: %s') % calculated_status_lbl,
974 'is currently: %s') % calculated_status_lbl,
970 category='warning')
975 category='warning')
971
976
972 Session().commit()
977 Session().commit()
973
978
974 if not request.is_xhr:
979 if not request.is_xhr:
975 return redirect(h.url('pullrequest_show', repo_name=repo_name,
980 return redirect(h.url('pullrequest_show', repo_name=repo_name,
976 pull_request_id=pull_request_id))
981 pull_request_id=pull_request_id))
977
982
978 data = {
983 data = {
979 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
984 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
980 }
985 }
981 if comm:
986 if comm:
982 c.co = comm
987 c.co = comm
983 c.inline_comment = True if comm.line_no else False
988 c.inline_comment = True if comm.line_no else False
984 data.update(comm.get_dict())
989 data.update(comm.get_dict())
985 data.update({'rendered_text':
990 data.update({'rendered_text':
986 render('changeset/changeset_comment_block.mako')})
991 render('changeset/changeset_comment_block.mako')})
987
992
988 return data
993 return data
989
994
990 @LoginRequired()
995 @LoginRequired()
991 @NotAnonymous()
996 @NotAnonymous()
992 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
997 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
993 'repository.admin')
998 'repository.admin')
994 @auth.CSRFRequired()
999 @auth.CSRFRequired()
995 @jsonify
1000 @jsonify
996 def delete_comment(self, repo_name, comment_id):
1001 def delete_comment(self, repo_name, comment_id):
997 return self._delete_comment(comment_id)
1002 return self._delete_comment(comment_id)
998
1003
999 def _delete_comment(self, comment_id):
1004 def _delete_comment(self, comment_id):
1000 comment_id = safe_int(comment_id)
1005 comment_id = safe_int(comment_id)
1001 co = ChangesetComment.get_or_404(comment_id)
1006 co = ChangesetComment.get_or_404(comment_id)
1002 if co.pull_request.is_closed():
1007 if co.pull_request.is_closed():
1003 # don't allow deleting comments on closed pull request
1008 # don't allow deleting comments on closed pull request
1004 raise HTTPForbidden()
1009 raise HTTPForbidden()
1005
1010
1006 is_owner = co.author.user_id == c.rhodecode_user.user_id
1011 is_owner = co.author.user_id == c.rhodecode_user.user_id
1007 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1012 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1008 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1013 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1009 old_calculated_status = co.pull_request.calculated_review_status()
1014 old_calculated_status = co.pull_request.calculated_review_status()
1010 CommentsModel().delete(comment=co)
1015 CommentsModel().delete(comment=co)
1011 Session().commit()
1016 Session().commit()
1012 calculated_status = co.pull_request.calculated_review_status()
1017 calculated_status = co.pull_request.calculated_review_status()
1013 if old_calculated_status != calculated_status:
1018 if old_calculated_status != calculated_status:
1014 PullRequestModel()._trigger_pull_request_hook(
1019 PullRequestModel()._trigger_pull_request_hook(
1015 co.pull_request, c.rhodecode_user, 'review_status_change')
1020 co.pull_request, c.rhodecode_user, 'review_status_change')
1016 return True
1021 return True
1017 else:
1022 else:
1018 raise HTTPForbidden()
1023 raise HTTPForbidden()
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,600 +1,615 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 var prButtonLockChecks = {
20 var prButtonLockChecks = {
21 'compare': false,
21 'compare': false,
22 'reviewers': false
22 'reviewers': false
23 };
23 };
24
24
25 /**
25 /**
26 * lock button until all checks and loads are made. E.g reviewer calculation
26 * lock button until all checks and loads are made. E.g reviewer calculation
27 * should prevent from submitting a PR
27 * should prevent from submitting a PR
28 * @param lockEnabled
28 * @param lockEnabled
29 * @param msg
29 * @param msg
30 * @param scope
30 * @param scope
31 */
31 */
32 var prButtonLock = function(lockEnabled, msg, scope) {
32 var prButtonLock = function(lockEnabled, msg, scope) {
33 scope = scope || 'all';
33 scope = scope || 'all';
34 if (scope == 'all'){
34 if (scope == 'all'){
35 prButtonLockChecks['compare'] = !lockEnabled;
35 prButtonLockChecks['compare'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 } else if (scope == 'compare') {
37 } else if (scope == 'compare') {
38 prButtonLockChecks['compare'] = !lockEnabled;
38 prButtonLockChecks['compare'] = !lockEnabled;
39 } else if (scope == 'reviewers'){
39 } else if (scope == 'reviewers'){
40 prButtonLockChecks['reviewers'] = !lockEnabled;
40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 }
41 }
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 if (lockEnabled) {
43 if (lockEnabled) {
44 $('#save').attr('disabled', 'disabled');
44 $('#save').attr('disabled', 'disabled');
45 }
45 }
46 else if (checksMeet) {
46 else if (checksMeet) {
47 $('#save').removeAttr('disabled');
47 $('#save').removeAttr('disabled');
48 }
48 }
49
49
50 if (msg) {
50 if (msg) {
51 $('#pr_open_message').html(msg);
51 $('#pr_open_message').html(msg);
52 }
52 }
53 };
53 };
54
54
55
55
56 /**
56 /**
57 Generate Title and Description for a PullRequest.
57 Generate Title and Description for a PullRequest.
58 In case of 1 commits, the title and description is that one commit
58 In case of 1 commits, the title and description is that one commit
59 in case of multiple commits, we iterate on them with max N number of commits,
59 in case of multiple commits, we iterate on them with max N number of commits,
60 and build description in a form
60 and build description in a form
61 - commitN
61 - commitN
62 - commitN+1
62 - commitN+1
63 ...
63 ...
64
64
65 Title is then constructed from branch names, or other references,
65 Title is then constructed from branch names, or other references,
66 replacing '-' and '_' into spaces
66 replacing '-' and '_' into spaces
67
67
68 * @param sourceRef
68 * @param sourceRef
69 * @param elements
69 * @param elements
70 * @param limit
70 * @param limit
71 * @returns {*[]}
71 * @returns {*[]}
72 */
72 */
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 var title = '';
74 var title = '';
75 var desc = '';
75 var desc = '';
76
76
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 });
80 });
81 // only 1 commit, use commit message as title
81 // only 1 commit, use commit message as title
82 if (elements.length === 1) {
82 if (elements.length === 1) {
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 }
84 }
85 else {
85 else {
86 // use reference name
86 // use reference name
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 }
88 }
89
89
90 return [title, desc]
90 return [title, desc]
91 };
91 };
92
92
93
93
94
94
95 ReviewersController = function () {
95 ReviewersController = function () {
96 var self = this;
96 var self = this;
97 this.$reviewRulesContainer = $('#review_rules');
97 this.$reviewRulesContainer = $('#review_rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 this.forbidReviewUsers = undefined;
99 this.forbidReviewUsers = undefined;
100 this.$reviewMembers = $('#review_members');
100 this.$reviewMembers = $('#review_members');
101 this.currentRequest = null;
101 this.currentRequest = null;
102
102
103 this.defaultForbidReviewUsers = function() {
103 this.defaultForbidReviewUsers = function() {
104 return [
104 return [
105 {'username': 'default',
105 {'username': 'default',
106 'user_id': templateContext.default_user.user_id}
106 'user_id': templateContext.default_user.user_id}
107 ];
107 ];
108 };
108 };
109
109
110 this.hideReviewRules = function() {
110 this.hideReviewRules = function() {
111 self.$reviewRulesContainer.hide();
111 self.$reviewRulesContainer.hide();
112 };
112 };
113
113
114 this.showReviewRules = function() {
114 this.showReviewRules = function() {
115 self.$reviewRulesContainer.show();
115 self.$reviewRulesContainer.show();
116 };
116 };
117
117
118 this.addRule = function(ruleText) {
118 this.addRule = function(ruleText) {
119 self.showReviewRules();
119 self.showReviewRules();
120 return '<div>- {0}</div>'.format(ruleText)
120 return '<div>- {0}</div>'.format(ruleText)
121 };
121 };
122
122
123 this.loadReviewRules = function(data) {
123 this.loadReviewRules = function(data) {
124 // reset forbidden Users
124 // reset forbidden Users
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126
126
127 // reset state of review rules
127 // reset state of review rules
128 self.$rulesList.html('');
128 self.$rulesList.html('');
129
129
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 // default rule, case for older repo that don't have any rules stored
131 // default rule, case for older repo that don't have any rules stored
132 self.$rulesList.append(
132 self.$rulesList.append(
133 self.addRule(
133 self.addRule(
134 _gettext('All reviewers must vote.'))
134 _gettext('All reviewers must vote.'))
135 );
135 );
136 return self.forbidReviewUsers
136 return self.forbidReviewUsers
137 }
137 }
138
138
139 if (data.rules.voting !== undefined) {
139 if (data.rules.voting !== undefined) {
140 if (data.rules.voting < 0){
140 if (data.rules.voting < 0){
141 self.$rulesList.append(
141 self.$rulesList.append(
142 self.addRule(
142 self.addRule(
143 _gettext('All reviewers must vote.'))
143 _gettext('All reviewers must vote.'))
144 )
144 )
145 } else if (data.rules.voting === 1) {
145 } else if (data.rules.voting === 1) {
146 self.$rulesList.append(
146 self.$rulesList.append(
147 self.addRule(
147 self.addRule(
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 )
149 )
150
150
151 } else {
151 } else {
152 self.$rulesList.append(
152 self.$rulesList.append(
153 self.addRule(
153 self.addRule(
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 )
155 )
156 }
156 }
157 }
157 }
158 if (data.rules.use_code_authors_for_review) {
158 if (data.rules.use_code_authors_for_review) {
159 self.$rulesList.append(
159 self.$rulesList.append(
160 self.addRule(
160 self.addRule(
161 _gettext('Reviewers picked from source code changes.'))
161 _gettext('Reviewers picked from source code changes.'))
162 )
162 )
163 }
163 }
164 if (data.rules.forbid_adding_reviewers) {
164 if (data.rules.forbid_adding_reviewers) {
165 $('#add_reviewer_input').remove();
165 $('#add_reviewer_input').remove();
166 self.$rulesList.append(
166 self.$rulesList.append(
167 self.addRule(
167 self.addRule(
168 _gettext('Adding new reviewers is forbidden.'))
168 _gettext('Adding new reviewers is forbidden.'))
169 )
169 )
170 }
170 }
171 if (data.rules.forbid_author_to_review) {
171 if (data.rules.forbid_author_to_review) {
172 self.forbidReviewUsers.push(data.rules_data.pr_author);
172 self.forbidReviewUsers.push(data.rules_data.pr_author);
173 self.$rulesList.append(
173 self.$rulesList.append(
174 self.addRule(
174 self.addRule(
175 _gettext('Author is not allowed to be a reviewer.'))
175 _gettext('Author is not allowed to be a reviewer.'))
176 )
176 )
177 }
177 }
178 if (data.rules.forbid_commit_author_to_review) {
179
180 if (data.rules_data.forbidden_users) {
181 $.each(data.rules_data.forbidden_users, function(index, member_data) {
182 self.forbidReviewUsers.push(member_data)
183 });
184
185 }
186
187 self.$rulesList.append(
188 self.addRule(
189 _gettext('Commit Authors are not allowed to be a reviewer.'))
190 )
191 }
192
178 return self.forbidReviewUsers
193 return self.forbidReviewUsers
179 };
194 };
180
195
181 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
196 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
182
197
183 if (self.currentRequest) {
198 if (self.currentRequest) {
184 // make sure we cleanup old running requests before triggering this
199 // make sure we cleanup old running requests before triggering this
185 // again
200 // again
186 self.currentRequest.abort();
201 self.currentRequest.abort();
187 }
202 }
188
203
189 $('.calculate-reviewers').show();
204 $('.calculate-reviewers').show();
190 // reset reviewer members
205 // reset reviewer members
191 self.$reviewMembers.empty();
206 self.$reviewMembers.empty();
192
207
193 prButtonLock(true, null, 'reviewers');
208 prButtonLock(true, null, 'reviewers');
194 $('#user').hide(); // hide user autocomplete before load
209 $('#user').hide(); // hide user autocomplete before load
195
210
196 var url = pyroutes.url('repo_default_reviewers_data',
211 var url = pyroutes.url('repo_default_reviewers_data',
197 {
212 {
198 'repo_name': templateContext.repo_name,
213 'repo_name': templateContext.repo_name,
199 'source_repo': sourceRepo,
214 'source_repo': sourceRepo,
200 'source_ref': sourceRef[2],
215 'source_ref': sourceRef[2],
201 'target_repo': targetRepo,
216 'target_repo': targetRepo,
202 'target_ref': targetRef[2]
217 'target_ref': targetRef[2]
203 });
218 });
204
219
205 self.currentRequest = $.get(url)
220 self.currentRequest = $.get(url)
206 .done(function(data) {
221 .done(function(data) {
207 self.currentRequest = null;
222 self.currentRequest = null;
208
223
209 // review rules
224 // review rules
210 self.loadReviewRules(data);
225 self.loadReviewRules(data);
211
226
212 for (var i = 0; i < data.reviewers.length; i++) {
227 for (var i = 0; i < data.reviewers.length; i++) {
213 var reviewer = data.reviewers[i];
228 var reviewer = data.reviewers[i];
214 self.addReviewMember(
229 self.addReviewMember(
215 reviewer.user_id, reviewer.firstname,
230 reviewer.user_id, reviewer.firstname,
216 reviewer.lastname, reviewer.username,
231 reviewer.lastname, reviewer.username,
217 reviewer.gravatar_link, reviewer.reasons,
232 reviewer.gravatar_link, reviewer.reasons,
218 reviewer.mandatory);
233 reviewer.mandatory);
219 }
234 }
220 $('.calculate-reviewers').hide();
235 $('.calculate-reviewers').hide();
221 prButtonLock(false, null, 'reviewers');
236 prButtonLock(false, null, 'reviewers');
222 $('#user').show(); // show user autocomplete after load
237 $('#user').show(); // show user autocomplete after load
223 });
238 });
224 };
239 };
225
240
226 // check those, refactor
241 // check those, refactor
227 this.removeReviewMember = function(reviewer_id, mark_delete) {
242 this.removeReviewMember = function(reviewer_id, mark_delete) {
228 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
243 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
229
244
230 if(typeof(mark_delete) === undefined){
245 if(typeof(mark_delete) === undefined){
231 mark_delete = false;
246 mark_delete = false;
232 }
247 }
233
248
234 if(mark_delete === true){
249 if(mark_delete === true){
235 if (reviewer){
250 if (reviewer){
236 // now delete the input
251 // now delete the input
237 $('#reviewer_{0} input'.format(reviewer_id)).remove();
252 $('#reviewer_{0} input'.format(reviewer_id)).remove();
238 // mark as to-delete
253 // mark as to-delete
239 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
254 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
240 obj.addClass('to-delete');
255 obj.addClass('to-delete');
241 obj.css({"text-decoration":"line-through", "opacity": 0.5});
256 obj.css({"text-decoration":"line-through", "opacity": 0.5});
242 }
257 }
243 }
258 }
244 else{
259 else{
245 $('#reviewer_{0}'.format(reviewer_id)).remove();
260 $('#reviewer_{0}'.format(reviewer_id)).remove();
246 }
261 }
247 };
262 };
248
263
249 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
264 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
250 var members = self.$reviewMembers.get(0);
265 var members = self.$reviewMembers.get(0);
251 var reasons_html = '';
266 var reasons_html = '';
252 var reasons_inputs = '';
267 var reasons_inputs = '';
253 var reasons = reasons || [];
268 var reasons = reasons || [];
254 var mandatory = mandatory || false;
269 var mandatory = mandatory || false;
255
270
256 if (reasons) {
271 if (reasons) {
257 for (var i = 0; i < reasons.length; i++) {
272 for (var i = 0; i < reasons.length; i++) {
258 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
273 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
259 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
274 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
260 }
275 }
261 }
276 }
262 var tmpl = '' +
277 var tmpl = '' +
263 '<li id="reviewer_{2}" class="reviewer_entry">'+
278 '<li id="reviewer_{2}" class="reviewer_entry">'+
264 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
279 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
265 '<div class="reviewer_status">'+
280 '<div class="reviewer_status">'+
266 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
281 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
267 '</div>'+
282 '</div>'+
268 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
283 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
269 '<span class="reviewer_name user">{1}</span>'+
284 '<span class="reviewer_name user">{1}</span>'+
270 reasons_html +
285 reasons_html +
271 '<input type="hidden" name="user_id" value="{2}">'+
286 '<input type="hidden" name="user_id" value="{2}">'+
272 '<input type="hidden" name="__start__" value="reasons:sequence">'+
287 '<input type="hidden" name="__start__" value="reasons:sequence">'+
273 '{3}'+
288 '{3}'+
274 '<input type="hidden" name="__end__" value="reasons:sequence">';
289 '<input type="hidden" name="__end__" value="reasons:sequence">';
275
290
276 if (mandatory) {
291 if (mandatory) {
277 tmpl += ''+
292 tmpl += ''+
278 '<div class="reviewer_member_mandatory_remove">' +
293 '<div class="reviewer_member_mandatory_remove">' +
279 '<i class="icon-remove-sign"></i>'+
294 '<i class="icon-remove-sign"></i>'+
280 '</div>' +
295 '</div>' +
281 '<input type="hidden" name="mandatory" value="true">'+
296 '<input type="hidden" name="mandatory" value="true">'+
282 '<div class="reviewer_member_mandatory">' +
297 '<div class="reviewer_member_mandatory">' +
283 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
298 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
284 '</div>';
299 '</div>';
285
300
286 } else {
301 } else {
287 tmpl += ''+
302 tmpl += ''+
288 '<input type="hidden" name="mandatory" value="false">'+
303 '<input type="hidden" name="mandatory" value="false">'+
289 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
304 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
290 '<i class="icon-remove-sign"></i>'+
305 '<i class="icon-remove-sign"></i>'+
291 '</div>';
306 '</div>';
292 }
307 }
293 // continue template
308 // continue template
294 tmpl += ''+
309 tmpl += ''+
295 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
310 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
296 '</li>' ;
311 '</li>' ;
297
312
298 var displayname = "{0} ({1} {2})".format(
313 var displayname = "{0} ({1} {2})".format(
299 nname, escapeHtml(fname), escapeHtml(lname));
314 nname, escapeHtml(fname), escapeHtml(lname));
300 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
315 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
301 // check if we don't have this ID already in
316 // check if we don't have this ID already in
302 var ids = [];
317 var ids = [];
303 var _els = self.$reviewMembers.find('li').toArray();
318 var _els = self.$reviewMembers.find('li').toArray();
304 for (el in _els){
319 for (el in _els){
305 ids.push(_els[el].id)
320 ids.push(_els[el].id)
306 }
321 }
307
322
308 var userAllowedReview = function(userId) {
323 var userAllowedReview = function(userId) {
309 var allowed = true;
324 var allowed = true;
310 $.each(self.forbidReviewUsers, function(index, member_data) {
325 $.each(self.forbidReviewUsers, function(index, member_data) {
311 if (parseInt(userId) === member_data['user_id']) {
326 if (parseInt(userId) === member_data['user_id']) {
312 allowed = false;
327 allowed = false;
313 return false // breaks the loop
328 return false // breaks the loop
314 }
329 }
315 });
330 });
316 return allowed
331 return allowed
317 };
332 };
318
333
319 var userAllowed = userAllowedReview(id);
334 var userAllowed = userAllowedReview(id);
320 if (!userAllowed){
335 if (!userAllowed){
321 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
336 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
322 }
337 }
323 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
338 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
324
339
325 if(shouldAdd) {
340 if(shouldAdd) {
326 // only add if it's not there
341 // only add if it's not there
327 members.innerHTML += element;
342 members.innerHTML += element;
328 }
343 }
329
344
330 };
345 };
331
346
332 this.updateReviewers = function(repo_name, pull_request_id){
347 this.updateReviewers = function(repo_name, pull_request_id){
333 var postData = '_method=put&' + $('#reviewers input').serialize();
348 var postData = '_method=put&' + $('#reviewers input').serialize();
334 _updatePullRequest(repo_name, pull_request_id, postData);
349 _updatePullRequest(repo_name, pull_request_id, postData);
335 };
350 };
336
351
337 };
352 };
338
353
339
354
340 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
355 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
341 var url = pyroutes.url(
356 var url = pyroutes.url(
342 'pullrequest_update',
357 'pullrequest_update',
343 {"repo_name": repo_name, "pull_request_id": pull_request_id});
358 {"repo_name": repo_name, "pull_request_id": pull_request_id});
344 if (typeof postData === 'string' ) {
359 if (typeof postData === 'string' ) {
345 postData += '&csrf_token=' + CSRF_TOKEN;
360 postData += '&csrf_token=' + CSRF_TOKEN;
346 } else {
361 } else {
347 postData.csrf_token = CSRF_TOKEN;
362 postData.csrf_token = CSRF_TOKEN;
348 }
363 }
349 var success = function(o) {
364 var success = function(o) {
350 window.location.reload();
365 window.location.reload();
351 };
366 };
352 ajaxPOST(url, postData, success);
367 ajaxPOST(url, postData, success);
353 };
368 };
354
369
355 /**
370 /**
356 * PULL REQUEST reject & close
371 * PULL REQUEST reject & close
357 */
372 */
358 var closePullRequest = function(repo_name, pull_request_id) {
373 var closePullRequest = function(repo_name, pull_request_id) {
359 var postData = {
374 var postData = {
360 '_method': 'put',
375 '_method': 'put',
361 'close_pull_request': true};
376 'close_pull_request': true};
362 _updatePullRequest(repo_name, pull_request_id, postData);
377 _updatePullRequest(repo_name, pull_request_id, postData);
363 };
378 };
364
379
365 /**
380 /**
366 * PULL REQUEST update commits
381 * PULL REQUEST update commits
367 */
382 */
368 var updateCommits = function(repo_name, pull_request_id) {
383 var updateCommits = function(repo_name, pull_request_id) {
369 var postData = {
384 var postData = {
370 '_method': 'put',
385 '_method': 'put',
371 'update_commits': true};
386 'update_commits': true};
372 _updatePullRequest(repo_name, pull_request_id, postData);
387 _updatePullRequest(repo_name, pull_request_id, postData);
373 };
388 };
374
389
375
390
376 /**
391 /**
377 * PULL REQUEST edit info
392 * PULL REQUEST edit info
378 */
393 */
379 var editPullRequest = function(repo_name, pull_request_id, title, description) {
394 var editPullRequest = function(repo_name, pull_request_id, title, description) {
380 var url = pyroutes.url(
395 var url = pyroutes.url(
381 'pullrequest_update',
396 'pullrequest_update',
382 {"repo_name": repo_name, "pull_request_id": pull_request_id});
397 {"repo_name": repo_name, "pull_request_id": pull_request_id});
383
398
384 var postData = {
399 var postData = {
385 '_method': 'put',
400 '_method': 'put',
386 'title': title,
401 'title': title,
387 'description': description,
402 'description': description,
388 'edit_pull_request': true,
403 'edit_pull_request': true,
389 'csrf_token': CSRF_TOKEN
404 'csrf_token': CSRF_TOKEN
390 };
405 };
391 var success = function(o) {
406 var success = function(o) {
392 window.location.reload();
407 window.location.reload();
393 };
408 };
394 ajaxPOST(url, postData, success);
409 ajaxPOST(url, postData, success);
395 };
410 };
396
411
397 var initPullRequestsCodeMirror = function (textAreaId) {
412 var initPullRequestsCodeMirror = function (textAreaId) {
398 var ta = $(textAreaId).get(0);
413 var ta = $(textAreaId).get(0);
399 var initialHeight = '100px';
414 var initialHeight = '100px';
400
415
401 // default options
416 // default options
402 var codeMirrorOptions = {
417 var codeMirrorOptions = {
403 mode: "text",
418 mode: "text",
404 lineNumbers: false,
419 lineNumbers: false,
405 indentUnit: 4,
420 indentUnit: 4,
406 theme: 'rc-input'
421 theme: 'rc-input'
407 };
422 };
408
423
409 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
424 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
410 // marker for manually set description
425 // marker for manually set description
411 codeMirrorInstance._userDefinedDesc = false;
426 codeMirrorInstance._userDefinedDesc = false;
412 codeMirrorInstance.setSize(null, initialHeight);
427 codeMirrorInstance.setSize(null, initialHeight);
413 codeMirrorInstance.on("change", function(instance, changeObj) {
428 codeMirrorInstance.on("change", function(instance, changeObj) {
414 var height = initialHeight;
429 var height = initialHeight;
415 var lines = instance.lineCount();
430 var lines = instance.lineCount();
416 if (lines > 6 && lines < 20) {
431 if (lines > 6 && lines < 20) {
417 height = "auto"
432 height = "auto"
418 }
433 }
419 else if (lines >= 20) {
434 else if (lines >= 20) {
420 height = 20 * 15;
435 height = 20 * 15;
421 }
436 }
422 instance.setSize(null, height);
437 instance.setSize(null, height);
423
438
424 // detect if the change was trigger by auto desc, or user input
439 // detect if the change was trigger by auto desc, or user input
425 changeOrigin = changeObj.origin;
440 changeOrigin = changeObj.origin;
426
441
427 if (changeOrigin === "setValue") {
442 if (changeOrigin === "setValue") {
428 cmLog.debug('Change triggered by setValue');
443 cmLog.debug('Change triggered by setValue');
429 }
444 }
430 else {
445 else {
431 cmLog.debug('user triggered change !');
446 cmLog.debug('user triggered change !');
432 // set special marker to indicate user has created an input.
447 // set special marker to indicate user has created an input.
433 instance._userDefinedDesc = true;
448 instance._userDefinedDesc = true;
434 }
449 }
435
450
436 });
451 });
437
452
438 return codeMirrorInstance
453 return codeMirrorInstance
439 };
454 };
440
455
441 /**
456 /**
442 * Reviewer autocomplete
457 * Reviewer autocomplete
443 */
458 */
444 var ReviewerAutoComplete = function(inputId) {
459 var ReviewerAutoComplete = function(inputId) {
445 $(inputId).autocomplete({
460 $(inputId).autocomplete({
446 serviceUrl: pyroutes.url('user_autocomplete_data'),
461 serviceUrl: pyroutes.url('user_autocomplete_data'),
447 minChars:2,
462 minChars:2,
448 maxHeight:400,
463 maxHeight:400,
449 deferRequestBy: 300, //miliseconds
464 deferRequestBy: 300, //miliseconds
450 showNoSuggestionNotice: true,
465 showNoSuggestionNotice: true,
451 tabDisabled: true,
466 tabDisabled: true,
452 autoSelectFirst: true,
467 autoSelectFirst: true,
453 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
468 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
454 formatResult: autocompleteFormatResult,
469 formatResult: autocompleteFormatResult,
455 lookupFilter: autocompleteFilterResult,
470 lookupFilter: autocompleteFilterResult,
456 onSelect: function(element, data) {
471 onSelect: function(element, data) {
457
472
458 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
473 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
459 if (data.value_type == 'user_group') {
474 if (data.value_type == 'user_group') {
460 reasons.push(_gettext('member of "{0}"').format(data.value_display));
475 reasons.push(_gettext('member of "{0}"').format(data.value_display));
461
476
462 $.each(data.members, function(index, member_data) {
477 $.each(data.members, function(index, member_data) {
463 reviewersController.addReviewMember(
478 reviewersController.addReviewMember(
464 member_data.id, member_data.first_name, member_data.last_name,
479 member_data.id, member_data.first_name, member_data.last_name,
465 member_data.username, member_data.icon_link, reasons);
480 member_data.username, member_data.icon_link, reasons);
466 })
481 })
467
482
468 } else {
483 } else {
469 reviewersController.addReviewMember(
484 reviewersController.addReviewMember(
470 data.id, data.first_name, data.last_name,
485 data.id, data.first_name, data.last_name,
471 data.username, data.icon_link, reasons);
486 data.username, data.icon_link, reasons);
472 }
487 }
473
488
474 $(inputId).val('');
489 $(inputId).val('');
475 }
490 }
476 });
491 });
477 };
492 };
478
493
479
494
480 VersionController = function () {
495 VersionController = function () {
481 var self = this;
496 var self = this;
482 this.$verSource = $('input[name=ver_source]');
497 this.$verSource = $('input[name=ver_source]');
483 this.$verTarget = $('input[name=ver_target]');
498 this.$verTarget = $('input[name=ver_target]');
484 this.$showVersionDiff = $('#show-version-diff');
499 this.$showVersionDiff = $('#show-version-diff');
485
500
486 this.adjustRadioSelectors = function (curNode) {
501 this.adjustRadioSelectors = function (curNode) {
487 var getVal = function (item) {
502 var getVal = function (item) {
488 if (item == 'latest') {
503 if (item == 'latest') {
489 return Number.MAX_SAFE_INTEGER
504 return Number.MAX_SAFE_INTEGER
490 }
505 }
491 else {
506 else {
492 return parseInt(item)
507 return parseInt(item)
493 }
508 }
494 };
509 };
495
510
496 var curVal = getVal($(curNode).val());
511 var curVal = getVal($(curNode).val());
497 var cleared = false;
512 var cleared = false;
498
513
499 $.each(self.$verSource, function (index, value) {
514 $.each(self.$verSource, function (index, value) {
500 var elVal = getVal($(value).val());
515 var elVal = getVal($(value).val());
501
516
502 if (elVal > curVal) {
517 if (elVal > curVal) {
503 if ($(value).is(':checked')) {
518 if ($(value).is(':checked')) {
504 cleared = true;
519 cleared = true;
505 }
520 }
506 $(value).attr('disabled', 'disabled');
521 $(value).attr('disabled', 'disabled');
507 $(value).removeAttr('checked');
522 $(value).removeAttr('checked');
508 $(value).css({'opacity': 0.1});
523 $(value).css({'opacity': 0.1});
509 }
524 }
510 else {
525 else {
511 $(value).css({'opacity': 1});
526 $(value).css({'opacity': 1});
512 $(value).removeAttr('disabled');
527 $(value).removeAttr('disabled');
513 }
528 }
514 });
529 });
515
530
516 if (cleared) {
531 if (cleared) {
517 // if we unchecked an active, set the next one to same loc.
532 // if we unchecked an active, set the next one to same loc.
518 $(this.$verSource).filter('[value={0}]'.format(
533 $(this.$verSource).filter('[value={0}]'.format(
519 curVal)).attr('checked', 'checked');
534 curVal)).attr('checked', 'checked');
520 }
535 }
521
536
522 self.setLockAction(false,
537 self.setLockAction(false,
523 $(curNode).data('verPos'),
538 $(curNode).data('verPos'),
524 $(this.$verSource).filter(':checked').data('verPos')
539 $(this.$verSource).filter(':checked').data('verPos')
525 );
540 );
526 };
541 };
527
542
528
543
529 this.attachVersionListener = function () {
544 this.attachVersionListener = function () {
530 self.$verTarget.change(function (e) {
545 self.$verTarget.change(function (e) {
531 self.adjustRadioSelectors(this)
546 self.adjustRadioSelectors(this)
532 });
547 });
533 self.$verSource.change(function (e) {
548 self.$verSource.change(function (e) {
534 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
549 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
535 });
550 });
536 };
551 };
537
552
538 this.init = function () {
553 this.init = function () {
539
554
540 var curNode = self.$verTarget.filter(':checked');
555 var curNode = self.$verTarget.filter(':checked');
541 self.adjustRadioSelectors(curNode);
556 self.adjustRadioSelectors(curNode);
542 self.setLockAction(true);
557 self.setLockAction(true);
543 self.attachVersionListener();
558 self.attachVersionListener();
544
559
545 };
560 };
546
561
547 this.setLockAction = function (state, selectedVersion, otherVersion) {
562 this.setLockAction = function (state, selectedVersion, otherVersion) {
548 var $showVersionDiff = this.$showVersionDiff;
563 var $showVersionDiff = this.$showVersionDiff;
549
564
550 if (state) {
565 if (state) {
551 $showVersionDiff.attr('disabled', 'disabled');
566 $showVersionDiff.attr('disabled', 'disabled');
552 $showVersionDiff.addClass('disabled');
567 $showVersionDiff.addClass('disabled');
553 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
568 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
554 }
569 }
555 else {
570 else {
556 $showVersionDiff.removeAttr('disabled');
571 $showVersionDiff.removeAttr('disabled');
557 $showVersionDiff.removeClass('disabled');
572 $showVersionDiff.removeClass('disabled');
558
573
559 if (selectedVersion == otherVersion) {
574 if (selectedVersion == otherVersion) {
560 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
575 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
561 } else {
576 } else {
562 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
577 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
563 }
578 }
564 }
579 }
565
580
566 };
581 };
567
582
568 this.showVersionDiff = function () {
583 this.showVersionDiff = function () {
569 var target = self.$verTarget.filter(':checked');
584 var target = self.$verTarget.filter(':checked');
570 var source = self.$verSource.filter(':checked');
585 var source = self.$verSource.filter(':checked');
571
586
572 if (target.val() && source.val()) {
587 if (target.val() && source.val()) {
573 var params = {
588 var params = {
574 'pull_request_id': templateContext.pull_request_data.pull_request_id,
589 'pull_request_id': templateContext.pull_request_data.pull_request_id,
575 'repo_name': templateContext.repo_name,
590 'repo_name': templateContext.repo_name,
576 'version': target.val(),
591 'version': target.val(),
577 'from_version': source.val()
592 'from_version': source.val()
578 };
593 };
579 window.location = pyroutes.url('pullrequest_show', params)
594 window.location = pyroutes.url('pullrequest_show', params)
580 }
595 }
581
596
582 return false;
597 return false;
583 };
598 };
584
599
585 this.toggleVersionView = function (elem) {
600 this.toggleVersionView = function (elem) {
586
601
587 if (this.$showVersionDiff.is(':visible')) {
602 if (this.$showVersionDiff.is(':visible')) {
588 $('.version-pr').hide();
603 $('.version-pr').hide();
589 this.$showVersionDiff.hide();
604 this.$showVersionDiff.hide();
590 $(elem).html($(elem).data('toggleOn'))
605 $(elem).html($(elem).data('toggleOn'))
591 } else {
606 } else {
592 $('.version-pr').show();
607 $('.version-pr').show();
593 this.$showVersionDiff.show();
608 this.$showVersionDiff.show();
594 $(elem).html($(elem).data('toggleOff'))
609 $(elem).html($(elem).data('toggleOff'))
595 }
610 }
596
611
597 return false
612 return false
598 }
613 }
599
614
600 }; No newline at end of file
615 };
General Comments 0
You need to be logged in to leave comments. Login now