##// END OF EJS Templates
pull-requests: migrated code from pylons to pyramid
marcink -
r1974:cb4db595 default
parent child Browse files
Show More
@@ -1,134 +1,134 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import pytest
22 import pytest
23 import urlobject
23 import urlobject
24 from pylons import url
25
24
26 from rhodecode.api.tests.utils import (
25 from rhodecode.api.tests.utils import (
27 build_data, api_call, assert_error, assert_ok)
26 build_data, api_call, assert_error, assert_ok)
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.utils2 import safe_unicode
28 from rhodecode.lib.utils2 import safe_unicode
29
29
30 pytestmark = pytest.mark.backends("git", "hg")
30 pytestmark = pytest.mark.backends("git", "hg")
31
31
32
32
33 @pytest.mark.usefixtures("testuser_api", "app")
33 @pytest.mark.usefixtures("testuser_api", "app")
34 class TestGetPullRequest(object):
34 class TestGetPullRequest(object):
35
35
36 def test_api_get_pull_request(self, pr_util, http_host_only_stub):
36 def test_api_get_pull_request(self, pr_util, http_host_only_stub):
37 from rhodecode.model.pull_request import PullRequestModel
37 from rhodecode.model.pull_request import PullRequestModel
38 pull_request = pr_util.create_pull_request(mergeable=True)
38 pull_request = pr_util.create_pull_request(mergeable=True)
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey, 'get_pull_request',
40 self.apikey, 'get_pull_request',
41 repoid=pull_request.target_repo.repo_name,
41 repoid=pull_request.target_repo.repo_name,
42 pullrequestid=pull_request.pull_request_id)
42 pullrequestid=pull_request.pull_request_id)
43
43
44 response = api_call(self.app, params)
44 response = api_call(self.app, params)
45
45
46 assert response.status == '200 OK'
46 assert response.status == '200 OK'
47
47
48 url_obj = urlobject.URLObject(
48 url_obj = urlobject.URLObject(
49 url(
49 h.route_url(
50 'pullrequest_show',
50 'pullrequest_show',
51 repo_name=pull_request.target_repo.repo_name,
51 repo_name=pull_request.target_repo.repo_name,
52 pull_request_id=pull_request.pull_request_id, qualified=True))
52 pull_request_id=pull_request.pull_request_id))
53
53
54 pr_url = safe_unicode(
54 pr_url = safe_unicode(
55 url_obj.with_netloc(http_host_only_stub))
55 url_obj.with_netloc(http_host_only_stub))
56 source_url = safe_unicode(
56 source_url = safe_unicode(
57 pull_request.source_repo.clone_url().with_netloc(http_host_only_stub))
57 pull_request.source_repo.clone_url().with_netloc(http_host_only_stub))
58 target_url = safe_unicode(
58 target_url = safe_unicode(
59 pull_request.target_repo.clone_url().with_netloc(http_host_only_stub))
59 pull_request.target_repo.clone_url().with_netloc(http_host_only_stub))
60 shadow_url = safe_unicode(
60 shadow_url = safe_unicode(
61 PullRequestModel().get_shadow_clone_url(pull_request))
61 PullRequestModel().get_shadow_clone_url(pull_request))
62
62
63 expected = {
63 expected = {
64 'pull_request_id': pull_request.pull_request_id,
64 'pull_request_id': pull_request.pull_request_id,
65 'url': pr_url,
65 'url': pr_url,
66 'title': pull_request.title,
66 'title': pull_request.title,
67 'description': pull_request.description,
67 'description': pull_request.description,
68 'status': pull_request.status,
68 'status': pull_request.status,
69 'created_on': pull_request.created_on,
69 'created_on': pull_request.created_on,
70 'updated_on': pull_request.updated_on,
70 'updated_on': pull_request.updated_on,
71 'commit_ids': pull_request.revisions,
71 'commit_ids': pull_request.revisions,
72 'review_status': pull_request.calculated_review_status(),
72 'review_status': pull_request.calculated_review_status(),
73 'mergeable': {
73 'mergeable': {
74 'status': True,
74 'status': True,
75 'message': 'This pull request can be automatically merged.',
75 'message': 'This pull request can be automatically merged.',
76 },
76 },
77 'source': {
77 'source': {
78 'clone_url': source_url,
78 'clone_url': source_url,
79 'repository': pull_request.source_repo.repo_name,
79 'repository': pull_request.source_repo.repo_name,
80 'reference': {
80 'reference': {
81 'name': pull_request.source_ref_parts.name,
81 'name': pull_request.source_ref_parts.name,
82 'type': pull_request.source_ref_parts.type,
82 'type': pull_request.source_ref_parts.type,
83 'commit_id': pull_request.source_ref_parts.commit_id,
83 'commit_id': pull_request.source_ref_parts.commit_id,
84 },
84 },
85 },
85 },
86 'target': {
86 'target': {
87 'clone_url': target_url,
87 'clone_url': target_url,
88 'repository': pull_request.target_repo.repo_name,
88 'repository': pull_request.target_repo.repo_name,
89 'reference': {
89 'reference': {
90 'name': pull_request.target_ref_parts.name,
90 'name': pull_request.target_ref_parts.name,
91 'type': pull_request.target_ref_parts.type,
91 'type': pull_request.target_ref_parts.type,
92 'commit_id': pull_request.target_ref_parts.commit_id,
92 'commit_id': pull_request.target_ref_parts.commit_id,
93 },
93 },
94 },
94 },
95 'merge': {
95 'merge': {
96 'clone_url': shadow_url,
96 'clone_url': shadow_url,
97 'reference': {
97 'reference': {
98 'name': pull_request.shadow_merge_ref.name,
98 'name': pull_request.shadow_merge_ref.name,
99 'type': pull_request.shadow_merge_ref.type,
99 'type': pull_request.shadow_merge_ref.type,
100 'commit_id': pull_request.shadow_merge_ref.commit_id,
100 'commit_id': pull_request.shadow_merge_ref.commit_id,
101 },
101 },
102 },
102 },
103 'author': pull_request.author.get_api_data(include_secrets=False,
103 'author': pull_request.author.get_api_data(include_secrets=False,
104 details='basic'),
104 details='basic'),
105 'reviewers': [
105 'reviewers': [
106 {
106 {
107 'user': reviewer.get_api_data(include_secrets=False,
107 'user': reviewer.get_api_data(include_secrets=False,
108 details='basic'),
108 details='basic'),
109 'reasons': reasons,
109 'reasons': reasons,
110 'review_status': st[0][1].status if st else 'not_reviewed',
110 'review_status': st[0][1].status if st else 'not_reviewed',
111 }
111 }
112 for reviewer, reasons, mandatory, st in
112 for reviewer, reasons, mandatory, st in
113 pull_request.reviewers_statuses()
113 pull_request.reviewers_statuses()
114 ]
114 ]
115 }
115 }
116 assert_ok(id_, expected, response.body)
116 assert_ok(id_, expected, response.body)
117
117
118 def test_api_get_pull_request_repo_error(self):
118 def test_api_get_pull_request_repo_error(self):
119 id_, params = build_data(
119 id_, params = build_data(
120 self.apikey, 'get_pull_request',
120 self.apikey, 'get_pull_request',
121 repoid=666, pullrequestid=1)
121 repoid=666, pullrequestid=1)
122 response = api_call(self.app, params)
122 response = api_call(self.app, params)
123
123
124 expected = 'repository `666` does not exist'
124 expected = 'repository `666` does not exist'
125 assert_error(id_, expected, given=response.body)
125 assert_error(id_, expected, given=response.body)
126
126
127 def test_api_get_pull_request_pull_request_error(self):
127 def test_api_get_pull_request_pull_request_error(self):
128 id_, params = build_data(
128 id_, params = build_data(
129 self.apikey, 'get_pull_request',
129 self.apikey, 'get_pull_request',
130 repoid=1, pullrequestid=666)
130 repoid=1, pullrequestid=666)
131 response = api_call(self.app, params)
131 response = api_call(self.app, params)
132
132
133 expected = 'pull request `666` does not exist'
133 expected = 'pull request `666` does not exist'
134 assert_error(id_, expected, given=response.body)
134 assert_error(id_, expected, given=response.body)
@@ -1,312 +1,357 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.apps._base import add_route_with_slash
20 from rhodecode.apps._base import add_route_with_slash
21
21
22
22
23 def includeme(config):
23 def includeme(config):
24
24
25 # Summary
25 # Summary
26 # NOTE(marcink): one additional route is defined in very bottom, catch
26 # NOTE(marcink): one additional route is defined in very bottom, catch
27 # all pattern
27 # all pattern
28 config.add_route(
28 config.add_route(
29 name='repo_summary_explicit',
29 name='repo_summary_explicit',
30 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
30 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
31 config.add_route(
31 config.add_route(
32 name='repo_summary_commits',
32 name='repo_summary_commits',
33 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
33 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
34
34
35 # Commits
35 # Commits
36 config.add_route(
36 config.add_route(
37 name='repo_commit',
37 name='repo_commit',
38 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
38 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
39
39
40 config.add_route(
40 config.add_route(
41 name='repo_commit_children',
41 name='repo_commit_children',
42 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
42 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
43
43
44 config.add_route(
44 config.add_route(
45 name='repo_commit_parents',
45 name='repo_commit_parents',
46 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
46 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
47
47
48 config.add_route(
48 config.add_route(
49 name='repo_commit_raw',
49 name='repo_commit_raw',
50 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
50 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
51
51
52 config.add_route(
52 config.add_route(
53 name='repo_commit_patch',
53 name='repo_commit_patch',
54 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
54 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
55
55
56 config.add_route(
56 config.add_route(
57 name='repo_commit_download',
57 name='repo_commit_download',
58 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
58 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
59
59
60 config.add_route(
60 config.add_route(
61 name='repo_commit_data',
61 name='repo_commit_data',
62 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
62 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
63
63
64 config.add_route(
64 config.add_route(
65 name='repo_commit_comment_create',
65 name='repo_commit_comment_create',
66 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
66 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
67
67
68 config.add_route(
68 config.add_route(
69 name='repo_commit_comment_preview',
69 name='repo_commit_comment_preview',
70 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
70 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
71
71
72 config.add_route(
72 config.add_route(
73 name='repo_commit_comment_delete',
73 name='repo_commit_comment_delete',
74 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
74 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
75
75
76 # still working url for backward compat.
76 # still working url for backward compat.
77 config.add_route(
77 config.add_route(
78 name='repo_commit_raw_deprecated',
78 name='repo_commit_raw_deprecated',
79 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
79 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
80
80
81 # Files
81 # Files
82 config.add_route(
82 config.add_route(
83 name='repo_archivefile',
83 name='repo_archivefile',
84 pattern='/{repo_name:.*?[^/]}/archive/{fname}', repo_route=True)
84 pattern='/{repo_name:.*?[^/]}/archive/{fname}', repo_route=True)
85
85
86 config.add_route(
86 config.add_route(
87 name='repo_files_diff',
87 name='repo_files_diff',
88 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
88 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
89 config.add_route( # legacy route to make old links work
89 config.add_route( # legacy route to make old links work
90 name='repo_files_diff_2way_redirect',
90 name='repo_files_diff_2way_redirect',
91 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
91 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
92
92
93 config.add_route(
93 config.add_route(
94 name='repo_files',
94 name='repo_files',
95 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
95 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
96 config.add_route(
96 config.add_route(
97 name='repo_files:default_path',
97 name='repo_files:default_path',
98 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
98 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
99 config.add_route(
99 config.add_route(
100 name='repo_files:default_commit',
100 name='repo_files:default_commit',
101 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
101 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
102
102
103 config.add_route(
103 config.add_route(
104 name='repo_files:rendered',
104 name='repo_files:rendered',
105 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
105 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
106
106
107 config.add_route(
107 config.add_route(
108 name='repo_files:annotated',
108 name='repo_files:annotated',
109 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
109 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
110 config.add_route(
110 config.add_route(
111 name='repo_files:annotated_previous',
111 name='repo_files:annotated_previous',
112 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
112 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
113
113
114 config.add_route(
114 config.add_route(
115 name='repo_nodetree_full',
115 name='repo_nodetree_full',
116 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
116 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
117 config.add_route(
117 config.add_route(
118 name='repo_nodetree_full:default_path',
118 name='repo_nodetree_full:default_path',
119 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
119 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
120
120
121 config.add_route(
121 config.add_route(
122 name='repo_files_nodelist',
122 name='repo_files_nodelist',
123 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
123 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
124
124
125 config.add_route(
125 config.add_route(
126 name='repo_file_raw',
126 name='repo_file_raw',
127 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
127 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
128
128
129 config.add_route(
129 config.add_route(
130 name='repo_file_download',
130 name='repo_file_download',
131 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
131 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
132 config.add_route( # backward compat to keep old links working
132 config.add_route( # backward compat to keep old links working
133 name='repo_file_download:legacy',
133 name='repo_file_download:legacy',
134 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
134 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
135 repo_route=True)
135 repo_route=True)
136
136
137 config.add_route(
137 config.add_route(
138 name='repo_file_history',
138 name='repo_file_history',
139 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
139 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
140
140
141 config.add_route(
141 config.add_route(
142 name='repo_file_authors',
142 name='repo_file_authors',
143 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
143 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
144
144
145 config.add_route(
145 config.add_route(
146 name='repo_files_remove_file',
146 name='repo_files_remove_file',
147 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
147 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
148 repo_route=True)
148 repo_route=True)
149 config.add_route(
149 config.add_route(
150 name='repo_files_delete_file',
150 name='repo_files_delete_file',
151 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
151 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
152 repo_route=True)
152 repo_route=True)
153 config.add_route(
153 config.add_route(
154 name='repo_files_edit_file',
154 name='repo_files_edit_file',
155 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
155 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
156 repo_route=True)
156 repo_route=True)
157 config.add_route(
157 config.add_route(
158 name='repo_files_update_file',
158 name='repo_files_update_file',
159 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
159 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
160 repo_route=True)
160 repo_route=True)
161 config.add_route(
161 config.add_route(
162 name='repo_files_add_file',
162 name='repo_files_add_file',
163 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
163 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
164 repo_route=True)
164 repo_route=True)
165 config.add_route(
165 config.add_route(
166 name='repo_files_create_file',
166 name='repo_files_create_file',
167 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
167 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
168 repo_route=True)
168 repo_route=True)
169
169
170 # Refs data
170 # Refs data
171 config.add_route(
171 config.add_route(
172 name='repo_refs_data',
172 name='repo_refs_data',
173 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
173 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
174
174
175 config.add_route(
175 config.add_route(
176 name='repo_refs_changelog_data',
176 name='repo_refs_changelog_data',
177 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
177 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
178
178
179 config.add_route(
179 config.add_route(
180 name='repo_stats',
180 name='repo_stats',
181 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
181 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
182
182
183 # Changelog
183 # Changelog
184 config.add_route(
184 config.add_route(
185 name='repo_changelog',
185 name='repo_changelog',
186 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
186 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
187 config.add_route(
187 config.add_route(
188 name='repo_changelog_file',
188 name='repo_changelog_file',
189 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
189 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
190 config.add_route(
190 config.add_route(
191 name='repo_changelog_elements',
191 name='repo_changelog_elements',
192 pattern='/{repo_name:.*?[^/]}/changelog_elements', repo_route=True)
192 pattern='/{repo_name:.*?[^/]}/changelog_elements', repo_route=True)
193
193
194 # Compare
194 # Compare
195 config.add_route(
195 config.add_route(
196 name='repo_compare_select',
196 name='repo_compare_select',
197 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
197 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
198
198
199 config.add_route(
199 config.add_route(
200 name='repo_compare',
200 name='repo_compare',
201 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
201 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
202
202
203 # Tags
203 # Tags
204 config.add_route(
204 config.add_route(
205 name='tags_home',
205 name='tags_home',
206 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
206 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
207
207
208 # Branches
208 # Branches
209 config.add_route(
209 config.add_route(
210 name='branches_home',
210 name='branches_home',
211 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
211 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
212
212
213 config.add_route(
213 config.add_route(
214 name='bookmarks_home',
214 name='bookmarks_home',
215 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
215 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
216
216
217 # Pull Requests
217 # Pull Requests
218 config.add_route(
218 config.add_route(
219 name='pullrequest_show',
219 name='pullrequest_show',
220 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}',
220 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
221 repo_route=True)
221 repo_route=True)
222
222
223 config.add_route(
223 config.add_route(
224 name='pullrequest_show_all',
224 name='pullrequest_show_all',
225 pattern='/{repo_name:.*?[^/]}/pull-request',
225 pattern='/{repo_name:.*?[^/]}/pull-request',
226 repo_route=True, repo_accepted_types=['hg', 'git'])
226 repo_route=True, repo_accepted_types=['hg', 'git'])
227
227
228 config.add_route(
228 config.add_route(
229 name='pullrequest_show_all_data',
229 name='pullrequest_show_all_data',
230 pattern='/{repo_name:.*?[^/]}/pull-request-data',
230 pattern='/{repo_name:.*?[^/]}/pull-request-data',
231 repo_route=True, repo_accepted_types=['hg', 'git'])
231 repo_route=True, repo_accepted_types=['hg', 'git'])
232
232
233 config.add_route(
234 name='pullrequest_repo_refs',
235 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
236 repo_route=True)
237
238 config.add_route(
239 name='pullrequest_repo_destinations',
240 pattern='/{repo_name:.*?[^/]}/pull-request/repo-destinations',
241 repo_route=True)
242
243 config.add_route(
244 name='pullrequest_new',
245 pattern='/{repo_name:.*?[^/]}/pull-request/new',
246 repo_route=True, repo_accepted_types=['hg', 'git'])
247
248 config.add_route(
249 name='pullrequest_create',
250 pattern='/{repo_name:.*?[^/]}/pull-request/create',
251 repo_route=True, repo_accepted_types=['hg', 'git'])
252
253 config.add_route(
254 name='pullrequest_update',
255 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
256 repo_route=True)
257
258 config.add_route(
259 name='pullrequest_merge',
260 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
261 repo_route=True)
262
263 config.add_route(
264 name='pullrequest_delete',
265 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
266 repo_route=True)
267
268 config.add_route(
269 name='pullrequest_comment_create',
270 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
271 repo_route=True)
272
273 config.add_route(
274 name='pullrequest_comment_delete',
275 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
276 repo_route=True, repo_accepted_types=['hg', 'git'])
277
233 # Settings
278 # Settings
234 config.add_route(
279 config.add_route(
235 name='edit_repo',
280 name='edit_repo',
236 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
281 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
237
282
238 # Settings advanced
283 # Settings advanced
239 config.add_route(
284 config.add_route(
240 name='edit_repo_advanced',
285 name='edit_repo_advanced',
241 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
286 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
242 config.add_route(
287 config.add_route(
243 name='edit_repo_advanced_delete',
288 name='edit_repo_advanced_delete',
244 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
289 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
245 config.add_route(
290 config.add_route(
246 name='edit_repo_advanced_locking',
291 name='edit_repo_advanced_locking',
247 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
292 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
248 config.add_route(
293 config.add_route(
249 name='edit_repo_advanced_journal',
294 name='edit_repo_advanced_journal',
250 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
295 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
251 config.add_route(
296 config.add_route(
252 name='edit_repo_advanced_fork',
297 name='edit_repo_advanced_fork',
253 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
298 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
254
299
255 # Caches
300 # Caches
256 config.add_route(
301 config.add_route(
257 name='edit_repo_caches',
302 name='edit_repo_caches',
258 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
303 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
259
304
260 # Permissions
305 # Permissions
261 config.add_route(
306 config.add_route(
262 name='edit_repo_perms',
307 name='edit_repo_perms',
263 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
308 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
264
309
265 # Repo Review Rules
310 # Repo Review Rules
266 config.add_route(
311 config.add_route(
267 name='repo_reviewers',
312 name='repo_reviewers',
268 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
313 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
269
314
270 config.add_route(
315 config.add_route(
271 name='repo_default_reviewers_data',
316 name='repo_default_reviewers_data',
272 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
317 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
273
318
274 # Maintenance
319 # Maintenance
275 config.add_route(
320 config.add_route(
276 name='repo_maintenance',
321 name='repo_maintenance',
277 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
322 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
278
323
279 config.add_route(
324 config.add_route(
280 name='repo_maintenance_execute',
325 name='repo_maintenance_execute',
281 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
326 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
282
327
283 # Strip
328 # Strip
284 config.add_route(
329 config.add_route(
285 name='strip',
330 name='strip',
286 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
331 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
287
332
288 config.add_route(
333 config.add_route(
289 name='strip_check',
334 name='strip_check',
290 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
335 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
291
336
292 config.add_route(
337 config.add_route(
293 name='strip_execute',
338 name='strip_execute',
294 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
339 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
295
340
296 # ATOM/RSS Feed
341 # ATOM/RSS Feed
297 config.add_route(
342 config.add_route(
298 name='rss_feed_home',
343 name='rss_feed_home',
299 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
344 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
300
345
301 config.add_route(
346 config.add_route(
302 name='atom_feed_home',
347 name='atom_feed_home',
303 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
348 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
304
349
305 # NOTE(marcink): needs to be at the end for catch-all
350 # NOTE(marcink): needs to be at the end for catch-all
306 add_route_with_slash(
351 add_route_with_slash(
307 config,
352 config,
308 name='repo_summary',
353 name='repo_summary',
309 pattern='/{repo_name:.*?[^/]}', repo_route=True)
354 pattern='/{repo_name:.*?[^/]}', repo_route=True)
310
355
311 # Scan module for configuration decorators.
356 # Scan module for configuration decorators.
312 config.scan()
357 config.scan()
@@ -1,1106 +1,1112 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import mock
20 import mock
22 import pytest
21 import pytest
23 from webob.exc import HTTPNotFound
24
22
25 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
26 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
27 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
28 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
29 from rhodecode.model.db import (
28 from rhodecode.model.db import (
30 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
31 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
32 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
33 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
34 from rhodecode.tests import (
33 from rhodecode.tests import (
35 assert_session_flash, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36 from rhodecode.tests.utils import AssertResponse
35 from rhodecode.tests.utils import AssertResponse
37
36
38
37
39 def route_path(name, params=None, **kwargs):
38 def route_path(name, params=None, **kwargs):
40 import urllib
39 import urllib
41
40
42 base_url = {
41 base_url = {
43 'repo_changelog':'/{repo_name}/changelog',
42 'repo_changelog': '/{repo_name}/changelog',
44 'repo_changelog_file':'/{repo_name}/changelog/{commit_id}/{f_path}',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
45 'pullrequest_show_all': '/{repo_name}/pull-request',
46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
49 'pullrequest_new': '/{repo_name}/pull-request/new',
50 'pullrequest_create': '/{repo_name}/pull-request/create',
51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
45 }[name].format(**kwargs)
56 }[name].format(**kwargs)
46
57
47 if params:
58 if params:
48 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
49 return base_url
60 return base_url
50
61
51
62
52 @pytest.mark.usefixtures('app', 'autologin_user')
63 @pytest.mark.usefixtures('app', 'autologin_user')
53 @pytest.mark.backends("git", "hg")
64 @pytest.mark.backends("git", "hg")
54 class TestPullrequestsController(object):
65 class TestPullrequestsView(object):
55
66
56 def test_index(self, backend):
67 def test_index(self, backend):
57 self.app.get(url(
68 self.app.get(route_path(
58 controller='pullrequests', action='index',
69 'pullrequest_new',
59 repo_name=backend.repo_name))
70 repo_name=backend.repo_name))
60
71
61 def test_option_menu_create_pull_request_exists(self, backend):
72 def test_option_menu_create_pull_request_exists(self, backend):
62 repo_name = backend.repo_name
73 repo_name = backend.repo_name
63 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
64
75
65 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
66 'pullrequest', repo_name=repo_name)
77 'pullrequest_new', repo_name=repo_name)
67 response.mustcontain(create_pr_link)
78 response.mustcontain(create_pr_link)
68
79
69 def test_create_pr_form_with_raw_commit_id(self, backend):
80 def test_create_pr_form_with_raw_commit_id(self, backend):
70 repo = backend.repo
81 repo = backend.repo
71
82
72 self.app.get(
83 self.app.get(
73 url(controller='pullrequests', action='index',
84 route_path('pullrequest_new',
74 repo_name=repo.repo_name,
85 repo_name=repo.repo_name,
75 commit=repo.get_commit().raw_id),
86 commit=repo.get_commit().raw_id),
76 status=200)
87 status=200)
77
88
78 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
79 def test_show(self, pr_util, pr_merge_enabled):
90 def test_show(self, pr_util, pr_merge_enabled):
80 pull_request = pr_util.create_pull_request(
91 pull_request = pr_util.create_pull_request(
81 mergeable=pr_merge_enabled, enable_notifications=False)
92 mergeable=pr_merge_enabled, enable_notifications=False)
82
93
83 response = self.app.get(url(
94 response = self.app.get(route_path(
84 controller='pullrequests', action='show',
95 'pullrequest_show',
85 repo_name=pull_request.target_repo.scm_instance().name,
96 repo_name=pull_request.target_repo.scm_instance().name,
86 pull_request_id=str(pull_request.pull_request_id)))
97 pull_request_id=pull_request.pull_request_id))
87
98
88 for commit_id in pull_request.revisions:
99 for commit_id in pull_request.revisions:
89 response.mustcontain(commit_id)
100 response.mustcontain(commit_id)
90
101
91 assert pull_request.target_ref_parts.type in response
102 assert pull_request.target_ref_parts.type in response
92 assert pull_request.target_ref_parts.name in response
103 assert pull_request.target_ref_parts.name in response
93 target_clone_url = pull_request.target_repo.clone_url()
104 target_clone_url = pull_request.target_repo.clone_url()
94 assert target_clone_url in response
105 assert target_clone_url in response
95
106
96 assert 'class="pull-request-merge"' in response
107 assert 'class="pull-request-merge"' in response
97 assert (
108 assert (
98 'Server-side pull request merging is disabled.'
109 'Server-side pull request merging is disabled.'
99 in response) != pr_merge_enabled
110 in response) != pr_merge_enabled
100
111
101 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
112 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
102 # Logout
113 # Logout
103 response = self.app.post(
114 response = self.app.post(
104 h.route_path('logout'),
115 h.route_path('logout'),
105 params={'csrf_token': csrf_token})
116 params={'csrf_token': csrf_token})
106 # Login as regular user
117 # Login as regular user
107 response = self.app.post(h.route_path('login'),
118 response = self.app.post(h.route_path('login'),
108 {'username': TEST_USER_REGULAR_LOGIN,
119 {'username': TEST_USER_REGULAR_LOGIN,
109 'password': 'test12'})
120 'password': 'test12'})
110
121
111 pull_request = pr_util.create_pull_request(
122 pull_request = pr_util.create_pull_request(
112 author=TEST_USER_REGULAR_LOGIN)
123 author=TEST_USER_REGULAR_LOGIN)
113
124
114 response = self.app.get(url(
125 response = self.app.get(route_path(
115 controller='pullrequests', action='show',
126 'pullrequest_show',
116 repo_name=pull_request.target_repo.scm_instance().name,
127 repo_name=pull_request.target_repo.scm_instance().name,
117 pull_request_id=str(pull_request.pull_request_id)))
128 pull_request_id=pull_request.pull_request_id))
118
129
119 response.mustcontain('Server-side pull request merging is disabled.')
130 response.mustcontain('Server-side pull request merging is disabled.')
120
131
121 assert_response = response.assert_response()
132 assert_response = response.assert_response()
122 # for regular user without a merge permissions, we don't see it
133 # for regular user without a merge permissions, we don't see it
123 assert_response.no_element_exists('#close-pull-request-action')
134 assert_response.no_element_exists('#close-pull-request-action')
124
135
125 user_util.grant_user_permission_to_repo(
136 user_util.grant_user_permission_to_repo(
126 pull_request.target_repo,
137 pull_request.target_repo,
127 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
138 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
128 'repository.write')
139 'repository.write')
129 response = self.app.get(url(
140 response = self.app.get(route_path(
130 controller='pullrequests', action='show',
141 'pullrequest_show',
131 repo_name=pull_request.target_repo.scm_instance().name,
142 repo_name=pull_request.target_repo.scm_instance().name,
132 pull_request_id=str(pull_request.pull_request_id)))
143 pull_request_id=pull_request.pull_request_id))
133
144
134 response.mustcontain('Server-side pull request merging is disabled.')
145 response.mustcontain('Server-side pull request merging is disabled.')
135
146
136 assert_response = response.assert_response()
147 assert_response = response.assert_response()
137 # now regular user has a merge permissions, we have CLOSE button
148 # now regular user has a merge permissions, we have CLOSE button
138 assert_response.one_element_exists('#close-pull-request-action')
149 assert_response.one_element_exists('#close-pull-request-action')
139
150
140 def test_show_invalid_commit_id(self, pr_util):
151 def test_show_invalid_commit_id(self, pr_util):
141 # Simulating invalid revisions which will cause a lookup error
152 # Simulating invalid revisions which will cause a lookup error
142 pull_request = pr_util.create_pull_request()
153 pull_request = pr_util.create_pull_request()
143 pull_request.revisions = ['invalid']
154 pull_request.revisions = ['invalid']
144 Session().add(pull_request)
155 Session().add(pull_request)
145 Session().commit()
156 Session().commit()
146
157
147 response = self.app.get(url(
158 response = self.app.get(route_path(
148 controller='pullrequests', action='show',
159 'pullrequest_show',
149 repo_name=pull_request.target_repo.scm_instance().name,
160 repo_name=pull_request.target_repo.scm_instance().name,
150 pull_request_id=str(pull_request.pull_request_id)))
161 pull_request_id=pull_request.pull_request_id))
151
162
152 for commit_id in pull_request.revisions:
163 for commit_id in pull_request.revisions:
153 response.mustcontain(commit_id)
164 response.mustcontain(commit_id)
154
165
155 def test_show_invalid_source_reference(self, pr_util):
166 def test_show_invalid_source_reference(self, pr_util):
156 pull_request = pr_util.create_pull_request()
167 pull_request = pr_util.create_pull_request()
157 pull_request.source_ref = 'branch:b:invalid'
168 pull_request.source_ref = 'branch:b:invalid'
158 Session().add(pull_request)
169 Session().add(pull_request)
159 Session().commit()
170 Session().commit()
160
171
161 self.app.get(url(
172 self.app.get(route_path(
162 controller='pullrequests', action='show',
173 'pullrequest_show',
163 repo_name=pull_request.target_repo.scm_instance().name,
174 repo_name=pull_request.target_repo.scm_instance().name,
164 pull_request_id=str(pull_request.pull_request_id)))
175 pull_request_id=pull_request.pull_request_id))
165
176
166 def test_edit_title_description(self, pr_util, csrf_token):
177 def test_edit_title_description(self, pr_util, csrf_token):
167 pull_request = pr_util.create_pull_request()
178 pull_request = pr_util.create_pull_request()
168 pull_request_id = pull_request.pull_request_id
179 pull_request_id = pull_request.pull_request_id
169
180
170 response = self.app.post(
181 response = self.app.post(
171 url(controller='pullrequests', action='update',
182 route_path('pullrequest_update',
172 repo_name=pull_request.target_repo.repo_name,
183 repo_name=pull_request.target_repo.repo_name,
173 pull_request_id=str(pull_request_id)),
184 pull_request_id=pull_request_id),
174 params={
185 params={
175 'edit_pull_request': 'true',
186 'edit_pull_request': 'true',
176 '_method': 'put',
177 'title': 'New title',
187 'title': 'New title',
178 'description': 'New description',
188 'description': 'New description',
179 'csrf_token': csrf_token})
189 'csrf_token': csrf_token})
180
190
181 assert_session_flash(
191 assert_session_flash(
182 response, u'Pull request title & description updated.',
192 response, u'Pull request title & description updated.',
183 category='success')
193 category='success')
184
194
185 pull_request = PullRequest.get(pull_request_id)
195 pull_request = PullRequest.get(pull_request_id)
186 assert pull_request.title == 'New title'
196 assert pull_request.title == 'New title'
187 assert pull_request.description == 'New description'
197 assert pull_request.description == 'New description'
188
198
189 def test_edit_title_description_closed(self, pr_util, csrf_token):
199 def test_edit_title_description_closed(self, pr_util, csrf_token):
190 pull_request = pr_util.create_pull_request()
200 pull_request = pr_util.create_pull_request()
191 pull_request_id = pull_request.pull_request_id
201 pull_request_id = pull_request.pull_request_id
192 pr_util.close()
202 pr_util.close()
193
203
194 response = self.app.post(
204 response = self.app.post(
195 url(controller='pullrequests', action='update',
205 route_path('pullrequest_update',
196 repo_name=pull_request.target_repo.repo_name,
206 repo_name=pull_request.target_repo.repo_name,
197 pull_request_id=str(pull_request_id)),
207 pull_request_id=pull_request_id),
198 params={
208 params={
199 'edit_pull_request': 'true',
209 'edit_pull_request': 'true',
200 '_method': 'put',
201 'title': 'New title',
210 'title': 'New title',
202 'description': 'New description',
211 'description': 'New description',
203 'csrf_token': csrf_token})
212 'csrf_token': csrf_token})
204
213
205 assert_session_flash(
214 assert_session_flash(
206 response, u'Cannot update closed pull requests.',
215 response, u'Cannot update closed pull requests.',
207 category='error')
216 category='error')
208
217
209 def test_update_invalid_source_reference(self, pr_util, csrf_token):
218 def test_update_invalid_source_reference(self, pr_util, csrf_token):
210 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
219 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
211
220
212 pull_request = pr_util.create_pull_request()
221 pull_request = pr_util.create_pull_request()
213 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
222 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
214 Session().add(pull_request)
223 Session().add(pull_request)
215 Session().commit()
224 Session().commit()
216
225
217 pull_request_id = pull_request.pull_request_id
226 pull_request_id = pull_request.pull_request_id
218
227
219 response = self.app.post(
228 response = self.app.post(
220 url(controller='pullrequests', action='update',
229 route_path('pullrequest_update',
221 repo_name=pull_request.target_repo.repo_name,
230 repo_name=pull_request.target_repo.repo_name,
222 pull_request_id=str(pull_request_id)),
231 pull_request_id=pull_request_id),
223 params={'update_commits': 'true', '_method': 'put',
232 params={'update_commits': 'true',
224 'csrf_token': csrf_token})
233 'csrf_token': csrf_token})
225
234
226 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
235 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
227 UpdateFailureReason.MISSING_SOURCE_REF]
236 UpdateFailureReason.MISSING_SOURCE_REF]
228 assert_session_flash(response, expected_msg, category='error')
237 assert_session_flash(response, expected_msg, category='error')
229
238
230 def test_missing_target_reference(self, pr_util, csrf_token):
239 def test_missing_target_reference(self, pr_util, csrf_token):
231 from rhodecode.lib.vcs.backends.base import MergeFailureReason
240 from rhodecode.lib.vcs.backends.base import MergeFailureReason
232 pull_request = pr_util.create_pull_request(
241 pull_request = pr_util.create_pull_request(
233 approved=True, mergeable=True)
242 approved=True, mergeable=True)
234 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
243 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
235 Session().add(pull_request)
244 Session().add(pull_request)
236 Session().commit()
245 Session().commit()
237
246
238 pull_request_id = pull_request.pull_request_id
247 pull_request_id = pull_request.pull_request_id
239 pull_request_url = url(
248 pull_request_url = route_path(
240 controller='pullrequests', action='show',
249 'pullrequest_show',
241 repo_name=pull_request.target_repo.repo_name,
250 repo_name=pull_request.target_repo.repo_name,
242 pull_request_id=str(pull_request_id))
251 pull_request_id=pull_request_id)
243
252
244 response = self.app.get(pull_request_url)
253 response = self.app.get(pull_request_url)
245
254
246 assertr = AssertResponse(response)
255 assertr = AssertResponse(response)
247 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
256 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
248 MergeFailureReason.MISSING_TARGET_REF]
257 MergeFailureReason.MISSING_TARGET_REF]
249 assertr.element_contains(
258 assertr.element_contains(
250 'span[data-role="merge-message"]', str(expected_msg))
259 'span[data-role="merge-message"]', str(expected_msg))
251
260
252 def test_comment_and_close_pull_request_custom_message_approved(
261 def test_comment_and_close_pull_request_custom_message_approved(
253 self, pr_util, csrf_token, xhr_header):
262 self, pr_util, csrf_token, xhr_header):
254
263
255 pull_request = pr_util.create_pull_request(approved=True)
264 pull_request = pr_util.create_pull_request(approved=True)
256 pull_request_id = pull_request.pull_request_id
265 pull_request_id = pull_request.pull_request_id
257 author = pull_request.user_id
266 author = pull_request.user_id
258 repo = pull_request.target_repo.repo_id
267 repo = pull_request.target_repo.repo_id
259
268
260 self.app.post(
269 self.app.post(
261 url(controller='pullrequests',
270 route_path('pullrequest_comment_create',
262 action='comment',
263 repo_name=pull_request.target_repo.scm_instance().name,
271 repo_name=pull_request.target_repo.scm_instance().name,
264 pull_request_id=str(pull_request_id)),
272 pull_request_id=pull_request_id),
265 params={
273 params={
266 'close_pull_request': '1',
274 'close_pull_request': '1',
267 'text': 'Closing a PR',
275 'text': 'Closing a PR',
268 'csrf_token': csrf_token},
276 'csrf_token': csrf_token},
269 extra_environ=xhr_header,)
277 extra_environ=xhr_header,)
270
278
271 journal = UserLog.query()\
279 journal = UserLog.query()\
272 .filter(UserLog.user_id == author)\
280 .filter(UserLog.user_id == author)\
273 .filter(UserLog.repository_id == repo) \
281 .filter(UserLog.repository_id == repo) \
274 .order_by('user_log_id') \
282 .order_by('user_log_id') \
275 .all()
283 .all()
276 assert journal[-1].action == 'repo.pull_request.close'
284 assert journal[-1].action == 'repo.pull_request.close'
277
285
278 pull_request = PullRequest.get(pull_request_id)
286 pull_request = PullRequest.get(pull_request_id)
279 assert pull_request.is_closed()
287 assert pull_request.is_closed()
280
288
281 status = ChangesetStatusModel().get_status(
289 status = ChangesetStatusModel().get_status(
282 pull_request.source_repo, pull_request=pull_request)
290 pull_request.source_repo, pull_request=pull_request)
283 assert status == ChangesetStatus.STATUS_APPROVED
291 assert status == ChangesetStatus.STATUS_APPROVED
284 comments = ChangesetComment().query() \
292 comments = ChangesetComment().query() \
285 .filter(ChangesetComment.pull_request == pull_request) \
293 .filter(ChangesetComment.pull_request == pull_request) \
286 .order_by(ChangesetComment.comment_id.asc())\
294 .order_by(ChangesetComment.comment_id.asc())\
287 .all()
295 .all()
288 assert comments[-1].text == 'Closing a PR'
296 assert comments[-1].text == 'Closing a PR'
289
297
290 def test_comment_force_close_pull_request_rejected(
298 def test_comment_force_close_pull_request_rejected(
291 self, pr_util, csrf_token, xhr_header):
299 self, pr_util, csrf_token, xhr_header):
292 pull_request = pr_util.create_pull_request()
300 pull_request = pr_util.create_pull_request()
293 pull_request_id = pull_request.pull_request_id
301 pull_request_id = pull_request.pull_request_id
294 PullRequestModel().update_reviewers(
302 PullRequestModel().update_reviewers(
295 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
303 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
296 pull_request.author)
304 pull_request.author)
297 author = pull_request.user_id
305 author = pull_request.user_id
298 repo = pull_request.target_repo.repo_id
306 repo = pull_request.target_repo.repo_id
299
307
300 self.app.post(
308 self.app.post(
301 url(controller='pullrequests',
309 route_path('pullrequest_comment_create',
302 action='comment',
303 repo_name=pull_request.target_repo.scm_instance().name,
310 repo_name=pull_request.target_repo.scm_instance().name,
304 pull_request_id=str(pull_request_id)),
311 pull_request_id=pull_request_id),
305 params={
312 params={
306 'close_pull_request': '1',
313 'close_pull_request': '1',
307 'csrf_token': csrf_token},
314 'csrf_token': csrf_token},
308 extra_environ=xhr_header)
315 extra_environ=xhr_header)
309
316
310 pull_request = PullRequest.get(pull_request_id)
317 pull_request = PullRequest.get(pull_request_id)
311
318
312 journal = UserLog.query()\
319 journal = UserLog.query()\
313 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
320 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
314 .order_by('user_log_id') \
321 .order_by('user_log_id') \
315 .all()
322 .all()
316 assert journal[-1].action == 'repo.pull_request.close'
323 assert journal[-1].action == 'repo.pull_request.close'
317
324
318 # check only the latest status, not the review status
325 # check only the latest status, not the review status
319 status = ChangesetStatusModel().get_status(
326 status = ChangesetStatusModel().get_status(
320 pull_request.source_repo, pull_request=pull_request)
327 pull_request.source_repo, pull_request=pull_request)
321 assert status == ChangesetStatus.STATUS_REJECTED
328 assert status == ChangesetStatus.STATUS_REJECTED
322
329
323 def test_comment_and_close_pull_request(
330 def test_comment_and_close_pull_request(
324 self, pr_util, csrf_token, xhr_header):
331 self, pr_util, csrf_token, xhr_header):
325 pull_request = pr_util.create_pull_request()
332 pull_request = pr_util.create_pull_request()
326 pull_request_id = pull_request.pull_request_id
333 pull_request_id = pull_request.pull_request_id
327
334
328 response = self.app.post(
335 response = self.app.post(
329 url(controller='pullrequests',
336 route_path('pullrequest_comment_create',
330 action='comment',
331 repo_name=pull_request.target_repo.scm_instance().name,
337 repo_name=pull_request.target_repo.scm_instance().name,
332 pull_request_id=str(pull_request.pull_request_id)),
338 pull_request_id=pull_request.pull_request_id),
333 params={
339 params={
334 'close_pull_request': 'true',
340 'close_pull_request': 'true',
335 'csrf_token': csrf_token},
341 'csrf_token': csrf_token},
336 extra_environ=xhr_header)
342 extra_environ=xhr_header)
337
343
338 assert response.json
344 assert response.json
339
345
340 pull_request = PullRequest.get(pull_request_id)
346 pull_request = PullRequest.get(pull_request_id)
341 assert pull_request.is_closed()
347 assert pull_request.is_closed()
342
348
343 # check only the latest status, not the review status
349 # check only the latest status, not the review status
344 status = ChangesetStatusModel().get_status(
350 status = ChangesetStatusModel().get_status(
345 pull_request.source_repo, pull_request=pull_request)
351 pull_request.source_repo, pull_request=pull_request)
346 assert status == ChangesetStatus.STATUS_REJECTED
352 assert status == ChangesetStatus.STATUS_REJECTED
347
353
348 def test_create_pull_request(self, backend, csrf_token):
354 def test_create_pull_request(self, backend, csrf_token):
349 commits = [
355 commits = [
350 {'message': 'ancestor'},
356 {'message': 'ancestor'},
351 {'message': 'change'},
357 {'message': 'change'},
352 {'message': 'change2'},
358 {'message': 'change2'},
353 ]
359 ]
354 commit_ids = backend.create_master_repo(commits)
360 commit_ids = backend.create_master_repo(commits)
355 target = backend.create_repo(heads=['ancestor'])
361 target = backend.create_repo(heads=['ancestor'])
356 source = backend.create_repo(heads=['change2'])
362 source = backend.create_repo(heads=['change2'])
357
363
358 response = self.app.post(
364 response = self.app.post(
359 url(
365 route_path('pullrequest_create', repo_name=source.repo_name),
360 controller='pullrequests',
361 action='create',
362 repo_name=source.repo_name
363 ),
364 [
366 [
365 ('source_repo', source.repo_name),
367 ('source_repo', source.repo_name),
366 ('source_ref', 'branch:default:' + commit_ids['change2']),
368 ('source_ref', 'branch:default:' + commit_ids['change2']),
367 ('target_repo', target.repo_name),
369 ('target_repo', target.repo_name),
368 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
370 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
369 ('common_ancestor', commit_ids['ancestor']),
371 ('common_ancestor', commit_ids['ancestor']),
370 ('pullrequest_desc', 'Description'),
372 ('pullrequest_desc', 'Description'),
371 ('pullrequest_title', 'Title'),
373 ('pullrequest_title', 'Title'),
372 ('__start__', 'review_members:sequence'),
374 ('__start__', 'review_members:sequence'),
373 ('__start__', 'reviewer:mapping'),
375 ('__start__', 'reviewer:mapping'),
374 ('user_id', '1'),
376 ('user_id', '1'),
375 ('__start__', 'reasons:sequence'),
377 ('__start__', 'reasons:sequence'),
376 ('reason', 'Some reason'),
378 ('reason', 'Some reason'),
377 ('__end__', 'reasons:sequence'),
379 ('__end__', 'reasons:sequence'),
378 ('mandatory', 'False'),
380 ('mandatory', 'False'),
379 ('__end__', 'reviewer:mapping'),
381 ('__end__', 'reviewer:mapping'),
380 ('__end__', 'review_members:sequence'),
382 ('__end__', 'review_members:sequence'),
381 ('__start__', 'revisions:sequence'),
383 ('__start__', 'revisions:sequence'),
382 ('revisions', commit_ids['change']),
384 ('revisions', commit_ids['change']),
383 ('revisions', commit_ids['change2']),
385 ('revisions', commit_ids['change2']),
384 ('__end__', 'revisions:sequence'),
386 ('__end__', 'revisions:sequence'),
385 ('user', ''),
387 ('user', ''),
386 ('csrf_token', csrf_token),
388 ('csrf_token', csrf_token),
387 ],
389 ],
388 status=302)
390 status=302)
389
391
390 location = response.headers['Location']
392 location = response.headers['Location']
391 pull_request_id = location.rsplit('/', 1)[1]
393 pull_request_id = location.rsplit('/', 1)[1]
392 assert pull_request_id != 'new'
394 assert pull_request_id != 'new'
393 pull_request = PullRequest.get(int(pull_request_id))
395 pull_request = PullRequest.get(int(pull_request_id))
394
396
395 # check that we have now both revisions
397 # check that we have now both revisions
396 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
398 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
397 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
399 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
398 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
400 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
399 assert pull_request.target_ref == expected_target_ref
401 assert pull_request.target_ref == expected_target_ref
400
402
401 def test_reviewer_notifications(self, backend, csrf_token):
403 def test_reviewer_notifications(self, backend, csrf_token):
402 # We have to use the app.post for this test so it will create the
404 # We have to use the app.post for this test so it will create the
403 # notifications properly with the new PR
405 # notifications properly with the new PR
404 commits = [
406 commits = [
405 {'message': 'ancestor',
407 {'message': 'ancestor',
406 'added': [FileNode('file_A', content='content_of_ancestor')]},
408 'added': [FileNode('file_A', content='content_of_ancestor')]},
407 {'message': 'change',
409 {'message': 'change',
408 'added': [FileNode('file_a', content='content_of_change')]},
410 'added': [FileNode('file_a', content='content_of_change')]},
409 {'message': 'change-child'},
411 {'message': 'change-child'},
410 {'message': 'ancestor-child', 'parents': ['ancestor'],
412 {'message': 'ancestor-child', 'parents': ['ancestor'],
411 'added': [
413 'added': [
412 FileNode('file_B', content='content_of_ancestor_child')]},
414 FileNode('file_B', content='content_of_ancestor_child')]},
413 {'message': 'ancestor-child-2'},
415 {'message': 'ancestor-child-2'},
414 ]
416 ]
415 commit_ids = backend.create_master_repo(commits)
417 commit_ids = backend.create_master_repo(commits)
416 target = backend.create_repo(heads=['ancestor-child'])
418 target = backend.create_repo(heads=['ancestor-child'])
417 source = backend.create_repo(heads=['change'])
419 source = backend.create_repo(heads=['change'])
418
420
419 response = self.app.post(
421 response = self.app.post(
420 url(
422 route_path('pullrequest_create', repo_name=source.repo_name),
421 controller='pullrequests',
422 action='create',
423 repo_name=source.repo_name
424 ),
425 [
423 [
426 ('source_repo', source.repo_name),
424 ('source_repo', source.repo_name),
427 ('source_ref', 'branch:default:' + commit_ids['change']),
425 ('source_ref', 'branch:default:' + commit_ids['change']),
428 ('target_repo', target.repo_name),
426 ('target_repo', target.repo_name),
429 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
427 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
430 ('common_ancestor', commit_ids['ancestor']),
428 ('common_ancestor', commit_ids['ancestor']),
431 ('pullrequest_desc', 'Description'),
429 ('pullrequest_desc', 'Description'),
432 ('pullrequest_title', 'Title'),
430 ('pullrequest_title', 'Title'),
433 ('__start__', 'review_members:sequence'),
431 ('__start__', 'review_members:sequence'),
434 ('__start__', 'reviewer:mapping'),
432 ('__start__', 'reviewer:mapping'),
435 ('user_id', '2'),
433 ('user_id', '2'),
436 ('__start__', 'reasons:sequence'),
434 ('__start__', 'reasons:sequence'),
437 ('reason', 'Some reason'),
435 ('reason', 'Some reason'),
438 ('__end__', 'reasons:sequence'),
436 ('__end__', 'reasons:sequence'),
439 ('mandatory', 'False'),
437 ('mandatory', 'False'),
440 ('__end__', 'reviewer:mapping'),
438 ('__end__', 'reviewer:mapping'),
441 ('__end__', 'review_members:sequence'),
439 ('__end__', 'review_members:sequence'),
442 ('__start__', 'revisions:sequence'),
440 ('__start__', 'revisions:sequence'),
443 ('revisions', commit_ids['change']),
441 ('revisions', commit_ids['change']),
444 ('__end__', 'revisions:sequence'),
442 ('__end__', 'revisions:sequence'),
445 ('user', ''),
443 ('user', ''),
446 ('csrf_token', csrf_token),
444 ('csrf_token', csrf_token),
447 ],
445 ],
448 status=302)
446 status=302)
449
447
450 location = response.headers['Location']
448 location = response.headers['Location']
451
449
452 pull_request_id = location.rsplit('/', 1)[1]
450 pull_request_id = location.rsplit('/', 1)[1]
453 assert pull_request_id != 'new'
451 assert pull_request_id != 'new'
454 pull_request = PullRequest.get(int(pull_request_id))
452 pull_request = PullRequest.get(int(pull_request_id))
455
453
456 # Check that a notification was made
454 # Check that a notification was made
457 notifications = Notification.query()\
455 notifications = Notification.query()\
458 .filter(Notification.created_by == pull_request.author.user_id,
456 .filter(Notification.created_by == pull_request.author.user_id,
459 Notification.type_ == Notification.TYPE_PULL_REQUEST,
457 Notification.type_ == Notification.TYPE_PULL_REQUEST,
460 Notification.subject.contains(
458 Notification.subject.contains(
461 "wants you to review pull request #%s" % pull_request_id))
459 "wants you to review pull request #%s" % pull_request_id))
462 assert len(notifications.all()) == 1
460 assert len(notifications.all()) == 1
463
461
464 # Change reviewers and check that a notification was made
462 # Change reviewers and check that a notification was made
465 PullRequestModel().update_reviewers(
463 PullRequestModel().update_reviewers(
466 pull_request.pull_request_id, [(1, [], False)],
464 pull_request.pull_request_id, [(1, [], False)],
467 pull_request.author)
465 pull_request.author)
468 assert len(notifications.all()) == 2
466 assert len(notifications.all()) == 2
469
467
470 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
468 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
471 csrf_token):
469 csrf_token):
472 commits = [
470 commits = [
473 {'message': 'ancestor',
471 {'message': 'ancestor',
474 'added': [FileNode('file_A', content='content_of_ancestor')]},
472 'added': [FileNode('file_A', content='content_of_ancestor')]},
475 {'message': 'change',
473 {'message': 'change',
476 'added': [FileNode('file_a', content='content_of_change')]},
474 'added': [FileNode('file_a', content='content_of_change')]},
477 {'message': 'change-child'},
475 {'message': 'change-child'},
478 {'message': 'ancestor-child', 'parents': ['ancestor'],
476 {'message': 'ancestor-child', 'parents': ['ancestor'],
479 'added': [
477 'added': [
480 FileNode('file_B', content='content_of_ancestor_child')]},
478 FileNode('file_B', content='content_of_ancestor_child')]},
481 {'message': 'ancestor-child-2'},
479 {'message': 'ancestor-child-2'},
482 ]
480 ]
483 commit_ids = backend.create_master_repo(commits)
481 commit_ids = backend.create_master_repo(commits)
484 target = backend.create_repo(heads=['ancestor-child'])
482 target = backend.create_repo(heads=['ancestor-child'])
485 source = backend.create_repo(heads=['change'])
483 source = backend.create_repo(heads=['change'])
486
484
487 response = self.app.post(
485 response = self.app.post(
488 url(
486 route_path('pullrequest_create', repo_name=source.repo_name),
489 controller='pullrequests',
490 action='create',
491 repo_name=source.repo_name
492 ),
493 [
487 [
494 ('source_repo', source.repo_name),
488 ('source_repo', source.repo_name),
495 ('source_ref', 'branch:default:' + commit_ids['change']),
489 ('source_ref', 'branch:default:' + commit_ids['change']),
496 ('target_repo', target.repo_name),
490 ('target_repo', target.repo_name),
497 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
491 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
498 ('common_ancestor', commit_ids['ancestor']),
492 ('common_ancestor', commit_ids['ancestor']),
499 ('pullrequest_desc', 'Description'),
493 ('pullrequest_desc', 'Description'),
500 ('pullrequest_title', 'Title'),
494 ('pullrequest_title', 'Title'),
501 ('__start__', 'review_members:sequence'),
495 ('__start__', 'review_members:sequence'),
502 ('__start__', 'reviewer:mapping'),
496 ('__start__', 'reviewer:mapping'),
503 ('user_id', '1'),
497 ('user_id', '1'),
504 ('__start__', 'reasons:sequence'),
498 ('__start__', 'reasons:sequence'),
505 ('reason', 'Some reason'),
499 ('reason', 'Some reason'),
506 ('__end__', 'reasons:sequence'),
500 ('__end__', 'reasons:sequence'),
507 ('mandatory', 'False'),
501 ('mandatory', 'False'),
508 ('__end__', 'reviewer:mapping'),
502 ('__end__', 'reviewer:mapping'),
509 ('__end__', 'review_members:sequence'),
503 ('__end__', 'review_members:sequence'),
510 ('__start__', 'revisions:sequence'),
504 ('__start__', 'revisions:sequence'),
511 ('revisions', commit_ids['change']),
505 ('revisions', commit_ids['change']),
512 ('__end__', 'revisions:sequence'),
506 ('__end__', 'revisions:sequence'),
513 ('user', ''),
507 ('user', ''),
514 ('csrf_token', csrf_token),
508 ('csrf_token', csrf_token),
515 ],
509 ],
516 status=302)
510 status=302)
517
511
518 location = response.headers['Location']
512 location = response.headers['Location']
519
513
520 pull_request_id = location.rsplit('/', 1)[1]
514 pull_request_id = location.rsplit('/', 1)[1]
521 assert pull_request_id != 'new'
515 assert pull_request_id != 'new'
522 pull_request = PullRequest.get(int(pull_request_id))
516 pull_request = PullRequest.get(int(pull_request_id))
523
517
524 # target_ref has to point to the ancestor's commit_id in order to
518 # target_ref has to point to the ancestor's commit_id in order to
525 # show the correct diff
519 # show the correct diff
526 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
520 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
527 assert pull_request.target_ref == expected_target_ref
521 assert pull_request.target_ref == expected_target_ref
528
522
529 # Check generated diff contents
523 # Check generated diff contents
530 response = response.follow()
524 response = response.follow()
531 assert 'content_of_ancestor' not in response.body
525 assert 'content_of_ancestor' not in response.body
532 assert 'content_of_ancestor-child' not in response.body
526 assert 'content_of_ancestor-child' not in response.body
533 assert 'content_of_change' in response.body
527 assert 'content_of_change' in response.body
534
528
535 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
529 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
536 # Clear any previous calls to rcextensions
530 # Clear any previous calls to rcextensions
537 rhodecode.EXTENSIONS.calls.clear()
531 rhodecode.EXTENSIONS.calls.clear()
538
532
539 pull_request = pr_util.create_pull_request(
533 pull_request = pr_util.create_pull_request(
540 approved=True, mergeable=True)
534 approved=True, mergeable=True)
541 pull_request_id = pull_request.pull_request_id
535 pull_request_id = pull_request.pull_request_id
542 repo_name = pull_request.target_repo.scm_instance().name,
536 repo_name = pull_request.target_repo.scm_instance().name,
543
537
544 response = self.app.post(
538 response = self.app.post(
545 url(controller='pullrequests',
539 route_path('pullrequest_merge',
546 action='merge',
547 repo_name=str(repo_name[0]),
540 repo_name=str(repo_name[0]),
548 pull_request_id=str(pull_request_id)),
541 pull_request_id=pull_request_id),
549 params={'csrf_token': csrf_token}).follow()
542 params={'csrf_token': csrf_token}).follow()
550
543
551 pull_request = PullRequest.get(pull_request_id)
544 pull_request = PullRequest.get(pull_request_id)
552
545
553 assert response.status_int == 200
546 assert response.status_int == 200
554 assert pull_request.is_closed()
547 assert pull_request.is_closed()
555 assert_pull_request_status(
548 assert_pull_request_status(
556 pull_request, ChangesetStatus.STATUS_APPROVED)
549 pull_request, ChangesetStatus.STATUS_APPROVED)
557
550
558 # Check the relevant log entries were added
551 # Check the relevant log entries were added
559 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
552 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
560 actions = [log.action for log in user_logs]
553 actions = [log.action for log in user_logs]
561 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
554 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
562 expected_actions = [
555 expected_actions = [
563 u'repo.pull_request.close',
556 u'repo.pull_request.close',
564 u'repo.pull_request.merge',
557 u'repo.pull_request.merge',
565 u'repo.pull_request.comment.create'
558 u'repo.pull_request.comment.create'
566 ]
559 ]
567 assert actions == expected_actions
560 assert actions == expected_actions
568
561
569 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
562 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
570 actions = [log for log in user_logs]
563 actions = [log for log in user_logs]
571 assert actions[-1].action == 'user.push'
564 assert actions[-1].action == 'user.push'
572 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
565 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
573
566
574 # Check post_push rcextension was really executed
567 # Check post_push rcextension was really executed
575 push_calls = rhodecode.EXTENSIONS.calls['post_push']
568 push_calls = rhodecode.EXTENSIONS.calls['post_push']
576 assert len(push_calls) == 1
569 assert len(push_calls) == 1
577 unused_last_call_args, last_call_kwargs = push_calls[0]
570 unused_last_call_args, last_call_kwargs = push_calls[0]
578 assert last_call_kwargs['action'] == 'push'
571 assert last_call_kwargs['action'] == 'push'
579 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
572 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
580
573
581 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
574 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
582 pull_request = pr_util.create_pull_request(mergeable=False)
575 pull_request = pr_util.create_pull_request(mergeable=False)
583 pull_request_id = pull_request.pull_request_id
576 pull_request_id = pull_request.pull_request_id
584 pull_request = PullRequest.get(pull_request_id)
577 pull_request = PullRequest.get(pull_request_id)
585
578
586 response = self.app.post(
579 response = self.app.post(
587 url(controller='pullrequests',
580 route_path('pullrequest_merge',
588 action='merge',
589 repo_name=pull_request.target_repo.scm_instance().name,
581 repo_name=pull_request.target_repo.scm_instance().name,
590 pull_request_id=str(pull_request.pull_request_id)),
582 pull_request_id=pull_request.pull_request_id),
591 params={'csrf_token': csrf_token}).follow()
583 params={'csrf_token': csrf_token}).follow()
592
584
593 assert response.status_int == 200
585 assert response.status_int == 200
594 response.mustcontain(
586 response.mustcontain(
595 'Merge is not currently possible because of below failed checks.')
587 'Merge is not currently possible because of below failed checks.')
596 response.mustcontain('Server-side pull request merging is disabled.')
588 response.mustcontain('Server-side pull request merging is disabled.')
597
589
598 @pytest.mark.skip_backends('svn')
590 @pytest.mark.skip_backends('svn')
599 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
591 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
600 pull_request = pr_util.create_pull_request(mergeable=True)
592 pull_request = pr_util.create_pull_request(mergeable=True)
601 pull_request_id = pull_request.pull_request_id
593 pull_request_id = pull_request.pull_request_id
602 repo_name = pull_request.target_repo.scm_instance().name,
594 repo_name = pull_request.target_repo.scm_instance().name
603
595
604 response = self.app.post(
596 response = self.app.post(
605 url(controller='pullrequests',
597 route_path('pullrequest_merge',
606 action='merge',
598 repo_name=repo_name,
607 repo_name=str(repo_name[0]),
599 pull_request_id=pull_request_id),
608 pull_request_id=str(pull_request_id)),
609 params={'csrf_token': csrf_token}).follow()
600 params={'csrf_token': csrf_token}).follow()
610
601
611 assert response.status_int == 200
602 assert response.status_int == 200
612
603
613 response.mustcontain(
604 response.mustcontain(
614 'Merge is not currently possible because of below failed checks.')
605 'Merge is not currently possible because of below failed checks.')
615 response.mustcontain('Pull request reviewer approval is pending.')
606 response.mustcontain('Pull request reviewer approval is pending.')
616
607
608 def test_merge_pull_request_renders_failure_reason(
609 self, user_regular, csrf_token, pr_util):
610 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
611 pull_request_id = pull_request.pull_request_id
612 repo_name = pull_request.target_repo.scm_instance().name
613
614 model_patcher = mock.patch.multiple(
615 PullRequestModel,
616 merge=mock.Mock(return_value=MergeResponse(
617 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
618 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
619
620 with model_patcher:
621 response = self.app.post(
622 route_path('pullrequest_merge',
623 repo_name=repo_name,
624 pull_request_id=pull_request_id),
625 params={'csrf_token': csrf_token}, status=302)
626
627 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
628 MergeFailureReason.PUSH_FAILED])
629
617 def test_update_source_revision(self, backend, csrf_token):
630 def test_update_source_revision(self, backend, csrf_token):
618 commits = [
631 commits = [
619 {'message': 'ancestor'},
632 {'message': 'ancestor'},
620 {'message': 'change'},
633 {'message': 'change'},
621 {'message': 'change-2'},
634 {'message': 'change-2'},
622 ]
635 ]
623 commit_ids = backend.create_master_repo(commits)
636 commit_ids = backend.create_master_repo(commits)
624 target = backend.create_repo(heads=['ancestor'])
637 target = backend.create_repo(heads=['ancestor'])
625 source = backend.create_repo(heads=['change'])
638 source = backend.create_repo(heads=['change'])
626
639
627 # create pr from a in source to A in target
640 # create pr from a in source to A in target
628 pull_request = PullRequest()
641 pull_request = PullRequest()
629 pull_request.source_repo = source
642 pull_request.source_repo = source
630 # TODO: johbo: Make sure that we write the source ref this way!
643 # TODO: johbo: Make sure that we write the source ref this way!
631 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
644 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
632 branch=backend.default_branch_name, commit_id=commit_ids['change'])
645 branch=backend.default_branch_name, commit_id=commit_ids['change'])
633 pull_request.target_repo = target
646 pull_request.target_repo = target
634
647
635 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
648 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
636 branch=backend.default_branch_name,
649 branch=backend.default_branch_name,
637 commit_id=commit_ids['ancestor'])
650 commit_id=commit_ids['ancestor'])
638 pull_request.revisions = [commit_ids['change']]
651 pull_request.revisions = [commit_ids['change']]
639 pull_request.title = u"Test"
652 pull_request.title = u"Test"
640 pull_request.description = u"Description"
653 pull_request.description = u"Description"
641 pull_request.author = UserModel().get_by_username(
654 pull_request.author = UserModel().get_by_username(
642 TEST_USER_ADMIN_LOGIN)
655 TEST_USER_ADMIN_LOGIN)
643 Session().add(pull_request)
656 Session().add(pull_request)
644 Session().commit()
657 Session().commit()
645 pull_request_id = pull_request.pull_request_id
658 pull_request_id = pull_request.pull_request_id
646
659
647 # source has ancestor - change - change-2
660 # source has ancestor - change - change-2
648 backend.pull_heads(source, heads=['change-2'])
661 backend.pull_heads(source, heads=['change-2'])
649
662
650 # update PR
663 # update PR
651 self.app.post(
664 self.app.post(
652 url(controller='pullrequests', action='update',
665 route_path('pullrequest_update',
653 repo_name=target.repo_name,
666 repo_name=target.repo_name,
654 pull_request_id=str(pull_request_id)),
667 pull_request_id=pull_request_id),
655 params={'update_commits': 'true', '_method': 'put',
668 params={'update_commits': 'true',
656 'csrf_token': csrf_token})
669 'csrf_token': csrf_token})
657
670
658 # check that we have now both revisions
671 # check that we have now both revisions
659 pull_request = PullRequest.get(pull_request_id)
672 pull_request = PullRequest.get(pull_request_id)
660 assert pull_request.revisions == [
673 assert pull_request.revisions == [
661 commit_ids['change-2'], commit_ids['change']]
674 commit_ids['change-2'], commit_ids['change']]
662
675
663 # TODO: johbo: this should be a test on its own
676 # TODO: johbo: this should be a test on its own
664 response = self.app.get(url(
677 response = self.app.get(route_path(
665 controller='pullrequests', action='index',
678 'pullrequest_new',
666 repo_name=target.repo_name))
679 repo_name=target.repo_name))
667 assert response.status_int == 200
680 assert response.status_int == 200
668 assert 'Pull request updated to' in response.body
681 assert 'Pull request updated to' in response.body
669 assert 'with 1 added, 0 removed commits.' in response.body
682 assert 'with 1 added, 0 removed commits.' in response.body
670
683
671 def test_update_target_revision(self, backend, csrf_token):
684 def test_update_target_revision(self, backend, csrf_token):
672 commits = [
685 commits = [
673 {'message': 'ancestor'},
686 {'message': 'ancestor'},
674 {'message': 'change'},
687 {'message': 'change'},
675 {'message': 'ancestor-new', 'parents': ['ancestor']},
688 {'message': 'ancestor-new', 'parents': ['ancestor']},
676 {'message': 'change-rebased'},
689 {'message': 'change-rebased'},
677 ]
690 ]
678 commit_ids = backend.create_master_repo(commits)
691 commit_ids = backend.create_master_repo(commits)
679 target = backend.create_repo(heads=['ancestor'])
692 target = backend.create_repo(heads=['ancestor'])
680 source = backend.create_repo(heads=['change'])
693 source = backend.create_repo(heads=['change'])
681
694
682 # create pr from a in source to A in target
695 # create pr from a in source to A in target
683 pull_request = PullRequest()
696 pull_request = PullRequest()
684 pull_request.source_repo = source
697 pull_request.source_repo = source
685 # TODO: johbo: Make sure that we write the source ref this way!
698 # TODO: johbo: Make sure that we write the source ref this way!
686 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
699 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
687 branch=backend.default_branch_name, commit_id=commit_ids['change'])
700 branch=backend.default_branch_name, commit_id=commit_ids['change'])
688 pull_request.target_repo = target
701 pull_request.target_repo = target
689 # TODO: johbo: Target ref should be branch based, since tip can jump
702 # TODO: johbo: Target ref should be branch based, since tip can jump
690 # from branch to branch
703 # from branch to branch
691 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
704 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
692 branch=backend.default_branch_name,
705 branch=backend.default_branch_name,
693 commit_id=commit_ids['ancestor'])
706 commit_id=commit_ids['ancestor'])
694 pull_request.revisions = [commit_ids['change']]
707 pull_request.revisions = [commit_ids['change']]
695 pull_request.title = u"Test"
708 pull_request.title = u"Test"
696 pull_request.description = u"Description"
709 pull_request.description = u"Description"
697 pull_request.author = UserModel().get_by_username(
710 pull_request.author = UserModel().get_by_username(
698 TEST_USER_ADMIN_LOGIN)
711 TEST_USER_ADMIN_LOGIN)
699 Session().add(pull_request)
712 Session().add(pull_request)
700 Session().commit()
713 Session().commit()
701 pull_request_id = pull_request.pull_request_id
714 pull_request_id = pull_request.pull_request_id
702
715
703 # target has ancestor - ancestor-new
716 # target has ancestor - ancestor-new
704 # source has ancestor - ancestor-new - change-rebased
717 # source has ancestor - ancestor-new - change-rebased
705 backend.pull_heads(target, heads=['ancestor-new'])
718 backend.pull_heads(target, heads=['ancestor-new'])
706 backend.pull_heads(source, heads=['change-rebased'])
719 backend.pull_heads(source, heads=['change-rebased'])
707
720
708 # update PR
721 # update PR
709 self.app.post(
722 self.app.post(
710 url(controller='pullrequests', action='update',
723 route_path('pullrequest_update',
711 repo_name=target.repo_name,
724 repo_name=target.repo_name,
712 pull_request_id=str(pull_request_id)),
725 pull_request_id=pull_request_id),
713 params={'update_commits': 'true', '_method': 'put',
726 params={'update_commits': 'true',
714 'csrf_token': csrf_token},
727 'csrf_token': csrf_token},
715 status=200)
728 status=200)
716
729
717 # check that we have now both revisions
730 # check that we have now both revisions
718 pull_request = PullRequest.get(pull_request_id)
731 pull_request = PullRequest.get(pull_request_id)
719 assert pull_request.revisions == [commit_ids['change-rebased']]
732 assert pull_request.revisions == [commit_ids['change-rebased']]
720 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
733 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
721 branch=backend.default_branch_name,
734 branch=backend.default_branch_name,
722 commit_id=commit_ids['ancestor-new'])
735 commit_id=commit_ids['ancestor-new'])
723
736
724 # TODO: johbo: This should be a test on its own
737 # TODO: johbo: This should be a test on its own
725 response = self.app.get(url(
738 response = self.app.get(route_path(
726 controller='pullrequests', action='index',
739 'pullrequest_new',
727 repo_name=target.repo_name))
740 repo_name=target.repo_name))
728 assert response.status_int == 200
741 assert response.status_int == 200
729 assert 'Pull request updated to' in response.body
742 assert 'Pull request updated to' in response.body
730 assert 'with 1 added, 1 removed commits.' in response.body
743 assert 'with 1 added, 1 removed commits.' in response.body
731
744
732 def test_update_of_ancestor_reference(self, backend, csrf_token):
745 def test_update_of_ancestor_reference(self, backend, csrf_token):
733 commits = [
746 commits = [
734 {'message': 'ancestor'},
747 {'message': 'ancestor'},
735 {'message': 'change'},
748 {'message': 'change'},
736 {'message': 'change-2'},
749 {'message': 'change-2'},
737 {'message': 'ancestor-new', 'parents': ['ancestor']},
750 {'message': 'ancestor-new', 'parents': ['ancestor']},
738 {'message': 'change-rebased'},
751 {'message': 'change-rebased'},
739 ]
752 ]
740 commit_ids = backend.create_master_repo(commits)
753 commit_ids = backend.create_master_repo(commits)
741 target = backend.create_repo(heads=['ancestor'])
754 target = backend.create_repo(heads=['ancestor'])
742 source = backend.create_repo(heads=['change'])
755 source = backend.create_repo(heads=['change'])
743
756
744 # create pr from a in source to A in target
757 # create pr from a in source to A in target
745 pull_request = PullRequest()
758 pull_request = PullRequest()
746 pull_request.source_repo = source
759 pull_request.source_repo = source
747 # TODO: johbo: Make sure that we write the source ref this way!
760 # TODO: johbo: Make sure that we write the source ref this way!
748 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
761 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
749 branch=backend.default_branch_name,
762 branch=backend.default_branch_name,
750 commit_id=commit_ids['change'])
763 commit_id=commit_ids['change'])
751 pull_request.target_repo = target
764 pull_request.target_repo = target
752 # TODO: johbo: Target ref should be branch based, since tip can jump
765 # TODO: johbo: Target ref should be branch based, since tip can jump
753 # from branch to branch
766 # from branch to branch
754 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
767 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
755 branch=backend.default_branch_name,
768 branch=backend.default_branch_name,
756 commit_id=commit_ids['ancestor'])
769 commit_id=commit_ids['ancestor'])
757 pull_request.revisions = [commit_ids['change']]
770 pull_request.revisions = [commit_ids['change']]
758 pull_request.title = u"Test"
771 pull_request.title = u"Test"
759 pull_request.description = u"Description"
772 pull_request.description = u"Description"
760 pull_request.author = UserModel().get_by_username(
773 pull_request.author = UserModel().get_by_username(
761 TEST_USER_ADMIN_LOGIN)
774 TEST_USER_ADMIN_LOGIN)
762 Session().add(pull_request)
775 Session().add(pull_request)
763 Session().commit()
776 Session().commit()
764 pull_request_id = pull_request.pull_request_id
777 pull_request_id = pull_request.pull_request_id
765
778
766 # target has ancestor - ancestor-new
779 # target has ancestor - ancestor-new
767 # source has ancestor - ancestor-new - change-rebased
780 # source has ancestor - ancestor-new - change-rebased
768 backend.pull_heads(target, heads=['ancestor-new'])
781 backend.pull_heads(target, heads=['ancestor-new'])
769 backend.pull_heads(source, heads=['change-rebased'])
782 backend.pull_heads(source, heads=['change-rebased'])
770
783
771 # update PR
784 # update PR
772 self.app.post(
785 self.app.post(
773 url(controller='pullrequests', action='update',
786 route_path('pullrequest_update',
774 repo_name=target.repo_name,
787 repo_name=target.repo_name,
775 pull_request_id=str(pull_request_id)),
788 pull_request_id=pull_request_id),
776 params={'update_commits': 'true', '_method': 'put',
789 params={'update_commits': 'true',
777 'csrf_token': csrf_token},
790 'csrf_token': csrf_token},
778 status=200)
791 status=200)
779
792
780 # Expect the target reference to be updated correctly
793 # Expect the target reference to be updated correctly
781 pull_request = PullRequest.get(pull_request_id)
794 pull_request = PullRequest.get(pull_request_id)
782 assert pull_request.revisions == [commit_ids['change-rebased']]
795 assert pull_request.revisions == [commit_ids['change-rebased']]
783 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
796 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
784 branch=backend.default_branch_name,
797 branch=backend.default_branch_name,
785 commit_id=commit_ids['ancestor-new'])
798 commit_id=commit_ids['ancestor-new'])
786 assert pull_request.target_ref == expected_target_ref
799 assert pull_request.target_ref == expected_target_ref
787
800
788 def test_remove_pull_request_branch(self, backend_git, csrf_token):
801 def test_remove_pull_request_branch(self, backend_git, csrf_token):
789 branch_name = 'development'
802 branch_name = 'development'
790 commits = [
803 commits = [
791 {'message': 'initial-commit'},
804 {'message': 'initial-commit'},
792 {'message': 'old-feature'},
805 {'message': 'old-feature'},
793 {'message': 'new-feature', 'branch': branch_name},
806 {'message': 'new-feature', 'branch': branch_name},
794 ]
807 ]
795 repo = backend_git.create_repo(commits)
808 repo = backend_git.create_repo(commits)
796 commit_ids = backend_git.commit_ids
809 commit_ids = backend_git.commit_ids
797
810
798 pull_request = PullRequest()
811 pull_request = PullRequest()
799 pull_request.source_repo = repo
812 pull_request.source_repo = repo
800 pull_request.target_repo = repo
813 pull_request.target_repo = repo
801 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
814 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
802 branch=branch_name, commit_id=commit_ids['new-feature'])
815 branch=branch_name, commit_id=commit_ids['new-feature'])
803 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
816 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
804 branch=backend_git.default_branch_name,
817 branch=backend_git.default_branch_name,
805 commit_id=commit_ids['old-feature'])
818 commit_id=commit_ids['old-feature'])
806 pull_request.revisions = [commit_ids['new-feature']]
819 pull_request.revisions = [commit_ids['new-feature']]
807 pull_request.title = u"Test"
820 pull_request.title = u"Test"
808 pull_request.description = u"Description"
821 pull_request.description = u"Description"
809 pull_request.author = UserModel().get_by_username(
822 pull_request.author = UserModel().get_by_username(
810 TEST_USER_ADMIN_LOGIN)
823 TEST_USER_ADMIN_LOGIN)
811 Session().add(pull_request)
824 Session().add(pull_request)
812 Session().commit()
825 Session().commit()
813
826
814 vcs = repo.scm_instance()
827 vcs = repo.scm_instance()
815 vcs.remove_ref('refs/heads/{}'.format(branch_name))
828 vcs.remove_ref('refs/heads/{}'.format(branch_name))
816
829
817 response = self.app.get(url(
830 response = self.app.get(route_path(
818 controller='pullrequests', action='show',
831 'pullrequest_show',
819 repo_name=repo.repo_name,
832 repo_name=repo.repo_name,
820 pull_request_id=str(pull_request.pull_request_id)))
833 pull_request_id=pull_request.pull_request_id))
821
834
822 assert response.status_int == 200
835 assert response.status_int == 200
823 assert_response = AssertResponse(response)
836 assert_response = AssertResponse(response)
824 assert_response.element_contains(
837 assert_response.element_contains(
825 '#changeset_compare_view_content .alert strong',
838 '#changeset_compare_view_content .alert strong',
826 'Missing commits')
839 'Missing commits')
827 assert_response.element_contains(
840 assert_response.element_contains(
828 '#changeset_compare_view_content .alert',
841 '#changeset_compare_view_content .alert',
829 'This pull request cannot be displayed, because one or more'
842 'This pull request cannot be displayed, because one or more'
830 ' commits no longer exist in the source repository.')
843 ' commits no longer exist in the source repository.')
831
844
832 def test_strip_commits_from_pull_request(
845 def test_strip_commits_from_pull_request(
833 self, backend, pr_util, csrf_token):
846 self, backend, pr_util, csrf_token):
834 commits = [
847 commits = [
835 {'message': 'initial-commit'},
848 {'message': 'initial-commit'},
836 {'message': 'old-feature'},
849 {'message': 'old-feature'},
837 {'message': 'new-feature', 'parents': ['initial-commit']},
850 {'message': 'new-feature', 'parents': ['initial-commit']},
838 ]
851 ]
839 pull_request = pr_util.create_pull_request(
852 pull_request = pr_util.create_pull_request(
840 commits, target_head='initial-commit', source_head='new-feature',
853 commits, target_head='initial-commit', source_head='new-feature',
841 revisions=['new-feature'])
854 revisions=['new-feature'])
842
855
843 vcs = pr_util.source_repository.scm_instance()
856 vcs = pr_util.source_repository.scm_instance()
844 if backend.alias == 'git':
857 if backend.alias == 'git':
845 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
858 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
846 else:
859 else:
847 vcs.strip(pr_util.commit_ids['new-feature'])
860 vcs.strip(pr_util.commit_ids['new-feature'])
848
861
849 response = self.app.get(url(
862 response = self.app.get(route_path(
850 controller='pullrequests', action='show',
863 'pullrequest_show',
851 repo_name=pr_util.target_repository.repo_name,
864 repo_name=pr_util.target_repository.repo_name,
852 pull_request_id=str(pull_request.pull_request_id)))
865 pull_request_id=pull_request.pull_request_id))
853
866
854 assert response.status_int == 200
867 assert response.status_int == 200
855 assert_response = AssertResponse(response)
868 assert_response = AssertResponse(response)
856 assert_response.element_contains(
869 assert_response.element_contains(
857 '#changeset_compare_view_content .alert strong',
870 '#changeset_compare_view_content .alert strong',
858 'Missing commits')
871 'Missing commits')
859 assert_response.element_contains(
872 assert_response.element_contains(
860 '#changeset_compare_view_content .alert',
873 '#changeset_compare_view_content .alert',
861 'This pull request cannot be displayed, because one or more'
874 'This pull request cannot be displayed, because one or more'
862 ' commits no longer exist in the source repository.')
875 ' commits no longer exist in the source repository.')
863 assert_response.element_contains(
876 assert_response.element_contains(
864 '#update_commits',
877 '#update_commits',
865 'Update commits')
878 'Update commits')
866
879
867 def test_strip_commits_and_update(
880 def test_strip_commits_and_update(
868 self, backend, pr_util, csrf_token):
881 self, backend, pr_util, csrf_token):
869 commits = [
882 commits = [
870 {'message': 'initial-commit'},
883 {'message': 'initial-commit'},
871 {'message': 'old-feature'},
884 {'message': 'old-feature'},
872 {'message': 'new-feature', 'parents': ['old-feature']},
885 {'message': 'new-feature', 'parents': ['old-feature']},
873 ]
886 ]
874 pull_request = pr_util.create_pull_request(
887 pull_request = pr_util.create_pull_request(
875 commits, target_head='old-feature', source_head='new-feature',
888 commits, target_head='old-feature', source_head='new-feature',
876 revisions=['new-feature'], mergeable=True)
889 revisions=['new-feature'], mergeable=True)
877
890
878 vcs = pr_util.source_repository.scm_instance()
891 vcs = pr_util.source_repository.scm_instance()
879 if backend.alias == 'git':
892 if backend.alias == 'git':
880 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
893 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
881 else:
894 else:
882 vcs.strip(pr_util.commit_ids['new-feature'])
895 vcs.strip(pr_util.commit_ids['new-feature'])
883
896
884 response = self.app.post(
897 response = self.app.post(
885 url(controller='pullrequests', action='update',
898 route_path('pullrequest_update',
886 repo_name=pull_request.target_repo.repo_name,
899 repo_name=pull_request.target_repo.repo_name,
887 pull_request_id=str(pull_request.pull_request_id)),
900 pull_request_id=pull_request.pull_request_id),
888 params={'update_commits': 'true', '_method': 'put',
901 params={'update_commits': 'true',
889 'csrf_token': csrf_token})
902 'csrf_token': csrf_token})
890
903
891 assert response.status_int == 200
904 assert response.status_int == 200
892 assert response.body == 'true'
905 assert response.body == 'true'
893
906
894 # Make sure that after update, it won't raise 500 errors
907 # Make sure that after update, it won't raise 500 errors
895 response = self.app.get(url(
908 response = self.app.get(route_path(
896 controller='pullrequests', action='show',
909 'pullrequest_show',
897 repo_name=pr_util.target_repository.repo_name,
910 repo_name=pr_util.target_repository.repo_name,
898 pull_request_id=str(pull_request.pull_request_id)))
911 pull_request_id=pull_request.pull_request_id))
899
912
900 assert response.status_int == 200
913 assert response.status_int == 200
901 assert_response = AssertResponse(response)
914 assert_response = AssertResponse(response)
902 assert_response.element_contains(
915 assert_response.element_contains(
903 '#changeset_compare_view_content .alert strong',
916 '#changeset_compare_view_content .alert strong',
904 'Missing commits')
917 'Missing commits')
905
918
906 def test_branch_is_a_link(self, pr_util):
919 def test_branch_is_a_link(self, pr_util):
907 pull_request = pr_util.create_pull_request()
920 pull_request = pr_util.create_pull_request()
908 pull_request.source_ref = 'branch:origin:1234567890abcdef'
921 pull_request.source_ref = 'branch:origin:1234567890abcdef'
909 pull_request.target_ref = 'branch:target:abcdef1234567890'
922 pull_request.target_ref = 'branch:target:abcdef1234567890'
910 Session().add(pull_request)
923 Session().add(pull_request)
911 Session().commit()
924 Session().commit()
912
925
913 response = self.app.get(url(
926 response = self.app.get(route_path(
914 controller='pullrequests', action='show',
927 'pullrequest_show',
915 repo_name=pull_request.target_repo.scm_instance().name,
928 repo_name=pull_request.target_repo.scm_instance().name,
916 pull_request_id=str(pull_request.pull_request_id)))
929 pull_request_id=pull_request.pull_request_id))
917 assert response.status_int == 200
930 assert response.status_int == 200
918 assert_response = AssertResponse(response)
931 assert_response = AssertResponse(response)
919
932
920 origin = assert_response.get_element('.pr-origininfo .tag')
933 origin = assert_response.get_element('.pr-origininfo .tag')
921 origin_children = origin.getchildren()
934 origin_children = origin.getchildren()
922 assert len(origin_children) == 1
935 assert len(origin_children) == 1
923 target = assert_response.get_element('.pr-targetinfo .tag')
936 target = assert_response.get_element('.pr-targetinfo .tag')
924 target_children = target.getchildren()
937 target_children = target.getchildren()
925 assert len(target_children) == 1
938 assert len(target_children) == 1
926
939
927 expected_origin_link = route_path(
940 expected_origin_link = route_path(
928 'repo_changelog',
941 'repo_changelog',
929 repo_name=pull_request.source_repo.scm_instance().name,
942 repo_name=pull_request.source_repo.scm_instance().name,
930 params=dict(branch='origin'))
943 params=dict(branch='origin'))
931 expected_target_link = route_path(
944 expected_target_link = route_path(
932 'repo_changelog',
945 'repo_changelog',
933 repo_name=pull_request.target_repo.scm_instance().name,
946 repo_name=pull_request.target_repo.scm_instance().name,
934 params=dict(branch='target'))
947 params=dict(branch='target'))
935 assert origin_children[0].attrib['href'] == expected_origin_link
948 assert origin_children[0].attrib['href'] == expected_origin_link
936 assert origin_children[0].text == 'branch: origin'
949 assert origin_children[0].text == 'branch: origin'
937 assert target_children[0].attrib['href'] == expected_target_link
950 assert target_children[0].attrib['href'] == expected_target_link
938 assert target_children[0].text == 'branch: target'
951 assert target_children[0].text == 'branch: target'
939
952
940 def test_bookmark_is_not_a_link(self, pr_util):
953 def test_bookmark_is_not_a_link(self, pr_util):
941 pull_request = pr_util.create_pull_request()
954 pull_request = pr_util.create_pull_request()
942 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
955 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
943 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
956 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
944 Session().add(pull_request)
957 Session().add(pull_request)
945 Session().commit()
958 Session().commit()
946
959
947 response = self.app.get(url(
960 response = self.app.get(route_path(
948 controller='pullrequests', action='show',
961 'pullrequest_show',
949 repo_name=pull_request.target_repo.scm_instance().name,
962 repo_name=pull_request.target_repo.scm_instance().name,
950 pull_request_id=str(pull_request.pull_request_id)))
963 pull_request_id=pull_request.pull_request_id))
951 assert response.status_int == 200
964 assert response.status_int == 200
952 assert_response = AssertResponse(response)
965 assert_response = AssertResponse(response)
953
966
954 origin = assert_response.get_element('.pr-origininfo .tag')
967 origin = assert_response.get_element('.pr-origininfo .tag')
955 assert origin.text.strip() == 'bookmark: origin'
968 assert origin.text.strip() == 'bookmark: origin'
956 assert origin.getchildren() == []
969 assert origin.getchildren() == []
957
970
958 target = assert_response.get_element('.pr-targetinfo .tag')
971 target = assert_response.get_element('.pr-targetinfo .tag')
959 assert target.text.strip() == 'bookmark: target'
972 assert target.text.strip() == 'bookmark: target'
960 assert target.getchildren() == []
973 assert target.getchildren() == []
961
974
962 def test_tag_is_not_a_link(self, pr_util):
975 def test_tag_is_not_a_link(self, pr_util):
963 pull_request = pr_util.create_pull_request()
976 pull_request = pr_util.create_pull_request()
964 pull_request.source_ref = 'tag:origin:1234567890abcdef'
977 pull_request.source_ref = 'tag:origin:1234567890abcdef'
965 pull_request.target_ref = 'tag:target:abcdef1234567890'
978 pull_request.target_ref = 'tag:target:abcdef1234567890'
966 Session().add(pull_request)
979 Session().add(pull_request)
967 Session().commit()
980 Session().commit()
968
981
969 response = self.app.get(url(
982 response = self.app.get(route_path(
970 controller='pullrequests', action='show',
983 'pullrequest_show',
971 repo_name=pull_request.target_repo.scm_instance().name,
984 repo_name=pull_request.target_repo.scm_instance().name,
972 pull_request_id=str(pull_request.pull_request_id)))
985 pull_request_id=pull_request.pull_request_id))
973 assert response.status_int == 200
986 assert response.status_int == 200
974 assert_response = AssertResponse(response)
987 assert_response = AssertResponse(response)
975
988
976 origin = assert_response.get_element('.pr-origininfo .tag')
989 origin = assert_response.get_element('.pr-origininfo .tag')
977 assert origin.text.strip() == 'tag: origin'
990 assert origin.text.strip() == 'tag: origin'
978 assert origin.getchildren() == []
991 assert origin.getchildren() == []
979
992
980 target = assert_response.get_element('.pr-targetinfo .tag')
993 target = assert_response.get_element('.pr-targetinfo .tag')
981 assert target.text.strip() == 'tag: target'
994 assert target.text.strip() == 'tag: target'
982 assert target.getchildren() == []
995 assert target.getchildren() == []
983
996
984 @pytest.mark.parametrize('mergeable', [True, False])
997 @pytest.mark.parametrize('mergeable', [True, False])
985 def test_shadow_repository_link(
998 def test_shadow_repository_link(
986 self, mergeable, pr_util, http_host_only_stub):
999 self, mergeable, pr_util, http_host_only_stub):
987 """
1000 """
988 Check that the pull request summary page displays a link to the shadow
1001 Check that the pull request summary page displays a link to the shadow
989 repository if the pull request is mergeable. If it is not mergeable
1002 repository if the pull request is mergeable. If it is not mergeable
990 the link should not be displayed.
1003 the link should not be displayed.
991 """
1004 """
992 pull_request = pr_util.create_pull_request(
1005 pull_request = pr_util.create_pull_request(
993 mergeable=mergeable, enable_notifications=False)
1006 mergeable=mergeable, enable_notifications=False)
994 target_repo = pull_request.target_repo.scm_instance()
1007 target_repo = pull_request.target_repo.scm_instance()
995 pr_id = pull_request.pull_request_id
1008 pr_id = pull_request.pull_request_id
996 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1009 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
997 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1010 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
998
1011
999 response = self.app.get(url(
1012 response = self.app.get(route_path(
1000 controller='pullrequests', action='show',
1013 'pullrequest_show',
1001 repo_name=target_repo.name,
1014 repo_name=target_repo.name,
1002 pull_request_id=str(pr_id)))
1015 pull_request_id=pr_id))
1003
1016
1004 assertr = AssertResponse(response)
1017 assertr = AssertResponse(response)
1005 if mergeable:
1018 if mergeable:
1006 assertr.element_value_contains(
1019 assertr.element_value_contains(
1007 'div.pr-mergeinfo input', shadow_url)
1020 'div.pr-mergeinfo input', shadow_url)
1008 assertr.element_value_contains(
1021 assertr.element_value_contains(
1009 'div.pr-mergeinfo input', 'pr-merge')
1022 'div.pr-mergeinfo input', 'pr-merge')
1010 else:
1023 else:
1011 assertr.no_element_exists('div.pr-mergeinfo')
1024 assertr.no_element_exists('div.pr-mergeinfo')
1012
1025
1013
1026
1014 @pytest.mark.usefixtures('app')
1027 @pytest.mark.usefixtures('app')
1015 @pytest.mark.backends("git", "hg")
1028 @pytest.mark.backends("git", "hg")
1016 class TestPullrequestsControllerDelete(object):
1029 class TestPullrequestsControllerDelete(object):
1017 def test_pull_request_delete_button_permissions_admin(
1030 def test_pull_request_delete_button_permissions_admin(
1018 self, autologin_user, user_admin, pr_util):
1031 self, autologin_user, user_admin, pr_util):
1019 pull_request = pr_util.create_pull_request(
1032 pull_request = pr_util.create_pull_request(
1020 author=user_admin.username, enable_notifications=False)
1033 author=user_admin.username, enable_notifications=False)
1021
1034
1022 response = self.app.get(url(
1035 response = self.app.get(route_path(
1023 controller='pullrequests', action='show',
1036 'pullrequest_show',
1024 repo_name=pull_request.target_repo.scm_instance().name,
1037 repo_name=pull_request.target_repo.scm_instance().name,
1025 pull_request_id=str(pull_request.pull_request_id)))
1038 pull_request_id=pull_request.pull_request_id))
1026
1039
1027 response.mustcontain('id="delete_pullrequest"')
1040 response.mustcontain('id="delete_pullrequest"')
1028 response.mustcontain('Confirm to delete this pull request')
1041 response.mustcontain('Confirm to delete this pull request')
1029
1042
1030 def test_pull_request_delete_button_permissions_owner(
1043 def test_pull_request_delete_button_permissions_owner(
1031 self, autologin_regular_user, user_regular, pr_util):
1044 self, autologin_regular_user, user_regular, pr_util):
1032 pull_request = pr_util.create_pull_request(
1045 pull_request = pr_util.create_pull_request(
1033 author=user_regular.username, enable_notifications=False)
1046 author=user_regular.username, enable_notifications=False)
1034
1047
1035 response = self.app.get(url(
1048 response = self.app.get(route_path(
1036 controller='pullrequests', action='show',
1049 'pullrequest_show',
1037 repo_name=pull_request.target_repo.scm_instance().name,
1050 repo_name=pull_request.target_repo.scm_instance().name,
1038 pull_request_id=str(pull_request.pull_request_id)))
1051 pull_request_id=pull_request.pull_request_id))
1039
1052
1040 response.mustcontain('id="delete_pullrequest"')
1053 response.mustcontain('id="delete_pullrequest"')
1041 response.mustcontain('Confirm to delete this pull request')
1054 response.mustcontain('Confirm to delete this pull request')
1042
1055
1043 def test_pull_request_delete_button_permissions_forbidden(
1056 def test_pull_request_delete_button_permissions_forbidden(
1044 self, autologin_regular_user, user_regular, user_admin, pr_util):
1057 self, autologin_regular_user, user_regular, user_admin, pr_util):
1045 pull_request = pr_util.create_pull_request(
1058 pull_request = pr_util.create_pull_request(
1046 author=user_admin.username, enable_notifications=False)
1059 author=user_admin.username, enable_notifications=False)
1047
1060
1048 response = self.app.get(url(
1061 response = self.app.get(route_path(
1049 controller='pullrequests', action='show',
1062 'pullrequest_show',
1050 repo_name=pull_request.target_repo.scm_instance().name,
1063 repo_name=pull_request.target_repo.scm_instance().name,
1051 pull_request_id=str(pull_request.pull_request_id)))
1064 pull_request_id=pull_request.pull_request_id))
1052 response.mustcontain(no=['id="delete_pullrequest"'])
1065 response.mustcontain(no=['id="delete_pullrequest"'])
1053 response.mustcontain(no=['Confirm to delete this pull request'])
1066 response.mustcontain(no=['Confirm to delete this pull request'])
1054
1067
1055 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1068 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1056 self, autologin_regular_user, user_regular, user_admin, pr_util,
1069 self, autologin_regular_user, user_regular, user_admin, pr_util,
1057 user_util):
1070 user_util):
1058
1071
1059 pull_request = pr_util.create_pull_request(
1072 pull_request = pr_util.create_pull_request(
1060 author=user_admin.username, enable_notifications=False)
1073 author=user_admin.username, enable_notifications=False)
1061
1074
1062 user_util.grant_user_permission_to_repo(
1075 user_util.grant_user_permission_to_repo(
1063 pull_request.target_repo, user_regular,
1076 pull_request.target_repo, user_regular,
1064 'repository.write')
1077 'repository.write')
1065
1078
1066 response = self.app.get(url(
1079 response = self.app.get(route_path(
1067 controller='pullrequests', action='show',
1080 'pullrequest_show',
1068 repo_name=pull_request.target_repo.scm_instance().name,
1081 repo_name=pull_request.target_repo.scm_instance().name,
1069 pull_request_id=str(pull_request.pull_request_id)))
1082 pull_request_id=pull_request.pull_request_id))
1070
1083
1071 response.mustcontain('id="open_edit_pullrequest"')
1084 response.mustcontain('id="open_edit_pullrequest"')
1072 response.mustcontain('id="delete_pullrequest"')
1085 response.mustcontain('id="delete_pullrequest"')
1073 response.mustcontain(no=['Confirm to delete this pull request'])
1086 response.mustcontain(no=['Confirm to delete this pull request'])
1074
1087
1075 def test_delete_comment_returns_404_if_comment_does_not_exist(
1088 def test_delete_comment_returns_404_if_comment_does_not_exist(
1076 self, autologin_user, pr_util, user_admin):
1089 self, autologin_user, pr_util, user_admin):
1077
1090
1078 pull_request = pr_util.create_pull_request(
1091 pull_request = pr_util.create_pull_request(
1079 author=user_admin.username, enable_notifications=False)
1092 author=user_admin.username, enable_notifications=False)
1080
1093
1081 self.app.get(url(
1094 self.app.get(route_path(
1082 controller='pullrequests', action='delete_comment',
1095 'pullrequest_comment_delete',
1083 repo_name=pull_request.target_repo.scm_instance().name,
1096 repo_name=pull_request.target_repo.scm_instance().name,
1097 pull_request_id=pull_request.pull_request_id,
1084 comment_id=1024404), status=404)
1098 comment_id=1024404), status=404)
1085
1099
1086
1100
1087 def assert_pull_request_status(pull_request, expected_status):
1101 def assert_pull_request_status(pull_request, expected_status):
1088 status = ChangesetStatusModel().calculated_review_status(
1102 status = ChangesetStatusModel().calculated_review_status(
1089 pull_request=pull_request)
1103 pull_request=pull_request)
1090 assert status == expected_status
1104 assert status == expected_status
1091
1105
1092
1106
1093 @pytest.mark.parametrize('action', ['index', 'create'])
1107 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1094 @pytest.mark.usefixtures("autologin_user")
1108 @pytest.mark.usefixtures("autologin_user")
1095 def test_redirects_to_repo_summary_for_svn_repositories(backend_svn, app, action):
1109 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1096 response = app.get(url(
1110 response = app.get(
1097 controller='pullrequests', action=action,
1111 route_path(route, repo_name=backend_svn.repo_name), status=404)
1098 repo_name=backend_svn.repo_name))
1099 assert response.status_int == 302
1100
1112
1101 # Not allowed, redirect to the summary
1102 redirected = response.follow()
1103 summary_url = h.route_path('repo_summary', repo_name=backend_svn.repo_name)
1104
1105 # URL adds leading slash and path doesn't have it
1106 assert redirected.request.path == summary_url
This diff has been collapsed as it changes many lines, (630 lines changed) Show them Hide them
@@ -1,584 +1,1182 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22
23
23 import collections
24 import formencode
24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
25 import peppercorn
26 from pyramid.httpexceptions import (
27 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
25 from pyramid.view import view_config
28 from pyramid.view import view_config
29 from pyramid.renderers import render
26
30
31 from rhodecode import events
27 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
28 from rhodecode.lib import helpers as h, diffs, codeblocks
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.ext_json import json
29 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
30 LoginRequired, HasRepoPermissionAnyDecorator)
38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
31 from rhodecode.lib.utils2 import str2bool, safe_int, safe_str
39 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
32 from rhodecode.lib.vcs.backends.base import EmptyCommit
40 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
33 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, \
41 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
34 RepositoryRequirementError, NodeDoesNotExistError
42 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
43 from rhodecode.model.changeset_status import ChangesetStatusModel
35 from rhodecode.model.comment import CommentsModel
44 from rhodecode.model.comment import CommentsModel
36 from rhodecode.model.db import PullRequest, PullRequestVersion, \
45 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
37 ChangesetComment, ChangesetStatus
46 ChangesetComment, ChangesetStatus, Repository)
47 from rhodecode.model.forms import PullRequestForm
48 from rhodecode.model.meta import Session
38 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
49 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
50 from rhodecode.model.scm import ScmModel
39
51
40 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
41
53
42
54
43 class RepoPullRequestsView(RepoAppView, DataGridAppView):
55 class RepoPullRequestsView(RepoAppView, DataGridAppView):
44
56
45 def load_default_context(self):
57 def load_default_context(self):
46 c = self._get_local_tmpl_context(include_app_defaults=True)
58 c = self._get_local_tmpl_context(include_app_defaults=True)
47 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
59 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
48 c.repo_info = self.db_repo
60 c.repo_info = self.db_repo
49 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
50 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
51 self._register_global_c(c)
63 self._register_global_c(c)
52 return c
64 return c
53
65
54 def _get_pull_requests_list(
66 def _get_pull_requests_list(
55 self, repo_name, source, filter_type, opened_by, statuses):
67 self, repo_name, source, filter_type, opened_by, statuses):
56
68
57 draw, start, limit = self._extract_chunk(self.request)
69 draw, start, limit = self._extract_chunk(self.request)
58 search_q, order_by, order_dir = self._extract_ordering(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
59 _render = self.request.get_partial_renderer(
71 _render = self.request.get_partial_renderer(
60 'data_table/_dt_elements.mako')
72 'data_table/_dt_elements.mako')
61
73
62 # pagination
74 # pagination
63
75
64 if filter_type == 'awaiting_review':
76 if filter_type == 'awaiting_review':
65 pull_requests = PullRequestModel().get_awaiting_review(
77 pull_requests = PullRequestModel().get_awaiting_review(
66 repo_name, source=source, opened_by=opened_by,
78 repo_name, source=source, opened_by=opened_by,
67 statuses=statuses, offset=start, length=limit,
79 statuses=statuses, offset=start, length=limit,
68 order_by=order_by, order_dir=order_dir)
80 order_by=order_by, order_dir=order_dir)
69 pull_requests_total_count = PullRequestModel().count_awaiting_review(
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
70 repo_name, source=source, statuses=statuses,
82 repo_name, source=source, statuses=statuses,
71 opened_by=opened_by)
83 opened_by=opened_by)
72 elif filter_type == 'awaiting_my_review':
84 elif filter_type == 'awaiting_my_review':
73 pull_requests = PullRequestModel().get_awaiting_my_review(
85 pull_requests = PullRequestModel().get_awaiting_my_review(
74 repo_name, source=source, opened_by=opened_by,
86 repo_name, source=source, opened_by=opened_by,
75 user_id=self._rhodecode_user.user_id, statuses=statuses,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
76 offset=start, length=limit, order_by=order_by,
88 offset=start, length=limit, order_by=order_by,
77 order_dir=order_dir)
89 order_dir=order_dir)
78 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
79 repo_name, source=source, user_id=self._rhodecode_user.user_id,
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
80 statuses=statuses, opened_by=opened_by)
92 statuses=statuses, opened_by=opened_by)
81 else:
93 else:
82 pull_requests = PullRequestModel().get_all(
94 pull_requests = PullRequestModel().get_all(
83 repo_name, source=source, opened_by=opened_by,
95 repo_name, source=source, opened_by=opened_by,
84 statuses=statuses, offset=start, length=limit,
96 statuses=statuses, offset=start, length=limit,
85 order_by=order_by, order_dir=order_dir)
97 order_by=order_by, order_dir=order_dir)
86 pull_requests_total_count = PullRequestModel().count_all(
98 pull_requests_total_count = PullRequestModel().count_all(
87 repo_name, source=source, statuses=statuses,
99 repo_name, source=source, statuses=statuses,
88 opened_by=opened_by)
100 opened_by=opened_by)
89
101
90 data = []
102 data = []
91 comments_model = CommentsModel()
103 comments_model = CommentsModel()
92 for pr in pull_requests:
104 for pr in pull_requests:
93 comments = comments_model.get_all_comments(
105 comments = comments_model.get_all_comments(
94 self.db_repo.repo_id, pull_request=pr)
106 self.db_repo.repo_id, pull_request=pr)
95
107
96 data.append({
108 data.append({
97 'name': _render('pullrequest_name',
109 'name': _render('pullrequest_name',
98 pr.pull_request_id, pr.target_repo.repo_name),
110 pr.pull_request_id, pr.target_repo.repo_name),
99 'name_raw': pr.pull_request_id,
111 'name_raw': pr.pull_request_id,
100 'status': _render('pullrequest_status',
112 'status': _render('pullrequest_status',
101 pr.calculated_review_status()),
113 pr.calculated_review_status()),
102 'title': _render(
114 'title': _render(
103 'pullrequest_title', pr.title, pr.description),
115 'pullrequest_title', pr.title, pr.description),
104 'description': h.escape(pr.description),
116 'description': h.escape(pr.description),
105 'updated_on': _render('pullrequest_updated_on',
117 'updated_on': _render('pullrequest_updated_on',
106 h.datetime_to_time(pr.updated_on)),
118 h.datetime_to_time(pr.updated_on)),
107 'updated_on_raw': h.datetime_to_time(pr.updated_on),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
108 'created_on': _render('pullrequest_updated_on',
120 'created_on': _render('pullrequest_updated_on',
109 h.datetime_to_time(pr.created_on)),
121 h.datetime_to_time(pr.created_on)),
110 'created_on_raw': h.datetime_to_time(pr.created_on),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
111 'author': _render('pullrequest_author',
123 'author': _render('pullrequest_author',
112 pr.author.full_contact, ),
124 pr.author.full_contact, ),
113 'author_raw': pr.author.full_name,
125 'author_raw': pr.author.full_name,
114 'comments': _render('pullrequest_comments', len(comments)),
126 'comments': _render('pullrequest_comments', len(comments)),
115 'comments_raw': len(comments),
127 'comments_raw': len(comments),
116 'closed': pr.is_closed(),
128 'closed': pr.is_closed(),
117 })
129 })
118
130
119 data = ({
131 data = ({
120 'draw': draw,
132 'draw': draw,
121 'data': data,
133 'data': data,
122 'recordsTotal': pull_requests_total_count,
134 'recordsTotal': pull_requests_total_count,
123 'recordsFiltered': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
124 })
136 })
125 return data
137 return data
126
138
127 @LoginRequired()
139 @LoginRequired()
128 @HasRepoPermissionAnyDecorator(
140 @HasRepoPermissionAnyDecorator(
129 'repository.read', 'repository.write', 'repository.admin')
141 'repository.read', 'repository.write', 'repository.admin')
130 @view_config(
142 @view_config(
131 route_name='pullrequest_show_all', request_method='GET',
143 route_name='pullrequest_show_all', request_method='GET',
132 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
133 def pull_request_list(self):
145 def pull_request_list(self):
134 c = self.load_default_context()
146 c = self.load_default_context()
135
147
136 req_get = self.request.GET
148 req_get = self.request.GET
137 c.source = str2bool(req_get.get('source'))
149 c.source = str2bool(req_get.get('source'))
138 c.closed = str2bool(req_get.get('closed'))
150 c.closed = str2bool(req_get.get('closed'))
139 c.my = str2bool(req_get.get('my'))
151 c.my = str2bool(req_get.get('my'))
140 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
141 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
142
154
143 c.active = 'open'
155 c.active = 'open'
144 if c.my:
156 if c.my:
145 c.active = 'my'
157 c.active = 'my'
146 if c.closed:
158 if c.closed:
147 c.active = 'closed'
159 c.active = 'closed'
148 if c.awaiting_review and not c.source:
160 if c.awaiting_review and not c.source:
149 c.active = 'awaiting'
161 c.active = 'awaiting'
150 if c.source and not c.awaiting_review:
162 if c.source and not c.awaiting_review:
151 c.active = 'source'
163 c.active = 'source'
152 if c.awaiting_my_review:
164 if c.awaiting_my_review:
153 c.active = 'awaiting_my'
165 c.active = 'awaiting_my'
154
166
155 return self._get_template_context(c)
167 return self._get_template_context(c)
156
168
157 @LoginRequired()
169 @LoginRequired()
158 @HasRepoPermissionAnyDecorator(
170 @HasRepoPermissionAnyDecorator(
159 'repository.read', 'repository.write', 'repository.admin')
171 'repository.read', 'repository.write', 'repository.admin')
160 @view_config(
172 @view_config(
161 route_name='pullrequest_show_all_data', request_method='GET',
173 route_name='pullrequest_show_all_data', request_method='GET',
162 renderer='json_ext', xhr=True)
174 renderer='json_ext', xhr=True)
163 def pull_request_list_data(self):
175 def pull_request_list_data(self):
164
176
165 # additional filters
177 # additional filters
166 req_get = self.request.GET
178 req_get = self.request.GET
167 source = str2bool(req_get.get('source'))
179 source = str2bool(req_get.get('source'))
168 closed = str2bool(req_get.get('closed'))
180 closed = str2bool(req_get.get('closed'))
169 my = str2bool(req_get.get('my'))
181 my = str2bool(req_get.get('my'))
170 awaiting_review = str2bool(req_get.get('awaiting_review'))
182 awaiting_review = str2bool(req_get.get('awaiting_review'))
171 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
172
184
173 filter_type = 'awaiting_review' if awaiting_review \
185 filter_type = 'awaiting_review' if awaiting_review \
174 else 'awaiting_my_review' if awaiting_my_review \
186 else 'awaiting_my_review' if awaiting_my_review \
175 else None
187 else None
176
188
177 opened_by = None
189 opened_by = None
178 if my:
190 if my:
179 opened_by = [self._rhodecode_user.user_id]
191 opened_by = [self._rhodecode_user.user_id]
180
192
181 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
182 if closed:
194 if closed:
183 statuses = [PullRequest.STATUS_CLOSED]
195 statuses = [PullRequest.STATUS_CLOSED]
184
196
185 data = self._get_pull_requests_list(
197 data = self._get_pull_requests_list(
186 repo_name=self.db_repo_name, source=source,
198 repo_name=self.db_repo_name, source=source,
187 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
188
200
189 return data
201 return data
190
202
191 def _get_pr_version(self, pull_request_id, version=None):
203 def _get_pr_version(self, pull_request_id, version=None):
192 pull_request_id = safe_int(pull_request_id)
193 at_version = None
204 at_version = None
194
205
195 if version and version == 'latest':
206 if version and version == 'latest':
196 pull_request_ver = PullRequest.get(pull_request_id)
207 pull_request_ver = PullRequest.get(pull_request_id)
197 pull_request_obj = pull_request_ver
208 pull_request_obj = pull_request_ver
198 _org_pull_request_obj = pull_request_obj
209 _org_pull_request_obj = pull_request_obj
199 at_version = 'latest'
210 at_version = 'latest'
200 elif version:
211 elif version:
201 pull_request_ver = PullRequestVersion.get_or_404(version)
212 pull_request_ver = PullRequestVersion.get_or_404(version)
202 pull_request_obj = pull_request_ver
213 pull_request_obj = pull_request_ver
203 _org_pull_request_obj = pull_request_ver.pull_request
214 _org_pull_request_obj = pull_request_ver.pull_request
204 at_version = pull_request_ver.pull_request_version_id
215 at_version = pull_request_ver.pull_request_version_id
205 else:
216 else:
206 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
207 pull_request_id)
218 pull_request_id)
208
219
209 pull_request_display_obj = PullRequest.get_pr_display_object(
220 pull_request_display_obj = PullRequest.get_pr_display_object(
210 pull_request_obj, _org_pull_request_obj)
221 pull_request_obj, _org_pull_request_obj)
211
222
212 return _org_pull_request_obj, pull_request_obj, \
223 return _org_pull_request_obj, pull_request_obj, \
213 pull_request_display_obj, at_version
224 pull_request_display_obj, at_version
214
225
215 def _get_diffset(self, source_repo_name, source_repo,
226 def _get_diffset(self, source_repo_name, source_repo,
216 source_ref_id, target_ref_id,
227 source_ref_id, target_ref_id,
217 target_commit, source_commit, diff_limit, fulldiff,
228 target_commit, source_commit, diff_limit, fulldiff,
218 file_limit, display_inline_comments):
229 file_limit, display_inline_comments):
219
230
220 vcs_diff = PullRequestModel().get_diff(
231 vcs_diff = PullRequestModel().get_diff(
221 source_repo, source_ref_id, target_ref_id)
232 source_repo, source_ref_id, target_ref_id)
222
233
223 diff_processor = diffs.DiffProcessor(
234 diff_processor = diffs.DiffProcessor(
224 vcs_diff, format='newdiff', diff_limit=diff_limit,
235 vcs_diff, format='newdiff', diff_limit=diff_limit,
225 file_limit=file_limit, show_full_diff=fulldiff)
236 file_limit=file_limit, show_full_diff=fulldiff)
226
237
227 _parsed = diff_processor.prepare()
238 _parsed = diff_processor.prepare()
228
239
229 def _node_getter(commit):
240 def _node_getter(commit):
230 def get_node(fname):
241 def get_node(fname):
231 try:
242 try:
232 return commit.get_node(fname)
243 return commit.get_node(fname)
233 except NodeDoesNotExistError:
244 except NodeDoesNotExistError:
234 return None
245 return None
235
246
236 return get_node
247 return get_node
237
248
238 diffset = codeblocks.DiffSet(
249 diffset = codeblocks.DiffSet(
239 repo_name=self.db_repo_name,
250 repo_name=self.db_repo_name,
240 source_repo_name=source_repo_name,
251 source_repo_name=source_repo_name,
241 source_node_getter=_node_getter(target_commit),
252 source_node_getter=_node_getter(target_commit),
242 target_node_getter=_node_getter(source_commit),
253 target_node_getter=_node_getter(source_commit),
243 comments=display_inline_comments
254 comments=display_inline_comments
244 )
255 )
245 diffset = diffset.render_patchset(
256 diffset = diffset.render_patchset(
246 _parsed, target_commit.raw_id, source_commit.raw_id)
257 _parsed, target_commit.raw_id, source_commit.raw_id)
247
258
248 return diffset
259 return diffset
249
260
250 @LoginRequired()
261 @LoginRequired()
251 @HasRepoPermissionAnyDecorator(
262 @HasRepoPermissionAnyDecorator(
252 'repository.read', 'repository.write', 'repository.admin')
263 'repository.read', 'repository.write', 'repository.admin')
253 # @view_config(
264 @view_config(
254 # route_name='pullrequest_show', request_method='GET',
265 route_name='pullrequest_show', request_method='GET',
255 # renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
256 def pull_request_show(self):
267 def pull_request_show(self):
257 pull_request_id = safe_int(
268 pull_request_id = self.request.matchdict.get('pull_request_id')
258 self.request.matchdict.get('pull_request_id'))
269
259 c = self.load_default_context()
270 c = self.load_default_context()
260
271
261 version = self.request.GET.get('version')
272 version = self.request.GET.get('version')
262 from_version = self.request.GET.get('from_version') or version
273 from_version = self.request.GET.get('from_version') or version
263 merge_checks = self.request.GET.get('merge_checks')
274 merge_checks = self.request.GET.get('merge_checks')
264 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
265
276
266 (pull_request_latest,
277 (pull_request_latest,
267 pull_request_at_ver,
278 pull_request_at_ver,
268 pull_request_display_obj,
279 pull_request_display_obj,
269 at_version) = self._get_pr_version(
280 at_version) = self._get_pr_version(
270 pull_request_id, version=version)
281 pull_request_id, version=version)
271 pr_closed = pull_request_latest.is_closed()
282 pr_closed = pull_request_latest.is_closed()
272
283
273 if pr_closed and (version or from_version):
284 if pr_closed and (version or from_version):
274 # not allow to browse versions
285 # not allow to browse versions
275 raise HTTPFound(h.route_path(
286 raise HTTPFound(h.route_path(
276 'pullrequest_show', repo_name=self.db_repo_name,
287 'pullrequest_show', repo_name=self.db_repo_name,
277 pull_request_id=pull_request_id))
288 pull_request_id=pull_request_id))
278
289
279 versions = pull_request_display_obj.versions()
290 versions = pull_request_display_obj.versions()
280
291
281 c.at_version = at_version
292 c.at_version = at_version
282 c.at_version_num = (at_version
293 c.at_version_num = (at_version
283 if at_version and at_version != 'latest'
294 if at_version and at_version != 'latest'
284 else None)
295 else None)
285 c.at_version_pos = ChangesetComment.get_index_from_version(
296 c.at_version_pos = ChangesetComment.get_index_from_version(
286 c.at_version_num, versions)
297 c.at_version_num, versions)
287
298
288 (prev_pull_request_latest,
299 (prev_pull_request_latest,
289 prev_pull_request_at_ver,
300 prev_pull_request_at_ver,
290 prev_pull_request_display_obj,
301 prev_pull_request_display_obj,
291 prev_at_version) = self._get_pr_version(
302 prev_at_version) = self._get_pr_version(
292 pull_request_id, version=from_version)
303 pull_request_id, version=from_version)
293
304
294 c.from_version = prev_at_version
305 c.from_version = prev_at_version
295 c.from_version_num = (prev_at_version
306 c.from_version_num = (prev_at_version
296 if prev_at_version and prev_at_version != 'latest'
307 if prev_at_version and prev_at_version != 'latest'
297 else None)
308 else None)
298 c.from_version_pos = ChangesetComment.get_index_from_version(
309 c.from_version_pos = ChangesetComment.get_index_from_version(
299 c.from_version_num, versions)
310 c.from_version_num, versions)
300
311
301 # define if we're in COMPARE mode or VIEW at version mode
312 # define if we're in COMPARE mode or VIEW at version mode
302 compare = at_version != prev_at_version
313 compare = at_version != prev_at_version
303
314
304 # pull_requests repo_name we opened it against
315 # pull_requests repo_name we opened it against
305 # ie. target_repo must match
316 # ie. target_repo must match
306 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
307 raise HTTPNotFound()
318 raise HTTPNotFound()
308
319
309 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
310 pull_request_at_ver)
321 pull_request_at_ver)
311
322
312 c.pull_request = pull_request_display_obj
323 c.pull_request = pull_request_display_obj
313 c.pull_request_latest = pull_request_latest
324 c.pull_request_latest = pull_request_latest
314
325
315 if compare or (at_version and not at_version == 'latest'):
326 if compare or (at_version and not at_version == 'latest'):
316 c.allowed_to_change_status = False
327 c.allowed_to_change_status = False
317 c.allowed_to_update = False
328 c.allowed_to_update = False
318 c.allowed_to_merge = False
329 c.allowed_to_merge = False
319 c.allowed_to_delete = False
330 c.allowed_to_delete = False
320 c.allowed_to_comment = False
331 c.allowed_to_comment = False
321 c.allowed_to_close = False
332 c.allowed_to_close = False
322 else:
333 else:
323 can_change_status = PullRequestModel().check_user_change_status(
334 can_change_status = PullRequestModel().check_user_change_status(
324 pull_request_at_ver, self._rhodecode_user)
335 pull_request_at_ver, self._rhodecode_user)
325 c.allowed_to_change_status = can_change_status and not pr_closed
336 c.allowed_to_change_status = can_change_status and not pr_closed
326
337
327 c.allowed_to_update = PullRequestModel().check_user_update(
338 c.allowed_to_update = PullRequestModel().check_user_update(
328 pull_request_latest, self._rhodecode_user) and not pr_closed
339 pull_request_latest, self._rhodecode_user) and not pr_closed
329 c.allowed_to_merge = PullRequestModel().check_user_merge(
340 c.allowed_to_merge = PullRequestModel().check_user_merge(
330 pull_request_latest, self._rhodecode_user) and not pr_closed
341 pull_request_latest, self._rhodecode_user) and not pr_closed
331 c.allowed_to_delete = PullRequestModel().check_user_delete(
342 c.allowed_to_delete = PullRequestModel().check_user_delete(
332 pull_request_latest, self._rhodecode_user) and not pr_closed
343 pull_request_latest, self._rhodecode_user) and not pr_closed
333 c.allowed_to_comment = not pr_closed
344 c.allowed_to_comment = not pr_closed
334 c.allowed_to_close = c.allowed_to_merge and not pr_closed
345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
335
346
336 c.forbid_adding_reviewers = False
347 c.forbid_adding_reviewers = False
337 c.forbid_author_to_review = False
348 c.forbid_author_to_review = False
338 c.forbid_commit_author_to_review = False
349 c.forbid_commit_author_to_review = False
339
350
340 if pull_request_latest.reviewer_data and \
351 if pull_request_latest.reviewer_data and \
341 'rules' in pull_request_latest.reviewer_data:
352 'rules' in pull_request_latest.reviewer_data:
342 rules = pull_request_latest.reviewer_data['rules'] or {}
353 rules = pull_request_latest.reviewer_data['rules'] or {}
343 try:
354 try:
344 c.forbid_adding_reviewers = rules.get(
355 c.forbid_adding_reviewers = rules.get(
345 'forbid_adding_reviewers')
356 'forbid_adding_reviewers')
346 c.forbid_author_to_review = rules.get(
357 c.forbid_author_to_review = rules.get(
347 'forbid_author_to_review')
358 'forbid_author_to_review')
348 c.forbid_commit_author_to_review = rules.get(
359 c.forbid_commit_author_to_review = rules.get(
349 'forbid_commit_author_to_review')
360 'forbid_commit_author_to_review')
350 except Exception:
361 except Exception:
351 pass
362 pass
352
363
353 # check merge capabilities
364 # check merge capabilities
354 _merge_check = MergeCheck.validate(
365 _merge_check = MergeCheck.validate(
355 pull_request_latest, user=self._rhodecode_user)
366 pull_request_latest, user=self._rhodecode_user)
356 c.pr_merge_errors = _merge_check.error_details
367 c.pr_merge_errors = _merge_check.error_details
357 c.pr_merge_possible = not _merge_check.failed
368 c.pr_merge_possible = not _merge_check.failed
358 c.pr_merge_message = _merge_check.merge_msg
369 c.pr_merge_message = _merge_check.merge_msg
359
370
360 c.pull_request_review_status = _merge_check.review_status
371 c.pull_request_review_status = _merge_check.review_status
361 if merge_checks:
372 if merge_checks:
362 self.request.override_renderer = \
373 self.request.override_renderer = \
363 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
374 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
364 return self._get_template_context(c)
375 return self._get_template_context(c)
365
376
366 comments_model = CommentsModel()
377 comments_model = CommentsModel()
367
378
368 # reviewers and statuses
379 # reviewers and statuses
369 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
380 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
370 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
381 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
371
382
372 # GENERAL COMMENTS with versions #
383 # GENERAL COMMENTS with versions #
373 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
384 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
374 q = q.order_by(ChangesetComment.comment_id.asc())
385 q = q.order_by(ChangesetComment.comment_id.asc())
375 general_comments = q
386 general_comments = q
376
387
377 # pick comments we want to render at current version
388 # pick comments we want to render at current version
378 c.comment_versions = comments_model.aggregate_comments(
389 c.comment_versions = comments_model.aggregate_comments(
379 general_comments, versions, c.at_version_num)
390 general_comments, versions, c.at_version_num)
380 c.comments = c.comment_versions[c.at_version_num]['until']
391 c.comments = c.comment_versions[c.at_version_num]['until']
381
392
382 # INLINE COMMENTS with versions #
393 # INLINE COMMENTS with versions #
383 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
394 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
384 q = q.order_by(ChangesetComment.comment_id.asc())
395 q = q.order_by(ChangesetComment.comment_id.asc())
385 inline_comments = q
396 inline_comments = q
386
397
387 c.inline_versions = comments_model.aggregate_comments(
398 c.inline_versions = comments_model.aggregate_comments(
388 inline_comments, versions, c.at_version_num, inline=True)
399 inline_comments, versions, c.at_version_num, inline=True)
389
400
390 # inject latest version
401 # inject latest version
391 latest_ver = PullRequest.get_pr_display_object(
402 latest_ver = PullRequest.get_pr_display_object(
392 pull_request_latest, pull_request_latest)
403 pull_request_latest, pull_request_latest)
393
404
394 c.versions = versions + [latest_ver]
405 c.versions = versions + [latest_ver]
395
406
396 # if we use version, then do not show later comments
407 # if we use version, then do not show later comments
397 # than current version
408 # than current version
398 display_inline_comments = collections.defaultdict(
409 display_inline_comments = collections.defaultdict(
399 lambda: collections.defaultdict(list))
410 lambda: collections.defaultdict(list))
400 for co in inline_comments:
411 for co in inline_comments:
401 if c.at_version_num:
412 if c.at_version_num:
402 # pick comments that are at least UPTO given version, so we
413 # pick comments that are at least UPTO given version, so we
403 # don't render comments for higher version
414 # don't render comments for higher version
404 should_render = co.pull_request_version_id and \
415 should_render = co.pull_request_version_id and \
405 co.pull_request_version_id <= c.at_version_num
416 co.pull_request_version_id <= c.at_version_num
406 else:
417 else:
407 # showing all, for 'latest'
418 # showing all, for 'latest'
408 should_render = True
419 should_render = True
409
420
410 if should_render:
421 if should_render:
411 display_inline_comments[co.f_path][co.line_no].append(co)
422 display_inline_comments[co.f_path][co.line_no].append(co)
412
423
413 # load diff data into template context, if we use compare mode then
424 # load diff data into template context, if we use compare mode then
414 # diff is calculated based on changes between versions of PR
425 # diff is calculated based on changes between versions of PR
415
426
416 source_repo = pull_request_at_ver.source_repo
427 source_repo = pull_request_at_ver.source_repo
417 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
428 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
418
429
419 target_repo = pull_request_at_ver.target_repo
430 target_repo = pull_request_at_ver.target_repo
420 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
431 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
421
432
422 if compare:
433 if compare:
423 # in compare switch the diff base to latest commit from prev version
434 # in compare switch the diff base to latest commit from prev version
424 target_ref_id = prev_pull_request_display_obj.revisions[0]
435 target_ref_id = prev_pull_request_display_obj.revisions[0]
425
436
426 # despite opening commits for bookmarks/branches/tags, we always
437 # despite opening commits for bookmarks/branches/tags, we always
427 # convert this to rev to prevent changes after bookmark or branch change
438 # convert this to rev to prevent changes after bookmark or branch change
428 c.source_ref_type = 'rev'
439 c.source_ref_type = 'rev'
429 c.source_ref = source_ref_id
440 c.source_ref = source_ref_id
430
441
431 c.target_ref_type = 'rev'
442 c.target_ref_type = 'rev'
432 c.target_ref = target_ref_id
443 c.target_ref = target_ref_id
433
444
434 c.source_repo = source_repo
445 c.source_repo = source_repo
435 c.target_repo = target_repo
446 c.target_repo = target_repo
436
447
437 c.commit_ranges = []
448 c.commit_ranges = []
438 source_commit = EmptyCommit()
449 source_commit = EmptyCommit()
439 target_commit = EmptyCommit()
450 target_commit = EmptyCommit()
440 c.missing_requirements = False
451 c.missing_requirements = False
441
452
442 source_scm = source_repo.scm_instance()
453 source_scm = source_repo.scm_instance()
443 target_scm = target_repo.scm_instance()
454 target_scm = target_repo.scm_instance()
444
455
445 # try first shadow repo, fallback to regular repo
456 # try first shadow repo, fallback to regular repo
446 try:
457 try:
447 commits_source_repo = pull_request_latest.get_shadow_repo()
458 commits_source_repo = pull_request_latest.get_shadow_repo()
448 except Exception:
459 except Exception:
449 log.debug('Failed to get shadow repo', exc_info=True)
460 log.debug('Failed to get shadow repo', exc_info=True)
450 commits_source_repo = source_scm
461 commits_source_repo = source_scm
451
462
452 c.commits_source_repo = commits_source_repo
463 c.commits_source_repo = commits_source_repo
453 commit_cache = {}
464 commit_cache = {}
454 try:
465 try:
455 pre_load = ["author", "branch", "date", "message"]
466 pre_load = ["author", "branch", "date", "message"]
456 show_revs = pull_request_at_ver.revisions
467 show_revs = pull_request_at_ver.revisions
457 for rev in show_revs:
468 for rev in show_revs:
458 comm = commits_source_repo.get_commit(
469 comm = commits_source_repo.get_commit(
459 commit_id=rev, pre_load=pre_load)
470 commit_id=rev, pre_load=pre_load)
460 c.commit_ranges.append(comm)
471 c.commit_ranges.append(comm)
461 commit_cache[comm.raw_id] = comm
472 commit_cache[comm.raw_id] = comm
462
473
463 # Order here matters, we first need to get target, and then
474 # Order here matters, we first need to get target, and then
464 # the source
475 # the source
465 target_commit = commits_source_repo.get_commit(
476 target_commit = commits_source_repo.get_commit(
466 commit_id=safe_str(target_ref_id))
477 commit_id=safe_str(target_ref_id))
467
478
468 source_commit = commits_source_repo.get_commit(
479 source_commit = commits_source_repo.get_commit(
469 commit_id=safe_str(source_ref_id))
480 commit_id=safe_str(source_ref_id))
470
481
471 except CommitDoesNotExistError:
482 except CommitDoesNotExistError:
472 log.warning(
483 log.warning(
473 'Failed to get commit from `{}` repo'.format(
484 'Failed to get commit from `{}` repo'.format(
474 commits_source_repo), exc_info=True)
485 commits_source_repo), exc_info=True)
475 except RepositoryRequirementError:
486 except RepositoryRequirementError:
476 log.warning(
487 log.warning(
477 'Failed to get all required data from repo', exc_info=True)
488 'Failed to get all required data from repo', exc_info=True)
478 c.missing_requirements = True
489 c.missing_requirements = True
479
490
480 c.ancestor = None # set it to None, to hide it from PR view
491 c.ancestor = None # set it to None, to hide it from PR view
481
492
482 try:
493 try:
483 ancestor_id = source_scm.get_common_ancestor(
494 ancestor_id = source_scm.get_common_ancestor(
484 source_commit.raw_id, target_commit.raw_id, target_scm)
495 source_commit.raw_id, target_commit.raw_id, target_scm)
485 c.ancestor_commit = source_scm.get_commit(ancestor_id)
496 c.ancestor_commit = source_scm.get_commit(ancestor_id)
486 except Exception:
497 except Exception:
487 c.ancestor_commit = None
498 c.ancestor_commit = None
488
499
489 c.statuses = source_repo.statuses(
500 c.statuses = source_repo.statuses(
490 [x.raw_id for x in c.commit_ranges])
501 [x.raw_id for x in c.commit_ranges])
491
502
492 # auto collapse if we have more than limit
503 # auto collapse if we have more than limit
493 collapse_limit = diffs.DiffProcessor._collapse_commits_over
504 collapse_limit = diffs.DiffProcessor._collapse_commits_over
494 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
505 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
495 c.compare_mode = compare
506 c.compare_mode = compare
496
507
497 # diff_limit is the old behavior, will cut off the whole diff
508 # diff_limit is the old behavior, will cut off the whole diff
498 # if the limit is applied otherwise will just hide the
509 # if the limit is applied otherwise will just hide the
499 # big files from the front-end
510 # big files from the front-end
500 diff_limit = c.visual.cut_off_limit_diff
511 diff_limit = c.visual.cut_off_limit_diff
501 file_limit = c.visual.cut_off_limit_file
512 file_limit = c.visual.cut_off_limit_file
502
513
503 c.missing_commits = False
514 c.missing_commits = False
504 if (c.missing_requirements
515 if (c.missing_requirements
505 or isinstance(source_commit, EmptyCommit)
516 or isinstance(source_commit, EmptyCommit)
506 or source_commit == target_commit):
517 or source_commit == target_commit):
507
518
508 c.missing_commits = True
519 c.missing_commits = True
509 else:
520 else:
510
521
511 c.diffset = self._get_diffset(
522 c.diffset = self._get_diffset(
512 c.source_repo.repo_name, commits_source_repo,
523 c.source_repo.repo_name, commits_source_repo,
513 source_ref_id, target_ref_id,
524 source_ref_id, target_ref_id,
514 target_commit, source_commit,
525 target_commit, source_commit,
515 diff_limit, c.fulldiff, file_limit, display_inline_comments)
526 diff_limit, c.fulldiff, file_limit, display_inline_comments)
516
527
517 c.limited_diff = c.diffset.limited_diff
528 c.limited_diff = c.diffset.limited_diff
518
529
519 # calculate removed files that are bound to comments
530 # calculate removed files that are bound to comments
520 comment_deleted_files = [
531 comment_deleted_files = [
521 fname for fname in display_inline_comments
532 fname for fname in display_inline_comments
522 if fname not in c.diffset.file_stats]
533 if fname not in c.diffset.file_stats]
523
534
524 c.deleted_files_comments = collections.defaultdict(dict)
535 c.deleted_files_comments = collections.defaultdict(dict)
525 for fname, per_line_comments in display_inline_comments.items():
536 for fname, per_line_comments in display_inline_comments.items():
526 if fname in comment_deleted_files:
537 if fname in comment_deleted_files:
527 c.deleted_files_comments[fname]['stats'] = 0
538 c.deleted_files_comments[fname]['stats'] = 0
528 c.deleted_files_comments[fname]['comments'] = list()
539 c.deleted_files_comments[fname]['comments'] = list()
529 for lno, comments in per_line_comments.items():
540 for lno, comments in per_line_comments.items():
530 c.deleted_files_comments[fname]['comments'].extend(
541 c.deleted_files_comments[fname]['comments'].extend(
531 comments)
542 comments)
532
543
533 # this is a hack to properly display links, when creating PR, the
544 # this is a hack to properly display links, when creating PR, the
534 # compare view and others uses different notation, and
545 # compare view and others uses different notation, and
535 # compare_commits.mako renders links based on the target_repo.
546 # compare_commits.mako renders links based on the target_repo.
536 # We need to swap that here to generate it properly on the html side
547 # We need to swap that here to generate it properly on the html side
537 c.target_repo = c.source_repo
548 c.target_repo = c.source_repo
538
549
539 c.commit_statuses = ChangesetStatus.STATUSES
550 c.commit_statuses = ChangesetStatus.STATUSES
540
551
541 c.show_version_changes = not pr_closed
552 c.show_version_changes = not pr_closed
542 if c.show_version_changes:
553 if c.show_version_changes:
543 cur_obj = pull_request_at_ver
554 cur_obj = pull_request_at_ver
544 prev_obj = prev_pull_request_at_ver
555 prev_obj = prev_pull_request_at_ver
545
556
546 old_commit_ids = prev_obj.revisions
557 old_commit_ids = prev_obj.revisions
547 new_commit_ids = cur_obj.revisions
558 new_commit_ids = cur_obj.revisions
548 commit_changes = PullRequestModel()._calculate_commit_id_changes(
559 commit_changes = PullRequestModel()._calculate_commit_id_changes(
549 old_commit_ids, new_commit_ids)
560 old_commit_ids, new_commit_ids)
550 c.commit_changes_summary = commit_changes
561 c.commit_changes_summary = commit_changes
551
562
552 # calculate the diff for commits between versions
563 # calculate the diff for commits between versions
553 c.commit_changes = []
564 c.commit_changes = []
554 mark = lambda cs, fw: list(
565 mark = lambda cs, fw: list(
555 h.itertools.izip_longest([], cs, fillvalue=fw))
566 h.itertools.izip_longest([], cs, fillvalue=fw))
556 for c_type, raw_id in mark(commit_changes.added, 'a') \
567 for c_type, raw_id in mark(commit_changes.added, 'a') \
557 + mark(commit_changes.removed, 'r') \
568 + mark(commit_changes.removed, 'r') \
558 + mark(commit_changes.common, 'c'):
569 + mark(commit_changes.common, 'c'):
559
570
560 if raw_id in commit_cache:
571 if raw_id in commit_cache:
561 commit = commit_cache[raw_id]
572 commit = commit_cache[raw_id]
562 else:
573 else:
563 try:
574 try:
564 commit = commits_source_repo.get_commit(raw_id)
575 commit = commits_source_repo.get_commit(raw_id)
565 except CommitDoesNotExistError:
576 except CommitDoesNotExistError:
566 # in case we fail extracting still use "dummy" commit
577 # in case we fail extracting still use "dummy" commit
567 # for display in commit diff
578 # for display in commit diff
568 commit = h.AttributeDict(
579 commit = h.AttributeDict(
569 {'raw_id': raw_id,
580 {'raw_id': raw_id,
570 'message': 'EMPTY or MISSING COMMIT'})
581 'message': 'EMPTY or MISSING COMMIT'})
571 c.commit_changes.append([c_type, commit])
582 c.commit_changes.append([c_type, commit])
572
583
573 # current user review statuses for each version
584 # current user review statuses for each version
574 c.review_versions = {}
585 c.review_versions = {}
575 if self._rhodecode_user.user_id in allowed_reviewers:
586 if self._rhodecode_user.user_id in allowed_reviewers:
576 for co in general_comments:
587 for co in general_comments:
577 if co.author.user_id == self._rhodecode_user.user_id:
588 if co.author.user_id == self._rhodecode_user.user_id:
578 # each comment has a status change
589 # each comment has a status change
579 status = co.status_change
590 status = co.status_change
580 if status:
591 if status:
581 _ver_pr = status[0].comment.pull_request_version_id
592 _ver_pr = status[0].comment.pull_request_version_id
582 c.review_versions[_ver_pr] = status[0]
593 c.review_versions[_ver_pr] = status[0]
583
594
584 return self._get_template_context(c)
595 return self._get_template_context(c)
596
597 def assure_not_empty_repo(self):
598 _ = self.request.translate
599
600 try:
601 self.db_repo.scm_instance().get_commit()
602 except EmptyRepositoryError:
603 h.flash(h.literal(_('There are no commits yet')),
604 category='warning')
605 raise HTTPFound(
606 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
607
608 @LoginRequired()
609 @NotAnonymous()
610 @HasRepoPermissionAnyDecorator(
611 'repository.read', 'repository.write', 'repository.admin')
612 @view_config(
613 route_name='pullrequest_new', request_method='GET',
614 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
615 def pull_request_new(self):
616 _ = self.request.translate
617 c = self.load_default_context()
618
619 self.assure_not_empty_repo()
620 source_repo = self.db_repo
621
622 commit_id = self.request.GET.get('commit')
623 branch_ref = self.request.GET.get('branch')
624 bookmark_ref = self.request.GET.get('bookmark')
625
626 try:
627 source_repo_data = PullRequestModel().generate_repo_data(
628 source_repo, commit_id=commit_id,
629 branch=branch_ref, bookmark=bookmark_ref)
630 except CommitDoesNotExistError as e:
631 log.exception(e)
632 h.flash(_('Commit does not exist'), 'error')
633 raise HTTPFound(
634 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
635
636 default_target_repo = source_repo
637
638 if source_repo.parent:
639 parent_vcs_obj = source_repo.parent.scm_instance()
640 if parent_vcs_obj and not parent_vcs_obj.is_empty():
641 # change default if we have a parent repo
642 default_target_repo = source_repo.parent
643
644 target_repo_data = PullRequestModel().generate_repo_data(
645 default_target_repo)
646
647 selected_source_ref = source_repo_data['refs']['selected_ref']
648
649 title_source_ref = selected_source_ref.split(':', 2)[1]
650 c.default_title = PullRequestModel().generate_pullrequest_title(
651 source=source_repo.repo_name,
652 source_ref=title_source_ref,
653 target=default_target_repo.repo_name
654 )
655
656 c.default_repo_data = {
657 'source_repo_name': source_repo.repo_name,
658 'source_refs_json': json.dumps(source_repo_data),
659 'target_repo_name': default_target_repo.repo_name,
660 'target_refs_json': json.dumps(target_repo_data),
661 }
662 c.default_source_ref = selected_source_ref
663
664 return self._get_template_context(c)
665
666 @LoginRequired()
667 @NotAnonymous()
668 @HasRepoPermissionAnyDecorator(
669 'repository.read', 'repository.write', 'repository.admin')
670 @view_config(
671 route_name='pullrequest_repo_refs', request_method='GET',
672 renderer='json_ext', xhr=True)
673 def pull_request_repo_refs(self):
674 target_repo_name = self.request.matchdict['target_repo_name']
675 repo = Repository.get_by_repo_name(target_repo_name)
676 if not repo:
677 raise HTTPNotFound()
678 return PullRequestModel().generate_repo_data(repo)
679
680 @LoginRequired()
681 @NotAnonymous()
682 @HasRepoPermissionAnyDecorator(
683 'repository.read', 'repository.write', 'repository.admin')
684 @view_config(
685 route_name='pullrequest_repo_destinations', request_method='GET',
686 renderer='json_ext', xhr=True)
687 def pull_request_repo_destinations(self):
688 _ = self.request.translate
689 filter_query = self.request.GET.get('query')
690
691 query = Repository.query() \
692 .order_by(func.length(Repository.repo_name)) \
693 .filter(
694 or_(Repository.repo_name == self.db_repo.repo_name,
695 Repository.fork_id == self.db_repo.repo_id))
696
697 if filter_query:
698 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
699 query = query.filter(
700 Repository.repo_name.ilike(ilike_expression))
701
702 add_parent = False
703 if self.db_repo.parent:
704 if filter_query in self.db_repo.parent.repo_name:
705 parent_vcs_obj = self.db_repo.parent.scm_instance()
706 if parent_vcs_obj and not parent_vcs_obj.is_empty():
707 add_parent = True
708
709 limit = 20 - 1 if add_parent else 20
710 all_repos = query.limit(limit).all()
711 if add_parent:
712 all_repos += [self.db_repo.parent]
713
714 repos = []
715 for obj in ScmModel().get_repos(all_repos):
716 repos.append({
717 'id': obj['name'],
718 'text': obj['name'],
719 'type': 'repo',
720 'obj': obj['dbrepo']
721 })
722
723 data = {
724 'more': False,
725 'results': [{
726 'text': _('Repositories'),
727 'children': repos
728 }] if repos else []
729 }
730 return data
731
732 @LoginRequired()
733 @NotAnonymous()
734 @HasRepoPermissionAnyDecorator(
735 'repository.read', 'repository.write', 'repository.admin')
736 @CSRFRequired()
737 @view_config(
738 route_name='pullrequest_create', request_method='POST',
739 renderer=None)
740 def pull_request_create(self):
741 _ = self.request.translate
742 self.assure_not_empty_repo()
743
744 controls = peppercorn.parse(self.request.POST.items())
745
746 try:
747 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
748 except formencode.Invalid as errors:
749 if errors.error_dict.get('revisions'):
750 msg = 'Revisions: %s' % errors.error_dict['revisions']
751 elif errors.error_dict.get('pullrequest_title'):
752 msg = _('Pull request requires a title with min. 3 chars')
753 else:
754 msg = _('Error creating pull request: {}').format(errors)
755 log.exception(msg)
756 h.flash(msg, 'error')
757
758 # would rather just go back to form ...
759 raise HTTPFound(
760 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
761
762 source_repo = _form['source_repo']
763 source_ref = _form['source_ref']
764 target_repo = _form['target_repo']
765 target_ref = _form['target_ref']
766 commit_ids = _form['revisions'][::-1]
767
768 # find the ancestor for this pr
769 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
770 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
771
772 source_scm = source_db_repo.scm_instance()
773 target_scm = target_db_repo.scm_instance()
774
775 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
776 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
777
778 ancestor = source_scm.get_common_ancestor(
779 source_commit.raw_id, target_commit.raw_id, target_scm)
780
781 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
782 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
783
784 pullrequest_title = _form['pullrequest_title']
785 title_source_ref = source_ref.split(':', 2)[1]
786 if not pullrequest_title:
787 pullrequest_title = PullRequestModel().generate_pullrequest_title(
788 source=source_repo,
789 source_ref=title_source_ref,
790 target=target_repo
791 )
792
793 description = _form['pullrequest_desc']
794
795 get_default_reviewers_data, validate_default_reviewers = \
796 PullRequestModel().get_reviewer_functions()
797
798 # recalculate reviewers logic, to make sure we can validate this
799 reviewer_rules = get_default_reviewers_data(
800 self._rhodecode_db_user, source_db_repo,
801 source_commit, target_db_repo, target_commit)
802
803 given_reviewers = _form['review_members']
804 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
805
806 try:
807 pull_request = PullRequestModel().create(
808 self._rhodecode_user.user_id, source_repo, source_ref, target_repo,
809 target_ref, commit_ids, reviewers, pullrequest_title,
810 description, reviewer_rules
811 )
812 Session().commit()
813 h.flash(_('Successfully opened new pull request'),
814 category='success')
815 except Exception as e:
816 msg = _('Error occurred during creation of this pull request.')
817 log.exception(msg)
818 h.flash(msg, category='error')
819 raise HTTPFound(
820 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
821
822 raise HTTPFound(
823 h.route_path('pullrequest_show', repo_name=target_repo,
824 pull_request_id=pull_request.pull_request_id))
825
826 @LoginRequired()
827 @NotAnonymous()
828 @HasRepoPermissionAnyDecorator(
829 'repository.read', 'repository.write', 'repository.admin')
830 @CSRFRequired()
831 @view_config(
832 route_name='pullrequest_update', request_method='POST',
833 renderer='json_ext')
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)
837
838 # only owner or admin can update it
839 allowed_to_update = PullRequestModel().check_user_update(
840 pull_request, self._rhodecode_user)
841 if allowed_to_update:
842 controls = peppercorn.parse(self.request.POST.items())
843
844 if 'review_members' in controls:
845 self._update_reviewers(
846 pull_request_id, controls['review_members'],
847 pull_request.reviewer_data)
848 elif str2bool(self.request.POST.get('update_commits', 'false')):
849 self._update_commits(pull_request)
850 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
851 self._edit_pull_request(pull_request)
852 else:
853 raise HTTPBadRequest()
854 return True
855 raise HTTPForbidden()
856
857 def _edit_pull_request(self, pull_request):
858 _ = self.request.translate
859 try:
860 PullRequestModel().edit(
861 pull_request, self.request.POST.get('title'),
862 self.request.POST.get('description'), self._rhodecode_user)
863 except ValueError:
864 msg = _(u'Cannot update closed pull requests.')
865 h.flash(msg, category='error')
866 return
867 else:
868 Session().commit()
869
870 msg = _(u'Pull request title & description updated.')
871 h.flash(msg, category='success')
872 return
873
874 def _update_commits(self, pull_request):
875 _ = self.request.translate
876 resp = PullRequestModel().update_commits(pull_request)
877
878 if resp.executed:
879
880 if resp.target_changed and resp.source_changed:
881 changed = 'target and source repositories'
882 elif resp.target_changed and not resp.source_changed:
883 changed = 'target repository'
884 elif not resp.target_changed and resp.source_changed:
885 changed = 'source repository'
886 else:
887 changed = 'nothing'
888
889 msg = _(
890 u'Pull request updated to "{source_commit_id}" with '
891 u'{count_added} added, {count_removed} removed commits. '
892 u'Source of changes: {change_source}')
893 msg = msg.format(
894 source_commit_id=pull_request.source_ref_parts.commit_id,
895 count_added=len(resp.changes.added),
896 count_removed=len(resp.changes.removed),
897 change_source=changed)
898 h.flash(msg, category='success')
899
900 channel = '/repo${}$/pr/{}'.format(
901 pull_request.target_repo.repo_name,
902 pull_request.pull_request_id)
903 message = msg + (
904 ' - <a onclick="window.location.reload()">'
905 '<strong>{}</strong></a>'.format(_('Reload page')))
906 channelstream.post_message(
907 channel, message, self._rhodecode_user.username,
908 registry=self.request.registry)
909 else:
910 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
911 warning_reasons = [
912 UpdateFailureReason.NO_CHANGE,
913 UpdateFailureReason.WRONG_REF_TYPE,
914 ]
915 category = 'warning' if resp.reason in warning_reasons else 'error'
916 h.flash(msg, category=category)
917
918 @LoginRequired()
919 @NotAnonymous()
920 @HasRepoPermissionAnyDecorator(
921 'repository.read', 'repository.write', 'repository.admin')
922 @CSRFRequired()
923 @view_config(
924 route_name='pullrequest_merge', request_method='POST',
925 renderer='json_ext')
926 def pull_request_merge(self):
927 """
928 Merge will perform a server-side merge of the specified
929 pull request, if the pull request is approved and mergeable.
930 After successful merging, the pull request is automatically
931 closed, with a relevant comment.
932 """
933 pull_request_id = self.request.matchdict['pull_request_id']
934 pull_request = PullRequest.get_or_404(pull_request_id)
935
936 check = MergeCheck.validate(pull_request, self._rhodecode_db_user)
937 merge_possible = not check.failed
938
939 for err_type, error_msg in check.errors:
940 h.flash(error_msg, category=err_type)
941
942 if merge_possible:
943 log.debug("Pre-conditions checked, trying to merge.")
944 extras = vcs_operation_context(
945 self.request.environ, repo_name=pull_request.target_repo.repo_name,
946 username=self._rhodecode_db_user.username, action='push',
947 scm=pull_request.target_repo.repo_type)
948 self._merge_pull_request(
949 pull_request, self._rhodecode_db_user, extras)
950 else:
951 log.debug("Pre-conditions failed, NOT merging.")
952
953 raise HTTPFound(
954 h.route_path('pullrequest_show',
955 repo_name=pull_request.target_repo.repo_name,
956 pull_request_id=pull_request.pull_request_id))
957
958 def _merge_pull_request(self, pull_request, user, extras):
959 _ = self.request.translate
960 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
961
962 if merge_resp.executed:
963 log.debug("The merge was successful, closing the pull request.")
964 PullRequestModel().close_pull_request(
965 pull_request.pull_request_id, user)
966 Session().commit()
967 msg = _('Pull request was successfully merged and closed.')
968 h.flash(msg, category='success')
969 else:
970 log.debug(
971 "The merge was not successful. Merge response: %s",
972 merge_resp)
973 msg = PullRequestModel().merge_status_message(
974 merge_resp.failure_reason)
975 h.flash(msg, category='error')
976
977 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
978 _ = self.request.translate
979 get_default_reviewers_data, validate_default_reviewers = \
980 PullRequestModel().get_reviewer_functions()
981
982 try:
983 reviewers = validate_default_reviewers(review_members, reviewer_rules)
984 except ValueError as e:
985 log.error('Reviewers Validation: {}'.format(e))
986 h.flash(e, category='error')
987 return
988
989 PullRequestModel().update_reviewers(
990 pull_request_id, reviewers, self._rhodecode_user)
991 h.flash(_('Pull request reviewers updated.'), category='success')
992 Session().commit()
993
994 @LoginRequired()
995 @NotAnonymous()
996 @HasRepoPermissionAnyDecorator(
997 'repository.read', 'repository.write', 'repository.admin')
998 @CSRFRequired()
999 @view_config(
1000 route_name='pullrequest_delete', request_method='POST',
1001 renderer='json_ext')
1002 def pull_request_delete(self):
1003 _ = self.request.translate
1004
1005 pull_request_id = self.request.matchdict['pull_request_id']
1006 pull_request = PullRequest.get_or_404(pull_request_id)
1007
1008 pr_closed = pull_request.is_closed()
1009 allowed_to_delete = PullRequestModel().check_user_delete(
1010 pull_request, self._rhodecode_user) and not pr_closed
1011
1012 # only owner can delete it !
1013 if allowed_to_delete:
1014 PullRequestModel().delete(pull_request, self._rhodecode_user)
1015 Session().commit()
1016 h.flash(_('Successfully deleted pull request'),
1017 category='success')
1018 raise HTTPFound(h.route_path('my_account_pullrequests'))
1019
1020 log.warning('user %s tried to delete pull request without access',
1021 self._rhodecode_user)
1022 raise HTTPNotFound()
1023
1024 @LoginRequired()
1025 @NotAnonymous()
1026 @HasRepoPermissionAnyDecorator(
1027 'repository.read', 'repository.write', 'repository.admin')
1028 @CSRFRequired()
1029 @view_config(
1030 route_name='pullrequest_comment_create', request_method='POST',
1031 renderer='json_ext')
1032 def pull_request_comment_create(self):
1033 _ = self.request.translate
1034 pull_request_id = self.request.matchdict['pull_request_id']
1035 pull_request = PullRequest.get_or_404(pull_request_id)
1036 if pull_request.is_closed():
1037 log.debug('comment: forbidden because pull request is closed')
1038 raise HTTPForbidden()
1039
1040 c = self.load_default_context()
1041
1042 status = self.request.POST.get('changeset_status', None)
1043 text = self.request.POST.get('text')
1044 comment_type = self.request.POST.get('comment_type')
1045 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1046 close_pull_request = self.request.POST.get('close_pull_request')
1047
1048 # the logic here should work like following, if we submit close
1049 # pr comment, use `close_pull_request_with_comment` function
1050 # else handle regular comment logic
1051
1052 if close_pull_request:
1053 # only owner or admin or person with write permissions
1054 allowed_to_close = PullRequestModel().check_user_update(
1055 pull_request, self._rhodecode_user)
1056 if not allowed_to_close:
1057 log.debug('comment: forbidden because not allowed to close '
1058 'pull request %s', pull_request_id)
1059 raise HTTPForbidden()
1060 comment, status = PullRequestModel().close_pull_request_with_comment(
1061 pull_request, self._rhodecode_user, self.db_repo, message=text)
1062 Session().flush()
1063 events.trigger(
1064 events.PullRequestCommentEvent(pull_request, comment))
1065
1066 else:
1067 # regular comment case, could be inline, or one with status.
1068 # for that one we check also permissions
1069
1070 allowed_to_change_status = PullRequestModel().check_user_change_status(
1071 pull_request, self._rhodecode_user)
1072
1073 if status and allowed_to_change_status:
1074 message = (_('Status change %(transition_icon)s %(status)s')
1075 % {'transition_icon': '>',
1076 'status': ChangesetStatus.get_status_lbl(status)})
1077 text = text or message
1078
1079 comment = CommentsModel().create(
1080 text=text,
1081 repo=self.db_repo.repo_id,
1082 user=self._rhodecode_user.user_id,
1083 pull_request=pull_request_id,
1084 f_path=self.request.POST.get('f_path'),
1085 line_no=self.request.POST.get('line'),
1086 status_change=(ChangesetStatus.get_status_lbl(status)
1087 if status and allowed_to_change_status else None),
1088 status_change_type=(status
1089 if status and allowed_to_change_status else None),
1090 comment_type=comment_type,
1091 resolves_comment_id=resolves_comment_id
1092 )
1093
1094 if allowed_to_change_status:
1095 # calculate old status before we change it
1096 old_calculated_status = pull_request.calculated_review_status()
1097
1098 # get status if set !
1099 if status:
1100 ChangesetStatusModel().set_status(
1101 self.db_repo.repo_id,
1102 status,
1103 self._rhodecode_user.user_id,
1104 comment,
1105 pull_request=pull_request_id
1106 )
1107
1108 Session().flush()
1109 events.trigger(
1110 events.PullRequestCommentEvent(pull_request, comment))
1111
1112 # we now calculate the status of pull request, and based on that
1113 # calculation we set the commits status
1114 calculated_status = pull_request.calculated_review_status()
1115 if old_calculated_status != calculated_status:
1116 PullRequestModel()._trigger_pull_request_hook(
1117 pull_request, self._rhodecode_user, 'review_status_change')
1118
1119 Session().commit()
1120
1121 data = {
1122 'target_id': h.safeid(h.safe_unicode(
1123 self.request.POST.get('f_path'))),
1124 }
1125 if comment:
1126 c.co = comment
1127 rendered_comment = render(
1128 'rhodecode:templates/changeset/changeset_comment_block.mako',
1129 self._get_template_context(c), self.request)
1130
1131 data.update(comment.get_dict())
1132 data.update({'rendered_text': rendered_comment})
1133
1134 return data
1135
1136 @LoginRequired()
1137 @NotAnonymous()
1138 @HasRepoPermissionAnyDecorator(
1139 'repository.read', 'repository.write', 'repository.admin')
1140 @CSRFRequired()
1141 @view_config(
1142 route_name='pullrequest_comment_delete', request_method='POST',
1143 renderer='json_ext')
1144 def pull_request_comment_delete(self):
1145 commit_id = self.request.matchdict['commit_id']
1146 comment_id = self.request.matchdict['comment_id']
1147 pull_request_id = self.request.matchdict['pull_request_id']
1148
1149 pull_request = PullRequest.get_or_404(pull_request_id)
1150 if pull_request.is_closed():
1151 log.debug('comment: forbidden because pull request is closed')
1152 raise HTTPForbidden()
1153
1154 comment = ChangesetComment.get_or_404(comment_id)
1155 if not comment:
1156 log.debug('Comment with id:%s not found, skipping', comment_id)
1157 # comment already deleted in another call probably
1158 return True
1159
1160 if comment.pull_request.is_closed():
1161 # don't allow deleting comments on closed pull request
1162 raise HTTPForbidden()
1163
1164 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1165 super_admin = h.HasPermissionAny('hg.admin')()
1166 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1167 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1168 comment_repo_admin = is_repo_admin and is_repo_comment
1169
1170 if super_admin or comment_owner or comment_repo_admin:
1171 old_calculated_status = comment.pull_request.calculated_review_status()
1172 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1173 Session().commit()
1174 calculated_status = comment.pull_request.calculated_review_status()
1175 if old_calculated_status != calculated_status:
1176 PullRequestModel()._trigger_pull_request_hook(
1177 comment.pull_request, self._rhodecode_user, 'review_status_change')
1178 return True
1179 else:
1180 log.warning('No permissions for user %s to delete comment_id: %s',
1181 self._rhodecode_db_user, comment_id)
1182 raise HTTPNotFound()
@@ -1,588 +1,521 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Routes configuration
22 Routes configuration
23
23
24 The more specific and detailed routes should be defined first so they
24 The more specific and detailed routes should be defined first so they
25 may take precedent over the more generic routes. For more information
25 may take precedent over the more generic routes. For more information
26 refer to the routes manual at http://routes.groovie.org/docs/
26 refer to the routes manual at http://routes.groovie.org/docs/
27
27
28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 and _route_name variable which uses some of stored naming here to do redirects.
29 and _route_name variable which uses some of stored naming here to do redirects.
30 """
30 """
31 import os
31 import os
32 import re
32 import re
33 from routes import Mapper
33 from routes import Mapper
34
34
35 # prefix for non repository related links needs to be prefixed with `/`
35 # prefix for non repository related links needs to be prefixed with `/`
36 ADMIN_PREFIX = '/_admin'
36 ADMIN_PREFIX = '/_admin'
37 STATIC_FILE_PREFIX = '/_static'
37 STATIC_FILE_PREFIX = '/_static'
38
38
39 # Default requirements for URL parts
39 # Default requirements for URL parts
40 URL_NAME_REQUIREMENTS = {
40 URL_NAME_REQUIREMENTS = {
41 # group name can have a slash in them, but they must not end with a slash
41 # group name can have a slash in them, but they must not end with a slash
42 'group_name': r'.*?[^/]',
42 'group_name': r'.*?[^/]',
43 'repo_group_name': r'.*?[^/]',
43 'repo_group_name': r'.*?[^/]',
44 # repo names can have a slash in them, but they must not end with a slash
44 # repo names can have a slash in them, but they must not end with a slash
45 'repo_name': r'.*?[^/]',
45 'repo_name': r'.*?[^/]',
46 # file path eats up everything at the end
46 # file path eats up everything at the end
47 'f_path': r'.*',
47 'f_path': r'.*',
48 # reference types
48 # reference types
49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 }
51 }
52
52
53
53
54 class JSRoutesMapper(Mapper):
54 class JSRoutesMapper(Mapper):
55 """
55 """
56 Wrapper for routes.Mapper to make pyroutes compatible url definitions
56 Wrapper for routes.Mapper to make pyroutes compatible url definitions
57 """
57 """
58 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
58 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
59 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
59 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
60 def __init__(self, *args, **kw):
60 def __init__(self, *args, **kw):
61 super(JSRoutesMapper, self).__init__(*args, **kw)
61 super(JSRoutesMapper, self).__init__(*args, **kw)
62 self._jsroutes = []
62 self._jsroutes = []
63
63
64 def connect(self, *args, **kw):
64 def connect(self, *args, **kw):
65 """
65 """
66 Wrapper for connect to take an extra argument jsroute=True
66 Wrapper for connect to take an extra argument jsroute=True
67
67
68 :param jsroute: boolean, if True will add the route to the pyroutes list
68 :param jsroute: boolean, if True will add the route to the pyroutes list
69 """
69 """
70 if kw.pop('jsroute', False):
70 if kw.pop('jsroute', False):
71 if not self._named_route_regex.match(args[0]):
71 if not self._named_route_regex.match(args[0]):
72 raise Exception('only named routes can be added to pyroutes')
72 raise Exception('only named routes can be added to pyroutes')
73 self._jsroutes.append(args[0])
73 self._jsroutes.append(args[0])
74
74
75 super(JSRoutesMapper, self).connect(*args, **kw)
75 super(JSRoutesMapper, self).connect(*args, **kw)
76
76
77 def _extract_route_information(self, route):
77 def _extract_route_information(self, route):
78 """
78 """
79 Convert a route into tuple(name, path, args), eg:
79 Convert a route into tuple(name, path, args), eg:
80 ('show_user', '/profile/%(username)s', ['username'])
80 ('show_user', '/profile/%(username)s', ['username'])
81 """
81 """
82 routepath = route.routepath
82 routepath = route.routepath
83 def replace(matchobj):
83 def replace(matchobj):
84 if matchobj.group(1):
84 if matchobj.group(1):
85 return "%%(%s)s" % matchobj.group(1).split(':')[0]
85 return "%%(%s)s" % matchobj.group(1).split(':')[0]
86 else:
86 else:
87 return "%%(%s)s" % matchobj.group(2)
87 return "%%(%s)s" % matchobj.group(2)
88
88
89 routepath = self._argument_prog.sub(replace, routepath)
89 routepath = self._argument_prog.sub(replace, routepath)
90 return (
90 return (
91 route.name,
91 route.name,
92 routepath,
92 routepath,
93 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
93 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
94 for arg in self._argument_prog.findall(route.routepath)]
94 for arg in self._argument_prog.findall(route.routepath)]
95 )
95 )
96
96
97 def jsroutes(self):
97 def jsroutes(self):
98 """
98 """
99 Return a list of pyroutes.js compatible routes
99 Return a list of pyroutes.js compatible routes
100 """
100 """
101 for route_name in self._jsroutes:
101 for route_name in self._jsroutes:
102 yield self._extract_route_information(self._routenames[route_name])
102 yield self._extract_route_information(self._routenames[route_name])
103
103
104
104
105 def make_map(config):
105 def make_map(config):
106 """Create, configure and return the routes Mapper"""
106 """Create, configure and return the routes Mapper"""
107 rmap = JSRoutesMapper(
107 rmap = JSRoutesMapper(
108 directory=config['pylons.paths']['controllers'],
108 directory=config['pylons.paths']['controllers'],
109 always_scan=config['debug'])
109 always_scan=config['debug'])
110 rmap.minimization = False
110 rmap.minimization = False
111 rmap.explicit = False
111 rmap.explicit = False
112
112
113 from rhodecode.lib.utils2 import str2bool
113 from rhodecode.lib.utils2 import str2bool
114 from rhodecode.model import repo, repo_group
114 from rhodecode.model import repo, repo_group
115
115
116 def check_repo(environ, match_dict):
116 def check_repo(environ, match_dict):
117 """
117 """
118 check for valid repository for proper 404 handling
118 check for valid repository for proper 404 handling
119
119
120 :param environ:
120 :param environ:
121 :param match_dict:
121 :param match_dict:
122 """
122 """
123 repo_name = match_dict.get('repo_name')
123 repo_name = match_dict.get('repo_name')
124
124
125 if match_dict.get('f_path'):
125 if match_dict.get('f_path'):
126 # fix for multiple initial slashes that causes errors
126 # fix for multiple initial slashes that causes errors
127 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
127 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
128 repo_model = repo.RepoModel()
128 repo_model = repo.RepoModel()
129 by_name_match = repo_model.get_by_repo_name(repo_name)
129 by_name_match = repo_model.get_by_repo_name(repo_name)
130 # if we match quickly from database, short circuit the operation,
130 # if we match quickly from database, short circuit the operation,
131 # and validate repo based on the type.
131 # and validate repo based on the type.
132 if by_name_match:
132 if by_name_match:
133 return True
133 return True
134
134
135 by_id_match = repo_model.get_repo_by_id(repo_name)
135 by_id_match = repo_model.get_repo_by_id(repo_name)
136 if by_id_match:
136 if by_id_match:
137 repo_name = by_id_match.repo_name
137 repo_name = by_id_match.repo_name
138 match_dict['repo_name'] = repo_name
138 match_dict['repo_name'] = repo_name
139 return True
139 return True
140
140
141 return False
141 return False
142
142
143 def check_group(environ, match_dict):
143 def check_group(environ, match_dict):
144 """
144 """
145 check for valid repository group path for proper 404 handling
145 check for valid repository group path for proper 404 handling
146
146
147 :param environ:
147 :param environ:
148 :param match_dict:
148 :param match_dict:
149 """
149 """
150 repo_group_name = match_dict.get('group_name')
150 repo_group_name = match_dict.get('group_name')
151 repo_group_model = repo_group.RepoGroupModel()
151 repo_group_model = repo_group.RepoGroupModel()
152 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
152 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
153 if by_name_match:
153 if by_name_match:
154 return True
154 return True
155
155
156 return False
156 return False
157
157
158 def check_user_group(environ, match_dict):
158 def check_user_group(environ, match_dict):
159 """
159 """
160 check for valid user group for proper 404 handling
160 check for valid user group for proper 404 handling
161
161
162 :param environ:
162 :param environ:
163 :param match_dict:
163 :param match_dict:
164 """
164 """
165 return True
165 return True
166
166
167 def check_int(environ, match_dict):
167 def check_int(environ, match_dict):
168 return match_dict.get('id').isdigit()
168 return match_dict.get('id').isdigit()
169
169
170
170
171 #==========================================================================
171 #==========================================================================
172 # CUSTOM ROUTES HERE
172 # CUSTOM ROUTES HERE
173 #==========================================================================
173 #==========================================================================
174
174
175 # ping and pylons error test
175 # ping and pylons error test
176 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
176 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
177 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
177 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
178
178
179 # ADMIN REPOSITORY ROUTES
179 # ADMIN REPOSITORY ROUTES
180 with rmap.submapper(path_prefix=ADMIN_PREFIX,
180 with rmap.submapper(path_prefix=ADMIN_PREFIX,
181 controller='admin/repos') as m:
181 controller='admin/repos') as m:
182 m.connect('repos', '/repos',
182 m.connect('repos', '/repos',
183 action='create', conditions={'method': ['POST']})
183 action='create', conditions={'method': ['POST']})
184 m.connect('repos', '/repos',
184 m.connect('repos', '/repos',
185 action='index', conditions={'method': ['GET']})
185 action='index', conditions={'method': ['GET']})
186 m.connect('new_repo', '/create_repository', jsroute=True,
186 m.connect('new_repo', '/create_repository', jsroute=True,
187 action='create_repository', conditions={'method': ['GET']})
187 action='create_repository', conditions={'method': ['GET']})
188 m.connect('delete_repo', '/repos/{repo_name}',
188 m.connect('delete_repo', '/repos/{repo_name}',
189 action='delete', conditions={'method': ['DELETE']},
189 action='delete', conditions={'method': ['DELETE']},
190 requirements=URL_NAME_REQUIREMENTS)
190 requirements=URL_NAME_REQUIREMENTS)
191 m.connect('repo', '/repos/{repo_name}',
191 m.connect('repo', '/repos/{repo_name}',
192 action='show', conditions={'method': ['GET'],
192 action='show', conditions={'method': ['GET'],
193 'function': check_repo},
193 'function': check_repo},
194 requirements=URL_NAME_REQUIREMENTS)
194 requirements=URL_NAME_REQUIREMENTS)
195
195
196 # ADMIN REPOSITORY GROUPS ROUTES
196 # ADMIN REPOSITORY GROUPS ROUTES
197 with rmap.submapper(path_prefix=ADMIN_PREFIX,
197 with rmap.submapper(path_prefix=ADMIN_PREFIX,
198 controller='admin/repo_groups') as m:
198 controller='admin/repo_groups') as m:
199 m.connect('repo_groups', '/repo_groups',
199 m.connect('repo_groups', '/repo_groups',
200 action='create', conditions={'method': ['POST']})
200 action='create', conditions={'method': ['POST']})
201 m.connect('repo_groups', '/repo_groups',
201 m.connect('repo_groups', '/repo_groups',
202 action='index', conditions={'method': ['GET']})
202 action='index', conditions={'method': ['GET']})
203 m.connect('new_repo_group', '/repo_groups/new',
203 m.connect('new_repo_group', '/repo_groups/new',
204 action='new', conditions={'method': ['GET']})
204 action='new', conditions={'method': ['GET']})
205 m.connect('update_repo_group', '/repo_groups/{group_name}',
205 m.connect('update_repo_group', '/repo_groups/{group_name}',
206 action='update', conditions={'method': ['PUT'],
206 action='update', conditions={'method': ['PUT'],
207 'function': check_group},
207 'function': check_group},
208 requirements=URL_NAME_REQUIREMENTS)
208 requirements=URL_NAME_REQUIREMENTS)
209
209
210 # EXTRAS REPO GROUP ROUTES
210 # EXTRAS REPO GROUP ROUTES
211 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
211 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
212 action='edit',
212 action='edit',
213 conditions={'method': ['GET'], 'function': check_group},
213 conditions={'method': ['GET'], 'function': check_group},
214 requirements=URL_NAME_REQUIREMENTS)
214 requirements=URL_NAME_REQUIREMENTS)
215 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
215 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
216 action='edit',
216 action='edit',
217 conditions={'method': ['PUT'], 'function': check_group},
217 conditions={'method': ['PUT'], 'function': check_group},
218 requirements=URL_NAME_REQUIREMENTS)
218 requirements=URL_NAME_REQUIREMENTS)
219
219
220 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
220 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
221 action='edit_repo_group_advanced',
221 action='edit_repo_group_advanced',
222 conditions={'method': ['GET'], 'function': check_group},
222 conditions={'method': ['GET'], 'function': check_group},
223 requirements=URL_NAME_REQUIREMENTS)
223 requirements=URL_NAME_REQUIREMENTS)
224 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
224 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
225 action='edit_repo_group_advanced',
225 action='edit_repo_group_advanced',
226 conditions={'method': ['PUT'], 'function': check_group},
226 conditions={'method': ['PUT'], 'function': check_group},
227 requirements=URL_NAME_REQUIREMENTS)
227 requirements=URL_NAME_REQUIREMENTS)
228
228
229 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
229 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
230 action='edit_repo_group_perms',
230 action='edit_repo_group_perms',
231 conditions={'method': ['GET'], 'function': check_group},
231 conditions={'method': ['GET'], 'function': check_group},
232 requirements=URL_NAME_REQUIREMENTS)
232 requirements=URL_NAME_REQUIREMENTS)
233 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
233 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
234 action='update_perms',
234 action='update_perms',
235 conditions={'method': ['PUT'], 'function': check_group},
235 conditions={'method': ['PUT'], 'function': check_group},
236 requirements=URL_NAME_REQUIREMENTS)
236 requirements=URL_NAME_REQUIREMENTS)
237
237
238 m.connect('delete_repo_group', '/repo_groups/{group_name}',
238 m.connect('delete_repo_group', '/repo_groups/{group_name}',
239 action='delete', conditions={'method': ['DELETE'],
239 action='delete', conditions={'method': ['DELETE'],
240 'function': check_group},
240 'function': check_group},
241 requirements=URL_NAME_REQUIREMENTS)
241 requirements=URL_NAME_REQUIREMENTS)
242
242
243 # ADMIN USER ROUTES
243 # ADMIN USER ROUTES
244 with rmap.submapper(path_prefix=ADMIN_PREFIX,
244 with rmap.submapper(path_prefix=ADMIN_PREFIX,
245 controller='admin/users') as m:
245 controller='admin/users') as m:
246 m.connect('users', '/users',
246 m.connect('users', '/users',
247 action='create', conditions={'method': ['POST']})
247 action='create', conditions={'method': ['POST']})
248 m.connect('new_user', '/users/new',
248 m.connect('new_user', '/users/new',
249 action='new', conditions={'method': ['GET']})
249 action='new', conditions={'method': ['GET']})
250 m.connect('update_user', '/users/{user_id}',
250 m.connect('update_user', '/users/{user_id}',
251 action='update', conditions={'method': ['PUT']})
251 action='update', conditions={'method': ['PUT']})
252 m.connect('delete_user', '/users/{user_id}',
252 m.connect('delete_user', '/users/{user_id}',
253 action='delete', conditions={'method': ['DELETE']})
253 action='delete', conditions={'method': ['DELETE']})
254 m.connect('edit_user', '/users/{user_id}/edit',
254 m.connect('edit_user', '/users/{user_id}/edit',
255 action='edit', conditions={'method': ['GET']}, jsroute=True)
255 action='edit', conditions={'method': ['GET']}, jsroute=True)
256 m.connect('user', '/users/{user_id}',
256 m.connect('user', '/users/{user_id}',
257 action='show', conditions={'method': ['GET']})
257 action='show', conditions={'method': ['GET']})
258 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
258 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
259 action='reset_password', conditions={'method': ['POST']})
259 action='reset_password', conditions={'method': ['POST']})
260 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
260 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
261 action='create_personal_repo_group', conditions={'method': ['POST']})
261 action='create_personal_repo_group', conditions={'method': ['POST']})
262
262
263 # EXTRAS USER ROUTES
263 # EXTRAS USER ROUTES
264 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
264 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
265 action='edit_advanced', conditions={'method': ['GET']})
265 action='edit_advanced', conditions={'method': ['GET']})
266 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
266 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
267 action='update_advanced', conditions={'method': ['PUT']})
267 action='update_advanced', conditions={'method': ['PUT']})
268
268
269 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
269 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
270 action='edit_global_perms', conditions={'method': ['GET']})
270 action='edit_global_perms', conditions={'method': ['GET']})
271 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
271 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
272 action='update_global_perms', conditions={'method': ['PUT']})
272 action='update_global_perms', conditions={'method': ['PUT']})
273
273
274 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
274 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
275 action='edit_perms_summary', conditions={'method': ['GET']})
275 action='edit_perms_summary', conditions={'method': ['GET']})
276
276
277 # ADMIN USER GROUPS REST ROUTES
277 # ADMIN USER GROUPS REST ROUTES
278 with rmap.submapper(path_prefix=ADMIN_PREFIX,
278 with rmap.submapper(path_prefix=ADMIN_PREFIX,
279 controller='admin/user_groups') as m:
279 controller='admin/user_groups') as m:
280 m.connect('users_groups', '/user_groups',
280 m.connect('users_groups', '/user_groups',
281 action='create', conditions={'method': ['POST']})
281 action='create', conditions={'method': ['POST']})
282 m.connect('users_groups', '/user_groups',
282 m.connect('users_groups', '/user_groups',
283 action='index', conditions={'method': ['GET']})
283 action='index', conditions={'method': ['GET']})
284 m.connect('new_users_group', '/user_groups/new',
284 m.connect('new_users_group', '/user_groups/new',
285 action='new', conditions={'method': ['GET']})
285 action='new', conditions={'method': ['GET']})
286 m.connect('update_users_group', '/user_groups/{user_group_id}',
286 m.connect('update_users_group', '/user_groups/{user_group_id}',
287 action='update', conditions={'method': ['PUT']})
287 action='update', conditions={'method': ['PUT']})
288 m.connect('delete_users_group', '/user_groups/{user_group_id}',
288 m.connect('delete_users_group', '/user_groups/{user_group_id}',
289 action='delete', conditions={'method': ['DELETE']})
289 action='delete', conditions={'method': ['DELETE']})
290 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
290 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
291 action='edit', conditions={'method': ['GET']},
291 action='edit', conditions={'method': ['GET']},
292 function=check_user_group)
292 function=check_user_group)
293
293
294 # EXTRAS USER GROUP ROUTES
294 # EXTRAS USER GROUP ROUTES
295 m.connect('edit_user_group_global_perms',
295 m.connect('edit_user_group_global_perms',
296 '/user_groups/{user_group_id}/edit/global_permissions',
296 '/user_groups/{user_group_id}/edit/global_permissions',
297 action='edit_global_perms', conditions={'method': ['GET']})
297 action='edit_global_perms', conditions={'method': ['GET']})
298 m.connect('edit_user_group_global_perms',
298 m.connect('edit_user_group_global_perms',
299 '/user_groups/{user_group_id}/edit/global_permissions',
299 '/user_groups/{user_group_id}/edit/global_permissions',
300 action='update_global_perms', conditions={'method': ['PUT']})
300 action='update_global_perms', conditions={'method': ['PUT']})
301 m.connect('edit_user_group_perms_summary',
301 m.connect('edit_user_group_perms_summary',
302 '/user_groups/{user_group_id}/edit/permissions_summary',
302 '/user_groups/{user_group_id}/edit/permissions_summary',
303 action='edit_perms_summary', conditions={'method': ['GET']})
303 action='edit_perms_summary', conditions={'method': ['GET']})
304
304
305 m.connect('edit_user_group_perms',
305 m.connect('edit_user_group_perms',
306 '/user_groups/{user_group_id}/edit/permissions',
306 '/user_groups/{user_group_id}/edit/permissions',
307 action='edit_perms', conditions={'method': ['GET']})
307 action='edit_perms', conditions={'method': ['GET']})
308 m.connect('edit_user_group_perms',
308 m.connect('edit_user_group_perms',
309 '/user_groups/{user_group_id}/edit/permissions',
309 '/user_groups/{user_group_id}/edit/permissions',
310 action='update_perms', conditions={'method': ['PUT']})
310 action='update_perms', conditions={'method': ['PUT']})
311
311
312 m.connect('edit_user_group_advanced',
312 m.connect('edit_user_group_advanced',
313 '/user_groups/{user_group_id}/edit/advanced',
313 '/user_groups/{user_group_id}/edit/advanced',
314 action='edit_advanced', conditions={'method': ['GET']})
314 action='edit_advanced', conditions={'method': ['GET']})
315
315
316 m.connect('edit_user_group_advanced_sync',
316 m.connect('edit_user_group_advanced_sync',
317 '/user_groups/{user_group_id}/edit/advanced/sync',
317 '/user_groups/{user_group_id}/edit/advanced/sync',
318 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
318 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
319
319
320 m.connect('edit_user_group_members',
320 m.connect('edit_user_group_members',
321 '/user_groups/{user_group_id}/edit/members', jsroute=True,
321 '/user_groups/{user_group_id}/edit/members', jsroute=True,
322 action='user_group_members', conditions={'method': ['GET']})
322 action='user_group_members', conditions={'method': ['GET']})
323
323
324 # ADMIN DEFAULTS REST ROUTES
324 # ADMIN DEFAULTS REST ROUTES
325 with rmap.submapper(path_prefix=ADMIN_PREFIX,
325 with rmap.submapper(path_prefix=ADMIN_PREFIX,
326 controller='admin/defaults') as m:
326 controller='admin/defaults') as m:
327 m.connect('admin_defaults_repositories', '/defaults/repositories',
327 m.connect('admin_defaults_repositories', '/defaults/repositories',
328 action='update_repository_defaults', conditions={'method': ['POST']})
328 action='update_repository_defaults', conditions={'method': ['POST']})
329 m.connect('admin_defaults_repositories', '/defaults/repositories',
329 m.connect('admin_defaults_repositories', '/defaults/repositories',
330 action='index', conditions={'method': ['GET']})
330 action='index', conditions={'method': ['GET']})
331
331
332 # ADMIN SETTINGS ROUTES
332 # ADMIN SETTINGS ROUTES
333 with rmap.submapper(path_prefix=ADMIN_PREFIX,
333 with rmap.submapper(path_prefix=ADMIN_PREFIX,
334 controller='admin/settings') as m:
334 controller='admin/settings') as m:
335
335
336 # default
336 # default
337 m.connect('admin_settings', '/settings',
337 m.connect('admin_settings', '/settings',
338 action='settings_global_update',
338 action='settings_global_update',
339 conditions={'method': ['POST']})
339 conditions={'method': ['POST']})
340 m.connect('admin_settings', '/settings',
340 m.connect('admin_settings', '/settings',
341 action='settings_global', conditions={'method': ['GET']})
341 action='settings_global', conditions={'method': ['GET']})
342
342
343 m.connect('admin_settings_vcs', '/settings/vcs',
343 m.connect('admin_settings_vcs', '/settings/vcs',
344 action='settings_vcs_update',
344 action='settings_vcs_update',
345 conditions={'method': ['POST']})
345 conditions={'method': ['POST']})
346 m.connect('admin_settings_vcs', '/settings/vcs',
346 m.connect('admin_settings_vcs', '/settings/vcs',
347 action='settings_vcs',
347 action='settings_vcs',
348 conditions={'method': ['GET']})
348 conditions={'method': ['GET']})
349 m.connect('admin_settings_vcs', '/settings/vcs',
349 m.connect('admin_settings_vcs', '/settings/vcs',
350 action='delete_svn_pattern',
350 action='delete_svn_pattern',
351 conditions={'method': ['DELETE']})
351 conditions={'method': ['DELETE']})
352
352
353 m.connect('admin_settings_mapping', '/settings/mapping',
353 m.connect('admin_settings_mapping', '/settings/mapping',
354 action='settings_mapping_update',
354 action='settings_mapping_update',
355 conditions={'method': ['POST']})
355 conditions={'method': ['POST']})
356 m.connect('admin_settings_mapping', '/settings/mapping',
356 m.connect('admin_settings_mapping', '/settings/mapping',
357 action='settings_mapping', conditions={'method': ['GET']})
357 action='settings_mapping', conditions={'method': ['GET']})
358
358
359 m.connect('admin_settings_global', '/settings/global',
359 m.connect('admin_settings_global', '/settings/global',
360 action='settings_global_update',
360 action='settings_global_update',
361 conditions={'method': ['POST']})
361 conditions={'method': ['POST']})
362 m.connect('admin_settings_global', '/settings/global',
362 m.connect('admin_settings_global', '/settings/global',
363 action='settings_global', conditions={'method': ['GET']})
363 action='settings_global', conditions={'method': ['GET']})
364
364
365 m.connect('admin_settings_visual', '/settings/visual',
365 m.connect('admin_settings_visual', '/settings/visual',
366 action='settings_visual_update',
366 action='settings_visual_update',
367 conditions={'method': ['POST']})
367 conditions={'method': ['POST']})
368 m.connect('admin_settings_visual', '/settings/visual',
368 m.connect('admin_settings_visual', '/settings/visual',
369 action='settings_visual', conditions={'method': ['GET']})
369 action='settings_visual', conditions={'method': ['GET']})
370
370
371 m.connect('admin_settings_issuetracker',
371 m.connect('admin_settings_issuetracker',
372 '/settings/issue-tracker', action='settings_issuetracker',
372 '/settings/issue-tracker', action='settings_issuetracker',
373 conditions={'method': ['GET']})
373 conditions={'method': ['GET']})
374 m.connect('admin_settings_issuetracker_save',
374 m.connect('admin_settings_issuetracker_save',
375 '/settings/issue-tracker/save',
375 '/settings/issue-tracker/save',
376 action='settings_issuetracker_save',
376 action='settings_issuetracker_save',
377 conditions={'method': ['POST']})
377 conditions={'method': ['POST']})
378 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
378 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
379 action='settings_issuetracker_test',
379 action='settings_issuetracker_test',
380 conditions={'method': ['POST']})
380 conditions={'method': ['POST']})
381 m.connect('admin_issuetracker_delete',
381 m.connect('admin_issuetracker_delete',
382 '/settings/issue-tracker/delete',
382 '/settings/issue-tracker/delete',
383 action='settings_issuetracker_delete',
383 action='settings_issuetracker_delete',
384 conditions={'method': ['DELETE']})
384 conditions={'method': ['DELETE']})
385
385
386 m.connect('admin_settings_email', '/settings/email',
386 m.connect('admin_settings_email', '/settings/email',
387 action='settings_email_update',
387 action='settings_email_update',
388 conditions={'method': ['POST']})
388 conditions={'method': ['POST']})
389 m.connect('admin_settings_email', '/settings/email',
389 m.connect('admin_settings_email', '/settings/email',
390 action='settings_email', conditions={'method': ['GET']})
390 action='settings_email', conditions={'method': ['GET']})
391
391
392 m.connect('admin_settings_hooks', '/settings/hooks',
392 m.connect('admin_settings_hooks', '/settings/hooks',
393 action='settings_hooks_update',
393 action='settings_hooks_update',
394 conditions={'method': ['POST', 'DELETE']})
394 conditions={'method': ['POST', 'DELETE']})
395 m.connect('admin_settings_hooks', '/settings/hooks',
395 m.connect('admin_settings_hooks', '/settings/hooks',
396 action='settings_hooks', conditions={'method': ['GET']})
396 action='settings_hooks', conditions={'method': ['GET']})
397
397
398 m.connect('admin_settings_search', '/settings/search',
398 m.connect('admin_settings_search', '/settings/search',
399 action='settings_search', conditions={'method': ['GET']})
399 action='settings_search', conditions={'method': ['GET']})
400
400
401 m.connect('admin_settings_supervisor', '/settings/supervisor',
401 m.connect('admin_settings_supervisor', '/settings/supervisor',
402 action='settings_supervisor', conditions={'method': ['GET']})
402 action='settings_supervisor', conditions={'method': ['GET']})
403 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
403 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
404 action='settings_supervisor_log', conditions={'method': ['GET']})
404 action='settings_supervisor_log', conditions={'method': ['GET']})
405
405
406 m.connect('admin_settings_labs', '/settings/labs',
406 m.connect('admin_settings_labs', '/settings/labs',
407 action='settings_labs_update',
407 action='settings_labs_update',
408 conditions={'method': ['POST']})
408 conditions={'method': ['POST']})
409 m.connect('admin_settings_labs', '/settings/labs',
409 m.connect('admin_settings_labs', '/settings/labs',
410 action='settings_labs', conditions={'method': ['GET']})
410 action='settings_labs', conditions={'method': ['GET']})
411
411
412 # ADMIN MY ACCOUNT
412 # ADMIN MY ACCOUNT
413 with rmap.submapper(path_prefix=ADMIN_PREFIX,
413 with rmap.submapper(path_prefix=ADMIN_PREFIX,
414 controller='admin/my_account') as m:
414 controller='admin/my_account') as m:
415
415
416 # NOTE(marcink): this needs to be kept for password force flag to be
416 # NOTE(marcink): this needs to be kept for password force flag to be
417 # handled in pylons controllers, remove after full migration to pyramid
417 # handled in pylons controllers, remove after full migration to pyramid
418 m.connect('my_account_password', '/my_account/password',
418 m.connect('my_account_password', '/my_account/password',
419 action='my_account_password', conditions={'method': ['GET']})
419 action='my_account_password', conditions={'method': ['GET']})
420
420
421 #==========================================================================
421 #==========================================================================
422 # REPOSITORY ROUTES
422 # REPOSITORY ROUTES
423 #==========================================================================
423 #==========================================================================
424
424
425 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
425 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
426 controller='admin/repos', action='repo_creating',
426 controller='admin/repos', action='repo_creating',
427 requirements=URL_NAME_REQUIREMENTS)
427 requirements=URL_NAME_REQUIREMENTS)
428 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
428 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
429 controller='admin/repos', action='repo_check',
429 controller='admin/repos', action='repo_check',
430 requirements=URL_NAME_REQUIREMENTS)
430 requirements=URL_NAME_REQUIREMENTS)
431
431
432 # repo edit options
432 # repo edit options
433 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
433 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
434 controller='admin/repos', action='edit_fields',
434 controller='admin/repos', action='edit_fields',
435 conditions={'method': ['GET'], 'function': check_repo},
435 conditions={'method': ['GET'], 'function': check_repo},
436 requirements=URL_NAME_REQUIREMENTS)
436 requirements=URL_NAME_REQUIREMENTS)
437 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
437 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
438 controller='admin/repos', action='create_repo_field',
438 controller='admin/repos', action='create_repo_field',
439 conditions={'method': ['PUT'], 'function': check_repo},
439 conditions={'method': ['PUT'], 'function': check_repo},
440 requirements=URL_NAME_REQUIREMENTS)
440 requirements=URL_NAME_REQUIREMENTS)
441 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
441 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
442 controller='admin/repos', action='delete_repo_field',
442 controller='admin/repos', action='delete_repo_field',
443 conditions={'method': ['DELETE'], 'function': check_repo},
443 conditions={'method': ['DELETE'], 'function': check_repo},
444 requirements=URL_NAME_REQUIREMENTS)
444 requirements=URL_NAME_REQUIREMENTS)
445
445
446 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
446 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
447 controller='admin/repos', action='toggle_locking',
447 controller='admin/repos', action='toggle_locking',
448 conditions={'method': ['GET'], 'function': check_repo},
448 conditions={'method': ['GET'], 'function': check_repo},
449 requirements=URL_NAME_REQUIREMENTS)
449 requirements=URL_NAME_REQUIREMENTS)
450
450
451 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
451 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
452 controller='admin/repos', action='edit_remote_form',
452 controller='admin/repos', action='edit_remote_form',
453 conditions={'method': ['GET'], 'function': check_repo},
453 conditions={'method': ['GET'], 'function': check_repo},
454 requirements=URL_NAME_REQUIREMENTS)
454 requirements=URL_NAME_REQUIREMENTS)
455 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
455 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
456 controller='admin/repos', action='edit_remote',
456 controller='admin/repos', action='edit_remote',
457 conditions={'method': ['PUT'], 'function': check_repo},
457 conditions={'method': ['PUT'], 'function': check_repo},
458 requirements=URL_NAME_REQUIREMENTS)
458 requirements=URL_NAME_REQUIREMENTS)
459
459
460 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
460 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
461 controller='admin/repos', action='edit_statistics_form',
461 controller='admin/repos', action='edit_statistics_form',
462 conditions={'method': ['GET'], 'function': check_repo},
462 conditions={'method': ['GET'], 'function': check_repo},
463 requirements=URL_NAME_REQUIREMENTS)
463 requirements=URL_NAME_REQUIREMENTS)
464 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
464 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
465 controller='admin/repos', action='edit_statistics',
465 controller='admin/repos', action='edit_statistics',
466 conditions={'method': ['PUT'], 'function': check_repo},
466 conditions={'method': ['PUT'], 'function': check_repo},
467 requirements=URL_NAME_REQUIREMENTS)
467 requirements=URL_NAME_REQUIREMENTS)
468 rmap.connect('repo_settings_issuetracker',
468 rmap.connect('repo_settings_issuetracker',
469 '/{repo_name}/settings/issue-tracker',
469 '/{repo_name}/settings/issue-tracker',
470 controller='admin/repos', action='repo_issuetracker',
470 controller='admin/repos', action='repo_issuetracker',
471 conditions={'method': ['GET'], 'function': check_repo},
471 conditions={'method': ['GET'], 'function': check_repo},
472 requirements=URL_NAME_REQUIREMENTS)
472 requirements=URL_NAME_REQUIREMENTS)
473 rmap.connect('repo_issuetracker_test',
473 rmap.connect('repo_issuetracker_test',
474 '/{repo_name}/settings/issue-tracker/test',
474 '/{repo_name}/settings/issue-tracker/test',
475 controller='admin/repos', action='repo_issuetracker_test',
475 controller='admin/repos', action='repo_issuetracker_test',
476 conditions={'method': ['POST'], 'function': check_repo},
476 conditions={'method': ['POST'], 'function': check_repo},
477 requirements=URL_NAME_REQUIREMENTS)
477 requirements=URL_NAME_REQUIREMENTS)
478 rmap.connect('repo_issuetracker_delete',
478 rmap.connect('repo_issuetracker_delete',
479 '/{repo_name}/settings/issue-tracker/delete',
479 '/{repo_name}/settings/issue-tracker/delete',
480 controller='admin/repos', action='repo_issuetracker_delete',
480 controller='admin/repos', action='repo_issuetracker_delete',
481 conditions={'method': ['DELETE'], 'function': check_repo},
481 conditions={'method': ['DELETE'], 'function': check_repo},
482 requirements=URL_NAME_REQUIREMENTS)
482 requirements=URL_NAME_REQUIREMENTS)
483 rmap.connect('repo_issuetracker_save',
483 rmap.connect('repo_issuetracker_save',
484 '/{repo_name}/settings/issue-tracker/save',
484 '/{repo_name}/settings/issue-tracker/save',
485 controller='admin/repos', action='repo_issuetracker_save',
485 controller='admin/repos', action='repo_issuetracker_save',
486 conditions={'method': ['POST'], 'function': check_repo},
486 conditions={'method': ['POST'], 'function': check_repo},
487 requirements=URL_NAME_REQUIREMENTS)
487 requirements=URL_NAME_REQUIREMENTS)
488 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
488 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
489 controller='admin/repos', action='repo_settings_vcs_update',
489 controller='admin/repos', action='repo_settings_vcs_update',
490 conditions={'method': ['POST'], 'function': check_repo},
490 conditions={'method': ['POST'], 'function': check_repo},
491 requirements=URL_NAME_REQUIREMENTS)
491 requirements=URL_NAME_REQUIREMENTS)
492 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
492 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
493 controller='admin/repos', action='repo_settings_vcs',
493 controller='admin/repos', action='repo_settings_vcs',
494 conditions={'method': ['GET'], 'function': check_repo},
494 conditions={'method': ['GET'], 'function': check_repo},
495 requirements=URL_NAME_REQUIREMENTS)
495 requirements=URL_NAME_REQUIREMENTS)
496 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
496 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
497 controller='admin/repos', action='repo_delete_svn_pattern',
497 controller='admin/repos', action='repo_delete_svn_pattern',
498 conditions={'method': ['DELETE'], 'function': check_repo},
498 conditions={'method': ['DELETE'], 'function': check_repo},
499 requirements=URL_NAME_REQUIREMENTS)
499 requirements=URL_NAME_REQUIREMENTS)
500 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
500 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
501 controller='admin/repos', action='repo_settings_pullrequest',
501 controller='admin/repos', action='repo_settings_pullrequest',
502 conditions={'method': ['GET', 'POST'], 'function': check_repo},
502 conditions={'method': ['GET', 'POST'], 'function': check_repo},
503 requirements=URL_NAME_REQUIREMENTS)
503 requirements=URL_NAME_REQUIREMENTS)
504
504
505
505
506 rmap.connect('pullrequest_home',
507 '/{repo_name}/pull-request/new', controller='pullrequests',
508 action='index', conditions={'function': check_repo,
509 'method': ['GET']},
510 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
511
512 rmap.connect('pullrequest',
513 '/{repo_name}/pull-request/new', controller='pullrequests',
514 action='create', conditions={'function': check_repo,
515 'method': ['POST']},
516 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
517
518 rmap.connect('pullrequest_repo_refs',
519 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
520 controller='pullrequests',
521 action='get_repo_refs',
522 conditions={'function': check_repo, 'method': ['GET']},
523 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
524
525 rmap.connect('pullrequest_repo_destinations',
526 '/{repo_name}/pull-request/repo-destinations',
527 controller='pullrequests',
528 action='get_repo_destinations',
529 conditions={'function': check_repo, 'method': ['GET']},
530 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
531
532 rmap.connect('pullrequest_show',
533 '/{repo_name}/pull-request/{pull_request_id}',
534 controller='pullrequests',
535 action='show', conditions={'function': check_repo,
536 'method': ['GET']},
537 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
538
539 rmap.connect('pullrequest_update',
540 '/{repo_name}/pull-request/{pull_request_id}',
541 controller='pullrequests',
542 action='update', conditions={'function': check_repo,
543 'method': ['PUT']},
544 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
545
546 rmap.connect('pullrequest_merge',
547 '/{repo_name}/pull-request/{pull_request_id}',
548 controller='pullrequests',
549 action='merge', conditions={'function': check_repo,
550 'method': ['POST']},
551 requirements=URL_NAME_REQUIREMENTS)
552
553 rmap.connect('pullrequest_delete',
554 '/{repo_name}/pull-request/{pull_request_id}',
555 controller='pullrequests',
556 action='delete', conditions={'function': check_repo,
557 'method': ['DELETE']},
558 requirements=URL_NAME_REQUIREMENTS)
559
560 rmap.connect('pullrequest_comment',
561 '/{repo_name}/pull-request-comment/{pull_request_id}',
562 controller='pullrequests',
563 action='comment', conditions={'function': check_repo,
564 'method': ['POST']},
565 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
566
567 rmap.connect('pullrequest_comment_delete',
568 '/{repo_name}/pull-request-comment/{comment_id}/delete',
569 controller='pullrequests', action='delete_comment',
570 conditions={'function': check_repo, 'method': ['DELETE']},
571 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
572
573 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
506 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
574 controller='forks', action='fork_create',
507 controller='forks', action='fork_create',
575 conditions={'function': check_repo, 'method': ['POST']},
508 conditions={'function': check_repo, 'method': ['POST']},
576 requirements=URL_NAME_REQUIREMENTS)
509 requirements=URL_NAME_REQUIREMENTS)
577
510
578 rmap.connect('repo_fork_home', '/{repo_name}/fork',
511 rmap.connect('repo_fork_home', '/{repo_name}/fork',
579 controller='forks', action='fork',
512 controller='forks', action='fork',
580 conditions={'function': check_repo},
513 conditions={'function': check_repo},
581 requirements=URL_NAME_REQUIREMENTS)
514 requirements=URL_NAME_REQUIREMENTS)
582
515
583 rmap.connect('repo_forks_home', '/{repo_name}/forks',
516 rmap.connect('repo_forks_home', '/{repo_name}/forks',
584 controller='forks', action='forks',
517 controller='forks', action='forks',
585 conditions={'function': check_repo},
518 conditions={'function': check_repo},
586 requirements=URL_NAME_REQUIREMENTS)
519 requirements=URL_NAME_REQUIREMENTS)
587
520
588 return rmap
521 return rmap
@@ -1,656 +1,655 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from datetime import datetime
29 from datetime import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry, get_current_request
32 from pyramid.threadlocal import get_current_registry, get_current_request
33 from sqlalchemy.sql.expression import null
33 from sqlalchemy.sql.expression import null
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs, channelstream
36 from rhodecode.lib import helpers as h, diffs, channelstream
37 from rhodecode.lib import audit_logger
37 from rhodecode.lib import audit_logger
38 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.channelstream import channelstream_request
39 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
39 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
43 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.notification import NotificationModel
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import VcsSettingsModel
45 from rhodecode.model.settings import VcsSettingsModel
46 from rhodecode.model.notification import EmailNotificationModel
46 from rhodecode.model.notification import EmailNotificationModel
47 from rhodecode.model.validation_schema.schemas import comment_schema
47 from rhodecode.model.validation_schema.schemas import comment_schema
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class CommentsModel(BaseModel):
53 class CommentsModel(BaseModel):
54
54
55 cls = ChangesetComment
55 cls = ChangesetComment
56
56
57 DIFF_CONTEXT_BEFORE = 3
57 DIFF_CONTEXT_BEFORE = 3
58 DIFF_CONTEXT_AFTER = 3
58 DIFF_CONTEXT_AFTER = 3
59
59
60 def __get_commit_comment(self, changeset_comment):
60 def __get_commit_comment(self, changeset_comment):
61 return self._get_instance(ChangesetComment, changeset_comment)
61 return self._get_instance(ChangesetComment, changeset_comment)
62
62
63 def __get_pull_request(self, pull_request):
63 def __get_pull_request(self, pull_request):
64 return self._get_instance(PullRequest, pull_request)
64 return self._get_instance(PullRequest, pull_request)
65
65
66 def _extract_mentions(self, s):
66 def _extract_mentions(self, s):
67 user_objects = []
67 user_objects = []
68 for username in extract_mentioned_users(s):
68 for username in extract_mentioned_users(s):
69 user_obj = User.get_by_username(username, case_insensitive=True)
69 user_obj = User.get_by_username(username, case_insensitive=True)
70 if user_obj:
70 if user_obj:
71 user_objects.append(user_obj)
71 user_objects.append(user_obj)
72 return user_objects
72 return user_objects
73
73
74 def _get_renderer(self, global_renderer='rst'):
74 def _get_renderer(self, global_renderer='rst'):
75 try:
75 try:
76 # try reading from visual context
76 # try reading from visual context
77 from pylons import tmpl_context
77 from pylons import tmpl_context
78 global_renderer = tmpl_context.visual.default_renderer
78 global_renderer = tmpl_context.visual.default_renderer
79 except AttributeError:
79 except AttributeError:
80 log.debug("Renderer not set, falling back "
80 log.debug("Renderer not set, falling back "
81 "to default renderer '%s'", global_renderer)
81 "to default renderer '%s'", global_renderer)
82 except Exception:
82 except Exception:
83 log.error(traceback.format_exc())
83 log.error(traceback.format_exc())
84 return global_renderer
84 return global_renderer
85
85
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
87 # group by versions, and count until, and display objects
87 # group by versions, and count until, and display objects
88
88
89 comment_groups = collections.defaultdict(list)
89 comment_groups = collections.defaultdict(list)
90 [comment_groups[
90 [comment_groups[
91 _co.pull_request_version_id].append(_co) for _co in comments]
91 _co.pull_request_version_id].append(_co) for _co in comments]
92
92
93 def yield_comments(pos):
93 def yield_comments(pos):
94 for co in comment_groups[pos]:
94 for co in comment_groups[pos]:
95 yield co
95 yield co
96
96
97 comment_versions = collections.defaultdict(
97 comment_versions = collections.defaultdict(
98 lambda: collections.defaultdict(list))
98 lambda: collections.defaultdict(list))
99 prev_prvid = -1
99 prev_prvid = -1
100 # fake last entry with None, to aggregate on "latest" version which
100 # fake last entry with None, to aggregate on "latest" version which
101 # doesn't have an pull_request_version_id
101 # doesn't have an pull_request_version_id
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
103 prvid = ver.pull_request_version_id
103 prvid = ver.pull_request_version_id
104 if prev_prvid == -1:
104 if prev_prvid == -1:
105 prev_prvid = prvid
105 prev_prvid = prvid
106
106
107 for co in yield_comments(prvid):
107 for co in yield_comments(prvid):
108 comment_versions[prvid]['at'].append(co)
108 comment_versions[prvid]['at'].append(co)
109
109
110 # save until
110 # save until
111 current = comment_versions[prvid]['at']
111 current = comment_versions[prvid]['at']
112 prev_until = comment_versions[prev_prvid]['until']
112 prev_until = comment_versions[prev_prvid]['until']
113 cur_until = prev_until + current
113 cur_until = prev_until + current
114 comment_versions[prvid]['until'].extend(cur_until)
114 comment_versions[prvid]['until'].extend(cur_until)
115
115
116 # save outdated
116 # save outdated
117 if inline:
117 if inline:
118 outdated = [x for x in cur_until
118 outdated = [x for x in cur_until
119 if x.outdated_at_version(show_version)]
119 if x.outdated_at_version(show_version)]
120 else:
120 else:
121 outdated = [x for x in cur_until
121 outdated = [x for x in cur_until
122 if x.older_than_version(show_version)]
122 if x.older_than_version(show_version)]
123 display = [x for x in cur_until if x not in outdated]
123 display = [x for x in cur_until if x not in outdated]
124
124
125 comment_versions[prvid]['outdated'] = outdated
125 comment_versions[prvid]['outdated'] = outdated
126 comment_versions[prvid]['display'] = display
126 comment_versions[prvid]['display'] = display
127
127
128 prev_prvid = prvid
128 prev_prvid = prvid
129
129
130 return comment_versions
130 return comment_versions
131
131
132 def get_unresolved_todos(self, pull_request, show_outdated=True):
132 def get_unresolved_todos(self, pull_request, show_outdated=True):
133
133
134 todos = Session().query(ChangesetComment) \
134 todos = Session().query(ChangesetComment) \
135 .filter(ChangesetComment.pull_request == pull_request) \
135 .filter(ChangesetComment.pull_request == pull_request) \
136 .filter(ChangesetComment.resolved_by == None) \
136 .filter(ChangesetComment.resolved_by == None) \
137 .filter(ChangesetComment.comment_type
137 .filter(ChangesetComment.comment_type
138 == ChangesetComment.COMMENT_TYPE_TODO)
138 == ChangesetComment.COMMENT_TYPE_TODO)
139
139
140 if not show_outdated:
140 if not show_outdated:
141 todos = todos.filter(
141 todos = todos.filter(
142 coalesce(ChangesetComment.display_state, '') !=
142 coalesce(ChangesetComment.display_state, '') !=
143 ChangesetComment.COMMENT_OUTDATED)
143 ChangesetComment.COMMENT_OUTDATED)
144
144
145 todos = todos.all()
145 todos = todos.all()
146
146
147 return todos
147 return todos
148
148
149 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
149 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
150
150
151 todos = Session().query(ChangesetComment) \
151 todos = Session().query(ChangesetComment) \
152 .filter(ChangesetComment.revision == commit_id) \
152 .filter(ChangesetComment.revision == commit_id) \
153 .filter(ChangesetComment.resolved_by == None) \
153 .filter(ChangesetComment.resolved_by == None) \
154 .filter(ChangesetComment.comment_type
154 .filter(ChangesetComment.comment_type
155 == ChangesetComment.COMMENT_TYPE_TODO)
155 == ChangesetComment.COMMENT_TYPE_TODO)
156
156
157 if not show_outdated:
157 if not show_outdated:
158 todos = todos.filter(
158 todos = todos.filter(
159 coalesce(ChangesetComment.display_state, '') !=
159 coalesce(ChangesetComment.display_state, '') !=
160 ChangesetComment.COMMENT_OUTDATED)
160 ChangesetComment.COMMENT_OUTDATED)
161
161
162 todos = todos.all()
162 todos = todos.all()
163
163
164 return todos
164 return todos
165
165
166 def _log_audit_action(self, action, action_data, user, comment):
166 def _log_audit_action(self, action, action_data, user, comment):
167 audit_logger.store(
167 audit_logger.store(
168 action=action,
168 action=action,
169 action_data=action_data,
169 action_data=action_data,
170 user=user,
170 user=user,
171 repo=comment.repo)
171 repo=comment.repo)
172
172
173 def create(self, text, repo, user, commit_id=None, pull_request=None,
173 def create(self, text, repo, user, commit_id=None, pull_request=None,
174 f_path=None, line_no=None, status_change=None,
174 f_path=None, line_no=None, status_change=None,
175 status_change_type=None, comment_type=None,
175 status_change_type=None, comment_type=None,
176 resolves_comment_id=None, closing_pr=False, send_email=True,
176 resolves_comment_id=None, closing_pr=False, send_email=True,
177 renderer=None):
177 renderer=None):
178 """
178 """
179 Creates new comment for commit or pull request.
179 Creates new comment for commit or pull request.
180 IF status_change is not none this comment is associated with a
180 IF status_change is not none this comment is associated with a
181 status change of commit or commit associated with pull request
181 status change of commit or commit associated with pull request
182
182
183 :param text:
183 :param text:
184 :param repo:
184 :param repo:
185 :param user:
185 :param user:
186 :param commit_id:
186 :param commit_id:
187 :param pull_request:
187 :param pull_request:
188 :param f_path:
188 :param f_path:
189 :param line_no:
189 :param line_no:
190 :param status_change: Label for status change
190 :param status_change: Label for status change
191 :param comment_type: Type of comment
191 :param comment_type: Type of comment
192 :param status_change_type: type of status change
192 :param status_change_type: type of status change
193 :param closing_pr:
193 :param closing_pr:
194 :param send_email:
194 :param send_email:
195 :param renderer: pick renderer for this comment
195 :param renderer: pick renderer for this comment
196 """
196 """
197 if not text:
197 if not text:
198 log.warning('Missing text for comment, skipping...')
198 log.warning('Missing text for comment, skipping...')
199 return
199 return
200
200
201 if not renderer:
201 if not renderer:
202 renderer = self._get_renderer()
202 renderer = self._get_renderer()
203
203
204 repo = self._get_repo(repo)
204 repo = self._get_repo(repo)
205 user = self._get_user(user)
205 user = self._get_user(user)
206
206
207 schema = comment_schema.CommentSchema()
207 schema = comment_schema.CommentSchema()
208 validated_kwargs = schema.deserialize(dict(
208 validated_kwargs = schema.deserialize(dict(
209 comment_body=text,
209 comment_body=text,
210 comment_type=comment_type,
210 comment_type=comment_type,
211 comment_file=f_path,
211 comment_file=f_path,
212 comment_line=line_no,
212 comment_line=line_no,
213 renderer_type=renderer,
213 renderer_type=renderer,
214 status_change=status_change_type,
214 status_change=status_change_type,
215 resolves_comment_id=resolves_comment_id,
215 resolves_comment_id=resolves_comment_id,
216 repo=repo.repo_id,
216 repo=repo.repo_id,
217 user=user.user_id,
217 user=user.user_id,
218 ))
218 ))
219
219
220 comment = ChangesetComment()
220 comment = ChangesetComment()
221 comment.renderer = validated_kwargs['renderer_type']
221 comment.renderer = validated_kwargs['renderer_type']
222 comment.text = validated_kwargs['comment_body']
222 comment.text = validated_kwargs['comment_body']
223 comment.f_path = validated_kwargs['comment_file']
223 comment.f_path = validated_kwargs['comment_file']
224 comment.line_no = validated_kwargs['comment_line']
224 comment.line_no = validated_kwargs['comment_line']
225 comment.comment_type = validated_kwargs['comment_type']
225 comment.comment_type = validated_kwargs['comment_type']
226
226
227 comment.repo = repo
227 comment.repo = repo
228 comment.author = user
228 comment.author = user
229 comment.resolved_comment = self.__get_commit_comment(
229 comment.resolved_comment = self.__get_commit_comment(
230 validated_kwargs['resolves_comment_id'])
230 validated_kwargs['resolves_comment_id'])
231
231
232 pull_request_id = pull_request
232 pull_request_id = pull_request
233
233
234 commit_obj = None
234 commit_obj = None
235 pull_request_obj = None
235 pull_request_obj = None
236
236
237 if commit_id:
237 if commit_id:
238 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
238 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
239 # do a lookup, so we don't pass something bad here
239 # do a lookup, so we don't pass something bad here
240 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
240 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
241 comment.revision = commit_obj.raw_id
241 comment.revision = commit_obj.raw_id
242
242
243 elif pull_request_id:
243 elif pull_request_id:
244 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
244 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
245 pull_request_obj = self.__get_pull_request(pull_request_id)
245 pull_request_obj = self.__get_pull_request(pull_request_id)
246 comment.pull_request = pull_request_obj
246 comment.pull_request = pull_request_obj
247 else:
247 else:
248 raise Exception('Please specify commit or pull_request_id')
248 raise Exception('Please specify commit or pull_request_id')
249
249
250 Session().add(comment)
250 Session().add(comment)
251 Session().flush()
251 Session().flush()
252 kwargs = {
252 kwargs = {
253 'user': user,
253 'user': user,
254 'renderer_type': renderer,
254 'renderer_type': renderer,
255 'repo_name': repo.repo_name,
255 'repo_name': repo.repo_name,
256 'status_change': status_change,
256 'status_change': status_change,
257 'status_change_type': status_change_type,
257 'status_change_type': status_change_type,
258 'comment_body': text,
258 'comment_body': text,
259 'comment_file': f_path,
259 'comment_file': f_path,
260 'comment_line': line_no,
260 'comment_line': line_no,
261 'comment_type': comment_type or 'note'
261 'comment_type': comment_type or 'note'
262 }
262 }
263
263
264 if commit_obj:
264 if commit_obj:
265 recipients = ChangesetComment.get_users(
265 recipients = ChangesetComment.get_users(
266 revision=commit_obj.raw_id)
266 revision=commit_obj.raw_id)
267 # add commit author if it's in RhodeCode system
267 # add commit author if it's in RhodeCode system
268 cs_author = User.get_from_cs_author(commit_obj.author)
268 cs_author = User.get_from_cs_author(commit_obj.author)
269 if not cs_author:
269 if not cs_author:
270 # use repo owner if we cannot extract the author correctly
270 # use repo owner if we cannot extract the author correctly
271 cs_author = repo.user
271 cs_author = repo.user
272 recipients += [cs_author]
272 recipients += [cs_author]
273
273
274 commit_comment_url = self.get_url(comment)
274 commit_comment_url = self.get_url(comment)
275
275
276 target_repo_url = h.link_to(
276 target_repo_url = h.link_to(
277 repo.repo_name,
277 repo.repo_name,
278 h.route_url('repo_summary', repo_name=repo.repo_name))
278 h.route_url('repo_summary', repo_name=repo.repo_name))
279
279
280 # commit specifics
280 # commit specifics
281 kwargs.update({
281 kwargs.update({
282 'commit': commit_obj,
282 'commit': commit_obj,
283 'commit_message': commit_obj.message,
283 'commit_message': commit_obj.message,
284 'commit_target_repo': target_repo_url,
284 'commit_target_repo': target_repo_url,
285 'commit_comment_url': commit_comment_url,
285 'commit_comment_url': commit_comment_url,
286 })
286 })
287
287
288 elif pull_request_obj:
288 elif pull_request_obj:
289 # get the current participants of this pull request
289 # get the current participants of this pull request
290 recipients = ChangesetComment.get_users(
290 recipients = ChangesetComment.get_users(
291 pull_request_id=pull_request_obj.pull_request_id)
291 pull_request_id=pull_request_obj.pull_request_id)
292 # add pull request author
292 # add pull request author
293 recipients += [pull_request_obj.author]
293 recipients += [pull_request_obj.author]
294
294
295 # add the reviewers to notification
295 # add the reviewers to notification
296 recipients += [x.user for x in pull_request_obj.reviewers]
296 recipients += [x.user for x in pull_request_obj.reviewers]
297
297
298 pr_target_repo = pull_request_obj.target_repo
298 pr_target_repo = pull_request_obj.target_repo
299 pr_source_repo = pull_request_obj.source_repo
299 pr_source_repo = pull_request_obj.source_repo
300
300
301 pr_comment_url = h.url(
301 pr_comment_url = h.route_url(
302 'pullrequest_show',
302 'pullrequest_show',
303 repo_name=pr_target_repo.repo_name,
303 repo_name=pr_target_repo.repo_name,
304 pull_request_id=pull_request_obj.pull_request_id,
304 pull_request_id=pull_request_obj.pull_request_id,
305 anchor='comment-%s' % comment.comment_id,
305 anchor='comment-%s' % comment.comment_id)
306 qualified=True,)
307
306
308 # set some variables for email notification
307 # set some variables for email notification
309 pr_target_repo_url = h.route_url(
308 pr_target_repo_url = h.route_url(
310 'repo_summary', repo_name=pr_target_repo.repo_name)
309 'repo_summary', repo_name=pr_target_repo.repo_name)
311
310
312 pr_source_repo_url = h.route_url(
311 pr_source_repo_url = h.route_url(
313 'repo_summary', repo_name=pr_source_repo.repo_name)
312 'repo_summary', repo_name=pr_source_repo.repo_name)
314
313
315 # pull request specifics
314 # pull request specifics
316 kwargs.update({
315 kwargs.update({
317 'pull_request': pull_request_obj,
316 'pull_request': pull_request_obj,
318 'pr_id': pull_request_obj.pull_request_id,
317 'pr_id': pull_request_obj.pull_request_id,
319 'pr_target_repo': pr_target_repo,
318 'pr_target_repo': pr_target_repo,
320 'pr_target_repo_url': pr_target_repo_url,
319 'pr_target_repo_url': pr_target_repo_url,
321 'pr_source_repo': pr_source_repo,
320 'pr_source_repo': pr_source_repo,
322 'pr_source_repo_url': pr_source_repo_url,
321 'pr_source_repo_url': pr_source_repo_url,
323 'pr_comment_url': pr_comment_url,
322 'pr_comment_url': pr_comment_url,
324 'pr_closing': closing_pr,
323 'pr_closing': closing_pr,
325 })
324 })
326 if send_email:
325 if send_email:
327 # pre-generate the subject for notification itself
326 # pre-generate the subject for notification itself
328 (subject,
327 (subject,
329 _h, _e, # we don't care about those
328 _h, _e, # we don't care about those
330 body_plaintext) = EmailNotificationModel().render_email(
329 body_plaintext) = EmailNotificationModel().render_email(
331 notification_type, **kwargs)
330 notification_type, **kwargs)
332
331
333 mention_recipients = set(
332 mention_recipients = set(
334 self._extract_mentions(text)).difference(recipients)
333 self._extract_mentions(text)).difference(recipients)
335
334
336 # create notification objects, and emails
335 # create notification objects, and emails
337 NotificationModel().create(
336 NotificationModel().create(
338 created_by=user,
337 created_by=user,
339 notification_subject=subject,
338 notification_subject=subject,
340 notification_body=body_plaintext,
339 notification_body=body_plaintext,
341 notification_type=notification_type,
340 notification_type=notification_type,
342 recipients=recipients,
341 recipients=recipients,
343 mention_recipients=mention_recipients,
342 mention_recipients=mention_recipients,
344 email_kwargs=kwargs,
343 email_kwargs=kwargs,
345 )
344 )
346
345
347 Session().flush()
346 Session().flush()
348 if comment.pull_request:
347 if comment.pull_request:
349 action = 'repo.pull_request.comment.create'
348 action = 'repo.pull_request.comment.create'
350 else:
349 else:
351 action = 'repo.commit.comment.create'
350 action = 'repo.commit.comment.create'
352
351
353 comment_data = comment.get_api_data()
352 comment_data = comment.get_api_data()
354 self._log_audit_action(
353 self._log_audit_action(
355 action, {'data': comment_data}, user, comment)
354 action, {'data': comment_data}, user, comment)
356
355
357 msg_url = ''
356 msg_url = ''
358 channel = None
357 channel = None
359 if commit_obj:
358 if commit_obj:
360 msg_url = commit_comment_url
359 msg_url = commit_comment_url
361 repo_name = repo.repo_name
360 repo_name = repo.repo_name
362 channel = u'/repo${}$/commit/{}'.format(
361 channel = u'/repo${}$/commit/{}'.format(
363 repo_name,
362 repo_name,
364 commit_obj.raw_id
363 commit_obj.raw_id
365 )
364 )
366 elif pull_request_obj:
365 elif pull_request_obj:
367 msg_url = pr_comment_url
366 msg_url = pr_comment_url
368 repo_name = pr_target_repo.repo_name
367 repo_name = pr_target_repo.repo_name
369 channel = u'/repo${}$/pr/{}'.format(
368 channel = u'/repo${}$/pr/{}'.format(
370 repo_name,
369 repo_name,
371 pull_request_id
370 pull_request_id
372 )
371 )
373
372
374 message = '<strong>{}</strong> {} - ' \
373 message = '<strong>{}</strong> {} - ' \
375 '<a onclick="window.location=\'{}\';' \
374 '<a onclick="window.location=\'{}\';' \
376 'window.location.reload()">' \
375 'window.location.reload()">' \
377 '<strong>{}</strong></a>'
376 '<strong>{}</strong></a>'
378 message = message.format(
377 message = message.format(
379 user.username, _('made a comment'), msg_url,
378 user.username, _('made a comment'), msg_url,
380 _('Show it now'))
379 _('Show it now'))
381
380
382 channelstream.post_message(
381 channelstream.post_message(
383 channel, message, user.username,
382 channel, message, user.username,
384 registry=get_current_registry())
383 registry=get_current_registry())
385
384
386 return comment
385 return comment
387
386
388 def delete(self, comment, user):
387 def delete(self, comment, user):
389 """
388 """
390 Deletes given comment
389 Deletes given comment
391 """
390 """
392 comment = self.__get_commit_comment(comment)
391 comment = self.__get_commit_comment(comment)
393 old_data = comment.get_api_data()
392 old_data = comment.get_api_data()
394 Session().delete(comment)
393 Session().delete(comment)
395
394
396 if comment.pull_request:
395 if comment.pull_request:
397 action = 'repo.pull_request.comment.delete'
396 action = 'repo.pull_request.comment.delete'
398 else:
397 else:
399 action = 'repo.commit.comment.delete'
398 action = 'repo.commit.comment.delete'
400
399
401 self._log_audit_action(
400 self._log_audit_action(
402 action, {'old_data': old_data}, user, comment)
401 action, {'old_data': old_data}, user, comment)
403
402
404 return comment
403 return comment
405
404
406 def get_all_comments(self, repo_id, revision=None, pull_request=None):
405 def get_all_comments(self, repo_id, revision=None, pull_request=None):
407 q = ChangesetComment.query()\
406 q = ChangesetComment.query()\
408 .filter(ChangesetComment.repo_id == repo_id)
407 .filter(ChangesetComment.repo_id == repo_id)
409 if revision:
408 if revision:
410 q = q.filter(ChangesetComment.revision == revision)
409 q = q.filter(ChangesetComment.revision == revision)
411 elif pull_request:
410 elif pull_request:
412 pull_request = self.__get_pull_request(pull_request)
411 pull_request = self.__get_pull_request(pull_request)
413 q = q.filter(ChangesetComment.pull_request == pull_request)
412 q = q.filter(ChangesetComment.pull_request == pull_request)
414 else:
413 else:
415 raise Exception('Please specify commit or pull_request')
414 raise Exception('Please specify commit or pull_request')
416 q = q.order_by(ChangesetComment.created_on)
415 q = q.order_by(ChangesetComment.created_on)
417 return q.all()
416 return q.all()
418
417
419 def get_url(self, comment, request=None, permalink=False):
418 def get_url(self, comment, request=None, permalink=False):
420 if not request:
419 if not request:
421 request = get_current_request()
420 request = get_current_request()
422
421
423 comment = self.__get_commit_comment(comment)
422 comment = self.__get_commit_comment(comment)
424 if comment.pull_request:
423 if comment.pull_request:
425 pull_request = comment.pull_request
424 pull_request = comment.pull_request
426 if permalink:
425 if permalink:
427 return request.route_url(
426 return request.route_url(
428 'pull_requests_global',
427 'pull_requests_global',
429 pull_request_id=pull_request.pull_request_id,
428 pull_request_id=pull_request.pull_request_id,
430 _anchor='comment-%s' % comment.comment_id)
429 _anchor='comment-%s' % comment.comment_id)
431 else:
430 else:
432 return request.route_url('pullrequest_show',
431 return request.route_url('pullrequest_show',
433 repo_name=safe_str(pull_request.target_repo.repo_name),
432 repo_name=safe_str(pull_request.target_repo.repo_name),
434 pull_request_id=pull_request.pull_request_id,
433 pull_request_id=pull_request.pull_request_id,
435 _anchor='comment-%s' % comment.comment_id)
434 _anchor='comment-%s' % comment.comment_id)
436
435
437 else:
436 else:
438 repo = comment.repo
437 repo = comment.repo
439 commit_id = comment.revision
438 commit_id = comment.revision
440
439
441 if permalink:
440 if permalink:
442 return request.route_url(
441 return request.route_url(
443 'repo_commit', repo_name=safe_str(repo.repo_id),
442 'repo_commit', repo_name=safe_str(repo.repo_id),
444 commit_id=commit_id,
443 commit_id=commit_id,
445 _anchor='comment-%s' % comment.comment_id)
444 _anchor='comment-%s' % comment.comment_id)
446
445
447 else:
446 else:
448 return request.route_url(
447 return request.route_url(
449 'repo_commit', repo_name=safe_str(repo.repo_name),
448 'repo_commit', repo_name=safe_str(repo.repo_name),
450 commit_id=commit_id,
449 commit_id=commit_id,
451 _anchor='comment-%s' % comment.comment_id)
450 _anchor='comment-%s' % comment.comment_id)
452
451
453 def get_comments(self, repo_id, revision=None, pull_request=None):
452 def get_comments(self, repo_id, revision=None, pull_request=None):
454 """
453 """
455 Gets main comments based on revision or pull_request_id
454 Gets main comments based on revision or pull_request_id
456
455
457 :param repo_id:
456 :param repo_id:
458 :param revision:
457 :param revision:
459 :param pull_request:
458 :param pull_request:
460 """
459 """
461
460
462 q = ChangesetComment.query()\
461 q = ChangesetComment.query()\
463 .filter(ChangesetComment.repo_id == repo_id)\
462 .filter(ChangesetComment.repo_id == repo_id)\
464 .filter(ChangesetComment.line_no == None)\
463 .filter(ChangesetComment.line_no == None)\
465 .filter(ChangesetComment.f_path == None)
464 .filter(ChangesetComment.f_path == None)
466 if revision:
465 if revision:
467 q = q.filter(ChangesetComment.revision == revision)
466 q = q.filter(ChangesetComment.revision == revision)
468 elif pull_request:
467 elif pull_request:
469 pull_request = self.__get_pull_request(pull_request)
468 pull_request = self.__get_pull_request(pull_request)
470 q = q.filter(ChangesetComment.pull_request == pull_request)
469 q = q.filter(ChangesetComment.pull_request == pull_request)
471 else:
470 else:
472 raise Exception('Please specify commit or pull_request')
471 raise Exception('Please specify commit or pull_request')
473 q = q.order_by(ChangesetComment.created_on)
472 q = q.order_by(ChangesetComment.created_on)
474 return q.all()
473 return q.all()
475
474
476 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
475 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
477 q = self._get_inline_comments_query(repo_id, revision, pull_request)
476 q = self._get_inline_comments_query(repo_id, revision, pull_request)
478 return self._group_comments_by_path_and_line_number(q)
477 return self._group_comments_by_path_and_line_number(q)
479
478
480 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
479 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
481 version=None):
480 version=None):
482 inline_cnt = 0
481 inline_cnt = 0
483 for fname, per_line_comments in inline_comments.iteritems():
482 for fname, per_line_comments in inline_comments.iteritems():
484 for lno, comments in per_line_comments.iteritems():
483 for lno, comments in per_line_comments.iteritems():
485 for comm in comments:
484 for comm in comments:
486 if not comm.outdated_at_version(version) and skip_outdated:
485 if not comm.outdated_at_version(version) and skip_outdated:
487 inline_cnt += 1
486 inline_cnt += 1
488
487
489 return inline_cnt
488 return inline_cnt
490
489
491 def get_outdated_comments(self, repo_id, pull_request):
490 def get_outdated_comments(self, repo_id, pull_request):
492 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
491 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
493 # of a pull request.
492 # of a pull request.
494 q = self._all_inline_comments_of_pull_request(pull_request)
493 q = self._all_inline_comments_of_pull_request(pull_request)
495 q = q.filter(
494 q = q.filter(
496 ChangesetComment.display_state ==
495 ChangesetComment.display_state ==
497 ChangesetComment.COMMENT_OUTDATED
496 ChangesetComment.COMMENT_OUTDATED
498 ).order_by(ChangesetComment.comment_id.asc())
497 ).order_by(ChangesetComment.comment_id.asc())
499
498
500 return self._group_comments_by_path_and_line_number(q)
499 return self._group_comments_by_path_and_line_number(q)
501
500
502 def _get_inline_comments_query(self, repo_id, revision, pull_request):
501 def _get_inline_comments_query(self, repo_id, revision, pull_request):
503 # TODO: johbo: Split this into two methods: One for PR and one for
502 # TODO: johbo: Split this into two methods: One for PR and one for
504 # commit.
503 # commit.
505 if revision:
504 if revision:
506 q = Session().query(ChangesetComment).filter(
505 q = Session().query(ChangesetComment).filter(
507 ChangesetComment.repo_id == repo_id,
506 ChangesetComment.repo_id == repo_id,
508 ChangesetComment.line_no != null(),
507 ChangesetComment.line_no != null(),
509 ChangesetComment.f_path != null(),
508 ChangesetComment.f_path != null(),
510 ChangesetComment.revision == revision)
509 ChangesetComment.revision == revision)
511
510
512 elif pull_request:
511 elif pull_request:
513 pull_request = self.__get_pull_request(pull_request)
512 pull_request = self.__get_pull_request(pull_request)
514 if not CommentsModel.use_outdated_comments(pull_request):
513 if not CommentsModel.use_outdated_comments(pull_request):
515 q = self._visible_inline_comments_of_pull_request(pull_request)
514 q = self._visible_inline_comments_of_pull_request(pull_request)
516 else:
515 else:
517 q = self._all_inline_comments_of_pull_request(pull_request)
516 q = self._all_inline_comments_of_pull_request(pull_request)
518
517
519 else:
518 else:
520 raise Exception('Please specify commit or pull_request_id')
519 raise Exception('Please specify commit or pull_request_id')
521 q = q.order_by(ChangesetComment.comment_id.asc())
520 q = q.order_by(ChangesetComment.comment_id.asc())
522 return q
521 return q
523
522
524 def _group_comments_by_path_and_line_number(self, q):
523 def _group_comments_by_path_and_line_number(self, q):
525 comments = q.all()
524 comments = q.all()
526 paths = collections.defaultdict(lambda: collections.defaultdict(list))
525 paths = collections.defaultdict(lambda: collections.defaultdict(list))
527 for co in comments:
526 for co in comments:
528 paths[co.f_path][co.line_no].append(co)
527 paths[co.f_path][co.line_no].append(co)
529 return paths
528 return paths
530
529
531 @classmethod
530 @classmethod
532 def needed_extra_diff_context(cls):
531 def needed_extra_diff_context(cls):
533 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
532 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
534
533
535 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
534 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
536 if not CommentsModel.use_outdated_comments(pull_request):
535 if not CommentsModel.use_outdated_comments(pull_request):
537 return
536 return
538
537
539 comments = self._visible_inline_comments_of_pull_request(pull_request)
538 comments = self._visible_inline_comments_of_pull_request(pull_request)
540 comments_to_outdate = comments.all()
539 comments_to_outdate = comments.all()
541
540
542 for comment in comments_to_outdate:
541 for comment in comments_to_outdate:
543 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
542 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
544
543
545 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
544 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
546 diff_line = _parse_comment_line_number(comment.line_no)
545 diff_line = _parse_comment_line_number(comment.line_no)
547
546
548 try:
547 try:
549 old_context = old_diff_proc.get_context_of_line(
548 old_context = old_diff_proc.get_context_of_line(
550 path=comment.f_path, diff_line=diff_line)
549 path=comment.f_path, diff_line=diff_line)
551 new_context = new_diff_proc.get_context_of_line(
550 new_context = new_diff_proc.get_context_of_line(
552 path=comment.f_path, diff_line=diff_line)
551 path=comment.f_path, diff_line=diff_line)
553 except (diffs.LineNotInDiffException,
552 except (diffs.LineNotInDiffException,
554 diffs.FileNotInDiffException):
553 diffs.FileNotInDiffException):
555 comment.display_state = ChangesetComment.COMMENT_OUTDATED
554 comment.display_state = ChangesetComment.COMMENT_OUTDATED
556 return
555 return
557
556
558 if old_context == new_context:
557 if old_context == new_context:
559 return
558 return
560
559
561 if self._should_relocate_diff_line(diff_line):
560 if self._should_relocate_diff_line(diff_line):
562 new_diff_lines = new_diff_proc.find_context(
561 new_diff_lines = new_diff_proc.find_context(
563 path=comment.f_path, context=old_context,
562 path=comment.f_path, context=old_context,
564 offset=self.DIFF_CONTEXT_BEFORE)
563 offset=self.DIFF_CONTEXT_BEFORE)
565 if not new_diff_lines:
564 if not new_diff_lines:
566 comment.display_state = ChangesetComment.COMMENT_OUTDATED
565 comment.display_state = ChangesetComment.COMMENT_OUTDATED
567 else:
566 else:
568 new_diff_line = self._choose_closest_diff_line(
567 new_diff_line = self._choose_closest_diff_line(
569 diff_line, new_diff_lines)
568 diff_line, new_diff_lines)
570 comment.line_no = _diff_to_comment_line_number(new_diff_line)
569 comment.line_no = _diff_to_comment_line_number(new_diff_line)
571 else:
570 else:
572 comment.display_state = ChangesetComment.COMMENT_OUTDATED
571 comment.display_state = ChangesetComment.COMMENT_OUTDATED
573
572
574 def _should_relocate_diff_line(self, diff_line):
573 def _should_relocate_diff_line(self, diff_line):
575 """
574 """
576 Checks if relocation shall be tried for the given `diff_line`.
575 Checks if relocation shall be tried for the given `diff_line`.
577
576
578 If a comment points into the first lines, then we can have a situation
577 If a comment points into the first lines, then we can have a situation
579 that after an update another line has been added on top. In this case
578 that after an update another line has been added on top. In this case
580 we would find the context still and move the comment around. This
579 we would find the context still and move the comment around. This
581 would be wrong.
580 would be wrong.
582 """
581 """
583 should_relocate = (
582 should_relocate = (
584 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
583 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
585 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
584 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
586 return should_relocate
585 return should_relocate
587
586
588 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
587 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
589 candidate = new_diff_lines[0]
588 candidate = new_diff_lines[0]
590 best_delta = _diff_line_delta(diff_line, candidate)
589 best_delta = _diff_line_delta(diff_line, candidate)
591 for new_diff_line in new_diff_lines[1:]:
590 for new_diff_line in new_diff_lines[1:]:
592 delta = _diff_line_delta(diff_line, new_diff_line)
591 delta = _diff_line_delta(diff_line, new_diff_line)
593 if delta < best_delta:
592 if delta < best_delta:
594 candidate = new_diff_line
593 candidate = new_diff_line
595 best_delta = delta
594 best_delta = delta
596 return candidate
595 return candidate
597
596
598 def _visible_inline_comments_of_pull_request(self, pull_request):
597 def _visible_inline_comments_of_pull_request(self, pull_request):
599 comments = self._all_inline_comments_of_pull_request(pull_request)
598 comments = self._all_inline_comments_of_pull_request(pull_request)
600 comments = comments.filter(
599 comments = comments.filter(
601 coalesce(ChangesetComment.display_state, '') !=
600 coalesce(ChangesetComment.display_state, '') !=
602 ChangesetComment.COMMENT_OUTDATED)
601 ChangesetComment.COMMENT_OUTDATED)
603 return comments
602 return comments
604
603
605 def _all_inline_comments_of_pull_request(self, pull_request):
604 def _all_inline_comments_of_pull_request(self, pull_request):
606 comments = Session().query(ChangesetComment)\
605 comments = Session().query(ChangesetComment)\
607 .filter(ChangesetComment.line_no != None)\
606 .filter(ChangesetComment.line_no != None)\
608 .filter(ChangesetComment.f_path != None)\
607 .filter(ChangesetComment.f_path != None)\
609 .filter(ChangesetComment.pull_request == pull_request)
608 .filter(ChangesetComment.pull_request == pull_request)
610 return comments
609 return comments
611
610
612 def _all_general_comments_of_pull_request(self, pull_request):
611 def _all_general_comments_of_pull_request(self, pull_request):
613 comments = Session().query(ChangesetComment)\
612 comments = Session().query(ChangesetComment)\
614 .filter(ChangesetComment.line_no == None)\
613 .filter(ChangesetComment.line_no == None)\
615 .filter(ChangesetComment.f_path == None)\
614 .filter(ChangesetComment.f_path == None)\
616 .filter(ChangesetComment.pull_request == pull_request)
615 .filter(ChangesetComment.pull_request == pull_request)
617 return comments
616 return comments
618
617
619 @staticmethod
618 @staticmethod
620 def use_outdated_comments(pull_request):
619 def use_outdated_comments(pull_request):
621 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
620 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
622 settings = settings_model.get_general_settings()
621 settings = settings_model.get_general_settings()
623 return settings.get('rhodecode_use_outdated_comments', False)
622 return settings.get('rhodecode_use_outdated_comments', False)
624
623
625
624
626 def _parse_comment_line_number(line_no):
625 def _parse_comment_line_number(line_no):
627 """
626 """
628 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
627 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
629 """
628 """
630 old_line = None
629 old_line = None
631 new_line = None
630 new_line = None
632 if line_no.startswith('o'):
631 if line_no.startswith('o'):
633 old_line = int(line_no[1:])
632 old_line = int(line_no[1:])
634 elif line_no.startswith('n'):
633 elif line_no.startswith('n'):
635 new_line = int(line_no[1:])
634 new_line = int(line_no[1:])
636 else:
635 else:
637 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
636 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
638 return diffs.DiffLineNumber(old_line, new_line)
637 return diffs.DiffLineNumber(old_line, new_line)
639
638
640
639
641 def _diff_to_comment_line_number(diff_line):
640 def _diff_to_comment_line_number(diff_line):
642 if diff_line.new is not None:
641 if diff_line.new is not None:
643 return u'n{}'.format(diff_line.new)
642 return u'n{}'.format(diff_line.new)
644 elif diff_line.old is not None:
643 elif diff_line.old is not None:
645 return u'o{}'.format(diff_line.old)
644 return u'o{}'.format(diff_line.old)
646 return u''
645 return u''
647
646
648
647
649 def _diff_line_delta(a, b):
648 def _diff_line_delta(a, b):
650 if None not in (a.new, b.new):
649 if None not in (a.new, b.new):
651 return abs(a.new - b.new)
650 return abs(a.new - b.new)
652 elif None not in (a.old, b.old):
651 elif None not in (a.old, b.old):
653 return abs(a.old - b.old)
652 return abs(a.old - b.old)
654 else:
653 else:
655 raise ValueError(
654 raise ValueError(
656 "Cannot compute delta between {} and {}".format(a, b))
655 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,1551 +1,1552 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35 from sqlalchemy import or_
35 from sqlalchemy import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.compat import OrderedDict
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.markup_renderer import (
42 from rhodecode.lib.markup_renderer import (
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 from rhodecode.lib.vcs.backends.base import (
45 from rhodecode.lib.vcs.backends.base import (
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.exceptions import (
48 from rhodecode.lib.vcs.exceptions import (
49 CommitDoesNotExistError, EmptyRepositoryError)
49 CommitDoesNotExistError, EmptyRepositoryError)
50 from rhodecode.model import BaseModel
50 from rhodecode.model import BaseModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.comment import CommentsModel
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository)
55 PullRequestVersion, ChangesetComment, Repository)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57 from rhodecode.model.notification import NotificationModel, \
57 from rhodecode.model.notification import NotificationModel, \
58 EmailNotificationModel
58 EmailNotificationModel
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.settings import VcsSettingsModel
60 from rhodecode.model.settings import VcsSettingsModel
61
61
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 # Data structure to hold the response data when updating commits during a pull
66 # Data structure to hold the response data when updating commits during a pull
67 # request update.
67 # request update.
68 UpdateResponse = namedtuple('UpdateResponse', [
68 UpdateResponse = namedtuple('UpdateResponse', [
69 'executed', 'reason', 'new', 'old', 'changes',
69 'executed', 'reason', 'new', 'old', 'changes',
70 'source_changed', 'target_changed'])
70 'source_changed', 'target_changed'])
71
71
72
72
73 class PullRequestModel(BaseModel):
73 class PullRequestModel(BaseModel):
74
74
75 cls = PullRequest
75 cls = PullRequest
76
76
77 DIFF_CONTEXT = 3
77 DIFF_CONTEXT = 3
78
78
79 MERGE_STATUS_MESSAGES = {
79 MERGE_STATUS_MESSAGES = {
80 MergeFailureReason.NONE: lazy_ugettext(
80 MergeFailureReason.NONE: lazy_ugettext(
81 'This pull request can be automatically merged.'),
81 'This pull request can be automatically merged.'),
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
83 'This pull request cannot be merged because of an unhandled'
83 'This pull request cannot be merged because of an unhandled'
84 ' exception.'),
84 ' exception.'),
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
86 'This pull request cannot be merged because of merge conflicts.'),
86 'This pull request cannot be merged because of merge conflicts.'),
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
88 'This pull request could not be merged because push to target'
88 'This pull request could not be merged because push to target'
89 ' failed.'),
89 ' failed.'),
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
91 'This pull request cannot be merged because the target is not a'
91 'This pull request cannot be merged because the target is not a'
92 ' head.'),
92 ' head.'),
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
94 'This pull request cannot be merged because the source contains'
94 'This pull request cannot be merged because the source contains'
95 ' more branches than the target.'),
95 ' more branches than the target.'),
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
97 'This pull request cannot be merged because the target has'
97 'This pull request cannot be merged because the target has'
98 ' multiple heads.'),
98 ' multiple heads.'),
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
100 'This pull request cannot be merged because the target repository'
100 'This pull request cannot be merged because the target repository'
101 ' is locked.'),
101 ' is locked.'),
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
103 'This pull request cannot be merged because the target or the '
103 'This pull request cannot be merged because the target or the '
104 'source reference is missing.'),
104 'source reference is missing.'),
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
106 'This pull request cannot be merged because the target '
106 'This pull request cannot be merged because the target '
107 'reference is missing.'),
107 'reference is missing.'),
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
109 'This pull request cannot be merged because the source '
109 'This pull request cannot be merged because the source '
110 'reference is missing.'),
110 'reference is missing.'),
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
112 'This pull request cannot be merged because of conflicts related '
112 'This pull request cannot be merged because of conflicts related '
113 'to sub repositories.'),
113 'to sub repositories.'),
114 }
114 }
115
115
116 UPDATE_STATUS_MESSAGES = {
116 UPDATE_STATUS_MESSAGES = {
117 UpdateFailureReason.NONE: lazy_ugettext(
117 UpdateFailureReason.NONE: lazy_ugettext(
118 'Pull request update successful.'),
118 'Pull request update successful.'),
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
120 'Pull request update failed because of an unknown error.'),
120 'Pull request update failed because of an unknown error.'),
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
122 'No update needed because the source and target have not changed.'),
122 'No update needed because the source and target have not changed.'),
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
124 'Pull request cannot be updated because the reference type is '
124 'Pull request cannot be updated because the reference type is '
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
127 'This pull request cannot be updated because the target '
127 'This pull request cannot be updated because the target '
128 'reference is missing.'),
128 'reference is missing.'),
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
130 'This pull request cannot be updated because the source '
130 'This pull request cannot be updated because the source '
131 'reference is missing.'),
131 'reference is missing.'),
132 }
132 }
133
133
134 def __get_pull_request(self, pull_request):
134 def __get_pull_request(self, pull_request):
135 return self._get_instance((
135 return self._get_instance((
136 PullRequest, PullRequestVersion), pull_request)
136 PullRequest, PullRequestVersion), pull_request)
137
137
138 def _check_perms(self, perms, pull_request, user, api=False):
138 def _check_perms(self, perms, pull_request, user, api=False):
139 if not api:
139 if not api:
140 return h.HasRepoPermissionAny(*perms)(
140 return h.HasRepoPermissionAny(*perms)(
141 user=user, repo_name=pull_request.target_repo.repo_name)
141 user=user, repo_name=pull_request.target_repo.repo_name)
142 else:
142 else:
143 return h.HasRepoPermissionAnyApi(*perms)(
143 return h.HasRepoPermissionAnyApi(*perms)(
144 user=user, repo_name=pull_request.target_repo.repo_name)
144 user=user, repo_name=pull_request.target_repo.repo_name)
145
145
146 def check_user_read(self, pull_request, user, api=False):
146 def check_user_read(self, pull_request, user, api=False):
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
148 return self._check_perms(_perms, pull_request, user, api)
148 return self._check_perms(_perms, pull_request, user, api)
149
149
150 def check_user_merge(self, pull_request, user, api=False):
150 def check_user_merge(self, pull_request, user, api=False):
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
152 return self._check_perms(_perms, pull_request, user, api)
152 return self._check_perms(_perms, pull_request, user, api)
153
153
154 def check_user_update(self, pull_request, user, api=False):
154 def check_user_update(self, pull_request, user, api=False):
155 owner = user.user_id == pull_request.user_id
155 owner = user.user_id == pull_request.user_id
156 return self.check_user_merge(pull_request, user, api) or owner
156 return self.check_user_merge(pull_request, user, api) or owner
157
157
158 def check_user_delete(self, pull_request, user):
158 def check_user_delete(self, pull_request, user):
159 owner = user.user_id == pull_request.user_id
159 owner = user.user_id == pull_request.user_id
160 _perms = ('repository.admin',)
160 _perms = ('repository.admin',)
161 return self._check_perms(_perms, pull_request, user) or owner
161 return self._check_perms(_perms, pull_request, user) or owner
162
162
163 def check_user_change_status(self, pull_request, user, api=False):
163 def check_user_change_status(self, pull_request, user, api=False):
164 reviewer = user.user_id in [x.user_id for x in
164 reviewer = user.user_id in [x.user_id for x in
165 pull_request.reviewers]
165 pull_request.reviewers]
166 return self.check_user_update(pull_request, user, api) or reviewer
166 return self.check_user_update(pull_request, user, api) or reviewer
167
167
168 def get(self, pull_request):
168 def get(self, pull_request):
169 return self.__get_pull_request(pull_request)
169 return self.__get_pull_request(pull_request)
170
170
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
172 opened_by=None, order_by=None,
172 opened_by=None, order_by=None,
173 order_dir='desc'):
173 order_dir='desc'):
174 repo = None
174 repo = None
175 if repo_name:
175 if repo_name:
176 repo = self._get_repo(repo_name)
176 repo = self._get_repo(repo_name)
177
177
178 q = PullRequest.query()
178 q = PullRequest.query()
179
179
180 # source or target
180 # source or target
181 if repo and source:
181 if repo and source:
182 q = q.filter(PullRequest.source_repo == repo)
182 q = q.filter(PullRequest.source_repo == repo)
183 elif repo:
183 elif repo:
184 q = q.filter(PullRequest.target_repo == repo)
184 q = q.filter(PullRequest.target_repo == repo)
185
185
186 # closed,opened
186 # closed,opened
187 if statuses:
187 if statuses:
188 q = q.filter(PullRequest.status.in_(statuses))
188 q = q.filter(PullRequest.status.in_(statuses))
189
189
190 # opened by filter
190 # opened by filter
191 if opened_by:
191 if opened_by:
192 q = q.filter(PullRequest.user_id.in_(opened_by))
192 q = q.filter(PullRequest.user_id.in_(opened_by))
193
193
194 if order_by:
194 if order_by:
195 order_map = {
195 order_map = {
196 'name_raw': PullRequest.pull_request_id,
196 'name_raw': PullRequest.pull_request_id,
197 'title': PullRequest.title,
197 'title': PullRequest.title,
198 'updated_on_raw': PullRequest.updated_on,
198 'updated_on_raw': PullRequest.updated_on,
199 'target_repo': PullRequest.target_repo_id
199 'target_repo': PullRequest.target_repo_id
200 }
200 }
201 if order_dir == 'asc':
201 if order_dir == 'asc':
202 q = q.order_by(order_map[order_by].asc())
202 q = q.order_by(order_map[order_by].asc())
203 else:
203 else:
204 q = q.order_by(order_map[order_by].desc())
204 q = q.order_by(order_map[order_by].desc())
205
205
206 return q
206 return q
207
207
208 def count_all(self, repo_name, source=False, statuses=None,
208 def count_all(self, repo_name, source=False, statuses=None,
209 opened_by=None):
209 opened_by=None):
210 """
210 """
211 Count the number of pull requests for a specific repository.
211 Count the number of pull requests for a specific repository.
212
212
213 :param repo_name: target or source repo
213 :param repo_name: target or source repo
214 :param source: boolean flag to specify if repo_name refers to source
214 :param source: boolean flag to specify if repo_name refers to source
215 :param statuses: list of pull request statuses
215 :param statuses: list of pull request statuses
216 :param opened_by: author user of the pull request
216 :param opened_by: author user of the pull request
217 :returns: int number of pull requests
217 :returns: int number of pull requests
218 """
218 """
219 q = self._prepare_get_all_query(
219 q = self._prepare_get_all_query(
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
221
221
222 return q.count()
222 return q.count()
223
223
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
225 offset=0, length=None, order_by=None, order_dir='desc'):
225 offset=0, length=None, order_by=None, order_dir='desc'):
226 """
226 """
227 Get all pull requests for a specific repository.
227 Get all pull requests for a specific repository.
228
228
229 :param repo_name: target or source repo
229 :param repo_name: target or source repo
230 :param source: boolean flag to specify if repo_name refers to source
230 :param source: boolean flag to specify if repo_name refers to source
231 :param statuses: list of pull request statuses
231 :param statuses: list of pull request statuses
232 :param opened_by: author user of the pull request
232 :param opened_by: author user of the pull request
233 :param offset: pagination offset
233 :param offset: pagination offset
234 :param length: length of returned list
234 :param length: length of returned list
235 :param order_by: order of the returned list
235 :param order_by: order of the returned list
236 :param order_dir: 'asc' or 'desc' ordering direction
236 :param order_dir: 'asc' or 'desc' ordering direction
237 :returns: list of pull requests
237 :returns: list of pull requests
238 """
238 """
239 q = self._prepare_get_all_query(
239 q = self._prepare_get_all_query(
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
241 order_by=order_by, order_dir=order_dir)
241 order_by=order_by, order_dir=order_dir)
242
242
243 if length:
243 if length:
244 pull_requests = q.limit(length).offset(offset).all()
244 pull_requests = q.limit(length).offset(offset).all()
245 else:
245 else:
246 pull_requests = q.all()
246 pull_requests = q.all()
247
247
248 return pull_requests
248 return pull_requests
249
249
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
251 opened_by=None):
251 opened_by=None):
252 """
252 """
253 Count the number of pull requests for a specific repository that are
253 Count the number of pull requests for a specific repository that are
254 awaiting review.
254 awaiting review.
255
255
256 :param repo_name: target or source repo
256 :param repo_name: target or source repo
257 :param source: boolean flag to specify if repo_name refers to source
257 :param source: boolean flag to specify if repo_name refers to source
258 :param statuses: list of pull request statuses
258 :param statuses: list of pull request statuses
259 :param opened_by: author user of the pull request
259 :param opened_by: author user of the pull request
260 :returns: int number of pull requests
260 :returns: int number of pull requests
261 """
261 """
262 pull_requests = self.get_awaiting_review(
262 pull_requests = self.get_awaiting_review(
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
264
264
265 return len(pull_requests)
265 return len(pull_requests)
266
266
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
268 opened_by=None, offset=0, length=None,
268 opened_by=None, offset=0, length=None,
269 order_by=None, order_dir='desc'):
269 order_by=None, order_dir='desc'):
270 """
270 """
271 Get all pull requests for a specific repository that are awaiting
271 Get all pull requests for a specific repository that are awaiting
272 review.
272 review.
273
273
274 :param repo_name: target or source repo
274 :param repo_name: target or source repo
275 :param source: boolean flag to specify if repo_name refers to source
275 :param source: boolean flag to specify if repo_name refers to source
276 :param statuses: list of pull request statuses
276 :param statuses: list of pull request statuses
277 :param opened_by: author user of the pull request
277 :param opened_by: author user of the pull request
278 :param offset: pagination offset
278 :param offset: pagination offset
279 :param length: length of returned list
279 :param length: length of returned list
280 :param order_by: order of the returned list
280 :param order_by: order of the returned list
281 :param order_dir: 'asc' or 'desc' ordering direction
281 :param order_dir: 'asc' or 'desc' ordering direction
282 :returns: list of pull requests
282 :returns: list of pull requests
283 """
283 """
284 pull_requests = self.get_all(
284 pull_requests = self.get_all(
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
286 order_by=order_by, order_dir=order_dir)
286 order_by=order_by, order_dir=order_dir)
287
287
288 _filtered_pull_requests = []
288 _filtered_pull_requests = []
289 for pr in pull_requests:
289 for pr in pull_requests:
290 status = pr.calculated_review_status()
290 status = pr.calculated_review_status()
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
293 _filtered_pull_requests.append(pr)
293 _filtered_pull_requests.append(pr)
294 if length:
294 if length:
295 return _filtered_pull_requests[offset:offset+length]
295 return _filtered_pull_requests[offset:offset+length]
296 else:
296 else:
297 return _filtered_pull_requests
297 return _filtered_pull_requests
298
298
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
300 opened_by=None, user_id=None):
300 opened_by=None, user_id=None):
301 """
301 """
302 Count the number of pull requests for a specific repository that are
302 Count the number of pull requests for a specific repository that are
303 awaiting review from a specific user.
303 awaiting review from a specific user.
304
304
305 :param repo_name: target or source repo
305 :param repo_name: target or source repo
306 :param source: boolean flag to specify if repo_name refers to source
306 :param source: boolean flag to specify if repo_name refers to source
307 :param statuses: list of pull request statuses
307 :param statuses: list of pull request statuses
308 :param opened_by: author user of the pull request
308 :param opened_by: author user of the pull request
309 :param user_id: reviewer user of the pull request
309 :param user_id: reviewer user of the pull request
310 :returns: int number of pull requests
310 :returns: int number of pull requests
311 """
311 """
312 pull_requests = self.get_awaiting_my_review(
312 pull_requests = self.get_awaiting_my_review(
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
314 user_id=user_id)
314 user_id=user_id)
315
315
316 return len(pull_requests)
316 return len(pull_requests)
317
317
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
319 opened_by=None, user_id=None, offset=0,
319 opened_by=None, user_id=None, offset=0,
320 length=None, order_by=None, order_dir='desc'):
320 length=None, order_by=None, order_dir='desc'):
321 """
321 """
322 Get all pull requests for a specific repository that are awaiting
322 Get all pull requests for a specific repository that are awaiting
323 review from a specific user.
323 review from a specific user.
324
324
325 :param repo_name: target or source repo
325 :param repo_name: target or source repo
326 :param source: boolean flag to specify if repo_name refers to source
326 :param source: boolean flag to specify if repo_name refers to source
327 :param statuses: list of pull request statuses
327 :param statuses: list of pull request statuses
328 :param opened_by: author user of the pull request
328 :param opened_by: author user of the pull request
329 :param user_id: reviewer user of the pull request
329 :param user_id: reviewer user of the pull request
330 :param offset: pagination offset
330 :param offset: pagination offset
331 :param length: length of returned list
331 :param length: length of returned list
332 :param order_by: order of the returned list
332 :param order_by: order of the returned list
333 :param order_dir: 'asc' or 'desc' ordering direction
333 :param order_dir: 'asc' or 'desc' ordering direction
334 :returns: list of pull requests
334 :returns: list of pull requests
335 """
335 """
336 pull_requests = self.get_all(
336 pull_requests = self.get_all(
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
338 order_by=order_by, order_dir=order_dir)
338 order_by=order_by, order_dir=order_dir)
339
339
340 _my = PullRequestModel().get_not_reviewed(user_id)
340 _my = PullRequestModel().get_not_reviewed(user_id)
341 my_participation = []
341 my_participation = []
342 for pr in pull_requests:
342 for pr in pull_requests:
343 if pr in _my:
343 if pr in _my:
344 my_participation.append(pr)
344 my_participation.append(pr)
345 _filtered_pull_requests = my_participation
345 _filtered_pull_requests = my_participation
346 if length:
346 if length:
347 return _filtered_pull_requests[offset:offset+length]
347 return _filtered_pull_requests[offset:offset+length]
348 else:
348 else:
349 return _filtered_pull_requests
349 return _filtered_pull_requests
350
350
351 def get_not_reviewed(self, user_id):
351 def get_not_reviewed(self, user_id):
352 return [
352 return [
353 x.pull_request for x in PullRequestReviewers.query().filter(
353 x.pull_request for x in PullRequestReviewers.query().filter(
354 PullRequestReviewers.user_id == user_id).all()
354 PullRequestReviewers.user_id == user_id).all()
355 ]
355 ]
356
356
357 def _prepare_participating_query(self, user_id=None, statuses=None,
357 def _prepare_participating_query(self, user_id=None, statuses=None,
358 order_by=None, order_dir='desc'):
358 order_by=None, order_dir='desc'):
359 q = PullRequest.query()
359 q = PullRequest.query()
360 if user_id:
360 if user_id:
361 reviewers_subquery = Session().query(
361 reviewers_subquery = Session().query(
362 PullRequestReviewers.pull_request_id).filter(
362 PullRequestReviewers.pull_request_id).filter(
363 PullRequestReviewers.user_id == user_id).subquery()
363 PullRequestReviewers.user_id == user_id).subquery()
364 user_filter= or_(
364 user_filter= or_(
365 PullRequest.user_id == user_id,
365 PullRequest.user_id == user_id,
366 PullRequest.pull_request_id.in_(reviewers_subquery)
366 PullRequest.pull_request_id.in_(reviewers_subquery)
367 )
367 )
368 q = PullRequest.query().filter(user_filter)
368 q = PullRequest.query().filter(user_filter)
369
369
370 # closed,opened
370 # closed,opened
371 if statuses:
371 if statuses:
372 q = q.filter(PullRequest.status.in_(statuses))
372 q = q.filter(PullRequest.status.in_(statuses))
373
373
374 if order_by:
374 if order_by:
375 order_map = {
375 order_map = {
376 'name_raw': PullRequest.pull_request_id,
376 'name_raw': PullRequest.pull_request_id,
377 'title': PullRequest.title,
377 'title': PullRequest.title,
378 'updated_on_raw': PullRequest.updated_on,
378 'updated_on_raw': PullRequest.updated_on,
379 'target_repo': PullRequest.target_repo_id
379 'target_repo': PullRequest.target_repo_id
380 }
380 }
381 if order_dir == 'asc':
381 if order_dir == 'asc':
382 q = q.order_by(order_map[order_by].asc())
382 q = q.order_by(order_map[order_by].asc())
383 else:
383 else:
384 q = q.order_by(order_map[order_by].desc())
384 q = q.order_by(order_map[order_by].desc())
385
385
386 return q
386 return q
387
387
388 def count_im_participating_in(self, user_id=None, statuses=None):
388 def count_im_participating_in(self, user_id=None, statuses=None):
389 q = self._prepare_participating_query(user_id, statuses=statuses)
389 q = self._prepare_participating_query(user_id, statuses=statuses)
390 return q.count()
390 return q.count()
391
391
392 def get_im_participating_in(
392 def get_im_participating_in(
393 self, user_id=None, statuses=None, offset=0,
393 self, user_id=None, statuses=None, offset=0,
394 length=None, order_by=None, order_dir='desc'):
394 length=None, order_by=None, order_dir='desc'):
395 """
395 """
396 Get all Pull requests that i'm participating in, or i have opened
396 Get all Pull requests that i'm participating in, or i have opened
397 """
397 """
398
398
399 q = self._prepare_participating_query(
399 q = self._prepare_participating_query(
400 user_id, statuses=statuses, order_by=order_by,
400 user_id, statuses=statuses, order_by=order_by,
401 order_dir=order_dir)
401 order_dir=order_dir)
402
402
403 if length:
403 if length:
404 pull_requests = q.limit(length).offset(offset).all()
404 pull_requests = q.limit(length).offset(offset).all()
405 else:
405 else:
406 pull_requests = q.all()
406 pull_requests = q.all()
407
407
408 return pull_requests
408 return pull_requests
409
409
410 def get_versions(self, pull_request):
410 def get_versions(self, pull_request):
411 """
411 """
412 returns version of pull request sorted by ID descending
412 returns version of pull request sorted by ID descending
413 """
413 """
414 return PullRequestVersion.query()\
414 return PullRequestVersion.query()\
415 .filter(PullRequestVersion.pull_request == pull_request)\
415 .filter(PullRequestVersion.pull_request == pull_request)\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
417 .all()
417 .all()
418
418
419 def create(self, created_by, source_repo, source_ref, target_repo,
419 def create(self, created_by, source_repo, source_ref, target_repo,
420 target_ref, revisions, reviewers, title, description=None,
420 target_ref, revisions, reviewers, title, description=None,
421 reviewer_data=None):
421 reviewer_data=None):
422
422
423 created_by_user = self._get_user(created_by)
423 created_by_user = self._get_user(created_by)
424 source_repo = self._get_repo(source_repo)
424 source_repo = self._get_repo(source_repo)
425 target_repo = self._get_repo(target_repo)
425 target_repo = self._get_repo(target_repo)
426
426
427 pull_request = PullRequest()
427 pull_request = PullRequest()
428 pull_request.source_repo = source_repo
428 pull_request.source_repo = source_repo
429 pull_request.source_ref = source_ref
429 pull_request.source_ref = source_ref
430 pull_request.target_repo = target_repo
430 pull_request.target_repo = target_repo
431 pull_request.target_ref = target_ref
431 pull_request.target_ref = target_ref
432 pull_request.revisions = revisions
432 pull_request.revisions = revisions
433 pull_request.title = title
433 pull_request.title = title
434 pull_request.description = description
434 pull_request.description = description
435 pull_request.author = created_by_user
435 pull_request.author = created_by_user
436 pull_request.reviewer_data = reviewer_data
436 pull_request.reviewer_data = reviewer_data
437
437
438 Session().add(pull_request)
438 Session().add(pull_request)
439 Session().flush()
439 Session().flush()
440
440
441 reviewer_ids = set()
441 reviewer_ids = set()
442 # members / reviewers
442 # members / reviewers
443 for reviewer_object in reviewers:
443 for reviewer_object in reviewers:
444 user_id, reasons, mandatory = reviewer_object
444 user_id, reasons, mandatory = reviewer_object
445 user = self._get_user(user_id)
445 user = self._get_user(user_id)
446
446
447 # skip duplicates
447 # skip duplicates
448 if user.user_id in reviewer_ids:
448 if user.user_id in reviewer_ids:
449 continue
449 continue
450
450
451 reviewer_ids.add(user.user_id)
451 reviewer_ids.add(user.user_id)
452
452
453 reviewer = PullRequestReviewers()
453 reviewer = PullRequestReviewers()
454 reviewer.user = user
454 reviewer.user = user
455 reviewer.pull_request = pull_request
455 reviewer.pull_request = pull_request
456 reviewer.reasons = reasons
456 reviewer.reasons = reasons
457 reviewer.mandatory = mandatory
457 reviewer.mandatory = mandatory
458 Session().add(reviewer)
458 Session().add(reviewer)
459
459
460 # Set approval status to "Under Review" for all commits which are
460 # Set approval status to "Under Review" for all commits which are
461 # part of this pull request.
461 # part of this pull request.
462 ChangesetStatusModel().set_status(
462 ChangesetStatusModel().set_status(
463 repo=target_repo,
463 repo=target_repo,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
465 user=created_by_user,
465 user=created_by_user,
466 pull_request=pull_request
466 pull_request=pull_request
467 )
467 )
468
468
469 self.notify_reviewers(pull_request, reviewer_ids)
469 self.notify_reviewers(pull_request, reviewer_ids)
470 self._trigger_pull_request_hook(
470 self._trigger_pull_request_hook(
471 pull_request, created_by_user, 'create')
471 pull_request, created_by_user, 'create')
472
472
473 creation_data = pull_request.get_api_data(with_merge_state=False)
473 creation_data = pull_request.get_api_data(with_merge_state=False)
474 self._log_audit_action(
474 self._log_audit_action(
475 'repo.pull_request.create', {'data': creation_data},
475 'repo.pull_request.create', {'data': creation_data},
476 created_by_user, pull_request)
476 created_by_user, pull_request)
477
477
478 return pull_request
478 return pull_request
479
479
480 def _trigger_pull_request_hook(self, pull_request, user, action):
480 def _trigger_pull_request_hook(self, pull_request, user, action):
481 pull_request = self.__get_pull_request(pull_request)
481 pull_request = self.__get_pull_request(pull_request)
482 target_scm = pull_request.target_repo.scm_instance()
482 target_scm = pull_request.target_repo.scm_instance()
483 if action == 'create':
483 if action == 'create':
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
485 elif action == 'merge':
485 elif action == 'merge':
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
487 elif action == 'close':
487 elif action == 'close':
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
489 elif action == 'review_status_change':
489 elif action == 'review_status_change':
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
491 elif action == 'update':
491 elif action == 'update':
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
493 else:
493 else:
494 return
494 return
495
495
496 trigger_hook(
496 trigger_hook(
497 username=user.username,
497 username=user.username,
498 repo_name=pull_request.target_repo.repo_name,
498 repo_name=pull_request.target_repo.repo_name,
499 repo_alias=target_scm.alias,
499 repo_alias=target_scm.alias,
500 pull_request=pull_request)
500 pull_request=pull_request)
501
501
502 def _get_commit_ids(self, pull_request):
502 def _get_commit_ids(self, pull_request):
503 """
503 """
504 Return the commit ids of the merged pull request.
504 Return the commit ids of the merged pull request.
505
505
506 This method is not dealing correctly yet with the lack of autoupdates
506 This method is not dealing correctly yet with the lack of autoupdates
507 nor with the implicit target updates.
507 nor with the implicit target updates.
508 For example: if a commit in the source repo is already in the target it
508 For example: if a commit in the source repo is already in the target it
509 will be reported anyways.
509 will be reported anyways.
510 """
510 """
511 merge_rev = pull_request.merge_rev
511 merge_rev = pull_request.merge_rev
512 if merge_rev is None:
512 if merge_rev is None:
513 raise ValueError('This pull request was not merged yet')
513 raise ValueError('This pull request was not merged yet')
514
514
515 commit_ids = list(pull_request.revisions)
515 commit_ids = list(pull_request.revisions)
516 if merge_rev not in commit_ids:
516 if merge_rev not in commit_ids:
517 commit_ids.append(merge_rev)
517 commit_ids.append(merge_rev)
518
518
519 return commit_ids
519 return commit_ids
520
520
521 def merge(self, pull_request, user, extras):
521 def merge(self, pull_request, user, extras):
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
524 if merge_state.executed:
524 if merge_state.executed:
525 log.debug(
525 log.debug(
526 "Merge was successful, updating the pull request comments.")
526 "Merge was successful, updating the pull request comments.")
527 self._comment_and_close_pr(pull_request, user, merge_state)
527 self._comment_and_close_pr(pull_request, user, merge_state)
528
528
529 self._log_audit_action(
529 self._log_audit_action(
530 'repo.pull_request.merge',
530 'repo.pull_request.merge',
531 {'merge_state': merge_state.__dict__},
531 {'merge_state': merge_state.__dict__},
532 user, pull_request)
532 user, pull_request)
533
533
534 else:
534 else:
535 log.warn("Merge failed, not updating the pull request.")
535 log.warn("Merge failed, not updating the pull request.")
536 return merge_state
536 return merge_state
537
537
538 def _merge_pull_request(self, pull_request, user, extras):
538 def _merge_pull_request(self, pull_request, user, extras):
539 target_vcs = pull_request.target_repo.scm_instance()
539 target_vcs = pull_request.target_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
541 target_ref = self._refresh_reference(
541 target_ref = self._refresh_reference(
542 pull_request.target_ref_parts, target_vcs)
542 pull_request.target_ref_parts, target_vcs)
543
543
544 message = _(
544 message = _(
545 'Merge pull request #%(pr_id)s from '
545 'Merge pull request #%(pr_id)s from '
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
547 'pr_id': pull_request.pull_request_id,
547 'pr_id': pull_request.pull_request_id,
548 'source_repo': source_vcs.name,
548 'source_repo': source_vcs.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
550 'pr_title': pull_request.title
550 'pr_title': pull_request.title
551 }
551 }
552
552
553 workspace_id = self._workspace_id(pull_request)
553 workspace_id = self._workspace_id(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
555
555
556 callback_daemon, extras = prepare_callback_daemon(
556 callback_daemon, extras = prepare_callback_daemon(
557 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
557 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
558 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
558 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
559
559
560 with callback_daemon:
560 with callback_daemon:
561 # TODO: johbo: Implement a clean way to run a config_override
561 # TODO: johbo: Implement a clean way to run a config_override
562 # for a single call.
562 # for a single call.
563 target_vcs.config.set(
563 target_vcs.config.set(
564 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
564 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
565 merge_state = target_vcs.merge(
565 merge_state = target_vcs.merge(
566 target_ref, source_vcs, pull_request.source_ref_parts,
566 target_ref, source_vcs, pull_request.source_ref_parts,
567 workspace_id, user_name=user.username,
567 workspace_id, user_name=user.username,
568 user_email=user.email, message=message, use_rebase=use_rebase)
568 user_email=user.email, message=message, use_rebase=use_rebase)
569 return merge_state
569 return merge_state
570
570
571 def _comment_and_close_pr(self, pull_request, user, merge_state):
571 def _comment_and_close_pr(self, pull_request, user, merge_state):
572 pull_request.merge_rev = merge_state.merge_ref.commit_id
572 pull_request.merge_rev = merge_state.merge_ref.commit_id
573 pull_request.updated_on = datetime.datetime.now()
573 pull_request.updated_on = datetime.datetime.now()
574
574
575 CommentsModel().create(
575 CommentsModel().create(
576 text=unicode(_('Pull request merged and closed')),
576 text=unicode(_('Pull request merged and closed')),
577 repo=pull_request.target_repo.repo_id,
577 repo=pull_request.target_repo.repo_id,
578 user=user.user_id,
578 user=user.user_id,
579 pull_request=pull_request.pull_request_id,
579 pull_request=pull_request.pull_request_id,
580 f_path=None,
580 f_path=None,
581 line_no=None,
581 line_no=None,
582 closing_pr=True
582 closing_pr=True
583 )
583 )
584
584
585 Session().add(pull_request)
585 Session().add(pull_request)
586 Session().flush()
586 Session().flush()
587 # TODO: paris: replace invalidation with less radical solution
587 # TODO: paris: replace invalidation with less radical solution
588 ScmModel().mark_for_invalidation(
588 ScmModel().mark_for_invalidation(
589 pull_request.target_repo.repo_name)
589 pull_request.target_repo.repo_name)
590 self._trigger_pull_request_hook(pull_request, user, 'merge')
590 self._trigger_pull_request_hook(pull_request, user, 'merge')
591
591
592 def has_valid_update_type(self, pull_request):
592 def has_valid_update_type(self, pull_request):
593 source_ref_type = pull_request.source_ref_parts.type
593 source_ref_type = pull_request.source_ref_parts.type
594 return source_ref_type in ['book', 'branch', 'tag']
594 return source_ref_type in ['book', 'branch', 'tag']
595
595
596 def update_commits(self, pull_request):
596 def update_commits(self, pull_request):
597 """
597 """
598 Get the updated list of commits for the pull request
598 Get the updated list of commits for the pull request
599 and return the new pull request version and the list
599 and return the new pull request version and the list
600 of commits processed by this update action
600 of commits processed by this update action
601 """
601 """
602 pull_request = self.__get_pull_request(pull_request)
602 pull_request = self.__get_pull_request(pull_request)
603 source_ref_type = pull_request.source_ref_parts.type
603 source_ref_type = pull_request.source_ref_parts.type
604 source_ref_name = pull_request.source_ref_parts.name
604 source_ref_name = pull_request.source_ref_parts.name
605 source_ref_id = pull_request.source_ref_parts.commit_id
605 source_ref_id = pull_request.source_ref_parts.commit_id
606
606
607 target_ref_type = pull_request.target_ref_parts.type
607 target_ref_type = pull_request.target_ref_parts.type
608 target_ref_name = pull_request.target_ref_parts.name
608 target_ref_name = pull_request.target_ref_parts.name
609 target_ref_id = pull_request.target_ref_parts.commit_id
609 target_ref_id = pull_request.target_ref_parts.commit_id
610
610
611 if not self.has_valid_update_type(pull_request):
611 if not self.has_valid_update_type(pull_request):
612 log.debug(
612 log.debug(
613 "Skipping update of pull request %s due to ref type: %s",
613 "Skipping update of pull request %s due to ref type: %s",
614 pull_request, source_ref_type)
614 pull_request, source_ref_type)
615 return UpdateResponse(
615 return UpdateResponse(
616 executed=False,
616 executed=False,
617 reason=UpdateFailureReason.WRONG_REF_TYPE,
617 reason=UpdateFailureReason.WRONG_REF_TYPE,
618 old=pull_request, new=None, changes=None,
618 old=pull_request, new=None, changes=None,
619 source_changed=False, target_changed=False)
619 source_changed=False, target_changed=False)
620
620
621 # source repo
621 # source repo
622 source_repo = pull_request.source_repo.scm_instance()
622 source_repo = pull_request.source_repo.scm_instance()
623 try:
623 try:
624 source_commit = source_repo.get_commit(commit_id=source_ref_name)
624 source_commit = source_repo.get_commit(commit_id=source_ref_name)
625 except CommitDoesNotExistError:
625 except CommitDoesNotExistError:
626 return UpdateResponse(
626 return UpdateResponse(
627 executed=False,
627 executed=False,
628 reason=UpdateFailureReason.MISSING_SOURCE_REF,
628 reason=UpdateFailureReason.MISSING_SOURCE_REF,
629 old=pull_request, new=None, changes=None,
629 old=pull_request, new=None, changes=None,
630 source_changed=False, target_changed=False)
630 source_changed=False, target_changed=False)
631
631
632 source_changed = source_ref_id != source_commit.raw_id
632 source_changed = source_ref_id != source_commit.raw_id
633
633
634 # target repo
634 # target repo
635 target_repo = pull_request.target_repo.scm_instance()
635 target_repo = pull_request.target_repo.scm_instance()
636 try:
636 try:
637 target_commit = target_repo.get_commit(commit_id=target_ref_name)
637 target_commit = target_repo.get_commit(commit_id=target_ref_name)
638 except CommitDoesNotExistError:
638 except CommitDoesNotExistError:
639 return UpdateResponse(
639 return UpdateResponse(
640 executed=False,
640 executed=False,
641 reason=UpdateFailureReason.MISSING_TARGET_REF,
641 reason=UpdateFailureReason.MISSING_TARGET_REF,
642 old=pull_request, new=None, changes=None,
642 old=pull_request, new=None, changes=None,
643 source_changed=False, target_changed=False)
643 source_changed=False, target_changed=False)
644 target_changed = target_ref_id != target_commit.raw_id
644 target_changed = target_ref_id != target_commit.raw_id
645
645
646 if not (source_changed or target_changed):
646 if not (source_changed or target_changed):
647 log.debug("Nothing changed in pull request %s", pull_request)
647 log.debug("Nothing changed in pull request %s", pull_request)
648 return UpdateResponse(
648 return UpdateResponse(
649 executed=False,
649 executed=False,
650 reason=UpdateFailureReason.NO_CHANGE,
650 reason=UpdateFailureReason.NO_CHANGE,
651 old=pull_request, new=None, changes=None,
651 old=pull_request, new=None, changes=None,
652 source_changed=target_changed, target_changed=source_changed)
652 source_changed=target_changed, target_changed=source_changed)
653
653
654 change_in_found = 'target repo' if target_changed else 'source repo'
654 change_in_found = 'target repo' if target_changed else 'source repo'
655 log.debug('Updating pull request because of change in %s detected',
655 log.debug('Updating pull request because of change in %s detected',
656 change_in_found)
656 change_in_found)
657
657
658 # Finally there is a need for an update, in case of source change
658 # Finally there is a need for an update, in case of source change
659 # we create a new version, else just an update
659 # we create a new version, else just an update
660 if source_changed:
660 if source_changed:
661 pull_request_version = self._create_version_from_snapshot(pull_request)
661 pull_request_version = self._create_version_from_snapshot(pull_request)
662 self._link_comments_to_version(pull_request_version)
662 self._link_comments_to_version(pull_request_version)
663 else:
663 else:
664 try:
664 try:
665 ver = pull_request.versions[-1]
665 ver = pull_request.versions[-1]
666 except IndexError:
666 except IndexError:
667 ver = None
667 ver = None
668
668
669 pull_request.pull_request_version_id = \
669 pull_request.pull_request_version_id = \
670 ver.pull_request_version_id if ver else None
670 ver.pull_request_version_id if ver else None
671 pull_request_version = pull_request
671 pull_request_version = pull_request
672
672
673 try:
673 try:
674 if target_ref_type in ('tag', 'branch', 'book'):
674 if target_ref_type in ('tag', 'branch', 'book'):
675 target_commit = target_repo.get_commit(target_ref_name)
675 target_commit = target_repo.get_commit(target_ref_name)
676 else:
676 else:
677 target_commit = target_repo.get_commit(target_ref_id)
677 target_commit = target_repo.get_commit(target_ref_id)
678 except CommitDoesNotExistError:
678 except CommitDoesNotExistError:
679 return UpdateResponse(
679 return UpdateResponse(
680 executed=False,
680 executed=False,
681 reason=UpdateFailureReason.MISSING_TARGET_REF,
681 reason=UpdateFailureReason.MISSING_TARGET_REF,
682 old=pull_request, new=None, changes=None,
682 old=pull_request, new=None, changes=None,
683 source_changed=source_changed, target_changed=target_changed)
683 source_changed=source_changed, target_changed=target_changed)
684
684
685 # re-compute commit ids
685 # re-compute commit ids
686 old_commit_ids = pull_request.revisions
686 old_commit_ids = pull_request.revisions
687 pre_load = ["author", "branch", "date", "message"]
687 pre_load = ["author", "branch", "date", "message"]
688 commit_ranges = target_repo.compare(
688 commit_ranges = target_repo.compare(
689 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
689 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
690 pre_load=pre_load)
690 pre_load=pre_load)
691
691
692 ancestor = target_repo.get_common_ancestor(
692 ancestor = target_repo.get_common_ancestor(
693 target_commit.raw_id, source_commit.raw_id, source_repo)
693 target_commit.raw_id, source_commit.raw_id, source_repo)
694
694
695 pull_request.source_ref = '%s:%s:%s' % (
695 pull_request.source_ref = '%s:%s:%s' % (
696 source_ref_type, source_ref_name, source_commit.raw_id)
696 source_ref_type, source_ref_name, source_commit.raw_id)
697 pull_request.target_ref = '%s:%s:%s' % (
697 pull_request.target_ref = '%s:%s:%s' % (
698 target_ref_type, target_ref_name, ancestor)
698 target_ref_type, target_ref_name, ancestor)
699
699
700 pull_request.revisions = [
700 pull_request.revisions = [
701 commit.raw_id for commit in reversed(commit_ranges)]
701 commit.raw_id for commit in reversed(commit_ranges)]
702 pull_request.updated_on = datetime.datetime.now()
702 pull_request.updated_on = datetime.datetime.now()
703 Session().add(pull_request)
703 Session().add(pull_request)
704 new_commit_ids = pull_request.revisions
704 new_commit_ids = pull_request.revisions
705
705
706 old_diff_data, new_diff_data = self._generate_update_diffs(
706 old_diff_data, new_diff_data = self._generate_update_diffs(
707 pull_request, pull_request_version)
707 pull_request, pull_request_version)
708
708
709 # calculate commit and file changes
709 # calculate commit and file changes
710 changes = self._calculate_commit_id_changes(
710 changes = self._calculate_commit_id_changes(
711 old_commit_ids, new_commit_ids)
711 old_commit_ids, new_commit_ids)
712 file_changes = self._calculate_file_changes(
712 file_changes = self._calculate_file_changes(
713 old_diff_data, new_diff_data)
713 old_diff_data, new_diff_data)
714
714
715 # set comments as outdated if DIFFS changed
715 # set comments as outdated if DIFFS changed
716 CommentsModel().outdate_comments(
716 CommentsModel().outdate_comments(
717 pull_request, old_diff_data=old_diff_data,
717 pull_request, old_diff_data=old_diff_data,
718 new_diff_data=new_diff_data)
718 new_diff_data=new_diff_data)
719
719
720 commit_changes = (changes.added or changes.removed)
720 commit_changes = (changes.added or changes.removed)
721 file_node_changes = (
721 file_node_changes = (
722 file_changes.added or file_changes.modified or file_changes.removed)
722 file_changes.added or file_changes.modified or file_changes.removed)
723 pr_has_changes = commit_changes or file_node_changes
723 pr_has_changes = commit_changes or file_node_changes
724
724
725 # Add an automatic comment to the pull request, in case
725 # Add an automatic comment to the pull request, in case
726 # anything has changed
726 # anything has changed
727 if pr_has_changes:
727 if pr_has_changes:
728 update_comment = CommentsModel().create(
728 update_comment = CommentsModel().create(
729 text=self._render_update_message(changes, file_changes),
729 text=self._render_update_message(changes, file_changes),
730 repo=pull_request.target_repo,
730 repo=pull_request.target_repo,
731 user=pull_request.author,
731 user=pull_request.author,
732 pull_request=pull_request,
732 pull_request=pull_request,
733 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
733 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
734
734
735 # Update status to "Under Review" for added commits
735 # Update status to "Under Review" for added commits
736 for commit_id in changes.added:
736 for commit_id in changes.added:
737 ChangesetStatusModel().set_status(
737 ChangesetStatusModel().set_status(
738 repo=pull_request.source_repo,
738 repo=pull_request.source_repo,
739 status=ChangesetStatus.STATUS_UNDER_REVIEW,
739 status=ChangesetStatus.STATUS_UNDER_REVIEW,
740 comment=update_comment,
740 comment=update_comment,
741 user=pull_request.author,
741 user=pull_request.author,
742 pull_request=pull_request,
742 pull_request=pull_request,
743 revision=commit_id)
743 revision=commit_id)
744
744
745 log.debug(
745 log.debug(
746 'Updated pull request %s, added_ids: %s, common_ids: %s, '
746 'Updated pull request %s, added_ids: %s, common_ids: %s, '
747 'removed_ids: %s', pull_request.pull_request_id,
747 'removed_ids: %s', pull_request.pull_request_id,
748 changes.added, changes.common, changes.removed)
748 changes.added, changes.common, changes.removed)
749 log.debug(
749 log.debug(
750 'Updated pull request with the following file changes: %s',
750 'Updated pull request with the following file changes: %s',
751 file_changes)
751 file_changes)
752
752
753 log.info(
753 log.info(
754 "Updated pull request %s from commit %s to commit %s, "
754 "Updated pull request %s from commit %s to commit %s, "
755 "stored new version %s of this pull request.",
755 "stored new version %s of this pull request.",
756 pull_request.pull_request_id, source_ref_id,
756 pull_request.pull_request_id, source_ref_id,
757 pull_request.source_ref_parts.commit_id,
757 pull_request.source_ref_parts.commit_id,
758 pull_request_version.pull_request_version_id)
758 pull_request_version.pull_request_version_id)
759 Session().commit()
759 Session().commit()
760 self._trigger_pull_request_hook(
760 self._trigger_pull_request_hook(
761 pull_request, pull_request.author, 'update')
761 pull_request, pull_request.author, 'update')
762
762
763 return UpdateResponse(
763 return UpdateResponse(
764 executed=True, reason=UpdateFailureReason.NONE,
764 executed=True, reason=UpdateFailureReason.NONE,
765 old=pull_request, new=pull_request_version, changes=changes,
765 old=pull_request, new=pull_request_version, changes=changes,
766 source_changed=source_changed, target_changed=target_changed)
766 source_changed=source_changed, target_changed=target_changed)
767
767
768 def _create_version_from_snapshot(self, pull_request):
768 def _create_version_from_snapshot(self, pull_request):
769 version = PullRequestVersion()
769 version = PullRequestVersion()
770 version.title = pull_request.title
770 version.title = pull_request.title
771 version.description = pull_request.description
771 version.description = pull_request.description
772 version.status = pull_request.status
772 version.status = pull_request.status
773 version.created_on = datetime.datetime.now()
773 version.created_on = datetime.datetime.now()
774 version.updated_on = pull_request.updated_on
774 version.updated_on = pull_request.updated_on
775 version.user_id = pull_request.user_id
775 version.user_id = pull_request.user_id
776 version.source_repo = pull_request.source_repo
776 version.source_repo = pull_request.source_repo
777 version.source_ref = pull_request.source_ref
777 version.source_ref = pull_request.source_ref
778 version.target_repo = pull_request.target_repo
778 version.target_repo = pull_request.target_repo
779 version.target_ref = pull_request.target_ref
779 version.target_ref = pull_request.target_ref
780
780
781 version._last_merge_source_rev = pull_request._last_merge_source_rev
781 version._last_merge_source_rev = pull_request._last_merge_source_rev
782 version._last_merge_target_rev = pull_request._last_merge_target_rev
782 version._last_merge_target_rev = pull_request._last_merge_target_rev
783 version.last_merge_status = pull_request.last_merge_status
783 version.last_merge_status = pull_request.last_merge_status
784 version.shadow_merge_ref = pull_request.shadow_merge_ref
784 version.shadow_merge_ref = pull_request.shadow_merge_ref
785 version.merge_rev = pull_request.merge_rev
785 version.merge_rev = pull_request.merge_rev
786 version.reviewer_data = pull_request.reviewer_data
786 version.reviewer_data = pull_request.reviewer_data
787
787
788 version.revisions = pull_request.revisions
788 version.revisions = pull_request.revisions
789 version.pull_request = pull_request
789 version.pull_request = pull_request
790 Session().add(version)
790 Session().add(version)
791 Session().flush()
791 Session().flush()
792
792
793 return version
793 return version
794
794
795 def _generate_update_diffs(self, pull_request, pull_request_version):
795 def _generate_update_diffs(self, pull_request, pull_request_version):
796
796
797 diff_context = (
797 diff_context = (
798 self.DIFF_CONTEXT +
798 self.DIFF_CONTEXT +
799 CommentsModel.needed_extra_diff_context())
799 CommentsModel.needed_extra_diff_context())
800
800
801 source_repo = pull_request_version.source_repo
801 source_repo = pull_request_version.source_repo
802 source_ref_id = pull_request_version.source_ref_parts.commit_id
802 source_ref_id = pull_request_version.source_ref_parts.commit_id
803 target_ref_id = pull_request_version.target_ref_parts.commit_id
803 target_ref_id = pull_request_version.target_ref_parts.commit_id
804 old_diff = self._get_diff_from_pr_or_version(
804 old_diff = self._get_diff_from_pr_or_version(
805 source_repo, source_ref_id, target_ref_id, context=diff_context)
805 source_repo, source_ref_id, target_ref_id, context=diff_context)
806
806
807 source_repo = pull_request.source_repo
807 source_repo = pull_request.source_repo
808 source_ref_id = pull_request.source_ref_parts.commit_id
808 source_ref_id = pull_request.source_ref_parts.commit_id
809 target_ref_id = pull_request.target_ref_parts.commit_id
809 target_ref_id = pull_request.target_ref_parts.commit_id
810
810
811 new_diff = self._get_diff_from_pr_or_version(
811 new_diff = self._get_diff_from_pr_or_version(
812 source_repo, source_ref_id, target_ref_id, context=diff_context)
812 source_repo, source_ref_id, target_ref_id, context=diff_context)
813
813
814 old_diff_data = diffs.DiffProcessor(old_diff)
814 old_diff_data = diffs.DiffProcessor(old_diff)
815 old_diff_data.prepare()
815 old_diff_data.prepare()
816 new_diff_data = diffs.DiffProcessor(new_diff)
816 new_diff_data = diffs.DiffProcessor(new_diff)
817 new_diff_data.prepare()
817 new_diff_data.prepare()
818
818
819 return old_diff_data, new_diff_data
819 return old_diff_data, new_diff_data
820
820
821 def _link_comments_to_version(self, pull_request_version):
821 def _link_comments_to_version(self, pull_request_version):
822 """
822 """
823 Link all unlinked comments of this pull request to the given version.
823 Link all unlinked comments of this pull request to the given version.
824
824
825 :param pull_request_version: The `PullRequestVersion` to which
825 :param pull_request_version: The `PullRequestVersion` to which
826 the comments shall be linked.
826 the comments shall be linked.
827
827
828 """
828 """
829 pull_request = pull_request_version.pull_request
829 pull_request = pull_request_version.pull_request
830 comments = ChangesetComment.query()\
830 comments = ChangesetComment.query()\
831 .filter(
831 .filter(
832 # TODO: johbo: Should we query for the repo at all here?
832 # TODO: johbo: Should we query for the repo at all here?
833 # Pending decision on how comments of PRs are to be related
833 # Pending decision on how comments of PRs are to be related
834 # to either the source repo, the target repo or no repo at all.
834 # to either the source repo, the target repo or no repo at all.
835 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
835 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
836 ChangesetComment.pull_request == pull_request,
836 ChangesetComment.pull_request == pull_request,
837 ChangesetComment.pull_request_version == None)\
837 ChangesetComment.pull_request_version == None)\
838 .order_by(ChangesetComment.comment_id.asc())
838 .order_by(ChangesetComment.comment_id.asc())
839
839
840 # TODO: johbo: Find out why this breaks if it is done in a bulk
840 # TODO: johbo: Find out why this breaks if it is done in a bulk
841 # operation.
841 # operation.
842 for comment in comments:
842 for comment in comments:
843 comment.pull_request_version_id = (
843 comment.pull_request_version_id = (
844 pull_request_version.pull_request_version_id)
844 pull_request_version.pull_request_version_id)
845 Session().add(comment)
845 Session().add(comment)
846
846
847 def _calculate_commit_id_changes(self, old_ids, new_ids):
847 def _calculate_commit_id_changes(self, old_ids, new_ids):
848 added = [x for x in new_ids if x not in old_ids]
848 added = [x for x in new_ids if x not in old_ids]
849 common = [x for x in new_ids if x in old_ids]
849 common = [x for x in new_ids if x in old_ids]
850 removed = [x for x in old_ids if x not in new_ids]
850 removed = [x for x in old_ids if x not in new_ids]
851 total = new_ids
851 total = new_ids
852 return ChangeTuple(added, common, removed, total)
852 return ChangeTuple(added, common, removed, total)
853
853
854 def _calculate_file_changes(self, old_diff_data, new_diff_data):
854 def _calculate_file_changes(self, old_diff_data, new_diff_data):
855
855
856 old_files = OrderedDict()
856 old_files = OrderedDict()
857 for diff_data in old_diff_data.parsed_diff:
857 for diff_data in old_diff_data.parsed_diff:
858 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
858 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
859
859
860 added_files = []
860 added_files = []
861 modified_files = []
861 modified_files = []
862 removed_files = []
862 removed_files = []
863 for diff_data in new_diff_data.parsed_diff:
863 for diff_data in new_diff_data.parsed_diff:
864 new_filename = diff_data['filename']
864 new_filename = diff_data['filename']
865 new_hash = md5_safe(diff_data['raw_diff'])
865 new_hash = md5_safe(diff_data['raw_diff'])
866
866
867 old_hash = old_files.get(new_filename)
867 old_hash = old_files.get(new_filename)
868 if not old_hash:
868 if not old_hash:
869 # file is not present in old diff, means it's added
869 # file is not present in old diff, means it's added
870 added_files.append(new_filename)
870 added_files.append(new_filename)
871 else:
871 else:
872 if new_hash != old_hash:
872 if new_hash != old_hash:
873 modified_files.append(new_filename)
873 modified_files.append(new_filename)
874 # now remove a file from old, since we have seen it already
874 # now remove a file from old, since we have seen it already
875 del old_files[new_filename]
875 del old_files[new_filename]
876
876
877 # removed files is when there are present in old, but not in NEW,
877 # removed files is when there are present in old, but not in NEW,
878 # since we remove old files that are present in new diff, left-overs
878 # since we remove old files that are present in new diff, left-overs
879 # if any should be the removed files
879 # if any should be the removed files
880 removed_files.extend(old_files.keys())
880 removed_files.extend(old_files.keys())
881
881
882 return FileChangeTuple(added_files, modified_files, removed_files)
882 return FileChangeTuple(added_files, modified_files, removed_files)
883
883
884 def _render_update_message(self, changes, file_changes):
884 def _render_update_message(self, changes, file_changes):
885 """
885 """
886 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
886 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
887 so it's always looking the same disregarding on which default
887 so it's always looking the same disregarding on which default
888 renderer system is using.
888 renderer system is using.
889
889
890 :param changes: changes named tuple
890 :param changes: changes named tuple
891 :param file_changes: file changes named tuple
891 :param file_changes: file changes named tuple
892
892
893 """
893 """
894 new_status = ChangesetStatus.get_status_lbl(
894 new_status = ChangesetStatus.get_status_lbl(
895 ChangesetStatus.STATUS_UNDER_REVIEW)
895 ChangesetStatus.STATUS_UNDER_REVIEW)
896
896
897 changed_files = (
897 changed_files = (
898 file_changes.added + file_changes.modified + file_changes.removed)
898 file_changes.added + file_changes.modified + file_changes.removed)
899
899
900 params = {
900 params = {
901 'under_review_label': new_status,
901 'under_review_label': new_status,
902 'added_commits': changes.added,
902 'added_commits': changes.added,
903 'removed_commits': changes.removed,
903 'removed_commits': changes.removed,
904 'changed_files': changed_files,
904 'changed_files': changed_files,
905 'added_files': file_changes.added,
905 'added_files': file_changes.added,
906 'modified_files': file_changes.modified,
906 'modified_files': file_changes.modified,
907 'removed_files': file_changes.removed,
907 'removed_files': file_changes.removed,
908 }
908 }
909 renderer = RstTemplateRenderer()
909 renderer = RstTemplateRenderer()
910 return renderer.render('pull_request_update.mako', **params)
910 return renderer.render('pull_request_update.mako', **params)
911
911
912 def edit(self, pull_request, title, description, user):
912 def edit(self, pull_request, title, description, user):
913 pull_request = self.__get_pull_request(pull_request)
913 pull_request = self.__get_pull_request(pull_request)
914 old_data = pull_request.get_api_data(with_merge_state=False)
914 old_data = pull_request.get_api_data(with_merge_state=False)
915 if pull_request.is_closed():
915 if pull_request.is_closed():
916 raise ValueError('This pull request is closed')
916 raise ValueError('This pull request is closed')
917 if title:
917 if title:
918 pull_request.title = title
918 pull_request.title = title
919 pull_request.description = description
919 pull_request.description = description
920 pull_request.updated_on = datetime.datetime.now()
920 pull_request.updated_on = datetime.datetime.now()
921 Session().add(pull_request)
921 Session().add(pull_request)
922 self._log_audit_action(
922 self._log_audit_action(
923 'repo.pull_request.edit', {'old_data': old_data},
923 'repo.pull_request.edit', {'old_data': old_data},
924 user, pull_request)
924 user, pull_request)
925
925
926 def update_reviewers(self, pull_request, reviewer_data, user):
926 def update_reviewers(self, pull_request, reviewer_data, user):
927 """
927 """
928 Update the reviewers in the pull request
928 Update the reviewers in the pull request
929
929
930 :param pull_request: the pr to update
930 :param pull_request: the pr to update
931 :param reviewer_data: list of tuples
931 :param reviewer_data: list of tuples
932 [(user, ['reason1', 'reason2'], mandatory_flag)]
932 [(user, ['reason1', 'reason2'], mandatory_flag)]
933 """
933 """
934
934
935 reviewers = {}
935 reviewers = {}
936 for user_id, reasons, mandatory in reviewer_data:
936 for user_id, reasons, mandatory in reviewer_data:
937 if isinstance(user_id, (int, basestring)):
937 if isinstance(user_id, (int, basestring)):
938 user_id = self._get_user(user_id).user_id
938 user_id = self._get_user(user_id).user_id
939 reviewers[user_id] = {
939 reviewers[user_id] = {
940 'reasons': reasons, 'mandatory': mandatory}
940 'reasons': reasons, 'mandatory': mandatory}
941
941
942 reviewers_ids = set(reviewers.keys())
942 reviewers_ids = set(reviewers.keys())
943 pull_request = self.__get_pull_request(pull_request)
943 pull_request = self.__get_pull_request(pull_request)
944 current_reviewers = PullRequestReviewers.query()\
944 current_reviewers = PullRequestReviewers.query()\
945 .filter(PullRequestReviewers.pull_request ==
945 .filter(PullRequestReviewers.pull_request ==
946 pull_request).all()
946 pull_request).all()
947 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
947 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
948
948
949 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
949 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
950 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
950 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
951
951
952 log.debug("Adding %s reviewers", ids_to_add)
952 log.debug("Adding %s reviewers", ids_to_add)
953 log.debug("Removing %s reviewers", ids_to_remove)
953 log.debug("Removing %s reviewers", ids_to_remove)
954 changed = False
954 changed = False
955 for uid in ids_to_add:
955 for uid in ids_to_add:
956 changed = True
956 changed = True
957 _usr = self._get_user(uid)
957 _usr = self._get_user(uid)
958 reviewer = PullRequestReviewers()
958 reviewer = PullRequestReviewers()
959 reviewer.user = _usr
959 reviewer.user = _usr
960 reviewer.pull_request = pull_request
960 reviewer.pull_request = pull_request
961 reviewer.reasons = reviewers[uid]['reasons']
961 reviewer.reasons = reviewers[uid]['reasons']
962 # NOTE(marcink): mandatory shouldn't be changed now
962 # NOTE(marcink): mandatory shouldn't be changed now
963 # reviewer.mandatory = reviewers[uid]['reasons']
963 # reviewer.mandatory = reviewers[uid]['reasons']
964 Session().add(reviewer)
964 Session().add(reviewer)
965 self._log_audit_action(
965 self._log_audit_action(
966 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
966 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
967 user, pull_request)
967 user, pull_request)
968
968
969 for uid in ids_to_remove:
969 for uid in ids_to_remove:
970 changed = True
970 changed = True
971 reviewers = PullRequestReviewers.query()\
971 reviewers = PullRequestReviewers.query()\
972 .filter(PullRequestReviewers.user_id == uid,
972 .filter(PullRequestReviewers.user_id == uid,
973 PullRequestReviewers.pull_request == pull_request)\
973 PullRequestReviewers.pull_request == pull_request)\
974 .all()
974 .all()
975 # use .all() in case we accidentally added the same person twice
975 # use .all() in case we accidentally added the same person twice
976 # this CAN happen due to the lack of DB checks
976 # this CAN happen due to the lack of DB checks
977 for obj in reviewers:
977 for obj in reviewers:
978 old_data = obj.get_dict()
978 old_data = obj.get_dict()
979 Session().delete(obj)
979 Session().delete(obj)
980 self._log_audit_action(
980 self._log_audit_action(
981 'repo.pull_request.reviewer.delete',
981 'repo.pull_request.reviewer.delete',
982 {'old_data': old_data}, user, pull_request)
982 {'old_data': old_data}, user, pull_request)
983
983
984 if changed:
984 if changed:
985 pull_request.updated_on = datetime.datetime.now()
985 pull_request.updated_on = datetime.datetime.now()
986 Session().add(pull_request)
986 Session().add(pull_request)
987
987
988 self.notify_reviewers(pull_request, ids_to_add)
988 self.notify_reviewers(pull_request, ids_to_add)
989 return ids_to_add, ids_to_remove
989 return ids_to_add, ids_to_remove
990
990
991 def get_url(self, pull_request, request=None, permalink=False):
991 def get_url(self, pull_request, request=None, permalink=False):
992 if not request:
992 if not request:
993 request = get_current_request()
993 request = get_current_request()
994
994
995 if permalink:
995 if permalink:
996 return request.route_url(
996 return request.route_url(
997 'pull_requests_global',
997 'pull_requests_global',
998 pull_request_id=pull_request.pull_request_id,)
998 pull_request_id=pull_request.pull_request_id,)
999 else:
999 else:
1000 return request.route_url('pullrequest_show',
1000 return request.route_url('pullrequest_show',
1001 repo_name=safe_str(pull_request.target_repo.repo_name),
1001 repo_name=safe_str(pull_request.target_repo.repo_name),
1002 pull_request_id=pull_request.pull_request_id,)
1002 pull_request_id=pull_request.pull_request_id,)
1003
1003
1004 def get_shadow_clone_url(self, pull_request):
1004 def get_shadow_clone_url(self, pull_request):
1005 """
1005 """
1006 Returns qualified url pointing to the shadow repository. If this pull
1006 Returns qualified url pointing to the shadow repository. If this pull
1007 request is closed there is no shadow repository and ``None`` will be
1007 request is closed there is no shadow repository and ``None`` will be
1008 returned.
1008 returned.
1009 """
1009 """
1010 if pull_request.is_closed():
1010 if pull_request.is_closed():
1011 return None
1011 return None
1012 else:
1012 else:
1013 pr_url = urllib.unquote(self.get_url(pull_request))
1013 pr_url = urllib.unquote(self.get_url(pull_request))
1014 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1014 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1015
1015
1016 def notify_reviewers(self, pull_request, reviewers_ids):
1016 def notify_reviewers(self, pull_request, reviewers_ids):
1017 # notification to reviewers
1017 # notification to reviewers
1018 if not reviewers_ids:
1018 if not reviewers_ids:
1019 return
1019 return
1020
1020
1021 pull_request_obj = pull_request
1021 pull_request_obj = pull_request
1022 # get the current participants of this pull request
1022 # get the current participants of this pull request
1023 recipients = reviewers_ids
1023 recipients = reviewers_ids
1024 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1024 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1025
1025
1026 pr_source_repo = pull_request_obj.source_repo
1026 pr_source_repo = pull_request_obj.source_repo
1027 pr_target_repo = pull_request_obj.target_repo
1027 pr_target_repo = pull_request_obj.target_repo
1028
1028
1029 pr_url = h.route_url('pullrequest_show',
1029 pr_url = h.route_url('pullrequest_show',
1030 repo_name=pr_target_repo.repo_name,
1030 repo_name=pr_target_repo.repo_name,
1031 pull_request_id=pull_request_obj.pull_request_id,)
1031 pull_request_id=pull_request_obj.pull_request_id,)
1032
1032
1033 # set some variables for email notification
1033 # set some variables for email notification
1034 pr_target_repo_url = h.route_url(
1034 pr_target_repo_url = h.route_url(
1035 'repo_summary', repo_name=pr_target_repo.repo_name)
1035 'repo_summary', repo_name=pr_target_repo.repo_name)
1036
1036
1037 pr_source_repo_url = h.route_url(
1037 pr_source_repo_url = h.route_url(
1038 'repo_summary', repo_name=pr_source_repo.repo_name)
1038 'repo_summary', repo_name=pr_source_repo.repo_name)
1039
1039
1040 # pull request specifics
1040 # pull request specifics
1041 pull_request_commits = [
1041 pull_request_commits = [
1042 (x.raw_id, x.message)
1042 (x.raw_id, x.message)
1043 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1043 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1044
1044
1045 kwargs = {
1045 kwargs = {
1046 'user': pull_request.author,
1046 'user': pull_request.author,
1047 'pull_request': pull_request_obj,
1047 'pull_request': pull_request_obj,
1048 'pull_request_commits': pull_request_commits,
1048 'pull_request_commits': pull_request_commits,
1049
1049
1050 'pull_request_target_repo': pr_target_repo,
1050 'pull_request_target_repo': pr_target_repo,
1051 'pull_request_target_repo_url': pr_target_repo_url,
1051 'pull_request_target_repo_url': pr_target_repo_url,
1052
1052
1053 'pull_request_source_repo': pr_source_repo,
1053 'pull_request_source_repo': pr_source_repo,
1054 'pull_request_source_repo_url': pr_source_repo_url,
1054 'pull_request_source_repo_url': pr_source_repo_url,
1055
1055
1056 'pull_request_url': pr_url,
1056 'pull_request_url': pr_url,
1057 }
1057 }
1058
1058
1059 # pre-generate the subject for notification itself
1059 # pre-generate the subject for notification itself
1060 (subject,
1060 (subject,
1061 _h, _e, # we don't care about those
1061 _h, _e, # we don't care about those
1062 body_plaintext) = EmailNotificationModel().render_email(
1062 body_plaintext) = EmailNotificationModel().render_email(
1063 notification_type, **kwargs)
1063 notification_type, **kwargs)
1064
1064
1065 # create notification objects, and emails
1065 # create notification objects, and emails
1066 NotificationModel().create(
1066 NotificationModel().create(
1067 created_by=pull_request.author,
1067 created_by=pull_request.author,
1068 notification_subject=subject,
1068 notification_subject=subject,
1069 notification_body=body_plaintext,
1069 notification_body=body_plaintext,
1070 notification_type=notification_type,
1070 notification_type=notification_type,
1071 recipients=recipients,
1071 recipients=recipients,
1072 email_kwargs=kwargs,
1072 email_kwargs=kwargs,
1073 )
1073 )
1074
1074
1075 def delete(self, pull_request, user):
1075 def delete(self, pull_request, user):
1076 pull_request = self.__get_pull_request(pull_request)
1076 pull_request = self.__get_pull_request(pull_request)
1077 old_data = pull_request.get_api_data(with_merge_state=False)
1077 old_data = pull_request.get_api_data(with_merge_state=False)
1078 self._cleanup_merge_workspace(pull_request)
1078 self._cleanup_merge_workspace(pull_request)
1079 self._log_audit_action(
1079 self._log_audit_action(
1080 'repo.pull_request.delete', {'old_data': old_data},
1080 'repo.pull_request.delete', {'old_data': old_data},
1081 user, pull_request)
1081 user, pull_request)
1082 Session().delete(pull_request)
1082 Session().delete(pull_request)
1083
1083
1084 def close_pull_request(self, pull_request, user):
1084 def close_pull_request(self, pull_request, user):
1085 pull_request = self.__get_pull_request(pull_request)
1085 pull_request = self.__get_pull_request(pull_request)
1086 self._cleanup_merge_workspace(pull_request)
1086 self._cleanup_merge_workspace(pull_request)
1087 pull_request.status = PullRequest.STATUS_CLOSED
1087 pull_request.status = PullRequest.STATUS_CLOSED
1088 pull_request.updated_on = datetime.datetime.now()
1088 pull_request.updated_on = datetime.datetime.now()
1089 Session().add(pull_request)
1089 Session().add(pull_request)
1090 self._trigger_pull_request_hook(
1090 self._trigger_pull_request_hook(
1091 pull_request, pull_request.author, 'close')
1091 pull_request, pull_request.author, 'close')
1092 self._log_audit_action(
1092 self._log_audit_action(
1093 'repo.pull_request.close', {}, user, pull_request)
1093 'repo.pull_request.close', {}, user, pull_request)
1094
1094
1095 def close_pull_request_with_comment(
1095 def close_pull_request_with_comment(
1096 self, pull_request, user, repo, message=None):
1096 self, pull_request, user, repo, message=None):
1097
1097
1098 pull_request_review_status = pull_request.calculated_review_status()
1098 pull_request_review_status = pull_request.calculated_review_status()
1099
1099
1100 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1100 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1101 # approved only if we have voting consent
1101 # approved only if we have voting consent
1102 status = ChangesetStatus.STATUS_APPROVED
1102 status = ChangesetStatus.STATUS_APPROVED
1103 else:
1103 else:
1104 status = ChangesetStatus.STATUS_REJECTED
1104 status = ChangesetStatus.STATUS_REJECTED
1105 status_lbl = ChangesetStatus.get_status_lbl(status)
1105 status_lbl = ChangesetStatus.get_status_lbl(status)
1106
1106
1107 default_message = (
1107 default_message = (
1108 _('Closing with status change {transition_icon} {status}.')
1108 _('Closing with status change {transition_icon} {status}.')
1109 ).format(transition_icon='>', status=status_lbl)
1109 ).format(transition_icon='>', status=status_lbl)
1110 text = message or default_message
1110 text = message or default_message
1111
1111
1112 # create a comment, and link it to new status
1112 # create a comment, and link it to new status
1113 comment = CommentsModel().create(
1113 comment = CommentsModel().create(
1114 text=text,
1114 text=text,
1115 repo=repo.repo_id,
1115 repo=repo.repo_id,
1116 user=user.user_id,
1116 user=user.user_id,
1117 pull_request=pull_request.pull_request_id,
1117 pull_request=pull_request.pull_request_id,
1118 status_change=status_lbl,
1118 status_change=status_lbl,
1119 status_change_type=status,
1119 status_change_type=status,
1120 closing_pr=True
1120 closing_pr=True
1121 )
1121 )
1122
1122
1123 # calculate old status before we change it
1123 # calculate old status before we change it
1124 old_calculated_status = pull_request.calculated_review_status()
1124 old_calculated_status = pull_request.calculated_review_status()
1125 ChangesetStatusModel().set_status(
1125 ChangesetStatusModel().set_status(
1126 repo.repo_id,
1126 repo.repo_id,
1127 status,
1127 status,
1128 user.user_id,
1128 user.user_id,
1129 comment=comment,
1129 comment=comment,
1130 pull_request=pull_request.pull_request_id
1130 pull_request=pull_request.pull_request_id
1131 )
1131 )
1132
1132
1133 Session().flush()
1133 Session().flush()
1134 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1134 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1135 # we now calculate the status of pull request again, and based on that
1135 # we now calculate the status of pull request again, and based on that
1136 # calculation trigger status change. This might happen in cases
1136 # calculation trigger status change. This might happen in cases
1137 # that non-reviewer admin closes a pr, which means his vote doesn't
1137 # that non-reviewer admin closes a pr, which means his vote doesn't
1138 # change the status, while if he's a reviewer this might change it.
1138 # change the status, while if he's a reviewer this might change it.
1139 calculated_status = pull_request.calculated_review_status()
1139 calculated_status = pull_request.calculated_review_status()
1140 if old_calculated_status != calculated_status:
1140 if old_calculated_status != calculated_status:
1141 self._trigger_pull_request_hook(
1141 self._trigger_pull_request_hook(
1142 pull_request, user, 'review_status_change')
1142 pull_request, user, 'review_status_change')
1143
1143
1144 # finally close the PR
1144 # finally close the PR
1145 PullRequestModel().close_pull_request(
1145 PullRequestModel().close_pull_request(
1146 pull_request.pull_request_id, user)
1146 pull_request.pull_request_id, user)
1147
1147
1148 return comment, status
1148 return comment, status
1149
1149
1150 def merge_status(self, pull_request):
1150 def merge_status(self, pull_request):
1151 if not self._is_merge_enabled(pull_request):
1151 if not self._is_merge_enabled(pull_request):
1152 return False, _('Server-side pull request merging is disabled.')
1152 return False, _('Server-side pull request merging is disabled.')
1153 if pull_request.is_closed():
1153 if pull_request.is_closed():
1154 return False, _('This pull request is closed.')
1154 return False, _('This pull request is closed.')
1155 merge_possible, msg = self._check_repo_requirements(
1155 merge_possible, msg = self._check_repo_requirements(
1156 target=pull_request.target_repo, source=pull_request.source_repo)
1156 target=pull_request.target_repo, source=pull_request.source_repo)
1157 if not merge_possible:
1157 if not merge_possible:
1158 return merge_possible, msg
1158 return merge_possible, msg
1159
1159
1160 try:
1160 try:
1161 resp = self._try_merge(pull_request)
1161 resp = self._try_merge(pull_request)
1162 log.debug("Merge response: %s", resp)
1162 log.debug("Merge response: %s", resp)
1163 status = resp.possible, self.merge_status_message(
1163 status = resp.possible, self.merge_status_message(
1164 resp.failure_reason)
1164 resp.failure_reason)
1165 except NotImplementedError:
1165 except NotImplementedError:
1166 status = False, _('Pull request merging is not supported.')
1166 status = False, _('Pull request merging is not supported.')
1167
1167
1168 return status
1168 return status
1169
1169
1170 def _check_repo_requirements(self, target, source):
1170 def _check_repo_requirements(self, target, source):
1171 """
1171 """
1172 Check if `target` and `source` have compatible requirements.
1172 Check if `target` and `source` have compatible requirements.
1173
1173
1174 Currently this is just checking for largefiles.
1174 Currently this is just checking for largefiles.
1175 """
1175 """
1176 target_has_largefiles = self._has_largefiles(target)
1176 target_has_largefiles = self._has_largefiles(target)
1177 source_has_largefiles = self._has_largefiles(source)
1177 source_has_largefiles = self._has_largefiles(source)
1178 merge_possible = True
1178 merge_possible = True
1179 message = u''
1179 message = u''
1180
1180
1181 if target_has_largefiles != source_has_largefiles:
1181 if target_has_largefiles != source_has_largefiles:
1182 merge_possible = False
1182 merge_possible = False
1183 if source_has_largefiles:
1183 if source_has_largefiles:
1184 message = _(
1184 message = _(
1185 'Target repository large files support is disabled.')
1185 'Target repository large files support is disabled.')
1186 else:
1186 else:
1187 message = _(
1187 message = _(
1188 'Source repository large files support is disabled.')
1188 'Source repository large files support is disabled.')
1189
1189
1190 return merge_possible, message
1190 return merge_possible, message
1191
1191
1192 def _has_largefiles(self, repo):
1192 def _has_largefiles(self, repo):
1193 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1193 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1194 'extensions', 'largefiles')
1194 'extensions', 'largefiles')
1195 return largefiles_ui and largefiles_ui[0].active
1195 return largefiles_ui and largefiles_ui[0].active
1196
1196
1197 def _try_merge(self, pull_request):
1197 def _try_merge(self, pull_request):
1198 """
1198 """
1199 Try to merge the pull request and return the merge status.
1199 Try to merge the pull request and return the merge status.
1200 """
1200 """
1201 log.debug(
1201 log.debug(
1202 "Trying out if the pull request %s can be merged.",
1202 "Trying out if the pull request %s can be merged.",
1203 pull_request.pull_request_id)
1203 pull_request.pull_request_id)
1204 target_vcs = pull_request.target_repo.scm_instance()
1204 target_vcs = pull_request.target_repo.scm_instance()
1205
1205
1206 # Refresh the target reference.
1206 # Refresh the target reference.
1207 try:
1207 try:
1208 target_ref = self._refresh_reference(
1208 target_ref = self._refresh_reference(
1209 pull_request.target_ref_parts, target_vcs)
1209 pull_request.target_ref_parts, target_vcs)
1210 except CommitDoesNotExistError:
1210 except CommitDoesNotExistError:
1211 merge_state = MergeResponse(
1211 merge_state = MergeResponse(
1212 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1212 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1213 return merge_state
1213 return merge_state
1214
1214
1215 target_locked = pull_request.target_repo.locked
1215 target_locked = pull_request.target_repo.locked
1216 if target_locked and target_locked[0]:
1216 if target_locked and target_locked[0]:
1217 log.debug("The target repository is locked.")
1217 log.debug("The target repository is locked.")
1218 merge_state = MergeResponse(
1218 merge_state = MergeResponse(
1219 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1219 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1220 elif self._needs_merge_state_refresh(pull_request, target_ref):
1220 elif self._needs_merge_state_refresh(pull_request, target_ref):
1221 log.debug("Refreshing the merge status of the repository.")
1221 log.debug("Refreshing the merge status of the repository.")
1222 merge_state = self._refresh_merge_state(
1222 merge_state = self._refresh_merge_state(
1223 pull_request, target_vcs, target_ref)
1223 pull_request, target_vcs, target_ref)
1224 else:
1224 else:
1225 possible = pull_request.\
1225 possible = pull_request.\
1226 last_merge_status == MergeFailureReason.NONE
1226 last_merge_status == MergeFailureReason.NONE
1227 merge_state = MergeResponse(
1227 merge_state = MergeResponse(
1228 possible, False, None, pull_request.last_merge_status)
1228 possible, False, None, pull_request.last_merge_status)
1229
1229
1230 return merge_state
1230 return merge_state
1231
1231
1232 def _refresh_reference(self, reference, vcs_repository):
1232 def _refresh_reference(self, reference, vcs_repository):
1233 if reference.type in ('branch', 'book'):
1233 if reference.type in ('branch', 'book'):
1234 name_or_id = reference.name
1234 name_or_id = reference.name
1235 else:
1235 else:
1236 name_or_id = reference.commit_id
1236 name_or_id = reference.commit_id
1237 refreshed_commit = vcs_repository.get_commit(name_or_id)
1237 refreshed_commit = vcs_repository.get_commit(name_or_id)
1238 refreshed_reference = Reference(
1238 refreshed_reference = Reference(
1239 reference.type, reference.name, refreshed_commit.raw_id)
1239 reference.type, reference.name, refreshed_commit.raw_id)
1240 return refreshed_reference
1240 return refreshed_reference
1241
1241
1242 def _needs_merge_state_refresh(self, pull_request, target_reference):
1242 def _needs_merge_state_refresh(self, pull_request, target_reference):
1243 return not(
1243 return not(
1244 pull_request.revisions and
1244 pull_request.revisions and
1245 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1245 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1246 target_reference.commit_id == pull_request._last_merge_target_rev)
1246 target_reference.commit_id == pull_request._last_merge_target_rev)
1247
1247
1248 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1248 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1249 workspace_id = self._workspace_id(pull_request)
1249 workspace_id = self._workspace_id(pull_request)
1250 source_vcs = pull_request.source_repo.scm_instance()
1250 source_vcs = pull_request.source_repo.scm_instance()
1251 use_rebase = self._use_rebase_for_merging(pull_request)
1251 use_rebase = self._use_rebase_for_merging(pull_request)
1252 merge_state = target_vcs.merge(
1252 merge_state = target_vcs.merge(
1253 target_reference, source_vcs, pull_request.source_ref_parts,
1253 target_reference, source_vcs, pull_request.source_ref_parts,
1254 workspace_id, dry_run=True, use_rebase=use_rebase)
1254 workspace_id, dry_run=True, use_rebase=use_rebase)
1255
1255
1256 # Do not store the response if there was an unknown error.
1256 # Do not store the response if there was an unknown error.
1257 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1257 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1258 pull_request._last_merge_source_rev = \
1258 pull_request._last_merge_source_rev = \
1259 pull_request.source_ref_parts.commit_id
1259 pull_request.source_ref_parts.commit_id
1260 pull_request._last_merge_target_rev = target_reference.commit_id
1260 pull_request._last_merge_target_rev = target_reference.commit_id
1261 pull_request.last_merge_status = merge_state.failure_reason
1261 pull_request.last_merge_status = merge_state.failure_reason
1262 pull_request.shadow_merge_ref = merge_state.merge_ref
1262 pull_request.shadow_merge_ref = merge_state.merge_ref
1263 Session().add(pull_request)
1263 Session().add(pull_request)
1264 Session().commit()
1264 Session().commit()
1265
1265
1266 return merge_state
1266 return merge_state
1267
1267
1268 def _workspace_id(self, pull_request):
1268 def _workspace_id(self, pull_request):
1269 workspace_id = 'pr-%s' % pull_request.pull_request_id
1269 workspace_id = 'pr-%s' % pull_request.pull_request_id
1270 return workspace_id
1270 return workspace_id
1271
1271
1272 def merge_status_message(self, status_code):
1272 def merge_status_message(self, status_code):
1273 """
1273 """
1274 Return a human friendly error message for the given merge status code.
1274 Return a human friendly error message for the given merge status code.
1275 """
1275 """
1276 return self.MERGE_STATUS_MESSAGES[status_code]
1276 return self.MERGE_STATUS_MESSAGES[status_code]
1277
1277
1278 def generate_repo_data(self, repo, commit_id=None, branch=None,
1278 def generate_repo_data(self, repo, commit_id=None, branch=None,
1279 bookmark=None):
1279 bookmark=None):
1280 all_refs, selected_ref = \
1280 all_refs, selected_ref = \
1281 self._get_repo_pullrequest_sources(
1281 self._get_repo_pullrequest_sources(
1282 repo.scm_instance(), commit_id=commit_id,
1282 repo.scm_instance(), commit_id=commit_id,
1283 branch=branch, bookmark=bookmark)
1283 branch=branch, bookmark=bookmark)
1284
1284
1285 refs_select2 = []
1285 refs_select2 = []
1286 for element in all_refs:
1286 for element in all_refs:
1287 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1287 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1288 refs_select2.append({'text': element[1], 'children': children})
1288 refs_select2.append({'text': element[1], 'children': children})
1289
1289
1290 return {
1290 return {
1291 'user': {
1291 'user': {
1292 'user_id': repo.user.user_id,
1292 'user_id': repo.user.user_id,
1293 'username': repo.user.username,
1293 'username': repo.user.username,
1294 'firstname': repo.user.first_name,
1294 'firstname': repo.user.first_name,
1295 'lastname': repo.user.last_name,
1295 'lastname': repo.user.last_name,
1296 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1296 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1297 },
1297 },
1298 'description': h.chop_at_smart(repo.description_safe, '\n'),
1298 'description': h.chop_at_smart(repo.description_safe, '\n'),
1299 'refs': {
1299 'refs': {
1300 'all_refs': all_refs,
1300 'all_refs': all_refs,
1301 'selected_ref': selected_ref,
1301 'selected_ref': selected_ref,
1302 'select2_refs': refs_select2
1302 'select2_refs': refs_select2
1303 }
1303 }
1304 }
1304 }
1305
1305
1306 def generate_pullrequest_title(self, source, source_ref, target):
1306 def generate_pullrequest_title(self, source, source_ref, target):
1307 return u'{source}#{at_ref} to {target}'.format(
1307 return u'{source}#{at_ref} to {target}'.format(
1308 source=source,
1308 source=source,
1309 at_ref=source_ref,
1309 at_ref=source_ref,
1310 target=target,
1310 target=target,
1311 )
1311 )
1312
1312
1313 def _cleanup_merge_workspace(self, pull_request):
1313 def _cleanup_merge_workspace(self, pull_request):
1314 # Merging related cleanup
1314 # Merging related cleanup
1315 target_scm = pull_request.target_repo.scm_instance()
1315 target_scm = pull_request.target_repo.scm_instance()
1316 workspace_id = 'pr-%s' % pull_request.pull_request_id
1316 workspace_id = 'pr-%s' % pull_request.pull_request_id
1317
1317
1318 try:
1318 try:
1319 target_scm.cleanup_merge_workspace(workspace_id)
1319 target_scm.cleanup_merge_workspace(workspace_id)
1320 except NotImplementedError:
1320 except NotImplementedError:
1321 pass
1321 pass
1322
1322
1323 def _get_repo_pullrequest_sources(
1323 def _get_repo_pullrequest_sources(
1324 self, repo, commit_id=None, branch=None, bookmark=None):
1324 self, repo, commit_id=None, branch=None, bookmark=None):
1325 """
1325 """
1326 Return a structure with repo's interesting commits, suitable for
1326 Return a structure with repo's interesting commits, suitable for
1327 the selectors in pullrequest controller
1327 the selectors in pullrequest controller
1328
1328
1329 :param commit_id: a commit that must be in the list somehow
1329 :param commit_id: a commit that must be in the list somehow
1330 and selected by default
1330 and selected by default
1331 :param branch: a branch that must be in the list and selected
1331 :param branch: a branch that must be in the list and selected
1332 by default - even if closed
1332 by default - even if closed
1333 :param bookmark: a bookmark that must be in the list and selected
1333 :param bookmark: a bookmark that must be in the list and selected
1334 """
1334 """
1335
1335
1336 commit_id = safe_str(commit_id) if commit_id else None
1336 commit_id = safe_str(commit_id) if commit_id else None
1337 branch = safe_str(branch) if branch else None
1337 branch = safe_str(branch) if branch else None
1338 bookmark = safe_str(bookmark) if bookmark else None
1338 bookmark = safe_str(bookmark) if bookmark else None
1339
1339
1340 selected = None
1340 selected = None
1341
1341
1342 # order matters: first source that has commit_id in it will be selected
1342 # order matters: first source that has commit_id in it will be selected
1343 sources = []
1343 sources = []
1344 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1344 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1345 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1345 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1346
1346
1347 if commit_id:
1347 if commit_id:
1348 ref_commit = (h.short_id(commit_id), commit_id)
1348 ref_commit = (h.short_id(commit_id), commit_id)
1349 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1349 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1350
1350
1351 sources.append(
1351 sources.append(
1352 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1352 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1353 )
1353 )
1354
1354
1355 groups = []
1355 groups = []
1356 for group_key, ref_list, group_name, match in sources:
1356 for group_key, ref_list, group_name, match in sources:
1357 group_refs = []
1357 group_refs = []
1358 for ref_name, ref_id in ref_list:
1358 for ref_name, ref_id in ref_list:
1359 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1359 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1360 group_refs.append((ref_key, ref_name))
1360 group_refs.append((ref_key, ref_name))
1361
1361
1362 if not selected:
1362 if not selected:
1363 if set([commit_id, match]) & set([ref_id, ref_name]):
1363 if set([commit_id, match]) & set([ref_id, ref_name]):
1364 selected = ref_key
1364 selected = ref_key
1365
1365
1366 if group_refs:
1366 if group_refs:
1367 groups.append((group_refs, group_name))
1367 groups.append((group_refs, group_name))
1368
1368
1369 if not selected:
1369 if not selected:
1370 ref = commit_id or branch or bookmark
1370 ref = commit_id or branch or bookmark
1371 if ref:
1371 if ref:
1372 raise CommitDoesNotExistError(
1372 raise CommitDoesNotExistError(
1373 'No commit refs could be found matching: %s' % ref)
1373 'No commit refs could be found matching: %s' % ref)
1374 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1374 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1375 selected = 'branch:%s:%s' % (
1375 selected = 'branch:%s:%s' % (
1376 repo.DEFAULT_BRANCH_NAME,
1376 repo.DEFAULT_BRANCH_NAME,
1377 repo.branches[repo.DEFAULT_BRANCH_NAME]
1377 repo.branches[repo.DEFAULT_BRANCH_NAME]
1378 )
1378 )
1379 elif repo.commit_ids:
1379 elif repo.commit_ids:
1380 rev = repo.commit_ids[0]
1380 rev = repo.commit_ids[0]
1381 selected = 'rev:%s:%s' % (rev, rev)
1381 selected = 'rev:%s:%s' % (rev, rev)
1382 else:
1382 else:
1383 raise EmptyRepositoryError()
1383 raise EmptyRepositoryError()
1384 return groups, selected
1384 return groups, selected
1385
1385
1386 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1386 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1387 return self._get_diff_from_pr_or_version(
1387 return self._get_diff_from_pr_or_version(
1388 source_repo, source_ref_id, target_ref_id, context=context)
1388 source_repo, source_ref_id, target_ref_id, context=context)
1389
1389
1390 def _get_diff_from_pr_or_version(
1390 def _get_diff_from_pr_or_version(
1391 self, source_repo, source_ref_id, target_ref_id, context):
1391 self, source_repo, source_ref_id, target_ref_id, context):
1392 target_commit = source_repo.get_commit(
1392 target_commit = source_repo.get_commit(
1393 commit_id=safe_str(target_ref_id))
1393 commit_id=safe_str(target_ref_id))
1394 source_commit = source_repo.get_commit(
1394 source_commit = source_repo.get_commit(
1395 commit_id=safe_str(source_ref_id))
1395 commit_id=safe_str(source_ref_id))
1396 if isinstance(source_repo, Repository):
1396 if isinstance(source_repo, Repository):
1397 vcs_repo = source_repo.scm_instance()
1397 vcs_repo = source_repo.scm_instance()
1398 else:
1398 else:
1399 vcs_repo = source_repo
1399 vcs_repo = source_repo
1400
1400
1401 # TODO: johbo: In the context of an update, we cannot reach
1401 # TODO: johbo: In the context of an update, we cannot reach
1402 # the old commit anymore with our normal mechanisms. It needs
1402 # the old commit anymore with our normal mechanisms. It needs
1403 # some sort of special support in the vcs layer to avoid this
1403 # some sort of special support in the vcs layer to avoid this
1404 # workaround.
1404 # workaround.
1405 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1405 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1406 vcs_repo.alias == 'git'):
1406 vcs_repo.alias == 'git'):
1407 source_commit.raw_id = safe_str(source_ref_id)
1407 source_commit.raw_id = safe_str(source_ref_id)
1408
1408
1409 log.debug('calculating diff between '
1409 log.debug('calculating diff between '
1410 'source_ref:%s and target_ref:%s for repo `%s`',
1410 'source_ref:%s and target_ref:%s for repo `%s`',
1411 target_ref_id, source_ref_id,
1411 target_ref_id, source_ref_id,
1412 safe_unicode(vcs_repo.path))
1412 safe_unicode(vcs_repo.path))
1413
1413
1414 vcs_diff = vcs_repo.get_diff(
1414 vcs_diff = vcs_repo.get_diff(
1415 commit1=target_commit, commit2=source_commit, context=context)
1415 commit1=target_commit, commit2=source_commit, context=context)
1416 return vcs_diff
1416 return vcs_diff
1417
1417
1418 def _is_merge_enabled(self, pull_request):
1418 def _is_merge_enabled(self, pull_request):
1419 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1419 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1420 settings = settings_model.get_general_settings()
1420 settings = settings_model.get_general_settings()
1421 return settings.get('rhodecode_pr_merge_enabled', False)
1421 return settings.get('rhodecode_pr_merge_enabled', False)
1422
1422
1423 def _use_rebase_for_merging(self, pull_request):
1423 def _use_rebase_for_merging(self, pull_request):
1424 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1424 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1425 settings = settings_model.get_general_settings()
1425 settings = settings_model.get_general_settings()
1426 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1426 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1427
1427
1428 def _log_audit_action(self, action, action_data, user, pull_request):
1428 def _log_audit_action(self, action, action_data, user, pull_request):
1429 audit_logger.store(
1429 audit_logger.store(
1430 action=action,
1430 action=action,
1431 action_data=action_data,
1431 action_data=action_data,
1432 user=user,
1432 user=user,
1433 repo=pull_request.target_repo)
1433 repo=pull_request.target_repo)
1434
1434
1435 def get_reviewer_functions(self):
1435 def get_reviewer_functions(self):
1436 """
1436 """
1437 Fetches functions for validation and fetching default reviewers.
1437 Fetches functions for validation and fetching default reviewers.
1438 If available we use the EE package, else we fallback to CE
1438 If available we use the EE package, else we fallback to CE
1439 package functions
1439 package functions
1440 """
1440 """
1441 try:
1441 try:
1442 from rc_reviewers.utils import get_default_reviewers_data
1442 from rc_reviewers.utils import get_default_reviewers_data
1443 from rc_reviewers.utils import validate_default_reviewers
1443 from rc_reviewers.utils import validate_default_reviewers
1444 except ImportError:
1444 except ImportError:
1445 from rhodecode.apps.repository.utils import \
1445 from rhodecode.apps.repository.utils import \
1446 get_default_reviewers_data
1446 get_default_reviewers_data
1447 from rhodecode.apps.repository.utils import \
1447 from rhodecode.apps.repository.utils import \
1448 validate_default_reviewers
1448 validate_default_reviewers
1449
1449
1450 return get_default_reviewers_data, validate_default_reviewers
1450 return get_default_reviewers_data, validate_default_reviewers
1451
1451
1452
1452
1453 class MergeCheck(object):
1453 class MergeCheck(object):
1454 """
1454 """
1455 Perform Merge Checks and returns a check object which stores information
1455 Perform Merge Checks and returns a check object which stores information
1456 about merge errors, and merge conditions
1456 about merge errors, and merge conditions
1457 """
1457 """
1458 TODO_CHECK = 'todo'
1458 TODO_CHECK = 'todo'
1459 PERM_CHECK = 'perm'
1459 PERM_CHECK = 'perm'
1460 REVIEW_CHECK = 'review'
1460 REVIEW_CHECK = 'review'
1461 MERGE_CHECK = 'merge'
1461 MERGE_CHECK = 'merge'
1462
1462
1463 def __init__(self):
1463 def __init__(self):
1464 self.review_status = None
1464 self.review_status = None
1465 self.merge_possible = None
1465 self.merge_possible = None
1466 self.merge_msg = ''
1466 self.merge_msg = ''
1467 self.failed = None
1467 self.failed = None
1468 self.errors = []
1468 self.errors = []
1469 self.error_details = OrderedDict()
1469 self.error_details = OrderedDict()
1470
1470
1471 def push_error(self, error_type, message, error_key, details):
1471 def push_error(self, error_type, message, error_key, details):
1472 self.failed = True
1472 self.failed = True
1473 self.errors.append([error_type, message])
1473 self.errors.append([error_type, message])
1474 self.error_details[error_key] = dict(
1474 self.error_details[error_key] = dict(
1475 details=details,
1475 details=details,
1476 error_type=error_type,
1476 error_type=error_type,
1477 message=message
1477 message=message
1478 )
1478 )
1479
1479
1480 @classmethod
1480 @classmethod
1481 def validate(cls, pull_request, user, fail_early=False, translator=None):
1481 def validate(cls, pull_request, user, fail_early=False, translator=None):
1482 # if migrated to pyramid...
1482 # if migrated to pyramid...
1483 # _ = lambda: translator or _ # use passed in translator if any
1483 # _ = lambda: translator or _ # use passed in translator if any
1484
1484
1485 merge_check = cls()
1485 merge_check = cls()
1486
1486
1487 # permissions to merge
1487 # permissions to merge
1488 user_allowed_to_merge = PullRequestModel().check_user_merge(
1488 user_allowed_to_merge = PullRequestModel().check_user_merge(
1489 pull_request, user)
1489 pull_request, user)
1490 if not user_allowed_to_merge:
1490 if not user_allowed_to_merge:
1491 log.debug("MergeCheck: cannot merge, approval is pending.")
1491 log.debug("MergeCheck: cannot merge, approval is pending.")
1492
1492
1493 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1493 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1494 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1494 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1495 if fail_early:
1495 if fail_early:
1496 return merge_check
1496 return merge_check
1497
1497
1498 # review status, must be always present
1498 # review status, must be always present
1499 review_status = pull_request.calculated_review_status()
1499 review_status = pull_request.calculated_review_status()
1500 merge_check.review_status = review_status
1500 merge_check.review_status = review_status
1501
1501
1502 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1502 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1503 if not status_approved:
1503 if not status_approved:
1504 log.debug("MergeCheck: cannot merge, approval is pending.")
1504 log.debug("MergeCheck: cannot merge, approval is pending.")
1505
1505
1506 msg = _('Pull request reviewer approval is pending.')
1506 msg = _('Pull request reviewer approval is pending.')
1507
1507
1508 merge_check.push_error(
1508 merge_check.push_error(
1509 'warning', msg, cls.REVIEW_CHECK, review_status)
1509 'warning', msg, cls.REVIEW_CHECK, review_status)
1510
1510
1511 if fail_early:
1511 if fail_early:
1512 return merge_check
1512 return merge_check
1513
1513
1514 # left over TODOs
1514 # left over TODOs
1515 todos = CommentsModel().get_unresolved_todos(pull_request)
1515 todos = CommentsModel().get_unresolved_todos(pull_request)
1516 if todos:
1516 if todos:
1517 log.debug("MergeCheck: cannot merge, {} "
1517 log.debug("MergeCheck: cannot merge, {} "
1518 "unresolved todos left.".format(len(todos)))
1518 "unresolved todos left.".format(len(todos)))
1519
1519
1520 if len(todos) == 1:
1520 if len(todos) == 1:
1521 msg = _('Cannot merge, {} TODO still not resolved.').format(
1521 msg = _('Cannot merge, {} TODO still not resolved.').format(
1522 len(todos))
1522 len(todos))
1523 else:
1523 else:
1524 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1524 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1525 len(todos))
1525 len(todos))
1526
1526
1527 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1527 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1528
1528
1529 if fail_early:
1529 if fail_early:
1530 return merge_check
1530 return merge_check
1531
1531
1532 # merge possible
1532 # merge possible
1533 merge_status, msg = PullRequestModel().merge_status(pull_request)
1533 merge_status, msg = PullRequestModel().merge_status(pull_request)
1534 merge_check.merge_possible = merge_status
1534 merge_check.merge_possible = merge_status
1535 merge_check.merge_msg = msg
1535 merge_check.merge_msg = msg
1536 if not merge_status:
1536 if not merge_status:
1537 log.debug(
1537 log.debug(
1538 "MergeCheck: cannot merge, pull request merge not possible.")
1538 "MergeCheck: cannot merge, pull request merge not possible.")
1539 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1539 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1540
1540
1541 if fail_early:
1541 if fail_early:
1542 return merge_check
1542 return merge_check
1543
1543
1544 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1544 return merge_check
1545 return merge_check
1545
1546
1546
1547
1547 ChangeTuple = namedtuple('ChangeTuple',
1548 ChangeTuple = namedtuple('ChangeTuple',
1548 ['added', 'common', 'removed', 'total'])
1549 ['added', 'common', 'removed', 'total'])
1549
1550
1550 FileChangeTuple = namedtuple('FileChangeTuple',
1551 FileChangeTuple = namedtuple('FileChangeTuple',
1551 ['added', 'modified', 'removed'])
1552 ['added', 'modified', 'removed'])
@@ -1,217 +1,218 b''
1
1
2 /******************************************************************************
2 /******************************************************************************
3 * *
3 * *
4 * DO NOT CHANGE THIS FILE MANUALLY *
4 * DO NOT CHANGE THIS FILE MANUALLY *
5 * *
5 * *
6 * *
6 * *
7 * This file is automatically generated when the app starts up with *
7 * This file is automatically generated when the app starts up with *
8 * generate_js_files = true *
8 * generate_js_files = true *
9 * *
9 * *
10 * To add a route here pass jsroute=True to the route definition in the app *
10 * To add a route here pass jsroute=True to the route definition in the app *
11 * *
11 * *
12 ******************************************************************************/
12 ******************************************************************************/
13 function registerRCRoutes() {
13 function registerRCRoutes() {
14 // routes registration
14 // routes registration
15 pyroutes.register('new_repo', '/_admin/create_repository', []);
15 pyroutes.register('new_repo', '/_admin/create_repository', []);
16 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
16 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
17 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
17 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
18 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
19 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
20 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
21 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
22 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
23 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
24 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
25 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
26 pyroutes.register('favicon', '/favicon.ico', []);
18 pyroutes.register('favicon', '/favicon.ico', []);
27 pyroutes.register('robots', '/robots.txt', []);
19 pyroutes.register('robots', '/robots.txt', []);
28 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
20 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
29 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
21 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
30 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
22 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
31 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
23 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
32 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
24 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
33 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
25 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
34 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/settings/integrations', ['repo_group_name']);
26 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/settings/integrations', ['repo_group_name']);
35 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
27 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
36 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/settings/integrations/new', ['repo_group_name']);
28 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/settings/integrations/new', ['repo_group_name']);
37 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
29 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
38 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
30 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
39 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
31 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
40 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
32 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
41 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
33 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
42 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
34 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
43 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
35 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
44 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
36 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
45 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
37 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
46 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
38 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
47 pyroutes.register('admin_home', '/_admin', []);
39 pyroutes.register('admin_home', '/_admin', []);
48 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
40 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
49 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
41 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
50 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
42 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
51 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
43 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
52 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
44 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
53 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
45 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
54 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
46 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
55 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
47 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
56 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
48 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
57 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
49 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
58 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
50 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
59 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
51 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
60 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
52 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
61 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
53 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
62 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
54 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
63 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
55 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
64 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
56 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
65 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
57 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
66 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
58 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
67 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
59 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
68 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
60 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
69 pyroutes.register('users', '/_admin/users', []);
61 pyroutes.register('users', '/_admin/users', []);
70 pyroutes.register('users_data', '/_admin/users_data', []);
62 pyroutes.register('users_data', '/_admin/users_data', []);
71 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
63 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
72 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
64 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
73 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
65 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
74 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
66 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
75 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
67 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
76 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
68 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
77 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
69 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
78 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
70 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
79 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
71 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
80 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
72 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
81 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
73 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
82 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
74 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
83 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
75 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
84 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
76 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
85 pyroutes.register('channelstream_proxy', '/_channelstream', []);
77 pyroutes.register('channelstream_proxy', '/_channelstream', []);
86 pyroutes.register('login', '/_admin/login', []);
78 pyroutes.register('login', '/_admin/login', []);
87 pyroutes.register('logout', '/_admin/logout', []);
79 pyroutes.register('logout', '/_admin/logout', []);
88 pyroutes.register('register', '/_admin/register', []);
80 pyroutes.register('register', '/_admin/register', []);
89 pyroutes.register('reset_password', '/_admin/password_reset', []);
81 pyroutes.register('reset_password', '/_admin/password_reset', []);
90 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
82 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
91 pyroutes.register('home', '/', []);
83 pyroutes.register('home', '/', []);
92 pyroutes.register('user_autocomplete_data', '/_users', []);
84 pyroutes.register('user_autocomplete_data', '/_users', []);
93 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
85 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
94 pyroutes.register('repo_list_data', '/_repos', []);
86 pyroutes.register('repo_list_data', '/_repos', []);
95 pyroutes.register('goto_switcher_data', '/_goto_data', []);
87 pyroutes.register('goto_switcher_data', '/_goto_data', []);
96 pyroutes.register('journal', '/_admin/journal', []);
88 pyroutes.register('journal', '/_admin/journal', []);
97 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
89 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
98 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
90 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
99 pyroutes.register('journal_public', '/_admin/public_journal', []);
91 pyroutes.register('journal_public', '/_admin/public_journal', []);
100 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
92 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
101 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
93 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
102 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
94 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
103 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
95 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
104 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
96 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
105 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
97 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
106 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
98 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
107 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
99 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
108 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
100 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
109 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
101 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
110 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
102 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
111 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
103 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
112 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
104 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
113 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
105 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
114 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
106 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
115 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
107 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
116 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
108 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
117 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
109 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
118 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
110 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
119 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
111 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
120 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
112 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
121 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
113 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
122 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
114 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
123 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
115 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
124 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
116 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
125 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
117 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
126 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
118 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
127 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
119 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
128 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
120 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
129 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
121 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
130 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
122 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
131 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
123 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
132 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
124 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
133 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
125 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
134 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
126 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
135 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
127 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
136 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
128 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
137 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
129 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
138 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
130 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
139 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
131 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
140 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
132 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
141 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
133 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
142 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
134 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
143 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
135 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
144 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
136 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
145 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
137 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
146 pyroutes.register('repo_changelog_elements', '/%(repo_name)s/changelog_elements', ['repo_name']);
138 pyroutes.register('repo_changelog_elements', '/%(repo_name)s/changelog_elements', ['repo_name']);
147 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
139 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
148 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
140 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
149 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
141 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
150 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
142 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
151 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
143 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
152 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
144 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
153 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
145 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
154 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
146 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
147 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
148 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
149 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
150 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
151 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
152 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
153 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
154 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
155 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
155 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
156 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
156 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
157 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
157 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
158 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
158 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
159 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
159 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
160 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
160 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
161 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
161 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
162 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
162 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
163 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
163 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
164 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
164 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
165 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
165 pyroutes.register('repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
166 pyroutes.register('repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
166 pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
167 pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
167 pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
168 pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
168 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
169 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
169 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
170 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
170 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
171 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
171 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
172 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
172 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
173 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
173 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
174 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
174 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
175 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
175 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
176 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
176 pyroutes.register('search', '/_admin/search', []);
177 pyroutes.register('search', '/_admin/search', []);
177 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
178 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
178 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
179 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
179 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
180 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
180 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
181 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
181 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
182 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
182 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
183 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
183 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
184 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
184 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
185 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
185 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
186 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
186 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
187 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
187 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
188 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
188 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
189 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
189 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
190 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
190 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
191 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
191 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
192 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
192 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
193 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
193 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
194 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
194 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
195 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
195 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
196 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
196 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
197 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
197 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
198 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
198 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
199 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
199 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
200 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
200 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
201 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
201 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
202 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
202 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
203 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
203 pyroutes.register('gists_show', '/_admin/gists', []);
204 pyroutes.register('gists_show', '/_admin/gists', []);
204 pyroutes.register('gists_new', '/_admin/gists/new', []);
205 pyroutes.register('gists_new', '/_admin/gists/new', []);
205 pyroutes.register('gists_create', '/_admin/gists/create', []);
206 pyroutes.register('gists_create', '/_admin/gists/create', []);
206 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
207 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
207 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
208 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
208 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
209 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
209 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
210 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
210 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
211 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
211 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
212 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
212 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
213 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
213 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
214 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
214 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
215 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
215 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
216 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
216 pyroutes.register('apiv2', '/_admin/api', []);
217 pyroutes.register('apiv2', '/_admin/api', []);
217 }
218 }
@@ -1,831 +1,831 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 var linkifyComments = function(comments) {
28 var linkifyComments = function(comments) {
29 var firstCommentId = null;
29 var firstCommentId = null;
30 if (comments) {
30 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
31 firstCommentId = $(comments[0]).data('comment-id');
32 }
32 }
33
33
34 if (firstCommentId){
34 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
36 }
37 };
37 };
38
38
39 var bindToggleButtons = function() {
39 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
40 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
42 });
43 };
43 };
44
44
45 /* Comment form for main and inline comments */
45 /* Comment form for main and inline comments */
46 (function(mod) {
46 (function(mod) {
47
47
48 if (typeof exports == "object" && typeof module == "object") {
48 if (typeof exports == "object" && typeof module == "object") {
49 // CommonJS
49 // CommonJS
50 module.exports = mod();
50 module.exports = mod();
51 }
51 }
52 else {
52 else {
53 // Plain browser env
53 // Plain browser env
54 (this || window).CommentForm = mod();
54 (this || window).CommentForm = mod();
55 }
55 }
56
56
57 })(function() {
57 })(function() {
58 "use strict";
58 "use strict";
59
59
60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
61 if (!(this instanceof CommentForm)) {
61 if (!(this instanceof CommentForm)) {
62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
63 }
63 }
64
64
65 // bind the element instance to our Form
65 // bind the element instance to our Form
66 $(formElement).get(0).CommentForm = this;
66 $(formElement).get(0).CommentForm = this;
67
67
68 this.withLineNo = function(selector) {
68 this.withLineNo = function(selector) {
69 var lineNo = this.lineNo;
69 var lineNo = this.lineNo;
70 if (lineNo === undefined) {
70 if (lineNo === undefined) {
71 return selector
71 return selector
72 } else {
72 } else {
73 return selector + '_' + lineNo;
73 return selector + '_' + lineNo;
74 }
74 }
75 };
75 };
76
76
77 this.commitId = commitId;
77 this.commitId = commitId;
78 this.pullRequestId = pullRequestId;
78 this.pullRequestId = pullRequestId;
79 this.lineNo = lineNo;
79 this.lineNo = lineNo;
80 this.initAutocompleteActions = initAutocompleteActions;
80 this.initAutocompleteActions = initAutocompleteActions;
81
81
82 this.previewButton = this.withLineNo('#preview-btn');
82 this.previewButton = this.withLineNo('#preview-btn');
83 this.previewContainer = this.withLineNo('#preview-container');
83 this.previewContainer = this.withLineNo('#preview-container');
84
84
85 this.previewBoxSelector = this.withLineNo('#preview-box');
85 this.previewBoxSelector = this.withLineNo('#preview-box');
86
86
87 this.editButton = this.withLineNo('#edit-btn');
87 this.editButton = this.withLineNo('#edit-btn');
88 this.editContainer = this.withLineNo('#edit-container');
88 this.editContainer = this.withLineNo('#edit-container');
89 this.cancelButton = this.withLineNo('#cancel-btn');
89 this.cancelButton = this.withLineNo('#cancel-btn');
90 this.commentType = this.withLineNo('#comment_type');
90 this.commentType = this.withLineNo('#comment_type');
91
91
92 this.resolvesId = null;
92 this.resolvesId = null;
93 this.resolvesActionId = null;
93 this.resolvesActionId = null;
94
94
95 this.closesPr = '#close_pull_request';
95 this.closesPr = '#close_pull_request';
96
96
97 this.cmBox = this.withLineNo('#text');
97 this.cmBox = this.withLineNo('#text');
98 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
98 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
99
99
100 this.statusChange = this.withLineNo('#change_status');
100 this.statusChange = this.withLineNo('#change_status');
101
101
102 this.submitForm = formElement;
102 this.submitForm = formElement;
103 this.submitButton = $(this.submitForm).find('input[type="submit"]');
103 this.submitButton = $(this.submitForm).find('input[type="submit"]');
104 this.submitButtonText = this.submitButton.val();
104 this.submitButtonText = this.submitButton.val();
105
105
106 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
106 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
107 {'repo_name': templateContext.repo_name,
107 {'repo_name': templateContext.repo_name,
108 'commit_id': templateContext.commit_data.commit_id});
108 'commit_id': templateContext.commit_data.commit_id});
109
109
110 if (resolvesCommentId){
110 if (resolvesCommentId){
111 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
111 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
112 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
112 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
113 $(this.commentType).prop('disabled', true);
113 $(this.commentType).prop('disabled', true);
114 $(this.commentType).addClass('disabled');
114 $(this.commentType).addClass('disabled');
115
115
116 // disable select
116 // disable select
117 setTimeout(function() {
117 setTimeout(function() {
118 $(self.statusChange).select2('readonly', true);
118 $(self.statusChange).select2('readonly', true);
119 }, 10);
119 }, 10);
120
120
121 var resolvedInfo = (
121 var resolvedInfo = (
122 '<li class="resolve-action">' +
122 '<li class="resolve-action">' +
123 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
123 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
124 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
124 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
125 '</li>'
125 '</li>'
126 ).format(resolvesCommentId, _gettext('resolve comment'));
126 ).format(resolvesCommentId, _gettext('resolve comment'));
127 $(resolvedInfo).insertAfter($(this.commentType).parent());
127 $(resolvedInfo).insertAfter($(this.commentType).parent());
128 }
128 }
129
129
130 // based on commitId, or pullRequestId decide where do we submit
130 // based on commitId, or pullRequestId decide where do we submit
131 // out data
131 // out data
132 if (this.commitId){
132 if (this.commitId){
133 this.submitUrl = pyroutes.url('repo_commit_comment_create',
133 this.submitUrl = pyroutes.url('repo_commit_comment_create',
134 {'repo_name': templateContext.repo_name,
134 {'repo_name': templateContext.repo_name,
135 'commit_id': this.commitId});
135 'commit_id': this.commitId});
136 this.selfUrl = pyroutes.url('repo_commit',
136 this.selfUrl = pyroutes.url('repo_commit',
137 {'repo_name': templateContext.repo_name,
137 {'repo_name': templateContext.repo_name,
138 'commit_id': this.commitId});
138 'commit_id': this.commitId});
139
139
140 } else if (this.pullRequestId) {
140 } else if (this.pullRequestId) {
141 this.submitUrl = pyroutes.url('pullrequest_comment',
141 this.submitUrl = pyroutes.url('pullrequest_comment_create',
142 {'repo_name': templateContext.repo_name,
142 {'repo_name': templateContext.repo_name,
143 'pull_request_id': this.pullRequestId});
143 'pull_request_id': this.pullRequestId});
144 this.selfUrl = pyroutes.url('pullrequest_show',
144 this.selfUrl = pyroutes.url('pullrequest_show',
145 {'repo_name': templateContext.repo_name,
145 {'repo_name': templateContext.repo_name,
146 'pull_request_id': this.pullRequestId});
146 'pull_request_id': this.pullRequestId});
147
147
148 } else {
148 } else {
149 throw new Error(
149 throw new Error(
150 'CommentForm requires pullRequestId, or commitId to be specified.')
150 'CommentForm requires pullRequestId, or commitId to be specified.')
151 }
151 }
152
152
153 // FUNCTIONS and helpers
153 // FUNCTIONS and helpers
154 var self = this;
154 var self = this;
155
155
156 this.isInline = function(){
156 this.isInline = function(){
157 return this.lineNo && this.lineNo != 'general';
157 return this.lineNo && this.lineNo != 'general';
158 };
158 };
159
159
160 this.getCmInstance = function(){
160 this.getCmInstance = function(){
161 return this.cm
161 return this.cm
162 };
162 };
163
163
164 this.setPlaceholder = function(placeholder) {
164 this.setPlaceholder = function(placeholder) {
165 var cm = this.getCmInstance();
165 var cm = this.getCmInstance();
166 if (cm){
166 if (cm){
167 cm.setOption('placeholder', placeholder);
167 cm.setOption('placeholder', placeholder);
168 }
168 }
169 };
169 };
170
170
171 this.getCommentStatus = function() {
171 this.getCommentStatus = function() {
172 return $(this.submitForm).find(this.statusChange).val();
172 return $(this.submitForm).find(this.statusChange).val();
173 };
173 };
174 this.getCommentType = function() {
174 this.getCommentType = function() {
175 return $(this.submitForm).find(this.commentType).val();
175 return $(this.submitForm).find(this.commentType).val();
176 };
176 };
177
177
178 this.getResolvesId = function() {
178 this.getResolvesId = function() {
179 return $(this.submitForm).find(this.resolvesId).val() || null;
179 return $(this.submitForm).find(this.resolvesId).val() || null;
180 };
180 };
181
181
182 this.getClosePr = function() {
182 this.getClosePr = function() {
183 return $(this.submitForm).find(this.closesPr).val() || null;
183 return $(this.submitForm).find(this.closesPr).val() || null;
184 };
184 };
185
185
186 this.markCommentResolved = function(resolvedCommentId){
186 this.markCommentResolved = function(resolvedCommentId){
187 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
187 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
188 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
188 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
189 };
189 };
190
190
191 this.isAllowedToSubmit = function() {
191 this.isAllowedToSubmit = function() {
192 return !$(this.submitButton).prop('disabled');
192 return !$(this.submitButton).prop('disabled');
193 };
193 };
194
194
195 this.initStatusChangeSelector = function(){
195 this.initStatusChangeSelector = function(){
196 var formatChangeStatus = function(state, escapeMarkup) {
196 var formatChangeStatus = function(state, escapeMarkup) {
197 var originalOption = state.element;
197 var originalOption = state.element;
198 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
198 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
199 '<span>' + escapeMarkup(state.text) + '</span>';
199 '<span>' + escapeMarkup(state.text) + '</span>';
200 };
200 };
201 var formatResult = function(result, container, query, escapeMarkup) {
201 var formatResult = function(result, container, query, escapeMarkup) {
202 return formatChangeStatus(result, escapeMarkup);
202 return formatChangeStatus(result, escapeMarkup);
203 };
203 };
204
204
205 var formatSelection = function(data, container, escapeMarkup) {
205 var formatSelection = function(data, container, escapeMarkup) {
206 return formatChangeStatus(data, escapeMarkup);
206 return formatChangeStatus(data, escapeMarkup);
207 };
207 };
208
208
209 $(this.submitForm).find(this.statusChange).select2({
209 $(this.submitForm).find(this.statusChange).select2({
210 placeholder: _gettext('Status Review'),
210 placeholder: _gettext('Status Review'),
211 formatResult: formatResult,
211 formatResult: formatResult,
212 formatSelection: formatSelection,
212 formatSelection: formatSelection,
213 containerCssClass: "drop-menu status_box_menu",
213 containerCssClass: "drop-menu status_box_menu",
214 dropdownCssClass: "drop-menu-dropdown",
214 dropdownCssClass: "drop-menu-dropdown",
215 dropdownAutoWidth: true,
215 dropdownAutoWidth: true,
216 minimumResultsForSearch: -1
216 minimumResultsForSearch: -1
217 });
217 });
218 $(this.submitForm).find(this.statusChange).on('change', function() {
218 $(this.submitForm).find(this.statusChange).on('change', function() {
219 var status = self.getCommentStatus();
219 var status = self.getCommentStatus();
220
220
221 if (status && !self.isInline()) {
221 if (status && !self.isInline()) {
222 $(self.submitButton).prop('disabled', false);
222 $(self.submitButton).prop('disabled', false);
223 }
223 }
224
224
225 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
225 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
226 self.setPlaceholder(placeholderText)
226 self.setPlaceholder(placeholderText)
227 })
227 })
228 };
228 };
229
229
230 // reset the comment form into it's original state
230 // reset the comment form into it's original state
231 this.resetCommentFormState = function(content) {
231 this.resetCommentFormState = function(content) {
232 content = content || '';
232 content = content || '';
233
233
234 $(this.editContainer).show();
234 $(this.editContainer).show();
235 $(this.editButton).parent().addClass('active');
235 $(this.editButton).parent().addClass('active');
236
236
237 $(this.previewContainer).hide();
237 $(this.previewContainer).hide();
238 $(this.previewButton).parent().removeClass('active');
238 $(this.previewButton).parent().removeClass('active');
239
239
240 this.setActionButtonsDisabled(true);
240 this.setActionButtonsDisabled(true);
241 self.cm.setValue(content);
241 self.cm.setValue(content);
242 self.cm.setOption("readOnly", false);
242 self.cm.setOption("readOnly", false);
243
243
244 if (this.resolvesId) {
244 if (this.resolvesId) {
245 // destroy the resolve action
245 // destroy the resolve action
246 $(this.resolvesId).parent().remove();
246 $(this.resolvesId).parent().remove();
247 }
247 }
248 // reset closingPR flag
248 // reset closingPR flag
249 $('.close-pr-input').remove();
249 $('.close-pr-input').remove();
250
250
251 $(this.statusChange).select2('readonly', false);
251 $(this.statusChange).select2('readonly', false);
252 };
252 };
253
253
254 this.globalSubmitSuccessCallback = function(){
254 this.globalSubmitSuccessCallback = function(){
255 // default behaviour is to call GLOBAL hook, if it's registered.
255 // default behaviour is to call GLOBAL hook, if it's registered.
256 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
256 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
257 commentFormGlobalSubmitSuccessCallback()
257 commentFormGlobalSubmitSuccessCallback()
258 }
258 }
259 };
259 };
260
260
261 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
261 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
262 failHandler = failHandler || function() {};
262 failHandler = failHandler || function() {};
263 var postData = toQueryString(postData);
263 var postData = toQueryString(postData);
264 var request = $.ajax({
264 var request = $.ajax({
265 url: url,
265 url: url,
266 type: 'POST',
266 type: 'POST',
267 data: postData,
267 data: postData,
268 headers: {'X-PARTIAL-XHR': true}
268 headers: {'X-PARTIAL-XHR': true}
269 })
269 })
270 .done(function(data) {
270 .done(function(data) {
271 successHandler(data);
271 successHandler(data);
272 })
272 })
273 .fail(function(data, textStatus, errorThrown){
273 .fail(function(data, textStatus, errorThrown){
274 alert(
274 alert(
275 "Error while submitting comment.\n" +
275 "Error while submitting comment.\n" +
276 "Error code {0} ({1}).".format(data.status, data.statusText));
276 "Error code {0} ({1}).".format(data.status, data.statusText));
277 failHandler()
277 failHandler()
278 });
278 });
279 return request;
279 return request;
280 };
280 };
281
281
282 // overwrite a submitHandler, we need to do it for inline comments
282 // overwrite a submitHandler, we need to do it for inline comments
283 this.setHandleFormSubmit = function(callback) {
283 this.setHandleFormSubmit = function(callback) {
284 this.handleFormSubmit = callback;
284 this.handleFormSubmit = callback;
285 };
285 };
286
286
287 // overwrite a submitSuccessHandler
287 // overwrite a submitSuccessHandler
288 this.setGlobalSubmitSuccessCallback = function(callback) {
288 this.setGlobalSubmitSuccessCallback = function(callback) {
289 this.globalSubmitSuccessCallback = callback;
289 this.globalSubmitSuccessCallback = callback;
290 };
290 };
291
291
292 // default handler for for submit for main comments
292 // default handler for for submit for main comments
293 this.handleFormSubmit = function() {
293 this.handleFormSubmit = function() {
294 var text = self.cm.getValue();
294 var text = self.cm.getValue();
295 var status = self.getCommentStatus();
295 var status = self.getCommentStatus();
296 var commentType = self.getCommentType();
296 var commentType = self.getCommentType();
297 var resolvesCommentId = self.getResolvesId();
297 var resolvesCommentId = self.getResolvesId();
298 var closePullRequest = self.getClosePr();
298 var closePullRequest = self.getClosePr();
299
299
300 if (text === "" && !status) {
300 if (text === "" && !status) {
301 return;
301 return;
302 }
302 }
303
303
304 var excludeCancelBtn = false;
304 var excludeCancelBtn = false;
305 var submitEvent = true;
305 var submitEvent = true;
306 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
306 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
307 self.cm.setOption("readOnly", true);
307 self.cm.setOption("readOnly", true);
308
308
309 var postData = {
309 var postData = {
310 'text': text,
310 'text': text,
311 'changeset_status': status,
311 'changeset_status': status,
312 'comment_type': commentType,
312 'comment_type': commentType,
313 'csrf_token': CSRF_TOKEN
313 'csrf_token': CSRF_TOKEN
314 };
314 };
315
315
316 if (resolvesCommentId) {
316 if (resolvesCommentId) {
317 postData['resolves_comment_id'] = resolvesCommentId;
317 postData['resolves_comment_id'] = resolvesCommentId;
318 }
318 }
319
319
320 if (closePullRequest) {
320 if (closePullRequest) {
321 postData['close_pull_request'] = true;
321 postData['close_pull_request'] = true;
322 }
322 }
323
323
324 var submitSuccessCallback = function(o) {
324 var submitSuccessCallback = function(o) {
325 // reload page if we change status for single commit.
325 // reload page if we change status for single commit.
326 if (status && self.commitId) {
326 if (status && self.commitId) {
327 location.reload(true);
327 location.reload(true);
328 } else {
328 } else {
329 $('#injected_page_comments').append(o.rendered_text);
329 $('#injected_page_comments').append(o.rendered_text);
330 self.resetCommentFormState();
330 self.resetCommentFormState();
331 timeagoActivate();
331 timeagoActivate();
332
332
333 // mark visually which comment was resolved
333 // mark visually which comment was resolved
334 if (resolvesCommentId) {
334 if (resolvesCommentId) {
335 self.markCommentResolved(resolvesCommentId);
335 self.markCommentResolved(resolvesCommentId);
336 }
336 }
337 }
337 }
338
338
339 // run global callback on submit
339 // run global callback on submit
340 self.globalSubmitSuccessCallback();
340 self.globalSubmitSuccessCallback();
341
341
342 };
342 };
343 var submitFailCallback = function(){
343 var submitFailCallback = function(){
344 self.resetCommentFormState(text);
344 self.resetCommentFormState(text);
345 };
345 };
346 self.submitAjaxPOST(
346 self.submitAjaxPOST(
347 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
347 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
348 };
348 };
349
349
350 this.previewSuccessCallback = function(o) {
350 this.previewSuccessCallback = function(o) {
351 $(self.previewBoxSelector).html(o);
351 $(self.previewBoxSelector).html(o);
352 $(self.previewBoxSelector).removeClass('unloaded');
352 $(self.previewBoxSelector).removeClass('unloaded');
353
353
354 // swap buttons, making preview active
354 // swap buttons, making preview active
355 $(self.previewButton).parent().addClass('active');
355 $(self.previewButton).parent().addClass('active');
356 $(self.editButton).parent().removeClass('active');
356 $(self.editButton).parent().removeClass('active');
357
357
358 // unlock buttons
358 // unlock buttons
359 self.setActionButtonsDisabled(false);
359 self.setActionButtonsDisabled(false);
360 };
360 };
361
361
362 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
362 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
363 excludeCancelBtn = excludeCancelBtn || false;
363 excludeCancelBtn = excludeCancelBtn || false;
364 submitEvent = submitEvent || false;
364 submitEvent = submitEvent || false;
365
365
366 $(this.editButton).prop('disabled', state);
366 $(this.editButton).prop('disabled', state);
367 $(this.previewButton).prop('disabled', state);
367 $(this.previewButton).prop('disabled', state);
368
368
369 if (!excludeCancelBtn) {
369 if (!excludeCancelBtn) {
370 $(this.cancelButton).prop('disabled', state);
370 $(this.cancelButton).prop('disabled', state);
371 }
371 }
372
372
373 var submitState = state;
373 var submitState = state;
374 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
374 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
375 // if the value of commit review status is set, we allow
375 // if the value of commit review status is set, we allow
376 // submit button, but only on Main form, isInline means inline
376 // submit button, but only on Main form, isInline means inline
377 submitState = false
377 submitState = false
378 }
378 }
379
379
380 $(this.submitButton).prop('disabled', submitState);
380 $(this.submitButton).prop('disabled', submitState);
381 if (submitEvent) {
381 if (submitEvent) {
382 $(this.submitButton).val(_gettext('Submitting...'));
382 $(this.submitButton).val(_gettext('Submitting...'));
383 } else {
383 } else {
384 $(this.submitButton).val(this.submitButtonText);
384 $(this.submitButton).val(this.submitButtonText);
385 }
385 }
386
386
387 };
387 };
388
388
389 // lock preview/edit/submit buttons on load, but exclude cancel button
389 // lock preview/edit/submit buttons on load, but exclude cancel button
390 var excludeCancelBtn = true;
390 var excludeCancelBtn = true;
391 this.setActionButtonsDisabled(true, excludeCancelBtn);
391 this.setActionButtonsDisabled(true, excludeCancelBtn);
392
392
393 // anonymous users don't have access to initialized CM instance
393 // anonymous users don't have access to initialized CM instance
394 if (this.cm !== undefined){
394 if (this.cm !== undefined){
395 this.cm.on('change', function(cMirror) {
395 this.cm.on('change', function(cMirror) {
396 if (cMirror.getValue() === "") {
396 if (cMirror.getValue() === "") {
397 self.setActionButtonsDisabled(true, excludeCancelBtn)
397 self.setActionButtonsDisabled(true, excludeCancelBtn)
398 } else {
398 } else {
399 self.setActionButtonsDisabled(false, excludeCancelBtn)
399 self.setActionButtonsDisabled(false, excludeCancelBtn)
400 }
400 }
401 });
401 });
402 }
402 }
403
403
404 $(this.editButton).on('click', function(e) {
404 $(this.editButton).on('click', function(e) {
405 e.preventDefault();
405 e.preventDefault();
406
406
407 $(self.previewButton).parent().removeClass('active');
407 $(self.previewButton).parent().removeClass('active');
408 $(self.previewContainer).hide();
408 $(self.previewContainer).hide();
409
409
410 $(self.editButton).parent().addClass('active');
410 $(self.editButton).parent().addClass('active');
411 $(self.editContainer).show();
411 $(self.editContainer).show();
412
412
413 });
413 });
414
414
415 $(this.previewButton).on('click', function(e) {
415 $(this.previewButton).on('click', function(e) {
416 e.preventDefault();
416 e.preventDefault();
417 var text = self.cm.getValue();
417 var text = self.cm.getValue();
418
418
419 if (text === "") {
419 if (text === "") {
420 return;
420 return;
421 }
421 }
422
422
423 var postData = {
423 var postData = {
424 'text': text,
424 'text': text,
425 'renderer': templateContext.visual.default_renderer,
425 'renderer': templateContext.visual.default_renderer,
426 'csrf_token': CSRF_TOKEN
426 'csrf_token': CSRF_TOKEN
427 };
427 };
428
428
429 // lock ALL buttons on preview
429 // lock ALL buttons on preview
430 self.setActionButtonsDisabled(true);
430 self.setActionButtonsDisabled(true);
431
431
432 $(self.previewBoxSelector).addClass('unloaded');
432 $(self.previewBoxSelector).addClass('unloaded');
433 $(self.previewBoxSelector).html(_gettext('Loading ...'));
433 $(self.previewBoxSelector).html(_gettext('Loading ...'));
434
434
435 $(self.editContainer).hide();
435 $(self.editContainer).hide();
436 $(self.previewContainer).show();
436 $(self.previewContainer).show();
437
437
438 // by default we reset state of comment preserving the text
438 // by default we reset state of comment preserving the text
439 var previewFailCallback = function(){
439 var previewFailCallback = function(){
440 self.resetCommentFormState(text)
440 self.resetCommentFormState(text)
441 };
441 };
442 self.submitAjaxPOST(
442 self.submitAjaxPOST(
443 self.previewUrl, postData, self.previewSuccessCallback,
443 self.previewUrl, postData, self.previewSuccessCallback,
444 previewFailCallback);
444 previewFailCallback);
445
445
446 $(self.previewButton).parent().addClass('active');
446 $(self.previewButton).parent().addClass('active');
447 $(self.editButton).parent().removeClass('active');
447 $(self.editButton).parent().removeClass('active');
448 });
448 });
449
449
450 $(this.submitForm).submit(function(e) {
450 $(this.submitForm).submit(function(e) {
451 e.preventDefault();
451 e.preventDefault();
452 var allowedToSubmit = self.isAllowedToSubmit();
452 var allowedToSubmit = self.isAllowedToSubmit();
453 if (!allowedToSubmit){
453 if (!allowedToSubmit){
454 return false;
454 return false;
455 }
455 }
456 self.handleFormSubmit();
456 self.handleFormSubmit();
457 });
457 });
458
458
459 }
459 }
460
460
461 return CommentForm;
461 return CommentForm;
462 });
462 });
463
463
464 /* comments controller */
464 /* comments controller */
465 var CommentsController = function() {
465 var CommentsController = function() {
466 var mainComment = '#text';
466 var mainComment = '#text';
467 var self = this;
467 var self = this;
468
468
469 this.cancelComment = function(node) {
469 this.cancelComment = function(node) {
470 var $node = $(node);
470 var $node = $(node);
471 var $td = $node.closest('td');
471 var $td = $node.closest('td');
472 $node.closest('.comment-inline-form').remove();
472 $node.closest('.comment-inline-form').remove();
473 return false;
473 return false;
474 };
474 };
475
475
476 this.getLineNumber = function(node) {
476 this.getLineNumber = function(node) {
477 var $node = $(node);
477 var $node = $(node);
478 return $node.closest('td').attr('data-line-number');
478 return $node.closest('td').attr('data-line-number');
479 };
479 };
480
480
481 this.scrollToComment = function(node, offset, outdated) {
481 this.scrollToComment = function(node, offset, outdated) {
482 if (offset === undefined) {
482 if (offset === undefined) {
483 offset = 0;
483 offset = 0;
484 }
484 }
485 var outdated = outdated || false;
485 var outdated = outdated || false;
486 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
486 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
487
487
488 if (!node) {
488 if (!node) {
489 node = $('.comment-selected');
489 node = $('.comment-selected');
490 if (!node.length) {
490 if (!node.length) {
491 node = $('comment-current')
491 node = $('comment-current')
492 }
492 }
493 }
493 }
494 $wrapper = $(node).closest('div.comment');
494 $wrapper = $(node).closest('div.comment');
495 $comment = $(node).closest(klass);
495 $comment = $(node).closest(klass);
496 $comments = $(klass);
496 $comments = $(klass);
497
497
498 // show hidden comment when referenced.
498 // show hidden comment when referenced.
499 if (!$wrapper.is(':visible')){
499 if (!$wrapper.is(':visible')){
500 $wrapper.show();
500 $wrapper.show();
501 }
501 }
502
502
503 $('.comment-selected').removeClass('comment-selected');
503 $('.comment-selected').removeClass('comment-selected');
504
504
505 var nextIdx = $(klass).index($comment) + offset;
505 var nextIdx = $(klass).index($comment) + offset;
506 if (nextIdx >= $comments.length) {
506 if (nextIdx >= $comments.length) {
507 nextIdx = 0;
507 nextIdx = 0;
508 }
508 }
509 var $next = $(klass).eq(nextIdx);
509 var $next = $(klass).eq(nextIdx);
510
510
511 var $cb = $next.closest('.cb');
511 var $cb = $next.closest('.cb');
512 $cb.removeClass('cb-collapsed');
512 $cb.removeClass('cb-collapsed');
513
513
514 var $filediffCollapseState = $cb.closest('.filediff').prev();
514 var $filediffCollapseState = $cb.closest('.filediff').prev();
515 $filediffCollapseState.prop('checked', false);
515 $filediffCollapseState.prop('checked', false);
516 $next.addClass('comment-selected');
516 $next.addClass('comment-selected');
517 scrollToElement($next);
517 scrollToElement($next);
518 return false;
518 return false;
519 };
519 };
520
520
521 this.nextComment = function(node) {
521 this.nextComment = function(node) {
522 return self.scrollToComment(node, 1);
522 return self.scrollToComment(node, 1);
523 };
523 };
524
524
525 this.prevComment = function(node) {
525 this.prevComment = function(node) {
526 return self.scrollToComment(node, -1);
526 return self.scrollToComment(node, -1);
527 };
527 };
528
528
529 this.nextOutdatedComment = function(node) {
529 this.nextOutdatedComment = function(node) {
530 return self.scrollToComment(node, 1, true);
530 return self.scrollToComment(node, 1, true);
531 };
531 };
532
532
533 this.prevOutdatedComment = function(node) {
533 this.prevOutdatedComment = function(node) {
534 return self.scrollToComment(node, -1, true);
534 return self.scrollToComment(node, -1, true);
535 };
535 };
536
536
537 this.deleteComment = function(node) {
537 this.deleteComment = function(node) {
538 if (!confirm(_gettext('Delete this comment?'))) {
538 if (!confirm(_gettext('Delete this comment?'))) {
539 return false;
539 return false;
540 }
540 }
541 var $node = $(node);
541 var $node = $(node);
542 var $td = $node.closest('td');
542 var $td = $node.closest('td');
543 var $comment = $node.closest('.comment');
543 var $comment = $node.closest('.comment');
544 var comment_id = $comment.attr('data-comment-id');
544 var comment_id = $comment.attr('data-comment-id');
545 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
545 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
546 var postData = {
546 var postData = {
547 '_method': 'delete',
547 '_method': 'delete',
548 'csrf_token': CSRF_TOKEN
548 'csrf_token': CSRF_TOKEN
549 };
549 };
550
550
551 $comment.addClass('comment-deleting');
551 $comment.addClass('comment-deleting');
552 $comment.hide('fast');
552 $comment.hide('fast');
553
553
554 var success = function(response) {
554 var success = function(response) {
555 $comment.remove();
555 $comment.remove();
556 return false;
556 return false;
557 };
557 };
558 var failure = function(data, textStatus, xhr) {
558 var failure = function(data, textStatus, xhr) {
559 alert("error processing request: " + textStatus);
559 alert("error processing request: " + textStatus);
560 $comment.show('fast');
560 $comment.show('fast');
561 $comment.removeClass('comment-deleting');
561 $comment.removeClass('comment-deleting');
562 return false;
562 return false;
563 };
563 };
564 ajaxPOST(url, postData, success, failure);
564 ajaxPOST(url, postData, success, failure);
565 };
565 };
566
566
567 this.toggleWideMode = function (node) {
567 this.toggleWideMode = function (node) {
568 if ($('#content').hasClass('wrapper')) {
568 if ($('#content').hasClass('wrapper')) {
569 $('#content').removeClass("wrapper");
569 $('#content').removeClass("wrapper");
570 $('#content').addClass("wide-mode-wrapper");
570 $('#content').addClass("wide-mode-wrapper");
571 $(node).addClass('btn-success');
571 $(node).addClass('btn-success');
572 } else {
572 } else {
573 $('#content').removeClass("wide-mode-wrapper");
573 $('#content').removeClass("wide-mode-wrapper");
574 $('#content').addClass("wrapper");
574 $('#content').addClass("wrapper");
575 $(node).removeClass('btn-success');
575 $(node).removeClass('btn-success');
576 }
576 }
577 return false;
577 return false;
578 };
578 };
579
579
580 this.toggleComments = function(node, show) {
580 this.toggleComments = function(node, show) {
581 var $filediff = $(node).closest('.filediff');
581 var $filediff = $(node).closest('.filediff');
582 if (show === true) {
582 if (show === true) {
583 $filediff.removeClass('hide-comments');
583 $filediff.removeClass('hide-comments');
584 } else if (show === false) {
584 } else if (show === false) {
585 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
585 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
586 $filediff.addClass('hide-comments');
586 $filediff.addClass('hide-comments');
587 } else {
587 } else {
588 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
588 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
589 $filediff.toggleClass('hide-comments');
589 $filediff.toggleClass('hide-comments');
590 }
590 }
591 return false;
591 return false;
592 };
592 };
593
593
594 this.toggleLineComments = function(node) {
594 this.toggleLineComments = function(node) {
595 self.toggleComments(node, true);
595 self.toggleComments(node, true);
596 var $node = $(node);
596 var $node = $(node);
597 $node.closest('tr').toggleClass('hide-line-comments');
597 $node.closest('tr').toggleClass('hide-line-comments');
598 };
598 };
599
599
600 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
600 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
601 var pullRequestId = templateContext.pull_request_data.pull_request_id;
601 var pullRequestId = templateContext.pull_request_data.pull_request_id;
602 var commitId = templateContext.commit_data.commit_id;
602 var commitId = templateContext.commit_data.commit_id;
603
603
604 var commentForm = new CommentForm(
604 var commentForm = new CommentForm(
605 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
605 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
606 var cm = commentForm.getCmInstance();
606 var cm = commentForm.getCmInstance();
607
607
608 if (resolvesCommentId){
608 if (resolvesCommentId){
609 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
609 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
610 }
610 }
611
611
612 setTimeout(function() {
612 setTimeout(function() {
613 // callbacks
613 // callbacks
614 if (cm !== undefined) {
614 if (cm !== undefined) {
615 commentForm.setPlaceholder(placeholderText);
615 commentForm.setPlaceholder(placeholderText);
616 if (commentForm.isInline()) {
616 if (commentForm.isInline()) {
617 cm.focus();
617 cm.focus();
618 cm.refresh();
618 cm.refresh();
619 }
619 }
620 }
620 }
621 }, 10);
621 }, 10);
622
622
623 // trigger scrolldown to the resolve comment, since it might be away
623 // trigger scrolldown to the resolve comment, since it might be away
624 // from the clicked
624 // from the clicked
625 if (resolvesCommentId){
625 if (resolvesCommentId){
626 var actionNode = $(commentForm.resolvesActionId).offset();
626 var actionNode = $(commentForm.resolvesActionId).offset();
627
627
628 setTimeout(function() {
628 setTimeout(function() {
629 if (actionNode) {
629 if (actionNode) {
630 $('body, html').animate({scrollTop: actionNode.top}, 10);
630 $('body, html').animate({scrollTop: actionNode.top}, 10);
631 }
631 }
632 }, 100);
632 }, 100);
633 }
633 }
634
634
635 return commentForm;
635 return commentForm;
636 };
636 };
637
637
638 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
638 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
639
639
640 var tmpl = $('#cb-comment-general-form-template').html();
640 var tmpl = $('#cb-comment-general-form-template').html();
641 tmpl = tmpl.format(null, 'general');
641 tmpl = tmpl.format(null, 'general');
642 var $form = $(tmpl);
642 var $form = $(tmpl);
643
643
644 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
644 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
645 var curForm = $formPlaceholder.find('form');
645 var curForm = $formPlaceholder.find('form');
646 if (curForm){
646 if (curForm){
647 curForm.remove();
647 curForm.remove();
648 }
648 }
649 $formPlaceholder.append($form);
649 $formPlaceholder.append($form);
650
650
651 var _form = $($form[0]);
651 var _form = $($form[0]);
652 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
652 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
653 var commentForm = this.createCommentForm(
653 var commentForm = this.createCommentForm(
654 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
654 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
655 commentForm.initStatusChangeSelector();
655 commentForm.initStatusChangeSelector();
656
656
657 return commentForm;
657 return commentForm;
658 };
658 };
659
659
660 this.createComment = function(node, resolutionComment) {
660 this.createComment = function(node, resolutionComment) {
661 var resolvesCommentId = resolutionComment || null;
661 var resolvesCommentId = resolutionComment || null;
662 var $node = $(node);
662 var $node = $(node);
663 var $td = $node.closest('td');
663 var $td = $node.closest('td');
664 var $form = $td.find('.comment-inline-form');
664 var $form = $td.find('.comment-inline-form');
665
665
666 if (!$form.length) {
666 if (!$form.length) {
667
667
668 var $filediff = $node.closest('.filediff');
668 var $filediff = $node.closest('.filediff');
669 $filediff.removeClass('hide-comments');
669 $filediff.removeClass('hide-comments');
670 var f_path = $filediff.attr('data-f-path');
670 var f_path = $filediff.attr('data-f-path');
671 var lineno = self.getLineNumber(node);
671 var lineno = self.getLineNumber(node);
672 // create a new HTML from template
672 // create a new HTML from template
673 var tmpl = $('#cb-comment-inline-form-template').html();
673 var tmpl = $('#cb-comment-inline-form-template').html();
674 tmpl = tmpl.format(f_path, lineno);
674 tmpl = tmpl.format(f_path, lineno);
675 $form = $(tmpl);
675 $form = $(tmpl);
676
676
677 var $comments = $td.find('.inline-comments');
677 var $comments = $td.find('.inline-comments');
678 if (!$comments.length) {
678 if (!$comments.length) {
679 $comments = $(
679 $comments = $(
680 $('#cb-comments-inline-container-template').html());
680 $('#cb-comments-inline-container-template').html());
681 $td.append($comments);
681 $td.append($comments);
682 }
682 }
683
683
684 $td.find('.cb-comment-add-button').before($form);
684 $td.find('.cb-comment-add-button').before($form);
685
685
686 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
686 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
687 var _form = $($form[0]).find('form');
687 var _form = $($form[0]).find('form');
688 var autocompleteActions = ['as_note', 'as_todo'];
688 var autocompleteActions = ['as_note', 'as_todo'];
689 var commentForm = this.createCommentForm(
689 var commentForm = this.createCommentForm(
690 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
690 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
691
691
692 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
692 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
693 form: _form,
693 form: _form,
694 parent: $td[0],
694 parent: $td[0],
695 lineno: lineno,
695 lineno: lineno,
696 f_path: f_path}
696 f_path: f_path}
697 );
697 );
698
698
699 // set a CUSTOM submit handler for inline comments.
699 // set a CUSTOM submit handler for inline comments.
700 commentForm.setHandleFormSubmit(function(o) {
700 commentForm.setHandleFormSubmit(function(o) {
701 var text = commentForm.cm.getValue();
701 var text = commentForm.cm.getValue();
702 var commentType = commentForm.getCommentType();
702 var commentType = commentForm.getCommentType();
703 var resolvesCommentId = commentForm.getResolvesId();
703 var resolvesCommentId = commentForm.getResolvesId();
704
704
705 if (text === "") {
705 if (text === "") {
706 return;
706 return;
707 }
707 }
708
708
709 if (lineno === undefined) {
709 if (lineno === undefined) {
710 alert('missing line !');
710 alert('missing line !');
711 return;
711 return;
712 }
712 }
713 if (f_path === undefined) {
713 if (f_path === undefined) {
714 alert('missing file path !');
714 alert('missing file path !');
715 return;
715 return;
716 }
716 }
717
717
718 var excludeCancelBtn = false;
718 var excludeCancelBtn = false;
719 var submitEvent = true;
719 var submitEvent = true;
720 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
720 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
721 commentForm.cm.setOption("readOnly", true);
721 commentForm.cm.setOption("readOnly", true);
722 var postData = {
722 var postData = {
723 'text': text,
723 'text': text,
724 'f_path': f_path,
724 'f_path': f_path,
725 'line': lineno,
725 'line': lineno,
726 'comment_type': commentType,
726 'comment_type': commentType,
727 'csrf_token': CSRF_TOKEN
727 'csrf_token': CSRF_TOKEN
728 };
728 };
729 if (resolvesCommentId){
729 if (resolvesCommentId){
730 postData['resolves_comment_id'] = resolvesCommentId;
730 postData['resolves_comment_id'] = resolvesCommentId;
731 }
731 }
732
732
733 var submitSuccessCallback = function(json_data) {
733 var submitSuccessCallback = function(json_data) {
734 $form.remove();
734 $form.remove();
735 try {
735 try {
736 var html = json_data.rendered_text;
736 var html = json_data.rendered_text;
737 var lineno = json_data.line_no;
737 var lineno = json_data.line_no;
738 var target_id = json_data.target_id;
738 var target_id = json_data.target_id;
739
739
740 $comments.find('.cb-comment-add-button').before(html);
740 $comments.find('.cb-comment-add-button').before(html);
741
741
742 //mark visually which comment was resolved
742 //mark visually which comment was resolved
743 if (resolvesCommentId) {
743 if (resolvesCommentId) {
744 commentForm.markCommentResolved(resolvesCommentId);
744 commentForm.markCommentResolved(resolvesCommentId);
745 }
745 }
746
746
747 // run global callback on submit
747 // run global callback on submit
748 commentForm.globalSubmitSuccessCallback();
748 commentForm.globalSubmitSuccessCallback();
749
749
750 } catch (e) {
750 } catch (e) {
751 console.error(e);
751 console.error(e);
752 }
752 }
753
753
754 // re trigger the linkification of next/prev navigation
754 // re trigger the linkification of next/prev navigation
755 linkifyComments($('.inline-comment-injected'));
755 linkifyComments($('.inline-comment-injected'));
756 timeagoActivate();
756 timeagoActivate();
757 commentForm.setActionButtonsDisabled(false);
757 commentForm.setActionButtonsDisabled(false);
758
758
759 };
759 };
760 var submitFailCallback = function(){
760 var submitFailCallback = function(){
761 commentForm.resetCommentFormState(text)
761 commentForm.resetCommentFormState(text)
762 };
762 };
763 commentForm.submitAjaxPOST(
763 commentForm.submitAjaxPOST(
764 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
764 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
765 });
765 });
766 }
766 }
767
767
768 $form.addClass('comment-inline-form-open');
768 $form.addClass('comment-inline-form-open');
769 };
769 };
770
770
771 this.createResolutionComment = function(commentId){
771 this.createResolutionComment = function(commentId){
772 // hide the trigger text
772 // hide the trigger text
773 $('#resolve-comment-{0}'.format(commentId)).hide();
773 $('#resolve-comment-{0}'.format(commentId)).hide();
774
774
775 var comment = $('#comment-'+commentId);
775 var comment = $('#comment-'+commentId);
776 var commentData = comment.data();
776 var commentData = comment.data();
777 if (commentData.commentInline) {
777 if (commentData.commentInline) {
778 this.createComment(comment, commentId)
778 this.createComment(comment, commentId)
779 } else {
779 } else {
780 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
780 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
781 }
781 }
782
782
783 return false;
783 return false;
784 };
784 };
785
785
786 this.submitResolution = function(commentId){
786 this.submitResolution = function(commentId){
787 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
787 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
788 var commentForm = form.get(0).CommentForm;
788 var commentForm = form.get(0).CommentForm;
789
789
790 var cm = commentForm.getCmInstance();
790 var cm = commentForm.getCmInstance();
791 var renderer = templateContext.visual.default_renderer;
791 var renderer = templateContext.visual.default_renderer;
792 if (renderer == 'rst'){
792 if (renderer == 'rst'){
793 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
793 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
794 } else if (renderer == 'markdown') {
794 } else if (renderer == 'markdown') {
795 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
795 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
796 } else {
796 } else {
797 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
797 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
798 }
798 }
799
799
800 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
800 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
801 form.submit();
801 form.submit();
802 return false;
802 return false;
803 };
803 };
804
804
805 this.renderInlineComments = function(file_comments) {
805 this.renderInlineComments = function(file_comments) {
806 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
806 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
807
807
808 for (var i = 0; i < file_comments.length; i++) {
808 for (var i = 0; i < file_comments.length; i++) {
809 var box = file_comments[i];
809 var box = file_comments[i];
810
810
811 var target_id = $(box).attr('target_id');
811 var target_id = $(box).attr('target_id');
812
812
813 // actually comments with line numbers
813 // actually comments with line numbers
814 var comments = box.children;
814 var comments = box.children;
815
815
816 for (var j = 0; j < comments.length; j++) {
816 for (var j = 0; j < comments.length; j++) {
817 var data = {
817 var data = {
818 'rendered_text': comments[j].outerHTML,
818 'rendered_text': comments[j].outerHTML,
819 'line_no': $(comments[j]).attr('line'),
819 'line_no': $(comments[j]).attr('line'),
820 'target_id': target_id
820 'target_id': target_id
821 };
821 };
822 }
822 }
823 }
823 }
824
824
825 // since order of injection is random, we're now re-iterating
825 // since order of injection is random, we're now re-iterating
826 // from correct order and filling in links
826 // from correct order and filling in links
827 linkifyComments($('.inline-comment-injected'));
827 linkifyComments($('.inline-comment-injected'));
828 firefoxAnchorFix();
828 firefoxAnchorFix();
829 };
829 };
830
830
831 };
831 };
@@ -1,605 +1,604 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 var prButtonLockChecks = {
20 var prButtonLockChecks = {
21 'compare': false,
21 'compare': false,
22 'reviewers': false
22 'reviewers': false
23 };
23 };
24
24
25 /**
25 /**
26 * lock button until all checks and loads are made. E.g reviewer calculation
26 * lock button until all checks and loads are made. E.g reviewer calculation
27 * should prevent from submitting a PR
27 * should prevent from submitting a PR
28 * @param lockEnabled
28 * @param lockEnabled
29 * @param msg
29 * @param msg
30 * @param scope
30 * @param scope
31 */
31 */
32 var prButtonLock = function(lockEnabled, msg, scope) {
32 var prButtonLock = function(lockEnabled, msg, scope) {
33 scope = scope || 'all';
33 scope = scope || 'all';
34 if (scope == 'all'){
34 if (scope == 'all'){
35 prButtonLockChecks['compare'] = !lockEnabled;
35 prButtonLockChecks['compare'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 } else if (scope == 'compare') {
37 } else if (scope == 'compare') {
38 prButtonLockChecks['compare'] = !lockEnabled;
38 prButtonLockChecks['compare'] = !lockEnabled;
39 } else if (scope == 'reviewers'){
39 } else if (scope == 'reviewers'){
40 prButtonLockChecks['reviewers'] = !lockEnabled;
40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 }
41 }
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 if (lockEnabled) {
43 if (lockEnabled) {
44 $('#save').attr('disabled', 'disabled');
44 $('#save').attr('disabled', 'disabled');
45 }
45 }
46 else if (checksMeet) {
46 else if (checksMeet) {
47 $('#save').removeAttr('disabled');
47 $('#save').removeAttr('disabled');
48 }
48 }
49
49
50 if (msg) {
50 if (msg) {
51 $('#pr_open_message').html(msg);
51 $('#pr_open_message').html(msg);
52 }
52 }
53 };
53 };
54
54
55
55
56 /**
56 /**
57 Generate Title and Description for a PullRequest.
57 Generate Title and Description for a PullRequest.
58 In case of 1 commits, the title and description is that one commit
58 In case of 1 commits, the title and description is that one commit
59 in case of multiple commits, we iterate on them with max N number of commits,
59 in case of multiple commits, we iterate on them with max N number of commits,
60 and build description in a form
60 and build description in a form
61 - commitN
61 - commitN
62 - commitN+1
62 - commitN+1
63 ...
63 ...
64
64
65 Title is then constructed from branch names, or other references,
65 Title is then constructed from branch names, or other references,
66 replacing '-' and '_' into spaces
66 replacing '-' and '_' into spaces
67
67
68 * @param sourceRef
68 * @param sourceRef
69 * @param elements
69 * @param elements
70 * @param limit
70 * @param limit
71 * @returns {*[]}
71 * @returns {*[]}
72 */
72 */
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 var title = '';
74 var title = '';
75 var desc = '';
75 var desc = '';
76
76
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 });
80 });
81 // only 1 commit, use commit message as title
81 // only 1 commit, use commit message as title
82 if (elements.length === 1) {
82 if (elements.length === 1) {
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 }
84 }
85 else {
85 else {
86 // use reference name
86 // use reference name
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 }
88 }
89
89
90 return [title, desc]
90 return [title, desc]
91 };
91 };
92
92
93
93
94
94
95 ReviewersController = function () {
95 ReviewersController = function () {
96 var self = this;
96 var self = this;
97 this.$reviewRulesContainer = $('#review_rules');
97 this.$reviewRulesContainer = $('#review_rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 this.forbidReviewUsers = undefined;
99 this.forbidReviewUsers = undefined;
100 this.$reviewMembers = $('#review_members');
100 this.$reviewMembers = $('#review_members');
101 this.currentRequest = null;
101 this.currentRequest = null;
102
102
103 this.defaultForbidReviewUsers = function() {
103 this.defaultForbidReviewUsers = function() {
104 return [
104 return [
105 {'username': 'default',
105 {'username': 'default',
106 'user_id': templateContext.default_user.user_id}
106 'user_id': templateContext.default_user.user_id}
107 ];
107 ];
108 };
108 };
109
109
110 this.hideReviewRules = function() {
110 this.hideReviewRules = function() {
111 self.$reviewRulesContainer.hide();
111 self.$reviewRulesContainer.hide();
112 };
112 };
113
113
114 this.showReviewRules = function() {
114 this.showReviewRules = function() {
115 self.$reviewRulesContainer.show();
115 self.$reviewRulesContainer.show();
116 };
116 };
117
117
118 this.addRule = function(ruleText) {
118 this.addRule = function(ruleText) {
119 self.showReviewRules();
119 self.showReviewRules();
120 return '<div>- {0}</div>'.format(ruleText)
120 return '<div>- {0}</div>'.format(ruleText)
121 };
121 };
122
122
123 this.loadReviewRules = function(data) {
123 this.loadReviewRules = function(data) {
124 // reset forbidden Users
124 // reset forbidden Users
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126
126
127 // reset state of review rules
127 // reset state of review rules
128 self.$rulesList.html('');
128 self.$rulesList.html('');
129
129
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 // default rule, case for older repo that don't have any rules stored
131 // default rule, case for older repo that don't have any rules stored
132 self.$rulesList.append(
132 self.$rulesList.append(
133 self.addRule(
133 self.addRule(
134 _gettext('All reviewers must vote.'))
134 _gettext('All reviewers must vote.'))
135 );
135 );
136 return self.forbidReviewUsers
136 return self.forbidReviewUsers
137 }
137 }
138
138
139 if (data.rules.voting !== undefined) {
139 if (data.rules.voting !== undefined) {
140 if (data.rules.voting < 0){
140 if (data.rules.voting < 0){
141 self.$rulesList.append(
141 self.$rulesList.append(
142 self.addRule(
142 self.addRule(
143 _gettext('All reviewers must vote.'))
143 _gettext('All reviewers must vote.'))
144 )
144 )
145 } else if (data.rules.voting === 1) {
145 } else if (data.rules.voting === 1) {
146 self.$rulesList.append(
146 self.$rulesList.append(
147 self.addRule(
147 self.addRule(
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 )
149 )
150
150
151 } else {
151 } else {
152 self.$rulesList.append(
152 self.$rulesList.append(
153 self.addRule(
153 self.addRule(
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 )
155 )
156 }
156 }
157 }
157 }
158 if (data.rules.use_code_authors_for_review) {
158 if (data.rules.use_code_authors_for_review) {
159 self.$rulesList.append(
159 self.$rulesList.append(
160 self.addRule(
160 self.addRule(
161 _gettext('Reviewers picked from source code changes.'))
161 _gettext('Reviewers picked from source code changes.'))
162 )
162 )
163 }
163 }
164 if (data.rules.forbid_adding_reviewers) {
164 if (data.rules.forbid_adding_reviewers) {
165 $('#add_reviewer_input').remove();
165 $('#add_reviewer_input').remove();
166 self.$rulesList.append(
166 self.$rulesList.append(
167 self.addRule(
167 self.addRule(
168 _gettext('Adding new reviewers is forbidden.'))
168 _gettext('Adding new reviewers is forbidden.'))
169 )
169 )
170 }
170 }
171 if (data.rules.forbid_author_to_review) {
171 if (data.rules.forbid_author_to_review) {
172 self.forbidReviewUsers.push(data.rules_data.pr_author);
172 self.forbidReviewUsers.push(data.rules_data.pr_author);
173 self.$rulesList.append(
173 self.$rulesList.append(
174 self.addRule(
174 self.addRule(
175 _gettext('Author is not allowed to be a reviewer.'))
175 _gettext('Author is not allowed to be a reviewer.'))
176 )
176 )
177 }
177 }
178 if (data.rules.forbid_commit_author_to_review) {
178 if (data.rules.forbid_commit_author_to_review) {
179
179
180 if (data.rules_data.forbidden_users) {
180 if (data.rules_data.forbidden_users) {
181 $.each(data.rules_data.forbidden_users, function(index, member_data) {
181 $.each(data.rules_data.forbidden_users, function(index, member_data) {
182 self.forbidReviewUsers.push(member_data)
182 self.forbidReviewUsers.push(member_data)
183 });
183 });
184
184
185 }
185 }
186
186
187 self.$rulesList.append(
187 self.$rulesList.append(
188 self.addRule(
188 self.addRule(
189 _gettext('Commit Authors are not allowed to be a reviewer.'))
189 _gettext('Commit Authors are not allowed to be a reviewer.'))
190 )
190 )
191 }
191 }
192
192
193 return self.forbidReviewUsers
193 return self.forbidReviewUsers
194 };
194 };
195
195
196 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
196 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
197
197
198 if (self.currentRequest) {
198 if (self.currentRequest) {
199 // make sure we cleanup old running requests before triggering this
199 // make sure we cleanup old running requests before triggering this
200 // again
200 // again
201 self.currentRequest.abort();
201 self.currentRequest.abort();
202 }
202 }
203
203
204 $('.calculate-reviewers').show();
204 $('.calculate-reviewers').show();
205 // reset reviewer members
205 // reset reviewer members
206 self.$reviewMembers.empty();
206 self.$reviewMembers.empty();
207
207
208 prButtonLock(true, null, 'reviewers');
208 prButtonLock(true, null, 'reviewers');
209 $('#user').hide(); // hide user autocomplete before load
209 $('#user').hide(); // hide user autocomplete before load
210
210
211 var url = pyroutes.url('repo_default_reviewers_data',
211 var url = pyroutes.url('repo_default_reviewers_data',
212 {
212 {
213 'repo_name': templateContext.repo_name,
213 'repo_name': templateContext.repo_name,
214 'source_repo': sourceRepo,
214 'source_repo': sourceRepo,
215 'source_ref': sourceRef[2],
215 'source_ref': sourceRef[2],
216 'target_repo': targetRepo,
216 'target_repo': targetRepo,
217 'target_ref': targetRef[2]
217 'target_ref': targetRef[2]
218 });
218 });
219
219
220 self.currentRequest = $.get(url)
220 self.currentRequest = $.get(url)
221 .done(function(data) {
221 .done(function(data) {
222 self.currentRequest = null;
222 self.currentRequest = null;
223
223
224 // review rules
224 // review rules
225 self.loadReviewRules(data);
225 self.loadReviewRules(data);
226
226
227 for (var i = 0; i < data.reviewers.length; i++) {
227 for (var i = 0; i < data.reviewers.length; i++) {
228 var reviewer = data.reviewers[i];
228 var reviewer = data.reviewers[i];
229 self.addReviewMember(
229 self.addReviewMember(
230 reviewer.user_id, reviewer.first_name,
230 reviewer.user_id, reviewer.first_name,
231 reviewer.last_name, reviewer.username,
231 reviewer.last_name, reviewer.username,
232 reviewer.gravatar_link, reviewer.reasons,
232 reviewer.gravatar_link, reviewer.reasons,
233 reviewer.mandatory);
233 reviewer.mandatory);
234 }
234 }
235 $('.calculate-reviewers').hide();
235 $('.calculate-reviewers').hide();
236 prButtonLock(false, null, 'reviewers');
236 prButtonLock(false, null, 'reviewers');
237 $('#user').show(); // show user autocomplete after load
237 $('#user').show(); // show user autocomplete after load
238 });
238 });
239 };
239 };
240
240
241 // check those, refactor
241 // check those, refactor
242 this.removeReviewMember = function(reviewer_id, mark_delete) {
242 this.removeReviewMember = function(reviewer_id, mark_delete) {
243 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
243 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
244
244
245 if(typeof(mark_delete) === undefined){
245 if(typeof(mark_delete) === undefined){
246 mark_delete = false;
246 mark_delete = false;
247 }
247 }
248
248
249 if(mark_delete === true){
249 if(mark_delete === true){
250 if (reviewer){
250 if (reviewer){
251 // now delete the input
251 // now delete the input
252 $('#reviewer_{0} input'.format(reviewer_id)).remove();
252 $('#reviewer_{0} input'.format(reviewer_id)).remove();
253 // mark as to-delete
253 // mark as to-delete
254 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
254 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
255 obj.addClass('to-delete');
255 obj.addClass('to-delete');
256 obj.css({"text-decoration":"line-through", "opacity": 0.5});
256 obj.css({"text-decoration":"line-through", "opacity": 0.5});
257 }
257 }
258 }
258 }
259 else{
259 else{
260 $('#reviewer_{0}'.format(reviewer_id)).remove();
260 $('#reviewer_{0}'.format(reviewer_id)).remove();
261 }
261 }
262 };
262 };
263
263
264 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
264 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
265 var members = self.$reviewMembers.get(0);
265 var members = self.$reviewMembers.get(0);
266 var reasons_html = '';
266 var reasons_html = '';
267 var reasons_inputs = '';
267 var reasons_inputs = '';
268 var reasons = reasons || [];
268 var reasons = reasons || [];
269 var mandatory = mandatory || false;
269 var mandatory = mandatory || false;
270
270
271 if (reasons) {
271 if (reasons) {
272 for (var i = 0; i < reasons.length; i++) {
272 for (var i = 0; i < reasons.length; i++) {
273 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
273 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
274 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
274 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
275 }
275 }
276 }
276 }
277 var tmpl = '' +
277 var tmpl = '' +
278 '<li id="reviewer_{2}" class="reviewer_entry">'+
278 '<li id="reviewer_{2}" class="reviewer_entry">'+
279 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
279 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
280 '<div class="reviewer_status">'+
280 '<div class="reviewer_status">'+
281 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
281 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
282 '</div>'+
282 '</div>'+
283 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
283 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
284 '<span class="reviewer_name user">{1}</span>'+
284 '<span class="reviewer_name user">{1}</span>'+
285 reasons_html +
285 reasons_html +
286 '<input type="hidden" name="user_id" value="{2}">'+
286 '<input type="hidden" name="user_id" value="{2}">'+
287 '<input type="hidden" name="__start__" value="reasons:sequence">'+
287 '<input type="hidden" name="__start__" value="reasons:sequence">'+
288 '{3}'+
288 '{3}'+
289 '<input type="hidden" name="__end__" value="reasons:sequence">';
289 '<input type="hidden" name="__end__" value="reasons:sequence">';
290
290
291 if (mandatory) {
291 if (mandatory) {
292 tmpl += ''+
292 tmpl += ''+
293 '<div class="reviewer_member_mandatory_remove">' +
293 '<div class="reviewer_member_mandatory_remove">' +
294 '<i class="icon-remove-sign"></i>'+
294 '<i class="icon-remove-sign"></i>'+
295 '</div>' +
295 '</div>' +
296 '<input type="hidden" name="mandatory" value="true">'+
296 '<input type="hidden" name="mandatory" value="true">'+
297 '<div class="reviewer_member_mandatory">' +
297 '<div class="reviewer_member_mandatory">' +
298 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
298 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
299 '</div>';
299 '</div>';
300
300
301 } else {
301 } else {
302 tmpl += ''+
302 tmpl += ''+
303 '<input type="hidden" name="mandatory" value="false">'+
303 '<input type="hidden" name="mandatory" value="false">'+
304 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
304 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
305 '<i class="icon-remove-sign"></i>'+
305 '<i class="icon-remove-sign"></i>'+
306 '</div>';
306 '</div>';
307 }
307 }
308 // continue template
308 // continue template
309 tmpl += ''+
309 tmpl += ''+
310 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
310 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
311 '</li>' ;
311 '</li>' ;
312
312
313 var displayname = "{0} ({1} {2})".format(
313 var displayname = "{0} ({1} {2})".format(
314 nname, escapeHtml(fname), escapeHtml(lname));
314 nname, escapeHtml(fname), escapeHtml(lname));
315 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
315 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
316 // check if we don't have this ID already in
316 // check if we don't have this ID already in
317 var ids = [];
317 var ids = [];
318 var _els = self.$reviewMembers.find('li').toArray();
318 var _els = self.$reviewMembers.find('li').toArray();
319 for (el in _els){
319 for (el in _els){
320 ids.push(_els[el].id)
320 ids.push(_els[el].id)
321 }
321 }
322
322
323 var userAllowedReview = function(userId) {
323 var userAllowedReview = function(userId) {
324 var allowed = true;
324 var allowed = true;
325 $.each(self.forbidReviewUsers, function(index, member_data) {
325 $.each(self.forbidReviewUsers, function(index, member_data) {
326 if (parseInt(userId) === member_data['user_id']) {
326 if (parseInt(userId) === member_data['user_id']) {
327 allowed = false;
327 allowed = false;
328 return false // breaks the loop
328 return false // breaks the loop
329 }
329 }
330 });
330 });
331 return allowed
331 return allowed
332 };
332 };
333
333
334 var userAllowed = userAllowedReview(id);
334 var userAllowed = userAllowedReview(id);
335 if (!userAllowed){
335 if (!userAllowed){
336 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
336 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
337 }
337 }
338 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
338 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
339
339
340 if(shouldAdd) {
340 if(shouldAdd) {
341 // only add if it's not there
341 // only add if it's not there
342 members.innerHTML += element;
342 members.innerHTML += element;
343 }
343 }
344
344
345 };
345 };
346
346
347 this.updateReviewers = function(repo_name, pull_request_id){
347 this.updateReviewers = function(repo_name, pull_request_id){
348 var postData = '_method=put&' + $('#reviewers input').serialize();
348 var postData = '_method=put&' + $('#reviewers input').serialize();
349 _updatePullRequest(repo_name, pull_request_id, postData);
349 _updatePullRequest(repo_name, pull_request_id, postData);
350 };
350 };
351
351
352 };
352 };
353
353
354
354
355 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
355 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
356 var url = pyroutes.url(
356 var url = pyroutes.url(
357 'pullrequest_update',
357 'pullrequest_update',
358 {"repo_name": repo_name, "pull_request_id": pull_request_id});
358 {"repo_name": repo_name, "pull_request_id": pull_request_id});
359 if (typeof postData === 'string' ) {
359 if (typeof postData === 'string' ) {
360 postData += '&csrf_token=' + CSRF_TOKEN;
360 postData += '&csrf_token=' + CSRF_TOKEN;
361 } else {
361 } else {
362 postData.csrf_token = CSRF_TOKEN;
362 postData.csrf_token = CSRF_TOKEN;
363 }
363 }
364 var success = function(o) {
364 var success = function(o) {
365 window.location.reload();
365 window.location.reload();
366 };
366 };
367 ajaxPOST(url, postData, success);
367 ajaxPOST(url, postData, success);
368 };
368 };
369
369
370 /**
370 /**
371 * PULL REQUEST update commits
371 * PULL REQUEST update commits
372 */
372 */
373 var updateCommits = function(repo_name, pull_request_id) {
373 var updateCommits = function(repo_name, pull_request_id) {
374 var postData = {
374 var postData = {
375 '_method': 'put',
375 '_method': 'put',
376 'update_commits': true};
376 'update_commits': true};
377 _updatePullRequest(repo_name, pull_request_id, postData);
377 _updatePullRequest(repo_name, pull_request_id, postData);
378 };
378 };
379
379
380
380
381 /**
381 /**
382 * PULL REQUEST edit info
382 * PULL REQUEST edit info
383 */
383 */
384 var editPullRequest = function(repo_name, pull_request_id, title, description) {
384 var editPullRequest = function(repo_name, pull_request_id, title, description) {
385 var url = pyroutes.url(
385 var url = pyroutes.url(
386 'pullrequest_update',
386 'pullrequest_update',
387 {"repo_name": repo_name, "pull_request_id": pull_request_id});
387 {"repo_name": repo_name, "pull_request_id": pull_request_id});
388
388
389 var postData = {
389 var postData = {
390 '_method': 'put',
391 'title': title,
390 'title': title,
392 'description': description,
391 'description': description,
393 'edit_pull_request': true,
392 'edit_pull_request': true,
394 'csrf_token': CSRF_TOKEN
393 'csrf_token': CSRF_TOKEN
395 };
394 };
396 var success = function(o) {
395 var success = function(o) {
397 window.location.reload();
396 window.location.reload();
398 };
397 };
399 ajaxPOST(url, postData, success);
398 ajaxPOST(url, postData, success);
400 };
399 };
401
400
402 var initPullRequestsCodeMirror = function (textAreaId) {
401 var initPullRequestsCodeMirror = function (textAreaId) {
403 var ta = $(textAreaId).get(0);
402 var ta = $(textAreaId).get(0);
404 var initialHeight = '100px';
403 var initialHeight = '100px';
405
404
406 // default options
405 // default options
407 var codeMirrorOptions = {
406 var codeMirrorOptions = {
408 mode: "text",
407 mode: "text",
409 lineNumbers: false,
408 lineNumbers: false,
410 indentUnit: 4,
409 indentUnit: 4,
411 theme: 'rc-input'
410 theme: 'rc-input'
412 };
411 };
413
412
414 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
413 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
415 // marker for manually set description
414 // marker for manually set description
416 codeMirrorInstance._userDefinedDesc = false;
415 codeMirrorInstance._userDefinedDesc = false;
417 codeMirrorInstance.setSize(null, initialHeight);
416 codeMirrorInstance.setSize(null, initialHeight);
418 codeMirrorInstance.on("change", function(instance, changeObj) {
417 codeMirrorInstance.on("change", function(instance, changeObj) {
419 var height = initialHeight;
418 var height = initialHeight;
420 var lines = instance.lineCount();
419 var lines = instance.lineCount();
421 if (lines > 6 && lines < 20) {
420 if (lines > 6 && lines < 20) {
422 height = "auto"
421 height = "auto"
423 }
422 }
424 else if (lines >= 20) {
423 else if (lines >= 20) {
425 height = 20 * 15;
424 height = 20 * 15;
426 }
425 }
427 instance.setSize(null, height);
426 instance.setSize(null, height);
428
427
429 // detect if the change was trigger by auto desc, or user input
428 // detect if the change was trigger by auto desc, or user input
430 changeOrigin = changeObj.origin;
429 changeOrigin = changeObj.origin;
431
430
432 if (changeOrigin === "setValue") {
431 if (changeOrigin === "setValue") {
433 cmLog.debug('Change triggered by setValue');
432 cmLog.debug('Change triggered by setValue');
434 }
433 }
435 else {
434 else {
436 cmLog.debug('user triggered change !');
435 cmLog.debug('user triggered change !');
437 // set special marker to indicate user has created an input.
436 // set special marker to indicate user has created an input.
438 instance._userDefinedDesc = true;
437 instance._userDefinedDesc = true;
439 }
438 }
440
439
441 });
440 });
442
441
443 return codeMirrorInstance
442 return codeMirrorInstance
444 };
443 };
445
444
446 /**
445 /**
447 * Reviewer autocomplete
446 * Reviewer autocomplete
448 */
447 */
449 var ReviewerAutoComplete = function(inputId) {
448 var ReviewerAutoComplete = function(inputId) {
450 $(inputId).autocomplete({
449 $(inputId).autocomplete({
451 serviceUrl: pyroutes.url('user_autocomplete_data'),
450 serviceUrl: pyroutes.url('user_autocomplete_data'),
452 minChars:2,
451 minChars:2,
453 maxHeight:400,
452 maxHeight:400,
454 deferRequestBy: 300, //miliseconds
453 deferRequestBy: 300, //miliseconds
455 showNoSuggestionNotice: true,
454 showNoSuggestionNotice: true,
456 tabDisabled: true,
455 tabDisabled: true,
457 autoSelectFirst: true,
456 autoSelectFirst: true,
458 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
457 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
459 formatResult: autocompleteFormatResult,
458 formatResult: autocompleteFormatResult,
460 lookupFilter: autocompleteFilterResult,
459 lookupFilter: autocompleteFilterResult,
461 onSelect: function(element, data) {
460 onSelect: function(element, data) {
462
461
463 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
462 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
464 if (data.value_type == 'user_group') {
463 if (data.value_type == 'user_group') {
465 reasons.push(_gettext('member of "{0}"').format(data.value_display));
464 reasons.push(_gettext('member of "{0}"').format(data.value_display));
466
465
467 $.each(data.members, function(index, member_data) {
466 $.each(data.members, function(index, member_data) {
468 reviewersController.addReviewMember(
467 reviewersController.addReviewMember(
469 member_data.id, member_data.first_name, member_data.last_name,
468 member_data.id, member_data.first_name, member_data.last_name,
470 member_data.username, member_data.icon_link, reasons);
469 member_data.username, member_data.icon_link, reasons);
471 })
470 })
472
471
473 } else {
472 } else {
474 reviewersController.addReviewMember(
473 reviewersController.addReviewMember(
475 data.id, data.first_name, data.last_name,
474 data.id, data.first_name, data.last_name,
476 data.username, data.icon_link, reasons);
475 data.username, data.icon_link, reasons);
477 }
476 }
478
477
479 $(inputId).val('');
478 $(inputId).val('');
480 }
479 }
481 });
480 });
482 };
481 };
483
482
484
483
485 VersionController = function () {
484 VersionController = function () {
486 var self = this;
485 var self = this;
487 this.$verSource = $('input[name=ver_source]');
486 this.$verSource = $('input[name=ver_source]');
488 this.$verTarget = $('input[name=ver_target]');
487 this.$verTarget = $('input[name=ver_target]');
489 this.$showVersionDiff = $('#show-version-diff');
488 this.$showVersionDiff = $('#show-version-diff');
490
489
491 this.adjustRadioSelectors = function (curNode) {
490 this.adjustRadioSelectors = function (curNode) {
492 var getVal = function (item) {
491 var getVal = function (item) {
493 if (item == 'latest') {
492 if (item == 'latest') {
494 return Number.MAX_SAFE_INTEGER
493 return Number.MAX_SAFE_INTEGER
495 }
494 }
496 else {
495 else {
497 return parseInt(item)
496 return parseInt(item)
498 }
497 }
499 };
498 };
500
499
501 var curVal = getVal($(curNode).val());
500 var curVal = getVal($(curNode).val());
502 var cleared = false;
501 var cleared = false;
503
502
504 $.each(self.$verSource, function (index, value) {
503 $.each(self.$verSource, function (index, value) {
505 var elVal = getVal($(value).val());
504 var elVal = getVal($(value).val());
506
505
507 if (elVal > curVal) {
506 if (elVal > curVal) {
508 if ($(value).is(':checked')) {
507 if ($(value).is(':checked')) {
509 cleared = true;
508 cleared = true;
510 }
509 }
511 $(value).attr('disabled', 'disabled');
510 $(value).attr('disabled', 'disabled');
512 $(value).removeAttr('checked');
511 $(value).removeAttr('checked');
513 $(value).css({'opacity': 0.1});
512 $(value).css({'opacity': 0.1});
514 }
513 }
515 else {
514 else {
516 $(value).css({'opacity': 1});
515 $(value).css({'opacity': 1});
517 $(value).removeAttr('disabled');
516 $(value).removeAttr('disabled');
518 }
517 }
519 });
518 });
520
519
521 if (cleared) {
520 if (cleared) {
522 // if we unchecked an active, set the next one to same loc.
521 // if we unchecked an active, set the next one to same loc.
523 $(this.$verSource).filter('[value={0}]'.format(
522 $(this.$verSource).filter('[value={0}]'.format(
524 curVal)).attr('checked', 'checked');
523 curVal)).attr('checked', 'checked');
525 }
524 }
526
525
527 self.setLockAction(false,
526 self.setLockAction(false,
528 $(curNode).data('verPos'),
527 $(curNode).data('verPos'),
529 $(this.$verSource).filter(':checked').data('verPos')
528 $(this.$verSource).filter(':checked').data('verPos')
530 );
529 );
531 };
530 };
532
531
533
532
534 this.attachVersionListener = function () {
533 this.attachVersionListener = function () {
535 self.$verTarget.change(function (e) {
534 self.$verTarget.change(function (e) {
536 self.adjustRadioSelectors(this)
535 self.adjustRadioSelectors(this)
537 });
536 });
538 self.$verSource.change(function (e) {
537 self.$verSource.change(function (e) {
539 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
538 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
540 });
539 });
541 };
540 };
542
541
543 this.init = function () {
542 this.init = function () {
544
543
545 var curNode = self.$verTarget.filter(':checked');
544 var curNode = self.$verTarget.filter(':checked');
546 self.adjustRadioSelectors(curNode);
545 self.adjustRadioSelectors(curNode);
547 self.setLockAction(true);
546 self.setLockAction(true);
548 self.attachVersionListener();
547 self.attachVersionListener();
549
548
550 };
549 };
551
550
552 this.setLockAction = function (state, selectedVersion, otherVersion) {
551 this.setLockAction = function (state, selectedVersion, otherVersion) {
553 var $showVersionDiff = this.$showVersionDiff;
552 var $showVersionDiff = this.$showVersionDiff;
554
553
555 if (state) {
554 if (state) {
556 $showVersionDiff.attr('disabled', 'disabled');
555 $showVersionDiff.attr('disabled', 'disabled');
557 $showVersionDiff.addClass('disabled');
556 $showVersionDiff.addClass('disabled');
558 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
557 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
559 }
558 }
560 else {
559 else {
561 $showVersionDiff.removeAttr('disabled');
560 $showVersionDiff.removeAttr('disabled');
562 $showVersionDiff.removeClass('disabled');
561 $showVersionDiff.removeClass('disabled');
563
562
564 if (selectedVersion == otherVersion) {
563 if (selectedVersion == otherVersion) {
565 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
564 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
566 } else {
565 } else {
567 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
566 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
568 }
567 }
569 }
568 }
570
569
571 };
570 };
572
571
573 this.showVersionDiff = function () {
572 this.showVersionDiff = function () {
574 var target = self.$verTarget.filter(':checked');
573 var target = self.$verTarget.filter(':checked');
575 var source = self.$verSource.filter(':checked');
574 var source = self.$verSource.filter(':checked');
576
575
577 if (target.val() && source.val()) {
576 if (target.val() && source.val()) {
578 var params = {
577 var params = {
579 'pull_request_id': templateContext.pull_request_data.pull_request_id,
578 'pull_request_id': templateContext.pull_request_data.pull_request_id,
580 'repo_name': templateContext.repo_name,
579 'repo_name': templateContext.repo_name,
581 'version': target.val(),
580 'version': target.val(),
582 'from_version': source.val()
581 'from_version': source.val()
583 };
582 };
584 window.location = pyroutes.url('pullrequest_show', params)
583 window.location = pyroutes.url('pullrequest_show', params)
585 }
584 }
586
585
587 return false;
586 return false;
588 };
587 };
589
588
590 this.toggleVersionView = function (elem) {
589 this.toggleVersionView = function (elem) {
591
590
592 if (this.$showVersionDiff.is(':visible')) {
591 if (this.$showVersionDiff.is(':visible')) {
593 $('.version-pr').hide();
592 $('.version-pr').hide();
594 this.$showVersionDiff.hide();
593 this.$showVersionDiff.hide();
595 $(elem).html($(elem).data('toggleOn'))
594 $(elem).html($(elem).data('toggleOn'))
596 } else {
595 } else {
597 $('.version-pr').show();
596 $('.version-pr').show();
598 this.$showVersionDiff.show();
597 this.$showVersionDiff.show();
599 $(elem).html($(elem).data('toggleOff'))
598 $(elem).html($(elem).data('toggleOff'))
600 }
599 }
601
600
602 return false
601 return false
603 }
602 }
604
603
605 }; No newline at end of file
604 };
@@ -1,609 +1,609 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="root.mako"/>
2 <%inherit file="root.mako"/>
3
3
4 <div class="outerwrapper">
4 <div class="outerwrapper">
5 <!-- HEADER -->
5 <!-- HEADER -->
6 <div class="header">
6 <div class="header">
7 <div id="header-inner" class="wrapper">
7 <div id="header-inner" class="wrapper">
8 <div id="logo">
8 <div id="logo">
9 <div class="logo-wrapper">
9 <div class="logo-wrapper">
10 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
10 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
11 </div>
11 </div>
12 %if c.rhodecode_name:
12 %if c.rhodecode_name:
13 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
13 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
14 %endif
14 %endif
15 </div>
15 </div>
16 <!-- MENU BAR NAV -->
16 <!-- MENU BAR NAV -->
17 ${self.menu_bar_nav()}
17 ${self.menu_bar_nav()}
18 <!-- END MENU BAR NAV -->
18 <!-- END MENU BAR NAV -->
19 </div>
19 </div>
20 </div>
20 </div>
21 ${self.menu_bar_subnav()}
21 ${self.menu_bar_subnav()}
22 <!-- END HEADER -->
22 <!-- END HEADER -->
23
23
24 <!-- CONTENT -->
24 <!-- CONTENT -->
25 <div id="content" class="wrapper">
25 <div id="content" class="wrapper">
26
26
27 <rhodecode-toast id="notifications"></rhodecode-toast>
27 <rhodecode-toast id="notifications"></rhodecode-toast>
28
28
29 <div class="main">
29 <div class="main">
30 ${next.main()}
30 ${next.main()}
31 </div>
31 </div>
32 </div>
32 </div>
33 <!-- END CONTENT -->
33 <!-- END CONTENT -->
34
34
35 </div>
35 </div>
36 <!-- FOOTER -->
36 <!-- FOOTER -->
37 <div id="footer">
37 <div id="footer">
38 <div id="footer-inner" class="title wrapper">
38 <div id="footer-inner" class="title wrapper">
39 <div>
39 <div>
40 <p class="footer-link-right">
40 <p class="footer-link-right">
41 % if c.visual.show_version:
41 % if c.visual.show_version:
42 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
42 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
43 % endif
43 % endif
44 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
44 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
45 % if c.visual.rhodecode_support_url:
45 % if c.visual.rhodecode_support_url:
46 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
46 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
47 % endif
47 % endif
48 </p>
48 </p>
49 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
49 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
50 <p class="server-instance" style="display:${sid}">
50 <p class="server-instance" style="display:${sid}">
51 ## display hidden instance ID if specially defined
51 ## display hidden instance ID if specially defined
52 % if c.rhodecode_instanceid:
52 % if c.rhodecode_instanceid:
53 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
53 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
54 % endif
54 % endif
55 </p>
55 </p>
56 </div>
56 </div>
57 </div>
57 </div>
58 </div>
58 </div>
59
59
60 <!-- END FOOTER -->
60 <!-- END FOOTER -->
61
61
62 ### MAKO DEFS ###
62 ### MAKO DEFS ###
63
63
64 <%def name="menu_bar_subnav()">
64 <%def name="menu_bar_subnav()">
65 </%def>
65 </%def>
66
66
67 <%def name="breadcrumbs(class_='breadcrumbs')">
67 <%def name="breadcrumbs(class_='breadcrumbs')">
68 <div class="${class_}">
68 <div class="${class_}">
69 ${self.breadcrumbs_links()}
69 ${self.breadcrumbs_links()}
70 </div>
70 </div>
71 </%def>
71 </%def>
72
72
73 <%def name="admin_menu()">
73 <%def name="admin_menu()">
74 <ul class="admin_menu submenu">
74 <ul class="admin_menu submenu">
75 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
75 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
76 <li><a href="${h.url('repos')}">${_('Repositories')}</a></li>
76 <li><a href="${h.url('repos')}">${_('Repositories')}</a></li>
77 <li><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
77 <li><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
78 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
78 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
79 <li><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
79 <li><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
80 <li><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
80 <li><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
81 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
81 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
82 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
82 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
83 <li><a href="${h.url('admin_defaults_repositories')}">${_('Defaults')}</a></li>
83 <li><a href="${h.url('admin_defaults_repositories')}">${_('Defaults')}</a></li>
84 <li class="last"><a href="${h.url('admin_settings')}">${_('Settings')}</a></li>
84 <li class="last"><a href="${h.url('admin_settings')}">${_('Settings')}</a></li>
85 </ul>
85 </ul>
86 </%def>
86 </%def>
87
87
88
88
89 <%def name="dt_info_panel(elements)">
89 <%def name="dt_info_panel(elements)">
90 <dl class="dl-horizontal">
90 <dl class="dl-horizontal">
91 %for dt, dd, title, show_items in elements:
91 %for dt, dd, title, show_items in elements:
92 <dt>${dt}:</dt>
92 <dt>${dt}:</dt>
93 <dd title="${h.tooltip(title)}">
93 <dd title="${h.tooltip(title)}">
94 %if callable(dd):
94 %if callable(dd):
95 ## allow lazy evaluation of elements
95 ## allow lazy evaluation of elements
96 ${dd()}
96 ${dd()}
97 %else:
97 %else:
98 ${dd}
98 ${dd}
99 %endif
99 %endif
100 %if show_items:
100 %if show_items:
101 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
101 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
102 %endif
102 %endif
103 </dd>
103 </dd>
104
104
105 %if show_items:
105 %if show_items:
106 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
106 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
107 %for item in show_items:
107 %for item in show_items:
108 <dt></dt>
108 <dt></dt>
109 <dd>${item}</dd>
109 <dd>${item}</dd>
110 %endfor
110 %endfor
111 </div>
111 </div>
112 %endif
112 %endif
113
113
114 %endfor
114 %endfor
115 </dl>
115 </dl>
116 </%def>
116 </%def>
117
117
118
118
119 <%def name="gravatar(email, size=16)">
119 <%def name="gravatar(email, size=16)">
120 <%
120 <%
121 if (size > 16):
121 if (size > 16):
122 gravatar_class = 'gravatar gravatar-large'
122 gravatar_class = 'gravatar gravatar-large'
123 else:
123 else:
124 gravatar_class = 'gravatar'
124 gravatar_class = 'gravatar'
125 %>
125 %>
126 <%doc>
126 <%doc>
127 TODO: johbo: For now we serve double size images to make it smooth
127 TODO: johbo: For now we serve double size images to make it smooth
128 for retina. This is how it worked until now. Should be replaced
128 for retina. This is how it worked until now. Should be replaced
129 with a better solution at some point.
129 with a better solution at some point.
130 </%doc>
130 </%doc>
131 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
131 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
132 </%def>
132 </%def>
133
133
134
134
135 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
135 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
136 <% email = h.email_or_none(contact) %>
136 <% email = h.email_or_none(contact) %>
137 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
137 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
138 ${self.gravatar(email, size)}
138 ${self.gravatar(email, size)}
139 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
139 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
140 </div>
140 </div>
141 </%def>
141 </%def>
142
142
143
143
144 ## admin menu used for people that have some admin resources
144 ## admin menu used for people that have some admin resources
145 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
145 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
146 <ul class="submenu">
146 <ul class="submenu">
147 %if repositories:
147 %if repositories:
148 <li class="local-admin-repos"><a href="${h.url('repos')}">${_('Repositories')}</a></li>
148 <li class="local-admin-repos"><a href="${h.url('repos')}">${_('Repositories')}</a></li>
149 %endif
149 %endif
150 %if repository_groups:
150 %if repository_groups:
151 <li class="local-admin-repo-groups"><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
151 <li class="local-admin-repo-groups"><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
152 %endif
152 %endif
153 %if user_groups:
153 %if user_groups:
154 <li class="local-admin-user-groups"><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
154 <li class="local-admin-user-groups"><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
155 %endif
155 %endif
156 </ul>
156 </ul>
157 </%def>
157 </%def>
158
158
159 <%def name="repo_page_title(repo_instance)">
159 <%def name="repo_page_title(repo_instance)">
160 <div class="title-content">
160 <div class="title-content">
161 <div class="title-main">
161 <div class="title-main">
162 ## SVN/HG/GIT icons
162 ## SVN/HG/GIT icons
163 %if h.is_hg(repo_instance):
163 %if h.is_hg(repo_instance):
164 <i class="icon-hg"></i>
164 <i class="icon-hg"></i>
165 %endif
165 %endif
166 %if h.is_git(repo_instance):
166 %if h.is_git(repo_instance):
167 <i class="icon-git"></i>
167 <i class="icon-git"></i>
168 %endif
168 %endif
169 %if h.is_svn(repo_instance):
169 %if h.is_svn(repo_instance):
170 <i class="icon-svn"></i>
170 <i class="icon-svn"></i>
171 %endif
171 %endif
172
172
173 ## public/private
173 ## public/private
174 %if repo_instance.private:
174 %if repo_instance.private:
175 <i class="icon-repo-private"></i>
175 <i class="icon-repo-private"></i>
176 %else:
176 %else:
177 <i class="icon-repo-public"></i>
177 <i class="icon-repo-public"></i>
178 %endif
178 %endif
179
179
180 ## repo name with group name
180 ## repo name with group name
181 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
181 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
182
182
183 </div>
183 </div>
184
184
185 ## FORKED
185 ## FORKED
186 %if repo_instance.fork:
186 %if repo_instance.fork:
187 <p>
187 <p>
188 <i class="icon-code-fork"></i> ${_('Fork of')}
188 <i class="icon-code-fork"></i> ${_('Fork of')}
189 <a href="${h.route_path('repo_summary',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
189 <a href="${h.route_path('repo_summary',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
190 </p>
190 </p>
191 %endif
191 %endif
192
192
193 ## IMPORTED FROM REMOTE
193 ## IMPORTED FROM REMOTE
194 %if repo_instance.clone_uri:
194 %if repo_instance.clone_uri:
195 <p>
195 <p>
196 <i class="icon-code-fork"></i> ${_('Clone from')}
196 <i class="icon-code-fork"></i> ${_('Clone from')}
197 <a href="${h.url(h.safe_str(h.hide_credentials(repo_instance.clone_uri)))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
197 <a href="${h.url(h.safe_str(h.hide_credentials(repo_instance.clone_uri)))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
198 </p>
198 </p>
199 %endif
199 %endif
200
200
201 ## LOCKING STATUS
201 ## LOCKING STATUS
202 %if repo_instance.locked[0]:
202 %if repo_instance.locked[0]:
203 <p class="locking_locked">
203 <p class="locking_locked">
204 <i class="icon-repo-lock"></i>
204 <i class="icon-repo-lock"></i>
205 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
205 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
206 </p>
206 </p>
207 %elif repo_instance.enable_locking:
207 %elif repo_instance.enable_locking:
208 <p class="locking_unlocked">
208 <p class="locking_unlocked">
209 <i class="icon-repo-unlock"></i>
209 <i class="icon-repo-unlock"></i>
210 ${_('Repository not locked. Pull repository to lock it.')}
210 ${_('Repository not locked. Pull repository to lock it.')}
211 </p>
211 </p>
212 %endif
212 %endif
213
213
214 </div>
214 </div>
215 </%def>
215 </%def>
216
216
217 <%def name="repo_menu(active=None)">
217 <%def name="repo_menu(active=None)">
218 <%
218 <%
219 def is_active(selected):
219 def is_active(selected):
220 if selected == active:
220 if selected == active:
221 return "active"
221 return "active"
222 %>
222 %>
223
223
224 <!--- CONTEXT BAR -->
224 <!--- CONTEXT BAR -->
225 <div id="context-bar">
225 <div id="context-bar">
226 <div class="wrapper">
226 <div class="wrapper">
227 <ul id="context-pages" class="horizontal-list navigation">
227 <ul id="context-pages" class="horizontal-list navigation">
228 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
228 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
229 <li class="${is_active('changelog')}"><a class="menulink" href="${h.route_path('repo_changelog', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
229 <li class="${is_active('changelog')}"><a class="menulink" href="${h.route_path('repo_changelog', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
230 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
230 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
231 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
231 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
232 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
232 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
233 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
233 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
234 <li class="${is_active('showpullrequest')}">
234 <li class="${is_active('showpullrequest')}">
235 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
235 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
236 %if c.repository_pull_requests:
236 %if c.repository_pull_requests:
237 <span class="pr_notifications">${c.repository_pull_requests}</span>
237 <span class="pr_notifications">${c.repository_pull_requests}</span>
238 %endif
238 %endif
239 <div class="menulabel">${_('Pull Requests')}</div>
239 <div class="menulabel">${_('Pull Requests')}</div>
240 </a>
240 </a>
241 </li>
241 </li>
242 %endif
242 %endif
243 <li class="${is_active('options')}">
243 <li class="${is_active('options')}">
244 <a class="menulink dropdown">
244 <a class="menulink dropdown">
245 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
245 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
246 </a>
246 </a>
247 <ul class="submenu">
247 <ul class="submenu">
248 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
248 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
249 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
249 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
250 %endif
250 %endif
251 %if c.rhodecode_db_repo.fork:
251 %if c.rhodecode_db_repo.fork:
252 <li>
252 <li>
253 <a title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
253 <a title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
254 href="${h.route_path('repo_compare',
254 href="${h.route_path('repo_compare',
255 repo_name=c.rhodecode_db_repo.fork.repo_name,
255 repo_name=c.rhodecode_db_repo.fork.repo_name,
256 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
256 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
257 source_ref=c.rhodecode_db_repo.landing_rev[1],
257 source_ref=c.rhodecode_db_repo.landing_rev[1],
258 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
258 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
259 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
259 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
260 _query=dict(merge=1))}"
260 _query=dict(merge=1))}"
261 >
261 >
262 ${_('Compare fork')}
262 ${_('Compare fork')}
263 </a>
263 </a>
264 </li>
264 </li>
265 %endif
265 %endif
266
266
267 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
267 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
268
268
269 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
269 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
270 %if c.rhodecode_db_repo.locked[0]:
270 %if c.rhodecode_db_repo.locked[0]:
271 <li><a class="locking_del" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
271 <li><a class="locking_del" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
272 %else:
272 %else:
273 <li><a class="locking_add" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
273 <li><a class="locking_add" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
274 %endif
274 %endif
275 %endif
275 %endif
276 %if c.rhodecode_user.username != h.DEFAULT_USER:
276 %if c.rhodecode_user.username != h.DEFAULT_USER:
277 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
277 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
278 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}">${_('Fork')}</a></li>
278 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}">${_('Fork')}</a></li>
279 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
279 <li><a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
280 %endif
280 %endif
281 %endif
281 %endif
282 </ul>
282 </ul>
283 </li>
283 </li>
284 </ul>
284 </ul>
285 </div>
285 </div>
286 <div class="clear"></div>
286 <div class="clear"></div>
287 </div>
287 </div>
288 <!--- END CONTEXT BAR -->
288 <!--- END CONTEXT BAR -->
289
289
290 </%def>
290 </%def>
291
291
292 <%def name="usermenu(active=False)">
292 <%def name="usermenu(active=False)">
293 ## USER MENU
293 ## USER MENU
294 <li id="quick_login_li" class="${'active' if active else ''}">
294 <li id="quick_login_li" class="${'active' if active else ''}">
295 <a id="quick_login_link" class="menulink childs">
295 <a id="quick_login_link" class="menulink childs">
296 ${gravatar(c.rhodecode_user.email, 20)}
296 ${gravatar(c.rhodecode_user.email, 20)}
297 <span class="user">
297 <span class="user">
298 %if c.rhodecode_user.username != h.DEFAULT_USER:
298 %if c.rhodecode_user.username != h.DEFAULT_USER:
299 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
299 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
300 %else:
300 %else:
301 <span>${_('Sign in')}</span>
301 <span>${_('Sign in')}</span>
302 %endif
302 %endif
303 </span>
303 </span>
304 </a>
304 </a>
305
305
306 <div class="user-menu submenu">
306 <div class="user-menu submenu">
307 <div id="quick_login">
307 <div id="quick_login">
308 %if c.rhodecode_user.username == h.DEFAULT_USER:
308 %if c.rhodecode_user.username == h.DEFAULT_USER:
309 <h4>${_('Sign in to your account')}</h4>
309 <h4>${_('Sign in to your account')}</h4>
310 ${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)}
310 ${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)}
311 <div class="form form-vertical">
311 <div class="form form-vertical">
312 <div class="fields">
312 <div class="fields">
313 <div class="field">
313 <div class="field">
314 <div class="label">
314 <div class="label">
315 <label for="username">${_('Username')}:</label>
315 <label for="username">${_('Username')}:</label>
316 </div>
316 </div>
317 <div class="input">
317 <div class="input">
318 ${h.text('username',class_='focus',tabindex=1)}
318 ${h.text('username',class_='focus',tabindex=1)}
319 </div>
319 </div>
320
320
321 </div>
321 </div>
322 <div class="field">
322 <div class="field">
323 <div class="label">
323 <div class="label">
324 <label for="password">${_('Password')}:</label>
324 <label for="password">${_('Password')}:</label>
325 %if h.HasPermissionAny('hg.password_reset.enabled')():
325 %if h.HasPermissionAny('hg.password_reset.enabled')():
326 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
326 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
327 %endif
327 %endif
328 </div>
328 </div>
329 <div class="input">
329 <div class="input">
330 ${h.password('password',class_='focus',tabindex=2)}
330 ${h.password('password',class_='focus',tabindex=2)}
331 </div>
331 </div>
332 </div>
332 </div>
333 <div class="buttons">
333 <div class="buttons">
334 <div class="register">
334 <div class="register">
335 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
335 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
336 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
336 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
337 %endif
337 %endif
338 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
338 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
339 </div>
339 </div>
340 <div class="submit">
340 <div class="submit">
341 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
341 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
342 </div>
342 </div>
343 </div>
343 </div>
344 </div>
344 </div>
345 </div>
345 </div>
346 ${h.end_form()}
346 ${h.end_form()}
347 %else:
347 %else:
348 <div class="">
348 <div class="">
349 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
349 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
350 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
350 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
351 <div class="email">${c.rhodecode_user.email}</div>
351 <div class="email">${c.rhodecode_user.email}</div>
352 </div>
352 </div>
353 <div class="">
353 <div class="">
354 <ol class="links">
354 <ol class="links">
355 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
355 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
356 % if c.rhodecode_user.personal_repo_group:
356 % if c.rhodecode_user.personal_repo_group:
357 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
357 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
358 % endif
358 % endif
359 <li class="logout">
359 <li class="logout">
360 ${h.secure_form(h.route_path('logout'), request=request)}
360 ${h.secure_form(h.route_path('logout'), request=request)}
361 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
361 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
362 ${h.end_form()}
362 ${h.end_form()}
363 </li>
363 </li>
364 </ol>
364 </ol>
365 </div>
365 </div>
366 %endif
366 %endif
367 </div>
367 </div>
368 </div>
368 </div>
369 %if c.rhodecode_user.username != h.DEFAULT_USER:
369 %if c.rhodecode_user.username != h.DEFAULT_USER:
370 <div class="pill_container">
370 <div class="pill_container">
371 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
371 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
372 </div>
372 </div>
373 % endif
373 % endif
374 </li>
374 </li>
375 </%def>
375 </%def>
376
376
377 <%def name="menu_items(active=None)">
377 <%def name="menu_items(active=None)">
378 <%
378 <%
379 def is_active(selected):
379 def is_active(selected):
380 if selected == active:
380 if selected == active:
381 return "active"
381 return "active"
382 return ""
382 return ""
383 %>
383 %>
384 <ul id="quick" class="main_nav navigation horizontal-list">
384 <ul id="quick" class="main_nav navigation horizontal-list">
385 <!-- repo switcher -->
385 <!-- repo switcher -->
386 <li class="${is_active('repositories')} repo_switcher_li has_select2">
386 <li class="${is_active('repositories')} repo_switcher_li has_select2">
387 <input id="repo_switcher" name="repo_switcher" type="hidden">
387 <input id="repo_switcher" name="repo_switcher" type="hidden">
388 </li>
388 </li>
389
389
390 ## ROOT MENU
390 ## ROOT MENU
391 %if c.rhodecode_user.username != h.DEFAULT_USER:
391 %if c.rhodecode_user.username != h.DEFAULT_USER:
392 <li class="${is_active('journal')}">
392 <li class="${is_active('journal')}">
393 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
393 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
394 <div class="menulabel">${_('Journal')}</div>
394 <div class="menulabel">${_('Journal')}</div>
395 </a>
395 </a>
396 </li>
396 </li>
397 %else:
397 %else:
398 <li class="${is_active('journal')}">
398 <li class="${is_active('journal')}">
399 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
399 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
400 <div class="menulabel">${_('Public journal')}</div>
400 <div class="menulabel">${_('Public journal')}</div>
401 </a>
401 </a>
402 </li>
402 </li>
403 %endif
403 %endif
404 <li class="${is_active('gists')}">
404 <li class="${is_active('gists')}">
405 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
405 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
406 <div class="menulabel">${_('Gists')}</div>
406 <div class="menulabel">${_('Gists')}</div>
407 </a>
407 </a>
408 </li>
408 </li>
409 <li class="${is_active('search')}">
409 <li class="${is_active('search')}">
410 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
410 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
411 <div class="menulabel">${_('Search')}</div>
411 <div class="menulabel">${_('Search')}</div>
412 </a>
412 </a>
413 </li>
413 </li>
414 % if h.HasPermissionAll('hg.admin')('access admin main page'):
414 % if h.HasPermissionAll('hg.admin')('access admin main page'):
415 <li class="${is_active('admin')}">
415 <li class="${is_active('admin')}">
416 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
416 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
417 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
417 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
418 </a>
418 </a>
419 ${admin_menu()}
419 ${admin_menu()}
420 </li>
420 </li>
421 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
421 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
422 <li class="${is_active('admin')}">
422 <li class="${is_active('admin')}">
423 <a class="menulink childs" title="${_('Delegated Admin settings')}">
423 <a class="menulink childs" title="${_('Delegated Admin settings')}">
424 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
424 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
425 </a>
425 </a>
426 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
426 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
427 c.rhodecode_user.repository_groups_admin,
427 c.rhodecode_user.repository_groups_admin,
428 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
428 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
429 </li>
429 </li>
430 % endif
430 % endif
431 % if c.debug_style:
431 % if c.debug_style:
432 <li class="${is_active('debug_style')}">
432 <li class="${is_active('debug_style')}">
433 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
433 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
434 <div class="menulabel">${_('Style')}</div>
434 <div class="menulabel">${_('Style')}</div>
435 </a>
435 </a>
436 </li>
436 </li>
437 % endif
437 % endif
438 ## render extra user menu
438 ## render extra user menu
439 ${usermenu(active=(active=='my_account'))}
439 ${usermenu(active=(active=='my_account'))}
440 </ul>
440 </ul>
441
441
442 <script type="text/javascript">
442 <script type="text/javascript">
443 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
443 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
444
444
445 /*format the look of items in the list*/
445 /*format the look of items in the list*/
446 var format = function(state, escapeMarkup){
446 var format = function(state, escapeMarkup){
447 if (!state.id){
447 if (!state.id){
448 return state.text; // optgroup
448 return state.text; // optgroup
449 }
449 }
450 var obj_dict = state.obj;
450 var obj_dict = state.obj;
451 var tmpl = '';
451 var tmpl = '';
452
452
453 if(obj_dict && state.type == 'repo'){
453 if(obj_dict && state.type == 'repo'){
454 if(obj_dict['repo_type'] === 'hg'){
454 if(obj_dict['repo_type'] === 'hg'){
455 tmpl += '<i class="icon-hg"></i> ';
455 tmpl += '<i class="icon-hg"></i> ';
456 }
456 }
457 else if(obj_dict['repo_type'] === 'git'){
457 else if(obj_dict['repo_type'] === 'git'){
458 tmpl += '<i class="icon-git"></i> ';
458 tmpl += '<i class="icon-git"></i> ';
459 }
459 }
460 else if(obj_dict['repo_type'] === 'svn'){
460 else if(obj_dict['repo_type'] === 'svn'){
461 tmpl += '<i class="icon-svn"></i> ';
461 tmpl += '<i class="icon-svn"></i> ';
462 }
462 }
463 if(obj_dict['private']){
463 if(obj_dict['private']){
464 tmpl += '<i class="icon-lock" ></i> ';
464 tmpl += '<i class="icon-lock" ></i> ';
465 }
465 }
466 else if(visual_show_public_icon){
466 else if(visual_show_public_icon){
467 tmpl += '<i class="icon-unlock-alt"></i> ';
467 tmpl += '<i class="icon-unlock-alt"></i> ';
468 }
468 }
469 }
469 }
470 if(obj_dict && state.type == 'commit') {
470 if(obj_dict && state.type == 'commit') {
471 tmpl += '<i class="icon-tag"></i>';
471 tmpl += '<i class="icon-tag"></i>';
472 }
472 }
473 if(obj_dict && state.type == 'group'){
473 if(obj_dict && state.type == 'group'){
474 tmpl += '<i class="icon-folder-close"></i> ';
474 tmpl += '<i class="icon-folder-close"></i> ';
475 }
475 }
476 tmpl += escapeMarkup(state.text);
476 tmpl += escapeMarkup(state.text);
477 return tmpl;
477 return tmpl;
478 };
478 };
479
479
480 var formatResult = function(result, container, query, escapeMarkup) {
480 var formatResult = function(result, container, query, escapeMarkup) {
481 return format(result, escapeMarkup);
481 return format(result, escapeMarkup);
482 };
482 };
483
483
484 var formatSelection = function(data, container, escapeMarkup) {
484 var formatSelection = function(data, container, escapeMarkup) {
485 return format(data, escapeMarkup);
485 return format(data, escapeMarkup);
486 };
486 };
487
487
488 $("#repo_switcher").select2({
488 $("#repo_switcher").select2({
489 cachedDataSource: {},
489 cachedDataSource: {},
490 minimumInputLength: 2,
490 minimumInputLength: 2,
491 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
491 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
492 dropdownAutoWidth: true,
492 dropdownAutoWidth: true,
493 formatResult: formatResult,
493 formatResult: formatResult,
494 formatSelection: formatSelection,
494 formatSelection: formatSelection,
495 containerCssClass: "repo-switcher",
495 containerCssClass: "repo-switcher",
496 dropdownCssClass: "repo-switcher-dropdown",
496 dropdownCssClass: "repo-switcher-dropdown",
497 escapeMarkup: function(m){
497 escapeMarkup: function(m){
498 // don't escape our custom placeholder
498 // don't escape our custom placeholder
499 if(m.substr(0,23) == '<div class="menulabel">'){
499 if(m.substr(0,23) == '<div class="menulabel">'){
500 return m;
500 return m;
501 }
501 }
502
502
503 return Select2.util.escapeMarkup(m);
503 return Select2.util.escapeMarkup(m);
504 },
504 },
505 query: $.debounce(250, function(query){
505 query: $.debounce(250, function(query){
506 self = this;
506 self = this;
507 var cacheKey = query.term;
507 var cacheKey = query.term;
508 var cachedData = self.cachedDataSource[cacheKey];
508 var cachedData = self.cachedDataSource[cacheKey];
509
509
510 if (cachedData) {
510 if (cachedData) {
511 query.callback({results: cachedData.results});
511 query.callback({results: cachedData.results});
512 } else {
512 } else {
513 $.ajax({
513 $.ajax({
514 url: pyroutes.url('goto_switcher_data'),
514 url: pyroutes.url('goto_switcher_data'),
515 data: {'query': query.term},
515 data: {'query': query.term},
516 dataType: 'json',
516 dataType: 'json',
517 type: 'GET',
517 type: 'GET',
518 success: function(data) {
518 success: function(data) {
519 self.cachedDataSource[cacheKey] = data;
519 self.cachedDataSource[cacheKey] = data;
520 query.callback({results: data.results});
520 query.callback({results: data.results});
521 },
521 },
522 error: function(data, textStatus, errorThrown) {
522 error: function(data, textStatus, errorThrown) {
523 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
523 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
524 }
524 }
525 })
525 })
526 }
526 }
527 })
527 })
528 });
528 });
529
529
530 $("#repo_switcher").on('select2-selecting', function(e){
530 $("#repo_switcher").on('select2-selecting', function(e){
531 e.preventDefault();
531 e.preventDefault();
532 window.location = e.choice.url;
532 window.location = e.choice.url;
533 });
533 });
534
534
535 </script>
535 </script>
536 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
536 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
537 </%def>
537 </%def>
538
538
539 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
539 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
540 <div class="modal-dialog">
540 <div class="modal-dialog">
541 <div class="modal-content">
541 <div class="modal-content">
542 <div class="modal-header">
542 <div class="modal-header">
543 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
543 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
544 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
544 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
545 </div>
545 </div>
546 <div class="modal-body">
546 <div class="modal-body">
547 <div class="block-left">
547 <div class="block-left">
548 <table class="keyboard-mappings">
548 <table class="keyboard-mappings">
549 <tbody>
549 <tbody>
550 <tr>
550 <tr>
551 <th></th>
551 <th></th>
552 <th>${_('Site-wide shortcuts')}</th>
552 <th>${_('Site-wide shortcuts')}</th>
553 </tr>
553 </tr>
554 <%
554 <%
555 elems = [
555 elems = [
556 ('/', 'Open quick search box'),
556 ('/', 'Open quick search box'),
557 ('g h', 'Goto home page'),
557 ('g h', 'Goto home page'),
558 ('g g', 'Goto my private gists page'),
558 ('g g', 'Goto my private gists page'),
559 ('g G', 'Goto my public gists page'),
559 ('g G', 'Goto my public gists page'),
560 ('n r', 'New repository page'),
560 ('n r', 'New repository page'),
561 ('n g', 'New gist page'),
561 ('n g', 'New gist page'),
562 ]
562 ]
563 %>
563 %>
564 %for key, desc in elems:
564 %for key, desc in elems:
565 <tr>
565 <tr>
566 <td class="keys">
566 <td class="keys">
567 <span class="key tag">${key}</span>
567 <span class="key tag">${key}</span>
568 </td>
568 </td>
569 <td>${desc}</td>
569 <td>${desc}</td>
570 </tr>
570 </tr>
571 %endfor
571 %endfor
572 </tbody>
572 </tbody>
573 </table>
573 </table>
574 </div>
574 </div>
575 <div class="block-left">
575 <div class="block-left">
576 <table class="keyboard-mappings">
576 <table class="keyboard-mappings">
577 <tbody>
577 <tbody>
578 <tr>
578 <tr>
579 <th></th>
579 <th></th>
580 <th>${_('Repositories')}</th>
580 <th>${_('Repositories')}</th>
581 </tr>
581 </tr>
582 <%
582 <%
583 elems = [
583 elems = [
584 ('g s', 'Goto summary page'),
584 ('g s', 'Goto summary page'),
585 ('g c', 'Goto changelog page'),
585 ('g c', 'Goto changelog page'),
586 ('g f', 'Goto files page'),
586 ('g f', 'Goto files page'),
587 ('g F', 'Goto files page with file search activated'),
587 ('g F', 'Goto files page with file search activated'),
588 ('g p', 'Goto pull requests page'),
588 ('g p', 'Goto pull requests page'),
589 ('g o', 'Goto repository settings'),
589 ('g o', 'Goto repository settings'),
590 ('g O', 'Goto repository permissions settings'),
590 ('g O', 'Goto repository permissions settings'),
591 ]
591 ]
592 %>
592 %>
593 %for key, desc in elems:
593 %for key, desc in elems:
594 <tr>
594 <tr>
595 <td class="keys">
595 <td class="keys">
596 <span class="key tag">${key}</span>
596 <span class="key tag">${key}</span>
597 </td>
597 </td>
598 <td>${desc}</td>
598 <td>${desc}</td>
599 </tr>
599 </tr>
600 %endfor
600 %endfor
601 </tbody>
601 </tbody>
602 </table>
602 </table>
603 </div>
603 </div>
604 </div>
604 </div>
605 <div class="modal-footer">
605 <div class="modal-footer">
606 </div>
606 </div>
607 </div><!-- /.modal-content -->
607 </div><!-- /.modal-content -->
608 </div><!-- /.modal-dialog -->
608 </div><!-- /.modal-dialog -->
609 </div><!-- /.modal -->
609 </div><!-- /.modal -->
@@ -1,297 +1,297 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%inherit file="/base/base.mako"/>
3 <%inherit file="/base/base.mako"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 ${_('%s Changelog') % c.repo_name}
6 ${_('%s Changelog') % c.repo_name}
7 %if c.changelog_for_path:
7 %if c.changelog_for_path:
8 /${c.changelog_for_path}
8 /${c.changelog_for_path}
9 %endif
9 %endif
10 %if c.rhodecode_name:
10 %if c.rhodecode_name:
11 &middot; ${h.branding(c.rhodecode_name)}
11 &middot; ${h.branding(c.rhodecode_name)}
12 %endif
12 %endif
13 </%def>
13 </%def>
14
14
15 <%def name="breadcrumbs_links()">
15 <%def name="breadcrumbs_links()">
16 %if c.changelog_for_path:
16 %if c.changelog_for_path:
17 /${c.changelog_for_path}
17 /${c.changelog_for_path}
18 %endif
18 %endif
19 </%def>
19 </%def>
20
20
21 <%def name="menu_bar_nav()">
21 <%def name="menu_bar_nav()">
22 ${self.menu_items(active='repositories')}
22 ${self.menu_items(active='repositories')}
23 </%def>
23 </%def>
24
24
25 <%def name="menu_bar_subnav()">
25 <%def name="menu_bar_subnav()">
26 ${self.repo_menu(active='changelog')}
26 ${self.repo_menu(active='changelog')}
27 </%def>
27 </%def>
28
28
29 <%def name="main()">
29 <%def name="main()">
30
30
31 <div class="box">
31 <div class="box">
32 <div class="title">
32 <div class="title">
33 ${self.repo_page_title(c.rhodecode_db_repo)}
33 ${self.repo_page_title(c.rhodecode_db_repo)}
34 <ul class="links">
34 <ul class="links">
35 <li>
35 <li>
36 <a href="#" class="btn btn-small" id="rev_range_container" style="display:none;"></a>
36 <a href="#" class="btn btn-small" id="rev_range_container" style="display:none;"></a>
37 %if c.rhodecode_db_repo.fork:
37 %if c.rhodecode_db_repo.fork:
38 <span>
38 <span>
39 <a id="compare_fork_button"
39 <a id="compare_fork_button"
40 title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
40 title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
41 class="btn btn-small"
41 class="btn btn-small"
42 href="${h.route_path('repo_compare',
42 href="${h.route_path('repo_compare',
43 repo_name=c.rhodecode_db_repo.fork.repo_name,
43 repo_name=c.rhodecode_db_repo.fork.repo_name,
44 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
44 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
45 source_ref=c.rhodecode_db_repo.landing_rev[1],
45 source_ref=c.rhodecode_db_repo.landing_rev[1],
46 target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
46 target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
47 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
47 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
48 _query=dict(merge=1, target_repo=c.repo_name))}"
48 _query=dict(merge=1, target_repo=c.repo_name))}"
49 >
49 >
50 ${_('Compare fork with Parent (%s)' % c.rhodecode_db_repo.fork.repo_name)}
50 ${_('Compare fork with Parent (%s)' % c.rhodecode_db_repo.fork.repo_name)}
51 </a>
51 </a>
52 </span>
52 </span>
53 %endif
53 %endif
54
54
55 ## pr open link
55 ## pr open link
56 %if h.is_hg(c.rhodecode_repo) or h.is_git(c.rhodecode_repo):
56 %if h.is_hg(c.rhodecode_repo) or h.is_git(c.rhodecode_repo):
57 <span>
57 <span>
58 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.url('pullrequest_home',repo_name=c.repo_name)}">
58 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
59 ${_('Open new pull request')}
59 ${_('Open new pull request')}
60 </a>
60 </a>
61 </span>
61 </span>
62 %endif
62 %endif
63
63
64 ## clear selection
64 ## clear selection
65 <div title="${_('Clear selection')}" class="btn" id="rev_range_clear" style="display:none">
65 <div title="${_('Clear selection')}" class="btn" id="rev_range_clear" style="display:none">
66 ${_('Clear selection')}
66 ${_('Clear selection')}
67 </div>
67 </div>
68
68
69 </li>
69 </li>
70 </ul>
70 </ul>
71 </div>
71 </div>
72
72
73 % if c.pagination:
73 % if c.pagination:
74 <script type="text/javascript" src="${h.asset('js/jquery.commits-graph.js')}"></script>
74 <script type="text/javascript" src="${h.asset('js/jquery.commits-graph.js')}"></script>
75
75
76 <div class="graph-header">
76 <div class="graph-header">
77 <div id="filter_changelog">
77 <div id="filter_changelog">
78 ${h.hidden('branch_filter')}
78 ${h.hidden('branch_filter')}
79 %if c.selected_name:
79 %if c.selected_name:
80 <div class="btn btn-default" id="clear_filter" >
80 <div class="btn btn-default" id="clear_filter" >
81 ${_('Clear filter')}
81 ${_('Clear filter')}
82 </div>
82 </div>
83 %endif
83 %endif
84 </div>
84 </div>
85 ${self.breadcrumbs('breadcrumbs_light')}
85 ${self.breadcrumbs('breadcrumbs_light')}
86 <div id="commit-counter" data-total=${c.total_cs} class="pull-right">
86 <div id="commit-counter" data-total=${c.total_cs} class="pull-right">
87 ${_ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)}
87 ${_ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)}
88 </div>
88 </div>
89 </div>
89 </div>
90
90
91 <div id="graph">
91 <div id="graph">
92 <div class="graph-col-wrapper">
92 <div class="graph-col-wrapper">
93 <div id="graph_nodes">
93 <div id="graph_nodes">
94 <div id="graph_canvas"></div>
94 <div id="graph_canvas"></div>
95 </div>
95 </div>
96 <div id="graph_content" class="main-content graph_full_width">
96 <div id="graph_content" class="main-content graph_full_width">
97
97
98 <div class="table">
98 <div class="table">
99 <table id="changesets" class="rctable">
99 <table id="changesets" class="rctable">
100 <tr>
100 <tr>
101 ## checkbox
101 ## checkbox
102 <th></th>
102 <th></th>
103 <th colspan="2"></th>
103 <th colspan="2"></th>
104
104
105 <th>${_('Commit')}</th>
105 <th>${_('Commit')}</th>
106 ## commit message expand arrow
106 ## commit message expand arrow
107 <th></th>
107 <th></th>
108 <th>${_('Commit Message')}</th>
108 <th>${_('Commit Message')}</th>
109
109
110 <th>${_('Age')}</th>
110 <th>${_('Age')}</th>
111 <th>${_('Author')}</th>
111 <th>${_('Author')}</th>
112
112
113 <th>${_('Refs')}</th>
113 <th>${_('Refs')}</th>
114 </tr>
114 </tr>
115
115
116 <tbody class="commits-range">
116 <tbody class="commits-range">
117 <%include file='changelog_elements.mako'/>
117 <%include file='changelog_elements.mako'/>
118 </tbody>
118 </tbody>
119 </table>
119 </table>
120 </div>
120 </div>
121 </div>
121 </div>
122 <div class="pagination-wh pagination-left">
122 <div class="pagination-wh pagination-left">
123 ${c.pagination.pager('$link_previous ~2~ $link_next')}
123 ${c.pagination.pager('$link_previous ~2~ $link_next')}
124 </div>
124 </div>
125 </div>
125 </div>
126
126
127 <script type="text/javascript">
127 <script type="text/javascript">
128 var cache = {};
128 var cache = {};
129 $(function(){
129 $(function(){
130
130
131 // Create links to commit ranges when range checkboxes are selected
131 // Create links to commit ranges when range checkboxes are selected
132 var $commitCheckboxes = $('.commit-range');
132 var $commitCheckboxes = $('.commit-range');
133 // cache elements
133 // cache elements
134 var $commitRangeContainer = $('#rev_range_container');
134 var $commitRangeContainer = $('#rev_range_container');
135 var $commitRangeClear = $('#rev_range_clear');
135 var $commitRangeClear = $('#rev_range_clear');
136
136
137 var checkboxRangeSelector = function(e){
137 var checkboxRangeSelector = function(e){
138 var selectedCheckboxes = [];
138 var selectedCheckboxes = [];
139 for (pos in $commitCheckboxes){
139 for (pos in $commitCheckboxes){
140 if($commitCheckboxes[pos].checked){
140 if($commitCheckboxes[pos].checked){
141 selectedCheckboxes.push($commitCheckboxes[pos]);
141 selectedCheckboxes.push($commitCheckboxes[pos]);
142 }
142 }
143 }
143 }
144 var open_new_pull_request = $('#open_new_pull_request');
144 var open_new_pull_request = $('#open_new_pull_request');
145 if(open_new_pull_request){
145 if(open_new_pull_request){
146 var selected_changes = selectedCheckboxes.length;
146 var selected_changes = selectedCheckboxes.length;
147 if (selected_changes > 1 || selected_changes == 1 && templateContext.repo_type != 'hg') {
147 if (selected_changes > 1 || selected_changes == 1 && templateContext.repo_type != 'hg') {
148 open_new_pull_request.hide();
148 open_new_pull_request.hide();
149 } else {
149 } else {
150 if (selected_changes == 1) {
150 if (selected_changes == 1) {
151 open_new_pull_request.html(_gettext('Open new pull request for selected commit'));
151 open_new_pull_request.html(_gettext('Open new pull request for selected commit'));
152 } else if (selected_changes == 0) {
152 } else if (selected_changes == 0) {
153 open_new_pull_request.html(_gettext('Open new pull request'));
153 open_new_pull_request.html(_gettext('Open new pull request'));
154 }
154 }
155 open_new_pull_request.show();
155 open_new_pull_request.show();
156 }
156 }
157 }
157 }
158
158
159 if (selectedCheckboxes.length>0){
159 if (selectedCheckboxes.length>0){
160 var revEnd = selectedCheckboxes[0].name;
160 var revEnd = selectedCheckboxes[0].name;
161 var revStart = selectedCheckboxes[selectedCheckboxes.length-1].name;
161 var revStart = selectedCheckboxes[selectedCheckboxes.length-1].name;
162 var url = pyroutes.url('repo_commit',
162 var url = pyroutes.url('repo_commit',
163 {'repo_name': '${c.repo_name}',
163 {'repo_name': '${c.repo_name}',
164 'commit_id': revStart+'...'+revEnd});
164 'commit_id': revStart+'...'+revEnd});
165
165
166 var link = (revStart == revEnd)
166 var link = (revStart == revEnd)
167 ? _gettext('Show selected commit __S')
167 ? _gettext('Show selected commit __S')
168 : _gettext('Show selected commits __S ... __E');
168 : _gettext('Show selected commits __S ... __E');
169
169
170 link = link.replace('__S', revStart.substr(0,6));
170 link = link.replace('__S', revStart.substr(0,6));
171 link = link.replace('__E', revEnd.substr(0,6));
171 link = link.replace('__E', revEnd.substr(0,6));
172
172
173 $commitRangeContainer
173 $commitRangeContainer
174 .attr('href',url)
174 .attr('href',url)
175 .html(link)
175 .html(link)
176 .show();
176 .show();
177
177
178 $commitRangeClear.show();
178 $commitRangeClear.show();
179 var _url = pyroutes.url('pullrequest_home',
179 var _url = pyroutes.url('pullrequest_new',
180 {'repo_name': '${c.repo_name}',
180 {'repo_name': '${c.repo_name}',
181 'commit': revEnd});
181 'commit': revEnd});
182 open_new_pull_request.attr('href', _url);
182 open_new_pull_request.attr('href', _url);
183 $('#compare_fork_button').hide();
183 $('#compare_fork_button').hide();
184 } else {
184 } else {
185 $commitRangeContainer.hide();
185 $commitRangeContainer.hide();
186 $commitRangeClear.hide();
186 $commitRangeClear.hide();
187
187
188 %if c.branch_name:
188 %if c.branch_name:
189 var _url = pyroutes.url('pullrequest_home',
189 var _url = pyroutes.url('pullrequest_new',
190 {'repo_name': '${c.repo_name}',
190 {'repo_name': '${c.repo_name}',
191 'branch':'${c.branch_name}'});
191 'branch':'${c.branch_name}'});
192 open_new_pull_request.attr('href', _url);
192 open_new_pull_request.attr('href', _url);
193 %else:
193 %else:
194 var _url = pyroutes.url('pullrequest_home',
194 var _url = pyroutes.url('pullrequest_new',
195 {'repo_name': '${c.repo_name}'});
195 {'repo_name': '${c.repo_name}'});
196 open_new_pull_request.attr('href', _url);
196 open_new_pull_request.attr('href', _url);
197 %endif
197 %endif
198 $('#compare_fork_button').show();
198 $('#compare_fork_button').show();
199 }
199 }
200 };
200 };
201
201
202 $commitCheckboxes.on('click', checkboxRangeSelector);
202 $commitCheckboxes.on('click', checkboxRangeSelector);
203
203
204 $commitRangeClear.on('click',function(e) {
204 $commitRangeClear.on('click',function(e) {
205 $commitCheckboxes.attr('checked', false);
205 $commitCheckboxes.attr('checked', false);
206 checkboxRangeSelector();
206 checkboxRangeSelector();
207 e.preventDefault();
207 e.preventDefault();
208 });
208 });
209
209
210 // make sure the buttons are consistent when navigate back and forth
210 // make sure the buttons are consistent when navigate back and forth
211 checkboxRangeSelector();
211 checkboxRangeSelector();
212
212
213 var msgs = $('.message');
213 var msgs = $('.message');
214 // get first element height
214 // get first element height
215 var el = $('#graph_content .container')[0];
215 var el = $('#graph_content .container')[0];
216 var row_h = el.clientHeight;
216 var row_h = el.clientHeight;
217 for (var i=0; i < msgs.length; i++) {
217 for (var i=0; i < msgs.length; i++) {
218 var m = msgs[i];
218 var m = msgs[i];
219
219
220 var h = m.clientHeight;
220 var h = m.clientHeight;
221 var pad = $(m).css('padding');
221 var pad = $(m).css('padding');
222 if (h > row_h) {
222 if (h > row_h) {
223 var offset = row_h - (h+12);
223 var offset = row_h - (h+12);
224 $(m.nextElementSibling).css('display','block');
224 $(m.nextElementSibling).css('display','block');
225 $(m.nextElementSibling).css('margin-top',offset+'px');
225 $(m.nextElementSibling).css('margin-top',offset+'px');
226 }
226 }
227 }
227 }
228
228
229 $("#clear_filter").on("click", function() {
229 $("#clear_filter").on("click", function() {
230 var filter = {'repo_name': '${c.repo_name}'};
230 var filter = {'repo_name': '${c.repo_name}'};
231 window.location = pyroutes.url('repo_changelog', filter);
231 window.location = pyroutes.url('repo_changelog', filter);
232 });
232 });
233
233
234 $("#branch_filter").select2({
234 $("#branch_filter").select2({
235 'dropdownAutoWidth': true,
235 'dropdownAutoWidth': true,
236 'width': 'resolve',
236 'width': 'resolve',
237 'placeholder': "${c.selected_name or _('Filter changelog')}",
237 'placeholder': "${c.selected_name or _('Filter changelog')}",
238 containerCssClass: "drop-menu",
238 containerCssClass: "drop-menu",
239 dropdownCssClass: "drop-menu-dropdown",
239 dropdownCssClass: "drop-menu-dropdown",
240 query: function(query){
240 query: function(query){
241 var key = 'cache';
241 var key = 'cache';
242 var cached = cache[key] ;
242 var cached = cache[key] ;
243 if(cached) {
243 if(cached) {
244 var data = {results: []};
244 var data = {results: []};
245 //filter results
245 //filter results
246 $.each(cached.results, function(){
246 $.each(cached.results, function(){
247 var section = this.text;
247 var section = this.text;
248 var children = [];
248 var children = [];
249 $.each(this.children, function(){
249 $.each(this.children, function(){
250 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
250 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
251 children.push({'id': this.id, 'text': this.text, 'type': this.type})
251 children.push({'id': this.id, 'text': this.text, 'type': this.type})
252 }
252 }
253 });
253 });
254 data.results.push({'text': section, 'children': children});
254 data.results.push({'text': section, 'children': children});
255 query.callback({results: data.results});
255 query.callback({results: data.results});
256 });
256 });
257 }else{
257 }else{
258 $.ajax({
258 $.ajax({
259 url: pyroutes.url('repo_refs_changelog_data', {'repo_name': '${c.repo_name}'}),
259 url: pyroutes.url('repo_refs_changelog_data', {'repo_name': '${c.repo_name}'}),
260 data: {},
260 data: {},
261 dataType: 'json',
261 dataType: 'json',
262 type: 'GET',
262 type: 'GET',
263 success: function(data) {
263 success: function(data) {
264 cache[key] = data;
264 cache[key] = data;
265 query.callback({results: data.results});
265 query.callback({results: data.results});
266 }
266 }
267 })
267 })
268 }
268 }
269 }
269 }
270 });
270 });
271 $('#branch_filter').on('change', function(e){
271 $('#branch_filter').on('change', function(e){
272 var data = $('#branch_filter').select2('data');
272 var data = $('#branch_filter').select2('data');
273 var selected = data.text;
273 var selected = data.text;
274 var filter = {'repo_name': '${c.repo_name}'};
274 var filter = {'repo_name': '${c.repo_name}'};
275 if(data.type == 'branch' || data.type == 'branch_closed'){
275 if(data.type == 'branch' || data.type == 'branch_closed'){
276 filter["branch"] = selected;
276 filter["branch"] = selected;
277 }
277 }
278 else if (data.type == 'book'){
278 else if (data.type == 'book'){
279 filter["bookmark"] = selected;
279 filter["bookmark"] = selected;
280 }
280 }
281 window.location = pyroutes.url('repo_changelog', filter);
281 window.location = pyroutes.url('repo_changelog', filter);
282 });
282 });
283
283
284 commitsController = new CommitsController();
284 commitsController = new CommitsController();
285 % if not c.changelog_for_path:
285 % if not c.changelog_for_path:
286 commitsController.reloadGraph();
286 commitsController.reloadGraph();
287 % endif
287 % endif
288
288
289 });
289 });
290
290
291 </script>
291 </script>
292 </div>
292 </div>
293 % else:
293 % else:
294 ${_('There are no changes yet')}
294 ${_('There are no changes yet')}
295 % endif
295 % endif
296 </div>
296 </div>
297 </%def>
297 </%def>
@@ -1,75 +1,75 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('%s Files Delete') % c.repo_name}
4 ${_('%s Files Delete') % c.repo_name}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="menu_bar_nav()">
10 <%def name="menu_bar_nav()">
11 ${self.menu_items(active='repositories')}
11 ${self.menu_items(active='repositories')}
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()">
14 <%def name="breadcrumbs_links()">
15 ${_('Delete file')} @ ${h.show_id(c.commit)}
15 ${_('Delete file')} @ ${h.show_id(c.commit)}
16 </%def>
16 </%def>
17
17
18 <%def name="menu_bar_subnav()">
18 <%def name="menu_bar_subnav()">
19 ${self.repo_menu(active='files')}
19 ${self.repo_menu(active='files')}
20 </%def>
20 </%def>
21
21
22 <%def name="main()">
22 <%def name="main()">
23 <div class="box">
23 <div class="box">
24 <div class="title">
24 <div class="title">
25 ${self.repo_page_title(c.rhodecode_db_repo)}
25 ${self.repo_page_title(c.rhodecode_db_repo)}
26 </div>
26 </div>
27 <div class="edit-file-title">
27 <div class="edit-file-title">
28 ${self.breadcrumbs()}
28 ${self.breadcrumbs()}
29 </div>
29 </div>
30 ${h.secure_form(h.route_path('repo_files_delete_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', method='POST', class_="form-horizontal")}
30 ${h.secure_form(h.route_path('repo_files_delete_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', method='POST', class_="form-horizontal", request=request)}
31 <div class="edit-file-fieldset">
31 <div class="edit-file-fieldset">
32 <div class="fieldset">
32 <div class="fieldset">
33 <div id="destination-label" class="left-label">
33 <div id="destination-label" class="left-label">
34 ${_('Path')}:
34 ${_('Path')}:
35 </div>
35 </div>
36 <div class="right-content">
36 <div class="right-content">
37 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path)}</span>
37 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path)}</span>
38 </div>
38 </div>
39 </div>
39 </div>
40 </div>
40 </div>
41
41
42 <div id="codeblock" class="codeblock delete-file-preview">
42 <div id="codeblock" class="codeblock delete-file-preview">
43 <div class="code-body">
43 <div class="code-body">
44 %if c.file.is_binary:
44 %if c.file.is_binary:
45 ${_('Binary file (%s)') % c.file.mimetype}
45 ${_('Binary file (%s)') % c.file.mimetype}
46 %else:
46 %else:
47 %if c.file.size < c.visual.cut_off_limit_file:
47 %if c.file.size < c.visual.cut_off_limit_file:
48 ${h.pygmentize(c.file,linenos=True,anchorlinenos=False,cssclass="code-highlight")}
48 ${h.pygmentize(c.file,linenos=True,anchorlinenos=False,cssclass="code-highlight")}
49 %else:
49 %else:
50 ${_('File size {} is bigger then allowed limit {}. ').format(h.format_byte_size_binary(c.file.size), h.format_byte_size_binary(c.visual.cut_off_limit_file))} ${h.link_to(_('Show as raw'),
50 ${_('File size {} is bigger then allowed limit {}. ').format(h.format_byte_size_binary(c.file.size), h.format_byte_size_binary(c.visual.cut_off_limit_file))} ${h.link_to(_('Show as raw'),
51 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
51 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
52 %endif
52 %endif
53 %endif
53 %endif
54 </div>
54 </div>
55 </div>
55 </div>
56
56
57 <div class="edit-file-fieldset">
57 <div class="edit-file-fieldset">
58 <div class="fieldset">
58 <div class="fieldset">
59 <div id="commit-message-label" class="commit-message-label left-label">
59 <div id="commit-message-label" class="commit-message-label left-label">
60 ${_('Commit Message')}:
60 ${_('Commit Message')}:
61 </div>
61 </div>
62 <div class="right-content">
62 <div class="right-content">
63 <div class="message">
63 <div class="message">
64 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
64 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
65 </div>
65 </div>
66 </div>
66 </div>
67 </div>
67 </div>
68 <div class="pull-right">
68 <div class="pull-right">
69 ${h.reset('reset',_('Cancel'),class_="btn btn-small btn-danger")}
69 ${h.reset('reset',_('Cancel'),class_="btn btn-small btn-danger")}
70 ${h.submit('commit',_('Delete File'),class_="btn btn-small btn-danger-action")}
70 ${h.submit('commit',_('Delete File'),class_="btn btn-small btn-danger-action")}
71 </div>
71 </div>
72 </div>
72 </div>
73 ${h.end_form()}
73 ${h.end_form()}
74 </div>
74 </div>
75 </%def>
75 </%def>
@@ -1,197 +1,197 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('%s File Edit') % c.repo_name}
4 ${_('%s File Edit') % c.repo_name}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="menu_bar_nav()">
10 <%def name="menu_bar_nav()">
11 ${self.menu_items(active='repositories')}
11 ${self.menu_items(active='repositories')}
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()">
14 <%def name="breadcrumbs_links()">
15 ${_('Edit file')} @ ${h.show_id(c.commit)}
15 ${_('Edit file')} @ ${h.show_id(c.commit)}
16 </%def>
16 </%def>
17
17
18 <%def name="menu_bar_subnav()">
18 <%def name="menu_bar_subnav()">
19 ${self.repo_menu(active='files')}
19 ${self.repo_menu(active='files')}
20 </%def>
20 </%def>
21
21
22 <%def name="main()">
22 <%def name="main()">
23 <% renderer = h.renderer_from_filename(c.f_path)%>
23 <% renderer = h.renderer_from_filename(c.f_path)%>
24 <div class="box">
24 <div class="box">
25 <div class="title">
25 <div class="title">
26 ${self.repo_page_title(c.rhodecode_db_repo)}
26 ${self.repo_page_title(c.rhodecode_db_repo)}
27 </div>
27 </div>
28 <div class="edit-file-title">
28 <div class="edit-file-title">
29 ${self.breadcrumbs()}
29 ${self.breadcrumbs()}
30 </div>
30 </div>
31 <div class="edit-file-fieldset">
31 <div class="edit-file-fieldset">
32 <div class="fieldset">
32 <div class="fieldset">
33 <div id="destination-label" class="left-label">
33 <div id="destination-label" class="left-label">
34 ${_('Path')}:
34 ${_('Path')}:
35 </div>
35 </div>
36 <div class="right-content">
36 <div class="right-content">
37 <div id="specify-custom-path-container">
37 <div id="specify-custom-path-container">
38 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path)}</span>
38 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path)}</span>
39 </div>
39 </div>
40 </div>
40 </div>
41 </div>
41 </div>
42 </div>
42 </div>
43
43
44 <div class="table">
44 <div class="table">
45 ${h.secure_form(h.route_path('repo_files_update_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', method='POST')}
45 ${h.secure_form(h.route_path('repo_files_update_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', method='POST', request=request)}
46 <div id="codeblock" class="codeblock" >
46 <div id="codeblock" class="codeblock" >
47 <div class="code-header">
47 <div class="code-header">
48 <div class="stats">
48 <div class="stats">
49 <i class="icon-file"></i>
49 <i class="icon-file"></i>
50 <span class="item">${h.link_to("r%s:%s" % (c.file.commit.idx,h.short_id(c.file.commit.raw_id)),h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.file.commit.raw_id))}</span>
50 <span class="item">${h.link_to("r%s:%s" % (c.file.commit.idx,h.short_id(c.file.commit.raw_id)),h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.file.commit.raw_id))}</span>
51 <span class="item">${h.format_byte_size_binary(c.file.size)}</span>
51 <span class="item">${h.format_byte_size_binary(c.file.size)}</span>
52 <span class="item last">${c.file.mimetype}</span>
52 <span class="item last">${c.file.mimetype}</span>
53 <div class="buttons">
53 <div class="buttons">
54 <a class="btn btn-mini" href="${h.route_path('repo_changelog_file',repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">
54 <a class="btn btn-mini" href="${h.route_path('repo_changelog_file',repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">
55 <i class="icon-time"></i> ${_('history')}
55 <i class="icon-time"></i> ${_('history')}
56 </a>
56 </a>
57
57
58 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
58 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
59 % if not c.file.is_binary:
59 % if not c.file.is_binary:
60 %if True:
60 %if True:
61 ${h.link_to(_('source'), h.route_path('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),class_="btn btn-mini")}
61 ${h.link_to(_('source'), h.route_path('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),class_="btn btn-mini")}
62 %else:
62 %else:
63 ${h.link_to(_('annotation'),h.route_path('repo_files:annotated',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),class_="btn btn-mini")}
63 ${h.link_to(_('annotation'),h.route_path('repo_files:annotated',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),class_="btn btn-mini")}
64 %endif
64 %endif
65
65
66 <a class="btn btn-mini" href="${h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
66 <a class="btn btn-mini" href="${h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
67 ${_('raw')}
67 ${_('raw')}
68 </a>
68 </a>
69 <a class="btn btn-mini" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
69 <a class="btn btn-mini" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
70 <i class="icon-archive"></i> ${_('download')}
70 <i class="icon-archive"></i> ${_('download')}
71 </a>
71 </a>
72 % endif
72 % endif
73 % endif
73 % endif
74 </div>
74 </div>
75 </div>
75 </div>
76 <div class="form">
76 <div class="form">
77 <label for="set_mode">${_('Editing file')}:</label>
77 <label for="set_mode">${_('Editing file')}:</label>
78 ${'%s /' % c.file.dir_path if c.file.dir_path else c.file.dir_path}
78 ${'%s /' % c.file.dir_path if c.file.dir_path else c.file.dir_path}
79 <input id="filename" type="text" name="filename" value="${c.file.name}">
79 <input id="filename" type="text" name="filename" value="${c.file.name}">
80
80
81 ${h.dropdownmenu('set_mode','plain',[('plain',_('plain'))],enable_filter=True)}
81 ${h.dropdownmenu('set_mode','plain',[('plain',_('plain'))],enable_filter=True)}
82 <label for="line_wrap">${_('line wraps')}</label>
82 <label for="line_wrap">${_('line wraps')}</label>
83 ${h.dropdownmenu('line_wrap', 'off', [('on', _('on')), ('off', _('off')),])}
83 ${h.dropdownmenu('line_wrap', 'off', [('on', _('on')), ('off', _('off')),])}
84
84
85 <div id="render_preview" class="btn btn-small preview hidden">${_('Preview')}</div>
85 <div id="render_preview" class="btn btn-small preview hidden">${_('Preview')}</div>
86 </div>
86 </div>
87 </div>
87 </div>
88 <div id="editor_container">
88 <div id="editor_container">
89 <pre id="editor_pre"></pre>
89 <pre id="editor_pre"></pre>
90 <textarea id="editor" name="content" >${h.escape(c.file.content)|n}</textarea>
90 <textarea id="editor" name="content" >${h.escape(c.file.content)|n}</textarea>
91 <div id="editor_preview" ></div>
91 <div id="editor_preview" ></div>
92 </div>
92 </div>
93 </div>
93 </div>
94 </div>
94 </div>
95
95
96 <div class="edit-file-fieldset">
96 <div class="edit-file-fieldset">
97 <div class="fieldset">
97 <div class="fieldset">
98 <div id="commit-message-label" class="commit-message-label left-label">
98 <div id="commit-message-label" class="commit-message-label left-label">
99 ${_('Commit Message')}:
99 ${_('Commit Message')}:
100 </div>
100 </div>
101 <div class="right-content">
101 <div class="right-content">
102 <div class="message">
102 <div class="message">
103 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
103 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
104 </div>
104 </div>
105 </div>
105 </div>
106 </div>
106 </div>
107 <div class="pull-right">
107 <div class="pull-right">
108 ${h.reset('reset',_('Cancel'),class_="btn btn-small")}
108 ${h.reset('reset',_('Cancel'),class_="btn btn-small")}
109 ${h.submit('commit',_('Commit changes'),class_="btn btn-small btn-success")}
109 ${h.submit('commit',_('Commit changes'),class_="btn btn-small btn-success")}
110 </div>
110 </div>
111 </div>
111 </div>
112 ${h.end_form()}
112 ${h.end_form()}
113 </div>
113 </div>
114
114
115 <script type="text/javascript">
115 <script type="text/javascript">
116 $(document).ready(function(){
116 $(document).ready(function(){
117 var renderer = "${renderer}";
117 var renderer = "${renderer}";
118 var reset_url = "${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.file.path)}";
118 var reset_url = "${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.file.path)}";
119 var myCodeMirror = initCodeMirror('editor', reset_url);
119 var myCodeMirror = initCodeMirror('editor', reset_url);
120
120
121 var modes_select = $('#set_mode');
121 var modes_select = $('#set_mode');
122 fillCodeMirrorOptions(modes_select);
122 fillCodeMirrorOptions(modes_select);
123
123
124 // try to detect the mode based on the file we edit
124 // try to detect the mode based on the file we edit
125 var mimetype = "${c.file.mimetype}";
125 var mimetype = "${c.file.mimetype}";
126 var detected_mode = detectCodeMirrorMode(
126 var detected_mode = detectCodeMirrorMode(
127 "${c.file.name}", mimetype);
127 "${c.file.name}", mimetype);
128
128
129 if(detected_mode){
129 if(detected_mode){
130 setCodeMirrorMode(myCodeMirror, detected_mode);
130 setCodeMirrorMode(myCodeMirror, detected_mode);
131 $(modes_select).select2("val", mimetype);
131 $(modes_select).select2("val", mimetype);
132 $(modes_select).change();
132 $(modes_select).change();
133 setCodeMirrorMode(myCodeMirror, detected_mode);
133 setCodeMirrorMode(myCodeMirror, detected_mode);
134 }
134 }
135
135
136 var filename_selector = '#filename';
136 var filename_selector = '#filename';
137 var callback = function(filename, mimetype, mode){
137 var callback = function(filename, mimetype, mode){
138 CodeMirrorPreviewEnable(mode);
138 CodeMirrorPreviewEnable(mode);
139 };
139 };
140 // on change of select field set mode
140 // on change of select field set mode
141 setCodeMirrorModeFromSelect(
141 setCodeMirrorModeFromSelect(
142 modes_select, filename_selector, myCodeMirror, callback);
142 modes_select, filename_selector, myCodeMirror, callback);
143
143
144 // on entering the new filename set mode, from given extension
144 // on entering the new filename set mode, from given extension
145 setCodeMirrorModeFromInput(
145 setCodeMirrorModeFromInput(
146 modes_select, filename_selector, myCodeMirror, callback);
146 modes_select, filename_selector, myCodeMirror, callback);
147
147
148 // if the file is renderable set line wraps automatically
148 // if the file is renderable set line wraps automatically
149 if (renderer !== ""){
149 if (renderer !== ""){
150 var line_wrap = 'on';
150 var line_wrap = 'on';
151 $($('#line_wrap option[value="'+line_wrap+'"]')[0]).attr("selected", "selected");
151 $($('#line_wrap option[value="'+line_wrap+'"]')[0]).attr("selected", "selected");
152 setCodeMirrorLineWrap(myCodeMirror, true);
152 setCodeMirrorLineWrap(myCodeMirror, true);
153 }
153 }
154 // on select line wraps change the editor
154 // on select line wraps change the editor
155 $('#line_wrap').on('change', function(e){
155 $('#line_wrap').on('change', function(e){
156 var selected = e.currentTarget;
156 var selected = e.currentTarget;
157 var line_wraps = {'on': true, 'off': false}[selected.value];
157 var line_wraps = {'on': true, 'off': false}[selected.value];
158 setCodeMirrorLineWrap(myCodeMirror, line_wraps)
158 setCodeMirrorLineWrap(myCodeMirror, line_wraps)
159 });
159 });
160
160
161 // render preview/edit button
161 // render preview/edit button
162 if (mimetype === 'text/x-rst' || mimetype === 'text/plain') {
162 if (mimetype === 'text/x-rst' || mimetype === 'text/plain') {
163 $('#render_preview').removeClass('hidden');
163 $('#render_preview').removeClass('hidden');
164 }
164 }
165 $('#render_preview').on('click', function(e){
165 $('#render_preview').on('click', function(e){
166 if($(this).hasClass('preview')){
166 if($(this).hasClass('preview')){
167 $(this).removeClass('preview');
167 $(this).removeClass('preview');
168 $(this).html("${_('Edit')}");
168 $(this).html("${_('Edit')}");
169 $('#editor_preview').show();
169 $('#editor_preview').show();
170 $(myCodeMirror.getWrapperElement()).hide();
170 $(myCodeMirror.getWrapperElement()).hide();
171
171
172 var possible_renderer = {
172 var possible_renderer = {
173 'rst':'rst',
173 'rst':'rst',
174 'markdown':'markdown',
174 'markdown':'markdown',
175 'gfm': 'markdown'}[myCodeMirror.getMode().name];
175 'gfm': 'markdown'}[myCodeMirror.getMode().name];
176 var _text = myCodeMirror.getValue();
176 var _text = myCodeMirror.getValue();
177 var _renderer = possible_renderer || DEFAULT_RENDERER;
177 var _renderer = possible_renderer || DEFAULT_RENDERER;
178 var post_data = {'text': _text, 'renderer': _renderer, 'csrf_token': CSRF_TOKEN};
178 var post_data = {'text': _text, 'renderer': _renderer, 'csrf_token': CSRF_TOKEN};
179 $('#editor_preview').html(_gettext('Loading ...'));
179 $('#editor_preview').html(_gettext('Loading ...'));
180 var url = pyroutes.url('repo_commit_comment_preview',
180 var url = pyroutes.url('repo_commit_comment_preview',
181 {'repo_name': '${c.repo_name}',
181 {'repo_name': '${c.repo_name}',
182 'commit_id': '${c.commit.raw_id}'});
182 'commit_id': '${c.commit.raw_id}'});
183 ajaxPOST(url, post_data, function(o){
183 ajaxPOST(url, post_data, function(o){
184 $('#editor_preview').html(o);
184 $('#editor_preview').html(o);
185 })
185 })
186 }
186 }
187 else{
187 else{
188 $(this).addClass('preview');
188 $(this).addClass('preview');
189 $(this).html("${_('Preview')}");
189 $(this).html("${_('Preview')}");
190 $('#editor_preview').hide();
190 $('#editor_preview').hide();
191 $(myCodeMirror.getWrapperElement()).show();
191 $(myCodeMirror.getWrapperElement()).show();
192 }
192 }
193 });
193 });
194
194
195 })
195 })
196 </script>
196 </script>
197 </%def>
197 </%def>
@@ -1,526 +1,526 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${c.repo_name} ${_('New pull request')}
4 ${c.repo_name} ${_('New pull request')}
5 </%def>
5 </%def>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${_('New pull request')}
8 ${_('New pull request')}
9 </%def>
9 </%def>
10
10
11 <%def name="menu_bar_nav()">
11 <%def name="menu_bar_nav()">
12 ${self.menu_items(active='repositories')}
12 ${self.menu_items(active='repositories')}
13 </%def>
13 </%def>
14
14
15 <%def name="menu_bar_subnav()">
15 <%def name="menu_bar_subnav()">
16 ${self.repo_menu(active='showpullrequest')}
16 ${self.repo_menu(active='showpullrequest')}
17 </%def>
17 </%def>
18
18
19 <%def name="main()">
19 <%def name="main()">
20 <div class="box">
20 <div class="box">
21 <div class="title">
21 <div class="title">
22 ${self.repo_page_title(c.rhodecode_db_repo)}
22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 </div>
23 </div>
24
24
25 ${h.secure_form(h.url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
25 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name), id='pull_request_form', method='POST', request=request)}
26
26
27 ${self.breadcrumbs()}
27 ${self.breadcrumbs()}
28
28
29 <div class="box pr-summary">
29 <div class="box pr-summary">
30
30
31 <div class="summary-details block-left">
31 <div class="summary-details block-left">
32
32
33
33
34 <div class="pr-details-title">
34 <div class="pr-details-title">
35 ${_('Pull request summary')}
35 ${_('Pull request summary')}
36 </div>
36 </div>
37
37
38 <div class="form" style="padding-top: 10px">
38 <div class="form" style="padding-top: 10px">
39 <!-- fields -->
39 <!-- fields -->
40
40
41 <div class="fields" >
41 <div class="fields" >
42
42
43 <div class="field">
43 <div class="field">
44 <div class="label">
44 <div class="label">
45 <label for="pullrequest_title">${_('Title')}:</label>
45 <label for="pullrequest_title">${_('Title')}:</label>
46 </div>
46 </div>
47 <div class="input">
47 <div class="input">
48 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
48 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
49 </div>
49 </div>
50 </div>
50 </div>
51
51
52 <div class="field">
52 <div class="field">
53 <div class="label label-textarea">
53 <div class="label label-textarea">
54 <label for="pullrequest_desc">${_('Description')}:</label>
54 <label for="pullrequest_desc">${_('Description')}:</label>
55 </div>
55 </div>
56 <div class="textarea text-area editor">
56 <div class="textarea text-area editor">
57 ${h.textarea('pullrequest_desc',size=30, )}
57 ${h.textarea('pullrequest_desc',size=30, )}
58 <span class="help-block">${_('Write a short description on this pull request')}</span>
58 <span class="help-block">${_('Write a short description on this pull request')}</span>
59 </div>
59 </div>
60 </div>
60 </div>
61
61
62 <div class="field">
62 <div class="field">
63 <div class="label label-textarea">
63 <div class="label label-textarea">
64 <label for="pullrequest_desc">${_('Commit flow')}:</label>
64 <label for="pullrequest_desc">${_('Commit flow')}:</label>
65 </div>
65 </div>
66
66
67 ## TODO: johbo: Abusing the "content" class here to get the
67 ## TODO: johbo: Abusing the "content" class here to get the
68 ## desired effect. Should be replaced by a proper solution.
68 ## desired effect. Should be replaced by a proper solution.
69
69
70 ##ORG
70 ##ORG
71 <div class="content">
71 <div class="content">
72 <strong>${_('Source repository')}:</strong>
72 <strong>${_('Source repository')}:</strong>
73 ${c.rhodecode_db_repo.description}
73 ${c.rhodecode_db_repo.description}
74 </div>
74 </div>
75 <div class="content">
75 <div class="content">
76 ${h.hidden('source_repo')}
76 ${h.hidden('source_repo')}
77 ${h.hidden('source_ref')}
77 ${h.hidden('source_ref')}
78 </div>
78 </div>
79
79
80 ##OTHER, most Probably the PARENT OF THIS FORK
80 ##OTHER, most Probably the PARENT OF THIS FORK
81 <div class="content">
81 <div class="content">
82 ## filled with JS
82 ## filled with JS
83 <div id="target_repo_desc"></div>
83 <div id="target_repo_desc"></div>
84 </div>
84 </div>
85
85
86 <div class="content">
86 <div class="content">
87 ${h.hidden('target_repo')}
87 ${h.hidden('target_repo')}
88 ${h.hidden('target_ref')}
88 ${h.hidden('target_ref')}
89 <span id="target_ref_loading" style="display: none">
89 <span id="target_ref_loading" style="display: none">
90 ${_('Loading refs...')}
90 ${_('Loading refs...')}
91 </span>
91 </span>
92 </div>
92 </div>
93 </div>
93 </div>
94
94
95 <div class="field">
95 <div class="field">
96 <div class="label label-textarea">
96 <div class="label label-textarea">
97 <label for="pullrequest_submit"></label>
97 <label for="pullrequest_submit"></label>
98 </div>
98 </div>
99 <div class="input">
99 <div class="input">
100 <div class="pr-submit-button">
100 <div class="pr-submit-button">
101 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
101 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
102 </div>
102 </div>
103 <div id="pr_open_message"></div>
103 <div id="pr_open_message"></div>
104 </div>
104 </div>
105 </div>
105 </div>
106
106
107 <div class="pr-spacing-container"></div>
107 <div class="pr-spacing-container"></div>
108 </div>
108 </div>
109 </div>
109 </div>
110 </div>
110 </div>
111 <div>
111 <div>
112 ## AUTHOR
112 ## AUTHOR
113 <div class="reviewers-title block-right">
113 <div class="reviewers-title block-right">
114 <div class="pr-details-title">
114 <div class="pr-details-title">
115 ${_('Author of this pull request')}
115 ${_('Author of this pull request')}
116 </div>
116 </div>
117 </div>
117 </div>
118 <div class="block-right pr-details-content reviewers">
118 <div class="block-right pr-details-content reviewers">
119 <ul class="group_members">
119 <ul class="group_members">
120 <li>
120 <li>
121 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
121 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
122 </li>
122 </li>
123 </ul>
123 </ul>
124 </div>
124 </div>
125
125
126 ## REVIEW RULES
126 ## REVIEW RULES
127 <div id="review_rules" style="display: none" class="reviewers-title block-right">
127 <div id="review_rules" style="display: none" class="reviewers-title block-right">
128 <div class="pr-details-title">
128 <div class="pr-details-title">
129 ${_('Reviewer rules')}
129 ${_('Reviewer rules')}
130 </div>
130 </div>
131 <div class="pr-reviewer-rules">
131 <div class="pr-reviewer-rules">
132 ## review rules will be appended here, by default reviewers logic
132 ## review rules will be appended here, by default reviewers logic
133 </div>
133 </div>
134 </div>
134 </div>
135
135
136 ## REVIEWERS
136 ## REVIEWERS
137 <div class="reviewers-title block-right">
137 <div class="reviewers-title block-right">
138 <div class="pr-details-title">
138 <div class="pr-details-title">
139 ${_('Pull request reviewers')}
139 ${_('Pull request reviewers')}
140 <span class="calculate-reviewers"> - ${_('loading...')}</span>
140 <span class="calculate-reviewers"> - ${_('loading...')}</span>
141 </div>
141 </div>
142 </div>
142 </div>
143 <div id="reviewers" class="block-right pr-details-content reviewers">
143 <div id="reviewers" class="block-right pr-details-content reviewers">
144 ## members goes here, filled via JS based on initial selection !
144 ## members goes here, filled via JS based on initial selection !
145 <input type="hidden" name="__start__" value="review_members:sequence">
145 <input type="hidden" name="__start__" value="review_members:sequence">
146 <ul id="review_members" class="group_members"></ul>
146 <ul id="review_members" class="group_members"></ul>
147 <input type="hidden" name="__end__" value="review_members:sequence">
147 <input type="hidden" name="__end__" value="review_members:sequence">
148 <div id="add_reviewer_input" class='ac'>
148 <div id="add_reviewer_input" class='ac'>
149 <div class="reviewer_ac">
149 <div class="reviewer_ac">
150 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
150 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
151 <div id="reviewers_container"></div>
151 <div id="reviewers_container"></div>
152 </div>
152 </div>
153 </div>
153 </div>
154 </div>
154 </div>
155 </div>
155 </div>
156 </div>
156 </div>
157 <div class="box">
157 <div class="box">
158 <div>
158 <div>
159 ## overview pulled by ajax
159 ## overview pulled by ajax
160 <div id="pull_request_overview"></div>
160 <div id="pull_request_overview"></div>
161 </div>
161 </div>
162 </div>
162 </div>
163 ${h.end_form()}
163 ${h.end_form()}
164 </div>
164 </div>
165
165
166 <script type="text/javascript">
166 <script type="text/javascript">
167 $(function(){
167 $(function(){
168 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
168 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
169 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
169 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
170 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
170 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
171 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
171 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
172
172
173 var $pullRequestForm = $('#pull_request_form');
173 var $pullRequestForm = $('#pull_request_form');
174 var $sourceRepo = $('#source_repo', $pullRequestForm);
174 var $sourceRepo = $('#source_repo', $pullRequestForm);
175 var $targetRepo = $('#target_repo', $pullRequestForm);
175 var $targetRepo = $('#target_repo', $pullRequestForm);
176 var $sourceRef = $('#source_ref', $pullRequestForm);
176 var $sourceRef = $('#source_ref', $pullRequestForm);
177 var $targetRef = $('#target_ref', $pullRequestForm);
177 var $targetRef = $('#target_ref', $pullRequestForm);
178
178
179 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
179 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
180 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
180 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
181
181
182 var targetRepo = function() { return $targetRepo.eq(0).val() };
182 var targetRepo = function() { return $targetRepo.eq(0).val() };
183 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
183 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
184
184
185 var calculateContainerWidth = function() {
185 var calculateContainerWidth = function() {
186 var maxWidth = 0;
186 var maxWidth = 0;
187 var repoSelect2Containers = ['#source_repo', '#target_repo'];
187 var repoSelect2Containers = ['#source_repo', '#target_repo'];
188 $.each(repoSelect2Containers, function(idx, value) {
188 $.each(repoSelect2Containers, function(idx, value) {
189 $(value).select2('container').width('auto');
189 $(value).select2('container').width('auto');
190 var curWidth = $(value).select2('container').width();
190 var curWidth = $(value).select2('container').width();
191 if (maxWidth <= curWidth) {
191 if (maxWidth <= curWidth) {
192 maxWidth = curWidth;
192 maxWidth = curWidth;
193 }
193 }
194 $.each(repoSelect2Containers, function(idx, value) {
194 $.each(repoSelect2Containers, function(idx, value) {
195 $(value).select2('container').width(maxWidth + 10);
195 $(value).select2('container').width(maxWidth + 10);
196 });
196 });
197 });
197 });
198 };
198 };
199
199
200 var initRefSelection = function(selectedRef) {
200 var initRefSelection = function(selectedRef) {
201 return function(element, callback) {
201 return function(element, callback) {
202 // translate our select2 id into a text, it's a mapping to show
202 // translate our select2 id into a text, it's a mapping to show
203 // simple label when selecting by internal ID.
203 // simple label when selecting by internal ID.
204 var id, refData;
204 var id, refData;
205 if (selectedRef === undefined) {
205 if (selectedRef === undefined) {
206 id = element.val();
206 id = element.val();
207 refData = element.val().split(':');
207 refData = element.val().split(':');
208 } else {
208 } else {
209 id = selectedRef;
209 id = selectedRef;
210 refData = selectedRef.split(':');
210 refData = selectedRef.split(':');
211 }
211 }
212
212
213 var text = refData[1];
213 var text = refData[1];
214 if (refData[0] === 'rev') {
214 if (refData[0] === 'rev') {
215 text = text.substring(0, 12);
215 text = text.substring(0, 12);
216 }
216 }
217
217
218 var data = {id: id, text: text};
218 var data = {id: id, text: text};
219
219
220 callback(data);
220 callback(data);
221 };
221 };
222 };
222 };
223
223
224 var formatRefSelection = function(item) {
224 var formatRefSelection = function(item) {
225 var prefix = '';
225 var prefix = '';
226 var refData = item.id.split(':');
226 var refData = item.id.split(':');
227 if (refData[0] === 'branch') {
227 if (refData[0] === 'branch') {
228 prefix = '<i class="icon-branch"></i>';
228 prefix = '<i class="icon-branch"></i>';
229 }
229 }
230 else if (refData[0] === 'book') {
230 else if (refData[0] === 'book') {
231 prefix = '<i class="icon-bookmark"></i>';
231 prefix = '<i class="icon-bookmark"></i>';
232 }
232 }
233 else if (refData[0] === 'tag') {
233 else if (refData[0] === 'tag') {
234 prefix = '<i class="icon-tag"></i>';
234 prefix = '<i class="icon-tag"></i>';
235 }
235 }
236
236
237 var originalOption = item.element;
237 var originalOption = item.element;
238 return prefix + item.text;
238 return prefix + item.text;
239 };
239 };
240
240
241 // custom code mirror
241 // custom code mirror
242 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
242 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
243
243
244 reviewersController = new ReviewersController();
244 reviewersController = new ReviewersController();
245
245
246 var queryTargetRepo = function(self, query) {
246 var queryTargetRepo = function(self, query) {
247 // cache ALL results if query is empty
247 // cache ALL results if query is empty
248 var cacheKey = query.term || '__';
248 var cacheKey = query.term || '__';
249 var cachedData = self.cachedDataSource[cacheKey];
249 var cachedData = self.cachedDataSource[cacheKey];
250
250
251 if (cachedData) {
251 if (cachedData) {
252 query.callback({results: cachedData.results});
252 query.callback({results: cachedData.results});
253 } else {
253 } else {
254 $.ajax({
254 $.ajax({
255 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
255 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
256 data: {query: query.term},
256 data: {query: query.term},
257 dataType: 'json',
257 dataType: 'json',
258 type: 'GET',
258 type: 'GET',
259 success: function(data) {
259 success: function(data) {
260 self.cachedDataSource[cacheKey] = data;
260 self.cachedDataSource[cacheKey] = data;
261 query.callback({results: data.results});
261 query.callback({results: data.results});
262 },
262 },
263 error: function(data, textStatus, errorThrown) {
263 error: function(data, textStatus, errorThrown) {
264 alert(
264 alert(
265 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
265 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
266 }
266 }
267 });
267 });
268 }
268 }
269 };
269 };
270
270
271 var queryTargetRefs = function(initialData, query) {
271 var queryTargetRefs = function(initialData, query) {
272 var data = {results: []};
272 var data = {results: []};
273 // filter initialData
273 // filter initialData
274 $.each(initialData, function() {
274 $.each(initialData, function() {
275 var section = this.text;
275 var section = this.text;
276 var children = [];
276 var children = [];
277 $.each(this.children, function() {
277 $.each(this.children, function() {
278 if (query.term.length === 0 ||
278 if (query.term.length === 0 ||
279 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
279 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
280 children.push({'id': this.id, 'text': this.text})
280 children.push({'id': this.id, 'text': this.text})
281 }
281 }
282 });
282 });
283 data.results.push({'text': section, 'children': children})
283 data.results.push({'text': section, 'children': children})
284 });
284 });
285 query.callback({results: data.results});
285 query.callback({results: data.results});
286 };
286 };
287
287
288 var loadRepoRefDiffPreview = function() {
288 var loadRepoRefDiffPreview = function() {
289
289
290 var url_data = {
290 var url_data = {
291 'repo_name': targetRepo(),
291 'repo_name': targetRepo(),
292 'target_repo': sourceRepo(),
292 'target_repo': sourceRepo(),
293 'source_ref': targetRef()[2],
293 'source_ref': targetRef()[2],
294 'source_ref_type': 'rev',
294 'source_ref_type': 'rev',
295 'target_ref': sourceRef()[2],
295 'target_ref': sourceRef()[2],
296 'target_ref_type': 'rev',
296 'target_ref_type': 'rev',
297 'merge': true,
297 'merge': true,
298 '_': Date.now() // bypass browser caching
298 '_': Date.now() // bypass browser caching
299 }; // gather the source/target ref and repo here
299 }; // gather the source/target ref and repo here
300
300
301 if (sourceRef().length !== 3 || targetRef().length !== 3) {
301 if (sourceRef().length !== 3 || targetRef().length !== 3) {
302 prButtonLock(true, "${_('Please select source and target')}");
302 prButtonLock(true, "${_('Please select source and target')}");
303 return;
303 return;
304 }
304 }
305 var url = pyroutes.url('repo_compare', url_data);
305 var url = pyroutes.url('repo_compare', url_data);
306
306
307 // lock PR button, so we cannot send PR before it's calculated
307 // lock PR button, so we cannot send PR before it's calculated
308 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
308 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
309
309
310 if (loadRepoRefDiffPreview._currentRequest) {
310 if (loadRepoRefDiffPreview._currentRequest) {
311 loadRepoRefDiffPreview._currentRequest.abort();
311 loadRepoRefDiffPreview._currentRequest.abort();
312 }
312 }
313
313
314 loadRepoRefDiffPreview._currentRequest = $.get(url)
314 loadRepoRefDiffPreview._currentRequest = $.get(url)
315 .error(function(data, textStatus, errorThrown) {
315 .error(function(data, textStatus, errorThrown) {
316 alert(
316 alert(
317 "Error while processing request.\nError code {0} ({1}).".format(
317 "Error while processing request.\nError code {0} ({1}).".format(
318 data.status, data.statusText));
318 data.status, data.statusText));
319 })
319 })
320 .done(function(data) {
320 .done(function(data) {
321 loadRepoRefDiffPreview._currentRequest = null;
321 loadRepoRefDiffPreview._currentRequest = null;
322 $('#pull_request_overview').html(data);
322 $('#pull_request_overview').html(data);
323
323
324 var commitElements = $(data).find('tr[commit_id]');
324 var commitElements = $(data).find('tr[commit_id]');
325
325
326 var prTitleAndDesc = getTitleAndDescription(
326 var prTitleAndDesc = getTitleAndDescription(
327 sourceRef()[1], commitElements, 5);
327 sourceRef()[1], commitElements, 5);
328
328
329 var title = prTitleAndDesc[0];
329 var title = prTitleAndDesc[0];
330 var proposedDescription = prTitleAndDesc[1];
330 var proposedDescription = prTitleAndDesc[1];
331
331
332 var useGeneratedTitle = (
332 var useGeneratedTitle = (
333 $('#pullrequest_title').hasClass('autogenerated-title') ||
333 $('#pullrequest_title').hasClass('autogenerated-title') ||
334 $('#pullrequest_title').val() === "");
334 $('#pullrequest_title').val() === "");
335
335
336 if (title && useGeneratedTitle) {
336 if (title && useGeneratedTitle) {
337 // use generated title if we haven't specified our own
337 // use generated title if we haven't specified our own
338 $('#pullrequest_title').val(title);
338 $('#pullrequest_title').val(title);
339 $('#pullrequest_title').addClass('autogenerated-title');
339 $('#pullrequest_title').addClass('autogenerated-title');
340
340
341 }
341 }
342
342
343 var useGeneratedDescription = (
343 var useGeneratedDescription = (
344 !codeMirrorInstance._userDefinedDesc ||
344 !codeMirrorInstance._userDefinedDesc ||
345 codeMirrorInstance.getValue() === "");
345 codeMirrorInstance.getValue() === "");
346
346
347 if (proposedDescription && useGeneratedDescription) {
347 if (proposedDescription && useGeneratedDescription) {
348 // set proposed content, if we haven't defined our own,
348 // set proposed content, if we haven't defined our own,
349 // or we don't have description written
349 // or we don't have description written
350 codeMirrorInstance._userDefinedDesc = false; // reset state
350 codeMirrorInstance._userDefinedDesc = false; // reset state
351 codeMirrorInstance.setValue(proposedDescription);
351 codeMirrorInstance.setValue(proposedDescription);
352 }
352 }
353
353
354 var msg = '';
354 var msg = '';
355 if (commitElements.length === 1) {
355 if (commitElements.length === 1) {
356 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
356 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
357 } else {
357 } else {
358 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
358 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
359 }
359 }
360
360
361 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
361 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
362
362
363 if (commitElements.length) {
363 if (commitElements.length) {
364 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
364 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
365 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
365 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
366 }
366 }
367 else {
367 else {
368 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
368 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
369 }
369 }
370
370
371
371
372 });
372 });
373 };
373 };
374
374
375 var Select2Box = function(element, overrides) {
375 var Select2Box = function(element, overrides) {
376 var globalDefaults = {
376 var globalDefaults = {
377 dropdownAutoWidth: true,
377 dropdownAutoWidth: true,
378 containerCssClass: "drop-menu",
378 containerCssClass: "drop-menu",
379 dropdownCssClass: "drop-menu-dropdown"
379 dropdownCssClass: "drop-menu-dropdown"
380 };
380 };
381
381
382 var initSelect2 = function(defaultOptions) {
382 var initSelect2 = function(defaultOptions) {
383 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
383 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
384 element.select2(options);
384 element.select2(options);
385 };
385 };
386
386
387 return {
387 return {
388 initRef: function() {
388 initRef: function() {
389 var defaultOptions = {
389 var defaultOptions = {
390 minimumResultsForSearch: 5,
390 minimumResultsForSearch: 5,
391 formatSelection: formatRefSelection
391 formatSelection: formatRefSelection
392 };
392 };
393
393
394 initSelect2(defaultOptions);
394 initSelect2(defaultOptions);
395 },
395 },
396
396
397 initRepo: function(defaultValue, readOnly) {
397 initRepo: function(defaultValue, readOnly) {
398 var defaultOptions = {
398 var defaultOptions = {
399 initSelection : function (element, callback) {
399 initSelection : function (element, callback) {
400 var data = {id: defaultValue, text: defaultValue};
400 var data = {id: defaultValue, text: defaultValue};
401 callback(data);
401 callback(data);
402 }
402 }
403 };
403 };
404
404
405 initSelect2(defaultOptions);
405 initSelect2(defaultOptions);
406
406
407 element.select2('val', defaultSourceRepo);
407 element.select2('val', defaultSourceRepo);
408 if (readOnly === true) {
408 if (readOnly === true) {
409 element.select2('readonly', true);
409 element.select2('readonly', true);
410 }
410 }
411 }
411 }
412 };
412 };
413 };
413 };
414
414
415 var initTargetRefs = function(refsData, selectedRef){
415 var initTargetRefs = function(refsData, selectedRef){
416 Select2Box($targetRef, {
416 Select2Box($targetRef, {
417 query: function(query) {
417 query: function(query) {
418 queryTargetRefs(refsData, query);
418 queryTargetRefs(refsData, query);
419 },
419 },
420 initSelection : initRefSelection(selectedRef)
420 initSelection : initRefSelection(selectedRef)
421 }).initRef();
421 }).initRef();
422
422
423 if (!(selectedRef === undefined)) {
423 if (!(selectedRef === undefined)) {
424 $targetRef.select2('val', selectedRef);
424 $targetRef.select2('val', selectedRef);
425 }
425 }
426 };
426 };
427
427
428 var targetRepoChanged = function(repoData) {
428 var targetRepoChanged = function(repoData) {
429 // generate new DESC of target repo displayed next to select
429 // generate new DESC of target repo displayed next to select
430 $('#target_repo_desc').html(
430 $('#target_repo_desc').html(
431 "<strong>${_('Target repository')}</strong>: {0}".format(repoData['description'])
431 "<strong>${_('Target repository')}</strong>: {0}".format(repoData['description'])
432 );
432 );
433
433
434 // generate dynamic select2 for refs.
434 // generate dynamic select2 for refs.
435 initTargetRefs(repoData['refs']['select2_refs'],
435 initTargetRefs(repoData['refs']['select2_refs'],
436 repoData['refs']['selected_ref']);
436 repoData['refs']['selected_ref']);
437
437
438 };
438 };
439
439
440 var sourceRefSelect2 = Select2Box($sourceRef, {
440 var sourceRefSelect2 = Select2Box($sourceRef, {
441 placeholder: "${_('Select commit reference')}",
441 placeholder: "${_('Select commit reference')}",
442 query: function(query) {
442 query: function(query) {
443 var initialData = defaultSourceRepoData['refs']['select2_refs'];
443 var initialData = defaultSourceRepoData['refs']['select2_refs'];
444 queryTargetRefs(initialData, query)
444 queryTargetRefs(initialData, query)
445 },
445 },
446 initSelection: initRefSelection()
446 initSelection: initRefSelection()
447 }
447 }
448 );
448 );
449
449
450 var sourceRepoSelect2 = Select2Box($sourceRepo, {
450 var sourceRepoSelect2 = Select2Box($sourceRepo, {
451 query: function(query) {}
451 query: function(query) {}
452 });
452 });
453
453
454 var targetRepoSelect2 = Select2Box($targetRepo, {
454 var targetRepoSelect2 = Select2Box($targetRepo, {
455 cachedDataSource: {},
455 cachedDataSource: {},
456 query: $.debounce(250, function(query) {
456 query: $.debounce(250, function(query) {
457 queryTargetRepo(this, query);
457 queryTargetRepo(this, query);
458 }),
458 }),
459 formatResult: formatResult
459 formatResult: formatResult
460 });
460 });
461
461
462 sourceRefSelect2.initRef();
462 sourceRefSelect2.initRef();
463
463
464 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
464 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
465
465
466 targetRepoSelect2.initRepo(defaultTargetRepo, false);
466 targetRepoSelect2.initRepo(defaultTargetRepo, false);
467
467
468 $sourceRef.on('change', function(e){
468 $sourceRef.on('change', function(e){
469 loadRepoRefDiffPreview();
469 loadRepoRefDiffPreview();
470 reviewersController.loadDefaultReviewers(
470 reviewersController.loadDefaultReviewers(
471 sourceRepo(), sourceRef(), targetRepo(), targetRef());
471 sourceRepo(), sourceRef(), targetRepo(), targetRef());
472 });
472 });
473
473
474 $targetRef.on('change', function(e){
474 $targetRef.on('change', function(e){
475 loadRepoRefDiffPreview();
475 loadRepoRefDiffPreview();
476 reviewersController.loadDefaultReviewers(
476 reviewersController.loadDefaultReviewers(
477 sourceRepo(), sourceRef(), targetRepo(), targetRef());
477 sourceRepo(), sourceRef(), targetRepo(), targetRef());
478 });
478 });
479
479
480 $targetRepo.on('change', function(e){
480 $targetRepo.on('change', function(e){
481 var repoName = $(this).val();
481 var repoName = $(this).val();
482 calculateContainerWidth();
482 calculateContainerWidth();
483 $targetRef.select2('destroy');
483 $targetRef.select2('destroy');
484 $('#target_ref_loading').show();
484 $('#target_ref_loading').show();
485
485
486 $.ajax({
486 $.ajax({
487 url: pyroutes.url('pullrequest_repo_refs',
487 url: pyroutes.url('pullrequest_repo_refs',
488 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
488 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
489 data: {},
489 data: {},
490 dataType: 'json',
490 dataType: 'json',
491 type: 'GET',
491 type: 'GET',
492 success: function(data) {
492 success: function(data) {
493 $('#target_ref_loading').hide();
493 $('#target_ref_loading').hide();
494 targetRepoChanged(data);
494 targetRepoChanged(data);
495 loadRepoRefDiffPreview();
495 loadRepoRefDiffPreview();
496 },
496 },
497 error: function(data, textStatus, errorThrown) {
497 error: function(data, textStatus, errorThrown) {
498 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
498 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
499 }
499 }
500 })
500 })
501
501
502 });
502 });
503
503
504 prButtonLock(true, "${_('Please select source and target')}", 'all');
504 prButtonLock(true, "${_('Please select source and target')}", 'all');
505
505
506 // auto-load on init, the target refs select2
506 // auto-load on init, the target refs select2
507 calculateContainerWidth();
507 calculateContainerWidth();
508 targetRepoChanged(defaultTargetRepoData);
508 targetRepoChanged(defaultTargetRepoData);
509
509
510 $('#pullrequest_title').on('keyup', function(e){
510 $('#pullrequest_title').on('keyup', function(e){
511 $(this).removeClass('autogenerated-title');
511 $(this).removeClass('autogenerated-title');
512 });
512 });
513
513
514 % if c.default_source_ref:
514 % if c.default_source_ref:
515 // in case we have a pre-selected value, use it now
515 // in case we have a pre-selected value, use it now
516 $sourceRef.select2('val', '${c.default_source_ref}');
516 $sourceRef.select2('val', '${c.default_source_ref}');
517 loadRepoRefDiffPreview();
517 loadRepoRefDiffPreview();
518 reviewersController.loadDefaultReviewers(
518 reviewersController.loadDefaultReviewers(
519 sourceRepo(), sourceRef(), targetRepo(), targetRef());
519 sourceRepo(), sourceRef(), targetRepo(), targetRef());
520 % endif
520 % endif
521
521
522 ReviewerAutoComplete('#user');
522 ReviewerAutoComplete('#user');
523 });
523 });
524 </script>
524 </script>
525
525
526 </%def>
526 </%def>
@@ -1,63 +1,63 b''
1
1
2 <div class="pull-request-wrap">
2 <div class="pull-request-wrap">
3
3
4 % if c.pr_merge_possible:
4 % if c.pr_merge_possible:
5 <h2 class="merge-status">
5 <h2 class="merge-status">
6 <span class="merge-icon success"><i class="icon-ok"></i></span>
6 <span class="merge-icon success"><i class="icon-ok"></i></span>
7 ${_('This pull request can be merged automatically.')}
7 ${_('This pull request can be merged automatically.')}
8 </h2>
8 </h2>
9 % else:
9 % else:
10 <h2 class="merge-status">
10 <h2 class="merge-status">
11 <span class="merge-icon warning"><i class="icon-false"></i></span>
11 <span class="merge-icon warning"><i class="icon-false"></i></span>
12 ${_('Merge is not currently possible because of below failed checks.')}
12 ${_('Merge is not currently possible because of below failed checks.')}
13 </h2>
13 </h2>
14 % endif
14 % endif
15
15
16 <ul>
16 <ul>
17 % for pr_check_key, pr_check_details in c.pr_merge_errors.items():
17 % for pr_check_key, pr_check_details in c.pr_merge_errors.items():
18 <% pr_check_type = pr_check_details['error_type'] %>
18 <% pr_check_type = pr_check_details['error_type'] %>
19 <li>
19 <li>
20 <span class="merge-message ${pr_check_type}" data-role="merge-message">
20 <span class="merge-message ${pr_check_type}" data-role="merge-message">
21 - ${pr_check_details['message']}
21 - ${pr_check_details['message']}
22 % if pr_check_key == 'todo':
22 % if pr_check_key == 'todo':
23 % for co in pr_check_details['details']:
23 % for co in pr_check_details['details']:
24 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'), 0, ${h.json.dumps(co.outdated)})"> #${co.comment_id}</a>${'' if loop.last else ','}
24 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'), 0, ${h.json.dumps(co.outdated)})"> #${co.comment_id}</a>${'' if loop.last else ','}
25 % endfor
25 % endfor
26 % endif
26 % endif
27 </span>
27 </span>
28 </li>
28 </li>
29 % endfor
29 % endfor
30 </ul>
30 </ul>
31
31
32 <div class="pull-request-merge-actions">
32 <div class="pull-request-merge-actions">
33 % if c.allowed_to_merge:
33 % if c.allowed_to_merge:
34 <div class="pull-right">
34 <div class="pull-right">
35 ${h.secure_form(h.url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
35 ${h.secure_form(h.route_path('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form', method='POST', request=request)}
36 <% merge_disabled = ' disabled' if c.pr_merge_possible is False else '' %>
36 <% merge_disabled = ' disabled' if c.pr_merge_possible is False else '' %>
37 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
37 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
38 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
38 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
39 ${h.end_form()}
39 ${h.end_form()}
40 </div>
40 </div>
41 % elif c.rhodecode_user.username != h.DEFAULT_USER:
41 % elif c.rhodecode_user.username != h.DEFAULT_USER:
42 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
42 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
43 <input type="submit" value="${_('Merge Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
43 <input type="submit" value="${_('Merge Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
44 % else:
44 % else:
45 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
45 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
46 % endif
46 % endif
47 </div>
47 </div>
48
48
49 % if c.allowed_to_close:
49 % if c.allowed_to_close:
50 ## close PR action, injected later next to COMMENT button
50 ## close PR action, injected later next to COMMENT button
51 <div id="close-pull-request-action" style="display: none">
51 <div id="close-pull-request-action" style="display: none">
52 % if c.pull_request_review_status == c.REVIEW_STATUS_APPROVED:
52 % if c.pull_request_review_status == c.REVIEW_STATUS_APPROVED:
53 <a class="btn btn-approved-status" href="#close-as-approved" onclick="closePullRequest('${c.REVIEW_STATUS_APPROVED}'); return false;">
53 <a class="btn btn-approved-status" href="#close-as-approved" onclick="closePullRequest('${c.REVIEW_STATUS_APPROVED}'); return false;">
54 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_APPROVED))}
54 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_APPROVED))}
55 </a>
55 </a>
56 % else:
56 % else:
57 <a class="btn btn-rejected-status" href="#close-as-rejected" onclick="closePullRequest('${c.REVIEW_STATUS_REJECTED}'); return false;">
57 <a class="btn btn-rejected-status" href="#close-as-rejected" onclick="closePullRequest('${c.REVIEW_STATUS_REJECTED}'); return false;">
58 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_REJECTED))}
58 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_REJECTED))}
59 </a>
59 </a>
60 % endif
60 % endif
61 </div>
61 </div>
62 % endif
62 % endif
63 </div>
63 </div>
@@ -1,860 +1,860 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 %if c.rhodecode_name:
6 %if c.rhodecode_name:
7 &middot; ${h.branding(c.rhodecode_name)}
7 &middot; ${h.branding(c.rhodecode_name)}
8 %endif
8 %endif
9 </%def>
9 </%def>
10
10
11 <%def name="breadcrumbs_links()">
11 <%def name="breadcrumbs_links()">
12 <span id="pr-title">
12 <span id="pr-title">
13 ${c.pull_request.title}
13 ${c.pull_request.title}
14 %if c.pull_request.is_closed():
14 %if c.pull_request.is_closed():
15 (${_('Closed')})
15 (${_('Closed')})
16 %endif
16 %endif
17 </span>
17 </span>
18 <div id="pr-title-edit" class="input" style="display: none;">
18 <div id="pr-title-edit" class="input" style="display: none;">
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 </div>
20 </div>
21 </%def>
21 </%def>
22
22
23 <%def name="menu_bar_nav()">
23 <%def name="menu_bar_nav()">
24 ${self.menu_items(active='repositories')}
24 ${self.menu_items(active='repositories')}
25 </%def>
25 </%def>
26
26
27 <%def name="menu_bar_subnav()">
27 <%def name="menu_bar_subnav()">
28 ${self.repo_menu(active='showpullrequest')}
28 ${self.repo_menu(active='showpullrequest')}
29 </%def>
29 </%def>
30
30
31 <%def name="main()">
31 <%def name="main()">
32
32
33 <script type="text/javascript">
33 <script type="text/javascript">
34 // TODO: marcink switch this to pyroutes
34 // TODO: marcink switch this to pyroutes
35 AJAX_COMMENT_DELETE_URL = "${h.url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
35 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 </script>
37 </script>
38 <div class="box">
38 <div class="box">
39
39
40 <div class="title">
40 <div class="title">
41 ${self.repo_page_title(c.rhodecode_db_repo)}
41 ${self.repo_page_title(c.rhodecode_db_repo)}
42 </div>
42 </div>
43
43
44 ${self.breadcrumbs()}
44 ${self.breadcrumbs()}
45
45
46 <div class="box pr-summary">
46 <div class="box pr-summary">
47
47
48 <div class="summary-details block-left">
48 <div class="summary-details block-left">
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 <div class="pr-details-title">
50 <div class="pr-details-title">
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 %if c.allowed_to_update:
52 %if c.allowed_to_update:
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 % if c.allowed_to_delete:
54 % if c.allowed_to_delete:
55 ${h.secure_form(h.url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
55 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), method='POST', request=request)}
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 ${h.end_form()}
58 ${h.end_form()}
59 % else:
59 % else:
60 ${_('Delete')}
60 ${_('Delete')}
61 % endif
61 % endif
62 </div>
62 </div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 %endif
65 %endif
66 </div>
66 </div>
67
67
68 <div id="summary" class="fields pr-details-content">
68 <div id="summary" class="fields pr-details-content">
69 <div class="field">
69 <div class="field">
70 <div class="label-summary">
70 <div class="label-summary">
71 <label>${_('Source')}:</label>
71 <label>${_('Source')}:</label>
72 </div>
72 </div>
73 <div class="input">
73 <div class="input">
74 <div class="pr-origininfo">
74 <div class="pr-origininfo">
75 ## branch link is only valid if it is a branch
75 ## branch link is only valid if it is a branch
76 <span class="tag">
76 <span class="tag">
77 %if c.pull_request.source_ref_parts.type == 'branch':
77 %if c.pull_request.source_ref_parts.type == 'branch':
78 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
78 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 %else:
79 %else:
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 %endif
81 %endif
82 </span>
82 </span>
83 <span class="clone-url">
83 <span class="clone-url">
84 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
84 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 </span>
85 </span>
86 <br/>
86 <br/>
87 % if c.ancestor_commit:
87 % if c.ancestor_commit:
88 ${_('Common ancestor')}:
88 ${_('Common ancestor')}:
89 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
89 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 % endif
90 % endif
91 </div>
91 </div>
92 <div class="pr-pullinfo">
92 <div class="pr-pullinfo">
93 %if h.is_hg(c.pull_request.source_repo):
93 %if h.is_hg(c.pull_request.source_repo):
94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
95 %elif h.is_git(c.pull_request.source_repo):
95 %elif h.is_git(c.pull_request.source_repo):
96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
97 %endif
97 %endif
98 </div>
98 </div>
99 </div>
99 </div>
100 </div>
100 </div>
101 <div class="field">
101 <div class="field">
102 <div class="label-summary">
102 <div class="label-summary">
103 <label>${_('Target')}:</label>
103 <label>${_('Target')}:</label>
104 </div>
104 </div>
105 <div class="input">
105 <div class="input">
106 <div class="pr-targetinfo">
106 <div class="pr-targetinfo">
107 ## branch link is only valid if it is a branch
107 ## branch link is only valid if it is a branch
108 <span class="tag">
108 <span class="tag">
109 %if c.pull_request.target_ref_parts.type == 'branch':
109 %if c.pull_request.target_ref_parts.type == 'branch':
110 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
110 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
111 %else:
111 %else:
112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
113 %endif
113 %endif
114 </span>
114 </span>
115 <span class="clone-url">
115 <span class="clone-url">
116 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
116 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
117 </span>
117 </span>
118 </div>
118 </div>
119 </div>
119 </div>
120 </div>
120 </div>
121
121
122 ## Link to the shadow repository.
122 ## Link to the shadow repository.
123 <div class="field">
123 <div class="field">
124 <div class="label-summary">
124 <div class="label-summary">
125 <label>${_('Merge')}:</label>
125 <label>${_('Merge')}:</label>
126 </div>
126 </div>
127 <div class="input">
127 <div class="input">
128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
129 <div class="pr-mergeinfo">
129 <div class="pr-mergeinfo">
130 %if h.is_hg(c.pull_request.target_repo):
130 %if h.is_hg(c.pull_request.target_repo):
131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
132 %elif h.is_git(c.pull_request.target_repo):
132 %elif h.is_git(c.pull_request.target_repo):
133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
134 %endif
134 %endif
135 </div>
135 </div>
136 % else:
136 % else:
137 <div class="">
137 <div class="">
138 ${_('Shadow repository data not available')}.
138 ${_('Shadow repository data not available')}.
139 </div>
139 </div>
140 % endif
140 % endif
141 </div>
141 </div>
142 </div>
142 </div>
143
143
144 <div class="field">
144 <div class="field">
145 <div class="label-summary">
145 <div class="label-summary">
146 <label>${_('Review')}:</label>
146 <label>${_('Review')}:</label>
147 </div>
147 </div>
148 <div class="input">
148 <div class="input">
149 %if c.pull_request_review_status:
149 %if c.pull_request_review_status:
150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
151 <span class="changeset-status-lbl tooltip">
151 <span class="changeset-status-lbl tooltip">
152 %if c.pull_request.is_closed():
152 %if c.pull_request.is_closed():
153 ${_('Closed')},
153 ${_('Closed')},
154 %endif
154 %endif
155 ${h.commit_status_lbl(c.pull_request_review_status)}
155 ${h.commit_status_lbl(c.pull_request_review_status)}
156 </span>
156 </span>
157 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
157 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
158 %endif
158 %endif
159 </div>
159 </div>
160 </div>
160 </div>
161 <div class="field">
161 <div class="field">
162 <div class="pr-description-label label-summary">
162 <div class="pr-description-label label-summary">
163 <label>${_('Description')}:</label>
163 <label>${_('Description')}:</label>
164 </div>
164 </div>
165 <div id="pr-desc" class="input">
165 <div id="pr-desc" class="input">
166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
167 </div>
167 </div>
168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
170 </div>
170 </div>
171 </div>
171 </div>
172
172
173 <div class="field">
173 <div class="field">
174 <div class="label-summary">
174 <div class="label-summary">
175 <label>${_('Versions')}:</label>
175 <label>${_('Versions')}:</label>
176 </div>
176 </div>
177
177
178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
180
180
181 <div class="pr-versions">
181 <div class="pr-versions">
182 % if c.show_version_changes:
182 % if c.show_version_changes:
183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
186 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
186 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
187 data-toggle-off="${_('Hide all versions of this pull request')}">
187 data-toggle-off="${_('Hide all versions of this pull request')}">
188 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
188 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
189 </a>
189 </a>
190 <table>
190 <table>
191 ## SHOW ALL VERSIONS OF PR
191 ## SHOW ALL VERSIONS OF PR
192 <% ver_pr = None %>
192 <% ver_pr = None %>
193
193
194 % for data in reversed(list(enumerate(c.versions, 1))):
194 % for data in reversed(list(enumerate(c.versions, 1))):
195 <% ver_pos = data[0] %>
195 <% ver_pos = data[0] %>
196 <% ver = data[1] %>
196 <% ver = data[1] %>
197 <% ver_pr = ver.pull_request_version_id %>
197 <% ver_pr = ver.pull_request_version_id %>
198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
199
199
200 <tr class="version-pr" style="display: ${display_row}">
200 <tr class="version-pr" style="display: ${display_row}">
201 <td>
201 <td>
202 <code>
202 <code>
203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
203 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
204 </code>
204 </code>
205 </td>
205 </td>
206 <td>
206 <td>
207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
209 </td>
209 </td>
210 <td>
210 <td>
211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
213 </div>
213 </div>
214 </td>
214 </td>
215 <td>
215 <td>
216 % if c.at_version_num != ver_pr:
216 % if c.at_version_num != ver_pr:
217 <i class="icon-comment"></i>
217 <i class="icon-comment"></i>
218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
220 </code>
220 </code>
221 % endif
221 % endif
222 </td>
222 </td>
223 <td>
223 <td>
224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
225 </td>
225 </td>
226 <td>
226 <td>
227 ${h.age_component(ver.updated_on, time_is_local=True)}
227 ${h.age_component(ver.updated_on, time_is_local=True)}
228 </td>
228 </td>
229 </tr>
229 </tr>
230 % endfor
230 % endfor
231
231
232 <tr>
232 <tr>
233 <td colspan="6">
233 <td colspan="6">
234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
235 data-label-text-locked="${_('select versions to show changes')}"
235 data-label-text-locked="${_('select versions to show changes')}"
236 data-label-text-diff="${_('show changes between versions')}"
236 data-label-text-diff="${_('show changes between versions')}"
237 data-label-text-show="${_('show pull request for this version')}"
237 data-label-text-show="${_('show pull request for this version')}"
238 >
238 >
239 ${_('select versions to show changes')}
239 ${_('select versions to show changes')}
240 </button>
240 </button>
241 </td>
241 </td>
242 </tr>
242 </tr>
243
243
244 ## show comment/inline comments summary
244 ## show comment/inline comments summary
245 <%def name="comments_summary()">
245 <%def name="comments_summary()">
246 <tr>
246 <tr>
247 <td colspan="6" class="comments-summary-td">
247 <td colspan="6" class="comments-summary-td">
248
248
249 % if c.at_version:
249 % if c.at_version:
250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
252 ${_('Comments at this version')}:
252 ${_('Comments at this version')}:
253 % else:
253 % else:
254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
256 ${_('Comments for this pull request')}:
256 ${_('Comments for this pull request')}:
257 % endif
257 % endif
258
258
259
259
260 %if general_comm_count_ver:
260 %if general_comm_count_ver:
261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
262 %else:
262 %else:
263 ${_("%d General ") % general_comm_count_ver}
263 ${_("%d General ") % general_comm_count_ver}
264 %endif
264 %endif
265
265
266 %if inline_comm_count_ver:
266 %if inline_comm_count_ver:
267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
268 %else:
268 %else:
269 , ${_("%d Inline") % inline_comm_count_ver}
269 , ${_("%d Inline") % inline_comm_count_ver}
270 %endif
270 %endif
271
271
272 %if outdated_comm_count_ver:
272 %if outdated_comm_count_ver:
273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
276 %else:
276 %else:
277 , ${_("%d Outdated") % outdated_comm_count_ver}
277 , ${_("%d Outdated") % outdated_comm_count_ver}
278 %endif
278 %endif
279 </td>
279 </td>
280 </tr>
280 </tr>
281 </%def>
281 </%def>
282 ${comments_summary()}
282 ${comments_summary()}
283 </table>
283 </table>
284 % else:
284 % else:
285 <div class="input">
285 <div class="input">
286 ${_('Pull request versions not available')}.
286 ${_('Pull request versions not available')}.
287 </div>
287 </div>
288 <div>
288 <div>
289 <table>
289 <table>
290 ${comments_summary()}
290 ${comments_summary()}
291 </table>
291 </table>
292 </div>
292 </div>
293 % endif
293 % endif
294 </div>
294 </div>
295 </div>
295 </div>
296
296
297 <div id="pr-save" class="field" style="display: none;">
297 <div id="pr-save" class="field" style="display: none;">
298 <div class="label-summary"></div>
298 <div class="label-summary"></div>
299 <div class="input">
299 <div class="input">
300 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
300 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
301 </div>
301 </div>
302 </div>
302 </div>
303 </div>
303 </div>
304 </div>
304 </div>
305 <div>
305 <div>
306 ## AUTHOR
306 ## AUTHOR
307 <div class="reviewers-title block-right">
307 <div class="reviewers-title block-right">
308 <div class="pr-details-title">
308 <div class="pr-details-title">
309 ${_('Author of this pull request')}
309 ${_('Author of this pull request')}
310 </div>
310 </div>
311 </div>
311 </div>
312 <div class="block-right pr-details-content reviewers">
312 <div class="block-right pr-details-content reviewers">
313 <ul class="group_members">
313 <ul class="group_members">
314 <li>
314 <li>
315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
316 </li>
316 </li>
317 </ul>
317 </ul>
318 </div>
318 </div>
319
319
320 ## REVIEW RULES
320 ## REVIEW RULES
321 <div id="review_rules" style="display: none" class="reviewers-title block-right">
321 <div id="review_rules" style="display: none" class="reviewers-title block-right">
322 <div class="pr-details-title">
322 <div class="pr-details-title">
323 ${_('Reviewer rules')}
323 ${_('Reviewer rules')}
324 %if c.allowed_to_update:
324 %if c.allowed_to_update:
325 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
325 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
326 %endif
326 %endif
327 </div>
327 </div>
328 <div class="pr-reviewer-rules">
328 <div class="pr-reviewer-rules">
329 ## review rules will be appended here, by default reviewers logic
329 ## review rules will be appended here, by default reviewers logic
330 </div>
330 </div>
331 <input id="review_data" type="hidden" name="review_data" value="">
331 <input id="review_data" type="hidden" name="review_data" value="">
332 </div>
332 </div>
333
333
334 ## REVIEWERS
334 ## REVIEWERS
335 <div class="reviewers-title block-right">
335 <div class="reviewers-title block-right">
336 <div class="pr-details-title">
336 <div class="pr-details-title">
337 ${_('Pull request reviewers')}
337 ${_('Pull request reviewers')}
338 %if c.allowed_to_update:
338 %if c.allowed_to_update:
339 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
339 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
340 %endif
340 %endif
341 </div>
341 </div>
342 </div>
342 </div>
343 <div id="reviewers" class="block-right pr-details-content reviewers">
343 <div id="reviewers" class="block-right pr-details-content reviewers">
344 ## members goes here !
344 ## members goes here !
345 <input type="hidden" name="__start__" value="review_members:sequence">
345 <input type="hidden" name="__start__" value="review_members:sequence">
346 <ul id="review_members" class="group_members">
346 <ul id="review_members" class="group_members">
347 %for member,reasons,mandatory,status in c.pull_request_reviewers:
347 %for member,reasons,mandatory,status in c.pull_request_reviewers:
348 <li id="reviewer_${member.user_id}" class="reviewer_entry">
348 <li id="reviewer_${member.user_id}" class="reviewer_entry">
349 <div class="reviewers_member">
349 <div class="reviewers_member">
350 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
350 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
351 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
351 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
352 </div>
352 </div>
353 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
353 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
354 ${self.gravatar_with_user(member.email, 16)}
354 ${self.gravatar_with_user(member.email, 16)}
355 </div>
355 </div>
356 <input type="hidden" name="__start__" value="reviewer:mapping">
356 <input type="hidden" name="__start__" value="reviewer:mapping">
357 <input type="hidden" name="__start__" value="reasons:sequence">
357 <input type="hidden" name="__start__" value="reasons:sequence">
358 %for reason in reasons:
358 %for reason in reasons:
359 <div class="reviewer_reason">- ${reason}</div>
359 <div class="reviewer_reason">- ${reason}</div>
360 <input type="hidden" name="reason" value="${reason}">
360 <input type="hidden" name="reason" value="${reason}">
361
361
362 %endfor
362 %endfor
363 <input type="hidden" name="__end__" value="reasons:sequence">
363 <input type="hidden" name="__end__" value="reasons:sequence">
364 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
364 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
365 <input type="hidden" name="mandatory" value="${mandatory}"/>
365 <input type="hidden" name="mandatory" value="${mandatory}"/>
366 <input type="hidden" name="__end__" value="reviewer:mapping">
366 <input type="hidden" name="__end__" value="reviewer:mapping">
367 % if mandatory:
367 % if mandatory:
368 <div class="reviewer_member_mandatory_remove">
368 <div class="reviewer_member_mandatory_remove">
369 <i class="icon-remove-sign"></i>
369 <i class="icon-remove-sign"></i>
370 </div>
370 </div>
371 <div class="reviewer_member_mandatory">
371 <div class="reviewer_member_mandatory">
372 <i class="icon-lock" title="${h.tooltip(_('Mandatory reviewer'))}"></i>
372 <i class="icon-lock" title="${h.tooltip(_('Mandatory reviewer'))}"></i>
373 </div>
373 </div>
374 % else:
374 % else:
375 %if c.allowed_to_update:
375 %if c.allowed_to_update:
376 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
376 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
377 <i class="icon-remove-sign" ></i>
377 <i class="icon-remove-sign" ></i>
378 </div>
378 </div>
379 %endif
379 %endif
380 % endif
380 % endif
381 </div>
381 </div>
382 </li>
382 </li>
383 %endfor
383 %endfor
384 </ul>
384 </ul>
385 <input type="hidden" name="__end__" value="review_members:sequence">
385 <input type="hidden" name="__end__" value="review_members:sequence">
386
386
387 %if not c.pull_request.is_closed():
387 %if not c.pull_request.is_closed():
388 <div id="add_reviewer" class="ac" style="display: none;">
388 <div id="add_reviewer" class="ac" style="display: none;">
389 %if c.allowed_to_update:
389 %if c.allowed_to_update:
390 % if not c.forbid_adding_reviewers:
390 % if not c.forbid_adding_reviewers:
391 <div id="add_reviewer_input" class="reviewer_ac">
391 <div id="add_reviewer_input" class="reviewer_ac">
392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
393 <div id="reviewers_container"></div>
393 <div id="reviewers_container"></div>
394 </div>
394 </div>
395 % endif
395 % endif
396 <div class="pull-right">
396 <div class="pull-right">
397 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
397 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
398 </div>
398 </div>
399 %endif
399 %endif
400 </div>
400 </div>
401 %endif
401 %endif
402 </div>
402 </div>
403 </div>
403 </div>
404 </div>
404 </div>
405 <div class="box">
405 <div class="box">
406 ##DIFF
406 ##DIFF
407 <div class="table" >
407 <div class="table" >
408 <div id="changeset_compare_view_content">
408 <div id="changeset_compare_view_content">
409 ##CS
409 ##CS
410 % if c.missing_requirements:
410 % if c.missing_requirements:
411 <div class="box">
411 <div class="box">
412 <div class="alert alert-warning">
412 <div class="alert alert-warning">
413 <div>
413 <div>
414 <strong>${_('Missing requirements:')}</strong>
414 <strong>${_('Missing requirements:')}</strong>
415 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
415 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
416 </div>
416 </div>
417 </div>
417 </div>
418 </div>
418 </div>
419 % elif c.missing_commits:
419 % elif c.missing_commits:
420 <div class="box">
420 <div class="box">
421 <div class="alert alert-warning">
421 <div class="alert alert-warning">
422 <div>
422 <div>
423 <strong>${_('Missing commits')}:</strong>
423 <strong>${_('Missing commits')}:</strong>
424 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
424 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
425 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
425 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
426 </div>
426 </div>
427 </div>
427 </div>
428 </div>
428 </div>
429 % endif
429 % endif
430
430
431 <div class="compare_view_commits_title">
431 <div class="compare_view_commits_title">
432 % if not c.compare_mode:
432 % if not c.compare_mode:
433
433
434 % if c.at_version_pos:
434 % if c.at_version_pos:
435 <h4>
435 <h4>
436 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
436 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
437 </h4>
437 </h4>
438 % endif
438 % endif
439
439
440 <div class="pull-left">
440 <div class="pull-left">
441 <div class="btn-group">
441 <div class="btn-group">
442 <a
442 <a
443 class="btn"
443 class="btn"
444 href="#"
444 href="#"
445 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
445 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
446 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
446 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
447 </a>
447 </a>
448 <a
448 <a
449 class="btn"
449 class="btn"
450 href="#"
450 href="#"
451 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
451 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
452 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
452 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
453 </a>
453 </a>
454 </div>
454 </div>
455 </div>
455 </div>
456
456
457 <div class="pull-right">
457 <div class="pull-right">
458 % if c.allowed_to_update and not c.pull_request.is_closed():
458 % if c.allowed_to_update and not c.pull_request.is_closed():
459 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
459 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
460 % else:
460 % else:
461 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
461 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
462 % endif
462 % endif
463
463
464 </div>
464 </div>
465 % endif
465 % endif
466 </div>
466 </div>
467
467
468 % if not c.missing_commits:
468 % if not c.missing_commits:
469 % if c.compare_mode:
469 % if c.compare_mode:
470 % if c.at_version:
470 % if c.at_version:
471 <h4>
471 <h4>
472 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
472 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
473 </h4>
473 </h4>
474
474
475 <div class="subtitle-compare">
475 <div class="subtitle-compare">
476 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
476 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
477 </div>
477 </div>
478
478
479 <div class="container">
479 <div class="container">
480 <table class="rctable compare_view_commits">
480 <table class="rctable compare_view_commits">
481 <tr>
481 <tr>
482 <th></th>
482 <th></th>
483 <th>${_('Time')}</th>
483 <th>${_('Time')}</th>
484 <th>${_('Author')}</th>
484 <th>${_('Author')}</th>
485 <th>${_('Commit')}</th>
485 <th>${_('Commit')}</th>
486 <th></th>
486 <th></th>
487 <th>${_('Description')}</th>
487 <th>${_('Description')}</th>
488 </tr>
488 </tr>
489
489
490 % for c_type, commit in c.commit_changes:
490 % for c_type, commit in c.commit_changes:
491 % if c_type in ['a', 'r']:
491 % if c_type in ['a', 'r']:
492 <%
492 <%
493 if c_type == 'a':
493 if c_type == 'a':
494 cc_title = _('Commit added in displayed changes')
494 cc_title = _('Commit added in displayed changes')
495 elif c_type == 'r':
495 elif c_type == 'r':
496 cc_title = _('Commit removed in displayed changes')
496 cc_title = _('Commit removed in displayed changes')
497 else:
497 else:
498 cc_title = ''
498 cc_title = ''
499 %>
499 %>
500 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
500 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
501 <td>
501 <td>
502 <div class="commit-change-indicator color-${c_type}-border">
502 <div class="commit-change-indicator color-${c_type}-border">
503 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
503 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
504 ${c_type.upper()}
504 ${c_type.upper()}
505 </div>
505 </div>
506 </div>
506 </div>
507 </td>
507 </td>
508 <td class="td-time">
508 <td class="td-time">
509 ${h.age_component(commit.date)}
509 ${h.age_component(commit.date)}
510 </td>
510 </td>
511 <td class="td-user">
511 <td class="td-user">
512 ${base.gravatar_with_user(commit.author, 16)}
512 ${base.gravatar_with_user(commit.author, 16)}
513 </td>
513 </td>
514 <td class="td-hash">
514 <td class="td-hash">
515 <code>
515 <code>
516 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
516 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
517 r${commit.revision}:${h.short_id(commit.raw_id)}
517 r${commit.revision}:${h.short_id(commit.raw_id)}
518 </a>
518 </a>
519 ${h.hidden('revisions', commit.raw_id)}
519 ${h.hidden('revisions', commit.raw_id)}
520 </code>
520 </code>
521 </td>
521 </td>
522 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
522 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
523 <div class="show_more_col">
523 <div class="show_more_col">
524 <i class="show_more"></i>
524 <i class="show_more"></i>
525 </div>
525 </div>
526 </td>
526 </td>
527 <td class="mid td-description">
527 <td class="mid td-description">
528 <div class="log-container truncate-wrap">
528 <div class="log-container truncate-wrap">
529 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
529 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
530 ${h.urlify_commit_message(commit.message, c.repo_name)}
530 ${h.urlify_commit_message(commit.message, c.repo_name)}
531 </div>
531 </div>
532 </div>
532 </div>
533 </td>
533 </td>
534 </tr>
534 </tr>
535 % endif
535 % endif
536 % endfor
536 % endfor
537 </table>
537 </table>
538 </div>
538 </div>
539
539
540 <script>
540 <script>
541 $('.expand_commit').on('click',function(e){
541 $('.expand_commit').on('click',function(e){
542 var target_expand = $(this);
542 var target_expand = $(this);
543 var cid = target_expand.data('commitId');
543 var cid = target_expand.data('commitId');
544
544
545 if (target_expand.hasClass('open')){
545 if (target_expand.hasClass('open')){
546 $('#c-'+cid).css({
546 $('#c-'+cid).css({
547 'height': '1.5em',
547 'height': '1.5em',
548 'white-space': 'nowrap',
548 'white-space': 'nowrap',
549 'text-overflow': 'ellipsis',
549 'text-overflow': 'ellipsis',
550 'overflow':'hidden'
550 'overflow':'hidden'
551 });
551 });
552 target_expand.removeClass('open');
552 target_expand.removeClass('open');
553 }
553 }
554 else {
554 else {
555 $('#c-'+cid).css({
555 $('#c-'+cid).css({
556 'height': 'auto',
556 'height': 'auto',
557 'white-space': 'pre-line',
557 'white-space': 'pre-line',
558 'text-overflow': 'initial',
558 'text-overflow': 'initial',
559 'overflow':'visible'
559 'overflow':'visible'
560 });
560 });
561 target_expand.addClass('open');
561 target_expand.addClass('open');
562 }
562 }
563 });
563 });
564 </script>
564 </script>
565
565
566 % endif
566 % endif
567
567
568 % else:
568 % else:
569 <%include file="/compare/compare_commits.mako" />
569 <%include file="/compare/compare_commits.mako" />
570 % endif
570 % endif
571
571
572 <div class="cs_files">
572 <div class="cs_files">
573 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
573 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
574 ${cbdiffs.render_diffset_menu()}
574 ${cbdiffs.render_diffset_menu()}
575 ${cbdiffs.render_diffset(
575 ${cbdiffs.render_diffset(
576 c.diffset, use_comments=True,
576 c.diffset, use_comments=True,
577 collapse_when_files_over=30,
577 collapse_when_files_over=30,
578 disable_new_comments=not c.allowed_to_comment,
578 disable_new_comments=not c.allowed_to_comment,
579 deleted_files_comments=c.deleted_files_comments)}
579 deleted_files_comments=c.deleted_files_comments)}
580 </div>
580 </div>
581 % else:
581 % else:
582 ## skipping commits we need to clear the view for missing commits
582 ## skipping commits we need to clear the view for missing commits
583 <div style="clear:both;"></div>
583 <div style="clear:both;"></div>
584 % endif
584 % endif
585
585
586 </div>
586 </div>
587 </div>
587 </div>
588
588
589 ## template for inline comment form
589 ## template for inline comment form
590 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
590 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
591
591
592 ## render general comments
592 ## render general comments
593
593
594 <div id="comment-tr-show">
594 <div id="comment-tr-show">
595 <div class="comment">
595 <div class="comment">
596 % if general_outdated_comm_count_ver:
596 % if general_outdated_comm_count_ver:
597 <div class="meta">
597 <div class="meta">
598 % if general_outdated_comm_count_ver == 1:
598 % if general_outdated_comm_count_ver == 1:
599 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
599 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
600 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
600 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
601 % else:
601 % else:
602 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
602 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
603 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
603 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
604 % endif
604 % endif
605 </div>
605 </div>
606 % endif
606 % endif
607 </div>
607 </div>
608 </div>
608 </div>
609
609
610 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
610 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
611
611
612 % if not c.pull_request.is_closed():
612 % if not c.pull_request.is_closed():
613 ## merge status, and merge action
613 ## merge status, and merge action
614 <div class="pull-request-merge">
614 <div class="pull-request-merge">
615 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
615 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
616 </div>
616 </div>
617
617
618 ## main comment form and it status
618 ## main comment form and it status
619 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
619 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
620 pull_request_id=c.pull_request.pull_request_id),
620 pull_request_id=c.pull_request.pull_request_id),
621 c.pull_request_review_status,
621 c.pull_request_review_status,
622 is_pull_request=True, change_status=c.allowed_to_change_status)}
622 is_pull_request=True, change_status=c.allowed_to_change_status)}
623 %endif
623 %endif
624
624
625 <script type="text/javascript">
625 <script type="text/javascript">
626 if (location.hash) {
626 if (location.hash) {
627 var result = splitDelimitedHash(location.hash);
627 var result = splitDelimitedHash(location.hash);
628 var line = $('html').find(result.loc);
628 var line = $('html').find(result.loc);
629 // show hidden comments if we use location.hash
629 // show hidden comments if we use location.hash
630 if (line.hasClass('comment-general')) {
630 if (line.hasClass('comment-general')) {
631 $(line).show();
631 $(line).show();
632 } else if (line.hasClass('comment-inline')) {
632 } else if (line.hasClass('comment-inline')) {
633 $(line).show();
633 $(line).show();
634 var $cb = $(line).closest('.cb');
634 var $cb = $(line).closest('.cb');
635 $cb.removeClass('cb-collapsed')
635 $cb.removeClass('cb-collapsed')
636 }
636 }
637 if (line.length > 0){
637 if (line.length > 0){
638 offsetScroll(line, 70);
638 offsetScroll(line, 70);
639 }
639 }
640 }
640 }
641
641
642 versionController = new VersionController();
642 versionController = new VersionController();
643 versionController.init();
643 versionController.init();
644
644
645 reviewersController = new ReviewersController();
645 reviewersController = new ReviewersController();
646
646
647 $(function(){
647 $(function(){
648
648
649 // custom code mirror
649 // custom code mirror
650 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
650 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
651
651
652 var PRDetails = {
652 var PRDetails = {
653 editButton: $('#open_edit_pullrequest'),
653 editButton: $('#open_edit_pullrequest'),
654 closeButton: $('#close_edit_pullrequest'),
654 closeButton: $('#close_edit_pullrequest'),
655 deleteButton: $('#delete_pullrequest'),
655 deleteButton: $('#delete_pullrequest'),
656 viewFields: $('#pr-desc, #pr-title'),
656 viewFields: $('#pr-desc, #pr-title'),
657 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
657 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
658
658
659 init: function() {
659 init: function() {
660 var that = this;
660 var that = this;
661 this.editButton.on('click', function(e) { that.edit(); });
661 this.editButton.on('click', function(e) { that.edit(); });
662 this.closeButton.on('click', function(e) { that.view(); });
662 this.closeButton.on('click', function(e) { that.view(); });
663 },
663 },
664
664
665 edit: function(event) {
665 edit: function(event) {
666 this.viewFields.hide();
666 this.viewFields.hide();
667 this.editButton.hide();
667 this.editButton.hide();
668 this.deleteButton.hide();
668 this.deleteButton.hide();
669 this.closeButton.show();
669 this.closeButton.show();
670 this.editFields.show();
670 this.editFields.show();
671 codeMirrorInstance.refresh();
671 codeMirrorInstance.refresh();
672 },
672 },
673
673
674 view: function(event) {
674 view: function(event) {
675 this.editButton.show();
675 this.editButton.show();
676 this.deleteButton.show();
676 this.deleteButton.show();
677 this.editFields.hide();
677 this.editFields.hide();
678 this.closeButton.hide();
678 this.closeButton.hide();
679 this.viewFields.show();
679 this.viewFields.show();
680 }
680 }
681 };
681 };
682
682
683 var ReviewersPanel = {
683 var ReviewersPanel = {
684 editButton: $('#open_edit_reviewers'),
684 editButton: $('#open_edit_reviewers'),
685 closeButton: $('#close_edit_reviewers'),
685 closeButton: $('#close_edit_reviewers'),
686 addButton: $('#add_reviewer'),
686 addButton: $('#add_reviewer'),
687 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove,.reviewer_member_mandatory'),
687 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove,.reviewer_member_mandatory'),
688
688
689 init: function() {
689 init: function() {
690 var self = this;
690 var self = this;
691 this.editButton.on('click', function(e) { self.edit(); });
691 this.editButton.on('click', function(e) { self.edit(); });
692 this.closeButton.on('click', function(e) { self.close(); });
692 this.closeButton.on('click', function(e) { self.close(); });
693 },
693 },
694
694
695 edit: function(event) {
695 edit: function(event) {
696 this.editButton.hide();
696 this.editButton.hide();
697 this.closeButton.show();
697 this.closeButton.show();
698 this.addButton.show();
698 this.addButton.show();
699 this.removeButtons.css('visibility', 'visible');
699 this.removeButtons.css('visibility', 'visible');
700 // review rules
700 // review rules
701 reviewersController.loadReviewRules(
701 reviewersController.loadReviewRules(
702 ${c.pull_request.reviewer_data_json | n});
702 ${c.pull_request.reviewer_data_json | n});
703 },
703 },
704
704
705 close: function(event) {
705 close: function(event) {
706 this.editButton.show();
706 this.editButton.show();
707 this.closeButton.hide();
707 this.closeButton.hide();
708 this.addButton.hide();
708 this.addButton.hide();
709 this.removeButtons.css('visibility', 'hidden');
709 this.removeButtons.css('visibility', 'hidden');
710 // hide review rules
710 // hide review rules
711 reviewersController.hideReviewRules()
711 reviewersController.hideReviewRules()
712 }
712 }
713 };
713 };
714
714
715 PRDetails.init();
715 PRDetails.init();
716 ReviewersPanel.init();
716 ReviewersPanel.init();
717
717
718 showOutdated = function(self){
718 showOutdated = function(self){
719 $('.comment-inline.comment-outdated').show();
719 $('.comment-inline.comment-outdated').show();
720 $('.filediff-outdated').show();
720 $('.filediff-outdated').show();
721 $('.showOutdatedComments').hide();
721 $('.showOutdatedComments').hide();
722 $('.hideOutdatedComments').show();
722 $('.hideOutdatedComments').show();
723 };
723 };
724
724
725 hideOutdated = function(self){
725 hideOutdated = function(self){
726 $('.comment-inline.comment-outdated').hide();
726 $('.comment-inline.comment-outdated').hide();
727 $('.filediff-outdated').hide();
727 $('.filediff-outdated').hide();
728 $('.hideOutdatedComments').hide();
728 $('.hideOutdatedComments').hide();
729 $('.showOutdatedComments').show();
729 $('.showOutdatedComments').show();
730 };
730 };
731
731
732 refreshMergeChecks = function(){
732 refreshMergeChecks = function(){
733 var loadUrl = "${h.url.current(merge_checks=1)}";
733 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
734 $('.pull-request-merge').css('opacity', 0.3);
734 $('.pull-request-merge').css('opacity', 0.3);
735 $('.action-buttons-extra').css('opacity', 0.3);
735 $('.action-buttons-extra').css('opacity', 0.3);
736
736
737 $('.pull-request-merge').load(
737 $('.pull-request-merge').load(
738 loadUrl, function() {
738 loadUrl, function() {
739 $('.pull-request-merge').css('opacity', 1);
739 $('.pull-request-merge').css('opacity', 1);
740
740
741 $('.action-buttons-extra').css('opacity', 1);
741 $('.action-buttons-extra').css('opacity', 1);
742 injectCloseAction();
742 injectCloseAction();
743 }
743 }
744 );
744 );
745 };
745 };
746
746
747 injectCloseAction = function() {
747 injectCloseAction = function() {
748 var closeAction = $('#close-pull-request-action').html();
748 var closeAction = $('#close-pull-request-action').html();
749 var $actionButtons = $('.action-buttons-extra');
749 var $actionButtons = $('.action-buttons-extra');
750 // clear the action before
750 // clear the action before
751 $actionButtons.html("");
751 $actionButtons.html("");
752 $actionButtons.html(closeAction);
752 $actionButtons.html(closeAction);
753 };
753 };
754
754
755 closePullRequest = function (status) {
755 closePullRequest = function (status) {
756 // inject closing flag
756 // inject closing flag
757 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
757 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
758 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
758 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
759 $(generalCommentForm.submitForm).submit();
759 $(generalCommentForm.submitForm).submit();
760 };
760 };
761
761
762 $('#show-outdated-comments').on('click', function(e){
762 $('#show-outdated-comments').on('click', function(e){
763 var button = $(this);
763 var button = $(this);
764 var outdated = $('.comment-outdated');
764 var outdated = $('.comment-outdated');
765
765
766 if (button.html() === "(Show)") {
766 if (button.html() === "(Show)") {
767 button.html("(Hide)");
767 button.html("(Hide)");
768 outdated.show();
768 outdated.show();
769 } else {
769 } else {
770 button.html("(Show)");
770 button.html("(Show)");
771 outdated.hide();
771 outdated.hide();
772 }
772 }
773 });
773 });
774
774
775 $('.show-inline-comments').on('change', function(e){
775 $('.show-inline-comments').on('change', function(e){
776 var show = 'none';
776 var show = 'none';
777 var target = e.currentTarget;
777 var target = e.currentTarget;
778 if(target.checked){
778 if(target.checked){
779 show = ''
779 show = ''
780 }
780 }
781 var boxid = $(target).attr('id_for');
781 var boxid = $(target).attr('id_for');
782 var comments = $('#{0} .inline-comments'.format(boxid));
782 var comments = $('#{0} .inline-comments'.format(boxid));
783 var fn_display = function(idx){
783 var fn_display = function(idx){
784 $(this).css('display', show);
784 $(this).css('display', show);
785 };
785 };
786 $(comments).each(fn_display);
786 $(comments).each(fn_display);
787 var btns = $('#{0} .inline-comments-button'.format(boxid));
787 var btns = $('#{0} .inline-comments-button'.format(boxid));
788 $(btns).each(fn_display);
788 $(btns).each(fn_display);
789 });
789 });
790
790
791 $('#merge_pull_request_form').submit(function() {
791 $('#merge_pull_request_form').submit(function() {
792 if (!$('#merge_pull_request').attr('disabled')) {
792 if (!$('#merge_pull_request').attr('disabled')) {
793 $('#merge_pull_request').attr('disabled', 'disabled');
793 $('#merge_pull_request').attr('disabled', 'disabled');
794 }
794 }
795 return true;
795 return true;
796 });
796 });
797
797
798 $('#edit_pull_request').on('click', function(e){
798 $('#edit_pull_request').on('click', function(e){
799 var title = $('#pr-title-input').val();
799 var title = $('#pr-title-input').val();
800 var description = codeMirrorInstance.getValue();
800 var description = codeMirrorInstance.getValue();
801 editPullRequest(
801 editPullRequest(
802 "${c.repo_name}", "${c.pull_request.pull_request_id}",
802 "${c.repo_name}", "${c.pull_request.pull_request_id}",
803 title, description);
803 title, description);
804 });
804 });
805
805
806 $('#update_pull_request').on('click', function(e){
806 $('#update_pull_request').on('click', function(e){
807 $(this).attr('disabled', 'disabled');
807 $(this).attr('disabled', 'disabled');
808 $(this).addClass('disabled');
808 $(this).addClass('disabled');
809 $(this).html(_gettext('Saving...'));
809 $(this).html(_gettext('Saving...'));
810 reviewersController.updateReviewers(
810 reviewersController.updateReviewers(
811 "${c.repo_name}", "${c.pull_request.pull_request_id}");
811 "${c.repo_name}", "${c.pull_request.pull_request_id}");
812 });
812 });
813
813
814 $('#update_commits').on('click', function(e){
814 $('#update_commits').on('click', function(e){
815 var isDisabled = !$(e.currentTarget).attr('disabled');
815 var isDisabled = !$(e.currentTarget).attr('disabled');
816 $(e.currentTarget).attr('disabled', 'disabled');
816 $(e.currentTarget).attr('disabled', 'disabled');
817 $(e.currentTarget).addClass('disabled');
817 $(e.currentTarget).addClass('disabled');
818 $(e.currentTarget).removeClass('btn-primary');
818 $(e.currentTarget).removeClass('btn-primary');
819 $(e.currentTarget).text(_gettext('Updating...'));
819 $(e.currentTarget).text(_gettext('Updating...'));
820 if(isDisabled){
820 if(isDisabled){
821 updateCommits(
821 updateCommits(
822 "${c.repo_name}", "${c.pull_request.pull_request_id}");
822 "${c.repo_name}", "${c.pull_request.pull_request_id}");
823 }
823 }
824 });
824 });
825 // fixing issue with caches on firefox
825 // fixing issue with caches on firefox
826 $('#update_commits').removeAttr("disabled");
826 $('#update_commits').removeAttr("disabled");
827
827
828 $('.show-inline-comments').on('click', function(e){
828 $('.show-inline-comments').on('click', function(e){
829 var boxid = $(this).attr('data-comment-id');
829 var boxid = $(this).attr('data-comment-id');
830 var button = $(this);
830 var button = $(this);
831
831
832 if(button.hasClass("comments-visible")) {
832 if(button.hasClass("comments-visible")) {
833 $('#{0} .inline-comments'.format(boxid)).each(function(index){
833 $('#{0} .inline-comments'.format(boxid)).each(function(index){
834 $(this).hide();
834 $(this).hide();
835 });
835 });
836 button.removeClass("comments-visible");
836 button.removeClass("comments-visible");
837 } else {
837 } else {
838 $('#{0} .inline-comments'.format(boxid)).each(function(index){
838 $('#{0} .inline-comments'.format(boxid)).each(function(index){
839 $(this).show();
839 $(this).show();
840 });
840 });
841 button.addClass("comments-visible");
841 button.addClass("comments-visible");
842 }
842 }
843 });
843 });
844
844
845 // register submit callback on commentForm form to track TODOs
845 // register submit callback on commentForm form to track TODOs
846 window.commentFormGlobalSubmitSuccessCallback = function(){
846 window.commentFormGlobalSubmitSuccessCallback = function(){
847 refreshMergeChecks();
847 refreshMergeChecks();
848 };
848 };
849 // initial injection
849 // initial injection
850 injectCloseAction();
850 injectCloseAction();
851
851
852 ReviewerAutoComplete('#user');
852 ReviewerAutoComplete('#user');
853
853
854 })
854 })
855 </script>
855 </script>
856
856
857 </div>
857 </div>
858 </div>
858 </div>
859
859
860 </%def>
860 </%def>
@@ -1,146 +1,146 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('%s Pull Requests') % c.repo_name}
4 ${_('%s Pull Requests') % c.repo_name}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()">
10 <%def name="breadcrumbs_links()">
11
11
12 </%def>
12 </%def>
13
13
14 <%def name="menu_bar_nav()">
14 <%def name="menu_bar_nav()">
15 ${self.menu_items(active='repositories')}
15 ${self.menu_items(active='repositories')}
16 </%def>
16 </%def>
17
17
18
18
19 <%def name="menu_bar_subnav()">
19 <%def name="menu_bar_subnav()">
20 ${self.repo_menu(active='showpullrequest')}
20 ${self.repo_menu(active='showpullrequest')}
21 </%def>
21 </%def>
22
22
23
23
24 <%def name="main()">
24 <%def name="main()">
25 <div class="box">
25 <div class="box">
26 <div class="title">
26 <div class="title">
27 ${self.repo_page_title(c.rhodecode_db_repo)}
27 ${self.repo_page_title(c.rhodecode_db_repo)}
28
28
29 <ul class="links">
29 <ul class="links">
30 <li>
30 <li>
31 %if c.rhodecode_user.username != h.DEFAULT_USER:
31 %if c.rhodecode_user.username != h.DEFAULT_USER:
32 <span>
32 <span>
33 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.url('pullrequest_home',repo_name=c.repo_name)}">
33 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
34 ${_('Open new Pull Request')}
34 ${_('Open new Pull Request')}
35 </a>
35 </a>
36 </span>
36 </span>
37 %endif
37 %endif
38 </li>
38 </li>
39 </ul>
39 </ul>
40
40
41 ${self.breadcrumbs()}
41 ${self.breadcrumbs()}
42 </div>
42 </div>
43
43
44 <div class="sidebar-col-wrapper">
44 <div class="sidebar-col-wrapper">
45 ##main
45 ##main
46 <div class="sidebar">
46 <div class="sidebar">
47 <ul class="nav nav-pills nav-stacked">
47 <ul class="nav nav-pills nav-stacked">
48 <li class="${'active' if c.active=='open' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
48 <li class="${'active' if c.active=='open' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
49 <li class="${'active' if c.active=='my' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
49 <li class="${'active' if c.active=='my' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
50 <li class="${'active' if c.active=='awaiting' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
50 <li class="${'active' if c.active=='awaiting' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
51 <li class="${'active' if c.active=='awaiting_my' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
51 <li class="${'active' if c.active=='awaiting_my' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
52 <li class="${'active' if c.active=='closed' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
52 <li class="${'active' if c.active=='closed' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
53 <li class="${'active' if c.active=='source' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
53 <li class="${'active' if c.active=='source' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
54 </ul>
54 </ul>
55 </div>
55 </div>
56
56
57 <div class="main-content-full-width">
57 <div class="main-content-full-width">
58 <div class="panel panel-default">
58 <div class="panel panel-default">
59 <div class="panel-heading">
59 <div class="panel-heading">
60 <h3 class="panel-title">
60 <h3 class="panel-title">
61 %if c.source:
61 %if c.source:
62 ${_('Pull Requests from %(repo_name)s repository') % {'repo_name': c.repo_name}}
62 ${_('Pull Requests from %(repo_name)s repository') % {'repo_name': c.repo_name}}
63 %elif c.closed:
63 %elif c.closed:
64 ${_('Closed Pull Requests to repository %(repo_name)s') % {'repo_name': c.repo_name}}
64 ${_('Closed Pull Requests to repository %(repo_name)s') % {'repo_name': c.repo_name}}
65 %elif c.my:
65 %elif c.my:
66 ${_('Pull Requests to %(repo_name)s repository opened by me') % {'repo_name': c.repo_name}}
66 ${_('Pull Requests to %(repo_name)s repository opened by me') % {'repo_name': c.repo_name}}
67 %elif c.awaiting_review:
67 %elif c.awaiting_review:
68 ${_('Pull Requests to %(repo_name)s repository awaiting review') % {'repo_name': c.repo_name}}
68 ${_('Pull Requests to %(repo_name)s repository awaiting review') % {'repo_name': c.repo_name}}
69 %elif c.awaiting_my_review:
69 %elif c.awaiting_my_review:
70 ${_('Pull Requests to %(repo_name)s repository awaiting my review') % {'repo_name': c.repo_name}}
70 ${_('Pull Requests to %(repo_name)s repository awaiting my review') % {'repo_name': c.repo_name}}
71 %else:
71 %else:
72 ${_('Pull Requests to %(repo_name)s repository') % {'repo_name': c.repo_name}}
72 ${_('Pull Requests to %(repo_name)s repository') % {'repo_name': c.repo_name}}
73 %endif
73 %endif
74 </h3>
74 </h3>
75 </div>
75 </div>
76 <div class="panel-body panel-body-min-height">
76 <div class="panel-body panel-body-min-height">
77 <table id="pull_request_list_table" class="display"></table>
77 <table id="pull_request_list_table" class="display"></table>
78 </div>
78 </div>
79 </div>
79 </div>
80 </div>
80 </div>
81 </div>
81 </div>
82 </div>
82 </div>
83
83
84 <script type="text/javascript">
84 <script type="text/javascript">
85 $(document).ready(function() {
85 $(document).ready(function() {
86
86
87 var $pullRequestListTable = $('#pull_request_list_table');
87 var $pullRequestListTable = $('#pull_request_list_table');
88
88
89 // object list
89 // object list
90 $pullRequestListTable.DataTable({
90 $pullRequestListTable.DataTable({
91 processing: true,
91 processing: true,
92 serverSide: true,
92 serverSide: true,
93 ajax: {
93 ajax: {
94 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
94 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
95 "data": function (d) {
95 "data": function (d) {
96 d.source = "${c.source}";
96 d.source = "${c.source}";
97 d.closed = "${c.closed}";
97 d.closed = "${c.closed}";
98 d.my = "${c.my}";
98 d.my = "${c.my}";
99 d.awaiting_review = "${c.awaiting_review}";
99 d.awaiting_review = "${c.awaiting_review}";
100 d.awaiting_my_review = "${c.awaiting_my_review}";
100 d.awaiting_my_review = "${c.awaiting_my_review}";
101 }
101 }
102 },
102 },
103 dom: 'rtp',
103 dom: 'rtp',
104 pageLength: ${c.visual.dashboard_items},
104 pageLength: ${c.visual.dashboard_items},
105 order: [[ 1, "desc" ]],
105 order: [[ 1, "desc" ]],
106 columns: [
106 columns: [
107 { data: {"_": "status",
107 { data: {"_": "status",
108 "sort": "status"}, title: "", className: "td-status", orderable: false},
108 "sort": "status"}, title: "", className: "td-status", orderable: false},
109 { data: {"_": "name",
109 { data: {"_": "name",
110 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
110 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
111 { data: {"_": "author",
111 { data: {"_": "author",
112 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
112 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
113 { data: {"_": "title",
113 { data: {"_": "title",
114 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
114 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
115 { data: {"_": "comments",
115 { data: {"_": "comments",
116 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
116 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
117 { data: {"_": "updated_on",
117 { data: {"_": "updated_on",
118 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
118 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
119 ],
119 ],
120 language: {
120 language: {
121 paginate: DEFAULT_GRID_PAGINATION,
121 paginate: DEFAULT_GRID_PAGINATION,
122 sProcessing: _gettext('loading...'),
122 sProcessing: _gettext('loading...'),
123 emptyTable: _gettext("No pull requests available yet.")
123 emptyTable: _gettext("No pull requests available yet.")
124 },
124 },
125 "drawCallback": function( settings, json ) {
125 "drawCallback": function( settings, json ) {
126 timeagoActivate();
126 timeagoActivate();
127 },
127 },
128 "createdRow": function ( row, data, index ) {
128 "createdRow": function ( row, data, index ) {
129 if (data['closed']) {
129 if (data['closed']) {
130 $(row).addClass('closed');
130 $(row).addClass('closed');
131 }
131 }
132 }
132 }
133 });
133 });
134
134
135 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
135 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
136 $pullRequestListTable.css('opacity', 1);
136 $pullRequestListTable.css('opacity', 1);
137 });
137 });
138
138
139 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
139 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
140 $pullRequestListTable.css('opacity', 0.3);
140 $pullRequestListTable.css('opacity', 0.3);
141 });
141 });
142
142
143 });
143 });
144
144
145 </script>
145 </script>
146 </%def>
146 </%def>
@@ -1,268 +1,269 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.tests import (
23 from rhodecode.tests import (
24 TestController, url, assert_session_flash, link_to, TEST_USER_ADMIN_LOGIN)
24 TestController, url, assert_session_flash, link_to, TEST_USER_ADMIN_LOGIN)
25 from rhodecode.model.db import User, UserGroup
25 from rhodecode.model.db import User, UserGroup
26 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
27 from rhodecode.tests.fixture import Fixture
27 from rhodecode.tests.fixture import Fixture
28
28
29 TEST_USER_GROUP = 'admins_test'
29 TEST_USER_GROUP = 'admins_test'
30
30
31 fixture = Fixture()
31 fixture = Fixture()
32
32
33
33
34 class TestAdminUsersGroupsController(TestController):
34 class TestAdminUsersGroupsController(TestController):
35
35
36 def test_index(self):
36 def test_index(self):
37 self.log_user()
37 self.log_user()
38 response = self.app.get(url('users_groups'))
38 response = self.app.get(url('users_groups'))
39 assert response.status_int == 200
39 assert response.status_int == 200
40
40
41 def test_create(self):
41 def test_create(self):
42 self.log_user()
42 self.log_user()
43 users_group_name = TEST_USER_GROUP
43 users_group_name = TEST_USER_GROUP
44 response = self.app.post(url('users_groups'), {
44 response = self.app.post(url('users_groups'), {
45 'users_group_name': users_group_name,
45 'users_group_name': users_group_name,
46 'user_group_description': 'DESC',
46 'user_group_description': 'DESC',
47 'active': True,
47 'active': True,
48 'csrf_token': self.csrf_token})
48 'csrf_token': self.csrf_token})
49
49
50 user_group_link = link_to(
50 user_group_link = link_to(
51 users_group_name,
51 users_group_name,
52 url('edit_users_group',
52 url('edit_users_group',
53 user_group_id=UserGroup.get_by_group_name(
53 user_group_id=UserGroup.get_by_group_name(
54 users_group_name).users_group_id))
54 users_group_name).users_group_id))
55 assert_session_flash(
55 assert_session_flash(
56 response,
56 response,
57 'Created user group %s' % user_group_link)
57 'Created user group %s' % user_group_link)
58
58
59 def test_set_synchronization(self):
59 def test_set_synchronization(self):
60 self.log_user()
60 self.log_user()
61 users_group_name = TEST_USER_GROUP + 'sync'
61 users_group_name = TEST_USER_GROUP + 'sync'
62 response = self.app.post(url('users_groups'), {
62 response = self.app.post(url('users_groups'), {
63 'users_group_name': users_group_name,
63 'users_group_name': users_group_name,
64 'user_group_description': 'DESC',
64 'user_group_description': 'DESC',
65 'active': True,
65 'active': True,
66 'csrf_token': self.csrf_token})
66 'csrf_token': self.csrf_token})
67
67
68 group = Session().query(UserGroup).filter(
68 group = Session().query(UserGroup).filter(
69 UserGroup.users_group_name == users_group_name).one()
69 UserGroup.users_group_name == users_group_name).one()
70
70
71 assert group.group_data.get('extern_type') is None
71 assert group.group_data.get('extern_type') is None
72
72
73 # enable
73 # enable
74 self.app.post(
74 self.app.post(
75 url('edit_user_group_advanced_sync', user_group_id=group.users_group_id),
75 url('edit_user_group_advanced_sync', user_group_id=group.users_group_id),
76 params={'csrf_token': self.csrf_token}, status=302)
76 params={'csrf_token': self.csrf_token}, status=302)
77
77
78 group = Session().query(UserGroup).filter(
78 group = Session().query(UserGroup).filter(
79 UserGroup.users_group_name == users_group_name).one()
79 UserGroup.users_group_name == users_group_name).one()
80 assert group.group_data.get('extern_type') == 'manual'
80 assert group.group_data.get('extern_type') == 'manual'
81 assert group.group_data.get('extern_type_set_by') == TEST_USER_ADMIN_LOGIN
81 assert group.group_data.get('extern_type_set_by') == TEST_USER_ADMIN_LOGIN
82
82
83 # disable
83 # disable
84 self.app.post(
84 self.app.post(
85 url('edit_user_group_advanced_sync',
85 url('edit_user_group_advanced_sync',
86 user_group_id=group.users_group_id),
86 user_group_id=group.users_group_id),
87 params={'csrf_token': self.csrf_token}, status=302)
87 params={'csrf_token': self.csrf_token}, status=302)
88
88
89 group = Session().query(UserGroup).filter(
89 group = Session().query(UserGroup).filter(
90 UserGroup.users_group_name == users_group_name).one()
90 UserGroup.users_group_name == users_group_name).one()
91 assert group.group_data.get('extern_type') is None
91 assert group.group_data.get('extern_type') is None
92 assert group.group_data.get('extern_type_set_by') == TEST_USER_ADMIN_LOGIN
92 assert group.group_data.get('extern_type_set_by') == TEST_USER_ADMIN_LOGIN
93
93
94 def test_delete(self):
94 def test_delete(self):
95 self.log_user()
95 self.log_user()
96 users_group_name = TEST_USER_GROUP + 'another'
96 users_group_name = TEST_USER_GROUP + 'another'
97 response = self.app.post(url('users_groups'), {
97 response = self.app.post(url('users_groups'), {
98 'users_group_name': users_group_name,
98 'users_group_name': users_group_name,
99 'user_group_description': 'DESC',
99 'user_group_description': 'DESC',
100 'active': True,
100 'active': True,
101 'csrf_token': self.csrf_token})
101 'csrf_token': self.csrf_token})
102
102
103 user_group_link = link_to(
103 user_group_link = link_to(
104 users_group_name,
104 users_group_name,
105 url('edit_users_group',
105 url('edit_users_group',
106 user_group_id=UserGroup.get_by_group_name(
106 user_group_id=UserGroup.get_by_group_name(
107 users_group_name).users_group_id))
107 users_group_name).users_group_id))
108 assert_session_flash(
108 assert_session_flash(
109 response,
109 response,
110 'Created user group %s' % user_group_link)
110 'Created user group %s' % user_group_link)
111
111
112 group = Session().query(UserGroup).filter(
112 group = Session().query(UserGroup).filter(
113 UserGroup.users_group_name == users_group_name).one()
113 UserGroup.users_group_name == users_group_name).one()
114
114
115 self.app.post(
115 self.app.post(
116 url('delete_users_group', user_group_id=group.users_group_id),
116 url('delete_users_group', user_group_id=group.users_group_id),
117 params={'_method': 'delete', 'csrf_token': self.csrf_token})
117 params={'_method': 'delete', 'csrf_token': self.csrf_token})
118
118
119 group = Session().query(UserGroup).filter(
119 group = Session().query(UserGroup).filter(
120 UserGroup.users_group_name == users_group_name).scalar()
120 UserGroup.users_group_name == users_group_name).scalar()
121
121
122 assert group is None
122 assert group is None
123
123
124 @pytest.mark.parametrize('repo_create, repo_create_write, user_group_create, repo_group_create, fork_create, inherit_default_permissions, expect_error, expect_form_error', [
124 @pytest.mark.parametrize('repo_create, repo_create_write, user_group_create, repo_group_create, fork_create, inherit_default_permissions, expect_error, expect_form_error', [
125 ('hg.create.none', 'hg.create.write_on_repogroup.false', 'hg.usergroup.create.false', 'hg.repogroup.create.false', 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
125 ('hg.create.none', 'hg.create.write_on_repogroup.false', 'hg.usergroup.create.false', 'hg.repogroup.create.false', 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
126 ('hg.create.repository', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, False),
126 ('hg.create.repository', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, False),
127 ('hg.create.XXX', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, True),
127 ('hg.create.XXX', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, True),
128 ('', '', '', '', '', '', True, False),
128 ('', '', '', '', '', '', True, False),
129 ])
129 ])
130 def test_global_perms_on_group(
130 def test_global_perms_on_group(
131 self, repo_create, repo_create_write, user_group_create,
131 self, repo_create, repo_create_write, user_group_create,
132 repo_group_create, fork_create, expect_error, expect_form_error,
132 repo_group_create, fork_create, expect_error, expect_form_error,
133 inherit_default_permissions):
133 inherit_default_permissions):
134 self.log_user()
134 self.log_user()
135 users_group_name = TEST_USER_GROUP + 'another2'
135 users_group_name = TEST_USER_GROUP + 'another2'
136 response = self.app.post(url('users_groups'),
136 response = self.app.post(url('users_groups'),
137 {'users_group_name': users_group_name,
137 {'users_group_name': users_group_name,
138 'user_group_description': 'DESC',
138 'user_group_description': 'DESC',
139 'active': True,
139 'active': True,
140 'csrf_token': self.csrf_token})
140 'csrf_token': self.csrf_token})
141
141
142 ug = UserGroup.get_by_group_name(users_group_name)
142 ug = UserGroup.get_by_group_name(users_group_name)
143 user_group_link = link_to(
143 user_group_link = link_to(
144 users_group_name,
144 users_group_name,
145 url('edit_users_group', user_group_id=ug.users_group_id))
145 url('edit_users_group', user_group_id=ug.users_group_id))
146 assert_session_flash(
146 assert_session_flash(
147 response,
147 response,
148 'Created user group %s' % user_group_link)
148 'Created user group %s' % user_group_link)
149 response.follow()
149 response.follow()
150
150
151 # ENABLE REPO CREATE ON A GROUP
151 # ENABLE REPO CREATE ON A GROUP
152 perm_params = {
152 perm_params = {
153 'inherit_default_permissions': False,
153 'inherit_default_permissions': False,
154 'default_repo_create': repo_create,
154 'default_repo_create': repo_create,
155 'default_repo_create_on_write': repo_create_write,
155 'default_repo_create_on_write': repo_create_write,
156 'default_user_group_create': user_group_create,
156 'default_user_group_create': user_group_create,
157 'default_repo_group_create': repo_group_create,
157 'default_repo_group_create': repo_group_create,
158 'default_fork_create': fork_create,
158 'default_fork_create': fork_create,
159 'default_inherit_default_permissions': inherit_default_permissions,
159 'default_inherit_default_permissions': inherit_default_permissions,
160
160
161 '_method': 'put',
161 '_method': 'put',
162 'csrf_token': self.csrf_token,
162 'csrf_token': self.csrf_token,
163 }
163 }
164 response = self.app.post(
164 response = self.app.post(
165 url('edit_user_group_global_perms',
165 url('edit_user_group_global_perms',
166 user_group_id=ug.users_group_id),
166 user_group_id=ug.users_group_id),
167 params=perm_params)
167 params=perm_params)
168
168
169 if expect_form_error:
169 if expect_form_error:
170 assert response.status_int == 200
170 assert response.status_int == 200
171 response.mustcontain('Value must be one of')
171 response.mustcontain('Value must be one of')
172 else:
172 else:
173 if expect_error:
173 if expect_error:
174 msg = 'An error occurred during permissions saving'
174 msg = 'An error occurred during permissions saving'
175 else:
175 else:
176 msg = 'User Group global permissions updated successfully'
176 msg = 'User Group global permissions updated successfully'
177 ug = UserGroup.get_by_group_name(users_group_name)
177 ug = UserGroup.get_by_group_name(users_group_name)
178 del perm_params['_method']
178 del perm_params['_method']
179 del perm_params['csrf_token']
179 del perm_params['csrf_token']
180 del perm_params['inherit_default_permissions']
180 del perm_params['inherit_default_permissions']
181 assert perm_params == ug.get_default_perms()
181 assert perm_params == ug.get_default_perms()
182 assert_session_flash(response, msg)
182 assert_session_flash(response, msg)
183
183
184 fixture.destroy_user_group(users_group_name)
184 fixture.destroy_user_group(users_group_name)
185
185
186 def test_edit_autocomplete(self):
186 def test_edit_autocomplete(self):
187 self.log_user()
187 self.log_user()
188 ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True)
188 ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True)
189 response = self.app.get(
189 response = self.app.get(
190 url('edit_users_group', user_group_id=ug.users_group_id))
190 url('edit_users_group', user_group_id=ug.users_group_id))
191 fixture.destroy_user_group(TEST_USER_GROUP)
191 fixture.destroy_user_group(TEST_USER_GROUP)
192
192
193 def test_edit_user_group_autocomplete_members(self, xhr_header):
193 def test_edit_user_group_autocomplete_members(self, xhr_header):
194 self.log_user()
194 self.log_user()
195 ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True)
195 ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True)
196 response = self.app.get(
196 response = self.app.get(
197 url('edit_user_group_members', user_group_id=ug.users_group_id),
197 url('edit_user_group_members', user_group_id=ug.users_group_id),
198 extra_environ=xhr_header)
198 extra_environ=xhr_header)
199
199
200 assert response.body == '{"members": []}'
200 assert response.body == '{"members": []}'
201 fixture.destroy_user_group(TEST_USER_GROUP)
201 fixture.destroy_user_group(TEST_USER_GROUP)
202
202
203 def test_usergroup_escape(self):
203 def test_usergroup_escape(self, user_util):
204 user = User.get_by_username('test_admin')
204 user = user_util.create_user(
205 user.name = '<img src="/image1" onload="alert(\'Hello, World!\');">'
205 username='escape_user',
206 user.lastname = (
206 firstname='<img src="/image2" onload="alert(\'Hello, World!\');">',
207 '<img src="/image2" onload="alert(\'Hello, World!\');">')
207 lastname='<img src="/image2" onload="alert(\'Hello, World!\');">'
208 Session().add(user)
208 )
209 Session().commit()
209
210 user_util.create_user_group(owner=user.username)
210
211
211 self.log_user()
212 self.log_user()
212 users_group_name = 'samplegroup'
213 users_group_name = 'samplegroup'
213 data = {
214 data = {
214 'users_group_name': users_group_name,
215 'users_group_name': users_group_name,
215 'user_group_description': (
216 'user_group_description': (
216 '<strong onload="alert();">DESC</strong>'),
217 '<strong onload="alert();">DESC</strong>'),
217 'active': True,
218 'active': True,
218 'csrf_token': self.csrf_token
219 'csrf_token': self.csrf_token
219 }
220 }
220
221
221 self.app.post(url('users_groups'), data)
222 self.app.post(url('users_groups'), data)
222 response = self.app.get(url('users_groups'))
223 response = self.app.get(url('users_groups'))
223
224
224 response.mustcontain(
225 response.mustcontain(
225 '&lt;strong onload=&#34;alert();&#34;&gt;'
226 '&lt;strong onload=&#34;alert();&#34;&gt;'
226 'DESC&lt;/strong&gt;')
227 'DESC&lt;/strong&gt;')
227 response.mustcontain(
228 response.mustcontain(
228 '&lt;img src=&#34;/image2&#34; onload=&#34;'
229 '&lt;img src=&#34;/image2&#34; onload=&#34;'
229 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
230 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
230
231
231 def test_update_members_from_user_ids(self, user_regular):
232 def test_update_members_from_user_ids(self, user_regular):
232 uid = user_regular.user_id
233 uid = user_regular.user_id
233 username = user_regular.username
234 username = user_regular.username
234 self.log_user()
235 self.log_user()
235
236
236 user_group = fixture.create_user_group('test_gr_ids')
237 user_group = fixture.create_user_group('test_gr_ids')
237 assert user_group.members == []
238 assert user_group.members == []
238 assert user_group.user != user_regular
239 assert user_group.user != user_regular
239 expected_active_state = not user_group.users_group_active
240 expected_active_state = not user_group.users_group_active
240
241
241 form_data = [
242 form_data = [
242 ('csrf_token', self.csrf_token),
243 ('csrf_token', self.csrf_token),
243 ('_method', 'put'),
244 ('_method', 'put'),
244 ('user', username),
245 ('user', username),
245 ('users_group_name', 'changed_name'),
246 ('users_group_name', 'changed_name'),
246 ('users_group_active', expected_active_state),
247 ('users_group_active', expected_active_state),
247 ('user_group_description', 'changed_description'),
248 ('user_group_description', 'changed_description'),
248
249
249 ('__start__', 'user_group_members:sequence'),
250 ('__start__', 'user_group_members:sequence'),
250 ('__start__', 'member:mapping'),
251 ('__start__', 'member:mapping'),
251 ('member_user_id', uid),
252 ('member_user_id', uid),
252 ('type', 'existing'),
253 ('type', 'existing'),
253 ('__end__', 'member:mapping'),
254 ('__end__', 'member:mapping'),
254 ('__end__', 'user_group_members:sequence'),
255 ('__end__', 'user_group_members:sequence'),
255 ]
256 ]
256 ugid = user_group.users_group_id
257 ugid = user_group.users_group_id
257 self.app.post(url('update_users_group', user_group_id=ugid), form_data)
258 self.app.post(url('update_users_group', user_group_id=ugid), form_data)
258
259
259 user_group = UserGroup.get(ugid)
260 user_group = UserGroup.get(ugid)
260 assert user_group
261 assert user_group
261
262
262 assert user_group.members[0].user_id == uid
263 assert user_group.members[0].user_id == uid
263 assert user_group.user_id == uid
264 assert user_group.user_id == uid
264 assert 'changed_name' in user_group.users_group_name
265 assert 'changed_name' in user_group.users_group_name
265 assert 'changed_description' in user_group.user_group_description
266 assert 'changed_description' in user_group.user_group_description
266 assert user_group.users_group_active == expected_active_state
267 assert user_group.users_group_active == expected_active_state
267
268
268 fixture.destroy_user_group(user_group)
269 fixture.destroy_user_group(user_group)
1 NO CONTENT: file was removed
NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (1018 lines changed) Show them Hide them
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now