##// END OF EJS Templates
compare: migrated code from pylons to pyramid views.
marcink -
r1957:00f3a509 default
parent child Browse files
Show More
@@ -0,0 +1,323 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2012-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 HTTPBadRequest, HTTPNotFound, HTTPFound
25 from pyramid.view import view_config
26 from pyramid.renderers import render
27 from pyramid.response import Response
28
29
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib import diffs, codeblocks
34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 from rhodecode.lib.utils import safe_str
36 from rhodecode.lib.utils2 import safe_unicode, str2bool
37 from rhodecode.lib.vcs.exceptions import (
38 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
39 NodeDoesNotExistError)
40 from rhodecode.model.db import Repository, ChangesetStatus
41
42 log = logging.getLogger(__name__)
43
44
45 class RepoCompareView(RepoAppView):
46 def load_default_context(self):
47 c = self._get_local_tmpl_context(include_app_defaults=True)
48
49 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
50 c.repo_info = self.db_repo
51 c.rhodecode_repo = self.rhodecode_vcs_repo
52
53 self._register_global_c(c)
54 return c
55
56 def _get_commit_or_redirect(
57 self, ref, ref_type, repo, redirect_after=True, partial=False):
58 """
59 This is a safe way to get a commit. If an error occurs it
60 redirects to a commit with a proper message. If partial is set
61 then it does not do redirect raise and throws an exception instead.
62 """
63 _ = self.request.translate
64 try:
65 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
66 except EmptyRepositoryError:
67 if not redirect_after:
68 return repo.scm_instance().EMPTY_COMMIT
69 h.flash(h.literal(_('There are no commits yet')),
70 category='warning')
71 raise HTTPFound(
72 h.route_path('repo_summary', repo_name=repo.repo_name))
73
74 except RepositoryError as e:
75 log.exception(safe_str(e))
76 h.flash(safe_str(h.escape(e)), category='warning')
77 if not partial:
78 raise HTTPFound(
79 h.route_path('repo_summary', repo_name=repo.repo_name))
80 raise HTTPBadRequest()
81
82 @LoginRequired()
83 @HasRepoPermissionAnyDecorator(
84 'repository.read', 'repository.write', 'repository.admin')
85 @view_config(
86 route_name='repo_compare_select', request_method='GET',
87 renderer='rhodecode:templates/compare/compare_diff.mako')
88 def compare_select(self):
89 _ = self.request.translate
90 c = self.load_default_context()
91
92 source_repo = self.db_repo_name
93 target_repo = self.request.GET.get('target_repo', source_repo)
94 c.source_repo = Repository.get_by_repo_name(source_repo)
95 c.target_repo = Repository.get_by_repo_name(target_repo)
96
97 if c.source_repo is None or c.target_repo is None:
98 raise HTTPNotFound()
99
100 c.compare_home = True
101 c.commit_ranges = []
102 c.collapse_all_commits = False
103 c.diffset = None
104 c.limited_diff = False
105 c.source_ref = c.target_ref = _('Select commit')
106 c.source_ref_type = ""
107 c.target_ref_type = ""
108 c.commit_statuses = ChangesetStatus.STATUSES
109 c.preview_mode = False
110 c.file_path = None
111
112 return self._get_template_context(c)
113
114 @LoginRequired()
115 @HasRepoPermissionAnyDecorator(
116 'repository.read', 'repository.write', 'repository.admin')
117 @view_config(
118 route_name='repo_compare', request_method='GET',
119 renderer=None)
120 def compare(self):
121 _ = self.request.translate
122 c = self.load_default_context()
123
124 source_ref_type = self.request.matchdict['source_ref_type']
125 source_ref = self.request.matchdict['source_ref']
126 target_ref_type = self.request.matchdict['target_ref_type']
127 target_ref = self.request.matchdict['target_ref']
128
129 # source_ref will be evaluated in source_repo
130 source_repo_name = self.db_repo_name
131 source_path, source_id = parse_path_ref(source_ref)
132
133 # target_ref will be evaluated in target_repo
134 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
135 target_path, target_id = parse_path_ref(
136 target_ref, default_path=self.request.GET.get('f_path', ''))
137
138 # if merge is True
139 # Show what changes since the shared ancestor commit of target/source
140 # the source would get if it was merged with target. Only commits
141 # which are in target but not in source will be shown.
142 merge = str2bool(self.request.GET.get('merge'))
143 # if merge is False
144 # Show a raw diff of source/target refs even if no ancestor exists
145
146 # c.fulldiff disables cut_off_limit
147 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
148
149 c.file_path = target_path
150 c.commit_statuses = ChangesetStatus.STATUSES
151
152 # if partial, returns just compare_commits.html (commits log)
153 partial = self.request.is_xhr
154
155 # swap url for compare_diff page
156 c.swap_url = h.route_path(
157 'repo_compare',
158 repo_name=target_repo_name,
159 source_ref_type=target_ref_type,
160 source_ref=target_ref,
161 target_repo=source_repo_name,
162 target_ref_type=source_ref_type,
163 target_ref=source_ref,
164 _query=dict(merge=merge and '1' or '', f_path=target_path))
165
166 source_repo = Repository.get_by_repo_name(source_repo_name)
167 target_repo = Repository.get_by_repo_name(target_repo_name)
168
169 if source_repo is None:
170 log.error('Could not find the source repo: {}'
171 .format(source_repo_name))
172 h.flash(_('Could not find the source repo: `{}`')
173 .format(h.escape(source_repo_name)), category='error')
174 raise HTTPFound(
175 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
176
177 if target_repo is None:
178 log.error('Could not find the target repo: {}'
179 .format(source_repo_name))
180 h.flash(_('Could not find the target repo: `{}`')
181 .format(h.escape(target_repo_name)), category='error')
182 raise HTTPFound(
183 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
184
185 source_scm = source_repo.scm_instance()
186 target_scm = target_repo.scm_instance()
187
188 source_alias = source_scm.alias
189 target_alias = target_scm.alias
190 if source_alias != target_alias:
191 msg = _('The comparison of two different kinds of remote repos '
192 'is not available')
193 log.error(msg)
194 h.flash(msg, category='error')
195 raise HTTPFound(
196 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
197
198 source_commit = self._get_commit_or_redirect(
199 ref=source_id, ref_type=source_ref_type, repo=source_repo,
200 partial=partial)
201 target_commit = self._get_commit_or_redirect(
202 ref=target_id, ref_type=target_ref_type, repo=target_repo,
203 partial=partial)
204
205 c.compare_home = False
206 c.source_repo = source_repo
207 c.target_repo = target_repo
208 c.source_ref = source_ref
209 c.target_ref = target_ref
210 c.source_ref_type = source_ref_type
211 c.target_ref_type = target_ref_type
212
213 pre_load = ["author", "branch", "date", "message"]
214 c.ancestor = None
215
216 if c.file_path:
217 if source_commit == target_commit:
218 c.commit_ranges = []
219 else:
220 c.commit_ranges = [target_commit]
221 else:
222 try:
223 c.commit_ranges = source_scm.compare(
224 source_commit.raw_id, target_commit.raw_id,
225 target_scm, merge, pre_load=pre_load)
226 if merge:
227 c.ancestor = source_scm.get_common_ancestor(
228 source_commit.raw_id, target_commit.raw_id, target_scm)
229 except RepositoryRequirementError:
230 msg = _('Could not compare repos with different '
231 'large file settings')
232 log.error(msg)
233 if partial:
234 return Response(msg)
235 h.flash(msg, category='error')
236 raise HTTPFound(
237 h.route_path('repo_compare_select',
238 repo_name=self.db_repo_name))
239
240 c.statuses = self.db_repo.statuses(
241 [x.raw_id for x in c.commit_ranges])
242
243 # auto collapse if we have more than limit
244 collapse_limit = diffs.DiffProcessor._collapse_commits_over
245 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
246
247 if partial: # for PR ajax commits loader
248 if not c.ancestor:
249 return Response('') # cannot merge if there is no ancestor
250
251 html = render(
252 'rhodecode:templates/compare/compare_commits.mako',
253 self._get_template_context(c), self.request)
254 return Response(html)
255
256 if c.ancestor:
257 # case we want a simple diff without incoming commits,
258 # previewing what will be merged.
259 # Make the diff on target repo (which is known to have target_ref)
260 log.debug('Using ancestor %s as source_ref instead of %s'
261 % (c.ancestor, source_ref))
262 source_repo = target_repo
263 source_commit = target_repo.get_commit(commit_id=c.ancestor)
264
265 # diff_limit will cut off the whole diff if the limit is applied
266 # otherwise it will just hide the big files from the front-end
267 diff_limit = c.visual.cut_off_limit_diff
268 file_limit = c.visual.cut_off_limit_file
269
270 log.debug('calculating diff between '
271 'source_ref:%s and target_ref:%s for repo `%s`',
272 source_commit, target_commit,
273 safe_unicode(source_repo.scm_instance().path))
274
275 if source_commit.repository != target_commit.repository:
276 msg = _(
277 "Repositories unrelated. "
278 "Cannot compare commit %(commit1)s from repository %(repo1)s "
279 "with commit %(commit2)s from repository %(repo2)s.") % {
280 'commit1': h.show_id(source_commit),
281 'repo1': source_repo.repo_name,
282 'commit2': h.show_id(target_commit),
283 'repo2': target_repo.repo_name,
284 }
285 h.flash(msg, category='error')
286 raise HTTPFound(
287 h.route_path('repo_compare_select',
288 repo_name=self.db_repo_name))
289
290 txt_diff = source_repo.scm_instance().get_diff(
291 commit1=source_commit, commit2=target_commit,
292 path=target_path, path1=source_path)
293
294 diff_processor = diffs.DiffProcessor(
295 txt_diff, format='newdiff', diff_limit=diff_limit,
296 file_limit=file_limit, show_full_diff=c.fulldiff)
297 _parsed = diff_processor.prepare()
298
299 def _node_getter(commit):
300 """ Returns a function that returns a node for a commit or None """
301 def get_node(fname):
302 try:
303 return commit.get_node(fname)
304 except NodeDoesNotExistError:
305 return None
306 return get_node
307
308 diffset = codeblocks.DiffSet(
309 repo_name=source_repo.repo_name,
310 source_node_getter=_node_getter(source_commit),
311 target_node_getter=_node_getter(target_commit),
312 )
313 c.diffset = diffset.render_patchset(
314 _parsed, source_ref, target_ref)
315
316 c.preview_mode = merge
317 c.source_commit = source_commit
318 c.target_commit = target_commit
319
320 html = render(
321 'rhodecode:templates/compare/compare_diff.mako',
322 self._get_template_context(c), self.request)
323 return Response(html) No newline at end of file
@@ -1,304 +1,312 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 # repo commits
36
35 # Commits
37 36 config.add_route(
38 37 name='repo_commit',
39 38 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
40 39
41 40 config.add_route(
42 41 name='repo_commit_children',
43 42 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
44 43
45 44 config.add_route(
46 45 name='repo_commit_parents',
47 46 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
48 47
49 # still working url for backward compat.
50 config.add_route(
51 name='repo_commit_raw_deprecated',
52 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
53
54 48 config.add_route(
55 49 name='repo_commit_raw',
56 50 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
57 51
58 52 config.add_route(
59 53 name='repo_commit_patch',
60 54 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
61 55
62 56 config.add_route(
63 57 name='repo_commit_download',
64 58 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
65 59
66 60 config.add_route(
67 61 name='repo_commit_data',
68 62 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
69 63
70 64 config.add_route(
71 65 name='repo_commit_comment_create',
72 66 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
73 67
74 68 config.add_route(
75 69 name='repo_commit_comment_preview',
76 70 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
77 71
78 72 config.add_route(
79 73 name='repo_commit_comment_delete',
80 74 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
81 75
82 # repo files
76 # still working url for backward compat.
77 config.add_route(
78 name='repo_commit_raw_deprecated',
79 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
80
81 # Files
83 82 config.add_route(
84 83 name='repo_archivefile',
85 84 pattern='/{repo_name:.*?[^/]}/archive/{fname}', repo_route=True)
86 85
87 86 config.add_route(
88 87 name='repo_files_diff',
89 88 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
90 89 config.add_route( # legacy route to make old links work
91 90 name='repo_files_diff_2way_redirect',
92 91 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
93 92
94 93 config.add_route(
95 94 name='repo_files',
96 95 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
97 96 config.add_route(
98 97 name='repo_files:default_path',
99 98 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
100 99 config.add_route(
101 100 name='repo_files:default_commit',
102 101 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
103 102
104 103 config.add_route(
105 104 name='repo_files:rendered',
106 105 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
107 106
108 107 config.add_route(
109 108 name='repo_files:annotated',
110 109 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
111 110 config.add_route(
112 111 name='repo_files:annotated_previous',
113 112 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
114 113
115 114 config.add_route(
116 115 name='repo_nodetree_full',
117 116 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
118 117 config.add_route(
119 118 name='repo_nodetree_full:default_path',
120 119 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
121 120
122 121 config.add_route(
123 122 name='repo_files_nodelist',
124 123 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
125 124
126 125 config.add_route(
127 126 name='repo_file_raw',
128 127 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
129 128
130 129 config.add_route(
131 130 name='repo_file_download',
132 131 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
133 132 config.add_route( # backward compat to keep old links working
134 133 name='repo_file_download:legacy',
135 134 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
136 135 repo_route=True)
137 136
138 137 config.add_route(
139 138 name='repo_file_history',
140 139 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
141 140
142 141 config.add_route(
143 142 name='repo_file_authors',
144 143 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
145 144
146 145 config.add_route(
147 146 name='repo_files_remove_file',
148 147 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
149 148 repo_route=True)
150 149 config.add_route(
151 150 name='repo_files_delete_file',
152 151 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
153 152 repo_route=True)
154 153 config.add_route(
155 154 name='repo_files_edit_file',
156 155 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
157 156 repo_route=True)
158 157 config.add_route(
159 158 name='repo_files_update_file',
160 159 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
161 160 repo_route=True)
162 161 config.add_route(
163 162 name='repo_files_add_file',
164 163 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
165 164 repo_route=True)
166 165 config.add_route(
167 166 name='repo_files_create_file',
168 167 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
169 168 repo_route=True)
170 169
171 # refs data
170 # Refs data
172 171 config.add_route(
173 172 name='repo_refs_data',
174 173 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
175 174
176 175 config.add_route(
177 176 name='repo_refs_changelog_data',
178 177 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
179 178
180 179 config.add_route(
181 180 name='repo_stats',
182 181 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
183 182
184 183 # Changelog
185 184 config.add_route(
186 185 name='repo_changelog',
187 186 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
188 187 config.add_route(
189 188 name='repo_changelog_file',
190 189 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
191 190 config.add_route(
192 191 name='repo_changelog_elements',
193 192 pattern='/{repo_name:.*?[^/]}/changelog_elements', repo_route=True)
194 193
194 # Compare
195 config.add_route(
196 name='repo_compare_select',
197 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
198
199 config.add_route(
200 name='repo_compare',
201 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
202
195 203 # Tags
196 204 config.add_route(
197 205 name='tags_home',
198 206 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
199 207
200 208 # Branches
201 209 config.add_route(
202 210 name='branches_home',
203 211 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
204 212
205 213 config.add_route(
206 214 name='bookmarks_home',
207 215 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
208 216
209 217 # Pull Requests
210 218 config.add_route(
211 219 name='pullrequest_show',
212 220 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}',
213 221 repo_route=True)
214 222
215 223 config.add_route(
216 224 name='pullrequest_show_all',
217 225 pattern='/{repo_name:.*?[^/]}/pull-request',
218 226 repo_route=True, repo_accepted_types=['hg', 'git'])
219 227
220 228 config.add_route(
221 229 name='pullrequest_show_all_data',
222 230 pattern='/{repo_name:.*?[^/]}/pull-request-data',
223 231 repo_route=True, repo_accepted_types=['hg', 'git'])
224 232
225 233 # Settings
226 234 config.add_route(
227 235 name='edit_repo',
228 236 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
229 237
230 238 # Settings advanced
231 239 config.add_route(
232 240 name='edit_repo_advanced',
233 241 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
234 242 config.add_route(
235 243 name='edit_repo_advanced_delete',
236 244 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
237 245 config.add_route(
238 246 name='edit_repo_advanced_locking',
239 247 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
240 248 config.add_route(
241 249 name='edit_repo_advanced_journal',
242 250 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
243 251 config.add_route(
244 252 name='edit_repo_advanced_fork',
245 253 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
246 254
247 255 # Caches
248 256 config.add_route(
249 257 name='edit_repo_caches',
250 258 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
251 259
252 260 # Permissions
253 261 config.add_route(
254 262 name='edit_repo_perms',
255 263 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
256 264
257 265 # Repo Review Rules
258 266 config.add_route(
259 267 name='repo_reviewers',
260 268 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
261 269
262 270 config.add_route(
263 271 name='repo_default_reviewers_data',
264 272 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
265 273
266 274 # Maintenance
267 275 config.add_route(
268 276 name='repo_maintenance',
269 277 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
270 278
271 279 config.add_route(
272 280 name='repo_maintenance_execute',
273 281 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
274 282
275 283 # Strip
276 284 config.add_route(
277 285 name='strip',
278 286 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
279 287
280 288 config.add_route(
281 289 name='strip_check',
282 290 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
283 291
284 292 config.add_route(
285 293 name='strip_execute',
286 294 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
287 295
288 296 # ATOM/RSS Feed
289 297 config.add_route(
290 298 name='rss_feed_home',
291 299 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
292 300
293 301 config.add_route(
294 302 name='atom_feed_home',
295 303 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
296 304
297 305 # NOTE(marcink): needs to be at the end for catch-all
298 306 add_route_with_slash(
299 307 config,
300 308 name='repo_summary',
301 309 pattern='/{repo_name:.*?[^/]}', repo_route=True)
302 310
303 311 # Scan module for configuration decorators.
304 312 config.scan()
@@ -1,675 +1,697 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 import lxml.html
24 24
25 25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 from rhodecode.tests import url, assert_session_flash
26 from rhodecode.tests import assert_session_flash
27 27 from rhodecode.tests.utils import AssertResponse, commit_change
28 28
29 29
30 def route_path(name, params=None, **kwargs):
31 import urllib
32
33 base_url = {
34 'repo_compare_select': '/{repo_name}/compare',
35 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
36 }[name].format(**kwargs)
37
38 if params:
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
40 return base_url
41
42
30 43 @pytest.mark.usefixtures("autologin_user", "app")
31 class TestCompareController(object):
44 class TestCompareView(object):
45
46 def test_compare_index_is_reached_at_least_once(self, backend):
47 repo = backend.repo
48 self.app.get(
49 route_path('repo_compare_select', repo_name=repo.repo_name))
32 50
33 51 @pytest.mark.xfail_backends("svn", reason="Requires pull")
34 52 def test_compare_remote_with_different_commit_indexes(self, backend):
35 53 # Preparing the following repository structure:
36 54 #
37 55 # Origin repository has two commits:
38 56 #
39 57 # 0 1
40 58 # A -- D
41 59 #
42 60 # The fork of it has a few more commits and "D" has a commit index
43 61 # which does not exist in origin.
44 62 #
45 63 # 0 1 2 3 4
46 64 # A -- -- -- D -- E
47 65 # \- B -- C
48 66 #
49 67
50 68 fork = backend.create_repo()
51 69
52 70 # prepare fork
53 71 commit0 = commit_change(
54 72 fork.repo_name, filename='file1', content='A',
55 73 message='A', vcs_type=backend.alias, parent=None, newfile=True)
56 74
57 75 commit1 = commit_change(
58 76 fork.repo_name, filename='file1', content='B',
59 77 message='B, child of A', vcs_type=backend.alias, parent=commit0)
60 78
61 79 commit_change( # commit 2
62 80 fork.repo_name, filename='file1', content='C',
63 81 message='C, child of B', vcs_type=backend.alias, parent=commit1)
64 82
65 83 commit3 = commit_change(
66 84 fork.repo_name, filename='file1', content='D',
67 85 message='D, child of A', vcs_type=backend.alias, parent=commit0)
68 86
69 87 commit4 = commit_change(
70 88 fork.repo_name, filename='file1', content='E',
71 89 message='E, child of D', vcs_type=backend.alias, parent=commit3)
72 90
73 91 # prepare origin repository, taking just the history up to D
74 92 origin = backend.create_repo()
75 93
76 94 origin_repo = origin.scm_instance(cache=False)
77 95 origin_repo.config.clear_section('hooks')
78 96 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
79 97
80 98 # Verify test fixture setup
81 99 # This does not work for git
82 100 if backend.alias != 'git':
83 101 assert 5 == len(fork.scm_instance().commit_ids)
84 102 assert 2 == len(origin_repo.commit_ids)
85 103
86 104 # Comparing the revisions
87 105 response = self.app.get(
88 url('compare_url',
106 route_path('repo_compare',
89 107 repo_name=origin.repo_name,
90 108 source_ref_type="rev",
91 109 source_ref=commit3.raw_id,
92 target_repo=fork.repo_name,
93 110 target_ref_type="rev",
94 111 target_ref=commit4.raw_id,
95 merge='1',))
112 params=dict(merge='1', target_repo=fork.repo_name)
113 ))
96 114
97 115 compare_page = ComparePage(response)
98 116 compare_page.contains_commits([commit4])
99 117
100 118 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
101 119 def test_compare_forks_on_branch_extra_commits(self, backend):
102 120 repo1 = backend.create_repo()
103 121
104 122 # commit something !
105 123 commit0 = commit_change(
106 124 repo1.repo_name, filename='file1', content='line1\n',
107 125 message='commit1', vcs_type=backend.alias, parent=None,
108 126 newfile=True)
109 127
110 128 # fork this repo
111 129 repo2 = backend.create_fork()
112 130
113 131 # add two extra commit into fork
114 132 commit1 = commit_change(
115 133 repo2.repo_name, filename='file1', content='line1\nline2\n',
116 134 message='commit2', vcs_type=backend.alias, parent=commit0)
117 135
118 136 commit2 = commit_change(
119 137 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
120 138 message='commit3', vcs_type=backend.alias, parent=commit1)
121 139
122 140 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
123 141 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
124 142
125 143 response = self.app.get(
126 url('compare_url',
144 route_path('repo_compare',
127 145 repo_name=repo1.repo_name,
128 146 source_ref_type="branch",
129 147 source_ref=commit_id2,
130 target_repo=repo2.repo_name,
131 148 target_ref_type="branch",
132 149 target_ref=commit_id1,
133 merge='1',))
150 params=dict(merge='1', target_repo=repo2.repo_name)
151 ))
134 152
135 153 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
136 154 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
137 155
138 156 compare_page = ComparePage(response)
139 157 compare_page.contains_change_summary(1, 2, 0)
140 158 compare_page.contains_commits([commit1, commit2])
141 159 compare_page.contains_file_links_and_anchors([
142 160 ('file1', 'a_c--826e8142e6ba'),
143 161 ])
144 162
145 163 # Swap is removed when comparing branches since it's a PR feature and
146 164 # it is then a preview mode
147 165 compare_page.swap_is_hidden()
148 166 compare_page.target_source_are_disabled()
149 167
150 168 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
151 169 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(
152 170 self, backend):
153 171 repo1 = backend.create_repo()
154 172
155 173 # commit something !
156 174 commit0 = commit_change(
157 175 repo1.repo_name, filename='file1', content='line1\n',
158 176 message='commit1', vcs_type=backend.alias, parent=None,
159 177 newfile=True)
160 178
161 179 # fork this repo
162 180 repo2 = backend.create_fork()
163 181
164 182 # now commit something to origin repo
165 183 commit_change(
166 184 repo1.repo_name, filename='file2', content='line1file2\n',
167 185 message='commit2', vcs_type=backend.alias, parent=commit0,
168 186 newfile=True)
169 187
170 188 # add two extra commit into fork
171 189 commit1 = commit_change(
172 190 repo2.repo_name, filename='file1', content='line1\nline2\n',
173 191 message='commit2', vcs_type=backend.alias, parent=commit0)
174 192
175 193 commit2 = commit_change(
176 194 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
177 195 message='commit3', vcs_type=backend.alias, parent=commit1)
178 196
179 197 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
180 198 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
181 199
182 200 response = self.app.get(
183 url('compare_url',
201 route_path('repo_compare',
184 202 repo_name=repo1.repo_name,
185 203 source_ref_type="branch",
186 204 source_ref=commit_id2,
187 target_repo=repo2.repo_name,
188 205 target_ref_type="branch",
189 206 target_ref=commit_id1,
190 merge='1'))
207 params=dict(merge='1', target_repo=repo2.repo_name),
208 ))
191 209
192 210 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
193 211 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
194 212
195 213 compare_page = ComparePage(response)
196 214 compare_page.contains_change_summary(1, 2, 0)
197 215 compare_page.contains_commits([commit1, commit2])
198 216 compare_page.contains_file_links_and_anchors([
199 217 ('file1', 'a_c--826e8142e6ba'),
200 218 ])
201 219
202 220 # Swap is removed when comparing branches since it's a PR feature and
203 221 # it is then a preview mode
204 222 compare_page.swap_is_hidden()
205 223 compare_page.target_source_are_disabled()
206 224
207 225 @pytest.mark.xfail_backends("svn")
208 226 # TODO(marcink): no svn support for compare two seperate repos
209 227 def test_compare_of_unrelated_forks(self, backend):
210 228 orig = backend.create_repo(number_of_commits=1)
211 229 fork = backend.create_repo(number_of_commits=1)
212 230
213 231 response = self.app.get(
214 url('compare_url',
232 route_path('repo_compare',
215 233 repo_name=orig.repo_name,
216 action="compare",
217 234 source_ref_type="rev",
218 235 source_ref="tip",
219 236 target_ref_type="rev",
220 237 target_ref="tip",
221 merge='1',
222 target_repo=fork.repo_name),
223 status=400)
224
238 params=dict(merge='1', target_repo=fork.repo_name),
239 ),
240 status=302)
241 response = response.follow()
225 242 response.mustcontain("Repositories unrelated.")
226 243
227 244 @pytest.mark.xfail_backends("svn")
228 245 def test_compare_cherry_pick_commits_from_bottom(self, backend):
229 246
230 247 # repo1:
231 248 # commit0:
232 249 # commit1:
233 250 # repo1-fork- in which we will cherry pick bottom commits
234 251 # commit0:
235 252 # commit1:
236 253 # commit2: x
237 254 # commit3: x
238 255 # commit4: x
239 256 # commit5:
240 257 # make repo1, and commit1+commit2
241 258
242 259 repo1 = backend.create_repo()
243 260
244 261 # commit something !
245 262 commit0 = commit_change(
246 263 repo1.repo_name, filename='file1', content='line1\n',
247 264 message='commit1', vcs_type=backend.alias, parent=None,
248 265 newfile=True)
249 266 commit1 = commit_change(
250 267 repo1.repo_name, filename='file1', content='line1\nline2\n',
251 268 message='commit2', vcs_type=backend.alias, parent=commit0)
252 269
253 270 # fork this repo
254 271 repo2 = backend.create_fork()
255 272
256 273 # now make commit3-6
257 274 commit2 = commit_change(
258 275 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
259 276 message='commit3', vcs_type=backend.alias, parent=commit1)
260 277 commit3 = commit_change(
261 278 repo1.repo_name, filename='file1',
262 279 content='line1\nline2\nline3\nline4\n', message='commit4',
263 280 vcs_type=backend.alias, parent=commit2)
264 281 commit4 = commit_change(
265 282 repo1.repo_name, filename='file1',
266 283 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
267 284 vcs_type=backend.alias, parent=commit3)
268 285 commit_change( # commit 5
269 286 repo1.repo_name, filename='file1',
270 287 content='line1\nline2\nline3\nline4\nline5\nline6\n',
271 288 message='commit6', vcs_type=backend.alias, parent=commit4)
272 289
273 290 response = self.app.get(
274 url('compare_url',
291 route_path('repo_compare',
275 292 repo_name=repo2.repo_name,
276 293 source_ref_type="rev",
277 294 # parent of commit2, in target repo2
278 295 source_ref=commit1.raw_id,
279 target_repo=repo1.repo_name,
280 296 target_ref_type="rev",
281 297 target_ref=commit4.raw_id,
282 merge='1',))
298 params=dict(merge='1', target_repo=repo1.repo_name),
299 ))
283 300 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
284 301 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
285 302
286 303 # files
287 304 compare_page = ComparePage(response)
288 305 compare_page.contains_change_summary(1, 3, 0)
289 306 compare_page.contains_commits([commit2, commit3, commit4])
290 307 compare_page.contains_file_links_and_anchors([
291 308 ('file1', 'a_c--826e8142e6ba'),
292 309 ])
293 310
294 311 @pytest.mark.xfail_backends("svn")
295 312 def test_compare_cherry_pick_commits_from_top(self, backend):
296 313 # repo1:
297 314 # commit0:
298 315 # commit1:
299 316 # repo1-fork- in which we will cherry pick bottom commits
300 317 # commit0:
301 318 # commit1:
302 319 # commit2:
303 320 # commit3: x
304 321 # commit4: x
305 322 # commit5: x
306 323
307 324 # make repo1, and commit1+commit2
308 325 repo1 = backend.create_repo()
309 326
310 327 # commit something !
311 328 commit0 = commit_change(
312 329 repo1.repo_name, filename='file1', content='line1\n',
313 330 message='commit1', vcs_type=backend.alias, parent=None,
314 331 newfile=True)
315 332 commit1 = commit_change(
316 333 repo1.repo_name, filename='file1', content='line1\nline2\n',
317 334 message='commit2', vcs_type=backend.alias, parent=commit0)
318 335
319 336 # fork this repo
320 337 backend.create_fork()
321 338
322 339 # now make commit3-6
323 340 commit2 = commit_change(
324 341 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
325 342 message='commit3', vcs_type=backend.alias, parent=commit1)
326 343 commit3 = commit_change(
327 344 repo1.repo_name, filename='file1',
328 345 content='line1\nline2\nline3\nline4\n', message='commit4',
329 346 vcs_type=backend.alias, parent=commit2)
330 347 commit4 = commit_change(
331 348 repo1.repo_name, filename='file1',
332 349 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
333 350 vcs_type=backend.alias, parent=commit3)
334 351 commit5 = commit_change(
335 352 repo1.repo_name, filename='file1',
336 353 content='line1\nline2\nline3\nline4\nline5\nline6\n',
337 354 message='commit6', vcs_type=backend.alias, parent=commit4)
338 355
339 356 response = self.app.get(
340 url('compare_url',
357 route_path('repo_compare',
341 358 repo_name=repo1.repo_name,
342 359 source_ref_type="rev",
343 360 # parent of commit3, not in source repo2
344 361 source_ref=commit2.raw_id,
345 362 target_ref_type="rev",
346 363 target_ref=commit5.raw_id,
347 merge='1',))
364 params=dict(merge='1'),
365 ))
348 366
349 367 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
350 368 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
351 369
352 370 compare_page = ComparePage(response)
353 371 compare_page.contains_change_summary(1, 3, 0)
354 372 compare_page.contains_commits([commit3, commit4, commit5])
355 373
356 374 # files
357 375 compare_page.contains_file_links_and_anchors([
358 376 ('file1', 'a_c--826e8142e6ba'),
359 377 ])
360 378
361 379 @pytest.mark.xfail_backends("svn")
362 380 def test_compare_remote_branches(self, backend):
363 381 repo1 = backend.repo
364 382 repo2 = backend.create_fork()
365 383
366 384 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
367 385 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
368 386
369 387 response = self.app.get(
370 url('compare_url',
388 route_path('repo_compare',
371 389 repo_name=repo1.repo_name,
372 390 source_ref_type="rev",
373 391 source_ref=commit_id1,
374 392 target_ref_type="rev",
375 393 target_ref=commit_id2,
376 target_repo=repo2.repo_name,
377 merge='1',))
394 params=dict(merge='1', target_repo=repo2.repo_name),
395 ))
378 396
379 397 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
380 398 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
381 399
382 400 compare_page = ComparePage(response)
383 401
384 402 # outgoing commits between those commits
385 403 compare_page.contains_commits(
386 404 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
387 405
388 406 # files
389 407 compare_page.contains_file_links_and_anchors([
390 408 ('vcs/backends/hg.py', 'a_c--9c390eb52cd6'),
391 409 ('vcs/backends/__init__.py', 'a_c--41b41c1f2796'),
392 410 ('vcs/backends/base.py', 'a_c--2f574d260608'),
393 411 ])
394 412
395 413 @pytest.mark.xfail_backends("svn")
396 414 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
397 415 repo1 = backend.create_repo()
398 416 r1_name = repo1.repo_name
399 417
400 418 commit0 = commit_change(
401 419 repo=r1_name, filename='file1',
402 420 content='line1', message='commit1', vcs_type=backend.alias,
403 421 newfile=True)
404 422 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
405 423
406 424 # fork the repo1
407 425 repo2 = backend.create_fork()
408 426 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
409 427
410 428 self.r2_id = repo2.repo_id
411 429 r2_name = repo2.repo_name
412 430
413 431 commit1 = commit_change(
414 432 repo=r2_name, filename='file1-fork',
415 433 content='file1-line1-from-fork', message='commit1-fork',
416 434 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
417 435 newfile=True)
418 436
419 437 commit2 = commit_change(
420 438 repo=r2_name, filename='file2-fork',
421 439 content='file2-line1-from-fork', message='commit2-fork',
422 440 vcs_type=backend.alias, parent=commit1,
423 441 newfile=True)
424 442
425 443 commit_change( # commit 3
426 444 repo=r2_name, filename='file3-fork',
427 445 content='file3-line1-from-fork', message='commit3-fork',
428 446 vcs_type=backend.alias, parent=commit2, newfile=True)
429 447
430 448 # compare !
431 449 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
432 450 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
433 451
434 452 response = self.app.get(
435 url('compare_url',
453 route_path('repo_compare',
436 454 repo_name=r2_name,
437 455 source_ref_type="branch",
438 456 source_ref=commit_id1,
439 457 target_ref_type="branch",
440 458 target_ref=commit_id2,
441 target_repo=r1_name,
442 merge='1',))
459 params=dict(merge='1', target_repo=r1_name),
460 ))
443 461
444 462 response.mustcontain('%s@%s' % (r2_name, commit_id1))
445 463 response.mustcontain('%s@%s' % (r1_name, commit_id2))
446 464 response.mustcontain('No files')
447 465 response.mustcontain('No commits in this compare')
448 466
449 467 commit0 = commit_change(
450 468 repo=r1_name, filename='file2',
451 469 content='line1-added-after-fork', message='commit2-parent',
452 470 vcs_type=backend.alias, parent=None, newfile=True)
453 471
454 472 # compare !
455 473 response = self.app.get(
456 url('compare_url',
474 route_path('repo_compare',
457 475 repo_name=r2_name,
458 476 source_ref_type="branch",
459 477 source_ref=commit_id1,
460 478 target_ref_type="branch",
461 479 target_ref=commit_id2,
462 target_repo=r1_name,
463 merge='1',))
480 params=dict(merge='1', target_repo=r1_name),
481 ))
464 482
465 483 response.mustcontain('%s@%s' % (r2_name, commit_id1))
466 484 response.mustcontain('%s@%s' % (r1_name, commit_id2))
467 485
468 486 response.mustcontain("""commit2-parent""")
469 487 response.mustcontain("""line1-added-after-fork""")
470 488 compare_page = ComparePage(response)
471 489 compare_page.contains_change_summary(1, 1, 0)
472 490
473 491 @pytest.mark.xfail_backends("svn")
474 492 def test_compare_commits(self, backend, xhr_header):
475 493 commit0 = backend.repo.get_commit(commit_idx=0)
476 494 commit1 = backend.repo.get_commit(commit_idx=1)
477 495
478 496 response = self.app.get(
479 url('compare_url',
497 route_path('repo_compare',
480 498 repo_name=backend.repo_name,
481 499 source_ref_type="rev",
482 500 source_ref=commit0.raw_id,
483 501 target_ref_type="rev",
484 502 target_ref=commit1.raw_id,
485 merge='1',),
503 params=dict(merge='1')
504 ),
486 505 extra_environ=xhr_header,)
487 506
488 507 # outgoing commits between those commits
489 508 compare_page = ComparePage(response)
490 509 compare_page.contains_commits(commits=[commit1], ancestors=[commit0])
491 510
492 511 def test_errors_when_comparing_unknown_source_repo(self, backend):
493 512 repo = backend.repo
494 513 badrepo = 'badrepo'
495 514
496 515 response = self.app.get(
497 url('compare_url',
516 route_path('repo_compare',
498 517 repo_name=badrepo,
499 518 source_ref_type="rev",
500 519 source_ref='tip',
501 520 target_ref_type="rev",
502 521 target_ref='tip',
503 target_repo=repo.repo_name,
504 merge='1',),
522 params=dict(merge='1', target_repo=repo.repo_name)
523 ),
505 524 status=404)
506 525
507 526 def test_errors_when_comparing_unknown_target_repo(self, backend):
508 527 repo = backend.repo
509 528 badrepo = 'badrepo'
510 529
511 530 response = self.app.get(
512 url('compare_url',
531 route_path('repo_compare',
513 532 repo_name=repo.repo_name,
514 533 source_ref_type="rev",
515 534 source_ref='tip',
516 535 target_ref_type="rev",
517 536 target_ref='tip',
518 target_repo=badrepo,
519 merge='1',),
537 params=dict(merge='1', target_repo=badrepo),
538 ),
520 539 status=302)
521 540 redirected = response.follow()
522 541 redirected.mustcontain(
523 542 'Could not find the target repo: `{}`'.format(badrepo))
524 543
525 544 def test_compare_not_in_preview_mode(self, backend_stub):
526 545 commit0 = backend_stub.repo.get_commit(commit_idx=0)
527 546 commit1 = backend_stub.repo.get_commit(commit_idx=1)
528 547
529 response = self.app.get(url('compare_url',
530 repo_name=backend_stub.repo_name,
531 source_ref_type="rev",
532 source_ref=commit0.raw_id,
533 target_ref_type="rev",
534 target_ref=commit1.raw_id,
535 ),)
548 response = self.app.get(
549 route_path('repo_compare',
550 repo_name=backend_stub.repo_name,
551 source_ref_type="rev",
552 source_ref=commit0.raw_id,
553 target_ref_type="rev",
554 target_ref=commit1.raw_id,
555 ))
536 556
537 557 # outgoing commits between those commits
538 558 compare_page = ComparePage(response)
539 559 compare_page.swap_is_visible()
540 560 compare_page.target_source_are_enabled()
541 561
542 562 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
543 563 orig = backend_hg.create_repo(number_of_commits=1)
544 564 fork = backend_hg.create_fork()
545 565
546 566 settings_util.create_repo_rhodecode_ui(
547 567 orig, 'extensions', value='', key='largefiles', active=False)
548 568 settings_util.create_repo_rhodecode_ui(
549 569 fork, 'extensions', value='', key='largefiles', active=True)
550 570
551 571 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
552 572 'MercurialRepository.compare')
553 573 with mock.patch(compare_module) as compare_mock:
554 574 compare_mock.side_effect = RepositoryRequirementError()
555 575
556 576 response = self.app.get(
557 url('compare_url',
577 route_path('repo_compare',
558 578 repo_name=orig.repo_name,
559 action="compare",
560 579 source_ref_type="rev",
561 580 source_ref="tip",
562 581 target_ref_type="rev",
563 582 target_ref="tip",
564 merge='1',
565 target_repo=fork.repo_name),
583 params=dict(merge='1', target_repo=fork.repo_name),
584 ),
566 585 status=302)
567 586
568 587 assert_session_flash(
569 588 response,
570 589 'Could not compare repos with different large file settings')
571 590
572 591
573 592 @pytest.mark.usefixtures("autologin_user")
574 593 class TestCompareControllerSvn(object):
575 594
576 595 def test_supports_references_with_path(self, app, backend_svn):
577 596 repo = backend_svn['svn-simple-layout']
578 597 commit_id = repo.get_commit(commit_idx=-1).raw_id
579 598 response = app.get(
580 url('compare_url',
599 route_path('repo_compare',
581 600 repo_name=repo.repo_name,
582 601 source_ref_type="tag",
583 602 source_ref="%s@%s" % ('tags/v0.1', commit_id),
584 603 target_ref_type="tag",
585 604 target_ref="%s@%s" % ('tags/v0.2', commit_id),
586 merge='1',),
605 params=dict(merge='1'),
606 ),
587 607 status=200)
588 608
589 609 # Expecting no commits, since both paths are at the same revision
590 610 response.mustcontain('No commits in this compare')
591 611
592 612 # Should find only one file changed when comparing those two tags
593 613 response.mustcontain('example.py')
594 614 compare_page = ComparePage(response)
595 615 compare_page.contains_change_summary(1, 5, 1)
596 616
597 617 def test_shows_commits_if_different_ids(self, app, backend_svn):
598 618 repo = backend_svn['svn-simple-layout']
599 619 source_id = repo.get_commit(commit_idx=-6).raw_id
600 620 target_id = repo.get_commit(commit_idx=-1).raw_id
601 621 response = app.get(
602 url('compare_url',
622 route_path('repo_compare',
603 623 repo_name=repo.repo_name,
604 624 source_ref_type="tag",
605 625 source_ref="%s@%s" % ('tags/v0.1', source_id),
606 626 target_ref_type="tag",
607 627 target_ref="%s@%s" % ('tags/v0.2', target_id),
608 merge='1',),
628 params=dict(merge='1')
629 ),
609 630 status=200)
610 631
611 632 # It should show commits
612 633 assert 'No commits in this compare' not in response.body
613 634
614 635 # Should find only one file changed when comparing those two tags
615 636 response.mustcontain('example.py')
616 637 compare_page = ComparePage(response)
617 638 compare_page.contains_change_summary(1, 5, 1)
618 639
619 640
620 641 class ComparePage(AssertResponse):
621 642 """
622 643 Abstracts the page template from the tests
623 644 """
624 645
625 646 def contains_file_links_and_anchors(self, files):
626 647 doc = lxml.html.fromstring(self.response.body)
627 648 for filename, file_id in files:
628 649 self.contains_one_anchor(file_id)
629 650 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
630 651 assert len(diffblock) == 1
631 652 assert len(diffblock[0].cssselect('a[href="#%s"]' % file_id)) == 1
632 653
633 654 def contains_change_summary(self, files_changed, inserted, deleted):
634 655 template = (
635 656 "{files_changed} file{plural} changed: "
636 657 "{inserted} inserted, {deleted} deleted")
637 658 self.response.mustcontain(template.format(
638 659 files_changed=files_changed,
639 660 plural="s" if files_changed > 1 else "",
640 661 inserted=inserted,
641 662 deleted=deleted))
642 663
643 664 def contains_commits(self, commits, ancestors=None):
644 665 response = self.response
645 666
646 667 for commit in commits:
647 668 # Expecting to see the commit message in an element which
648 669 # has the ID "c-{commit.raw_id}"
649 670 self.element_contains('#c-' + commit.raw_id, commit.message)
650 671 self.contains_one_link(
651 672 'r%s:%s' % (commit.idx, commit.short_id),
652 673 self._commit_url(commit))
653 674 if ancestors:
654 675 response.mustcontain('Ancestor')
655 676 for ancestor in ancestors:
656 677 self.contains_one_link(
657 678 ancestor.short_id, self._commit_url(ancestor))
658 679
659 680 def _commit_url(self, commit):
660 681 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
661 682
662 683 def swap_is_hidden(self):
663 684 assert '<a id="btn-swap"' not in self.response.text
664 685
665 686 def swap_is_visible(self):
666 687 assert '<a id="btn-swap"' in self.response.text
667 688
668 689 def target_source_are_disabled(self):
669 690 response = self.response
670 691 response.mustcontain("var enable_fields = false;")
671 692 response.mustcontain('.select2("enable", enable_fields)')
672 693
673 694 def target_source_are_enabled(self):
674 695 response = self.response
675 696 response.mustcontain("var enable_fields = true;")
697
@@ -1,148 +1,163 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 from rhodecode.tests import url
24 from rhodecode.tests.functional.test_compare import ComparePage
23 from .test_repo_compare import ComparePage
24
25
26 def route_path(name, params=None, **kwargs):
27 import urllib
28
29 base_url = {
30 'repo_compare_select': '/{repo_name}/compare',
31 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
32 }[name].format(**kwargs)
33
34 if params:
35 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
36 return base_url
25 37
26 38
27 39 @pytest.mark.usefixtures("autologin_user", "app")
28 class TestCompareController:
40 class TestCompareView(object):
29 41
30 42 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
31 43 def test_compare_tag(self, backend):
32 44 tag1 = 'v0.1.2'
33 45 tag2 = 'v0.1.3'
34 46 response = self.app.get(
35 url(
36 'compare_url',
47 route_path(
48 'repo_compare',
37 49 repo_name=backend.repo_name,
38 50 source_ref_type="tag",
39 51 source_ref=tag1,
40 52 target_ref_type="tag",
41 53 target_ref=tag2),
42 54 status=200)
43 55
44 56 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
45 57 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
46 58
47 59 # outgoing commits between tags
48 60 commit_indexes = {
49 61 'git': [113] + range(115, 121),
50 62 'hg': [112] + range(115, 121),
51 63 }
52 64 repo = backend.repo
53 65 commits = (repo.get_commit(commit_idx=idx)
54 66 for idx in commit_indexes[backend.alias])
55 67 compare_page = ComparePage(response)
56 68 compare_page.contains_change_summary(11, 94, 64)
57 69 compare_page.contains_commits(commits)
58 70
59 71 # files diff
60 72 compare_page.contains_file_links_and_anchors([
61 73 ('docs/api/utils/index.rst', 'a_c--1c5cf9e91c12'),
62 74 ('test_and_report.sh', 'a_c--e3305437df55'),
63 75 ('.hgignore', 'a_c--c8e92ef85cd1'),
64 76 ('.hgtags', 'a_c--6e08b694d687'),
65 77 ('docs/api/index.rst', 'a_c--2c14b00f3393'),
66 78 ('vcs/__init__.py', 'a_c--430ccbc82bdf'),
67 79 ('vcs/backends/hg.py', 'a_c--9c390eb52cd6'),
68 80 ('vcs/utils/__init__.py', 'a_c--ebb592c595c0'),
69 81 ('vcs/utils/annotate.py', 'a_c--7abc741b5052'),
70 82 ('vcs/utils/diffs.py', 'a_c--2ef0ef106c56'),
71 83 ('vcs/utils/lazy.py', 'a_c--3150cb87d4b7'),
72 84 ])
73 85
74 86 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
75 87 def test_compare_tag_branch(self, backend):
76 88 revisions = {
77 89 'hg': {
78 90 'tag': 'v0.2.0',
79 91 'branch': 'default',
80 92 'response': (147, 5701, 10177)
81 93 },
82 94 'git': {
83 95 'tag': 'v0.2.2',
84 96 'branch': 'master',
85 97 'response': (71, 2269, 3416)
86 98 },
87 99 }
88 100
89 101 # Backend specific data, depends on the test repository for
90 102 # functional tests.
91 103 data = revisions[backend.alias]
92 104
93 response = self.app.get(url(
94 'compare_url',
105 response = self.app.get(
106 route_path(
107 'repo_compare',
95 108 repo_name=backend.repo_name,
96 109 source_ref_type='branch',
97 110 source_ref=data['branch'],
98 111 target_ref_type="tag",
99 112 target_ref=data['tag'],
100 113 ))
101 114
102 115 response.mustcontain('%s@%s' % (backend.repo_name, data['branch']))
103 116 response.mustcontain('%s@%s' % (backend.repo_name, data['tag']))
104 117 compare_page = ComparePage(response)
105 118 compare_page.contains_change_summary(*data['response'])
106 119
107 120 def test_index_branch(self, backend):
108 121 head_id = backend.default_head_id
109 response = self.app.get(url(
110 'compare_url',
122 response = self.app.get(
123 route_path(
124 'repo_compare',
111 125 repo_name=backend.repo_name,
112 126 source_ref_type="branch",
113 127 source_ref=head_id,
114 128 target_ref_type="branch",
115 129 target_ref=head_id,
116 130 ))
117 131
118 132 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
119 133
120 134 # branches are equal
121 135 response.mustcontain('No files')
122 136 response.mustcontain('No commits in this compare')
123 137
124 138 def test_compare_commits(self, backend):
125 139 repo = backend.repo
126 140 commit1 = repo.get_commit(commit_idx=0)
127 141 commit2 = repo.get_commit(commit_idx=1)
128 142
129 response = self.app.get(url(
130 'compare_url',
143 response = self.app.get(
144 route_path(
145 'repo_compare',
131 146 repo_name=backend.repo_name,
132 147 source_ref_type="rev",
133 148 source_ref=commit1.raw_id,
134 149 target_ref_type="rev",
135 150 target_ref=commit2.raw_id,
136 151 ))
137 152 response.mustcontain('%s@%s' % (backend.repo_name, commit1.raw_id))
138 153 response.mustcontain('%s@%s' % (backend.repo_name, commit2.raw_id))
139 154 compare_page = ComparePage(response)
140 155
141 156 # files
142 157 compare_page.contains_change_summary(1, 7, 0)
143 158
144 159 # outgoing commits between those commits
145 160 compare_page.contains_commits([commit2])
146 161 compare_page.contains_file_links_and_anchors([
147 162 ('.hgignore', 'a_c--c8e92ef85cd1'),
148 163 ])
@@ -1,179 +1,183 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.lib.vcs import nodes
24 from rhodecode.tests import url
25 24 from rhodecode.tests.fixture import Fixture
26 25 from rhodecode.tests.utils import commit_change
27 26
28 27 fixture = Fixture()
29 28
30 29
30 def route_path(name, params=None, **kwargs):
31 import urllib
32
33 base_url = {
34 'repo_compare_select': '/{repo_name}/compare',
35 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
36 }[name].format(**kwargs)
37
38 if params:
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
40 return base_url
41
42
31 43 @pytest.mark.usefixtures("autologin_user", "app")
32 44 class TestSideBySideDiff(object):
33 45
34 46 def test_diff_side_by_side(self, app, backend, backend_stub):
35 47 f_path = 'test_sidebyside_file.py'
36 48 commit1_content = 'content-25d7e49c18b159446c\n'
37 49 commit2_content = 'content-603d6c72c46d953420\n'
38 50 repo = backend.create_repo()
39 51
40 52 commit1 = commit_change(
41 53 repo.repo_name, filename=f_path, content=commit1_content,
42 54 message='A', vcs_type=backend.alias, parent=None, newfile=True)
43 55
44 56 commit2 = commit_change(
45 57 repo.repo_name, filename=f_path, content=commit2_content,
46 58 message='B, child of A', vcs_type=backend.alias, parent=commit1)
47 59
48 compare_url = url(
49 'compare_url',
60 response = self.app.get(route_path(
61 'repo_compare',
50 62 repo_name=repo.repo_name,
51 63 source_ref_type='rev',
52 64 source_ref=commit1.raw_id,
53 target_repo=repo.repo_name,
54 65 target_ref_type='rev',
55 66 target_ref=commit2.raw_id,
56 f_path=f_path,
57 diffmode='sidebyside')
58
59 response = self.app.get(compare_url)
67 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
68 ))
60 69
61 70 response.mustcontain('Expand 1 commit')
62 71 response.mustcontain('1 file changed')
63 72
64 73 response.mustcontain(
65 74 'r%s:%s...r%s:%s' % (
66 75 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
67 76
68 77 response.mustcontain('<strong>{}</strong>'.format(f_path))
69 78
70 79 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
71 80 commits = [
72 81 {'message': 'First commit'},
73 82 {'message': 'Commit with binary',
74 83 'added': [nodes.FileNode('file.empty', content='')]},
75 84 ]
76 85 f_path = 'file.empty'
77 86 repo = backend.create_repo(commits=commits)
78 87 commit1 = repo.get_commit(commit_idx=0)
79 88 commit2 = repo.get_commit(commit_idx=1)
80 89
81 compare_url = url(
82 'compare_url',
90 response = self.app.get(route_path(
91 'repo_compare',
83 92 repo_name=repo.repo_name,
84 93 source_ref_type='rev',
85 94 source_ref=commit1.raw_id,
86 target_repo=repo.repo_name,
87 95 target_ref_type='rev',
88 96 target_ref=commit2.raw_id,
89 f_path=f_path,
90 diffmode='sidebyside')
91
92 response = self.app.get(compare_url)
97 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
98 ))
93 99
94 100 response.mustcontain('Expand 1 commit')
95 101 response.mustcontain('1 file changed')
96 102
97 103 response.mustcontain(
98 104 'r%s:%s...r%s:%s' % (
99 105 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
100 106
101 107 response.mustcontain('<strong>{}</strong>'.format(f_path))
102 108
103 109 def test_diff_sidebyside_two_commits(self, app, backend):
104 110 commit_id_range = {
105 111 'hg': {
106 112 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
107 113 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
108 114 'changes': '21 files changed: 943 inserted, 288 deleted'
109 115 },
110 116 'git': {
111 117 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
112 118 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
113 119 'changes': '21 files changed: 943 inserted, 288 deleted'
114 120 },
115 121
116 122 'svn': {
117 123 'commits': ['336',
118 124 '337'],
119 125 'changes': '21 files changed: 943 inserted, 288 deleted'
120 126 },
121 127 }
122 128
123 129 commit_info = commit_id_range[backend.alias]
124 130 commit2, commit1 = commit_info['commits']
125 131 file_changes = commit_info['changes']
126 132
127 compare_url = url(
128 'compare_url',
133 response = self.app.get(route_path(
134 'repo_compare',
129 135 repo_name=backend.repo_name,
130 136 source_ref_type='rev',
131 137 source_ref=commit2,
132 138 target_repo=backend.repo_name,
133 139 target_ref_type='rev',
134 140 target_ref=commit1,
135 diffmode='sidebyside')
136 response = self.app.get(compare_url)
141 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
142 ))
137 143
138 144 response.mustcontain('Expand 1 commit')
139 145 response.mustcontain(file_changes)
140 146
141 147 def test_diff_sidebyside_two_commits_single_file(self, app, backend):
142 148 commit_id_range = {
143 149 'hg': {
144 150 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
145 151 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
146 152 'changes': '1 file changed: 1 inserted, 1 deleted'
147 153 },
148 154 'git': {
149 155 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
150 156 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
151 157 'changes': '1 file changed: 1 inserted, 1 deleted'
152 158 },
153 159
154 160 'svn': {
155 161 'commits': ['336',
156 162 '337'],
157 163 'changes': '1 file changed: 1 inserted, 1 deleted'
158 164 },
159 165 }
160 166 f_path = 'docs/conf.py'
161 167
162 168 commit_info = commit_id_range[backend.alias]
163 169 commit2, commit1 = commit_info['commits']
164 170 file_changes = commit_info['changes']
165 171
166 compare_url = url(
167 'compare_url',
172 response = self.app.get(route_path(
173 'repo_compare',
168 174 repo_name=backend.repo_name,
169 175 source_ref_type='rev',
170 176 source_ref=commit2,
171 target_repo=backend.repo_name,
172 177 target_ref_type='rev',
173 178 target_ref=commit1,
174 f_path=f_path,
175 diffmode='sidebyside')
176 response = self.app.get(compare_url)
179 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
180 ))
177 181
178 182 response.mustcontain('Expand 1 commit')
179 183 response.mustcontain(file_changes)
@@ -1,1278 +1,1279 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import itertools
22 22 import logging
23 23 import os
24 24 import shutil
25 25 import tempfile
26 26 import collections
27 27
28 28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 from rhodecode.apps._base import RepoAppView
34 34
35 35 from rhodecode.controllers.utils import parse_path_ref
36 36 from rhodecode.lib import diffs, helpers as h, caches
37 37 from rhodecode.lib import audit_logger
38 38 from rhodecode.lib.exceptions import NonRelativePathError
39 39 from rhodecode.lib.codeblocks import (
40 40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 41 from rhodecode.lib.utils2 import (
42 42 convert_line_endings, detect_mode, safe_str, str2bool)
43 43 from rhodecode.lib.auth import (
44 44 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
45 45 from rhodecode.lib.vcs import path as vcspath
46 46 from rhodecode.lib.vcs.backends.base import EmptyCommit
47 47 from rhodecode.lib.vcs.conf import settings
48 48 from rhodecode.lib.vcs.nodes import FileNode
49 49 from rhodecode.lib.vcs.exceptions import (
50 50 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
51 51 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
52 52 NodeDoesNotExistError, CommitError, NodeError)
53 53
54 54 from rhodecode.model.scm import ScmModel
55 55 from rhodecode.model.db import Repository
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoFilesView(RepoAppView):
61 61
62 62 @staticmethod
63 63 def adjust_file_path_for_svn(f_path, repo):
64 64 """
65 65 Computes the relative path of `f_path`.
66 66
67 67 This is mainly based on prefix matching of the recognized tags and
68 68 branches in the underlying repository.
69 69 """
70 70 tags_and_branches = itertools.chain(
71 71 repo.branches.iterkeys(),
72 72 repo.tags.iterkeys())
73 73 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
74 74
75 75 for name in tags_and_branches:
76 76 if f_path.startswith('{}/'.format(name)):
77 77 f_path = vcspath.relpath(f_path, name)
78 78 break
79 79 return f_path
80 80
81 81 def load_default_context(self):
82 82 c = self._get_local_tmpl_context(include_app_defaults=True)
83 83
84 84 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
85 85 c.repo_info = self.db_repo
86 86 c.rhodecode_repo = self.rhodecode_vcs_repo
87 87
88 88 self._register_global_c(c)
89 89 return c
90 90
91 91 def _ensure_not_locked(self):
92 92 _ = self.request.translate
93 93
94 94 repo = self.db_repo
95 95 if repo.enable_locking and repo.locked[0]:
96 96 h.flash(_('This repository has been locked by %s on %s')
97 97 % (h.person_by_id(repo.locked[0]),
98 98 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 99 'warning')
100 100 files_url = h.route_path(
101 101 'repo_files:default_path',
102 102 repo_name=self.db_repo_name, commit_id='tip')
103 103 raise HTTPFound(files_url)
104 104
105 105 def _get_commit_and_path(self):
106 106 default_commit_id = self.db_repo.landing_rev[1]
107 107 default_f_path = '/'
108 108
109 109 commit_id = self.request.matchdict.get(
110 110 'commit_id', default_commit_id)
111 111 f_path = self._get_f_path(self.request.matchdict, default_f_path)
112 112 return commit_id, f_path
113 113
114 114 def _get_default_encoding(self, c):
115 115 enc_list = getattr(c, 'default_encodings', [])
116 116 return enc_list[0] if enc_list else 'UTF-8'
117 117
118 118 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
119 119 """
120 120 This is a safe way to get commit. If an error occurs it redirects to
121 121 tip with proper message
122 122
123 123 :param commit_id: id of commit to fetch
124 124 :param redirect_after: toggle redirection
125 125 """
126 126 _ = self.request.translate
127 127
128 128 try:
129 129 return self.rhodecode_vcs_repo.get_commit(commit_id)
130 130 except EmptyRepositoryError:
131 131 if not redirect_after:
132 132 return None
133 133
134 134 _url = h.route_path(
135 135 'repo_files_add_file',
136 136 repo_name=self.db_repo_name, commit_id=0, f_path='',
137 137 _anchor='edit')
138 138
139 139 if h.HasRepoPermissionAny(
140 140 'repository.write', 'repository.admin')(self.db_repo_name):
141 141 add_new = h.link_to(
142 142 _('Click here to add a new file.'), _url, class_="alert-link")
143 143 else:
144 144 add_new = ""
145 145
146 146 h.flash(h.literal(
147 147 _('There are no files yet. %s') % add_new), category='warning')
148 148 raise HTTPFound(
149 149 h.route_path('repo_summary', repo_name=self.db_repo_name))
150 150
151 151 except (CommitDoesNotExistError, LookupError):
152 152 msg = _('No such commit exists for this repository')
153 153 h.flash(msg, category='error')
154 154 raise HTTPNotFound()
155 155 except RepositoryError as e:
156 156 h.flash(safe_str(h.escape(e)), category='error')
157 157 raise HTTPNotFound()
158 158
159 159 def _get_filenode_or_redirect(self, commit_obj, path):
160 160 """
161 161 Returns file_node, if error occurs or given path is directory,
162 162 it'll redirect to top level path
163 163 """
164 164 _ = self.request.translate
165 165
166 166 try:
167 167 file_node = commit_obj.get_node(path)
168 168 if file_node.is_dir():
169 169 raise RepositoryError('The given path is a directory')
170 170 except CommitDoesNotExistError:
171 171 log.exception('No such commit exists for this repository')
172 172 h.flash(_('No such commit exists for this repository'), category='error')
173 173 raise HTTPNotFound()
174 174 except RepositoryError as e:
175 175 log.warning('Repository error while fetching '
176 176 'filenode `%s`. Err:%s', path, e)
177 177 h.flash(safe_str(h.escape(e)), category='error')
178 178 raise HTTPNotFound()
179 179
180 180 return file_node
181 181
182 182 def _is_valid_head(self, commit_id, repo):
183 183 # check if commit is a branch identifier- basically we cannot
184 184 # create multiple heads via file editing
185 185 valid_heads = repo.branches.keys() + repo.branches.values()
186 186
187 187 if h.is_svn(repo) and not repo.is_empty():
188 188 # Note: Subversion only has one head, we add it here in case there
189 189 # is no branch matched.
190 190 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
191 191
192 192 # check if commit is a branch name or branch hash
193 193 return commit_id in valid_heads
194 194
195 195 def _get_tree_cache_manager(self, namespace_type):
196 196 _namespace = caches.get_repo_namespace_key(
197 197 namespace_type, self.db_repo_name)
198 198 return caches.get_cache_manager('repo_cache_long', _namespace)
199 199
200 200 def _get_tree_at_commit(
201 201 self, c, commit_id, f_path, full_load=False, force=False):
202 202 def _cached_tree():
203 203 log.debug('Generating cached file tree for %s, %s, %s',
204 204 self.db_repo_name, commit_id, f_path)
205 205
206 206 c.full_load = full_load
207 207 return render(
208 208 'rhodecode:templates/files/files_browser_tree.mako',
209 209 self._get_template_context(c), self.request)
210 210
211 211 cache_manager = self._get_tree_cache_manager(caches.FILE_TREE)
212 212
213 213 cache_key = caches.compute_key_from_params(
214 214 self.db_repo_name, commit_id, f_path)
215 215
216 216 if force:
217 217 # we want to force recompute of caches
218 218 cache_manager.remove_value(cache_key)
219 219
220 220 return cache_manager.get(cache_key, createfunc=_cached_tree)
221 221
222 222 def _get_archive_spec(self, fname):
223 223 log.debug('Detecting archive spec for: `%s`', fname)
224 224
225 225 fileformat = None
226 226 ext = None
227 227 content_type = None
228 228 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
229 229 content_type, extension = ext_data
230 230
231 231 if fname.endswith(extension):
232 232 fileformat = a_type
233 233 log.debug('archive is of type: %s', fileformat)
234 234 ext = extension
235 235 break
236 236
237 237 if not fileformat:
238 238 raise ValueError()
239 239
240 240 # left over part of whole fname is the commit
241 241 commit_id = fname[:-len(ext)]
242 242
243 243 return commit_id, ext, fileformat, content_type
244 244
245 245 @LoginRequired()
246 246 @HasRepoPermissionAnyDecorator(
247 247 'repository.read', 'repository.write', 'repository.admin')
248 248 @view_config(
249 249 route_name='repo_archivefile', request_method='GET',
250 250 renderer=None)
251 251 def repo_archivefile(self):
252 252 # archive cache config
253 253 from rhodecode import CONFIG
254 254 _ = self.request.translate
255 255 self.load_default_context()
256 256
257 257 fname = self.request.matchdict['fname']
258 258 subrepos = self.request.GET.get('subrepos') == 'true'
259 259
260 260 if not self.db_repo.enable_downloads:
261 261 return Response(_('Downloads disabled'))
262 262
263 263 try:
264 264 commit_id, ext, fileformat, content_type = \
265 265 self._get_archive_spec(fname)
266 266 except ValueError:
267 267 return Response(_('Unknown archive type for: `{}`').format(fname))
268 268
269 269 try:
270 270 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
271 271 except CommitDoesNotExistError:
272 272 return Response(_('Unknown commit_id %s') % commit_id)
273 273 except EmptyRepositoryError:
274 274 return Response(_('Empty repository'))
275 275
276 276 archive_name = '%s-%s%s%s' % (
277 277 safe_str(self.db_repo_name.replace('/', '_')),
278 278 '-sub' if subrepos else '',
279 279 safe_str(commit.short_id), ext)
280 280
281 281 use_cached_archive = False
282 282 archive_cache_enabled = CONFIG.get(
283 283 'archive_cache_dir') and not self.request.GET.get('no_cache')
284 284
285 285 if archive_cache_enabled:
286 286 # check if we it's ok to write
287 287 if not os.path.isdir(CONFIG['archive_cache_dir']):
288 288 os.makedirs(CONFIG['archive_cache_dir'])
289 289 cached_archive_path = os.path.join(
290 290 CONFIG['archive_cache_dir'], archive_name)
291 291 if os.path.isfile(cached_archive_path):
292 292 log.debug('Found cached archive in %s', cached_archive_path)
293 293 fd, archive = None, cached_archive_path
294 294 use_cached_archive = True
295 295 else:
296 296 log.debug('Archive %s is not yet cached', archive_name)
297 297
298 298 if not use_cached_archive:
299 299 # generate new archive
300 300 fd, archive = tempfile.mkstemp()
301 301 log.debug('Creating new temp archive in %s', archive)
302 302 try:
303 303 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
304 304 except ImproperArchiveTypeError:
305 305 return _('Unknown archive type')
306 306 if archive_cache_enabled:
307 307 # if we generated the archive and we have cache enabled
308 308 # let's use this for future
309 309 log.debug('Storing new archive in %s', cached_archive_path)
310 310 shutil.move(archive, cached_archive_path)
311 311 archive = cached_archive_path
312 312
313 313 # store download action
314 314 audit_logger.store_web(
315 315 'repo.archive.download', action_data={
316 316 'user_agent': self.request.user_agent,
317 317 'archive_name': archive_name,
318 318 'archive_spec': fname,
319 319 'archive_cached': use_cached_archive},
320 320 user=self._rhodecode_user,
321 321 repo=self.db_repo,
322 322 commit=True
323 323 )
324 324
325 325 def get_chunked_archive(archive):
326 326 with open(archive, 'rb') as stream:
327 327 while True:
328 328 data = stream.read(16 * 1024)
329 329 if not data:
330 330 if fd: # fd means we used temporary file
331 331 os.close(fd)
332 332 if not archive_cache_enabled:
333 333 log.debug('Destroying temp archive %s', archive)
334 334 os.remove(archive)
335 335 break
336 336 yield data
337 337
338 338 response = Response(app_iter=get_chunked_archive(archive))
339 339 response.content_disposition = str(
340 340 'attachment; filename=%s' % archive_name)
341 341 response.content_type = str(content_type)
342 342
343 343 return response
344 344
345 345 def _get_file_node(self, commit_id, f_path):
346 346 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
347 347 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
348 348 try:
349 349 node = commit.get_node(f_path)
350 350 if node.is_dir():
351 351 raise NodeError('%s path is a %s not a file'
352 352 % (node, type(node)))
353 353 except NodeDoesNotExistError:
354 354 commit = EmptyCommit(
355 355 commit_id=commit_id,
356 356 idx=commit.idx,
357 357 repo=commit.repository,
358 358 alias=commit.repository.alias,
359 359 message=commit.message,
360 360 author=commit.author,
361 361 date=commit.date)
362 362 node = FileNode(f_path, '', commit=commit)
363 363 else:
364 364 commit = EmptyCommit(
365 365 repo=self.rhodecode_vcs_repo,
366 366 alias=self.rhodecode_vcs_repo.alias)
367 367 node = FileNode(f_path, '', commit=commit)
368 368 return node
369 369
370 370 @LoginRequired()
371 371 @HasRepoPermissionAnyDecorator(
372 372 'repository.read', 'repository.write', 'repository.admin')
373 373 @view_config(
374 374 route_name='repo_files_diff', request_method='GET',
375 375 renderer=None)
376 376 def repo_files_diff(self):
377 377 c = self.load_default_context()
378 378 f_path = self._get_f_path(self.request.matchdict)
379 379 diff1 = self.request.GET.get('diff1', '')
380 380 diff2 = self.request.GET.get('diff2', '')
381 381
382 382 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
383 383
384 384 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
385 385 line_context = self.request.GET.get('context', 3)
386 386
387 387 if not any((diff1, diff2)):
388 388 h.flash(
389 389 'Need query parameter "diff1" or "diff2" to generate a diff.',
390 390 category='error')
391 391 raise HTTPBadRequest()
392 392
393 393 c.action = self.request.GET.get('diff')
394 394 if c.action not in ['download', 'raw']:
395 compare_url = h.url(
396 'compare_url', repo_name=self.db_repo_name,
395 compare_url = h.route_path(
396 'repo_compare',
397 repo_name=self.db_repo_name,
397 398 source_ref_type='rev',
398 399 source_ref=diff1,
399 400 target_repo=self.db_repo_name,
400 401 target_ref_type='rev',
401 402 target_ref=diff2,
402 f_path=f_path)
403 _query=dict(f_path=f_path))
403 404 # redirect to new view if we render diff
404 405 raise HTTPFound(compare_url)
405 406
406 407 try:
407 408 node1 = self._get_file_node(diff1, path1)
408 409 node2 = self._get_file_node(diff2, f_path)
409 410 except (RepositoryError, NodeError):
410 411 log.exception("Exception while trying to get node from repository")
411 412 raise HTTPFound(
412 413 h.route_path('repo_files', repo_name=self.db_repo_name,
413 414 commit_id='tip', f_path=f_path))
414 415
415 416 if all(isinstance(node.commit, EmptyCommit)
416 417 for node in (node1, node2)):
417 418 raise HTTPNotFound()
418 419
419 420 c.commit_1 = node1.commit
420 421 c.commit_2 = node2.commit
421 422
422 423 if c.action == 'download':
423 424 _diff = diffs.get_gitdiff(node1, node2,
424 425 ignore_whitespace=ignore_whitespace,
425 426 context=line_context)
426 427 diff = diffs.DiffProcessor(_diff, format='gitdiff')
427 428
428 429 response = Response(diff.as_raw())
429 430 response.content_type = 'text/plain'
430 431 response.content_disposition = (
431 432 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
432 433 )
433 434 charset = self._get_default_encoding(c)
434 435 if charset:
435 436 response.charset = charset
436 437 return response
437 438
438 439 elif c.action == 'raw':
439 440 _diff = diffs.get_gitdiff(node1, node2,
440 441 ignore_whitespace=ignore_whitespace,
441 442 context=line_context)
442 443 diff = diffs.DiffProcessor(_diff, format='gitdiff')
443 444
444 445 response = Response(diff.as_raw())
445 446 response.content_type = 'text/plain'
446 447 charset = self._get_default_encoding(c)
447 448 if charset:
448 449 response.charset = charset
449 450 return response
450 451
451 452 # in case we ever end up here
452 453 raise HTTPNotFound()
453 454
454 455 @LoginRequired()
455 456 @HasRepoPermissionAnyDecorator(
456 457 'repository.read', 'repository.write', 'repository.admin')
457 458 @view_config(
458 459 route_name='repo_files_diff_2way_redirect', request_method='GET',
459 460 renderer=None)
460 461 def repo_files_diff_2way_redirect(self):
461 462 """
462 463 Kept only to make OLD links work
463 464 """
464 465 f_path = self._get_f_path(self.request.matchdict)
465 466 diff1 = self.request.GET.get('diff1', '')
466 467 diff2 = self.request.GET.get('diff2', '')
467 468
468 469 if not any((diff1, diff2)):
469 470 h.flash(
470 471 'Need query parameter "diff1" or "diff2" to generate a diff.',
471 472 category='error')
472 473 raise HTTPBadRequest()
473 474
474 compare_url = h.url(
475 'compare_url', repo_name=self.db_repo_name,
475 compare_url = h.route_path(
476 'repo_compare',
477 repo_name=self.db_repo_name,
476 478 source_ref_type='rev',
477 479 source_ref=diff1,
478 target_repo=self.db_repo_name,
479 480 target_ref_type='rev',
480 481 target_ref=diff2,
481 f_path=f_path,
482 diffmode='sideside')
482 _query=dict(f_path=f_path, diffmode='sideside',
483 target_repo=self.db_repo_name,))
483 484 raise HTTPFound(compare_url)
484 485
485 486 @LoginRequired()
486 487 @HasRepoPermissionAnyDecorator(
487 488 'repository.read', 'repository.write', 'repository.admin')
488 489 @view_config(
489 490 route_name='repo_files', request_method='GET',
490 491 renderer=None)
491 492 @view_config(
492 493 route_name='repo_files:default_path', request_method='GET',
493 494 renderer=None)
494 495 @view_config(
495 496 route_name='repo_files:default_commit', request_method='GET',
496 497 renderer=None)
497 498 @view_config(
498 499 route_name='repo_files:rendered', request_method='GET',
499 500 renderer=None)
500 501 @view_config(
501 502 route_name='repo_files:annotated', request_method='GET',
502 503 renderer=None)
503 504 def repo_files(self):
504 505 c = self.load_default_context()
505 506
506 507 view_name = getattr(self.request.matched_route, 'name', None)
507 508
508 509 c.annotate = view_name == 'repo_files:annotated'
509 510 # default is false, but .rst/.md files later are auto rendered, we can
510 511 # overwrite auto rendering by setting this GET flag
511 512 c.renderer = view_name == 'repo_files:rendered' or \
512 513 not self.request.GET.get('no-render', False)
513 514
514 515 # redirect to given commit_id from form if given
515 516 get_commit_id = self.request.GET.get('at_rev', None)
516 517 if get_commit_id:
517 518 self._get_commit_or_redirect(get_commit_id)
518 519
519 520 commit_id, f_path = self._get_commit_and_path()
520 521 c.commit = self._get_commit_or_redirect(commit_id)
521 522 c.branch = self.request.GET.get('branch', None)
522 523 c.f_path = f_path
523 524
524 525 # prev link
525 526 try:
526 527 prev_commit = c.commit.prev(c.branch)
527 528 c.prev_commit = prev_commit
528 529 c.url_prev = h.route_path(
529 530 'repo_files', repo_name=self.db_repo_name,
530 531 commit_id=prev_commit.raw_id, f_path=f_path)
531 532 if c.branch:
532 533 c.url_prev += '?branch=%s' % c.branch
533 534 except (CommitDoesNotExistError, VCSError):
534 535 c.url_prev = '#'
535 536 c.prev_commit = EmptyCommit()
536 537
537 538 # next link
538 539 try:
539 540 next_commit = c.commit.next(c.branch)
540 541 c.next_commit = next_commit
541 542 c.url_next = h.route_path(
542 543 'repo_files', repo_name=self.db_repo_name,
543 544 commit_id=next_commit.raw_id, f_path=f_path)
544 545 if c.branch:
545 546 c.url_next += '?branch=%s' % c.branch
546 547 except (CommitDoesNotExistError, VCSError):
547 548 c.url_next = '#'
548 549 c.next_commit = EmptyCommit()
549 550
550 551 # files or dirs
551 552 try:
552 553 c.file = c.commit.get_node(f_path)
553 554 c.file_author = True
554 555 c.file_tree = ''
555 556
556 557 # load file content
557 558 if c.file.is_file():
558 559 c.lf_node = c.file.get_largefile_node()
559 560
560 561 c.file_source_page = 'true'
561 562 c.file_last_commit = c.file.last_commit
562 563 if c.file.size < c.visual.cut_off_limit_diff:
563 564 if c.annotate: # annotation has precedence over renderer
564 565 c.annotated_lines = filenode_as_annotated_lines_tokens(
565 566 c.file
566 567 )
567 568 else:
568 569 c.renderer = (
569 570 c.renderer and h.renderer_from_filename(c.file.path)
570 571 )
571 572 if not c.renderer:
572 573 c.lines = filenode_as_lines_tokens(c.file)
573 574
574 575 c.on_branch_head = self._is_valid_head(
575 576 commit_id, self.rhodecode_vcs_repo)
576 577
577 578 branch = c.commit.branch if (
578 579 c.commit.branch and '/' not in c.commit.branch) else None
579 580 c.branch_or_raw_id = branch or c.commit.raw_id
580 581 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
581 582
582 583 author = c.file_last_commit.author
583 584 c.authors = [[
584 585 h.email(author),
585 586 h.person(author, 'username_or_name_or_email'),
586 587 1
587 588 ]]
588 589
589 590 else: # load tree content at path
590 591 c.file_source_page = 'false'
591 592 c.authors = []
592 593 # this loads a simple tree without metadata to speed things up
593 594 # later via ajax we call repo_nodetree_full and fetch whole
594 595 c.file_tree = self._get_tree_at_commit(
595 596 c, c.commit.raw_id, f_path)
596 597
597 598 except RepositoryError as e:
598 599 h.flash(safe_str(h.escape(e)), category='error')
599 600 raise HTTPNotFound()
600 601
601 602 if self.request.environ.get('HTTP_X_PJAX'):
602 603 html = render('rhodecode:templates/files/files_pjax.mako',
603 604 self._get_template_context(c), self.request)
604 605 else:
605 606 html = render('rhodecode:templates/files/files.mako',
606 607 self._get_template_context(c), self.request)
607 608 return Response(html)
608 609
609 610 @HasRepoPermissionAnyDecorator(
610 611 'repository.read', 'repository.write', 'repository.admin')
611 612 @view_config(
612 613 route_name='repo_files:annotated_previous', request_method='GET',
613 614 renderer=None)
614 615 def repo_files_annotated_previous(self):
615 616 self.load_default_context()
616 617
617 618 commit_id, f_path = self._get_commit_and_path()
618 619 commit = self._get_commit_or_redirect(commit_id)
619 620 prev_commit_id = commit.raw_id
620 621 line_anchor = self.request.GET.get('line_anchor')
621 622 is_file = False
622 623 try:
623 624 _file = commit.get_node(f_path)
624 625 is_file = _file.is_file()
625 626 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
626 627 pass
627 628
628 629 if is_file:
629 630 history = commit.get_file_history(f_path)
630 631 prev_commit_id = history[1].raw_id \
631 632 if len(history) > 1 else prev_commit_id
632 633 prev_url = h.route_path(
633 634 'repo_files:annotated', repo_name=self.db_repo_name,
634 635 commit_id=prev_commit_id, f_path=f_path,
635 636 _anchor='L{}'.format(line_anchor))
636 637
637 638 raise HTTPFound(prev_url)
638 639
639 640 @LoginRequired()
640 641 @HasRepoPermissionAnyDecorator(
641 642 'repository.read', 'repository.write', 'repository.admin')
642 643 @view_config(
643 644 route_name='repo_nodetree_full', request_method='GET',
644 645 renderer=None, xhr=True)
645 646 @view_config(
646 647 route_name='repo_nodetree_full:default_path', request_method='GET',
647 648 renderer=None, xhr=True)
648 649 def repo_nodetree_full(self):
649 650 """
650 651 Returns rendered html of file tree that contains commit date,
651 652 author, commit_id for the specified combination of
652 653 repo, commit_id and file path
653 654 """
654 655 c = self.load_default_context()
655 656
656 657 commit_id, f_path = self._get_commit_and_path()
657 658 commit = self._get_commit_or_redirect(commit_id)
658 659 try:
659 660 dir_node = commit.get_node(f_path)
660 661 except RepositoryError as e:
661 662 return Response('error: {}'.format(safe_str(e)))
662 663
663 664 if dir_node.is_file():
664 665 return Response('')
665 666
666 667 c.file = dir_node
667 668 c.commit = commit
668 669
669 670 # using force=True here, make a little trick. We flush the cache and
670 671 # compute it using the same key as without previous full_load, so now
671 672 # the fully loaded tree is now returned instead of partial,
672 673 # and we store this in caches
673 674 html = self._get_tree_at_commit(
674 675 c, commit.raw_id, dir_node.path, full_load=True, force=True)
675 676
676 677 return Response(html)
677 678
678 679 def _get_attachement_disposition(self, f_path):
679 680 return 'attachment; filename=%s' % \
680 681 safe_str(f_path.split(Repository.NAME_SEP)[-1])
681 682
682 683 @LoginRequired()
683 684 @HasRepoPermissionAnyDecorator(
684 685 'repository.read', 'repository.write', 'repository.admin')
685 686 @view_config(
686 687 route_name='repo_file_raw', request_method='GET',
687 688 renderer=None)
688 689 def repo_file_raw(self):
689 690 """
690 691 Action for show as raw, some mimetypes are "rendered",
691 692 those include images, icons.
692 693 """
693 694 c = self.load_default_context()
694 695
695 696 commit_id, f_path = self._get_commit_and_path()
696 697 commit = self._get_commit_or_redirect(commit_id)
697 698 file_node = self._get_filenode_or_redirect(commit, f_path)
698 699
699 700 raw_mimetype_mapping = {
700 701 # map original mimetype to a mimetype used for "show as raw"
701 702 # you can also provide a content-disposition to override the
702 703 # default "attachment" disposition.
703 704 # orig_type: (new_type, new_dispo)
704 705
705 706 # show images inline:
706 707 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
707 708 # for example render an SVG with javascript inside or even render
708 709 # HTML.
709 710 'image/x-icon': ('image/x-icon', 'inline'),
710 711 'image/png': ('image/png', 'inline'),
711 712 'image/gif': ('image/gif', 'inline'),
712 713 'image/jpeg': ('image/jpeg', 'inline'),
713 714 'application/pdf': ('application/pdf', 'inline'),
714 715 }
715 716
716 717 mimetype = file_node.mimetype
717 718 try:
718 719 mimetype, disposition = raw_mimetype_mapping[mimetype]
719 720 except KeyError:
720 721 # we don't know anything special about this, handle it safely
721 722 if file_node.is_binary:
722 723 # do same as download raw for binary files
723 724 mimetype, disposition = 'application/octet-stream', 'attachment'
724 725 else:
725 726 # do not just use the original mimetype, but force text/plain,
726 727 # otherwise it would serve text/html and that might be unsafe.
727 728 # Note: underlying vcs library fakes text/plain mimetype if the
728 729 # mimetype can not be determined and it thinks it is not
729 730 # binary.This might lead to erroneous text display in some
730 731 # cases, but helps in other cases, like with text files
731 732 # without extension.
732 733 mimetype, disposition = 'text/plain', 'inline'
733 734
734 735 if disposition == 'attachment':
735 736 disposition = self._get_attachement_disposition(f_path)
736 737
737 738 def stream_node():
738 739 yield file_node.raw_bytes
739 740
740 741 response = Response(app_iter=stream_node())
741 742 response.content_disposition = disposition
742 743 response.content_type = mimetype
743 744
744 745 charset = self._get_default_encoding(c)
745 746 if charset:
746 747 response.charset = charset
747 748
748 749 return response
749 750
750 751 @LoginRequired()
751 752 @HasRepoPermissionAnyDecorator(
752 753 'repository.read', 'repository.write', 'repository.admin')
753 754 @view_config(
754 755 route_name='repo_file_download', request_method='GET',
755 756 renderer=None)
756 757 @view_config(
757 758 route_name='repo_file_download:legacy', request_method='GET',
758 759 renderer=None)
759 760 def repo_file_download(self):
760 761 c = self.load_default_context()
761 762
762 763 commit_id, f_path = self._get_commit_and_path()
763 764 commit = self._get_commit_or_redirect(commit_id)
764 765 file_node = self._get_filenode_or_redirect(commit, f_path)
765 766
766 767 if self.request.GET.get('lf'):
767 768 # only if lf get flag is passed, we download this file
768 769 # as LFS/Largefile
769 770 lf_node = file_node.get_largefile_node()
770 771 if lf_node:
771 772 # overwrite our pointer with the REAL large-file
772 773 file_node = lf_node
773 774
774 775 disposition = self._get_attachement_disposition(f_path)
775 776
776 777 def stream_node():
777 778 yield file_node.raw_bytes
778 779
779 780 response = Response(app_iter=stream_node())
780 781 response.content_disposition = disposition
781 782 response.content_type = file_node.mimetype
782 783
783 784 charset = self._get_default_encoding(c)
784 785 if charset:
785 786 response.charset = charset
786 787
787 788 return response
788 789
789 790 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
790 791 def _cached_nodes():
791 792 log.debug('Generating cached nodelist for %s, %s, %s',
792 793 repo_name, commit_id, f_path)
793 794 _d, _f = ScmModel().get_nodes(
794 795 repo_name, commit_id, f_path, flat=False)
795 796 return _d + _f
796 797
797 798 cache_manager = self._get_tree_cache_manager(caches.FILE_SEARCH_TREE_META)
798 799
799 800 cache_key = caches.compute_key_from_params(
800 801 repo_name, commit_id, f_path)
801 802 return cache_manager.get(cache_key, createfunc=_cached_nodes)
802 803
803 804 @LoginRequired()
804 805 @HasRepoPermissionAnyDecorator(
805 806 'repository.read', 'repository.write', 'repository.admin')
806 807 @view_config(
807 808 route_name='repo_files_nodelist', request_method='GET',
808 809 renderer='json_ext', xhr=True)
809 810 def repo_nodelist(self):
810 811 self.load_default_context()
811 812
812 813 commit_id, f_path = self._get_commit_and_path()
813 814 commit = self._get_commit_or_redirect(commit_id)
814 815
815 816 metadata = self._get_nodelist_at_commit(
816 817 self.db_repo_name, commit.raw_id, f_path)
817 818 return {'nodes': metadata}
818 819
819 820 def _create_references(
820 821 self, branches_or_tags, symbolic_reference, f_path):
821 822 items = []
822 823 for name, commit_id in branches_or_tags.items():
823 824 sym_ref = symbolic_reference(commit_id, name, f_path)
824 825 items.append((sym_ref, name))
825 826 return items
826 827
827 828 def _symbolic_reference(self, commit_id, name, f_path):
828 829 return commit_id
829 830
830 831 def _symbolic_reference_svn(self, commit_id, name, f_path):
831 832 new_f_path = vcspath.join(name, f_path)
832 833 return u'%s@%s' % (new_f_path, commit_id)
833 834
834 835 def _get_node_history(self, commit_obj, f_path, commits=None):
835 836 """
836 837 get commit history for given node
837 838
838 839 :param commit_obj: commit to calculate history
839 840 :param f_path: path for node to calculate history for
840 841 :param commits: if passed don't calculate history and take
841 842 commits defined in this list
842 843 """
843 844 _ = self.request.translate
844 845
845 846 # calculate history based on tip
846 847 tip = self.rhodecode_vcs_repo.get_commit()
847 848 if commits is None:
848 849 pre_load = ["author", "branch"]
849 850 try:
850 851 commits = tip.get_file_history(f_path, pre_load=pre_load)
851 852 except (NodeDoesNotExistError, CommitError):
852 853 # this node is not present at tip!
853 854 commits = commit_obj.get_file_history(f_path, pre_load=pre_load)
854 855
855 856 history = []
856 857 commits_group = ([], _("Changesets"))
857 858 for commit in commits:
858 859 branch = ' (%s)' % commit.branch if commit.branch else ''
859 860 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
860 861 commits_group[0].append((commit.raw_id, n_desc,))
861 862 history.append(commits_group)
862 863
863 864 symbolic_reference = self._symbolic_reference
864 865
865 866 if self.rhodecode_vcs_repo.alias == 'svn':
866 867 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
867 868 f_path, self.rhodecode_vcs_repo)
868 869 if adjusted_f_path != f_path:
869 870 log.debug(
870 871 'Recognized svn tag or branch in file "%s", using svn '
871 872 'specific symbolic references', f_path)
872 873 f_path = adjusted_f_path
873 874 symbolic_reference = self._symbolic_reference_svn
874 875
875 876 branches = self._create_references(
876 877 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
877 878 branches_group = (branches, _("Branches"))
878 879
879 880 tags = self._create_references(
880 881 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
881 882 tags_group = (tags, _("Tags"))
882 883
883 884 history.append(branches_group)
884 885 history.append(tags_group)
885 886
886 887 return history, commits
887 888
888 889 @LoginRequired()
889 890 @HasRepoPermissionAnyDecorator(
890 891 'repository.read', 'repository.write', 'repository.admin')
891 892 @view_config(
892 893 route_name='repo_file_history', request_method='GET',
893 894 renderer='json_ext')
894 895 def repo_file_history(self):
895 896 self.load_default_context()
896 897
897 898 commit_id, f_path = self._get_commit_and_path()
898 899 commit = self._get_commit_or_redirect(commit_id)
899 900 file_node = self._get_filenode_or_redirect(commit, f_path)
900 901
901 902 if file_node.is_file():
902 903 file_history, _hist = self._get_node_history(commit, f_path)
903 904
904 905 res = []
905 906 for obj in file_history:
906 907 res.append({
907 908 'text': obj[1],
908 909 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
909 910 })
910 911
911 912 data = {
912 913 'more': False,
913 914 'results': res
914 915 }
915 916 return data
916 917
917 918 log.warning('Cannot fetch history for directory')
918 919 raise HTTPBadRequest()
919 920
920 921 @LoginRequired()
921 922 @HasRepoPermissionAnyDecorator(
922 923 'repository.read', 'repository.write', 'repository.admin')
923 924 @view_config(
924 925 route_name='repo_file_authors', request_method='GET',
925 926 renderer='rhodecode:templates/files/file_authors_box.mako')
926 927 def repo_file_authors(self):
927 928 c = self.load_default_context()
928 929
929 930 commit_id, f_path = self._get_commit_and_path()
930 931 commit = self._get_commit_or_redirect(commit_id)
931 932 file_node = self._get_filenode_or_redirect(commit, f_path)
932 933
933 934 if not file_node.is_file():
934 935 raise HTTPBadRequest()
935 936
936 937 c.file_last_commit = file_node.last_commit
937 938 if self.request.GET.get('annotate') == '1':
938 939 # use _hist from annotation if annotation mode is on
939 940 commit_ids = set(x[1] for x in file_node.annotate)
940 941 _hist = (
941 942 self.rhodecode_vcs_repo.get_commit(commit_id)
942 943 for commit_id in commit_ids)
943 944 else:
944 945 _f_history, _hist = self._get_node_history(commit, f_path)
945 946 c.file_author = False
946 947
947 948 unique = collections.OrderedDict()
948 949 for commit in _hist:
949 950 author = commit.author
950 951 if author not in unique:
951 952 unique[commit.author] = [
952 953 h.email(author),
953 954 h.person(author, 'username_or_name_or_email'),
954 955 1 # counter
955 956 ]
956 957
957 958 else:
958 959 # increase counter
959 960 unique[commit.author][2] += 1
960 961
961 962 c.authors = [val for val in unique.values()]
962 963
963 964 return self._get_template_context(c)
964 965
965 966 @LoginRequired()
966 967 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
967 968 @view_config(
968 969 route_name='repo_files_remove_file', request_method='GET',
969 970 renderer='rhodecode:templates/files/files_delete.mako')
970 971 def repo_files_remove_file(self):
971 972 _ = self.request.translate
972 973 c = self.load_default_context()
973 974 commit_id, f_path = self._get_commit_and_path()
974 975
975 976 self._ensure_not_locked()
976 977
977 978 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
978 979 h.flash(_('You can only delete files with commit '
979 980 'being a valid branch '), category='warning')
980 981 raise HTTPFound(
981 982 h.route_path('repo_files',
982 983 repo_name=self.db_repo_name, commit_id='tip',
983 984 f_path=f_path))
984 985
985 986 c.commit = self._get_commit_or_redirect(commit_id)
986 987 c.file = self._get_filenode_or_redirect(c.commit, f_path)
987 988
988 989 c.default_message = _(
989 990 'Deleted file {} via RhodeCode Enterprise').format(f_path)
990 991 c.f_path = f_path
991 992
992 993 return self._get_template_context(c)
993 994
994 995 @LoginRequired()
995 996 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
996 997 @CSRFRequired()
997 998 @view_config(
998 999 route_name='repo_files_delete_file', request_method='POST',
999 1000 renderer=None)
1000 1001 def repo_files_delete_file(self):
1001 1002 _ = self.request.translate
1002 1003
1003 1004 c = self.load_default_context()
1004 1005 commit_id, f_path = self._get_commit_and_path()
1005 1006
1006 1007 self._ensure_not_locked()
1007 1008
1008 1009 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1009 1010 h.flash(_('You can only delete files with commit '
1010 1011 'being a valid branch '), category='warning')
1011 1012 raise HTTPFound(
1012 1013 h.route_path('repo_files',
1013 1014 repo_name=self.db_repo_name, commit_id='tip',
1014 1015 f_path=f_path))
1015 1016
1016 1017 c.commit = self._get_commit_or_redirect(commit_id)
1017 1018 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1018 1019
1019 1020 c.default_message = _(
1020 1021 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1021 1022 c.f_path = f_path
1022 1023 node_path = f_path
1023 1024 author = self._rhodecode_db_user.full_contact
1024 1025 message = self.request.POST.get('message') or c.default_message
1025 1026 try:
1026 1027 nodes = {
1027 1028 node_path: {
1028 1029 'content': ''
1029 1030 }
1030 1031 }
1031 1032 ScmModel().delete_nodes(
1032 1033 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1033 1034 message=message,
1034 1035 nodes=nodes,
1035 1036 parent_commit=c.commit,
1036 1037 author=author,
1037 1038 )
1038 1039
1039 1040 h.flash(
1040 1041 _('Successfully deleted file `{}`').format(
1041 1042 h.escape(f_path)), category='success')
1042 1043 except Exception:
1043 1044 log.exception('Error during commit operation')
1044 1045 h.flash(_('Error occurred during commit'), category='error')
1045 1046 raise HTTPFound(
1046 1047 h.route_path('repo_commit', repo_name=self.db_repo_name,
1047 1048 commit_id='tip'))
1048 1049
1049 1050 @LoginRequired()
1050 1051 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1051 1052 @view_config(
1052 1053 route_name='repo_files_edit_file', request_method='GET',
1053 1054 renderer='rhodecode:templates/files/files_edit.mako')
1054 1055 def repo_files_edit_file(self):
1055 1056 _ = self.request.translate
1056 1057 c = self.load_default_context()
1057 1058 commit_id, f_path = self._get_commit_and_path()
1058 1059
1059 1060 self._ensure_not_locked()
1060 1061
1061 1062 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1062 1063 h.flash(_('You can only edit files with commit '
1063 1064 'being a valid branch '), category='warning')
1064 1065 raise HTTPFound(
1065 1066 h.route_path('repo_files',
1066 1067 repo_name=self.db_repo_name, commit_id='tip',
1067 1068 f_path=f_path))
1068 1069
1069 1070 c.commit = self._get_commit_or_redirect(commit_id)
1070 1071 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1071 1072
1072 1073 if c.file.is_binary:
1073 1074 files_url = h.route_path(
1074 1075 'repo_files',
1075 1076 repo_name=self.db_repo_name,
1076 1077 commit_id=c.commit.raw_id, f_path=f_path)
1077 1078 raise HTTPFound(files_url)
1078 1079
1079 1080 c.default_message = _(
1080 1081 'Edited file {} via RhodeCode Enterprise').format(f_path)
1081 1082 c.f_path = f_path
1082 1083
1083 1084 return self._get_template_context(c)
1084 1085
1085 1086 @LoginRequired()
1086 1087 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1087 1088 @CSRFRequired()
1088 1089 @view_config(
1089 1090 route_name='repo_files_update_file', request_method='POST',
1090 1091 renderer=None)
1091 1092 def repo_files_update_file(self):
1092 1093 _ = self.request.translate
1093 1094 c = self.load_default_context()
1094 1095 commit_id, f_path = self._get_commit_and_path()
1095 1096
1096 1097 self._ensure_not_locked()
1097 1098
1098 1099 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1099 1100 h.flash(_('You can only edit files with commit '
1100 1101 'being a valid branch '), category='warning')
1101 1102 raise HTTPFound(
1102 1103 h.route_path('repo_files',
1103 1104 repo_name=self.db_repo_name, commit_id='tip',
1104 1105 f_path=f_path))
1105 1106
1106 1107 c.commit = self._get_commit_or_redirect(commit_id)
1107 1108 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1108 1109
1109 1110 if c.file.is_binary:
1110 1111 raise HTTPFound(
1111 1112 h.route_path('repo_files',
1112 1113 repo_name=self.db_repo_name,
1113 1114 commit_id=c.commit.raw_id,
1114 1115 f_path=f_path))
1115 1116
1116 1117 c.default_message = _(
1117 1118 'Edited file {} via RhodeCode Enterprise').format(f_path)
1118 1119 c.f_path = f_path
1119 1120 old_content = c.file.content
1120 1121 sl = old_content.splitlines(1)
1121 1122 first_line = sl[0] if sl else ''
1122 1123
1123 1124 r_post = self.request.POST
1124 1125 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1125 1126 mode = detect_mode(first_line, 0)
1126 1127 content = convert_line_endings(r_post.get('content', ''), mode)
1127 1128
1128 1129 message = r_post.get('message') or c.default_message
1129 1130 org_f_path = c.file.unicode_path
1130 1131 filename = r_post['filename']
1131 1132 org_filename = c.file.name
1132 1133
1133 1134 if content == old_content and filename == org_filename:
1134 1135 h.flash(_('No changes'), category='warning')
1135 1136 raise HTTPFound(
1136 1137 h.route_path('repo_commit', repo_name=self.db_repo_name,
1137 1138 commit_id='tip'))
1138 1139 try:
1139 1140 mapping = {
1140 1141 org_f_path: {
1141 1142 'org_filename': org_f_path,
1142 1143 'filename': os.path.join(c.file.dir_path, filename),
1143 1144 'content': content,
1144 1145 'lexer': '',
1145 1146 'op': 'mod',
1146 1147 }
1147 1148 }
1148 1149
1149 1150 ScmModel().update_nodes(
1150 1151 user=self._rhodecode_db_user.user_id,
1151 1152 repo=self.db_repo,
1152 1153 message=message,
1153 1154 nodes=mapping,
1154 1155 parent_commit=c.commit,
1155 1156 )
1156 1157
1157 1158 h.flash(
1158 1159 _('Successfully committed changes to file `{}`').format(
1159 1160 h.escape(f_path)), category='success')
1160 1161 except Exception:
1161 1162 log.exception('Error occurred during commit')
1162 1163 h.flash(_('Error occurred during commit'), category='error')
1163 1164 raise HTTPFound(
1164 1165 h.route_path('repo_commit', repo_name=self.db_repo_name,
1165 1166 commit_id='tip'))
1166 1167
1167 1168 @LoginRequired()
1168 1169 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1169 1170 @view_config(
1170 1171 route_name='repo_files_add_file', request_method='GET',
1171 1172 renderer='rhodecode:templates/files/files_add.mako')
1172 1173 def repo_files_add_file(self):
1173 1174 _ = self.request.translate
1174 1175 c = self.load_default_context()
1175 1176 commit_id, f_path = self._get_commit_and_path()
1176 1177
1177 1178 self._ensure_not_locked()
1178 1179
1179 1180 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1180 1181 if c.commit is None:
1181 1182 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1182 1183 c.default_message = (_('Added file via RhodeCode Enterprise'))
1183 1184 c.f_path = f_path
1184 1185
1185 1186 return self._get_template_context(c)
1186 1187
1187 1188 @LoginRequired()
1188 1189 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1189 1190 @CSRFRequired()
1190 1191 @view_config(
1191 1192 route_name='repo_files_create_file', request_method='POST',
1192 1193 renderer=None)
1193 1194 def repo_files_create_file(self):
1194 1195 _ = self.request.translate
1195 1196 c = self.load_default_context()
1196 1197 commit_id, f_path = self._get_commit_and_path()
1197 1198
1198 1199 self._ensure_not_locked()
1199 1200
1200 1201 r_post = self.request.POST
1201 1202
1202 1203 c.commit = self._get_commit_or_redirect(
1203 1204 commit_id, redirect_after=False)
1204 1205 if c.commit is None:
1205 1206 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1206 1207 c.default_message = (_('Added file via RhodeCode Enterprise'))
1207 1208 c.f_path = f_path
1208 1209 unix_mode = 0
1209 1210 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1210 1211
1211 1212 message = r_post.get('message') or c.default_message
1212 1213 filename = r_post.get('filename')
1213 1214 location = r_post.get('location', '') # dir location
1214 1215 file_obj = r_post.get('upload_file', None)
1215 1216
1216 1217 if file_obj is not None and hasattr(file_obj, 'filename'):
1217 1218 filename = r_post.get('filename_upload')
1218 1219 content = file_obj.file
1219 1220
1220 1221 if hasattr(content, 'file'):
1221 1222 # non posix systems store real file under file attr
1222 1223 content = content.file
1223 1224
1224 1225 default_redirect_url = h.route_path(
1225 1226 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1226 1227
1227 1228 # If there's no commit, redirect to repo summary
1228 1229 if type(c.commit) is EmptyCommit:
1229 1230 redirect_url = h.route_path(
1230 1231 'repo_summary', repo_name=self.db_repo_name)
1231 1232 else:
1232 1233 redirect_url = default_redirect_url
1233 1234
1234 1235 if not filename:
1235 1236 h.flash(_('No filename'), category='warning')
1236 1237 raise HTTPFound(redirect_url)
1237 1238
1238 1239 # extract the location from filename,
1239 1240 # allows using foo/bar.txt syntax to create subdirectories
1240 1241 subdir_loc = filename.rsplit('/', 1)
1241 1242 if len(subdir_loc) == 2:
1242 1243 location = os.path.join(location, subdir_loc[0])
1243 1244
1244 1245 # strip all crap out of file, just leave the basename
1245 1246 filename = os.path.basename(filename)
1246 1247 node_path = os.path.join(location, filename)
1247 1248 author = self._rhodecode_db_user.full_contact
1248 1249
1249 1250 try:
1250 1251 nodes = {
1251 1252 node_path: {
1252 1253 'content': content
1253 1254 }
1254 1255 }
1255 1256 ScmModel().create_nodes(
1256 1257 user=self._rhodecode_db_user.user_id,
1257 1258 repo=self.db_repo,
1258 1259 message=message,
1259 1260 nodes=nodes,
1260 1261 parent_commit=c.commit,
1261 1262 author=author,
1262 1263 )
1263 1264
1264 1265 h.flash(
1265 1266 _('Successfully committed new file `{}`').format(
1266 1267 h.escape(node_path)), category='success')
1267 1268 except NonRelativePathError:
1268 1269 h.flash(_(
1269 1270 'The location specified must be a relative path and must not '
1270 1271 'contain .. in the path'), category='warning')
1271 1272 raise HTTPFound(default_redirect_url)
1272 1273 except (NodeError, NodeAlreadyExistsError) as e:
1273 1274 h.flash(_(h.escape(e)), category='error')
1274 1275 except Exception:
1275 1276 log.exception('Error occurred during commit')
1276 1277 h.flash(_('Error occurred during commit'), category='error')
1277 1278
1278 1279 raise HTTPFound(default_redirect_url)
@@ -1,599 +1,588 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 277 # ADMIN USER GROUPS REST ROUTES
278 278 with rmap.submapper(path_prefix=ADMIN_PREFIX,
279 279 controller='admin/user_groups') as m:
280 280 m.connect('users_groups', '/user_groups',
281 281 action='create', conditions={'method': ['POST']})
282 282 m.connect('users_groups', '/user_groups',
283 283 action='index', conditions={'method': ['GET']})
284 284 m.connect('new_users_group', '/user_groups/new',
285 285 action='new', conditions={'method': ['GET']})
286 286 m.connect('update_users_group', '/user_groups/{user_group_id}',
287 287 action='update', conditions={'method': ['PUT']})
288 288 m.connect('delete_users_group', '/user_groups/{user_group_id}',
289 289 action='delete', conditions={'method': ['DELETE']})
290 290 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
291 291 action='edit', conditions={'method': ['GET']},
292 292 function=check_user_group)
293 293
294 294 # EXTRAS USER GROUP ROUTES
295 295 m.connect('edit_user_group_global_perms',
296 296 '/user_groups/{user_group_id}/edit/global_permissions',
297 297 action='edit_global_perms', conditions={'method': ['GET']})
298 298 m.connect('edit_user_group_global_perms',
299 299 '/user_groups/{user_group_id}/edit/global_permissions',
300 300 action='update_global_perms', conditions={'method': ['PUT']})
301 301 m.connect('edit_user_group_perms_summary',
302 302 '/user_groups/{user_group_id}/edit/permissions_summary',
303 303 action='edit_perms_summary', conditions={'method': ['GET']})
304 304
305 305 m.connect('edit_user_group_perms',
306 306 '/user_groups/{user_group_id}/edit/permissions',
307 307 action='edit_perms', conditions={'method': ['GET']})
308 308 m.connect('edit_user_group_perms',
309 309 '/user_groups/{user_group_id}/edit/permissions',
310 310 action='update_perms', conditions={'method': ['PUT']})
311 311
312 312 m.connect('edit_user_group_advanced',
313 313 '/user_groups/{user_group_id}/edit/advanced',
314 314 action='edit_advanced', conditions={'method': ['GET']})
315 315
316 316 m.connect('edit_user_group_advanced_sync',
317 317 '/user_groups/{user_group_id}/edit/advanced/sync',
318 318 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
319 319
320 320 m.connect('edit_user_group_members',
321 321 '/user_groups/{user_group_id}/edit/members', jsroute=True,
322 322 action='user_group_members', conditions={'method': ['GET']})
323 323
324 324 # ADMIN DEFAULTS REST ROUTES
325 325 with rmap.submapper(path_prefix=ADMIN_PREFIX,
326 326 controller='admin/defaults') as m:
327 327 m.connect('admin_defaults_repositories', '/defaults/repositories',
328 328 action='update_repository_defaults', conditions={'method': ['POST']})
329 329 m.connect('admin_defaults_repositories', '/defaults/repositories',
330 330 action='index', conditions={'method': ['GET']})
331 331
332 332 # ADMIN SETTINGS ROUTES
333 333 with rmap.submapper(path_prefix=ADMIN_PREFIX,
334 334 controller='admin/settings') as m:
335 335
336 336 # default
337 337 m.connect('admin_settings', '/settings',
338 338 action='settings_global_update',
339 339 conditions={'method': ['POST']})
340 340 m.connect('admin_settings', '/settings',
341 341 action='settings_global', conditions={'method': ['GET']})
342 342
343 343 m.connect('admin_settings_vcs', '/settings/vcs',
344 344 action='settings_vcs_update',
345 345 conditions={'method': ['POST']})
346 346 m.connect('admin_settings_vcs', '/settings/vcs',
347 347 action='settings_vcs',
348 348 conditions={'method': ['GET']})
349 349 m.connect('admin_settings_vcs', '/settings/vcs',
350 350 action='delete_svn_pattern',
351 351 conditions={'method': ['DELETE']})
352 352
353 353 m.connect('admin_settings_mapping', '/settings/mapping',
354 354 action='settings_mapping_update',
355 355 conditions={'method': ['POST']})
356 356 m.connect('admin_settings_mapping', '/settings/mapping',
357 357 action='settings_mapping', conditions={'method': ['GET']})
358 358
359 359 m.connect('admin_settings_global', '/settings/global',
360 360 action='settings_global_update',
361 361 conditions={'method': ['POST']})
362 362 m.connect('admin_settings_global', '/settings/global',
363 363 action='settings_global', conditions={'method': ['GET']})
364 364
365 365 m.connect('admin_settings_visual', '/settings/visual',
366 366 action='settings_visual_update',
367 367 conditions={'method': ['POST']})
368 368 m.connect('admin_settings_visual', '/settings/visual',
369 369 action='settings_visual', conditions={'method': ['GET']})
370 370
371 371 m.connect('admin_settings_issuetracker',
372 372 '/settings/issue-tracker', action='settings_issuetracker',
373 373 conditions={'method': ['GET']})
374 374 m.connect('admin_settings_issuetracker_save',
375 375 '/settings/issue-tracker/save',
376 376 action='settings_issuetracker_save',
377 377 conditions={'method': ['POST']})
378 378 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
379 379 action='settings_issuetracker_test',
380 380 conditions={'method': ['POST']})
381 381 m.connect('admin_issuetracker_delete',
382 382 '/settings/issue-tracker/delete',
383 383 action='settings_issuetracker_delete',
384 384 conditions={'method': ['DELETE']})
385 385
386 386 m.connect('admin_settings_email', '/settings/email',
387 387 action='settings_email_update',
388 388 conditions={'method': ['POST']})
389 389 m.connect('admin_settings_email', '/settings/email',
390 390 action='settings_email', conditions={'method': ['GET']})
391 391
392 392 m.connect('admin_settings_hooks', '/settings/hooks',
393 393 action='settings_hooks_update',
394 394 conditions={'method': ['POST', 'DELETE']})
395 395 m.connect('admin_settings_hooks', '/settings/hooks',
396 396 action='settings_hooks', conditions={'method': ['GET']})
397 397
398 398 m.connect('admin_settings_search', '/settings/search',
399 399 action='settings_search', conditions={'method': ['GET']})
400 400
401 401 m.connect('admin_settings_supervisor', '/settings/supervisor',
402 402 action='settings_supervisor', conditions={'method': ['GET']})
403 403 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
404 404 action='settings_supervisor_log', conditions={'method': ['GET']})
405 405
406 406 m.connect('admin_settings_labs', '/settings/labs',
407 407 action='settings_labs_update',
408 408 conditions={'method': ['POST']})
409 409 m.connect('admin_settings_labs', '/settings/labs',
410 410 action='settings_labs', conditions={'method': ['GET']})
411 411
412 412 # ADMIN MY ACCOUNT
413 413 with rmap.submapper(path_prefix=ADMIN_PREFIX,
414 414 controller='admin/my_account') as m:
415 415
416 416 # NOTE(marcink): this needs to be kept for password force flag to be
417 417 # handled in pylons controllers, remove after full migration to pyramid
418 418 m.connect('my_account_password', '/my_account/password',
419 419 action='my_account_password', conditions={'method': ['GET']})
420 420
421 421 #==========================================================================
422 422 # REPOSITORY ROUTES
423 423 #==========================================================================
424 424
425 425 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
426 426 controller='admin/repos', action='repo_creating',
427 427 requirements=URL_NAME_REQUIREMENTS)
428 428 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
429 429 controller='admin/repos', action='repo_check',
430 430 requirements=URL_NAME_REQUIREMENTS)
431 431
432 432 # repo edit options
433 433 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
434 434 controller='admin/repos', action='edit_fields',
435 435 conditions={'method': ['GET'], 'function': check_repo},
436 436 requirements=URL_NAME_REQUIREMENTS)
437 437 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
438 438 controller='admin/repos', action='create_repo_field',
439 439 conditions={'method': ['PUT'], 'function': check_repo},
440 440 requirements=URL_NAME_REQUIREMENTS)
441 441 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
442 442 controller='admin/repos', action='delete_repo_field',
443 443 conditions={'method': ['DELETE'], 'function': check_repo},
444 444 requirements=URL_NAME_REQUIREMENTS)
445 445
446 446 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
447 447 controller='admin/repos', action='toggle_locking',
448 448 conditions={'method': ['GET'], 'function': check_repo},
449 449 requirements=URL_NAME_REQUIREMENTS)
450 450
451 451 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
452 452 controller='admin/repos', action='edit_remote_form',
453 453 conditions={'method': ['GET'], 'function': check_repo},
454 454 requirements=URL_NAME_REQUIREMENTS)
455 455 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
456 456 controller='admin/repos', action='edit_remote',
457 457 conditions={'method': ['PUT'], 'function': check_repo},
458 458 requirements=URL_NAME_REQUIREMENTS)
459 459
460 460 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
461 461 controller='admin/repos', action='edit_statistics_form',
462 462 conditions={'method': ['GET'], 'function': check_repo},
463 463 requirements=URL_NAME_REQUIREMENTS)
464 464 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
465 465 controller='admin/repos', action='edit_statistics',
466 466 conditions={'method': ['PUT'], 'function': check_repo},
467 467 requirements=URL_NAME_REQUIREMENTS)
468 468 rmap.connect('repo_settings_issuetracker',
469 469 '/{repo_name}/settings/issue-tracker',
470 470 controller='admin/repos', action='repo_issuetracker',
471 471 conditions={'method': ['GET'], 'function': check_repo},
472 472 requirements=URL_NAME_REQUIREMENTS)
473 473 rmap.connect('repo_issuetracker_test',
474 474 '/{repo_name}/settings/issue-tracker/test',
475 475 controller='admin/repos', action='repo_issuetracker_test',
476 476 conditions={'method': ['POST'], 'function': check_repo},
477 477 requirements=URL_NAME_REQUIREMENTS)
478 478 rmap.connect('repo_issuetracker_delete',
479 479 '/{repo_name}/settings/issue-tracker/delete',
480 480 controller='admin/repos', action='repo_issuetracker_delete',
481 481 conditions={'method': ['DELETE'], 'function': check_repo},
482 482 requirements=URL_NAME_REQUIREMENTS)
483 483 rmap.connect('repo_issuetracker_save',
484 484 '/{repo_name}/settings/issue-tracker/save',
485 485 controller='admin/repos', action='repo_issuetracker_save',
486 486 conditions={'method': ['POST'], 'function': check_repo},
487 487 requirements=URL_NAME_REQUIREMENTS)
488 488 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
489 489 controller='admin/repos', action='repo_settings_vcs_update',
490 490 conditions={'method': ['POST'], 'function': check_repo},
491 491 requirements=URL_NAME_REQUIREMENTS)
492 492 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
493 493 controller='admin/repos', action='repo_settings_vcs',
494 494 conditions={'method': ['GET'], 'function': check_repo},
495 495 requirements=URL_NAME_REQUIREMENTS)
496 496 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
497 497 controller='admin/repos', action='repo_delete_svn_pattern',
498 498 conditions={'method': ['DELETE'], 'function': check_repo},
499 499 requirements=URL_NAME_REQUIREMENTS)
500 500 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
501 501 controller='admin/repos', action='repo_settings_pullrequest',
502 502 conditions={'method': ['GET', 'POST'], 'function': check_repo},
503 503 requirements=URL_NAME_REQUIREMENTS)
504 504
505 rmap.connect('compare_home',
506 '/{repo_name}/compare',
507 controller='compare', action='index',
508 conditions={'function': check_repo},
509 requirements=URL_NAME_REQUIREMENTS)
510
511 rmap.connect('compare_url',
512 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
513 controller='compare', action='compare',
514 conditions={'function': check_repo},
515 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
516 505
517 506 rmap.connect('pullrequest_home',
518 507 '/{repo_name}/pull-request/new', controller='pullrequests',
519 508 action='index', conditions={'function': check_repo,
520 509 'method': ['GET']},
521 510 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
522 511
523 512 rmap.connect('pullrequest',
524 513 '/{repo_name}/pull-request/new', controller='pullrequests',
525 514 action='create', conditions={'function': check_repo,
526 515 'method': ['POST']},
527 516 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
528 517
529 518 rmap.connect('pullrequest_repo_refs',
530 519 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
531 520 controller='pullrequests',
532 521 action='get_repo_refs',
533 522 conditions={'function': check_repo, 'method': ['GET']},
534 523 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
535 524
536 525 rmap.connect('pullrequest_repo_destinations',
537 526 '/{repo_name}/pull-request/repo-destinations',
538 527 controller='pullrequests',
539 528 action='get_repo_destinations',
540 529 conditions={'function': check_repo, 'method': ['GET']},
541 530 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
542 531
543 532 rmap.connect('pullrequest_show',
544 533 '/{repo_name}/pull-request/{pull_request_id}',
545 534 controller='pullrequests',
546 535 action='show', conditions={'function': check_repo,
547 536 'method': ['GET']},
548 537 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
549 538
550 539 rmap.connect('pullrequest_update',
551 540 '/{repo_name}/pull-request/{pull_request_id}',
552 541 controller='pullrequests',
553 542 action='update', conditions={'function': check_repo,
554 543 'method': ['PUT']},
555 544 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
556 545
557 546 rmap.connect('pullrequest_merge',
558 547 '/{repo_name}/pull-request/{pull_request_id}',
559 548 controller='pullrequests',
560 549 action='merge', conditions={'function': check_repo,
561 550 'method': ['POST']},
562 551 requirements=URL_NAME_REQUIREMENTS)
563 552
564 553 rmap.connect('pullrequest_delete',
565 554 '/{repo_name}/pull-request/{pull_request_id}',
566 555 controller='pullrequests',
567 556 action='delete', conditions={'function': check_repo,
568 557 'method': ['DELETE']},
569 558 requirements=URL_NAME_REQUIREMENTS)
570 559
571 560 rmap.connect('pullrequest_comment',
572 561 '/{repo_name}/pull-request-comment/{pull_request_id}',
573 562 controller='pullrequests',
574 563 action='comment', conditions={'function': check_repo,
575 564 'method': ['POST']},
576 565 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
577 566
578 567 rmap.connect('pullrequest_comment_delete',
579 568 '/{repo_name}/pull-request-comment/{comment_id}/delete',
580 569 controller='pullrequests', action='delete_comment',
581 570 conditions={'function': check_repo, 'method': ['DELETE']},
582 571 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
583 572
584 573 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
585 574 controller='forks', action='fork_create',
586 575 conditions={'function': check_repo, 'method': ['POST']},
587 576 requirements=URL_NAME_REQUIREMENTS)
588 577
589 578 rmap.connect('repo_fork_home', '/{repo_name}/fork',
590 579 controller='forks', action='fork',
591 580 conditions={'function': check_repo},
592 581 requirements=URL_NAME_REQUIREMENTS)
593 582
594 583 rmap.connect('repo_forks_home', '/{repo_name}/forks',
595 584 controller='forks', action='forks',
596 585 conditions={'function': check_repo},
597 586 requirements=URL_NAME_REQUIREMENTS)
598 587
599 588 return rmap
@@ -1,216 +1,217 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 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']);
19 18 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
20 19 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
21 20 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
22 21 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
23 22 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
24 23 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
25 24 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
26 25 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
27 26 pyroutes.register('favicon', '/favicon.ico', []);
28 27 pyroutes.register('robots', '/robots.txt', []);
29 28 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
30 29 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
31 30 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
32 31 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
33 32 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
34 33 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
35 34 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/settings/integrations', ['repo_group_name']);
36 35 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
37 36 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/settings/integrations/new', ['repo_group_name']);
38 37 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
39 38 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
40 39 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
41 40 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
42 41 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
43 42 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
44 43 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
45 44 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
46 45 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
47 46 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
48 47 pyroutes.register('admin_home', '/_admin', []);
49 48 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
50 49 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
51 50 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
52 51 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
53 52 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
54 53 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
55 54 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
56 55 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
57 56 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
58 57 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
59 58 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
60 59 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
61 60 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
62 61 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
63 62 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
64 63 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
65 64 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
66 65 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
67 66 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
68 67 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
69 68 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
70 69 pyroutes.register('users', '/_admin/users', []);
71 70 pyroutes.register('users_data', '/_admin/users_data', []);
72 71 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
73 72 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
74 73 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
75 74 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
76 75 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
77 76 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
78 77 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
79 78 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
80 79 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
81 80 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
82 81 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
83 82 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
84 83 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
85 84 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
86 85 pyroutes.register('channelstream_proxy', '/_channelstream', []);
87 86 pyroutes.register('login', '/_admin/login', []);
88 87 pyroutes.register('logout', '/_admin/logout', []);
89 88 pyroutes.register('register', '/_admin/register', []);
90 89 pyroutes.register('reset_password', '/_admin/password_reset', []);
91 90 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
92 91 pyroutes.register('home', '/', []);
93 92 pyroutes.register('user_autocomplete_data', '/_users', []);
94 93 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
95 94 pyroutes.register('repo_list_data', '/_repos', []);
96 95 pyroutes.register('goto_switcher_data', '/_goto_data', []);
97 96 pyroutes.register('journal', '/_admin/journal', []);
98 97 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
99 98 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
100 99 pyroutes.register('journal_public', '/_admin/public_journal', []);
101 100 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
102 101 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
103 102 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
104 103 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
105 104 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
106 105 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
107 106 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
108 107 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
109 108 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
110 109 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
111 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
112 110 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
113 111 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
114 112 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
115 113 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
116 114 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
117 115 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
118 116 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
117 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
119 118 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
120 119 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
121 120 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
122 121 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
123 122 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
124 123 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
125 124 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
126 125 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
127 126 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
128 127 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
129 128 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
130 129 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
131 130 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
132 131 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
133 132 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
134 133 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
135 134 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
136 135 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
137 136 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
138 137 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
139 138 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
140 139 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
141 140 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
142 141 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
143 142 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
144 143 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
145 144 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
146 145 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
147 146 pyroutes.register('repo_changelog_elements', '/%(repo_name)s/changelog_elements', ['repo_name']);
147 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
148 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
148 149 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
149 150 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
150 151 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
151 152 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
152 153 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
153 154 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
154 155 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
155 156 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
156 157 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
157 158 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
158 159 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
159 160 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
160 161 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
161 162 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
162 163 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
163 164 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
164 165 pyroutes.register('repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
165 166 pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
166 167 pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
167 168 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
168 169 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
169 170 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
170 171 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
171 172 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
172 173 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
173 174 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
174 175 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
175 176 pyroutes.register('search', '/_admin/search', []);
176 177 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
177 178 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
178 179 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
179 180 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
180 181 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
181 182 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
182 183 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
183 184 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
184 185 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
185 186 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
186 187 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
187 188 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
188 189 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
189 190 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
190 191 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
191 192 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
192 193 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
193 194 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
194 195 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
195 196 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
196 197 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
197 198 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
198 199 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
199 200 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
200 201 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
201 202 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
202 203 pyroutes.register('gists_show', '/_admin/gists', []);
203 204 pyroutes.register('gists_new', '/_admin/gists/new', []);
204 205 pyroutes.register('gists_create', '/_admin/gists/create', []);
205 206 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
206 207 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
207 208 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
208 209 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
209 210 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
210 211 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
211 212 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
212 213 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
213 214 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
214 215 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
215 216 pyroutes.register('apiv2', '/_admin/api', []);
216 217 }
@@ -1,501 +1,501 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 RhodeCode JS Files
21 21 **/
22 22
23 23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 24 console = { log: function() {} }
25 25 }
26 26
27 27 // TODO: move the following function to submodules
28 28
29 29 /**
30 30 * show more
31 31 */
32 32 var show_more_event = function(){
33 33 $('table .show_more').click(function(e) {
34 34 var cid = e.target.id.substring(1);
35 35 var button = $(this);
36 36 if (button.hasClass('open')) {
37 37 $('#'+cid).hide();
38 38 button.removeClass('open');
39 39 } else {
40 40 $('#'+cid).show();
41 41 button.addClass('open one');
42 42 }
43 43 });
44 44 };
45 45
46 46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 47 $('#compare_action').on('click', function(e){
48 48 e.preventDefault();
49 49
50 50 var source = $('input[name=compare_source]:checked').val();
51 51 var target = $('input[name=compare_target]:checked').val();
52 52 if(source && target){
53 53 var url_data = {
54 54 repo_name: repo_name,
55 55 source_ref: source,
56 56 source_ref_type: compare_ref_type,
57 57 target_ref: target,
58 58 target_ref_type: compare_ref_type,
59 59 merge: 1
60 60 };
61 window.location = pyroutes.url('compare_url', url_data);
61 window.location = pyroutes.url('repo_compare', url_data);
62 62 }
63 63 });
64 64 $('.compare-radio-button').on('click', function(e){
65 65 var source = $('input[name=compare_source]:checked').val();
66 66 var target = $('input[name=compare_target]:checked').val();
67 67 if(source && target){
68 68 $('#compare_action').removeAttr("disabled");
69 69 $('#compare_action').removeClass("disabled");
70 70 }
71 71 })
72 72 };
73 73
74 74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 75 var container = $('#' + target);
76 76 var url = pyroutes.url('repo_stats',
77 77 {"repo_name": repo_name, "commit_id": commit_id});
78 78
79 79 if (!container.hasClass('loaded')) {
80 80 $.ajax({url: url})
81 81 .complete(function (data) {
82 82 var responseJSON = data.responseJSON;
83 83 container.addClass('loaded');
84 84 container.html(responseJSON.size);
85 85 callback(responseJSON.code_stats)
86 86 })
87 87 .fail(function (data) {
88 88 console.log('failed to load repo stats');
89 89 });
90 90 }
91 91
92 92 };
93 93
94 94 var showRepoStats = function(target, data){
95 95 var container = $('#' + target);
96 96
97 97 if (container.hasClass('loaded')) {
98 98 return
99 99 }
100 100
101 101 var total = 0;
102 102 var no_data = true;
103 103 var tbl = document.createElement('table');
104 104 tbl.setAttribute('class', 'trending_language_tbl');
105 105
106 106 $.each(data, function(key, val){
107 107 total += val.count;
108 108 });
109 109
110 110 var sortedStats = [];
111 111 for (var obj in data){
112 112 sortedStats.push([obj, data[obj]])
113 113 }
114 114 var sortedData = sortedStats.sort(function (a, b) {
115 115 return b[1].count - a[1].count
116 116 });
117 117 var cnt = 0;
118 118 $.each(sortedData, function(idx, val){
119 119 cnt += 1;
120 120 no_data = false;
121 121
122 122 var hide = cnt > 2;
123 123 var tr = document.createElement('tr');
124 124 if (hide) {
125 125 tr.setAttribute('style', 'display:none');
126 126 tr.setAttribute('class', 'stats_hidden');
127 127 }
128 128
129 129 var key = val[0];
130 130 var obj = {"desc": val[1].desc, "count": val[1].count};
131 131
132 132 var percentage = Math.round((obj.count / total * 100), 2);
133 133
134 134 var td1 = document.createElement('td');
135 135 td1.width = 300;
136 136 var trending_language_label = document.createElement('div');
137 137 trending_language_label.innerHTML = obj.desc + " (.{0})".format(key);
138 138 td1.appendChild(trending_language_label);
139 139
140 140 var td2 = document.createElement('td');
141 141 var trending_language = document.createElement('div');
142 142 var nr_files = obj.count +" "+ _ngettext('file', 'files', obj.count);
143 143
144 144 trending_language.title = key + " " + nr_files;
145 145
146 146 trending_language.innerHTML = "<span>" + percentage + "% " + nr_files
147 147 + "</span><b>" + percentage + "% " + nr_files + "</b>";
148 148
149 149 trending_language.setAttribute("class", 'trending_language');
150 150 $('b', trending_language)[0].style.width = percentage + "%";
151 151 td2.appendChild(trending_language);
152 152
153 153 tr.appendChild(td1);
154 154 tr.appendChild(td2);
155 155 tbl.appendChild(tr);
156 156 if (cnt == 3) {
157 157 var show_more = document.createElement('tr');
158 158 var td = document.createElement('td');
159 159 lnk = document.createElement('a');
160 160
161 161 lnk.href = '#';
162 162 lnk.innerHTML = _gettext('Show more');
163 163 lnk.id = 'code_stats_show_more';
164 164 td.appendChild(lnk);
165 165
166 166 show_more.appendChild(td);
167 167 show_more.appendChild(document.createElement('td'));
168 168 tbl.appendChild(show_more);
169 169 }
170 170 });
171 171
172 172 $(container).html(tbl);
173 173 $(container).addClass('loaded');
174 174
175 175 $('#code_stats_show_more').on('click', function (e) {
176 176 e.preventDefault();
177 177 $('.stats_hidden').each(function (idx) {
178 178 $(this).css("display", "");
179 179 });
180 180 $('#code_stats_show_more').hide();
181 181 });
182 182
183 183 };
184 184
185 185 // returns a node from given html;
186 186 var fromHTML = function(html){
187 187 var _html = document.createElement('element');
188 188 _html.innerHTML = html;
189 189 return _html;
190 190 };
191 191
192 192 // Toggle Collapsable Content
193 193 function collapsableContent() {
194 194
195 195 $('.collapsable-content').not('.no-hide').hide();
196 196
197 197 $('.btn-collapse').unbind(); //in case we've been here before
198 198 $('.btn-collapse').click(function() {
199 199 var button = $(this);
200 200 var togglename = $(this).data("toggle");
201 201 $('.collapsable-content[data-toggle='+togglename+']').toggle();
202 202 if ($(this).html()=="Show Less")
203 203 $(this).html("Show More");
204 204 else
205 205 $(this).html("Show Less");
206 206 });
207 207 };
208 208
209 209 var timeagoActivate = function() {
210 210 $("time.timeago").timeago();
211 211 };
212 212
213 213
214 214 var clipboardActivate = function() {
215 215 /*
216 216 *
217 217 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
218 218 * */
219 219 var clipboard = new Clipboard('.clipboard-action');
220 220
221 221 clipboard.on('success', function(e) {
222 222 var callback = function () {
223 223 $(e.trigger).animate({'opacity': 1.00}, 200)
224 224 };
225 225 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
226 226 e.clearSelection();
227 227 });
228 228 };
229 229
230 230
231 231 // Formatting values in a Select2 dropdown of commit references
232 232 var formatSelect2SelectionRefs = function(commit_ref){
233 233 var tmpl = '';
234 234 if (!commit_ref.text || commit_ref.type === 'sha'){
235 235 return commit_ref.text;
236 236 }
237 237 if (commit_ref.type === 'branch'){
238 238 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
239 239 } else if (commit_ref.type === 'tag'){
240 240 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
241 241 } else if (commit_ref.type === 'book'){
242 242 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
243 243 }
244 244 return tmpl.concat(commit_ref.text);
245 245 };
246 246
247 247 // takes a given html element and scrolls it down offset pixels
248 248 function offsetScroll(element, offset) {
249 249 setTimeout(function() {
250 250 var location = element.offset().top;
251 251 // some browsers use body, some use html
252 252 $('html, body').animate({ scrollTop: (location - offset) });
253 253 }, 100);
254 254 }
255 255
256 256 // scroll an element `percent`% from the top of page in `time` ms
257 257 function scrollToElement(element, percent, time) {
258 258 percent = (percent === undefined ? 25 : percent);
259 259 time = (time === undefined ? 100 : time);
260 260
261 261 var $element = $(element);
262 262 if ($element.length == 0) {
263 263 throw('Cannot scroll to {0}'.format(element))
264 264 }
265 265 var elOffset = $element.offset().top;
266 266 var elHeight = $element.height();
267 267 var windowHeight = $(window).height();
268 268 var offset = elOffset;
269 269 if (elHeight < windowHeight) {
270 270 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
271 271 }
272 272 setTimeout(function() {
273 273 $('html, body').animate({ scrollTop: offset});
274 274 }, time);
275 275 }
276 276
277 277 /**
278 278 * global hooks after DOM is loaded
279 279 */
280 280 $(document).ready(function() {
281 281 firefoxAnchorFix();
282 282
283 283 $('.navigation a.menulink').on('click', function(e){
284 284 var menuitem = $(this).parent('li');
285 285 if (menuitem.hasClass('open')) {
286 286 menuitem.removeClass('open');
287 287 } else {
288 288 menuitem.addClass('open');
289 289 $(document).on('click', function(event) {
290 290 if (!$(event.target).closest(menuitem).length) {
291 291 menuitem.removeClass('open');
292 292 }
293 293 });
294 294 }
295 295 });
296 296 $('.compare_view_files').on(
297 297 'mouseenter mouseleave', 'tr.line .lineno a',function(event) {
298 298 if (event.type === "mouseenter") {
299 299 $(this).parents('tr.line').addClass('hover');
300 300 } else {
301 301 $(this).parents('tr.line').removeClass('hover');
302 302 }
303 303 });
304 304
305 305 $('.compare_view_files').on(
306 306 'mouseenter mouseleave', 'tr.line .add-comment-line a',function(event){
307 307 if (event.type === "mouseenter") {
308 308 $(this).parents('tr.line').addClass('commenting');
309 309 } else {
310 310 $(this).parents('tr.line').removeClass('commenting');
311 311 }
312 312 });
313 313
314 314 $('body').on( /* TODO: replace the $('.compare_view_files').on('click') below
315 315 when new diffs are integrated */
316 316 'click', '.cb-lineno a', function(event) {
317 317
318 318 if ($(this).attr('data-line-no') !== ""){
319 319 $('.cb-line-selected').removeClass('cb-line-selected');
320 320 var td = $(this).parent();
321 321 td.addClass('cb-line-selected'); // line number td
322 322 td.prev().addClass('cb-line-selected'); // line data td
323 323 td.next().addClass('cb-line-selected'); // line content td
324 324
325 325 // Replace URL without jumping to it if browser supports.
326 326 // Default otherwise
327 327 if (history.pushState) {
328 328 var new_location = location.href.rstrip('#');
329 329 if (location.hash) {
330 330 new_location = new_location.replace(location.hash, "");
331 331 }
332 332
333 333 // Make new anchor url
334 334 new_location = new_location + $(this).attr('href');
335 335 history.pushState(true, document.title, new_location);
336 336
337 337 return false;
338 338 }
339 339 }
340 340 });
341 341
342 342 $('.compare_view_files').on( /* TODO: replace this with .cb function above
343 343 when new diffs are integrated */
344 344 'click', 'tr.line .lineno a',function(event) {
345 345 if ($(this).text() != ""){
346 346 $('tr.line').removeClass('selected');
347 347 $(this).parents("tr.line").addClass('selected');
348 348
349 349 // Replace URL without jumping to it if browser supports.
350 350 // Default otherwise
351 351 if (history.pushState) {
352 352 var new_location = location.href;
353 353 if (location.hash){
354 354 new_location = new_location.replace(location.hash, "");
355 355 }
356 356
357 357 // Make new anchor url
358 358 var new_location = new_location+$(this).attr('href');
359 359 history.pushState(true, document.title, new_location);
360 360
361 361 return false;
362 362 }
363 363 }
364 364 });
365 365
366 366 $('.compare_view_files').on(
367 367 'click', 'tr.line .add-comment-line a',function(event) {
368 368 var tr = $(event.currentTarget).parents('tr.line')[0];
369 369 injectInlineForm(tr);
370 370 return false;
371 371 });
372 372
373 373 $('.collapse_file').on('click', function(e) {
374 374 e.stopPropagation();
375 375 if ($(e.target).is('a')) { return; }
376 376 var node = $(e.delegateTarget).first();
377 377 var icon = $($(node.children().first()).children().first());
378 378 var id = node.attr('fid');
379 379 var target = $('#'+id);
380 380 var tr = $('#tr_'+id);
381 381 var diff = $('#diff_'+id);
382 382 if(node.hasClass('expand_file')){
383 383 node.removeClass('expand_file');
384 384 icon.removeClass('expand_file_icon');
385 385 node.addClass('collapse_file');
386 386 icon.addClass('collapse_file_icon');
387 387 diff.show();
388 388 tr.show();
389 389 target.show();
390 390 } else {
391 391 node.removeClass('collapse_file');
392 392 icon.removeClass('collapse_file_icon');
393 393 node.addClass('expand_file');
394 394 icon.addClass('expand_file_icon');
395 395 diff.hide();
396 396 tr.hide();
397 397 target.hide();
398 398 }
399 399 });
400 400
401 401 $('#expand_all_files').click(function() {
402 402 $('.expand_file').each(function() {
403 403 var node = $(this);
404 404 var icon = $($(node.children().first()).children().first());
405 405 var id = $(this).attr('fid');
406 406 var target = $('#'+id);
407 407 var tr = $('#tr_'+id);
408 408 var diff = $('#diff_'+id);
409 409 node.removeClass('expand_file');
410 410 icon.removeClass('expand_file_icon');
411 411 node.addClass('collapse_file');
412 412 icon.addClass('collapse_file_icon');
413 413 diff.show();
414 414 tr.show();
415 415 target.show();
416 416 });
417 417 });
418 418
419 419 $('#collapse_all_files').click(function() {
420 420 $('.collapse_file').each(function() {
421 421 var node = $(this);
422 422 var icon = $($(node.children().first()).children().first());
423 423 var id = $(this).attr('fid');
424 424 var target = $('#'+id);
425 425 var tr = $('#tr_'+id);
426 426 var diff = $('#diff_'+id);
427 427 node.removeClass('collapse_file');
428 428 icon.removeClass('collapse_file_icon');
429 429 node.addClass('expand_file');
430 430 icon.addClass('expand_file_icon');
431 431 diff.hide();
432 432 tr.hide();
433 433 target.hide();
434 434 });
435 435 });
436 436
437 437 // Mouse over behavior for comments and line selection
438 438
439 439 // Select the line that comes from the url anchor
440 440 // At the time of development, Chrome didn't seem to support jquery's :target
441 441 // element, so I had to scroll manually
442 442
443 443 if (location.hash) {
444 444 var result = splitDelimitedHash(location.hash);
445 445 var loc = result.loc;
446 446 if (loc.length > 1) {
447 447
448 448 var highlightable_line_tds = [];
449 449
450 450 // source code line format
451 451 var page_highlights = loc.substring(
452 452 loc.indexOf('#') + 1).split('L');
453 453
454 454 if (page_highlights.length > 1) {
455 455 var highlight_ranges = page_highlights[1].split(",");
456 456 var h_lines = [];
457 457 for (var pos in highlight_ranges) {
458 458 var _range = highlight_ranges[pos].split('-');
459 459 if (_range.length === 2) {
460 460 var start = parseInt(_range[0]);
461 461 var end = parseInt(_range[1]);
462 462 if (start < end) {
463 463 for (var i = start; i <= end; i++) {
464 464 h_lines.push(i);
465 465 }
466 466 }
467 467 }
468 468 else {
469 469 h_lines.push(parseInt(highlight_ranges[pos]));
470 470 }
471 471 }
472 472 for (pos in h_lines) {
473 473 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
474 474 if (line_td.length) {
475 475 highlightable_line_tds.push(line_td);
476 476 }
477 477 }
478 478 }
479 479
480 480 // now check a direct id reference (diff page)
481 481 if ($(loc).length && $(loc).hasClass('cb-lineno')) {
482 482 highlightable_line_tds.push($(loc));
483 483 }
484 484 $.each(highlightable_line_tds, function (i, $td) {
485 485 $td.addClass('cb-line-selected'); // line number td
486 486 $td.prev().addClass('cb-line-selected'); // line data
487 487 $td.next().addClass('cb-line-selected'); // line content
488 488 });
489 489
490 490 if (highlightable_line_tds.length) {
491 491 var $first_line_td = highlightable_line_tds[0];
492 492 scrollToElement($first_line_td);
493 493 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
494 494 td: $first_line_td,
495 495 remainder: result.remainder
496 496 });
497 497 }
498 498 }
499 499 }
500 500 collapsableContent();
501 501 });
@@ -1,600 +1,609 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.route_path('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 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 <li class="${is_active('compare')}">
232 <a class="menulink" href="${h.url('compare_home',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a>
233 </li>
231 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
234 232 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
235 233 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
236 234 <li class="${is_active('showpullrequest')}">
237 235 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
238 236 %if c.repository_pull_requests:
239 237 <span class="pr_notifications">${c.repository_pull_requests}</span>
240 238 %endif
241 239 <div class="menulabel">${_('Pull Requests')}</div>
242 240 </a>
243 241 </li>
244 242 %endif
245 243 <li class="${is_active('options')}">
246 244 <a class="menulink dropdown">
247 245 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
248 246 </a>
249 247 <ul class="submenu">
250 248 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
251 249 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
252 250 %endif
253 251 %if c.rhodecode_db_repo.fork:
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 ${_('Compare fork')}</a></li>
252 <li>
253 <a title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
254 href="${h.route_path('repo_compare',
255 repo_name=c.rhodecode_db_repo.fork.repo_name,
256 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
257 source_ref=c.rhodecode_db_repo.landing_rev[1],
258 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
259 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
260 _query=dict(merge=1))}"
261 >
262 ${_('Compare fork')}
263 </a>
264 </li>
256 265 %endif
257 266
258 267 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
259 268
260 269 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
261 270 %if c.rhodecode_db_repo.locked[0]:
262 271 <li><a class="locking_del" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
263 272 %else:
264 273 <li><a class="locking_add" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
265 274 %endif
266 275 %endif
267 276 %if c.rhodecode_user.username != h.DEFAULT_USER:
268 277 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
269 278 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}">${_('Fork')}</a></li>
270 279 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
271 280 %endif
272 281 %endif
273 282 </ul>
274 283 </li>
275 284 </ul>
276 285 </div>
277 286 <div class="clear"></div>
278 287 </div>
279 288 <!--- END CONTEXT BAR -->
280 289
281 290 </%def>
282 291
283 292 <%def name="usermenu(active=False)">
284 293 ## USER MENU
285 294 <li id="quick_login_li" class="${'active' if active else ''}">
286 295 <a id="quick_login_link" class="menulink childs">
287 296 ${gravatar(c.rhodecode_user.email, 20)}
288 297 <span class="user">
289 298 %if c.rhodecode_user.username != h.DEFAULT_USER:
290 299 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
291 300 %else:
292 301 <span>${_('Sign in')}</span>
293 302 %endif
294 303 </span>
295 304 </a>
296 305
297 306 <div class="user-menu submenu">
298 307 <div id="quick_login">
299 308 %if c.rhodecode_user.username == h.DEFAULT_USER:
300 309 <h4>${_('Sign in to your account')}</h4>
301 310 ${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)}
302 311 <div class="form form-vertical">
303 312 <div class="fields">
304 313 <div class="field">
305 314 <div class="label">
306 315 <label for="username">${_('Username')}:</label>
307 316 </div>
308 317 <div class="input">
309 318 ${h.text('username',class_='focus',tabindex=1)}
310 319 </div>
311 320
312 321 </div>
313 322 <div class="field">
314 323 <div class="label">
315 324 <label for="password">${_('Password')}:</label>
316 325 %if h.HasPermissionAny('hg.password_reset.enabled')():
317 326 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
318 327 %endif
319 328 </div>
320 329 <div class="input">
321 330 ${h.password('password',class_='focus',tabindex=2)}
322 331 </div>
323 332 </div>
324 333 <div class="buttons">
325 334 <div class="register">
326 335 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
327 336 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
328 337 %endif
329 338 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
330 339 </div>
331 340 <div class="submit">
332 341 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
333 342 </div>
334 343 </div>
335 344 </div>
336 345 </div>
337 346 ${h.end_form()}
338 347 %else:
339 348 <div class="">
340 349 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
341 350 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
342 351 <div class="email">${c.rhodecode_user.email}</div>
343 352 </div>
344 353 <div class="">
345 354 <ol class="links">
346 355 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
347 356 % if c.rhodecode_user.personal_repo_group:
348 357 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
349 358 % endif
350 359 <li class="logout">
351 360 ${h.secure_form(h.route_path('logout'), request=request)}
352 361 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
353 362 ${h.end_form()}
354 363 </li>
355 364 </ol>
356 365 </div>
357 366 %endif
358 367 </div>
359 368 </div>
360 369 %if c.rhodecode_user.username != h.DEFAULT_USER:
361 370 <div class="pill_container">
362 371 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
363 372 </div>
364 373 % endif
365 374 </li>
366 375 </%def>
367 376
368 377 <%def name="menu_items(active=None)">
369 378 <%
370 379 def is_active(selected):
371 380 if selected == active:
372 381 return "active"
373 382 return ""
374 383 %>
375 384 <ul id="quick" class="main_nav navigation horizontal-list">
376 385 <!-- repo switcher -->
377 386 <li class="${is_active('repositories')} repo_switcher_li has_select2">
378 387 <input id="repo_switcher" name="repo_switcher" type="hidden">
379 388 </li>
380 389
381 390 ## ROOT MENU
382 391 %if c.rhodecode_user.username != h.DEFAULT_USER:
383 392 <li class="${is_active('journal')}">
384 393 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
385 394 <div class="menulabel">${_('Journal')}</div>
386 395 </a>
387 396 </li>
388 397 %else:
389 398 <li class="${is_active('journal')}">
390 399 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
391 400 <div class="menulabel">${_('Public journal')}</div>
392 401 </a>
393 402 </li>
394 403 %endif
395 404 <li class="${is_active('gists')}">
396 405 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
397 406 <div class="menulabel">${_('Gists')}</div>
398 407 </a>
399 408 </li>
400 409 <li class="${is_active('search')}">
401 410 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
402 411 <div class="menulabel">${_('Search')}</div>
403 412 </a>
404 413 </li>
405 414 % if h.HasPermissionAll('hg.admin')('access admin main page'):
406 415 <li class="${is_active('admin')}">
407 416 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
408 417 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
409 418 </a>
410 419 ${admin_menu()}
411 420 </li>
412 421 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
413 422 <li class="${is_active('admin')}">
414 423 <a class="menulink childs" title="${_('Delegated Admin settings')}">
415 424 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
416 425 </a>
417 426 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
418 427 c.rhodecode_user.repository_groups_admin,
419 428 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
420 429 </li>
421 430 % endif
422 431 % if c.debug_style:
423 432 <li class="${is_active('debug_style')}">
424 433 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
425 434 <div class="menulabel">${_('Style')}</div>
426 435 </a>
427 436 </li>
428 437 % endif
429 438 ## render extra user menu
430 439 ${usermenu(active=(active=='my_account'))}
431 440 </ul>
432 441
433 442 <script type="text/javascript">
434 443 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
435 444
436 445 /*format the look of items in the list*/
437 446 var format = function(state, escapeMarkup){
438 447 if (!state.id){
439 448 return state.text; // optgroup
440 449 }
441 450 var obj_dict = state.obj;
442 451 var tmpl = '';
443 452
444 453 if(obj_dict && state.type == 'repo'){
445 454 if(obj_dict['repo_type'] === 'hg'){
446 455 tmpl += '<i class="icon-hg"></i> ';
447 456 }
448 457 else if(obj_dict['repo_type'] === 'git'){
449 458 tmpl += '<i class="icon-git"></i> ';
450 459 }
451 460 else if(obj_dict['repo_type'] === 'svn'){
452 461 tmpl += '<i class="icon-svn"></i> ';
453 462 }
454 463 if(obj_dict['private']){
455 464 tmpl += '<i class="icon-lock" ></i> ';
456 465 }
457 466 else if(visual_show_public_icon){
458 467 tmpl += '<i class="icon-unlock-alt"></i> ';
459 468 }
460 469 }
461 470 if(obj_dict && state.type == 'commit') {
462 471 tmpl += '<i class="icon-tag"></i>';
463 472 }
464 473 if(obj_dict && state.type == 'group'){
465 474 tmpl += '<i class="icon-folder-close"></i> ';
466 475 }
467 476 tmpl += escapeMarkup(state.text);
468 477 return tmpl;
469 478 };
470 479
471 480 var formatResult = function(result, container, query, escapeMarkup) {
472 481 return format(result, escapeMarkup);
473 482 };
474 483
475 484 var formatSelection = function(data, container, escapeMarkup) {
476 485 return format(data, escapeMarkup);
477 486 };
478 487
479 488 $("#repo_switcher").select2({
480 489 cachedDataSource: {},
481 490 minimumInputLength: 2,
482 491 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
483 492 dropdownAutoWidth: true,
484 493 formatResult: formatResult,
485 494 formatSelection: formatSelection,
486 495 containerCssClass: "repo-switcher",
487 496 dropdownCssClass: "repo-switcher-dropdown",
488 497 escapeMarkup: function(m){
489 498 // don't escape our custom placeholder
490 499 if(m.substr(0,23) == '<div class="menulabel">'){
491 500 return m;
492 501 }
493 502
494 503 return Select2.util.escapeMarkup(m);
495 504 },
496 505 query: $.debounce(250, function(query){
497 506 self = this;
498 507 var cacheKey = query.term;
499 508 var cachedData = self.cachedDataSource[cacheKey];
500 509
501 510 if (cachedData) {
502 511 query.callback({results: cachedData.results});
503 512 } else {
504 513 $.ajax({
505 514 url: pyroutes.url('goto_switcher_data'),
506 515 data: {'query': query.term},
507 516 dataType: 'json',
508 517 type: 'GET',
509 518 success: function(data) {
510 519 self.cachedDataSource[cacheKey] = data;
511 520 query.callback({results: data.results});
512 521 },
513 522 error: function(data, textStatus, errorThrown) {
514 523 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
515 524 }
516 525 })
517 526 }
518 527 })
519 528 });
520 529
521 530 $("#repo_switcher").on('select2-selecting', function(e){
522 531 e.preventDefault();
523 532 window.location = e.choice.url;
524 533 });
525 534
526 535 </script>
527 536 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
528 537 </%def>
529 538
530 539 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
531 540 <div class="modal-dialog">
532 541 <div class="modal-content">
533 542 <div class="modal-header">
534 543 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
535 544 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
536 545 </div>
537 546 <div class="modal-body">
538 547 <div class="block-left">
539 548 <table class="keyboard-mappings">
540 549 <tbody>
541 550 <tr>
542 551 <th></th>
543 552 <th>${_('Site-wide shortcuts')}</th>
544 553 </tr>
545 554 <%
546 555 elems = [
547 556 ('/', 'Open quick search box'),
548 557 ('g h', 'Goto home page'),
549 558 ('g g', 'Goto my private gists page'),
550 559 ('g G', 'Goto my public gists page'),
551 560 ('n r', 'New repository page'),
552 561 ('n g', 'New gist page'),
553 562 ]
554 563 %>
555 564 %for key, desc in elems:
556 565 <tr>
557 566 <td class="keys">
558 567 <span class="key tag">${key}</span>
559 568 </td>
560 569 <td>${desc}</td>
561 570 </tr>
562 571 %endfor
563 572 </tbody>
564 573 </table>
565 574 </div>
566 575 <div class="block-left">
567 576 <table class="keyboard-mappings">
568 577 <tbody>
569 578 <tr>
570 579 <th></th>
571 580 <th>${_('Repositories')}</th>
572 581 </tr>
573 582 <%
574 583 elems = [
575 584 ('g s', 'Goto summary page'),
576 585 ('g c', 'Goto changelog page'),
577 586 ('g f', 'Goto files page'),
578 587 ('g F', 'Goto files page with file search activated'),
579 588 ('g p', 'Goto pull requests page'),
580 589 ('g o', 'Goto repository settings'),
581 590 ('g O', 'Goto repository permissions settings'),
582 591 ]
583 592 %>
584 593 %for key, desc in elems:
585 594 <tr>
586 595 <td class="keys">
587 596 <span class="key tag">${key}</span>
588 597 </td>
589 598 <td>${desc}</td>
590 599 </tr>
591 600 %endfor
592 601 </tbody>
593 602 </table>
594 603 </div>
595 604 </div>
596 605 <div class="modal-footer">
597 606 </div>
598 607 </div><!-- /.modal-content -->
599 608 </div><!-- /.modal-dialog -->
600 609 </div><!-- /.modal -->
@@ -1,299 +1,297 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 href="${h.url('compare_url',
42 href="${h.route_path('repo_compare',
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 target_repo=c.repo_name,
47 46 target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
48 47 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
49 merge=1)}"
48 _query=dict(merge=1, target_repo=c.repo_name))}"
50 49 >
51 <i class="icon-loop"></i>
52 ${_('Compare fork with Parent (%s)' % c.rhodecode_db_repo.fork.repo_name)}
50 ${_('Compare fork with Parent (%s)' % c.rhodecode_db_repo.fork.repo_name)}
53 51 </a>
54 52 </span>
55 53 %endif
56 54
57 55 ## pr open link
58 56 %if h.is_hg(c.rhodecode_repo) or h.is_git(c.rhodecode_repo):
59 57 <span>
60 58 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.url('pullrequest_home',repo_name=c.repo_name)}">
61 59 ${_('Open new pull request')}
62 60 </a>
63 61 </span>
64 62 %endif
65 63
66 64 ## clear selection
67 65 <div title="${_('Clear selection')}" class="btn" id="rev_range_clear" style="display:none">
68 66 ${_('Clear selection')}
69 67 </div>
70 68
71 69 </li>
72 70 </ul>
73 71 </div>
74 72
75 73 % if c.pagination:
76 74 <script type="text/javascript" src="${h.asset('js/jquery.commits-graph.js')}"></script>
77 75
78 76 <div class="graph-header">
79 77 <div id="filter_changelog">
80 78 ${h.hidden('branch_filter')}
81 79 %if c.selected_name:
82 80 <div class="btn btn-default" id="clear_filter" >
83 81 ${_('Clear filter')}
84 82 </div>
85 83 %endif
86 84 </div>
87 85 ${self.breadcrumbs('breadcrumbs_light')}
88 86 <div id="commit-counter" data-total=${c.total_cs} class="pull-right">
89 87 ${_ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)}
90 88 </div>
91 89 </div>
92 90
93 91 <div id="graph">
94 92 <div class="graph-col-wrapper">
95 93 <div id="graph_nodes">
96 94 <div id="graph_canvas"></div>
97 95 </div>
98 96 <div id="graph_content" class="main-content graph_full_width">
99 97
100 98 <div class="table">
101 99 <table id="changesets" class="rctable">
102 100 <tr>
103 101 ## checkbox
104 102 <th></th>
105 103 <th colspan="2"></th>
106 104
107 105 <th>${_('Commit')}</th>
108 106 ## commit message expand arrow
109 107 <th></th>
110 108 <th>${_('Commit Message')}</th>
111 109
112 110 <th>${_('Age')}</th>
113 111 <th>${_('Author')}</th>
114 112
115 113 <th>${_('Refs')}</th>
116 114 </tr>
117 115
118 116 <tbody class="commits-range">
119 117 <%include file='changelog_elements.mako'/>
120 118 </tbody>
121 119 </table>
122 120 </div>
123 121 </div>
124 122 <div class="pagination-wh pagination-left">
125 123 ${c.pagination.pager('$link_previous ~2~ $link_next')}
126 124 </div>
127 125 </div>
128 126
129 127 <script type="text/javascript">
130 128 var cache = {};
131 129 $(function(){
132 130
133 131 // Create links to commit ranges when range checkboxes are selected
134 132 var $commitCheckboxes = $('.commit-range');
135 133 // cache elements
136 134 var $commitRangeContainer = $('#rev_range_container');
137 135 var $commitRangeClear = $('#rev_range_clear');
138 136
139 137 var checkboxRangeSelector = function(e){
140 138 var selectedCheckboxes = [];
141 139 for (pos in $commitCheckboxes){
142 140 if($commitCheckboxes[pos].checked){
143 141 selectedCheckboxes.push($commitCheckboxes[pos]);
144 142 }
145 143 }
146 144 var open_new_pull_request = $('#open_new_pull_request');
147 145 if(open_new_pull_request){
148 146 var selected_changes = selectedCheckboxes.length;
149 147 if (selected_changes > 1 || selected_changes == 1 && templateContext.repo_type != 'hg') {
150 148 open_new_pull_request.hide();
151 149 } else {
152 150 if (selected_changes == 1) {
153 151 open_new_pull_request.html(_gettext('Open new pull request for selected commit'));
154 152 } else if (selected_changes == 0) {
155 153 open_new_pull_request.html(_gettext('Open new pull request'));
156 154 }
157 155 open_new_pull_request.show();
158 156 }
159 157 }
160 158
161 159 if (selectedCheckboxes.length>0){
162 160 var revEnd = selectedCheckboxes[0].name;
163 161 var revStart = selectedCheckboxes[selectedCheckboxes.length-1].name;
164 162 var url = pyroutes.url('repo_commit',
165 163 {'repo_name': '${c.repo_name}',
166 164 'commit_id': revStart+'...'+revEnd});
167 165
168 166 var link = (revStart == revEnd)
169 167 ? _gettext('Show selected commit __S')
170 168 : _gettext('Show selected commits __S ... __E');
171 169
172 170 link = link.replace('__S', revStart.substr(0,6));
173 171 link = link.replace('__E', revEnd.substr(0,6));
174 172
175 173 $commitRangeContainer
176 174 .attr('href',url)
177 175 .html(link)
178 176 .show();
179 177
180 178 $commitRangeClear.show();
181 179 var _url = pyroutes.url('pullrequest_home',
182 180 {'repo_name': '${c.repo_name}',
183 181 'commit': revEnd});
184 182 open_new_pull_request.attr('href', _url);
185 183 $('#compare_fork_button').hide();
186 184 } else {
187 185 $commitRangeContainer.hide();
188 186 $commitRangeClear.hide();
189 187
190 188 %if c.branch_name:
191 189 var _url = pyroutes.url('pullrequest_home',
192 190 {'repo_name': '${c.repo_name}',
193 191 'branch':'${c.branch_name}'});
194 192 open_new_pull_request.attr('href', _url);
195 193 %else:
196 194 var _url = pyroutes.url('pullrequest_home',
197 195 {'repo_name': '${c.repo_name}'});
198 196 open_new_pull_request.attr('href', _url);
199 197 %endif
200 198 $('#compare_fork_button').show();
201 199 }
202 200 };
203 201
204 202 $commitCheckboxes.on('click', checkboxRangeSelector);
205 203
206 204 $commitRangeClear.on('click',function(e) {
207 205 $commitCheckboxes.attr('checked', false);
208 206 checkboxRangeSelector();
209 207 e.preventDefault();
210 208 });
211 209
212 210 // make sure the buttons are consistent when navigate back and forth
213 211 checkboxRangeSelector();
214 212
215 213 var msgs = $('.message');
216 214 // get first element height
217 215 var el = $('#graph_content .container')[0];
218 216 var row_h = el.clientHeight;
219 217 for (var i=0; i < msgs.length; i++) {
220 218 var m = msgs[i];
221 219
222 220 var h = m.clientHeight;
223 221 var pad = $(m).css('padding');
224 222 if (h > row_h) {
225 223 var offset = row_h - (h+12);
226 224 $(m.nextElementSibling).css('display','block');
227 225 $(m.nextElementSibling).css('margin-top',offset+'px');
228 226 }
229 227 }
230 228
231 229 $("#clear_filter").on("click", function() {
232 230 var filter = {'repo_name': '${c.repo_name}'};
233 231 window.location = pyroutes.url('repo_changelog', filter);
234 232 });
235 233
236 234 $("#branch_filter").select2({
237 235 'dropdownAutoWidth': true,
238 236 'width': 'resolve',
239 237 'placeholder': "${c.selected_name or _('Filter changelog')}",
240 238 containerCssClass: "drop-menu",
241 239 dropdownCssClass: "drop-menu-dropdown",
242 240 query: function(query){
243 241 var key = 'cache';
244 242 var cached = cache[key] ;
245 243 if(cached) {
246 244 var data = {results: []};
247 245 //filter results
248 246 $.each(cached.results, function(){
249 247 var section = this.text;
250 248 var children = [];
251 249 $.each(this.children, function(){
252 250 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
253 251 children.push({'id': this.id, 'text': this.text, 'type': this.type})
254 252 }
255 253 });
256 254 data.results.push({'text': section, 'children': children});
257 255 query.callback({results: data.results});
258 256 });
259 257 }else{
260 258 $.ajax({
261 259 url: pyroutes.url('repo_refs_changelog_data', {'repo_name': '${c.repo_name}'}),
262 260 data: {},
263 261 dataType: 'json',
264 262 type: 'GET',
265 263 success: function(data) {
266 264 cache[key] = data;
267 265 query.callback({results: data.results});
268 266 }
269 267 })
270 268 }
271 269 }
272 270 });
273 271 $('#branch_filter').on('change', function(e){
274 272 var data = $('#branch_filter').select2('data');
275 273 var selected = data.text;
276 274 var filter = {'repo_name': '${c.repo_name}'};
277 275 if(data.type == 'branch' || data.type == 'branch_closed'){
278 276 filter["branch"] = selected;
279 277 }
280 278 else if (data.type == 'book'){
281 279 filter["bookmark"] = selected;
282 280 }
283 281 window.location = pyroutes.url('repo_changelog', filter);
284 282 });
285 283
286 284 commitsController = new CommitsController();
287 285 % if not c.changelog_for_path:
288 286 commitsController.reloadGraph();
289 287 % endif
290 288
291 289 });
292 290
293 291 </script>
294 292 </div>
295 293 % else:
296 294 ${_('There are no changes yet')}
297 295 % endif
298 296 </div>
299 297 </%def>
@@ -1,125 +1,131 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Commits') % c.repo_name} -
6 6 r${c.commit_ranges[0].revision}:${h.short_id(c.commit_ranges[0].raw_id)}
7 7 ...
8 8 r${c.commit_ranges[-1].revision}:${h.short_id(c.commit_ranges[-1].raw_id)}
9 9 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
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 ${_('Commits')} -
17 17 r${c.commit_ranges[0].revision}:${h.short_id(c.commit_ranges[0].raw_id)}
18 18 ...
19 19 r${c.commit_ranges[-1].revision}:${h.short_id(c.commit_ranges[-1].raw_id)}
20 20 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
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='changelog')}
29 29 </%def>
30 30
31 31 <%def name="main()">
32 32 <div class="summary-header">
33 33 <div class="title">
34 34 ${self.repo_page_title(c.rhodecode_db_repo)}
35 35 </div>
36 36 </div>
37 37
38 38
39 39 <div class="summary changeset">
40 40 <div class="summary-detail">
41 41 <div class="summary-detail-header">
42 42 <span class="breadcrumbs files_location">
43 43 <h4>
44 44 ${_('Commit Range')}
45 45 <code>
46 46 r${c.commit_ranges[0].revision}:${h.short_id(c.commit_ranges[0].raw_id)}...r${c.commit_ranges[-1].revision}:${h.short_id(c.commit_ranges[-1].raw_id)}
47 47 </code>
48 48 </h4>
49 49 </span>
50 50 </div>
51 51
52 52 <div class="fieldset">
53 53 <div class="left-label">
54 54 ${_('Diff option')}:
55 55 </div>
56 56 <div class="right-content">
57 57 <div class="header-buttons">
58 <a href="${h.url('compare_url', repo_name=c.repo_name, source_ref_type='rev', source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'), target_ref_type='rev', target_ref=c.commit_ranges[-1].raw_id)}">
58 <a href="${h.route_path('repo_compare',
59 repo_name=c.repo_name,
60 source_ref_type='rev',
61 source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'),
62 target_ref_type='rev',
63 target_ref=c.commit_ranges[-1].raw_id)}"
64 >
59 65 ${_('Show combined compare')}
60 66 </a>
61 67 </div>
62 68 </div>
63 69 </div>
64 70
65 71 <%doc>
66 72 ##TODO(marcink): implement this and diff menus
67 73 <div class="fieldset">
68 74 <div class="left-label">
69 75 ${_('Diff options')}:
70 76 </div>
71 77 <div class="right-content">
72 78 <div class="diff-actions">
73 79 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id='?')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
74 80 ${_('Raw Diff')}
75 81 </a>
76 82 |
77 83 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id='?')}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
78 84 ${_('Patch Diff')}
79 85 </a>
80 86 |
81 87 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id='?',_query=dict(diff='download'))}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
82 88 ${_('Download Diff')}
83 89 </a>
84 90 </div>
85 91 </div>
86 92 </div>
87 93 </%doc>
88 94 </div> <!-- end summary-detail -->
89 95
90 96 </div> <!-- end summary -->
91 97
92 98 <div id="changeset_compare_view_content">
93 99 <div class="pull-left">
94 100 <div class="btn-group">
95 101 <a
96 102 class="btn"
97 103 href="#"
98 104 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
99 105 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
100 106 </a>
101 107 <a
102 108 class="btn"
103 109 href="#"
104 110 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
105 111 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
106 112 </a>
107 113 </div>
108 114 </div>
109 115 ## Commit range generated below
110 116 <%include file="../compare/compare_commits.mako"/>
111 117 <div class="cs_files">
112 118 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
113 119 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
114 120 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
115 121 ${cbdiffs.render_diffset_menu()}
116 122 %for commit in c.commit_ranges:
117 123 ${cbdiffs.render_diffset(
118 124 diffset=c.changes[commit.raw_id],
119 125 collapse_when_files_over=5,
120 126 commit=commit,
121 127 )}
122 128 %endfor
123 129 </div>
124 130 </div>
125 131 </%def>
@@ -1,333 +1,333 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
4 4
5 5 <%def name="title()">
6 6 %if c.compare_home:
7 7 ${_('%s Compare') % c.repo_name}
8 8 %else:
9 9 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.source_repo.repo_name, c.source_ref)} &gt; ${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}
10 10 %endif
11 11 %if c.rhodecode_name:
12 12 &middot; ${h.branding(c.rhodecode_name)}
13 13 %endif
14 14 </%def>
15 15
16 16 <%def name="breadcrumbs_links()">
17 17 ${_ungettext('%s commit','%s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
18 18 </%def>
19 19
20 20 <%def name="menu_bar_nav()">
21 21 ${self.menu_items(active='repositories')}
22 22 </%def>
23 23
24 24 <%def name="menu_bar_subnav()">
25 25 ${self.repo_menu(active='compare')}
26 26 </%def>
27 27
28 28 <%def name="main()">
29 29 <script type="text/javascript">
30 30 // set fake commitId on this commit-range page
31 31 templateContext.commit_data.commit_id = "${h.EmptyCommit().raw_id}";
32 32 </script>
33 33
34 34 <div class="box">
35 35 <div class="title">
36 36 ${self.repo_page_title(c.rhodecode_db_repo)}
37 37 </div>
38 38
39 39 <div class="summary changeset">
40 40 <div class="summary-detail">
41 41 <div class="summary-detail-header">
42 42 <span class="breadcrumbs files_location">
43 43 <h4>
44 44 ${_('Compare Commits')}
45 45 % if c.file_path:
46 46 ${_('for file')} <a href="#${'a_' + h.FID('',c.file_path)}">${c.file_path}</a>
47 47 % endif
48 48
49 49 % if c.commit_ranges:
50 50 <code>
51 51 r${c.source_commit.revision}:${h.short_id(c.source_commit.raw_id)}...r${c.target_commit.revision}:${h.short_id(c.target_commit.raw_id)}
52 52 </code>
53 53 % endif
54 54 </h4>
55 55 </span>
56 56 </div>
57 57
58 58 <div class="fieldset">
59 59 <div class="left-label">
60 60 ${_('Target')}:
61 61 </div>
62 62 <div class="right-content">
63 63 <div>
64 64 <div class="code-header" >
65 65 <div class="compare_header">
66 66 ## The hidden elements are replaced with a select2 widget
67 67 ${h.hidden('compare_source')}
68 68 </div>
69 69 </div>
70 70 </div>
71 71 </div>
72 72 </div>
73 73
74 74 <div class="fieldset">
75 75 <div class="left-label">
76 76 ${_('Source')}:
77 77 </div>
78 78 <div class="right-content">
79 79 <div>
80 80 <div class="code-header" >
81 81 <div class="compare_header">
82 82 ## The hidden elements are replaced with a select2 widget
83 83 ${h.hidden('compare_target')}
84 84 </div>
85 85 </div>
86 86 </div>
87 87 </div>
88 88 </div>
89 89
90 90 <div class="fieldset">
91 91 <div class="left-label">
92 92 ${_('Actions')}:
93 93 </div>
94 94 <div class="right-content">
95 95 <div>
96 96 <div class="code-header" >
97 97 <div class="compare_header">
98 98
99 99 <div class="compare-buttons">
100 100 % if c.compare_home:
101 101 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
102 102
103 103 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
104 104 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
105 105 <div id="changeset_compare_view_content">
106 106 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
107 107 </div>
108 108
109 109 % elif c.preview_mode:
110 110 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
111 111 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
112 112 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
113 113
114 114 % else:
115 115 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
116 116 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
117 117
118 118 ## allow comment only if there are commits to comment on
119 119 % if c.diffset and c.diffset.files and c.commit_ranges:
120 120 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
121 121 % else:
122 122 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
123 123 % endif
124 124 % endif
125 125 </div>
126 126 </div>
127 127 </div>
128 128 </div>
129 129 </div>
130 130 </div>
131 131
132 132 <%doc>
133 133 ##TODO(marcink): implement this and diff menus
134 134 <div class="fieldset">
135 135 <div class="left-label">
136 136 ${_('Diff options')}:
137 137 </div>
138 138 <div class="right-content">
139 139 <div class="diff-actions">
140 140 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id='?')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
141 141 ${_('Raw Diff')}
142 142 </a>
143 143 |
144 144 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id='?')}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
145 145 ${_('Patch Diff')}
146 146 </a>
147 147 |
148 148 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id='?',_query=dict(diff='download'))}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
149 149 ${_('Download Diff')}
150 150 </a>
151 151 </div>
152 152 </div>
153 153 </div>
154 154 </%doc>
155 155
156 156 ## commit status form
157 157 <div class="fieldset" id="compare_changeset_status" style="display: none; margin-bottom: -80px;">
158 158 <div class="left-label">
159 159 ${_('Commit status')}:
160 160 </div>
161 161 <div class="right-content">
162 162 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
163 163 ## main comment form and it status
164 164 <%
165 165 def revs(_revs):
166 166 form_inputs = []
167 167 for cs in _revs:
168 168 tmpl = '<input type="hidden" data-commit-id="%(cid)s" name="commit_ids" value="%(cid)s">' % {'cid': cs.raw_id}
169 169 form_inputs.append(tmpl)
170 170 return form_inputs
171 171 %>
172 172 <div>
173 173 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id='0'*16), None, is_compare=True, form_extras=revs(c.commit_ranges))}
174 174 </div>
175 175 </div>
176 176 </div>
177 177
178 178 </div> <!-- end summary-detail -->
179 179 </div> <!-- end summary -->
180 180
181 181 ## use JS script to load it quickly before potentially large diffs render long time
182 182 ## this prevents from situation when large diffs block rendering of select2 fields
183 183 <script type="text/javascript">
184 184
185 185 var cache = {};
186 186
187 187 var formatSelection = function(repoName){
188 188 return function(data, container, escapeMarkup) {
189 189 var selection = data ? this.text(data) : "";
190 190 return escapeMarkup('{0}@{1}'.format(repoName, selection));
191 191 }
192 192 };
193 193
194 194 var feedCompareData = function(query, cachedValue){
195 195 var data = {results: []};
196 196 //filter results
197 197 $.each(cachedValue.results, function() {
198 198 var section = this.text;
199 199 var children = [];
200 200 $.each(this.children, function() {
201 201 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
202 202 children.push({
203 203 'id': this.id,
204 204 'text': this.text,
205 205 'type': this.type
206 206 })
207 207 }
208 208 });
209 209 data.results.push({
210 210 'text': section,
211 211 'children': children
212 212 })
213 213 });
214 214 //push the typed in changeset
215 215 data.results.push({
216 216 'text': _gettext('specify commit'),
217 217 'children': [{
218 218 'id': query.term,
219 219 'text': query.term,
220 220 'type': 'rev'
221 221 }]
222 222 });
223 223 query.callback(data);
224 224 };
225 225
226 226 var loadCompareData = function(repoName, query, cache){
227 227 $.ajax({
228 228 url: pyroutes.url('repo_refs_data', {'repo_name': repoName}),
229 229 data: {},
230 230 dataType: 'json',
231 231 type: 'GET',
232 232 success: function(data) {
233 233 cache[repoName] = data;
234 234 query.callback({results: data.results});
235 235 }
236 236 })
237 237 };
238 238
239 239 var enable_fields = ${"false" if c.preview_mode else "true"};
240 240 $("#compare_source").select2({
241 241 placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}",
242 242 containerCssClass: "drop-menu",
243 243 dropdownCssClass: "drop-menu-dropdown",
244 244 formatSelection: formatSelection("${c.source_repo.repo_name}"),
245 245 dropdownAutoWidth: true,
246 246 query: function(query) {
247 247 var repoName = '${c.source_repo.repo_name}';
248 248 var cachedValue = cache[repoName];
249 249
250 250 if (cachedValue){
251 251 feedCompareData(query, cachedValue);
252 252 }
253 253 else {
254 254 loadCompareData(repoName, query, cache);
255 255 }
256 256 }
257 257 }).select2("enable", enable_fields);
258 258
259 259 $("#compare_target").select2({
260 260 placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}",
261 261 dropdownAutoWidth: true,
262 262 containerCssClass: "drop-menu",
263 263 dropdownCssClass: "drop-menu-dropdown",
264 264 formatSelection: formatSelection("${c.target_repo.repo_name}"),
265 265 query: function(query) {
266 266 var repoName = '${c.target_repo.repo_name}';
267 267 var cachedValue = cache[repoName];
268 268
269 269 if (cachedValue){
270 270 feedCompareData(query, cachedValue);
271 271 }
272 272 else {
273 273 loadCompareData(repoName, query, cache);
274 274 }
275 275 }
276 276 }).select2("enable", enable_fields);
277 277 var initial_compare_source = {id: "${c.source_ref}", type:"${c.source_ref_type}"};
278 278 var initial_compare_target = {id: "${c.target_ref}", type:"${c.target_ref_type}"};
279 279
280 280 $('#compare_revs').on('click', function(e) {
281 281 var source = $('#compare_source').select2('data') || initial_compare_source;
282 282 var target = $('#compare_target').select2('data') || initial_compare_target;
283 283 if (source && target) {
284 284 var url_data = {
285 285 repo_name: "${c.repo_name}",
286 286 source_ref: source.id,
287 287 source_ref_type: source.type,
288 288 target_ref: target.id,
289 289 target_ref_type: target.type
290 290 };
291 window.location = pyroutes.url('compare_url', url_data);
291 window.location = pyroutes.url('repo_compare', url_data);
292 292 }
293 293 });
294 294 $('#compare_changeset_status_toggle').on('click', function(e) {
295 295 $('#compare_changeset_status').toggle();
296 296 });
297 297
298 298 </script>
299 299
300 300 ## table diff data
301 301 <div class="table">
302 302
303 303
304 304 % if not c.compare_home:
305 305 <div id="changeset_compare_view_content">
306 306 <div class="pull-left">
307 307 <div class="btn-group">
308 308 <a
309 309 class="btn"
310 310 href="#"
311 311 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
312 312 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
313 313 </a>
314 314 <a
315 315 class="btn"
316 316 href="#"
317 317 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
318 318 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
319 319 </a>
320 320 </div>
321 321 </div>
322 322 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
323 323 ## commit compare generated below
324 324 <%include file="compare_commits.mako"/>
325 325 ${cbdiffs.render_diffset_menu()}
326 326 ${cbdiffs.render_diffset(c.diffset)}
327 327 </div>
328 328 % endif
329 329
330 330 </div>
331 331 </div>
332 332
333 333 </%def> No newline at end of file
@@ -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 window.location = pyroutes.url('compare_url', url_data);
177 window.location = pyroutes.url('repo_compare', 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 222 var url = pyroutes.url('repo_changelog_file',
223 223 {'repo_name': templateContext.repo_name,
224 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,47 +1,56 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 % if c.forks_pager:
5 5 <table class="rctable fork_summary">
6 6 <tr>
7 7 <th>${_('Owner')}</th>
8 8 <th>${_('Fork')}</th>
9 9 <th>${_('Description')}</th>
10 10 <th>${_('Forked')}</th>
11 11 <th></th>
12 12 </tr>
13 13 % for f in c.forks_pager:
14 14 <tr>
15 15 <td class="td-user fork_user">
16 16 ${base.gravatar_with_user(f.user.email, 16)}
17 17 </td>
18 18 <td class="td-componentname">
19 19 ${h.link_to(f.repo_name,h.route_path('repo_summary',repo_name=f.repo_name))}
20 20 </td>
21 21 <td class="td-description">
22 22 <div class="truncate">${f.description}</div>
23 23 </td>
24 24 <td class="td-time follower_date">
25 25 ${h.age_component(f.created_on, time_is_local=True)}
26 26 </td>
27 27 <td class="td-compare">
28 28 <a title="${h.tooltip(_('Compare fork with %s' % c.repo_name))}"
29 href="${h.url('compare_url',repo_name=c.repo_name, source_ref_type=c.rhodecode_db_repo.landing_rev[0],source_ref=c.rhodecode_db_repo.landing_rev[1],target_repo=f.repo_name,target_ref_type=c.rhodecode_db_repo.landing_rev[0],target_ref=c.rhodecode_db_repo.landing_rev[1], merge=1)}"
30 class="btn-link"><i class="icon-loop"></i> ${_('Compare fork')}</a>
29 class="btn-link"
30 href="${h.route_path('repo_compare',
31 repo_name=c.repo_name,
32 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
33 source_ref=c.rhodecode_db_repo.landing_rev[1],
34 target_ref_type=c.rhodecode_db_repo.landing_rev[0],
35 target_ref=c.rhodecode_db_repo.landing_rev[1],
36 _query=dict(merge=1, target_repo=f.repo_name))}"
37 >
38 ${_('Compare fork')}
39 </a>
31 40 </td>
32 41 </tr>
33 42 % endfor
34 43 </table>
35 44 <div class="pagination-wh pagination-left">
36 45 <script type="text/javascript">
37 46 $(document).pjax('#forks .pager_link','#forks');
38 47 $(document).on('pjax:success',function(){
39 48 show_more_event();
40 49 timeagoActivate();
41 50 });
42 51 </script>
43 52 ${c.forks_pager.pager('$link_previous ~2~ $link_next')}
44 53 </div>
45 54 % else:
46 55 ${_('There are no forks yet')}
47 56 % endif
@@ -1,526 +1,526 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('New pull request')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('New pull request')}
9 9 </%def>
10 10
11 11 <%def name="menu_bar_nav()">
12 12 ${self.menu_items(active='repositories')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_subnav()">
16 16 ${self.repo_menu(active='showpullrequest')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 23 </div>
24 24
25 25 ${h.secure_form(h.url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
26 26
27 27 ${self.breadcrumbs()}
28 28
29 29 <div class="box pr-summary">
30 30
31 31 <div class="summary-details block-left">
32 32
33 33
34 34 <div class="pr-details-title">
35 35 ${_('Pull request summary')}
36 36 </div>
37 37
38 38 <div class="form" style="padding-top: 10px">
39 39 <!-- fields -->
40 40
41 41 <div class="fields" >
42 42
43 43 <div class="field">
44 44 <div class="label">
45 45 <label for="pullrequest_title">${_('Title')}:</label>
46 46 </div>
47 47 <div class="input">
48 48 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
49 49 </div>
50 50 </div>
51 51
52 52 <div class="field">
53 53 <div class="label label-textarea">
54 54 <label for="pullrequest_desc">${_('Description')}:</label>
55 55 </div>
56 56 <div class="textarea text-area editor">
57 57 ${h.textarea('pullrequest_desc',size=30, )}
58 58 <span class="help-block">${_('Write a short description on this pull request')}</span>
59 59 </div>
60 60 </div>
61 61
62 62 <div class="field">
63 63 <div class="label label-textarea">
64 64 <label for="pullrequest_desc">${_('Commit flow')}:</label>
65 65 </div>
66 66
67 67 ## TODO: johbo: Abusing the "content" class here to get the
68 68 ## desired effect. Should be replaced by a proper solution.
69 69
70 70 ##ORG
71 71 <div class="content">
72 72 <strong>${_('Source repository')}:</strong>
73 73 ${c.rhodecode_db_repo.description}
74 74 </div>
75 75 <div class="content">
76 76 ${h.hidden('source_repo')}
77 77 ${h.hidden('source_ref')}
78 78 </div>
79 79
80 80 ##OTHER, most Probably the PARENT OF THIS FORK
81 81 <div class="content">
82 82 ## filled with JS
83 83 <div id="target_repo_desc"></div>
84 84 </div>
85 85
86 86 <div class="content">
87 87 ${h.hidden('target_repo')}
88 88 ${h.hidden('target_ref')}
89 89 <span id="target_ref_loading" style="display: none">
90 90 ${_('Loading refs...')}
91 91 </span>
92 92 </div>
93 93 </div>
94 94
95 95 <div class="field">
96 96 <div class="label label-textarea">
97 97 <label for="pullrequest_submit"></label>
98 98 </div>
99 99 <div class="input">
100 100 <div class="pr-submit-button">
101 101 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
102 102 </div>
103 103 <div id="pr_open_message"></div>
104 104 </div>
105 105 </div>
106 106
107 107 <div class="pr-spacing-container"></div>
108 108 </div>
109 109 </div>
110 110 </div>
111 111 <div>
112 112 ## AUTHOR
113 113 <div class="reviewers-title block-right">
114 114 <div class="pr-details-title">
115 115 ${_('Author of this pull request')}
116 116 </div>
117 117 </div>
118 118 <div class="block-right pr-details-content reviewers">
119 119 <ul class="group_members">
120 120 <li>
121 121 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
122 122 </li>
123 123 </ul>
124 124 </div>
125 125
126 126 ## REVIEW RULES
127 127 <div id="review_rules" style="display: none" class="reviewers-title block-right">
128 128 <div class="pr-details-title">
129 129 ${_('Reviewer rules')}
130 130 </div>
131 131 <div class="pr-reviewer-rules">
132 132 ## review rules will be appended here, by default reviewers logic
133 133 </div>
134 134 </div>
135 135
136 136 ## REVIEWERS
137 137 <div class="reviewers-title block-right">
138 138 <div class="pr-details-title">
139 139 ${_('Pull request reviewers')}
140 140 <span class="calculate-reviewers"> - ${_('loading...')}</span>
141 141 </div>
142 142 </div>
143 143 <div id="reviewers" class="block-right pr-details-content reviewers">
144 144 ## members goes here, filled via JS based on initial selection !
145 145 <input type="hidden" name="__start__" value="review_members:sequence">
146 146 <ul id="review_members" class="group_members"></ul>
147 147 <input type="hidden" name="__end__" value="review_members:sequence">
148 148 <div id="add_reviewer_input" class='ac'>
149 149 <div class="reviewer_ac">
150 150 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
151 151 <div id="reviewers_container"></div>
152 152 </div>
153 153 </div>
154 154 </div>
155 155 </div>
156 156 </div>
157 157 <div class="box">
158 158 <div>
159 159 ## overview pulled by ajax
160 160 <div id="pull_request_overview"></div>
161 161 </div>
162 162 </div>
163 163 ${h.end_form()}
164 164 </div>
165 165
166 166 <script type="text/javascript">
167 167 $(function(){
168 168 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
169 169 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
170 170 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
171 171 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
172 172
173 173 var $pullRequestForm = $('#pull_request_form');
174 174 var $sourceRepo = $('#source_repo', $pullRequestForm);
175 175 var $targetRepo = $('#target_repo', $pullRequestForm);
176 176 var $sourceRef = $('#source_ref', $pullRequestForm);
177 177 var $targetRef = $('#target_ref', $pullRequestForm);
178 178
179 179 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
180 180 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
181 181
182 182 var targetRepo = function() { return $targetRepo.eq(0).val() };
183 183 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
184 184
185 185 var calculateContainerWidth = function() {
186 186 var maxWidth = 0;
187 187 var repoSelect2Containers = ['#source_repo', '#target_repo'];
188 188 $.each(repoSelect2Containers, function(idx, value) {
189 189 $(value).select2('container').width('auto');
190 190 var curWidth = $(value).select2('container').width();
191 191 if (maxWidth <= curWidth) {
192 192 maxWidth = curWidth;
193 193 }
194 194 $.each(repoSelect2Containers, function(idx, value) {
195 195 $(value).select2('container').width(maxWidth + 10);
196 196 });
197 197 });
198 198 };
199 199
200 200 var initRefSelection = function(selectedRef) {
201 201 return function(element, callback) {
202 202 // translate our select2 id into a text, it's a mapping to show
203 203 // simple label when selecting by internal ID.
204 204 var id, refData;
205 205 if (selectedRef === undefined) {
206 206 id = element.val();
207 207 refData = element.val().split(':');
208 208 } else {
209 209 id = selectedRef;
210 210 refData = selectedRef.split(':');
211 211 }
212 212
213 213 var text = refData[1];
214 214 if (refData[0] === 'rev') {
215 215 text = text.substring(0, 12);
216 216 }
217 217
218 218 var data = {id: id, text: text};
219 219
220 220 callback(data);
221 221 };
222 222 };
223 223
224 224 var formatRefSelection = function(item) {
225 225 var prefix = '';
226 226 var refData = item.id.split(':');
227 227 if (refData[0] === 'branch') {
228 228 prefix = '<i class="icon-branch"></i>';
229 229 }
230 230 else if (refData[0] === 'book') {
231 231 prefix = '<i class="icon-bookmark"></i>';
232 232 }
233 233 else if (refData[0] === 'tag') {
234 234 prefix = '<i class="icon-tag"></i>';
235 235 }
236 236
237 237 var originalOption = item.element;
238 238 return prefix + item.text;
239 239 };
240 240
241 241 // custom code mirror
242 242 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
243 243
244 244 reviewersController = new ReviewersController();
245 245
246 246 var queryTargetRepo = function(self, query) {
247 247 // cache ALL results if query is empty
248 248 var cacheKey = query.term || '__';
249 249 var cachedData = self.cachedDataSource[cacheKey];
250 250
251 251 if (cachedData) {
252 252 query.callback({results: cachedData.results});
253 253 } else {
254 254 $.ajax({
255 255 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
256 256 data: {query: query.term},
257 257 dataType: 'json',
258 258 type: 'GET',
259 259 success: function(data) {
260 260 self.cachedDataSource[cacheKey] = data;
261 261 query.callback({results: data.results});
262 262 },
263 263 error: function(data, textStatus, errorThrown) {
264 264 alert(
265 265 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
266 266 }
267 267 });
268 268 }
269 269 };
270 270
271 271 var queryTargetRefs = function(initialData, query) {
272 272 var data = {results: []};
273 273 // filter initialData
274 274 $.each(initialData, function() {
275 275 var section = this.text;
276 276 var children = [];
277 277 $.each(this.children, function() {
278 278 if (query.term.length === 0 ||
279 279 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
280 280 children.push({'id': this.id, 'text': this.text})
281 281 }
282 282 });
283 283 data.results.push({'text': section, 'children': children})
284 284 });
285 285 query.callback({results: data.results});
286 286 };
287 287
288 288 var loadRepoRefDiffPreview = function() {
289 289
290 290 var url_data = {
291 291 'repo_name': targetRepo(),
292 292 'target_repo': sourceRepo(),
293 293 'source_ref': targetRef()[2],
294 294 'source_ref_type': 'rev',
295 295 'target_ref': sourceRef()[2],
296 296 'target_ref_type': 'rev',
297 297 'merge': true,
298 298 '_': Date.now() // bypass browser caching
299 299 }; // gather the source/target ref and repo here
300 300
301 301 if (sourceRef().length !== 3 || targetRef().length !== 3) {
302 302 prButtonLock(true, "${_('Please select source and target')}");
303 303 return;
304 304 }
305 var url = pyroutes.url('compare_url', url_data);
305 var url = pyroutes.url('repo_compare', url_data);
306 306
307 307 // lock PR button, so we cannot send PR before it's calculated
308 308 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
309 309
310 310 if (loadRepoRefDiffPreview._currentRequest) {
311 311 loadRepoRefDiffPreview._currentRequest.abort();
312 312 }
313 313
314 314 loadRepoRefDiffPreview._currentRequest = $.get(url)
315 315 .error(function(data, textStatus, errorThrown) {
316 316 alert(
317 317 "Error while processing request.\nError code {0} ({1}).".format(
318 318 data.status, data.statusText));
319 319 })
320 320 .done(function(data) {
321 321 loadRepoRefDiffPreview._currentRequest = null;
322 322 $('#pull_request_overview').html(data);
323 323
324 324 var commitElements = $(data).find('tr[commit_id]');
325 325
326 326 var prTitleAndDesc = getTitleAndDescription(
327 327 sourceRef()[1], commitElements, 5);
328 328
329 329 var title = prTitleAndDesc[0];
330 330 var proposedDescription = prTitleAndDesc[1];
331 331
332 332 var useGeneratedTitle = (
333 333 $('#pullrequest_title').hasClass('autogenerated-title') ||
334 334 $('#pullrequest_title').val() === "");
335 335
336 336 if (title && useGeneratedTitle) {
337 337 // use generated title if we haven't specified our own
338 338 $('#pullrequest_title').val(title);
339 339 $('#pullrequest_title').addClass('autogenerated-title');
340 340
341 341 }
342 342
343 343 var useGeneratedDescription = (
344 344 !codeMirrorInstance._userDefinedDesc ||
345 345 codeMirrorInstance.getValue() === "");
346 346
347 347 if (proposedDescription && useGeneratedDescription) {
348 348 // set proposed content, if we haven't defined our own,
349 349 // or we don't have description written
350 350 codeMirrorInstance._userDefinedDesc = false; // reset state
351 351 codeMirrorInstance.setValue(proposedDescription);
352 352 }
353 353
354 354 var msg = '';
355 355 if (commitElements.length === 1) {
356 356 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
357 357 } else {
358 358 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
359 359 }
360 360
361 361 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
362 362
363 363 if (commitElements.length) {
364 364 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
365 365 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
366 366 }
367 367 else {
368 368 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
369 369 }
370 370
371 371
372 372 });
373 373 };
374 374
375 375 var Select2Box = function(element, overrides) {
376 376 var globalDefaults = {
377 377 dropdownAutoWidth: true,
378 378 containerCssClass: "drop-menu",
379 379 dropdownCssClass: "drop-menu-dropdown"
380 380 };
381 381
382 382 var initSelect2 = function(defaultOptions) {
383 383 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
384 384 element.select2(options);
385 385 };
386 386
387 387 return {
388 388 initRef: function() {
389 389 var defaultOptions = {
390 390 minimumResultsForSearch: 5,
391 391 formatSelection: formatRefSelection
392 392 };
393 393
394 394 initSelect2(defaultOptions);
395 395 },
396 396
397 397 initRepo: function(defaultValue, readOnly) {
398 398 var defaultOptions = {
399 399 initSelection : function (element, callback) {
400 400 var data = {id: defaultValue, text: defaultValue};
401 401 callback(data);
402 402 }
403 403 };
404 404
405 405 initSelect2(defaultOptions);
406 406
407 407 element.select2('val', defaultSourceRepo);
408 408 if (readOnly === true) {
409 409 element.select2('readonly', true);
410 410 }
411 411 }
412 412 };
413 413 };
414 414
415 415 var initTargetRefs = function(refsData, selectedRef){
416 416 Select2Box($targetRef, {
417 417 query: function(query) {
418 418 queryTargetRefs(refsData, query);
419 419 },
420 420 initSelection : initRefSelection(selectedRef)
421 421 }).initRef();
422 422
423 423 if (!(selectedRef === undefined)) {
424 424 $targetRef.select2('val', selectedRef);
425 425 }
426 426 };
427 427
428 428 var targetRepoChanged = function(repoData) {
429 429 // generate new DESC of target repo displayed next to select
430 430 $('#target_repo_desc').html(
431 431 "<strong>${_('Target repository')}</strong>: {0}".format(repoData['description'])
432 432 );
433 433
434 434 // generate dynamic select2 for refs.
435 435 initTargetRefs(repoData['refs']['select2_refs'],
436 436 repoData['refs']['selected_ref']);
437 437
438 438 };
439 439
440 440 var sourceRefSelect2 = Select2Box($sourceRef, {
441 441 placeholder: "${_('Select commit reference')}",
442 442 query: function(query) {
443 443 var initialData = defaultSourceRepoData['refs']['select2_refs'];
444 444 queryTargetRefs(initialData, query)
445 445 },
446 446 initSelection: initRefSelection()
447 447 }
448 448 );
449 449
450 450 var sourceRepoSelect2 = Select2Box($sourceRepo, {
451 451 query: function(query) {}
452 452 });
453 453
454 454 var targetRepoSelect2 = Select2Box($targetRepo, {
455 455 cachedDataSource: {},
456 456 query: $.debounce(250, function(query) {
457 457 queryTargetRepo(this, query);
458 458 }),
459 459 formatResult: formatResult
460 460 });
461 461
462 462 sourceRefSelect2.initRef();
463 463
464 464 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
465 465
466 466 targetRepoSelect2.initRepo(defaultTargetRepo, false);
467 467
468 468 $sourceRef.on('change', function(e){
469 469 loadRepoRefDiffPreview();
470 470 reviewersController.loadDefaultReviewers(
471 471 sourceRepo(), sourceRef(), targetRepo(), targetRef());
472 472 });
473 473
474 474 $targetRef.on('change', function(e){
475 475 loadRepoRefDiffPreview();
476 476 reviewersController.loadDefaultReviewers(
477 477 sourceRepo(), sourceRef(), targetRepo(), targetRef());
478 478 });
479 479
480 480 $targetRepo.on('change', function(e){
481 481 var repoName = $(this).val();
482 482 calculateContainerWidth();
483 483 $targetRef.select2('destroy');
484 484 $('#target_ref_loading').show();
485 485
486 486 $.ajax({
487 487 url: pyroutes.url('pullrequest_repo_refs',
488 488 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
489 489 data: {},
490 490 dataType: 'json',
491 491 type: 'GET',
492 492 success: function(data) {
493 493 $('#target_ref_loading').hide();
494 494 targetRepoChanged(data);
495 495 loadRepoRefDiffPreview();
496 496 },
497 497 error: function(data, textStatus, errorThrown) {
498 498 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
499 499 }
500 500 })
501 501
502 502 });
503 503
504 504 prButtonLock(true, "${_('Please select source and target')}", 'all');
505 505
506 506 // auto-load on init, the target refs select2
507 507 calculateContainerWidth();
508 508 targetRepoChanged(defaultTargetRepoData);
509 509
510 510 $('#pullrequest_title').on('keyup', function(e){
511 511 $(this).removeClass('autogenerated-title');
512 512 });
513 513
514 514 % if c.default_source_ref:
515 515 // in case we have a pre-selected value, use it now
516 516 $sourceRef.select2('val', '${c.default_source_ref}');
517 517 loadRepoRefDiffPreview();
518 518 reviewersController.loadDefaultReviewers(
519 519 sourceRepo(), sourceRef(), targetRepo(), targetRef());
520 520 % endif
521 521
522 522 ReviewerAutoComplete('#user');
523 523 });
524 524 </script>
525 525
526 526 </%def>
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now