##// END OF EJS Templates
changelog: ported to pyramid views.
marcink -
r1931:f3f88cc8 default
parent child Browse files
Show More
@@ -0,0 +1,302 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import logging
23
24 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
25 from pyramid.view import view_config
26 from pyramid.renderers import render
27 from pyramid.response import Response
28
29 from rhodecode.apps._base import RepoAppView
30 import rhodecode.lib.helpers as h
31 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator)
33
34 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.graphmod import _colored, _dagwalker
36 from rhodecode.lib.helpers import RepoPage
37 from rhodecode.lib.utils2 import safe_int, safe_str
38 from rhodecode.lib.vcs.exceptions import (
39 RepositoryError, CommitDoesNotExistError,
40 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
41
42 log = logging.getLogger(__name__)
43
44 DEFAULT_CHANGELOG_SIZE = 20
45
46
47 class RepoChangelogView(RepoAppView):
48
49 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
50 """
51 This is a safe way to get commit. If an error occurs it redirects to
52 tip with proper message
53
54 :param commit_id: id of commit to fetch
55 :param redirect_after: toggle redirection
56 """
57 _ = self.request.translate
58
59 try:
60 return self.rhodecode_vcs_repo.get_commit(commit_id)
61 except EmptyRepositoryError:
62 if not redirect_after:
63 return None
64
65 h.flash(h.literal(
66 _('There are no commits yet')), category='warning')
67 raise HTTPFound(
68 h.route_path('repo_summary', repo_name=self.db_repo_name))
69
70 except (CommitDoesNotExistError, LookupError):
71 msg = _('No such commit exists for this repository')
72 h.flash(msg, category='error')
73 raise HTTPNotFound()
74 except RepositoryError as e:
75 h.flash(safe_str(h.escape(e)), category='error')
76 raise HTTPNotFound()
77
78 def _graph(self, repo, commits, prev_data=None, next_data=None):
79 """
80 Generates a DAG graph for repo
81
82 :param repo: repo instance
83 :param commits: list of commits
84 """
85 if not commits:
86 return json.dumps([])
87
88 def serialize(commit, parents=True):
89 data = dict(
90 raw_id=commit.raw_id,
91 idx=commit.idx,
92 branch=commit.branch,
93 )
94 if parents:
95 data['parents'] = [
96 serialize(x, parents=False) for x in commit.parents]
97 return data
98
99 prev_data = prev_data or []
100 next_data = next_data or []
101
102 current = [serialize(x) for x in commits]
103 commits = prev_data + current + next_data
104
105 dag = _dagwalker(repo, commits)
106
107 data = [[commit_id, vtx, edges, branch]
108 for commit_id, vtx, edges, branch in _colored(dag)]
109 return json.dumps(data), json.dumps(current)
110
111 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
112 if branch_name not in self.rhodecode_vcs_repo.branches_all:
113 h.flash('Branch {} is not found.'.format(h.escape(branch_name)),
114 category='warning')
115 redirect_url = h.route_path(
116 'repo_changelog_file', repo_name=repo_name,
117 commit_id=branch_name, f_path=f_path or '')
118 raise HTTPFound(redirect_url)
119
120 def _load_changelog_data(
121 self, c, collection, page, chunk_size, branch_name=None,
122 dynamic=False):
123
124 def url_generator(**kw):
125 query_params = {}
126 query_params.update(kw)
127 return h.route_path(
128 'repo_changelog',
129 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
130
131 c.total_cs = len(collection)
132 c.showing_commits = min(chunk_size, c.total_cs)
133 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
134 items_per_page=chunk_size, branch=branch_name,
135 url=url_generator)
136
137 c.next_page = c.pagination.next_page
138 c.prev_page = c.pagination.previous_page
139
140 if dynamic:
141 if self.request.GET.get('chunk') != 'next':
142 c.next_page = None
143 if self.request.GET.get('chunk') != 'prev':
144 c.prev_page = None
145
146 page_commit_ids = [x.raw_id for x in c.pagination]
147 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
148 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
149
150 def load_default_context(self):
151 c = self._get_local_tmpl_context(include_app_defaults=True)
152
153 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
154 c.repo_info = self.db_repo
155 c.rhodecode_repo = self.rhodecode_vcs_repo
156
157 self._register_global_c(c)
158 return c
159
160 @LoginRequired()
161 @HasRepoPermissionAnyDecorator(
162 'repository.read', 'repository.write', 'repository.admin')
163 @view_config(
164 route_name='repo_changelog', request_method='GET',
165 renderer='rhodecode:templates/changelog/changelog.mako')
166 @view_config(
167 route_name='repo_changelog_file', request_method='GET',
168 renderer='rhodecode:templates/changelog/changelog.mako')
169 def repo_changelog(self):
170 c = self.load_default_context()
171
172 commit_id = self.request.matchdict.get('commit_id')
173 f_path = self._get_f_path(self.request.matchdict)
174
175 chunk_size = 20
176
177 c.branch_name = branch_name = self.request.GET.get('branch') or ''
178 c.book_name = book_name = self.request.GET.get('bookmark') or ''
179 hist_limit = safe_int(self.request.GET.get('limit')) or None
180
181 p = safe_int(self.request.GET.get('page', 1), 1)
182
183 c.selected_name = branch_name or book_name
184 if not commit_id and branch_name:
185 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
186
187 c.changelog_for_path = f_path
188 pre_load = ['author', 'branch', 'date', 'message', 'parents']
189 commit_ids = []
190
191 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
192
193 try:
194 if f_path:
195 log.debug('generating changelog for path %s', f_path)
196 # get the history for the file !
197 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
198 try:
199 collection = base_commit.get_file_history(
200 f_path, limit=hist_limit, pre_load=pre_load)
201 if collection and partial_xhr:
202 # for ajax call we remove first one since we're looking
203 # at it right now in the context of a file commit
204 collection.pop(0)
205 except (NodeDoesNotExistError, CommitError):
206 # this node is not present at tip!
207 try:
208 commit = self._get_commit_or_redirect(commit_id)
209 collection = commit.get_file_history(f_path)
210 except RepositoryError as e:
211 h.flash(safe_str(e), category='warning')
212 redirect_url = h.route_path(
213 'repo_changelog', repo_name=self.db_repo_name)
214 raise HTTPFound(redirect_url)
215 collection = list(reversed(collection))
216 else:
217 collection = self.rhodecode_vcs_repo.get_commits(
218 branch_name=branch_name, pre_load=pre_load)
219
220 self._load_changelog_data(
221 c, collection, p, chunk_size, c.branch_name, dynamic=f_path)
222
223 except EmptyRepositoryError as e:
224 h.flash(safe_str(h.escape(e)), category='warning')
225 raise HTTPFound(
226 h.route_path('repo_summary', repo_name=self.db_repo_name))
227 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
228 log.exception(safe_str(e))
229 h.flash(safe_str(h.escape(e)), category='error')
230 raise HTTPFound(
231 h.route_path('repo_changelog', repo_name=self.db_repo_name))
232
233 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
234 # loading from ajax, we don't want the first result, it's popped
235 # in the code above
236 html = render(
237 'rhodecode:templates/changelog/changelog_file_history.mako',
238 self._get_template_context(c), self.request)
239 return Response(html)
240
241 if not f_path:
242 commit_ids = c.pagination
243
244 c.graph_data, c.graph_commits = self._graph(
245 self.rhodecode_vcs_repo, commit_ids)
246
247 return self._get_template_context(c)
248
249 @LoginRequired()
250 @HasRepoPermissionAnyDecorator(
251 'repository.read', 'repository.write', 'repository.admin')
252 @view_config(
253 route_name='repo_changelog_elements', request_method=('GET', 'POST'),
254 renderer='rhodecode:templates/changelog/changelog_elements.mako',
255 xhr=True)
256 def repo_changelog_elements(self):
257 c = self.load_default_context()
258 chunk_size = 20
259
260 def wrap_for_error(err):
261 html = '<tr>' \
262 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
263 '</tr>'.format(err)
264 return Response(html)
265
266 c.branch_name = branch_name = self.request.GET.get('branch') or ''
267 c.book_name = book_name = self.request.GET.get('bookmark') or ''
268
269 c.selected_name = branch_name or book_name
270 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
271 return wrap_for_error(
272 safe_str('Branch: {} is not valid'.format(branch_name)))
273
274 pre_load = ['author', 'branch', 'date', 'message', 'parents']
275 collection = self.rhodecode_vcs_repo.get_commits(
276 branch_name=branch_name, pre_load=pre_load)
277
278 p = safe_int(self.request.GET.get('page', 1), 1)
279 try:
280 self._load_changelog_data(
281 c, collection, p, chunk_size, dynamic=True)
282 except EmptyRepositoryError as e:
283 return wrap_for_error(safe_str(e))
284 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
285 log.exception('Failed to fetch commits')
286 return wrap_for_error(safe_str(e))
287
288 prev_data = None
289 next_data = None
290
291 prev_graph = json.loads(self.request.POST.get('graph', ''))
292
293 if self.request.GET.get('chunk') == 'prev':
294 next_data = prev_graph
295 elif self.request.GET.get('chunk') == 'next':
296 prev_data = prev_graph
297
298 c.graph_data, c.graph_commits = self._graph(
299 self.rhodecode_vcs_repo, c.pagination,
300 prev_data=prev_data, next_data=next_data)
301
302 return self._get_template_context(c)
@@ -1,266 +1,277 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.apps._base import add_route_with_slash
21 21
22 22
23 23 def includeme(config):
24 24
25 25 # Summary
26 26 # NOTE(marcink): one additional route is defined in very bottom, catch
27 27 # all pattern
28 28 config.add_route(
29 29 name='repo_summary_explicit',
30 30 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
31 31 config.add_route(
32 32 name='repo_summary_commits',
33 33 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
34 34
35 35 # repo commits
36 36 config.add_route(
37 37 name='repo_commit',
38 38 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
39 39
40 40 # repo files
41 41 config.add_route(
42 42 name='repo_archivefile',
43 43 pattern='/{repo_name:.*?[^/]}/archive/{fname}', repo_route=True)
44 44
45 45 config.add_route(
46 46 name='repo_files_diff',
47 47 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
48 48 config.add_route( # legacy route to make old links work
49 49 name='repo_files_diff_2way_redirect',
50 50 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
51 51
52 52 config.add_route(
53 53 name='repo_files',
54 54 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
55 55 config.add_route(
56 56 name='repo_files:default_path',
57 57 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
58 58 config.add_route(
59 59 name='repo_files:default_commit',
60 60 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
61 61
62 62 config.add_route(
63 63 name='repo_files:rendered',
64 64 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
65 65
66 66 config.add_route(
67 67 name='repo_files:annotated',
68 68 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
69 69 config.add_route(
70 70 name='repo_files:annotated_previous',
71 71 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
72 72
73 73 config.add_route(
74 74 name='repo_nodetree_full',
75 75 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
76 76 config.add_route(
77 77 name='repo_nodetree_full:default_path',
78 78 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
79 79
80 80 config.add_route(
81 81 name='repo_files_nodelist',
82 82 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
83 83
84 84 config.add_route(
85 85 name='repo_file_raw',
86 86 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
87 87
88 88 config.add_route(
89 89 name='repo_file_download',
90 90 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
91 91 config.add_route( # backward compat to keep old links working
92 92 name='repo_file_download:legacy',
93 93 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
94 94 repo_route=True)
95 95
96 96 config.add_route(
97 97 name='repo_file_history',
98 98 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
99 99
100 100 config.add_route(
101 101 name='repo_file_authors',
102 102 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
103 103
104 104 config.add_route(
105 105 name='repo_files_remove_file',
106 106 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
107 107 repo_route=True)
108 108 config.add_route(
109 109 name='repo_files_delete_file',
110 110 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
111 111 repo_route=True)
112 112 config.add_route(
113 113 name='repo_files_edit_file',
114 114 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
115 115 repo_route=True)
116 116 config.add_route(
117 117 name='repo_files_update_file',
118 118 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
119 119 repo_route=True)
120 120 config.add_route(
121 121 name='repo_files_add_file',
122 122 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
123 123 repo_route=True)
124 124 config.add_route(
125 125 name='repo_files_create_file',
126 126 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
127 127 repo_route=True)
128 128
129 129 # refs data
130 130 config.add_route(
131 131 name='repo_refs_data',
132 132 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
133 133
134 134 config.add_route(
135 135 name='repo_refs_changelog_data',
136 136 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
137 137
138 138 config.add_route(
139 139 name='repo_stats',
140 140 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
141 141
142 # Changelog
143 config.add_route(
144 name='repo_changelog',
145 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
146 config.add_route(
147 name='repo_changelog_file',
148 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
149 config.add_route(
150 name='repo_changelog_elements',
151 pattern='/{repo_name:.*?[^/]}/changelog_elements', repo_route=True)
152
142 153 # Tags
143 154 config.add_route(
144 155 name='tags_home',
145 156 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
146 157
147 158 # Branches
148 159 config.add_route(
149 160 name='branches_home',
150 161 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
151 162
152 163 config.add_route(
153 164 name='bookmarks_home',
154 165 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
155 166
156 167 # Pull Requests
157 168 config.add_route(
158 169 name='pullrequest_show',
159 170 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}',
160 171 repo_route=True)
161 172
162 173 config.add_route(
163 174 name='pullrequest_show_all',
164 175 pattern='/{repo_name:.*?[^/]}/pull-request',
165 176 repo_route=True, repo_accepted_types=['hg', 'git'])
166 177
167 178 config.add_route(
168 179 name='pullrequest_show_all_data',
169 180 pattern='/{repo_name:.*?[^/]}/pull-request-data',
170 181 repo_route=True, repo_accepted_types=['hg', 'git'])
171 182
172 183 # commits aka changesets
173 184 # TODO(dan): handle default landing revision ?
174 185 config.add_route(
175 186 name='changeset_home',
176 187 pattern='/{repo_name:.*?[^/]}/changeset/{revision}',
177 188 repo_route=True)
178 189 config.add_route(
179 190 name='changeset_children',
180 191 pattern='/{repo_name:.*?[^/]}/changeset_children/{revision}',
181 192 repo_route=True)
182 193 config.add_route(
183 194 name='changeset_parents',
184 195 pattern='/{repo_name:.*?[^/]}/changeset_parents/{revision}',
185 196 repo_route=True)
186 197
187 198 # Settings
188 199 config.add_route(
189 200 name='edit_repo',
190 201 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
191 202
192 203 # Settings advanced
193 204 config.add_route(
194 205 name='edit_repo_advanced',
195 206 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
196 207 config.add_route(
197 208 name='edit_repo_advanced_delete',
198 209 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
199 210 config.add_route(
200 211 name='edit_repo_advanced_locking',
201 212 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
202 213 config.add_route(
203 214 name='edit_repo_advanced_journal',
204 215 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
205 216 config.add_route(
206 217 name='edit_repo_advanced_fork',
207 218 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
208 219
209 220 # Caches
210 221 config.add_route(
211 222 name='edit_repo_caches',
212 223 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
213 224
214 225 # Permissions
215 226 config.add_route(
216 227 name='edit_repo_perms',
217 228 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
218 229
219 230 # Repo Review Rules
220 231 config.add_route(
221 232 name='repo_reviewers',
222 233 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
223 234
224 235 config.add_route(
225 236 name='repo_default_reviewers_data',
226 237 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
227 238
228 239 # Maintenance
229 240 config.add_route(
230 241 name='repo_maintenance',
231 242 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
232 243
233 244 config.add_route(
234 245 name='repo_maintenance_execute',
235 246 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
236 247
237 248 # Strip
238 249 config.add_route(
239 250 name='strip',
240 251 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
241 252
242 253 config.add_route(
243 254 name='strip_check',
244 255 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
245 256
246 257 config.add_route(
247 258 name='strip_execute',
248 259 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
249 260
250 261 # ATOM/RSS Feed
251 262 config.add_route(
252 263 name='rss_feed_home',
253 264 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
254 265
255 266 config.add_route(
256 267 name='atom_feed_home',
257 268 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
258 269
259 270 # NOTE(marcink): needs to be at the end for catch-all
260 271 add_route_with_slash(
261 272 config,
262 273 name='repo_summary',
263 274 pattern='/{repo_name:.*?[^/]}', repo_route=True)
264 275
265 276 # Scan module for configuration decorators.
266 277 config.scan()
@@ -1,192 +1,195 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22
23 23 import pytest
24 24
25 from rhodecode.controllers.changelog import DEFAULT_CHANGELOG_SIZE
26 from rhodecode.tests import url, TestController
27 from rhodecode.tests.utils import AssertResponse
28
25 from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
26 from rhodecode.tests import TestController
29 27
30 28 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
31 29
32 30
31 def route_path(name, params=None, **kwargs):
32 import urllib
33
34 base_url = {
35 'repo_changelog':'/{repo_name}/changelog',
36 'repo_changelog_file':'/{repo_name}/changelog/{commit_id}/{f_path}',
37 'repo_changelog_elements':'/{repo_name}/changelog_elements',
38 }[name].format(**kwargs)
39
40 if params:
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 return base_url
43
44
33 45 class TestChangelogController(TestController):
34 46
35 def test_index(self, backend):
47 def test_changelog(self, backend):
36 48 self.log_user()
37 response = self.app.get(url(controller='changelog', action='index',
38 repo_name=backend.repo_name))
49 response = self.app.get(
50 route_path('repo_changelog', repo_name=backend.repo_name))
39 51
40 52 first_idx = -1
41 53 last_idx = -DEFAULT_CHANGELOG_SIZE
42 54 self.assert_commit_range_on_page(
43 55 response, first_idx, last_idx, backend)
44 56
45 57 @pytest.mark.backends("hg", "git")
46 def test_index_filtered_by_branch(self, backend):
58 def test_changelog_filtered_by_branch(self, backend):
47 59 self.log_user()
48 60 self.app.get(
49 url(
50 controller='changelog',
51 action='index',
52 repo_name=backend.repo_name,
53 branch=backend.default_branch_name),
61 route_path('repo_changelog', repo_name=backend.repo_name,
62 params=dict(branch=backend.default_branch_name)),
54 63 status=200)
55 64
56 65 @pytest.mark.backends("svn")
57 def test_index_filtered_by_branch_svn(self, autologin_user, backend):
66 def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
58 67 repo = backend['svn-simple-layout']
59 68 response = self.app.get(
60 url(
61 controller='changelog',
62 action='index',
63 repo_name=repo.repo_name,
64 branch='trunk'),
69 route_path('repo_changelog', repo_name=repo.repo_name,
70 params=dict(branch='trunk')),
65 71 status=200)
66 72
67 73 self.assert_commits_on_page(
68 74 response, indexes=[15, 12, 7, 3, 2, 1])
69 75
70 def test_index_filtered_by_wrong_branch(self, backend):
76 def test_changelog_filtered_by_wrong_branch(self, backend):
71 77 self.log_user()
72 78 branch = 'wrong-branch-name'
73 79 response = self.app.get(
74 url(
75 controller='changelog',
76 action='index',
77 repo_name=backend.repo_name,
78 branch=branch),
80 route_path('repo_changelog', repo_name=backend.repo_name,
81 params=dict(branch=branch)),
79 82 status=302)
80 83 expected_url = '/{repo}/changelog/{branch}'.format(
81 84 repo=backend.repo_name, branch=branch)
82 85 assert expected_url in response.location
83 86 response = response.follow()
84 87 expected_warning = 'Branch {} is not found.'.format(branch)
85 88 assert expected_warning in response.body
86 89
87 90 def assert_commits_on_page(self, response, indexes):
88 91 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.body)]
89 92 assert found_indexes == indexes
90 93
91 94 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
92 def test_index_filtered_by_branch_with_merges(
95 def test_changelog_filtered_by_branch_with_merges(
93 96 self, autologin_user, backend):
94 97
95 98 # Note: The changelog of branch "b" does not contain the commit "a1"
96 99 # although this is a parent of commit "b1". And branch "b" has commits
97 100 # which have a smaller index than commit "a1".
98 101 commits = [
99 102 {'message': 'a'},
100 103 {'message': 'b', 'branch': 'b'},
101 104 {'message': 'a1', 'parents': ['a']},
102 105 {'message': 'b1', 'branch': 'b', 'parents': ['b', 'a1']},
103 106 ]
104 107 backend.create_repo(commits)
105 108
106 109 self.app.get(
107 url('changelog_home',
108 controller='changelog',
109 action='index',
110 repo_name=backend.repo_name,
111 branch='b'),
110 route_path('repo_changelog', repo_name=backend.repo_name,
111 params=dict(branch='b')),
112 112 status=200)
113 113
114 114 @pytest.mark.backends("hg")
115 def test_index_closed_branches(self, autologin_user, backend):
115 def test_changelog_closed_branches(self, autologin_user, backend):
116 116 repo = backend['closed_branch']
117 117 response = self.app.get(
118 url(
119 controller='changelog',
120 action='index',
121 repo_name=repo.repo_name,
122 branch='experimental'),
118 route_path('repo_changelog', repo_name=repo.repo_name,
119 params=dict(branch='experimental')),
123 120 status=200)
124 121
125 122 self.assert_commits_on_page(
126 123 response, indexes=[3, 1])
127 124
128 def test_index_pagination(self, backend):
125 def test_changelog_pagination(self, backend):
129 126 self.log_user()
130 127 # pagination, walk up to page 6
131 changelog_url = url(
132 controller='changelog', action='index',
133 repo_name=backend.repo_name)
128 changelog_url = route_path(
129 'repo_changelog', repo_name=backend.repo_name)
130
134 131 for page in range(1, 7):
135 132 response = self.app.get(changelog_url, {'page': page})
136 133
137 134 first_idx = -DEFAULT_CHANGELOG_SIZE * (page - 1) - 1
138 135 last_idx = -DEFAULT_CHANGELOG_SIZE * page
139 136 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
140 137
141 138 def assert_commit_range_on_page(
142 139 self, response, first_idx, last_idx, backend):
143 140 input_template = (
144 141 """<input class="commit-range" id="%(raw_id)s" """
145 142 """name="%(raw_id)s" type="checkbox" value="1" />"""
146 143 )
147 144 commit_span_template = """<span class="commit_hash">r%s:%s</span>"""
148 145 repo = backend.repo
149 146
150 147 first_commit_on_page = repo.get_commit(commit_idx=first_idx)
151 148 response.mustcontain(
152 149 input_template % {'raw_id': first_commit_on_page.raw_id})
153 150 response.mustcontain(commit_span_template % (
154 151 first_commit_on_page.idx, first_commit_on_page.short_id)
155 152 )
156 153
157 154 last_commit_on_page = repo.get_commit(commit_idx=last_idx)
158 155 response.mustcontain(
159 156 input_template % {'raw_id': last_commit_on_page.raw_id})
160 157 response.mustcontain(commit_span_template % (
161 158 last_commit_on_page.idx, last_commit_on_page.short_id)
162 159 )
163 160
164 161 first_commit_of_next_page = repo.get_commit(commit_idx=last_idx - 1)
165 162 first_span_of_next_page = commit_span_template % (
166 163 first_commit_of_next_page.idx, first_commit_of_next_page.short_id)
167 164 assert first_span_of_next_page not in response
168 165
169 def test_index_with_filenode(self, backend):
166 @pytest.mark.parametrize('test_path', [
167 'vcs/exceptions.py',
168 '/vcs/exceptions.py',
169 '//vcs/exceptions.py'
170 ])
171 def test_changelog_with_filenode(self, backend, test_path):
170 172 self.log_user()
171 response = self.app.get(url(
172 controller='changelog', action='index', revision='tip',
173 f_path='/vcs/exceptions.py', repo_name=backend.repo_name))
173 response = self.app.get(
174 route_path('repo_changelog_file', repo_name=backend.repo_name,
175 commit_id='tip', f_path=test_path),
176 )
174 177
175 178 # history commits messages
176 179 response.mustcontain('Added exceptions module, this time for real')
177 180 response.mustcontain('Added not implemented hg backend test case')
178 181 response.mustcontain('Added BaseChangeset class')
179 182
180 def test_index_with_filenode_that_is_dirnode(self, backend):
183 def test_changelog_with_filenode_that_is_dirnode(self, backend):
181 184 self.log_user()
182 response = self.app.get(url(controller='changelog', action='index',
183 revision='tip', f_path='/tests',
184 repo_name=backend.repo_name))
185 assert response.status == '302 Found'
185 self.app.get(
186 route_path('repo_changelog_file', repo_name=backend.repo_name,
187 commit_id='tip', f_path='/tests'),
188 status=302)
186 189
187 def test_index_with_filenode_not_existing(self, backend):
190 def test_changelog_with_filenode_not_existing(self, backend):
188 191 self.log_user()
189 response = self.app.get(url(controller='changelog', action='index',
190 revision='tip', f_path='/wrong_path',
191 repo_name=backend.repo_name))
192 assert response.status == '302 Found'
192 self.app.get(
193 route_path('repo_changelog_file', repo_name=backend.repo_name,
194 commit_id='tip', f_path='wrong_path'),
195 status=302)
@@ -1,731 +1,715 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 # prefix for non repository related links needs to be prefixed with `/`
36 36 ADMIN_PREFIX = '/_admin'
37 37 STATIC_FILE_PREFIX = '/_static'
38 38
39 39 # Default requirements for URL parts
40 40 URL_NAME_REQUIREMENTS = {
41 41 # group name can have a slash in them, but they must not end with a slash
42 42 'group_name': r'.*?[^/]',
43 43 'repo_group_name': r'.*?[^/]',
44 44 # repo names can have a slash in them, but they must not end with a slash
45 45 'repo_name': r'.*?[^/]',
46 46 # file path eats up everything at the end
47 47 'f_path': r'.*',
48 48 # reference types
49 49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 51 }
52 52
53 53
54 54 class JSRoutesMapper(Mapper):
55 55 """
56 56 Wrapper for routes.Mapper to make pyroutes compatible url definitions
57 57 """
58 58 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
59 59 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
60 60 def __init__(self, *args, **kw):
61 61 super(JSRoutesMapper, self).__init__(*args, **kw)
62 62 self._jsroutes = []
63 63
64 64 def connect(self, *args, **kw):
65 65 """
66 66 Wrapper for connect to take an extra argument jsroute=True
67 67
68 68 :param jsroute: boolean, if True will add the route to the pyroutes list
69 69 """
70 70 if kw.pop('jsroute', False):
71 71 if not self._named_route_regex.match(args[0]):
72 72 raise Exception('only named routes can be added to pyroutes')
73 73 self._jsroutes.append(args[0])
74 74
75 75 super(JSRoutesMapper, self).connect(*args, **kw)
76 76
77 77 def _extract_route_information(self, route):
78 78 """
79 79 Convert a route into tuple(name, path, args), eg:
80 80 ('show_user', '/profile/%(username)s', ['username'])
81 81 """
82 82 routepath = route.routepath
83 83 def replace(matchobj):
84 84 if matchobj.group(1):
85 85 return "%%(%s)s" % matchobj.group(1).split(':')[0]
86 86 else:
87 87 return "%%(%s)s" % matchobj.group(2)
88 88
89 89 routepath = self._argument_prog.sub(replace, routepath)
90 90 return (
91 91 route.name,
92 92 routepath,
93 93 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
94 94 for arg in self._argument_prog.findall(route.routepath)]
95 95 )
96 96
97 97 def jsroutes(self):
98 98 """
99 99 Return a list of pyroutes.js compatible routes
100 100 """
101 101 for route_name in self._jsroutes:
102 102 yield self._extract_route_information(self._routenames[route_name])
103 103
104 104
105 105 def make_map(config):
106 106 """Create, configure and return the routes Mapper"""
107 107 rmap = JSRoutesMapper(
108 108 directory=config['pylons.paths']['controllers'],
109 109 always_scan=config['debug'])
110 110 rmap.minimization = False
111 111 rmap.explicit = False
112 112
113 113 from rhodecode.lib.utils2 import str2bool
114 114 from rhodecode.model import repo, repo_group
115 115
116 116 def check_repo(environ, match_dict):
117 117 """
118 118 check for valid repository for proper 404 handling
119 119
120 120 :param environ:
121 121 :param match_dict:
122 122 """
123 123 repo_name = match_dict.get('repo_name')
124 124
125 125 if match_dict.get('f_path'):
126 126 # fix for multiple initial slashes that causes errors
127 127 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
128 128 repo_model = repo.RepoModel()
129 129 by_name_match = repo_model.get_by_repo_name(repo_name)
130 130 # if we match quickly from database, short circuit the operation,
131 131 # and validate repo based on the type.
132 132 if by_name_match:
133 133 return True
134 134
135 135 by_id_match = repo_model.get_repo_by_id(repo_name)
136 136 if by_id_match:
137 137 repo_name = by_id_match.repo_name
138 138 match_dict['repo_name'] = repo_name
139 139 return True
140 140
141 141 return False
142 142
143 143 def check_group(environ, match_dict):
144 144 """
145 145 check for valid repository group path for proper 404 handling
146 146
147 147 :param environ:
148 148 :param match_dict:
149 149 """
150 150 repo_group_name = match_dict.get('group_name')
151 151 repo_group_model = repo_group.RepoGroupModel()
152 152 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
153 153 if by_name_match:
154 154 return True
155 155
156 156 return False
157 157
158 158 def check_user_group(environ, match_dict):
159 159 """
160 160 check for valid user group for proper 404 handling
161 161
162 162 :param environ:
163 163 :param match_dict:
164 164 """
165 165 return True
166 166
167 167 def check_int(environ, match_dict):
168 168 return match_dict.get('id').isdigit()
169 169
170 170
171 171 #==========================================================================
172 172 # CUSTOM ROUTES HERE
173 173 #==========================================================================
174 174
175 175 # ping and pylons error test
176 176 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
177 177 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
178 178
179 179 # ADMIN REPOSITORY ROUTES
180 180 with rmap.submapper(path_prefix=ADMIN_PREFIX,
181 181 controller='admin/repos') as m:
182 182 m.connect('repos', '/repos',
183 183 action='create', conditions={'method': ['POST']})
184 184 m.connect('repos', '/repos',
185 185 action='index', conditions={'method': ['GET']})
186 186 m.connect('new_repo', '/create_repository', jsroute=True,
187 187 action='create_repository', conditions={'method': ['GET']})
188 188 m.connect('delete_repo', '/repos/{repo_name}',
189 189 action='delete', conditions={'method': ['DELETE']},
190 190 requirements=URL_NAME_REQUIREMENTS)
191 191 m.connect('repo', '/repos/{repo_name}',
192 192 action='show', conditions={'method': ['GET'],
193 193 'function': check_repo},
194 194 requirements=URL_NAME_REQUIREMENTS)
195 195
196 196 # ADMIN REPOSITORY GROUPS ROUTES
197 197 with rmap.submapper(path_prefix=ADMIN_PREFIX,
198 198 controller='admin/repo_groups') as m:
199 199 m.connect('repo_groups', '/repo_groups',
200 200 action='create', conditions={'method': ['POST']})
201 201 m.connect('repo_groups', '/repo_groups',
202 202 action='index', conditions={'method': ['GET']})
203 203 m.connect('new_repo_group', '/repo_groups/new',
204 204 action='new', conditions={'method': ['GET']})
205 205 m.connect('update_repo_group', '/repo_groups/{group_name}',
206 206 action='update', conditions={'method': ['PUT'],
207 207 'function': check_group},
208 208 requirements=URL_NAME_REQUIREMENTS)
209 209
210 210 # EXTRAS REPO GROUP ROUTES
211 211 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
212 212 action='edit',
213 213 conditions={'method': ['GET'], 'function': check_group},
214 214 requirements=URL_NAME_REQUIREMENTS)
215 215 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
216 216 action='edit',
217 217 conditions={'method': ['PUT'], 'function': check_group},
218 218 requirements=URL_NAME_REQUIREMENTS)
219 219
220 220 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
221 221 action='edit_repo_group_advanced',
222 222 conditions={'method': ['GET'], 'function': check_group},
223 223 requirements=URL_NAME_REQUIREMENTS)
224 224 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
225 225 action='edit_repo_group_advanced',
226 226 conditions={'method': ['PUT'], 'function': check_group},
227 227 requirements=URL_NAME_REQUIREMENTS)
228 228
229 229 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
230 230 action='edit_repo_group_perms',
231 231 conditions={'method': ['GET'], 'function': check_group},
232 232 requirements=URL_NAME_REQUIREMENTS)
233 233 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
234 234 action='update_perms',
235 235 conditions={'method': ['PUT'], 'function': check_group},
236 236 requirements=URL_NAME_REQUIREMENTS)
237 237
238 238 m.connect('delete_repo_group', '/repo_groups/{group_name}',
239 239 action='delete', conditions={'method': ['DELETE'],
240 240 'function': check_group},
241 241 requirements=URL_NAME_REQUIREMENTS)
242 242
243 243 # ADMIN USER ROUTES
244 244 with rmap.submapper(path_prefix=ADMIN_PREFIX,
245 245 controller='admin/users') as m:
246 246 m.connect('users', '/users',
247 247 action='create', conditions={'method': ['POST']})
248 248 m.connect('new_user', '/users/new',
249 249 action='new', conditions={'method': ['GET']})
250 250 m.connect('update_user', '/users/{user_id}',
251 251 action='update', conditions={'method': ['PUT']})
252 252 m.connect('delete_user', '/users/{user_id}',
253 253 action='delete', conditions={'method': ['DELETE']})
254 254 m.connect('edit_user', '/users/{user_id}/edit',
255 255 action='edit', conditions={'method': ['GET']}, jsroute=True)
256 256 m.connect('user', '/users/{user_id}',
257 257 action='show', conditions={'method': ['GET']})
258 258 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
259 259 action='reset_password', conditions={'method': ['POST']})
260 260 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
261 261 action='create_personal_repo_group', conditions={'method': ['POST']})
262 262
263 263 # EXTRAS USER ROUTES
264 264 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
265 265 action='edit_advanced', conditions={'method': ['GET']})
266 266 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
267 267 action='update_advanced', conditions={'method': ['PUT']})
268 268
269 269 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
270 270 action='edit_global_perms', conditions={'method': ['GET']})
271 271 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
272 272 action='update_global_perms', conditions={'method': ['PUT']})
273 273
274 274 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
275 275 action='edit_perms_summary', conditions={'method': ['GET']})
276 276
277
278 277 # ADMIN USER GROUPS REST ROUTES
279 278 with rmap.submapper(path_prefix=ADMIN_PREFIX,
280 279 controller='admin/user_groups') as m:
281 280 m.connect('users_groups', '/user_groups',
282 281 action='create', conditions={'method': ['POST']})
283 282 m.connect('users_groups', '/user_groups',
284 283 action='index', conditions={'method': ['GET']})
285 284 m.connect('new_users_group', '/user_groups/new',
286 285 action='new', conditions={'method': ['GET']})
287 286 m.connect('update_users_group', '/user_groups/{user_group_id}',
288 287 action='update', conditions={'method': ['PUT']})
289 288 m.connect('delete_users_group', '/user_groups/{user_group_id}',
290 289 action='delete', conditions={'method': ['DELETE']})
291 290 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
292 291 action='edit', conditions={'method': ['GET']},
293 292 function=check_user_group)
294 293
295 294 # EXTRAS USER GROUP ROUTES
296 295 m.connect('edit_user_group_global_perms',
297 296 '/user_groups/{user_group_id}/edit/global_permissions',
298 297 action='edit_global_perms', conditions={'method': ['GET']})
299 298 m.connect('edit_user_group_global_perms',
300 299 '/user_groups/{user_group_id}/edit/global_permissions',
301 300 action='update_global_perms', conditions={'method': ['PUT']})
302 301 m.connect('edit_user_group_perms_summary',
303 302 '/user_groups/{user_group_id}/edit/permissions_summary',
304 303 action='edit_perms_summary', conditions={'method': ['GET']})
305 304
306 305 m.connect('edit_user_group_perms',
307 306 '/user_groups/{user_group_id}/edit/permissions',
308 307 action='edit_perms', conditions={'method': ['GET']})
309 308 m.connect('edit_user_group_perms',
310 309 '/user_groups/{user_group_id}/edit/permissions',
311 310 action='update_perms', conditions={'method': ['PUT']})
312 311
313 312 m.connect('edit_user_group_advanced',
314 313 '/user_groups/{user_group_id}/edit/advanced',
315 314 action='edit_advanced', conditions={'method': ['GET']})
316 315
317 316 m.connect('edit_user_group_advanced_sync',
318 317 '/user_groups/{user_group_id}/edit/advanced/sync',
319 318 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
320 319
321 320 m.connect('edit_user_group_members',
322 321 '/user_groups/{user_group_id}/edit/members', jsroute=True,
323 322 action='user_group_members', conditions={'method': ['GET']})
324 323
325 324 # ADMIN PERMISSIONS ROUTES
326 325 with rmap.submapper(path_prefix=ADMIN_PREFIX,
327 326 controller='admin/permissions') as m:
328 327 m.connect('admin_permissions_application', '/permissions/application',
329 328 action='permission_application_update', conditions={'method': ['POST']})
330 329 m.connect('admin_permissions_application', '/permissions/application',
331 330 action='permission_application', conditions={'method': ['GET']})
332 331
333 332 m.connect('admin_permissions_global', '/permissions/global',
334 333 action='permission_global_update', conditions={'method': ['POST']})
335 334 m.connect('admin_permissions_global', '/permissions/global',
336 335 action='permission_global', conditions={'method': ['GET']})
337 336
338 337 m.connect('admin_permissions_object', '/permissions/object',
339 338 action='permission_objects_update', conditions={'method': ['POST']})
340 339 m.connect('admin_permissions_object', '/permissions/object',
341 340 action='permission_objects', conditions={'method': ['GET']})
342 341
343 342 m.connect('admin_permissions_ips', '/permissions/ips',
344 343 action='permission_ips', conditions={'method': ['POST']})
345 344 m.connect('admin_permissions_ips', '/permissions/ips',
346 345 action='permission_ips', conditions={'method': ['GET']})
347 346
348 347 m.connect('admin_permissions_overview', '/permissions/overview',
349 348 action='permission_perms', conditions={'method': ['GET']})
350 349
351 350 # ADMIN DEFAULTS REST ROUTES
352 351 with rmap.submapper(path_prefix=ADMIN_PREFIX,
353 352 controller='admin/defaults') as m:
354 353 m.connect('admin_defaults_repositories', '/defaults/repositories',
355 354 action='update_repository_defaults', conditions={'method': ['POST']})
356 355 m.connect('admin_defaults_repositories', '/defaults/repositories',
357 356 action='index', conditions={'method': ['GET']})
358 357
359 358 # ADMIN SETTINGS ROUTES
360 359 with rmap.submapper(path_prefix=ADMIN_PREFIX,
361 360 controller='admin/settings') as m:
362 361
363 362 # default
364 363 m.connect('admin_settings', '/settings',
365 364 action='settings_global_update',
366 365 conditions={'method': ['POST']})
367 366 m.connect('admin_settings', '/settings',
368 367 action='settings_global', conditions={'method': ['GET']})
369 368
370 369 m.connect('admin_settings_vcs', '/settings/vcs',
371 370 action='settings_vcs_update',
372 371 conditions={'method': ['POST']})
373 372 m.connect('admin_settings_vcs', '/settings/vcs',
374 373 action='settings_vcs',
375 374 conditions={'method': ['GET']})
376 375 m.connect('admin_settings_vcs', '/settings/vcs',
377 376 action='delete_svn_pattern',
378 377 conditions={'method': ['DELETE']})
379 378
380 379 m.connect('admin_settings_mapping', '/settings/mapping',
381 380 action='settings_mapping_update',
382 381 conditions={'method': ['POST']})
383 382 m.connect('admin_settings_mapping', '/settings/mapping',
384 383 action='settings_mapping', conditions={'method': ['GET']})
385 384
386 385 m.connect('admin_settings_global', '/settings/global',
387 386 action='settings_global_update',
388 387 conditions={'method': ['POST']})
389 388 m.connect('admin_settings_global', '/settings/global',
390 389 action='settings_global', conditions={'method': ['GET']})
391 390
392 391 m.connect('admin_settings_visual', '/settings/visual',
393 392 action='settings_visual_update',
394 393 conditions={'method': ['POST']})
395 394 m.connect('admin_settings_visual', '/settings/visual',
396 395 action='settings_visual', conditions={'method': ['GET']})
397 396
398 397 m.connect('admin_settings_issuetracker',
399 398 '/settings/issue-tracker', action='settings_issuetracker',
400 399 conditions={'method': ['GET']})
401 400 m.connect('admin_settings_issuetracker_save',
402 401 '/settings/issue-tracker/save',
403 402 action='settings_issuetracker_save',
404 403 conditions={'method': ['POST']})
405 404 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
406 405 action='settings_issuetracker_test',
407 406 conditions={'method': ['POST']})
408 407 m.connect('admin_issuetracker_delete',
409 408 '/settings/issue-tracker/delete',
410 409 action='settings_issuetracker_delete',
411 410 conditions={'method': ['DELETE']})
412 411
413 412 m.connect('admin_settings_email', '/settings/email',
414 413 action='settings_email_update',
415 414 conditions={'method': ['POST']})
416 415 m.connect('admin_settings_email', '/settings/email',
417 416 action='settings_email', conditions={'method': ['GET']})
418 417
419 418 m.connect('admin_settings_hooks', '/settings/hooks',
420 419 action='settings_hooks_update',
421 420 conditions={'method': ['POST', 'DELETE']})
422 421 m.connect('admin_settings_hooks', '/settings/hooks',
423 422 action='settings_hooks', conditions={'method': ['GET']})
424 423
425 424 m.connect('admin_settings_search', '/settings/search',
426 425 action='settings_search', conditions={'method': ['GET']})
427 426
428 427 m.connect('admin_settings_supervisor', '/settings/supervisor',
429 428 action='settings_supervisor', conditions={'method': ['GET']})
430 429 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
431 430 action='settings_supervisor_log', conditions={'method': ['GET']})
432 431
433 432 m.connect('admin_settings_labs', '/settings/labs',
434 433 action='settings_labs_update',
435 434 conditions={'method': ['POST']})
436 435 m.connect('admin_settings_labs', '/settings/labs',
437 436 action='settings_labs', conditions={'method': ['GET']})
438 437
439 438 # ADMIN MY ACCOUNT
440 439 with rmap.submapper(path_prefix=ADMIN_PREFIX,
441 440 controller='admin/my_account') as m:
442 441
443 442 # NOTE(marcink): this needs to be kept for password force flag to be
444 443 # handled in pylons controllers, remove after full migration to pyramid
445 444 m.connect('my_account_password', '/my_account/password',
446 445 action='my_account_password', conditions={'method': ['GET']})
447 446
448 447 # USER JOURNAL
449 448 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
450 449 controller='journal', action='index')
451 450 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
452 451 controller='journal', action='journal_rss')
453 452 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
454 453 controller='journal', action='journal_atom')
455 454
456 455 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
457 456 controller='journal', action='public_journal')
458 457
459 458 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
460 459 controller='journal', action='public_journal_rss')
461 460
462 461 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
463 462 controller='journal', action='public_journal_rss')
464 463
465 464 rmap.connect('public_journal_atom',
466 465 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
467 466 action='public_journal_atom')
468 467
469 468 rmap.connect('public_journal_atom_old',
470 469 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
471 470 action='public_journal_atom')
472 471
473 472 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
474 473 controller='journal', action='toggle_following', jsroute=True,
475 474 conditions={'method': ['POST']})
476 475
477 476 #==========================================================================
478 477 # REPOSITORY ROUTES
479 478 #==========================================================================
480 479
481 480 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
482 481 controller='admin/repos', action='repo_creating',
483 482 requirements=URL_NAME_REQUIREMENTS)
484 483 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
485 484 controller='admin/repos', action='repo_check',
486 485 requirements=URL_NAME_REQUIREMENTS)
487 486
488 487 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
489 488 controller='changeset', revision='tip',
490 489 conditions={'function': check_repo},
491 490 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
492 491 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
493 492 controller='changeset', revision='tip', action='changeset_children',
494 493 conditions={'function': check_repo},
495 494 requirements=URL_NAME_REQUIREMENTS)
496 495 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
497 496 controller='changeset', revision='tip', action='changeset_parents',
498 497 conditions={'function': check_repo},
499 498 requirements=URL_NAME_REQUIREMENTS)
500 499
501 500 # repo edit options
502 501 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
503 502 controller='admin/repos', action='edit_fields',
504 503 conditions={'method': ['GET'], 'function': check_repo},
505 504 requirements=URL_NAME_REQUIREMENTS)
506 505 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
507 506 controller='admin/repos', action='create_repo_field',
508 507 conditions={'method': ['PUT'], 'function': check_repo},
509 508 requirements=URL_NAME_REQUIREMENTS)
510 509 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
511 510 controller='admin/repos', action='delete_repo_field',
512 511 conditions={'method': ['DELETE'], 'function': check_repo},
513 512 requirements=URL_NAME_REQUIREMENTS)
514 513
515 514 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
516 515 controller='admin/repos', action='toggle_locking',
517 516 conditions={'method': ['GET'], 'function': check_repo},
518 517 requirements=URL_NAME_REQUIREMENTS)
519 518
520 519 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
521 520 controller='admin/repos', action='edit_remote_form',
522 521 conditions={'method': ['GET'], 'function': check_repo},
523 522 requirements=URL_NAME_REQUIREMENTS)
524 523 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
525 524 controller='admin/repos', action='edit_remote',
526 525 conditions={'method': ['PUT'], 'function': check_repo},
527 526 requirements=URL_NAME_REQUIREMENTS)
528 527
529 528 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
530 529 controller='admin/repos', action='edit_statistics_form',
531 530 conditions={'method': ['GET'], 'function': check_repo},
532 531 requirements=URL_NAME_REQUIREMENTS)
533 532 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
534 533 controller='admin/repos', action='edit_statistics',
535 534 conditions={'method': ['PUT'], 'function': check_repo},
536 535 requirements=URL_NAME_REQUIREMENTS)
537 536 rmap.connect('repo_settings_issuetracker',
538 537 '/{repo_name}/settings/issue-tracker',
539 538 controller='admin/repos', action='repo_issuetracker',
540 539 conditions={'method': ['GET'], 'function': check_repo},
541 540 requirements=URL_NAME_REQUIREMENTS)
542 541 rmap.connect('repo_issuetracker_test',
543 542 '/{repo_name}/settings/issue-tracker/test',
544 543 controller='admin/repos', action='repo_issuetracker_test',
545 544 conditions={'method': ['POST'], 'function': check_repo},
546 545 requirements=URL_NAME_REQUIREMENTS)
547 546 rmap.connect('repo_issuetracker_delete',
548 547 '/{repo_name}/settings/issue-tracker/delete',
549 548 controller='admin/repos', action='repo_issuetracker_delete',
550 549 conditions={'method': ['DELETE'], 'function': check_repo},
551 550 requirements=URL_NAME_REQUIREMENTS)
552 551 rmap.connect('repo_issuetracker_save',
553 552 '/{repo_name}/settings/issue-tracker/save',
554 553 controller='admin/repos', action='repo_issuetracker_save',
555 554 conditions={'method': ['POST'], 'function': check_repo},
556 555 requirements=URL_NAME_REQUIREMENTS)
557 556 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
558 557 controller='admin/repos', action='repo_settings_vcs_update',
559 558 conditions={'method': ['POST'], 'function': check_repo},
560 559 requirements=URL_NAME_REQUIREMENTS)
561 560 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
562 561 controller='admin/repos', action='repo_settings_vcs',
563 562 conditions={'method': ['GET'], 'function': check_repo},
564 563 requirements=URL_NAME_REQUIREMENTS)
565 564 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
566 565 controller='admin/repos', action='repo_delete_svn_pattern',
567 566 conditions={'method': ['DELETE'], 'function': check_repo},
568 567 requirements=URL_NAME_REQUIREMENTS)
569 568 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
570 569 controller='admin/repos', action='repo_settings_pullrequest',
571 570 conditions={'method': ['GET', 'POST'], 'function': check_repo},
572 571 requirements=URL_NAME_REQUIREMENTS)
573 572
574 573 # still working url for backward compat.
575 574 rmap.connect('raw_changeset_home_depraced',
576 575 '/{repo_name}/raw-changeset/{revision}',
577 576 controller='changeset', action='changeset_raw',
578 577 revision='tip', conditions={'function': check_repo},
579 578 requirements=URL_NAME_REQUIREMENTS)
580 579
581 580 # new URLs
582 581 rmap.connect('changeset_raw_home',
583 582 '/{repo_name}/changeset-diff/{revision}',
584 583 controller='changeset', action='changeset_raw',
585 584 revision='tip', conditions={'function': check_repo},
586 585 requirements=URL_NAME_REQUIREMENTS)
587 586
588 587 rmap.connect('changeset_patch_home',
589 588 '/{repo_name}/changeset-patch/{revision}',
590 589 controller='changeset', action='changeset_patch',
591 590 revision='tip', conditions={'function': check_repo},
592 591 requirements=URL_NAME_REQUIREMENTS)
593 592
594 593 rmap.connect('changeset_download_home',
595 594 '/{repo_name}/changeset-download/{revision}',
596 595 controller='changeset', action='changeset_download',
597 596 revision='tip', conditions={'function': check_repo},
598 597 requirements=URL_NAME_REQUIREMENTS)
599 598
600 599 rmap.connect('changeset_comment',
601 600 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
602 601 controller='changeset', revision='tip', action='comment',
603 602 conditions={'function': check_repo},
604 603 requirements=URL_NAME_REQUIREMENTS)
605 604
606 605 rmap.connect('changeset_comment_preview',
607 606 '/{repo_name}/changeset/comment/preview', jsroute=True,
608 607 controller='changeset', action='preview_comment',
609 608 conditions={'function': check_repo, 'method': ['POST']},
610 609 requirements=URL_NAME_REQUIREMENTS)
611 610
612 611 rmap.connect('changeset_comment_delete',
613 612 '/{repo_name}/changeset/comment/{comment_id}/delete',
614 613 controller='changeset', action='delete_comment',
615 614 conditions={'function': check_repo, 'method': ['DELETE']},
616 615 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
617 616
618 617 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
619 618 controller='changeset', action='changeset_info',
620 619 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
621 620
622 621 rmap.connect('compare_home',
623 622 '/{repo_name}/compare',
624 623 controller='compare', action='index',
625 624 conditions={'function': check_repo},
626 625 requirements=URL_NAME_REQUIREMENTS)
627 626
628 627 rmap.connect('compare_url',
629 628 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
630 629 controller='compare', action='compare',
631 630 conditions={'function': check_repo},
632 631 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
633 632
634 633 rmap.connect('pullrequest_home',
635 634 '/{repo_name}/pull-request/new', controller='pullrequests',
636 635 action='index', conditions={'function': check_repo,
637 636 'method': ['GET']},
638 637 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
639 638
640 639 rmap.connect('pullrequest',
641 640 '/{repo_name}/pull-request/new', controller='pullrequests',
642 641 action='create', conditions={'function': check_repo,
643 642 'method': ['POST']},
644 643 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
645 644
646 645 rmap.connect('pullrequest_repo_refs',
647 646 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
648 647 controller='pullrequests',
649 648 action='get_repo_refs',
650 649 conditions={'function': check_repo, 'method': ['GET']},
651 650 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
652 651
653 652 rmap.connect('pullrequest_repo_destinations',
654 653 '/{repo_name}/pull-request/repo-destinations',
655 654 controller='pullrequests',
656 655 action='get_repo_destinations',
657 656 conditions={'function': check_repo, 'method': ['GET']},
658 657 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
659 658
660 659 rmap.connect('pullrequest_show',
661 660 '/{repo_name}/pull-request/{pull_request_id}',
662 661 controller='pullrequests',
663 662 action='show', conditions={'function': check_repo,
664 663 'method': ['GET']},
665 664 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
666 665
667 666 rmap.connect('pullrequest_update',
668 667 '/{repo_name}/pull-request/{pull_request_id}',
669 668 controller='pullrequests',
670 669 action='update', conditions={'function': check_repo,
671 670 'method': ['PUT']},
672 671 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
673 672
674 673 rmap.connect('pullrequest_merge',
675 674 '/{repo_name}/pull-request/{pull_request_id}',
676 675 controller='pullrequests',
677 676 action='merge', conditions={'function': check_repo,
678 677 'method': ['POST']},
679 678 requirements=URL_NAME_REQUIREMENTS)
680 679
681 680 rmap.connect('pullrequest_delete',
682 681 '/{repo_name}/pull-request/{pull_request_id}',
683 682 controller='pullrequests',
684 683 action='delete', conditions={'function': check_repo,
685 684 'method': ['DELETE']},
686 685 requirements=URL_NAME_REQUIREMENTS)
687 686
688 687 rmap.connect('pullrequest_comment',
689 688 '/{repo_name}/pull-request-comment/{pull_request_id}',
690 689 controller='pullrequests',
691 690 action='comment', conditions={'function': check_repo,
692 691 'method': ['POST']},
693 692 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
694 693
695 694 rmap.connect('pullrequest_comment_delete',
696 695 '/{repo_name}/pull-request-comment/{comment_id}/delete',
697 696 controller='pullrequests', action='delete_comment',
698 697 conditions={'function': check_repo, 'method': ['DELETE']},
699 698 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
700 699
701 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
702 controller='changelog', conditions={'function': check_repo},
703 requirements=URL_NAME_REQUIREMENTS)
704
705 rmap.connect('changelog_file_home',
706 '/{repo_name}/changelog/{revision}/{f_path}',
707 controller='changelog', f_path=None,
708 conditions={'function': check_repo},
709 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
710
711 rmap.connect('changelog_elements', '/{repo_name}/changelog_details',
712 controller='changelog', action='changelog_elements',
713 conditions={'function': check_repo},
714 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
715
716 700 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
717 701 controller='forks', action='fork_create',
718 702 conditions={'function': check_repo, 'method': ['POST']},
719 703 requirements=URL_NAME_REQUIREMENTS)
720 704
721 705 rmap.connect('repo_fork_home', '/{repo_name}/fork',
722 706 controller='forks', action='fork',
723 707 conditions={'function': check_repo},
724 708 requirements=URL_NAME_REQUIREMENTS)
725 709
726 710 rmap.connect('repo_forks_home', '/{repo_name}/forks',
727 711 controller='forks', action='forks',
728 712 conditions={'function': check_repo},
729 713 requirements=URL_NAME_REQUIREMENTS)
730 714
731 715 return rmap
@@ -1,492 +1,491 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 commit controller for RhodeCode showing changes between commits
23 23 """
24 24
25 25 import logging
26 26
27 27 from collections import defaultdict
28 28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29 29
30 30 from pylons import tmpl_context as c, request, response
31 31 from pylons.i18n.translation import _
32 32 from pylons.controllers.util import redirect
33 33
34 34 from rhodecode.lib import auth
35 35 from rhodecode.lib import diffs, codeblocks
36 36 from rhodecode.lib.auth import (
37 37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 41 import rhodecode.lib.helpers as h
42 42 from rhodecode.lib.utils import jsonify
43 43 from rhodecode.lib.utils2 import safe_unicode, safe_int
44 44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.comment import CommentsModel
50 50 from rhodecode.model.meta import Session
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 def _update_with_GET(params, GET):
57 57 for k in ['diff1', 'diff2', 'diff']:
58 58 params[k] += GET.getall(k)
59 59
60 60
61 61 def get_ignore_ws(fid, GET):
62 62 ig_ws_global = GET.get('ignorews')
63 63 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
64 64 if ig_ws:
65 65 try:
66 66 return int(ig_ws[0].split(':')[-1])
67 67 except Exception:
68 68 pass
69 69 return ig_ws_global
70 70
71 71
72 72 def _ignorews_url(GET, fileid=None):
73 73 fileid = str(fileid) if fileid else None
74 74 params = defaultdict(list)
75 75 _update_with_GET(params, GET)
76 76 label = _('Show whitespace')
77 77 tooltiplbl = _('Show whitespace for all diffs')
78 78 ig_ws = get_ignore_ws(fileid, GET)
79 79 ln_ctx = get_line_ctx(fileid, GET)
80 80
81 81 if ig_ws is None:
82 82 params['ignorews'] += [1]
83 83 label = _('Ignore whitespace')
84 84 tooltiplbl = _('Ignore whitespace for all diffs')
85 85 ctx_key = 'context'
86 86 ctx_val = ln_ctx
87 87
88 88 # if we have passed in ln_ctx pass it along to our params
89 89 if ln_ctx:
90 90 params[ctx_key] += [ctx_val]
91 91
92 92 if fileid:
93 93 params['anchor'] = 'a_' + fileid
94 94 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
95 95
96 96
97 97 def get_line_ctx(fid, GET):
98 98 ln_ctx_global = GET.get('context')
99 99 if fid:
100 100 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 101 else:
102 102 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
103 103 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
104 104 if ln_ctx:
105 105 ln_ctx = [ln_ctx]
106 106
107 107 if ln_ctx:
108 108 retval = ln_ctx[0].split(':')[-1]
109 109 else:
110 110 retval = ln_ctx_global
111 111
112 112 try:
113 113 return int(retval)
114 114 except Exception:
115 115 return 3
116 116
117 117
118 118 def _context_url(GET, fileid=None):
119 119 """
120 120 Generates a url for context lines.
121 121
122 122 :param fileid:
123 123 """
124 124
125 125 fileid = str(fileid) if fileid else None
126 126 ig_ws = get_ignore_ws(fileid, GET)
127 127 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
128 128
129 129 params = defaultdict(list)
130 130 _update_with_GET(params, GET)
131 131
132 132 if ln_ctx > 0:
133 133 params['context'] += [ln_ctx]
134 134
135 135 if ig_ws:
136 136 ig_ws_key = 'ignorews'
137 137 ig_ws_val = 1
138 138 params[ig_ws_key] += [ig_ws_val]
139 139
140 140 lbl = _('Increase context')
141 141 tooltiplbl = _('Increase context for all diffs')
142 142
143 143 if fileid:
144 144 params['anchor'] = 'a_' + fileid
145 145 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
146 146
147 147
148 148 class ChangesetController(BaseRepoController):
149 149
150 150 def __before__(self):
151 151 super(ChangesetController, self).__before__()
152 c.affected_files_cut_off = 60
153 152
154 153 def _index(self, commit_id_range, method):
155 154 c.ignorews_url = _ignorews_url
156 155 c.context_url = _context_url
157 156 c.fulldiff = fulldiff = request.GET.get('fulldiff')
158 157
159 158 # fetch global flags of ignore ws or context lines
160 159 context_lcl = get_line_ctx('', request.GET)
161 160 ign_whitespace_lcl = get_ignore_ws('', request.GET)
162 161
163 162 # diff_limit will cut off the whole diff if the limit is applied
164 163 # otherwise it will just hide the big files from the front-end
165 164 diff_limit = self.cut_off_limit_diff
166 165 file_limit = self.cut_off_limit_file
167 166
168 167 # get ranges of commit ids if preset
169 168 commit_range = commit_id_range.split('...')[:2]
170 169
171 170 try:
172 171 pre_load = ['affected_files', 'author', 'branch', 'date',
173 172 'message', 'parents']
174 173
175 174 if len(commit_range) == 2:
176 175 commits = c.rhodecode_repo.get_commits(
177 176 start_id=commit_range[0], end_id=commit_range[1],
178 177 pre_load=pre_load)
179 178 commits = list(commits)
180 179 else:
181 180 commits = [c.rhodecode_repo.get_commit(
182 181 commit_id=commit_id_range, pre_load=pre_load)]
183 182
184 183 c.commit_ranges = commits
185 184 if not c.commit_ranges:
186 185 raise RepositoryError(
187 186 'The commit range returned an empty result')
188 187 except CommitDoesNotExistError:
189 188 msg = _('No such commit exists for this repository')
190 189 h.flash(msg, category='error')
191 190 raise HTTPNotFound()
192 191 except Exception:
193 192 log.exception("General failure")
194 193 raise HTTPNotFound()
195 194
196 195 c.changes = OrderedDict()
197 196 c.lines_added = 0
198 197 c.lines_deleted = 0
199 198
200 199 # auto collapse if we have more than limit
201 200 collapse_limit = diffs.DiffProcessor._collapse_commits_over
202 201 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
203 202
204 203 c.commit_statuses = ChangesetStatus.STATUSES
205 204 c.inline_comments = []
206 205 c.files = []
207 206
208 207 c.statuses = []
209 208 c.comments = []
210 209 c.unresolved_comments = []
211 210 if len(c.commit_ranges) == 1:
212 211 commit = c.commit_ranges[0]
213 212 c.comments = CommentsModel().get_comments(
214 213 c.rhodecode_db_repo.repo_id,
215 214 revision=commit.raw_id)
216 215 c.statuses.append(ChangesetStatusModel().get_status(
217 216 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 217 # comments from PR
219 218 statuses = ChangesetStatusModel().get_statuses(
220 219 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 220 with_revisions=True)
222 221 prs = set(st.pull_request for st in statuses
223 222 if st.pull_request is not None)
224 223 # from associated statuses, check the pull requests, and
225 224 # show comments from them
226 225 for pr in prs:
227 226 c.comments.extend(pr.comments)
228 227
229 228 c.unresolved_comments = CommentsModel()\
230 229 .get_commit_unresolved_todos(commit.raw_id)
231 230
232 231 # Iterate over ranges (default commit view is always one commit)
233 232 for commit in c.commit_ranges:
234 233 c.changes[commit.raw_id] = []
235 234
236 235 commit2 = commit
237 236 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
238 237
239 238 _diff = c.rhodecode_repo.get_diff(
240 239 commit1, commit2,
241 240 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
242 241 diff_processor = diffs.DiffProcessor(
243 242 _diff, format='newdiff', diff_limit=diff_limit,
244 243 file_limit=file_limit, show_full_diff=fulldiff)
245 244
246 245 commit_changes = OrderedDict()
247 246 if method == 'show':
248 247 _parsed = diff_processor.prepare()
249 248 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
250 249
251 250 _parsed = diff_processor.prepare()
252 251
253 252 def _node_getter(commit):
254 253 def get_node(fname):
255 254 try:
256 255 return commit.get_node(fname)
257 256 except NodeDoesNotExistError:
258 257 return None
259 258 return get_node
260 259
261 260 inline_comments = CommentsModel().get_inline_comments(
262 261 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
263 262 c.inline_cnt = CommentsModel().get_inline_comments_count(
264 263 inline_comments)
265 264
266 265 diffset = codeblocks.DiffSet(
267 266 repo_name=c.repo_name,
268 267 source_node_getter=_node_getter(commit1),
269 268 target_node_getter=_node_getter(commit2),
270 269 comments=inline_comments)
271 270 diffset = diffset.render_patchset(
272 271 _parsed, commit1.raw_id, commit2.raw_id)
273 272
274 273 c.changes[commit.raw_id] = diffset
275 274 else:
276 275 # downloads/raw we only need RAW diff nothing else
277 276 diff = diff_processor.as_raw()
278 277 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
279 278
280 279 # sort comments by how they were generated
281 280 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
282 281
283 282 if len(c.commit_ranges) == 1:
284 283 c.commit = c.commit_ranges[0]
285 284 c.parent_tmpl = ''.join(
286 285 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
287 286 if method == 'download':
288 287 response.content_type = 'text/plain'
289 288 response.content_disposition = (
290 289 'attachment; filename=%s.diff' % commit_id_range[:12])
291 290 return diff
292 291 elif method == 'patch':
293 292 response.content_type = 'text/plain'
294 293 c.diff = safe_unicode(diff)
295 294 return render('changeset/patch_changeset.mako')
296 295 elif method == 'raw':
297 296 response.content_type = 'text/plain'
298 297 return diff
299 298 elif method == 'show':
300 299 if len(c.commit_ranges) == 1:
301 300 return render('changeset/changeset.mako')
302 301 else:
303 302 c.ancestor = None
304 303 c.target_repo = c.rhodecode_db_repo
305 304 return render('changeset/changeset_range.mako')
306 305
307 306 @LoginRequired()
308 307 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
309 308 'repository.admin')
310 309 def index(self, revision, method='show'):
311 310 return self._index(revision, method=method)
312 311
313 312 @LoginRequired()
314 313 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
315 314 'repository.admin')
316 315 def changeset_raw(self, revision):
317 316 return self._index(revision, method='raw')
318 317
319 318 @LoginRequired()
320 319 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
321 320 'repository.admin')
322 321 def changeset_patch(self, revision):
323 322 return self._index(revision, method='patch')
324 323
325 324 @LoginRequired()
326 325 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
327 326 'repository.admin')
328 327 def changeset_download(self, revision):
329 328 return self._index(revision, method='download')
330 329
331 330 @LoginRequired()
332 331 @NotAnonymous()
333 332 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
334 333 'repository.admin')
335 334 @auth.CSRFRequired()
336 335 @jsonify
337 336 def comment(self, repo_name, revision):
338 337 commit_id = revision
339 338 status = request.POST.get('changeset_status', None)
340 339 text = request.POST.get('text')
341 340 comment_type = request.POST.get('comment_type')
342 341 resolves_comment_id = request.POST.get('resolves_comment_id', None)
343 342
344 343 if status:
345 344 text = text or (_('Status change %(transition_icon)s %(status)s')
346 345 % {'transition_icon': '>',
347 346 'status': ChangesetStatus.get_status_lbl(status)})
348 347
349 348 multi_commit_ids = []
350 349 for _commit_id in request.POST.get('commit_ids', '').split(','):
351 350 if _commit_id not in ['', None, EmptyCommit.raw_id]:
352 351 if _commit_id not in multi_commit_ids:
353 352 multi_commit_ids.append(_commit_id)
354 353
355 354 commit_ids = multi_commit_ids or [commit_id]
356 355
357 356 comment = None
358 357 for current_id in filter(None, commit_ids):
359 358 c.co = comment = CommentsModel().create(
360 359 text=text,
361 360 repo=c.rhodecode_db_repo.repo_id,
362 361 user=c.rhodecode_user.user_id,
363 362 commit_id=current_id,
364 363 f_path=request.POST.get('f_path'),
365 364 line_no=request.POST.get('line'),
366 365 status_change=(ChangesetStatus.get_status_lbl(status)
367 366 if status else None),
368 367 status_change_type=status,
369 368 comment_type=comment_type,
370 369 resolves_comment_id=resolves_comment_id
371 370 )
372 371
373 372 # get status if set !
374 373 if status:
375 374 # if latest status was from pull request and it's closed
376 375 # disallow changing status !
377 376 # dont_allow_on_closed_pull_request = True !
378 377
379 378 try:
380 379 ChangesetStatusModel().set_status(
381 380 c.rhodecode_db_repo.repo_id,
382 381 status,
383 382 c.rhodecode_user.user_id,
384 383 comment,
385 384 revision=current_id,
386 385 dont_allow_on_closed_pull_request=True
387 386 )
388 387 except StatusChangeOnClosedPullRequestError:
389 388 msg = _('Changing the status of a commit associated with '
390 389 'a closed pull request is not allowed')
391 390 log.exception(msg)
392 391 h.flash(msg, category='warning')
393 392 return redirect(h.url(
394 393 'changeset_home', repo_name=repo_name,
395 394 revision=current_id))
396 395
397 396 # finalize, commit and redirect
398 397 Session().commit()
399 398
400 399 data = {
401 400 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
402 401 }
403 402 if comment:
404 403 data.update(comment.get_dict())
405 404 data.update({'rendered_text':
406 405 render('changeset/changeset_comment_block.mako')})
407 406
408 407 return data
409 408
410 409 @LoginRequired()
411 410 @NotAnonymous()
412 411 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
413 412 'repository.admin')
414 413 @auth.CSRFRequired()
415 414 def preview_comment(self):
416 415 # Technically a CSRF token is not needed as no state changes with this
417 416 # call. However, as this is a POST is better to have it, so automated
418 417 # tools don't flag it as potential CSRF.
419 418 # Post is required because the payload could be bigger than the maximum
420 419 # allowed by GET.
421 420 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
422 421 raise HTTPBadRequest()
423 422 text = request.POST.get('text')
424 423 renderer = request.POST.get('renderer') or 'rst'
425 424 if text:
426 425 return h.render(text, renderer=renderer, mentions=True)
427 426 return ''
428 427
429 428 @LoginRequired()
430 429 @NotAnonymous()
431 430 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
432 431 'repository.admin')
433 432 @auth.CSRFRequired()
434 433 @jsonify
435 434 def delete_comment(self, repo_name, comment_id):
436 435 comment = ChangesetComment.get_or_404(safe_int(comment_id))
437 436 if not comment:
438 437 log.debug('Comment with id:%s not found, skipping', comment_id)
439 438 # comment already deleted in another call probably
440 439 return True
441 440
442 441 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
443 442 super_admin = h.HasPermissionAny('hg.admin')()
444 443 comment_owner = (comment.author.user_id == c.rhodecode_user.user_id)
445 444 is_repo_comment = comment.repo.repo_name == c.repo_name
446 445 comment_repo_admin = is_repo_admin and is_repo_comment
447 446
448 447 if super_admin or comment_owner or comment_repo_admin:
449 448 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
450 449 Session().commit()
451 450 return True
452 451 else:
453 452 log.warning('No permissions for user %s to delete comment_id: %s',
454 453 c.rhodecode_user, comment_id)
455 454 raise HTTPNotFound()
456 455
457 456 @LoginRequired()
458 457 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
459 458 'repository.admin')
460 459 @jsonify
461 460 def changeset_info(self, repo_name, revision):
462 461 if request.is_xhr:
463 462 try:
464 463 return c.rhodecode_repo.get_commit(commit_id=revision)
465 464 except CommitDoesNotExistError as e:
466 465 return EmptyCommit(message=str(e))
467 466 else:
468 467 raise HTTPBadRequest()
469 468
470 469 @LoginRequired()
471 470 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
472 471 'repository.admin')
473 472 @jsonify
474 473 def changeset_children(self, repo_name, revision):
475 474 if request.is_xhr:
476 475 commit = c.rhodecode_repo.get_commit(commit_id=revision)
477 476 result = {"results": commit.children}
478 477 return result
479 478 else:
480 479 raise HTTPBadRequest()
481 480
482 481 @LoginRequired()
483 482 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
484 483 'repository.admin')
485 484 @jsonify
486 485 def changeset_parents(self, repo_name, revision):
487 486 if request.is_xhr:
488 487 commit = c.rhodecode_repo.get_commit(commit_id=revision)
489 488 result = {"results": commit.parents}
490 489 return result
491 490 else:
492 491 raise HTTPBadRequest()
@@ -1,306 +1,304 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Journal / user event log controller for rhodecode
23 23 """
24 24
25 25 import logging
26 26 from itertools import groupby
27 27
28 28 from sqlalchemy import or_
29 29 from sqlalchemy.orm import joinedload
30 30
31 31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
32 32
33 33 from webob.exc import HTTPBadRequest
34 34 from pylons import request, tmpl_context as c, response, url
35 35 from pylons.i18n.translation import _
36 36
37 37 from rhodecode.model.db import UserLog, UserFollowing, User, UserApiKeys
38 38 from rhodecode.model.meta import Session
39 39 import rhodecode.lib.helpers as h
40 40 from rhodecode.lib.helpers import Page
41 41 from rhodecode.lib.user_log_filter import user_log_filter
42 42 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
43 43 from rhodecode.lib.base import BaseController, render
44 44 from rhodecode.lib.utils2 import safe_int, AttributeDict
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class JournalController(BaseController):
50 50
51 51 def __before__(self):
52 52 super(JournalController, self).__before__()
53 53 self.language = 'en-us'
54 54 self.ttl = "5"
55 55 self.feed_nr = 20
56 56 c.search_term = request.GET.get('filter')
57 57
58 58 def _get_daily_aggregate(self, journal):
59 59 groups = []
60 60 for k, g in groupby(journal, lambda x: x.action_as_day):
61 61 user_group = []
62 62 #groupby username if it's a present value, else fallback to journal username
63 63 for _, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
64 64 l = list(g2)
65 65 user_group.append((l[0].user, l))
66 66
67 67 groups.append((k, user_group,))
68 68
69 69 return groups
70 70
71 71 def _get_journal_data(self, following_repos):
72 72 repo_ids = [x.follows_repository.repo_id for x in following_repos
73 73 if x.follows_repository is not None]
74 74 user_ids = [x.follows_user.user_id for x in following_repos
75 75 if x.follows_user is not None]
76 76
77 77 filtering_criterion = None
78 78
79 79 if repo_ids and user_ids:
80 80 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
81 81 UserLog.user_id.in_(user_ids))
82 82 if repo_ids and not user_ids:
83 83 filtering_criterion = UserLog.repository_id.in_(repo_ids)
84 84 if not repo_ids and user_ids:
85 85 filtering_criterion = UserLog.user_id.in_(user_ids)
86 86 if filtering_criterion is not None:
87 87 journal = self.sa.query(UserLog)\
88 88 .options(joinedload(UserLog.user))\
89 89 .options(joinedload(UserLog.repository))
90 90 #filter
91 91 try:
92 92 journal = user_log_filter(journal, c.search_term)
93 93 except Exception:
94 94 # we want this to crash for now
95 95 raise
96 96 journal = journal.filter(filtering_criterion)\
97 97 .order_by(UserLog.action_date.desc())
98 98 else:
99 99 journal = []
100 100
101 101 return journal
102 102
103 103 def _atom_feed(self, repos, public=True):
104 104 journal = self._get_journal_data(repos)
105 105 if public:
106 106 _link = url('public_journal_atom', qualified=True)
107 107 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
108 108 'atom feed')
109 109 else:
110 110 _link = url('journal_atom', qualified=True)
111 111 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'atom feed')
112 112
113 113 feed = Atom1Feed(title=_desc,
114 114 link=_link,
115 115 description=_desc,
116 116 language=self.language,
117 117 ttl=self.ttl)
118 118
119 119 for entry in journal[:self.feed_nr]:
120 120 user = entry.user
121 121 if user is None:
122 122 #fix deleted users
123 123 user = AttributeDict({'short_contact': entry.username,
124 124 'email': '',
125 125 'full_contact': ''})
126 126 action, action_extra, ico = h.action_parser(entry, feed=True)
127 127 title = "%s - %s %s" % (user.short_contact, action(),
128 128 entry.repository.repo_name)
129 129 desc = action_extra()
130 130 _url = None
131 131 if entry.repository is not None:
132 _url = url('changelog_home',
133 repo_name=entry.repository.repo_name,
134 qualified=True)
132 _url = h.route_url('repo_changelog',
133 repo_name=entry.repository.repo_name)
135 134
136 135 feed.add_item(title=title,
137 136 pubdate=entry.action_date,
138 137 link=_url or url('', qualified=True),
139 138 author_email=user.email,
140 139 author_name=user.full_contact,
141 140 description=desc)
142 141
143 142 response.content_type = feed.mime_type
144 143 return feed.writeString('utf-8')
145 144
146 145 def _rss_feed(self, repos, public=True):
147 146 journal = self._get_journal_data(repos)
148 147 if public:
149 148 _link = url('public_journal_atom', qualified=True)
150 149 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
151 150 'rss feed')
152 151 else:
153 152 _link = url('journal_atom', qualified=True)
154 153 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'rss feed')
155 154
156 155 feed = Rss201rev2Feed(title=_desc,
157 156 link=_link,
158 157 description=_desc,
159 158 language=self.language,
160 159 ttl=self.ttl)
161 160
162 161 for entry in journal[:self.feed_nr]:
163 162 user = entry.user
164 163 if user is None:
165 164 #fix deleted users
166 165 user = AttributeDict({'short_contact': entry.username,
167 166 'email': '',
168 167 'full_contact': ''})
169 168 action, action_extra, ico = h.action_parser(entry, feed=True)
170 169 title = "%s - %s %s" % (user.short_contact, action(),
171 170 entry.repository.repo_name)
172 171 desc = action_extra()
173 172 _url = None
174 173 if entry.repository is not None:
175 _url = url('changelog_home',
176 repo_name=entry.repository.repo_name,
177 qualified=True)
174 _url = h.route_url('repo_changelog',
175 repo_name=entry.repository.repo_name)
178 176
179 177 feed.add_item(title=title,
180 178 pubdate=entry.action_date,
181 179 link=_url or url('', qualified=True),
182 180 author_email=user.email,
183 181 author_name=user.full_contact,
184 182 description=desc)
185 183
186 184 response.content_type = feed.mime_type
187 185 return feed.writeString('utf-8')
188 186
189 187 @LoginRequired()
190 188 @NotAnonymous()
191 189 def index(self):
192 190 # Return a rendered template
193 191 p = safe_int(request.GET.get('page', 1), 1)
194 192 c.user = User.get(c.rhodecode_user.user_id)
195 193 following = self.sa.query(UserFollowing)\
196 194 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
197 195 .options(joinedload(UserFollowing.follows_repository))\
198 196 .all()
199 197
200 198 journal = self._get_journal_data(following)
201 199
202 200 def url_generator(**kw):
203 201 return url.current(filter=c.search_term, **kw)
204 202
205 203 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
206 204 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
207 205
208 206 c.journal_data = render('journal/journal_data.mako')
209 207 if request.is_xhr:
210 208 return c.journal_data
211 209
212 210 return render('journal/journal.mako')
213 211
214 212 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
215 213 @NotAnonymous()
216 214 def journal_atom(self):
217 215 """
218 216 Produce an atom-1.0 feed via feedgenerator module
219 217 """
220 218 following = self.sa.query(UserFollowing)\
221 219 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
222 220 .options(joinedload(UserFollowing.follows_repository))\
223 221 .all()
224 222 return self._atom_feed(following, public=False)
225 223
226 224 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
227 225 @NotAnonymous()
228 226 def journal_rss(self):
229 227 """
230 228 Produce an rss feed via feedgenerator module
231 229 """
232 230 following = self.sa.query(UserFollowing)\
233 231 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
234 232 .options(joinedload(UserFollowing.follows_repository))\
235 233 .all()
236 234 return self._rss_feed(following, public=False)
237 235
238 236 @CSRFRequired()
239 237 @LoginRequired()
240 238 @NotAnonymous()
241 239 def toggle_following(self):
242 240 user_id = request.POST.get('follows_user_id')
243 241 if user_id:
244 242 try:
245 243 self.scm_model.toggle_following_user(
246 244 user_id, c.rhodecode_user.user_id)
247 245 Session().commit()
248 246 return 'ok'
249 247 except Exception:
250 248 raise HTTPBadRequest()
251 249
252 250 repo_id = request.POST.get('follows_repo_id')
253 251 if repo_id:
254 252 try:
255 253 self.scm_model.toggle_following_repo(
256 254 repo_id, c.rhodecode_user.user_id)
257 255 Session().commit()
258 256 return 'ok'
259 257 except Exception:
260 258 raise HTTPBadRequest()
261 259
262 260
263 261 @LoginRequired()
264 262 def public_journal(self):
265 263 # Return a rendered template
266 264 p = safe_int(request.GET.get('page', 1), 1)
267 265
268 266 c.following = self.sa.query(UserFollowing)\
269 267 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
270 268 .options(joinedload(UserFollowing.follows_repository))\
271 269 .all()
272 270
273 271 journal = self._get_journal_data(c.following)
274 272
275 273 c.journal_pager = Page(journal, page=p, items_per_page=20)
276 274
277 275 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
278 276
279 277 c.journal_data = render('journal/journal_data.mako')
280 278 if request.is_xhr:
281 279 return c.journal_data
282 280 return render('journal/public_journal.mako')
283 281
284 282 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
285 283 def public_journal_atom(self):
286 284 """
287 285 Produce an atom-1.0 feed via feedgenerator module
288 286 """
289 287 c.following = self.sa.query(UserFollowing)\
290 288 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
291 289 .options(joinedload(UserFollowing.follows_repository))\
292 290 .all()
293 291
294 292 return self._atom_feed(c.following)
295 293
296 294 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
297 295 def public_journal_rss(self):
298 296 """
299 297 Produce an rss2 feed via feedgenerator module
300 298 """
301 299 c.following = self.sa.query(UserFollowing)\
302 300 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
303 301 .options(joinedload(UserFollowing.follows_repository))\
304 302 .all()
305 303
306 304 return self._rss_feed(c.following)
@@ -1,632 +1,634 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import ipaddress
31 31 import pyramid.threadlocal
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36 from pylons import config, tmpl_context as c, request, url
37 37 from pylons.controllers import WSGIController
38 38 from pylons.controllers.util import redirect
39 39 from pylons.i18n import translation
40 40 # marcink: don't remove this import
41 41 from pylons.templating import render_mako as render # noqa
42 42 from pylons.i18n.translation import _
43 43 from webob.exc import HTTPFound
44 44
45 45
46 46 import rhodecode
47 47 from rhodecode.authentication.base import VCS_TYPE
48 48 from rhodecode.lib import auth, utils2
49 49 from rhodecode.lib import helpers as h
50 50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 51 from rhodecode.lib.exceptions import UserCreationError
52 52 from rhodecode.lib.utils import (
53 53 get_repo_slug, set_rhodecode_config, password_changed,
54 54 get_enabled_hook_classes)
55 55 from rhodecode.lib.utils2 import (
56 56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 58 from rhodecode.model import meta
59 59 from rhodecode.model.db import Repository, User, ChangesetComment
60 60 from rhodecode.model.notification import NotificationModel
61 61 from rhodecode.model.scm import ScmModel
62 62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63 63
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 def _filter_proxy(ip):
69 69 """
70 70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 71 ips. Those comma separated IPs are passed from various proxies in the
72 72 chain of request processing. The left-most being the original client.
73 73 We only care about the first IP which came from the org. client.
74 74
75 75 :param ip: ip string from headers
76 76 """
77 77 if ',' in ip:
78 78 _ips = ip.split(',')
79 79 _first_ip = _ips[0].strip()
80 80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 81 return _first_ip
82 82 return ip
83 83
84 84
85 85 def _filter_port(ip):
86 86 """
87 87 Removes a port from ip, there are 4 main cases to handle here.
88 88 - ipv4 eg. 127.0.0.1
89 89 - ipv6 eg. ::1
90 90 - ipv4+port eg. 127.0.0.1:8080
91 91 - ipv6+port eg. [::1]:8080
92 92
93 93 :param ip:
94 94 """
95 95 def is_ipv6(ip_addr):
96 96 if hasattr(socket, 'inet_pton'):
97 97 try:
98 98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 99 except socket.error:
100 100 return False
101 101 else:
102 102 # fallback to ipaddress
103 103 try:
104 104 ipaddress.IPv6Address(safe_unicode(ip_addr))
105 105 except Exception:
106 106 return False
107 107 return True
108 108
109 109 if ':' not in ip: # must be ipv4 pure ip
110 110 return ip
111 111
112 112 if '[' in ip and ']' in ip: # ipv6 with port
113 113 return ip.split(']')[0][1:].lower()
114 114
115 115 # must be ipv6 or ipv4 with port
116 116 if is_ipv6(ip):
117 117 return ip
118 118 else:
119 119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 120 return ip
121 121
122 122
123 123 def get_ip_addr(environ):
124 124 proxy_key = 'HTTP_X_REAL_IP'
125 125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 126 def_key = 'REMOTE_ADDR'
127 127 _filters = lambda x: _filter_port(_filter_proxy(x))
128 128
129 129 ip = environ.get(proxy_key)
130 130 if ip:
131 131 return _filters(ip)
132 132
133 133 ip = environ.get(proxy_key2)
134 134 if ip:
135 135 return _filters(ip)
136 136
137 137 ip = environ.get(def_key, '0.0.0.0')
138 138 return _filters(ip)
139 139
140 140
141 141 def get_server_ip_addr(environ, log_errors=True):
142 142 hostname = environ.get('SERVER_NAME')
143 143 try:
144 144 return socket.gethostbyname(hostname)
145 145 except Exception as e:
146 146 if log_errors:
147 147 # in some cases this lookup is not possible, and we don't want to
148 148 # make it an exception in logs
149 149 log.exception('Could not retrieve server ip address: %s', e)
150 150 return hostname
151 151
152 152
153 153 def get_server_port(environ):
154 154 return environ.get('SERVER_PORT')
155 155
156 156
157 157 def get_access_path(environ):
158 158 path = environ.get('PATH_INFO')
159 159 org_req = environ.get('pylons.original_request')
160 160 if org_req:
161 161 path = org_req.environ.get('PATH_INFO')
162 162 return path
163 163
164 164
165 165 def get_user_agent(environ):
166 166 return environ.get('HTTP_USER_AGENT')
167 167
168 168
169 169 def vcs_operation_context(
170 170 environ, repo_name, username, action, scm, check_locking=True,
171 171 is_shadow_repo=False):
172 172 """
173 173 Generate the context for a vcs operation, e.g. push or pull.
174 174
175 175 This context is passed over the layers so that hooks triggered by the
176 176 vcs operation know details like the user, the user's IP address etc.
177 177
178 178 :param check_locking: Allows to switch of the computation of the locking
179 179 data. This serves mainly the need of the simplevcs middleware to be
180 180 able to disable this for certain operations.
181 181
182 182 """
183 183 # Tri-state value: False: unlock, None: nothing, True: lock
184 184 make_lock = None
185 185 locked_by = [None, None, None]
186 186 is_anonymous = username == User.DEFAULT_USER
187 187 if not is_anonymous and check_locking:
188 188 log.debug('Checking locking on repository "%s"', repo_name)
189 189 user = User.get_by_username(username)
190 190 repo = Repository.get_by_repo_name(repo_name)
191 191 make_lock, __, locked_by = repo.get_locking_state(
192 192 action, user.user_id)
193 193
194 194 settings_model = VcsSettingsModel(repo=repo_name)
195 195 ui_settings = settings_model.get_ui_settings()
196 196
197 197 extras = {
198 198 'ip': get_ip_addr(environ),
199 199 'username': username,
200 200 'action': action,
201 201 'repository': repo_name,
202 202 'scm': scm,
203 203 'config': rhodecode.CONFIG['__file__'],
204 204 'make_lock': make_lock,
205 205 'locked_by': locked_by,
206 206 'server_url': utils2.get_server_url(environ),
207 207 'user_agent': get_user_agent(environ),
208 208 'hooks': get_enabled_hook_classes(ui_settings),
209 209 'is_shadow_repo': is_shadow_repo,
210 210 }
211 211 return extras
212 212
213 213
214 214 class BasicAuth(AuthBasicAuthenticator):
215 215
216 216 def __init__(self, realm, authfunc, registry, auth_http_code=None,
217 217 initial_call_detection=False, acl_repo_name=None):
218 218 self.realm = realm
219 219 self.initial_call = initial_call_detection
220 220 self.authfunc = authfunc
221 221 self.registry = registry
222 222 self.acl_repo_name = acl_repo_name
223 223 self._rc_auth_http_code = auth_http_code
224 224
225 225 def _get_response_from_code(self, http_code):
226 226 try:
227 227 return get_exception(safe_int(http_code))
228 228 except Exception:
229 229 log.exception('Failed to fetch response for code %s' % http_code)
230 230 return HTTPForbidden
231 231
232 232 def build_authentication(self):
233 233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
234 234 if self._rc_auth_http_code and not self.initial_call:
235 235 # return alternative HTTP code if alternative http return code
236 236 # is specified in RhodeCode config, but ONLY if it's not the
237 237 # FIRST call
238 238 custom_response_klass = self._get_response_from_code(
239 239 self._rc_auth_http_code)
240 240 return custom_response_klass(headers=head)
241 241 return HTTPUnauthorized(headers=head)
242 242
243 243 def authenticate(self, environ):
244 244 authorization = AUTHORIZATION(environ)
245 245 if not authorization:
246 246 return self.build_authentication()
247 247 (authmeth, auth) = authorization.split(' ', 1)
248 248 if 'basic' != authmeth.lower():
249 249 return self.build_authentication()
250 250 auth = auth.strip().decode('base64')
251 251 _parts = auth.split(':', 1)
252 252 if len(_parts) == 2:
253 253 username, password = _parts
254 254 if self.authfunc(
255 255 username, password, environ, VCS_TYPE,
256 256 registry=self.registry, acl_repo_name=self.acl_repo_name):
257 257 return username
258 258 if username and password:
259 259 # we mark that we actually executed authentication once, at
260 260 # that point we can use the alternative auth code
261 261 self.initial_call = False
262 262
263 263 return self.build_authentication()
264 264
265 265 __call__ = authenticate
266 266
267 267
268 268 def calculate_version_hash():
269 269 return md5(
270 270 config.get('beaker.session.secret', '') +
271 271 rhodecode.__version__)[:8]
272 272
273 273
274 274 def get_current_lang(request):
275 275 # NOTE(marcink): remove after pyramid move
276 276 try:
277 277 return translation.get_lang()[0]
278 278 except:
279 279 pass
280 280
281 281 return getattr(request, '_LOCALE_', request.locale_name)
282 282
283 283
284 284 def attach_context_attributes(context, request, user_id):
285 285 """
286 286 Attach variables into template context called `c`, please note that
287 287 request could be pylons or pyramid request in here.
288 288 """
289 289
290 290 rc_config = SettingsModel().get_all_settings(cache=True)
291 291
292 292 context.rhodecode_version = rhodecode.__version__
293 293 context.rhodecode_edition = config.get('rhodecode.edition')
294 294 # unique secret + version does not leak the version but keep consistency
295 295 context.rhodecode_version_hash = calculate_version_hash()
296 296
297 297 # Default language set for the incoming request
298 298 context.language = get_current_lang(request)
299 299
300 300 # Visual options
301 301 context.visual = AttributeDict({})
302 302
303 303 # DB stored Visual Items
304 304 context.visual.show_public_icon = str2bool(
305 305 rc_config.get('rhodecode_show_public_icon'))
306 306 context.visual.show_private_icon = str2bool(
307 307 rc_config.get('rhodecode_show_private_icon'))
308 308 context.visual.stylify_metatags = str2bool(
309 309 rc_config.get('rhodecode_stylify_metatags'))
310 310 context.visual.dashboard_items = safe_int(
311 311 rc_config.get('rhodecode_dashboard_items', 100))
312 312 context.visual.admin_grid_items = safe_int(
313 313 rc_config.get('rhodecode_admin_grid_items', 100))
314 314 context.visual.repository_fields = str2bool(
315 315 rc_config.get('rhodecode_repository_fields'))
316 316 context.visual.show_version = str2bool(
317 317 rc_config.get('rhodecode_show_version'))
318 318 context.visual.use_gravatar = str2bool(
319 319 rc_config.get('rhodecode_use_gravatar'))
320 320 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
321 321 context.visual.default_renderer = rc_config.get(
322 322 'rhodecode_markup_renderer', 'rst')
323 323 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
324 324 context.visual.rhodecode_support_url = \
325 325 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
326 326
327 context.visual.affected_files_cut_off = 60
328
327 329 context.pre_code = rc_config.get('rhodecode_pre_code')
328 330 context.post_code = rc_config.get('rhodecode_post_code')
329 331 context.rhodecode_name = rc_config.get('rhodecode_title')
330 332 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
331 333 # if we have specified default_encoding in the request, it has more
332 334 # priority
333 335 if request.GET.get('default_encoding'):
334 336 context.default_encodings.insert(0, request.GET.get('default_encoding'))
335 337 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
336 338
337 339 # INI stored
338 340 context.labs_active = str2bool(
339 341 config.get('labs_settings_active', 'false'))
340 342 context.visual.allow_repo_location_change = str2bool(
341 343 config.get('allow_repo_location_change', True))
342 344 context.visual.allow_custom_hooks_settings = str2bool(
343 345 config.get('allow_custom_hooks_settings', True))
344 346 context.debug_style = str2bool(config.get('debug_style', False))
345 347
346 348 context.rhodecode_instanceid = config.get('instance_id')
347 349
348 350 context.visual.cut_off_limit_diff = safe_int(
349 351 config.get('cut_off_limit_diff'))
350 352 context.visual.cut_off_limit_file = safe_int(
351 353 config.get('cut_off_limit_file'))
352 354
353 355 # AppEnlight
354 356 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
355 357 context.appenlight_api_public_key = config.get(
356 358 'appenlight.api_public_key', '')
357 359 context.appenlight_server_url = config.get('appenlight.server_url', '')
358 360
359 361 # JS template context
360 362 context.template_context = {
361 363 'repo_name': None,
362 364 'repo_type': None,
363 365 'repo_landing_commit': None,
364 366 'rhodecode_user': {
365 367 'username': None,
366 368 'email': None,
367 369 'notification_status': False
368 370 },
369 371 'visual': {
370 372 'default_renderer': None
371 373 },
372 374 'commit_data': {
373 375 'commit_id': None
374 376 },
375 377 'pull_request_data': {'pull_request_id': None},
376 378 'timeago': {
377 379 'refresh_time': 120 * 1000,
378 380 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
379 381 },
380 382 'pylons_dispatch': {
381 383 # 'controller': request.environ['pylons.routes_dict']['controller'],
382 384 # 'action': request.environ['pylons.routes_dict']['action'],
383 385 },
384 386 'pyramid_dispatch': {
385 387
386 388 },
387 389 'extra': {'plugins': {}}
388 390 }
389 391 # END CONFIG VARS
390 392
391 393 # TODO: This dosn't work when called from pylons compatibility tween.
392 394 # Fix this and remove it from base controller.
393 395 # context.repo_name = get_repo_slug(request) # can be empty
394 396
395 397 diffmode = 'sideside'
396 398 if request.GET.get('diffmode'):
397 399 if request.GET['diffmode'] == 'unified':
398 400 diffmode = 'unified'
399 401 elif request.session.get('diffmode'):
400 402 diffmode = request.session['diffmode']
401 403
402 404 context.diffmode = diffmode
403 405
404 406 if request.session.get('diffmode') != diffmode:
405 407 request.session['diffmode'] = diffmode
406 408
407 409 context.csrf_token = auth.get_csrf_token(session=request.session)
408 410 context.backends = rhodecode.BACKENDS.keys()
409 411 context.backends.sort()
410 412 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
411 413
412 414 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
413 415 # given request will ALWAYS be pyramid one
414 416 pyramid_request = pyramid.threadlocal.get_current_request()
415 417 context.pyramid_request = pyramid_request
416 418
417 419 # web case
418 420 if hasattr(pyramid_request, 'user'):
419 421 context.auth_user = pyramid_request.user
420 422 context.rhodecode_user = pyramid_request.user
421 423
422 424 # api case
423 425 if hasattr(pyramid_request, 'rpc_user'):
424 426 context.auth_user = pyramid_request.rpc_user
425 427 context.rhodecode_user = pyramid_request.rpc_user
426 428
427 429 # attach the whole call context to the request
428 430 request.call_context = context
429 431
430 432
431 433 def get_auth_user(request):
432 434 environ = request.environ
433 435 session = request.session
434 436
435 437 ip_addr = get_ip_addr(environ)
436 438 # make sure that we update permissions each time we call controller
437 439 _auth_token = (request.GET.get('auth_token', '') or
438 440 request.GET.get('api_key', ''))
439 441
440 442 if _auth_token:
441 443 # when using API_KEY we assume user exists, and
442 444 # doesn't need auth based on cookies.
443 445 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
444 446 authenticated = False
445 447 else:
446 448 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
447 449 try:
448 450 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
449 451 ip_addr=ip_addr)
450 452 except UserCreationError as e:
451 453 h.flash(e, 'error')
452 454 # container auth or other auth functions that create users
453 455 # on the fly can throw this exception signaling that there's
454 456 # issue with user creation, explanation should be provided
455 457 # in Exception itself. We then create a simple blank
456 458 # AuthUser
457 459 auth_user = AuthUser(ip_addr=ip_addr)
458 460
459 461 if password_changed(auth_user, session):
460 462 session.invalidate()
461 463 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
462 464 auth_user = AuthUser(ip_addr=ip_addr)
463 465
464 466 authenticated = cookie_store.get('is_authenticated')
465 467
466 468 if not auth_user.is_authenticated and auth_user.is_user_object:
467 469 # user is not authenticated and not empty
468 470 auth_user.set_authenticated(authenticated)
469 471
470 472 return auth_user
471 473
472 474
473 475 class BaseController(WSGIController):
474 476
475 477 def __before__(self):
476 478 """
477 479 __before__ is called before controller methods and after __call__
478 480 """
479 481 # on each call propagate settings calls into global settings.
480 482 set_rhodecode_config(config)
481 483 attach_context_attributes(c, request, self._rhodecode_user.user_id)
482 484
483 485 # TODO: Remove this when fixed in attach_context_attributes()
484 486 c.repo_name = get_repo_slug(request) # can be empty
485 487
486 488 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
487 489 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
488 490 self.sa = meta.Session
489 491 self.scm_model = ScmModel(self.sa)
490 492
491 493 # set user language
492 494 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
493 495 if user_lang:
494 496 translation.set_lang(user_lang)
495 497 log.debug('set language to %s for user %s',
496 498 user_lang, self._rhodecode_user)
497 499
498 500 def _dispatch_redirect(self, with_url, environ, start_response):
499 501 resp = HTTPFound(with_url)
500 502 environ['SCRIPT_NAME'] = '' # handle prefix middleware
501 503 environ['PATH_INFO'] = with_url
502 504 return resp(environ, start_response)
503 505
504 506 def __call__(self, environ, start_response):
505 507 """Invoke the Controller"""
506 508 # WSGIController.__call__ dispatches to the Controller method
507 509 # the request is routed to. This routing information is
508 510 # available in environ['pylons.routes_dict']
509 511 from rhodecode.lib import helpers as h
510 512
511 513 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
512 514 if environ.get('debugtoolbar.wants_pylons_context', False):
513 515 environ['debugtoolbar.pylons_context'] = c._current_obj()
514 516
515 517 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
516 518 environ['pylons.routes_dict']['action']])
517 519
518 520 self.rc_config = SettingsModel().get_all_settings(cache=True)
519 521 self.ip_addr = get_ip_addr(environ)
520 522
521 523 # The rhodecode auth user is looked up and passed through the
522 524 # environ by the pylons compatibility tween in pyramid.
523 525 # So we can just grab it from there.
524 526 auth_user = environ['rc_auth_user']
525 527
526 528 # set globals for auth user
527 529 request.user = auth_user
528 530 self._rhodecode_user = auth_user
529 531
530 532 log.info('IP: %s User: %s accessed %s [%s]' % (
531 533 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
532 534 _route_name)
533 535 )
534 536
535 537 user_obj = auth_user.get_instance()
536 538 if user_obj and user_obj.user_data.get('force_password_change'):
537 539 h.flash('You are required to change your password', 'warning',
538 540 ignore_duplicate=True)
539 541 return self._dispatch_redirect(
540 542 url('my_account_password'), environ, start_response)
541 543
542 544 return WSGIController.__call__(self, environ, start_response)
543 545
544 546
545 547 class BaseRepoController(BaseController):
546 548 """
547 549 Base class for controllers responsible for loading all needed data for
548 550 repository loaded items are
549 551
550 552 c.rhodecode_repo: instance of scm repository
551 553 c.rhodecode_db_repo: instance of db
552 554 c.repository_requirements_missing: shows that repository specific data
553 555 could not be displayed due to the missing requirements
554 556 c.repository_pull_requests: show number of open pull requests
555 557 """
556 558
557 559 def __before__(self):
558 560 super(BaseRepoController, self).__before__()
559 561 if c.repo_name: # extracted from routes
560 562 db_repo = Repository.get_by_repo_name(c.repo_name)
561 563 if not db_repo:
562 564 return
563 565
564 566 log.debug(
565 567 'Found repository in database %s with state `%s`',
566 568 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
567 569 route = getattr(request.environ.get('routes.route'), 'name', '')
568 570
569 571 # allow to delete repos that are somehow damages in filesystem
570 572 if route in ['delete_repo']:
571 573 return
572 574
573 575 if db_repo.repo_state in [Repository.STATE_PENDING]:
574 576 if route in ['repo_creating_home']:
575 577 return
576 578 check_url = url('repo_creating_home', repo_name=c.repo_name)
577 579 return redirect(check_url)
578 580
579 581 self.rhodecode_db_repo = db_repo
580 582
581 583 missing_requirements = False
582 584 try:
583 585 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
584 586 except RepositoryRequirementError as e:
585 587 missing_requirements = True
586 588 self._handle_missing_requirements(e)
587 589
588 590 if self.rhodecode_repo is None and not missing_requirements:
589 591 log.error('%s this repository is present in database but it '
590 592 'cannot be created as an scm instance', c.repo_name)
591 593
592 594 h.flash(_(
593 595 "The repository at %(repo_name)s cannot be located.") %
594 596 {'repo_name': c.repo_name},
595 597 category='error', ignore_duplicate=True)
596 598 redirect(h.route_path('home'))
597 599
598 600 # update last change according to VCS data
599 601 if not missing_requirements:
600 602 commit = db_repo.get_commit(
601 603 pre_load=["author", "date", "message", "parents"])
602 604 db_repo.update_commit_cache(commit)
603 605
604 606 # Prepare context
605 607 c.rhodecode_db_repo = db_repo
606 608 c.rhodecode_repo = self.rhodecode_repo
607 609 c.repository_requirements_missing = missing_requirements
608 610
609 611 self._update_global_counters(self.scm_model, db_repo)
610 612
611 613 def _update_global_counters(self, scm_model, db_repo):
612 614 """
613 615 Base variables that are exposed to every page of repository
614 616 """
615 617 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
616 618
617 619 def _handle_missing_requirements(self, error):
618 620 self.rhodecode_repo = None
619 621 log.error(
620 622 'Requirements are missing for repository %s: %s',
621 623 c.repo_name, error.message)
622 624
623 625 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
624 626 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
625 627 settings_update_url = url('repo', repo_name=c.repo_name)
626 628 path = request.path
627 629 should_redirect = (
628 630 path not in (summary_url, settings_update_url)
629 631 and '/settings' not in path or path == statistics_url
630 632 )
631 633 if should_redirect:
632 634 redirect(summary_url)
@@ -1,101 +1,101 b''
1 1 // Global keyboard bindings
2 2
3 3 function setRCMouseBindings(repoName, repoLandingRev) {
4 4
5 5 /** custom callback for supressing mousetrap from firing */
6 6 Mousetrap.stopCallback = function(e, element) {
7 7 // if the element has the class "mousetrap" then no need to stop
8 8 if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
9 9 return false;
10 10 }
11 11
12 12 // stop for input, select, and textarea
13 13 return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
14 14 };
15 15
16 16 // general help "?"
17 17 Mousetrap.bind(['?'], function(e) {
18 18 $('#help_kb').modal({});
19 19 });
20 20
21 21 // / open the quick filter
22 22 Mousetrap.bind(['/'], function(e) {
23 23 $('#repo_switcher').select2('open');
24 24
25 25 // return false to prevent default browser behavior
26 26 // and stop event from bubbling
27 27 return false;
28 28 });
29 29
30 30 // ctrl/command+b, show the the main bar
31 31 Mousetrap.bind(['command+b', 'ctrl+b'], function(e) {
32 32 var $headerInner = $('#header-inner'),
33 33 $content = $('#content');
34 34 if ($headerInner.hasClass('hover') && $content.hasClass('hover')) {
35 35 $headerInner.removeClass('hover');
36 36 $content.removeClass('hover');
37 37 } else {
38 38 $headerInner.addClass('hover');
39 39 $content.addClass('hover');
40 40 }
41 41 return false;
42 42 });
43 43
44 44 // general nav g + action
45 45 Mousetrap.bind(['g h'], function(e) {
46 46 window.location = pyroutes.url('home');
47 47 });
48 48 Mousetrap.bind(['g g'], function(e) {
49 49 window.location = pyroutes.url('gists_show', {'private': 1});
50 50 });
51 51 Mousetrap.bind(['g G'], function(e) {
52 52 window.location = pyroutes.url('gists_show', {'public': 1});
53 53 });
54 54 Mousetrap.bind(['n g'], function(e) {
55 55 window.location = pyroutes.url('gists_new');
56 56 });
57 57 Mousetrap.bind(['n r'], function(e) {
58 58 window.location = pyroutes.url('new_repo');
59 59 });
60 60
61 61 if (repoName && repoName != '') {
62 62 // nav in repo context
63 63 Mousetrap.bind(['g s'], function(e) {
64 64 window.location = pyroutes.url(
65 65 'repo_summary', {'repo_name': repoName});
66 66 });
67 67 Mousetrap.bind(['g c'], function(e) {
68 68 window.location = pyroutes.url(
69 'changelog_home', {'repo_name': repoName});
69 'repo_changelog', {'repo_name': repoName});
70 70 });
71 71 Mousetrap.bind(['g F'], function(e) {
72 72 window.location = pyroutes.url(
73 73 'repo_files',
74 74 {
75 75 'repo_name': repoName,
76 76 'commit_id': repoLandingRev,
77 77 'f_path': '',
78 78 'search': '1'
79 79 });
80 80 });
81 81 Mousetrap.bind(['g f'], function(e) {
82 82 window.location = pyroutes.url(
83 83 'repo_files',
84 84 {
85 85 'repo_name': repoName,
86 86 'commit_id': repoLandingRev,
87 87 'f_path': ''
88 88 });
89 89 });
90 90 Mousetrap.bind(['g o'], function(e) {
91 91 window.location = pyroutes.url(
92 92 'edit_repo', {'repo_name': repoName});
93 93 });
94 94 Mousetrap.bind(['g O'], function(e) {
95 95 window.location = pyroutes.url(
96 96 'edit_repo_perms', {'repo_name': repoName});
97 97 });
98 98 }
99 99 }
100 100
101 101 setRCMouseBindings(templateContext.repo_name, templateContext.repo_landing_commit);
@@ -1,198 +1,198 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('new_repo', '/_admin/create_repository', []);
16 16 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
17 17 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
18 18 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
19 19 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
20 20 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
21 21 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
22 22 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
23 23 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
24 24 pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
25 25 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
26 26 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
27 27 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
28 28 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
29 29 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
30 30 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
31 31 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
32 32 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
33 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
34 pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
35 pyroutes.register('changelog_elements', '/%(repo_name)s/changelog_details', ['repo_name']);
36 33 pyroutes.register('favicon', '/favicon.ico', []);
37 34 pyroutes.register('robots', '/robots.txt', []);
38 35 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
39 36 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
40 37 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
41 38 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
42 39 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
43 40 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
44 41 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/settings/integrations', ['repo_group_name']);
45 42 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
46 43 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/settings/integrations/new', ['repo_group_name']);
47 44 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
48 45 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
49 46 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
50 47 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
51 48 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
52 49 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
53 50 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
54 51 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
55 52 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
56 53 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
57 54 pyroutes.register('admin_home', '/_admin', []);
58 55 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
59 56 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
60 57 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
61 58 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
62 59 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
63 60 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
64 61 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
65 62 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
66 63 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
67 64 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
68 65 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
69 66 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
70 67 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
71 68 pyroutes.register('users', '/_admin/users', []);
72 69 pyroutes.register('users_data', '/_admin/users_data', []);
73 70 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
74 71 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
75 72 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
76 73 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
77 74 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
78 75 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
79 76 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
80 77 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
81 78 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
82 79 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
83 80 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
84 81 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
85 82 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
86 83 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
87 84 pyroutes.register('channelstream_proxy', '/_channelstream', []);
88 85 pyroutes.register('login', '/_admin/login', []);
89 86 pyroutes.register('logout', '/_admin/logout', []);
90 87 pyroutes.register('register', '/_admin/register', []);
91 88 pyroutes.register('reset_password', '/_admin/password_reset', []);
92 89 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
93 90 pyroutes.register('home', '/', []);
94 91 pyroutes.register('user_autocomplete_data', '/_users', []);
95 92 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
96 93 pyroutes.register('repo_list_data', '/_repos', []);
97 94 pyroutes.register('goto_switcher_data', '/_goto_data', []);
98 95 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
99 96 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
100 97 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
101 98 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
102 99 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
103 100 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
104 101 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
105 102 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
106 103 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
107 104 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
108 105 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
109 106 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
110 107 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
111 108 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
112 109 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
113 110 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
114 111 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
115 112 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
116 113 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
117 114 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
118 115 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
119 116 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
120 117 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
121 118 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
122 119 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
123 120 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
124 121 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
125 122 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
126 123 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
124 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
125 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
126 pyroutes.register('repo_changelog_elements', '/%(repo_name)s/changelog_elements', ['repo_name']);
127 127 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
128 128 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
129 129 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
130 130 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
131 131 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
132 132 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
133 133 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
134 134 pyroutes.register('changeset_children', '/%(repo_name)s/changeset_children/%(revision)s', ['repo_name', 'revision']);
135 135 pyroutes.register('changeset_parents', '/%(repo_name)s/changeset_parents/%(revision)s', ['repo_name', 'revision']);
136 136 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
137 137 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
138 138 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
139 139 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
140 140 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
141 141 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
142 142 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
143 143 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
144 144 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
145 145 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
146 146 pyroutes.register('repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
147 147 pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
148 148 pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
149 149 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
150 150 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
151 151 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
152 152 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
153 153 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
154 154 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
155 155 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
156 156 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
157 157 pyroutes.register('search', '/_admin/search', []);
158 158 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
159 159 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
160 160 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
161 161 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
162 162 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
163 163 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
164 164 pyroutes.register('my_account_password_update', '/_admin/my_account/password', []);
165 165 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
166 166 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
167 167 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
168 168 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
169 169 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
170 170 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
171 171 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
172 172 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
173 173 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
174 174 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
175 175 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
176 176 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
177 177 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
178 178 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
179 179 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
180 180 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
181 181 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
182 182 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
183 183 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
184 184 pyroutes.register('gists_show', '/_admin/gists', []);
185 185 pyroutes.register('gists_new', '/_admin/gists/new', []);
186 186 pyroutes.register('gists_create', '/_admin/gists/create', []);
187 187 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
188 188 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
189 189 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
190 190 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
191 191 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
192 192 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
193 193 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
194 194 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
195 195 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
196 196 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
197 197 pyroutes.register('apiv2', '/_admin/api', []);
198 198 }
@@ -1,173 +1,173 b''
1 1 // # Copyright (C) 2016-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 var CommitsController = function () {
21 21 var self = this;
22 22 this.$graphCanvas = $('#graph_canvas');
23 23 this.$commitCounter = $('#commit-counter');
24 24
25 25 this.getCurrentGraphData = function () {
26 26 // raw form
27 27 return self.$graphCanvas.data('commits');
28 28 };
29 29
30 30 this.setLabelText = function (graphData) {
31 31 var shown = $('.commit_hash').length;
32 32 var total = self.$commitCounter.data('total');
33 33
34 34 if (shown == 1) {
35 35 var text = _gettext('showing {0} out of {1} commit').format(shown, total);
36 36 } else {
37 37 var text = _gettext('showing {0} out of {1} commits').format(shown, total);
38 38 }
39 39 self.$commitCounter.html(text)
40 40 };
41 41
42 42 this.reloadGraph = function (chunk) {
43 43 chunk = chunk || 'next';
44 44
45 45 // reset state on re-render !
46 46 self.$graphCanvas.html('');
47 47
48 48 var edgeData = $("[data-graph]").data('graph') || this.$graphCanvas.data('graph') || [];
49 49
50 50 // Determine max number of edges per row in graph
51 51 var edgeCount = 1;
52 52 $.each(edgeData, function (i, item) {
53 53 $.each(item[2], function (key, value) {
54 54 if (value[1] > edgeCount) {
55 55 edgeCount = value[1];
56 56 }
57 57 });
58 58 });
59 59
60 60 var x_step = Math.min(10, Math.floor(86 / edgeCount));
61 61 var graph_options = {
62 62 width: 100,
63 63 height: $('#changesets').find('.commits-range').height(),
64 64 x_step: x_step,
65 65 y_step: 42,
66 66 dotRadius: 3.5,
67 67 lineWidth: 2.5
68 68 };
69 69
70 70 var prevCommitsData = this.$graphCanvas.data('commits') || [];
71 71 var nextCommitsData = $("[data-graph]").data('commits') || [];
72 72
73 73 if (chunk == 'next') {
74 74 var commitData = $.merge(prevCommitsData, nextCommitsData);
75 75 } else {
76 76 var commitData = $.merge(nextCommitsData, prevCommitsData);
77 77 }
78 78
79 79 this.$graphCanvas.data('graph', edgeData);
80 80 this.$graphCanvas.data('commits', commitData);
81 81
82 82 // destroy dynamic loaded graph
83 83 $("[data-graph]").remove();
84 84
85 85 this.$graphCanvas.commits(graph_options);
86 86
87 87 this.setLabelText(edgeData);
88 88 if ($('.load-more-commits').find('.prev-commits').get(0)) {
89 89 var padding = 75;
90 90
91 91 } else {
92 92 var padding = 43;
93 93 }
94 94 $('#graph_nodes').css({'padding-top': padding});
95 95 };
96 96
97 97 this.getChunkUrl = function (page, chunk, branch) {
98 98 var urlData = {
99 99 'repo_name': templateContext.repo_name,
100 100 'page': page,
101 101 'chunk': chunk
102 102 };
103 103
104 104 if (branch !== undefined && branch !== '') {
105 105 urlData['branch'] = branch;
106 106 }
107 107
108 return pyroutes.url('changelog_elements', urlData);
108 return pyroutes.url('repo_changelog_elements', urlData);
109 109 };
110 110
111 111 this.loadNext = function (node, page, branch) {
112 112 var loadUrl = this.getChunkUrl(page, 'next', branch);
113 113 var postData = {'graph': JSON.stringify(this.getCurrentGraphData())};
114 114
115 115 $.post(loadUrl, postData, function (data) {
116 116 $(node).closest('tbody').append(data);
117 117 $(node).closest('td').remove();
118 118 self.reloadGraph('next');
119 119 })
120 120 };
121 121
122 122 this.loadPrev = function (node, page, branch) {
123 123 var loadUrl = this.getChunkUrl(page, 'prev', branch);
124 124 var postData = {'graph': JSON.stringify(this.getCurrentGraphData())};
125 125
126 126 $.post(loadUrl, postData, function (data) {
127 127 $(node).closest('tbody').prepend(data);
128 128 $(node).closest('td').remove();
129 129 self.reloadGraph('prev');
130 130 })
131 131 };
132 132
133 133 this.expandCommit = function (node) {
134 134
135 135 var target_expand = $(node);
136 136 var cid = target_expand.data('commitId');
137 137
138 138 if (target_expand.hasClass('open')) {
139 139 $('#c-' + cid).css({
140 140 'height': '1.5em',
141 141 'white-space': 'nowrap',
142 142 'text-overflow': 'ellipsis',
143 143 'overflow': 'hidden'
144 144 });
145 145 $('#t-' + cid).css({
146 146 'height': 'auto',
147 147 'line-height': '.9em',
148 148 'text-overflow': 'ellipsis',
149 149 'overflow': 'hidden',
150 150 'white-space': 'nowrap'
151 151 });
152 152 target_expand.removeClass('open');
153 153 }
154 154 else {
155 155 $('#c-' + cid).css({
156 156 'height': 'auto',
157 157 'white-space': 'pre-line',
158 158 'text-overflow': 'initial',
159 159 'overflow': 'visible'
160 160 });
161 161 $('#t-' + cid).css({
162 162 'height': 'auto',
163 163 'max-height': 'none',
164 164 'text-overflow': 'initial',
165 165 'overflow': 'visible',
166 166 'white-space': 'normal'
167 167 });
168 168 target_expand.addClass('open');
169 169 }
170 170 // redraw the graph
171 171 self.reloadGraph();
172 172 }
173 173 };
@@ -1,600 +1,600 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <div class="outerwrapper">
5 5 <!-- HEADER -->
6 6 <div class="header">
7 7 <div id="header-inner" class="wrapper">
8 8 <div id="logo">
9 9 <div class="logo-wrapper">
10 10 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
11 11 </div>
12 12 %if c.rhodecode_name:
13 13 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
14 14 %endif
15 15 </div>
16 16 <!-- MENU BAR NAV -->
17 17 ${self.menu_bar_nav()}
18 18 <!-- END MENU BAR NAV -->
19 19 </div>
20 20 </div>
21 21 ${self.menu_bar_subnav()}
22 22 <!-- END HEADER -->
23 23
24 24 <!-- CONTENT -->
25 25 <div id="content" class="wrapper">
26 26
27 27 <rhodecode-toast id="notifications"></rhodecode-toast>
28 28
29 29 <div class="main">
30 30 ${next.main()}
31 31 </div>
32 32 </div>
33 33 <!-- END CONTENT -->
34 34
35 35 </div>
36 36 <!-- FOOTER -->
37 37 <div id="footer">
38 38 <div id="footer-inner" class="title wrapper">
39 39 <div>
40 40 <p class="footer-link-right">
41 41 % if c.visual.show_version:
42 42 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
43 43 % endif
44 44 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
45 45 % if c.visual.rhodecode_support_url:
46 46 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
47 47 % endif
48 48 </p>
49 49 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
50 50 <p class="server-instance" style="display:${sid}">
51 51 ## display hidden instance ID if specially defined
52 52 % if c.rhodecode_instanceid:
53 53 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
54 54 % endif
55 55 </p>
56 56 </div>
57 57 </div>
58 58 </div>
59 59
60 60 <!-- END FOOTER -->
61 61
62 62 ### MAKO DEFS ###
63 63
64 64 <%def name="menu_bar_subnav()">
65 65 </%def>
66 66
67 67 <%def name="breadcrumbs(class_='breadcrumbs')">
68 68 <div class="${class_}">
69 69 ${self.breadcrumbs_links()}
70 70 </div>
71 71 </%def>
72 72
73 73 <%def name="admin_menu()">
74 74 <ul class="admin_menu submenu">
75 75 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
76 76 <li><a href="${h.url('repos')}">${_('Repositories')}</a></li>
77 77 <li><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
78 78 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
79 79 <li><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
80 80 <li><a href="${h.url('admin_permissions_application')}">${_('Permissions')}</a></li>
81 81 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
82 82 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
83 83 <li><a href="${h.url('admin_defaults_repositories')}">${_('Defaults')}</a></li>
84 84 <li class="last"><a href="${h.url('admin_settings')}">${_('Settings')}</a></li>
85 85 </ul>
86 86 </%def>
87 87
88 88
89 89 <%def name="dt_info_panel(elements)">
90 90 <dl class="dl-horizontal">
91 91 %for dt, dd, title, show_items in elements:
92 92 <dt>${dt}:</dt>
93 93 <dd title="${h.tooltip(title)}">
94 94 %if callable(dd):
95 95 ## allow lazy evaluation of elements
96 96 ${dd()}
97 97 %else:
98 98 ${dd}
99 99 %endif
100 100 %if show_items:
101 101 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
102 102 %endif
103 103 </dd>
104 104
105 105 %if show_items:
106 106 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
107 107 %for item in show_items:
108 108 <dt></dt>
109 109 <dd>${item}</dd>
110 110 %endfor
111 111 </div>
112 112 %endif
113 113
114 114 %endfor
115 115 </dl>
116 116 </%def>
117 117
118 118
119 119 <%def name="gravatar(email, size=16)">
120 120 <%
121 121 if (size > 16):
122 122 gravatar_class = 'gravatar gravatar-large'
123 123 else:
124 124 gravatar_class = 'gravatar'
125 125 %>
126 126 <%doc>
127 127 TODO: johbo: For now we serve double size images to make it smooth
128 128 for retina. This is how it worked until now. Should be replaced
129 129 with a better solution at some point.
130 130 </%doc>
131 131 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
132 132 </%def>
133 133
134 134
135 135 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
136 136 <% email = h.email_or_none(contact) %>
137 137 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
138 138 ${self.gravatar(email, size)}
139 139 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
140 140 </div>
141 141 </%def>
142 142
143 143
144 144 ## admin menu used for people that have some admin resources
145 145 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
146 146 <ul class="submenu">
147 147 %if repositories:
148 148 <li class="local-admin-repos"><a href="${h.url('repos')}">${_('Repositories')}</a></li>
149 149 %endif
150 150 %if repository_groups:
151 151 <li class="local-admin-repo-groups"><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
152 152 %endif
153 153 %if user_groups:
154 154 <li class="local-admin-user-groups"><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
155 155 %endif
156 156 </ul>
157 157 </%def>
158 158
159 159 <%def name="repo_page_title(repo_instance)">
160 160 <div class="title-content">
161 161 <div class="title-main">
162 162 ## SVN/HG/GIT icons
163 163 %if h.is_hg(repo_instance):
164 164 <i class="icon-hg"></i>
165 165 %endif
166 166 %if h.is_git(repo_instance):
167 167 <i class="icon-git"></i>
168 168 %endif
169 169 %if h.is_svn(repo_instance):
170 170 <i class="icon-svn"></i>
171 171 %endif
172 172
173 173 ## public/private
174 174 %if repo_instance.private:
175 175 <i class="icon-repo-private"></i>
176 176 %else:
177 177 <i class="icon-repo-public"></i>
178 178 %endif
179 179
180 180 ## repo name with group name
181 181 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
182 182
183 183 </div>
184 184
185 185 ## FORKED
186 186 %if repo_instance.fork:
187 187 <p>
188 188 <i class="icon-code-fork"></i> ${_('Fork of')}
189 189 <a href="${h.route_path('repo_summary',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
190 190 </p>
191 191 %endif
192 192
193 193 ## IMPORTED FROM REMOTE
194 194 %if repo_instance.clone_uri:
195 195 <p>
196 196 <i class="icon-code-fork"></i> ${_('Clone from')}
197 197 <a href="${h.url(h.safe_str(h.hide_credentials(repo_instance.clone_uri)))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
198 198 </p>
199 199 %endif
200 200
201 201 ## LOCKING STATUS
202 202 %if repo_instance.locked[0]:
203 203 <p class="locking_locked">
204 204 <i class="icon-repo-lock"></i>
205 205 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
206 206 </p>
207 207 %elif repo_instance.enable_locking:
208 208 <p class="locking_unlocked">
209 209 <i class="icon-repo-unlock"></i>
210 210 ${_('Repository not locked. Pull repository to lock it.')}
211 211 </p>
212 212 %endif
213 213
214 214 </div>
215 215 </%def>
216 216
217 217 <%def name="repo_menu(active=None)">
218 218 <%
219 219 def is_active(selected):
220 220 if selected == active:
221 221 return "active"
222 222 %>
223 223
224 224 <!--- CONTEXT BAR -->
225 225 <div id="context-bar">
226 226 <div class="wrapper">
227 227 <ul id="context-pages" class="horizontal-list navigation">
228 228 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
229 <li class="${is_active('changelog')}"><a class="menulink" href="${h.url('changelog_home', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
229 <li class="${is_active('changelog')}"><a class="menulink" href="${h.route_path('repo_changelog', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
230 230 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
231 231 <li class="${is_active('compare')}">
232 232 <a class="menulink" href="${h.url('compare_home',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a>
233 233 </li>
234 234 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
235 235 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
236 236 <li class="${is_active('showpullrequest')}">
237 237 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
238 238 %if c.repository_pull_requests:
239 239 <span class="pr_notifications">${c.repository_pull_requests}</span>
240 240 %endif
241 241 <div class="menulabel">${_('Pull Requests')}</div>
242 242 </a>
243 243 </li>
244 244 %endif
245 245 <li class="${is_active('options')}">
246 246 <a class="menulink dropdown">
247 247 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
248 248 </a>
249 249 <ul class="submenu">
250 250 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
251 251 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
252 252 %endif
253 253 %if c.rhodecode_db_repo.fork:
254 254 <li><a href="${h.url('compare_url',repo_name=c.rhodecode_db_repo.fork.repo_name,source_ref_type=c.rhodecode_db_repo.landing_rev[0],source_ref=c.rhodecode_db_repo.landing_rev[1], target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1], merge=1)}">
255 255 ${_('Compare fork')}</a></li>
256 256 %endif
257 257
258 258 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
259 259
260 260 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
261 261 %if c.rhodecode_db_repo.locked[0]:
262 262 <li><a class="locking_del" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
263 263 %else:
264 264 <li><a class="locking_add" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
265 265 %endif
266 266 %endif
267 267 %if c.rhodecode_user.username != h.DEFAULT_USER:
268 268 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
269 269 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}">${_('Fork')}</a></li>
270 270 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
271 271 %endif
272 272 %endif
273 273 </ul>
274 274 </li>
275 275 </ul>
276 276 </div>
277 277 <div class="clear"></div>
278 278 </div>
279 279 <!--- END CONTEXT BAR -->
280 280
281 281 </%def>
282 282
283 283 <%def name="usermenu(active=False)">
284 284 ## USER MENU
285 285 <li id="quick_login_li" class="${'active' if active else ''}">
286 286 <a id="quick_login_link" class="menulink childs">
287 287 ${gravatar(c.rhodecode_user.email, 20)}
288 288 <span class="user">
289 289 %if c.rhodecode_user.username != h.DEFAULT_USER:
290 290 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
291 291 %else:
292 292 <span>${_('Sign in')}</span>
293 293 %endif
294 294 </span>
295 295 </a>
296 296
297 297 <div class="user-menu submenu">
298 298 <div id="quick_login">
299 299 %if c.rhodecode_user.username == h.DEFAULT_USER:
300 300 <h4>${_('Sign in to your account')}</h4>
301 301 ${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)}
302 302 <div class="form form-vertical">
303 303 <div class="fields">
304 304 <div class="field">
305 305 <div class="label">
306 306 <label for="username">${_('Username')}:</label>
307 307 </div>
308 308 <div class="input">
309 309 ${h.text('username',class_='focus',tabindex=1)}
310 310 </div>
311 311
312 312 </div>
313 313 <div class="field">
314 314 <div class="label">
315 315 <label for="password">${_('Password')}:</label>
316 316 %if h.HasPermissionAny('hg.password_reset.enabled')():
317 317 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
318 318 %endif
319 319 </div>
320 320 <div class="input">
321 321 ${h.password('password',class_='focus',tabindex=2)}
322 322 </div>
323 323 </div>
324 324 <div class="buttons">
325 325 <div class="register">
326 326 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
327 327 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
328 328 %endif
329 329 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
330 330 </div>
331 331 <div class="submit">
332 332 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
333 333 </div>
334 334 </div>
335 335 </div>
336 336 </div>
337 337 ${h.end_form()}
338 338 %else:
339 339 <div class="">
340 340 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
341 341 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
342 342 <div class="email">${c.rhodecode_user.email}</div>
343 343 </div>
344 344 <div class="">
345 345 <ol class="links">
346 346 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
347 347 % if c.rhodecode_user.personal_repo_group:
348 348 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
349 349 % endif
350 350 <li class="logout">
351 351 ${h.secure_form(h.route_path('logout'), request=request)}
352 352 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
353 353 ${h.end_form()}
354 354 </li>
355 355 </ol>
356 356 </div>
357 357 %endif
358 358 </div>
359 359 </div>
360 360 %if c.rhodecode_user.username != h.DEFAULT_USER:
361 361 <div class="pill_container">
362 362 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
363 363 </div>
364 364 % endif
365 365 </li>
366 366 </%def>
367 367
368 368 <%def name="menu_items(active=None)">
369 369 <%
370 370 def is_active(selected):
371 371 if selected == active:
372 372 return "active"
373 373 return ""
374 374 %>
375 375 <ul id="quick" class="main_nav navigation horizontal-list">
376 376 <!-- repo switcher -->
377 377 <li class="${is_active('repositories')} repo_switcher_li has_select2">
378 378 <input id="repo_switcher" name="repo_switcher" type="hidden">
379 379 </li>
380 380
381 381 ## ROOT MENU
382 382 %if c.rhodecode_user.username != h.DEFAULT_USER:
383 383 <li class="${is_active('journal')}">
384 384 <a class="menulink" title="${_('Show activity journal')}" href="${h.url('journal')}">
385 385 <div class="menulabel">${_('Journal')}</div>
386 386 </a>
387 387 </li>
388 388 %else:
389 389 <li class="${is_active('journal')}">
390 390 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.url('public_journal')}">
391 391 <div class="menulabel">${_('Public journal')}</div>
392 392 </a>
393 393 </li>
394 394 %endif
395 395 <li class="${is_active('gists')}">
396 396 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
397 397 <div class="menulabel">${_('Gists')}</div>
398 398 </a>
399 399 </li>
400 400 <li class="${is_active('search')}">
401 401 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
402 402 <div class="menulabel">${_('Search')}</div>
403 403 </a>
404 404 </li>
405 405 % if h.HasPermissionAll('hg.admin')('access admin main page'):
406 406 <li class="${is_active('admin')}">
407 407 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
408 408 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
409 409 </a>
410 410 ${admin_menu()}
411 411 </li>
412 412 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
413 413 <li class="${is_active('admin')}">
414 414 <a class="menulink childs" title="${_('Delegated Admin settings')}">
415 415 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
416 416 </a>
417 417 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
418 418 c.rhodecode_user.repository_groups_admin,
419 419 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
420 420 </li>
421 421 % endif
422 422 % if c.debug_style:
423 423 <li class="${is_active('debug_style')}">
424 424 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
425 425 <div class="menulabel">${_('Style')}</div>
426 426 </a>
427 427 </li>
428 428 % endif
429 429 ## render extra user menu
430 430 ${usermenu(active=(active=='my_account'))}
431 431 </ul>
432 432
433 433 <script type="text/javascript">
434 434 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
435 435
436 436 /*format the look of items in the list*/
437 437 var format = function(state, escapeMarkup){
438 438 if (!state.id){
439 439 return state.text; // optgroup
440 440 }
441 441 var obj_dict = state.obj;
442 442 var tmpl = '';
443 443
444 444 if(obj_dict && state.type == 'repo'){
445 445 if(obj_dict['repo_type'] === 'hg'){
446 446 tmpl += '<i class="icon-hg"></i> ';
447 447 }
448 448 else if(obj_dict['repo_type'] === 'git'){
449 449 tmpl += '<i class="icon-git"></i> ';
450 450 }
451 451 else if(obj_dict['repo_type'] === 'svn'){
452 452 tmpl += '<i class="icon-svn"></i> ';
453 453 }
454 454 if(obj_dict['private']){
455 455 tmpl += '<i class="icon-lock" ></i> ';
456 456 }
457 457 else if(visual_show_public_icon){
458 458 tmpl += '<i class="icon-unlock-alt"></i> ';
459 459 }
460 460 }
461 461 if(obj_dict && state.type == 'commit') {
462 462 tmpl += '<i class="icon-tag"></i>';
463 463 }
464 464 if(obj_dict && state.type == 'group'){
465 465 tmpl += '<i class="icon-folder-close"></i> ';
466 466 }
467 467 tmpl += escapeMarkup(state.text);
468 468 return tmpl;
469 469 };
470 470
471 471 var formatResult = function(result, container, query, escapeMarkup) {
472 472 return format(result, escapeMarkup);
473 473 };
474 474
475 475 var formatSelection = function(data, container, escapeMarkup) {
476 476 return format(data, escapeMarkup);
477 477 };
478 478
479 479 $("#repo_switcher").select2({
480 480 cachedDataSource: {},
481 481 minimumInputLength: 2,
482 482 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
483 483 dropdownAutoWidth: true,
484 484 formatResult: formatResult,
485 485 formatSelection: formatSelection,
486 486 containerCssClass: "repo-switcher",
487 487 dropdownCssClass: "repo-switcher-dropdown",
488 488 escapeMarkup: function(m){
489 489 // don't escape our custom placeholder
490 490 if(m.substr(0,23) == '<div class="menulabel">'){
491 491 return m;
492 492 }
493 493
494 494 return Select2.util.escapeMarkup(m);
495 495 },
496 496 query: $.debounce(250, function(query){
497 497 self = this;
498 498 var cacheKey = query.term;
499 499 var cachedData = self.cachedDataSource[cacheKey];
500 500
501 501 if (cachedData) {
502 502 query.callback({results: cachedData.results});
503 503 } else {
504 504 $.ajax({
505 505 url: pyroutes.url('goto_switcher_data'),
506 506 data: {'query': query.term},
507 507 dataType: 'json',
508 508 type: 'GET',
509 509 success: function(data) {
510 510 self.cachedDataSource[cacheKey] = data;
511 511 query.callback({results: data.results});
512 512 },
513 513 error: function(data, textStatus, errorThrown) {
514 514 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
515 515 }
516 516 })
517 517 }
518 518 })
519 519 });
520 520
521 521 $("#repo_switcher").on('select2-selecting', function(e){
522 522 e.preventDefault();
523 523 window.location = e.choice.url;
524 524 });
525 525
526 526 </script>
527 527 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
528 528 </%def>
529 529
530 530 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
531 531 <div class="modal-dialog">
532 532 <div class="modal-content">
533 533 <div class="modal-header">
534 534 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
535 535 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
536 536 </div>
537 537 <div class="modal-body">
538 538 <div class="block-left">
539 539 <table class="keyboard-mappings">
540 540 <tbody>
541 541 <tr>
542 542 <th></th>
543 543 <th>${_('Site-wide shortcuts')}</th>
544 544 </tr>
545 545 <%
546 546 elems = [
547 547 ('/', 'Open quick search box'),
548 548 ('g h', 'Goto home page'),
549 549 ('g g', 'Goto my private gists page'),
550 550 ('g G', 'Goto my public gists page'),
551 551 ('n r', 'New repository page'),
552 552 ('n g', 'New gist page'),
553 553 ]
554 554 %>
555 555 %for key, desc in elems:
556 556 <tr>
557 557 <td class="keys">
558 558 <span class="key tag">${key}</span>
559 559 </td>
560 560 <td>${desc}</td>
561 561 </tr>
562 562 %endfor
563 563 </tbody>
564 564 </table>
565 565 </div>
566 566 <div class="block-left">
567 567 <table class="keyboard-mappings">
568 568 <tbody>
569 569 <tr>
570 570 <th></th>
571 571 <th>${_('Repositories')}</th>
572 572 </tr>
573 573 <%
574 574 elems = [
575 575 ('g s', 'Goto summary page'),
576 576 ('g c', 'Goto changelog page'),
577 577 ('g f', 'Goto files page'),
578 578 ('g F', 'Goto files page with file search activated'),
579 579 ('g p', 'Goto pull requests page'),
580 580 ('g o', 'Goto repository settings'),
581 581 ('g O', 'Goto repository permissions settings'),
582 582 ]
583 583 %>
584 584 %for key, desc in elems:
585 585 <tr>
586 586 <td class="keys">
587 587 <span class="key tag">${key}</span>
588 588 </td>
589 589 <td>${desc}</td>
590 590 </tr>
591 591 %endfor
592 592 </tbody>
593 593 </table>
594 594 </div>
595 595 </div>
596 596 <div class="modal-footer">
597 597 </div>
598 598 </div><!-- /.modal-content -->
599 599 </div><!-- /.modal-dialog -->
600 600 </div><!-- /.modal -->
@@ -1,299 +1,299 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.mako"/>
4 4
5 5 <%def name="title()">
6 6 ${_('%s Changelog') % c.repo_name}
7 7 %if c.changelog_for_path:
8 8 /${c.changelog_for_path}
9 9 %endif
10 10 %if c.rhodecode_name:
11 11 &middot; ${h.branding(c.rhodecode_name)}
12 12 %endif
13 13 </%def>
14 14
15 15 <%def name="breadcrumbs_links()">
16 16 %if c.changelog_for_path:
17 17 /${c.changelog_for_path}
18 18 %endif
19 19 </%def>
20 20
21 21 <%def name="menu_bar_nav()">
22 22 ${self.menu_items(active='repositories')}
23 23 </%def>
24 24
25 25 <%def name="menu_bar_subnav()">
26 26 ${self.repo_menu(active='changelog')}
27 27 </%def>
28 28
29 29 <%def name="main()">
30 30
31 31 <div class="box">
32 32 <div class="title">
33 33 ${self.repo_page_title(c.rhodecode_db_repo)}
34 34 <ul class="links">
35 35 <li>
36 36 <a href="#" class="btn btn-small" id="rev_range_container" style="display:none;"></a>
37 37 %if c.rhodecode_db_repo.fork:
38 38 <span>
39 39 <a id="compare_fork_button"
40 40 title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
41 41 class="btn btn-small"
42 42 href="${h.url('compare_url',
43 43 repo_name=c.rhodecode_db_repo.fork.repo_name,
44 44 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
45 45 source_ref=c.rhodecode_db_repo.landing_rev[1],
46 46 target_repo=c.repo_name,
47 47 target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
48 48 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
49 49 merge=1)}"
50 50 >
51 51 <i class="icon-loop"></i>
52 52 ${_('Compare fork with Parent (%s)' % c.rhodecode_db_repo.fork.repo_name)}
53 53 </a>
54 54 </span>
55 55 %endif
56 56
57 57 ## pr open link
58 58 %if h.is_hg(c.rhodecode_repo) or h.is_git(c.rhodecode_repo):
59 59 <span>
60 60 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.url('pullrequest_home',repo_name=c.repo_name)}">
61 61 ${_('Open new pull request')}
62 62 </a>
63 63 </span>
64 64 %endif
65 65
66 66 ## clear selection
67 67 <div title="${_('Clear selection')}" class="btn" id="rev_range_clear" style="display:none">
68 68 ${_('Clear selection')}
69 69 </div>
70 70
71 71 </li>
72 72 </ul>
73 73 </div>
74 74
75 75 % if c.pagination:
76 76 <script type="text/javascript" src="${h.asset('js/jquery.commits-graph.js')}"></script>
77 77
78 78 <div class="graph-header">
79 79 <div id="filter_changelog">
80 80 ${h.hidden('branch_filter')}
81 81 %if c.selected_name:
82 82 <div class="btn btn-default" id="clear_filter" >
83 83 ${_('Clear filter')}
84 84 </div>
85 85 %endif
86 86 </div>
87 87 ${self.breadcrumbs('breadcrumbs_light')}
88 88 <div id="commit-counter" data-total=${c.total_cs} class="pull-right">
89 ${ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)}
89 ${_ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)}
90 90 </div>
91 91 </div>
92 92
93 93 <div id="graph">
94 94 <div class="graph-col-wrapper">
95 95 <div id="graph_nodes">
96 96 <div id="graph_canvas"></div>
97 97 </div>
98 98 <div id="graph_content" class="main-content graph_full_width">
99 99
100 100 <div class="table">
101 101 <table id="changesets" class="rctable">
102 102 <tr>
103 103 ## checkbox
104 104 <th></th>
105 105 <th colspan="2"></th>
106 106
107 107 <th>${_('Commit')}</th>
108 108 ## commit message expand arrow
109 109 <th></th>
110 110 <th>${_('Commit Message')}</th>
111 111
112 112 <th>${_('Age')}</th>
113 113 <th>${_('Author')}</th>
114 114
115 115 <th>${_('Refs')}</th>
116 116 </tr>
117 117
118 118 <tbody class="commits-range">
119 119 <%include file='changelog_elements.mako'/>
120 120 </tbody>
121 121 </table>
122 122 </div>
123 123 </div>
124 124 <div class="pagination-wh pagination-left">
125 125 ${c.pagination.pager('$link_previous ~2~ $link_next')}
126 126 </div>
127 127 </div>
128 128
129 129 <script type="text/javascript">
130 130 var cache = {};
131 131 $(function(){
132 132
133 133 // Create links to commit ranges when range checkboxes are selected
134 134 var $commitCheckboxes = $('.commit-range');
135 135 // cache elements
136 136 var $commitRangeContainer = $('#rev_range_container');
137 137 var $commitRangeClear = $('#rev_range_clear');
138 138
139 139 var checkboxRangeSelector = function(e){
140 140 var selectedCheckboxes = [];
141 141 for (pos in $commitCheckboxes){
142 142 if($commitCheckboxes[pos].checked){
143 143 selectedCheckboxes.push($commitCheckboxes[pos]);
144 144 }
145 145 }
146 146 var open_new_pull_request = $('#open_new_pull_request');
147 147 if(open_new_pull_request){
148 148 var selected_changes = selectedCheckboxes.length;
149 149 if (selected_changes > 1 || selected_changes == 1 && templateContext.repo_type != 'hg') {
150 150 open_new_pull_request.hide();
151 151 } else {
152 152 if (selected_changes == 1) {
153 153 open_new_pull_request.html(_gettext('Open new pull request for selected commit'));
154 154 } else if (selected_changes == 0) {
155 155 open_new_pull_request.html(_gettext('Open new pull request'));
156 156 }
157 157 open_new_pull_request.show();
158 158 }
159 159 }
160 160
161 161 if (selectedCheckboxes.length>0){
162 162 var revEnd = selectedCheckboxes[0].name;
163 163 var revStart = selectedCheckboxes[selectedCheckboxes.length-1].name;
164 164 var url = pyroutes.url('changeset_home',
165 165 {'repo_name': '${c.repo_name}',
166 166 'revision': revStart+'...'+revEnd});
167 167
168 168 var link = (revStart == revEnd)
169 169 ? _gettext('Show selected commit __S')
170 170 : _gettext('Show selected commits __S ... __E');
171 171
172 172 link = link.replace('__S', revStart.substr(0,6));
173 173 link = link.replace('__E', revEnd.substr(0,6));
174 174
175 175 $commitRangeContainer
176 176 .attr('href',url)
177 177 .html(link)
178 178 .show();
179 179
180 180 $commitRangeClear.show();
181 181 var _url = pyroutes.url('pullrequest_home',
182 182 {'repo_name': '${c.repo_name}',
183 183 'commit': revEnd});
184 184 open_new_pull_request.attr('href', _url);
185 185 $('#compare_fork_button').hide();
186 186 } else {
187 187 $commitRangeContainer.hide();
188 188 $commitRangeClear.hide();
189 189
190 190 %if c.branch_name:
191 191 var _url = pyroutes.url('pullrequest_home',
192 192 {'repo_name': '${c.repo_name}',
193 193 'branch':'${c.branch_name}'});
194 194 open_new_pull_request.attr('href', _url);
195 195 %else:
196 196 var _url = pyroutes.url('pullrequest_home',
197 197 {'repo_name': '${c.repo_name}'});
198 198 open_new_pull_request.attr('href', _url);
199 199 %endif
200 200 $('#compare_fork_button').show();
201 201 }
202 202 };
203 203
204 204 $commitCheckboxes.on('click', checkboxRangeSelector);
205 205
206 206 $commitRangeClear.on('click',function(e) {
207 207 $commitCheckboxes.attr('checked', false);
208 208 checkboxRangeSelector();
209 209 e.preventDefault();
210 210 });
211 211
212 212 // make sure the buttons are consistent when navigate back and forth
213 213 checkboxRangeSelector();
214 214
215 215 var msgs = $('.message');
216 216 // get first element height
217 217 var el = $('#graph_content .container')[0];
218 218 var row_h = el.clientHeight;
219 219 for (var i=0; i < msgs.length; i++) {
220 220 var m = msgs[i];
221 221
222 222 var h = m.clientHeight;
223 223 var pad = $(m).css('padding');
224 224 if (h > row_h) {
225 225 var offset = row_h - (h+12);
226 226 $(m.nextElementSibling).css('display','block');
227 227 $(m.nextElementSibling).css('margin-top',offset+'px');
228 228 }
229 229 }
230 230
231 231 $("#clear_filter").on("click", function() {
232 232 var filter = {'repo_name': '${c.repo_name}'};
233 window.location = pyroutes.url('changelog_home', filter);
233 window.location = pyroutes.url('repo_changelog', filter);
234 234 });
235 235
236 236 $("#branch_filter").select2({
237 237 'dropdownAutoWidth': true,
238 238 'width': 'resolve',
239 239 'placeholder': "${c.selected_name or _('Filter changelog')}",
240 240 containerCssClass: "drop-menu",
241 241 dropdownCssClass: "drop-menu-dropdown",
242 242 query: function(query){
243 243 var key = 'cache';
244 244 var cached = cache[key] ;
245 245 if(cached) {
246 246 var data = {results: []};
247 247 //filter results
248 248 $.each(cached.results, function(){
249 249 var section = this.text;
250 250 var children = [];
251 251 $.each(this.children, function(){
252 252 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
253 253 children.push({'id': this.id, 'text': this.text, 'type': this.type})
254 254 }
255 255 });
256 256 data.results.push({'text': section, 'children': children});
257 257 query.callback({results: data.results});
258 258 });
259 259 }else{
260 260 $.ajax({
261 261 url: pyroutes.url('repo_refs_changelog_data', {'repo_name': '${c.repo_name}'}),
262 262 data: {},
263 263 dataType: 'json',
264 264 type: 'GET',
265 265 success: function(data) {
266 266 cache[key] = data;
267 267 query.callback({results: data.results});
268 268 }
269 269 })
270 270 }
271 271 }
272 272 });
273 273 $('#branch_filter').on('change', function(e){
274 274 var data = $('#branch_filter').select2('data');
275 275 var selected = data.text;
276 276 var filter = {'repo_name': '${c.repo_name}'};
277 277 if(data.type == 'branch' || data.type == 'branch_closed'){
278 278 filter["branch"] = selected;
279 279 }
280 280 else if (data.type == 'book'){
281 281 filter["bookmark"] = selected;
282 282 }
283 window.location = pyroutes.url('changelog_home', filter);
283 window.location = pyroutes.url('repo_changelog', filter);
284 284 });
285 285
286 286 commitsController = new CommitsController();
287 287 % if not c.changelog_for_path:
288 288 commitsController.reloadGraph();
289 289 % endif
290 290
291 291 });
292 292
293 293 </script>
294 294 </div>
295 295 % else:
296 296 ${_('There are no changes yet')}
297 297 % endif
298 298 </div>
299 299 </%def>
@@ -1,142 +1,142 b''
1 1 ## small box that displays changed/added/removed details fetched by AJAX
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4
5 5 % if c.prev_page:
6 6 <tr>
7 7 <td colspan="9" class="load-more-commits">
8 8 <a class="prev-commits" href="#loadPrevCommits" onclick="commitsController.loadPrev(this, ${c.prev_page}, '${c.branch_name}');return false">
9 9 ${_('load previous')}
10 10 </a>
11 11 </td>
12 12 </tr>
13 13 % endif
14 14
15 15 % for cnt,commit in enumerate(c.pagination):
16 16 <tr id="sha_${commit.raw_id}" class="changelogRow container ${'tablerow%s' % (cnt%2)}">
17 17
18 18 <td class="td-checkbox">
19 19 ${h.checkbox(commit.raw_id,class_="commit-range")}
20 20 </td>
21 21 <td class="td-status">
22 22
23 23 %if c.statuses.get(commit.raw_id):
24 24 <div class="changeset-status-ico">
25 25 %if c.statuses.get(commit.raw_id)[2]:
26 26 <a class="tooltip" title="${_('Commit status: %s\nClick to open associated pull request #%s') % (h.commit_status_lbl(c.statuses.get(commit.raw_id)[0]), c.statuses.get(commit.raw_id)[2])}" href="${h.route_path('pullrequest_show',repo_name=c.statuses.get(commit.raw_id)[3],pull_request_id=c.statuses.get(commit.raw_id)[2])}">
27 27 <div class="${'flag_status %s' % c.statuses.get(commit.raw_id)[0]}"></div>
28 28 </a>
29 29 %else:
30 30 <a class="tooltip" title="${_('Commit status: %s') % h.commit_status_lbl(c.statuses.get(commit.raw_id)[0])}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id,anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
31 31 <div class="${'flag_status %s' % c.statuses.get(commit.raw_id)[0]}"></div>
32 32 </a>
33 33 %endif
34 34 </div>
35 35 %else:
36 36 <div class="tooltip flag_status not_reviewed" title="${_('Commit status: Not Reviewed')}"></div>
37 37 %endif
38 38 </td>
39 39 <td class="td-comments comments-col">
40 40 %if c.comments.get(commit.raw_id):
41 41 <a title="${_('Commit has comments')}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id,anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
42 42 <i class="icon-comment"></i> ${len(c.comments[commit.raw_id])}
43 43 </a>
44 44 %endif
45 45 </td>
46 46 <td class="td-hash">
47 47 <code>
48 48 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">
49 49 <span class="${'commit_hash obsolete' if getattr(commit, 'obsolete', None) else 'commit_hash'}">${h.show_id(commit)}</span>
50 50 </a>
51 51 % if hasattr(commit, 'phase'):
52 52 % if commit.phase != 'public':
53 53 <span class="tag phase-${commit.phase} tooltip" title="${_('Commit phase')}">${commit.phase}</span>
54 54 % endif
55 55 % endif
56 56
57 57 ## obsolete commits
58 58 % if hasattr(commit, 'obsolete'):
59 59 % if commit.obsolete:
60 60 <span class="tag obsolete-${commit.obsolete} tooltip" title="${_('Evolve State')}">${_('obsolete')}</span>
61 61 % endif
62 62 % endif
63 63
64 64 ## hidden commits
65 65 % if hasattr(commit, 'hidden'):
66 66 % if commit.hidden:
67 67 <span class="tag obsolete-${commit.hidden} tooltip" title="${_('Evolve State')}">${_('hidden')}</span>
68 68 % endif
69 69 % endif
70 70
71 71 </code>
72 72 </td>
73 73 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
74 74 <div class="show_more_col">
75 75 <i class="show_more"></i>&nbsp;
76 76 </div>
77 77 </td>
78 78 <td class="td-description mid">
79 79 <div class="log-container truncate-wrap">
80 80 <div class="message truncate" id="c-${commit.raw_id}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
81 81 </div>
82 82 </td>
83 83
84 84 <td class="td-time">
85 85 ${h.age_component(commit.date)}
86 86 </td>
87 87 <td class="td-user">
88 88 ${base.gravatar_with_user(commit.author)}
89 89 </td>
90 90
91 91 <td class="td-tags tags-col">
92 92 <div id="t-${commit.raw_id}">
93 93
94 94 ## merge
95 95 %if commit.merge:
96 96 <span class="tag mergetag">
97 97 <i class="icon-merge"></i>${_('merge')}
98 98 </span>
99 99 %endif
100 100
101 101 ## branch
102 102 %if commit.branch:
103 103 <span class="tag branchtag" title="${h.tooltip(_('Branch %s') % commit.branch)}">
104 <a href="${h.url('changelog_home',repo_name=c.repo_name,branch=commit.branch)}"><i class="icon-code-fork"></i>${h.shorter(commit.branch)}</a>
104 <a href="${h.route_path('repo_changelog',repo_name=c.repo_name,_query=dict(branch=commit.branch))}"><i class="icon-code-fork"></i>${h.shorter(commit.branch)}</a>
105 105 </span>
106 106 %endif
107 107
108 108 ## bookmarks
109 109 %if h.is_hg(c.rhodecode_repo):
110 110 %for book in commit.bookmarks:
111 111 <span class="tag booktag" title="${h.tooltip(_('Bookmark %s') % book)}">
112 112 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id, _query=dict(at=book))}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
113 113 </span>
114 114 %endfor
115 115 %endif
116 116
117 117 ## tags
118 118 %for tag in commit.tags:
119 119 <span class="tag tagtag" title="${h.tooltip(_('Tag %s') % tag)}">
120 120 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id, _query=dict(at=tag))}"><i class="icon-tag"></i>${h.shorter(tag)}</a>
121 121 </span>
122 122 %endfor
123 123
124 124 </div>
125 125 </td>
126 126 </tr>
127 127 % endfor
128 128
129 129 % if c.next_page:
130 130 <tr>
131 131 <td colspan="9" class="load-more-commits">
132 132 <a class="next-commits" href="#loadNextCommits" onclick="commitsController.loadNext(this, ${c.next_page}, '${c.branch_name}');return false">
133 133 ${_('load next')}
134 134 </a>
135 135 </td>
136 136 </tr>
137 137 % endif
138 138 <tr class="chunk-graph-data" style="display:none"
139 139 data-graph='${c.graph_data|n}'
140 140 data-node='${c.prev_page}:${c.next_page}'
141 141 data-commits='${c.graph_commits|n}'>
142 142 </tr> No newline at end of file
@@ -1,317 +1,317 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5
6 6 ## REPOSITORY RENDERERS
7 7 <%def name="quick_menu(repo_name)">
8 8 <i class="pointer icon-more"></i>
9 9 <div class="menu_items_container hidden">
10 10 <ul class="menu_items">
11 11 <li>
12 12 <a title="${_('Summary')}" href="${h.route_path('repo_summary',repo_name=repo_name)}">
13 13 <span>${_('Summary')}</span>
14 14 </a>
15 15 </li>
16 16 <li>
17 <a title="${_('Changelog')}" href="${h.url('changelog_home',repo_name=repo_name)}">
17 <a title="${_('Changelog')}" href="${h.route_path('repo_changelog',repo_name=repo_name)}">
18 18 <span>${_('Changelog')}</span>
19 19 </a>
20 20 </li>
21 21 <li>
22 22 <a title="${_('Files')}" href="${h.route_path('repo_files:default_commit',repo_name=repo_name)}">
23 23 <span>${_('Files')}</span>
24 24 </a>
25 25 </li>
26 26 <li>
27 27 <a title="${_('Fork')}" href="${h.url('repo_fork_home',repo_name=repo_name)}">
28 28 <span>${_('Fork')}</span>
29 29 </a>
30 30 </li>
31 31 </ul>
32 32 </div>
33 33 </%def>
34 34
35 35 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
36 36 <%
37 37 def get_name(name,short_name=short_name):
38 38 if short_name:
39 39 return name.split('/')[-1]
40 40 else:
41 41 return name
42 42 %>
43 43 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
44 44 ##NAME
45 45 <a href="${h.route_path('edit_repo',repo_name=name) if admin else h.route_path('repo_summary',repo_name=name)}">
46 46
47 47 ##TYPE OF REPO
48 48 %if h.is_hg(rtype):
49 49 <span title="${_('Mercurial repository')}"><i class="icon-hg"></i></span>
50 50 %elif h.is_git(rtype):
51 51 <span title="${_('Git repository')}"><i class="icon-git"></i></span>
52 52 %elif h.is_svn(rtype):
53 53 <span title="${_('Subversion repository')}"><i class="icon-svn"></i></span>
54 54 %endif
55 55
56 56 ##PRIVATE/PUBLIC
57 57 %if private and c.visual.show_private_icon:
58 58 <i class="icon-lock" title="${_('Private repository')}"></i>
59 59 %elif not private and c.visual.show_public_icon:
60 60 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
61 61 %else:
62 62 <span></span>
63 63 %endif
64 64 ${get_name(name)}
65 65 </a>
66 66 %if fork_of:
67 67 <a href="${h.route_path('repo_summary',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
68 68 %endif
69 69 %if rstate == 'repo_state_pending':
70 70 <i class="icon-cogs" title="${_('Repository creating in progress...')}"></i>
71 71 %endif
72 72 </div>
73 73 </%def>
74 74
75 75 <%def name="repo_desc(description)">
76 76 <div class="truncate-wrap">${description}</div>
77 77 </%def>
78 78
79 79 <%def name="last_change(last_change)">
80 80 ${h.age_component(last_change)}
81 81 </%def>
82 82
83 83 <%def name="revision(name,rev,tip,author,last_msg)">
84 84 <div>
85 85 %if rev >= 0:
86 86 <code><a title="${h.tooltip('%s:\n\n%s' % (author,last_msg))}" class="tooltip" href="${h.url('changeset_home',repo_name=name,revision=tip)}">${'r%s:%s' % (rev,h.short_id(tip))}</a></code>
87 87 %else:
88 88 ${_('No commits yet')}
89 89 %endif
90 90 </div>
91 91 </%def>
92 92
93 93 <%def name="rss(name)">
94 94 %if c.rhodecode_user.username != h.DEFAULT_USER:
95 95 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
96 96 %else:
97 97 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
98 98 %endif
99 99 </%def>
100 100
101 101 <%def name="atom(name)">
102 102 %if c.rhodecode_user.username != h.DEFAULT_USER:
103 103 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
104 104 %else:
105 105 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
106 106 %endif
107 107 </%def>
108 108
109 109 <%def name="user_gravatar(email, size=16)">
110 110 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
111 111 ${base.gravatar(email, 16)}
112 112 </div>
113 113 </%def>
114 114
115 115 <%def name="repo_actions(repo_name, super_user=True)">
116 116 <div>
117 117 <div class="grid_edit">
118 118 <a href="${h.route_path('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
119 119 <i class="icon-pencil"></i>Edit</a>
120 120 </div>
121 121 <div class="grid_delete">
122 122 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), method='POST', request=request)}
123 123 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
124 124 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
125 125 ${h.end_form()}
126 126 </div>
127 127 </div>
128 128 </%def>
129 129
130 130 <%def name="repo_state(repo_state)">
131 131 <div>
132 132 %if repo_state == 'repo_state_pending':
133 133 <div class="tag tag4">${_('Creating')}</div>
134 134 %elif repo_state == 'repo_state_created':
135 135 <div class="tag tag1">${_('Created')}</div>
136 136 %else:
137 137 <div class="tag alert2" title="${h.tooltip(repo_state)}">invalid</div>
138 138 %endif
139 139 </div>
140 140 </%def>
141 141
142 142
143 143 ## REPO GROUP RENDERERS
144 144 <%def name="quick_repo_group_menu(repo_group_name)">
145 145 <i class="pointer icon-more"></i>
146 146 <div class="menu_items_container hidden">
147 147 <ul class="menu_items">
148 148 <li>
149 149 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
150 150 <span class="icon">
151 151 <i class="icon-file-text"></i>
152 152 </span>
153 153 <span>${_('Summary')}</span>
154 154 </a>
155 155 </li>
156 156
157 157 </ul>
158 158 </div>
159 159 </%def>
160 160
161 161 <%def name="repo_group_name(repo_group_name, children_groups=None)">
162 162 <div>
163 163 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
164 164 <i class="icon-folder-close" title="${_('Repository group')}"></i>
165 165 %if children_groups:
166 166 ${h.literal(' &raquo; '.join(children_groups))}
167 167 %else:
168 168 ${repo_group_name}
169 169 %endif
170 170 </a>
171 171 </div>
172 172 </%def>
173 173
174 174 <%def name="repo_group_desc(description)">
175 175 <div class="truncate-wrap">${description}</div>
176 176 </%def>
177 177
178 178 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
179 179 <div class="grid_edit">
180 180 <a href="${h.url('edit_repo_group',group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
181 181 </div>
182 182 <div class="grid_delete">
183 183 ${h.secure_form(h.url('delete_repo_group', group_name=repo_group_name),method='delete')}
184 184 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
185 185 onclick="return confirm('"+ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
186 186 ${h.end_form()}
187 187 </div>
188 188 </%def>
189 189
190 190
191 191 <%def name="user_actions(user_id, username)">
192 192 <div class="grid_edit">
193 193 <a href="${h.url('edit_user',user_id=user_id)}" title="${_('Edit')}">
194 194 <i class="icon-pencil"></i>Edit</a>
195 195 </div>
196 196 <div class="grid_delete">
197 197 ${h.secure_form(h.url('delete_user', user_id=user_id),method='delete')}
198 198 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
199 199 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
200 200 ${h.end_form()}
201 201 </div>
202 202 </%def>
203 203
204 204 <%def name="user_group_actions(user_group_id, user_group_name)">
205 205 <div class="grid_edit">
206 206 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
207 207 </div>
208 208 <div class="grid_delete">
209 209 ${h.secure_form(h.url('delete_users_group', user_group_id=user_group_id),method='delete')}
210 210 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
211 211 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
212 212 ${h.end_form()}
213 213 </div>
214 214 </%def>
215 215
216 216
217 217 <%def name="user_name(user_id, username)">
218 218 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.url('edit_user', user_id=user_id))}
219 219 </%def>
220 220
221 221 <%def name="user_profile(username)">
222 222 ${base.gravatar_with_user(username, 16)}
223 223 </%def>
224 224
225 225 <%def name="user_group_name(user_group_id, user_group_name)">
226 226 <div>
227 227 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}">
228 228 <i class="icon-group" title="${_('User group')}"></i> ${user_group_name}</a>
229 229 </div>
230 230 </%def>
231 231
232 232
233 233 ## GISTS
234 234
235 235 <%def name="gist_gravatar(full_contact)">
236 236 <div class="gist_gravatar">
237 237 ${base.gravatar(full_contact, 30)}
238 238 </div>
239 239 </%def>
240 240
241 241 <%def name="gist_access_id(gist_access_id, full_contact)">
242 242 <div>
243 243 <b>
244 244 <a href="${h.route_path('gist_show', gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
245 245 </b>
246 246 </div>
247 247 </%def>
248 248
249 249 <%def name="gist_author(full_contact, created_on, expires)">
250 250 ${base.gravatar_with_user(full_contact, 16)}
251 251 </%def>
252 252
253 253
254 254 <%def name="gist_created(created_on)">
255 255 <div class="created">
256 256 ${h.age_component(created_on, time_is_local=True)}
257 257 </div>
258 258 </%def>
259 259
260 260 <%def name="gist_expires(expires)">
261 261 <div class="created">
262 262 %if expires == -1:
263 263 ${_('never')}
264 264 %else:
265 265 ${h.age_component(h.time_to_utcdatetime(expires))}
266 266 %endif
267 267 </div>
268 268 </%def>
269 269
270 270 <%def name="gist_type(gist_type)">
271 271 %if gist_type != 'public':
272 272 <div class="tag">${_('Private')}</div>
273 273 %endif
274 274 </%def>
275 275
276 276 <%def name="gist_description(gist_description)">
277 277 ${gist_description}
278 278 </%def>
279 279
280 280
281 281 ## PULL REQUESTS GRID RENDERERS
282 282
283 283 <%def name="pullrequest_target_repo(repo_name)">
284 284 <div class="truncate">
285 285 ${h.link_to(repo_name,h.route_path('repo_summary',repo_name=repo_name))}
286 286 </div>
287 287 </%def>
288 288 <%def name="pullrequest_status(status)">
289 289 <div class="${'flag_status %s' % status} pull-left"></div>
290 290 </%def>
291 291
292 292 <%def name="pullrequest_title(title, description)">
293 293 ${title} <br/>
294 294 ${h.shorter(description, 40)}
295 295 </%def>
296 296
297 297 <%def name="pullrequest_comments(comments_nr)">
298 298 <i class="icon-comment"></i> ${comments_nr}
299 299 </%def>
300 300
301 301 <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
302 302 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
303 303 % if short:
304 304 #${pull_request_id}
305 305 % else:
306 306 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
307 307 % endif
308 308 </a>
309 309 </%def>
310 310
311 311 <%def name="pullrequest_updated_on(updated_on)">
312 312 ${h.age_component(h.time_to_utcdatetime(updated_on))}
313 313 </%def>
314 314
315 315 <%def name="pullrequest_author(full_contact)">
316 316 ${base.gravatar_with_user(full_contact, 16)}
317 317 </%def>
@@ -1,324 +1,324 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title(*args)">
4 4 ${_('%s Files') % c.repo_name}
5 5 %if hasattr(c,'file'):
6 6 &middot; ${h.safe_unicode(c.file.path) or '\\'}
7 7 %endif
8 8
9 9 %if c.rhodecode_name:
10 10 &middot; ${h.branding(c.rhodecode_name)}
11 11 %endif
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${_('Files')}
16 16 %if c.file:
17 17 @ ${h.show_id(c.commit)}
18 18 %endif
19 19 </%def>
20 20
21 21 <%def name="menu_bar_nav()">
22 22 ${self.menu_items(active='repositories')}
23 23 </%def>
24 24
25 25 <%def name="menu_bar_subnav()">
26 26 ${self.repo_menu(active='files')}
27 27 </%def>
28 28
29 29 <%def name="main()">
30 30 <div class="title">
31 31 ${self.repo_page_title(c.rhodecode_db_repo)}
32 32 </div>
33 33
34 34 <div id="pjax-container" class="summary">
35 35 <div id="files_data">
36 36 <%include file='files_pjax.mako'/>
37 37 </div>
38 38 </div>
39 39 <script>
40 40 var curState = {
41 41 commit_id: "${c.commit.raw_id}"
42 42 };
43 43
44 44 var getState = function(context) {
45 45 var url = $(location).attr('href');
46 46 var _base_url = '${h.route_path("repo_files",repo_name=c.repo_name,commit_id='',f_path='')}';
47 47 var _annotate_url = '${h.route_path("repo_files:annotated",repo_name=c.repo_name,commit_id='',f_path='')}';
48 48 _base_url = _base_url.replace('//', '/');
49 49 _annotate_url = _annotate_url.replace('//', '/');
50 50
51 51 //extract f_path from url.
52 52 var parts = url.split(_base_url);
53 53 if (parts.length != 2) {
54 54 parts = url.split(_annotate_url);
55 55 if (parts.length != 2) {
56 56 var rev = "tip";
57 57 var f_path = "";
58 58 } else {
59 59 var parts2 = parts[1].split('/');
60 60 var rev = parts2.shift(); // pop the first element which is the revision
61 61 var f_path = parts2.join('/');
62 62 }
63 63
64 64 } else {
65 65 var parts2 = parts[1].split('/');
66 66 var rev = parts2.shift(); // pop the first element which is the revision
67 67 var f_path = parts2.join('/');
68 68 }
69 69
70 70 var _node_list_url = pyroutes.url('repo_files_nodelist',
71 71 {repo_name: templateContext.repo_name,
72 72 commit_id: rev, f_path: f_path});
73 73 var _url_base = pyroutes.url('repo_files',
74 74 {repo_name: templateContext.repo_name,
75 75 commit_id: rev, f_path:'__FPATH__'});
76 76 return {
77 77 url: url,
78 78 f_path: f_path,
79 79 rev: rev,
80 80 commit_id: curState.commit_id,
81 81 node_list_url: _node_list_url,
82 82 url_base: _url_base
83 83 };
84 84 };
85 85
86 86 var metadataRequest = null;
87 87 var getFilesMetadata = function() {
88 88 if (metadataRequest && metadataRequest.readyState != 4) {
89 89 metadataRequest.abort();
90 90 }
91 91 if (fileSourcePage) {
92 92 return false;
93 93 }
94 94
95 95 if ($('#file-tree-wrapper').hasClass('full-load')) {
96 96 // in case our HTML wrapper has full-load class we don't
97 97 // trigger the async load of metadata
98 98 return false;
99 99 }
100 100
101 101 var state = getState('metadata');
102 102 var url_data = {
103 103 'repo_name': templateContext.repo_name,
104 104 'commit_id': state.commit_id,
105 105 'f_path': state.f_path
106 106 };
107 107
108 108 var url = pyroutes.url('repo_nodetree_full', url_data);
109 109
110 110 metadataRequest = $.ajax({url: url});
111 111
112 112 metadataRequest.done(function(data) {
113 113 $('#file-tree').html(data);
114 114 timeagoActivate();
115 115 });
116 116 metadataRequest.fail(function (data, textStatus, errorThrown) {
117 117 console.log(data);
118 118 if (data.status != 0) {
119 119 alert("Error while fetching metadata.\nError code {0} ({1}).Please consider reloading the page".format(data.status,data.statusText));
120 120 }
121 121 });
122 122 };
123 123
124 124 var callbacks = function() {
125 125 var state = getState('callbacks');
126 126 timeagoActivate();
127 127
128 128 // used for history, and switch to
129 129 var initialCommitData = {
130 130 id: null,
131 131 text: '${_("Pick Commit")}',
132 132 type: 'sha',
133 133 raw_id: null,
134 134 files_url: null
135 135 };
136 136
137 137 if ($('#trimmed_message_box').height() < 50) {
138 138 $('#message_expand').hide();
139 139 }
140 140
141 141 $('#message_expand').on('click', function(e) {
142 142 $('#trimmed_message_box').css('max-height', 'none');
143 143 $(this).hide();
144 144 });
145 145
146 146 if (fileSourcePage) {
147 147 // variants for with source code, not tree view
148 148
149 149 // select code link event
150 150 $("#hlcode").mouseup(getSelectionLink);
151 151
152 152 // file history select2
153 153 select2FileHistorySwitcher('#diff1', initialCommitData, state);
154 154
155 155 // show at, diff to actions handlers
156 156 $('#diff1').on('change', function(e) {
157 157 $('#diff_to_commit').removeClass('disabled').removeAttr("disabled");
158 158 $('#diff_to_commit').val(_gettext('Diff to Commit ') + e.val.truncateAfter(8, '...'));
159 159
160 160 $('#show_at_commit').removeClass('disabled').removeAttr("disabled");
161 161 $('#show_at_commit').val(_gettext('Show at Commit ') + e.val.truncateAfter(8, '...'));
162 162 });
163 163
164 164 $('#diff_to_commit').on('click', function(e) {
165 165 var diff1 = $('#diff1').val();
166 166 var diff2 = $('#diff2').val();
167 167
168 168 var url_data = {
169 169 repo_name: templateContext.repo_name,
170 170 source_ref: diff1,
171 171 source_ref_type: 'rev',
172 172 target_ref: diff2,
173 173 target_ref_type: 'rev',
174 174 merge: 1,
175 175 f_path: state.f_path
176 176 };
177 177 window.location = pyroutes.url('compare_url', url_data);
178 178 });
179 179
180 180 $('#show_at_commit').on('click', function(e) {
181 181 var diff1 = $('#diff1').val();
182 182
183 183 var annotate = $('#annotate').val();
184 184 if (annotate === "True") {
185 185 var url = pyroutes.url('repo_files:annotated',
186 186 {'repo_name': templateContext.repo_name,
187 187 'commit_id': diff1, 'f_path': state.f_path});
188 188 } else {
189 189 var url = pyroutes.url('repo_files',
190 190 {'repo_name': templateContext.repo_name,
191 191 'commit_id': diff1, 'f_path': state.f_path});
192 192 }
193 193 window.location = url;
194 194
195 195 });
196 196
197 197 // show more authors
198 198 $('#show_authors').on('click', function(e) {
199 199 e.preventDefault();
200 200 var url = pyroutes.url('repo_file_authors',
201 201 {'repo_name': templateContext.repo_name,
202 202 'commit_id': state.rev, 'f_path': state.f_path});
203 203
204 204 $.pjax({
205 205 url: url,
206 206 data: 'annotate=${"1" if c.annotate else "0"}',
207 207 container: '#file_authors',
208 208 push: false,
209 209 timeout: pjaxTimeout
210 210 }).complete(function(){
211 211 $('#show_authors').hide();
212 212 })
213 213 });
214 214
215 215 // load file short history
216 216 $('#file_history_overview').on('click', function(e) {
217 217 e.preventDefault();
218 218 path = state.f_path;
219 219 if (path.indexOf("#") >= 0) {
220 220 path = path.slice(0, path.indexOf("#"));
221 221 }
222 var url = pyroutes.url('changelog_file_home',
222 var url = pyroutes.url('repo_changelog_file',
223 223 {'repo_name': templateContext.repo_name,
224 'revision': state.rev, 'f_path': path, 'limit': 6});
224 'commit_id': state.rev, 'f_path': path, 'limit': 6});
225 225 $('#file_history_container').show();
226 226 $('#file_history_container').html('<div class="file-history-inner">{0}</div>'.format(_gettext('Loading ...')));
227 227
228 228 $.pjax({
229 229 url: url,
230 230 container: '#file_history_container',
231 231 push: false,
232 232 timeout: pjaxTimeout
233 233 })
234 234 });
235 235
236 236 }
237 237 else {
238 238 getFilesMetadata();
239 239
240 240 // fuzzy file filter
241 241 fileBrowserListeners(state.node_list_url, state.url_base);
242 242
243 243 // switch to widget
244 244 select2RefSwitcher('#refs_filter', initialCommitData);
245 245 $('#refs_filter').on('change', function(e) {
246 246 var data = $('#refs_filter').select2('data');
247 247 curState.commit_id = data.raw_id;
248 248 $.pjax({url: data.files_url, container: '#pjax-container', timeout: pjaxTimeout});
249 249 });
250 250
251 251 $("#prev_commit_link").on('click', function(e) {
252 252 var data = $(this).data();
253 253 curState.commit_id = data.commitId;
254 254 });
255 255
256 256 $("#next_commit_link").on('click', function(e) {
257 257 var data = $(this).data();
258 258 curState.commit_id = data.commitId;
259 259 });
260 260
261 261 $('#at_rev').on("keypress", function(e) {
262 262 /* ENTER PRESSED */
263 263 if (e.keyCode === 13) {
264 264 var rev = $('#at_rev').val();
265 265 // explicit reload page here. with pjax entering bad input
266 266 // produces not so nice results
267 267 window.location = pyroutes.url('repo_files',
268 268 {'repo_name': templateContext.repo_name,
269 269 'commit_id': rev, 'f_path': state.f_path});
270 270 }
271 271 });
272 272 }
273 273 };
274 274
275 275 var pjaxTimeout = 5000;
276 276
277 277 $(document).pjax(".pjax-link", "#pjax-container", {
278 278 "fragment": "#pjax-content",
279 279 "maxCacheLength": 1000,
280 280 "timeout": pjaxTimeout
281 281 });
282 282
283 283 // define global back/forward states
284 284 var isPjaxPopState = false;
285 285 $(document).on('pjax:popstate', function() {
286 286 isPjaxPopState = true;
287 287 });
288 288
289 289 $(document).on('pjax:end', function(xhr, options) {
290 290 if (isPjaxPopState) {
291 291 isPjaxPopState = false;
292 292 callbacks();
293 293 _NODEFILTER.resetFilter();
294 294 }
295 295
296 296 // run callback for tracking if defined for google analytics etc.
297 297 // this is used to trigger tracking on pjax
298 298 if (typeof window.rhodecode_statechange_callback !== 'undefined') {
299 299 var state = getState('statechange');
300 300 rhodecode_statechange_callback(state.url, null)
301 301 }
302 302 });
303 303
304 304 $(document).on('pjax:success', function(event, xhr, options) {
305 305 if (event.target.id == "file_history_container") {
306 306 $('#file_history_overview').hide();
307 307 $('#file_history_overview_full').show();
308 308 timeagoActivate();
309 309 } else {
310 310 callbacks();
311 311 }
312 312 });
313 313
314 314 $(document).ready(function() {
315 315 callbacks();
316 316 var search_GET = "${request.GET.get('search','')}";
317 317 if (search_GET == "1") {
318 318 _NODEFILTER.initFilter();
319 319 }
320 320 });
321 321
322 322 </script>
323 323
324 324 </%def>
@@ -1,196 +1,196 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s File Edit') % c.repo_name}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${_('Edit file')} @ ${h.show_id(c.commit)}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_subnav()">
19 19 ${self.repo_menu(active='files')}
20 20 </%def>
21 21
22 22 <%def name="main()">
23 23 <% renderer = h.renderer_from_filename(c.f_path)%>
24 24 <div class="box">
25 25 <div class="title">
26 26 ${self.repo_page_title(c.rhodecode_db_repo)}
27 27 </div>
28 28 <div class="edit-file-title">
29 29 ${self.breadcrumbs()}
30 30 </div>
31 31 <div class="edit-file-fieldset">
32 32 <div class="fieldset">
33 33 <div id="destination-label" class="left-label">
34 34 ${_('Path')}:
35 35 </div>
36 36 <div class="right-content">
37 37 <div id="specify-custom-path-container">
38 38 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path)}</span>
39 39 </div>
40 40 </div>
41 41 </div>
42 42 </div>
43 43
44 44 <div class="table">
45 45 ${h.secure_form(h.route_path('repo_files_update_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', method='POST')}
46 46 <div id="codeblock" class="codeblock" >
47 47 <div class="code-header">
48 48 <div class="stats">
49 49 <i class="icon-file"></i>
50 50 <span class="item">${h.link_to("r%s:%s" % (c.file.commit.idx,h.short_id(c.file.commit.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=c.file.commit.raw_id))}</span>
51 51 <span class="item">${h.format_byte_size_binary(c.file.size)}</span>
52 52 <span class="item last">${c.file.mimetype}</span>
53 53 <div class="buttons">
54 <a class="btn btn-mini" href="${h.url('changelog_file_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path=c.f_path)}">
54 <a class="btn btn-mini" href="${h.route_path('repo_changelog_file',repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">
55 55 <i class="icon-time"></i> ${_('history')}
56 56 </a>
57 57
58 58 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
59 59 % if not c.file.is_binary:
60 60 %if True:
61 61 ${h.link_to(_('source'), h.route_path('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),class_="btn btn-mini")}
62 62 %else:
63 63 ${h.link_to(_('annotation'),h.route_path('repo_files:annotated',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),class_="btn btn-mini")}
64 64 %endif
65 65
66 66 <a class="btn btn-mini" href="${h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
67 67 ${_('raw')}
68 68 </a>
69 69 <a class="btn btn-mini" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
70 70 <i class="icon-archive"></i> ${_('download')}
71 71 </a>
72 72 % endif
73 73 % endif
74 74 </div>
75 75 </div>
76 76 <div class="form">
77 77 <label for="set_mode">${_('Editing file')}:</label>
78 78 ${'%s /' % c.file.dir_path if c.file.dir_path else c.file.dir_path}
79 79 <input id="filename" type="text" name="filename" value="${c.file.name}">
80 80
81 81 ${h.dropdownmenu('set_mode','plain',[('plain',_('plain'))],enable_filter=True)}
82 82 <label for="line_wrap">${_('line wraps')}</label>
83 83 ${h.dropdownmenu('line_wrap', 'off', [('on', _('on')), ('off', _('off')),])}
84 84
85 85 <div id="render_preview" class="btn btn-small preview hidden">${_('Preview')}</div>
86 86 </div>
87 87 </div>
88 88 <div id="editor_container">
89 89 <pre id="editor_pre"></pre>
90 90 <textarea id="editor" name="content" >${h.escape(c.file.content)|n}</textarea>
91 91 <div id="editor_preview" ></div>
92 92 </div>
93 93 </div>
94 94 </div>
95 95
96 96 <div class="edit-file-fieldset">
97 97 <div class="fieldset">
98 98 <div id="commit-message-label" class="commit-message-label left-label">
99 99 ${_('Commit Message')}:
100 100 </div>
101 101 <div class="right-content">
102 102 <div class="message">
103 103 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
104 104 </div>
105 105 </div>
106 106 </div>
107 107 <div class="pull-right">
108 108 ${h.reset('reset',_('Cancel'),class_="btn btn-small")}
109 109 ${h.submit('commit',_('Commit changes'),class_="btn btn-small btn-success")}
110 110 </div>
111 111 </div>
112 112 ${h.end_form()}
113 113 </div>
114 114
115 115 <script type="text/javascript">
116 116 $(document).ready(function(){
117 117 var renderer = "${renderer}";
118 118 var reset_url = "${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.file.path)}";
119 119 var myCodeMirror = initCodeMirror('editor', reset_url);
120 120
121 121 var modes_select = $('#set_mode');
122 122 fillCodeMirrorOptions(modes_select);
123 123
124 124 // try to detect the mode based on the file we edit
125 125 var mimetype = "${c.file.mimetype}";
126 126 var detected_mode = detectCodeMirrorMode(
127 127 "${c.file.name}", mimetype);
128 128
129 129 if(detected_mode){
130 130 setCodeMirrorMode(myCodeMirror, detected_mode);
131 131 $(modes_select).select2("val", mimetype);
132 132 $(modes_select).change();
133 133 setCodeMirrorMode(myCodeMirror, detected_mode);
134 134 }
135 135
136 136 var filename_selector = '#filename';
137 137 var callback = function(filename, mimetype, mode){
138 138 CodeMirrorPreviewEnable(mode);
139 139 };
140 140 // on change of select field set mode
141 141 setCodeMirrorModeFromSelect(
142 142 modes_select, filename_selector, myCodeMirror, callback);
143 143
144 144 // on entering the new filename set mode, from given extension
145 145 setCodeMirrorModeFromInput(
146 146 modes_select, filename_selector, myCodeMirror, callback);
147 147
148 148 // if the file is renderable set line wraps automatically
149 149 if (renderer !== ""){
150 150 var line_wrap = 'on';
151 151 $($('#line_wrap option[value="'+line_wrap+'"]')[0]).attr("selected", "selected");
152 152 setCodeMirrorLineWrap(myCodeMirror, true);
153 153 }
154 154 // on select line wraps change the editor
155 155 $('#line_wrap').on('change', function(e){
156 156 var selected = e.currentTarget;
157 157 var line_wraps = {'on': true, 'off': false}[selected.value];
158 158 setCodeMirrorLineWrap(myCodeMirror, line_wraps)
159 159 });
160 160
161 161 // render preview/edit button
162 162 if (mimetype === 'text/x-rst' || mimetype === 'text/plain') {
163 163 $('#render_preview').removeClass('hidden');
164 164 }
165 165 $('#render_preview').on('click', function(e){
166 166 if($(this).hasClass('preview')){
167 167 $(this).removeClass('preview');
168 168 $(this).html("${_('Edit')}");
169 169 $('#editor_preview').show();
170 170 $(myCodeMirror.getWrapperElement()).hide();
171 171
172 172 var possible_renderer = {
173 173 'rst':'rst',
174 174 'markdown':'markdown',
175 175 'gfm': 'markdown'}[myCodeMirror.getMode().name];
176 176 var _text = myCodeMirror.getValue();
177 177 var _renderer = possible_renderer || DEFAULT_RENDERER;
178 178 var post_data = {'text': _text, 'renderer': _renderer, 'csrf_token': CSRF_TOKEN};
179 179 $('#editor_preview').html(_gettext('Loading ...'));
180 180 var url = pyroutes.url('changeset_comment_preview', {'repo_name': '${c.repo_name}'});
181 181
182 182 ajaxPOST(url, post_data, function(o){
183 183 $('#editor_preview').html(o);
184 184 })
185 185 }
186 186 else{
187 187 $(this).addClass('preview');
188 188 $(this).html("${_('Preview')}");
189 189 $('#editor_preview').hide();
190 190 $(myCodeMirror.getWrapperElement()).show();
191 191 }
192 192 });
193 193
194 194 })
195 195 </script>
196 196 </%def>
@@ -1,92 +1,92 b''
1 1 <%namespace name="sourceblock" file="/codeblocks/source.mako"/>
2 2
3 3 <div id="codeblock" class="codeblock">
4 4 <div class="codeblock-header">
5 5 <div class="stats">
6 6 <span> <strong>${c.file}</strong></span>
7 7 % if c.lf_node:
8 8 <span title="${_('This file is a pointer to large binary file')}"> | ${_('LargeFile')} ${h.format_byte_size_binary(c.lf_node.size)} </span>
9 9 % endif
10 10 <span> | ${c.file.lines()[0]} ${_ungettext('line', 'lines', c.file.lines()[0])}</span>
11 11 <span> | ${h.format_byte_size_binary(c.file.size)}</span>
12 12 <span> | ${c.file.mimetype} </span>
13 13 <span class="item last"> | ${h.get_lexer_for_filenode(c.file).__class__.__name__}</span>
14 14 </div>
15 15 <div class="buttons">
16 16 <a id="file_history_overview" href="#">
17 17 ${_('History')}
18 18 </a>
19 <a id="file_history_overview_full" style="display: none" href="${h.url('changelog_file_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path=c.f_path)}">
19 <a id="file_history_overview_full" style="display: none" href="${h.route_path('repo_changelog_file',repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">
20 20 ${_('Show Full History')}
21 21 </a> |
22 22 %if c.annotate:
23 23 ${h.link_to(_('Source'), h.route_path('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
24 24 %else:
25 25 ${h.link_to(_('Annotation'), h.route_path('repo_files:annotated',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
26 26 %endif
27 27 | ${h.link_to(_('Raw'), h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
28 28 |
29 29 % if c.lf_node:
30 30 <a href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path, _query=dict(lf=1))}">
31 31 ${_('Download largefile')}
32 32 </a>
33 33 % else:
34 34 <a href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
35 35 ${_('Download')}
36 36 </a>
37 37 % endif
38 38
39 39 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
40 40 |
41 41 %if c.on_branch_head and c.branch_or_raw_id and not c.file.is_binary:
42 42 <a href="${h.route_path('repo_files_edit_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _anchor='edit')}">
43 43 ${_('Edit on Branch:{}').format(c.branch_name)}
44 44 </a>
45 45 | <a class="btn-danger btn-link" href="${h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _anchor='edit')}">${_('Delete')}
46 46 </a>
47 47 %elif c.on_branch_head and c.branch_or_raw_id and c.file.is_binary:
48 48 ${h.link_to(_('Edit'), '#', class_="btn btn-link disabled tooltip", title=_('Editing binary files not allowed'))}
49 49 | ${h.link_to(_('Delete'), h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _anchor='edit'),class_="btn-danger btn-link")}
50 50 %else:
51 51 ${h.link_to(_('Edit'), '#', class_="btn btn-link disabled tooltip", title=_('Editing files allowed only when on branch head commit'))}
52 52 | ${h.link_to(_('Delete'), '#', class_="btn btn-danger btn-link disabled tooltip", title=_('Deleting files allowed only when on branch head commit'))}
53 53 %endif
54 54 %endif
55 55 </div>
56 56 </div>
57 57 <div id="file_history_container"></div>
58 58 <div class="code-body">
59 59 %if c.file.is_binary:
60 60 <% rendered_binary = h.render_binary(c.repo_name, c.file)%>
61 61 % if rendered_binary:
62 62 ${rendered_binary}
63 63 % else:
64 64 <div>
65 65 ${_('Binary file (%s)') % c.file.mimetype}
66 66 </div>
67 67 % endif
68 68 %else:
69 69 % if c.file.size < c.visual.cut_off_limit_file:
70 70 %if c.renderer and not c.annotate:
71 71 ${h.render(c.file.content, renderer=c.renderer, relative_url=h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
72 72 %else:
73 73 <table class="cb codehilite">
74 74 %if c.annotate:
75 75 <% color_hasher = h.color_hasher() %>
76 76 %for annotation, lines in c.annotated_lines:
77 77 ${sourceblock.render_annotation_lines(annotation, lines, color_hasher)}
78 78 %endfor
79 79 %else:
80 80 %for line_num, tokens in enumerate(c.lines, 1):
81 81 ${sourceblock.render_line(line_num, tokens)}
82 82 %endfor
83 83 %endif
84 84 </table>
85 85 %endif
86 86 %else:
87 87 ${_('File size {} is bigger then allowed limit {}. ').format(h.format_byte_size_binary(c.file.size), h.format_byte_size_binary(c.visual.cut_off_limit_file))} ${h.link_to(_('Show as raw'),
88 88 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
89 89 %endif
90 90 %endif
91 91 </div>
92 92 </div> No newline at end of file
@@ -1,860 +1,860 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 <span id="pr-title">
13 13 ${c.pull_request.title}
14 14 %if c.pull_request.is_closed():
15 15 (${_('Closed')})
16 16 %endif
17 17 </span>
18 18 <div id="pr-title-edit" class="input" style="display: none;">
19 19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 20 </div>
21 21 </%def>
22 22
23 23 <%def name="menu_bar_nav()">
24 24 ${self.menu_items(active='repositories')}
25 25 </%def>
26 26
27 27 <%def name="menu_bar_subnav()">
28 28 ${self.repo_menu(active='showpullrequest')}
29 29 </%def>
30 30
31 31 <%def name="main()">
32 32
33 33 <script type="text/javascript">
34 34 // TODO: marcink switch this to pyroutes
35 35 AJAX_COMMENT_DELETE_URL = "${h.url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
36 36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 37 </script>
38 38 <div class="box">
39 39
40 40 <div class="title">
41 41 ${self.repo_page_title(c.rhodecode_db_repo)}
42 42 </div>
43 43
44 44 ${self.breadcrumbs()}
45 45
46 46 <div class="box pr-summary">
47 47
48 48 <div class="summary-details block-left">
49 49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 50 <div class="pr-details-title">
51 51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 52 %if c.allowed_to_update:
53 53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 54 % if c.allowed_to_delete:
55 55 ${h.secure_form(h.url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
56 56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 58 ${h.end_form()}
59 59 % else:
60 60 ${_('Delete')}
61 61 % endif
62 62 </div>
63 63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 65 %endif
66 66 </div>
67 67
68 68 <div id="summary" class="fields pr-details-content">
69 69 <div class="field">
70 70 <div class="label-summary">
71 71 <label>${_('Source')}:</label>
72 72 </div>
73 73 <div class="input">
74 74 <div class="pr-origininfo">
75 75 ## branch link is only valid if it is a branch
76 76 <span class="tag">
77 77 %if c.pull_request.source_ref_parts.type == 'branch':
78 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
78 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 79 %else:
80 80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 81 %endif
82 82 </span>
83 83 <span class="clone-url">
84 84 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 85 </span>
86 86 <br/>
87 87 % if c.ancestor_commit:
88 88 ${_('Common ancestor')}:
89 89 <code><a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 90 % endif
91 91 </div>
92 92 <div class="pr-pullinfo">
93 93 %if h.is_hg(c.pull_request.source_repo):
94 94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
95 95 %elif h.is_git(c.pull_request.source_repo):
96 96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
97 97 %endif
98 98 </div>
99 99 </div>
100 100 </div>
101 101 <div class="field">
102 102 <div class="label-summary">
103 103 <label>${_('Target')}:</label>
104 104 </div>
105 105 <div class="input">
106 106 <div class="pr-targetinfo">
107 107 ## branch link is only valid if it is a branch
108 108 <span class="tag">
109 109 %if c.pull_request.target_ref_parts.type == 'branch':
110 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
110 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
111 111 %else:
112 112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
113 113 %endif
114 114 </span>
115 115 <span class="clone-url">
116 116 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
117 117 </span>
118 118 </div>
119 119 </div>
120 120 </div>
121 121
122 122 ## Link to the shadow repository.
123 123 <div class="field">
124 124 <div class="label-summary">
125 125 <label>${_('Merge')}:</label>
126 126 </div>
127 127 <div class="input">
128 128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
129 129 <div class="pr-mergeinfo">
130 130 %if h.is_hg(c.pull_request.target_repo):
131 131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
132 132 %elif h.is_git(c.pull_request.target_repo):
133 133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
134 134 %endif
135 135 </div>
136 136 % else:
137 137 <div class="">
138 138 ${_('Shadow repository data not available')}.
139 139 </div>
140 140 % endif
141 141 </div>
142 142 </div>
143 143
144 144 <div class="field">
145 145 <div class="label-summary">
146 146 <label>${_('Review')}:</label>
147 147 </div>
148 148 <div class="input">
149 149 %if c.pull_request_review_status:
150 150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
151 151 <span class="changeset-status-lbl tooltip">
152 152 %if c.pull_request.is_closed():
153 153 ${_('Closed')},
154 154 %endif
155 155 ${h.commit_status_lbl(c.pull_request_review_status)}
156 156 </span>
157 157 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
158 158 %endif
159 159 </div>
160 160 </div>
161 161 <div class="field">
162 162 <div class="pr-description-label label-summary">
163 163 <label>${_('Description')}:</label>
164 164 </div>
165 165 <div id="pr-desc" class="input">
166 166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
167 167 </div>
168 168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
169 169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
170 170 </div>
171 171 </div>
172 172
173 173 <div class="field">
174 174 <div class="label-summary">
175 175 <label>${_('Versions')}:</label>
176 176 </div>
177 177
178 178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
179 179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
180 180
181 181 <div class="pr-versions">
182 182 % if c.show_version_changes:
183 183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
184 184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
185 185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
186 186 data-toggle-on="${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
187 187 data-toggle-off="${_('Hide all versions of this pull request')}">
188 188 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
189 189 </a>
190 190 <table>
191 191 ## SHOW ALL VERSIONS OF PR
192 192 <% ver_pr = None %>
193 193
194 194 % for data in reversed(list(enumerate(c.versions, 1))):
195 195 <% ver_pos = data[0] %>
196 196 <% ver = data[1] %>
197 197 <% ver_pr = ver.pull_request_version_id %>
198 198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
199 199
200 200 <tr class="version-pr" style="display: ${display_row}">
201 201 <td>
202 202 <code>
203 203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
204 204 </code>
205 205 </td>
206 206 <td>
207 207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
208 208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
209 209 </td>
210 210 <td>
211 211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
212 212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
213 213 </div>
214 214 </td>
215 215 <td>
216 216 % if c.at_version_num != ver_pr:
217 217 <i class="icon-comment"></i>
218 218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
219 219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
220 220 </code>
221 221 % endif
222 222 </td>
223 223 <td>
224 224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
225 225 </td>
226 226 <td>
227 227 ${h.age_component(ver.updated_on, time_is_local=True)}
228 228 </td>
229 229 </tr>
230 230 % endfor
231 231
232 232 <tr>
233 233 <td colspan="6">
234 234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
235 235 data-label-text-locked="${_('select versions to show changes')}"
236 236 data-label-text-diff="${_('show changes between versions')}"
237 237 data-label-text-show="${_('show pull request for this version')}"
238 238 >
239 239 ${_('select versions to show changes')}
240 240 </button>
241 241 </td>
242 242 </tr>
243 243
244 244 ## show comment/inline comments summary
245 245 <%def name="comments_summary()">
246 246 <tr>
247 247 <td colspan="6" class="comments-summary-td">
248 248
249 249 % if c.at_version:
250 250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
251 251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
252 252 ${_('Comments at this version')}:
253 253 % else:
254 254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
255 255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
256 256 ${_('Comments for this pull request')}:
257 257 % endif
258 258
259 259
260 260 %if general_comm_count_ver:
261 261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
262 262 %else:
263 263 ${_("%d General ") % general_comm_count_ver}
264 264 %endif
265 265
266 266 %if inline_comm_count_ver:
267 267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
268 268 %else:
269 269 , ${_("%d Inline") % inline_comm_count_ver}
270 270 %endif
271 271
272 272 %if outdated_comm_count_ver:
273 273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
274 274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
275 275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
276 276 %else:
277 277 , ${_("%d Outdated") % outdated_comm_count_ver}
278 278 %endif
279 279 </td>
280 280 </tr>
281 281 </%def>
282 282 ${comments_summary()}
283 283 </table>
284 284 % else:
285 285 <div class="input">
286 286 ${_('Pull request versions not available')}.
287 287 </div>
288 288 <div>
289 289 <table>
290 290 ${comments_summary()}
291 291 </table>
292 292 </div>
293 293 % endif
294 294 </div>
295 295 </div>
296 296
297 297 <div id="pr-save" class="field" style="display: none;">
298 298 <div class="label-summary"></div>
299 299 <div class="input">
300 300 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
301 301 </div>
302 302 </div>
303 303 </div>
304 304 </div>
305 305 <div>
306 306 ## AUTHOR
307 307 <div class="reviewers-title block-right">
308 308 <div class="pr-details-title">
309 309 ${_('Author of this pull request')}
310 310 </div>
311 311 </div>
312 312 <div class="block-right pr-details-content reviewers">
313 313 <ul class="group_members">
314 314 <li>
315 315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
316 316 </li>
317 317 </ul>
318 318 </div>
319 319
320 320 ## REVIEW RULES
321 321 <div id="review_rules" style="display: none" class="reviewers-title block-right">
322 322 <div class="pr-details-title">
323 323 ${_('Reviewer rules')}
324 324 %if c.allowed_to_update:
325 325 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
326 326 %endif
327 327 </div>
328 328 <div class="pr-reviewer-rules">
329 329 ## review rules will be appended here, by default reviewers logic
330 330 </div>
331 331 <input id="review_data" type="hidden" name="review_data" value="">
332 332 </div>
333 333
334 334 ## REVIEWERS
335 335 <div class="reviewers-title block-right">
336 336 <div class="pr-details-title">
337 337 ${_('Pull request reviewers')}
338 338 %if c.allowed_to_update:
339 339 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
340 340 %endif
341 341 </div>
342 342 </div>
343 343 <div id="reviewers" class="block-right pr-details-content reviewers">
344 344 ## members goes here !
345 345 <input type="hidden" name="__start__" value="review_members:sequence">
346 346 <ul id="review_members" class="group_members">
347 347 %for member,reasons,mandatory,status in c.pull_request_reviewers:
348 348 <li id="reviewer_${member.user_id}" class="reviewer_entry">
349 349 <div class="reviewers_member">
350 350 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
351 351 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
352 352 </div>
353 353 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
354 354 ${self.gravatar_with_user(member.email, 16)}
355 355 </div>
356 356 <input type="hidden" name="__start__" value="reviewer:mapping">
357 357 <input type="hidden" name="__start__" value="reasons:sequence">
358 358 %for reason in reasons:
359 359 <div class="reviewer_reason">- ${reason}</div>
360 360 <input type="hidden" name="reason" value="${reason}">
361 361
362 362 %endfor
363 363 <input type="hidden" name="__end__" value="reasons:sequence">
364 364 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
365 365 <input type="hidden" name="mandatory" value="${mandatory}"/>
366 366 <input type="hidden" name="__end__" value="reviewer:mapping">
367 367 % if mandatory:
368 368 <div class="reviewer_member_mandatory_remove">
369 369 <i class="icon-remove-sign"></i>
370 370 </div>
371 371 <div class="reviewer_member_mandatory">
372 372 <i class="icon-lock" title="${h.tooltip(_('Mandatory reviewer'))}"></i>
373 373 </div>
374 374 % else:
375 375 %if c.allowed_to_update:
376 376 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
377 377 <i class="icon-remove-sign" ></i>
378 378 </div>
379 379 %endif
380 380 % endif
381 381 </div>
382 382 </li>
383 383 %endfor
384 384 </ul>
385 385 <input type="hidden" name="__end__" value="review_members:sequence">
386 386
387 387 %if not c.pull_request.is_closed():
388 388 <div id="add_reviewer" class="ac" style="display: none;">
389 389 %if c.allowed_to_update:
390 390 % if not c.forbid_adding_reviewers:
391 391 <div id="add_reviewer_input" class="reviewer_ac">
392 392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
393 393 <div id="reviewers_container"></div>
394 394 </div>
395 395 % endif
396 396 <div class="pull-right">
397 397 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
398 398 </div>
399 399 %endif
400 400 </div>
401 401 %endif
402 402 </div>
403 403 </div>
404 404 </div>
405 405 <div class="box">
406 406 ##DIFF
407 407 <div class="table" >
408 408 <div id="changeset_compare_view_content">
409 409 ##CS
410 410 % if c.missing_requirements:
411 411 <div class="box">
412 412 <div class="alert alert-warning">
413 413 <div>
414 414 <strong>${_('Missing requirements:')}</strong>
415 415 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
416 416 </div>
417 417 </div>
418 418 </div>
419 419 % elif c.missing_commits:
420 420 <div class="box">
421 421 <div class="alert alert-warning">
422 422 <div>
423 423 <strong>${_('Missing commits')}:</strong>
424 424 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
425 425 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
426 426 </div>
427 427 </div>
428 428 </div>
429 429 % endif
430 430
431 431 <div class="compare_view_commits_title">
432 432 % if not c.compare_mode:
433 433
434 434 % if c.at_version_pos:
435 435 <h4>
436 436 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
437 437 </h4>
438 438 % endif
439 439
440 440 <div class="pull-left">
441 441 <div class="btn-group">
442 442 <a
443 443 class="btn"
444 444 href="#"
445 445 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
446 446 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
447 447 </a>
448 448 <a
449 449 class="btn"
450 450 href="#"
451 451 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
452 452 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
453 453 </a>
454 454 </div>
455 455 </div>
456 456
457 457 <div class="pull-right">
458 458 % if c.allowed_to_update and not c.pull_request.is_closed():
459 459 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
460 460 % else:
461 461 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
462 462 % endif
463 463
464 464 </div>
465 465 % endif
466 466 </div>
467 467
468 468 % if not c.missing_commits:
469 469 % if c.compare_mode:
470 470 % if c.at_version:
471 471 <h4>
472 472 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
473 473 </h4>
474 474
475 475 <div class="subtitle-compare">
476 476 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
477 477 </div>
478 478
479 479 <div class="container">
480 480 <table class="rctable compare_view_commits">
481 481 <tr>
482 482 <th></th>
483 483 <th>${_('Time')}</th>
484 484 <th>${_('Author')}</th>
485 485 <th>${_('Commit')}</th>
486 486 <th></th>
487 487 <th>${_('Description')}</th>
488 488 </tr>
489 489
490 490 % for c_type, commit in c.commit_changes:
491 491 % if c_type in ['a', 'r']:
492 492 <%
493 493 if c_type == 'a':
494 494 cc_title = _('Commit added in displayed changes')
495 495 elif c_type == 'r':
496 496 cc_title = _('Commit removed in displayed changes')
497 497 else:
498 498 cc_title = ''
499 499 %>
500 500 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
501 501 <td>
502 502 <div class="commit-change-indicator color-${c_type}-border">
503 503 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
504 504 ${c_type.upper()}
505 505 </div>
506 506 </div>
507 507 </td>
508 508 <td class="td-time">
509 509 ${h.age_component(commit.date)}
510 510 </td>
511 511 <td class="td-user">
512 512 ${base.gravatar_with_user(commit.author, 16)}
513 513 </td>
514 514 <td class="td-hash">
515 515 <code>
516 516 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
517 517 r${commit.revision}:${h.short_id(commit.raw_id)}
518 518 </a>
519 519 ${h.hidden('revisions', commit.raw_id)}
520 520 </code>
521 521 </td>
522 522 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
523 523 <div class="show_more_col">
524 524 <i class="show_more"></i>
525 525 </div>
526 526 </td>
527 527 <td class="mid td-description">
528 528 <div class="log-container truncate-wrap">
529 529 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
530 530 ${h.urlify_commit_message(commit.message, c.repo_name)}
531 531 </div>
532 532 </div>
533 533 </td>
534 534 </tr>
535 535 % endif
536 536 % endfor
537 537 </table>
538 538 </div>
539 539
540 540 <script>
541 541 $('.expand_commit').on('click',function(e){
542 542 var target_expand = $(this);
543 543 var cid = target_expand.data('commitId');
544 544
545 545 if (target_expand.hasClass('open')){
546 546 $('#c-'+cid).css({
547 547 'height': '1.5em',
548 548 'white-space': 'nowrap',
549 549 'text-overflow': 'ellipsis',
550 550 'overflow':'hidden'
551 551 });
552 552 target_expand.removeClass('open');
553 553 }
554 554 else {
555 555 $('#c-'+cid).css({
556 556 'height': 'auto',
557 557 'white-space': 'pre-line',
558 558 'text-overflow': 'initial',
559 559 'overflow':'visible'
560 560 });
561 561 target_expand.addClass('open');
562 562 }
563 563 });
564 564 </script>
565 565
566 566 % endif
567 567
568 568 % else:
569 569 <%include file="/compare/compare_commits.mako" />
570 570 % endif
571 571
572 572 <div class="cs_files">
573 573 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
574 574 ${cbdiffs.render_diffset_menu()}
575 575 ${cbdiffs.render_diffset(
576 576 c.diffset, use_comments=True,
577 577 collapse_when_files_over=30,
578 578 disable_new_comments=not c.allowed_to_comment,
579 579 deleted_files_comments=c.deleted_files_comments)}
580 580 </div>
581 581 % else:
582 582 ## skipping commits we need to clear the view for missing commits
583 583 <div style="clear:both;"></div>
584 584 % endif
585 585
586 586 </div>
587 587 </div>
588 588
589 589 ## template for inline comment form
590 590 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
591 591
592 592 ## render general comments
593 593
594 594 <div id="comment-tr-show">
595 595 <div class="comment">
596 596 % if general_outdated_comm_count_ver:
597 597 <div class="meta">
598 598 % if general_outdated_comm_count_ver == 1:
599 599 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
600 600 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
601 601 % else:
602 602 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
603 603 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
604 604 % endif
605 605 </div>
606 606 % endif
607 607 </div>
608 608 </div>
609 609
610 610 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
611 611
612 612 % if not c.pull_request.is_closed():
613 613 ## merge status, and merge action
614 614 <div class="pull-request-merge">
615 615 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
616 616 </div>
617 617
618 618 ## main comment form and it status
619 619 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
620 620 pull_request_id=c.pull_request.pull_request_id),
621 621 c.pull_request_review_status,
622 622 is_pull_request=True, change_status=c.allowed_to_change_status)}
623 623 %endif
624 624
625 625 <script type="text/javascript">
626 626 if (location.hash) {
627 627 var result = splitDelimitedHash(location.hash);
628 628 var line = $('html').find(result.loc);
629 629 // show hidden comments if we use location.hash
630 630 if (line.hasClass('comment-general')) {
631 631 $(line).show();
632 632 } else if (line.hasClass('comment-inline')) {
633 633 $(line).show();
634 634 var $cb = $(line).closest('.cb');
635 635 $cb.removeClass('cb-collapsed')
636 636 }
637 637 if (line.length > 0){
638 638 offsetScroll(line, 70);
639 639 }
640 640 }
641 641
642 642 versionController = new VersionController();
643 643 versionController.init();
644 644
645 645 reviewersController = new ReviewersController();
646 646
647 647 $(function(){
648 648
649 649 // custom code mirror
650 650 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
651 651
652 652 var PRDetails = {
653 653 editButton: $('#open_edit_pullrequest'),
654 654 closeButton: $('#close_edit_pullrequest'),
655 655 deleteButton: $('#delete_pullrequest'),
656 656 viewFields: $('#pr-desc, #pr-title'),
657 657 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
658 658
659 659 init: function() {
660 660 var that = this;
661 661 this.editButton.on('click', function(e) { that.edit(); });
662 662 this.closeButton.on('click', function(e) { that.view(); });
663 663 },
664 664
665 665 edit: function(event) {
666 666 this.viewFields.hide();
667 667 this.editButton.hide();
668 668 this.deleteButton.hide();
669 669 this.closeButton.show();
670 670 this.editFields.show();
671 671 codeMirrorInstance.refresh();
672 672 },
673 673
674 674 view: function(event) {
675 675 this.editButton.show();
676 676 this.deleteButton.show();
677 677 this.editFields.hide();
678 678 this.closeButton.hide();
679 679 this.viewFields.show();
680 680 }
681 681 };
682 682
683 683 var ReviewersPanel = {
684 684 editButton: $('#open_edit_reviewers'),
685 685 closeButton: $('#close_edit_reviewers'),
686 686 addButton: $('#add_reviewer'),
687 687 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove,.reviewer_member_mandatory'),
688 688
689 689 init: function() {
690 690 var self = this;
691 691 this.editButton.on('click', function(e) { self.edit(); });
692 692 this.closeButton.on('click', function(e) { self.close(); });
693 693 },
694 694
695 695 edit: function(event) {
696 696 this.editButton.hide();
697 697 this.closeButton.show();
698 698 this.addButton.show();
699 699 this.removeButtons.css('visibility', 'visible');
700 700 // review rules
701 701 reviewersController.loadReviewRules(
702 702 ${c.pull_request.reviewer_data_json | n});
703 703 },
704 704
705 705 close: function(event) {
706 706 this.editButton.show();
707 707 this.closeButton.hide();
708 708 this.addButton.hide();
709 709 this.removeButtons.css('visibility', 'hidden');
710 710 // hide review rules
711 711 reviewersController.hideReviewRules()
712 712 }
713 713 };
714 714
715 715 PRDetails.init();
716 716 ReviewersPanel.init();
717 717
718 718 showOutdated = function(self){
719 719 $('.comment-inline.comment-outdated').show();
720 720 $('.filediff-outdated').show();
721 721 $('.showOutdatedComments').hide();
722 722 $('.hideOutdatedComments').show();
723 723 };
724 724
725 725 hideOutdated = function(self){
726 726 $('.comment-inline.comment-outdated').hide();
727 727 $('.filediff-outdated').hide();
728 728 $('.hideOutdatedComments').hide();
729 729 $('.showOutdatedComments').show();
730 730 };
731 731
732 732 refreshMergeChecks = function(){
733 733 var loadUrl = "${h.url.current(merge_checks=1)}";
734 734 $('.pull-request-merge').css('opacity', 0.3);
735 735 $('.action-buttons-extra').css('opacity', 0.3);
736 736
737 737 $('.pull-request-merge').load(
738 738 loadUrl, function() {
739 739 $('.pull-request-merge').css('opacity', 1);
740 740
741 741 $('.action-buttons-extra').css('opacity', 1);
742 742 injectCloseAction();
743 743 }
744 744 );
745 745 };
746 746
747 747 injectCloseAction = function() {
748 748 var closeAction = $('#close-pull-request-action').html();
749 749 var $actionButtons = $('.action-buttons-extra');
750 750 // clear the action before
751 751 $actionButtons.html("");
752 752 $actionButtons.html(closeAction);
753 753 };
754 754
755 755 closePullRequest = function (status) {
756 756 // inject closing flag
757 757 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
758 758 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
759 759 $(generalCommentForm.submitForm).submit();
760 760 };
761 761
762 762 $('#show-outdated-comments').on('click', function(e){
763 763 var button = $(this);
764 764 var outdated = $('.comment-outdated');
765 765
766 766 if (button.html() === "(Show)") {
767 767 button.html("(Hide)");
768 768 outdated.show();
769 769 } else {
770 770 button.html("(Show)");
771 771 outdated.hide();
772 772 }
773 773 });
774 774
775 775 $('.show-inline-comments').on('change', function(e){
776 776 var show = 'none';
777 777 var target = e.currentTarget;
778 778 if(target.checked){
779 779 show = ''
780 780 }
781 781 var boxid = $(target).attr('id_for');
782 782 var comments = $('#{0} .inline-comments'.format(boxid));
783 783 var fn_display = function(idx){
784 784 $(this).css('display', show);
785 785 };
786 786 $(comments).each(fn_display);
787 787 var btns = $('#{0} .inline-comments-button'.format(boxid));
788 788 $(btns).each(fn_display);
789 789 });
790 790
791 791 $('#merge_pull_request_form').submit(function() {
792 792 if (!$('#merge_pull_request').attr('disabled')) {
793 793 $('#merge_pull_request').attr('disabled', 'disabled');
794 794 }
795 795 return true;
796 796 });
797 797
798 798 $('#edit_pull_request').on('click', function(e){
799 799 var title = $('#pr-title-input').val();
800 800 var description = codeMirrorInstance.getValue();
801 801 editPullRequest(
802 802 "${c.repo_name}", "${c.pull_request.pull_request_id}",
803 803 title, description);
804 804 });
805 805
806 806 $('#update_pull_request').on('click', function(e){
807 807 $(this).attr('disabled', 'disabled');
808 808 $(this).addClass('disabled');
809 809 $(this).html(_gettext('Saving...'));
810 810 reviewersController.updateReviewers(
811 811 "${c.repo_name}", "${c.pull_request.pull_request_id}");
812 812 });
813 813
814 814 $('#update_commits').on('click', function(e){
815 815 var isDisabled = !$(e.currentTarget).attr('disabled');
816 816 $(e.currentTarget).attr('disabled', 'disabled');
817 817 $(e.currentTarget).addClass('disabled');
818 818 $(e.currentTarget).removeClass('btn-primary');
819 819 $(e.currentTarget).text(_gettext('Updating...'));
820 820 if(isDisabled){
821 821 updateCommits(
822 822 "${c.repo_name}", "${c.pull_request.pull_request_id}");
823 823 }
824 824 });
825 825 // fixing issue with caches on firefox
826 826 $('#update_commits').removeAttr("disabled");
827 827
828 828 $('.show-inline-comments').on('click', function(e){
829 829 var boxid = $(this).attr('data-comment-id');
830 830 var button = $(this);
831 831
832 832 if(button.hasClass("comments-visible")) {
833 833 $('#{0} .inline-comments'.format(boxid)).each(function(index){
834 834 $(this).hide();
835 835 });
836 836 button.removeClass("comments-visible");
837 837 } else {
838 838 $('#{0} .inline-comments'.format(boxid)).each(function(index){
839 839 $(this).show();
840 840 });
841 841 button.addClass("comments-visible");
842 842 }
843 843 });
844 844
845 845 // register submit callback on commentForm form to track TODOs
846 846 window.commentFormGlobalSubmitSuccessCallback = function(){
847 847 refreshMergeChecks();
848 848 };
849 849 // initial injection
850 850 injectCloseAction();
851 851
852 852 ReviewerAutoComplete('#user');
853 853
854 854 })
855 855 </script>
856 856
857 857 </div>
858 858 </div>
859 859
860 860 </%def>
@@ -1,100 +1,100 b''
1 1 <%def name="highlight_text_file(terms, text, url, line_context=3,
2 2 max_lines=10,
3 3 mimetype=None, filepath=None)">
4 4 <%
5 5 lines = text.split('\n')
6 6 lines_of_interest = set()
7 7 matching_lines = h.get_matching_line_offsets(lines, terms)
8 8 shown_matching_lines = 0
9 9
10 10 for line_number in matching_lines:
11 11 if len(lines_of_interest) < max_lines:
12 12 lines_of_interest |= set(range(
13 13 max(line_number - line_context, 0),
14 14 min(line_number + line_context, len(lines) + 1)))
15 15 shown_matching_lines += 1
16 16
17 17 %>
18 18 ${h.code_highlight(
19 19 text,
20 20 h.get_lexer_safe(
21 21 mimetype=mimetype,
22 22 filepath=filepath,
23 23 ),
24 24 h.SearchContentCodeHtmlFormatter(
25 25 linenos=True,
26 26 cssclass="code-highlight",
27 27 url=url,
28 28 query_terms=terms,
29 29 only_line_numbers=lines_of_interest
30 30 ))|n}
31 31
32 32 %if len(matching_lines) > shown_matching_lines:
33 33 <a href="${url}">
34 34 ${len(matching_lines) - shown_matching_lines} ${_('more matches in this file')}
35 35 </a>
36 36 %endif
37 37 </%def>
38 38
39 39 <div class="search-results">
40 40 %for entry in c.formatted_results:
41 41 ## search results are additionally filtered, and this check is just a safe gate
42 42 % if h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results content check'):
43 43 <div id="codeblock" class="codeblock">
44 44 <div class="codeblock-header">
45 45 <h2>
46 46 %if h.get_repo_type_by_name(entry.get('repository')) == 'hg':
47 47 <i class="icon-hg"></i>
48 48 %elif h.get_repo_type_by_name(entry.get('repository')) == 'git':
49 49 <i class="icon-git"></i>
50 50 %elif h.get_repo_type_by_name(entry.get('repository')) == 'svn':
51 51 <i class="icon-svn"></i>
52 52 %endif
53 53 ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
54 54 </h2>
55 55 <div class="stats">
56 56 ${h.link_to(h.literal(entry['f_path']), h.route_path('repo_files',repo_name=entry['repository'],commit_id=entry.get('commit_id', 'tip'),f_path=entry['f_path']))}
57 57 %if entry.get('lines'):
58 58 | ${entry.get('lines', 0.)} ${_ungettext('line', 'lines', entry.get('lines', 0.))}
59 59 %endif
60 60 %if entry.get('size'):
61 61 | ${h.format_byte_size_binary(entry['size'])}
62 62 %endif
63 63 %if entry.get('mimetype'):
64 64 | ${entry.get('mimetype', "unknown mimetype")}
65 65 %endif
66 66 </div>
67 67 <div class="buttons">
68 <a id="file_history_overview_full" href="${h.url('changelog_file_home',repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}">
68 <a id="file_history_overview_full" href="${h.route_path('repo_changelog_file',repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}">
69 69 ${_('Show Full History')}
70 70 </a>
71 71 | ${h.link_to(_('Annotation'), h.route_path('repo_files:annotated', repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
72 72 | ${h.link_to(_('Raw'), h.route_path('repo_file_raw', repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
73 73 | ${h.link_to(_('Download'), h.route_path('repo_file_download',repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
74 74 </div>
75 75 </div>
76 76 <div class="code-body search-code-body">
77 77 ${highlight_text_file(c.cur_query, entry['content'],
78 78 url=h.route_path('repo_files',repo_name=entry['repository'],commit_id=entry.get('commit_id', 'tip'),f_path=entry['f_path']),
79 79 mimetype=entry.get('mimetype'), filepath=entry.get('path'))}
80 80 </div>
81 81 </div>
82 82 % endif
83 83 %endfor
84 84 </div>
85 85 %if c.cur_query and c.formatted_results:
86 86 <div class="pagination-wh pagination-left" >
87 87 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
88 88 </div>
89 89 %endif
90 90
91 91 %if c.cur_query:
92 92 <script type="text/javascript">
93 93 $(function(){
94 94 $(".code").mark(
95 95 '${' '.join(h.normalize_text_for_matching(c.cur_query).split())}',
96 96 {"className": 'match',
97 97 });
98 98 })
99 99 </script>
100 100 %endif No newline at end of file
@@ -1,202 +1,202 b''
1 1 <%def name="refs_counters(branches, closed_branches, tags, bookmarks)">
2 2 <span class="branchtag tag">
3 3 <a href="${h.route_path('branches_home',repo_name=c.repo_name)}" class="childs">
4 4 <i class="icon-branch"></i>${_ungettext(
5 5 '%(num)s Branch','%(num)s Branches', len(branches)) % {'num': len(branches)}}</a>
6 6 </span>
7 7
8 8 %if closed_branches:
9 9 <span class="branchtag tag">
10 10 <a href="${h.route_path('branches_home',repo_name=c.repo_name)}" class="childs">
11 11 <i class="icon-branch"></i>${_ungettext(
12 12 '%(num)s Closed Branch', '%(num)s Closed Branches', len(closed_branches)) % {'num': len(closed_branches)}}</a>
13 13 </span>
14 14 %endif
15 15
16 16 <span class="tagtag tag">
17 17 <a href="${h.route_path('tags_home',repo_name=c.repo_name)}" class="childs">
18 18 <i class="icon-tag"></i>${_ungettext(
19 19 '%(num)s Tag', '%(num)s Tags', len(tags)) % {'num': len(tags)}}</a>
20 20 </span>
21 21
22 22 %if bookmarks:
23 23 <span class="booktag tag">
24 24 <a href="${h.route_path('bookmarks_home',repo_name=c.repo_name)}" class="childs">
25 25 <i class="icon-bookmark"></i>${_ungettext(
26 26 '%(num)s Bookmark', '%(num)s Bookmarks', len(bookmarks)) % {'num': len(bookmarks)}}</a>
27 27 </span>
28 28 %endif
29 29 </%def>
30 30
31 31 <%def name="summary_detail(breadcrumbs_links, show_downloads=True)">
32 32 <% summary = lambda n:{False:'summary-short'}.get(n) %>
33 33
34 34 <div id="summary-menu-stats" class="summary-detail">
35 35 <div class="summary-detail-header">
36 36 <div class="breadcrumbs files_location">
37 37 <h4>
38 38 ${breadcrumbs_links}
39 39 </h4>
40 40 </div>
41 41 <div id="summary_details_expand" class="btn-collapse" data-toggle="summary-details">
42 42 ${_('Show More')}
43 43 </div>
44 44 </div>
45 45
46 46 <div class="fieldset">
47 47 %if h.is_svn_without_proxy(c.rhodecode_db_repo):
48 48 <div class="left-label disabled">
49 49 ${_('Read-only url')}:
50 50 </div>
51 51 <div class="right-content disabled">
52 52 <input type="text" class="input-monospace" id="clone_url" disabled value="${c.clone_repo_url}"/>
53 53 <input type="text" class="input-monospace" id="clone_url_id" disabled value="${c.clone_repo_url_id}" style="display: none;"/>
54 54 <a id="clone_by_name" class="clone" style="display: none;">${_('Show by Name')}</a>
55 55 <a id="clone_by_id" class="clone">${_('Show by ID')}</a>
56 56 <p class="help-block">${_('SVN Protocol is disabled. To enable it, see the')} <a href="${h.route_url('enterprise_svn_setup')}" target="_blank">${_('documentation here')}</a>.</p>
57 57 </div>
58 58 %else:
59 59 <div class="left-label">
60 60 ${_('Clone url')}:
61 61 </div>
62 62 <div class="right-content">
63 63 <input type="text" class="input-monospace" id="clone_url" readonly="readonly" value="${c.clone_repo_url}"/>
64 64 <input type="text" class="input-monospace" id="clone_url_id" readonly="readonly" value="${c.clone_repo_url_id}" style="display: none;"/>
65 65 <a id="clone_by_name" class="clone" style="display: none;">${_('Show by Name')}</a>
66 66 <a id="clone_by_id" class="clone">${_('Show by ID')}</a>
67 67 </div>
68 68 %endif
69 69 </div>
70 70
71 71 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
72 72 <div class="left-label">
73 73 ${_('Description')}:
74 74 </div>
75 75 <div class="right-content">
76 76 %if c.visual.stylify_metatags:
77 77 <div class="input ${summary(c.show_stats)} desc">${h.urlify_text(h.escaped_stylize(c.rhodecode_db_repo.description))}</div>
78 78 %else:
79 79 <div class="input ${summary(c.show_stats)} desc">${h.urlify_text(h.html_escape(c.rhodecode_db_repo.description))}</div>
80 80 %endif
81 81 </div>
82 82 </div>
83 83
84 84 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
85 85 <div class="left-label">
86 86 ${_('Information')}:
87 87 </div>
88 88 <div class="right-content">
89 89
90 90 <div class="repo-size">
91 91 <% commit_rev = c.rhodecode_db_repo.changeset_cache.get('revision') %>
92 92
93 93 ## commits
94 94 % if commit_rev == -1:
95 95 ${_ungettext('%(num)s Commit', '%(num)s Commits', 0) % {'num': 0}},
96 96 % else:
97 <a href="${h.url('changelog_home', repo_name=c.repo_name)}">
97 <a href="${h.route_path('repo_changelog', repo_name=c.repo_name)}">
98 98 ${_ungettext('%(num)s Commit', '%(num)s Commits', commit_rev) % {'num': commit_rev}}</a>,
99 99 % endif
100 100
101 101 ## forks
102 102 <a title="${_('Number of Repository Forks')}" href="${h.url('repo_forks_home', repo_name=c.repo_name)}">
103 103 ${c.repository_forks} ${_ungettext('Fork', 'Forks', c.repository_forks)}</a>,
104 104
105 105 ## repo size
106 106 % if commit_rev == -1:
107 107 <span class="stats-bullet">0 B</span>
108 108 % else:
109 109 <span class="stats-bullet" id="repo_size_container">
110 110 ${_('Calculating Repository Size...')}
111 111 </span>
112 112 % endif
113 113 </div>
114 114
115 115 <div class="commit-info">
116 116 <div class="tags">
117 117 % if c.rhodecode_repo:
118 118 ${refs_counters(
119 119 c.rhodecode_repo.branches,
120 120 c.rhodecode_repo.branches_closed,
121 121 c.rhodecode_repo.tags,
122 122 c.rhodecode_repo.bookmarks)}
123 123 % else:
124 124 ## missing requirements can make c.rhodecode_repo None
125 125 ${refs_counters([], [], [], [])}
126 126 % endif
127 127 </div>
128 128 </div>
129 129
130 130 </div>
131 131 </div>
132 132
133 133 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
134 134 <div class="left-label">
135 135 ${_('Statistics')}:
136 136 </div>
137 137 <div class="right-content">
138 138 <div class="input ${summary(c.show_stats)} statistics">
139 139 % if c.show_stats:
140 140 <div id="lang_stats" class="enabled">
141 141 ${_('Calculating Code Statistics...')}
142 142 </div>
143 143 % else:
144 144 <span class="disabled">
145 145 ${_('Statistics are disabled for this repository')}
146 146 </span>
147 147 % if h.HasPermissionAll('hg.admin')('enable stats on from summary'):
148 148 , ${h.link_to(_('enable statistics'),h.route_path('edit_repo',repo_name=c.repo_name, anchor='repo_enable_statistics'))}
149 149 % endif
150 150 % endif
151 151 </div>
152 152
153 153 </div>
154 154 </div>
155 155
156 156 % if show_downloads:
157 157 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
158 158 <div class="left-label">
159 159 ${_('Downloads')}:
160 160 </div>
161 161 <div class="right-content">
162 162 <div class="input ${summary(c.show_stats)} downloads">
163 163 % if c.rhodecode_repo and len(c.rhodecode_repo.revisions) == 0:
164 164 <span class="disabled">
165 165 ${_('There are no downloads yet')}
166 166 </span>
167 167 % elif not c.enable_downloads:
168 168 <span class="disabled">
169 169 ${_('Downloads are disabled for this repository')}
170 170 </span>
171 171 % if h.HasPermissionAll('hg.admin')('enable downloads on from summary'):
172 172 , ${h.link_to(_('enable downloads'),h.route_path('edit_repo',repo_name=c.repo_name, anchor='repo_enable_downloads'))}
173 173 % endif
174 174 % else:
175 175 <span class="enabled">
176 176 <a id="archive_link" class="btn btn-small" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name,fname='tip.zip')}">
177 177 <i class="icon-archive"></i> tip.zip
178 178 ## replaced by some JS on select
179 179 </a>
180 180 </span>
181 181 ${h.hidden('download_options')}
182 182 % endif
183 183 </div>
184 184 </div>
185 185 </div>
186 186 % endif
187 187
188 188 </div><!--end summary-detail-->
189 189 </%def>
190 190
191 191 <%def name="summary_stats(gravatar_function)">
192 192 <div class="sidebar-right">
193 193 <div class="summary-detail-header">
194 194 <h4 class="item">
195 195 ${_('Owner')}
196 196 </h4>
197 197 </div>
198 198 <div class="sidebar-right-content">
199 199 ${gravatar_function(c.rhodecode_db_repo.user.email, 16)}
200 200 </div>
201 201 </div><!--end sidebar-right-->
202 202 </%def>
@@ -1,136 +1,136 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3 %if c.repo_commits:
4 4 <table class="rctable repo_summary table_disp">
5 5 <tr>
6 6
7 7 <th class="status" colspan="2"></th>
8 8 <th>${_('Commit')}</th>
9 9 <th>${_('Commit message')}</th>
10 10 <th>${_('Age')}</th>
11 11 <th>${_('Author')}</th>
12 12 <th>${_('Refs')}</th>
13 13 </tr>
14 14 %for cnt,cs in enumerate(c.repo_commits):
15 15 <tr class="parity${cnt%2}">
16 16
17 17 <td class="td-status">
18 18 %if c.statuses.get(cs.raw_id):
19 19 <div class="changeset-status-ico shortlog">
20 20 %if c.statuses.get(cs.raw_id)[2]:
21 21 <a class="tooltip" title="${_('Commit status: %s\nClick to open associated pull request #%s') % (c.statuses.get(cs.raw_id)[0], c.statuses.get(cs.raw_id)[2])}" href="${h.route_path('pullrequest_show',repo_name=c.statuses.get(cs.raw_id)[3],pull_request_id=c.statuses.get(cs.raw_id)[2])}">
22 22 <div class="${'flag_status %s' % c.statuses.get(cs.raw_id)[0]}"></div>
23 23 </a>
24 24 %else:
25 25 <a class="tooltip" title="${_('Commit status: %s') % h.commit_status_lbl(c.statuses.get(cs.raw_id)[0])}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
26 26 <div class="${'flag_status %s' % c.statuses.get(cs.raw_id)[0]}"></div>
27 27 </a>
28 28 %endif
29 29 </div>
30 30 %else:
31 31 <div class="tooltip flag_status not_reviewed" title="${_('Commit status: Not Reviewed')}"></div>
32 32 %endif
33 33 </td>
34 34 <td class="td-comments">
35 35 %if c.comments.get(cs.raw_id,[]):
36 36 <a title="${_('Commit has comments')}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
37 37 <i class="icon-comment"></i> ${len(c.comments[cs.raw_id])}
38 38 </a>
39 39 %endif
40 40 </td>
41 41 <td class="td-commit">
42 42 <pre><a href="${h.url('changeset_home', repo_name=c.repo_name, revision=cs.raw_id)}">${h.show_id(cs)}</a></pre>
43 43 </td>
44 44
45 45 <td class="td-description mid">
46 46 <div class="log-container truncate-wrap">
47 47 <div class="message truncate" id="c-${cs.raw_id}">${h.urlify_commit_message(cs.message, c.repo_name)}</div>
48 48 </div>
49 49 </td>
50 50
51 51 <td class="td-time">
52 52 ${h.age_component(cs.date)}
53 53 </td>
54 54 <td class="td-user author">
55 55 ${base.gravatar_with_user(cs.author)}
56 56 </td>
57 57
58 58 <td class="td-tags">
59 59 <div class="autoexpand">
60 60 %if h.is_hg(c.rhodecode_repo):
61 61 %for book in cs.bookmarks:
62 62 <span class="booktag tag" title="${h.tooltip(_('Bookmark %s') % book)}">
63 63 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=cs.raw_id, _query=dict(at=book))}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
64 64 </span>
65 65 %endfor
66 66 %endif
67 67 ## tags
68 68 %for tag in cs.tags:
69 69 <span class="tagtag tag" title="${h.tooltip(_('Tag %s') % tag)}">
70 70 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=cs.raw_id, _query=dict(at=tag))}"><i class="icon-tag"></i>${h.shorter(tag)}</a>
71 71 </span>
72 72 %endfor
73 73
74 74 ## branch
75 75 %if cs.branch:
76 76 <span class="branchtag tag" title="${h.tooltip(_('Branch %s') % cs.branch)}">
77 <a href="${h.url('changelog_home',repo_name=c.repo_name,branch=cs.branch)}"><i class="icon-code-fork"></i>${h.shorter(cs.branch)}</a>
77 <a href="${h.route_path('repo_changelog',repo_name=c.repo_name,_query=dict(branch=cs.branch))}"><i class="icon-code-fork"></i>${h.shorter(cs.branch)}</a>
78 78 </span>
79 79 %endif
80 80 </div>
81 81 </td>
82 82 </tr>
83 83 %endfor
84 84
85 85 </table>
86 86
87 87 <script type="text/javascript">
88 88 $(document).pjax('#shortlog_data .pager_link','#shortlog_data', {timeout: 2000, scrollTo: false });
89 89 $(document).on('pjax:success', function(){ timeagoActivate(); });
90 90 </script>
91 91
92 92 <div class="pagination-wh pagination-left">
93 93 ${c.repo_commits.pager('$link_previous ~2~ $link_next')}
94 94 </div>
95 95 %else:
96 96
97 97 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
98 98 <div class="quick_start">
99 99 <div class="fieldset">
100 100 <div class="left-label">${_('Add or upload files directly via RhodeCode:')}</div>
101 101 <div class="right-content">
102 102 <div id="add_node_id" class="add_node">
103 103 <a href="${h.route_path('repo_files_add_file',repo_name=c.repo_name,commit_id=0, f_path='', _anchor='edit')}" class="btn btn-default">${_('Add New File')}</a>
104 104 </div>
105 105 </div>
106 106 %endif
107 107 </div>
108 108
109 109 %if not h.is_svn(c.rhodecode_repo):
110 110 <div class="fieldset">
111 111 <div class="left-label">${_('Push new repo:')}</div>
112 112 <div class="right-content">
113 113 <pre>
114 114 ${c.rhodecode_repo.alias} clone ${c.clone_repo_url}
115 115 ${c.rhodecode_repo.alias} add README # add first file
116 116 ${c.rhodecode_repo.alias} commit -m "Initial" # commit with message
117 117 ${c.rhodecode_repo.alias} push ${'origin master' if h.is_git(c.rhodecode_repo) else ''} # push changes back
118 118 </pre>
119 119 </div>
120 120 </div>
121 121 <div class="fieldset">
122 122 <div class="left-label">${_('Existing repository?')}</div>
123 123 <div class="right-content">
124 124 <pre>
125 125 %if h.is_git(c.rhodecode_repo):
126 126 git remote add origin ${c.clone_repo_url}
127 127 git push -u origin master
128 128 %else:
129 129 hg push ${c.clone_repo_url}
130 130 %endif
131 131 </pre>
132 132 </div>
133 133 </div>
134 134 %endif
135 135 </div>
136 136 %endif
@@ -1,1094 +1,1107 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23 23 from webob.exc import HTTPNotFound
24 24
25 25 import rhodecode
26 26 from rhodecode.lib.vcs.nodes import FileNode
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.model.changeset_status import ChangesetStatusModel
29 29 from rhodecode.model.db import (
30 30 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
31 31 from rhodecode.model.meta import Session
32 32 from rhodecode.model.pull_request import PullRequestModel
33 33 from rhodecode.model.user import UserModel
34 34 from rhodecode.tests import (
35 35 assert_session_flash, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36 36 from rhodecode.tests.utils import AssertResponse
37 37
38 38
39 def route_path(name, params=None, **kwargs):
40 import urllib
41
42 base_url = {
43 'repo_changelog':'/{repo_name}/changelog',
44 'repo_changelog_file':'/{repo_name}/changelog/{commit_id}/{f_path}',
45 }[name].format(**kwargs)
46
47 if params:
48 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
49 return base_url
50
51
39 52 @pytest.mark.usefixtures('app', 'autologin_user')
40 53 @pytest.mark.backends("git", "hg")
41 54 class TestPullrequestsController(object):
42 55
43 56 def test_index(self, backend):
44 57 self.app.get(url(
45 58 controller='pullrequests', action='index',
46 59 repo_name=backend.repo_name))
47 60
48 61 def test_option_menu_create_pull_request_exists(self, backend):
49 62 repo_name = backend.repo_name
50 63 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
51 64
52 65 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
53 66 'pullrequest', repo_name=repo_name)
54 67 response.mustcontain(create_pr_link)
55 68
56 69 def test_create_pr_form_with_raw_commit_id(self, backend):
57 70 repo = backend.repo
58 71
59 72 self.app.get(
60 73 url(controller='pullrequests', action='index',
61 74 repo_name=repo.repo_name,
62 75 commit=repo.get_commit().raw_id),
63 76 status=200)
64 77
65 78 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
66 79 def test_show(self, pr_util, pr_merge_enabled):
67 80 pull_request = pr_util.create_pull_request(
68 81 mergeable=pr_merge_enabled, enable_notifications=False)
69 82
70 83 response = self.app.get(url(
71 84 controller='pullrequests', action='show',
72 85 repo_name=pull_request.target_repo.scm_instance().name,
73 86 pull_request_id=str(pull_request.pull_request_id)))
74 87
75 88 for commit_id in pull_request.revisions:
76 89 response.mustcontain(commit_id)
77 90
78 91 assert pull_request.target_ref_parts.type in response
79 92 assert pull_request.target_ref_parts.name in response
80 93 target_clone_url = pull_request.target_repo.clone_url()
81 94 assert target_clone_url in response
82 95
83 96 assert 'class="pull-request-merge"' in response
84 97 assert (
85 98 'Server-side pull request merging is disabled.'
86 99 in response) != pr_merge_enabled
87 100
88 101 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
89 102 from rhodecode.tests.functional.test_login import login_url, logut_url
90 103 # Logout
91 104 response = self.app.post(
92 105 logut_url,
93 106 params={'csrf_token': csrf_token})
94 107 # Login as regular user
95 108 response = self.app.post(login_url,
96 109 {'username': TEST_USER_REGULAR_LOGIN,
97 110 'password': 'test12'})
98 111
99 112 pull_request = pr_util.create_pull_request(
100 113 author=TEST_USER_REGULAR_LOGIN)
101 114
102 115 response = self.app.get(url(
103 116 controller='pullrequests', action='show',
104 117 repo_name=pull_request.target_repo.scm_instance().name,
105 118 pull_request_id=str(pull_request.pull_request_id)))
106 119
107 120 response.mustcontain('Server-side pull request merging is disabled.')
108 121
109 122 assert_response = response.assert_response()
110 123 # for regular user without a merge permissions, we don't see it
111 124 assert_response.no_element_exists('#close-pull-request-action')
112 125
113 126 user_util.grant_user_permission_to_repo(
114 127 pull_request.target_repo,
115 128 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
116 129 'repository.write')
117 130 response = self.app.get(url(
118 131 controller='pullrequests', action='show',
119 132 repo_name=pull_request.target_repo.scm_instance().name,
120 133 pull_request_id=str(pull_request.pull_request_id)))
121 134
122 135 response.mustcontain('Server-side pull request merging is disabled.')
123 136
124 137 assert_response = response.assert_response()
125 138 # now regular user has a merge permissions, we have CLOSE button
126 139 assert_response.one_element_exists('#close-pull-request-action')
127 140
128 141 def test_show_invalid_commit_id(self, pr_util):
129 142 # Simulating invalid revisions which will cause a lookup error
130 143 pull_request = pr_util.create_pull_request()
131 144 pull_request.revisions = ['invalid']
132 145 Session().add(pull_request)
133 146 Session().commit()
134 147
135 148 response = self.app.get(url(
136 149 controller='pullrequests', action='show',
137 150 repo_name=pull_request.target_repo.scm_instance().name,
138 151 pull_request_id=str(pull_request.pull_request_id)))
139 152
140 153 for commit_id in pull_request.revisions:
141 154 response.mustcontain(commit_id)
142 155
143 156 def test_show_invalid_source_reference(self, pr_util):
144 157 pull_request = pr_util.create_pull_request()
145 158 pull_request.source_ref = 'branch:b:invalid'
146 159 Session().add(pull_request)
147 160 Session().commit()
148 161
149 162 self.app.get(url(
150 163 controller='pullrequests', action='show',
151 164 repo_name=pull_request.target_repo.scm_instance().name,
152 165 pull_request_id=str(pull_request.pull_request_id)))
153 166
154 167 def test_edit_title_description(self, pr_util, csrf_token):
155 168 pull_request = pr_util.create_pull_request()
156 169 pull_request_id = pull_request.pull_request_id
157 170
158 171 response = self.app.post(
159 172 url(controller='pullrequests', action='update',
160 173 repo_name=pull_request.target_repo.repo_name,
161 174 pull_request_id=str(pull_request_id)),
162 175 params={
163 176 'edit_pull_request': 'true',
164 177 '_method': 'put',
165 178 'title': 'New title',
166 179 'description': 'New description',
167 180 'csrf_token': csrf_token})
168 181
169 182 assert_session_flash(
170 183 response, u'Pull request title & description updated.',
171 184 category='success')
172 185
173 186 pull_request = PullRequest.get(pull_request_id)
174 187 assert pull_request.title == 'New title'
175 188 assert pull_request.description == 'New description'
176 189
177 190 def test_edit_title_description_closed(self, pr_util, csrf_token):
178 191 pull_request = pr_util.create_pull_request()
179 192 pull_request_id = pull_request.pull_request_id
180 193 pr_util.close()
181 194
182 195 response = self.app.post(
183 196 url(controller='pullrequests', action='update',
184 197 repo_name=pull_request.target_repo.repo_name,
185 198 pull_request_id=str(pull_request_id)),
186 199 params={
187 200 'edit_pull_request': 'true',
188 201 '_method': 'put',
189 202 'title': 'New title',
190 203 'description': 'New description',
191 204 'csrf_token': csrf_token})
192 205
193 206 assert_session_flash(
194 207 response, u'Cannot update closed pull requests.',
195 208 category='error')
196 209
197 210 def test_update_invalid_source_reference(self, pr_util, csrf_token):
198 211 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
199 212
200 213 pull_request = pr_util.create_pull_request()
201 214 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
202 215 Session().add(pull_request)
203 216 Session().commit()
204 217
205 218 pull_request_id = pull_request.pull_request_id
206 219
207 220 response = self.app.post(
208 221 url(controller='pullrequests', action='update',
209 222 repo_name=pull_request.target_repo.repo_name,
210 223 pull_request_id=str(pull_request_id)),
211 224 params={'update_commits': 'true', '_method': 'put',
212 225 'csrf_token': csrf_token})
213 226
214 227 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
215 228 UpdateFailureReason.MISSING_SOURCE_REF]
216 229 assert_session_flash(response, expected_msg, category='error')
217 230
218 231 def test_missing_target_reference(self, pr_util, csrf_token):
219 232 from rhodecode.lib.vcs.backends.base import MergeFailureReason
220 233 pull_request = pr_util.create_pull_request(
221 234 approved=True, mergeable=True)
222 235 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
223 236 Session().add(pull_request)
224 237 Session().commit()
225 238
226 239 pull_request_id = pull_request.pull_request_id
227 240 pull_request_url = url(
228 241 controller='pullrequests', action='show',
229 242 repo_name=pull_request.target_repo.repo_name,
230 243 pull_request_id=str(pull_request_id))
231 244
232 245 response = self.app.get(pull_request_url)
233 246
234 247 assertr = AssertResponse(response)
235 248 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
236 249 MergeFailureReason.MISSING_TARGET_REF]
237 250 assertr.element_contains(
238 251 'span[data-role="merge-message"]', str(expected_msg))
239 252
240 253 def test_comment_and_close_pull_request_custom_message_approved(
241 254 self, pr_util, csrf_token, xhr_header):
242 255
243 256 pull_request = pr_util.create_pull_request(approved=True)
244 257 pull_request_id = pull_request.pull_request_id
245 258 author = pull_request.user_id
246 259 repo = pull_request.target_repo.repo_id
247 260
248 261 self.app.post(
249 262 url(controller='pullrequests',
250 263 action='comment',
251 264 repo_name=pull_request.target_repo.scm_instance().name,
252 265 pull_request_id=str(pull_request_id)),
253 266 params={
254 267 'close_pull_request': '1',
255 268 'text': 'Closing a PR',
256 269 'csrf_token': csrf_token},
257 270 extra_environ=xhr_header,)
258 271
259 272 journal = UserLog.query()\
260 273 .filter(UserLog.user_id == author)\
261 274 .filter(UserLog.repository_id == repo) \
262 275 .order_by('user_log_id') \
263 276 .all()
264 277 assert journal[-1].action == 'repo.pull_request.close'
265 278
266 279 pull_request = PullRequest.get(pull_request_id)
267 280 assert pull_request.is_closed()
268 281
269 282 status = ChangesetStatusModel().get_status(
270 283 pull_request.source_repo, pull_request=pull_request)
271 284 assert status == ChangesetStatus.STATUS_APPROVED
272 285 comments = ChangesetComment().query() \
273 286 .filter(ChangesetComment.pull_request == pull_request) \
274 287 .order_by(ChangesetComment.comment_id.asc())\
275 288 .all()
276 289 assert comments[-1].text == 'Closing a PR'
277 290
278 291 def test_comment_force_close_pull_request_rejected(
279 292 self, pr_util, csrf_token, xhr_header):
280 293 pull_request = pr_util.create_pull_request()
281 294 pull_request_id = pull_request.pull_request_id
282 295 PullRequestModel().update_reviewers(
283 296 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
284 297 pull_request.author)
285 298 author = pull_request.user_id
286 299 repo = pull_request.target_repo.repo_id
287 300
288 301 self.app.post(
289 302 url(controller='pullrequests',
290 303 action='comment',
291 304 repo_name=pull_request.target_repo.scm_instance().name,
292 305 pull_request_id=str(pull_request_id)),
293 306 params={
294 307 'close_pull_request': '1',
295 308 'csrf_token': csrf_token},
296 309 extra_environ=xhr_header)
297 310
298 311 pull_request = PullRequest.get(pull_request_id)
299 312
300 313 journal = UserLog.query()\
301 314 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
302 315 .order_by('user_log_id') \
303 316 .all()
304 317 assert journal[-1].action == 'repo.pull_request.close'
305 318
306 319 # check only the latest status, not the review status
307 320 status = ChangesetStatusModel().get_status(
308 321 pull_request.source_repo, pull_request=pull_request)
309 322 assert status == ChangesetStatus.STATUS_REJECTED
310 323
311 324 def test_comment_and_close_pull_request(
312 325 self, pr_util, csrf_token, xhr_header):
313 326 pull_request = pr_util.create_pull_request()
314 327 pull_request_id = pull_request.pull_request_id
315 328
316 329 response = self.app.post(
317 330 url(controller='pullrequests',
318 331 action='comment',
319 332 repo_name=pull_request.target_repo.scm_instance().name,
320 333 pull_request_id=str(pull_request.pull_request_id)),
321 334 params={
322 335 'close_pull_request': 'true',
323 336 'csrf_token': csrf_token},
324 337 extra_environ=xhr_header)
325 338
326 339 assert response.json
327 340
328 341 pull_request = PullRequest.get(pull_request_id)
329 342 assert pull_request.is_closed()
330 343
331 344 # check only the latest status, not the review status
332 345 status = ChangesetStatusModel().get_status(
333 346 pull_request.source_repo, pull_request=pull_request)
334 347 assert status == ChangesetStatus.STATUS_REJECTED
335 348
336 349 def test_create_pull_request(self, backend, csrf_token):
337 350 commits = [
338 351 {'message': 'ancestor'},
339 352 {'message': 'change'},
340 353 {'message': 'change2'},
341 354 ]
342 355 commit_ids = backend.create_master_repo(commits)
343 356 target = backend.create_repo(heads=['ancestor'])
344 357 source = backend.create_repo(heads=['change2'])
345 358
346 359 response = self.app.post(
347 360 url(
348 361 controller='pullrequests',
349 362 action='create',
350 363 repo_name=source.repo_name
351 364 ),
352 365 [
353 366 ('source_repo', source.repo_name),
354 367 ('source_ref', 'branch:default:' + commit_ids['change2']),
355 368 ('target_repo', target.repo_name),
356 369 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
357 370 ('common_ancestor', commit_ids['ancestor']),
358 371 ('pullrequest_desc', 'Description'),
359 372 ('pullrequest_title', 'Title'),
360 373 ('__start__', 'review_members:sequence'),
361 374 ('__start__', 'reviewer:mapping'),
362 375 ('user_id', '1'),
363 376 ('__start__', 'reasons:sequence'),
364 377 ('reason', 'Some reason'),
365 378 ('__end__', 'reasons:sequence'),
366 379 ('mandatory', 'False'),
367 380 ('__end__', 'reviewer:mapping'),
368 381 ('__end__', 'review_members:sequence'),
369 382 ('__start__', 'revisions:sequence'),
370 383 ('revisions', commit_ids['change']),
371 384 ('revisions', commit_ids['change2']),
372 385 ('__end__', 'revisions:sequence'),
373 386 ('user', ''),
374 387 ('csrf_token', csrf_token),
375 388 ],
376 389 status=302)
377 390
378 391 location = response.headers['Location']
379 392 pull_request_id = location.rsplit('/', 1)[1]
380 393 assert pull_request_id != 'new'
381 394 pull_request = PullRequest.get(int(pull_request_id))
382 395
383 396 # check that we have now both revisions
384 397 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
385 398 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
386 399 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
387 400 assert pull_request.target_ref == expected_target_ref
388 401
389 402 def test_reviewer_notifications(self, backend, csrf_token):
390 403 # We have to use the app.post for this test so it will create the
391 404 # notifications properly with the new PR
392 405 commits = [
393 406 {'message': 'ancestor',
394 407 'added': [FileNode('file_A', content='content_of_ancestor')]},
395 408 {'message': 'change',
396 409 'added': [FileNode('file_a', content='content_of_change')]},
397 410 {'message': 'change-child'},
398 411 {'message': 'ancestor-child', 'parents': ['ancestor'],
399 412 'added': [
400 413 FileNode('file_B', content='content_of_ancestor_child')]},
401 414 {'message': 'ancestor-child-2'},
402 415 ]
403 416 commit_ids = backend.create_master_repo(commits)
404 417 target = backend.create_repo(heads=['ancestor-child'])
405 418 source = backend.create_repo(heads=['change'])
406 419
407 420 response = self.app.post(
408 421 url(
409 422 controller='pullrequests',
410 423 action='create',
411 424 repo_name=source.repo_name
412 425 ),
413 426 [
414 427 ('source_repo', source.repo_name),
415 428 ('source_ref', 'branch:default:' + commit_ids['change']),
416 429 ('target_repo', target.repo_name),
417 430 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
418 431 ('common_ancestor', commit_ids['ancestor']),
419 432 ('pullrequest_desc', 'Description'),
420 433 ('pullrequest_title', 'Title'),
421 434 ('__start__', 'review_members:sequence'),
422 435 ('__start__', 'reviewer:mapping'),
423 436 ('user_id', '2'),
424 437 ('__start__', 'reasons:sequence'),
425 438 ('reason', 'Some reason'),
426 439 ('__end__', 'reasons:sequence'),
427 440 ('mandatory', 'False'),
428 441 ('__end__', 'reviewer:mapping'),
429 442 ('__end__', 'review_members:sequence'),
430 443 ('__start__', 'revisions:sequence'),
431 444 ('revisions', commit_ids['change']),
432 445 ('__end__', 'revisions:sequence'),
433 446 ('user', ''),
434 447 ('csrf_token', csrf_token),
435 448 ],
436 449 status=302)
437 450
438 451 location = response.headers['Location']
439 452
440 453 pull_request_id = location.rsplit('/', 1)[1]
441 454 assert pull_request_id != 'new'
442 455 pull_request = PullRequest.get(int(pull_request_id))
443 456
444 457 # Check that a notification was made
445 458 notifications = Notification.query()\
446 459 .filter(Notification.created_by == pull_request.author.user_id,
447 460 Notification.type_ == Notification.TYPE_PULL_REQUEST,
448 461 Notification.subject.contains(
449 462 "wants you to review pull request #%s" % pull_request_id))
450 463 assert len(notifications.all()) == 1
451 464
452 465 # Change reviewers and check that a notification was made
453 466 PullRequestModel().update_reviewers(
454 467 pull_request.pull_request_id, [(1, [], False)],
455 468 pull_request.author)
456 469 assert len(notifications.all()) == 2
457 470
458 471 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
459 472 csrf_token):
460 473 commits = [
461 474 {'message': 'ancestor',
462 475 'added': [FileNode('file_A', content='content_of_ancestor')]},
463 476 {'message': 'change',
464 477 'added': [FileNode('file_a', content='content_of_change')]},
465 478 {'message': 'change-child'},
466 479 {'message': 'ancestor-child', 'parents': ['ancestor'],
467 480 'added': [
468 481 FileNode('file_B', content='content_of_ancestor_child')]},
469 482 {'message': 'ancestor-child-2'},
470 483 ]
471 484 commit_ids = backend.create_master_repo(commits)
472 485 target = backend.create_repo(heads=['ancestor-child'])
473 486 source = backend.create_repo(heads=['change'])
474 487
475 488 response = self.app.post(
476 489 url(
477 490 controller='pullrequests',
478 491 action='create',
479 492 repo_name=source.repo_name
480 493 ),
481 494 [
482 495 ('source_repo', source.repo_name),
483 496 ('source_ref', 'branch:default:' + commit_ids['change']),
484 497 ('target_repo', target.repo_name),
485 498 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
486 499 ('common_ancestor', commit_ids['ancestor']),
487 500 ('pullrequest_desc', 'Description'),
488 501 ('pullrequest_title', 'Title'),
489 502 ('__start__', 'review_members:sequence'),
490 503 ('__start__', 'reviewer:mapping'),
491 504 ('user_id', '1'),
492 505 ('__start__', 'reasons:sequence'),
493 506 ('reason', 'Some reason'),
494 507 ('__end__', 'reasons:sequence'),
495 508 ('mandatory', 'False'),
496 509 ('__end__', 'reviewer:mapping'),
497 510 ('__end__', 'review_members:sequence'),
498 511 ('__start__', 'revisions:sequence'),
499 512 ('revisions', commit_ids['change']),
500 513 ('__end__', 'revisions:sequence'),
501 514 ('user', ''),
502 515 ('csrf_token', csrf_token),
503 516 ],
504 517 status=302)
505 518
506 519 location = response.headers['Location']
507 520
508 521 pull_request_id = location.rsplit('/', 1)[1]
509 522 assert pull_request_id != 'new'
510 523 pull_request = PullRequest.get(int(pull_request_id))
511 524
512 525 # target_ref has to point to the ancestor's commit_id in order to
513 526 # show the correct diff
514 527 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
515 528 assert pull_request.target_ref == expected_target_ref
516 529
517 530 # Check generated diff contents
518 531 response = response.follow()
519 532 assert 'content_of_ancestor' not in response.body
520 533 assert 'content_of_ancestor-child' not in response.body
521 534 assert 'content_of_change' in response.body
522 535
523 536 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
524 537 # Clear any previous calls to rcextensions
525 538 rhodecode.EXTENSIONS.calls.clear()
526 539
527 540 pull_request = pr_util.create_pull_request(
528 541 approved=True, mergeable=True)
529 542 pull_request_id = pull_request.pull_request_id
530 543 repo_name = pull_request.target_repo.scm_instance().name,
531 544
532 545 response = self.app.post(
533 546 url(controller='pullrequests',
534 547 action='merge',
535 548 repo_name=str(repo_name[0]),
536 549 pull_request_id=str(pull_request_id)),
537 550 params={'csrf_token': csrf_token}).follow()
538 551
539 552 pull_request = PullRequest.get(pull_request_id)
540 553
541 554 assert response.status_int == 200
542 555 assert pull_request.is_closed()
543 556 assert_pull_request_status(
544 557 pull_request, ChangesetStatus.STATUS_APPROVED)
545 558
546 559 # Check the relevant log entries were added
547 560 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
548 561 actions = [log.action for log in user_logs]
549 562 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
550 563 expected_actions = [
551 564 u'repo.pull_request.close',
552 565 u'repo.pull_request.merge',
553 566 u'repo.pull_request.comment.create'
554 567 ]
555 568 assert actions == expected_actions
556 569
557 570 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
558 571 actions = [log for log in user_logs]
559 572 assert actions[-1].action == 'user.push'
560 573 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
561 574
562 575 # Check post_push rcextension was really executed
563 576 push_calls = rhodecode.EXTENSIONS.calls['post_push']
564 577 assert len(push_calls) == 1
565 578 unused_last_call_args, last_call_kwargs = push_calls[0]
566 579 assert last_call_kwargs['action'] == 'push'
567 580 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
568 581
569 582 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
570 583 pull_request = pr_util.create_pull_request(mergeable=False)
571 584 pull_request_id = pull_request.pull_request_id
572 585 pull_request = PullRequest.get(pull_request_id)
573 586
574 587 response = self.app.post(
575 588 url(controller='pullrequests',
576 589 action='merge',
577 590 repo_name=pull_request.target_repo.scm_instance().name,
578 591 pull_request_id=str(pull_request.pull_request_id)),
579 592 params={'csrf_token': csrf_token}).follow()
580 593
581 594 assert response.status_int == 200
582 595 response.mustcontain(
583 596 'Merge is not currently possible because of below failed checks.')
584 597 response.mustcontain('Server-side pull request merging is disabled.')
585 598
586 599 @pytest.mark.skip_backends('svn')
587 600 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
588 601 pull_request = pr_util.create_pull_request(mergeable=True)
589 602 pull_request_id = pull_request.pull_request_id
590 603 repo_name = pull_request.target_repo.scm_instance().name,
591 604
592 605 response = self.app.post(
593 606 url(controller='pullrequests',
594 607 action='merge',
595 608 repo_name=str(repo_name[0]),
596 609 pull_request_id=str(pull_request_id)),
597 610 params={'csrf_token': csrf_token}).follow()
598 611
599 612 assert response.status_int == 200
600 613
601 614 response.mustcontain(
602 615 'Merge is not currently possible because of below failed checks.')
603 616 response.mustcontain('Pull request reviewer approval is pending.')
604 617
605 618 def test_update_source_revision(self, backend, csrf_token):
606 619 commits = [
607 620 {'message': 'ancestor'},
608 621 {'message': 'change'},
609 622 {'message': 'change-2'},
610 623 ]
611 624 commit_ids = backend.create_master_repo(commits)
612 625 target = backend.create_repo(heads=['ancestor'])
613 626 source = backend.create_repo(heads=['change'])
614 627
615 628 # create pr from a in source to A in target
616 629 pull_request = PullRequest()
617 630 pull_request.source_repo = source
618 631 # TODO: johbo: Make sure that we write the source ref this way!
619 632 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
620 633 branch=backend.default_branch_name, commit_id=commit_ids['change'])
621 634 pull_request.target_repo = target
622 635
623 636 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
624 637 branch=backend.default_branch_name,
625 638 commit_id=commit_ids['ancestor'])
626 639 pull_request.revisions = [commit_ids['change']]
627 640 pull_request.title = u"Test"
628 641 pull_request.description = u"Description"
629 642 pull_request.author = UserModel().get_by_username(
630 643 TEST_USER_ADMIN_LOGIN)
631 644 Session().add(pull_request)
632 645 Session().commit()
633 646 pull_request_id = pull_request.pull_request_id
634 647
635 648 # source has ancestor - change - change-2
636 649 backend.pull_heads(source, heads=['change-2'])
637 650
638 651 # update PR
639 652 self.app.post(
640 653 url(controller='pullrequests', action='update',
641 654 repo_name=target.repo_name,
642 655 pull_request_id=str(pull_request_id)),
643 656 params={'update_commits': 'true', '_method': 'put',
644 657 'csrf_token': csrf_token})
645 658
646 659 # check that we have now both revisions
647 660 pull_request = PullRequest.get(pull_request_id)
648 661 assert pull_request.revisions == [
649 662 commit_ids['change-2'], commit_ids['change']]
650 663
651 664 # TODO: johbo: this should be a test on its own
652 665 response = self.app.get(url(
653 666 controller='pullrequests', action='index',
654 667 repo_name=target.repo_name))
655 668 assert response.status_int == 200
656 669 assert 'Pull request updated to' in response.body
657 670 assert 'with 1 added, 0 removed commits.' in response.body
658 671
659 672 def test_update_target_revision(self, backend, csrf_token):
660 673 commits = [
661 674 {'message': 'ancestor'},
662 675 {'message': 'change'},
663 676 {'message': 'ancestor-new', 'parents': ['ancestor']},
664 677 {'message': 'change-rebased'},
665 678 ]
666 679 commit_ids = backend.create_master_repo(commits)
667 680 target = backend.create_repo(heads=['ancestor'])
668 681 source = backend.create_repo(heads=['change'])
669 682
670 683 # create pr from a in source to A in target
671 684 pull_request = PullRequest()
672 685 pull_request.source_repo = source
673 686 # TODO: johbo: Make sure that we write the source ref this way!
674 687 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
675 688 branch=backend.default_branch_name, commit_id=commit_ids['change'])
676 689 pull_request.target_repo = target
677 690 # TODO: johbo: Target ref should be branch based, since tip can jump
678 691 # from branch to branch
679 692 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
680 693 branch=backend.default_branch_name,
681 694 commit_id=commit_ids['ancestor'])
682 695 pull_request.revisions = [commit_ids['change']]
683 696 pull_request.title = u"Test"
684 697 pull_request.description = u"Description"
685 698 pull_request.author = UserModel().get_by_username(
686 699 TEST_USER_ADMIN_LOGIN)
687 700 Session().add(pull_request)
688 701 Session().commit()
689 702 pull_request_id = pull_request.pull_request_id
690 703
691 704 # target has ancestor - ancestor-new
692 705 # source has ancestor - ancestor-new - change-rebased
693 706 backend.pull_heads(target, heads=['ancestor-new'])
694 707 backend.pull_heads(source, heads=['change-rebased'])
695 708
696 709 # update PR
697 710 self.app.post(
698 711 url(controller='pullrequests', action='update',
699 712 repo_name=target.repo_name,
700 713 pull_request_id=str(pull_request_id)),
701 714 params={'update_commits': 'true', '_method': 'put',
702 715 'csrf_token': csrf_token},
703 716 status=200)
704 717
705 718 # check that we have now both revisions
706 719 pull_request = PullRequest.get(pull_request_id)
707 720 assert pull_request.revisions == [commit_ids['change-rebased']]
708 721 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
709 722 branch=backend.default_branch_name,
710 723 commit_id=commit_ids['ancestor-new'])
711 724
712 725 # TODO: johbo: This should be a test on its own
713 726 response = self.app.get(url(
714 727 controller='pullrequests', action='index',
715 728 repo_name=target.repo_name))
716 729 assert response.status_int == 200
717 730 assert 'Pull request updated to' in response.body
718 731 assert 'with 1 added, 1 removed commits.' in response.body
719 732
720 733 def test_update_of_ancestor_reference(self, backend, csrf_token):
721 734 commits = [
722 735 {'message': 'ancestor'},
723 736 {'message': 'change'},
724 737 {'message': 'change-2'},
725 738 {'message': 'ancestor-new', 'parents': ['ancestor']},
726 739 {'message': 'change-rebased'},
727 740 ]
728 741 commit_ids = backend.create_master_repo(commits)
729 742 target = backend.create_repo(heads=['ancestor'])
730 743 source = backend.create_repo(heads=['change'])
731 744
732 745 # create pr from a in source to A in target
733 746 pull_request = PullRequest()
734 747 pull_request.source_repo = source
735 748 # TODO: johbo: Make sure that we write the source ref this way!
736 749 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
737 750 branch=backend.default_branch_name,
738 751 commit_id=commit_ids['change'])
739 752 pull_request.target_repo = target
740 753 # TODO: johbo: Target ref should be branch based, since tip can jump
741 754 # from branch to branch
742 755 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
743 756 branch=backend.default_branch_name,
744 757 commit_id=commit_ids['ancestor'])
745 758 pull_request.revisions = [commit_ids['change']]
746 759 pull_request.title = u"Test"
747 760 pull_request.description = u"Description"
748 761 pull_request.author = UserModel().get_by_username(
749 762 TEST_USER_ADMIN_LOGIN)
750 763 Session().add(pull_request)
751 764 Session().commit()
752 765 pull_request_id = pull_request.pull_request_id
753 766
754 767 # target has ancestor - ancestor-new
755 768 # source has ancestor - ancestor-new - change-rebased
756 769 backend.pull_heads(target, heads=['ancestor-new'])
757 770 backend.pull_heads(source, heads=['change-rebased'])
758 771
759 772 # update PR
760 773 self.app.post(
761 774 url(controller='pullrequests', action='update',
762 775 repo_name=target.repo_name,
763 776 pull_request_id=str(pull_request_id)),
764 777 params={'update_commits': 'true', '_method': 'put',
765 778 'csrf_token': csrf_token},
766 779 status=200)
767 780
768 781 # Expect the target reference to be updated correctly
769 782 pull_request = PullRequest.get(pull_request_id)
770 783 assert pull_request.revisions == [commit_ids['change-rebased']]
771 784 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
772 785 branch=backend.default_branch_name,
773 786 commit_id=commit_ids['ancestor-new'])
774 787 assert pull_request.target_ref == expected_target_ref
775 788
776 789 def test_remove_pull_request_branch(self, backend_git, csrf_token):
777 790 branch_name = 'development'
778 791 commits = [
779 792 {'message': 'initial-commit'},
780 793 {'message': 'old-feature'},
781 794 {'message': 'new-feature', 'branch': branch_name},
782 795 ]
783 796 repo = backend_git.create_repo(commits)
784 797 commit_ids = backend_git.commit_ids
785 798
786 799 pull_request = PullRequest()
787 800 pull_request.source_repo = repo
788 801 pull_request.target_repo = repo
789 802 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
790 803 branch=branch_name, commit_id=commit_ids['new-feature'])
791 804 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
792 805 branch=backend_git.default_branch_name,
793 806 commit_id=commit_ids['old-feature'])
794 807 pull_request.revisions = [commit_ids['new-feature']]
795 808 pull_request.title = u"Test"
796 809 pull_request.description = u"Description"
797 810 pull_request.author = UserModel().get_by_username(
798 811 TEST_USER_ADMIN_LOGIN)
799 812 Session().add(pull_request)
800 813 Session().commit()
801 814
802 815 vcs = repo.scm_instance()
803 816 vcs.remove_ref('refs/heads/{}'.format(branch_name))
804 817
805 818 response = self.app.get(url(
806 819 controller='pullrequests', action='show',
807 820 repo_name=repo.repo_name,
808 821 pull_request_id=str(pull_request.pull_request_id)))
809 822
810 823 assert response.status_int == 200
811 824 assert_response = AssertResponse(response)
812 825 assert_response.element_contains(
813 826 '#changeset_compare_view_content .alert strong',
814 827 'Missing commits')
815 828 assert_response.element_contains(
816 829 '#changeset_compare_view_content .alert',
817 830 'This pull request cannot be displayed, because one or more'
818 831 ' commits no longer exist in the source repository.')
819 832
820 833 def test_strip_commits_from_pull_request(
821 834 self, backend, pr_util, csrf_token):
822 835 commits = [
823 836 {'message': 'initial-commit'},
824 837 {'message': 'old-feature'},
825 838 {'message': 'new-feature', 'parents': ['initial-commit']},
826 839 ]
827 840 pull_request = pr_util.create_pull_request(
828 841 commits, target_head='initial-commit', source_head='new-feature',
829 842 revisions=['new-feature'])
830 843
831 844 vcs = pr_util.source_repository.scm_instance()
832 845 if backend.alias == 'git':
833 846 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
834 847 else:
835 848 vcs.strip(pr_util.commit_ids['new-feature'])
836 849
837 850 response = self.app.get(url(
838 851 controller='pullrequests', action='show',
839 852 repo_name=pr_util.target_repository.repo_name,
840 853 pull_request_id=str(pull_request.pull_request_id)))
841 854
842 855 assert response.status_int == 200
843 856 assert_response = AssertResponse(response)
844 857 assert_response.element_contains(
845 858 '#changeset_compare_view_content .alert strong',
846 859 'Missing commits')
847 860 assert_response.element_contains(
848 861 '#changeset_compare_view_content .alert',
849 862 'This pull request cannot be displayed, because one or more'
850 863 ' commits no longer exist in the source repository.')
851 864 assert_response.element_contains(
852 865 '#update_commits',
853 866 'Update commits')
854 867
855 868 def test_strip_commits_and_update(
856 869 self, backend, pr_util, csrf_token):
857 870 commits = [
858 871 {'message': 'initial-commit'},
859 872 {'message': 'old-feature'},
860 873 {'message': 'new-feature', 'parents': ['old-feature']},
861 874 ]
862 875 pull_request = pr_util.create_pull_request(
863 876 commits, target_head='old-feature', source_head='new-feature',
864 877 revisions=['new-feature'], mergeable=True)
865 878
866 879 vcs = pr_util.source_repository.scm_instance()
867 880 if backend.alias == 'git':
868 881 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
869 882 else:
870 883 vcs.strip(pr_util.commit_ids['new-feature'])
871 884
872 885 response = self.app.post(
873 886 url(controller='pullrequests', action='update',
874 887 repo_name=pull_request.target_repo.repo_name,
875 888 pull_request_id=str(pull_request.pull_request_id)),
876 889 params={'update_commits': 'true', '_method': 'put',
877 890 'csrf_token': csrf_token})
878 891
879 892 assert response.status_int == 200
880 893 assert response.body == 'true'
881 894
882 895 # Make sure that after update, it won't raise 500 errors
883 896 response = self.app.get(url(
884 897 controller='pullrequests', action='show',
885 898 repo_name=pr_util.target_repository.repo_name,
886 899 pull_request_id=str(pull_request.pull_request_id)))
887 900
888 901 assert response.status_int == 200
889 902 assert_response = AssertResponse(response)
890 903 assert_response.element_contains(
891 904 '#changeset_compare_view_content .alert strong',
892 905 'Missing commits')
893 906
894 907 def test_branch_is_a_link(self, pr_util):
895 908 pull_request = pr_util.create_pull_request()
896 909 pull_request.source_ref = 'branch:origin:1234567890abcdef'
897 910 pull_request.target_ref = 'branch:target:abcdef1234567890'
898 911 Session().add(pull_request)
899 912 Session().commit()
900 913
901 914 response = self.app.get(url(
902 915 controller='pullrequests', action='show',
903 916 repo_name=pull_request.target_repo.scm_instance().name,
904 917 pull_request_id=str(pull_request.pull_request_id)))
905 918 assert response.status_int == 200
906 919 assert_response = AssertResponse(response)
907 920
908 921 origin = assert_response.get_element('.pr-origininfo .tag')
909 922 origin_children = origin.getchildren()
910 923 assert len(origin_children) == 1
911 924 target = assert_response.get_element('.pr-targetinfo .tag')
912 925 target_children = target.getchildren()
913 926 assert len(target_children) == 1
914 927
915 expected_origin_link = url(
916 'changelog_home',
928 expected_origin_link = route_path(
929 'repo_changelog',
917 930 repo_name=pull_request.source_repo.scm_instance().name,
918 branch='origin')
919 expected_target_link = url(
920 'changelog_home',
931 params=dict(branch='origin'))
932 expected_target_link = route_path(
933 'repo_changelog',
921 934 repo_name=pull_request.target_repo.scm_instance().name,
922 branch='target')
935 params=dict(branch='target'))
923 936 assert origin_children[0].attrib['href'] == expected_origin_link
924 937 assert origin_children[0].text == 'branch: origin'
925 938 assert target_children[0].attrib['href'] == expected_target_link
926 939 assert target_children[0].text == 'branch: target'
927 940
928 941 def test_bookmark_is_not_a_link(self, pr_util):
929 942 pull_request = pr_util.create_pull_request()
930 943 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
931 944 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
932 945 Session().add(pull_request)
933 946 Session().commit()
934 947
935 948 response = self.app.get(url(
936 949 controller='pullrequests', action='show',
937 950 repo_name=pull_request.target_repo.scm_instance().name,
938 951 pull_request_id=str(pull_request.pull_request_id)))
939 952 assert response.status_int == 200
940 953 assert_response = AssertResponse(response)
941 954
942 955 origin = assert_response.get_element('.pr-origininfo .tag')
943 956 assert origin.text.strip() == 'bookmark: origin'
944 957 assert origin.getchildren() == []
945 958
946 959 target = assert_response.get_element('.pr-targetinfo .tag')
947 960 assert target.text.strip() == 'bookmark: target'
948 961 assert target.getchildren() == []
949 962
950 963 def test_tag_is_not_a_link(self, pr_util):
951 964 pull_request = pr_util.create_pull_request()
952 965 pull_request.source_ref = 'tag:origin:1234567890abcdef'
953 966 pull_request.target_ref = 'tag:target:abcdef1234567890'
954 967 Session().add(pull_request)
955 968 Session().commit()
956 969
957 970 response = self.app.get(url(
958 971 controller='pullrequests', action='show',
959 972 repo_name=pull_request.target_repo.scm_instance().name,
960 973 pull_request_id=str(pull_request.pull_request_id)))
961 974 assert response.status_int == 200
962 975 assert_response = AssertResponse(response)
963 976
964 977 origin = assert_response.get_element('.pr-origininfo .tag')
965 978 assert origin.text.strip() == 'tag: origin'
966 979 assert origin.getchildren() == []
967 980
968 981 target = assert_response.get_element('.pr-targetinfo .tag')
969 982 assert target.text.strip() == 'tag: target'
970 983 assert target.getchildren() == []
971 984
972 985 @pytest.mark.parametrize('mergeable', [True, False])
973 986 def test_shadow_repository_link(
974 987 self, mergeable, pr_util, http_host_only_stub):
975 988 """
976 989 Check that the pull request summary page displays a link to the shadow
977 990 repository if the pull request is mergeable. If it is not mergeable
978 991 the link should not be displayed.
979 992 """
980 993 pull_request = pr_util.create_pull_request(
981 994 mergeable=mergeable, enable_notifications=False)
982 995 target_repo = pull_request.target_repo.scm_instance()
983 996 pr_id = pull_request.pull_request_id
984 997 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
985 998 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
986 999
987 1000 response = self.app.get(url(
988 1001 controller='pullrequests', action='show',
989 1002 repo_name=target_repo.name,
990 1003 pull_request_id=str(pr_id)))
991 1004
992 1005 assertr = AssertResponse(response)
993 1006 if mergeable:
994 1007 assertr.element_value_contains(
995 1008 'div.pr-mergeinfo input', shadow_url)
996 1009 assertr.element_value_contains(
997 1010 'div.pr-mergeinfo input', 'pr-merge')
998 1011 else:
999 1012 assertr.no_element_exists('div.pr-mergeinfo')
1000 1013
1001 1014
1002 1015 @pytest.mark.usefixtures('app')
1003 1016 @pytest.mark.backends("git", "hg")
1004 1017 class TestPullrequestsControllerDelete(object):
1005 1018 def test_pull_request_delete_button_permissions_admin(
1006 1019 self, autologin_user, user_admin, pr_util):
1007 1020 pull_request = pr_util.create_pull_request(
1008 1021 author=user_admin.username, enable_notifications=False)
1009 1022
1010 1023 response = self.app.get(url(
1011 1024 controller='pullrequests', action='show',
1012 1025 repo_name=pull_request.target_repo.scm_instance().name,
1013 1026 pull_request_id=str(pull_request.pull_request_id)))
1014 1027
1015 1028 response.mustcontain('id="delete_pullrequest"')
1016 1029 response.mustcontain('Confirm to delete this pull request')
1017 1030
1018 1031 def test_pull_request_delete_button_permissions_owner(
1019 1032 self, autologin_regular_user, user_regular, pr_util):
1020 1033 pull_request = pr_util.create_pull_request(
1021 1034 author=user_regular.username, enable_notifications=False)
1022 1035
1023 1036 response = self.app.get(url(
1024 1037 controller='pullrequests', action='show',
1025 1038 repo_name=pull_request.target_repo.scm_instance().name,
1026 1039 pull_request_id=str(pull_request.pull_request_id)))
1027 1040
1028 1041 response.mustcontain('id="delete_pullrequest"')
1029 1042 response.mustcontain('Confirm to delete this pull request')
1030 1043
1031 1044 def test_pull_request_delete_button_permissions_forbidden(
1032 1045 self, autologin_regular_user, user_regular, user_admin, pr_util):
1033 1046 pull_request = pr_util.create_pull_request(
1034 1047 author=user_admin.username, enable_notifications=False)
1035 1048
1036 1049 response = self.app.get(url(
1037 1050 controller='pullrequests', action='show',
1038 1051 repo_name=pull_request.target_repo.scm_instance().name,
1039 1052 pull_request_id=str(pull_request.pull_request_id)))
1040 1053 response.mustcontain(no=['id="delete_pullrequest"'])
1041 1054 response.mustcontain(no=['Confirm to delete this pull request'])
1042 1055
1043 1056 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1044 1057 self, autologin_regular_user, user_regular, user_admin, pr_util,
1045 1058 user_util):
1046 1059
1047 1060 pull_request = pr_util.create_pull_request(
1048 1061 author=user_admin.username, enable_notifications=False)
1049 1062
1050 1063 user_util.grant_user_permission_to_repo(
1051 1064 pull_request.target_repo, user_regular,
1052 1065 'repository.write')
1053 1066
1054 1067 response = self.app.get(url(
1055 1068 controller='pullrequests', action='show',
1056 1069 repo_name=pull_request.target_repo.scm_instance().name,
1057 1070 pull_request_id=str(pull_request.pull_request_id)))
1058 1071
1059 1072 response.mustcontain('id="open_edit_pullrequest"')
1060 1073 response.mustcontain('id="delete_pullrequest"')
1061 1074 response.mustcontain(no=['Confirm to delete this pull request'])
1062 1075
1063 1076 def test_delete_comment_returns_404_if_comment_does_not_exist(
1064 1077 self, autologin_user, pr_util, user_admin):
1065 1078
1066 1079 pull_request = pr_util.create_pull_request(
1067 1080 author=user_admin.username, enable_notifications=False)
1068 1081
1069 1082 self.app.get(url(
1070 1083 controller='pullrequests', action='delete_comment',
1071 1084 repo_name=pull_request.target_repo.scm_instance().name,
1072 1085 comment_id=1024404), status=404)
1073 1086
1074 1087
1075 1088 def assert_pull_request_status(pull_request, expected_status):
1076 1089 status = ChangesetStatusModel().calculated_review_status(
1077 1090 pull_request=pull_request)
1078 1091 assert status == expected_status
1079 1092
1080 1093
1081 1094 @pytest.mark.parametrize('action', ['index', 'create'])
1082 1095 @pytest.mark.usefixtures("autologin_user")
1083 1096 def test_redirects_to_repo_summary_for_svn_repositories(backend_svn, app, action):
1084 1097 response = app.get(url(
1085 1098 controller='pullrequests', action=action,
1086 1099 repo_name=backend_svn.repo_name))
1087 1100 assert response.status_int == 302
1088 1101
1089 1102 # Not allowed, redirect to the summary
1090 1103 redirected = response.follow()
1091 1104 summary_url = h.route_path('repo_summary', repo_name=backend_svn.repo_name)
1092 1105
1093 1106 # URL adds leading slash and path doesn't have it
1094 1107 assert redirected.request.path == summary_url
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now