##// END OF EJS Templates
pull-request: code cleanup...
marcink -
r1979:f2f8f5ce default
parent child Browse files
Show More
@@ -1,179 +1,179 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 from rhodecode.apps.admin.navigation import NavigationRegistry
23 23 from rhodecode.config.routing import ADMIN_PREFIX
24 24 from rhodecode.lib.utils2 import str2bool
25 25
26 26
27 27 def admin_routes(config):
28 28 """
29 29 Admin prefixed routes
30 30 """
31 31
32 32 config.add_route(
33 33 name='admin_audit_logs',
34 34 pattern='/audit_logs')
35 35
36 36 config.add_route(
37 37 name='pull_requests_global_0', # backward compat
38 pattern='/pull_requests/{pull_request_id:[0-9]+}')
38 pattern='/pull_requests/{pull_request_id:\d+}')
39 39 config.add_route(
40 40 name='pull_requests_global_1', # backward compat
41 pattern='/pull-requests/{pull_request_id:[0-9]+}')
41 pattern='/pull-requests/{pull_request_id:\d+}')
42 42 config.add_route(
43 43 name='pull_requests_global',
44 pattern='/pull-request/{pull_request_id:[0-9]+}')
44 pattern='/pull-request/{pull_request_id:\d+}')
45 45
46 46 config.add_route(
47 47 name='admin_settings_open_source',
48 48 pattern='/settings/open_source')
49 49 config.add_route(
50 50 name='admin_settings_vcs_svn_generate_cfg',
51 51 pattern='/settings/vcs/svn_generate_cfg')
52 52
53 53 config.add_route(
54 54 name='admin_settings_system',
55 55 pattern='/settings/system')
56 56 config.add_route(
57 57 name='admin_settings_system_update',
58 58 pattern='/settings/system/updates')
59 59
60 60 config.add_route(
61 61 name='admin_settings_sessions',
62 62 pattern='/settings/sessions')
63 63 config.add_route(
64 64 name='admin_settings_sessions_cleanup',
65 65 pattern='/settings/sessions/cleanup')
66 66
67 67 config.add_route(
68 68 name='admin_settings_process_management',
69 69 pattern='/settings/process_management')
70 70 config.add_route(
71 71 name='admin_settings_process_management_signal',
72 72 pattern='/settings/process_management/signal')
73 73
74 74 # global permissions
75 75
76 76 config.add_route(
77 77 name='admin_permissions_application',
78 78 pattern='/permissions/application')
79 79 config.add_route(
80 80 name='admin_permissions_application_update',
81 81 pattern='/permissions/application/update')
82 82
83 83 config.add_route(
84 84 name='admin_permissions_global',
85 85 pattern='/permissions/global')
86 86 config.add_route(
87 87 name='admin_permissions_global_update',
88 88 pattern='/permissions/global/update')
89 89
90 90 config.add_route(
91 91 name='admin_permissions_object',
92 92 pattern='/permissions/object')
93 93 config.add_route(
94 94 name='admin_permissions_object_update',
95 95 pattern='/permissions/object/update')
96 96
97 97 config.add_route(
98 98 name='admin_permissions_ips',
99 99 pattern='/permissions/ips')
100 100
101 101 config.add_route(
102 102 name='admin_permissions_overview',
103 103 pattern='/permissions/overview')
104 104
105 105 config.add_route(
106 106 name='admin_permissions_auth_token_access',
107 107 pattern='/permissions/auth_token_access')
108 108
109 109 # users admin
110 110 config.add_route(
111 111 name='users',
112 112 pattern='/users')
113 113
114 114 config.add_route(
115 115 name='users_data',
116 116 pattern='/users_data')
117 117
118 118 # user auth tokens
119 119 config.add_route(
120 120 name='edit_user_auth_tokens',
121 121 pattern='/users/{user_id:\d+}/edit/auth_tokens')
122 122 config.add_route(
123 123 name='edit_user_auth_tokens_add',
124 124 pattern='/users/{user_id:\d+}/edit/auth_tokens/new')
125 125 config.add_route(
126 126 name='edit_user_auth_tokens_delete',
127 127 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete')
128 128
129 129 # user emails
130 130 config.add_route(
131 131 name='edit_user_emails',
132 132 pattern='/users/{user_id:\d+}/edit/emails')
133 133 config.add_route(
134 134 name='edit_user_emails_add',
135 135 pattern='/users/{user_id:\d+}/edit/emails/new')
136 136 config.add_route(
137 137 name='edit_user_emails_delete',
138 138 pattern='/users/{user_id:\d+}/edit/emails/delete')
139 139
140 140 # user IPs
141 141 config.add_route(
142 142 name='edit_user_ips',
143 143 pattern='/users/{user_id:\d+}/edit/ips')
144 144 config.add_route(
145 145 name='edit_user_ips_add',
146 146 pattern='/users/{user_id:\d+}/edit/ips/new')
147 147 config.add_route(
148 148 name='edit_user_ips_delete',
149 149 pattern='/users/{user_id:\d+}/edit/ips/delete')
150 150
151 151 # user groups management
152 152 config.add_route(
153 153 name='edit_user_groups_management',
154 154 pattern='/users/{user_id:\d+}/edit/groups_management')
155 155
156 156 config.add_route(
157 157 name='edit_user_groups_management_updates',
158 158 pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates')
159 159
160 160 # user audit logs
161 161 config.add_route(
162 162 name='edit_user_audit_logs',
163 163 pattern='/users/{user_id:\d+}/edit/audit')
164 164
165 165
166 166 def includeme(config):
167 167 settings = config.get_settings()
168 168
169 169 # Create admin navigation registry and add it to the pyramid registry.
170 170 labs_active = str2bool(settings.get('labs_settings_active', False))
171 171 navigation_registry = NavigationRegistry(labs_active=labs_active)
172 172 config.registry.registerUtility(navigation_registry)
173 173
174 174 # main admin routes
175 175 config.add_route(name='admin_home', pattern=ADMIN_PREFIX)
176 176 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
177 177
178 178 # Scan module for configuration decorators.
179 179 config.scan()
@@ -1,63 +1,65 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24 from pyramid.httpexceptions import HTTPFound
25 25 from pyramid.view import view_config
26 26
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 30 from rhodecode.model.db import PullRequest
31 31
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class AdminMainView(BaseAppView):
37 37
38 38 @LoginRequired()
39 39 @HasPermissionAllDecorator('hg.admin')
40 40 @view_config(
41 41 route_name='admin_home', request_method='GET')
42 42 def admin_main(self):
43 43 # redirect _admin to audit logs...
44 44 raise HTTPFound(h.route_path('admin_audit_logs'))
45 45
46 46 @LoginRequired()
47 47 @view_config(route_name='pull_requests_global_0', request_method='GET')
48 48 @view_config(route_name='pull_requests_global_1', request_method='GET')
49 49 @view_config(route_name='pull_requests_global', request_method='GET')
50 50 def pull_requests(self):
51 51 """
52 52 Global redirect for Pull Requests
53 53
54 54 :param pull_request_id: id of pull requests in the system
55 55 """
56 56
57 pull_request_id = self.request.matchdict.get('pull_request_id')
58 pull_request = PullRequest.get_or_404(pull_request_id)
57 pull_request = PullRequest.get_or_404(
58 self.request.matchdict['pull_request_id'])
59 pull_request_id = pull_request.pull_request_id
60
59 61 repo_name = pull_request.target_repo.repo_name
60 62
61 63 raise HTTPFound(
62 64 h.route_path('pullrequest_show', repo_name=repo_name,
63 65 pull_request_id=pull_request_id))
@@ -1,150 +1,150 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.lib.utils2 import safe_unicode, safe_str
24 24 from rhodecode.model.db import Repository
25 25 from rhodecode.model.repo import RepoModel
26 26 from rhodecode.tests import (
27 27 HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator)
28 28 from rhodecode.tests.fixture import Fixture
29 29 from rhodecode.tests.utils import repo_on_filesystem
30 30
31 31 fixture = Fixture()
32 32
33 33
34 34 def route_path(name, params=None, **kwargs):
35 35 import urllib
36 36
37 37 base_url = {
38 38 'repo_summary_explicit': '/{repo_name}/summary',
39 39 'repo_summary': '/{repo_name}',
40 40 'edit_repo_advanced': '/{repo_name}/settings/advanced',
41 41 'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete',
42 42 'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork',
43 43 'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking',
44 44 'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal',
45 45
46 46 }[name].format(**kwargs)
47 47
48 48 if params:
49 49 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
50 50 return base_url
51 51
52 52
53 53 @pytest.mark.usefixtures('autologin_user', 'app')
54 54 class TestAdminRepoSettingsAdvanced(object):
55 55
56 56 def test_set_repo_fork_has_no_self_id(self, autologin_user, backend):
57 57 repo = backend.repo
58 58 response = self.app.get(
59 59 route_path('edit_repo_advanced', repo_name=backend.repo_name))
60 60 opt = """<option value="%s">vcs_test_git</option>""" % repo.repo_id
61 61 response.mustcontain(no=[opt])
62 62
63 63 def test_set_fork_of_target_repo(
64 64 self, autologin_user, backend, csrf_token):
65 65 target_repo = 'target_%s' % backend.alias
66 66 fixture.create_repo(target_repo, repo_type=backend.alias)
67 67 repo2 = Repository.get_by_repo_name(target_repo)
68 68 response = self.app.post(
69 69 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
70 70 params={'id_fork_of': repo2.repo_id,
71 71 'csrf_token': csrf_token})
72 72 repo = Repository.get_by_repo_name(backend.repo_name)
73 73 repo2 = Repository.get_by_repo_name(target_repo)
74 74 assert_session_flash(
75 75 response,
76 76 'Marked repo %s as fork of %s' % (repo.repo_name, repo2.repo_name))
77 77
78 78 assert repo.fork == repo2
79 79 response = response.follow()
80 80 # check if given repo is selected
81 81
82 82 opt = 'This repository is a fork of <a href="%s">%s</a>' % (
83 83 route_path('repo_summary', repo_name=repo2.repo_name),
84 84 repo2.repo_name)
85 85
86 86 response.mustcontain(opt)
87 87
88 88 fixture.destroy_repo(target_repo, forks='detach')
89 89
90 90 @pytest.mark.backends("hg", "git")
91 91 def test_set_fork_of_other_type_repo(
92 92 self, autologin_user, backend, csrf_token):
93 93 TARGET_REPO_MAP = {
94 94 'git': {
95 95 'type': 'hg',
96 96 'repo_name': HG_REPO},
97 97 'hg': {
98 98 'type': 'git',
99 99 'repo_name': GIT_REPO},
100 100 }
101 101 target_repo = TARGET_REPO_MAP[backend.alias]
102 102
103 103 repo2 = Repository.get_by_repo_name(target_repo['repo_name'])
104 104 response = self.app.post(
105 105 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
106 106 params={'id_fork_of': repo2.repo_id,
107 107 'csrf_token': csrf_token})
108 108 assert_session_flash(
109 109 response,
110 110 'Cannot set repository as fork of repository with other type')
111 111
112 112 def test_set_fork_of_none(self, autologin_user, backend, csrf_token):
113 113 # mark it as None
114 114 response = self.app.post(
115 115 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
116 params={'id_fork_of': None, '_method': 'put',
116 params={'id_fork_of': None,
117 117 'csrf_token': csrf_token})
118 118 assert_session_flash(
119 119 response,
120 120 'Marked repo %s as fork of %s'
121 121 % (backend.repo_name, "Nothing"))
122 122 assert backend.repo.fork is None
123 123
124 124 def test_set_fork_of_same_repo(self, autologin_user, backend, csrf_token):
125 125 repo = Repository.get_by_repo_name(backend.repo_name)
126 126 response = self.app.post(
127 127 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
128 128 params={'id_fork_of': repo.repo_id, 'csrf_token': csrf_token})
129 129 assert_session_flash(
130 130 response, 'An error occurred during this operation')
131 131
132 132 @pytest.mark.parametrize(
133 133 "suffix",
134 134 ['', u'Δ…Δ™Ε‚' , '123'],
135 135 ids=no_newline_id_generator)
136 136 def test_advanced_delete(self, autologin_user, backend, suffix, csrf_token):
137 137 repo = backend.create_repo(name_suffix=suffix)
138 138 repo_name = repo.repo_name
139 139 repo_name_str = safe_str(repo.repo_name)
140 140
141 141 response = self.app.post(
142 142 route_path('edit_repo_advanced_delete', repo_name=repo_name_str),
143 143 params={'csrf_token': csrf_token})
144 144 assert_session_flash(response,
145 145 u'Deleted repository `{}`'.format(repo_name))
146 146 response.follow()
147 147
148 148 # check if repo was deleted from db
149 149 assert RepoModel().get_by_repo_name(repo_name) is None
150 150 assert not repo_on_filesystem(repo_name_str)
@@ -1,1181 +1,1186 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import peppercorn
26 26 from pyramid.httpexceptions import (
27 27 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 28 from pyramid.view import view_config
29 29 from pyramid.renderers import render
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.auth import (
38 38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
39 39 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
40 40 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
41 41 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
42 42 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
43 43 from rhodecode.model.changeset_status import ChangesetStatusModel
44 44 from rhodecode.model.comment import CommentsModel
45 45 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
46 46 ChangesetComment, ChangesetStatus, Repository)
47 47 from rhodecode.model.forms import PullRequestForm
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
50 50 from rhodecode.model.scm import ScmModel
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class RepoPullRequestsView(RepoAppView, DataGridAppView):
56 56
57 57 def load_default_context(self):
58 58 c = self._get_local_tmpl_context(include_app_defaults=True)
59 59 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
60 60 c.repo_info = self.db_repo
61 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 63 self._register_global_c(c)
64 64 return c
65 65
66 66 def _get_pull_requests_list(
67 67 self, repo_name, source, filter_type, opened_by, statuses):
68 68
69 69 draw, start, limit = self._extract_chunk(self.request)
70 70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 71 _render = self.request.get_partial_renderer(
72 72 'data_table/_dt_elements.mako')
73 73
74 74 # pagination
75 75
76 76 if filter_type == 'awaiting_review':
77 77 pull_requests = PullRequestModel().get_awaiting_review(
78 78 repo_name, source=source, opened_by=opened_by,
79 79 statuses=statuses, offset=start, length=limit,
80 80 order_by=order_by, order_dir=order_dir)
81 81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 82 repo_name, source=source, statuses=statuses,
83 83 opened_by=opened_by)
84 84 elif filter_type == 'awaiting_my_review':
85 85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 86 repo_name, source=source, opened_by=opened_by,
87 87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 88 offset=start, length=limit, order_by=order_by,
89 89 order_dir=order_dir)
90 90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 92 statuses=statuses, opened_by=opened_by)
93 93 else:
94 94 pull_requests = PullRequestModel().get_all(
95 95 repo_name, source=source, opened_by=opened_by,
96 96 statuses=statuses, offset=start, length=limit,
97 97 order_by=order_by, order_dir=order_dir)
98 98 pull_requests_total_count = PullRequestModel().count_all(
99 99 repo_name, source=source, statuses=statuses,
100 100 opened_by=opened_by)
101 101
102 102 data = []
103 103 comments_model = CommentsModel()
104 104 for pr in pull_requests:
105 105 comments = comments_model.get_all_comments(
106 106 self.db_repo.repo_id, pull_request=pr)
107 107
108 108 data.append({
109 109 'name': _render('pullrequest_name',
110 110 pr.pull_request_id, pr.target_repo.repo_name),
111 111 'name_raw': pr.pull_request_id,
112 112 'status': _render('pullrequest_status',
113 113 pr.calculated_review_status()),
114 114 'title': _render(
115 115 'pullrequest_title', pr.title, pr.description),
116 116 'description': h.escape(pr.description),
117 117 'updated_on': _render('pullrequest_updated_on',
118 118 h.datetime_to_time(pr.updated_on)),
119 119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 120 'created_on': _render('pullrequest_updated_on',
121 121 h.datetime_to_time(pr.created_on)),
122 122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 123 'author': _render('pullrequest_author',
124 124 pr.author.full_contact, ),
125 125 'author_raw': pr.author.full_name,
126 126 'comments': _render('pullrequest_comments', len(comments)),
127 127 'comments_raw': len(comments),
128 128 'closed': pr.is_closed(),
129 129 })
130 130
131 131 data = ({
132 132 'draw': draw,
133 133 'data': data,
134 134 'recordsTotal': pull_requests_total_count,
135 135 'recordsFiltered': pull_requests_total_count,
136 136 })
137 137 return data
138 138
139 139 @LoginRequired()
140 140 @HasRepoPermissionAnyDecorator(
141 141 'repository.read', 'repository.write', 'repository.admin')
142 142 @view_config(
143 143 route_name='pullrequest_show_all', request_method='GET',
144 144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 145 def pull_request_list(self):
146 146 c = self.load_default_context()
147 147
148 148 req_get = self.request.GET
149 149 c.source = str2bool(req_get.get('source'))
150 150 c.closed = str2bool(req_get.get('closed'))
151 151 c.my = str2bool(req_get.get('my'))
152 152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 154
155 155 c.active = 'open'
156 156 if c.my:
157 157 c.active = 'my'
158 158 if c.closed:
159 159 c.active = 'closed'
160 160 if c.awaiting_review and not c.source:
161 161 c.active = 'awaiting'
162 162 if c.source and not c.awaiting_review:
163 163 c.active = 'source'
164 164 if c.awaiting_my_review:
165 165 c.active = 'awaiting_my'
166 166
167 167 return self._get_template_context(c)
168 168
169 169 @LoginRequired()
170 170 @HasRepoPermissionAnyDecorator(
171 171 'repository.read', 'repository.write', 'repository.admin')
172 172 @view_config(
173 173 route_name='pullrequest_show_all_data', request_method='GET',
174 174 renderer='json_ext', xhr=True)
175 175 def pull_request_list_data(self):
176 176
177 177 # additional filters
178 178 req_get = self.request.GET
179 179 source = str2bool(req_get.get('source'))
180 180 closed = str2bool(req_get.get('closed'))
181 181 my = str2bool(req_get.get('my'))
182 182 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184 184
185 185 filter_type = 'awaiting_review' if awaiting_review \
186 186 else 'awaiting_my_review' if awaiting_my_review \
187 187 else None
188 188
189 189 opened_by = None
190 190 if my:
191 191 opened_by = [self._rhodecode_user.user_id]
192 192
193 193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 194 if closed:
195 195 statuses = [PullRequest.STATUS_CLOSED]
196 196
197 197 data = self._get_pull_requests_list(
198 198 repo_name=self.db_repo_name, source=source,
199 199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200 200
201 201 return data
202 202
203 203 def _get_pr_version(self, pull_request_id, version=None):
204 204 at_version = None
205 205
206 206 if version and version == 'latest':
207 207 pull_request_ver = PullRequest.get(pull_request_id)
208 208 pull_request_obj = pull_request_ver
209 209 _org_pull_request_obj = pull_request_obj
210 210 at_version = 'latest'
211 211 elif version:
212 212 pull_request_ver = PullRequestVersion.get_or_404(version)
213 213 pull_request_obj = pull_request_ver
214 214 _org_pull_request_obj = pull_request_ver.pull_request
215 215 at_version = pull_request_ver.pull_request_version_id
216 216 else:
217 217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 218 pull_request_id)
219 219
220 220 pull_request_display_obj = PullRequest.get_pr_display_object(
221 221 pull_request_obj, _org_pull_request_obj)
222 222
223 223 return _org_pull_request_obj, pull_request_obj, \
224 224 pull_request_display_obj, at_version
225 225
226 226 def _get_diffset(self, source_repo_name, source_repo,
227 227 source_ref_id, target_ref_id,
228 228 target_commit, source_commit, diff_limit, fulldiff,
229 229 file_limit, display_inline_comments):
230 230
231 231 vcs_diff = PullRequestModel().get_diff(
232 232 source_repo, source_ref_id, target_ref_id)
233 233
234 234 diff_processor = diffs.DiffProcessor(
235 235 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 236 file_limit=file_limit, show_full_diff=fulldiff)
237 237
238 238 _parsed = diff_processor.prepare()
239 239
240 240 def _node_getter(commit):
241 241 def get_node(fname):
242 242 try:
243 243 return commit.get_node(fname)
244 244 except NodeDoesNotExistError:
245 245 return None
246 246
247 247 return get_node
248 248
249 249 diffset = codeblocks.DiffSet(
250 250 repo_name=self.db_repo_name,
251 251 source_repo_name=source_repo_name,
252 252 source_node_getter=_node_getter(target_commit),
253 253 target_node_getter=_node_getter(source_commit),
254 254 comments=display_inline_comments
255 255 )
256 256 diffset = diffset.render_patchset(
257 257 _parsed, target_commit.raw_id, source_commit.raw_id)
258 258
259 259 return diffset
260 260
261 261 @LoginRequired()
262 262 @HasRepoPermissionAnyDecorator(
263 263 'repository.read', 'repository.write', 'repository.admin')
264 264 @view_config(
265 265 route_name='pullrequest_show', request_method='GET',
266 266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 267 def pull_request_show(self):
268 pull_request_id = self.request.matchdict.get('pull_request_id')
268 pull_request_id = self.request.matchdict['pull_request_id']
269 269
270 270 c = self.load_default_context()
271 271
272 272 version = self.request.GET.get('version')
273 273 from_version = self.request.GET.get('from_version') or version
274 274 merge_checks = self.request.GET.get('merge_checks')
275 275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276 276
277 277 (pull_request_latest,
278 278 pull_request_at_ver,
279 279 pull_request_display_obj,
280 280 at_version) = self._get_pr_version(
281 281 pull_request_id, version=version)
282 282 pr_closed = pull_request_latest.is_closed()
283 283
284 284 if pr_closed and (version or from_version):
285 285 # not allow to browse versions
286 286 raise HTTPFound(h.route_path(
287 287 'pullrequest_show', repo_name=self.db_repo_name,
288 288 pull_request_id=pull_request_id))
289 289
290 290 versions = pull_request_display_obj.versions()
291 291
292 292 c.at_version = at_version
293 293 c.at_version_num = (at_version
294 294 if at_version and at_version != 'latest'
295 295 else None)
296 296 c.at_version_pos = ChangesetComment.get_index_from_version(
297 297 c.at_version_num, versions)
298 298
299 299 (prev_pull_request_latest,
300 300 prev_pull_request_at_ver,
301 301 prev_pull_request_display_obj,
302 302 prev_at_version) = self._get_pr_version(
303 303 pull_request_id, version=from_version)
304 304
305 305 c.from_version = prev_at_version
306 306 c.from_version_num = (prev_at_version
307 307 if prev_at_version and prev_at_version != 'latest'
308 308 else None)
309 309 c.from_version_pos = ChangesetComment.get_index_from_version(
310 310 c.from_version_num, versions)
311 311
312 312 # define if we're in COMPARE mode or VIEW at version mode
313 313 compare = at_version != prev_at_version
314 314
315 315 # pull_requests repo_name we opened it against
316 316 # ie. target_repo must match
317 317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 318 raise HTTPNotFound()
319 319
320 320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 321 pull_request_at_ver)
322 322
323 323 c.pull_request = pull_request_display_obj
324 324 c.pull_request_latest = pull_request_latest
325 325
326 326 if compare or (at_version and not at_version == 'latest'):
327 327 c.allowed_to_change_status = False
328 328 c.allowed_to_update = False
329 329 c.allowed_to_merge = False
330 330 c.allowed_to_delete = False
331 331 c.allowed_to_comment = False
332 332 c.allowed_to_close = False
333 333 else:
334 334 can_change_status = PullRequestModel().check_user_change_status(
335 335 pull_request_at_ver, self._rhodecode_user)
336 336 c.allowed_to_change_status = can_change_status and not pr_closed
337 337
338 338 c.allowed_to_update = PullRequestModel().check_user_update(
339 339 pull_request_latest, self._rhodecode_user) and not pr_closed
340 340 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 341 pull_request_latest, self._rhodecode_user) and not pr_closed
342 342 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 343 pull_request_latest, self._rhodecode_user) and not pr_closed
344 344 c.allowed_to_comment = not pr_closed
345 345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346 346
347 347 c.forbid_adding_reviewers = False
348 348 c.forbid_author_to_review = False
349 349 c.forbid_commit_author_to_review = False
350 350
351 351 if pull_request_latest.reviewer_data and \
352 352 'rules' in pull_request_latest.reviewer_data:
353 353 rules = pull_request_latest.reviewer_data['rules'] or {}
354 354 try:
355 355 c.forbid_adding_reviewers = rules.get(
356 356 'forbid_adding_reviewers')
357 357 c.forbid_author_to_review = rules.get(
358 358 'forbid_author_to_review')
359 359 c.forbid_commit_author_to_review = rules.get(
360 360 'forbid_commit_author_to_review')
361 361 except Exception:
362 362 pass
363 363
364 364 # check merge capabilities
365 365 _merge_check = MergeCheck.validate(
366 366 pull_request_latest, user=self._rhodecode_user)
367 367 c.pr_merge_errors = _merge_check.error_details
368 368 c.pr_merge_possible = not _merge_check.failed
369 369 c.pr_merge_message = _merge_check.merge_msg
370 370
371 371 c.pull_request_review_status = _merge_check.review_status
372 372 if merge_checks:
373 373 self.request.override_renderer = \
374 374 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
375 375 return self._get_template_context(c)
376 376
377 377 comments_model = CommentsModel()
378 378
379 379 # reviewers and statuses
380 380 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
381 381 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
382 382
383 383 # GENERAL COMMENTS with versions #
384 384 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
385 385 q = q.order_by(ChangesetComment.comment_id.asc())
386 386 general_comments = q
387 387
388 388 # pick comments we want to render at current version
389 389 c.comment_versions = comments_model.aggregate_comments(
390 390 general_comments, versions, c.at_version_num)
391 391 c.comments = c.comment_versions[c.at_version_num]['until']
392 392
393 393 # INLINE COMMENTS with versions #
394 394 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
395 395 q = q.order_by(ChangesetComment.comment_id.asc())
396 396 inline_comments = q
397 397
398 398 c.inline_versions = comments_model.aggregate_comments(
399 399 inline_comments, versions, c.at_version_num, inline=True)
400 400
401 401 # inject latest version
402 402 latest_ver = PullRequest.get_pr_display_object(
403 403 pull_request_latest, pull_request_latest)
404 404
405 405 c.versions = versions + [latest_ver]
406 406
407 407 # if we use version, then do not show later comments
408 408 # than current version
409 409 display_inline_comments = collections.defaultdict(
410 410 lambda: collections.defaultdict(list))
411 411 for co in inline_comments:
412 412 if c.at_version_num:
413 413 # pick comments that are at least UPTO given version, so we
414 414 # don't render comments for higher version
415 415 should_render = co.pull_request_version_id and \
416 416 co.pull_request_version_id <= c.at_version_num
417 417 else:
418 418 # showing all, for 'latest'
419 419 should_render = True
420 420
421 421 if should_render:
422 422 display_inline_comments[co.f_path][co.line_no].append(co)
423 423
424 424 # load diff data into template context, if we use compare mode then
425 425 # diff is calculated based on changes between versions of PR
426 426
427 427 source_repo = pull_request_at_ver.source_repo
428 428 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
429 429
430 430 target_repo = pull_request_at_ver.target_repo
431 431 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
432 432
433 433 if compare:
434 434 # in compare switch the diff base to latest commit from prev version
435 435 target_ref_id = prev_pull_request_display_obj.revisions[0]
436 436
437 437 # despite opening commits for bookmarks/branches/tags, we always
438 438 # convert this to rev to prevent changes after bookmark or branch change
439 439 c.source_ref_type = 'rev'
440 440 c.source_ref = source_ref_id
441 441
442 442 c.target_ref_type = 'rev'
443 443 c.target_ref = target_ref_id
444 444
445 445 c.source_repo = source_repo
446 446 c.target_repo = target_repo
447 447
448 448 c.commit_ranges = []
449 449 source_commit = EmptyCommit()
450 450 target_commit = EmptyCommit()
451 451 c.missing_requirements = False
452 452
453 453 source_scm = source_repo.scm_instance()
454 454 target_scm = target_repo.scm_instance()
455 455
456 456 # try first shadow repo, fallback to regular repo
457 457 try:
458 458 commits_source_repo = pull_request_latest.get_shadow_repo()
459 459 except Exception:
460 460 log.debug('Failed to get shadow repo', exc_info=True)
461 461 commits_source_repo = source_scm
462 462
463 463 c.commits_source_repo = commits_source_repo
464 464 commit_cache = {}
465 465 try:
466 466 pre_load = ["author", "branch", "date", "message"]
467 467 show_revs = pull_request_at_ver.revisions
468 468 for rev in show_revs:
469 469 comm = commits_source_repo.get_commit(
470 470 commit_id=rev, pre_load=pre_load)
471 471 c.commit_ranges.append(comm)
472 472 commit_cache[comm.raw_id] = comm
473 473
474 474 # Order here matters, we first need to get target, and then
475 475 # the source
476 476 target_commit = commits_source_repo.get_commit(
477 477 commit_id=safe_str(target_ref_id))
478 478
479 479 source_commit = commits_source_repo.get_commit(
480 480 commit_id=safe_str(source_ref_id))
481 481
482 482 except CommitDoesNotExistError:
483 483 log.warning(
484 484 'Failed to get commit from `{}` repo'.format(
485 485 commits_source_repo), exc_info=True)
486 486 except RepositoryRequirementError:
487 487 log.warning(
488 488 'Failed to get all required data from repo', exc_info=True)
489 489 c.missing_requirements = True
490 490
491 491 c.ancestor = None # set it to None, to hide it from PR view
492 492
493 493 try:
494 494 ancestor_id = source_scm.get_common_ancestor(
495 495 source_commit.raw_id, target_commit.raw_id, target_scm)
496 496 c.ancestor_commit = source_scm.get_commit(ancestor_id)
497 497 except Exception:
498 498 c.ancestor_commit = None
499 499
500 500 c.statuses = source_repo.statuses(
501 501 [x.raw_id for x in c.commit_ranges])
502 502
503 503 # auto collapse if we have more than limit
504 504 collapse_limit = diffs.DiffProcessor._collapse_commits_over
505 505 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
506 506 c.compare_mode = compare
507 507
508 508 # diff_limit is the old behavior, will cut off the whole diff
509 509 # if the limit is applied otherwise will just hide the
510 510 # big files from the front-end
511 511 diff_limit = c.visual.cut_off_limit_diff
512 512 file_limit = c.visual.cut_off_limit_file
513 513
514 514 c.missing_commits = False
515 515 if (c.missing_requirements
516 516 or isinstance(source_commit, EmptyCommit)
517 517 or source_commit == target_commit):
518 518
519 519 c.missing_commits = True
520 520 else:
521 521
522 522 c.diffset = self._get_diffset(
523 523 c.source_repo.repo_name, commits_source_repo,
524 524 source_ref_id, target_ref_id,
525 525 target_commit, source_commit,
526 526 diff_limit, c.fulldiff, file_limit, display_inline_comments)
527 527
528 528 c.limited_diff = c.diffset.limited_diff
529 529
530 530 # calculate removed files that are bound to comments
531 531 comment_deleted_files = [
532 532 fname for fname in display_inline_comments
533 533 if fname not in c.diffset.file_stats]
534 534
535 535 c.deleted_files_comments = collections.defaultdict(dict)
536 536 for fname, per_line_comments in display_inline_comments.items():
537 537 if fname in comment_deleted_files:
538 538 c.deleted_files_comments[fname]['stats'] = 0
539 539 c.deleted_files_comments[fname]['comments'] = list()
540 540 for lno, comments in per_line_comments.items():
541 541 c.deleted_files_comments[fname]['comments'].extend(
542 542 comments)
543 543
544 544 # this is a hack to properly display links, when creating PR, the
545 545 # compare view and others uses different notation, and
546 546 # compare_commits.mako renders links based on the target_repo.
547 547 # We need to swap that here to generate it properly on the html side
548 548 c.target_repo = c.source_repo
549 549
550 550 c.commit_statuses = ChangesetStatus.STATUSES
551 551
552 552 c.show_version_changes = not pr_closed
553 553 if c.show_version_changes:
554 554 cur_obj = pull_request_at_ver
555 555 prev_obj = prev_pull_request_at_ver
556 556
557 557 old_commit_ids = prev_obj.revisions
558 558 new_commit_ids = cur_obj.revisions
559 559 commit_changes = PullRequestModel()._calculate_commit_id_changes(
560 560 old_commit_ids, new_commit_ids)
561 561 c.commit_changes_summary = commit_changes
562 562
563 563 # calculate the diff for commits between versions
564 564 c.commit_changes = []
565 565 mark = lambda cs, fw: list(
566 566 h.itertools.izip_longest([], cs, fillvalue=fw))
567 567 for c_type, raw_id in mark(commit_changes.added, 'a') \
568 568 + mark(commit_changes.removed, 'r') \
569 569 + mark(commit_changes.common, 'c'):
570 570
571 571 if raw_id in commit_cache:
572 572 commit = commit_cache[raw_id]
573 573 else:
574 574 try:
575 575 commit = commits_source_repo.get_commit(raw_id)
576 576 except CommitDoesNotExistError:
577 577 # in case we fail extracting still use "dummy" commit
578 578 # for display in commit diff
579 579 commit = h.AttributeDict(
580 580 {'raw_id': raw_id,
581 581 'message': 'EMPTY or MISSING COMMIT'})
582 582 c.commit_changes.append([c_type, commit])
583 583
584 584 # current user review statuses for each version
585 585 c.review_versions = {}
586 586 if self._rhodecode_user.user_id in allowed_reviewers:
587 587 for co in general_comments:
588 588 if co.author.user_id == self._rhodecode_user.user_id:
589 589 # each comment has a status change
590 590 status = co.status_change
591 591 if status:
592 592 _ver_pr = status[0].comment.pull_request_version_id
593 593 c.review_versions[_ver_pr] = status[0]
594 594
595 595 return self._get_template_context(c)
596 596
597 597 def assure_not_empty_repo(self):
598 598 _ = self.request.translate
599 599
600 600 try:
601 601 self.db_repo.scm_instance().get_commit()
602 602 except EmptyRepositoryError:
603 603 h.flash(h.literal(_('There are no commits yet')),
604 604 category='warning')
605 605 raise HTTPFound(
606 606 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
607 607
608 608 @LoginRequired()
609 609 @NotAnonymous()
610 610 @HasRepoPermissionAnyDecorator(
611 611 'repository.read', 'repository.write', 'repository.admin')
612 612 @view_config(
613 613 route_name='pullrequest_new', request_method='GET',
614 614 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
615 615 def pull_request_new(self):
616 616 _ = self.request.translate
617 617 c = self.load_default_context()
618 618
619 619 self.assure_not_empty_repo()
620 620 source_repo = self.db_repo
621 621
622 622 commit_id = self.request.GET.get('commit')
623 623 branch_ref = self.request.GET.get('branch')
624 624 bookmark_ref = self.request.GET.get('bookmark')
625 625
626 626 try:
627 627 source_repo_data = PullRequestModel().generate_repo_data(
628 628 source_repo, commit_id=commit_id,
629 629 branch=branch_ref, bookmark=bookmark_ref)
630 630 except CommitDoesNotExistError as e:
631 631 log.exception(e)
632 632 h.flash(_('Commit does not exist'), 'error')
633 633 raise HTTPFound(
634 634 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
635 635
636 636 default_target_repo = source_repo
637 637
638 638 if source_repo.parent:
639 639 parent_vcs_obj = source_repo.parent.scm_instance()
640 640 if parent_vcs_obj and not parent_vcs_obj.is_empty():
641 641 # change default if we have a parent repo
642 642 default_target_repo = source_repo.parent
643 643
644 644 target_repo_data = PullRequestModel().generate_repo_data(
645 645 default_target_repo)
646 646
647 647 selected_source_ref = source_repo_data['refs']['selected_ref']
648 648
649 649 title_source_ref = selected_source_ref.split(':', 2)[1]
650 650 c.default_title = PullRequestModel().generate_pullrequest_title(
651 651 source=source_repo.repo_name,
652 652 source_ref=title_source_ref,
653 653 target=default_target_repo.repo_name
654 654 )
655 655
656 656 c.default_repo_data = {
657 657 'source_repo_name': source_repo.repo_name,
658 658 'source_refs_json': json.dumps(source_repo_data),
659 659 'target_repo_name': default_target_repo.repo_name,
660 660 'target_refs_json': json.dumps(target_repo_data),
661 661 }
662 662 c.default_source_ref = selected_source_ref
663 663
664 664 return self._get_template_context(c)
665 665
666 666 @LoginRequired()
667 667 @NotAnonymous()
668 668 @HasRepoPermissionAnyDecorator(
669 669 'repository.read', 'repository.write', 'repository.admin')
670 670 @view_config(
671 671 route_name='pullrequest_repo_refs', request_method='GET',
672 672 renderer='json_ext', xhr=True)
673 673 def pull_request_repo_refs(self):
674 674 target_repo_name = self.request.matchdict['target_repo_name']
675 675 repo = Repository.get_by_repo_name(target_repo_name)
676 676 if not repo:
677 677 raise HTTPNotFound()
678 678 return PullRequestModel().generate_repo_data(repo)
679 679
680 680 @LoginRequired()
681 681 @NotAnonymous()
682 682 @HasRepoPermissionAnyDecorator(
683 683 'repository.read', 'repository.write', 'repository.admin')
684 684 @view_config(
685 685 route_name='pullrequest_repo_destinations', request_method='GET',
686 686 renderer='json_ext', xhr=True)
687 687 def pull_request_repo_destinations(self):
688 688 _ = self.request.translate
689 689 filter_query = self.request.GET.get('query')
690 690
691 691 query = Repository.query() \
692 692 .order_by(func.length(Repository.repo_name)) \
693 693 .filter(
694 694 or_(Repository.repo_name == self.db_repo.repo_name,
695 695 Repository.fork_id == self.db_repo.repo_id))
696 696
697 697 if filter_query:
698 698 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
699 699 query = query.filter(
700 700 Repository.repo_name.ilike(ilike_expression))
701 701
702 702 add_parent = False
703 703 if self.db_repo.parent:
704 704 if filter_query in self.db_repo.parent.repo_name:
705 705 parent_vcs_obj = self.db_repo.parent.scm_instance()
706 706 if parent_vcs_obj and not parent_vcs_obj.is_empty():
707 707 add_parent = True
708 708
709 709 limit = 20 - 1 if add_parent else 20
710 710 all_repos = query.limit(limit).all()
711 711 if add_parent:
712 712 all_repos += [self.db_repo.parent]
713 713
714 714 repos = []
715 715 for obj in ScmModel().get_repos(all_repos):
716 716 repos.append({
717 717 'id': obj['name'],
718 718 'text': obj['name'],
719 719 'type': 'repo',
720 720 'obj': obj['dbrepo']
721 721 })
722 722
723 723 data = {
724 724 'more': False,
725 725 'results': [{
726 726 'text': _('Repositories'),
727 727 'children': repos
728 728 }] if repos else []
729 729 }
730 730 return data
731 731
732 732 @LoginRequired()
733 733 @NotAnonymous()
734 734 @HasRepoPermissionAnyDecorator(
735 735 'repository.read', 'repository.write', 'repository.admin')
736 736 @CSRFRequired()
737 737 @view_config(
738 738 route_name='pullrequest_create', request_method='POST',
739 739 renderer=None)
740 740 def pull_request_create(self):
741 741 _ = self.request.translate
742 742 self.assure_not_empty_repo()
743 743
744 744 controls = peppercorn.parse(self.request.POST.items())
745 745
746 746 try:
747 747 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
748 748 except formencode.Invalid as errors:
749 749 if errors.error_dict.get('revisions'):
750 750 msg = 'Revisions: %s' % errors.error_dict['revisions']
751 751 elif errors.error_dict.get('pullrequest_title'):
752 752 msg = _('Pull request requires a title with min. 3 chars')
753 753 else:
754 754 msg = _('Error creating pull request: {}').format(errors)
755 755 log.exception(msg)
756 756 h.flash(msg, 'error')
757 757
758 758 # would rather just go back to form ...
759 759 raise HTTPFound(
760 760 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
761 761
762 762 source_repo = _form['source_repo']
763 763 source_ref = _form['source_ref']
764 764 target_repo = _form['target_repo']
765 765 target_ref = _form['target_ref']
766 766 commit_ids = _form['revisions'][::-1]
767 767
768 768 # find the ancestor for this pr
769 769 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
770 770 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
771 771
772 772 source_scm = source_db_repo.scm_instance()
773 773 target_scm = target_db_repo.scm_instance()
774 774
775 775 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
776 776 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
777 777
778 778 ancestor = source_scm.get_common_ancestor(
779 779 source_commit.raw_id, target_commit.raw_id, target_scm)
780 780
781 781 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
782 782 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
783 783
784 784 pullrequest_title = _form['pullrequest_title']
785 785 title_source_ref = source_ref.split(':', 2)[1]
786 786 if not pullrequest_title:
787 787 pullrequest_title = PullRequestModel().generate_pullrequest_title(
788 788 source=source_repo,
789 789 source_ref=title_source_ref,
790 790 target=target_repo
791 791 )
792 792
793 793 description = _form['pullrequest_desc']
794 794
795 795 get_default_reviewers_data, validate_default_reviewers = \
796 796 PullRequestModel().get_reviewer_functions()
797 797
798 798 # recalculate reviewers logic, to make sure we can validate this
799 799 reviewer_rules = get_default_reviewers_data(
800 800 self._rhodecode_db_user, source_db_repo,
801 801 source_commit, target_db_repo, target_commit)
802 802
803 803 given_reviewers = _form['review_members']
804 804 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
805 805
806 806 try:
807 807 pull_request = PullRequestModel().create(
808 808 self._rhodecode_user.user_id, source_repo, source_ref, target_repo,
809 809 target_ref, commit_ids, reviewers, pullrequest_title,
810 810 description, reviewer_rules
811 811 )
812 812 Session().commit()
813 813 h.flash(_('Successfully opened new pull request'),
814 814 category='success')
815 815 except Exception as e:
816 816 msg = _('Error occurred during creation of this pull request.')
817 817 log.exception(msg)
818 818 h.flash(msg, category='error')
819 819 raise HTTPFound(
820 820 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
821 821
822 822 raise HTTPFound(
823 823 h.route_path('pullrequest_show', repo_name=target_repo,
824 824 pull_request_id=pull_request.pull_request_id))
825 825
826 826 @LoginRequired()
827 827 @NotAnonymous()
828 828 @HasRepoPermissionAnyDecorator(
829 829 'repository.read', 'repository.write', 'repository.admin')
830 830 @CSRFRequired()
831 831 @view_config(
832 832 route_name='pullrequest_update', request_method='POST',
833 833 renderer='json_ext')
834 834 def pull_request_update(self):
835 pull_request_id = self.request.matchdict['pull_request_id']
836 pull_request = PullRequest.get_or_404(pull_request_id)
835 pull_request = PullRequest.get_or_404(
836 self.request.matchdict['pull_request_id'])
837 837
838 838 # only owner or admin can update it
839 839 allowed_to_update = PullRequestModel().check_user_update(
840 840 pull_request, self._rhodecode_user)
841 841 if allowed_to_update:
842 842 controls = peppercorn.parse(self.request.POST.items())
843 843
844 844 if 'review_members' in controls:
845 845 self._update_reviewers(
846 pull_request_id, controls['review_members'],
846 pull_request, controls['review_members'],
847 847 pull_request.reviewer_data)
848 848 elif str2bool(self.request.POST.get('update_commits', 'false')):
849 849 self._update_commits(pull_request)
850 850 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
851 851 self._edit_pull_request(pull_request)
852 852 else:
853 853 raise HTTPBadRequest()
854 854 return True
855 855 raise HTTPForbidden()
856 856
857 857 def _edit_pull_request(self, pull_request):
858 858 _ = self.request.translate
859 859 try:
860 860 PullRequestModel().edit(
861 861 pull_request, self.request.POST.get('title'),
862 862 self.request.POST.get('description'), self._rhodecode_user)
863 863 except ValueError:
864 864 msg = _(u'Cannot update closed pull requests.')
865 865 h.flash(msg, category='error')
866 866 return
867 867 else:
868 868 Session().commit()
869 869
870 870 msg = _(u'Pull request title & description updated.')
871 871 h.flash(msg, category='success')
872 872 return
873 873
874 874 def _update_commits(self, pull_request):
875 875 _ = self.request.translate
876 876 resp = PullRequestModel().update_commits(pull_request)
877 877
878 878 if resp.executed:
879 879
880 880 if resp.target_changed and resp.source_changed:
881 881 changed = 'target and source repositories'
882 882 elif resp.target_changed and not resp.source_changed:
883 883 changed = 'target repository'
884 884 elif not resp.target_changed and resp.source_changed:
885 885 changed = 'source repository'
886 886 else:
887 887 changed = 'nothing'
888 888
889 889 msg = _(
890 890 u'Pull request updated to "{source_commit_id}" with '
891 891 u'{count_added} added, {count_removed} removed commits. '
892 892 u'Source of changes: {change_source}')
893 893 msg = msg.format(
894 894 source_commit_id=pull_request.source_ref_parts.commit_id,
895 895 count_added=len(resp.changes.added),
896 896 count_removed=len(resp.changes.removed),
897 897 change_source=changed)
898 898 h.flash(msg, category='success')
899 899
900 900 channel = '/repo${}$/pr/{}'.format(
901 901 pull_request.target_repo.repo_name,
902 902 pull_request.pull_request_id)
903 903 message = msg + (
904 904 ' - <a onclick="window.location.reload()">'
905 905 '<strong>{}</strong></a>'.format(_('Reload page')))
906 906 channelstream.post_message(
907 907 channel, message, self._rhodecode_user.username,
908 908 registry=self.request.registry)
909 909 else:
910 910 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
911 911 warning_reasons = [
912 912 UpdateFailureReason.NO_CHANGE,
913 913 UpdateFailureReason.WRONG_REF_TYPE,
914 914 ]
915 915 category = 'warning' if resp.reason in warning_reasons else 'error'
916 916 h.flash(msg, category=category)
917 917
918 918 @LoginRequired()
919 919 @NotAnonymous()
920 920 @HasRepoPermissionAnyDecorator(
921 921 'repository.read', 'repository.write', 'repository.admin')
922 922 @CSRFRequired()
923 923 @view_config(
924 924 route_name='pullrequest_merge', request_method='POST',
925 925 renderer='json_ext')
926 926 def pull_request_merge(self):
927 927 """
928 928 Merge will perform a server-side merge of the specified
929 929 pull request, if the pull request is approved and mergeable.
930 930 After successful merging, the pull request is automatically
931 931 closed, with a relevant comment.
932 932 """
933 pull_request_id = self.request.matchdict['pull_request_id']
934 pull_request = PullRequest.get_or_404(pull_request_id)
933 pull_request = PullRequest.get_or_404(
934 self.request.matchdict['pull_request_id'])
935 935
936 936 check = MergeCheck.validate(pull_request, self._rhodecode_db_user)
937 937 merge_possible = not check.failed
938 938
939 939 for err_type, error_msg in check.errors:
940 940 h.flash(error_msg, category=err_type)
941 941
942 942 if merge_possible:
943 943 log.debug("Pre-conditions checked, trying to merge.")
944 944 extras = vcs_operation_context(
945 945 self.request.environ, repo_name=pull_request.target_repo.repo_name,
946 946 username=self._rhodecode_db_user.username, action='push',
947 947 scm=pull_request.target_repo.repo_type)
948 948 self._merge_pull_request(
949 949 pull_request, self._rhodecode_db_user, extras)
950 950 else:
951 951 log.debug("Pre-conditions failed, NOT merging.")
952 952
953 953 raise HTTPFound(
954 954 h.route_path('pullrequest_show',
955 955 repo_name=pull_request.target_repo.repo_name,
956 956 pull_request_id=pull_request.pull_request_id))
957 957
958 958 def _merge_pull_request(self, pull_request, user, extras):
959 959 _ = self.request.translate
960 960 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
961 961
962 962 if merge_resp.executed:
963 963 log.debug("The merge was successful, closing the pull request.")
964 964 PullRequestModel().close_pull_request(
965 965 pull_request.pull_request_id, user)
966 966 Session().commit()
967 967 msg = _('Pull request was successfully merged and closed.')
968 968 h.flash(msg, category='success')
969 969 else:
970 970 log.debug(
971 971 "The merge was not successful. Merge response: %s",
972 972 merge_resp)
973 973 msg = PullRequestModel().merge_status_message(
974 974 merge_resp.failure_reason)
975 975 h.flash(msg, category='error')
976 976
977 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
977 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
978 978 _ = self.request.translate
979 979 get_default_reviewers_data, validate_default_reviewers = \
980 980 PullRequestModel().get_reviewer_functions()
981 981
982 982 try:
983 983 reviewers = validate_default_reviewers(review_members, reviewer_rules)
984 984 except ValueError as e:
985 985 log.error('Reviewers Validation: {}'.format(e))
986 986 h.flash(e, category='error')
987 987 return
988 988
989 989 PullRequestModel().update_reviewers(
990 pull_request_id, reviewers, self._rhodecode_user)
990 pull_request, reviewers, self._rhodecode_user)
991 991 h.flash(_('Pull request reviewers updated.'), category='success')
992 992 Session().commit()
993 993
994 994 @LoginRequired()
995 995 @NotAnonymous()
996 996 @HasRepoPermissionAnyDecorator(
997 997 'repository.read', 'repository.write', 'repository.admin')
998 998 @CSRFRequired()
999 999 @view_config(
1000 1000 route_name='pullrequest_delete', request_method='POST',
1001 1001 renderer='json_ext')
1002 1002 def pull_request_delete(self):
1003 1003 _ = self.request.translate
1004 1004
1005 pull_request_id = self.request.matchdict['pull_request_id']
1006 pull_request = PullRequest.get_or_404(pull_request_id)
1005 pull_request = PullRequest.get_or_404(
1006 self.request.matchdict['pull_request_id'])
1007 1007
1008 1008 pr_closed = pull_request.is_closed()
1009 1009 allowed_to_delete = PullRequestModel().check_user_delete(
1010 1010 pull_request, self._rhodecode_user) and not pr_closed
1011 1011
1012 1012 # only owner can delete it !
1013 1013 if allowed_to_delete:
1014 1014 PullRequestModel().delete(pull_request, self._rhodecode_user)
1015 1015 Session().commit()
1016 1016 h.flash(_('Successfully deleted pull request'),
1017 1017 category='success')
1018 1018 raise HTTPFound(h.route_path('my_account_pullrequests'))
1019 1019
1020 1020 log.warning('user %s tried to delete pull request without access',
1021 1021 self._rhodecode_user)
1022 1022 raise HTTPNotFound()
1023 1023
1024 1024 @LoginRequired()
1025 1025 @NotAnonymous()
1026 1026 @HasRepoPermissionAnyDecorator(
1027 1027 'repository.read', 'repository.write', 'repository.admin')
1028 1028 @CSRFRequired()
1029 1029 @view_config(
1030 1030 route_name='pullrequest_comment_create', request_method='POST',
1031 1031 renderer='json_ext')
1032 1032 def pull_request_comment_create(self):
1033 1033 _ = self.request.translate
1034 pull_request_id = self.request.matchdict['pull_request_id']
1035 pull_request = PullRequest.get_or_404(pull_request_id)
1034
1035 pull_request = PullRequest.get_or_404(
1036 self.request.matchdict['pull_request_id'])
1037 pull_request_id = pull_request.pull_request_id
1038
1036 1039 if pull_request.is_closed():
1037 1040 log.debug('comment: forbidden because pull request is closed')
1038 1041 raise HTTPForbidden()
1039 1042
1040 1043 c = self.load_default_context()
1041 1044
1042 1045 status = self.request.POST.get('changeset_status', None)
1043 1046 text = self.request.POST.get('text')
1044 1047 comment_type = self.request.POST.get('comment_type')
1045 1048 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1046 1049 close_pull_request = self.request.POST.get('close_pull_request')
1047 1050
1048 1051 # the logic here should work like following, if we submit close
1049 1052 # pr comment, use `close_pull_request_with_comment` function
1050 1053 # else handle regular comment logic
1051 1054
1052 1055 if close_pull_request:
1053 1056 # only owner or admin or person with write permissions
1054 1057 allowed_to_close = PullRequestModel().check_user_update(
1055 1058 pull_request, self._rhodecode_user)
1056 1059 if not allowed_to_close:
1057 1060 log.debug('comment: forbidden because not allowed to close '
1058 1061 'pull request %s', pull_request_id)
1059 1062 raise HTTPForbidden()
1060 1063 comment, status = PullRequestModel().close_pull_request_with_comment(
1061 1064 pull_request, self._rhodecode_user, self.db_repo, message=text)
1062 1065 Session().flush()
1063 1066 events.trigger(
1064 1067 events.PullRequestCommentEvent(pull_request, comment))
1065 1068
1066 1069 else:
1067 1070 # regular comment case, could be inline, or one with status.
1068 1071 # for that one we check also permissions
1069 1072
1070 1073 allowed_to_change_status = PullRequestModel().check_user_change_status(
1071 1074 pull_request, self._rhodecode_user)
1072 1075
1073 1076 if status and allowed_to_change_status:
1074 1077 message = (_('Status change %(transition_icon)s %(status)s')
1075 1078 % {'transition_icon': '>',
1076 1079 'status': ChangesetStatus.get_status_lbl(status)})
1077 1080 text = text or message
1078 1081
1079 1082 comment = CommentsModel().create(
1080 1083 text=text,
1081 1084 repo=self.db_repo.repo_id,
1082 1085 user=self._rhodecode_user.user_id,
1083 pull_request=pull_request_id,
1086 pull_request=pull_request,
1084 1087 f_path=self.request.POST.get('f_path'),
1085 1088 line_no=self.request.POST.get('line'),
1086 1089 status_change=(ChangesetStatus.get_status_lbl(status)
1087 1090 if status and allowed_to_change_status else None),
1088 1091 status_change_type=(status
1089 1092 if status and allowed_to_change_status else None),
1090 1093 comment_type=comment_type,
1091 1094 resolves_comment_id=resolves_comment_id
1092 1095 )
1093 1096
1094 1097 if allowed_to_change_status:
1095 1098 # calculate old status before we change it
1096 1099 old_calculated_status = pull_request.calculated_review_status()
1097 1100
1098 1101 # get status if set !
1099 1102 if status:
1100 1103 ChangesetStatusModel().set_status(
1101 1104 self.db_repo.repo_id,
1102 1105 status,
1103 1106 self._rhodecode_user.user_id,
1104 1107 comment,
1105 pull_request=pull_request_id
1108 pull_request=pull_request
1106 1109 )
1107 1110
1108 1111 Session().flush()
1109 1112 events.trigger(
1110 1113 events.PullRequestCommentEvent(pull_request, comment))
1111 1114
1112 1115 # we now calculate the status of pull request, and based on that
1113 1116 # calculation we set the commits status
1114 1117 calculated_status = pull_request.calculated_review_status()
1115 1118 if old_calculated_status != calculated_status:
1116 1119 PullRequestModel()._trigger_pull_request_hook(
1117 1120 pull_request, self._rhodecode_user, 'review_status_change')
1118 1121
1119 1122 Session().commit()
1120 1123
1121 1124 data = {
1122 1125 'target_id': h.safeid(h.safe_unicode(
1123 1126 self.request.POST.get('f_path'))),
1124 1127 }
1125 1128 if comment:
1126 1129 c.co = comment
1127 1130 rendered_comment = render(
1128 1131 'rhodecode:templates/changeset/changeset_comment_block.mako',
1129 1132 self._get_template_context(c), self.request)
1130 1133
1131 1134 data.update(comment.get_dict())
1132 1135 data.update({'rendered_text': rendered_comment})
1133 1136
1134 1137 return data
1135 1138
1136 1139 @LoginRequired()
1137 1140 @NotAnonymous()
1138 1141 @HasRepoPermissionAnyDecorator(
1139 1142 'repository.read', 'repository.write', 'repository.admin')
1140 1143 @CSRFRequired()
1141 1144 @view_config(
1142 1145 route_name='pullrequest_comment_delete', request_method='POST',
1143 1146 renderer='json_ext')
1144 1147 def pull_request_comment_delete(self):
1145 pull_request_id = self.request.matchdict['pull_request_id']
1146 comment_id = self.request.matchdict['comment_id']
1148 pull_request = PullRequest.get_or_404(
1149 self.request.matchdict['pull_request_id'])
1147 1150
1148 pull_request = PullRequest.get_or_404(pull_request_id)
1151 comment = ChangesetComment.get_or_404(
1152 self.request.matchdict['comment_id'])
1153 comment_id = comment.comment_id
1154
1149 1155 if pull_request.is_closed():
1150 1156 log.debug('comment: forbidden because pull request is closed')
1151 1157 raise HTTPForbidden()
1152 1158
1153 comment = ChangesetComment.get_or_404(comment_id)
1154 1159 if not comment:
1155 1160 log.debug('Comment with id:%s not found, skipping', comment_id)
1156 1161 # comment already deleted in another call probably
1157 1162 return True
1158 1163
1159 1164 if comment.pull_request.is_closed():
1160 1165 # don't allow deleting comments on closed pull request
1161 1166 raise HTTPForbidden()
1162 1167
1163 1168 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1164 1169 super_admin = h.HasPermissionAny('hg.admin')()
1165 1170 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1166 1171 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1167 1172 comment_repo_admin = is_repo_admin and is_repo_comment
1168 1173
1169 1174 if super_admin or comment_owner or comment_repo_admin:
1170 1175 old_calculated_status = comment.pull_request.calculated_review_status()
1171 1176 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1172 1177 Session().commit()
1173 1178 calculated_status = comment.pull_request.calculated_review_status()
1174 1179 if old_calculated_status != calculated_status:
1175 1180 PullRequestModel()._trigger_pull_request_hook(
1176 1181 comment.pull_request, self._rhodecode_user, 'review_status_change')
1177 1182 return True
1178 1183 else:
1179 1184 log.warning('No permissions for user %s to delete comment_id: %s',
1180 1185 self._rhodecode_db_user, comment_id)
1181 1186 raise HTTPNotFound()
@@ -1,831 +1,830 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45 /* Comment form for main and inline comments */
46 46 (function(mod) {
47 47
48 48 if (typeof exports == "object" && typeof module == "object") {
49 49 // CommonJS
50 50 module.exports = mod();
51 51 }
52 52 else {
53 53 // Plain browser env
54 54 (this || window).CommentForm = mod();
55 55 }
56 56
57 57 })(function() {
58 58 "use strict";
59 59
60 60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
61 61 if (!(this instanceof CommentForm)) {
62 62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
63 63 }
64 64
65 65 // bind the element instance to our Form
66 66 $(formElement).get(0).CommentForm = this;
67 67
68 68 this.withLineNo = function(selector) {
69 69 var lineNo = this.lineNo;
70 70 if (lineNo === undefined) {
71 71 return selector
72 72 } else {
73 73 return selector + '_' + lineNo;
74 74 }
75 75 };
76 76
77 77 this.commitId = commitId;
78 78 this.pullRequestId = pullRequestId;
79 79 this.lineNo = lineNo;
80 80 this.initAutocompleteActions = initAutocompleteActions;
81 81
82 82 this.previewButton = this.withLineNo('#preview-btn');
83 83 this.previewContainer = this.withLineNo('#preview-container');
84 84
85 85 this.previewBoxSelector = this.withLineNo('#preview-box');
86 86
87 87 this.editButton = this.withLineNo('#edit-btn');
88 88 this.editContainer = this.withLineNo('#edit-container');
89 89 this.cancelButton = this.withLineNo('#cancel-btn');
90 90 this.commentType = this.withLineNo('#comment_type');
91 91
92 92 this.resolvesId = null;
93 93 this.resolvesActionId = null;
94 94
95 95 this.closesPr = '#close_pull_request';
96 96
97 97 this.cmBox = this.withLineNo('#text');
98 98 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
99 99
100 100 this.statusChange = this.withLineNo('#change_status');
101 101
102 102 this.submitForm = formElement;
103 103 this.submitButton = $(this.submitForm).find('input[type="submit"]');
104 104 this.submitButtonText = this.submitButton.val();
105 105
106 106 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
107 107 {'repo_name': templateContext.repo_name,
108 108 'commit_id': templateContext.commit_data.commit_id});
109 109
110 110 if (resolvesCommentId){
111 111 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
112 112 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
113 113 $(this.commentType).prop('disabled', true);
114 114 $(this.commentType).addClass('disabled');
115 115
116 116 // disable select
117 117 setTimeout(function() {
118 118 $(self.statusChange).select2('readonly', true);
119 119 }, 10);
120 120
121 121 var resolvedInfo = (
122 122 '<li class="resolve-action">' +
123 123 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
124 124 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
125 125 '</li>'
126 126 ).format(resolvesCommentId, _gettext('resolve comment'));
127 127 $(resolvedInfo).insertAfter($(this.commentType).parent());
128 128 }
129 129
130 130 // based on commitId, or pullRequestId decide where do we submit
131 131 // out data
132 132 if (this.commitId){
133 133 this.submitUrl = pyroutes.url('repo_commit_comment_create',
134 134 {'repo_name': templateContext.repo_name,
135 135 'commit_id': this.commitId});
136 136 this.selfUrl = pyroutes.url('repo_commit',
137 137 {'repo_name': templateContext.repo_name,
138 138 'commit_id': this.commitId});
139 139
140 140 } else if (this.pullRequestId) {
141 141 this.submitUrl = pyroutes.url('pullrequest_comment_create',
142 142 {'repo_name': templateContext.repo_name,
143 143 'pull_request_id': this.pullRequestId});
144 144 this.selfUrl = pyroutes.url('pullrequest_show',
145 145 {'repo_name': templateContext.repo_name,
146 146 'pull_request_id': this.pullRequestId});
147 147
148 148 } else {
149 149 throw new Error(
150 150 'CommentForm requires pullRequestId, or commitId to be specified.')
151 151 }
152 152
153 153 // FUNCTIONS and helpers
154 154 var self = this;
155 155
156 156 this.isInline = function(){
157 157 return this.lineNo && this.lineNo != 'general';
158 158 };
159 159
160 160 this.getCmInstance = function(){
161 161 return this.cm
162 162 };
163 163
164 164 this.setPlaceholder = function(placeholder) {
165 165 var cm = this.getCmInstance();
166 166 if (cm){
167 167 cm.setOption('placeholder', placeholder);
168 168 }
169 169 };
170 170
171 171 this.getCommentStatus = function() {
172 172 return $(this.submitForm).find(this.statusChange).val();
173 173 };
174 174 this.getCommentType = function() {
175 175 return $(this.submitForm).find(this.commentType).val();
176 176 };
177 177
178 178 this.getResolvesId = function() {
179 179 return $(this.submitForm).find(this.resolvesId).val() || null;
180 180 };
181 181
182 182 this.getClosePr = function() {
183 183 return $(this.submitForm).find(this.closesPr).val() || null;
184 184 };
185 185
186 186 this.markCommentResolved = function(resolvedCommentId){
187 187 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
188 188 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
189 189 };
190 190
191 191 this.isAllowedToSubmit = function() {
192 192 return !$(this.submitButton).prop('disabled');
193 193 };
194 194
195 195 this.initStatusChangeSelector = function(){
196 196 var formatChangeStatus = function(state, escapeMarkup) {
197 197 var originalOption = state.element;
198 198 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
199 199 '<span>' + escapeMarkup(state.text) + '</span>';
200 200 };
201 201 var formatResult = function(result, container, query, escapeMarkup) {
202 202 return formatChangeStatus(result, escapeMarkup);
203 203 };
204 204
205 205 var formatSelection = function(data, container, escapeMarkup) {
206 206 return formatChangeStatus(data, escapeMarkup);
207 207 };
208 208
209 209 $(this.submitForm).find(this.statusChange).select2({
210 210 placeholder: _gettext('Status Review'),
211 211 formatResult: formatResult,
212 212 formatSelection: formatSelection,
213 213 containerCssClass: "drop-menu status_box_menu",
214 214 dropdownCssClass: "drop-menu-dropdown",
215 215 dropdownAutoWidth: true,
216 216 minimumResultsForSearch: -1
217 217 });
218 218 $(this.submitForm).find(this.statusChange).on('change', function() {
219 219 var status = self.getCommentStatus();
220 220
221 221 if (status && !self.isInline()) {
222 222 $(self.submitButton).prop('disabled', false);
223 223 }
224 224
225 225 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
226 226 self.setPlaceholder(placeholderText)
227 227 })
228 228 };
229 229
230 230 // reset the comment form into it's original state
231 231 this.resetCommentFormState = function(content) {
232 232 content = content || '';
233 233
234 234 $(this.editContainer).show();
235 235 $(this.editButton).parent().addClass('active');
236 236
237 237 $(this.previewContainer).hide();
238 238 $(this.previewButton).parent().removeClass('active');
239 239
240 240 this.setActionButtonsDisabled(true);
241 241 self.cm.setValue(content);
242 242 self.cm.setOption("readOnly", false);
243 243
244 244 if (this.resolvesId) {
245 245 // destroy the resolve action
246 246 $(this.resolvesId).parent().remove();
247 247 }
248 248 // reset closingPR flag
249 249 $('.close-pr-input').remove();
250 250
251 251 $(this.statusChange).select2('readonly', false);
252 252 };
253 253
254 254 this.globalSubmitSuccessCallback = function(){
255 255 // default behaviour is to call GLOBAL hook, if it's registered.
256 256 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
257 257 commentFormGlobalSubmitSuccessCallback()
258 258 }
259 259 };
260 260
261 261 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
262 262 failHandler = failHandler || function() {};
263 263 var postData = toQueryString(postData);
264 264 var request = $.ajax({
265 265 url: url,
266 266 type: 'POST',
267 267 data: postData,
268 268 headers: {'X-PARTIAL-XHR': true}
269 269 })
270 270 .done(function(data) {
271 271 successHandler(data);
272 272 })
273 273 .fail(function(data, textStatus, errorThrown){
274 274 alert(
275 275 "Error while submitting comment.\n" +
276 276 "Error code {0} ({1}).".format(data.status, data.statusText));
277 277 failHandler()
278 278 });
279 279 return request;
280 280 };
281 281
282 282 // overwrite a submitHandler, we need to do it for inline comments
283 283 this.setHandleFormSubmit = function(callback) {
284 284 this.handleFormSubmit = callback;
285 285 };
286 286
287 287 // overwrite a submitSuccessHandler
288 288 this.setGlobalSubmitSuccessCallback = function(callback) {
289 289 this.globalSubmitSuccessCallback = callback;
290 290 };
291 291
292 292 // default handler for for submit for main comments
293 293 this.handleFormSubmit = function() {
294 294 var text = self.cm.getValue();
295 295 var status = self.getCommentStatus();
296 296 var commentType = self.getCommentType();
297 297 var resolvesCommentId = self.getResolvesId();
298 298 var closePullRequest = self.getClosePr();
299 299
300 300 if (text === "" && !status) {
301 301 return;
302 302 }
303 303
304 304 var excludeCancelBtn = false;
305 305 var submitEvent = true;
306 306 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
307 307 self.cm.setOption("readOnly", true);
308 308
309 309 var postData = {
310 310 'text': text,
311 311 'changeset_status': status,
312 312 'comment_type': commentType,
313 313 'csrf_token': CSRF_TOKEN
314 314 };
315 315
316 316 if (resolvesCommentId) {
317 317 postData['resolves_comment_id'] = resolvesCommentId;
318 318 }
319 319
320 320 if (closePullRequest) {
321 321 postData['close_pull_request'] = true;
322 322 }
323 323
324 324 var submitSuccessCallback = function(o) {
325 325 // reload page if we change status for single commit.
326 326 if (status && self.commitId) {
327 327 location.reload(true);
328 328 } else {
329 329 $('#injected_page_comments').append(o.rendered_text);
330 330 self.resetCommentFormState();
331 331 timeagoActivate();
332 332
333 333 // mark visually which comment was resolved
334 334 if (resolvesCommentId) {
335 335 self.markCommentResolved(resolvesCommentId);
336 336 }
337 337 }
338 338
339 339 // run global callback on submit
340 340 self.globalSubmitSuccessCallback();
341 341
342 342 };
343 343 var submitFailCallback = function(){
344 344 self.resetCommentFormState(text);
345 345 };
346 346 self.submitAjaxPOST(
347 347 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
348 348 };
349 349
350 350 this.previewSuccessCallback = function(o) {
351 351 $(self.previewBoxSelector).html(o);
352 352 $(self.previewBoxSelector).removeClass('unloaded');
353 353
354 354 // swap buttons, making preview active
355 355 $(self.previewButton).parent().addClass('active');
356 356 $(self.editButton).parent().removeClass('active');
357 357
358 358 // unlock buttons
359 359 self.setActionButtonsDisabled(false);
360 360 };
361 361
362 362 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
363 363 excludeCancelBtn = excludeCancelBtn || false;
364 364 submitEvent = submitEvent || false;
365 365
366 366 $(this.editButton).prop('disabled', state);
367 367 $(this.previewButton).prop('disabled', state);
368 368
369 369 if (!excludeCancelBtn) {
370 370 $(this.cancelButton).prop('disabled', state);
371 371 }
372 372
373 373 var submitState = state;
374 374 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
375 375 // if the value of commit review status is set, we allow
376 376 // submit button, but only on Main form, isInline means inline
377 377 submitState = false
378 378 }
379 379
380 380 $(this.submitButton).prop('disabled', submitState);
381 381 if (submitEvent) {
382 382 $(this.submitButton).val(_gettext('Submitting...'));
383 383 } else {
384 384 $(this.submitButton).val(this.submitButtonText);
385 385 }
386 386
387 387 };
388 388
389 389 // lock preview/edit/submit buttons on load, but exclude cancel button
390 390 var excludeCancelBtn = true;
391 391 this.setActionButtonsDisabled(true, excludeCancelBtn);
392 392
393 393 // anonymous users don't have access to initialized CM instance
394 394 if (this.cm !== undefined){
395 395 this.cm.on('change', function(cMirror) {
396 396 if (cMirror.getValue() === "") {
397 397 self.setActionButtonsDisabled(true, excludeCancelBtn)
398 398 } else {
399 399 self.setActionButtonsDisabled(false, excludeCancelBtn)
400 400 }
401 401 });
402 402 }
403 403
404 404 $(this.editButton).on('click', function(e) {
405 405 e.preventDefault();
406 406
407 407 $(self.previewButton).parent().removeClass('active');
408 408 $(self.previewContainer).hide();
409 409
410 410 $(self.editButton).parent().addClass('active');
411 411 $(self.editContainer).show();
412 412
413 413 });
414 414
415 415 $(this.previewButton).on('click', function(e) {
416 416 e.preventDefault();
417 417 var text = self.cm.getValue();
418 418
419 419 if (text === "") {
420 420 return;
421 421 }
422 422
423 423 var postData = {
424 424 'text': text,
425 425 'renderer': templateContext.visual.default_renderer,
426 426 'csrf_token': CSRF_TOKEN
427 427 };
428 428
429 429 // lock ALL buttons on preview
430 430 self.setActionButtonsDisabled(true);
431 431
432 432 $(self.previewBoxSelector).addClass('unloaded');
433 433 $(self.previewBoxSelector).html(_gettext('Loading ...'));
434 434
435 435 $(self.editContainer).hide();
436 436 $(self.previewContainer).show();
437 437
438 438 // by default we reset state of comment preserving the text
439 439 var previewFailCallback = function(){
440 440 self.resetCommentFormState(text)
441 441 };
442 442 self.submitAjaxPOST(
443 443 self.previewUrl, postData, self.previewSuccessCallback,
444 444 previewFailCallback);
445 445
446 446 $(self.previewButton).parent().addClass('active');
447 447 $(self.editButton).parent().removeClass('active');
448 448 });
449 449
450 450 $(this.submitForm).submit(function(e) {
451 451 e.preventDefault();
452 452 var allowedToSubmit = self.isAllowedToSubmit();
453 453 if (!allowedToSubmit){
454 454 return false;
455 455 }
456 456 self.handleFormSubmit();
457 457 });
458 458
459 459 }
460 460
461 461 return CommentForm;
462 462 });
463 463
464 464 /* comments controller */
465 465 var CommentsController = function() {
466 466 var mainComment = '#text';
467 467 var self = this;
468 468
469 469 this.cancelComment = function(node) {
470 470 var $node = $(node);
471 471 var $td = $node.closest('td');
472 472 $node.closest('.comment-inline-form').remove();
473 473 return false;
474 474 };
475 475
476 476 this.getLineNumber = function(node) {
477 477 var $node = $(node);
478 478 return $node.closest('td').attr('data-line-number');
479 479 };
480 480
481 481 this.scrollToComment = function(node, offset, outdated) {
482 482 if (offset === undefined) {
483 483 offset = 0;
484 484 }
485 485 var outdated = outdated || false;
486 486 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
487 487
488 488 if (!node) {
489 489 node = $('.comment-selected');
490 490 if (!node.length) {
491 491 node = $('comment-current')
492 492 }
493 493 }
494 494 $wrapper = $(node).closest('div.comment');
495 495 $comment = $(node).closest(klass);
496 496 $comments = $(klass);
497 497
498 498 // show hidden comment when referenced.
499 499 if (!$wrapper.is(':visible')){
500 500 $wrapper.show();
501 501 }
502 502
503 503 $('.comment-selected').removeClass('comment-selected');
504 504
505 505 var nextIdx = $(klass).index($comment) + offset;
506 506 if (nextIdx >= $comments.length) {
507 507 nextIdx = 0;
508 508 }
509 509 var $next = $(klass).eq(nextIdx);
510 510
511 511 var $cb = $next.closest('.cb');
512 512 $cb.removeClass('cb-collapsed');
513 513
514 514 var $filediffCollapseState = $cb.closest('.filediff').prev();
515 515 $filediffCollapseState.prop('checked', false);
516 516 $next.addClass('comment-selected');
517 517 scrollToElement($next);
518 518 return false;
519 519 };
520 520
521 521 this.nextComment = function(node) {
522 522 return self.scrollToComment(node, 1);
523 523 };
524 524
525 525 this.prevComment = function(node) {
526 526 return self.scrollToComment(node, -1);
527 527 };
528 528
529 529 this.nextOutdatedComment = function(node) {
530 530 return self.scrollToComment(node, 1, true);
531 531 };
532 532
533 533 this.prevOutdatedComment = function(node) {
534 534 return self.scrollToComment(node, -1, true);
535 535 };
536 536
537 537 this.deleteComment = function(node) {
538 538 if (!confirm(_gettext('Delete this comment?'))) {
539 539 return false;
540 540 }
541 541 var $node = $(node);
542 542 var $td = $node.closest('td');
543 543 var $comment = $node.closest('.comment');
544 544 var comment_id = $comment.attr('data-comment-id');
545 545 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
546 546 var postData = {
547 '_method': 'delete',
548 547 'csrf_token': CSRF_TOKEN
549 548 };
550 549
551 550 $comment.addClass('comment-deleting');
552 551 $comment.hide('fast');
553 552
554 553 var success = function(response) {
555 554 $comment.remove();
556 555 return false;
557 556 };
558 557 var failure = function(data, textStatus, xhr) {
559 558 alert("error processing request: " + textStatus);
560 559 $comment.show('fast');
561 560 $comment.removeClass('comment-deleting');
562 561 return false;
563 562 };
564 563 ajaxPOST(url, postData, success, failure);
565 564 };
566 565
567 566 this.toggleWideMode = function (node) {
568 567 if ($('#content').hasClass('wrapper')) {
569 568 $('#content').removeClass("wrapper");
570 569 $('#content').addClass("wide-mode-wrapper");
571 570 $(node).addClass('btn-success');
572 571 } else {
573 572 $('#content').removeClass("wide-mode-wrapper");
574 573 $('#content').addClass("wrapper");
575 574 $(node).removeClass('btn-success');
576 575 }
577 576 return false;
578 577 };
579 578
580 579 this.toggleComments = function(node, show) {
581 580 var $filediff = $(node).closest('.filediff');
582 581 if (show === true) {
583 582 $filediff.removeClass('hide-comments');
584 583 } else if (show === false) {
585 584 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
586 585 $filediff.addClass('hide-comments');
587 586 } else {
588 587 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
589 588 $filediff.toggleClass('hide-comments');
590 589 }
591 590 return false;
592 591 };
593 592
594 593 this.toggleLineComments = function(node) {
595 594 self.toggleComments(node, true);
596 595 var $node = $(node);
597 596 $node.closest('tr').toggleClass('hide-line-comments');
598 597 };
599 598
600 599 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
601 600 var pullRequestId = templateContext.pull_request_data.pull_request_id;
602 601 var commitId = templateContext.commit_data.commit_id;
603 602
604 603 var commentForm = new CommentForm(
605 604 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
606 605 var cm = commentForm.getCmInstance();
607 606
608 607 if (resolvesCommentId){
609 608 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
610 609 }
611 610
612 611 setTimeout(function() {
613 612 // callbacks
614 613 if (cm !== undefined) {
615 614 commentForm.setPlaceholder(placeholderText);
616 615 if (commentForm.isInline()) {
617 616 cm.focus();
618 617 cm.refresh();
619 618 }
620 619 }
621 620 }, 10);
622 621
623 622 // trigger scrolldown to the resolve comment, since it might be away
624 623 // from the clicked
625 624 if (resolvesCommentId){
626 625 var actionNode = $(commentForm.resolvesActionId).offset();
627 626
628 627 setTimeout(function() {
629 628 if (actionNode) {
630 629 $('body, html').animate({scrollTop: actionNode.top}, 10);
631 630 }
632 631 }, 100);
633 632 }
634 633
635 634 return commentForm;
636 635 };
637 636
638 637 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
639 638
640 639 var tmpl = $('#cb-comment-general-form-template').html();
641 640 tmpl = tmpl.format(null, 'general');
642 641 var $form = $(tmpl);
643 642
644 643 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
645 644 var curForm = $formPlaceholder.find('form');
646 645 if (curForm){
647 646 curForm.remove();
648 647 }
649 648 $formPlaceholder.append($form);
650 649
651 650 var _form = $($form[0]);
652 651 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
653 652 var commentForm = this.createCommentForm(
654 653 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
655 654 commentForm.initStatusChangeSelector();
656 655
657 656 return commentForm;
658 657 };
659 658
660 659 this.createComment = function(node, resolutionComment) {
661 660 var resolvesCommentId = resolutionComment || null;
662 661 var $node = $(node);
663 662 var $td = $node.closest('td');
664 663 var $form = $td.find('.comment-inline-form');
665 664
666 665 if (!$form.length) {
667 666
668 667 var $filediff = $node.closest('.filediff');
669 668 $filediff.removeClass('hide-comments');
670 669 var f_path = $filediff.attr('data-f-path');
671 670 var lineno = self.getLineNumber(node);
672 671 // create a new HTML from template
673 672 var tmpl = $('#cb-comment-inline-form-template').html();
674 673 tmpl = tmpl.format(f_path, lineno);
675 674 $form = $(tmpl);
676 675
677 676 var $comments = $td.find('.inline-comments');
678 677 if (!$comments.length) {
679 678 $comments = $(
680 679 $('#cb-comments-inline-container-template').html());
681 680 $td.append($comments);
682 681 }
683 682
684 683 $td.find('.cb-comment-add-button').before($form);
685 684
686 685 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
687 686 var _form = $($form[0]).find('form');
688 687 var autocompleteActions = ['as_note', 'as_todo'];
689 688 var commentForm = this.createCommentForm(
690 689 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
691 690
692 691 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
693 692 form: _form,
694 693 parent: $td[0],
695 694 lineno: lineno,
696 695 f_path: f_path}
697 696 );
698 697
699 698 // set a CUSTOM submit handler for inline comments.
700 699 commentForm.setHandleFormSubmit(function(o) {
701 700 var text = commentForm.cm.getValue();
702 701 var commentType = commentForm.getCommentType();
703 702 var resolvesCommentId = commentForm.getResolvesId();
704 703
705 704 if (text === "") {
706 705 return;
707 706 }
708 707
709 708 if (lineno === undefined) {
710 709 alert('missing line !');
711 710 return;
712 711 }
713 712 if (f_path === undefined) {
714 713 alert('missing file path !');
715 714 return;
716 715 }
717 716
718 717 var excludeCancelBtn = false;
719 718 var submitEvent = true;
720 719 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
721 720 commentForm.cm.setOption("readOnly", true);
722 721 var postData = {
723 722 'text': text,
724 723 'f_path': f_path,
725 724 'line': lineno,
726 725 'comment_type': commentType,
727 726 'csrf_token': CSRF_TOKEN
728 727 };
729 728 if (resolvesCommentId){
730 729 postData['resolves_comment_id'] = resolvesCommentId;
731 730 }
732 731
733 732 var submitSuccessCallback = function(json_data) {
734 733 $form.remove();
735 734 try {
736 735 var html = json_data.rendered_text;
737 736 var lineno = json_data.line_no;
738 737 var target_id = json_data.target_id;
739 738
740 739 $comments.find('.cb-comment-add-button').before(html);
741 740
742 741 //mark visually which comment was resolved
743 742 if (resolvesCommentId) {
744 743 commentForm.markCommentResolved(resolvesCommentId);
745 744 }
746 745
747 746 // run global callback on submit
748 747 commentForm.globalSubmitSuccessCallback();
749 748
750 749 } catch (e) {
751 750 console.error(e);
752 751 }
753 752
754 753 // re trigger the linkification of next/prev navigation
755 754 linkifyComments($('.inline-comment-injected'));
756 755 timeagoActivate();
757 756 commentForm.setActionButtonsDisabled(false);
758 757
759 758 };
760 759 var submitFailCallback = function(){
761 760 commentForm.resetCommentFormState(text)
762 761 };
763 762 commentForm.submitAjaxPOST(
764 763 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
765 764 });
766 765 }
767 766
768 767 $form.addClass('comment-inline-form-open');
769 768 };
770 769
771 770 this.createResolutionComment = function(commentId){
772 771 // hide the trigger text
773 772 $('#resolve-comment-{0}'.format(commentId)).hide();
774 773
775 774 var comment = $('#comment-'+commentId);
776 775 var commentData = comment.data();
777 776 if (commentData.commentInline) {
778 777 this.createComment(comment, commentId)
779 778 } else {
780 779 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
781 780 }
782 781
783 782 return false;
784 783 };
785 784
786 785 this.submitResolution = function(commentId){
787 786 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
788 787 var commentForm = form.get(0).CommentForm;
789 788
790 789 var cm = commentForm.getCmInstance();
791 790 var renderer = templateContext.visual.default_renderer;
792 791 if (renderer == 'rst'){
793 792 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
794 793 } else if (renderer == 'markdown') {
795 794 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
796 795 } else {
797 796 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
798 797 }
799 798
800 799 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
801 800 form.submit();
802 801 return false;
803 802 };
804 803
805 804 this.renderInlineComments = function(file_comments) {
806 805 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
807 806
808 807 for (var i = 0; i < file_comments.length; i++) {
809 808 var box = file_comments[i];
810 809
811 810 var target_id = $(box).attr('target_id');
812 811
813 812 // actually comments with line numbers
814 813 var comments = box.children;
815 814
816 815 for (var j = 0; j < comments.length; j++) {
817 816 var data = {
818 817 'rendered_text': comments[j].outerHTML,
819 818 'line_no': $(comments[j]).attr('line'),
820 819 'target_id': target_id
821 820 };
822 821 }
823 822 }
824 823
825 824 // since order of injection is random, we're now re-iterating
826 825 // from correct order and filling in links
827 826 linkifyComments($('.inline-comment-injected'));
828 827 firefoxAnchorFix();
829 828 };
830 829
831 830 };
@@ -1,604 +1,603 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 var prButtonLockChecks = {
21 21 'compare': false,
22 22 'reviewers': false
23 23 };
24 24
25 25 /**
26 26 * lock button until all checks and loads are made. E.g reviewer calculation
27 27 * should prevent from submitting a PR
28 28 * @param lockEnabled
29 29 * @param msg
30 30 * @param scope
31 31 */
32 32 var prButtonLock = function(lockEnabled, msg, scope) {
33 33 scope = scope || 'all';
34 34 if (scope == 'all'){
35 35 prButtonLockChecks['compare'] = !lockEnabled;
36 36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 37 } else if (scope == 'compare') {
38 38 prButtonLockChecks['compare'] = !lockEnabled;
39 39 } else if (scope == 'reviewers'){
40 40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 41 }
42 42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 43 if (lockEnabled) {
44 44 $('#save').attr('disabled', 'disabled');
45 45 }
46 46 else if (checksMeet) {
47 47 $('#save').removeAttr('disabled');
48 48 }
49 49
50 50 if (msg) {
51 51 $('#pr_open_message').html(msg);
52 52 }
53 53 };
54 54
55 55
56 56 /**
57 57 Generate Title and Description for a PullRequest.
58 58 In case of 1 commits, the title and description is that one commit
59 59 in case of multiple commits, we iterate on them with max N number of commits,
60 60 and build description in a form
61 61 - commitN
62 62 - commitN+1
63 63 ...
64 64
65 65 Title is then constructed from branch names, or other references,
66 66 replacing '-' and '_' into spaces
67 67
68 68 * @param sourceRef
69 69 * @param elements
70 70 * @param limit
71 71 * @returns {*[]}
72 72 */
73 73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 74 var title = '';
75 75 var desc = '';
76 76
77 77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 80 });
81 81 // only 1 commit, use commit message as title
82 82 if (elements.length === 1) {
83 83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 84 }
85 85 else {
86 86 // use reference name
87 87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 88 }
89 89
90 90 return [title, desc]
91 91 };
92 92
93 93
94 94
95 95 ReviewersController = function () {
96 96 var self = this;
97 97 this.$reviewRulesContainer = $('#review_rules');
98 98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 99 this.forbidReviewUsers = undefined;
100 100 this.$reviewMembers = $('#review_members');
101 101 this.currentRequest = null;
102 102
103 103 this.defaultForbidReviewUsers = function() {
104 104 return [
105 105 {'username': 'default',
106 106 'user_id': templateContext.default_user.user_id}
107 107 ];
108 108 };
109 109
110 110 this.hideReviewRules = function() {
111 111 self.$reviewRulesContainer.hide();
112 112 };
113 113
114 114 this.showReviewRules = function() {
115 115 self.$reviewRulesContainer.show();
116 116 };
117 117
118 118 this.addRule = function(ruleText) {
119 119 self.showReviewRules();
120 120 return '<div>- {0}</div>'.format(ruleText)
121 121 };
122 122
123 123 this.loadReviewRules = function(data) {
124 124 // reset forbidden Users
125 125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126 126
127 127 // reset state of review rules
128 128 self.$rulesList.html('');
129 129
130 130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 131 // default rule, case for older repo that don't have any rules stored
132 132 self.$rulesList.append(
133 133 self.addRule(
134 134 _gettext('All reviewers must vote.'))
135 135 );
136 136 return self.forbidReviewUsers
137 137 }
138 138
139 139 if (data.rules.voting !== undefined) {
140 140 if (data.rules.voting < 0){
141 141 self.$rulesList.append(
142 142 self.addRule(
143 143 _gettext('All reviewers must vote.'))
144 144 )
145 145 } else if (data.rules.voting === 1) {
146 146 self.$rulesList.append(
147 147 self.addRule(
148 148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 149 )
150 150
151 151 } else {
152 152 self.$rulesList.append(
153 153 self.addRule(
154 154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 155 )
156 156 }
157 157 }
158 158 if (data.rules.use_code_authors_for_review) {
159 159 self.$rulesList.append(
160 160 self.addRule(
161 161 _gettext('Reviewers picked from source code changes.'))
162 162 )
163 163 }
164 164 if (data.rules.forbid_adding_reviewers) {
165 165 $('#add_reviewer_input').remove();
166 166 self.$rulesList.append(
167 167 self.addRule(
168 168 _gettext('Adding new reviewers is forbidden.'))
169 169 )
170 170 }
171 171 if (data.rules.forbid_author_to_review) {
172 172 self.forbidReviewUsers.push(data.rules_data.pr_author);
173 173 self.$rulesList.append(
174 174 self.addRule(
175 175 _gettext('Author is not allowed to be a reviewer.'))
176 176 )
177 177 }
178 178 if (data.rules.forbid_commit_author_to_review) {
179 179
180 180 if (data.rules_data.forbidden_users) {
181 181 $.each(data.rules_data.forbidden_users, function(index, member_data) {
182 182 self.forbidReviewUsers.push(member_data)
183 183 });
184 184
185 185 }
186 186
187 187 self.$rulesList.append(
188 188 self.addRule(
189 189 _gettext('Commit Authors are not allowed to be a reviewer.'))
190 190 )
191 191 }
192 192
193 193 return self.forbidReviewUsers
194 194 };
195 195
196 196 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
197 197
198 198 if (self.currentRequest) {
199 199 // make sure we cleanup old running requests before triggering this
200 200 // again
201 201 self.currentRequest.abort();
202 202 }
203 203
204 204 $('.calculate-reviewers').show();
205 205 // reset reviewer members
206 206 self.$reviewMembers.empty();
207 207
208 208 prButtonLock(true, null, 'reviewers');
209 209 $('#user').hide(); // hide user autocomplete before load
210 210
211 211 var url = pyroutes.url('repo_default_reviewers_data',
212 212 {
213 213 'repo_name': templateContext.repo_name,
214 214 'source_repo': sourceRepo,
215 215 'source_ref': sourceRef[2],
216 216 'target_repo': targetRepo,
217 217 'target_ref': targetRef[2]
218 218 });
219 219
220 220 self.currentRequest = $.get(url)
221 221 .done(function(data) {
222 222 self.currentRequest = null;
223 223
224 224 // review rules
225 225 self.loadReviewRules(data);
226 226
227 227 for (var i = 0; i < data.reviewers.length; i++) {
228 228 var reviewer = data.reviewers[i];
229 229 self.addReviewMember(
230 230 reviewer.user_id, reviewer.first_name,
231 231 reviewer.last_name, reviewer.username,
232 232 reviewer.gravatar_link, reviewer.reasons,
233 233 reviewer.mandatory);
234 234 }
235 235 $('.calculate-reviewers').hide();
236 236 prButtonLock(false, null, 'reviewers');
237 237 $('#user').show(); // show user autocomplete after load
238 238 });
239 239 };
240 240
241 241 // check those, refactor
242 242 this.removeReviewMember = function(reviewer_id, mark_delete) {
243 243 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
244 244
245 245 if(typeof(mark_delete) === undefined){
246 246 mark_delete = false;
247 247 }
248 248
249 249 if(mark_delete === true){
250 250 if (reviewer){
251 251 // now delete the input
252 252 $('#reviewer_{0} input'.format(reviewer_id)).remove();
253 253 // mark as to-delete
254 254 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
255 255 obj.addClass('to-delete');
256 256 obj.css({"text-decoration":"line-through", "opacity": 0.5});
257 257 }
258 258 }
259 259 else{
260 260 $('#reviewer_{0}'.format(reviewer_id)).remove();
261 261 }
262 262 };
263 263
264 264 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
265 265 var members = self.$reviewMembers.get(0);
266 266 var reasons_html = '';
267 267 var reasons_inputs = '';
268 268 var reasons = reasons || [];
269 269 var mandatory = mandatory || false;
270 270
271 271 if (reasons) {
272 272 for (var i = 0; i < reasons.length; i++) {
273 273 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
274 274 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
275 275 }
276 276 }
277 277 var tmpl = '' +
278 278 '<li id="reviewer_{2}" class="reviewer_entry">'+
279 279 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
280 280 '<div class="reviewer_status">'+
281 281 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
282 282 '</div>'+
283 283 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
284 284 '<span class="reviewer_name user">{1}</span>'+
285 285 reasons_html +
286 286 '<input type="hidden" name="user_id" value="{2}">'+
287 287 '<input type="hidden" name="__start__" value="reasons:sequence">'+
288 288 '{3}'+
289 289 '<input type="hidden" name="__end__" value="reasons:sequence">';
290 290
291 291 if (mandatory) {
292 292 tmpl += ''+
293 293 '<div class="reviewer_member_mandatory_remove">' +
294 294 '<i class="icon-remove-sign"></i>'+
295 295 '</div>' +
296 296 '<input type="hidden" name="mandatory" value="true">'+
297 297 '<div class="reviewer_member_mandatory">' +
298 298 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
299 299 '</div>';
300 300
301 301 } else {
302 302 tmpl += ''+
303 303 '<input type="hidden" name="mandatory" value="false">'+
304 304 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
305 305 '<i class="icon-remove-sign"></i>'+
306 306 '</div>';
307 307 }
308 308 // continue template
309 309 tmpl += ''+
310 310 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
311 311 '</li>' ;
312 312
313 313 var displayname = "{0} ({1} {2})".format(
314 314 nname, escapeHtml(fname), escapeHtml(lname));
315 315 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
316 316 // check if we don't have this ID already in
317 317 var ids = [];
318 318 var _els = self.$reviewMembers.find('li').toArray();
319 319 for (el in _els){
320 320 ids.push(_els[el].id)
321 321 }
322 322
323 323 var userAllowedReview = function(userId) {
324 324 var allowed = true;
325 325 $.each(self.forbidReviewUsers, function(index, member_data) {
326 326 if (parseInt(userId) === member_data['user_id']) {
327 327 allowed = false;
328 328 return false // breaks the loop
329 329 }
330 330 });
331 331 return allowed
332 332 };
333 333
334 334 var userAllowed = userAllowedReview(id);
335 335 if (!userAllowed){
336 336 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
337 337 }
338 338 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
339 339
340 340 if(shouldAdd) {
341 341 // only add if it's not there
342 342 members.innerHTML += element;
343 343 }
344 344
345 345 };
346 346
347 347 this.updateReviewers = function(repo_name, pull_request_id){
348 348 var postData = '_method=put&' + $('#reviewers input').serialize();
349 349 _updatePullRequest(repo_name, pull_request_id, postData);
350 350 };
351 351
352 352 };
353 353
354 354
355 355 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
356 356 var url = pyroutes.url(
357 357 'pullrequest_update',
358 358 {"repo_name": repo_name, "pull_request_id": pull_request_id});
359 359 if (typeof postData === 'string' ) {
360 360 postData += '&csrf_token=' + CSRF_TOKEN;
361 361 } else {
362 362 postData.csrf_token = CSRF_TOKEN;
363 363 }
364 364 var success = function(o) {
365 365 window.location.reload();
366 366 };
367 367 ajaxPOST(url, postData, success);
368 368 };
369 369
370 370 /**
371 371 * PULL REQUEST update commits
372 372 */
373 373 var updateCommits = function(repo_name, pull_request_id) {
374 374 var postData = {
375 '_method': 'put',
376 375 'update_commits': true};
377 376 _updatePullRequest(repo_name, pull_request_id, postData);
378 377 };
379 378
380 379
381 380 /**
382 381 * PULL REQUEST edit info
383 382 */
384 383 var editPullRequest = function(repo_name, pull_request_id, title, description) {
385 384 var url = pyroutes.url(
386 385 'pullrequest_update',
387 386 {"repo_name": repo_name, "pull_request_id": pull_request_id});
388 387
389 388 var postData = {
390 389 'title': title,
391 390 'description': description,
392 391 'edit_pull_request': true,
393 392 'csrf_token': CSRF_TOKEN
394 393 };
395 394 var success = function(o) {
396 395 window.location.reload();
397 396 };
398 397 ajaxPOST(url, postData, success);
399 398 };
400 399
401 400 var initPullRequestsCodeMirror = function (textAreaId) {
402 401 var ta = $(textAreaId).get(0);
403 402 var initialHeight = '100px';
404 403
405 404 // default options
406 405 var codeMirrorOptions = {
407 406 mode: "text",
408 407 lineNumbers: false,
409 408 indentUnit: 4,
410 409 theme: 'rc-input'
411 410 };
412 411
413 412 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
414 413 // marker for manually set description
415 414 codeMirrorInstance._userDefinedDesc = false;
416 415 codeMirrorInstance.setSize(null, initialHeight);
417 416 codeMirrorInstance.on("change", function(instance, changeObj) {
418 417 var height = initialHeight;
419 418 var lines = instance.lineCount();
420 419 if (lines > 6 && lines < 20) {
421 420 height = "auto"
422 421 }
423 422 else if (lines >= 20) {
424 423 height = 20 * 15;
425 424 }
426 425 instance.setSize(null, height);
427 426
428 427 // detect if the change was trigger by auto desc, or user input
429 428 changeOrigin = changeObj.origin;
430 429
431 430 if (changeOrigin === "setValue") {
432 431 cmLog.debug('Change triggered by setValue');
433 432 }
434 433 else {
435 434 cmLog.debug('user triggered change !');
436 435 // set special marker to indicate user has created an input.
437 436 instance._userDefinedDesc = true;
438 437 }
439 438
440 439 });
441 440
442 441 return codeMirrorInstance
443 442 };
444 443
445 444 /**
446 445 * Reviewer autocomplete
447 446 */
448 447 var ReviewerAutoComplete = function(inputId) {
449 448 $(inputId).autocomplete({
450 449 serviceUrl: pyroutes.url('user_autocomplete_data'),
451 450 minChars:2,
452 451 maxHeight:400,
453 452 deferRequestBy: 300, //miliseconds
454 453 showNoSuggestionNotice: true,
455 454 tabDisabled: true,
456 455 autoSelectFirst: true,
457 456 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
458 457 formatResult: autocompleteFormatResult,
459 458 lookupFilter: autocompleteFilterResult,
460 459 onSelect: function(element, data) {
461 460
462 461 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
463 462 if (data.value_type == 'user_group') {
464 463 reasons.push(_gettext('member of "{0}"').format(data.value_display));
465 464
466 465 $.each(data.members, function(index, member_data) {
467 466 reviewersController.addReviewMember(
468 467 member_data.id, member_data.first_name, member_data.last_name,
469 468 member_data.username, member_data.icon_link, reasons);
470 469 })
471 470
472 471 } else {
473 472 reviewersController.addReviewMember(
474 473 data.id, data.first_name, data.last_name,
475 474 data.username, data.icon_link, reasons);
476 475 }
477 476
478 477 $(inputId).val('');
479 478 }
480 479 });
481 480 };
482 481
483 482
484 483 VersionController = function () {
485 484 var self = this;
486 485 this.$verSource = $('input[name=ver_source]');
487 486 this.$verTarget = $('input[name=ver_target]');
488 487 this.$showVersionDiff = $('#show-version-diff');
489 488
490 489 this.adjustRadioSelectors = function (curNode) {
491 490 var getVal = function (item) {
492 491 if (item == 'latest') {
493 492 return Number.MAX_SAFE_INTEGER
494 493 }
495 494 else {
496 495 return parseInt(item)
497 496 }
498 497 };
499 498
500 499 var curVal = getVal($(curNode).val());
501 500 var cleared = false;
502 501
503 502 $.each(self.$verSource, function (index, value) {
504 503 var elVal = getVal($(value).val());
505 504
506 505 if (elVal > curVal) {
507 506 if ($(value).is(':checked')) {
508 507 cleared = true;
509 508 }
510 509 $(value).attr('disabled', 'disabled');
511 510 $(value).removeAttr('checked');
512 511 $(value).css({'opacity': 0.1});
513 512 }
514 513 else {
515 514 $(value).css({'opacity': 1});
516 515 $(value).removeAttr('disabled');
517 516 }
518 517 });
519 518
520 519 if (cleared) {
521 520 // if we unchecked an active, set the next one to same loc.
522 521 $(this.$verSource).filter('[value={0}]'.format(
523 522 curVal)).attr('checked', 'checked');
524 523 }
525 524
526 525 self.setLockAction(false,
527 526 $(curNode).data('verPos'),
528 527 $(this.$verSource).filter(':checked').data('verPos')
529 528 );
530 529 };
531 530
532 531
533 532 this.attachVersionListener = function () {
534 533 self.$verTarget.change(function (e) {
535 534 self.adjustRadioSelectors(this)
536 535 });
537 536 self.$verSource.change(function (e) {
538 537 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
539 538 });
540 539 };
541 540
542 541 this.init = function () {
543 542
544 543 var curNode = self.$verTarget.filter(':checked');
545 544 self.adjustRadioSelectors(curNode);
546 545 self.setLockAction(true);
547 546 self.attachVersionListener();
548 547
549 548 };
550 549
551 550 this.setLockAction = function (state, selectedVersion, otherVersion) {
552 551 var $showVersionDiff = this.$showVersionDiff;
553 552
554 553 if (state) {
555 554 $showVersionDiff.attr('disabled', 'disabled');
556 555 $showVersionDiff.addClass('disabled');
557 556 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
558 557 }
559 558 else {
560 559 $showVersionDiff.removeAttr('disabled');
561 560 $showVersionDiff.removeClass('disabled');
562 561
563 562 if (selectedVersion == otherVersion) {
564 563 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
565 564 } else {
566 565 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
567 566 }
568 567 }
569 568
570 569 };
571 570
572 571 this.showVersionDiff = function () {
573 572 var target = self.$verTarget.filter(':checked');
574 573 var source = self.$verSource.filter(':checked');
575 574
576 575 if (target.val() && source.val()) {
577 576 var params = {
578 577 'pull_request_id': templateContext.pull_request_data.pull_request_id,
579 578 'repo_name': templateContext.repo_name,
580 579 'version': target.val(),
581 580 'from_version': source.val()
582 581 };
583 582 window.location = pyroutes.url('pullrequest_show', params)
584 583 }
585 584
586 585 return false;
587 586 };
588 587
589 588 this.toggleVersionView = function (elem) {
590 589
591 590 if (this.$showVersionDiff.is(':visible')) {
592 591 $('.version-pr').hide();
593 592 this.$showVersionDiff.hide();
594 593 $(elem).html($(elem).data('toggleOn'))
595 594 } else {
596 595 $('.version-pr').show();
597 596 this.$showVersionDiff.show();
598 597 $(elem).html($(elem).data('toggleOff'))
599 598 }
600 599
601 600 return false
602 601 }
603 602
604 603 }; No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now