##// END OF EJS Templates
pull-requests: overhaul of the UX by adding new sidebar...
marcink -
r4482:3b004b10 default
parent child Browse files
Show More

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

@@ -1,533 +1,543 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 # repo creating checks, special cases that aren't repo routes
25 # repo creating checks, special cases that aren't repo routes
26 config.add_route(
26 config.add_route(
27 name='repo_creating',
27 name='repo_creating',
28 pattern='/{repo_name:.*?[^/]}/repo_creating')
28 pattern='/{repo_name:.*?[^/]}/repo_creating')
29
29
30 config.add_route(
30 config.add_route(
31 name='repo_creating_check',
31 name='repo_creating_check',
32 pattern='/{repo_name:.*?[^/]}/repo_creating_check')
32 pattern='/{repo_name:.*?[^/]}/repo_creating_check')
33
33
34 # Summary
34 # Summary
35 # NOTE(marcink): one additional route is defined in very bottom, catch
35 # NOTE(marcink): one additional route is defined in very bottom, catch
36 # all pattern
36 # all pattern
37 config.add_route(
37 config.add_route(
38 name='repo_summary_explicit',
38 name='repo_summary_explicit',
39 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
39 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
40 config.add_route(
40 config.add_route(
41 name='repo_summary_commits',
41 name='repo_summary_commits',
42 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
42 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
43
43
44 # Commits
44 # Commits
45 config.add_route(
45 config.add_route(
46 name='repo_commit',
46 name='repo_commit',
47 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
47 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
48
48
49 config.add_route(
49 config.add_route(
50 name='repo_commit_children',
50 name='repo_commit_children',
51 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
51 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
52
52
53 config.add_route(
53 config.add_route(
54 name='repo_commit_parents',
54 name='repo_commit_parents',
55 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
55 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
56
56
57 config.add_route(
57 config.add_route(
58 name='repo_commit_raw',
58 name='repo_commit_raw',
59 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
59 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
60
60
61 config.add_route(
61 config.add_route(
62 name='repo_commit_patch',
62 name='repo_commit_patch',
63 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
63 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
64
64
65 config.add_route(
65 config.add_route(
66 name='repo_commit_download',
66 name='repo_commit_download',
67 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
67 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
68
68
69 config.add_route(
69 config.add_route(
70 name='repo_commit_data',
70 name='repo_commit_data',
71 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
71 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
72
72
73 config.add_route(
73 config.add_route(
74 name='repo_commit_comment_create',
74 name='repo_commit_comment_create',
75 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
75 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
76
76
77 config.add_route(
77 config.add_route(
78 name='repo_commit_comment_preview',
78 name='repo_commit_comment_preview',
79 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
79 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
80
80
81 config.add_route(
81 config.add_route(
82 name='repo_commit_comment_history_view',
82 name='repo_commit_comment_history_view',
83 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_history_id}/history_view', repo_route=True)
83 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_history_id}/history_view', repo_route=True)
84
84
85 config.add_route(
85 config.add_route(
86 name='repo_commit_comment_attachment_upload',
86 name='repo_commit_comment_attachment_upload',
87 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
87 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
88
88
89 config.add_route(
89 config.add_route(
90 name='repo_commit_comment_delete',
90 name='repo_commit_comment_delete',
91 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
91 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
92
92
93 config.add_route(
93 config.add_route(
94 name='repo_commit_comment_edit',
94 name='repo_commit_comment_edit',
95 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
95 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
96
96
97 # still working url for backward compat.
97 # still working url for backward compat.
98 config.add_route(
98 config.add_route(
99 name='repo_commit_raw_deprecated',
99 name='repo_commit_raw_deprecated',
100 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
100 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
101
101
102 # Files
102 # Files
103 config.add_route(
103 config.add_route(
104 name='repo_archivefile',
104 name='repo_archivefile',
105 pattern='/{repo_name:.*?[^/]}/archive/{fname:.*}', repo_route=True)
105 pattern='/{repo_name:.*?[^/]}/archive/{fname:.*}', repo_route=True)
106
106
107 config.add_route(
107 config.add_route(
108 name='repo_files_diff',
108 name='repo_files_diff',
109 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
109 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
110 config.add_route( # legacy route to make old links work
110 config.add_route( # legacy route to make old links work
111 name='repo_files_diff_2way_redirect',
111 name='repo_files_diff_2way_redirect',
112 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
112 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
113
113
114 config.add_route(
114 config.add_route(
115 name='repo_files',
115 name='repo_files',
116 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
116 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
117 config.add_route(
117 config.add_route(
118 name='repo_files:default_path',
118 name='repo_files:default_path',
119 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
119 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
120 config.add_route(
120 config.add_route(
121 name='repo_files:default_commit',
121 name='repo_files:default_commit',
122 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
122 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
123
123
124 config.add_route(
124 config.add_route(
125 name='repo_files:rendered',
125 name='repo_files:rendered',
126 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
126 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
127
127
128 config.add_route(
128 config.add_route(
129 name='repo_files:annotated',
129 name='repo_files:annotated',
130 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
130 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
131 config.add_route(
131 config.add_route(
132 name='repo_files:annotated_previous',
132 name='repo_files:annotated_previous',
133 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
133 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
134
134
135 config.add_route(
135 config.add_route(
136 name='repo_nodetree_full',
136 name='repo_nodetree_full',
137 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
137 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
138 config.add_route(
138 config.add_route(
139 name='repo_nodetree_full:default_path',
139 name='repo_nodetree_full:default_path',
140 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
140 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
141
141
142 config.add_route(
142 config.add_route(
143 name='repo_files_nodelist',
143 name='repo_files_nodelist',
144 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
144 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
145
145
146 config.add_route(
146 config.add_route(
147 name='repo_file_raw',
147 name='repo_file_raw',
148 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
148 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
149
149
150 config.add_route(
150 config.add_route(
151 name='repo_file_download',
151 name='repo_file_download',
152 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
152 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
153 config.add_route( # backward compat to keep old links working
153 config.add_route( # backward compat to keep old links working
154 name='repo_file_download:legacy',
154 name='repo_file_download:legacy',
155 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
155 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
156 repo_route=True)
156 repo_route=True)
157
157
158 config.add_route(
158 config.add_route(
159 name='repo_file_history',
159 name='repo_file_history',
160 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
160 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
161
161
162 config.add_route(
162 config.add_route(
163 name='repo_file_authors',
163 name='repo_file_authors',
164 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
164 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
165
165
166 config.add_route(
166 config.add_route(
167 name='repo_files_check_head',
167 name='repo_files_check_head',
168 pattern='/{repo_name:.*?[^/]}/check_head/{commit_id}/{f_path:.*}',
168 pattern='/{repo_name:.*?[^/]}/check_head/{commit_id}/{f_path:.*}',
169 repo_route=True)
169 repo_route=True)
170 config.add_route(
170 config.add_route(
171 name='repo_files_remove_file',
171 name='repo_files_remove_file',
172 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
172 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
173 repo_route=True)
173 repo_route=True)
174 config.add_route(
174 config.add_route(
175 name='repo_files_delete_file',
175 name='repo_files_delete_file',
176 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
176 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
177 repo_route=True)
177 repo_route=True)
178 config.add_route(
178 config.add_route(
179 name='repo_files_edit_file',
179 name='repo_files_edit_file',
180 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
180 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
181 repo_route=True)
181 repo_route=True)
182 config.add_route(
182 config.add_route(
183 name='repo_files_update_file',
183 name='repo_files_update_file',
184 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
184 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
185 repo_route=True)
185 repo_route=True)
186 config.add_route(
186 config.add_route(
187 name='repo_files_add_file',
187 name='repo_files_add_file',
188 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
188 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
189 repo_route=True)
189 repo_route=True)
190 config.add_route(
190 config.add_route(
191 name='repo_files_upload_file',
191 name='repo_files_upload_file',
192 pattern='/{repo_name:.*?[^/]}/upload_file/{commit_id}/{f_path:.*}',
192 pattern='/{repo_name:.*?[^/]}/upload_file/{commit_id}/{f_path:.*}',
193 repo_route=True)
193 repo_route=True)
194 config.add_route(
194 config.add_route(
195 name='repo_files_create_file',
195 name='repo_files_create_file',
196 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
196 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
197 repo_route=True)
197 repo_route=True)
198
198
199 # Refs data
199 # Refs data
200 config.add_route(
200 config.add_route(
201 name='repo_refs_data',
201 name='repo_refs_data',
202 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
202 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
203
203
204 config.add_route(
204 config.add_route(
205 name='repo_refs_changelog_data',
205 name='repo_refs_changelog_data',
206 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
206 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
207
207
208 config.add_route(
208 config.add_route(
209 name='repo_stats',
209 name='repo_stats',
210 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
210 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
211
211
212 # Commits
212 # Commits
213 config.add_route(
213 config.add_route(
214 name='repo_commits',
214 name='repo_commits',
215 pattern='/{repo_name:.*?[^/]}/commits', repo_route=True)
215 pattern='/{repo_name:.*?[^/]}/commits', repo_route=True)
216 config.add_route(
216 config.add_route(
217 name='repo_commits_file',
217 name='repo_commits_file',
218 pattern='/{repo_name:.*?[^/]}/commits/{commit_id}/{f_path:.*}', repo_route=True)
218 pattern='/{repo_name:.*?[^/]}/commits/{commit_id}/{f_path:.*}', repo_route=True)
219 config.add_route(
219 config.add_route(
220 name='repo_commits_elements',
220 name='repo_commits_elements',
221 pattern='/{repo_name:.*?[^/]}/commits_elements', repo_route=True)
221 pattern='/{repo_name:.*?[^/]}/commits_elements', repo_route=True)
222 config.add_route(
222 config.add_route(
223 name='repo_commits_elements_file',
223 name='repo_commits_elements_file',
224 pattern='/{repo_name:.*?[^/]}/commits_elements/{commit_id}/{f_path:.*}', repo_route=True)
224 pattern='/{repo_name:.*?[^/]}/commits_elements/{commit_id}/{f_path:.*}', repo_route=True)
225
225
226 # Changelog (old deprecated name for commits page)
226 # Changelog (old deprecated name for commits page)
227 config.add_route(
227 config.add_route(
228 name='repo_changelog',
228 name='repo_changelog',
229 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
229 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
230 config.add_route(
230 config.add_route(
231 name='repo_changelog_file',
231 name='repo_changelog_file',
232 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
232 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
233
233
234 # Compare
234 # Compare
235 config.add_route(
235 config.add_route(
236 name='repo_compare_select',
236 name='repo_compare_select',
237 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
237 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
238
238
239 config.add_route(
239 config.add_route(
240 name='repo_compare',
240 name='repo_compare',
241 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
241 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
242
242
243 # Tags
243 # Tags
244 config.add_route(
244 config.add_route(
245 name='tags_home',
245 name='tags_home',
246 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
246 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
247
247
248 # Branches
248 # Branches
249 config.add_route(
249 config.add_route(
250 name='branches_home',
250 name='branches_home',
251 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
251 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
252
252
253 # Bookmarks
253 # Bookmarks
254 config.add_route(
254 config.add_route(
255 name='bookmarks_home',
255 name='bookmarks_home',
256 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
256 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
257
257
258 # Forks
258 # Forks
259 config.add_route(
259 config.add_route(
260 name='repo_fork_new',
260 name='repo_fork_new',
261 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
261 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
262 repo_forbid_when_archived=True,
262 repo_forbid_when_archived=True,
263 repo_accepted_types=['hg', 'git'])
263 repo_accepted_types=['hg', 'git'])
264
264
265 config.add_route(
265 config.add_route(
266 name='repo_fork_create',
266 name='repo_fork_create',
267 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
267 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
268 repo_forbid_when_archived=True,
268 repo_forbid_when_archived=True,
269 repo_accepted_types=['hg', 'git'])
269 repo_accepted_types=['hg', 'git'])
270
270
271 config.add_route(
271 config.add_route(
272 name='repo_forks_show_all',
272 name='repo_forks_show_all',
273 pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
273 pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
274 repo_accepted_types=['hg', 'git'])
274 repo_accepted_types=['hg', 'git'])
275 config.add_route(
275 config.add_route(
276 name='repo_forks_data',
276 name='repo_forks_data',
277 pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
277 pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
278 repo_accepted_types=['hg', 'git'])
278 repo_accepted_types=['hg', 'git'])
279
279
280 # Pull Requests
280 # Pull Requests
281 config.add_route(
281 config.add_route(
282 name='pullrequest_show',
282 name='pullrequest_show',
283 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
283 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
284 repo_route=True)
284 repo_route=True)
285
285
286 config.add_route(
286 config.add_route(
287 name='pullrequest_show_all',
287 name='pullrequest_show_all',
288 pattern='/{repo_name:.*?[^/]}/pull-request',
288 pattern='/{repo_name:.*?[^/]}/pull-request',
289 repo_route=True, repo_accepted_types=['hg', 'git'])
289 repo_route=True, repo_accepted_types=['hg', 'git'])
290
290
291 config.add_route(
291 config.add_route(
292 name='pullrequest_show_all_data',
292 name='pullrequest_show_all_data',
293 pattern='/{repo_name:.*?[^/]}/pull-request-data',
293 pattern='/{repo_name:.*?[^/]}/pull-request-data',
294 repo_route=True, repo_accepted_types=['hg', 'git'])
294 repo_route=True, repo_accepted_types=['hg', 'git'])
295
295
296 config.add_route(
296 config.add_route(
297 name='pullrequest_repo_refs',
297 name='pullrequest_repo_refs',
298 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
298 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
299 repo_route=True)
299 repo_route=True)
300
300
301 config.add_route(
301 config.add_route(
302 name='pullrequest_repo_targets',
302 name='pullrequest_repo_targets',
303 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
303 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
304 repo_route=True)
304 repo_route=True)
305
305
306 config.add_route(
306 config.add_route(
307 name='pullrequest_new',
307 name='pullrequest_new',
308 pattern='/{repo_name:.*?[^/]}/pull-request/new',
308 pattern='/{repo_name:.*?[^/]}/pull-request/new',
309 repo_route=True, repo_accepted_types=['hg', 'git'],
309 repo_route=True, repo_accepted_types=['hg', 'git'],
310 repo_forbid_when_archived=True)
310 repo_forbid_when_archived=True)
311
311
312 config.add_route(
312 config.add_route(
313 name='pullrequest_create',
313 name='pullrequest_create',
314 pattern='/{repo_name:.*?[^/]}/pull-request/create',
314 pattern='/{repo_name:.*?[^/]}/pull-request/create',
315 repo_route=True, repo_accepted_types=['hg', 'git'],
315 repo_route=True, repo_accepted_types=['hg', 'git'],
316 repo_forbid_when_archived=True)
316 repo_forbid_when_archived=True)
317
317
318 config.add_route(
318 config.add_route(
319 name='pullrequest_update',
319 name='pullrequest_update',
320 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
320 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
321 repo_route=True, repo_forbid_when_archived=True)
321 repo_route=True, repo_forbid_when_archived=True)
322
322
323 config.add_route(
323 config.add_route(
324 name='pullrequest_merge',
324 name='pullrequest_merge',
325 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
325 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
326 repo_route=True, repo_forbid_when_archived=True)
326 repo_route=True, repo_forbid_when_archived=True)
327
327
328 config.add_route(
328 config.add_route(
329 name='pullrequest_delete',
329 name='pullrequest_delete',
330 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
330 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
331 repo_route=True, repo_forbid_when_archived=True)
331 repo_route=True, repo_forbid_when_archived=True)
332
332
333 config.add_route(
333 config.add_route(
334 name='pullrequest_comment_create',
334 name='pullrequest_comment_create',
335 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
335 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
336 repo_route=True)
336 repo_route=True)
337
337
338 config.add_route(
338 config.add_route(
339 name='pullrequest_comment_edit',
339 name='pullrequest_comment_edit',
340 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
340 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
341 repo_route=True, repo_accepted_types=['hg', 'git'])
341 repo_route=True, repo_accepted_types=['hg', 'git'])
342
342
343 config.add_route(
343 config.add_route(
344 name='pullrequest_comment_delete',
344 name='pullrequest_comment_delete',
345 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
345 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
346 repo_route=True, repo_accepted_types=['hg', 'git'])
346 repo_route=True, repo_accepted_types=['hg', 'git'])
347
347
348 config.add_route(
349 name='pullrequest_comments',
350 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
351 repo_route=True)
352
353 config.add_route(
354 name='pullrequest_todos',
355 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
356 repo_route=True)
357
348 # Artifacts, (EE feature)
358 # Artifacts, (EE feature)
349 config.add_route(
359 config.add_route(
350 name='repo_artifacts_list',
360 name='repo_artifacts_list',
351 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
361 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
352
362
353 # Settings
363 # Settings
354 config.add_route(
364 config.add_route(
355 name='edit_repo',
365 name='edit_repo',
356 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
366 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
357 # update is POST on edit_repo
367 # update is POST on edit_repo
358
368
359 # Settings advanced
369 # Settings advanced
360 config.add_route(
370 config.add_route(
361 name='edit_repo_advanced',
371 name='edit_repo_advanced',
362 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
372 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
363 config.add_route(
373 config.add_route(
364 name='edit_repo_advanced_archive',
374 name='edit_repo_advanced_archive',
365 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
375 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
366 config.add_route(
376 config.add_route(
367 name='edit_repo_advanced_delete',
377 name='edit_repo_advanced_delete',
368 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
378 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
369 config.add_route(
379 config.add_route(
370 name='edit_repo_advanced_locking',
380 name='edit_repo_advanced_locking',
371 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
381 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
372 config.add_route(
382 config.add_route(
373 name='edit_repo_advanced_journal',
383 name='edit_repo_advanced_journal',
374 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
384 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
375 config.add_route(
385 config.add_route(
376 name='edit_repo_advanced_fork',
386 name='edit_repo_advanced_fork',
377 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
387 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
378
388
379 config.add_route(
389 config.add_route(
380 name='edit_repo_advanced_hooks',
390 name='edit_repo_advanced_hooks',
381 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
391 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
382
392
383 # Caches
393 # Caches
384 config.add_route(
394 config.add_route(
385 name='edit_repo_caches',
395 name='edit_repo_caches',
386 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
396 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
387
397
388 # Permissions
398 # Permissions
389 config.add_route(
399 config.add_route(
390 name='edit_repo_perms',
400 name='edit_repo_perms',
391 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
401 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
392
402
393 config.add_route(
403 config.add_route(
394 name='edit_repo_perms_set_private',
404 name='edit_repo_perms_set_private',
395 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
405 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
396
406
397 # Permissions Branch (EE feature)
407 # Permissions Branch (EE feature)
398 config.add_route(
408 config.add_route(
399 name='edit_repo_perms_branch',
409 name='edit_repo_perms_branch',
400 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
410 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
401 config.add_route(
411 config.add_route(
402 name='edit_repo_perms_branch_delete',
412 name='edit_repo_perms_branch_delete',
403 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
413 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
404 repo_route=True)
414 repo_route=True)
405
415
406 # Maintenance
416 # Maintenance
407 config.add_route(
417 config.add_route(
408 name='edit_repo_maintenance',
418 name='edit_repo_maintenance',
409 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
419 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
410
420
411 config.add_route(
421 config.add_route(
412 name='edit_repo_maintenance_execute',
422 name='edit_repo_maintenance_execute',
413 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
423 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
414
424
415 # Fields
425 # Fields
416 config.add_route(
426 config.add_route(
417 name='edit_repo_fields',
427 name='edit_repo_fields',
418 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
428 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
419 config.add_route(
429 config.add_route(
420 name='edit_repo_fields_create',
430 name='edit_repo_fields_create',
421 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
431 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
422 config.add_route(
432 config.add_route(
423 name='edit_repo_fields_delete',
433 name='edit_repo_fields_delete',
424 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
434 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
425
435
426 # Locking
436 # Locking
427 config.add_route(
437 config.add_route(
428 name='repo_edit_toggle_locking',
438 name='repo_edit_toggle_locking',
429 pattern='/{repo_name:.*?[^/]}/settings/toggle_locking', repo_route=True)
439 pattern='/{repo_name:.*?[^/]}/settings/toggle_locking', repo_route=True)
430
440
431 # Remote
441 # Remote
432 config.add_route(
442 config.add_route(
433 name='edit_repo_remote',
443 name='edit_repo_remote',
434 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
444 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
435 config.add_route(
445 config.add_route(
436 name='edit_repo_remote_pull',
446 name='edit_repo_remote_pull',
437 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
447 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
438 config.add_route(
448 config.add_route(
439 name='edit_repo_remote_push',
449 name='edit_repo_remote_push',
440 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
450 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
441
451
442 # Statistics
452 # Statistics
443 config.add_route(
453 config.add_route(
444 name='edit_repo_statistics',
454 name='edit_repo_statistics',
445 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
455 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
446 config.add_route(
456 config.add_route(
447 name='edit_repo_statistics_reset',
457 name='edit_repo_statistics_reset',
448 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
458 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
449
459
450 # Issue trackers
460 # Issue trackers
451 config.add_route(
461 config.add_route(
452 name='edit_repo_issuetracker',
462 name='edit_repo_issuetracker',
453 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
463 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
454 config.add_route(
464 config.add_route(
455 name='edit_repo_issuetracker_test',
465 name='edit_repo_issuetracker_test',
456 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
466 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
457 config.add_route(
467 config.add_route(
458 name='edit_repo_issuetracker_delete',
468 name='edit_repo_issuetracker_delete',
459 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
469 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
460 config.add_route(
470 config.add_route(
461 name='edit_repo_issuetracker_update',
471 name='edit_repo_issuetracker_update',
462 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
472 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
463
473
464 # VCS Settings
474 # VCS Settings
465 config.add_route(
475 config.add_route(
466 name='edit_repo_vcs',
476 name='edit_repo_vcs',
467 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
477 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
468 config.add_route(
478 config.add_route(
469 name='edit_repo_vcs_update',
479 name='edit_repo_vcs_update',
470 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
480 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
471
481
472 # svn pattern
482 # svn pattern
473 config.add_route(
483 config.add_route(
474 name='edit_repo_vcs_svn_pattern_delete',
484 name='edit_repo_vcs_svn_pattern_delete',
475 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
485 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
476
486
477 # Repo Review Rules (EE feature)
487 # Repo Review Rules (EE feature)
478 config.add_route(
488 config.add_route(
479 name='repo_reviewers',
489 name='repo_reviewers',
480 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
490 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
481
491
482 config.add_route(
492 config.add_route(
483 name='repo_default_reviewers_data',
493 name='repo_default_reviewers_data',
484 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
494 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
485
495
486 # Repo Automation (EE feature)
496 # Repo Automation (EE feature)
487 config.add_route(
497 config.add_route(
488 name='repo_automation',
498 name='repo_automation',
489 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
499 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
490
500
491 # Strip
501 # Strip
492 config.add_route(
502 config.add_route(
493 name='edit_repo_strip',
503 name='edit_repo_strip',
494 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
504 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
495
505
496 config.add_route(
506 config.add_route(
497 name='strip_check',
507 name='strip_check',
498 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
508 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
499
509
500 config.add_route(
510 config.add_route(
501 name='strip_execute',
511 name='strip_execute',
502 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
512 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
503
513
504 # Audit logs
514 # Audit logs
505 config.add_route(
515 config.add_route(
506 name='edit_repo_audit_logs',
516 name='edit_repo_audit_logs',
507 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
517 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
508
518
509 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
519 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
510 config.add_route(
520 config.add_route(
511 name='rss_feed_home',
521 name='rss_feed_home',
512 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
522 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
513
523
514 config.add_route(
524 config.add_route(
515 name='atom_feed_home',
525 name='atom_feed_home',
516 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
526 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
517
527
518 config.add_route(
528 config.add_route(
519 name='rss_feed_home_old',
529 name='rss_feed_home_old',
520 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
530 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
521
531
522 config.add_route(
532 config.add_route(
523 name='atom_feed_home_old',
533 name='atom_feed_home_old',
524 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
534 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
525
535
526 # NOTE(marcink): needs to be at the end for catch-all
536 # NOTE(marcink): needs to be at the end for catch-all
527 add_route_with_slash(
537 add_route_with_slash(
528 config,
538 config,
529 name='repo_summary',
539 name='repo_summary',
530 pattern='/{repo_name:.*?[^/]}', repo_route=True)
540 pattern='/{repo_name:.*?[^/]}', repo_route=True)
531
541
532 # Scan module for configuration decorators.
542 # Scan module for configuration decorators.
533 config.scan('.views', ignore='.tests')
543 config.scan('.views', ignore='.tests')
@@ -1,723 +1,725 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 logging
22 import logging
23
23
24 from pyramid.httpexceptions import (
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store import utils as store_utils
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33
33
34 from rhodecode.lib import diffs, codeblocks
34 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37
37
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.diffs import (
39 from rhodecode.lib.diffs import (
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 get_diff_whitespace_flag)
41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 import rhodecode.lib.helpers as h
43 import rhodecode.lib.helpers as h
44 from rhodecode.lib.utils2 import safe_unicode, str2bool
44 from rhodecode.lib.utils2 import safe_unicode, str2bool
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 RepositoryError, CommitDoesNotExistError)
47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 ChangesetCommentHistory
49 ChangesetCommentHistory
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.settings import VcsSettingsModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 def _update_with_GET(params, request):
58 def _update_with_GET(params, request):
59 for k in ['diff1', 'diff2', 'diff']:
59 for k in ['diff1', 'diff2', 'diff']:
60 params[k] += request.GET.getall(k)
60 params[k] += request.GET.getall(k)
61
61
62
62
63 class RepoCommitsView(RepoAppView):
63 class RepoCommitsView(RepoAppView):
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c = self._get_local_tmpl_context(include_app_defaults=True)
66 c.rhodecode_repo = self.rhodecode_vcs_repo
66 c.rhodecode_repo = self.rhodecode_vcs_repo
67
67
68 return c
68 return c
69
69
70 def _is_diff_cache_enabled(self, target_repo):
70 def _is_diff_cache_enabled(self, target_repo):
71 caching_enabled = self._get_general_setting(
71 caching_enabled = self._get_general_setting(
72 target_repo, 'rhodecode_diff_cache')
72 target_repo, 'rhodecode_diff_cache')
73 log.debug('Diff caching enabled: %s', caching_enabled)
73 log.debug('Diff caching enabled: %s', caching_enabled)
74 return caching_enabled
74 return caching_enabled
75
75
76 def _commit(self, commit_id_range, method):
76 def _commit(self, commit_id_range, method):
77 _ = self.request.translate
77 _ = self.request.translate
78 c = self.load_default_context()
78 c = self.load_default_context()
79 c.fulldiff = self.request.GET.get('fulldiff')
79 c.fulldiff = self.request.GET.get('fulldiff')
80
80
81 # fetch global flags of ignore ws or context lines
81 # fetch global flags of ignore ws or context lines
82 diff_context = get_diff_context(self.request)
82 diff_context = get_diff_context(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84
84
85 # diff_limit will cut off the whole diff if the limit is applied
85 # diff_limit will cut off the whole diff if the limit is applied
86 # otherwise it will just hide the big files from the front-end
86 # otherwise it will just hide the big files from the front-end
87 diff_limit = c.visual.cut_off_limit_diff
87 diff_limit = c.visual.cut_off_limit_diff
88 file_limit = c.visual.cut_off_limit_file
88 file_limit = c.visual.cut_off_limit_file
89
89
90
90 # get ranges of commit ids if preset
91 # get ranges of commit ids if preset
91 commit_range = commit_id_range.split('...')[:2]
92 commit_range = commit_id_range.split('...')[:2]
92
93
93 try:
94 try:
94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 pre_load = ['affected_files', 'author', 'branch', 'date',
95 'message', 'parents']
96 'message', 'parents']
96 if self.rhodecode_vcs_repo.alias == 'hg':
97 if self.rhodecode_vcs_repo.alias == 'hg':
97 pre_load += ['hidden', 'obsolete', 'phase']
98 pre_load += ['hidden', 'obsolete', 'phase']
98
99
99 if len(commit_range) == 2:
100 if len(commit_range) == 2:
100 commits = self.rhodecode_vcs_repo.get_commits(
101 commits = self.rhodecode_vcs_repo.get_commits(
101 start_id=commit_range[0], end_id=commit_range[1],
102 start_id=commit_range[0], end_id=commit_range[1],
102 pre_load=pre_load, translate_tags=False)
103 pre_load=pre_load, translate_tags=False)
103 commits = list(commits)
104 commits = list(commits)
104 else:
105 else:
105 commits = [self.rhodecode_vcs_repo.get_commit(
106 commits = [self.rhodecode_vcs_repo.get_commit(
106 commit_id=commit_id_range, pre_load=pre_load)]
107 commit_id=commit_id_range, pre_load=pre_load)]
107
108
108 c.commit_ranges = commits
109 c.commit_ranges = commits
109 if not c.commit_ranges:
110 if not c.commit_ranges:
110 raise RepositoryError('The commit range returned an empty result')
111 raise RepositoryError('The commit range returned an empty result')
111 except CommitDoesNotExistError as e:
112 except CommitDoesNotExistError as e:
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 h.flash(msg, category='error')
114 h.flash(msg, category='error')
114 raise HTTPNotFound()
115 raise HTTPNotFound()
115 except Exception:
116 except Exception:
116 log.exception("General failure")
117 log.exception("General failure")
117 raise HTTPNotFound()
118 raise HTTPNotFound()
118
119
119 c.changes = OrderedDict()
120 c.changes = OrderedDict()
120 c.lines_added = 0
121 c.lines_added = 0
121 c.lines_deleted = 0
122 c.lines_deleted = 0
122
123
123 # auto collapse if we have more than limit
124 # auto collapse if we have more than limit
124 collapse_limit = diffs.DiffProcessor._collapse_commits_over
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
125 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
126
127
127 c.commit_statuses = ChangesetStatus.STATUSES
128 c.commit_statuses = ChangesetStatus.STATUSES
128 c.inline_comments = []
129 c.inline_comments = []
129 c.files = []
130 c.files = []
130
131
131 c.statuses = []
132 c.statuses = []
132 c.comments = []
133 c.comments = []
133 c.unresolved_comments = []
134 c.unresolved_comments = []
134 c.resolved_comments = []
135 c.resolved_comments = []
135 if len(c.commit_ranges) == 1:
136 if len(c.commit_ranges) == 1:
136 commit = c.commit_ranges[0]
137 commit = c.commit_ranges[0]
137 c.comments = CommentsModel().get_comments(
138 c.comments = CommentsModel().get_comments(
138 self.db_repo.repo_id,
139 self.db_repo.repo_id,
139 revision=commit.raw_id)
140 revision=commit.raw_id)
140 c.statuses.append(ChangesetStatusModel().get_status(
141 c.statuses.append(ChangesetStatusModel().get_status(
141 self.db_repo.repo_id, commit.raw_id))
142 self.db_repo.repo_id, commit.raw_id))
142 # comments from PR
143 # comments from PR
143 statuses = ChangesetStatusModel().get_statuses(
144 statuses = ChangesetStatusModel().get_statuses(
144 self.db_repo.repo_id, commit.raw_id,
145 self.db_repo.repo_id, commit.raw_id,
145 with_revisions=True)
146 with_revisions=True)
146 prs = set(st.pull_request for st in statuses
147 prs = set(st.pull_request for st in statuses
147 if st.pull_request is not None)
148 if st.pull_request is not None)
148 # from associated statuses, check the pull requests, and
149 # from associated statuses, check the pull requests, and
149 # show comments from them
150 # show comments from them
150 for pr in prs:
151 for pr in prs:
151 c.comments.extend(pr.comments)
152 c.comments.extend(pr.comments)
152
153
153 c.unresolved_comments = CommentsModel()\
154 c.unresolved_comments = CommentsModel()\
154 .get_commit_unresolved_todos(commit.raw_id)
155 .get_commit_unresolved_todos(commit.raw_id)
155 c.resolved_comments = CommentsModel()\
156 c.resolved_comments = CommentsModel()\
156 .get_commit_resolved_todos(commit.raw_id)
157 .get_commit_resolved_todos(commit.raw_id)
157
158
158 diff = None
159 diff = None
159 # Iterate over ranges (default commit view is always one commit)
160 # Iterate over ranges (default commit view is always one commit)
160 for commit in c.commit_ranges:
161 for commit in c.commit_ranges:
161 c.changes[commit.raw_id] = []
162 c.changes[commit.raw_id] = []
162
163
163 commit2 = commit
164 commit2 = commit
164 commit1 = commit.first_parent
165 commit1 = commit.first_parent
165
166
166 if method == 'show':
167 if method == 'show':
167 inline_comments = CommentsModel().get_inline_comments(
168 inline_comments = CommentsModel().get_inline_comments(
168 self.db_repo.repo_id, revision=commit.raw_id)
169 self.db_repo.repo_id, revision=commit.raw_id)
169 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
170 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
170 inline_comments))
171 inline_comments))
171 c.inline_comments = inline_comments
172 c.inline_comments = inline_comments
172
173
173 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
174 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
174 self.db_repo)
175 self.db_repo)
175 cache_file_path = diff_cache_exist(
176 cache_file_path = diff_cache_exist(
176 cache_path, 'diff', commit.raw_id,
177 cache_path, 'diff', commit.raw_id,
177 hide_whitespace_changes, diff_context, c.fulldiff)
178 hide_whitespace_changes, diff_context, c.fulldiff)
178
179
179 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
180 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
180 force_recache = str2bool(self.request.GET.get('force_recache'))
181 force_recache = str2bool(self.request.GET.get('force_recache'))
181
182
182 cached_diff = None
183 cached_diff = None
183 if caching_enabled:
184 if caching_enabled:
184 cached_diff = load_cached_diff(cache_file_path)
185 cached_diff = load_cached_diff(cache_file_path)
185
186
186 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
187 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
187 if not force_recache and has_proper_diff_cache:
188 if not force_recache and has_proper_diff_cache:
188 diffset = cached_diff['diff']
189 diffset = cached_diff['diff']
189 else:
190 else:
190 vcs_diff = self.rhodecode_vcs_repo.get_diff(
191 vcs_diff = self.rhodecode_vcs_repo.get_diff(
191 commit1, commit2,
192 commit1, commit2,
192 ignore_whitespace=hide_whitespace_changes,
193 ignore_whitespace=hide_whitespace_changes,
193 context=diff_context)
194 context=diff_context)
194
195
195 diff_processor = diffs.DiffProcessor(
196 diff_processor = diffs.DiffProcessor(
196 vcs_diff, format='newdiff', diff_limit=diff_limit,
197 vcs_diff, format='newdiff', diff_limit=diff_limit,
197 file_limit=file_limit, show_full_diff=c.fulldiff)
198 file_limit=file_limit, show_full_diff=c.fulldiff)
198
199
199 _parsed = diff_processor.prepare()
200 _parsed = diff_processor.prepare()
200
201
201 diffset = codeblocks.DiffSet(
202 diffset = codeblocks.DiffSet(
202 repo_name=self.db_repo_name,
203 repo_name=self.db_repo_name,
203 source_node_getter=codeblocks.diffset_node_getter(commit1),
204 source_node_getter=codeblocks.diffset_node_getter(commit1),
204 target_node_getter=codeblocks.diffset_node_getter(commit2))
205 target_node_getter=codeblocks.diffset_node_getter(commit2))
205
206
206 diffset = self.path_filter.render_patchset_filtered(
207 diffset = self.path_filter.render_patchset_filtered(
207 diffset, _parsed, commit1.raw_id, commit2.raw_id)
208 diffset, _parsed, commit1.raw_id, commit2.raw_id)
208
209
209 # save cached diff
210 # save cached diff
210 if caching_enabled:
211 if caching_enabled:
211 cache_diff(cache_file_path, diffset, None)
212 cache_diff(cache_file_path, diffset, None)
212
213
213 c.limited_diff = diffset.limited_diff
214 c.limited_diff = diffset.limited_diff
214 c.changes[commit.raw_id] = diffset
215 c.changes[commit.raw_id] = diffset
215 else:
216 else:
216 # TODO(marcink): no cache usage here...
217 # TODO(marcink): no cache usage here...
217 _diff = self.rhodecode_vcs_repo.get_diff(
218 _diff = self.rhodecode_vcs_repo.get_diff(
218 commit1, commit2,
219 commit1, commit2,
219 ignore_whitespace=hide_whitespace_changes, context=diff_context)
220 ignore_whitespace=hide_whitespace_changes, context=diff_context)
220 diff_processor = diffs.DiffProcessor(
221 diff_processor = diffs.DiffProcessor(
221 _diff, format='newdiff', diff_limit=diff_limit,
222 _diff, format='newdiff', diff_limit=diff_limit,
222 file_limit=file_limit, show_full_diff=c.fulldiff)
223 file_limit=file_limit, show_full_diff=c.fulldiff)
223 # downloads/raw we only need RAW diff nothing else
224 # downloads/raw we only need RAW diff nothing else
224 diff = self.path_filter.get_raw_patch(diff_processor)
225 diff = self.path_filter.get_raw_patch(diff_processor)
225 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
226 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
226
227
227 # sort comments by how they were generated
228 # sort comments by how they were generated
228 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
229 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
230 c.at_version_num = None
229
231
230 if len(c.commit_ranges) == 1:
232 if len(c.commit_ranges) == 1:
231 c.commit = c.commit_ranges[0]
233 c.commit = c.commit_ranges[0]
232 c.parent_tmpl = ''.join(
234 c.parent_tmpl = ''.join(
233 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
235 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
234
236
235 if method == 'download':
237 if method == 'download':
236 response = Response(diff)
238 response = Response(diff)
237 response.content_type = 'text/plain'
239 response.content_type = 'text/plain'
238 response.content_disposition = (
240 response.content_disposition = (
239 'attachment; filename=%s.diff' % commit_id_range[:12])
241 'attachment; filename=%s.diff' % commit_id_range[:12])
240 return response
242 return response
241 elif method == 'patch':
243 elif method == 'patch':
242 c.diff = safe_unicode(diff)
244 c.diff = safe_unicode(diff)
243 patch = render(
245 patch = render(
244 'rhodecode:templates/changeset/patch_changeset.mako',
246 'rhodecode:templates/changeset/patch_changeset.mako',
245 self._get_template_context(c), self.request)
247 self._get_template_context(c), self.request)
246 response = Response(patch)
248 response = Response(patch)
247 response.content_type = 'text/plain'
249 response.content_type = 'text/plain'
248 return response
250 return response
249 elif method == 'raw':
251 elif method == 'raw':
250 response = Response(diff)
252 response = Response(diff)
251 response.content_type = 'text/plain'
253 response.content_type = 'text/plain'
252 return response
254 return response
253 elif method == 'show':
255 elif method == 'show':
254 if len(c.commit_ranges) == 1:
256 if len(c.commit_ranges) == 1:
255 html = render(
257 html = render(
256 'rhodecode:templates/changeset/changeset.mako',
258 'rhodecode:templates/changeset/changeset.mako',
257 self._get_template_context(c), self.request)
259 self._get_template_context(c), self.request)
258 return Response(html)
260 return Response(html)
259 else:
261 else:
260 c.ancestor = None
262 c.ancestor = None
261 c.target_repo = self.db_repo
263 c.target_repo = self.db_repo
262 html = render(
264 html = render(
263 'rhodecode:templates/changeset/changeset_range.mako',
265 'rhodecode:templates/changeset/changeset_range.mako',
264 self._get_template_context(c), self.request)
266 self._get_template_context(c), self.request)
265 return Response(html)
267 return Response(html)
266
268
267 raise HTTPBadRequest()
269 raise HTTPBadRequest()
268
270
269 @LoginRequired()
271 @LoginRequired()
270 @HasRepoPermissionAnyDecorator(
272 @HasRepoPermissionAnyDecorator(
271 'repository.read', 'repository.write', 'repository.admin')
273 'repository.read', 'repository.write', 'repository.admin')
272 @view_config(
274 @view_config(
273 route_name='repo_commit', request_method='GET',
275 route_name='repo_commit', request_method='GET',
274 renderer=None)
276 renderer=None)
275 def repo_commit_show(self):
277 def repo_commit_show(self):
276 commit_id = self.request.matchdict['commit_id']
278 commit_id = self.request.matchdict['commit_id']
277 return self._commit(commit_id, method='show')
279 return self._commit(commit_id, method='show')
278
280
279 @LoginRequired()
281 @LoginRequired()
280 @HasRepoPermissionAnyDecorator(
282 @HasRepoPermissionAnyDecorator(
281 'repository.read', 'repository.write', 'repository.admin')
283 'repository.read', 'repository.write', 'repository.admin')
282 @view_config(
284 @view_config(
283 route_name='repo_commit_raw', request_method='GET',
285 route_name='repo_commit_raw', request_method='GET',
284 renderer=None)
286 renderer=None)
285 @view_config(
287 @view_config(
286 route_name='repo_commit_raw_deprecated', request_method='GET',
288 route_name='repo_commit_raw_deprecated', request_method='GET',
287 renderer=None)
289 renderer=None)
288 def repo_commit_raw(self):
290 def repo_commit_raw(self):
289 commit_id = self.request.matchdict['commit_id']
291 commit_id = self.request.matchdict['commit_id']
290 return self._commit(commit_id, method='raw')
292 return self._commit(commit_id, method='raw')
291
293
292 @LoginRequired()
294 @LoginRequired()
293 @HasRepoPermissionAnyDecorator(
295 @HasRepoPermissionAnyDecorator(
294 'repository.read', 'repository.write', 'repository.admin')
296 'repository.read', 'repository.write', 'repository.admin')
295 @view_config(
297 @view_config(
296 route_name='repo_commit_patch', request_method='GET',
298 route_name='repo_commit_patch', request_method='GET',
297 renderer=None)
299 renderer=None)
298 def repo_commit_patch(self):
300 def repo_commit_patch(self):
299 commit_id = self.request.matchdict['commit_id']
301 commit_id = self.request.matchdict['commit_id']
300 return self._commit(commit_id, method='patch')
302 return self._commit(commit_id, method='patch')
301
303
302 @LoginRequired()
304 @LoginRequired()
303 @HasRepoPermissionAnyDecorator(
305 @HasRepoPermissionAnyDecorator(
304 'repository.read', 'repository.write', 'repository.admin')
306 'repository.read', 'repository.write', 'repository.admin')
305 @view_config(
307 @view_config(
306 route_name='repo_commit_download', request_method='GET',
308 route_name='repo_commit_download', request_method='GET',
307 renderer=None)
309 renderer=None)
308 def repo_commit_download(self):
310 def repo_commit_download(self):
309 commit_id = self.request.matchdict['commit_id']
311 commit_id = self.request.matchdict['commit_id']
310 return self._commit(commit_id, method='download')
312 return self._commit(commit_id, method='download')
311
313
312 @LoginRequired()
314 @LoginRequired()
313 @NotAnonymous()
315 @NotAnonymous()
314 @HasRepoPermissionAnyDecorator(
316 @HasRepoPermissionAnyDecorator(
315 'repository.read', 'repository.write', 'repository.admin')
317 'repository.read', 'repository.write', 'repository.admin')
316 @CSRFRequired()
318 @CSRFRequired()
317 @view_config(
319 @view_config(
318 route_name='repo_commit_comment_create', request_method='POST',
320 route_name='repo_commit_comment_create', request_method='POST',
319 renderer='json_ext')
321 renderer='json_ext')
320 def repo_commit_comment_create(self):
322 def repo_commit_comment_create(self):
321 _ = self.request.translate
323 _ = self.request.translate
322 commit_id = self.request.matchdict['commit_id']
324 commit_id = self.request.matchdict['commit_id']
323
325
324 c = self.load_default_context()
326 c = self.load_default_context()
325 status = self.request.POST.get('changeset_status', None)
327 status = self.request.POST.get('changeset_status', None)
326 text = self.request.POST.get('text')
328 text = self.request.POST.get('text')
327 comment_type = self.request.POST.get('comment_type')
329 comment_type = self.request.POST.get('comment_type')
328 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
330 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
329
331
330 if status:
332 if status:
331 text = text or (_('Status change %(transition_icon)s %(status)s')
333 text = text or (_('Status change %(transition_icon)s %(status)s')
332 % {'transition_icon': '>',
334 % {'transition_icon': '>',
333 'status': ChangesetStatus.get_status_lbl(status)})
335 'status': ChangesetStatus.get_status_lbl(status)})
334
336
335 multi_commit_ids = []
337 multi_commit_ids = []
336 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
338 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
337 if _commit_id not in ['', None, EmptyCommit.raw_id]:
339 if _commit_id not in ['', None, EmptyCommit.raw_id]:
338 if _commit_id not in multi_commit_ids:
340 if _commit_id not in multi_commit_ids:
339 multi_commit_ids.append(_commit_id)
341 multi_commit_ids.append(_commit_id)
340
342
341 commit_ids = multi_commit_ids or [commit_id]
343 commit_ids = multi_commit_ids or [commit_id]
342
344
343 comment = None
345 comment = None
344 for current_id in filter(None, commit_ids):
346 for current_id in filter(None, commit_ids):
345 comment = CommentsModel().create(
347 comment = CommentsModel().create(
346 text=text,
348 text=text,
347 repo=self.db_repo.repo_id,
349 repo=self.db_repo.repo_id,
348 user=self._rhodecode_db_user.user_id,
350 user=self._rhodecode_db_user.user_id,
349 commit_id=current_id,
351 commit_id=current_id,
350 f_path=self.request.POST.get('f_path'),
352 f_path=self.request.POST.get('f_path'),
351 line_no=self.request.POST.get('line'),
353 line_no=self.request.POST.get('line'),
352 status_change=(ChangesetStatus.get_status_lbl(status)
354 status_change=(ChangesetStatus.get_status_lbl(status)
353 if status else None),
355 if status else None),
354 status_change_type=status,
356 status_change_type=status,
355 comment_type=comment_type,
357 comment_type=comment_type,
356 resolves_comment_id=resolves_comment_id,
358 resolves_comment_id=resolves_comment_id,
357 auth_user=self._rhodecode_user
359 auth_user=self._rhodecode_user
358 )
360 )
359
361
360 # get status if set !
362 # get status if set !
361 if status:
363 if status:
362 # if latest status was from pull request and it's closed
364 # if latest status was from pull request and it's closed
363 # disallow changing status !
365 # disallow changing status !
364 # dont_allow_on_closed_pull_request = True !
366 # dont_allow_on_closed_pull_request = True !
365
367
366 try:
368 try:
367 ChangesetStatusModel().set_status(
369 ChangesetStatusModel().set_status(
368 self.db_repo.repo_id,
370 self.db_repo.repo_id,
369 status,
371 status,
370 self._rhodecode_db_user.user_id,
372 self._rhodecode_db_user.user_id,
371 comment,
373 comment,
372 revision=current_id,
374 revision=current_id,
373 dont_allow_on_closed_pull_request=True
375 dont_allow_on_closed_pull_request=True
374 )
376 )
375 except StatusChangeOnClosedPullRequestError:
377 except StatusChangeOnClosedPullRequestError:
376 msg = _('Changing the status of a commit associated with '
378 msg = _('Changing the status of a commit associated with '
377 'a closed pull request is not allowed')
379 'a closed pull request is not allowed')
378 log.exception(msg)
380 log.exception(msg)
379 h.flash(msg, category='warning')
381 h.flash(msg, category='warning')
380 raise HTTPFound(h.route_path(
382 raise HTTPFound(h.route_path(
381 'repo_commit', repo_name=self.db_repo_name,
383 'repo_commit', repo_name=self.db_repo_name,
382 commit_id=current_id))
384 commit_id=current_id))
383
385
384 commit = self.db_repo.get_commit(current_id)
386 commit = self.db_repo.get_commit(current_id)
385 CommentsModel().trigger_commit_comment_hook(
387 CommentsModel().trigger_commit_comment_hook(
386 self.db_repo, self._rhodecode_user, 'create',
388 self.db_repo, self._rhodecode_user, 'create',
387 data={'comment': comment, 'commit': commit})
389 data={'comment': comment, 'commit': commit})
388
390
389 # finalize, commit and redirect
391 # finalize, commit and redirect
390 Session().commit()
392 Session().commit()
391
393
392 data = {
394 data = {
393 'target_id': h.safeid(h.safe_unicode(
395 'target_id': h.safeid(h.safe_unicode(
394 self.request.POST.get('f_path'))),
396 self.request.POST.get('f_path'))),
395 }
397 }
396 if comment:
398 if comment:
397 c.co = comment
399 c.co = comment
398 rendered_comment = render(
400 rendered_comment = render(
399 'rhodecode:templates/changeset/changeset_comment_block.mako',
401 'rhodecode:templates/changeset/changeset_comment_block.mako',
400 self._get_template_context(c), self.request)
402 self._get_template_context(c), self.request)
401
403
402 data.update(comment.get_dict())
404 data.update(comment.get_dict())
403 data.update({'rendered_text': rendered_comment})
405 data.update({'rendered_text': rendered_comment})
404
406
405 return data
407 return data
406
408
407 @LoginRequired()
409 @LoginRequired()
408 @NotAnonymous()
410 @NotAnonymous()
409 @HasRepoPermissionAnyDecorator(
411 @HasRepoPermissionAnyDecorator(
410 'repository.read', 'repository.write', 'repository.admin')
412 'repository.read', 'repository.write', 'repository.admin')
411 @CSRFRequired()
413 @CSRFRequired()
412 @view_config(
414 @view_config(
413 route_name='repo_commit_comment_preview', request_method='POST',
415 route_name='repo_commit_comment_preview', request_method='POST',
414 renderer='string', xhr=True)
416 renderer='string', xhr=True)
415 def repo_commit_comment_preview(self):
417 def repo_commit_comment_preview(self):
416 # Technically a CSRF token is not needed as no state changes with this
418 # Technically a CSRF token is not needed as no state changes with this
417 # call. However, as this is a POST is better to have it, so automated
419 # call. However, as this is a POST is better to have it, so automated
418 # tools don't flag it as potential CSRF.
420 # tools don't flag it as potential CSRF.
419 # Post is required because the payload could be bigger than the maximum
421 # Post is required because the payload could be bigger than the maximum
420 # allowed by GET.
422 # allowed by GET.
421
423
422 text = self.request.POST.get('text')
424 text = self.request.POST.get('text')
423 renderer = self.request.POST.get('renderer') or 'rst'
425 renderer = self.request.POST.get('renderer') or 'rst'
424 if text:
426 if text:
425 return h.render(text, renderer=renderer, mentions=True,
427 return h.render(text, renderer=renderer, mentions=True,
426 repo_name=self.db_repo_name)
428 repo_name=self.db_repo_name)
427 return ''
429 return ''
428
430
429 @LoginRequired()
431 @LoginRequired()
430 @NotAnonymous()
432 @NotAnonymous()
431 @HasRepoPermissionAnyDecorator(
433 @HasRepoPermissionAnyDecorator(
432 'repository.read', 'repository.write', 'repository.admin')
434 'repository.read', 'repository.write', 'repository.admin')
433 @CSRFRequired()
435 @CSRFRequired()
434 @view_config(
436 @view_config(
435 route_name='repo_commit_comment_history_view', request_method='POST',
437 route_name='repo_commit_comment_history_view', request_method='POST',
436 renderer='string', xhr=True)
438 renderer='string', xhr=True)
437 def repo_commit_comment_history_view(self):
439 def repo_commit_comment_history_view(self):
438 c = self.load_default_context()
440 c = self.load_default_context()
439
441
440 comment_history_id = self.request.matchdict['comment_history_id']
442 comment_history_id = self.request.matchdict['comment_history_id']
441 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
443 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
442 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
444 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
443
445
444 if is_repo_comment:
446 if is_repo_comment:
445 c.comment_history = comment_history
447 c.comment_history = comment_history
446
448
447 rendered_comment = render(
449 rendered_comment = render(
448 'rhodecode:templates/changeset/comment_history.mako',
450 'rhodecode:templates/changeset/comment_history.mako',
449 self._get_template_context(c)
451 self._get_template_context(c)
450 , self.request)
452 , self.request)
451 return rendered_comment
453 return rendered_comment
452 else:
454 else:
453 log.warning('No permissions for user %s to show comment_history_id: %s',
455 log.warning('No permissions for user %s to show comment_history_id: %s',
454 self._rhodecode_db_user, comment_history_id)
456 self._rhodecode_db_user, comment_history_id)
455 raise HTTPNotFound()
457 raise HTTPNotFound()
456
458
457 @LoginRequired()
459 @LoginRequired()
458 @NotAnonymous()
460 @NotAnonymous()
459 @HasRepoPermissionAnyDecorator(
461 @HasRepoPermissionAnyDecorator(
460 'repository.read', 'repository.write', 'repository.admin')
462 'repository.read', 'repository.write', 'repository.admin')
461 @CSRFRequired()
463 @CSRFRequired()
462 @view_config(
464 @view_config(
463 route_name='repo_commit_comment_attachment_upload', request_method='POST',
465 route_name='repo_commit_comment_attachment_upload', request_method='POST',
464 renderer='json_ext', xhr=True)
466 renderer='json_ext', xhr=True)
465 def repo_commit_comment_attachment_upload(self):
467 def repo_commit_comment_attachment_upload(self):
466 c = self.load_default_context()
468 c = self.load_default_context()
467 upload_key = 'attachment'
469 upload_key = 'attachment'
468
470
469 file_obj = self.request.POST.get(upload_key)
471 file_obj = self.request.POST.get(upload_key)
470
472
471 if file_obj is None:
473 if file_obj is None:
472 self.request.response.status = 400
474 self.request.response.status = 400
473 return {'store_fid': None,
475 return {'store_fid': None,
474 'access_path': None,
476 'access_path': None,
475 'error': '{} data field is missing'.format(upload_key)}
477 'error': '{} data field is missing'.format(upload_key)}
476
478
477 if not hasattr(file_obj, 'filename'):
479 if not hasattr(file_obj, 'filename'):
478 self.request.response.status = 400
480 self.request.response.status = 400
479 return {'store_fid': None,
481 return {'store_fid': None,
480 'access_path': None,
482 'access_path': None,
481 'error': 'filename cannot be read from the data field'}
483 'error': 'filename cannot be read from the data field'}
482
484
483 filename = file_obj.filename
485 filename = file_obj.filename
484 file_display_name = filename
486 file_display_name = filename
485
487
486 metadata = {
488 metadata = {
487 'user_uploaded': {'username': self._rhodecode_user.username,
489 'user_uploaded': {'username': self._rhodecode_user.username,
488 'user_id': self._rhodecode_user.user_id,
490 'user_id': self._rhodecode_user.user_id,
489 'ip': self._rhodecode_user.ip_addr}}
491 'ip': self._rhodecode_user.ip_addr}}
490
492
491 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
493 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
492 allowed_extensions = [
494 allowed_extensions = [
493 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
495 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
494 '.pptx', '.txt', '.xlsx', '.zip']
496 '.pptx', '.txt', '.xlsx', '.zip']
495 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
497 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
496
498
497 try:
499 try:
498 storage = store_utils.get_file_storage(self.request.registry.settings)
500 storage = store_utils.get_file_storage(self.request.registry.settings)
499 store_uid, metadata = storage.save_file(
501 store_uid, metadata = storage.save_file(
500 file_obj.file, filename, extra_metadata=metadata,
502 file_obj.file, filename, extra_metadata=metadata,
501 extensions=allowed_extensions, max_filesize=max_file_size)
503 extensions=allowed_extensions, max_filesize=max_file_size)
502 except FileNotAllowedException:
504 except FileNotAllowedException:
503 self.request.response.status = 400
505 self.request.response.status = 400
504 permitted_extensions = ', '.join(allowed_extensions)
506 permitted_extensions = ', '.join(allowed_extensions)
505 error_msg = 'File `{}` is not allowed. ' \
507 error_msg = 'File `{}` is not allowed. ' \
506 'Only following extensions are permitted: {}'.format(
508 'Only following extensions are permitted: {}'.format(
507 filename, permitted_extensions)
509 filename, permitted_extensions)
508 return {'store_fid': None,
510 return {'store_fid': None,
509 'access_path': None,
511 'access_path': None,
510 'error': error_msg}
512 'error': error_msg}
511 except FileOverSizeException:
513 except FileOverSizeException:
512 self.request.response.status = 400
514 self.request.response.status = 400
513 limit_mb = h.format_byte_size_binary(max_file_size)
515 limit_mb = h.format_byte_size_binary(max_file_size)
514 return {'store_fid': None,
516 return {'store_fid': None,
515 'access_path': None,
517 'access_path': None,
516 'error': 'File {} is exceeding allowed limit of {}.'.format(
518 'error': 'File {} is exceeding allowed limit of {}.'.format(
517 filename, limit_mb)}
519 filename, limit_mb)}
518
520
519 try:
521 try:
520 entry = FileStore.create(
522 entry = FileStore.create(
521 file_uid=store_uid, filename=metadata["filename"],
523 file_uid=store_uid, filename=metadata["filename"],
522 file_hash=metadata["sha256"], file_size=metadata["size"],
524 file_hash=metadata["sha256"], file_size=metadata["size"],
523 file_display_name=file_display_name,
525 file_display_name=file_display_name,
524 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
526 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
525 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
527 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
526 scope_repo_id=self.db_repo.repo_id
528 scope_repo_id=self.db_repo.repo_id
527 )
529 )
528 Session().add(entry)
530 Session().add(entry)
529 Session().commit()
531 Session().commit()
530 log.debug('Stored upload in DB as %s', entry)
532 log.debug('Stored upload in DB as %s', entry)
531 except Exception:
533 except Exception:
532 log.exception('Failed to store file %s', filename)
534 log.exception('Failed to store file %s', filename)
533 self.request.response.status = 400
535 self.request.response.status = 400
534 return {'store_fid': None,
536 return {'store_fid': None,
535 'access_path': None,
537 'access_path': None,
536 'error': 'File {} failed to store in DB.'.format(filename)}
538 'error': 'File {} failed to store in DB.'.format(filename)}
537
539
538 Session().commit()
540 Session().commit()
539
541
540 return {
542 return {
541 'store_fid': store_uid,
543 'store_fid': store_uid,
542 'access_path': h.route_path(
544 'access_path': h.route_path(
543 'download_file', fid=store_uid),
545 'download_file', fid=store_uid),
544 'fqn_access_path': h.route_url(
546 'fqn_access_path': h.route_url(
545 'download_file', fid=store_uid),
547 'download_file', fid=store_uid),
546 'repo_access_path': h.route_path(
548 'repo_access_path': h.route_path(
547 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
549 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
548 'repo_fqn_access_path': h.route_url(
550 'repo_fqn_access_path': h.route_url(
549 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
551 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
550 }
552 }
551
553
552 @LoginRequired()
554 @LoginRequired()
553 @NotAnonymous()
555 @NotAnonymous()
554 @HasRepoPermissionAnyDecorator(
556 @HasRepoPermissionAnyDecorator(
555 'repository.read', 'repository.write', 'repository.admin')
557 'repository.read', 'repository.write', 'repository.admin')
556 @CSRFRequired()
558 @CSRFRequired()
557 @view_config(
559 @view_config(
558 route_name='repo_commit_comment_delete', request_method='POST',
560 route_name='repo_commit_comment_delete', request_method='POST',
559 renderer='json_ext')
561 renderer='json_ext')
560 def repo_commit_comment_delete(self):
562 def repo_commit_comment_delete(self):
561 commit_id = self.request.matchdict['commit_id']
563 commit_id = self.request.matchdict['commit_id']
562 comment_id = self.request.matchdict['comment_id']
564 comment_id = self.request.matchdict['comment_id']
563
565
564 comment = ChangesetComment.get_or_404(comment_id)
566 comment = ChangesetComment.get_or_404(comment_id)
565 if not comment:
567 if not comment:
566 log.debug('Comment with id:%s not found, skipping', comment_id)
568 log.debug('Comment with id:%s not found, skipping', comment_id)
567 # comment already deleted in another call probably
569 # comment already deleted in another call probably
568 return True
570 return True
569
571
570 if comment.immutable:
572 if comment.immutable:
571 # don't allow deleting comments that are immutable
573 # don't allow deleting comments that are immutable
572 raise HTTPForbidden()
574 raise HTTPForbidden()
573
575
574 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
576 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
575 super_admin = h.HasPermissionAny('hg.admin')()
577 super_admin = h.HasPermissionAny('hg.admin')()
576 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
578 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
577 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
579 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
578 comment_repo_admin = is_repo_admin and is_repo_comment
580 comment_repo_admin = is_repo_admin and is_repo_comment
579
581
580 if super_admin or comment_owner or comment_repo_admin:
582 if super_admin or comment_owner or comment_repo_admin:
581 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
583 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
582 Session().commit()
584 Session().commit()
583 return True
585 return True
584 else:
586 else:
585 log.warning('No permissions for user %s to delete comment_id: %s',
587 log.warning('No permissions for user %s to delete comment_id: %s',
586 self._rhodecode_db_user, comment_id)
588 self._rhodecode_db_user, comment_id)
587 raise HTTPNotFound()
589 raise HTTPNotFound()
588
590
589 @LoginRequired()
591 @LoginRequired()
590 @NotAnonymous()
592 @NotAnonymous()
591 @HasRepoPermissionAnyDecorator(
593 @HasRepoPermissionAnyDecorator(
592 'repository.read', 'repository.write', 'repository.admin')
594 'repository.read', 'repository.write', 'repository.admin')
593 @CSRFRequired()
595 @CSRFRequired()
594 @view_config(
596 @view_config(
595 route_name='repo_commit_comment_edit', request_method='POST',
597 route_name='repo_commit_comment_edit', request_method='POST',
596 renderer='json_ext')
598 renderer='json_ext')
597 def repo_commit_comment_edit(self):
599 def repo_commit_comment_edit(self):
598 self.load_default_context()
600 self.load_default_context()
599
601
600 comment_id = self.request.matchdict['comment_id']
602 comment_id = self.request.matchdict['comment_id']
601 comment = ChangesetComment.get_or_404(comment_id)
603 comment = ChangesetComment.get_or_404(comment_id)
602
604
603 if comment.immutable:
605 if comment.immutable:
604 # don't allow deleting comments that are immutable
606 # don't allow deleting comments that are immutable
605 raise HTTPForbidden()
607 raise HTTPForbidden()
606
608
607 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
609 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
608 super_admin = h.HasPermissionAny('hg.admin')()
610 super_admin = h.HasPermissionAny('hg.admin')()
609 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
611 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
610 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
612 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
611 comment_repo_admin = is_repo_admin and is_repo_comment
613 comment_repo_admin = is_repo_admin and is_repo_comment
612
614
613 if super_admin or comment_owner or comment_repo_admin:
615 if super_admin or comment_owner or comment_repo_admin:
614 text = self.request.POST.get('text')
616 text = self.request.POST.get('text')
615 version = self.request.POST.get('version')
617 version = self.request.POST.get('version')
616 if text == comment.text:
618 if text == comment.text:
617 log.warning(
619 log.warning(
618 'Comment(repo): '
620 'Comment(repo): '
619 'Trying to create new version '
621 'Trying to create new version '
620 'with the same comment body {}'.format(
622 'with the same comment body {}'.format(
621 comment_id,
623 comment_id,
622 )
624 )
623 )
625 )
624 raise HTTPNotFound()
626 raise HTTPNotFound()
625
627
626 if version.isdigit():
628 if version.isdigit():
627 version = int(version)
629 version = int(version)
628 else:
630 else:
629 log.warning(
631 log.warning(
630 'Comment(repo): Wrong version type {} {} '
632 'Comment(repo): Wrong version type {} {} '
631 'for comment {}'.format(
633 'for comment {}'.format(
632 version,
634 version,
633 type(version),
635 type(version),
634 comment_id,
636 comment_id,
635 )
637 )
636 )
638 )
637 raise HTTPNotFound()
639 raise HTTPNotFound()
638
640
639 try:
641 try:
640 comment_history = CommentsModel().edit(
642 comment_history = CommentsModel().edit(
641 comment_id=comment_id,
643 comment_id=comment_id,
642 text=text,
644 text=text,
643 auth_user=self._rhodecode_user,
645 auth_user=self._rhodecode_user,
644 version=version,
646 version=version,
645 )
647 )
646 except CommentVersionMismatch:
648 except CommentVersionMismatch:
647 raise HTTPConflict()
649 raise HTTPConflict()
648
650
649 if not comment_history:
651 if not comment_history:
650 raise HTTPNotFound()
652 raise HTTPNotFound()
651
653
652 commit_id = self.request.matchdict['commit_id']
654 commit_id = self.request.matchdict['commit_id']
653 commit = self.db_repo.get_commit(commit_id)
655 commit = self.db_repo.get_commit(commit_id)
654 CommentsModel().trigger_commit_comment_hook(
656 CommentsModel().trigger_commit_comment_hook(
655 self.db_repo, self._rhodecode_user, 'edit',
657 self.db_repo, self._rhodecode_user, 'edit',
656 data={'comment': comment, 'commit': commit})
658 data={'comment': comment, 'commit': commit})
657
659
658 Session().commit()
660 Session().commit()
659 return {
661 return {
660 'comment_history_id': comment_history.comment_history_id,
662 'comment_history_id': comment_history.comment_history_id,
661 'comment_id': comment.comment_id,
663 'comment_id': comment.comment_id,
662 'comment_version': comment_history.version,
664 'comment_version': comment_history.version,
663 'comment_author_username': comment_history.author.username,
665 'comment_author_username': comment_history.author.username,
664 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
666 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
665 'comment_created_on': h.age_component(comment_history.created_on,
667 'comment_created_on': h.age_component(comment_history.created_on,
666 time_is_local=True),
668 time_is_local=True),
667 }
669 }
668 else:
670 else:
669 log.warning('No permissions for user %s to edit comment_id: %s',
671 log.warning('No permissions for user %s to edit comment_id: %s',
670 self._rhodecode_db_user, comment_id)
672 self._rhodecode_db_user, comment_id)
671 raise HTTPNotFound()
673 raise HTTPNotFound()
672
674
673 @LoginRequired()
675 @LoginRequired()
674 @HasRepoPermissionAnyDecorator(
676 @HasRepoPermissionAnyDecorator(
675 'repository.read', 'repository.write', 'repository.admin')
677 'repository.read', 'repository.write', 'repository.admin')
676 @view_config(
678 @view_config(
677 route_name='repo_commit_data', request_method='GET',
679 route_name='repo_commit_data', request_method='GET',
678 renderer='json_ext', xhr=True)
680 renderer='json_ext', xhr=True)
679 def repo_commit_data(self):
681 def repo_commit_data(self):
680 commit_id = self.request.matchdict['commit_id']
682 commit_id = self.request.matchdict['commit_id']
681 self.load_default_context()
683 self.load_default_context()
682
684
683 try:
685 try:
684 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
686 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
685 except CommitDoesNotExistError as e:
687 except CommitDoesNotExistError as e:
686 return EmptyCommit(message=str(e))
688 return EmptyCommit(message=str(e))
687
689
688 @LoginRequired()
690 @LoginRequired()
689 @HasRepoPermissionAnyDecorator(
691 @HasRepoPermissionAnyDecorator(
690 'repository.read', 'repository.write', 'repository.admin')
692 'repository.read', 'repository.write', 'repository.admin')
691 @view_config(
693 @view_config(
692 route_name='repo_commit_children', request_method='GET',
694 route_name='repo_commit_children', request_method='GET',
693 renderer='json_ext', xhr=True)
695 renderer='json_ext', xhr=True)
694 def repo_commit_children(self):
696 def repo_commit_children(self):
695 commit_id = self.request.matchdict['commit_id']
697 commit_id = self.request.matchdict['commit_id']
696 self.load_default_context()
698 self.load_default_context()
697
699
698 try:
700 try:
699 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
701 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
700 children = commit.children
702 children = commit.children
701 except CommitDoesNotExistError:
703 except CommitDoesNotExistError:
702 children = []
704 children = []
703
705
704 result = {"results": children}
706 result = {"results": children}
705 return result
707 return result
706
708
707 @LoginRequired()
709 @LoginRequired()
708 @HasRepoPermissionAnyDecorator(
710 @HasRepoPermissionAnyDecorator(
709 'repository.read', 'repository.write', 'repository.admin')
711 'repository.read', 'repository.write', 'repository.admin')
710 @view_config(
712 @view_config(
711 route_name='repo_commit_parents', request_method='GET',
713 route_name='repo_commit_parents', request_method='GET',
712 renderer='json_ext')
714 renderer='json_ext')
713 def repo_commit_parents(self):
715 def repo_commit_parents(self):
714 commit_id = self.request.matchdict['commit_id']
716 commit_id = self.request.matchdict['commit_id']
715 self.load_default_context()
717 self.load_default_context()
716
718
717 try:
719 try:
718 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
720 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
719 parents = commit.parents
721 parents = commit.parents
720 except CommitDoesNotExistError:
722 except CommitDoesNotExistError:
721 parents = []
723 parents = []
722 result = {"results": parents}
724 result = {"results": parents}
723 return result
725 return result
@@ -1,1639 +1,1753 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.db import (
48 from rhodecode.model.db import (
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
50 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59
59
60 def load_default_context(self):
60 def load_default_context(self):
61 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 # backward compat., we use for OLD PRs a plain renderer
64 # backward compat., we use for OLD PRs a plain renderer
65 c.renderer = 'plain'
65 c.renderer = 'plain'
66 return c
66 return c
67
67
68 def _get_pull_requests_list(
68 def _get_pull_requests_list(
69 self, repo_name, source, filter_type, opened_by, statuses):
69 self, repo_name, source, filter_type, opened_by, statuses):
70
70
71 draw, start, limit = self._extract_chunk(self.request)
71 draw, start, limit = self._extract_chunk(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 _render = self.request.get_partial_renderer(
73 _render = self.request.get_partial_renderer(
74 'rhodecode:templates/data_table/_dt_elements.mako')
74 'rhodecode:templates/data_table/_dt_elements.mako')
75
75
76 # pagination
76 # pagination
77
77
78 if filter_type == 'awaiting_review':
78 if filter_type == 'awaiting_review':
79 pull_requests = PullRequestModel().get_awaiting_review(
79 pull_requests = PullRequestModel().get_awaiting_review(
80 repo_name, search_q=search_q, source=source, opened_by=opened_by,
80 repo_name, search_q=search_q, source=source, opened_by=opened_by,
81 statuses=statuses, offset=start, length=limit,
81 statuses=statuses, offset=start, length=limit,
82 order_by=order_by, order_dir=order_dir)
82 order_by=order_by, order_dir=order_dir)
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 repo_name, search_q=search_q, source=source, statuses=statuses,
84 repo_name, search_q=search_q, source=source, statuses=statuses,
85 opened_by=opened_by)
85 opened_by=opened_by)
86 elif filter_type == 'awaiting_my_review':
86 elif filter_type == 'awaiting_my_review':
87 pull_requests = PullRequestModel().get_awaiting_my_review(
87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 repo_name, search_q=search_q, source=source, opened_by=opened_by,
88 repo_name, search_q=search_q, source=source, opened_by=opened_by,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 offset=start, length=limit, order_by=order_by,
90 offset=start, length=limit, order_by=order_by,
91 order_dir=order_dir)
91 order_dir=order_dir)
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
93 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 statuses=statuses, opened_by=opened_by)
94 statuses=statuses, opened_by=opened_by)
95 else:
95 else:
96 pull_requests = PullRequestModel().get_all(
96 pull_requests = PullRequestModel().get_all(
97 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 statuses=statuses, offset=start, length=limit,
98 statuses=statuses, offset=start, length=limit,
99 order_by=order_by, order_dir=order_dir)
99 order_by=order_by, order_dir=order_dir)
100 pull_requests_total_count = PullRequestModel().count_all(
100 pull_requests_total_count = PullRequestModel().count_all(
101 repo_name, search_q=search_q, source=source, statuses=statuses,
101 repo_name, search_q=search_q, source=source, statuses=statuses,
102 opened_by=opened_by)
102 opened_by=opened_by)
103
103
104 data = []
104 data = []
105 comments_model = CommentsModel()
105 comments_model = CommentsModel()
106 for pr in pull_requests:
106 for pr in pull_requests:
107 comments = comments_model.get_all_comments(
107 comments = comments_model.get_all_comments(
108 self.db_repo.repo_id, pull_request=pr)
108 self.db_repo.repo_id, pull_request=pr)
109
109
110 data.append({
110 data.append({
111 'name': _render('pullrequest_name',
111 'name': _render('pullrequest_name',
112 pr.pull_request_id, pr.pull_request_state,
112 pr.pull_request_id, pr.pull_request_state,
113 pr.work_in_progress, pr.target_repo.repo_name),
113 pr.work_in_progress, pr.target_repo.repo_name),
114 'name_raw': pr.pull_request_id,
114 'name_raw': pr.pull_request_id,
115 'status': _render('pullrequest_status',
115 'status': _render('pullrequest_status',
116 pr.calculated_review_status()),
116 pr.calculated_review_status()),
117 'title': _render('pullrequest_title', pr.title, pr.description),
117 'title': _render('pullrequest_title', pr.title, pr.description),
118 'description': h.escape(pr.description),
118 'description': h.escape(pr.description),
119 'updated_on': _render('pullrequest_updated_on',
119 'updated_on': _render('pullrequest_updated_on',
120 h.datetime_to_time(pr.updated_on)),
120 h.datetime_to_time(pr.updated_on)),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 'created_on': _render('pullrequest_updated_on',
122 'created_on': _render('pullrequest_updated_on',
123 h.datetime_to_time(pr.created_on)),
123 h.datetime_to_time(pr.created_on)),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
125 'state': pr.pull_request_state,
125 'state': pr.pull_request_state,
126 'author': _render('pullrequest_author',
126 'author': _render('pullrequest_author',
127 pr.author.full_contact, ),
127 pr.author.full_contact, ),
128 'author_raw': pr.author.full_name,
128 'author_raw': pr.author.full_name,
129 'comments': _render('pullrequest_comments', len(comments)),
129 'comments': _render('pullrequest_comments', len(comments)),
130 'comments_raw': len(comments),
130 'comments_raw': len(comments),
131 'closed': pr.is_closed(),
131 'closed': pr.is_closed(),
132 })
132 })
133
133
134 data = ({
134 data = ({
135 'draw': draw,
135 'draw': draw,
136 'data': data,
136 'data': data,
137 'recordsTotal': pull_requests_total_count,
137 'recordsTotal': pull_requests_total_count,
138 'recordsFiltered': pull_requests_total_count,
138 'recordsFiltered': pull_requests_total_count,
139 })
139 })
140 return data
140 return data
141
141
142 @LoginRequired()
142 @LoginRequired()
143 @HasRepoPermissionAnyDecorator(
143 @HasRepoPermissionAnyDecorator(
144 'repository.read', 'repository.write', 'repository.admin')
144 'repository.read', 'repository.write', 'repository.admin')
145 @view_config(
145 @view_config(
146 route_name='pullrequest_show_all', request_method='GET',
146 route_name='pullrequest_show_all', request_method='GET',
147 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
147 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
148 def pull_request_list(self):
148 def pull_request_list(self):
149 c = self.load_default_context()
149 c = self.load_default_context()
150
150
151 req_get = self.request.GET
151 req_get = self.request.GET
152 c.source = str2bool(req_get.get('source'))
152 c.source = str2bool(req_get.get('source'))
153 c.closed = str2bool(req_get.get('closed'))
153 c.closed = str2bool(req_get.get('closed'))
154 c.my = str2bool(req_get.get('my'))
154 c.my = str2bool(req_get.get('my'))
155 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
155 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
156 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
156 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
157
157
158 c.active = 'open'
158 c.active = 'open'
159 if c.my:
159 if c.my:
160 c.active = 'my'
160 c.active = 'my'
161 if c.closed:
161 if c.closed:
162 c.active = 'closed'
162 c.active = 'closed'
163 if c.awaiting_review and not c.source:
163 if c.awaiting_review and not c.source:
164 c.active = 'awaiting'
164 c.active = 'awaiting'
165 if c.source and not c.awaiting_review:
165 if c.source and not c.awaiting_review:
166 c.active = 'source'
166 c.active = 'source'
167 if c.awaiting_my_review:
167 if c.awaiting_my_review:
168 c.active = 'awaiting_my'
168 c.active = 'awaiting_my'
169
169
170 return self._get_template_context(c)
170 return self._get_template_context(c)
171
171
172 @LoginRequired()
172 @LoginRequired()
173 @HasRepoPermissionAnyDecorator(
173 @HasRepoPermissionAnyDecorator(
174 'repository.read', 'repository.write', 'repository.admin')
174 'repository.read', 'repository.write', 'repository.admin')
175 @view_config(
175 @view_config(
176 route_name='pullrequest_show_all_data', request_method='GET',
176 route_name='pullrequest_show_all_data', request_method='GET',
177 renderer='json_ext', xhr=True)
177 renderer='json_ext', xhr=True)
178 def pull_request_list_data(self):
178 def pull_request_list_data(self):
179 self.load_default_context()
179 self.load_default_context()
180
180
181 # additional filters
181 # additional filters
182 req_get = self.request.GET
182 req_get = self.request.GET
183 source = str2bool(req_get.get('source'))
183 source = str2bool(req_get.get('source'))
184 closed = str2bool(req_get.get('closed'))
184 closed = str2bool(req_get.get('closed'))
185 my = str2bool(req_get.get('my'))
185 my = str2bool(req_get.get('my'))
186 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_review = str2bool(req_get.get('awaiting_review'))
187 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
188
188
189 filter_type = 'awaiting_review' if awaiting_review \
189 filter_type = 'awaiting_review' if awaiting_review \
190 else 'awaiting_my_review' if awaiting_my_review \
190 else 'awaiting_my_review' if awaiting_my_review \
191 else None
191 else None
192
192
193 opened_by = None
193 opened_by = None
194 if my:
194 if my:
195 opened_by = [self._rhodecode_user.user_id]
195 opened_by = [self._rhodecode_user.user_id]
196
196
197 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
198 if closed:
198 if closed:
199 statuses = [PullRequest.STATUS_CLOSED]
199 statuses = [PullRequest.STATUS_CLOSED]
200
200
201 data = self._get_pull_requests_list(
201 data = self._get_pull_requests_list(
202 repo_name=self.db_repo_name, source=source,
202 repo_name=self.db_repo_name, source=source,
203 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
204
204
205 return data
205 return data
206
206
207 def _is_diff_cache_enabled(self, target_repo):
207 def _is_diff_cache_enabled(self, target_repo):
208 caching_enabled = self._get_general_setting(
208 caching_enabled = self._get_general_setting(
209 target_repo, 'rhodecode_diff_cache')
209 target_repo, 'rhodecode_diff_cache')
210 log.debug('Diff caching enabled: %s', caching_enabled)
210 log.debug('Diff caching enabled: %s', caching_enabled)
211 return caching_enabled
211 return caching_enabled
212
212
213 def _get_diffset(self, source_repo_name, source_repo,
213 def _get_diffset(self, source_repo_name, source_repo,
214 ancestor_commit,
214 ancestor_commit,
215 source_ref_id, target_ref_id,
215 source_ref_id, target_ref_id,
216 target_commit, source_commit, diff_limit, file_limit,
216 target_commit, source_commit, diff_limit, file_limit,
217 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
217 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
218
218
219 if use_ancestor:
219 if use_ancestor:
220 # we might want to not use it for versions
220 # we might want to not use it for versions
221 target_ref_id = ancestor_commit.raw_id
221 target_ref_id = ancestor_commit.raw_id
222
222
223 vcs_diff = PullRequestModel().get_diff(
223 vcs_diff = PullRequestModel().get_diff(
224 source_repo, source_ref_id, target_ref_id,
224 source_repo, source_ref_id, target_ref_id,
225 hide_whitespace_changes, diff_context)
225 hide_whitespace_changes, diff_context)
226
226
227 diff_processor = diffs.DiffProcessor(
227 diff_processor = diffs.DiffProcessor(
228 vcs_diff, format='newdiff', diff_limit=diff_limit,
228 vcs_diff, format='newdiff', diff_limit=diff_limit,
229 file_limit=file_limit, show_full_diff=fulldiff)
229 file_limit=file_limit, show_full_diff=fulldiff)
230
230
231 _parsed = diff_processor.prepare()
231 _parsed = diff_processor.prepare()
232
232
233 diffset = codeblocks.DiffSet(
233 diffset = codeblocks.DiffSet(
234 repo_name=self.db_repo_name,
234 repo_name=self.db_repo_name,
235 source_repo_name=source_repo_name,
235 source_repo_name=source_repo_name,
236 source_node_getter=codeblocks.diffset_node_getter(target_commit),
236 source_node_getter=codeblocks.diffset_node_getter(target_commit),
237 target_node_getter=codeblocks.diffset_node_getter(source_commit),
237 target_node_getter=codeblocks.diffset_node_getter(source_commit),
238 )
238 )
239 diffset = self.path_filter.render_patchset_filtered(
239 diffset = self.path_filter.render_patchset_filtered(
240 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
240 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
241
241
242 return diffset
242 return diffset
243
243
244 def _get_range_diffset(self, source_scm, source_repo,
244 def _get_range_diffset(self, source_scm, source_repo,
245 commit1, commit2, diff_limit, file_limit,
245 commit1, commit2, diff_limit, file_limit,
246 fulldiff, hide_whitespace_changes, diff_context):
246 fulldiff, hide_whitespace_changes, diff_context):
247 vcs_diff = source_scm.get_diff(
247 vcs_diff = source_scm.get_diff(
248 commit1, commit2,
248 commit1, commit2,
249 ignore_whitespace=hide_whitespace_changes,
249 ignore_whitespace=hide_whitespace_changes,
250 context=diff_context)
250 context=diff_context)
251
251
252 diff_processor = diffs.DiffProcessor(
252 diff_processor = diffs.DiffProcessor(
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 file_limit=file_limit, show_full_diff=fulldiff)
254 file_limit=file_limit, show_full_diff=fulldiff)
255
255
256 _parsed = diff_processor.prepare()
256 _parsed = diff_processor.prepare()
257
257
258 diffset = codeblocks.DiffSet(
258 diffset = codeblocks.DiffSet(
259 repo_name=source_repo.repo_name,
259 repo_name=source_repo.repo_name,
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
262
262
263 diffset = self.path_filter.render_patchset_filtered(
263 diffset = self.path_filter.render_patchset_filtered(
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265
265
266 return diffset
266 return diffset
267
267
268 def register_comments_vars(self, c, pull_request, versions):
269 comments_model = CommentsModel()
270
271 # GENERAL COMMENTS with versions #
272 q = comments_model._all_general_comments_of_pull_request(pull_request)
273 q = q.order_by(ChangesetComment.comment_id.asc())
274 general_comments = q
275
276 # pick comments we want to render at current version
277 c.comment_versions = comments_model.aggregate_comments(
278 general_comments, versions, c.at_version_num)
279
280 # INLINE COMMENTS with versions #
281 q = comments_model._all_inline_comments_of_pull_request(pull_request)
282 q = q.order_by(ChangesetComment.comment_id.asc())
283 inline_comments = q
284
285 c.inline_versions = comments_model.aggregate_comments(
286 inline_comments, versions, c.at_version_num, inline=True)
287
288 # Comments inline+general
289 if c.at_version:
290 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
291 c.comments = c.comment_versions[c.at_version_num]['display']
292 else:
293 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
294 c.comments = c.comment_versions[c.at_version_num]['until']
295
296 return general_comments, inline_comments
297
268 @LoginRequired()
298 @LoginRequired()
269 @HasRepoPermissionAnyDecorator(
299 @HasRepoPermissionAnyDecorator(
270 'repository.read', 'repository.write', 'repository.admin')
300 'repository.read', 'repository.write', 'repository.admin')
271 @view_config(
301 @view_config(
272 route_name='pullrequest_show', request_method='GET',
302 route_name='pullrequest_show', request_method='GET',
273 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
303 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
274 def pull_request_show(self):
304 def pull_request_show(self):
275 _ = self.request.translate
305 _ = self.request.translate
276 c = self.load_default_context()
306 c = self.load_default_context()
277
307
278 pull_request = PullRequest.get_or_404(
308 pull_request = PullRequest.get_or_404(
279 self.request.matchdict['pull_request_id'])
309 self.request.matchdict['pull_request_id'])
280 pull_request_id = pull_request.pull_request_id
310 pull_request_id = pull_request.pull_request_id
281
311
282 c.state_progressing = pull_request.is_state_changing()
312 c.state_progressing = pull_request.is_state_changing()
313 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
314 pull_request.target_repo.repo_name, pull_request.pull_request_id)
283
315
284 _new_state = {
316 _new_state = {
285 'created': PullRequest.STATE_CREATED,
317 'created': PullRequest.STATE_CREATED,
286 }.get(self.request.GET.get('force_state'))
318 }.get(self.request.GET.get('force_state'))
287
319
288 if c.is_super_admin and _new_state:
320 if c.is_super_admin and _new_state:
289 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
321 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
290 h.flash(
322 h.flash(
291 _('Pull Request state was force changed to `{}`').format(_new_state),
323 _('Pull Request state was force changed to `{}`').format(_new_state),
292 category='success')
324 category='success')
293 Session().commit()
325 Session().commit()
294
326
295 raise HTTPFound(h.route_path(
327 raise HTTPFound(h.route_path(
296 'pullrequest_show', repo_name=self.db_repo_name,
328 'pullrequest_show', repo_name=self.db_repo_name,
297 pull_request_id=pull_request_id))
329 pull_request_id=pull_request_id))
298
330
299 version = self.request.GET.get('version')
331 version = self.request.GET.get('version')
300 from_version = self.request.GET.get('from_version') or version
332 from_version = self.request.GET.get('from_version') or version
301 merge_checks = self.request.GET.get('merge_checks')
333 merge_checks = self.request.GET.get('merge_checks')
302 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
334 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
335 force_refresh = str2bool(self.request.GET.get('force_refresh'))
336 c.range_diff_on = self.request.GET.get('range-diff') == "1"
303
337
304 # fetch global flags of ignore ws or context lines
338 # fetch global flags of ignore ws or context lines
305 diff_context = diffs.get_diff_context(self.request)
339 diff_context = diffs.get_diff_context(self.request)
306 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
340 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
307
341
308 force_refresh = str2bool(self.request.GET.get('force_refresh'))
309
310 (pull_request_latest,
342 (pull_request_latest,
311 pull_request_at_ver,
343 pull_request_at_ver,
312 pull_request_display_obj,
344 pull_request_display_obj,
313 at_version) = PullRequestModel().get_pr_version(
345 at_version) = PullRequestModel().get_pr_version(
314 pull_request_id, version=version)
346 pull_request_id, version=version)
347
315 pr_closed = pull_request_latest.is_closed()
348 pr_closed = pull_request_latest.is_closed()
316
349
317 if pr_closed and (version or from_version):
350 if pr_closed and (version or from_version):
318 # not allow to browse versions
351 # not allow to browse versions for closed PR
319 raise HTTPFound(h.route_path(
352 raise HTTPFound(h.route_path(
320 'pullrequest_show', repo_name=self.db_repo_name,
353 'pullrequest_show', repo_name=self.db_repo_name,
321 pull_request_id=pull_request_id))
354 pull_request_id=pull_request_id))
322
355
323 versions = pull_request_display_obj.versions()
356 versions = pull_request_display_obj.versions()
324 # used to store per-commit range diffs
357 # used to store per-commit range diffs
325 c.changes = collections.OrderedDict()
358 c.changes = collections.OrderedDict()
326 c.range_diff_on = self.request.GET.get('range-diff') == "1"
327
359
328 c.at_version = at_version
360 c.at_version = at_version
329 c.at_version_num = (at_version
361 c.at_version_num = (at_version
330 if at_version and at_version != 'latest'
362 if at_version and at_version != PullRequest.LATEST_VER
331 else None)
363 else None)
332 c.at_version_pos = ChangesetComment.get_index_from_version(
364
365 c.at_version_index = ChangesetComment.get_index_from_version(
333 c.at_version_num, versions)
366 c.at_version_num, versions)
334
367
335 (prev_pull_request_latest,
368 (prev_pull_request_latest,
336 prev_pull_request_at_ver,
369 prev_pull_request_at_ver,
337 prev_pull_request_display_obj,
370 prev_pull_request_display_obj,
338 prev_at_version) = PullRequestModel().get_pr_version(
371 prev_at_version) = PullRequestModel().get_pr_version(
339 pull_request_id, version=from_version)
372 pull_request_id, version=from_version)
340
373
341 c.from_version = prev_at_version
374 c.from_version = prev_at_version
342 c.from_version_num = (prev_at_version
375 c.from_version_num = (prev_at_version
343 if prev_at_version and prev_at_version != 'latest'
376 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
344 else None)
377 else None)
345 c.from_version_pos = ChangesetComment.get_index_from_version(
378 c.from_version_index = ChangesetComment.get_index_from_version(
346 c.from_version_num, versions)
379 c.from_version_num, versions)
347
380
348 # define if we're in COMPARE mode or VIEW at version mode
381 # define if we're in COMPARE mode or VIEW at version mode
349 compare = at_version != prev_at_version
382 compare = at_version != prev_at_version
350
383
351 # pull_requests repo_name we opened it against
384 # pull_requests repo_name we opened it against
352 # ie. target_repo must match
385 # ie. target_repo must match
353 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
386 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
354 log.warning('Mismatch between the current repo: %s, and target %s',
387 log.warning('Mismatch between the current repo: %s, and target %s',
355 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
388 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
356 raise HTTPNotFound()
389 raise HTTPNotFound()
357
390
358 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
391 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
359 pull_request_at_ver)
360
392
361 c.pull_request = pull_request_display_obj
393 c.pull_request = pull_request_display_obj
362 c.renderer = pull_request_at_ver.description_renderer or c.renderer
394 c.renderer = pull_request_at_ver.description_renderer or c.renderer
363 c.pull_request_latest = pull_request_latest
395 c.pull_request_latest = pull_request_latest
364
396
365 if compare or (at_version and not at_version == 'latest'):
397 # inject latest version
398 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
399 c.versions = versions + [latest_ver]
400
401 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
366 c.allowed_to_change_status = False
402 c.allowed_to_change_status = False
367 c.allowed_to_update = False
403 c.allowed_to_update = False
368 c.allowed_to_merge = False
404 c.allowed_to_merge = False
369 c.allowed_to_delete = False
405 c.allowed_to_delete = False
370 c.allowed_to_comment = False
406 c.allowed_to_comment = False
371 c.allowed_to_close = False
407 c.allowed_to_close = False
372 else:
408 else:
373 can_change_status = PullRequestModel().check_user_change_status(
409 can_change_status = PullRequestModel().check_user_change_status(
374 pull_request_at_ver, self._rhodecode_user)
410 pull_request_at_ver, self._rhodecode_user)
375 c.allowed_to_change_status = can_change_status and not pr_closed
411 c.allowed_to_change_status = can_change_status and not pr_closed
376
412
377 c.allowed_to_update = PullRequestModel().check_user_update(
413 c.allowed_to_update = PullRequestModel().check_user_update(
378 pull_request_latest, self._rhodecode_user) and not pr_closed
414 pull_request_latest, self._rhodecode_user) and not pr_closed
379 c.allowed_to_merge = PullRequestModel().check_user_merge(
415 c.allowed_to_merge = PullRequestModel().check_user_merge(
380 pull_request_latest, self._rhodecode_user) and not pr_closed
416 pull_request_latest, self._rhodecode_user) and not pr_closed
381 c.allowed_to_delete = PullRequestModel().check_user_delete(
417 c.allowed_to_delete = PullRequestModel().check_user_delete(
382 pull_request_latest, self._rhodecode_user) and not pr_closed
418 pull_request_latest, self._rhodecode_user) and not pr_closed
383 c.allowed_to_comment = not pr_closed
419 c.allowed_to_comment = not pr_closed
384 c.allowed_to_close = c.allowed_to_merge and not pr_closed
420 c.allowed_to_close = c.allowed_to_merge and not pr_closed
385
421
386 c.forbid_adding_reviewers = False
422 c.forbid_adding_reviewers = False
387 c.forbid_author_to_review = False
423 c.forbid_author_to_review = False
388 c.forbid_commit_author_to_review = False
424 c.forbid_commit_author_to_review = False
389
425
390 if pull_request_latest.reviewer_data and \
426 if pull_request_latest.reviewer_data and \
391 'rules' in pull_request_latest.reviewer_data:
427 'rules' in pull_request_latest.reviewer_data:
392 rules = pull_request_latest.reviewer_data['rules'] or {}
428 rules = pull_request_latest.reviewer_data['rules'] or {}
393 try:
429 try:
394 c.forbid_adding_reviewers = rules.get(
430 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
395 'forbid_adding_reviewers')
431 c.forbid_author_to_review = rules.get('forbid_author_to_review')
396 c.forbid_author_to_review = rules.get(
432 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
397 'forbid_author_to_review')
398 c.forbid_commit_author_to_review = rules.get(
399 'forbid_commit_author_to_review')
400 except Exception:
433 except Exception:
401 pass
434 pass
402
435
403 # check merge capabilities
436 # check merge capabilities
404 _merge_check = MergeCheck.validate(
437 _merge_check = MergeCheck.validate(
405 pull_request_latest, auth_user=self._rhodecode_user,
438 pull_request_latest, auth_user=self._rhodecode_user,
406 translator=self.request.translate,
439 translator=self.request.translate,
407 force_shadow_repo_refresh=force_refresh)
440 force_shadow_repo_refresh=force_refresh)
408
441
409 c.pr_merge_errors = _merge_check.error_details
442 c.pr_merge_errors = _merge_check.error_details
410 c.pr_merge_possible = not _merge_check.failed
443 c.pr_merge_possible = not _merge_check.failed
411 c.pr_merge_message = _merge_check.merge_msg
444 c.pr_merge_message = _merge_check.merge_msg
412 c.pr_merge_source_commit = _merge_check.source_commit
445 c.pr_merge_source_commit = _merge_check.source_commit
413 c.pr_merge_target_commit = _merge_check.target_commit
446 c.pr_merge_target_commit = _merge_check.target_commit
414
447
415 c.pr_merge_info = MergeCheck.get_merge_conditions(
448 c.pr_merge_info = MergeCheck.get_merge_conditions(
416 pull_request_latest, translator=self.request.translate)
449 pull_request_latest, translator=self.request.translate)
417
450
418 c.pull_request_review_status = _merge_check.review_status
451 c.pull_request_review_status = _merge_check.review_status
419 if merge_checks:
452 if merge_checks:
420 self.request.override_renderer = \
453 self.request.override_renderer = \
421 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
454 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
422 return self._get_template_context(c)
455 return self._get_template_context(c)
423
456
424 comments_model = CommentsModel()
457 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
425
458
426 # reviewers and statuses
459 # reviewers and statuses
427 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
460 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
428 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
461 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
429
462
430 # GENERAL COMMENTS with versions #
463 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
431 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
464 member_reviewer = h.reviewer_as_json(
432 q = q.order_by(ChangesetComment.comment_id.asc())
465 member, reasons=reasons, mandatory=mandatory,
433 general_comments = q
466 user_group=review_obj.rule_user_group_data()
467 )
434
468
435 # pick comments we want to render at current version
469 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
436 c.comment_versions = comments_model.aggregate_comments(
470 member_reviewer['review_status'] = current_review_status
437 general_comments, versions, c.at_version_num)
471 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
438 c.comments = c.comment_versions[c.at_version_num]['until']
472 member_reviewer['allowed_to_update'] = c.allowed_to_update
473 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
439
474
440 # INLINE COMMENTS with versions #
475 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
441 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
476
442 q = q.order_by(ChangesetComment.comment_id.asc())
477
443 inline_comments = q
444
478
445 c.inline_versions = comments_model.aggregate_comments(
479
446 inline_comments, versions, c.at_version_num, inline=True)
480 general_comments, inline_comments = \
481 self.register_comments_vars(c, pull_request_latest, versions)
447
482
448 # TODOs
483 # TODOs
449 c.unresolved_comments = CommentsModel() \
484 c.unresolved_comments = CommentsModel() \
450 .get_pull_request_unresolved_todos(pull_request)
485 .get_pull_request_unresolved_todos(pull_request_latest)
451 c.resolved_comments = CommentsModel() \
486 c.resolved_comments = CommentsModel() \
452 .get_pull_request_resolved_todos(pull_request)
487 .get_pull_request_resolved_todos(pull_request_latest)
453
454 # inject latest version
455 latest_ver = PullRequest.get_pr_display_object(
456 pull_request_latest, pull_request_latest)
457
458 c.versions = versions + [latest_ver]
459
488
460 # if we use version, then do not show later comments
489 # if we use version, then do not show later comments
461 # than current version
490 # than current version
462 display_inline_comments = collections.defaultdict(
491 display_inline_comments = collections.defaultdict(
463 lambda: collections.defaultdict(list))
492 lambda: collections.defaultdict(list))
464 for co in inline_comments:
493 for co in inline_comments:
465 if c.at_version_num:
494 if c.at_version_num:
466 # pick comments that are at least UPTO given version, so we
495 # pick comments that are at least UPTO given version, so we
467 # don't render comments for higher version
496 # don't render comments for higher version
468 should_render = co.pull_request_version_id and \
497 should_render = co.pull_request_version_id and \
469 co.pull_request_version_id <= c.at_version_num
498 co.pull_request_version_id <= c.at_version_num
470 else:
499 else:
471 # showing all, for 'latest'
500 # showing all, for 'latest'
472 should_render = True
501 should_render = True
473
502
474 if should_render:
503 if should_render:
475 display_inline_comments[co.f_path][co.line_no].append(co)
504 display_inline_comments[co.f_path][co.line_no].append(co)
476
505
477 # load diff data into template context, if we use compare mode then
506 # load diff data into template context, if we use compare mode then
478 # diff is calculated based on changes between versions of PR
507 # diff is calculated based on changes between versions of PR
479
508
480 source_repo = pull_request_at_ver.source_repo
509 source_repo = pull_request_at_ver.source_repo
481 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
510 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
482
511
483 target_repo = pull_request_at_ver.target_repo
512 target_repo = pull_request_at_ver.target_repo
484 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
513 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
485
514
486 if compare:
515 if compare:
487 # in compare switch the diff base to latest commit from prev version
516 # in compare switch the diff base to latest commit from prev version
488 target_ref_id = prev_pull_request_display_obj.revisions[0]
517 target_ref_id = prev_pull_request_display_obj.revisions[0]
489
518
490 # despite opening commits for bookmarks/branches/tags, we always
519 # despite opening commits for bookmarks/branches/tags, we always
491 # convert this to rev to prevent changes after bookmark or branch change
520 # convert this to rev to prevent changes after bookmark or branch change
492 c.source_ref_type = 'rev'
521 c.source_ref_type = 'rev'
493 c.source_ref = source_ref_id
522 c.source_ref = source_ref_id
494
523
495 c.target_ref_type = 'rev'
524 c.target_ref_type = 'rev'
496 c.target_ref = target_ref_id
525 c.target_ref = target_ref_id
497
526
498 c.source_repo = source_repo
527 c.source_repo = source_repo
499 c.target_repo = target_repo
528 c.target_repo = target_repo
500
529
501 c.commit_ranges = []
530 c.commit_ranges = []
502 source_commit = EmptyCommit()
531 source_commit = EmptyCommit()
503 target_commit = EmptyCommit()
532 target_commit = EmptyCommit()
504 c.missing_requirements = False
533 c.missing_requirements = False
505
534
506 source_scm = source_repo.scm_instance()
535 source_scm = source_repo.scm_instance()
507 target_scm = target_repo.scm_instance()
536 target_scm = target_repo.scm_instance()
508
537
509 shadow_scm = None
538 shadow_scm = None
510 try:
539 try:
511 shadow_scm = pull_request_latest.get_shadow_repo()
540 shadow_scm = pull_request_latest.get_shadow_repo()
512 except Exception:
541 except Exception:
513 log.debug('Failed to get shadow repo', exc_info=True)
542 log.debug('Failed to get shadow repo', exc_info=True)
514 # try first the existing source_repo, and then shadow
543 # try first the existing source_repo, and then shadow
515 # repo if we can obtain one
544 # repo if we can obtain one
516 commits_source_repo = source_scm
545 commits_source_repo = source_scm
517 if shadow_scm:
546 if shadow_scm:
518 commits_source_repo = shadow_scm
547 commits_source_repo = shadow_scm
519
548
520 c.commits_source_repo = commits_source_repo
549 c.commits_source_repo = commits_source_repo
521 c.ancestor = None # set it to None, to hide it from PR view
550 c.ancestor = None # set it to None, to hide it from PR view
522
551
523 # empty version means latest, so we keep this to prevent
552 # empty version means latest, so we keep this to prevent
524 # double caching
553 # double caching
525 version_normalized = version or 'latest'
554 version_normalized = version or PullRequest.LATEST_VER
526 from_version_normalized = from_version or 'latest'
555 from_version_normalized = from_version or PullRequest.LATEST_VER
527
556
528 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
557 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
529 cache_file_path = diff_cache_exist(
558 cache_file_path = diff_cache_exist(
530 cache_path, 'pull_request', pull_request_id, version_normalized,
559 cache_path, 'pull_request', pull_request_id, version_normalized,
531 from_version_normalized, source_ref_id, target_ref_id,
560 from_version_normalized, source_ref_id, target_ref_id,
532 hide_whitespace_changes, diff_context, c.fulldiff)
561 hide_whitespace_changes, diff_context, c.fulldiff)
533
562
534 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
563 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
535 force_recache = self.get_recache_flag()
564 force_recache = self.get_recache_flag()
536
565
537 cached_diff = None
566 cached_diff = None
538 if caching_enabled:
567 if caching_enabled:
539 cached_diff = load_cached_diff(cache_file_path)
568 cached_diff = load_cached_diff(cache_file_path)
540
569
541 has_proper_commit_cache = (
570 has_proper_commit_cache = (
542 cached_diff and cached_diff.get('commits')
571 cached_diff and cached_diff.get('commits')
543 and len(cached_diff.get('commits', [])) == 5
572 and len(cached_diff.get('commits', [])) == 5
544 and cached_diff.get('commits')[0]
573 and cached_diff.get('commits')[0]
545 and cached_diff.get('commits')[3])
574 and cached_diff.get('commits')[3])
546
575
547 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
576 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
548 diff_commit_cache = \
577 diff_commit_cache = \
549 (ancestor_commit, commit_cache, missing_requirements,
578 (ancestor_commit, commit_cache, missing_requirements,
550 source_commit, target_commit) = cached_diff['commits']
579 source_commit, target_commit) = cached_diff['commits']
551 else:
580 else:
552 # NOTE(marcink): we reach potentially unreachable errors when a PR has
581 # NOTE(marcink): we reach potentially unreachable errors when a PR has
553 # merge errors resulting in potentially hidden commits in the shadow repo.
582 # merge errors resulting in potentially hidden commits in the shadow repo.
554 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
583 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
555 and _merge_check.merge_response
584 and _merge_check.merge_response
556 maybe_unreachable = maybe_unreachable \
585 maybe_unreachable = maybe_unreachable \
557 and _merge_check.merge_response.metadata.get('unresolved_files')
586 and _merge_check.merge_response.metadata.get('unresolved_files')
558 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
587 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
559 diff_commit_cache = \
588 diff_commit_cache = \
560 (ancestor_commit, commit_cache, missing_requirements,
589 (ancestor_commit, commit_cache, missing_requirements,
561 source_commit, target_commit) = self.get_commits(
590 source_commit, target_commit) = self.get_commits(
562 commits_source_repo,
591 commits_source_repo,
563 pull_request_at_ver,
592 pull_request_at_ver,
564 source_commit,
593 source_commit,
565 source_ref_id,
594 source_ref_id,
566 source_scm,
595 source_scm,
567 target_commit,
596 target_commit,
568 target_ref_id,
597 target_ref_id,
569 target_scm,
598 target_scm,
570 maybe_unreachable=maybe_unreachable)
599 maybe_unreachable=maybe_unreachable)
571
600
572 # register our commit range
601 # register our commit range
573 for comm in commit_cache.values():
602 for comm in commit_cache.values():
574 c.commit_ranges.append(comm)
603 c.commit_ranges.append(comm)
575
604
576 c.missing_requirements = missing_requirements
605 c.missing_requirements = missing_requirements
577 c.ancestor_commit = ancestor_commit
606 c.ancestor_commit = ancestor_commit
578 c.statuses = source_repo.statuses(
607 c.statuses = source_repo.statuses(
579 [x.raw_id for x in c.commit_ranges])
608 [x.raw_id for x in c.commit_ranges])
580
609
581 # auto collapse if we have more than limit
610 # auto collapse if we have more than limit
582 collapse_limit = diffs.DiffProcessor._collapse_commits_over
611 collapse_limit = diffs.DiffProcessor._collapse_commits_over
583 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
612 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
584 c.compare_mode = compare
613 c.compare_mode = compare
585
614
586 # diff_limit is the old behavior, will cut off the whole diff
615 # diff_limit is the old behavior, will cut off the whole diff
587 # if the limit is applied otherwise will just hide the
616 # if the limit is applied otherwise will just hide the
588 # big files from the front-end
617 # big files from the front-end
589 diff_limit = c.visual.cut_off_limit_diff
618 diff_limit = c.visual.cut_off_limit_diff
590 file_limit = c.visual.cut_off_limit_file
619 file_limit = c.visual.cut_off_limit_file
591
620
592 c.missing_commits = False
621 c.missing_commits = False
593 if (c.missing_requirements
622 if (c.missing_requirements
594 or isinstance(source_commit, EmptyCommit)
623 or isinstance(source_commit, EmptyCommit)
595 or source_commit == target_commit):
624 or source_commit == target_commit):
596
625
597 c.missing_commits = True
626 c.missing_commits = True
598 else:
627 else:
599 c.inline_comments = display_inline_comments
628 c.inline_comments = display_inline_comments
600
629
601 use_ancestor = True
630 use_ancestor = True
602 if from_version_normalized != version_normalized:
631 if from_version_normalized != version_normalized:
603 use_ancestor = False
632 use_ancestor = False
604
633
605 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
634 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
606 if not force_recache and has_proper_diff_cache:
635 if not force_recache and has_proper_diff_cache:
607 c.diffset = cached_diff['diff']
636 c.diffset = cached_diff['diff']
608 else:
637 else:
609 try:
638 try:
610 c.diffset = self._get_diffset(
639 c.diffset = self._get_diffset(
611 c.source_repo.repo_name, commits_source_repo,
640 c.source_repo.repo_name, commits_source_repo,
612 c.ancestor_commit,
641 c.ancestor_commit,
613 source_ref_id, target_ref_id,
642 source_ref_id, target_ref_id,
614 target_commit, source_commit,
643 target_commit, source_commit,
615 diff_limit, file_limit, c.fulldiff,
644 diff_limit, file_limit, c.fulldiff,
616 hide_whitespace_changes, diff_context,
645 hide_whitespace_changes, diff_context,
617 use_ancestor=use_ancestor
646 use_ancestor=use_ancestor
618 )
647 )
619
648
620 # save cached diff
649 # save cached diff
621 if caching_enabled:
650 if caching_enabled:
622 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
651 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
623 except CommitDoesNotExistError:
652 except CommitDoesNotExistError:
624 log.exception('Failed to generate diffset')
653 log.exception('Failed to generate diffset')
625 c.missing_commits = True
654 c.missing_commits = True
626
655
627 if not c.missing_commits:
656 if not c.missing_commits:
628
657
629 c.limited_diff = c.diffset.limited_diff
658 c.limited_diff = c.diffset.limited_diff
630
659
631 # calculate removed files that are bound to comments
660 # calculate removed files that are bound to comments
632 comment_deleted_files = [
661 comment_deleted_files = [
633 fname for fname in display_inline_comments
662 fname for fname in display_inline_comments
634 if fname not in c.diffset.file_stats]
663 if fname not in c.diffset.file_stats]
635
664
636 c.deleted_files_comments = collections.defaultdict(dict)
665 c.deleted_files_comments = collections.defaultdict(dict)
637 for fname, per_line_comments in display_inline_comments.items():
666 for fname, per_line_comments in display_inline_comments.items():
638 if fname in comment_deleted_files:
667 if fname in comment_deleted_files:
639 c.deleted_files_comments[fname]['stats'] = 0
668 c.deleted_files_comments[fname]['stats'] = 0
640 c.deleted_files_comments[fname]['comments'] = list()
669 c.deleted_files_comments[fname]['comments'] = list()
641 for lno, comments in per_line_comments.items():
670 for lno, comments in per_line_comments.items():
642 c.deleted_files_comments[fname]['comments'].extend(comments)
671 c.deleted_files_comments[fname]['comments'].extend(comments)
643
672
644 # maybe calculate the range diff
673 # maybe calculate the range diff
645 if c.range_diff_on:
674 if c.range_diff_on:
646 # TODO(marcink): set whitespace/context
675 # TODO(marcink): set whitespace/context
647 context_lcl = 3
676 context_lcl = 3
648 ign_whitespace_lcl = False
677 ign_whitespace_lcl = False
649
678
650 for commit in c.commit_ranges:
679 for commit in c.commit_ranges:
651 commit2 = commit
680 commit2 = commit
652 commit1 = commit.first_parent
681 commit1 = commit.first_parent
653
682
654 range_diff_cache_file_path = diff_cache_exist(
683 range_diff_cache_file_path = diff_cache_exist(
655 cache_path, 'diff', commit.raw_id,
684 cache_path, 'diff', commit.raw_id,
656 ign_whitespace_lcl, context_lcl, c.fulldiff)
685 ign_whitespace_lcl, context_lcl, c.fulldiff)
657
686
658 cached_diff = None
687 cached_diff = None
659 if caching_enabled:
688 if caching_enabled:
660 cached_diff = load_cached_diff(range_diff_cache_file_path)
689 cached_diff = load_cached_diff(range_diff_cache_file_path)
661
690
662 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
691 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
663 if not force_recache and has_proper_diff_cache:
692 if not force_recache and has_proper_diff_cache:
664 diffset = cached_diff['diff']
693 diffset = cached_diff['diff']
665 else:
694 else:
666 diffset = self._get_range_diffset(
695 diffset = self._get_range_diffset(
667 commits_source_repo, source_repo,
696 commits_source_repo, source_repo,
668 commit1, commit2, diff_limit, file_limit,
697 commit1, commit2, diff_limit, file_limit,
669 c.fulldiff, ign_whitespace_lcl, context_lcl
698 c.fulldiff, ign_whitespace_lcl, context_lcl
670 )
699 )
671
700
672 # save cached diff
701 # save cached diff
673 if caching_enabled:
702 if caching_enabled:
674 cache_diff(range_diff_cache_file_path, diffset, None)
703 cache_diff(range_diff_cache_file_path, diffset, None)
675
704
676 c.changes[commit.raw_id] = diffset
705 c.changes[commit.raw_id] = diffset
677
706
678 # this is a hack to properly display links, when creating PR, the
707 # this is a hack to properly display links, when creating PR, the
679 # compare view and others uses different notation, and
708 # compare view and others uses different notation, and
680 # compare_commits.mako renders links based on the target_repo.
709 # compare_commits.mako renders links based on the target_repo.
681 # We need to swap that here to generate it properly on the html side
710 # We need to swap that here to generate it properly on the html side
682 c.target_repo = c.source_repo
711 c.target_repo = c.source_repo
683
712
684 c.commit_statuses = ChangesetStatus.STATUSES
713 c.commit_statuses = ChangesetStatus.STATUSES
685
714
686 c.show_version_changes = not pr_closed
715 c.show_version_changes = not pr_closed
687 if c.show_version_changes:
716 if c.show_version_changes:
688 cur_obj = pull_request_at_ver
717 cur_obj = pull_request_at_ver
689 prev_obj = prev_pull_request_at_ver
718 prev_obj = prev_pull_request_at_ver
690
719
691 old_commit_ids = prev_obj.revisions
720 old_commit_ids = prev_obj.revisions
692 new_commit_ids = cur_obj.revisions
721 new_commit_ids = cur_obj.revisions
693 commit_changes = PullRequestModel()._calculate_commit_id_changes(
722 commit_changes = PullRequestModel()._calculate_commit_id_changes(
694 old_commit_ids, new_commit_ids)
723 old_commit_ids, new_commit_ids)
695 c.commit_changes_summary = commit_changes
724 c.commit_changes_summary = commit_changes
696
725
697 # calculate the diff for commits between versions
726 # calculate the diff for commits between versions
698 c.commit_changes = []
727 c.commit_changes = []
699
728
700 def mark(cs, fw):
729 def mark(cs, fw):
701 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
730 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
702
731
703 for c_type, raw_id in mark(commit_changes.added, 'a') \
732 for c_type, raw_id in mark(commit_changes.added, 'a') \
704 + mark(commit_changes.removed, 'r') \
733 + mark(commit_changes.removed, 'r') \
705 + mark(commit_changes.common, 'c'):
734 + mark(commit_changes.common, 'c'):
706
735
707 if raw_id in commit_cache:
736 if raw_id in commit_cache:
708 commit = commit_cache[raw_id]
737 commit = commit_cache[raw_id]
709 else:
738 else:
710 try:
739 try:
711 commit = commits_source_repo.get_commit(raw_id)
740 commit = commits_source_repo.get_commit(raw_id)
712 except CommitDoesNotExistError:
741 except CommitDoesNotExistError:
713 # in case we fail extracting still use "dummy" commit
742 # in case we fail extracting still use "dummy" commit
714 # for display in commit diff
743 # for display in commit diff
715 commit = h.AttributeDict(
744 commit = h.AttributeDict(
716 {'raw_id': raw_id,
745 {'raw_id': raw_id,
717 'message': 'EMPTY or MISSING COMMIT'})
746 'message': 'EMPTY or MISSING COMMIT'})
718 c.commit_changes.append([c_type, commit])
747 c.commit_changes.append([c_type, commit])
719
748
720 # current user review statuses for each version
749 # current user review statuses for each version
721 c.review_versions = {}
750 c.review_versions = {}
722 if self._rhodecode_user.user_id in allowed_reviewers:
751 if self._rhodecode_user.user_id in c.allowed_reviewers:
723 for co in general_comments:
752 for co in general_comments:
724 if co.author.user_id == self._rhodecode_user.user_id:
753 if co.author.user_id == self._rhodecode_user.user_id:
725 status = co.status_change
754 status = co.status_change
726 if status:
755 if status:
727 _ver_pr = status[0].comment.pull_request_version_id
756 _ver_pr = status[0].comment.pull_request_version_id
728 c.review_versions[_ver_pr] = status[0]
757 c.review_versions[_ver_pr] = status[0]
729
758
730 return self._get_template_context(c)
759 return self._get_template_context(c)
731
760
732 def get_commits(
761 def get_commits(
733 self, commits_source_repo, pull_request_at_ver, source_commit,
762 self, commits_source_repo, pull_request_at_ver, source_commit,
734 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
763 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
735 maybe_unreachable=False):
764 maybe_unreachable=False):
736
765
737 commit_cache = collections.OrderedDict()
766 commit_cache = collections.OrderedDict()
738 missing_requirements = False
767 missing_requirements = False
739
768
740 try:
769 try:
741 pre_load = ["author", "date", "message", "branch", "parents"]
770 pre_load = ["author", "date", "message", "branch", "parents"]
742
771
743 pull_request_commits = pull_request_at_ver.revisions
772 pull_request_commits = pull_request_at_ver.revisions
744 log.debug('Loading %s commits from %s',
773 log.debug('Loading %s commits from %s',
745 len(pull_request_commits), commits_source_repo)
774 len(pull_request_commits), commits_source_repo)
746
775
747 for rev in pull_request_commits:
776 for rev in pull_request_commits:
748 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
777 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
749 maybe_unreachable=maybe_unreachable)
778 maybe_unreachable=maybe_unreachable)
750 commit_cache[comm.raw_id] = comm
779 commit_cache[comm.raw_id] = comm
751
780
752 # Order here matters, we first need to get target, and then
781 # Order here matters, we first need to get target, and then
753 # the source
782 # the source
754 target_commit = commits_source_repo.get_commit(
783 target_commit = commits_source_repo.get_commit(
755 commit_id=safe_str(target_ref_id))
784 commit_id=safe_str(target_ref_id))
756
785
757 source_commit = commits_source_repo.get_commit(
786 source_commit = commits_source_repo.get_commit(
758 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
787 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
759 except CommitDoesNotExistError:
788 except CommitDoesNotExistError:
760 log.warning('Failed to get commit from `{}` repo'.format(
789 log.warning('Failed to get commit from `{}` repo'.format(
761 commits_source_repo), exc_info=True)
790 commits_source_repo), exc_info=True)
762 except RepositoryRequirementError:
791 except RepositoryRequirementError:
763 log.warning('Failed to get all required data from repo', exc_info=True)
792 log.warning('Failed to get all required data from repo', exc_info=True)
764 missing_requirements = True
793 missing_requirements = True
765
794
766 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
795 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
767
796
768 try:
797 try:
769 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
798 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
770 except Exception:
799 except Exception:
771 ancestor_commit = None
800 ancestor_commit = None
772
801
773 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
802 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
774
803
775 def assure_not_empty_repo(self):
804 def assure_not_empty_repo(self):
776 _ = self.request.translate
805 _ = self.request.translate
777
806
778 try:
807 try:
779 self.db_repo.scm_instance().get_commit()
808 self.db_repo.scm_instance().get_commit()
780 except EmptyRepositoryError:
809 except EmptyRepositoryError:
781 h.flash(h.literal(_('There are no commits yet')),
810 h.flash(h.literal(_('There are no commits yet')),
782 category='warning')
811 category='warning')
783 raise HTTPFound(
812 raise HTTPFound(
784 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
813 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
785
814
786 @LoginRequired()
815 @LoginRequired()
787 @NotAnonymous()
816 @NotAnonymous()
788 @HasRepoPermissionAnyDecorator(
817 @HasRepoPermissionAnyDecorator(
789 'repository.read', 'repository.write', 'repository.admin')
818 'repository.read', 'repository.write', 'repository.admin')
790 @view_config(
819 @view_config(
791 route_name='pullrequest_new', request_method='GET',
820 route_name='pullrequest_new', request_method='GET',
792 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
821 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
793 def pull_request_new(self):
822 def pull_request_new(self):
794 _ = self.request.translate
823 _ = self.request.translate
795 c = self.load_default_context()
824 c = self.load_default_context()
796
825
797 self.assure_not_empty_repo()
826 self.assure_not_empty_repo()
798 source_repo = self.db_repo
827 source_repo = self.db_repo
799
828
800 commit_id = self.request.GET.get('commit')
829 commit_id = self.request.GET.get('commit')
801 branch_ref = self.request.GET.get('branch')
830 branch_ref = self.request.GET.get('branch')
802 bookmark_ref = self.request.GET.get('bookmark')
831 bookmark_ref = self.request.GET.get('bookmark')
803
832
804 try:
833 try:
805 source_repo_data = PullRequestModel().generate_repo_data(
834 source_repo_data = PullRequestModel().generate_repo_data(
806 source_repo, commit_id=commit_id,
835 source_repo, commit_id=commit_id,
807 branch=branch_ref, bookmark=bookmark_ref,
836 branch=branch_ref, bookmark=bookmark_ref,
808 translator=self.request.translate)
837 translator=self.request.translate)
809 except CommitDoesNotExistError as e:
838 except CommitDoesNotExistError as e:
810 log.exception(e)
839 log.exception(e)
811 h.flash(_('Commit does not exist'), 'error')
840 h.flash(_('Commit does not exist'), 'error')
812 raise HTTPFound(
841 raise HTTPFound(
813 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
842 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
814
843
815 default_target_repo = source_repo
844 default_target_repo = source_repo
816
845
817 if source_repo.parent and c.has_origin_repo_read_perm:
846 if source_repo.parent and c.has_origin_repo_read_perm:
818 parent_vcs_obj = source_repo.parent.scm_instance()
847 parent_vcs_obj = source_repo.parent.scm_instance()
819 if parent_vcs_obj and not parent_vcs_obj.is_empty():
848 if parent_vcs_obj and not parent_vcs_obj.is_empty():
820 # change default if we have a parent repo
849 # change default if we have a parent repo
821 default_target_repo = source_repo.parent
850 default_target_repo = source_repo.parent
822
851
823 target_repo_data = PullRequestModel().generate_repo_data(
852 target_repo_data = PullRequestModel().generate_repo_data(
824 default_target_repo, translator=self.request.translate)
853 default_target_repo, translator=self.request.translate)
825
854
826 selected_source_ref = source_repo_data['refs']['selected_ref']
855 selected_source_ref = source_repo_data['refs']['selected_ref']
827 title_source_ref = ''
856 title_source_ref = ''
828 if selected_source_ref:
857 if selected_source_ref:
829 title_source_ref = selected_source_ref.split(':', 2)[1]
858 title_source_ref = selected_source_ref.split(':', 2)[1]
830 c.default_title = PullRequestModel().generate_pullrequest_title(
859 c.default_title = PullRequestModel().generate_pullrequest_title(
831 source=source_repo.repo_name,
860 source=source_repo.repo_name,
832 source_ref=title_source_ref,
861 source_ref=title_source_ref,
833 target=default_target_repo.repo_name
862 target=default_target_repo.repo_name
834 )
863 )
835
864
836 c.default_repo_data = {
865 c.default_repo_data = {
837 'source_repo_name': source_repo.repo_name,
866 'source_repo_name': source_repo.repo_name,
838 'source_refs_json': json.dumps(source_repo_data),
867 'source_refs_json': json.dumps(source_repo_data),
839 'target_repo_name': default_target_repo.repo_name,
868 'target_repo_name': default_target_repo.repo_name,
840 'target_refs_json': json.dumps(target_repo_data),
869 'target_refs_json': json.dumps(target_repo_data),
841 }
870 }
842 c.default_source_ref = selected_source_ref
871 c.default_source_ref = selected_source_ref
843
872
844 return self._get_template_context(c)
873 return self._get_template_context(c)
845
874
846 @LoginRequired()
875 @LoginRequired()
847 @NotAnonymous()
876 @NotAnonymous()
848 @HasRepoPermissionAnyDecorator(
877 @HasRepoPermissionAnyDecorator(
849 'repository.read', 'repository.write', 'repository.admin')
878 'repository.read', 'repository.write', 'repository.admin')
850 @view_config(
879 @view_config(
851 route_name='pullrequest_repo_refs', request_method='GET',
880 route_name='pullrequest_repo_refs', request_method='GET',
852 renderer='json_ext', xhr=True)
881 renderer='json_ext', xhr=True)
853 def pull_request_repo_refs(self):
882 def pull_request_repo_refs(self):
854 self.load_default_context()
883 self.load_default_context()
855 target_repo_name = self.request.matchdict['target_repo_name']
884 target_repo_name = self.request.matchdict['target_repo_name']
856 repo = Repository.get_by_repo_name(target_repo_name)
885 repo = Repository.get_by_repo_name(target_repo_name)
857 if not repo:
886 if not repo:
858 raise HTTPNotFound()
887 raise HTTPNotFound()
859
888
860 target_perm = HasRepoPermissionAny(
889 target_perm = HasRepoPermissionAny(
861 'repository.read', 'repository.write', 'repository.admin')(
890 'repository.read', 'repository.write', 'repository.admin')(
862 target_repo_name)
891 target_repo_name)
863 if not target_perm:
892 if not target_perm:
864 raise HTTPNotFound()
893 raise HTTPNotFound()
865
894
866 return PullRequestModel().generate_repo_data(
895 return PullRequestModel().generate_repo_data(
867 repo, translator=self.request.translate)
896 repo, translator=self.request.translate)
868
897
869 @LoginRequired()
898 @LoginRequired()
870 @NotAnonymous()
899 @NotAnonymous()
871 @HasRepoPermissionAnyDecorator(
900 @HasRepoPermissionAnyDecorator(
872 'repository.read', 'repository.write', 'repository.admin')
901 'repository.read', 'repository.write', 'repository.admin')
873 @view_config(
902 @view_config(
874 route_name='pullrequest_repo_targets', request_method='GET',
903 route_name='pullrequest_repo_targets', request_method='GET',
875 renderer='json_ext', xhr=True)
904 renderer='json_ext', xhr=True)
876 def pullrequest_repo_targets(self):
905 def pullrequest_repo_targets(self):
877 _ = self.request.translate
906 _ = self.request.translate
878 filter_query = self.request.GET.get('query')
907 filter_query = self.request.GET.get('query')
879
908
880 # get the parents
909 # get the parents
881 parent_target_repos = []
910 parent_target_repos = []
882 if self.db_repo.parent:
911 if self.db_repo.parent:
883 parents_query = Repository.query() \
912 parents_query = Repository.query() \
884 .order_by(func.length(Repository.repo_name)) \
913 .order_by(func.length(Repository.repo_name)) \
885 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
914 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
886
915
887 if filter_query:
916 if filter_query:
888 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
917 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
889 parents_query = parents_query.filter(
918 parents_query = parents_query.filter(
890 Repository.repo_name.ilike(ilike_expression))
919 Repository.repo_name.ilike(ilike_expression))
891 parents = parents_query.limit(20).all()
920 parents = parents_query.limit(20).all()
892
921
893 for parent in parents:
922 for parent in parents:
894 parent_vcs_obj = parent.scm_instance()
923 parent_vcs_obj = parent.scm_instance()
895 if parent_vcs_obj and not parent_vcs_obj.is_empty():
924 if parent_vcs_obj and not parent_vcs_obj.is_empty():
896 parent_target_repos.append(parent)
925 parent_target_repos.append(parent)
897
926
898 # get other forks, and repo itself
927 # get other forks, and repo itself
899 query = Repository.query() \
928 query = Repository.query() \
900 .order_by(func.length(Repository.repo_name)) \
929 .order_by(func.length(Repository.repo_name)) \
901 .filter(
930 .filter(
902 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
931 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
903 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
932 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
904 ) \
933 ) \
905 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
934 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
906
935
907 if filter_query:
936 if filter_query:
908 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
937 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
909 query = query.filter(Repository.repo_name.ilike(ilike_expression))
938 query = query.filter(Repository.repo_name.ilike(ilike_expression))
910
939
911 limit = max(20 - len(parent_target_repos), 5) # not less then 5
940 limit = max(20 - len(parent_target_repos), 5) # not less then 5
912 target_repos = query.limit(limit).all()
941 target_repos = query.limit(limit).all()
913
942
914 all_target_repos = target_repos + parent_target_repos
943 all_target_repos = target_repos + parent_target_repos
915
944
916 repos = []
945 repos = []
917 # This checks permissions to the repositories
946 # This checks permissions to the repositories
918 for obj in ScmModel().get_repos(all_target_repos):
947 for obj in ScmModel().get_repos(all_target_repos):
919 repos.append({
948 repos.append({
920 'id': obj['name'],
949 'id': obj['name'],
921 'text': obj['name'],
950 'text': obj['name'],
922 'type': 'repo',
951 'type': 'repo',
923 'repo_id': obj['dbrepo']['repo_id'],
952 'repo_id': obj['dbrepo']['repo_id'],
924 'repo_type': obj['dbrepo']['repo_type'],
953 'repo_type': obj['dbrepo']['repo_type'],
925 'private': obj['dbrepo']['private'],
954 'private': obj['dbrepo']['private'],
926
955
927 })
956 })
928
957
929 data = {
958 data = {
930 'more': False,
959 'more': False,
931 'results': [{
960 'results': [{
932 'text': _('Repositories'),
961 'text': _('Repositories'),
933 'children': repos
962 'children': repos
934 }] if repos else []
963 }] if repos else []
935 }
964 }
936 return data
965 return data
937
966
938 @LoginRequired()
967 @LoginRequired()
939 @NotAnonymous()
968 @NotAnonymous()
940 @HasRepoPermissionAnyDecorator(
969 @HasRepoPermissionAnyDecorator(
941 'repository.read', 'repository.write', 'repository.admin')
970 'repository.read', 'repository.write', 'repository.admin')
971 @view_config(
972 route_name='pullrequest_comments', request_method='POST',
973 renderer='string', xhr=True)
974 def pullrequest_comments(self):
975 self.load_default_context()
976
977 pull_request = PullRequest.get_or_404(
978 self.request.matchdict['pull_request_id'])
979 pull_request_id = pull_request.pull_request_id
980 version = self.request.GET.get('version')
981
982 _render = self.request.get_partial_renderer(
983 'rhodecode:templates/pullrequests/pullrequest_show.mako')
984 c = _render.get_call_context()
985
986 (pull_request_latest,
987 pull_request_at_ver,
988 pull_request_display_obj,
989 at_version) = PullRequestModel().get_pr_version(
990 pull_request_id, version=version)
991 versions = pull_request_display_obj.versions()
992 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
993 c.versions = versions + [latest_ver]
994
995 c.at_version = at_version
996 c.at_version_num = (at_version
997 if at_version and at_version != PullRequest.LATEST_VER
998 else None)
999
1000 self.register_comments_vars(c, pull_request_latest, versions)
1001 all_comments = c.inline_comments_flat + c.comments
1002 return _render('comments_table', all_comments, len(all_comments))
1003
1004 @LoginRequired()
1005 @NotAnonymous()
1006 @HasRepoPermissionAnyDecorator(
1007 'repository.read', 'repository.write', 'repository.admin')
1008 @view_config(
1009 route_name='pullrequest_todos', request_method='POST',
1010 renderer='string', xhr=True)
1011 def pullrequest_todos(self):
1012 self.load_default_context()
1013
1014 pull_request = PullRequest.get_or_404(
1015 self.request.matchdict['pull_request_id'])
1016 pull_request_id = pull_request.pull_request_id
1017 version = self.request.GET.get('version')
1018
1019 _render = self.request.get_partial_renderer(
1020 'rhodecode:templates/pullrequests/pullrequest_show.mako')
1021 c = _render.get_call_context()
1022 (pull_request_latest,
1023 pull_request_at_ver,
1024 pull_request_display_obj,
1025 at_version) = PullRequestModel().get_pr_version(
1026 pull_request_id, version=version)
1027 versions = pull_request_display_obj.versions()
1028 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1029 c.versions = versions + [latest_ver]
1030
1031 c.at_version = at_version
1032 c.at_version_num = (at_version
1033 if at_version and at_version != PullRequest.LATEST_VER
1034 else None)
1035
1036 c.unresolved_comments = CommentsModel() \
1037 .get_pull_request_unresolved_todos(pull_request)
1038 c.resolved_comments = CommentsModel() \
1039 .get_pull_request_resolved_todos(pull_request)
1040
1041 all_comments = c.unresolved_comments + c.resolved_comments
1042 return _render('comments_table', all_comments, len(c.unresolved_comments), todo_comments=True)
1043
1044 @LoginRequired()
1045 @NotAnonymous()
1046 @HasRepoPermissionAnyDecorator(
1047 'repository.read', 'repository.write', 'repository.admin')
942 @CSRFRequired()
1048 @CSRFRequired()
943 @view_config(
1049 @view_config(
944 route_name='pullrequest_create', request_method='POST',
1050 route_name='pullrequest_create', request_method='POST',
945 renderer=None)
1051 renderer=None)
946 def pull_request_create(self):
1052 def pull_request_create(self):
947 _ = self.request.translate
1053 _ = self.request.translate
948 self.assure_not_empty_repo()
1054 self.assure_not_empty_repo()
949 self.load_default_context()
1055 self.load_default_context()
950
1056
951 controls = peppercorn.parse(self.request.POST.items())
1057 controls = peppercorn.parse(self.request.POST.items())
952
1058
953 try:
1059 try:
954 form = PullRequestForm(
1060 form = PullRequestForm(
955 self.request.translate, self.db_repo.repo_id)()
1061 self.request.translate, self.db_repo.repo_id)()
956 _form = form.to_python(controls)
1062 _form = form.to_python(controls)
957 except formencode.Invalid as errors:
1063 except formencode.Invalid as errors:
958 if errors.error_dict.get('revisions'):
1064 if errors.error_dict.get('revisions'):
959 msg = 'Revisions: %s' % errors.error_dict['revisions']
1065 msg = 'Revisions: %s' % errors.error_dict['revisions']
960 elif errors.error_dict.get('pullrequest_title'):
1066 elif errors.error_dict.get('pullrequest_title'):
961 msg = errors.error_dict.get('pullrequest_title')
1067 msg = errors.error_dict.get('pullrequest_title')
962 else:
1068 else:
963 msg = _('Error creating pull request: {}').format(errors)
1069 msg = _('Error creating pull request: {}').format(errors)
964 log.exception(msg)
1070 log.exception(msg)
965 h.flash(msg, 'error')
1071 h.flash(msg, 'error')
966
1072
967 # would rather just go back to form ...
1073 # would rather just go back to form ...
968 raise HTTPFound(
1074 raise HTTPFound(
969 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1075 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
970
1076
971 source_repo = _form['source_repo']
1077 source_repo = _form['source_repo']
972 source_ref = _form['source_ref']
1078 source_ref = _form['source_ref']
973 target_repo = _form['target_repo']
1079 target_repo = _form['target_repo']
974 target_ref = _form['target_ref']
1080 target_ref = _form['target_ref']
975 commit_ids = _form['revisions'][::-1]
1081 commit_ids = _form['revisions'][::-1]
976 common_ancestor_id = _form['common_ancestor']
1082 common_ancestor_id = _form['common_ancestor']
977
1083
978 # find the ancestor for this pr
1084 # find the ancestor for this pr
979 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1085 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
980 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1086 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
981
1087
982 if not (source_db_repo or target_db_repo):
1088 if not (source_db_repo or target_db_repo):
983 h.flash(_('source_repo or target repo not found'), category='error')
1089 h.flash(_('source_repo or target repo not found'), category='error')
984 raise HTTPFound(
1090 raise HTTPFound(
985 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1091 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
986
1092
987 # re-check permissions again here
1093 # re-check permissions again here
988 # source_repo we must have read permissions
1094 # source_repo we must have read permissions
989
1095
990 source_perm = HasRepoPermissionAny(
1096 source_perm = HasRepoPermissionAny(
991 'repository.read', 'repository.write', 'repository.admin')(
1097 'repository.read', 'repository.write', 'repository.admin')(
992 source_db_repo.repo_name)
1098 source_db_repo.repo_name)
993 if not source_perm:
1099 if not source_perm:
994 msg = _('Not Enough permissions to source repo `{}`.'.format(
1100 msg = _('Not Enough permissions to source repo `{}`.'.format(
995 source_db_repo.repo_name))
1101 source_db_repo.repo_name))
996 h.flash(msg, category='error')
1102 h.flash(msg, category='error')
997 # copy the args back to redirect
1103 # copy the args back to redirect
998 org_query = self.request.GET.mixed()
1104 org_query = self.request.GET.mixed()
999 raise HTTPFound(
1105 raise HTTPFound(
1000 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1106 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1001 _query=org_query))
1107 _query=org_query))
1002
1108
1003 # target repo we must have read permissions, and also later on
1109 # target repo we must have read permissions, and also later on
1004 # we want to check branch permissions here
1110 # we want to check branch permissions here
1005 target_perm = HasRepoPermissionAny(
1111 target_perm = HasRepoPermissionAny(
1006 'repository.read', 'repository.write', 'repository.admin')(
1112 'repository.read', 'repository.write', 'repository.admin')(
1007 target_db_repo.repo_name)
1113 target_db_repo.repo_name)
1008 if not target_perm:
1114 if not target_perm:
1009 msg = _('Not Enough permissions to target repo `{}`.'.format(
1115 msg = _('Not Enough permissions to target repo `{}`.'.format(
1010 target_db_repo.repo_name))
1116 target_db_repo.repo_name))
1011 h.flash(msg, category='error')
1117 h.flash(msg, category='error')
1012 # copy the args back to redirect
1118 # copy the args back to redirect
1013 org_query = self.request.GET.mixed()
1119 org_query = self.request.GET.mixed()
1014 raise HTTPFound(
1120 raise HTTPFound(
1015 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1121 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1016 _query=org_query))
1122 _query=org_query))
1017
1123
1018 source_scm = source_db_repo.scm_instance()
1124 source_scm = source_db_repo.scm_instance()
1019 target_scm = target_db_repo.scm_instance()
1125 target_scm = target_db_repo.scm_instance()
1020
1126
1021 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1127 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1022 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1128 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1023
1129
1024 ancestor = source_scm.get_common_ancestor(
1130 ancestor = source_scm.get_common_ancestor(
1025 source_commit.raw_id, target_commit.raw_id, target_scm)
1131 source_commit.raw_id, target_commit.raw_id, target_scm)
1026
1132
1027 # recalculate target ref based on ancestor
1133 # recalculate target ref based on ancestor
1028 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1134 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1029 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1135 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1030
1136
1031 get_default_reviewers_data, validate_default_reviewers = \
1137 get_default_reviewers_data, validate_default_reviewers = \
1032 PullRequestModel().get_reviewer_functions()
1138 PullRequestModel().get_reviewer_functions()
1033
1139
1034 # recalculate reviewers logic, to make sure we can validate this
1140 # recalculate reviewers logic, to make sure we can validate this
1035 reviewer_rules = get_default_reviewers_data(
1141 reviewer_rules = get_default_reviewers_data(
1036 self._rhodecode_db_user, source_db_repo,
1142 self._rhodecode_db_user, source_db_repo,
1037 source_commit, target_db_repo, target_commit)
1143 source_commit, target_db_repo, target_commit)
1038
1144
1039 given_reviewers = _form['review_members']
1145 given_reviewers = _form['review_members']
1040 reviewers = validate_default_reviewers(
1146 reviewers = validate_default_reviewers(
1041 given_reviewers, reviewer_rules)
1147 given_reviewers, reviewer_rules)
1042
1148
1043 pullrequest_title = _form['pullrequest_title']
1149 pullrequest_title = _form['pullrequest_title']
1044 title_source_ref = source_ref.split(':', 2)[1]
1150 title_source_ref = source_ref.split(':', 2)[1]
1045 if not pullrequest_title:
1151 if not pullrequest_title:
1046 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1152 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1047 source=source_repo,
1153 source=source_repo,
1048 source_ref=title_source_ref,
1154 source_ref=title_source_ref,
1049 target=target_repo
1155 target=target_repo
1050 )
1156 )
1051
1157
1052 description = _form['pullrequest_desc']
1158 description = _form['pullrequest_desc']
1053 description_renderer = _form['description_renderer']
1159 description_renderer = _form['description_renderer']
1054
1160
1055 try:
1161 try:
1056 pull_request = PullRequestModel().create(
1162 pull_request = PullRequestModel().create(
1057 created_by=self._rhodecode_user.user_id,
1163 created_by=self._rhodecode_user.user_id,
1058 source_repo=source_repo,
1164 source_repo=source_repo,
1059 source_ref=source_ref,
1165 source_ref=source_ref,
1060 target_repo=target_repo,
1166 target_repo=target_repo,
1061 target_ref=target_ref,
1167 target_ref=target_ref,
1062 revisions=commit_ids,
1168 revisions=commit_ids,
1063 common_ancestor_id=common_ancestor_id,
1169 common_ancestor_id=common_ancestor_id,
1064 reviewers=reviewers,
1170 reviewers=reviewers,
1065 title=pullrequest_title,
1171 title=pullrequest_title,
1066 description=description,
1172 description=description,
1067 description_renderer=description_renderer,
1173 description_renderer=description_renderer,
1068 reviewer_data=reviewer_rules,
1174 reviewer_data=reviewer_rules,
1069 auth_user=self._rhodecode_user
1175 auth_user=self._rhodecode_user
1070 )
1176 )
1071 Session().commit()
1177 Session().commit()
1072
1178
1073 h.flash(_('Successfully opened new pull request'),
1179 h.flash(_('Successfully opened new pull request'),
1074 category='success')
1180 category='success')
1075 except Exception:
1181 except Exception:
1076 msg = _('Error occurred during creation of this pull request.')
1182 msg = _('Error occurred during creation of this pull request.')
1077 log.exception(msg)
1183 log.exception(msg)
1078 h.flash(msg, category='error')
1184 h.flash(msg, category='error')
1079
1185
1080 # copy the args back to redirect
1186 # copy the args back to redirect
1081 org_query = self.request.GET.mixed()
1187 org_query = self.request.GET.mixed()
1082 raise HTTPFound(
1188 raise HTTPFound(
1083 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1189 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1084 _query=org_query))
1190 _query=org_query))
1085
1191
1086 raise HTTPFound(
1192 raise HTTPFound(
1087 h.route_path('pullrequest_show', repo_name=target_repo,
1193 h.route_path('pullrequest_show', repo_name=target_repo,
1088 pull_request_id=pull_request.pull_request_id))
1194 pull_request_id=pull_request.pull_request_id))
1089
1195
1090 @LoginRequired()
1196 @LoginRequired()
1091 @NotAnonymous()
1197 @NotAnonymous()
1092 @HasRepoPermissionAnyDecorator(
1198 @HasRepoPermissionAnyDecorator(
1093 'repository.read', 'repository.write', 'repository.admin')
1199 'repository.read', 'repository.write', 'repository.admin')
1094 @CSRFRequired()
1200 @CSRFRequired()
1095 @view_config(
1201 @view_config(
1096 route_name='pullrequest_update', request_method='POST',
1202 route_name='pullrequest_update', request_method='POST',
1097 renderer='json_ext')
1203 renderer='json_ext')
1098 def pull_request_update(self):
1204 def pull_request_update(self):
1099 pull_request = PullRequest.get_or_404(
1205 pull_request = PullRequest.get_or_404(
1100 self.request.matchdict['pull_request_id'])
1206 self.request.matchdict['pull_request_id'])
1101 _ = self.request.translate
1207 _ = self.request.translate
1102
1208
1103 self.load_default_context()
1209 c = self.load_default_context()
1104 redirect_url = None
1210 redirect_url = None
1105
1211
1106 if pull_request.is_closed():
1212 if pull_request.is_closed():
1107 log.debug('update: forbidden because pull request is closed')
1213 log.debug('update: forbidden because pull request is closed')
1108 msg = _(u'Cannot update closed pull requests.')
1214 msg = _(u'Cannot update closed pull requests.')
1109 h.flash(msg, category='error')
1215 h.flash(msg, category='error')
1110 return {'response': True,
1216 return {'response': True,
1111 'redirect_url': redirect_url}
1217 'redirect_url': redirect_url}
1112
1218
1113 is_state_changing = pull_request.is_state_changing()
1219 is_state_changing = pull_request.is_state_changing()
1220 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
1221 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1114
1222
1115 # only owner or admin can update it
1223 # only owner or admin can update it
1116 allowed_to_update = PullRequestModel().check_user_update(
1224 allowed_to_update = PullRequestModel().check_user_update(
1117 pull_request, self._rhodecode_user)
1225 pull_request, self._rhodecode_user)
1118 if allowed_to_update:
1226 if allowed_to_update:
1119 controls = peppercorn.parse(self.request.POST.items())
1227 controls = peppercorn.parse(self.request.POST.items())
1120 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1228 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1121
1229
1122 if 'review_members' in controls:
1230 if 'review_members' in controls:
1123 self._update_reviewers(
1231 self._update_reviewers(
1124 pull_request, controls['review_members'],
1232 pull_request, controls['review_members'],
1125 pull_request.reviewer_data)
1233 pull_request.reviewer_data)
1126 elif str2bool(self.request.POST.get('update_commits', 'false')):
1234 elif str2bool(self.request.POST.get('update_commits', 'false')):
1127 if is_state_changing:
1235 if is_state_changing:
1128 log.debug('commits update: forbidden because pull request is in state %s',
1236 log.debug('commits update: forbidden because pull request is in state %s',
1129 pull_request.pull_request_state)
1237 pull_request.pull_request_state)
1130 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1238 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1131 u'Current state is: `{}`').format(
1239 u'Current state is: `{}`').format(
1132 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1240 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1133 h.flash(msg, category='error')
1241 h.flash(msg, category='error')
1134 return {'response': True,
1242 return {'response': True,
1135 'redirect_url': redirect_url}
1243 'redirect_url': redirect_url}
1136
1244
1137 self._update_commits(pull_request)
1245 self._update_commits(c, pull_request)
1138 if force_refresh:
1246 if force_refresh:
1139 redirect_url = h.route_path(
1247 redirect_url = h.route_path(
1140 'pullrequest_show', repo_name=self.db_repo_name,
1248 'pullrequest_show', repo_name=self.db_repo_name,
1141 pull_request_id=pull_request.pull_request_id,
1249 pull_request_id=pull_request.pull_request_id,
1142 _query={"force_refresh": 1})
1250 _query={"force_refresh": 1})
1143 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1251 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1144 self._edit_pull_request(pull_request)
1252 self._edit_pull_request(pull_request)
1145 else:
1253 else:
1146 raise HTTPBadRequest()
1254 raise HTTPBadRequest()
1147
1255
1148 return {'response': True,
1256 return {'response': True,
1149 'redirect_url': redirect_url}
1257 'redirect_url': redirect_url}
1150 raise HTTPForbidden()
1258 raise HTTPForbidden()
1151
1259
1152 def _edit_pull_request(self, pull_request):
1260 def _edit_pull_request(self, pull_request):
1153 _ = self.request.translate
1261 _ = self.request.translate
1154
1262
1155 try:
1263 try:
1156 PullRequestModel().edit(
1264 PullRequestModel().edit(
1157 pull_request,
1265 pull_request,
1158 self.request.POST.get('title'),
1266 self.request.POST.get('title'),
1159 self.request.POST.get('description'),
1267 self.request.POST.get('description'),
1160 self.request.POST.get('description_renderer'),
1268 self.request.POST.get('description_renderer'),
1161 self._rhodecode_user)
1269 self._rhodecode_user)
1162 except ValueError:
1270 except ValueError:
1163 msg = _(u'Cannot update closed pull requests.')
1271 msg = _(u'Cannot update closed pull requests.')
1164 h.flash(msg, category='error')
1272 h.flash(msg, category='error')
1165 return
1273 return
1166 else:
1274 else:
1167 Session().commit()
1275 Session().commit()
1168
1276
1169 msg = _(u'Pull request title & description updated.')
1277 msg = _(u'Pull request title & description updated.')
1170 h.flash(msg, category='success')
1278 h.flash(msg, category='success')
1171 return
1279 return
1172
1280
1173 def _update_commits(self, pull_request):
1281 def _update_commits(self, c, pull_request):
1174 _ = self.request.translate
1282 _ = self.request.translate
1175
1283
1176 with pull_request.set_state(PullRequest.STATE_UPDATING):
1284 with pull_request.set_state(PullRequest.STATE_UPDATING):
1177 resp = PullRequestModel().update_commits(
1285 resp = PullRequestModel().update_commits(
1178 pull_request, self._rhodecode_db_user)
1286 pull_request, self._rhodecode_db_user)
1179
1287
1180 if resp.executed:
1288 if resp.executed:
1181
1289
1182 if resp.target_changed and resp.source_changed:
1290 if resp.target_changed and resp.source_changed:
1183 changed = 'target and source repositories'
1291 changed = 'target and source repositories'
1184 elif resp.target_changed and not resp.source_changed:
1292 elif resp.target_changed and not resp.source_changed:
1185 changed = 'target repository'
1293 changed = 'target repository'
1186 elif not resp.target_changed and resp.source_changed:
1294 elif not resp.target_changed and resp.source_changed:
1187 changed = 'source repository'
1295 changed = 'source repository'
1188 else:
1296 else:
1189 changed = 'nothing'
1297 changed = 'nothing'
1190
1298
1191 msg = _(u'Pull request updated to "{source_commit_id}" with '
1299 msg = _(u'Pull request updated to "{source_commit_id}" with '
1192 u'{count_added} added, {count_removed} removed commits. '
1300 u'{count_added} added, {count_removed} removed commits. '
1193 u'Source of changes: {change_source}')
1301 u'Source of changes: {change_source}')
1194 msg = msg.format(
1302 msg = msg.format(
1195 source_commit_id=pull_request.source_ref_parts.commit_id,
1303 source_commit_id=pull_request.source_ref_parts.commit_id,
1196 count_added=len(resp.changes.added),
1304 count_added=len(resp.changes.added),
1197 count_removed=len(resp.changes.removed),
1305 count_removed=len(resp.changes.removed),
1198 change_source=changed)
1306 change_source=changed)
1199 h.flash(msg, category='success')
1307 h.flash(msg, category='success')
1200
1308
1201 channel = '/repo${}$/pr/{}'.format(
1202 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1203 message = msg + (
1309 message = msg + (
1204 ' - <a onclick="window.location.reload()">'
1310 ' - <a onclick="window.location.reload()">'
1205 '<strong>{}</strong></a>'.format(_('Reload page')))
1311 '<strong>{}</strong></a>'.format(_('Reload page')))
1312
1313 message_obj = {
1314 'message': message,
1315 'level': 'success',
1316 'topic': '/notifications'
1317 }
1318
1206 channelstream.post_message(
1319 channelstream.post_message(
1207 channel, message, self._rhodecode_user.username,
1320 c.pr_broadcast_channel, message_obj, self._rhodecode_user.username,
1208 registry=self.request.registry)
1321 registry=self.request.registry)
1209 else:
1322 else:
1210 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1323 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1211 warning_reasons = [
1324 warning_reasons = [
1212 UpdateFailureReason.NO_CHANGE,
1325 UpdateFailureReason.NO_CHANGE,
1213 UpdateFailureReason.WRONG_REF_TYPE,
1326 UpdateFailureReason.WRONG_REF_TYPE,
1214 ]
1327 ]
1215 category = 'warning' if resp.reason in warning_reasons else 'error'
1328 category = 'warning' if resp.reason in warning_reasons else 'error'
1216 h.flash(msg, category=category)
1329 h.flash(msg, category=category)
1217
1330
1218 @LoginRequired()
1331 @LoginRequired()
1219 @NotAnonymous()
1332 @NotAnonymous()
1220 @HasRepoPermissionAnyDecorator(
1333 @HasRepoPermissionAnyDecorator(
1221 'repository.read', 'repository.write', 'repository.admin')
1334 'repository.read', 'repository.write', 'repository.admin')
1222 @CSRFRequired()
1335 @CSRFRequired()
1223 @view_config(
1336 @view_config(
1224 route_name='pullrequest_merge', request_method='POST',
1337 route_name='pullrequest_merge', request_method='POST',
1225 renderer='json_ext')
1338 renderer='json_ext')
1226 def pull_request_merge(self):
1339 def pull_request_merge(self):
1227 """
1340 """
1228 Merge will perform a server-side merge of the specified
1341 Merge will perform a server-side merge of the specified
1229 pull request, if the pull request is approved and mergeable.
1342 pull request, if the pull request is approved and mergeable.
1230 After successful merging, the pull request is automatically
1343 After successful merging, the pull request is automatically
1231 closed, with a relevant comment.
1344 closed, with a relevant comment.
1232 """
1345 """
1233 pull_request = PullRequest.get_or_404(
1346 pull_request = PullRequest.get_or_404(
1234 self.request.matchdict['pull_request_id'])
1347 self.request.matchdict['pull_request_id'])
1235 _ = self.request.translate
1348 _ = self.request.translate
1236
1349
1237 if pull_request.is_state_changing():
1350 if pull_request.is_state_changing():
1238 log.debug('show: forbidden because pull request is in state %s',
1351 log.debug('show: forbidden because pull request is in state %s',
1239 pull_request.pull_request_state)
1352 pull_request.pull_request_state)
1240 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1353 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1241 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1354 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1242 pull_request.pull_request_state)
1355 pull_request.pull_request_state)
1243 h.flash(msg, category='error')
1356 h.flash(msg, category='error')
1244 raise HTTPFound(
1357 raise HTTPFound(
1245 h.route_path('pullrequest_show',
1358 h.route_path('pullrequest_show',
1246 repo_name=pull_request.target_repo.repo_name,
1359 repo_name=pull_request.target_repo.repo_name,
1247 pull_request_id=pull_request.pull_request_id))
1360 pull_request_id=pull_request.pull_request_id))
1248
1361
1249 self.load_default_context()
1362 self.load_default_context()
1250
1363
1251 with pull_request.set_state(PullRequest.STATE_UPDATING):
1364 with pull_request.set_state(PullRequest.STATE_UPDATING):
1252 check = MergeCheck.validate(
1365 check = MergeCheck.validate(
1253 pull_request, auth_user=self._rhodecode_user,
1366 pull_request, auth_user=self._rhodecode_user,
1254 translator=self.request.translate)
1367 translator=self.request.translate)
1255 merge_possible = not check.failed
1368 merge_possible = not check.failed
1256
1369
1257 for err_type, error_msg in check.errors:
1370 for err_type, error_msg in check.errors:
1258 h.flash(error_msg, category=err_type)
1371 h.flash(error_msg, category=err_type)
1259
1372
1260 if merge_possible:
1373 if merge_possible:
1261 log.debug("Pre-conditions checked, trying to merge.")
1374 log.debug("Pre-conditions checked, trying to merge.")
1262 extras = vcs_operation_context(
1375 extras = vcs_operation_context(
1263 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1376 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1264 username=self._rhodecode_db_user.username, action='push',
1377 username=self._rhodecode_db_user.username, action='push',
1265 scm=pull_request.target_repo.repo_type)
1378 scm=pull_request.target_repo.repo_type)
1266 with pull_request.set_state(PullRequest.STATE_UPDATING):
1379 with pull_request.set_state(PullRequest.STATE_UPDATING):
1267 self._merge_pull_request(
1380 self._merge_pull_request(
1268 pull_request, self._rhodecode_db_user, extras)
1381 pull_request, self._rhodecode_db_user, extras)
1269 else:
1382 else:
1270 log.debug("Pre-conditions failed, NOT merging.")
1383 log.debug("Pre-conditions failed, NOT merging.")
1271
1384
1272 raise HTTPFound(
1385 raise HTTPFound(
1273 h.route_path('pullrequest_show',
1386 h.route_path('pullrequest_show',
1274 repo_name=pull_request.target_repo.repo_name,
1387 repo_name=pull_request.target_repo.repo_name,
1275 pull_request_id=pull_request.pull_request_id))
1388 pull_request_id=pull_request.pull_request_id))
1276
1389
1277 def _merge_pull_request(self, pull_request, user, extras):
1390 def _merge_pull_request(self, pull_request, user, extras):
1278 _ = self.request.translate
1391 _ = self.request.translate
1279 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1392 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1280
1393
1281 if merge_resp.executed:
1394 if merge_resp.executed:
1282 log.debug("The merge was successful, closing the pull request.")
1395 log.debug("The merge was successful, closing the pull request.")
1283 PullRequestModel().close_pull_request(
1396 PullRequestModel().close_pull_request(
1284 pull_request.pull_request_id, user)
1397 pull_request.pull_request_id, user)
1285 Session().commit()
1398 Session().commit()
1286 msg = _('Pull request was successfully merged and closed.')
1399 msg = _('Pull request was successfully merged and closed.')
1287 h.flash(msg, category='success')
1400 h.flash(msg, category='success')
1288 else:
1401 else:
1289 log.debug(
1402 log.debug(
1290 "The merge was not successful. Merge response: %s", merge_resp)
1403 "The merge was not successful. Merge response: %s", merge_resp)
1291 msg = merge_resp.merge_status_message
1404 msg = merge_resp.merge_status_message
1292 h.flash(msg, category='error')
1405 h.flash(msg, category='error')
1293
1406
1294 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1407 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1295 _ = self.request.translate
1408 _ = self.request.translate
1296
1409
1297 get_default_reviewers_data, validate_default_reviewers = \
1410 get_default_reviewers_data, validate_default_reviewers = \
1298 PullRequestModel().get_reviewer_functions()
1411 PullRequestModel().get_reviewer_functions()
1299
1412
1300 try:
1413 try:
1301 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1414 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1302 except ValueError as e:
1415 except ValueError as e:
1303 log.error('Reviewers Validation: {}'.format(e))
1416 log.error('Reviewers Validation: {}'.format(e))
1304 h.flash(e, category='error')
1417 h.flash(e, category='error')
1305 return
1418 return
1306
1419
1307 old_calculated_status = pull_request.calculated_review_status()
1420 old_calculated_status = pull_request.calculated_review_status()
1308 PullRequestModel().update_reviewers(
1421 PullRequestModel().update_reviewers(
1309 pull_request, reviewers, self._rhodecode_user)
1422 pull_request, reviewers, self._rhodecode_user)
1310 h.flash(_('Pull request reviewers updated.'), category='success')
1423 h.flash(_('Pull request reviewers updated.'), category='success')
1311 Session().commit()
1424 Session().commit()
1312
1425
1313 # trigger status changed if change in reviewers changes the status
1426 # trigger status changed if change in reviewers changes the status
1314 calculated_status = pull_request.calculated_review_status()
1427 calculated_status = pull_request.calculated_review_status()
1315 if old_calculated_status != calculated_status:
1428 if old_calculated_status != calculated_status:
1316 PullRequestModel().trigger_pull_request_hook(
1429 PullRequestModel().trigger_pull_request_hook(
1317 pull_request, self._rhodecode_user, 'review_status_change',
1430 pull_request, self._rhodecode_user, 'review_status_change',
1318 data={'status': calculated_status})
1431 data={'status': calculated_status})
1319
1432
1320 @LoginRequired()
1433 @LoginRequired()
1321 @NotAnonymous()
1434 @NotAnonymous()
1322 @HasRepoPermissionAnyDecorator(
1435 @HasRepoPermissionAnyDecorator(
1323 'repository.read', 'repository.write', 'repository.admin')
1436 'repository.read', 'repository.write', 'repository.admin')
1324 @CSRFRequired()
1437 @CSRFRequired()
1325 @view_config(
1438 @view_config(
1326 route_name='pullrequest_delete', request_method='POST',
1439 route_name='pullrequest_delete', request_method='POST',
1327 renderer='json_ext')
1440 renderer='json_ext')
1328 def pull_request_delete(self):
1441 def pull_request_delete(self):
1329 _ = self.request.translate
1442 _ = self.request.translate
1330
1443
1331 pull_request = PullRequest.get_or_404(
1444 pull_request = PullRequest.get_or_404(
1332 self.request.matchdict['pull_request_id'])
1445 self.request.matchdict['pull_request_id'])
1333 self.load_default_context()
1446 self.load_default_context()
1334
1447
1335 pr_closed = pull_request.is_closed()
1448 pr_closed = pull_request.is_closed()
1336 allowed_to_delete = PullRequestModel().check_user_delete(
1449 allowed_to_delete = PullRequestModel().check_user_delete(
1337 pull_request, self._rhodecode_user) and not pr_closed
1450 pull_request, self._rhodecode_user) and not pr_closed
1338
1451
1339 # only owner can delete it !
1452 # only owner can delete it !
1340 if allowed_to_delete:
1453 if allowed_to_delete:
1341 PullRequestModel().delete(pull_request, self._rhodecode_user)
1454 PullRequestModel().delete(pull_request, self._rhodecode_user)
1342 Session().commit()
1455 Session().commit()
1343 h.flash(_('Successfully deleted pull request'),
1456 h.flash(_('Successfully deleted pull request'),
1344 category='success')
1457 category='success')
1345 raise HTTPFound(h.route_path('pullrequest_show_all',
1458 raise HTTPFound(h.route_path('pullrequest_show_all',
1346 repo_name=self.db_repo_name))
1459 repo_name=self.db_repo_name))
1347
1460
1348 log.warning('user %s tried to delete pull request without access',
1461 log.warning('user %s tried to delete pull request without access',
1349 self._rhodecode_user)
1462 self._rhodecode_user)
1350 raise HTTPNotFound()
1463 raise HTTPNotFound()
1351
1464
1352 @LoginRequired()
1465 @LoginRequired()
1353 @NotAnonymous()
1466 @NotAnonymous()
1354 @HasRepoPermissionAnyDecorator(
1467 @HasRepoPermissionAnyDecorator(
1355 'repository.read', 'repository.write', 'repository.admin')
1468 'repository.read', 'repository.write', 'repository.admin')
1356 @CSRFRequired()
1469 @CSRFRequired()
1357 @view_config(
1470 @view_config(
1358 route_name='pullrequest_comment_create', request_method='POST',
1471 route_name='pullrequest_comment_create', request_method='POST',
1359 renderer='json_ext')
1472 renderer='json_ext')
1360 def pull_request_comment_create(self):
1473 def pull_request_comment_create(self):
1361 _ = self.request.translate
1474 _ = self.request.translate
1362
1475
1363 pull_request = PullRequest.get_or_404(
1476 pull_request = PullRequest.get_or_404(
1364 self.request.matchdict['pull_request_id'])
1477 self.request.matchdict['pull_request_id'])
1365 pull_request_id = pull_request.pull_request_id
1478 pull_request_id = pull_request.pull_request_id
1366
1479
1367 if pull_request.is_closed():
1480 if pull_request.is_closed():
1368 log.debug('comment: forbidden because pull request is closed')
1481 log.debug('comment: forbidden because pull request is closed')
1369 raise HTTPForbidden()
1482 raise HTTPForbidden()
1370
1483
1371 allowed_to_comment = PullRequestModel().check_user_comment(
1484 allowed_to_comment = PullRequestModel().check_user_comment(
1372 pull_request, self._rhodecode_user)
1485 pull_request, self._rhodecode_user)
1373 if not allowed_to_comment:
1486 if not allowed_to_comment:
1374 log.debug(
1487 log.debug(
1375 'comment: forbidden because pull request is from forbidden repo')
1488 'comment: forbidden because pull request is from forbidden repo')
1376 raise HTTPForbidden()
1489 raise HTTPForbidden()
1377
1490
1378 c = self.load_default_context()
1491 c = self.load_default_context()
1379
1492
1380 status = self.request.POST.get('changeset_status', None)
1493 status = self.request.POST.get('changeset_status', None)
1381 text = self.request.POST.get('text')
1494 text = self.request.POST.get('text')
1382 comment_type = self.request.POST.get('comment_type')
1495 comment_type = self.request.POST.get('comment_type')
1383 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1496 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1384 close_pull_request = self.request.POST.get('close_pull_request')
1497 close_pull_request = self.request.POST.get('close_pull_request')
1385
1498
1386 # the logic here should work like following, if we submit close
1499 # the logic here should work like following, if we submit close
1387 # pr comment, use `close_pull_request_with_comment` function
1500 # pr comment, use `close_pull_request_with_comment` function
1388 # else handle regular comment logic
1501 # else handle regular comment logic
1389
1502
1390 if close_pull_request:
1503 if close_pull_request:
1391 # only owner or admin or person with write permissions
1504 # only owner or admin or person with write permissions
1392 allowed_to_close = PullRequestModel().check_user_update(
1505 allowed_to_close = PullRequestModel().check_user_update(
1393 pull_request, self._rhodecode_user)
1506 pull_request, self._rhodecode_user)
1394 if not allowed_to_close:
1507 if not allowed_to_close:
1395 log.debug('comment: forbidden because not allowed to close '
1508 log.debug('comment: forbidden because not allowed to close '
1396 'pull request %s', pull_request_id)
1509 'pull request %s', pull_request_id)
1397 raise HTTPForbidden()
1510 raise HTTPForbidden()
1398
1511
1399 # This also triggers `review_status_change`
1512 # This also triggers `review_status_change`
1400 comment, status = PullRequestModel().close_pull_request_with_comment(
1513 comment, status = PullRequestModel().close_pull_request_with_comment(
1401 pull_request, self._rhodecode_user, self.db_repo, message=text,
1514 pull_request, self._rhodecode_user, self.db_repo, message=text,
1402 auth_user=self._rhodecode_user)
1515 auth_user=self._rhodecode_user)
1403 Session().flush()
1516 Session().flush()
1404
1517
1405 PullRequestModel().trigger_pull_request_hook(
1518 PullRequestModel().trigger_pull_request_hook(
1406 pull_request, self._rhodecode_user, 'comment',
1519 pull_request, self._rhodecode_user, 'comment',
1407 data={'comment': comment})
1520 data={'comment': comment})
1408
1521
1409 else:
1522 else:
1410 # regular comment case, could be inline, or one with status.
1523 # regular comment case, could be inline, or one with status.
1411 # for that one we check also permissions
1524 # for that one we check also permissions
1412
1525
1413 allowed_to_change_status = PullRequestModel().check_user_change_status(
1526 allowed_to_change_status = PullRequestModel().check_user_change_status(
1414 pull_request, self._rhodecode_user)
1527 pull_request, self._rhodecode_user)
1415
1528
1416 if status and allowed_to_change_status:
1529 if status and allowed_to_change_status:
1417 message = (_('Status change %(transition_icon)s %(status)s')
1530 message = (_('Status change %(transition_icon)s %(status)s')
1418 % {'transition_icon': '>',
1531 % {'transition_icon': '>',
1419 'status': ChangesetStatus.get_status_lbl(status)})
1532 'status': ChangesetStatus.get_status_lbl(status)})
1420 text = text or message
1533 text = text or message
1421
1534
1422 comment = CommentsModel().create(
1535 comment = CommentsModel().create(
1423 text=text,
1536 text=text,
1424 repo=self.db_repo.repo_id,
1537 repo=self.db_repo.repo_id,
1425 user=self._rhodecode_user.user_id,
1538 user=self._rhodecode_user.user_id,
1426 pull_request=pull_request,
1539 pull_request=pull_request,
1427 f_path=self.request.POST.get('f_path'),
1540 f_path=self.request.POST.get('f_path'),
1428 line_no=self.request.POST.get('line'),
1541 line_no=self.request.POST.get('line'),
1429 status_change=(ChangesetStatus.get_status_lbl(status)
1542 status_change=(ChangesetStatus.get_status_lbl(status)
1430 if status and allowed_to_change_status else None),
1543 if status and allowed_to_change_status else None),
1431 status_change_type=(status
1544 status_change_type=(status
1432 if status and allowed_to_change_status else None),
1545 if status and allowed_to_change_status else None),
1433 comment_type=comment_type,
1546 comment_type=comment_type,
1434 resolves_comment_id=resolves_comment_id,
1547 resolves_comment_id=resolves_comment_id,
1435 auth_user=self._rhodecode_user
1548 auth_user=self._rhodecode_user
1436 )
1549 )
1437
1550
1438 if allowed_to_change_status:
1551 if allowed_to_change_status:
1439 # calculate old status before we change it
1552 # calculate old status before we change it
1440 old_calculated_status = pull_request.calculated_review_status()
1553 old_calculated_status = pull_request.calculated_review_status()
1441
1554
1442 # get status if set !
1555 # get status if set !
1443 if status:
1556 if status:
1444 ChangesetStatusModel().set_status(
1557 ChangesetStatusModel().set_status(
1445 self.db_repo.repo_id,
1558 self.db_repo.repo_id,
1446 status,
1559 status,
1447 self._rhodecode_user.user_id,
1560 self._rhodecode_user.user_id,
1448 comment,
1561 comment,
1449 pull_request=pull_request
1562 pull_request=pull_request
1450 )
1563 )
1451
1564
1452 Session().flush()
1565 Session().flush()
1453 # this is somehow required to get access to some relationship
1566 # this is somehow required to get access to some relationship
1454 # loaded on comment
1567 # loaded on comment
1455 Session().refresh(comment)
1568 Session().refresh(comment)
1456
1569
1457 PullRequestModel().trigger_pull_request_hook(
1570 PullRequestModel().trigger_pull_request_hook(
1458 pull_request, self._rhodecode_user, 'comment',
1571 pull_request, self._rhodecode_user, 'comment',
1459 data={'comment': comment})
1572 data={'comment': comment})
1460
1573
1461 # we now calculate the status of pull request, and based on that
1574 # we now calculate the status of pull request, and based on that
1462 # calculation we set the commits status
1575 # calculation we set the commits status
1463 calculated_status = pull_request.calculated_review_status()
1576 calculated_status = pull_request.calculated_review_status()
1464 if old_calculated_status != calculated_status:
1577 if old_calculated_status != calculated_status:
1465 PullRequestModel().trigger_pull_request_hook(
1578 PullRequestModel().trigger_pull_request_hook(
1466 pull_request, self._rhodecode_user, 'review_status_change',
1579 pull_request, self._rhodecode_user, 'review_status_change',
1467 data={'status': calculated_status})
1580 data={'status': calculated_status})
1468
1581
1469 Session().commit()
1582 Session().commit()
1470
1583
1471 data = {
1584 data = {
1472 'target_id': h.safeid(h.safe_unicode(
1585 'target_id': h.safeid(h.safe_unicode(
1473 self.request.POST.get('f_path'))),
1586 self.request.POST.get('f_path'))),
1474 }
1587 }
1475 if comment:
1588 if comment:
1476 c.co = comment
1589 c.co = comment
1590 c.at_version_num = None
1477 rendered_comment = render(
1591 rendered_comment = render(
1478 'rhodecode:templates/changeset/changeset_comment_block.mako',
1592 'rhodecode:templates/changeset/changeset_comment_block.mako',
1479 self._get_template_context(c), self.request)
1593 self._get_template_context(c), self.request)
1480
1594
1481 data.update(comment.get_dict())
1595 data.update(comment.get_dict())
1482 data.update({'rendered_text': rendered_comment})
1596 data.update({'rendered_text': rendered_comment})
1483
1597
1484 return data
1598 return data
1485
1599
1486 @LoginRequired()
1600 @LoginRequired()
1487 @NotAnonymous()
1601 @NotAnonymous()
1488 @HasRepoPermissionAnyDecorator(
1602 @HasRepoPermissionAnyDecorator(
1489 'repository.read', 'repository.write', 'repository.admin')
1603 'repository.read', 'repository.write', 'repository.admin')
1490 @CSRFRequired()
1604 @CSRFRequired()
1491 @view_config(
1605 @view_config(
1492 route_name='pullrequest_comment_delete', request_method='POST',
1606 route_name='pullrequest_comment_delete', request_method='POST',
1493 renderer='json_ext')
1607 renderer='json_ext')
1494 def pull_request_comment_delete(self):
1608 def pull_request_comment_delete(self):
1495 pull_request = PullRequest.get_or_404(
1609 pull_request = PullRequest.get_or_404(
1496 self.request.matchdict['pull_request_id'])
1610 self.request.matchdict['pull_request_id'])
1497
1611
1498 comment = ChangesetComment.get_or_404(
1612 comment = ChangesetComment.get_or_404(
1499 self.request.matchdict['comment_id'])
1613 self.request.matchdict['comment_id'])
1500 comment_id = comment.comment_id
1614 comment_id = comment.comment_id
1501
1615
1502 if comment.immutable:
1616 if comment.immutable:
1503 # don't allow deleting comments that are immutable
1617 # don't allow deleting comments that are immutable
1504 raise HTTPForbidden()
1618 raise HTTPForbidden()
1505
1619
1506 if pull_request.is_closed():
1620 if pull_request.is_closed():
1507 log.debug('comment: forbidden because pull request is closed')
1621 log.debug('comment: forbidden because pull request is closed')
1508 raise HTTPForbidden()
1622 raise HTTPForbidden()
1509
1623
1510 if not comment:
1624 if not comment:
1511 log.debug('Comment with id:%s not found, skipping', comment_id)
1625 log.debug('Comment with id:%s not found, skipping', comment_id)
1512 # comment already deleted in another call probably
1626 # comment already deleted in another call probably
1513 return True
1627 return True
1514
1628
1515 if comment.pull_request.is_closed():
1629 if comment.pull_request.is_closed():
1516 # don't allow deleting comments on closed pull request
1630 # don't allow deleting comments on closed pull request
1517 raise HTTPForbidden()
1631 raise HTTPForbidden()
1518
1632
1519 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1633 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1520 super_admin = h.HasPermissionAny('hg.admin')()
1634 super_admin = h.HasPermissionAny('hg.admin')()
1521 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1635 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1522 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1636 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1523 comment_repo_admin = is_repo_admin and is_repo_comment
1637 comment_repo_admin = is_repo_admin and is_repo_comment
1524
1638
1525 if super_admin or comment_owner or comment_repo_admin:
1639 if super_admin or comment_owner or comment_repo_admin:
1526 old_calculated_status = comment.pull_request.calculated_review_status()
1640 old_calculated_status = comment.pull_request.calculated_review_status()
1527 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1641 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1528 Session().commit()
1642 Session().commit()
1529 calculated_status = comment.pull_request.calculated_review_status()
1643 calculated_status = comment.pull_request.calculated_review_status()
1530 if old_calculated_status != calculated_status:
1644 if old_calculated_status != calculated_status:
1531 PullRequestModel().trigger_pull_request_hook(
1645 PullRequestModel().trigger_pull_request_hook(
1532 comment.pull_request, self._rhodecode_user, 'review_status_change',
1646 comment.pull_request, self._rhodecode_user, 'review_status_change',
1533 data={'status': calculated_status})
1647 data={'status': calculated_status})
1534 return True
1648 return True
1535 else:
1649 else:
1536 log.warning('No permissions for user %s to delete comment_id: %s',
1650 log.warning('No permissions for user %s to delete comment_id: %s',
1537 self._rhodecode_db_user, comment_id)
1651 self._rhodecode_db_user, comment_id)
1538 raise HTTPNotFound()
1652 raise HTTPNotFound()
1539
1653
1540 @LoginRequired()
1654 @LoginRequired()
1541 @NotAnonymous()
1655 @NotAnonymous()
1542 @HasRepoPermissionAnyDecorator(
1656 @HasRepoPermissionAnyDecorator(
1543 'repository.read', 'repository.write', 'repository.admin')
1657 'repository.read', 'repository.write', 'repository.admin')
1544 @CSRFRequired()
1658 @CSRFRequired()
1545 @view_config(
1659 @view_config(
1546 route_name='pullrequest_comment_edit', request_method='POST',
1660 route_name='pullrequest_comment_edit', request_method='POST',
1547 renderer='json_ext')
1661 renderer='json_ext')
1548 def pull_request_comment_edit(self):
1662 def pull_request_comment_edit(self):
1549 self.load_default_context()
1663 self.load_default_context()
1550
1664
1551 pull_request = PullRequest.get_or_404(
1665 pull_request = PullRequest.get_or_404(
1552 self.request.matchdict['pull_request_id']
1666 self.request.matchdict['pull_request_id']
1553 )
1667 )
1554 comment = ChangesetComment.get_or_404(
1668 comment = ChangesetComment.get_or_404(
1555 self.request.matchdict['comment_id']
1669 self.request.matchdict['comment_id']
1556 )
1670 )
1557 comment_id = comment.comment_id
1671 comment_id = comment.comment_id
1558
1672
1559 if comment.immutable:
1673 if comment.immutable:
1560 # don't allow deleting comments that are immutable
1674 # don't allow deleting comments that are immutable
1561 raise HTTPForbidden()
1675 raise HTTPForbidden()
1562
1676
1563 if pull_request.is_closed():
1677 if pull_request.is_closed():
1564 log.debug('comment: forbidden because pull request is closed')
1678 log.debug('comment: forbidden because pull request is closed')
1565 raise HTTPForbidden()
1679 raise HTTPForbidden()
1566
1680
1567 if not comment:
1681 if not comment:
1568 log.debug('Comment with id:%s not found, skipping', comment_id)
1682 log.debug('Comment with id:%s not found, skipping', comment_id)
1569 # comment already deleted in another call probably
1683 # comment already deleted in another call probably
1570 return True
1684 return True
1571
1685
1572 if comment.pull_request.is_closed():
1686 if comment.pull_request.is_closed():
1573 # don't allow deleting comments on closed pull request
1687 # don't allow deleting comments on closed pull request
1574 raise HTTPForbidden()
1688 raise HTTPForbidden()
1575
1689
1576 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1690 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1577 super_admin = h.HasPermissionAny('hg.admin')()
1691 super_admin = h.HasPermissionAny('hg.admin')()
1578 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1692 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1579 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1693 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1580 comment_repo_admin = is_repo_admin and is_repo_comment
1694 comment_repo_admin = is_repo_admin and is_repo_comment
1581
1695
1582 if super_admin or comment_owner or comment_repo_admin:
1696 if super_admin or comment_owner or comment_repo_admin:
1583 text = self.request.POST.get('text')
1697 text = self.request.POST.get('text')
1584 version = self.request.POST.get('version')
1698 version = self.request.POST.get('version')
1585 if text == comment.text:
1699 if text == comment.text:
1586 log.warning(
1700 log.warning(
1587 'Comment(PR): '
1701 'Comment(PR): '
1588 'Trying to create new version '
1702 'Trying to create new version '
1589 'with the same comment body {}'.format(
1703 'with the same comment body {}'.format(
1590 comment_id,
1704 comment_id,
1591 )
1705 )
1592 )
1706 )
1593 raise HTTPNotFound()
1707 raise HTTPNotFound()
1594
1708
1595 if version.isdigit():
1709 if version.isdigit():
1596 version = int(version)
1710 version = int(version)
1597 else:
1711 else:
1598 log.warning(
1712 log.warning(
1599 'Comment(PR): Wrong version type {} {} '
1713 'Comment(PR): Wrong version type {} {} '
1600 'for comment {}'.format(
1714 'for comment {}'.format(
1601 version,
1715 version,
1602 type(version),
1716 type(version),
1603 comment_id,
1717 comment_id,
1604 )
1718 )
1605 )
1719 )
1606 raise HTTPNotFound()
1720 raise HTTPNotFound()
1607
1721
1608 try:
1722 try:
1609 comment_history = CommentsModel().edit(
1723 comment_history = CommentsModel().edit(
1610 comment_id=comment_id,
1724 comment_id=comment_id,
1611 text=text,
1725 text=text,
1612 auth_user=self._rhodecode_user,
1726 auth_user=self._rhodecode_user,
1613 version=version,
1727 version=version,
1614 )
1728 )
1615 except CommentVersionMismatch:
1729 except CommentVersionMismatch:
1616 raise HTTPConflict()
1730 raise HTTPConflict()
1617
1731
1618 if not comment_history:
1732 if not comment_history:
1619 raise HTTPNotFound()
1733 raise HTTPNotFound()
1620
1734
1621 Session().commit()
1735 Session().commit()
1622
1736
1623 PullRequestModel().trigger_pull_request_hook(
1737 PullRequestModel().trigger_pull_request_hook(
1624 pull_request, self._rhodecode_user, 'comment_edit',
1738 pull_request, self._rhodecode_user, 'comment_edit',
1625 data={'comment': comment})
1739 data={'comment': comment})
1626
1740
1627 return {
1741 return {
1628 'comment_history_id': comment_history.comment_history_id,
1742 'comment_history_id': comment_history.comment_history_id,
1629 'comment_id': comment.comment_id,
1743 'comment_id': comment.comment_id,
1630 'comment_version': comment_history.version,
1744 'comment_version': comment_history.version,
1631 'comment_author_username': comment_history.author.username,
1745 'comment_author_username': comment_history.author.username,
1632 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1746 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1633 'comment_created_on': h.age_component(comment_history.created_on,
1747 'comment_created_on': h.age_component(comment_history.created_on,
1634 time_is_local=True),
1748 time_is_local=True),
1635 }
1749 }
1636 else:
1750 else:
1637 log.warning('No permissions for user %s to edit comment_id: %s',
1751 log.warning('No permissions for user %s to edit comment_id: %s',
1638 self._rhodecode_db_user, comment_id)
1752 self._rhodecode_db_user, comment_id)
1639 raise HTTPNotFound()
1753 raise HTTPNotFound()
@@ -1,2092 +1,2106 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27 import base64
27 import base64
28
28
29 import os
29 import os
30 import random
30 import random
31 import hashlib
31 import hashlib
32 import StringIO
32 import StringIO
33 import textwrap
33 import textwrap
34 import urllib
34 import urllib
35 import math
35 import math
36 import logging
36 import logging
37 import re
37 import re
38 import time
38 import time
39 import string
39 import string
40 import hashlib
40 import hashlib
41 from collections import OrderedDict
41 from collections import OrderedDict
42
42
43 import pygments
43 import pygments
44 import itertools
44 import itertools
45 import fnmatch
45 import fnmatch
46 import bleach
46 import bleach
47
47
48 from pyramid import compat
48 from pyramid import compat
49 from datetime import datetime
49 from datetime import datetime
50 from functools import partial
50 from functools import partial
51 from pygments.formatters.html import HtmlFormatter
51 from pygments.formatters.html import HtmlFormatter
52 from pygments.lexers import (
52 from pygments.lexers import (
53 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
54
54
55 from pyramid.threadlocal import get_current_request
55 from pyramid.threadlocal import get_current_request
56 from tempita import looper
56 from tempita import looper
57 from webhelpers2.html import literal, HTML, escape
57 from webhelpers2.html import literal, HTML, escape
58 from webhelpers2.html._autolink import _auto_link_urls
58 from webhelpers2.html._autolink import _auto_link_urls
59 from webhelpers2.html.tools import (
59 from webhelpers2.html.tools import (
60 button_to, highlight, js_obfuscate, strip_links, strip_tags)
60 button_to, highlight, js_obfuscate, strip_links, strip_tags)
61
61
62 from webhelpers2.text import (
62 from webhelpers2.text import (
63 chop_at, collapse, convert_accented_entities,
63 chop_at, collapse, convert_accented_entities,
64 convert_misc_entities, lchop, plural, rchop, remove_formatting,
64 convert_misc_entities, lchop, plural, rchop, remove_formatting,
65 replace_whitespace, urlify, truncate, wrap_paragraphs)
65 replace_whitespace, urlify, truncate, wrap_paragraphs)
66 from webhelpers2.date import time_ago_in_words
66 from webhelpers2.date import time_ago_in_words
67
67
68 from webhelpers2.html.tags import (
68 from webhelpers2.html.tags import (
69 _input, NotGiven, _make_safe_id_component as safeid,
69 _input, NotGiven, _make_safe_id_component as safeid,
70 form as insecure_form,
70 form as insecure_form,
71 auto_discovery_link, checkbox, end_form, file,
71 auto_discovery_link, checkbox, end_form, file,
72 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
72 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
73 select as raw_select, stylesheet_link, submit, text, password, textarea,
73 select as raw_select, stylesheet_link, submit, text, password, textarea,
74 ul, radio, Options)
74 ul, radio, Options)
75
75
76 from webhelpers2.number import format_byte_size
76 from webhelpers2.number import format_byte_size
77
77
78 from rhodecode.lib.action_parser import action_parser
78 from rhodecode.lib.action_parser import action_parser
79 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
79 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
80 from rhodecode.lib.ext_json import json
80 from rhodecode.lib.ext_json import json
81 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
81 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
82 from rhodecode.lib.utils2 import (
82 from rhodecode.lib.utils2 import (
83 str2bool, safe_unicode, safe_str,
83 str2bool, safe_unicode, safe_str,
84 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
84 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
85 AttributeDict, safe_int, md5, md5_safe, get_host_info)
85 AttributeDict, safe_int, md5, md5_safe, get_host_info)
86 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
86 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
87 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
87 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
88 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
88 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
89 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
89 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
90 from rhodecode.lib.index.search_utils import get_matching_line_offsets
90 from rhodecode.lib.index.search_utils import get_matching_line_offsets
91 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
91 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
92 from rhodecode.model.changeset_status import ChangesetStatusModel
92 from rhodecode.model.changeset_status import ChangesetStatusModel
93 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
93 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
94 from rhodecode.model.repo_group import RepoGroupModel
94 from rhodecode.model.repo_group import RepoGroupModel
95 from rhodecode.model.settings import IssueTrackerSettingsModel
95 from rhodecode.model.settings import IssueTrackerSettingsModel
96
96
97
97
98 log = logging.getLogger(__name__)
98 log = logging.getLogger(__name__)
99
99
100
100
101 DEFAULT_USER = User.DEFAULT_USER
101 DEFAULT_USER = User.DEFAULT_USER
102 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
102 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
103
103
104
104
105 def asset(path, ver=None, **kwargs):
105 def asset(path, ver=None, **kwargs):
106 """
106 """
107 Helper to generate a static asset file path for rhodecode assets
107 Helper to generate a static asset file path for rhodecode assets
108
108
109 eg. h.asset('images/image.png', ver='3923')
109 eg. h.asset('images/image.png', ver='3923')
110
110
111 :param path: path of asset
111 :param path: path of asset
112 :param ver: optional version query param to append as ?ver=
112 :param ver: optional version query param to append as ?ver=
113 """
113 """
114 request = get_current_request()
114 request = get_current_request()
115 query = {}
115 query = {}
116 query.update(kwargs)
116 query.update(kwargs)
117 if ver:
117 if ver:
118 query = {'ver': ver}
118 query = {'ver': ver}
119 return request.static_path(
119 return request.static_path(
120 'rhodecode:public/{}'.format(path), _query=query)
120 'rhodecode:public/{}'.format(path), _query=query)
121
121
122
122
123 default_html_escape_table = {
123 default_html_escape_table = {
124 ord('&'): u'&amp;',
124 ord('&'): u'&amp;',
125 ord('<'): u'&lt;',
125 ord('<'): u'&lt;',
126 ord('>'): u'&gt;',
126 ord('>'): u'&gt;',
127 ord('"'): u'&quot;',
127 ord('"'): u'&quot;',
128 ord("'"): u'&#39;',
128 ord("'"): u'&#39;',
129 }
129 }
130
130
131
131
132 def html_escape(text, html_escape_table=default_html_escape_table):
132 def html_escape(text, html_escape_table=default_html_escape_table):
133 """Produce entities within text."""
133 """Produce entities within text."""
134 return text.translate(html_escape_table)
134 return text.translate(html_escape_table)
135
135
136
136
137 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
137 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
138 """
138 """
139 Truncate string ``s`` at the first occurrence of ``sub``.
139 Truncate string ``s`` at the first occurrence of ``sub``.
140
140
141 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
141 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
142 """
142 """
143 suffix_if_chopped = suffix_if_chopped or ''
143 suffix_if_chopped = suffix_if_chopped or ''
144 pos = s.find(sub)
144 pos = s.find(sub)
145 if pos == -1:
145 if pos == -1:
146 return s
146 return s
147
147
148 if inclusive:
148 if inclusive:
149 pos += len(sub)
149 pos += len(sub)
150
150
151 chopped = s[:pos]
151 chopped = s[:pos]
152 left = s[pos:].strip()
152 left = s[pos:].strip()
153
153
154 if left and suffix_if_chopped:
154 if left and suffix_if_chopped:
155 chopped += suffix_if_chopped
155 chopped += suffix_if_chopped
156
156
157 return chopped
157 return chopped
158
158
159
159
160 def shorter(text, size=20, prefix=False):
160 def shorter(text, size=20, prefix=False):
161 postfix = '...'
161 postfix = '...'
162 if len(text) > size:
162 if len(text) > size:
163 if prefix:
163 if prefix:
164 # shorten in front
164 # shorten in front
165 return postfix + text[-(size - len(postfix)):]
165 return postfix + text[-(size - len(postfix)):]
166 else:
166 else:
167 return text[:size - len(postfix)] + postfix
167 return text[:size - len(postfix)] + postfix
168 return text
168 return text
169
169
170
170
171 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
171 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
172 """
172 """
173 Reset button
173 Reset button
174 """
174 """
175 return _input(type, name, value, id, attrs)
175 return _input(type, name, value, id, attrs)
176
176
177
177
178 def select(name, selected_values, options, id=NotGiven, **attrs):
178 def select(name, selected_values, options, id=NotGiven, **attrs):
179
179
180 if isinstance(options, (list, tuple)):
180 if isinstance(options, (list, tuple)):
181 options_iter = options
181 options_iter = options
182 # Handle old value,label lists ... where value also can be value,label lists
182 # Handle old value,label lists ... where value also can be value,label lists
183 options = Options()
183 options = Options()
184 for opt in options_iter:
184 for opt in options_iter:
185 if isinstance(opt, tuple) and len(opt) == 2:
185 if isinstance(opt, tuple) and len(opt) == 2:
186 value, label = opt
186 value, label = opt
187 elif isinstance(opt, basestring):
187 elif isinstance(opt, basestring):
188 value = label = opt
188 value = label = opt
189 else:
189 else:
190 raise ValueError('invalid select option type %r' % type(opt))
190 raise ValueError('invalid select option type %r' % type(opt))
191
191
192 if isinstance(value, (list, tuple)):
192 if isinstance(value, (list, tuple)):
193 option_group = options.add_optgroup(label)
193 option_group = options.add_optgroup(label)
194 for opt2 in value:
194 for opt2 in value:
195 if isinstance(opt2, tuple) and len(opt2) == 2:
195 if isinstance(opt2, tuple) and len(opt2) == 2:
196 group_value, group_label = opt2
196 group_value, group_label = opt2
197 elif isinstance(opt2, basestring):
197 elif isinstance(opt2, basestring):
198 group_value = group_label = opt2
198 group_value = group_label = opt2
199 else:
199 else:
200 raise ValueError('invalid select option type %r' % type(opt2))
200 raise ValueError('invalid select option type %r' % type(opt2))
201
201
202 option_group.add_option(group_label, group_value)
202 option_group.add_option(group_label, group_value)
203 else:
203 else:
204 options.add_option(label, value)
204 options.add_option(label, value)
205
205
206 return raw_select(name, selected_values, options, id=id, **attrs)
206 return raw_select(name, selected_values, options, id=id, **attrs)
207
207
208
208
209 def branding(name, length=40):
209 def branding(name, length=40):
210 return truncate(name, length, indicator="")
210 return truncate(name, length, indicator="")
211
211
212
212
213 def FID(raw_id, path):
213 def FID(raw_id, path):
214 """
214 """
215 Creates a unique ID for filenode based on it's hash of path and commit
215 Creates a unique ID for filenode based on it's hash of path and commit
216 it's safe to use in urls
216 it's safe to use in urls
217
217
218 :param raw_id:
218 :param raw_id:
219 :param path:
219 :param path:
220 """
220 """
221
221
222 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
222 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
223
223
224
224
225 class _GetError(object):
225 class _GetError(object):
226 """Get error from form_errors, and represent it as span wrapped error
226 """Get error from form_errors, and represent it as span wrapped error
227 message
227 message
228
228
229 :param field_name: field to fetch errors for
229 :param field_name: field to fetch errors for
230 :param form_errors: form errors dict
230 :param form_errors: form errors dict
231 """
231 """
232
232
233 def __call__(self, field_name, form_errors):
233 def __call__(self, field_name, form_errors):
234 tmpl = """<span class="error_msg">%s</span>"""
234 tmpl = """<span class="error_msg">%s</span>"""
235 if form_errors and field_name in form_errors:
235 if form_errors and field_name in form_errors:
236 return literal(tmpl % form_errors.get(field_name))
236 return literal(tmpl % form_errors.get(field_name))
237
237
238
238
239 get_error = _GetError()
239 get_error = _GetError()
240
240
241
241
242 class _ToolTip(object):
242 class _ToolTip(object):
243
243
244 def __call__(self, tooltip_title, trim_at=50):
244 def __call__(self, tooltip_title, trim_at=50):
245 """
245 """
246 Special function just to wrap our text into nice formatted
246 Special function just to wrap our text into nice formatted
247 autowrapped text
247 autowrapped text
248
248
249 :param tooltip_title:
249 :param tooltip_title:
250 """
250 """
251 tooltip_title = escape(tooltip_title)
251 tooltip_title = escape(tooltip_title)
252 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
252 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
253 return tooltip_title
253 return tooltip_title
254
254
255
255
256 tooltip = _ToolTip()
256 tooltip = _ToolTip()
257
257
258 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
258 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
259
259
260
260
261 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
261 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
262 limit_items=False, linkify_last_item=False, hide_last_item=False,
262 limit_items=False, linkify_last_item=False, hide_last_item=False,
263 copy_path_icon=True):
263 copy_path_icon=True):
264 if isinstance(file_path, str):
264 if isinstance(file_path, str):
265 file_path = safe_unicode(file_path)
265 file_path = safe_unicode(file_path)
266
266
267 if at_ref:
267 if at_ref:
268 route_qry = {'at': at_ref}
268 route_qry = {'at': at_ref}
269 default_landing_ref = at_ref or landing_ref_name or commit_id
269 default_landing_ref = at_ref or landing_ref_name or commit_id
270 else:
270 else:
271 route_qry = None
271 route_qry = None
272 default_landing_ref = commit_id
272 default_landing_ref = commit_id
273
273
274 # first segment is a `HOME` link to repo files root location
274 # first segment is a `HOME` link to repo files root location
275 root_name = literal(u'<i class="icon-home"></i>')
275 root_name = literal(u'<i class="icon-home"></i>')
276
276
277 url_segments = [
277 url_segments = [
278 link_to(
278 link_to(
279 root_name,
279 root_name,
280 repo_files_by_ref_url(
280 repo_files_by_ref_url(
281 repo_name,
281 repo_name,
282 repo_type,
282 repo_type,
283 f_path=None, # None here is a special case for SVN repos,
283 f_path=None, # None here is a special case for SVN repos,
284 # that won't prefix with a ref
284 # that won't prefix with a ref
285 ref_name=default_landing_ref,
285 ref_name=default_landing_ref,
286 commit_id=commit_id,
286 commit_id=commit_id,
287 query=route_qry
287 query=route_qry
288 )
288 )
289 )]
289 )]
290
290
291 path_segments = file_path.split('/')
291 path_segments = file_path.split('/')
292 last_cnt = len(path_segments) - 1
292 last_cnt = len(path_segments) - 1
293 for cnt, segment in enumerate(path_segments):
293 for cnt, segment in enumerate(path_segments):
294 if not segment:
294 if not segment:
295 continue
295 continue
296 segment_html = escape(segment)
296 segment_html = escape(segment)
297
297
298 last_item = cnt == last_cnt
298 last_item = cnt == last_cnt
299
299
300 if last_item and hide_last_item:
300 if last_item and hide_last_item:
301 # iterate over and hide last element
301 # iterate over and hide last element
302 continue
302 continue
303
303
304 if last_item and linkify_last_item is False:
304 if last_item and linkify_last_item is False:
305 # plain version
305 # plain version
306 url_segments.append(segment_html)
306 url_segments.append(segment_html)
307 else:
307 else:
308 url_segments.append(
308 url_segments.append(
309 link_to(
309 link_to(
310 segment_html,
310 segment_html,
311 repo_files_by_ref_url(
311 repo_files_by_ref_url(
312 repo_name,
312 repo_name,
313 repo_type,
313 repo_type,
314 f_path='/'.join(path_segments[:cnt + 1]),
314 f_path='/'.join(path_segments[:cnt + 1]),
315 ref_name=default_landing_ref,
315 ref_name=default_landing_ref,
316 commit_id=commit_id,
316 commit_id=commit_id,
317 query=route_qry
317 query=route_qry
318 ),
318 ),
319 ))
319 ))
320
320
321 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
321 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
322 if limit_items and len(limited_url_segments) < len(url_segments):
322 if limit_items and len(limited_url_segments) < len(url_segments):
323 url_segments = limited_url_segments
323 url_segments = limited_url_segments
324
324
325 full_path = file_path
325 full_path = file_path
326 if copy_path_icon:
326 if copy_path_icon:
327 icon = files_icon.format(escape(full_path))
327 icon = files_icon.format(escape(full_path))
328 else:
328 else:
329 icon = ''
329 icon = ''
330
330
331 if file_path == '':
331 if file_path == '':
332 return root_name
332 return root_name
333 else:
333 else:
334 return literal(' / '.join(url_segments) + icon)
334 return literal(' / '.join(url_segments) + icon)
335
335
336
336
337 def files_url_data(request):
337 def files_url_data(request):
338 matchdict = request.matchdict
338 matchdict = request.matchdict
339
339
340 if 'f_path' not in matchdict:
340 if 'f_path' not in matchdict:
341 matchdict['f_path'] = ''
341 matchdict['f_path'] = ''
342
342
343 if 'commit_id' not in matchdict:
343 if 'commit_id' not in matchdict:
344 matchdict['commit_id'] = 'tip'
344 matchdict['commit_id'] = 'tip'
345
345
346 return json.dumps(matchdict)
346 return json.dumps(matchdict)
347
347
348
348
349 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
349 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
350 _is_svn = is_svn(db_repo_type)
350 _is_svn = is_svn(db_repo_type)
351 final_f_path = f_path
351 final_f_path = f_path
352
352
353 if _is_svn:
353 if _is_svn:
354 """
354 """
355 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
355 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
356 actually commit_id followed by the ref_name. This should be done only in case
356 actually commit_id followed by the ref_name. This should be done only in case
357 This is a initial landing url, without additional paths.
357 This is a initial landing url, without additional paths.
358
358
359 like: /1000/tags/1.0.0/?at=tags/1.0.0
359 like: /1000/tags/1.0.0/?at=tags/1.0.0
360 """
360 """
361
361
362 if ref_name and ref_name != 'tip':
362 if ref_name and ref_name != 'tip':
363 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
363 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
364 # for SVN we only do this magic prefix if it's root, .eg landing revision
364 # for SVN we only do this magic prefix if it's root, .eg landing revision
365 # of files link. If we are in the tree we don't need this since we traverse the url
365 # of files link. If we are in the tree we don't need this since we traverse the url
366 # that has everything stored
366 # that has everything stored
367 if f_path in ['', '/']:
367 if f_path in ['', '/']:
368 final_f_path = '/'.join([ref_name, f_path])
368 final_f_path = '/'.join([ref_name, f_path])
369
369
370 # SVN always needs a commit_id explicitly, without a named REF
370 # SVN always needs a commit_id explicitly, without a named REF
371 default_commit_id = commit_id
371 default_commit_id = commit_id
372 else:
372 else:
373 """
373 """
374 For git and mercurial we construct a new URL using the names instead of commit_id
374 For git and mercurial we construct a new URL using the names instead of commit_id
375 like: /master/some_path?at=master
375 like: /master/some_path?at=master
376 """
376 """
377 # We currently do not support branches with slashes
377 # We currently do not support branches with slashes
378 if '/' in ref_name:
378 if '/' in ref_name:
379 default_commit_id = commit_id
379 default_commit_id = commit_id
380 else:
380 else:
381 default_commit_id = ref_name
381 default_commit_id = ref_name
382
382
383 # sometimes we pass f_path as None, to indicate explicit no prefix,
383 # sometimes we pass f_path as None, to indicate explicit no prefix,
384 # we translate it to string to not have None
384 # we translate it to string to not have None
385 final_f_path = final_f_path or ''
385 final_f_path = final_f_path or ''
386
386
387 files_url = route_path(
387 files_url = route_path(
388 'repo_files',
388 'repo_files',
389 repo_name=db_repo_name,
389 repo_name=db_repo_name,
390 commit_id=default_commit_id,
390 commit_id=default_commit_id,
391 f_path=final_f_path,
391 f_path=final_f_path,
392 _query=query
392 _query=query
393 )
393 )
394 return files_url
394 return files_url
395
395
396
396
397 def code_highlight(code, lexer, formatter, use_hl_filter=False):
397 def code_highlight(code, lexer, formatter, use_hl_filter=False):
398 """
398 """
399 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
399 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
400
400
401 If ``outfile`` is given and a valid file object (an object
401 If ``outfile`` is given and a valid file object (an object
402 with a ``write`` method), the result will be written to it, otherwise
402 with a ``write`` method), the result will be written to it, otherwise
403 it is returned as a string.
403 it is returned as a string.
404 """
404 """
405 if use_hl_filter:
405 if use_hl_filter:
406 # add HL filter
406 # add HL filter
407 from rhodecode.lib.index import search_utils
407 from rhodecode.lib.index import search_utils
408 lexer.add_filter(search_utils.ElasticSearchHLFilter())
408 lexer.add_filter(search_utils.ElasticSearchHLFilter())
409 return pygments.format(pygments.lex(code, lexer), formatter)
409 return pygments.format(pygments.lex(code, lexer), formatter)
410
410
411
411
412 class CodeHtmlFormatter(HtmlFormatter):
412 class CodeHtmlFormatter(HtmlFormatter):
413 """
413 """
414 My code Html Formatter for source codes
414 My code Html Formatter for source codes
415 """
415 """
416
416
417 def wrap(self, source, outfile):
417 def wrap(self, source, outfile):
418 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
418 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
419
419
420 def _wrap_code(self, source):
420 def _wrap_code(self, source):
421 for cnt, it in enumerate(source):
421 for cnt, it in enumerate(source):
422 i, t = it
422 i, t = it
423 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
423 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
424 yield i, t
424 yield i, t
425
425
426 def _wrap_tablelinenos(self, inner):
426 def _wrap_tablelinenos(self, inner):
427 dummyoutfile = StringIO.StringIO()
427 dummyoutfile = StringIO.StringIO()
428 lncount = 0
428 lncount = 0
429 for t, line in inner:
429 for t, line in inner:
430 if t:
430 if t:
431 lncount += 1
431 lncount += 1
432 dummyoutfile.write(line)
432 dummyoutfile.write(line)
433
433
434 fl = self.linenostart
434 fl = self.linenostart
435 mw = len(str(lncount + fl - 1))
435 mw = len(str(lncount + fl - 1))
436 sp = self.linenospecial
436 sp = self.linenospecial
437 st = self.linenostep
437 st = self.linenostep
438 la = self.lineanchors
438 la = self.lineanchors
439 aln = self.anchorlinenos
439 aln = self.anchorlinenos
440 nocls = self.noclasses
440 nocls = self.noclasses
441 if sp:
441 if sp:
442 lines = []
442 lines = []
443
443
444 for i in range(fl, fl + lncount):
444 for i in range(fl, fl + lncount):
445 if i % st == 0:
445 if i % st == 0:
446 if i % sp == 0:
446 if i % sp == 0:
447 if aln:
447 if aln:
448 lines.append('<a href="#%s%d" class="special">%*d</a>' %
448 lines.append('<a href="#%s%d" class="special">%*d</a>' %
449 (la, i, mw, i))
449 (la, i, mw, i))
450 else:
450 else:
451 lines.append('<span class="special">%*d</span>' % (mw, i))
451 lines.append('<span class="special">%*d</span>' % (mw, i))
452 else:
452 else:
453 if aln:
453 if aln:
454 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
454 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
455 else:
455 else:
456 lines.append('%*d' % (mw, i))
456 lines.append('%*d' % (mw, i))
457 else:
457 else:
458 lines.append('')
458 lines.append('')
459 ls = '\n'.join(lines)
459 ls = '\n'.join(lines)
460 else:
460 else:
461 lines = []
461 lines = []
462 for i in range(fl, fl + lncount):
462 for i in range(fl, fl + lncount):
463 if i % st == 0:
463 if i % st == 0:
464 if aln:
464 if aln:
465 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
465 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
466 else:
466 else:
467 lines.append('%*d' % (mw, i))
467 lines.append('%*d' % (mw, i))
468 else:
468 else:
469 lines.append('')
469 lines.append('')
470 ls = '\n'.join(lines)
470 ls = '\n'.join(lines)
471
471
472 # in case you wonder about the seemingly redundant <div> here: since the
472 # in case you wonder about the seemingly redundant <div> here: since the
473 # content in the other cell also is wrapped in a div, some browsers in
473 # content in the other cell also is wrapped in a div, some browsers in
474 # some configurations seem to mess up the formatting...
474 # some configurations seem to mess up the formatting...
475 if nocls:
475 if nocls:
476 yield 0, ('<table class="%stable">' % self.cssclass +
476 yield 0, ('<table class="%stable">' % self.cssclass +
477 '<tr><td><div class="linenodiv" '
477 '<tr><td><div class="linenodiv" '
478 'style="background-color: #f0f0f0; padding-right: 10px">'
478 'style="background-color: #f0f0f0; padding-right: 10px">'
479 '<pre style="line-height: 125%">' +
479 '<pre style="line-height: 125%">' +
480 ls + '</pre></div></td><td id="hlcode" class="code">')
480 ls + '</pre></div></td><td id="hlcode" class="code">')
481 else:
481 else:
482 yield 0, ('<table class="%stable">' % self.cssclass +
482 yield 0, ('<table class="%stable">' % self.cssclass +
483 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
483 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
484 ls + '</pre></div></td><td id="hlcode" class="code">')
484 ls + '</pre></div></td><td id="hlcode" class="code">')
485 yield 0, dummyoutfile.getvalue()
485 yield 0, dummyoutfile.getvalue()
486 yield 0, '</td></tr></table>'
486 yield 0, '</td></tr></table>'
487
487
488
488
489 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
489 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
490 def __init__(self, **kw):
490 def __init__(self, **kw):
491 # only show these line numbers if set
491 # only show these line numbers if set
492 self.only_lines = kw.pop('only_line_numbers', [])
492 self.only_lines = kw.pop('only_line_numbers', [])
493 self.query_terms = kw.pop('query_terms', [])
493 self.query_terms = kw.pop('query_terms', [])
494 self.max_lines = kw.pop('max_lines', 5)
494 self.max_lines = kw.pop('max_lines', 5)
495 self.line_context = kw.pop('line_context', 3)
495 self.line_context = kw.pop('line_context', 3)
496 self.url = kw.pop('url', None)
496 self.url = kw.pop('url', None)
497
497
498 super(CodeHtmlFormatter, self).__init__(**kw)
498 super(CodeHtmlFormatter, self).__init__(**kw)
499
499
500 def _wrap_code(self, source):
500 def _wrap_code(self, source):
501 for cnt, it in enumerate(source):
501 for cnt, it in enumerate(source):
502 i, t = it
502 i, t = it
503 t = '<pre>%s</pre>' % t
503 t = '<pre>%s</pre>' % t
504 yield i, t
504 yield i, t
505
505
506 def _wrap_tablelinenos(self, inner):
506 def _wrap_tablelinenos(self, inner):
507 yield 0, '<table class="code-highlight %stable">' % self.cssclass
507 yield 0, '<table class="code-highlight %stable">' % self.cssclass
508
508
509 last_shown_line_number = 0
509 last_shown_line_number = 0
510 current_line_number = 1
510 current_line_number = 1
511
511
512 for t, line in inner:
512 for t, line in inner:
513 if not t:
513 if not t:
514 yield t, line
514 yield t, line
515 continue
515 continue
516
516
517 if current_line_number in self.only_lines:
517 if current_line_number in self.only_lines:
518 if last_shown_line_number + 1 != current_line_number:
518 if last_shown_line_number + 1 != current_line_number:
519 yield 0, '<tr>'
519 yield 0, '<tr>'
520 yield 0, '<td class="line">...</td>'
520 yield 0, '<td class="line">...</td>'
521 yield 0, '<td id="hlcode" class="code"></td>'
521 yield 0, '<td id="hlcode" class="code"></td>'
522 yield 0, '</tr>'
522 yield 0, '</tr>'
523
523
524 yield 0, '<tr>'
524 yield 0, '<tr>'
525 if self.url:
525 if self.url:
526 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
526 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
527 self.url, current_line_number, current_line_number)
527 self.url, current_line_number, current_line_number)
528 else:
528 else:
529 yield 0, '<td class="line"><a href="">%i</a></td>' % (
529 yield 0, '<td class="line"><a href="">%i</a></td>' % (
530 current_line_number)
530 current_line_number)
531 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
531 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
532 yield 0, '</tr>'
532 yield 0, '</tr>'
533
533
534 last_shown_line_number = current_line_number
534 last_shown_line_number = current_line_number
535
535
536 current_line_number += 1
536 current_line_number += 1
537
537
538 yield 0, '</table>'
538 yield 0, '</table>'
539
539
540
540
541 def hsv_to_rgb(h, s, v):
541 def hsv_to_rgb(h, s, v):
542 """ Convert hsv color values to rgb """
542 """ Convert hsv color values to rgb """
543
543
544 if s == 0.0:
544 if s == 0.0:
545 return v, v, v
545 return v, v, v
546 i = int(h * 6.0) # XXX assume int() truncates!
546 i = int(h * 6.0) # XXX assume int() truncates!
547 f = (h * 6.0) - i
547 f = (h * 6.0) - i
548 p = v * (1.0 - s)
548 p = v * (1.0 - s)
549 q = v * (1.0 - s * f)
549 q = v * (1.0 - s * f)
550 t = v * (1.0 - s * (1.0 - f))
550 t = v * (1.0 - s * (1.0 - f))
551 i = i % 6
551 i = i % 6
552 if i == 0:
552 if i == 0:
553 return v, t, p
553 return v, t, p
554 if i == 1:
554 if i == 1:
555 return q, v, p
555 return q, v, p
556 if i == 2:
556 if i == 2:
557 return p, v, t
557 return p, v, t
558 if i == 3:
558 if i == 3:
559 return p, q, v
559 return p, q, v
560 if i == 4:
560 if i == 4:
561 return t, p, v
561 return t, p, v
562 if i == 5:
562 if i == 5:
563 return v, p, q
563 return v, p, q
564
564
565
565
566 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
566 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
567 """
567 """
568 Generator for getting n of evenly distributed colors using
568 Generator for getting n of evenly distributed colors using
569 hsv color and golden ratio. It always return same order of colors
569 hsv color and golden ratio. It always return same order of colors
570
570
571 :param n: number of colors to generate
571 :param n: number of colors to generate
572 :param saturation: saturation of returned colors
572 :param saturation: saturation of returned colors
573 :param lightness: lightness of returned colors
573 :param lightness: lightness of returned colors
574 :returns: RGB tuple
574 :returns: RGB tuple
575 """
575 """
576
576
577 golden_ratio = 0.618033988749895
577 golden_ratio = 0.618033988749895
578 h = 0.22717784590367374
578 h = 0.22717784590367374
579
579
580 for _ in xrange(n):
580 for _ in xrange(n):
581 h += golden_ratio
581 h += golden_ratio
582 h %= 1
582 h %= 1
583 HSV_tuple = [h, saturation, lightness]
583 HSV_tuple = [h, saturation, lightness]
584 RGB_tuple = hsv_to_rgb(*HSV_tuple)
584 RGB_tuple = hsv_to_rgb(*HSV_tuple)
585 yield map(lambda x: str(int(x * 256)), RGB_tuple)
585 yield map(lambda x: str(int(x * 256)), RGB_tuple)
586
586
587
587
588 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
588 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
589 """
589 """
590 Returns a function which when called with an argument returns a unique
590 Returns a function which when called with an argument returns a unique
591 color for that argument, eg.
591 color for that argument, eg.
592
592
593 :param n: number of colors to generate
593 :param n: number of colors to generate
594 :param saturation: saturation of returned colors
594 :param saturation: saturation of returned colors
595 :param lightness: lightness of returned colors
595 :param lightness: lightness of returned colors
596 :returns: css RGB string
596 :returns: css RGB string
597
597
598 >>> color_hash = color_hasher()
598 >>> color_hash = color_hasher()
599 >>> color_hash('hello')
599 >>> color_hash('hello')
600 'rgb(34, 12, 59)'
600 'rgb(34, 12, 59)'
601 >>> color_hash('hello')
601 >>> color_hash('hello')
602 'rgb(34, 12, 59)'
602 'rgb(34, 12, 59)'
603 >>> color_hash('other')
603 >>> color_hash('other')
604 'rgb(90, 224, 159)'
604 'rgb(90, 224, 159)'
605 """
605 """
606
606
607 color_dict = {}
607 color_dict = {}
608 cgenerator = unique_color_generator(
608 cgenerator = unique_color_generator(
609 saturation=saturation, lightness=lightness)
609 saturation=saturation, lightness=lightness)
610
610
611 def get_color_string(thing):
611 def get_color_string(thing):
612 if thing in color_dict:
612 if thing in color_dict:
613 col = color_dict[thing]
613 col = color_dict[thing]
614 else:
614 else:
615 col = color_dict[thing] = cgenerator.next()
615 col = color_dict[thing] = cgenerator.next()
616 return "rgb(%s)" % (', '.join(col))
616 return "rgb(%s)" % (', '.join(col))
617
617
618 return get_color_string
618 return get_color_string
619
619
620
620
621 def get_lexer_safe(mimetype=None, filepath=None):
621 def get_lexer_safe(mimetype=None, filepath=None):
622 """
622 """
623 Tries to return a relevant pygments lexer using mimetype/filepath name,
623 Tries to return a relevant pygments lexer using mimetype/filepath name,
624 defaulting to plain text if none could be found
624 defaulting to plain text if none could be found
625 """
625 """
626 lexer = None
626 lexer = None
627 try:
627 try:
628 if mimetype:
628 if mimetype:
629 lexer = get_lexer_for_mimetype(mimetype)
629 lexer = get_lexer_for_mimetype(mimetype)
630 if not lexer:
630 if not lexer:
631 lexer = get_lexer_for_filename(filepath)
631 lexer = get_lexer_for_filename(filepath)
632 except pygments.util.ClassNotFound:
632 except pygments.util.ClassNotFound:
633 pass
633 pass
634
634
635 if not lexer:
635 if not lexer:
636 lexer = get_lexer_by_name('text')
636 lexer = get_lexer_by_name('text')
637
637
638 return lexer
638 return lexer
639
639
640
640
641 def get_lexer_for_filenode(filenode):
641 def get_lexer_for_filenode(filenode):
642 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
642 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
643 return lexer
643 return lexer
644
644
645
645
646 def pygmentize(filenode, **kwargs):
646 def pygmentize(filenode, **kwargs):
647 """
647 """
648 pygmentize function using pygments
648 pygmentize function using pygments
649
649
650 :param filenode:
650 :param filenode:
651 """
651 """
652 lexer = get_lexer_for_filenode(filenode)
652 lexer = get_lexer_for_filenode(filenode)
653 return literal(code_highlight(filenode.content, lexer,
653 return literal(code_highlight(filenode.content, lexer,
654 CodeHtmlFormatter(**kwargs)))
654 CodeHtmlFormatter(**kwargs)))
655
655
656
656
657 def is_following_repo(repo_name, user_id):
657 def is_following_repo(repo_name, user_id):
658 from rhodecode.model.scm import ScmModel
658 from rhodecode.model.scm import ScmModel
659 return ScmModel().is_following_repo(repo_name, user_id)
659 return ScmModel().is_following_repo(repo_name, user_id)
660
660
661
661
662 class _Message(object):
662 class _Message(object):
663 """A message returned by ``Flash.pop_messages()``.
663 """A message returned by ``Flash.pop_messages()``.
664
664
665 Converting the message to a string returns the message text. Instances
665 Converting the message to a string returns the message text. Instances
666 also have the following attributes:
666 also have the following attributes:
667
667
668 * ``message``: the message text.
668 * ``message``: the message text.
669 * ``category``: the category specified when the message was created.
669 * ``category``: the category specified when the message was created.
670 """
670 """
671
671
672 def __init__(self, category, message, sub_data=None):
672 def __init__(self, category, message, sub_data=None):
673 self.category = category
673 self.category = category
674 self.message = message
674 self.message = message
675 self.sub_data = sub_data or {}
675 self.sub_data = sub_data or {}
676
676
677 def __str__(self):
677 def __str__(self):
678 return self.message
678 return self.message
679
679
680 __unicode__ = __str__
680 __unicode__ = __str__
681
681
682 def __html__(self):
682 def __html__(self):
683 return escape(safe_unicode(self.message))
683 return escape(safe_unicode(self.message))
684
684
685
685
686 class Flash(object):
686 class Flash(object):
687 # List of allowed categories. If None, allow any category.
687 # List of allowed categories. If None, allow any category.
688 categories = ["warning", "notice", "error", "success"]
688 categories = ["warning", "notice", "error", "success"]
689
689
690 # Default category if none is specified.
690 # Default category if none is specified.
691 default_category = "notice"
691 default_category = "notice"
692
692
693 def __init__(self, session_key="flash", categories=None,
693 def __init__(self, session_key="flash", categories=None,
694 default_category=None):
694 default_category=None):
695 """
695 """
696 Instantiate a ``Flash`` object.
696 Instantiate a ``Flash`` object.
697
697
698 ``session_key`` is the key to save the messages under in the user's
698 ``session_key`` is the key to save the messages under in the user's
699 session.
699 session.
700
700
701 ``categories`` is an optional list which overrides the default list
701 ``categories`` is an optional list which overrides the default list
702 of categories.
702 of categories.
703
703
704 ``default_category`` overrides the default category used for messages
704 ``default_category`` overrides the default category used for messages
705 when none is specified.
705 when none is specified.
706 """
706 """
707 self.session_key = session_key
707 self.session_key = session_key
708 if categories is not None:
708 if categories is not None:
709 self.categories = categories
709 self.categories = categories
710 if default_category is not None:
710 if default_category is not None:
711 self.default_category = default_category
711 self.default_category = default_category
712 if self.categories and self.default_category not in self.categories:
712 if self.categories and self.default_category not in self.categories:
713 raise ValueError(
713 raise ValueError(
714 "unrecognized default category %r" % (self.default_category,))
714 "unrecognized default category %r" % (self.default_category,))
715
715
716 def pop_messages(self, session=None, request=None):
716 def pop_messages(self, session=None, request=None):
717 """
717 """
718 Return all accumulated messages and delete them from the session.
718 Return all accumulated messages and delete them from the session.
719
719
720 The return value is a list of ``Message`` objects.
720 The return value is a list of ``Message`` objects.
721 """
721 """
722 messages = []
722 messages = []
723
723
724 if not session:
724 if not session:
725 if not request:
725 if not request:
726 request = get_current_request()
726 request = get_current_request()
727 session = request.session
727 session = request.session
728
728
729 # Pop the 'old' pylons flash messages. They are tuples of the form
729 # Pop the 'old' pylons flash messages. They are tuples of the form
730 # (category, message)
730 # (category, message)
731 for cat, msg in session.pop(self.session_key, []):
731 for cat, msg in session.pop(self.session_key, []):
732 messages.append(_Message(cat, msg))
732 messages.append(_Message(cat, msg))
733
733
734 # Pop the 'new' pyramid flash messages for each category as list
734 # Pop the 'new' pyramid flash messages for each category as list
735 # of strings.
735 # of strings.
736 for cat in self.categories:
736 for cat in self.categories:
737 for msg in session.pop_flash(queue=cat):
737 for msg in session.pop_flash(queue=cat):
738 sub_data = {}
738 sub_data = {}
739 if hasattr(msg, 'rsplit'):
739 if hasattr(msg, 'rsplit'):
740 flash_data = msg.rsplit('|DELIM|', 1)
740 flash_data = msg.rsplit('|DELIM|', 1)
741 org_message = flash_data[0]
741 org_message = flash_data[0]
742 if len(flash_data) > 1:
742 if len(flash_data) > 1:
743 sub_data = json.loads(flash_data[1])
743 sub_data = json.loads(flash_data[1])
744 else:
744 else:
745 org_message = msg
745 org_message = msg
746
746
747 messages.append(_Message(cat, org_message, sub_data=sub_data))
747 messages.append(_Message(cat, org_message, sub_data=sub_data))
748
748
749 # Map messages from the default queue to the 'notice' category.
749 # Map messages from the default queue to the 'notice' category.
750 for msg in session.pop_flash():
750 for msg in session.pop_flash():
751 messages.append(_Message('notice', msg))
751 messages.append(_Message('notice', msg))
752
752
753 session.save()
753 session.save()
754 return messages
754 return messages
755
755
756 def json_alerts(self, session=None, request=None):
756 def json_alerts(self, session=None, request=None):
757 payloads = []
757 payloads = []
758 messages = flash.pop_messages(session=session, request=request) or []
758 messages = flash.pop_messages(session=session, request=request) or []
759 for message in messages:
759 for message in messages:
760 payloads.append({
760 payloads.append({
761 'message': {
761 'message': {
762 'message': u'{}'.format(message.message),
762 'message': u'{}'.format(message.message),
763 'level': message.category,
763 'level': message.category,
764 'force': True,
764 'force': True,
765 'subdata': message.sub_data
765 'subdata': message.sub_data
766 }
766 }
767 })
767 })
768 return json.dumps(payloads)
768 return json.dumps(payloads)
769
769
770 def __call__(self, message, category=None, ignore_duplicate=True,
770 def __call__(self, message, category=None, ignore_duplicate=True,
771 session=None, request=None):
771 session=None, request=None):
772
772
773 if not session:
773 if not session:
774 if not request:
774 if not request:
775 request = get_current_request()
775 request = get_current_request()
776 session = request.session
776 session = request.session
777
777
778 session.flash(
778 session.flash(
779 message, queue=category, allow_duplicate=not ignore_duplicate)
779 message, queue=category, allow_duplicate=not ignore_duplicate)
780
780
781
781
782 flash = Flash()
782 flash = Flash()
783
783
784 #==============================================================================
784 #==============================================================================
785 # SCM FILTERS available via h.
785 # SCM FILTERS available via h.
786 #==============================================================================
786 #==============================================================================
787 from rhodecode.lib.vcs.utils import author_name, author_email
787 from rhodecode.lib.vcs.utils import author_name, author_email
788 from rhodecode.lib.utils2 import age, age_from_seconds
788 from rhodecode.lib.utils2 import age, age_from_seconds
789 from rhodecode.model.db import User, ChangesetStatus
789 from rhodecode.model.db import User, ChangesetStatus
790
790
791
791
792 email = author_email
792 email = author_email
793
793
794
794
795 def capitalize(raw_text):
795 def capitalize(raw_text):
796 return raw_text.capitalize()
796 return raw_text.capitalize()
797
797
798
798
799 def short_id(long_id):
799 def short_id(long_id):
800 return long_id[:12]
800 return long_id[:12]
801
801
802
802
803 def hide_credentials(url):
803 def hide_credentials(url):
804 from rhodecode.lib.utils2 import credentials_filter
804 from rhodecode.lib.utils2 import credentials_filter
805 return credentials_filter(url)
805 return credentials_filter(url)
806
806
807
807
808 import pytz
808 import pytz
809 import tzlocal
809 import tzlocal
810 local_timezone = tzlocal.get_localzone()
810 local_timezone = tzlocal.get_localzone()
811
811
812
812
813 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
813 def get_timezone(datetime_iso, time_is_local=False):
814 title = value or format_date(datetime_iso)
815 tzinfo = '+00:00'
814 tzinfo = '+00:00'
816
815
817 # detect if we have a timezone info, otherwise, add it
816 # detect if we have a timezone info, otherwise, add it
818 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
817 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
819 force_timezone = os.environ.get('RC_TIMEZONE', '')
818 force_timezone = os.environ.get('RC_TIMEZONE', '')
820 if force_timezone:
819 if force_timezone:
821 force_timezone = pytz.timezone(force_timezone)
820 force_timezone = pytz.timezone(force_timezone)
822 timezone = force_timezone or local_timezone
821 timezone = force_timezone or local_timezone
823 offset = timezone.localize(datetime_iso).strftime('%z')
822 offset = timezone.localize(datetime_iso).strftime('%z')
824 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
823 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
824 return tzinfo
825
826
827 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
828 title = value or format_date(datetime_iso)
829 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
825
830
826 return literal(
831 return literal(
827 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
832 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
828 cls='tooltip' if tooltip else '',
833 cls='tooltip' if tooltip else '',
829 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
834 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
830 title=title, dt=datetime_iso, tzinfo=tzinfo
835 title=title, dt=datetime_iso, tzinfo=tzinfo
831 ))
836 ))
832
837
833
838
834 def _shorten_commit_id(commit_id, commit_len=None):
839 def _shorten_commit_id(commit_id, commit_len=None):
835 if commit_len is None:
840 if commit_len is None:
836 request = get_current_request()
841 request = get_current_request()
837 commit_len = request.call_context.visual.show_sha_length
842 commit_len = request.call_context.visual.show_sha_length
838 return commit_id[:commit_len]
843 return commit_id[:commit_len]
839
844
840
845
841 def show_id(commit, show_idx=None, commit_len=None):
846 def show_id(commit, show_idx=None, commit_len=None):
842 """
847 """
843 Configurable function that shows ID
848 Configurable function that shows ID
844 by default it's r123:fffeeefffeee
849 by default it's r123:fffeeefffeee
845
850
846 :param commit: commit instance
851 :param commit: commit instance
847 """
852 """
848 if show_idx is None:
853 if show_idx is None:
849 request = get_current_request()
854 request = get_current_request()
850 show_idx = request.call_context.visual.show_revision_number
855 show_idx = request.call_context.visual.show_revision_number
851
856
852 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
857 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
853 if show_idx:
858 if show_idx:
854 return 'r%s:%s' % (commit.idx, raw_id)
859 return 'r%s:%s' % (commit.idx, raw_id)
855 else:
860 else:
856 return '%s' % (raw_id, )
861 return '%s' % (raw_id, )
857
862
858
863
859 def format_date(date):
864 def format_date(date):
860 """
865 """
861 use a standardized formatting for dates used in RhodeCode
866 use a standardized formatting for dates used in RhodeCode
862
867
863 :param date: date/datetime object
868 :param date: date/datetime object
864 :return: formatted date
869 :return: formatted date
865 """
870 """
866
871
867 if date:
872 if date:
868 _fmt = "%a, %d %b %Y %H:%M:%S"
873 _fmt = "%a, %d %b %Y %H:%M:%S"
869 return safe_unicode(date.strftime(_fmt))
874 return safe_unicode(date.strftime(_fmt))
870
875
871 return u""
876 return u""
872
877
873
878
874 class _RepoChecker(object):
879 class _RepoChecker(object):
875
880
876 def __init__(self, backend_alias):
881 def __init__(self, backend_alias):
877 self._backend_alias = backend_alias
882 self._backend_alias = backend_alias
878
883
879 def __call__(self, repository):
884 def __call__(self, repository):
880 if hasattr(repository, 'alias'):
885 if hasattr(repository, 'alias'):
881 _type = repository.alias
886 _type = repository.alias
882 elif hasattr(repository, 'repo_type'):
887 elif hasattr(repository, 'repo_type'):
883 _type = repository.repo_type
888 _type = repository.repo_type
884 else:
889 else:
885 _type = repository
890 _type = repository
886 return _type == self._backend_alias
891 return _type == self._backend_alias
887
892
888
893
889 is_git = _RepoChecker('git')
894 is_git = _RepoChecker('git')
890 is_hg = _RepoChecker('hg')
895 is_hg = _RepoChecker('hg')
891 is_svn = _RepoChecker('svn')
896 is_svn = _RepoChecker('svn')
892
897
893
898
894 def get_repo_type_by_name(repo_name):
899 def get_repo_type_by_name(repo_name):
895 repo = Repository.get_by_repo_name(repo_name)
900 repo = Repository.get_by_repo_name(repo_name)
896 if repo:
901 if repo:
897 return repo.repo_type
902 return repo.repo_type
898
903
899
904
900 def is_svn_without_proxy(repository):
905 def is_svn_without_proxy(repository):
901 if is_svn(repository):
906 if is_svn(repository):
902 from rhodecode.model.settings import VcsSettingsModel
907 from rhodecode.model.settings import VcsSettingsModel
903 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
908 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
904 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
909 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
905 return False
910 return False
906
911
907
912
908 def discover_user(author):
913 def discover_user(author):
909 """
914 """
910 Tries to discover RhodeCode User based on the author string. Author string
915 Tries to discover RhodeCode User based on the author string. Author string
911 is typically `FirstName LastName <email@address.com>`
916 is typically `FirstName LastName <email@address.com>`
912 """
917 """
913
918
914 # if author is already an instance use it for extraction
919 # if author is already an instance use it for extraction
915 if isinstance(author, User):
920 if isinstance(author, User):
916 return author
921 return author
917
922
918 # Valid email in the attribute passed, see if they're in the system
923 # Valid email in the attribute passed, see if they're in the system
919 _email = author_email(author)
924 _email = author_email(author)
920 if _email != '':
925 if _email != '':
921 user = User.get_by_email(_email, case_insensitive=True, cache=True)
926 user = User.get_by_email(_email, case_insensitive=True, cache=True)
922 if user is not None:
927 if user is not None:
923 return user
928 return user
924
929
925 # Maybe it's a username, we try to extract it and fetch by username ?
930 # Maybe it's a username, we try to extract it and fetch by username ?
926 _author = author_name(author)
931 _author = author_name(author)
927 user = User.get_by_username(_author, case_insensitive=True, cache=True)
932 user = User.get_by_username(_author, case_insensitive=True, cache=True)
928 if user is not None:
933 if user is not None:
929 return user
934 return user
930
935
931 return None
936 return None
932
937
933
938
934 def email_or_none(author):
939 def email_or_none(author):
935 # extract email from the commit string
940 # extract email from the commit string
936 _email = author_email(author)
941 _email = author_email(author)
937
942
938 # If we have an email, use it, otherwise
943 # If we have an email, use it, otherwise
939 # see if it contains a username we can get an email from
944 # see if it contains a username we can get an email from
940 if _email != '':
945 if _email != '':
941 return _email
946 return _email
942 else:
947 else:
943 user = User.get_by_username(
948 user = User.get_by_username(
944 author_name(author), case_insensitive=True, cache=True)
949 author_name(author), case_insensitive=True, cache=True)
945
950
946 if user is not None:
951 if user is not None:
947 return user.email
952 return user.email
948
953
949 # No valid email, not a valid user in the system, none!
954 # No valid email, not a valid user in the system, none!
950 return None
955 return None
951
956
952
957
953 def link_to_user(author, length=0, **kwargs):
958 def link_to_user(author, length=0, **kwargs):
954 user = discover_user(author)
959 user = discover_user(author)
955 # user can be None, but if we have it already it means we can re-use it
960 # user can be None, but if we have it already it means we can re-use it
956 # in the person() function, so we save 1 intensive-query
961 # in the person() function, so we save 1 intensive-query
957 if user:
962 if user:
958 author = user
963 author = user
959
964
960 display_person = person(author, 'username_or_name_or_email')
965 display_person = person(author, 'username_or_name_or_email')
961 if length:
966 if length:
962 display_person = shorter(display_person, length)
967 display_person = shorter(display_person, length)
963
968
964 if user and user.username != user.DEFAULT_USER:
969 if user and user.username != user.DEFAULT_USER:
965 return link_to(
970 return link_to(
966 escape(display_person),
971 escape(display_person),
967 route_path('user_profile', username=user.username),
972 route_path('user_profile', username=user.username),
968 **kwargs)
973 **kwargs)
969 else:
974 else:
970 return escape(display_person)
975 return escape(display_person)
971
976
972
977
973 def link_to_group(users_group_name, **kwargs):
978 def link_to_group(users_group_name, **kwargs):
974 return link_to(
979 return link_to(
975 escape(users_group_name),
980 escape(users_group_name),
976 route_path('user_group_profile', user_group_name=users_group_name),
981 route_path('user_group_profile', user_group_name=users_group_name),
977 **kwargs)
982 **kwargs)
978
983
979
984
980 def person(author, show_attr="username_and_name"):
985 def person(author, show_attr="username_and_name"):
981 user = discover_user(author)
986 user = discover_user(author)
982 if user:
987 if user:
983 return getattr(user, show_attr)
988 return getattr(user, show_attr)
984 else:
989 else:
985 _author = author_name(author)
990 _author = author_name(author)
986 _email = email(author)
991 _email = email(author)
987 return _author or _email
992 return _author or _email
988
993
989
994
990 def author_string(email):
995 def author_string(email):
991 if email:
996 if email:
992 user = User.get_by_email(email, case_insensitive=True, cache=True)
997 user = User.get_by_email(email, case_insensitive=True, cache=True)
993 if user:
998 if user:
994 if user.first_name or user.last_name:
999 if user.first_name or user.last_name:
995 return '%s %s &lt;%s&gt;' % (
1000 return '%s %s &lt;%s&gt;' % (
996 user.first_name, user.last_name, email)
1001 user.first_name, user.last_name, email)
997 else:
1002 else:
998 return email
1003 return email
999 else:
1004 else:
1000 return email
1005 return email
1001 else:
1006 else:
1002 return None
1007 return None
1003
1008
1004
1009
1005 def person_by_id(id_, show_attr="username_and_name"):
1010 def person_by_id(id_, show_attr="username_and_name"):
1006 # attr to return from fetched user
1011 # attr to return from fetched user
1007 person_getter = lambda usr: getattr(usr, show_attr)
1012 person_getter = lambda usr: getattr(usr, show_attr)
1008
1013
1009 #maybe it's an ID ?
1014 #maybe it's an ID ?
1010 if str(id_).isdigit() or isinstance(id_, int):
1015 if str(id_).isdigit() or isinstance(id_, int):
1011 id_ = int(id_)
1016 id_ = int(id_)
1012 user = User.get(id_)
1017 user = User.get(id_)
1013 if user is not None:
1018 if user is not None:
1014 return person_getter(user)
1019 return person_getter(user)
1015 return id_
1020 return id_
1016
1021
1017
1022
1018 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1023 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1019 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1024 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1020 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1025 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1021
1026
1022
1027
1023 tags_paterns = OrderedDict((
1028 tags_paterns = OrderedDict((
1024 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1029 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1025 '<div class="metatag" tag="lang">\\2</div>')),
1030 '<div class="metatag" tag="lang">\\2</div>')),
1026
1031
1027 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1032 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1028 '<div class="metatag" tag="see">see: \\1 </div>')),
1033 '<div class="metatag" tag="see">see: \\1 </div>')),
1029
1034
1030 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1035 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1031 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1036 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1032
1037
1033 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1038 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1034 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1039 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1035
1040
1036 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1041 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1037 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1042 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1038
1043
1039 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1044 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1040 '<div class="metatag" tag="state \\1">\\1</div>')),
1045 '<div class="metatag" tag="state \\1">\\1</div>')),
1041
1046
1042 # label in grey
1047 # label in grey
1043 ('label', (re.compile(r'\[([a-z]+)\]'),
1048 ('label', (re.compile(r'\[([a-z]+)\]'),
1044 '<div class="metatag" tag="label">\\1</div>')),
1049 '<div class="metatag" tag="label">\\1</div>')),
1045
1050
1046 # generic catch all in grey
1051 # generic catch all in grey
1047 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1052 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1048 '<div class="metatag" tag="generic">\\1</div>')),
1053 '<div class="metatag" tag="generic">\\1</div>')),
1049 ))
1054 ))
1050
1055
1051
1056
1052 def extract_metatags(value):
1057 def extract_metatags(value):
1053 """
1058 """
1054 Extract supported meta-tags from given text value
1059 Extract supported meta-tags from given text value
1055 """
1060 """
1056 tags = []
1061 tags = []
1057 if not value:
1062 if not value:
1058 return tags, ''
1063 return tags, ''
1059
1064
1060 for key, val in tags_paterns.items():
1065 for key, val in tags_paterns.items():
1061 pat, replace_html = val
1066 pat, replace_html = val
1062 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1067 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1063 value = pat.sub('', value)
1068 value = pat.sub('', value)
1064
1069
1065 return tags, value
1070 return tags, value
1066
1071
1067
1072
1068 def style_metatag(tag_type, value):
1073 def style_metatag(tag_type, value):
1069 """
1074 """
1070 converts tags from value into html equivalent
1075 converts tags from value into html equivalent
1071 """
1076 """
1072 if not value:
1077 if not value:
1073 return ''
1078 return ''
1074
1079
1075 html_value = value
1080 html_value = value
1076 tag_data = tags_paterns.get(tag_type)
1081 tag_data = tags_paterns.get(tag_type)
1077 if tag_data:
1082 if tag_data:
1078 pat, replace_html = tag_data
1083 pat, replace_html = tag_data
1079 # convert to plain `unicode` instead of a markup tag to be used in
1084 # convert to plain `unicode` instead of a markup tag to be used in
1080 # regex expressions. safe_unicode doesn't work here
1085 # regex expressions. safe_unicode doesn't work here
1081 html_value = pat.sub(replace_html, unicode(value))
1086 html_value = pat.sub(replace_html, unicode(value))
1082
1087
1083 return html_value
1088 return html_value
1084
1089
1085
1090
1086 def bool2icon(value, show_at_false=True):
1091 def bool2icon(value, show_at_false=True):
1087 """
1092 """
1088 Returns boolean value of a given value, represented as html element with
1093 Returns boolean value of a given value, represented as html element with
1089 classes that will represent icons
1094 classes that will represent icons
1090
1095
1091 :param value: given value to convert to html node
1096 :param value: given value to convert to html node
1092 """
1097 """
1093
1098
1094 if value: # does bool conversion
1099 if value: # does bool conversion
1095 return HTML.tag('i', class_="icon-true", title='True')
1100 return HTML.tag('i', class_="icon-true", title='True')
1096 else: # not true as bool
1101 else: # not true as bool
1097 if show_at_false:
1102 if show_at_false:
1098 return HTML.tag('i', class_="icon-false", title='False')
1103 return HTML.tag('i', class_="icon-false", title='False')
1099 return HTML.tag('i')
1104 return HTML.tag('i')
1100
1105
1101 #==============================================================================
1106 #==============================================================================
1102 # PERMS
1107 # PERMS
1103 #==============================================================================
1108 #==============================================================================
1104 from rhodecode.lib.auth import (
1109 from rhodecode.lib.auth import (
1105 HasPermissionAny, HasPermissionAll,
1110 HasPermissionAny, HasPermissionAll,
1106 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1111 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1107 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1112 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1108 csrf_token_key, AuthUser)
1113 csrf_token_key, AuthUser)
1109
1114
1110
1115
1111 #==============================================================================
1116 #==============================================================================
1112 # GRAVATAR URL
1117 # GRAVATAR URL
1113 #==============================================================================
1118 #==============================================================================
1114 class InitialsGravatar(object):
1119 class InitialsGravatar(object):
1115 def __init__(self, email_address, first_name, last_name, size=30,
1120 def __init__(self, email_address, first_name, last_name, size=30,
1116 background=None, text_color='#fff'):
1121 background=None, text_color='#fff'):
1117 self.size = size
1122 self.size = size
1118 self.first_name = first_name
1123 self.first_name = first_name
1119 self.last_name = last_name
1124 self.last_name = last_name
1120 self.email_address = email_address
1125 self.email_address = email_address
1121 self.background = background or self.str2color(email_address)
1126 self.background = background or self.str2color(email_address)
1122 self.text_color = text_color
1127 self.text_color = text_color
1123
1128
1124 def get_color_bank(self):
1129 def get_color_bank(self):
1125 """
1130 """
1126 returns a predefined list of colors that gravatars can use.
1131 returns a predefined list of colors that gravatars can use.
1127 Those are randomized distinct colors that guarantee readability and
1132 Those are randomized distinct colors that guarantee readability and
1128 uniqueness.
1133 uniqueness.
1129
1134
1130 generated with: http://phrogz.net/css/distinct-colors.html
1135 generated with: http://phrogz.net/css/distinct-colors.html
1131 """
1136 """
1132 return [
1137 return [
1133 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1138 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1134 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1139 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1135 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1140 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1136 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1141 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1137 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1142 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1138 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1143 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1139 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1144 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1140 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1145 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1141 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1146 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1142 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1147 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1143 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1148 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1144 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1149 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1145 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1150 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1146 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1151 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1147 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1152 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1148 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1153 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1149 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1154 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1150 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1155 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1151 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1156 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1152 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1157 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1153 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1158 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1154 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1159 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1155 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1160 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1156 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1161 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1157 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1162 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1158 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1163 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1159 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1164 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1160 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1165 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1161 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1166 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1162 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1167 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1163 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1168 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1164 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1169 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1165 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1170 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1166 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1171 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1167 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1172 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1168 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1173 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1169 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1174 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1170 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1175 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1171 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1176 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1172 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1177 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1173 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1178 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1174 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1179 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1175 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1180 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1176 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1181 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1177 '#4f8c46', '#368dd9', '#5c0073'
1182 '#4f8c46', '#368dd9', '#5c0073'
1178 ]
1183 ]
1179
1184
1180 def rgb_to_hex_color(self, rgb_tuple):
1185 def rgb_to_hex_color(self, rgb_tuple):
1181 """
1186 """
1182 Converts an rgb_tuple passed to an hex color.
1187 Converts an rgb_tuple passed to an hex color.
1183
1188
1184 :param rgb_tuple: tuple with 3 ints represents rgb color space
1189 :param rgb_tuple: tuple with 3 ints represents rgb color space
1185 """
1190 """
1186 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1191 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1187
1192
1188 def email_to_int_list(self, email_str):
1193 def email_to_int_list(self, email_str):
1189 """
1194 """
1190 Get every byte of the hex digest value of email and turn it to integer.
1195 Get every byte of the hex digest value of email and turn it to integer.
1191 It's going to be always between 0-255
1196 It's going to be always between 0-255
1192 """
1197 """
1193 digest = md5_safe(email_str.lower())
1198 digest = md5_safe(email_str.lower())
1194 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1199 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1195
1200
1196 def pick_color_bank_index(self, email_str, color_bank):
1201 def pick_color_bank_index(self, email_str, color_bank):
1197 return self.email_to_int_list(email_str)[0] % len(color_bank)
1202 return self.email_to_int_list(email_str)[0] % len(color_bank)
1198
1203
1199 def str2color(self, email_str):
1204 def str2color(self, email_str):
1200 """
1205 """
1201 Tries to map in a stable algorithm an email to color
1206 Tries to map in a stable algorithm an email to color
1202
1207
1203 :param email_str:
1208 :param email_str:
1204 """
1209 """
1205 color_bank = self.get_color_bank()
1210 color_bank = self.get_color_bank()
1206 # pick position (module it's length so we always find it in the
1211 # pick position (module it's length so we always find it in the
1207 # bank even if it's smaller than 256 values
1212 # bank even if it's smaller than 256 values
1208 pos = self.pick_color_bank_index(email_str, color_bank)
1213 pos = self.pick_color_bank_index(email_str, color_bank)
1209 return color_bank[pos]
1214 return color_bank[pos]
1210
1215
1211 def normalize_email(self, email_address):
1216 def normalize_email(self, email_address):
1212 import unicodedata
1217 import unicodedata
1213 # default host used to fill in the fake/missing email
1218 # default host used to fill in the fake/missing email
1214 default_host = u'localhost'
1219 default_host = u'localhost'
1215
1220
1216 if not email_address:
1221 if not email_address:
1217 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1222 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1218
1223
1219 email_address = safe_unicode(email_address)
1224 email_address = safe_unicode(email_address)
1220
1225
1221 if u'@' not in email_address:
1226 if u'@' not in email_address:
1222 email_address = u'%s@%s' % (email_address, default_host)
1227 email_address = u'%s@%s' % (email_address, default_host)
1223
1228
1224 if email_address.endswith(u'@'):
1229 if email_address.endswith(u'@'):
1225 email_address = u'%s%s' % (email_address, default_host)
1230 email_address = u'%s%s' % (email_address, default_host)
1226
1231
1227 email_address = unicodedata.normalize('NFKD', email_address)\
1232 email_address = unicodedata.normalize('NFKD', email_address)\
1228 .encode('ascii', 'ignore')
1233 .encode('ascii', 'ignore')
1229 return email_address
1234 return email_address
1230
1235
1231 def get_initials(self):
1236 def get_initials(self):
1232 """
1237 """
1233 Returns 2 letter initials calculated based on the input.
1238 Returns 2 letter initials calculated based on the input.
1234 The algorithm picks first given email address, and takes first letter
1239 The algorithm picks first given email address, and takes first letter
1235 of part before @, and then the first letter of server name. In case
1240 of part before @, and then the first letter of server name. In case
1236 the part before @ is in a format of `somestring.somestring2` it replaces
1241 the part before @ is in a format of `somestring.somestring2` it replaces
1237 the server letter with first letter of somestring2
1242 the server letter with first letter of somestring2
1238
1243
1239 In case function was initialized with both first and lastname, this
1244 In case function was initialized with both first and lastname, this
1240 overrides the extraction from email by first letter of the first and
1245 overrides the extraction from email by first letter of the first and
1241 last name. We add special logic to that functionality, In case Full name
1246 last name. We add special logic to that functionality, In case Full name
1242 is compound, like Guido Von Rossum, we use last part of the last name
1247 is compound, like Guido Von Rossum, we use last part of the last name
1243 (Von Rossum) picking `R`.
1248 (Von Rossum) picking `R`.
1244
1249
1245 Function also normalizes the non-ascii characters to they ascii
1250 Function also normalizes the non-ascii characters to they ascii
1246 representation, eg Ą => A
1251 representation, eg Ą => A
1247 """
1252 """
1248 import unicodedata
1253 import unicodedata
1249 # replace non-ascii to ascii
1254 # replace non-ascii to ascii
1250 first_name = unicodedata.normalize(
1255 first_name = unicodedata.normalize(
1251 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1256 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1252 last_name = unicodedata.normalize(
1257 last_name = unicodedata.normalize(
1253 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1258 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1254
1259
1255 # do NFKD encoding, and also make sure email has proper format
1260 # do NFKD encoding, and also make sure email has proper format
1256 email_address = self.normalize_email(self.email_address)
1261 email_address = self.normalize_email(self.email_address)
1257
1262
1258 # first push the email initials
1263 # first push the email initials
1259 prefix, server = email_address.split('@', 1)
1264 prefix, server = email_address.split('@', 1)
1260
1265
1261 # check if prefix is maybe a 'first_name.last_name' syntax
1266 # check if prefix is maybe a 'first_name.last_name' syntax
1262 _dot_split = prefix.rsplit('.', 1)
1267 _dot_split = prefix.rsplit('.', 1)
1263 if len(_dot_split) == 2 and _dot_split[1]:
1268 if len(_dot_split) == 2 and _dot_split[1]:
1264 initials = [_dot_split[0][0], _dot_split[1][0]]
1269 initials = [_dot_split[0][0], _dot_split[1][0]]
1265 else:
1270 else:
1266 initials = [prefix[0], server[0]]
1271 initials = [prefix[0], server[0]]
1267
1272
1268 # then try to replace either first_name or last_name
1273 # then try to replace either first_name or last_name
1269 fn_letter = (first_name or " ")[0].strip()
1274 fn_letter = (first_name or " ")[0].strip()
1270 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1275 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1271
1276
1272 if fn_letter:
1277 if fn_letter:
1273 initials[0] = fn_letter
1278 initials[0] = fn_letter
1274
1279
1275 if ln_letter:
1280 if ln_letter:
1276 initials[1] = ln_letter
1281 initials[1] = ln_letter
1277
1282
1278 return ''.join(initials).upper()
1283 return ''.join(initials).upper()
1279
1284
1280 def get_img_data_by_type(self, font_family, img_type):
1285 def get_img_data_by_type(self, font_family, img_type):
1281 default_user = """
1286 default_user = """
1282 <svg xmlns="http://www.w3.org/2000/svg"
1287 <svg xmlns="http://www.w3.org/2000/svg"
1283 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1288 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1284 viewBox="-15 -10 439.165 429.164"
1289 viewBox="-15 -10 439.165 429.164"
1285
1290
1286 xml:space="preserve"
1291 xml:space="preserve"
1287 style="background:{background};" >
1292 style="background:{background};" >
1288
1293
1289 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1294 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1290 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1295 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1291 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1296 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1292 168.596,153.916,216.671,
1297 168.596,153.916,216.671,
1293 204.583,216.671z" fill="{text_color}"/>
1298 204.583,216.671z" fill="{text_color}"/>
1294 <path d="M407.164,374.717L360.88,
1299 <path d="M407.164,374.717L360.88,
1295 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1300 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1296 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1301 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1297 15.366-44.203,23.488-69.076,23.488c-24.877,
1302 15.366-44.203,23.488-69.076,23.488c-24.877,
1298 0-48.762-8.122-69.078-23.488
1303 0-48.762-8.122-69.078-23.488
1299 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1304 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1300 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1305 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1301 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1306 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1302 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1307 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1303 19.402-10.527 C409.699,390.129,
1308 19.402-10.527 C409.699,390.129,
1304 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1309 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1305 </svg>""".format(
1310 </svg>""".format(
1306 size=self.size,
1311 size=self.size,
1307 background='#979797', # @grey4
1312 background='#979797', # @grey4
1308 text_color=self.text_color,
1313 text_color=self.text_color,
1309 font_family=font_family)
1314 font_family=font_family)
1310
1315
1311 return {
1316 return {
1312 "default_user": default_user
1317 "default_user": default_user
1313 }[img_type]
1318 }[img_type]
1314
1319
1315 def get_img_data(self, svg_type=None):
1320 def get_img_data(self, svg_type=None):
1316 """
1321 """
1317 generates the svg metadata for image
1322 generates the svg metadata for image
1318 """
1323 """
1319 fonts = [
1324 fonts = [
1320 '-apple-system',
1325 '-apple-system',
1321 'BlinkMacSystemFont',
1326 'BlinkMacSystemFont',
1322 'Segoe UI',
1327 'Segoe UI',
1323 'Roboto',
1328 'Roboto',
1324 'Oxygen-Sans',
1329 'Oxygen-Sans',
1325 'Ubuntu',
1330 'Ubuntu',
1326 'Cantarell',
1331 'Cantarell',
1327 'Helvetica Neue',
1332 'Helvetica Neue',
1328 'sans-serif'
1333 'sans-serif'
1329 ]
1334 ]
1330 font_family = ','.join(fonts)
1335 font_family = ','.join(fonts)
1331 if svg_type:
1336 if svg_type:
1332 return self.get_img_data_by_type(font_family, svg_type)
1337 return self.get_img_data_by_type(font_family, svg_type)
1333
1338
1334 initials = self.get_initials()
1339 initials = self.get_initials()
1335 img_data = """
1340 img_data = """
1336 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1341 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1337 width="{size}" height="{size}"
1342 width="{size}" height="{size}"
1338 style="width: 100%; height: 100%; background-color: {background}"
1343 style="width: 100%; height: 100%; background-color: {background}"
1339 viewBox="0 0 {size} {size}">
1344 viewBox="0 0 {size} {size}">
1340 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1345 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1341 pointer-events="auto" fill="{text_color}"
1346 pointer-events="auto" fill="{text_color}"
1342 font-family="{font_family}"
1347 font-family="{font_family}"
1343 style="font-weight: 400; font-size: {f_size}px;">{text}
1348 style="font-weight: 400; font-size: {f_size}px;">{text}
1344 </text>
1349 </text>
1345 </svg>""".format(
1350 </svg>""".format(
1346 size=self.size,
1351 size=self.size,
1347 f_size=self.size/2.05, # scale the text inside the box nicely
1352 f_size=self.size/2.05, # scale the text inside the box nicely
1348 background=self.background,
1353 background=self.background,
1349 text_color=self.text_color,
1354 text_color=self.text_color,
1350 text=initials.upper(),
1355 text=initials.upper(),
1351 font_family=font_family)
1356 font_family=font_family)
1352
1357
1353 return img_data
1358 return img_data
1354
1359
1355 def generate_svg(self, svg_type=None):
1360 def generate_svg(self, svg_type=None):
1356 img_data = self.get_img_data(svg_type)
1361 img_data = self.get_img_data(svg_type)
1357 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1362 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1358
1363
1359
1364
1360 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1365 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1361
1366
1362 svg_type = None
1367 svg_type = None
1363 if email_address == User.DEFAULT_USER_EMAIL:
1368 if email_address == User.DEFAULT_USER_EMAIL:
1364 svg_type = 'default_user'
1369 svg_type = 'default_user'
1365
1370
1366 klass = InitialsGravatar(email_address, first_name, last_name, size)
1371 klass = InitialsGravatar(email_address, first_name, last_name, size)
1367
1372
1368 if store_on_disk:
1373 if store_on_disk:
1369 from rhodecode.apps.file_store import utils as store_utils
1374 from rhodecode.apps.file_store import utils as store_utils
1370 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1375 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1371 FileOverSizeException
1376 FileOverSizeException
1372 from rhodecode.model.db import Session
1377 from rhodecode.model.db import Session
1373
1378
1374 image_key = md5_safe(email_address.lower()
1379 image_key = md5_safe(email_address.lower()
1375 + first_name.lower() + last_name.lower())
1380 + first_name.lower() + last_name.lower())
1376
1381
1377 storage = store_utils.get_file_storage(request.registry.settings)
1382 storage = store_utils.get_file_storage(request.registry.settings)
1378 filename = '{}.svg'.format(image_key)
1383 filename = '{}.svg'.format(image_key)
1379 subdir = 'gravatars'
1384 subdir = 'gravatars'
1380 # since final name has a counter, we apply the 0
1385 # since final name has a counter, we apply the 0
1381 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1386 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1382 store_uid = os.path.join(subdir, uid)
1387 store_uid = os.path.join(subdir, uid)
1383
1388
1384 db_entry = FileStore.get_by_store_uid(store_uid)
1389 db_entry = FileStore.get_by_store_uid(store_uid)
1385 if db_entry:
1390 if db_entry:
1386 return request.route_path('download_file', fid=store_uid)
1391 return request.route_path('download_file', fid=store_uid)
1387
1392
1388 img_data = klass.get_img_data(svg_type=svg_type)
1393 img_data = klass.get_img_data(svg_type=svg_type)
1389 img_file = store_utils.bytes_to_file_obj(img_data)
1394 img_file = store_utils.bytes_to_file_obj(img_data)
1390
1395
1391 try:
1396 try:
1392 store_uid, metadata = storage.save_file(
1397 store_uid, metadata = storage.save_file(
1393 img_file, filename, directory=subdir,
1398 img_file, filename, directory=subdir,
1394 extensions=['.svg'], randomized_name=False)
1399 extensions=['.svg'], randomized_name=False)
1395 except (FileNotAllowedException, FileOverSizeException):
1400 except (FileNotAllowedException, FileOverSizeException):
1396 raise
1401 raise
1397
1402
1398 try:
1403 try:
1399 entry = FileStore.create(
1404 entry = FileStore.create(
1400 file_uid=store_uid, filename=metadata["filename"],
1405 file_uid=store_uid, filename=metadata["filename"],
1401 file_hash=metadata["sha256"], file_size=metadata["size"],
1406 file_hash=metadata["sha256"], file_size=metadata["size"],
1402 file_display_name=filename,
1407 file_display_name=filename,
1403 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1408 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1404 hidden=True, check_acl=False, user_id=1
1409 hidden=True, check_acl=False, user_id=1
1405 )
1410 )
1406 Session().add(entry)
1411 Session().add(entry)
1407 Session().commit()
1412 Session().commit()
1408 log.debug('Stored upload in DB as %s', entry)
1413 log.debug('Stored upload in DB as %s', entry)
1409 except Exception:
1414 except Exception:
1410 raise
1415 raise
1411
1416
1412 return request.route_path('download_file', fid=store_uid)
1417 return request.route_path('download_file', fid=store_uid)
1413
1418
1414 else:
1419 else:
1415 return klass.generate_svg(svg_type=svg_type)
1420 return klass.generate_svg(svg_type=svg_type)
1416
1421
1417
1422
1418 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1423 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1419 return safe_str(gravatar_url_tmpl)\
1424 return safe_str(gravatar_url_tmpl)\
1420 .replace('{email}', email_address) \
1425 .replace('{email}', email_address) \
1421 .replace('{md5email}', md5_safe(email_address.lower())) \
1426 .replace('{md5email}', md5_safe(email_address.lower())) \
1422 .replace('{netloc}', request.host) \
1427 .replace('{netloc}', request.host) \
1423 .replace('{scheme}', request.scheme) \
1428 .replace('{scheme}', request.scheme) \
1424 .replace('{size}', safe_str(size))
1429 .replace('{size}', safe_str(size))
1425
1430
1426
1431
1427 def gravatar_url(email_address, size=30, request=None):
1432 def gravatar_url(email_address, size=30, request=None):
1428 request = request or get_current_request()
1433 request = request or get_current_request()
1429 _use_gravatar = request.call_context.visual.use_gravatar
1434 _use_gravatar = request.call_context.visual.use_gravatar
1430
1435
1431 email_address = email_address or User.DEFAULT_USER_EMAIL
1436 email_address = email_address or User.DEFAULT_USER_EMAIL
1432 if isinstance(email_address, unicode):
1437 if isinstance(email_address, unicode):
1433 # hashlib crashes on unicode items
1438 # hashlib crashes on unicode items
1434 email_address = safe_str(email_address)
1439 email_address = safe_str(email_address)
1435
1440
1436 # empty email or default user
1441 # empty email or default user
1437 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1442 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1438 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1443 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1439
1444
1440 if _use_gravatar:
1445 if _use_gravatar:
1441 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1446 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1442 or User.DEFAULT_GRAVATAR_URL
1447 or User.DEFAULT_GRAVATAR_URL
1443 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1448 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1444
1449
1445 else:
1450 else:
1446 return initials_gravatar(request, email_address, '', '', size=size)
1451 return initials_gravatar(request, email_address, '', '', size=size)
1447
1452
1448
1453
1449 def breadcrumb_repo_link(repo):
1454 def breadcrumb_repo_link(repo):
1450 """
1455 """
1451 Makes a breadcrumbs path link to repo
1456 Makes a breadcrumbs path link to repo
1452
1457
1453 ex::
1458 ex::
1454 group >> subgroup >> repo
1459 group >> subgroup >> repo
1455
1460
1456 :param repo: a Repository instance
1461 :param repo: a Repository instance
1457 """
1462 """
1458
1463
1459 path = [
1464 path = [
1460 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1465 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1461 title='last change:{}'.format(format_date(group.last_commit_change)))
1466 title='last change:{}'.format(format_date(group.last_commit_change)))
1462 for group in repo.groups_with_parents
1467 for group in repo.groups_with_parents
1463 ] + [
1468 ] + [
1464 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1469 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1465 title='last change:{}'.format(format_date(repo.last_commit_change)))
1470 title='last change:{}'.format(format_date(repo.last_commit_change)))
1466 ]
1471 ]
1467
1472
1468 return literal(' &raquo; '.join(path))
1473 return literal(' &raquo; '.join(path))
1469
1474
1470
1475
1471 def breadcrumb_repo_group_link(repo_group):
1476 def breadcrumb_repo_group_link(repo_group):
1472 """
1477 """
1473 Makes a breadcrumbs path link to repo
1478 Makes a breadcrumbs path link to repo
1474
1479
1475 ex::
1480 ex::
1476 group >> subgroup
1481 group >> subgroup
1477
1482
1478 :param repo_group: a Repository Group instance
1483 :param repo_group: a Repository Group instance
1479 """
1484 """
1480
1485
1481 path = [
1486 path = [
1482 link_to(group.name,
1487 link_to(group.name,
1483 route_path('repo_group_home', repo_group_name=group.group_name),
1488 route_path('repo_group_home', repo_group_name=group.group_name),
1484 title='last change:{}'.format(format_date(group.last_commit_change)))
1489 title='last change:{}'.format(format_date(group.last_commit_change)))
1485 for group in repo_group.parents
1490 for group in repo_group.parents
1486 ] + [
1491 ] + [
1487 link_to(repo_group.name,
1492 link_to(repo_group.name,
1488 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1493 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1489 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1494 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1490 ]
1495 ]
1491
1496
1492 return literal(' &raquo; '.join(path))
1497 return literal(' &raquo; '.join(path))
1493
1498
1494
1499
1495 def format_byte_size_binary(file_size):
1500 def format_byte_size_binary(file_size):
1496 """
1501 """
1497 Formats file/folder sizes to standard.
1502 Formats file/folder sizes to standard.
1498 """
1503 """
1499 if file_size is None:
1504 if file_size is None:
1500 file_size = 0
1505 file_size = 0
1501
1506
1502 formatted_size = format_byte_size(file_size, binary=True)
1507 formatted_size = format_byte_size(file_size, binary=True)
1503 return formatted_size
1508 return formatted_size
1504
1509
1505
1510
1506 def urlify_text(text_, safe=True, **href_attrs):
1511 def urlify_text(text_, safe=True, **href_attrs):
1507 """
1512 """
1508 Extract urls from text and make html links out of them
1513 Extract urls from text and make html links out of them
1509 """
1514 """
1510
1515
1511 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1516 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1512 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1517 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1513
1518
1514 def url_func(match_obj):
1519 def url_func(match_obj):
1515 url_full = match_obj.groups()[0]
1520 url_full = match_obj.groups()[0]
1516 a_options = dict(href_attrs)
1521 a_options = dict(href_attrs)
1517 a_options['href'] = url_full
1522 a_options['href'] = url_full
1518 a_text = url_full
1523 a_text = url_full
1519 return HTML.tag("a", a_text, **a_options)
1524 return HTML.tag("a", a_text, **a_options)
1520
1525
1521 _new_text = url_pat.sub(url_func, text_)
1526 _new_text = url_pat.sub(url_func, text_)
1522
1527
1523 if safe:
1528 if safe:
1524 return literal(_new_text)
1529 return literal(_new_text)
1525 return _new_text
1530 return _new_text
1526
1531
1527
1532
1528 def urlify_commits(text_, repo_name):
1533 def urlify_commits(text_, repo_name):
1529 """
1534 """
1530 Extract commit ids from text and make link from them
1535 Extract commit ids from text and make link from them
1531
1536
1532 :param text_:
1537 :param text_:
1533 :param repo_name: repo name to build the URL with
1538 :param repo_name: repo name to build the URL with
1534 """
1539 """
1535
1540
1536 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1541 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1537
1542
1538 def url_func(match_obj):
1543 def url_func(match_obj):
1539 commit_id = match_obj.groups()[1]
1544 commit_id = match_obj.groups()[1]
1540 pref = match_obj.groups()[0]
1545 pref = match_obj.groups()[0]
1541 suf = match_obj.groups()[2]
1546 suf = match_obj.groups()[2]
1542
1547
1543 tmpl = (
1548 tmpl = (
1544 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1549 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1545 '%(commit_id)s</a>%(suf)s'
1550 '%(commit_id)s</a>%(suf)s'
1546 )
1551 )
1547 return tmpl % {
1552 return tmpl % {
1548 'pref': pref,
1553 'pref': pref,
1549 'cls': 'revision-link',
1554 'cls': 'revision-link',
1550 'url': route_url(
1555 'url': route_url(
1551 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1556 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1552 'commit_id': commit_id,
1557 'commit_id': commit_id,
1553 'suf': suf,
1558 'suf': suf,
1554 'hovercard_alt': 'Commit: {}'.format(commit_id),
1559 'hovercard_alt': 'Commit: {}'.format(commit_id),
1555 'hovercard_url': route_url(
1560 'hovercard_url': route_url(
1556 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1561 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1557 }
1562 }
1558
1563
1559 new_text = url_pat.sub(url_func, text_)
1564 new_text = url_pat.sub(url_func, text_)
1560
1565
1561 return new_text
1566 return new_text
1562
1567
1563
1568
1564 def _process_url_func(match_obj, repo_name, uid, entry,
1569 def _process_url_func(match_obj, repo_name, uid, entry,
1565 return_raw_data=False, link_format='html'):
1570 return_raw_data=False, link_format='html'):
1566 pref = ''
1571 pref = ''
1567 if match_obj.group().startswith(' '):
1572 if match_obj.group().startswith(' '):
1568 pref = ' '
1573 pref = ' '
1569
1574
1570 issue_id = ''.join(match_obj.groups())
1575 issue_id = ''.join(match_obj.groups())
1571
1576
1572 if link_format == 'html':
1577 if link_format == 'html':
1573 tmpl = (
1578 tmpl = (
1574 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1579 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1575 '%(issue-prefix)s%(id-repr)s'
1580 '%(issue-prefix)s%(id-repr)s'
1576 '</a>')
1581 '</a>')
1577 elif link_format == 'html+hovercard':
1582 elif link_format == 'html+hovercard':
1578 tmpl = (
1583 tmpl = (
1579 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1584 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1580 '%(issue-prefix)s%(id-repr)s'
1585 '%(issue-prefix)s%(id-repr)s'
1581 '</a>')
1586 '</a>')
1582 elif link_format in ['rst', 'rst+hovercard']:
1587 elif link_format in ['rst', 'rst+hovercard']:
1583 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1588 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1584 elif link_format in ['markdown', 'markdown+hovercard']:
1589 elif link_format in ['markdown', 'markdown+hovercard']:
1585 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1590 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1586 else:
1591 else:
1587 raise ValueError('Bad link_format:{}'.format(link_format))
1592 raise ValueError('Bad link_format:{}'.format(link_format))
1588
1593
1589 (repo_name_cleaned,
1594 (repo_name_cleaned,
1590 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1595 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1591
1596
1592 # variables replacement
1597 # variables replacement
1593 named_vars = {
1598 named_vars = {
1594 'id': issue_id,
1599 'id': issue_id,
1595 'repo': repo_name,
1600 'repo': repo_name,
1596 'repo_name': repo_name_cleaned,
1601 'repo_name': repo_name_cleaned,
1597 'group_name': parent_group_name,
1602 'group_name': parent_group_name,
1598 # set dummy keys so we always have them
1603 # set dummy keys so we always have them
1599 'hostname': '',
1604 'hostname': '',
1600 'netloc': '',
1605 'netloc': '',
1601 'scheme': ''
1606 'scheme': ''
1602 }
1607 }
1603
1608
1604 request = get_current_request()
1609 request = get_current_request()
1605 if request:
1610 if request:
1606 # exposes, hostname, netloc, scheme
1611 # exposes, hostname, netloc, scheme
1607 host_data = get_host_info(request)
1612 host_data = get_host_info(request)
1608 named_vars.update(host_data)
1613 named_vars.update(host_data)
1609
1614
1610 # named regex variables
1615 # named regex variables
1611 named_vars.update(match_obj.groupdict())
1616 named_vars.update(match_obj.groupdict())
1612 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1617 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1613 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1618 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1614 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1619 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1615
1620
1616 def quote_cleaner(input_str):
1621 def quote_cleaner(input_str):
1617 """Remove quotes as it's HTML"""
1622 """Remove quotes as it's HTML"""
1618 return input_str.replace('"', '')
1623 return input_str.replace('"', '')
1619
1624
1620 data = {
1625 data = {
1621 'pref': pref,
1626 'pref': pref,
1622 'cls': quote_cleaner('issue-tracker-link'),
1627 'cls': quote_cleaner('issue-tracker-link'),
1623 'url': quote_cleaner(_url),
1628 'url': quote_cleaner(_url),
1624 'id-repr': issue_id,
1629 'id-repr': issue_id,
1625 'issue-prefix': entry['pref'],
1630 'issue-prefix': entry['pref'],
1626 'serv': entry['url'],
1631 'serv': entry['url'],
1627 'title': bleach.clean(desc, strip=True),
1632 'title': bleach.clean(desc, strip=True),
1628 'hovercard_url': hovercard_url
1633 'hovercard_url': hovercard_url
1629 }
1634 }
1630
1635
1631 if return_raw_data:
1636 if return_raw_data:
1632 return {
1637 return {
1633 'id': issue_id,
1638 'id': issue_id,
1634 'url': _url
1639 'url': _url
1635 }
1640 }
1636 return tmpl % data
1641 return tmpl % data
1637
1642
1638
1643
1639 def get_active_pattern_entries(repo_name):
1644 def get_active_pattern_entries(repo_name):
1640 repo = None
1645 repo = None
1641 if repo_name:
1646 if repo_name:
1642 # Retrieving repo_name to avoid invalid repo_name to explode on
1647 # Retrieving repo_name to avoid invalid repo_name to explode on
1643 # IssueTrackerSettingsModel but still passing invalid name further down
1648 # IssueTrackerSettingsModel but still passing invalid name further down
1644 repo = Repository.get_by_repo_name(repo_name, cache=True)
1649 repo = Repository.get_by_repo_name(repo_name, cache=True)
1645
1650
1646 settings_model = IssueTrackerSettingsModel(repo=repo)
1651 settings_model = IssueTrackerSettingsModel(repo=repo)
1647 active_entries = settings_model.get_settings(cache=True)
1652 active_entries = settings_model.get_settings(cache=True)
1648 return active_entries
1653 return active_entries
1649
1654
1650
1655
1651 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1656 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1652
1657
1658 allowed_link_formats = [
1659 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1660
1653
1661
1654 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1662 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1655
1663
1656 allowed_formats = ['html', 'rst', 'markdown',
1664 if link_format not in allowed_link_formats:
1657 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1658 if link_format not in allowed_formats:
1659 raise ValueError('Link format can be only one of:{} got {}'.format(
1665 raise ValueError('Link format can be only one of:{} got {}'.format(
1660 allowed_formats, link_format))
1666 allowed_link_formats, link_format))
1661
1667
1662 if active_entries is None:
1668 if active_entries is None:
1663 log.debug('Fetch active patterns for repo: %s', repo_name)
1669 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1664 active_entries = get_active_pattern_entries(repo_name)
1670 active_entries = get_active_pattern_entries(repo_name)
1665
1671
1666 issues_data = []
1672 issues_data = []
1667 new_text = text_string
1673 new_text = text_string
1668
1674
1669 log.debug('Got %s entries to process', len(active_entries))
1675 log.debug('Got %s entries to process', len(active_entries))
1670 for uid, entry in active_entries.items():
1676 for uid, entry in active_entries.items():
1671 log.debug('found issue tracker entry with uid %s', uid)
1677 log.debug('found issue tracker entry with uid %s', uid)
1672
1678
1673 if not (entry['pat'] and entry['url']):
1679 if not (entry['pat'] and entry['url']):
1674 log.debug('skipping due to missing data')
1680 log.debug('skipping due to missing data')
1675 continue
1681 continue
1676
1682
1677 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1683 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1678 uid, entry['pat'], entry['url'], entry['pref'])
1684 uid, entry['pat'], entry['url'], entry['pref'])
1679
1685
1680 if entry.get('pat_compiled'):
1686 if entry.get('pat_compiled'):
1681 pattern = entry['pat_compiled']
1687 pattern = entry['pat_compiled']
1682 else:
1688 else:
1683 try:
1689 try:
1684 pattern = re.compile(r'%s' % entry['pat'])
1690 pattern = re.compile(r'%s' % entry['pat'])
1685 except re.error:
1691 except re.error:
1686 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1692 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1687 continue
1693 continue
1688
1694
1689 data_func = partial(
1695 data_func = partial(
1690 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1696 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1691 return_raw_data=True)
1697 return_raw_data=True)
1692
1698
1693 for match_obj in pattern.finditer(text_string):
1699 for match_obj in pattern.finditer(text_string):
1694 issues_data.append(data_func(match_obj))
1700 issues_data.append(data_func(match_obj))
1695
1701
1696 url_func = partial(
1702 url_func = partial(
1697 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1703 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1698 link_format=link_format)
1704 link_format=link_format)
1699
1705
1700 new_text = pattern.sub(url_func, new_text)
1706 new_text = pattern.sub(url_func, new_text)
1701 log.debug('processed prefix:uid `%s`', uid)
1707 log.debug('processed prefix:uid `%s`', uid)
1702
1708
1703 # finally use global replace, eg !123 -> pr-link, those will not catch
1709 # finally use global replace, eg !123 -> pr-link, those will not catch
1704 # if already similar pattern exists
1710 # if already similar pattern exists
1705 server_url = '${scheme}://${netloc}'
1711 server_url = '${scheme}://${netloc}'
1706 pr_entry = {
1712 pr_entry = {
1707 'pref': '!',
1713 'pref': '!',
1708 'url': server_url + '/_admin/pull-requests/${id}',
1714 'url': server_url + '/_admin/pull-requests/${id}',
1709 'desc': 'Pull Request !${id}',
1715 'desc': 'Pull Request !${id}',
1710 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1716 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1711 }
1717 }
1712 pr_url_func = partial(
1718 pr_url_func = partial(
1713 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1719 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1714 link_format=link_format+'+hovercard')
1720 link_format=link_format+'+hovercard')
1715 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1721 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1716 log.debug('processed !pr pattern')
1722 log.debug('processed !pr pattern')
1717
1723
1718 return new_text, issues_data
1724 return new_text, issues_data
1719
1725
1720
1726
1721 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1727 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1728 issues_container=None):
1722 """
1729 """
1723 Parses given text message and makes proper links.
1730 Parses given text message and makes proper links.
1724 issues are linked to given issue-server, and rest is a commit link
1731 issues are linked to given issue-server, and rest is a commit link
1725 """
1732 """
1726
1733
1727 def escaper(_text):
1734 def escaper(_text):
1728 return _text.replace('<', '&lt;').replace('>', '&gt;')
1735 return _text.replace('<', '&lt;').replace('>', '&gt;')
1729
1736
1730 new_text = escaper(commit_text)
1737 new_text = escaper(commit_text)
1731
1738
1732 # extract http/https links and make them real urls
1739 # extract http/https links and make them real urls
1733 new_text = urlify_text(new_text, safe=False)
1740 new_text = urlify_text(new_text, safe=False)
1734
1741
1735 # urlify commits - extract commit ids and make link out of them, if we have
1742 # urlify commits - extract commit ids and make link out of them, if we have
1736 # the scope of repository present.
1743 # the scope of repository present.
1737 if repository:
1744 if repository:
1738 new_text = urlify_commits(new_text, repository)
1745 new_text = urlify_commits(new_text, repository)
1739
1746
1740 # process issue tracker patterns
1747 # process issue tracker patterns
1741 new_text, issues = process_patterns(new_text, repository or '',
1748 new_text, issues = process_patterns(new_text, repository or '',
1742 active_entries=active_pattern_entries)
1749 active_entries=active_pattern_entries)
1743
1750
1751 if issues_container is not None:
1752 issues_container.extend(issues)
1753
1744 return literal(new_text)
1754 return literal(new_text)
1745
1755
1746
1756
1747 def render_binary(repo_name, file_obj):
1757 def render_binary(repo_name, file_obj):
1748 """
1758 """
1749 Choose how to render a binary file
1759 Choose how to render a binary file
1750 """
1760 """
1751
1761
1752 # unicode
1762 # unicode
1753 filename = file_obj.name
1763 filename = file_obj.name
1754
1764
1755 # images
1765 # images
1756 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1766 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1757 if fnmatch.fnmatch(filename, pat=ext):
1767 if fnmatch.fnmatch(filename, pat=ext):
1758 src = route_path(
1768 src = route_path(
1759 'repo_file_raw', repo_name=repo_name,
1769 'repo_file_raw', repo_name=repo_name,
1760 commit_id=file_obj.commit.raw_id,
1770 commit_id=file_obj.commit.raw_id,
1761 f_path=file_obj.path)
1771 f_path=file_obj.path)
1762
1772
1763 return literal(
1773 return literal(
1764 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1774 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1765
1775
1766
1776
1767 def renderer_from_filename(filename, exclude=None):
1777 def renderer_from_filename(filename, exclude=None):
1768 """
1778 """
1769 choose a renderer based on filename, this works only for text based files
1779 choose a renderer based on filename, this works only for text based files
1770 """
1780 """
1771
1781
1772 # ipython
1782 # ipython
1773 for ext in ['*.ipynb']:
1783 for ext in ['*.ipynb']:
1774 if fnmatch.fnmatch(filename, pat=ext):
1784 if fnmatch.fnmatch(filename, pat=ext):
1775 return 'jupyter'
1785 return 'jupyter'
1776
1786
1777 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1787 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1778 if is_markup:
1788 if is_markup:
1779 return is_markup
1789 return is_markup
1780 return None
1790 return None
1781
1791
1782
1792
1783 def render(source, renderer='rst', mentions=False, relative_urls=None,
1793 def render(source, renderer='rst', mentions=False, relative_urls=None,
1784 repo_name=None, active_pattern_entries=None):
1794 repo_name=None, active_pattern_entries=None, issues_container=None):
1785
1795
1786 def maybe_convert_relative_links(html_source):
1796 def maybe_convert_relative_links(html_source):
1787 if relative_urls:
1797 if relative_urls:
1788 return relative_links(html_source, relative_urls)
1798 return relative_links(html_source, relative_urls)
1789 return html_source
1799 return html_source
1790
1800
1791 if renderer == 'plain':
1801 if renderer == 'plain':
1792 return literal(
1802 return literal(
1793 MarkupRenderer.plain(source, leading_newline=False))
1803 MarkupRenderer.plain(source, leading_newline=False))
1794
1804
1795 elif renderer == 'rst':
1805 elif renderer == 'rst':
1796 if repo_name:
1806 if repo_name:
1797 # process patterns on comments if we pass in repo name
1807 # process patterns on comments if we pass in repo name
1798 source, issues = process_patterns(
1808 source, issues = process_patterns(
1799 source, repo_name, link_format='rst',
1809 source, repo_name, link_format='rst',
1800 active_entries=active_pattern_entries)
1810 active_entries=active_pattern_entries)
1811 if issues_container is not None:
1812 issues_container.extend(issues)
1801
1813
1802 return literal(
1814 return literal(
1803 '<div class="rst-block">%s</div>' %
1815 '<div class="rst-block">%s</div>' %
1804 maybe_convert_relative_links(
1816 maybe_convert_relative_links(
1805 MarkupRenderer.rst(source, mentions=mentions)))
1817 MarkupRenderer.rst(source, mentions=mentions)))
1806
1818
1807 elif renderer == 'markdown':
1819 elif renderer == 'markdown':
1808 if repo_name:
1820 if repo_name:
1809 # process patterns on comments if we pass in repo name
1821 # process patterns on comments if we pass in repo name
1810 source, issues = process_patterns(
1822 source, issues = process_patterns(
1811 source, repo_name, link_format='markdown',
1823 source, repo_name, link_format='markdown',
1812 active_entries=active_pattern_entries)
1824 active_entries=active_pattern_entries)
1825 if issues_container is not None:
1826 issues_container.extend(issues)
1813
1827
1814 return literal(
1828 return literal(
1815 '<div class="markdown-block">%s</div>' %
1829 '<div class="markdown-block">%s</div>' %
1816 maybe_convert_relative_links(
1830 maybe_convert_relative_links(
1817 MarkupRenderer.markdown(source, flavored=True,
1831 MarkupRenderer.markdown(source, flavored=True,
1818 mentions=mentions)))
1832 mentions=mentions)))
1819
1833
1820 elif renderer == 'jupyter':
1834 elif renderer == 'jupyter':
1821 return literal(
1835 return literal(
1822 '<div class="ipynb">%s</div>' %
1836 '<div class="ipynb">%s</div>' %
1823 maybe_convert_relative_links(
1837 maybe_convert_relative_links(
1824 MarkupRenderer.jupyter(source)))
1838 MarkupRenderer.jupyter(source)))
1825
1839
1826 # None means just show the file-source
1840 # None means just show the file-source
1827 return None
1841 return None
1828
1842
1829
1843
1830 def commit_status(repo, commit_id):
1844 def commit_status(repo, commit_id):
1831 return ChangesetStatusModel().get_status(repo, commit_id)
1845 return ChangesetStatusModel().get_status(repo, commit_id)
1832
1846
1833
1847
1834 def commit_status_lbl(commit_status):
1848 def commit_status_lbl(commit_status):
1835 return dict(ChangesetStatus.STATUSES).get(commit_status)
1849 return dict(ChangesetStatus.STATUSES).get(commit_status)
1836
1850
1837
1851
1838 def commit_time(repo_name, commit_id):
1852 def commit_time(repo_name, commit_id):
1839 repo = Repository.get_by_repo_name(repo_name)
1853 repo = Repository.get_by_repo_name(repo_name)
1840 commit = repo.get_commit(commit_id=commit_id)
1854 commit = repo.get_commit(commit_id=commit_id)
1841 return commit.date
1855 return commit.date
1842
1856
1843
1857
1844 def get_permission_name(key):
1858 def get_permission_name(key):
1845 return dict(Permission.PERMS).get(key)
1859 return dict(Permission.PERMS).get(key)
1846
1860
1847
1861
1848 def journal_filter_help(request):
1862 def journal_filter_help(request):
1849 _ = request.translate
1863 _ = request.translate
1850 from rhodecode.lib.audit_logger import ACTIONS
1864 from rhodecode.lib.audit_logger import ACTIONS
1851 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1865 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1852
1866
1853 return _(
1867 return _(
1854 'Example filter terms:\n' +
1868 'Example filter terms:\n' +
1855 ' repository:vcs\n' +
1869 ' repository:vcs\n' +
1856 ' username:marcin\n' +
1870 ' username:marcin\n' +
1857 ' username:(NOT marcin)\n' +
1871 ' username:(NOT marcin)\n' +
1858 ' action:*push*\n' +
1872 ' action:*push*\n' +
1859 ' ip:127.0.0.1\n' +
1873 ' ip:127.0.0.1\n' +
1860 ' date:20120101\n' +
1874 ' date:20120101\n' +
1861 ' date:[20120101100000 TO 20120102]\n' +
1875 ' date:[20120101100000 TO 20120102]\n' +
1862 '\n' +
1876 '\n' +
1863 'Actions: {actions}\n' +
1877 'Actions: {actions}\n' +
1864 '\n' +
1878 '\n' +
1865 'Generate wildcards using \'*\' character:\n' +
1879 'Generate wildcards using \'*\' character:\n' +
1866 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1880 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1867 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1881 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1868 '\n' +
1882 '\n' +
1869 'Optional AND / OR operators in queries\n' +
1883 'Optional AND / OR operators in queries\n' +
1870 ' "repository:vcs OR repository:test"\n' +
1884 ' "repository:vcs OR repository:test"\n' +
1871 ' "username:test AND repository:test*"\n'
1885 ' "username:test AND repository:test*"\n'
1872 ).format(actions=actions)
1886 ).format(actions=actions)
1873
1887
1874
1888
1875 def not_mapped_error(repo_name):
1889 def not_mapped_error(repo_name):
1876 from rhodecode.translation import _
1890 from rhodecode.translation import _
1877 flash(_('%s repository is not mapped to db perhaps'
1891 flash(_('%s repository is not mapped to db perhaps'
1878 ' it was created or renamed from the filesystem'
1892 ' it was created or renamed from the filesystem'
1879 ' please run the application again'
1893 ' please run the application again'
1880 ' in order to rescan repositories') % repo_name, category='error')
1894 ' in order to rescan repositories') % repo_name, category='error')
1881
1895
1882
1896
1883 def ip_range(ip_addr):
1897 def ip_range(ip_addr):
1884 from rhodecode.model.db import UserIpMap
1898 from rhodecode.model.db import UserIpMap
1885 s, e = UserIpMap._get_ip_range(ip_addr)
1899 s, e = UserIpMap._get_ip_range(ip_addr)
1886 return '%s - %s' % (s, e)
1900 return '%s - %s' % (s, e)
1887
1901
1888
1902
1889 def form(url, method='post', needs_csrf_token=True, **attrs):
1903 def form(url, method='post', needs_csrf_token=True, **attrs):
1890 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1904 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1891 if method.lower() != 'get' and needs_csrf_token:
1905 if method.lower() != 'get' and needs_csrf_token:
1892 raise Exception(
1906 raise Exception(
1893 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1907 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1894 'CSRF token. If the endpoint does not require such token you can ' +
1908 'CSRF token. If the endpoint does not require such token you can ' +
1895 'explicitly set the parameter needs_csrf_token to false.')
1909 'explicitly set the parameter needs_csrf_token to false.')
1896
1910
1897 return insecure_form(url, method=method, **attrs)
1911 return insecure_form(url, method=method, **attrs)
1898
1912
1899
1913
1900 def secure_form(form_url, method="POST", multipart=False, **attrs):
1914 def secure_form(form_url, method="POST", multipart=False, **attrs):
1901 """Start a form tag that points the action to an url. This
1915 """Start a form tag that points the action to an url. This
1902 form tag will also include the hidden field containing
1916 form tag will also include the hidden field containing
1903 the auth token.
1917 the auth token.
1904
1918
1905 The url options should be given either as a string, or as a
1919 The url options should be given either as a string, or as a
1906 ``url()`` function. The method for the form defaults to POST.
1920 ``url()`` function. The method for the form defaults to POST.
1907
1921
1908 Options:
1922 Options:
1909
1923
1910 ``multipart``
1924 ``multipart``
1911 If set to True, the enctype is set to "multipart/form-data".
1925 If set to True, the enctype is set to "multipart/form-data".
1912 ``method``
1926 ``method``
1913 The method to use when submitting the form, usually either
1927 The method to use when submitting the form, usually either
1914 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1928 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1915 hidden input with name _method is added to simulate the verb
1929 hidden input with name _method is added to simulate the verb
1916 over POST.
1930 over POST.
1917
1931
1918 """
1932 """
1919
1933
1920 if 'request' in attrs:
1934 if 'request' in attrs:
1921 session = attrs['request'].session
1935 session = attrs['request'].session
1922 del attrs['request']
1936 del attrs['request']
1923 else:
1937 else:
1924 raise ValueError(
1938 raise ValueError(
1925 'Calling this form requires request= to be passed as argument')
1939 'Calling this form requires request= to be passed as argument')
1926
1940
1927 _form = insecure_form(form_url, method, multipart, **attrs)
1941 _form = insecure_form(form_url, method, multipart, **attrs)
1928 token = literal(
1942 token = literal(
1929 '<input type="hidden" name="{}" value="{}">'.format(
1943 '<input type="hidden" name="{}" value="{}">'.format(
1930 csrf_token_key, get_csrf_token(session)))
1944 csrf_token_key, get_csrf_token(session)))
1931
1945
1932 return literal("%s\n%s" % (_form, token))
1946 return literal("%s\n%s" % (_form, token))
1933
1947
1934
1948
1935 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1949 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1936 select_html = select(name, selected, options, **attrs)
1950 select_html = select(name, selected, options, **attrs)
1937
1951
1938 select2 = """
1952 select2 = """
1939 <script>
1953 <script>
1940 $(document).ready(function() {
1954 $(document).ready(function() {
1941 $('#%s').select2({
1955 $('#%s').select2({
1942 containerCssClass: 'drop-menu %s',
1956 containerCssClass: 'drop-menu %s',
1943 dropdownCssClass: 'drop-menu-dropdown',
1957 dropdownCssClass: 'drop-menu-dropdown',
1944 dropdownAutoWidth: true%s
1958 dropdownAutoWidth: true%s
1945 });
1959 });
1946 });
1960 });
1947 </script>
1961 </script>
1948 """
1962 """
1949
1963
1950 filter_option = """,
1964 filter_option = """,
1951 minimumResultsForSearch: -1
1965 minimumResultsForSearch: -1
1952 """
1966 """
1953 input_id = attrs.get('id') or name
1967 input_id = attrs.get('id') or name
1954 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1968 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1955 filter_enabled = "" if enable_filter else filter_option
1969 filter_enabled = "" if enable_filter else filter_option
1956 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1970 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1957
1971
1958 return literal(select_html+select_script)
1972 return literal(select_html+select_script)
1959
1973
1960
1974
1961 def get_visual_attr(tmpl_context_var, attr_name):
1975 def get_visual_attr(tmpl_context_var, attr_name):
1962 """
1976 """
1963 A safe way to get a variable from visual variable of template context
1977 A safe way to get a variable from visual variable of template context
1964
1978
1965 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1979 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1966 :param attr_name: name of the attribute we fetch from the c.visual
1980 :param attr_name: name of the attribute we fetch from the c.visual
1967 """
1981 """
1968 visual = getattr(tmpl_context_var, 'visual', None)
1982 visual = getattr(tmpl_context_var, 'visual', None)
1969 if not visual:
1983 if not visual:
1970 return
1984 return
1971 else:
1985 else:
1972 return getattr(visual, attr_name, None)
1986 return getattr(visual, attr_name, None)
1973
1987
1974
1988
1975 def get_last_path_part(file_node):
1989 def get_last_path_part(file_node):
1976 if not file_node.path:
1990 if not file_node.path:
1977 return u'/'
1991 return u'/'
1978
1992
1979 path = safe_unicode(file_node.path.split('/')[-1])
1993 path = safe_unicode(file_node.path.split('/')[-1])
1980 return u'../' + path
1994 return u'../' + path
1981
1995
1982
1996
1983 def route_url(*args, **kwargs):
1997 def route_url(*args, **kwargs):
1984 """
1998 """
1985 Wrapper around pyramids `route_url` (fully qualified url) function.
1999 Wrapper around pyramids `route_url` (fully qualified url) function.
1986 """
2000 """
1987 req = get_current_request()
2001 req = get_current_request()
1988 return req.route_url(*args, **kwargs)
2002 return req.route_url(*args, **kwargs)
1989
2003
1990
2004
1991 def route_path(*args, **kwargs):
2005 def route_path(*args, **kwargs):
1992 """
2006 """
1993 Wrapper around pyramids `route_path` function.
2007 Wrapper around pyramids `route_path` function.
1994 """
2008 """
1995 req = get_current_request()
2009 req = get_current_request()
1996 return req.route_path(*args, **kwargs)
2010 return req.route_path(*args, **kwargs)
1997
2011
1998
2012
1999 def route_path_or_none(*args, **kwargs):
2013 def route_path_or_none(*args, **kwargs):
2000 try:
2014 try:
2001 return route_path(*args, **kwargs)
2015 return route_path(*args, **kwargs)
2002 except KeyError:
2016 except KeyError:
2003 return None
2017 return None
2004
2018
2005
2019
2006 def current_route_path(request, **kw):
2020 def current_route_path(request, **kw):
2007 new_args = request.GET.mixed()
2021 new_args = request.GET.mixed()
2008 new_args.update(kw)
2022 new_args.update(kw)
2009 return request.current_route_path(_query=new_args)
2023 return request.current_route_path(_query=new_args)
2010
2024
2011
2025
2012 def curl_api_example(method, args):
2026 def curl_api_example(method, args):
2013 args_json = json.dumps(OrderedDict([
2027 args_json = json.dumps(OrderedDict([
2014 ('id', 1),
2028 ('id', 1),
2015 ('auth_token', 'SECRET'),
2029 ('auth_token', 'SECRET'),
2016 ('method', method),
2030 ('method', method),
2017 ('args', args)
2031 ('args', args)
2018 ]))
2032 ]))
2019
2033
2020 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2034 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2021 api_url=route_url('apiv2'),
2035 api_url=route_url('apiv2'),
2022 args_json=args_json
2036 args_json=args_json
2023 )
2037 )
2024
2038
2025
2039
2026 def api_call_example(method, args):
2040 def api_call_example(method, args):
2027 """
2041 """
2028 Generates an API call example via CURL
2042 Generates an API call example via CURL
2029 """
2043 """
2030 curl_call = curl_api_example(method, args)
2044 curl_call = curl_api_example(method, args)
2031
2045
2032 return literal(
2046 return literal(
2033 curl_call +
2047 curl_call +
2034 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2048 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2035 "and needs to be of `api calls` role."
2049 "and needs to be of `api calls` role."
2036 .format(token_url=route_url('my_account_auth_tokens')))
2050 .format(token_url=route_url('my_account_auth_tokens')))
2037
2051
2038
2052
2039 def notification_description(notification, request):
2053 def notification_description(notification, request):
2040 """
2054 """
2041 Generate notification human readable description based on notification type
2055 Generate notification human readable description based on notification type
2042 """
2056 """
2043 from rhodecode.model.notification import NotificationModel
2057 from rhodecode.model.notification import NotificationModel
2044 return NotificationModel().make_description(
2058 return NotificationModel().make_description(
2045 notification, translate=request.translate)
2059 notification, translate=request.translate)
2046
2060
2047
2061
2048 def go_import_header(request, db_repo=None):
2062 def go_import_header(request, db_repo=None):
2049 """
2063 """
2050 Creates a header for go-import functionality in Go Lang
2064 Creates a header for go-import functionality in Go Lang
2051 """
2065 """
2052
2066
2053 if not db_repo:
2067 if not db_repo:
2054 return
2068 return
2055 if 'go-get' not in request.GET:
2069 if 'go-get' not in request.GET:
2056 return
2070 return
2057
2071
2058 clone_url = db_repo.clone_url()
2072 clone_url = db_repo.clone_url()
2059 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2073 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2060 # we have a repo and go-get flag,
2074 # we have a repo and go-get flag,
2061 return literal('<meta name="go-import" content="{} {} {}">'.format(
2075 return literal('<meta name="go-import" content="{} {} {}">'.format(
2062 prefix, db_repo.repo_type, clone_url))
2076 prefix, db_repo.repo_type, clone_url))
2063
2077
2064
2078
2065 def reviewer_as_json(*args, **kwargs):
2079 def reviewer_as_json(*args, **kwargs):
2066 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2080 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2067 return _reviewer_as_json(*args, **kwargs)
2081 return _reviewer_as_json(*args, **kwargs)
2068
2082
2069
2083
2070 def get_repo_view_type(request):
2084 def get_repo_view_type(request):
2071 route_name = request.matched_route.name
2085 route_name = request.matched_route.name
2072 route_to_view_type = {
2086 route_to_view_type = {
2073 'repo_changelog': 'commits',
2087 'repo_changelog': 'commits',
2074 'repo_commits': 'commits',
2088 'repo_commits': 'commits',
2075 'repo_files': 'files',
2089 'repo_files': 'files',
2076 'repo_summary': 'summary',
2090 'repo_summary': 'summary',
2077 'repo_commit': 'commit'
2091 'repo_commit': 'commit'
2078 }
2092 }
2079
2093
2080 return route_to_view_type.get(route_name)
2094 return route_to_view_type.get(route_name)
2081
2095
2082
2096
2083 def is_active(menu_entry, selected):
2097 def is_active(menu_entry, selected):
2084 """
2098 """
2085 Returns active class for selecting menus in templates
2099 Returns active class for selecting menus in templates
2086 <li class=${h.is_active('settings', current_active)}></li>
2100 <li class=${h.is_active('settings', current_active)}></li>
2087 """
2101 """
2088 if not isinstance(menu_entry, list):
2102 if not isinstance(menu_entry, list):
2089 menu_entry = [menu_entry]
2103 menu_entry = [menu_entry]
2090
2104
2091 if selected in menu_entry:
2105 if selected in menu_entry:
2092 return "active"
2106 return "active"
@@ -1,840 +1,855 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 import datetime
24 import datetime
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28 import collections
28 import collections
29
29
30 from pyramid.threadlocal import get_current_registry, get_current_request
30 from pyramid.threadlocal import get_current_registry, get_current_request
31 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.expression import null
32 from sqlalchemy.sql.functions import coalesce
32 from sqlalchemy.sql.functions import coalesce
33
33
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 from rhodecode.lib import audit_logger
35 from rhodecode.lib import audit_logger
36 from rhodecode.lib.exceptions import CommentVersionMismatch
36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 from rhodecode.model import BaseModel
38 from rhodecode.model import BaseModel
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 ChangesetComment,
40 ChangesetComment,
41 User,
41 User,
42 Notification,
42 Notification,
43 PullRequest,
43 PullRequest,
44 AttributeDict,
44 AttributeDict,
45 ChangesetCommentHistory,
45 ChangesetCommentHistory,
46 )
46 )
47 from rhodecode.model.notification import NotificationModel
47 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.settings import VcsSettingsModel
49 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.notification import EmailNotificationModel
50 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.validation_schema.schemas import comment_schema
51 from rhodecode.model.validation_schema.schemas import comment_schema
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class CommentsModel(BaseModel):
57 class CommentsModel(BaseModel):
58
58
59 cls = ChangesetComment
59 cls = ChangesetComment
60
60
61 DIFF_CONTEXT_BEFORE = 3
61 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_AFTER = 3
62 DIFF_CONTEXT_AFTER = 3
63
63
64 def __get_commit_comment(self, changeset_comment):
64 def __get_commit_comment(self, changeset_comment):
65 return self._get_instance(ChangesetComment, changeset_comment)
65 return self._get_instance(ChangesetComment, changeset_comment)
66
66
67 def __get_pull_request(self, pull_request):
67 def __get_pull_request(self, pull_request):
68 return self._get_instance(PullRequest, pull_request)
68 return self._get_instance(PullRequest, pull_request)
69
69
70 def _extract_mentions(self, s):
70 def _extract_mentions(self, s):
71 user_objects = []
71 user_objects = []
72 for username in extract_mentioned_users(s):
72 for username in extract_mentioned_users(s):
73 user_obj = User.get_by_username(username, case_insensitive=True)
73 user_obj = User.get_by_username(username, case_insensitive=True)
74 if user_obj:
74 if user_obj:
75 user_objects.append(user_obj)
75 user_objects.append(user_obj)
76 return user_objects
76 return user_objects
77
77
78 def _get_renderer(self, global_renderer='rst', request=None):
78 def _get_renderer(self, global_renderer='rst', request=None):
79 request = request or get_current_request()
79 request = request or get_current_request()
80
80
81 try:
81 try:
82 global_renderer = request.call_context.visual.default_renderer
82 global_renderer = request.call_context.visual.default_renderer
83 except AttributeError:
83 except AttributeError:
84 log.debug("Renderer not set, falling back "
84 log.debug("Renderer not set, falling back "
85 "to default renderer '%s'", global_renderer)
85 "to default renderer '%s'", global_renderer)
86 except Exception:
86 except Exception:
87 log.error(traceback.format_exc())
87 log.error(traceback.format_exc())
88 return global_renderer
88 return global_renderer
89
89
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 # group by versions, and count until, and display objects
91 # group by versions, and count until, and display objects
92
92
93 comment_groups = collections.defaultdict(list)
93 comment_groups = collections.defaultdict(list)
94 [comment_groups[
94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95 _co.pull_request_version_id].append(_co) for _co in comments]
96
95
97 def yield_comments(pos):
96 def yield_comments(pos):
98 for co in comment_groups[pos]:
97 for co in comment_groups[pos]:
99 yield co
98 yield co
100
99
101 comment_versions = collections.defaultdict(
100 comment_versions = collections.defaultdict(
102 lambda: collections.defaultdict(list))
101 lambda: collections.defaultdict(list))
103 prev_prvid = -1
102 prev_prvid = -1
104 # fake last entry with None, to aggregate on "latest" version which
103 # fake last entry with None, to aggregate on "latest" version which
105 # doesn't have an pull_request_version_id
104 # doesn't have an pull_request_version_id
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
105 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
107 prvid = ver.pull_request_version_id
106 prvid = ver.pull_request_version_id
108 if prev_prvid == -1:
107 if prev_prvid == -1:
109 prev_prvid = prvid
108 prev_prvid = prvid
110
109
111 for co in yield_comments(prvid):
110 for co in yield_comments(prvid):
112 comment_versions[prvid]['at'].append(co)
111 comment_versions[prvid]['at'].append(co)
113
112
114 # save until
113 # save until
115 current = comment_versions[prvid]['at']
114 current = comment_versions[prvid]['at']
116 prev_until = comment_versions[prev_prvid]['until']
115 prev_until = comment_versions[prev_prvid]['until']
117 cur_until = prev_until + current
116 cur_until = prev_until + current
118 comment_versions[prvid]['until'].extend(cur_until)
117 comment_versions[prvid]['until'].extend(cur_until)
119
118
120 # save outdated
119 # save outdated
121 if inline:
120 if inline:
122 outdated = [x for x in cur_until
121 outdated = [x for x in cur_until
123 if x.outdated_at_version(show_version)]
122 if x.outdated_at_version(show_version)]
124 else:
123 else:
125 outdated = [x for x in cur_until
124 outdated = [x for x in cur_until
126 if x.older_than_version(show_version)]
125 if x.older_than_version(show_version)]
127 display = [x for x in cur_until if x not in outdated]
126 display = [x for x in cur_until if x not in outdated]
128
127
129 comment_versions[prvid]['outdated'] = outdated
128 comment_versions[prvid]['outdated'] = outdated
130 comment_versions[prvid]['display'] = display
129 comment_versions[prvid]['display'] = display
131
130
132 prev_prvid = prvid
131 prev_prvid = prvid
133
132
134 return comment_versions
133 return comment_versions
135
134
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
135 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
137 qry = Session().query(ChangesetComment) \
136 qry = Session().query(ChangesetComment) \
138 .filter(ChangesetComment.repo == repo)
137 .filter(ChangesetComment.repo == repo)
139
138
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
139 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
140 qry = qry.filter(ChangesetComment.comment_type == comment_type)
142
141
143 if user:
142 if user:
144 user = self._get_user(user)
143 user = self._get_user(user)
145 if user:
144 if user:
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
145 qry = qry.filter(ChangesetComment.user_id == user.user_id)
147
146
148 if commit_id:
147 if commit_id:
149 qry = qry.filter(ChangesetComment.revision == commit_id)
148 qry = qry.filter(ChangesetComment.revision == commit_id)
150
149
151 qry = qry.order_by(ChangesetComment.created_on)
150 qry = qry.order_by(ChangesetComment.created_on)
152 return qry.all()
151 return qry.all()
153
152
154 def get_repository_unresolved_todos(self, repo):
153 def get_repository_unresolved_todos(self, repo):
155 todos = Session().query(ChangesetComment) \
154 todos = Session().query(ChangesetComment) \
156 .filter(ChangesetComment.repo == repo) \
155 .filter(ChangesetComment.repo == repo) \
157 .filter(ChangesetComment.resolved_by == None) \
156 .filter(ChangesetComment.resolved_by == None) \
158 .filter(ChangesetComment.comment_type
157 .filter(ChangesetComment.comment_type
159 == ChangesetComment.COMMENT_TYPE_TODO)
158 == ChangesetComment.COMMENT_TYPE_TODO)
160 todos = todos.all()
159 todos = todos.all()
161
160
162 return todos
161 return todos
163
162
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
165
164
166 todos = Session().query(ChangesetComment) \
165 todos = Session().query(ChangesetComment) \
167 .filter(ChangesetComment.pull_request == pull_request) \
166 .filter(ChangesetComment.pull_request == pull_request) \
168 .filter(ChangesetComment.resolved_by == None) \
167 .filter(ChangesetComment.resolved_by == None) \
169 .filter(ChangesetComment.comment_type
168 .filter(ChangesetComment.comment_type
170 == ChangesetComment.COMMENT_TYPE_TODO)
169 == ChangesetComment.COMMENT_TYPE_TODO)
171
170
172 if not show_outdated:
171 if not show_outdated:
173 todos = todos.filter(
172 todos = todos.filter(
174 coalesce(ChangesetComment.display_state, '') !=
173 coalesce(ChangesetComment.display_state, '') !=
175 ChangesetComment.COMMENT_OUTDATED)
174 ChangesetComment.COMMENT_OUTDATED)
176
175
177 todos = todos.all()
176 todos = todos.all()
178
177
179 return todos
178 return todos
180
179
181 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
182
181
183 todos = Session().query(ChangesetComment) \
182 todos = Session().query(ChangesetComment) \
184 .filter(ChangesetComment.pull_request == pull_request) \
183 .filter(ChangesetComment.pull_request == pull_request) \
185 .filter(ChangesetComment.resolved_by != None) \
184 .filter(ChangesetComment.resolved_by != None) \
186 .filter(ChangesetComment.comment_type
185 .filter(ChangesetComment.comment_type
187 == ChangesetComment.COMMENT_TYPE_TODO)
186 == ChangesetComment.COMMENT_TYPE_TODO)
188
187
189 if not show_outdated:
188 if not show_outdated:
190 todos = todos.filter(
189 todos = todos.filter(
191 coalesce(ChangesetComment.display_state, '') !=
190 coalesce(ChangesetComment.display_state, '') !=
192 ChangesetComment.COMMENT_OUTDATED)
191 ChangesetComment.COMMENT_OUTDATED)
193
192
194 todos = todos.all()
193 todos = todos.all()
195
194
196 return todos
195 return todos
197
196
198 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
199
198
200 todos = Session().query(ChangesetComment) \
199 todos = Session().query(ChangesetComment) \
201 .filter(ChangesetComment.revision == commit_id) \
200 .filter(ChangesetComment.revision == commit_id) \
202 .filter(ChangesetComment.resolved_by == None) \
201 .filter(ChangesetComment.resolved_by == None) \
203 .filter(ChangesetComment.comment_type
202 .filter(ChangesetComment.comment_type
204 == ChangesetComment.COMMENT_TYPE_TODO)
203 == ChangesetComment.COMMENT_TYPE_TODO)
205
204
206 if not show_outdated:
205 if not show_outdated:
207 todos = todos.filter(
206 todos = todos.filter(
208 coalesce(ChangesetComment.display_state, '') !=
207 coalesce(ChangesetComment.display_state, '') !=
209 ChangesetComment.COMMENT_OUTDATED)
208 ChangesetComment.COMMENT_OUTDATED)
210
209
211 todos = todos.all()
210 todos = todos.all()
212
211
213 return todos
212 return todos
214
213
215 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
216
215
217 todos = Session().query(ChangesetComment) \
216 todos = Session().query(ChangesetComment) \
218 .filter(ChangesetComment.revision == commit_id) \
217 .filter(ChangesetComment.revision == commit_id) \
219 .filter(ChangesetComment.resolved_by != None) \
218 .filter(ChangesetComment.resolved_by != None) \
220 .filter(ChangesetComment.comment_type
219 .filter(ChangesetComment.comment_type
221 == ChangesetComment.COMMENT_TYPE_TODO)
220 == ChangesetComment.COMMENT_TYPE_TODO)
222
221
223 if not show_outdated:
222 if not show_outdated:
224 todos = todos.filter(
223 todos = todos.filter(
225 coalesce(ChangesetComment.display_state, '') !=
224 coalesce(ChangesetComment.display_state, '') !=
226 ChangesetComment.COMMENT_OUTDATED)
225 ChangesetComment.COMMENT_OUTDATED)
227
226
228 todos = todos.all()
227 todos = todos.all()
229
228
230 return todos
229 return todos
231
230
232 def _log_audit_action(self, action, action_data, auth_user, comment):
231 def _log_audit_action(self, action, action_data, auth_user, comment):
233 audit_logger.store(
232 audit_logger.store(
234 action=action,
233 action=action,
235 action_data=action_data,
234 action_data=action_data,
236 user=auth_user,
235 user=auth_user,
237 repo=comment.repo)
236 repo=comment.repo)
238
237
239 def create(self, text, repo, user, commit_id=None, pull_request=None,
238 def create(self, text, repo, user, commit_id=None, pull_request=None,
240 f_path=None, line_no=None, status_change=None,
239 f_path=None, line_no=None, status_change=None,
241 status_change_type=None, comment_type=None,
240 status_change_type=None, comment_type=None,
242 resolves_comment_id=None, closing_pr=False, send_email=True,
241 resolves_comment_id=None, closing_pr=False, send_email=True,
243 renderer=None, auth_user=None, extra_recipients=None):
242 renderer=None, auth_user=None, extra_recipients=None):
244 """
243 """
245 Creates new comment for commit or pull request.
244 Creates new comment for commit or pull request.
246 IF status_change is not none this comment is associated with a
245 IF status_change is not none this comment is associated with a
247 status change of commit or commit associated with pull request
246 status change of commit or commit associated with pull request
248
247
249 :param text:
248 :param text:
250 :param repo:
249 :param repo:
251 :param user:
250 :param user:
252 :param commit_id:
251 :param commit_id:
253 :param pull_request:
252 :param pull_request:
254 :param f_path:
253 :param f_path:
255 :param line_no:
254 :param line_no:
256 :param status_change: Label for status change
255 :param status_change: Label for status change
257 :param comment_type: Type of comment
256 :param comment_type: Type of comment
258 :param resolves_comment_id: id of comment which this one will resolve
257 :param resolves_comment_id: id of comment which this one will resolve
259 :param status_change_type: type of status change
258 :param status_change_type: type of status change
260 :param closing_pr:
259 :param closing_pr:
261 :param send_email:
260 :param send_email:
262 :param renderer: pick renderer for this comment
261 :param renderer: pick renderer for this comment
263 :param auth_user: current authenticated user calling this method
262 :param auth_user: current authenticated user calling this method
264 :param extra_recipients: list of extra users to be added to recipients
263 :param extra_recipients: list of extra users to be added to recipients
265 """
264 """
266
265
267 if not text:
266 if not text:
268 log.warning('Missing text for comment, skipping...')
267 log.warning('Missing text for comment, skipping...')
269 return
268 return
270 request = get_current_request()
269 request = get_current_request()
271 _ = request.translate
270 _ = request.translate
272
271
273 if not renderer:
272 if not renderer:
274 renderer = self._get_renderer(request=request)
273 renderer = self._get_renderer(request=request)
275
274
276 repo = self._get_repo(repo)
275 repo = self._get_repo(repo)
277 user = self._get_user(user)
276 user = self._get_user(user)
278 auth_user = auth_user or user
277 auth_user = auth_user or user
279
278
280 schema = comment_schema.CommentSchema()
279 schema = comment_schema.CommentSchema()
281 validated_kwargs = schema.deserialize(dict(
280 validated_kwargs = schema.deserialize(dict(
282 comment_body=text,
281 comment_body=text,
283 comment_type=comment_type,
282 comment_type=comment_type,
284 comment_file=f_path,
283 comment_file=f_path,
285 comment_line=line_no,
284 comment_line=line_no,
286 renderer_type=renderer,
285 renderer_type=renderer,
287 status_change=status_change_type,
286 status_change=status_change_type,
288 resolves_comment_id=resolves_comment_id,
287 resolves_comment_id=resolves_comment_id,
289 repo=repo.repo_id,
288 repo=repo.repo_id,
290 user=user.user_id,
289 user=user.user_id,
291 ))
290 ))
292
291
293 comment = ChangesetComment()
292 comment = ChangesetComment()
294 comment.renderer = validated_kwargs['renderer_type']
293 comment.renderer = validated_kwargs['renderer_type']
295 comment.text = validated_kwargs['comment_body']
294 comment.text = validated_kwargs['comment_body']
296 comment.f_path = validated_kwargs['comment_file']
295 comment.f_path = validated_kwargs['comment_file']
297 comment.line_no = validated_kwargs['comment_line']
296 comment.line_no = validated_kwargs['comment_line']
298 comment.comment_type = validated_kwargs['comment_type']
297 comment.comment_type = validated_kwargs['comment_type']
299
298
300 comment.repo = repo
299 comment.repo = repo
301 comment.author = user
300 comment.author = user
302 resolved_comment = self.__get_commit_comment(
301 resolved_comment = self.__get_commit_comment(
303 validated_kwargs['resolves_comment_id'])
302 validated_kwargs['resolves_comment_id'])
304 # check if the comment actually belongs to this PR
303 # check if the comment actually belongs to this PR
305 if resolved_comment and resolved_comment.pull_request and \
304 if resolved_comment and resolved_comment.pull_request and \
306 resolved_comment.pull_request != pull_request:
305 resolved_comment.pull_request != pull_request:
307 log.warning('Comment tried to resolved unrelated todo comment: %s',
306 log.warning('Comment tried to resolved unrelated todo comment: %s',
308 resolved_comment)
307 resolved_comment)
309 # comment not bound to this pull request, forbid
308 # comment not bound to this pull request, forbid
310 resolved_comment = None
309 resolved_comment = None
311
310
312 elif resolved_comment and resolved_comment.repo and \
311 elif resolved_comment and resolved_comment.repo and \
313 resolved_comment.repo != repo:
312 resolved_comment.repo != repo:
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
313 log.warning('Comment tried to resolved unrelated todo comment: %s',
315 resolved_comment)
314 resolved_comment)
316 # comment not bound to this repo, forbid
315 # comment not bound to this repo, forbid
317 resolved_comment = None
316 resolved_comment = None
318
317
319 comment.resolved_comment = resolved_comment
318 comment.resolved_comment = resolved_comment
320
319
321 pull_request_id = pull_request
320 pull_request_id = pull_request
322
321
323 commit_obj = None
322 commit_obj = None
324 pull_request_obj = None
323 pull_request_obj = None
325
324
326 if commit_id:
325 if commit_id:
327 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
326 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
328 # do a lookup, so we don't pass something bad here
327 # do a lookup, so we don't pass something bad here
329 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
328 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
330 comment.revision = commit_obj.raw_id
329 comment.revision = commit_obj.raw_id
331
330
332 elif pull_request_id:
331 elif pull_request_id:
333 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
332 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
334 pull_request_obj = self.__get_pull_request(pull_request_id)
333 pull_request_obj = self.__get_pull_request(pull_request_id)
335 comment.pull_request = pull_request_obj
334 comment.pull_request = pull_request_obj
336 else:
335 else:
337 raise Exception('Please specify commit or pull_request_id')
336 raise Exception('Please specify commit or pull_request_id')
338
337
339 Session().add(comment)
338 Session().add(comment)
340 Session().flush()
339 Session().flush()
341 kwargs = {
340 kwargs = {
342 'user': user,
341 'user': user,
343 'renderer_type': renderer,
342 'renderer_type': renderer,
344 'repo_name': repo.repo_name,
343 'repo_name': repo.repo_name,
345 'status_change': status_change,
344 'status_change': status_change,
346 'status_change_type': status_change_type,
345 'status_change_type': status_change_type,
347 'comment_body': text,
346 'comment_body': text,
348 'comment_file': f_path,
347 'comment_file': f_path,
349 'comment_line': line_no,
348 'comment_line': line_no,
350 'comment_type': comment_type or 'note',
349 'comment_type': comment_type or 'note',
351 'comment_id': comment.comment_id
350 'comment_id': comment.comment_id
352 }
351 }
353
352
354 if commit_obj:
353 if commit_obj:
355 recipients = ChangesetComment.get_users(
354 recipients = ChangesetComment.get_users(
356 revision=commit_obj.raw_id)
355 revision=commit_obj.raw_id)
357 # add commit author if it's in RhodeCode system
356 # add commit author if it's in RhodeCode system
358 cs_author = User.get_from_cs_author(commit_obj.author)
357 cs_author = User.get_from_cs_author(commit_obj.author)
359 if not cs_author:
358 if not cs_author:
360 # use repo owner if we cannot extract the author correctly
359 # use repo owner if we cannot extract the author correctly
361 cs_author = repo.user
360 cs_author = repo.user
362 recipients += [cs_author]
361 recipients += [cs_author]
363
362
364 commit_comment_url = self.get_url(comment, request=request)
363 commit_comment_url = self.get_url(comment, request=request)
365 commit_comment_reply_url = self.get_url(
364 commit_comment_reply_url = self.get_url(
366 comment, request=request,
365 comment, request=request,
367 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
366 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
368
367
369 target_repo_url = h.link_to(
368 target_repo_url = h.link_to(
370 repo.repo_name,
369 repo.repo_name,
371 h.route_url('repo_summary', repo_name=repo.repo_name))
370 h.route_url('repo_summary', repo_name=repo.repo_name))
372
371
373 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
372 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
374 commit_id=commit_id)
373 commit_id=commit_id)
375
374
376 # commit specifics
375 # commit specifics
377 kwargs.update({
376 kwargs.update({
378 'commit': commit_obj,
377 'commit': commit_obj,
379 'commit_message': commit_obj.message,
378 'commit_message': commit_obj.message,
380 'commit_target_repo_url': target_repo_url,
379 'commit_target_repo_url': target_repo_url,
381 'commit_comment_url': commit_comment_url,
380 'commit_comment_url': commit_comment_url,
382 'commit_comment_reply_url': commit_comment_reply_url,
381 'commit_comment_reply_url': commit_comment_reply_url,
383 'commit_url': commit_url,
382 'commit_url': commit_url,
384 'thread_ids': [commit_url, commit_comment_url],
383 'thread_ids': [commit_url, commit_comment_url],
385 })
384 })
386
385
387 elif pull_request_obj:
386 elif pull_request_obj:
388 # get the current participants of this pull request
387 # get the current participants of this pull request
389 recipients = ChangesetComment.get_users(
388 recipients = ChangesetComment.get_users(
390 pull_request_id=pull_request_obj.pull_request_id)
389 pull_request_id=pull_request_obj.pull_request_id)
391 # add pull request author
390 # add pull request author
392 recipients += [pull_request_obj.author]
391 recipients += [pull_request_obj.author]
393
392
394 # add the reviewers to notification
393 # add the reviewers to notification
395 recipients += [x.user for x in pull_request_obj.reviewers]
394 recipients += [x.user for x in pull_request_obj.reviewers]
396
395
397 pr_target_repo = pull_request_obj.target_repo
396 pr_target_repo = pull_request_obj.target_repo
398 pr_source_repo = pull_request_obj.source_repo
397 pr_source_repo = pull_request_obj.source_repo
399
398
400 pr_comment_url = self.get_url(comment, request=request)
399 pr_comment_url = self.get_url(comment, request=request)
401 pr_comment_reply_url = self.get_url(
400 pr_comment_reply_url = self.get_url(
402 comment, request=request,
401 comment, request=request,
403 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
402 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
404
403
405 pr_url = h.route_url(
404 pr_url = h.route_url(
406 'pullrequest_show',
405 'pullrequest_show',
407 repo_name=pr_target_repo.repo_name,
406 repo_name=pr_target_repo.repo_name,
408 pull_request_id=pull_request_obj.pull_request_id, )
407 pull_request_id=pull_request_obj.pull_request_id, )
409
408
410 # set some variables for email notification
409 # set some variables for email notification
411 pr_target_repo_url = h.route_url(
410 pr_target_repo_url = h.route_url(
412 'repo_summary', repo_name=pr_target_repo.repo_name)
411 'repo_summary', repo_name=pr_target_repo.repo_name)
413
412
414 pr_source_repo_url = h.route_url(
413 pr_source_repo_url = h.route_url(
415 'repo_summary', repo_name=pr_source_repo.repo_name)
414 'repo_summary', repo_name=pr_source_repo.repo_name)
416
415
417 # pull request specifics
416 # pull request specifics
418 kwargs.update({
417 kwargs.update({
419 'pull_request': pull_request_obj,
418 'pull_request': pull_request_obj,
420 'pr_id': pull_request_obj.pull_request_id,
419 'pr_id': pull_request_obj.pull_request_id,
421 'pull_request_url': pr_url,
420 'pull_request_url': pr_url,
422 'pull_request_target_repo': pr_target_repo,
421 'pull_request_target_repo': pr_target_repo,
423 'pull_request_target_repo_url': pr_target_repo_url,
422 'pull_request_target_repo_url': pr_target_repo_url,
424 'pull_request_source_repo': pr_source_repo,
423 'pull_request_source_repo': pr_source_repo,
425 'pull_request_source_repo_url': pr_source_repo_url,
424 'pull_request_source_repo_url': pr_source_repo_url,
426 'pr_comment_url': pr_comment_url,
425 'pr_comment_url': pr_comment_url,
427 'pr_comment_reply_url': pr_comment_reply_url,
426 'pr_comment_reply_url': pr_comment_reply_url,
428 'pr_closing': closing_pr,
427 'pr_closing': closing_pr,
429 'thread_ids': [pr_url, pr_comment_url],
428 'thread_ids': [pr_url, pr_comment_url],
430 })
429 })
431
430
432 recipients += [self._get_user(u) for u in (extra_recipients or [])]
431 recipients += [self._get_user(u) for u in (extra_recipients or [])]
433
432
434 if send_email:
433 if send_email:
435 # pre-generate the subject for notification itself
434 # pre-generate the subject for notification itself
436 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
435 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
437 notification_type, **kwargs)
436 notification_type, **kwargs)
438
437
439 mention_recipients = set(
438 mention_recipients = set(
440 self._extract_mentions(text)).difference(recipients)
439 self._extract_mentions(text)).difference(recipients)
441
440
442 # create notification objects, and emails
441 # create notification objects, and emails
443 NotificationModel().create(
442 NotificationModel().create(
444 created_by=user,
443 created_by=user,
445 notification_subject=subject,
444 notification_subject=subject,
446 notification_body=body_plaintext,
445 notification_body=body_plaintext,
447 notification_type=notification_type,
446 notification_type=notification_type,
448 recipients=recipients,
447 recipients=recipients,
449 mention_recipients=mention_recipients,
448 mention_recipients=mention_recipients,
450 email_kwargs=kwargs,
449 email_kwargs=kwargs,
451 )
450 )
452
451
453 Session().flush()
452 Session().flush()
454 if comment.pull_request:
453 if comment.pull_request:
455 action = 'repo.pull_request.comment.create'
454 action = 'repo.pull_request.comment.create'
456 else:
455 else:
457 action = 'repo.commit.comment.create'
456 action = 'repo.commit.comment.create'
458
457
458 comment_id = comment.comment_id
459 comment_data = comment.get_api_data()
459 comment_data = comment.get_api_data()
460
460 self._log_audit_action(
461 self._log_audit_action(
461 action, {'data': comment_data}, auth_user, comment)
462 action, {'data': comment_data}, auth_user, comment)
462
463
463 msg_url = ''
464 channel = None
464 channel = None
465 if commit_obj:
465 if commit_obj:
466 msg_url = commit_comment_url
467 repo_name = repo.repo_name
466 repo_name = repo.repo_name
468 channel = u'/repo${}$/commit/{}'.format(
467 channel = u'/repo${}$/commit/{}'.format(
469 repo_name,
468 repo_name,
470 commit_obj.raw_id
469 commit_obj.raw_id
471 )
470 )
472 elif pull_request_obj:
471 elif pull_request_obj:
473 msg_url = pr_comment_url
474 repo_name = pr_target_repo.repo_name
472 repo_name = pr_target_repo.repo_name
475 channel = u'/repo${}$/pr/{}'.format(
473 channel = u'/repo${}$/pr/{}'.format(
476 repo_name,
474 repo_name,
477 pull_request_id
475 pull_request_obj.pull_request_id
478 )
476 )
479
477
480 message = '<strong>{}</strong> {} - ' \
478 if channel:
481 '<a onclick="window.location=\'{}\';' \
479 username = user.username
482 'window.location.reload()">' \
480 message = '<strong>{}</strong> {} #{}, {}'
483 '<strong>{}</strong></a>'
484 message = message.format(
481 message = message.format(
485 user.username, _('made a comment'), msg_url,
482 username,
486 _('Show it now'))
483 _('posted a new comment'),
484 comment_id,
485 _('Refresh the page to see new comments.'))
486
487 message_obj = {
488 'message': message,
489 'level': 'success',
490 'topic': '/notifications'
491 }
487
492
488 channelstream.post_message(
493 channelstream.post_message(
489 channel, message, user.username,
494 channel, message_obj, user.username,
495 registry=get_current_registry())
496
497 message_obj = {
498 'message': None,
499 'user': username,
500 'comment_id': comment_id,
501 'topic': '/comment'
502 }
503 channelstream.post_message(
504 channel, message_obj, user.username,
490 registry=get_current_registry())
505 registry=get_current_registry())
491
506
492 return comment
507 return comment
493
508
494 def edit(self, comment_id, text, auth_user, version):
509 def edit(self, comment_id, text, auth_user, version):
495 """
510 """
496 Change existing comment for commit or pull request.
511 Change existing comment for commit or pull request.
497
512
498 :param comment_id:
513 :param comment_id:
499 :param text:
514 :param text:
500 :param auth_user: current authenticated user calling this method
515 :param auth_user: current authenticated user calling this method
501 :param version: last comment version
516 :param version: last comment version
502 """
517 """
503 if not text:
518 if not text:
504 log.warning('Missing text for comment, skipping...')
519 log.warning('Missing text for comment, skipping...')
505 return
520 return
506
521
507 comment = ChangesetComment.get(comment_id)
522 comment = ChangesetComment.get(comment_id)
508 old_comment_text = comment.text
523 old_comment_text = comment.text
509 comment.text = text
524 comment.text = text
510 comment.modified_at = datetime.datetime.now()
525 comment.modified_at = datetime.datetime.now()
511 version = safe_int(version)
526 version = safe_int(version)
512
527
513 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
528 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
514 # would return 3 here
529 # would return 3 here
515 comment_version = ChangesetCommentHistory.get_version(comment_id)
530 comment_version = ChangesetCommentHistory.get_version(comment_id)
516
531
517 if isinstance(version, (int, long)) and (comment_version - version) != 1:
532 if isinstance(version, (int, long)) and (comment_version - version) != 1:
518 log.warning(
533 log.warning(
519 'Version mismatch comment_version {} submitted {}, skipping'.format(
534 'Version mismatch comment_version {} submitted {}, skipping'.format(
520 comment_version-1, # -1 since note above
535 comment_version-1, # -1 since note above
521 version
536 version
522 )
537 )
523 )
538 )
524 raise CommentVersionMismatch()
539 raise CommentVersionMismatch()
525
540
526 comment_history = ChangesetCommentHistory()
541 comment_history = ChangesetCommentHistory()
527 comment_history.comment_id = comment_id
542 comment_history.comment_id = comment_id
528 comment_history.version = comment_version
543 comment_history.version = comment_version
529 comment_history.created_by_user_id = auth_user.user_id
544 comment_history.created_by_user_id = auth_user.user_id
530 comment_history.text = old_comment_text
545 comment_history.text = old_comment_text
531 # TODO add email notification
546 # TODO add email notification
532 Session().add(comment_history)
547 Session().add(comment_history)
533 Session().add(comment)
548 Session().add(comment)
534 Session().flush()
549 Session().flush()
535
550
536 if comment.pull_request:
551 if comment.pull_request:
537 action = 'repo.pull_request.comment.edit'
552 action = 'repo.pull_request.comment.edit'
538 else:
553 else:
539 action = 'repo.commit.comment.edit'
554 action = 'repo.commit.comment.edit'
540
555
541 comment_data = comment.get_api_data()
556 comment_data = comment.get_api_data()
542 comment_data['old_comment_text'] = old_comment_text
557 comment_data['old_comment_text'] = old_comment_text
543 self._log_audit_action(
558 self._log_audit_action(
544 action, {'data': comment_data}, auth_user, comment)
559 action, {'data': comment_data}, auth_user, comment)
545
560
546 return comment_history
561 return comment_history
547
562
548 def delete(self, comment, auth_user):
563 def delete(self, comment, auth_user):
549 """
564 """
550 Deletes given comment
565 Deletes given comment
551 """
566 """
552 comment = self.__get_commit_comment(comment)
567 comment = self.__get_commit_comment(comment)
553 old_data = comment.get_api_data()
568 old_data = comment.get_api_data()
554 Session().delete(comment)
569 Session().delete(comment)
555
570
556 if comment.pull_request:
571 if comment.pull_request:
557 action = 'repo.pull_request.comment.delete'
572 action = 'repo.pull_request.comment.delete'
558 else:
573 else:
559 action = 'repo.commit.comment.delete'
574 action = 'repo.commit.comment.delete'
560
575
561 self._log_audit_action(
576 self._log_audit_action(
562 action, {'old_data': old_data}, auth_user, comment)
577 action, {'old_data': old_data}, auth_user, comment)
563
578
564 return comment
579 return comment
565
580
566 def get_all_comments(self, repo_id, revision=None, pull_request=None):
581 def get_all_comments(self, repo_id, revision=None, pull_request=None):
567 q = ChangesetComment.query()\
582 q = ChangesetComment.query()\
568 .filter(ChangesetComment.repo_id == repo_id)
583 .filter(ChangesetComment.repo_id == repo_id)
569 if revision:
584 if revision:
570 q = q.filter(ChangesetComment.revision == revision)
585 q = q.filter(ChangesetComment.revision == revision)
571 elif pull_request:
586 elif pull_request:
572 pull_request = self.__get_pull_request(pull_request)
587 pull_request = self.__get_pull_request(pull_request)
573 q = q.filter(ChangesetComment.pull_request == pull_request)
588 q = q.filter(ChangesetComment.pull_request == pull_request)
574 else:
589 else:
575 raise Exception('Please specify commit or pull_request')
590 raise Exception('Please specify commit or pull_request')
576 q = q.order_by(ChangesetComment.created_on)
591 q = q.order_by(ChangesetComment.created_on)
577 return q.all()
592 return q.all()
578
593
579 def get_url(self, comment, request=None, permalink=False, anchor=None):
594 def get_url(self, comment, request=None, permalink=False, anchor=None):
580 if not request:
595 if not request:
581 request = get_current_request()
596 request = get_current_request()
582
597
583 comment = self.__get_commit_comment(comment)
598 comment = self.__get_commit_comment(comment)
584 if anchor is None:
599 if anchor is None:
585 anchor = 'comment-{}'.format(comment.comment_id)
600 anchor = 'comment-{}'.format(comment.comment_id)
586
601
587 if comment.pull_request:
602 if comment.pull_request:
588 pull_request = comment.pull_request
603 pull_request = comment.pull_request
589 if permalink:
604 if permalink:
590 return request.route_url(
605 return request.route_url(
591 'pull_requests_global',
606 'pull_requests_global',
592 pull_request_id=pull_request.pull_request_id,
607 pull_request_id=pull_request.pull_request_id,
593 _anchor=anchor)
608 _anchor=anchor)
594 else:
609 else:
595 return request.route_url(
610 return request.route_url(
596 'pullrequest_show',
611 'pullrequest_show',
597 repo_name=safe_str(pull_request.target_repo.repo_name),
612 repo_name=safe_str(pull_request.target_repo.repo_name),
598 pull_request_id=pull_request.pull_request_id,
613 pull_request_id=pull_request.pull_request_id,
599 _anchor=anchor)
614 _anchor=anchor)
600
615
601 else:
616 else:
602 repo = comment.repo
617 repo = comment.repo
603 commit_id = comment.revision
618 commit_id = comment.revision
604
619
605 if permalink:
620 if permalink:
606 return request.route_url(
621 return request.route_url(
607 'repo_commit', repo_name=safe_str(repo.repo_id),
622 'repo_commit', repo_name=safe_str(repo.repo_id),
608 commit_id=commit_id,
623 commit_id=commit_id,
609 _anchor=anchor)
624 _anchor=anchor)
610
625
611 else:
626 else:
612 return request.route_url(
627 return request.route_url(
613 'repo_commit', repo_name=safe_str(repo.repo_name),
628 'repo_commit', repo_name=safe_str(repo.repo_name),
614 commit_id=commit_id,
629 commit_id=commit_id,
615 _anchor=anchor)
630 _anchor=anchor)
616
631
617 def get_comments(self, repo_id, revision=None, pull_request=None):
632 def get_comments(self, repo_id, revision=None, pull_request=None):
618 """
633 """
619 Gets main comments based on revision or pull_request_id
634 Gets main comments based on revision or pull_request_id
620
635
621 :param repo_id:
636 :param repo_id:
622 :param revision:
637 :param revision:
623 :param pull_request:
638 :param pull_request:
624 """
639 """
625
640
626 q = ChangesetComment.query()\
641 q = ChangesetComment.query()\
627 .filter(ChangesetComment.repo_id == repo_id)\
642 .filter(ChangesetComment.repo_id == repo_id)\
628 .filter(ChangesetComment.line_no == None)\
643 .filter(ChangesetComment.line_no == None)\
629 .filter(ChangesetComment.f_path == None)
644 .filter(ChangesetComment.f_path == None)
630 if revision:
645 if revision:
631 q = q.filter(ChangesetComment.revision == revision)
646 q = q.filter(ChangesetComment.revision == revision)
632 elif pull_request:
647 elif pull_request:
633 pull_request = self.__get_pull_request(pull_request)
648 pull_request = self.__get_pull_request(pull_request)
634 q = q.filter(ChangesetComment.pull_request == pull_request)
649 q = q.filter(ChangesetComment.pull_request == pull_request)
635 else:
650 else:
636 raise Exception('Please specify commit or pull_request')
651 raise Exception('Please specify commit or pull_request')
637 q = q.order_by(ChangesetComment.created_on)
652 q = q.order_by(ChangesetComment.created_on)
638 return q.all()
653 return q.all()
639
654
640 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
655 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
641 q = self._get_inline_comments_query(repo_id, revision, pull_request)
656 q = self._get_inline_comments_query(repo_id, revision, pull_request)
642 return self._group_comments_by_path_and_line_number(q)
657 return self._group_comments_by_path_and_line_number(q)
643
658
644 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
659 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
645 version=None):
660 version=None):
646 inline_cnt = 0
661 inline_comms = []
647 for fname, per_line_comments in inline_comments.iteritems():
662 for fname, per_line_comments in inline_comments.iteritems():
648 for lno, comments in per_line_comments.iteritems():
663 for lno, comments in per_line_comments.iteritems():
649 for comm in comments:
664 for comm in comments:
650 if not comm.outdated_at_version(version) and skip_outdated:
665 if not comm.outdated_at_version(version) and skip_outdated:
651 inline_cnt += 1
666 inline_comms.append(comm)
652
667
653 return inline_cnt
668 return inline_comms
654
669
655 def get_outdated_comments(self, repo_id, pull_request):
670 def get_outdated_comments(self, repo_id, pull_request):
656 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
671 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
657 # of a pull request.
672 # of a pull request.
658 q = self._all_inline_comments_of_pull_request(pull_request)
673 q = self._all_inline_comments_of_pull_request(pull_request)
659 q = q.filter(
674 q = q.filter(
660 ChangesetComment.display_state ==
675 ChangesetComment.display_state ==
661 ChangesetComment.COMMENT_OUTDATED
676 ChangesetComment.COMMENT_OUTDATED
662 ).order_by(ChangesetComment.comment_id.asc())
677 ).order_by(ChangesetComment.comment_id.asc())
663
678
664 return self._group_comments_by_path_and_line_number(q)
679 return self._group_comments_by_path_and_line_number(q)
665
680
666 def _get_inline_comments_query(self, repo_id, revision, pull_request):
681 def _get_inline_comments_query(self, repo_id, revision, pull_request):
667 # TODO: johbo: Split this into two methods: One for PR and one for
682 # TODO: johbo: Split this into two methods: One for PR and one for
668 # commit.
683 # commit.
669 if revision:
684 if revision:
670 q = Session().query(ChangesetComment).filter(
685 q = Session().query(ChangesetComment).filter(
671 ChangesetComment.repo_id == repo_id,
686 ChangesetComment.repo_id == repo_id,
672 ChangesetComment.line_no != null(),
687 ChangesetComment.line_no != null(),
673 ChangesetComment.f_path != null(),
688 ChangesetComment.f_path != null(),
674 ChangesetComment.revision == revision)
689 ChangesetComment.revision == revision)
675
690
676 elif pull_request:
691 elif pull_request:
677 pull_request = self.__get_pull_request(pull_request)
692 pull_request = self.__get_pull_request(pull_request)
678 if not CommentsModel.use_outdated_comments(pull_request):
693 if not CommentsModel.use_outdated_comments(pull_request):
679 q = self._visible_inline_comments_of_pull_request(pull_request)
694 q = self._visible_inline_comments_of_pull_request(pull_request)
680 else:
695 else:
681 q = self._all_inline_comments_of_pull_request(pull_request)
696 q = self._all_inline_comments_of_pull_request(pull_request)
682
697
683 else:
698 else:
684 raise Exception('Please specify commit or pull_request_id')
699 raise Exception('Please specify commit or pull_request_id')
685 q = q.order_by(ChangesetComment.comment_id.asc())
700 q = q.order_by(ChangesetComment.comment_id.asc())
686 return q
701 return q
687
702
688 def _group_comments_by_path_and_line_number(self, q):
703 def _group_comments_by_path_and_line_number(self, q):
689 comments = q.all()
704 comments = q.all()
690 paths = collections.defaultdict(lambda: collections.defaultdict(list))
705 paths = collections.defaultdict(lambda: collections.defaultdict(list))
691 for co in comments:
706 for co in comments:
692 paths[co.f_path][co.line_no].append(co)
707 paths[co.f_path][co.line_no].append(co)
693 return paths
708 return paths
694
709
695 @classmethod
710 @classmethod
696 def needed_extra_diff_context(cls):
711 def needed_extra_diff_context(cls):
697 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
712 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
698
713
699 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
714 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
700 if not CommentsModel.use_outdated_comments(pull_request):
715 if not CommentsModel.use_outdated_comments(pull_request):
701 return
716 return
702
717
703 comments = self._visible_inline_comments_of_pull_request(pull_request)
718 comments = self._visible_inline_comments_of_pull_request(pull_request)
704 comments_to_outdate = comments.all()
719 comments_to_outdate = comments.all()
705
720
706 for comment in comments_to_outdate:
721 for comment in comments_to_outdate:
707 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
722 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
708
723
709 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
724 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
710 diff_line = _parse_comment_line_number(comment.line_no)
725 diff_line = _parse_comment_line_number(comment.line_no)
711
726
712 try:
727 try:
713 old_context = old_diff_proc.get_context_of_line(
728 old_context = old_diff_proc.get_context_of_line(
714 path=comment.f_path, diff_line=diff_line)
729 path=comment.f_path, diff_line=diff_line)
715 new_context = new_diff_proc.get_context_of_line(
730 new_context = new_diff_proc.get_context_of_line(
716 path=comment.f_path, diff_line=diff_line)
731 path=comment.f_path, diff_line=diff_line)
717 except (diffs.LineNotInDiffException,
732 except (diffs.LineNotInDiffException,
718 diffs.FileNotInDiffException):
733 diffs.FileNotInDiffException):
719 comment.display_state = ChangesetComment.COMMENT_OUTDATED
734 comment.display_state = ChangesetComment.COMMENT_OUTDATED
720 return
735 return
721
736
722 if old_context == new_context:
737 if old_context == new_context:
723 return
738 return
724
739
725 if self._should_relocate_diff_line(diff_line):
740 if self._should_relocate_diff_line(diff_line):
726 new_diff_lines = new_diff_proc.find_context(
741 new_diff_lines = new_diff_proc.find_context(
727 path=comment.f_path, context=old_context,
742 path=comment.f_path, context=old_context,
728 offset=self.DIFF_CONTEXT_BEFORE)
743 offset=self.DIFF_CONTEXT_BEFORE)
729 if not new_diff_lines:
744 if not new_diff_lines:
730 comment.display_state = ChangesetComment.COMMENT_OUTDATED
745 comment.display_state = ChangesetComment.COMMENT_OUTDATED
731 else:
746 else:
732 new_diff_line = self._choose_closest_diff_line(
747 new_diff_line = self._choose_closest_diff_line(
733 diff_line, new_diff_lines)
748 diff_line, new_diff_lines)
734 comment.line_no = _diff_to_comment_line_number(new_diff_line)
749 comment.line_no = _diff_to_comment_line_number(new_diff_line)
735 else:
750 else:
736 comment.display_state = ChangesetComment.COMMENT_OUTDATED
751 comment.display_state = ChangesetComment.COMMENT_OUTDATED
737
752
738 def _should_relocate_diff_line(self, diff_line):
753 def _should_relocate_diff_line(self, diff_line):
739 """
754 """
740 Checks if relocation shall be tried for the given `diff_line`.
755 Checks if relocation shall be tried for the given `diff_line`.
741
756
742 If a comment points into the first lines, then we can have a situation
757 If a comment points into the first lines, then we can have a situation
743 that after an update another line has been added on top. In this case
758 that after an update another line has been added on top. In this case
744 we would find the context still and move the comment around. This
759 we would find the context still and move the comment around. This
745 would be wrong.
760 would be wrong.
746 """
761 """
747 should_relocate = (
762 should_relocate = (
748 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
763 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
749 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
764 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
750 return should_relocate
765 return should_relocate
751
766
752 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
767 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
753 candidate = new_diff_lines[0]
768 candidate = new_diff_lines[0]
754 best_delta = _diff_line_delta(diff_line, candidate)
769 best_delta = _diff_line_delta(diff_line, candidate)
755 for new_diff_line in new_diff_lines[1:]:
770 for new_diff_line in new_diff_lines[1:]:
756 delta = _diff_line_delta(diff_line, new_diff_line)
771 delta = _diff_line_delta(diff_line, new_diff_line)
757 if delta < best_delta:
772 if delta < best_delta:
758 candidate = new_diff_line
773 candidate = new_diff_line
759 best_delta = delta
774 best_delta = delta
760 return candidate
775 return candidate
761
776
762 def _visible_inline_comments_of_pull_request(self, pull_request):
777 def _visible_inline_comments_of_pull_request(self, pull_request):
763 comments = self._all_inline_comments_of_pull_request(pull_request)
778 comments = self._all_inline_comments_of_pull_request(pull_request)
764 comments = comments.filter(
779 comments = comments.filter(
765 coalesce(ChangesetComment.display_state, '') !=
780 coalesce(ChangesetComment.display_state, '') !=
766 ChangesetComment.COMMENT_OUTDATED)
781 ChangesetComment.COMMENT_OUTDATED)
767 return comments
782 return comments
768
783
769 def _all_inline_comments_of_pull_request(self, pull_request):
784 def _all_inline_comments_of_pull_request(self, pull_request):
770 comments = Session().query(ChangesetComment)\
785 comments = Session().query(ChangesetComment)\
771 .filter(ChangesetComment.line_no != None)\
786 .filter(ChangesetComment.line_no != None)\
772 .filter(ChangesetComment.f_path != None)\
787 .filter(ChangesetComment.f_path != None)\
773 .filter(ChangesetComment.pull_request == pull_request)
788 .filter(ChangesetComment.pull_request == pull_request)
774 return comments
789 return comments
775
790
776 def _all_general_comments_of_pull_request(self, pull_request):
791 def _all_general_comments_of_pull_request(self, pull_request):
777 comments = Session().query(ChangesetComment)\
792 comments = Session().query(ChangesetComment)\
778 .filter(ChangesetComment.line_no == None)\
793 .filter(ChangesetComment.line_no == None)\
779 .filter(ChangesetComment.f_path == None)\
794 .filter(ChangesetComment.f_path == None)\
780 .filter(ChangesetComment.pull_request == pull_request)
795 .filter(ChangesetComment.pull_request == pull_request)
781
796
782 return comments
797 return comments
783
798
784 @staticmethod
799 @staticmethod
785 def use_outdated_comments(pull_request):
800 def use_outdated_comments(pull_request):
786 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
801 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
787 settings = settings_model.get_general_settings()
802 settings = settings_model.get_general_settings()
788 return settings.get('rhodecode_use_outdated_comments', False)
803 return settings.get('rhodecode_use_outdated_comments', False)
789
804
790 def trigger_commit_comment_hook(self, repo, user, action, data=None):
805 def trigger_commit_comment_hook(self, repo, user, action, data=None):
791 repo = self._get_repo(repo)
806 repo = self._get_repo(repo)
792 target_scm = repo.scm_instance()
807 target_scm = repo.scm_instance()
793 if action == 'create':
808 if action == 'create':
794 trigger_hook = hooks_utils.trigger_comment_commit_hooks
809 trigger_hook = hooks_utils.trigger_comment_commit_hooks
795 elif action == 'edit':
810 elif action == 'edit':
796 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
811 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
797 else:
812 else:
798 return
813 return
799
814
800 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
815 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
801 repo, action, trigger_hook)
816 repo, action, trigger_hook)
802 trigger_hook(
817 trigger_hook(
803 username=user.username,
818 username=user.username,
804 repo_name=repo.repo_name,
819 repo_name=repo.repo_name,
805 repo_type=target_scm.alias,
820 repo_type=target_scm.alias,
806 repo=repo,
821 repo=repo,
807 data=data)
822 data=data)
808
823
809
824
810 def _parse_comment_line_number(line_no):
825 def _parse_comment_line_number(line_no):
811 """
826 """
812 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
827 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
813 """
828 """
814 old_line = None
829 old_line = None
815 new_line = None
830 new_line = None
816 if line_no.startswith('o'):
831 if line_no.startswith('o'):
817 old_line = int(line_no[1:])
832 old_line = int(line_no[1:])
818 elif line_no.startswith('n'):
833 elif line_no.startswith('n'):
819 new_line = int(line_no[1:])
834 new_line = int(line_no[1:])
820 else:
835 else:
821 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
836 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
822 return diffs.DiffLineNumber(old_line, new_line)
837 return diffs.DiffLineNumber(old_line, new_line)
823
838
824
839
825 def _diff_to_comment_line_number(diff_line):
840 def _diff_to_comment_line_number(diff_line):
826 if diff_line.new is not None:
841 if diff_line.new is not None:
827 return u'n{}'.format(diff_line.new)
842 return u'n{}'.format(diff_line.new)
828 elif diff_line.old is not None:
843 elif diff_line.old is not None:
829 return u'o{}'.format(diff_line.old)
844 return u'o{}'.format(diff_line.old)
830 return u''
845 return u''
831
846
832
847
833 def _diff_line_delta(a, b):
848 def _diff_line_delta(a, b):
834 if None not in (a.new, b.new):
849 if None not in (a.new, b.new):
835 return abs(a.new - b.new)
850 return abs(a.new - b.new)
836 elif None not in (a.old, b.old):
851 elif None not in (a.old, b.old):
837 return abs(a.old - b.old)
852 return abs(a.old - b.old)
838 else:
853 else:
839 raise ValueError(
854 raise ValueError(
840 "Cannot compute delta between {} and {}".format(a, b))
855 "Cannot compute delta between {} and {}".format(a, b))
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,400 +1,400 b''
1
1
2 /** MODAL **/
2 /** MODAL **/
3 .modal-open {
3 .modal-open {
4 overflow:hidden;
4 overflow:hidden;
5 }
5 }
6 body.modal-open, .modal-open .navbar-fixed-top, .modal-open .navbar-fixed-bottom {
6 body.modal-open, .modal-open .navbar-fixed-top, .modal-open .navbar-fixed-bottom {
7 margin-right:15px;
7 margin-right:15px;
8 }
8 }
9 .modal {
9 .modal {
10 position:fixed;
10 position:fixed;
11 top:0;
11 top:0;
12 right:0;
12 right:0;
13 bottom:0;
13 bottom:0;
14 left:0;
14 left:0;
15 z-index:1040;
15 z-index:1040;
16 display:none;
16 display:none;
17 overflow-y:scroll;
17 overflow-y:scroll;
18 &.fade .modal-dialog {
18 &.fade .modal-dialog {
19 -webkit-transform:translate(0,-25%);
19 -webkit-transform:translate(0,-25%);
20 -ms-transform:translate(0,-25%);
20 -ms-transform:translate(0,-25%);
21 transform:translate(0,-25%);
21 transform:translate(0,-25%);
22 -webkit-transition:-webkit-transform 0.3s ease-out;
22 -webkit-transition:-webkit-transform 0.3s ease-out;
23 -moz-transition:-moz-transform 0.3s ease-out;
23 -moz-transition:-moz-transform 0.3s ease-out;
24 -o-transition:-o-transform 0.3s ease-out;
24 -o-transition:-o-transform 0.3s ease-out;
25 transition:transform 0.3s ease-out;
25 transition:transform 0.3s ease-out;
26 }
26 }
27 &.in .modal-dialog {
27 &.in .modal-dialog {
28 -webkit-transform:translate(0,0);
28 -webkit-transform:translate(0,0);
29 -ms-transform:translate(0,0);
29 -ms-transform:translate(0,0);
30 transform:translate(0,0);
30 transform:translate(0,0);
31 }
31 }
32 }
32 }
33 .modal-dialog {
33 .modal-dialog {
34 z-index:1050;
34 z-index:1050;
35 width:auto;
35 width:auto;
36 padding:10px;
36 padding:10px;
37 margin-right:auto;
37 margin-right:auto;
38 margin-left:auto;
38 margin-left:auto;
39 }
39 }
40 .modal-content {
40 .modal-content {
41 position:relative;
41 position:relative;
42 background-color:#ffffff;
42 background-color:#ffffff;
43 border: @border-thickness solid rgba(0,0,0,0.2);
43 border: @border-thickness solid rgba(0,0,0,0.2);
44 .border-radius(@border-radius);
44 .border-radius(@border-radius);
45 outline:none;
45 outline:none;
46 -webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);
46 -webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);
47 box-shadow:0 3px 9px rgba(0,0,0,0.5);
47 box-shadow:0 3px 9px rgba(0,0,0,0.5);
48 background-clip:padding-box;
48 background-clip:padding-box;
49 }
49 }
50 .modal-backdrop {
50 .modal-backdrop {
51 position:fixed;
51 position:fixed;
52 top:0;
52 top:0;
53 right:0;
53 right:0;
54 bottom:0;
54 bottom:0;
55 left:0;
55 left:0;
56 z-index:1030;
56 z-index:1030;
57 background-color:#000000;
57 background-color:#000000;
58
58
59 &.modal-backdrop.fade {
59 &.modal-backdrop.fade {
60 opacity:0;
60 opacity:0;
61 filter:alpha(opacity=0);
61 filter:alpha(opacity=0);
62 }
62 }
63 &.in {
63 &.in {
64 opacity:0.5;
64 opacity:0.5;
65 filter:alpha(opacity=50);
65 filter:alpha(opacity=50);
66 }
66 }
67 }
67 }
68 .modal-header {
68 .modal-header {
69 min-height:16.428571429px;
69 min-height:16.428571429px;
70 padding:15px;
70 padding:15px;
71 border-bottom: @border-thickness solid @grey6;
71 border-bottom: @border-thickness solid @grey6;
72 .close {
72 .close {
73 margin-top:-2px;
73 margin-top:-2px;
74 }
74 }
75 }
75 }
76 .modal-title {
76 .modal-title {
77 margin:0;
77 margin:0;
78 line-height:1.428571429;
78 line-height:1.428571429;
79 }
79 }
80 .modal-body {
80 .modal-body {
81 position:relative;
81 position:relative;
82 padding:20px;
82 padding:20px;
83 }
83 }
84 .modal-footer {
84 .modal-footer {
85 padding:19px 20px 20px;
85 padding:19px 20px 20px;
86 margin-top:15px;
86 margin-top:15px;
87 text-align:right;
87 text-align:right;
88 border-top:1px solid #e5e5e5;
88 border-top:1px solid #e5e5e5;
89 .btn + .btn {
89 .btn + .btn {
90 margin-bottom:0;
90 margin-bottom:0;
91 margin-left:5px;
91 margin-left:5px;
92 }
92 }
93 .btn-group .btn + .btn {
93 .btn-group .btn + .btn {
94 margin-left:-1px;
94 margin-left:-1px;
95 }
95 }
96 .btn-block + .btn-block {
96 .btn-block + .btn-block {
97 margin-left:0;
97 margin-left:0;
98 }
98 }
99 &:before {
99 &:before {
100 display:table;
100 display:table;
101 content:" ";
101 content:" ";
102 }
102 }
103 &:after {
103 &:after {
104 display:table;
104 display:table;
105 content:" ";
105 content:" ";
106 clear:both;
106 clear:both;
107 }
107 }
108 }
108 }
109
109
110 /** MARKDOWN styling **/
110 /** MARKDOWN styling **/
111 div.markdown-block {
111 div.markdown-block {
112 clear: both;
112 clear: both;
113 overflow: hidden;
113 overflow: hidden;
114 margin: 0;
114 margin: 0;
115 padding: 3px 15px 3px;
115 padding: 3px 15px 3px;
116 }
116 }
117
117
118 div.markdown-block h1,
118 div.markdown-block h1,
119 div.markdown-block h2,
119 div.markdown-block h2,
120 div.markdown-block h3,
120 div.markdown-block h3,
121 div.markdown-block h4,
121 div.markdown-block h4,
122 div.markdown-block h5,
122 div.markdown-block h5,
123 div.markdown-block h6 {
123 div.markdown-block h6 {
124 border-bottom: none !important;
124 border-bottom: none !important;
125 padding: 0 !important;
125 padding: 0 !important;
126 overflow: visible !important;
126 overflow: visible !important;
127 }
127 }
128
128
129 div.markdown-block h1,
129 div.markdown-block h1,
130 div.markdown-block h2 {
130 div.markdown-block h2 {
131 border-bottom: 1px #e6e5e5 solid !important;
131 border-bottom: 1px #e6e5e5 solid !important;
132 }
132 }
133
133
134 div.markdown-block h1 {
134 div.markdown-block h1 {
135 font-size: 32px;
135 font-size: 32px;
136 margin: 15px 0 15px 0 !important;
136 margin: 15px 0 15px 0 !important;
137 }
137 }
138
138
139 div.markdown-block h2 {
139 div.markdown-block h2 {
140 font-size: 24px !important;
140 font-size: 24px !important;
141 margin: 34px 0 10px 0 !important;
141 margin: 34px 0 10px 0 !important;
142 }
142 }
143
143
144 div.markdown-block h3 {
144 div.markdown-block h3 {
145 font-size: 18px !important;
145 font-size: 18px !important;
146 margin: 30px 0 8px 0 !important;
146 margin: 30px 0 8px 0 !important;
147 padding-bottom: 2px !important;
147 padding-bottom: 2px !important;
148 }
148 }
149
149
150 div.markdown-block h4 {
150 div.markdown-block h4 {
151 font-size: 13px !important;
151 font-size: 13px !important;
152 margin: 18px 0 3px 0 !important;
152 margin: 18px 0 3px 0 !important;
153 }
153 }
154
154
155 div.markdown-block h5 {
155 div.markdown-block h5 {
156 font-size: 12px !important;
156 font-size: 12px !important;
157 margin: 15px 0 3px 0 !important;
157 margin: 15px 0 3px 0 !important;
158 }
158 }
159
159
160 div.markdown-block h6 {
160 div.markdown-block h6 {
161 font-size: 12px;
161 font-size: 12px;
162 color: #777777;
162 color: #777777;
163 margin: 15px 0 3px 0 !important;
163 margin: 15px 0 3px 0 !important;
164 }
164 }
165
165
166 div.markdown-block hr {
166 div.markdown-block hr {
167 border: 0;
167 border: 0;
168 color: #e6e5e5;
168 color: #e6e5e5;
169 background-color: #e6e5e5;
169 background-color: #e6e5e5;
170 height: 3px;
170 height: 3px;
171 margin-bottom: 13px;
171 margin-bottom: 13px;
172 }
172 }
173
173
174 div.markdown-block blockquote {
174 div.markdown-block blockquote {
175 color: #424242 !important;
175 color: #424242 !important;
176 padding: 8px 21px;
176 padding: 8px 21px;
177 margin: 12px 0;
177 margin: 12px 0;
178 border-left: 4px solid @grey6;
178 border-left: 4px solid @grey6;
179 }
179 }
180
180
181 div.markdown-block blockquote p {
181 div.markdown-block blockquote p {
182 color: #424242 !important;
182 color: #424242 !important;
183 padding: 0 !important;
183 padding: 0 !important;
184 margin: 0 !important;
184 margin: 0 !important;
185 line-height: 1.5;
185 line-height: 1.5;
186 }
186 }
187
187
188
188
189 div.markdown-block ol,
189 div.markdown-block ol,
190 div.markdown-block ul,
190 div.markdown-block ul,
191 div.markdown-block p,
191 div.markdown-block p,
192 div.markdown-block blockquote,
192 div.markdown-block blockquote,
193 div.markdown-block dl,
193 div.markdown-block dl,
194 div.markdown-block li,
194 div.markdown-block li,
195 div.markdown-block table {
195 div.markdown-block table {
196 color: #424242 !important;
196 color: #424242 !important;
197 font-size: 13px !important;
197 font-size: 13px !important;
198 font-family: @text-regular;
198 font-family: @text-regular;
199 font-weight: normal !important;
199 font-weight: normal !important;
200 overflow: visible !important;
200 overflow: visible !important;
201 }
201 }
202
202
203 div.markdown-block pre {
203 div.markdown-block pre {
204 margin: 3px 0px 13px 0px !important;
204 margin: 3px 0px 13px 0px !important;
205 padding: .5em;
205 padding: .5em;
206 color: #424242 !important;
206 color: #424242 !important;
207 font-size: 13px !important;
207 font-size: 13px !important;
208 overflow: visible !important;
208 overflow: visible !important;
209 line-height: 140% !important;
209 line-height: 140% !important;
210 background-color: @grey7;
210 background-color: @grey7;
211 }
211 }
212
212
213 div.markdown-block img {
213 div.markdown-block img {
214 border-style: none;
214 border-style: none;
215 background-color: #fff;
215 background-color: #fff;
216 padding-right: 20px;
216 padding-right: 20px;
217 max-width: 100%;
217 max-width: 100%;
218 }
218 }
219
219
220
220
221 div.markdown-block strong {
221 div.markdown-block strong {
222 font-weight: 600;
222 font-weight: 600;
223 margin: 0;
223 margin: 0;
224 }
224 }
225
225
226 div.markdown-block ul.checkbox,
226 div.markdown-block ul.checkbox,
227 div.markdown-block ol.checkbox {
227 div.markdown-block ol.checkbox {
228 padding-left: 20px !important;
228 padding-left: 20px !important;
229 margin-top: 0px !important;
229 margin-top: 0px !important;
230 margin-bottom: 18px !important;
230 margin-bottom: 18px !important;
231 }
231 }
232
232
233 div.markdown-block ul,
233 div.markdown-block ul,
234 div.markdown-block ol {
234 div.markdown-block ol {
235 padding-left: 30px !important;
235 padding-left: 30px !important;
236 margin-top: 0px !important;
236 margin-top: 0px !important;
237 margin-bottom: 18px !important;
237 margin-bottom: 18px !important;
238 }
238 }
239
239
240 div.markdown-block ul.checkbox li,
240 div.markdown-block ul.checkbox li,
241 div.markdown-block ol.checkbox li {
241 div.markdown-block ol.checkbox li {
242 list-style: none !important;
242 list-style: none !important;
243 margin: 6px !important;
243 margin: 0px !important;
244 padding: 0 !important;
244 padding: 0 !important;
245 }
245 }
246
246
247 div.markdown-block ul li,
247 div.markdown-block ul li,
248 div.markdown-block ol li {
248 div.markdown-block ol li {
249 list-style: disc !important;
249 list-style: disc !important;
250 margin: 6px !important;
250 margin: 0px !important;
251 padding: 0 !important;
251 padding: 0 !important;
252 }
252 }
253
253
254 div.markdown-block ol li {
254 div.markdown-block ol li {
255 list-style: decimal !important;
255 list-style: decimal !important;
256 }
256 }
257
257
258
258
259 div.markdown-block #message {
259 div.markdown-block #message {
260 .border-radius(@border-radius);
260 .border-radius(@border-radius);
261 border: @border-thickness solid @grey5;
261 border: @border-thickness solid @grey5;
262 display: block;
262 display: block;
263 width: 100%;
263 width: 100%;
264 height: 60px;
264 height: 60px;
265 margin: 6px 0px;
265 margin: 6px 0px;
266 }
266 }
267
267
268 div.markdown-block button,
268 div.markdown-block button,
269 div.markdown-block #ws {
269 div.markdown-block #ws {
270 font-size: @basefontsize;
270 font-size: @basefontsize;
271 padding: 4px 6px;
271 padding: 4px 6px;
272 .border-radius(@border-radius);
272 .border-radius(@border-radius);
273 border: @border-thickness solid @grey5;
273 border: @border-thickness solid @grey5;
274 background-color: @grey6;
274 background-color: @grey6;
275 }
275 }
276
276
277 div.markdown-block code,
277 div.markdown-block code,
278 div.markdown-block pre,
278 div.markdown-block pre,
279 div.markdown-block #ws,
279 div.markdown-block #ws,
280 div.markdown-block #message {
280 div.markdown-block #message {
281 font-family: @text-monospace;
281 font-family: @text-monospace;
282 font-size: 11px;
282 font-size: 11px;
283 .border-radius(@border-radius);
283 .border-radius(@border-radius);
284 background-color: white;
284 background-color: white;
285 color: @grey3;
285 color: @grey3;
286 }
286 }
287
287
288
288
289 div.markdown-block code {
289 div.markdown-block code {
290 border: @border-thickness solid @grey6;
290 border: @border-thickness solid @grey6;
291 margin: 0 2px;
291 margin: 0 2px;
292 padding: 0 5px;
292 padding: 0 5px;
293 }
293 }
294
294
295 div.markdown-block pre {
295 div.markdown-block pre {
296 border: @border-thickness solid @grey5;
296 border: @border-thickness solid @grey5;
297 overflow: auto;
297 overflow: auto;
298 padding: .5em;
298 padding: .5em;
299 background-color: @grey7;
299 background-color: @grey7;
300 }
300 }
301
301
302 div.markdown-block pre > code {
302 div.markdown-block pre > code {
303 border: 0;
303 border: 0;
304 margin: 0;
304 margin: 0;
305 padding: 0;
305 padding: 0;
306 }
306 }
307
307
308 /** RST STYLE **/
308 /** RST STYLE **/
309 div.rst-block {
309 div.rst-block {
310 clear: both;
310 clear: both;
311 overflow: hidden;
311 overflow: hidden;
312 margin: 0;
312 margin: 0;
313 padding: 3px 15px 3px;
313 padding: 3px 15px 3px;
314 }
314 }
315
315
316 div.rst-block h2 {
316 div.rst-block h2 {
317 font-weight: normal;
317 font-weight: normal;
318 }
318 }
319
319
320 div.rst-block h1,
320 div.rst-block h1,
321 div.rst-block h2,
321 div.rst-block h2,
322 div.rst-block h3,
322 div.rst-block h3,
323 div.rst-block h4,
323 div.rst-block h4,
324 div.rst-block h5,
324 div.rst-block h5,
325 div.rst-block h6 {
325 div.rst-block h6 {
326 border-bottom: 0 !important;
326 border-bottom: 0 !important;
327 margin: 0 !important;
327 margin: 0 !important;
328 padding: 0 !important;
328 padding: 0 !important;
329 line-height: 1.5em !important;
329 line-height: 1.5em !important;
330 }
330 }
331
331
332
332
333 div.rst-block h1:first-child {
333 div.rst-block h1:first-child {
334 padding-top: .25em !important;
334 padding-top: .25em !important;
335 }
335 }
336
336
337 div.rst-block h2,
337 div.rst-block h2,
338 div.rst-block h3 {
338 div.rst-block h3 {
339 margin: 1em 0 !important;
339 margin: 1em 0 !important;
340 }
340 }
341
341
342 div.rst-block h1,
342 div.rst-block h1,
343 div.rst-block h2 {
343 div.rst-block h2 {
344 border-bottom: 1px #e6e5e5 solid !important;
344 border-bottom: 1px #e6e5e5 solid !important;
345 }
345 }
346
346
347 div.rst-block h2 {
347 div.rst-block h2 {
348 margin-top: 1.5em !important;
348 margin-top: 1.5em !important;
349 padding-top: .5em !important;
349 padding-top: .5em !important;
350 }
350 }
351
351
352 div.rst-block p {
352 div.rst-block p {
353 color: black !important;
353 color: black !important;
354 margin: 1em 0 !important;
354 margin: 1em 0 !important;
355 line-height: 1.5em !important;
355 line-height: 1.5em !important;
356 }
356 }
357
357
358 div.rst-block ul {
358 div.rst-block ul {
359 list-style: disc !important;
359 list-style: disc !important;
360 margin: 1em 0 1em 2em !important;
360 margin: 1em 0 1em 2em !important;
361 clear: both;
361 clear: both;
362 }
362 }
363
363
364 div.rst-block ol {
364 div.rst-block ol {
365 list-style: decimal;
365 list-style: decimal;
366 margin: 1em 0 1em 2em !important;
366 margin: 1em 0 1em 2em !important;
367 }
367 }
368
368
369 div.rst-block pre,
369 div.rst-block pre,
370 div.rst-block code {
370 div.rst-block code {
371 font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
371 font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
372 }
372 }
373
373
374 div.rst-block code {
374 div.rst-block code {
375 font-size: 12px !important;
375 font-size: 12px !important;
376 background-color: ghostWhite !important;
376 background-color: ghostWhite !important;
377 color: #444 !important;
377 color: #444 !important;
378 padding: 0 .2em !important;
378 padding: 0 .2em !important;
379 border: 1px solid #dedede !important;
379 border: 1px solid #dedede !important;
380 }
380 }
381
381
382 div.rst-block pre code {
382 div.rst-block pre code {
383 padding: 0 !important;
383 padding: 0 !important;
384 font-size: 12px !important;
384 font-size: 12px !important;
385 background-color: #eee !important;
385 background-color: #eee !important;
386 border: none !important;
386 border: none !important;
387 }
387 }
388
388
389 div.rst-block pre {
389 div.rst-block pre {
390 margin: 1em 0;
390 margin: 1em 0;
391 padding: @padding;
391 padding: @padding;
392 border: 1px solid @grey6;
392 border: 1px solid @grey6;
393 .border-radius(@border-radius);
393 .border-radius(@border-radius);
394 overflow: auto;
394 overflow: auto;
395 font-size: 12px;
395 font-size: 12px;
396 color: #444;
396 color: #444;
397 background-color: @grey7;
397 background-color: @grey7;
398 }
398 }
399
399
400
400
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now