Show More
@@ -1,508 +1,505 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2016-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import time |
|
22 | 22 | import logging |
|
23 | 23 | import operator |
|
24 | 24 | |
|
25 | 25 | from pyramid.httpexceptions import HTTPFound |
|
26 | 26 | |
|
27 | 27 | from rhodecode.lib import helpers as h |
|
28 | 28 | from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time |
|
29 | 29 | from rhodecode.lib.vcs.exceptions import RepositoryRequirementError |
|
30 | 30 | from rhodecode.model import repo |
|
31 | 31 | from rhodecode.model import repo_group |
|
32 | 32 | from rhodecode.model import user_group |
|
33 | 33 | from rhodecode.model.db import User |
|
34 | 34 | from rhodecode.model.scm import ScmModel |
|
35 | 35 | |
|
36 | 36 | log = logging.getLogger(__name__) |
|
37 | 37 | |
|
38 | 38 | |
|
39 | 39 | ADMIN_PREFIX = '/_admin' |
|
40 | 40 | STATIC_FILE_PREFIX = '/_static' |
|
41 | 41 | |
|
42 | 42 | URL_NAME_REQUIREMENTS = { |
|
43 | 43 | # group name can have a slash in them, but they must not end with a slash |
|
44 | 44 | 'group_name': r'.*?[^/]', |
|
45 | 45 | 'repo_group_name': r'.*?[^/]', |
|
46 | 46 | # repo names can have a slash in them, but they must not end with a slash |
|
47 | 47 | 'repo_name': r'.*?[^/]', |
|
48 | 48 | # file path eats up everything at the end |
|
49 | 49 | 'f_path': r'.*', |
|
50 | 50 | # reference types |
|
51 | 51 | 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)', |
|
52 | 52 | 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)', |
|
53 | 53 | } |
|
54 | 54 | |
|
55 | 55 | |
|
56 | 56 | def add_route_with_slash(config,name, pattern, **kw): |
|
57 | 57 | config.add_route(name, pattern, **kw) |
|
58 | 58 | if not pattern.endswith('/'): |
|
59 | 59 | config.add_route(name + '_slash', pattern + '/', **kw) |
|
60 | 60 | |
|
61 | 61 | |
|
62 | 62 | def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS): |
|
63 | 63 | """ |
|
64 | 64 | Adds regex requirements to pyramid routes using a mapping dict |
|
65 | 65 | e.g:: |
|
66 | 66 | add_route_requirements('{repo_name}/settings') |
|
67 | 67 | """ |
|
68 | 68 | for key, regex in requirements.items(): |
|
69 | 69 | route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex)) |
|
70 | 70 | return route_path |
|
71 | 71 | |
|
72 | 72 | |
|
73 | 73 | def get_format_ref_id(repo): |
|
74 | 74 | """Returns a `repo` specific reference formatter function""" |
|
75 | 75 | if h.is_svn(repo): |
|
76 | 76 | return _format_ref_id_svn |
|
77 | 77 | else: |
|
78 | 78 | return _format_ref_id |
|
79 | 79 | |
|
80 | 80 | |
|
81 | 81 | def _format_ref_id(name, raw_id): |
|
82 | 82 | """Default formatting of a given reference `name`""" |
|
83 | 83 | return name |
|
84 | 84 | |
|
85 | 85 | |
|
86 | 86 | def _format_ref_id_svn(name, raw_id): |
|
87 | 87 | """Special way of formatting a reference for Subversion including path""" |
|
88 | 88 | return '%s@%s' % (name, raw_id) |
|
89 | 89 | |
|
90 | 90 | |
|
91 | 91 | class TemplateArgs(StrictAttributeDict): |
|
92 | 92 | pass |
|
93 | 93 | |
|
94 | 94 | |
|
95 | 95 | class BaseAppView(object): |
|
96 | 96 | |
|
97 | 97 | def __init__(self, context, request): |
|
98 | 98 | self.request = request |
|
99 | 99 | self.context = context |
|
100 | 100 | self.session = request.session |
|
101 | 101 | self._rhodecode_user = request.user # auth user |
|
102 | 102 | self._rhodecode_db_user = self._rhodecode_user.get_instance() |
|
103 | 103 | self._maybe_needs_password_change( |
|
104 | 104 | request.matched_route.name, self._rhodecode_db_user) |
|
105 | 105 | |
|
106 | 106 | def _maybe_needs_password_change(self, view_name, user_obj): |
|
107 | 107 | log.debug('Checking if user %s needs password change on view %s', |
|
108 | 108 | user_obj, view_name) |
|
109 | 109 | skip_user_views = [ |
|
110 | 110 | 'logout', 'login', |
|
111 | 111 | 'my_account_password', 'my_account_password_update' |
|
112 | 112 | ] |
|
113 | 113 | |
|
114 | 114 | if not user_obj: |
|
115 | 115 | return |
|
116 | 116 | |
|
117 | 117 | if user_obj.username == User.DEFAULT_USER: |
|
118 | 118 | return |
|
119 | 119 | |
|
120 | 120 | now = time.time() |
|
121 | 121 | should_change = user_obj.user_data.get('force_password_change') |
|
122 | 122 | change_after = safe_int(should_change) or 0 |
|
123 | 123 | if should_change and now > change_after: |
|
124 | 124 | log.debug('User %s requires password change', user_obj) |
|
125 | 125 | h.flash('You are required to change your password', 'warning', |
|
126 | 126 | ignore_duplicate=True) |
|
127 | 127 | |
|
128 | 128 | if view_name not in skip_user_views: |
|
129 | 129 | raise HTTPFound( |
|
130 | 130 | self.request.route_path('my_account_password')) |
|
131 | 131 | |
|
132 | 132 | def _log_creation_exception(self, e, repo_name): |
|
133 | 133 | _ = self.request.translate |
|
134 | 134 | reason = None |
|
135 | 135 | if len(e.args) == 2: |
|
136 | 136 | reason = e.args[1] |
|
137 | 137 | |
|
138 | 138 | if reason == 'INVALID_CERTIFICATE': |
|
139 | 139 | log.exception( |
|
140 | 140 | 'Exception creating a repository: invalid certificate') |
|
141 | 141 | msg = (_('Error creating repository %s: invalid certificate') |
|
142 | 142 | % repo_name) |
|
143 | 143 | else: |
|
144 | 144 | log.exception("Exception creating a repository") |
|
145 | 145 | msg = (_('Error creating repository %s') |
|
146 | 146 | % repo_name) |
|
147 | 147 | return msg |
|
148 | 148 | |
|
149 | 149 | def _get_local_tmpl_context(self, include_app_defaults=False): |
|
150 | 150 | c = TemplateArgs() |
|
151 | 151 | c.auth_user = self.request.user |
|
152 | 152 | # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user |
|
153 | 153 | c.rhodecode_user = self.request.user |
|
154 | 154 | |
|
155 | 155 | if include_app_defaults: |
|
156 | 156 | # NOTE(marcink): after full pyramid migration include_app_defaults |
|
157 | 157 | # should be turned on by default |
|
158 | 158 | from rhodecode.lib.base import attach_context_attributes |
|
159 | 159 | attach_context_attributes(c, self.request, self.request.user.user_id) |
|
160 | 160 | |
|
161 | 161 | return c |
|
162 | 162 | |
|
163 | 163 | def _register_global_c(self, tmpl_args): |
|
164 | 164 | """ |
|
165 | 165 | Registers attributes to pylons global `c` |
|
166 | 166 | """ |
|
167 | 167 | |
|
168 | 168 | # TODO(marcink): remove once pyramid migration is finished |
|
169 | 169 | from pylons import tmpl_context as c |
|
170 | 170 | try: |
|
171 | 171 | for k, v in tmpl_args.items(): |
|
172 | 172 | setattr(c, k, v) |
|
173 | 173 | except TypeError: |
|
174 | 174 | log.exception('Failed to register pylons C') |
|
175 | 175 | pass |
|
176 | 176 | |
|
177 | 177 | def _get_template_context(self, tmpl_args): |
|
178 | 178 | self._register_global_c(tmpl_args) |
|
179 | 179 | |
|
180 | 180 | local_tmpl_args = { |
|
181 | 181 | 'defaults': {}, |
|
182 | 182 | 'errors': {}, |
|
183 | 183 | # register a fake 'c' to be used in templates instead of global |
|
184 | 184 | # pylons c, after migration to pyramid we should rename it to 'c' |
|
185 | 185 | # make sure we replace usage of _c in templates too |
|
186 | 186 | '_c': tmpl_args |
|
187 | 187 | } |
|
188 | 188 | local_tmpl_args.update(tmpl_args) |
|
189 | 189 | return local_tmpl_args |
|
190 | 190 | |
|
191 | 191 | def load_default_context(self): |
|
192 | 192 | """ |
|
193 | 193 | example: |
|
194 | 194 | |
|
195 | 195 | def load_default_context(self): |
|
196 | 196 | c = self._get_local_tmpl_context() |
|
197 | 197 | c.custom_var = 'foobar' |
|
198 | 198 | self._register_global_c(c) |
|
199 | 199 | return c |
|
200 | 200 | """ |
|
201 | 201 | raise NotImplementedError('Needs implementation in view class') |
|
202 | 202 | |
|
203 | 203 | |
|
204 | 204 | class RepoAppView(BaseAppView): |
|
205 | 205 | |
|
206 | 206 | def __init__(self, context, request): |
|
207 | 207 | super(RepoAppView, self).__init__(context, request) |
|
208 | 208 | self.db_repo = request.db_repo |
|
209 | 209 | self.db_repo_name = self.db_repo.repo_name |
|
210 | 210 | self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo) |
|
211 | 211 | |
|
212 | 212 | def _handle_missing_requirements(self, error): |
|
213 | 213 | log.error( |
|
214 | 214 | 'Requirements are missing for repository %s: %s', |
|
215 | 215 | self.db_repo_name, error.message) |
|
216 | 216 | |
|
217 | 217 | def _get_local_tmpl_context(self, include_app_defaults=False): |
|
218 | 218 | _ = self.request.translate |
|
219 | 219 | c = super(RepoAppView, self)._get_local_tmpl_context( |
|
220 | 220 | include_app_defaults=include_app_defaults) |
|
221 | 221 | |
|
222 | 222 | # register common vars for this type of view |
|
223 | 223 | c.rhodecode_db_repo = self.db_repo |
|
224 | 224 | c.repo_name = self.db_repo_name |
|
225 | 225 | c.repository_pull_requests = self.db_repo_pull_requests |
|
226 | 226 | |
|
227 | 227 | c.repository_requirements_missing = False |
|
228 | 228 | try: |
|
229 | 229 | self.rhodecode_vcs_repo = self.db_repo.scm_instance() |
|
230 | 230 | except RepositoryRequirementError as e: |
|
231 | 231 | c.repository_requirements_missing = True |
|
232 | 232 | self._handle_missing_requirements(e) |
|
233 | 233 | self.rhodecode_vcs_repo = None |
|
234 | 234 | |
|
235 | 235 | if (not c.repository_requirements_missing |
|
236 | 236 | and self.rhodecode_vcs_repo is None): |
|
237 | 237 | # unable to fetch this repo as vcs instance, report back to user |
|
238 | 238 | h.flash(_( |
|
239 | 239 | "The repository `%(repo_name)s` cannot be loaded in filesystem. " |
|
240 | 240 | "Please check if it exist, or is not damaged.") % |
|
241 | 241 | {'repo_name': c.repo_name}, |
|
242 | 242 | category='error', ignore_duplicate=True) |
|
243 | 243 | raise HTTPFound(h.route_path('home')) |
|
244 | 244 | |
|
245 | 245 | return c |
|
246 | 246 | |
|
247 | 247 | def _get_f_path(self, matchdict, default=None): |
|
248 | 248 | f_path = matchdict.get('f_path') |
|
249 | 249 | if f_path: |
|
250 | 250 | # fix for multiple initial slashes that causes errors for GIT |
|
251 | 251 | return f_path.lstrip('/') |
|
252 | 252 | |
|
253 | 253 | return default |
|
254 | 254 | |
|
255 | 255 | |
|
256 | 256 | class RepoGroupAppView(BaseAppView): |
|
257 | 257 | def __init__(self, context, request): |
|
258 | 258 | super(RepoGroupAppView, self).__init__(context, request) |
|
259 | 259 | self.db_repo_group = request.db_repo_group |
|
260 | 260 | self.db_repo_group_name = self.db_repo_group.group_name |
|
261 | 261 | |
|
262 | 262 | |
|
263 | 263 | class UserGroupAppView(BaseAppView): |
|
264 | 264 | def __init__(self, context, request): |
|
265 | 265 | super(UserGroupAppView, self).__init__(context, request) |
|
266 | 266 | self.db_user_group = request.db_user_group |
|
267 | 267 | self.db_user_group_name = self.db_user_group.users_group_name |
|
268 | 268 | |
|
269 | 269 | |
|
270 | 270 | class DataGridAppView(object): |
|
271 | 271 | """ |
|
272 | 272 | Common class to have re-usable grid rendering components |
|
273 | 273 | """ |
|
274 | 274 | |
|
275 | 275 | def _extract_ordering(self, request, column_map=None): |
|
276 | 276 | column_map = column_map or {} |
|
277 | 277 | column_index = safe_int(request.GET.get('order[0][column]')) |
|
278 | 278 | order_dir = request.GET.get( |
|
279 | 279 | 'order[0][dir]', 'desc') |
|
280 | 280 | order_by = request.GET.get( |
|
281 | 281 | 'columns[%s][data][sort]' % column_index, 'name_raw') |
|
282 | 282 | |
|
283 | 283 | # translate datatable to DB columns |
|
284 | 284 | order_by = column_map.get(order_by) or order_by |
|
285 | 285 | |
|
286 | 286 | search_q = request.GET.get('search[value]') |
|
287 | 287 | return search_q, order_by, order_dir |
|
288 | 288 | |
|
289 | 289 | def _extract_chunk(self, request): |
|
290 | 290 | start = safe_int(request.GET.get('start'), 0) |
|
291 | 291 | length = safe_int(request.GET.get('length'), 25) |
|
292 | 292 | draw = safe_int(request.GET.get('draw')) |
|
293 | 293 | return draw, start, length |
|
294 | 294 | |
|
295 | 295 | def _get_order_col(self, order_by, model): |
|
296 | 296 | if isinstance(order_by, basestring): |
|
297 | 297 | try: |
|
298 | 298 | return operator.attrgetter(order_by)(model) |
|
299 | 299 | except AttributeError: |
|
300 | 300 | return None |
|
301 | 301 | else: |
|
302 | 302 | return order_by |
|
303 | 303 | |
|
304 | 304 | |
|
305 | 305 | class BaseReferencesView(RepoAppView): |
|
306 | 306 | """ |
|
307 | 307 | Base for reference view for branches, tags and bookmarks. |
|
308 | 308 | """ |
|
309 | 309 | def load_default_context(self): |
|
310 | 310 | c = self._get_local_tmpl_context() |
|
311 | 311 | |
|
312 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
313 | c.repo_info = self.db_repo | |
|
314 | ||
|
315 | 312 | self._register_global_c(c) |
|
316 | 313 | return c |
|
317 | 314 | |
|
318 | 315 | def load_refs_context(self, ref_items, partials_template): |
|
319 | 316 | _render = self.request.get_partial_renderer(partials_template) |
|
320 | 317 | pre_load = ["author", "date", "message"] |
|
321 | 318 | |
|
322 | 319 | is_svn = h.is_svn(self.rhodecode_vcs_repo) |
|
323 | 320 | is_hg = h.is_hg(self.rhodecode_vcs_repo) |
|
324 | 321 | |
|
325 | 322 | format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo) |
|
326 | 323 | |
|
327 | 324 | closed_refs = {} |
|
328 | 325 | if is_hg: |
|
329 | 326 | closed_refs = self.rhodecode_vcs_repo.branches_closed |
|
330 | 327 | |
|
331 | 328 | data = [] |
|
332 | 329 | for ref_name, commit_id in ref_items: |
|
333 | 330 | commit = self.rhodecode_vcs_repo.get_commit( |
|
334 | 331 | commit_id=commit_id, pre_load=pre_load) |
|
335 | 332 | closed = ref_name in closed_refs |
|
336 | 333 | |
|
337 | 334 | # TODO: johbo: Unify generation of reference links |
|
338 | 335 | use_commit_id = '/' in ref_name or is_svn |
|
339 | 336 | |
|
340 | 337 | if use_commit_id: |
|
341 | 338 | files_url = h.route_path( |
|
342 | 339 | 'repo_files', |
|
343 | 340 | repo_name=self.db_repo_name, |
|
344 | 341 | f_path=ref_name if is_svn else '', |
|
345 | 342 | commit_id=commit_id) |
|
346 | 343 | |
|
347 | 344 | else: |
|
348 | 345 | files_url = h.route_path( |
|
349 | 346 | 'repo_files', |
|
350 | 347 | repo_name=self.db_repo_name, |
|
351 | 348 | f_path=ref_name if is_svn else '', |
|
352 | 349 | commit_id=ref_name, |
|
353 | 350 | _query=dict(at=ref_name)) |
|
354 | 351 | |
|
355 | 352 | data.append({ |
|
356 | 353 | "name": _render('name', ref_name, files_url, closed), |
|
357 | 354 | "name_raw": ref_name, |
|
358 | 355 | "date": _render('date', commit.date), |
|
359 | 356 | "date_raw": datetime_to_time(commit.date), |
|
360 | 357 | "author": _render('author', commit.author), |
|
361 | 358 | "commit": _render( |
|
362 | 359 | 'commit', commit.message, commit.raw_id, commit.idx), |
|
363 | 360 | "commit_raw": commit.idx, |
|
364 | 361 | "compare": _render( |
|
365 | 362 | 'compare', format_ref_id(ref_name, commit.raw_id)), |
|
366 | 363 | }) |
|
367 | 364 | |
|
368 | 365 | return data |
|
369 | 366 | |
|
370 | 367 | |
|
371 | 368 | class RepoRoutePredicate(object): |
|
372 | 369 | def __init__(self, val, config): |
|
373 | 370 | self.val = val |
|
374 | 371 | |
|
375 | 372 | def text(self): |
|
376 | 373 | return 'repo_route = %s' % self.val |
|
377 | 374 | |
|
378 | 375 | phash = text |
|
379 | 376 | |
|
380 | 377 | def __call__(self, info, request): |
|
381 | 378 | |
|
382 | 379 | if hasattr(request, 'vcs_call'): |
|
383 | 380 | # skip vcs calls |
|
384 | 381 | return |
|
385 | 382 | |
|
386 | 383 | repo_name = info['match']['repo_name'] |
|
387 | 384 | repo_model = repo.RepoModel() |
|
388 | 385 | by_name_match = repo_model.get_by_repo_name(repo_name, cache=True) |
|
389 | 386 | |
|
390 | 387 | def redirect_if_creating(db_repo): |
|
391 | 388 | if db_repo.repo_state in [repo.Repository.STATE_PENDING]: |
|
392 | 389 | raise HTTPFound( |
|
393 | 390 | request.route_path('repo_creating', |
|
394 | 391 | repo_name=db_repo.repo_name)) |
|
395 | 392 | |
|
396 | 393 | if by_name_match: |
|
397 | 394 | # register this as request object we can re-use later |
|
398 | 395 | request.db_repo = by_name_match |
|
399 | 396 | redirect_if_creating(by_name_match) |
|
400 | 397 | return True |
|
401 | 398 | |
|
402 | 399 | by_id_match = repo_model.get_repo_by_id(repo_name) |
|
403 | 400 | if by_id_match: |
|
404 | 401 | request.db_repo = by_id_match |
|
405 | 402 | redirect_if_creating(by_id_match) |
|
406 | 403 | return True |
|
407 | 404 | |
|
408 | 405 | return False |
|
409 | 406 | |
|
410 | 407 | |
|
411 | 408 | class RepoTypeRoutePredicate(object): |
|
412 | 409 | def __init__(self, val, config): |
|
413 | 410 | self.val = val or ['hg', 'git', 'svn'] |
|
414 | 411 | |
|
415 | 412 | def text(self): |
|
416 | 413 | return 'repo_accepted_type = %s' % self.val |
|
417 | 414 | |
|
418 | 415 | phash = text |
|
419 | 416 | |
|
420 | 417 | def __call__(self, info, request): |
|
421 | 418 | if hasattr(request, 'vcs_call'): |
|
422 | 419 | # skip vcs calls |
|
423 | 420 | return |
|
424 | 421 | |
|
425 | 422 | rhodecode_db_repo = request.db_repo |
|
426 | 423 | |
|
427 | 424 | log.debug( |
|
428 | 425 | '%s checking repo type for %s in %s', |
|
429 | 426 | self.__class__.__name__, rhodecode_db_repo.repo_type, self.val) |
|
430 | 427 | |
|
431 | 428 | if rhodecode_db_repo.repo_type in self.val: |
|
432 | 429 | return True |
|
433 | 430 | else: |
|
434 | 431 | log.warning('Current view is not supported for repo type:%s', |
|
435 | 432 | rhodecode_db_repo.repo_type) |
|
436 | 433 | # |
|
437 | 434 | # h.flash(h.literal( |
|
438 | 435 | # _('Action not supported for %s.' % rhodecode_repo.alias)), |
|
439 | 436 | # category='warning') |
|
440 | 437 | # return redirect( |
|
441 | 438 | # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name)) |
|
442 | 439 | |
|
443 | 440 | return False |
|
444 | 441 | |
|
445 | 442 | |
|
446 | 443 | class RepoGroupRoutePredicate(object): |
|
447 | 444 | def __init__(self, val, config): |
|
448 | 445 | self.val = val |
|
449 | 446 | |
|
450 | 447 | def text(self): |
|
451 | 448 | return 'repo_group_route = %s' % self.val |
|
452 | 449 | |
|
453 | 450 | phash = text |
|
454 | 451 | |
|
455 | 452 | def __call__(self, info, request): |
|
456 | 453 | if hasattr(request, 'vcs_call'): |
|
457 | 454 | # skip vcs calls |
|
458 | 455 | return |
|
459 | 456 | |
|
460 | 457 | repo_group_name = info['match']['repo_group_name'] |
|
461 | 458 | repo_group_model = repo_group.RepoGroupModel() |
|
462 | 459 | by_name_match = repo_group_model.get_by_group_name( |
|
463 | 460 | repo_group_name, cache=True) |
|
464 | 461 | |
|
465 | 462 | if by_name_match: |
|
466 | 463 | # register this as request object we can re-use later |
|
467 | 464 | request.db_repo_group = by_name_match |
|
468 | 465 | return True |
|
469 | 466 | |
|
470 | 467 | return False |
|
471 | 468 | |
|
472 | 469 | |
|
473 | 470 | class UserGroupRoutePredicate(object): |
|
474 | 471 | def __init__(self, val, config): |
|
475 | 472 | self.val = val |
|
476 | 473 | |
|
477 | 474 | def text(self): |
|
478 | 475 | return 'user_group_route = %s' % self.val |
|
479 | 476 | |
|
480 | 477 | phash = text |
|
481 | 478 | |
|
482 | 479 | def __call__(self, info, request): |
|
483 | 480 | if hasattr(request, 'vcs_call'): |
|
484 | 481 | # skip vcs calls |
|
485 | 482 | return |
|
486 | 483 | |
|
487 | 484 | user_group_id = info['match']['user_group_id'] |
|
488 | 485 | user_group_model = user_group.UserGroup() |
|
489 | 486 | by_name_match = user_group_model.get( |
|
490 | 487 | user_group_id, cache=True) |
|
491 | 488 | |
|
492 | 489 | if by_name_match: |
|
493 | 490 | # register this as request object we can re-use later |
|
494 | 491 | request.db_user_group = by_name_match |
|
495 | 492 | return True |
|
496 | 493 | |
|
497 | 494 | return False |
|
498 | 495 | |
|
499 | 496 | |
|
500 | 497 | def includeme(config): |
|
501 | 498 | config.add_route_predicate( |
|
502 | 499 | 'repo_route', RepoRoutePredicate) |
|
503 | 500 | config.add_route_predicate( |
|
504 | 501 | 'repo_accepted_types', RepoTypeRoutePredicate) |
|
505 | 502 | config.add_route_predicate( |
|
506 | 503 | 'repo_group_route', RepoGroupRoutePredicate) |
|
507 | 504 | config.add_route_predicate( |
|
508 | 505 | 'user_group_route', UserGroupRoutePredicate) |
@@ -1,78 +1,75 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | |
|
23 | 23 | from pyramid.httpexceptions import HTTPFound |
|
24 | 24 | from pyramid.view import view_config |
|
25 | 25 | |
|
26 | 26 | from rhodecode.apps._base import RepoAppView |
|
27 | 27 | from rhodecode.lib.auth import ( |
|
28 | 28 | LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired) |
|
29 | 29 | from rhodecode.lib import helpers as h |
|
30 | 30 | from rhodecode.model.meta import Session |
|
31 | 31 | from rhodecode.model.scm import ScmModel |
|
32 | 32 | |
|
33 | 33 | log = logging.getLogger(__name__) |
|
34 | 34 | |
|
35 | 35 | |
|
36 | 36 | class RepoCachesView(RepoAppView): |
|
37 | 37 | def load_default_context(self): |
|
38 | 38 | c = self._get_local_tmpl_context() |
|
39 | 39 | |
|
40 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
41 | c.repo_info = self.db_repo | |
|
42 | ||
|
43 | 40 | self._register_global_c(c) |
|
44 | 41 | return c |
|
45 | 42 | |
|
46 | 43 | @LoginRequired() |
|
47 | 44 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
48 | 45 | @view_config( |
|
49 | 46 | route_name='edit_repo_caches', request_method='GET', |
|
50 | 47 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
51 | 48 | def repo_caches(self): |
|
52 | 49 | c = self.load_default_context() |
|
53 | 50 | c.active = 'caches' |
|
54 | 51 | |
|
55 | 52 | return self._get_template_context(c) |
|
56 | 53 | |
|
57 | 54 | @LoginRequired() |
|
58 | 55 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
59 | 56 | @CSRFRequired() |
|
60 | 57 | @view_config( |
|
61 | 58 | route_name='edit_repo_caches', request_method='POST') |
|
62 | 59 | def repo_caches_purge(self): |
|
63 | 60 | _ = self.request.translate |
|
64 | 61 | c = self.load_default_context() |
|
65 | 62 | c.active = 'caches' |
|
66 | 63 | |
|
67 | 64 | try: |
|
68 | 65 | ScmModel().mark_for_invalidation(self.db_repo_name, delete=True) |
|
69 | 66 | Session().commit() |
|
70 | 67 | h.flash(_('Cache invalidation successful'), |
|
71 | 68 | category='success') |
|
72 | 69 | except Exception: |
|
73 | 70 | log.exception("Exception during cache invalidation") |
|
74 | 71 | h.flash(_('An error occurred during cache invalidation'), |
|
75 | 72 | category='error') |
|
76 | 73 | |
|
77 | 74 | raise HTTPFound(h.route_path( |
|
78 | 75 | 'edit_repo_caches', repo_name=self.db_repo_name)) No newline at end of file |
@@ -1,302 +1,299 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 | import logging |
|
23 | 23 | |
|
24 | 24 | from pyramid.httpexceptions import HTTPNotFound, HTTPFound |
|
25 | 25 | from pyramid.view import view_config |
|
26 | 26 | from pyramid.renderers import render |
|
27 | 27 | from pyramid.response import Response |
|
28 | 28 | |
|
29 | 29 | from rhodecode.apps._base import RepoAppView |
|
30 | 30 | import rhodecode.lib.helpers as h |
|
31 | 31 | from rhodecode.lib.auth import ( |
|
32 | 32 | LoginRequired, HasRepoPermissionAnyDecorator) |
|
33 | 33 | |
|
34 | 34 | from rhodecode.lib.ext_json import json |
|
35 | 35 | from rhodecode.lib.graphmod import _colored, _dagwalker |
|
36 | 36 | from rhodecode.lib.helpers import RepoPage |
|
37 | 37 | from rhodecode.lib.utils2 import safe_int, safe_str |
|
38 | 38 | from rhodecode.lib.vcs.exceptions import ( |
|
39 | 39 | RepositoryError, CommitDoesNotExistError, |
|
40 | 40 | CommitError, NodeDoesNotExistError, EmptyRepositoryError) |
|
41 | 41 | |
|
42 | 42 | log = logging.getLogger(__name__) |
|
43 | 43 | |
|
44 | 44 | DEFAULT_CHANGELOG_SIZE = 20 |
|
45 | 45 | |
|
46 | 46 | |
|
47 | 47 | class RepoChangelogView(RepoAppView): |
|
48 | 48 | |
|
49 | 49 | def _get_commit_or_redirect(self, commit_id, redirect_after=True): |
|
50 | 50 | """ |
|
51 | 51 | This is a safe way to get commit. If an error occurs it redirects to |
|
52 | 52 | tip with proper message |
|
53 | 53 | |
|
54 | 54 | :param commit_id: id of commit to fetch |
|
55 | 55 | :param redirect_after: toggle redirection |
|
56 | 56 | """ |
|
57 | 57 | _ = self.request.translate |
|
58 | 58 | |
|
59 | 59 | try: |
|
60 | 60 | return self.rhodecode_vcs_repo.get_commit(commit_id) |
|
61 | 61 | except EmptyRepositoryError: |
|
62 | 62 | if not redirect_after: |
|
63 | 63 | return None |
|
64 | 64 | |
|
65 | 65 | h.flash(h.literal( |
|
66 | 66 | _('There are no commits yet')), category='warning') |
|
67 | 67 | raise HTTPFound( |
|
68 | 68 | h.route_path('repo_summary', repo_name=self.db_repo_name)) |
|
69 | 69 | |
|
70 | 70 | except (CommitDoesNotExistError, LookupError): |
|
71 | 71 | msg = _('No such commit exists for this repository') |
|
72 | 72 | h.flash(msg, category='error') |
|
73 | 73 | raise HTTPNotFound() |
|
74 | 74 | except RepositoryError as e: |
|
75 | 75 | h.flash(safe_str(h.escape(e)), category='error') |
|
76 | 76 | raise HTTPNotFound() |
|
77 | 77 | |
|
78 | 78 | def _graph(self, repo, commits, prev_data=None, next_data=None): |
|
79 | 79 | """ |
|
80 | 80 | Generates a DAG graph for repo |
|
81 | 81 | |
|
82 | 82 | :param repo: repo instance |
|
83 | 83 | :param commits: list of commits |
|
84 | 84 | """ |
|
85 | 85 | if not commits: |
|
86 | 86 | return json.dumps([]) |
|
87 | 87 | |
|
88 | 88 | def serialize(commit, parents=True): |
|
89 | 89 | data = dict( |
|
90 | 90 | raw_id=commit.raw_id, |
|
91 | 91 | idx=commit.idx, |
|
92 | 92 | branch=commit.branch, |
|
93 | 93 | ) |
|
94 | 94 | if parents: |
|
95 | 95 | data['parents'] = [ |
|
96 | 96 | serialize(x, parents=False) for x in commit.parents] |
|
97 | 97 | return data |
|
98 | 98 | |
|
99 | 99 | prev_data = prev_data or [] |
|
100 | 100 | next_data = next_data or [] |
|
101 | 101 | |
|
102 | 102 | current = [serialize(x) for x in commits] |
|
103 | 103 | commits = prev_data + current + next_data |
|
104 | 104 | |
|
105 | 105 | dag = _dagwalker(repo, commits) |
|
106 | 106 | |
|
107 | 107 | data = [[commit_id, vtx, edges, branch] |
|
108 | 108 | for commit_id, vtx, edges, branch in _colored(dag)] |
|
109 | 109 | return json.dumps(data), json.dumps(current) |
|
110 | 110 | |
|
111 | 111 | def _check_if_valid_branch(self, branch_name, repo_name, f_path): |
|
112 | 112 | if branch_name not in self.rhodecode_vcs_repo.branches_all: |
|
113 | 113 | h.flash('Branch {} is not found.'.format(h.escape(branch_name)), |
|
114 | 114 | category='warning') |
|
115 | 115 | redirect_url = h.route_path( |
|
116 | 116 | 'repo_changelog_file', repo_name=repo_name, |
|
117 | 117 | commit_id=branch_name, f_path=f_path or '') |
|
118 | 118 | raise HTTPFound(redirect_url) |
|
119 | 119 | |
|
120 | 120 | def _load_changelog_data( |
|
121 | 121 | self, c, collection, page, chunk_size, branch_name=None, |
|
122 | 122 | dynamic=False): |
|
123 | 123 | |
|
124 | 124 | def url_generator(**kw): |
|
125 | 125 | query_params = {} |
|
126 | 126 | query_params.update(kw) |
|
127 | 127 | return h.route_path( |
|
128 | 128 | 'repo_changelog', |
|
129 | 129 | repo_name=c.rhodecode_db_repo.repo_name, _query=query_params) |
|
130 | 130 | |
|
131 | 131 | c.total_cs = len(collection) |
|
132 | 132 | c.showing_commits = min(chunk_size, c.total_cs) |
|
133 | 133 | c.pagination = RepoPage(collection, page=page, item_count=c.total_cs, |
|
134 | 134 | items_per_page=chunk_size, branch=branch_name, |
|
135 | 135 | url=url_generator) |
|
136 | 136 | |
|
137 | 137 | c.next_page = c.pagination.next_page |
|
138 | 138 | c.prev_page = c.pagination.previous_page |
|
139 | 139 | |
|
140 | 140 | if dynamic: |
|
141 | 141 | if self.request.GET.get('chunk') != 'next': |
|
142 | 142 | c.next_page = None |
|
143 | 143 | if self.request.GET.get('chunk') != 'prev': |
|
144 | 144 | c.prev_page = None |
|
145 | 145 | |
|
146 | 146 | page_commit_ids = [x.raw_id for x in c.pagination] |
|
147 | 147 | c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids) |
|
148 | 148 | c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids) |
|
149 | 149 | |
|
150 | 150 | def load_default_context(self): |
|
151 | 151 | c = self._get_local_tmpl_context(include_app_defaults=True) |
|
152 | 152 | |
|
153 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
154 | c.repo_info = self.db_repo | |
|
155 | 153 | c.rhodecode_repo = self.rhodecode_vcs_repo |
|
156 | ||
|
157 | 154 | self._register_global_c(c) |
|
158 | 155 | return c |
|
159 | 156 | |
|
160 | 157 | @LoginRequired() |
|
161 | 158 | @HasRepoPermissionAnyDecorator( |
|
162 | 159 | 'repository.read', 'repository.write', 'repository.admin') |
|
163 | 160 | @view_config( |
|
164 | 161 | route_name='repo_changelog', request_method='GET', |
|
165 | 162 | renderer='rhodecode:templates/changelog/changelog.mako') |
|
166 | 163 | @view_config( |
|
167 | 164 | route_name='repo_changelog_file', request_method='GET', |
|
168 | 165 | renderer='rhodecode:templates/changelog/changelog.mako') |
|
169 | 166 | def repo_changelog(self): |
|
170 | 167 | c = self.load_default_context() |
|
171 | 168 | |
|
172 | 169 | commit_id = self.request.matchdict.get('commit_id') |
|
173 | 170 | f_path = self._get_f_path(self.request.matchdict) |
|
174 | 171 | |
|
175 | 172 | chunk_size = 20 |
|
176 | 173 | |
|
177 | 174 | c.branch_name = branch_name = self.request.GET.get('branch') or '' |
|
178 | 175 | c.book_name = book_name = self.request.GET.get('bookmark') or '' |
|
179 | 176 | hist_limit = safe_int(self.request.GET.get('limit')) or None |
|
180 | 177 | |
|
181 | 178 | p = safe_int(self.request.GET.get('page', 1), 1) |
|
182 | 179 | |
|
183 | 180 | c.selected_name = branch_name or book_name |
|
184 | 181 | if not commit_id and branch_name: |
|
185 | 182 | self._check_if_valid_branch(branch_name, self.db_repo_name, f_path) |
|
186 | 183 | |
|
187 | 184 | c.changelog_for_path = f_path |
|
188 | 185 | pre_load = ['author', 'branch', 'date', 'message', 'parents'] |
|
189 | 186 | commit_ids = [] |
|
190 | 187 | |
|
191 | 188 | partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR') |
|
192 | 189 | |
|
193 | 190 | try: |
|
194 | 191 | if f_path: |
|
195 | 192 | log.debug('generating changelog for path %s', f_path) |
|
196 | 193 | # get the history for the file ! |
|
197 | 194 | base_commit = self.rhodecode_vcs_repo.get_commit(commit_id) |
|
198 | 195 | try: |
|
199 | 196 | collection = base_commit.get_file_history( |
|
200 | 197 | f_path, limit=hist_limit, pre_load=pre_load) |
|
201 | 198 | if collection and partial_xhr: |
|
202 | 199 | # for ajax call we remove first one since we're looking |
|
203 | 200 | # at it right now in the context of a file commit |
|
204 | 201 | collection.pop(0) |
|
205 | 202 | except (NodeDoesNotExistError, CommitError): |
|
206 | 203 | # this node is not present at tip! |
|
207 | 204 | try: |
|
208 | 205 | commit = self._get_commit_or_redirect(commit_id) |
|
209 | 206 | collection = commit.get_file_history(f_path) |
|
210 | 207 | except RepositoryError as e: |
|
211 | 208 | h.flash(safe_str(e), category='warning') |
|
212 | 209 | redirect_url = h.route_path( |
|
213 | 210 | 'repo_changelog', repo_name=self.db_repo_name) |
|
214 | 211 | raise HTTPFound(redirect_url) |
|
215 | 212 | collection = list(reversed(collection)) |
|
216 | 213 | else: |
|
217 | 214 | collection = self.rhodecode_vcs_repo.get_commits( |
|
218 | 215 | branch_name=branch_name, pre_load=pre_load) |
|
219 | 216 | |
|
220 | 217 | self._load_changelog_data( |
|
221 | 218 | c, collection, p, chunk_size, c.branch_name, dynamic=f_path) |
|
222 | 219 | |
|
223 | 220 | except EmptyRepositoryError as e: |
|
224 | 221 | h.flash(safe_str(h.escape(e)), category='warning') |
|
225 | 222 | raise HTTPFound( |
|
226 | 223 | h.route_path('repo_summary', repo_name=self.db_repo_name)) |
|
227 | 224 | except (RepositoryError, CommitDoesNotExistError, Exception) as e: |
|
228 | 225 | log.exception(safe_str(e)) |
|
229 | 226 | h.flash(safe_str(h.escape(e)), category='error') |
|
230 | 227 | raise HTTPFound( |
|
231 | 228 | h.route_path('repo_changelog', repo_name=self.db_repo_name)) |
|
232 | 229 | |
|
233 | 230 | if partial_xhr or self.request.environ.get('HTTP_X_PJAX'): |
|
234 | 231 | # loading from ajax, we don't want the first result, it's popped |
|
235 | 232 | # in the code above |
|
236 | 233 | html = render( |
|
237 | 234 | 'rhodecode:templates/changelog/changelog_file_history.mako', |
|
238 | 235 | self._get_template_context(c), self.request) |
|
239 | 236 | return Response(html) |
|
240 | 237 | |
|
241 | 238 | if not f_path: |
|
242 | 239 | commit_ids = c.pagination |
|
243 | 240 | |
|
244 | 241 | c.graph_data, c.graph_commits = self._graph( |
|
245 | 242 | self.rhodecode_vcs_repo, commit_ids) |
|
246 | 243 | |
|
247 | 244 | return self._get_template_context(c) |
|
248 | 245 | |
|
249 | 246 | @LoginRequired() |
|
250 | 247 | @HasRepoPermissionAnyDecorator( |
|
251 | 248 | 'repository.read', 'repository.write', 'repository.admin') |
|
252 | 249 | @view_config( |
|
253 | 250 | route_name='repo_changelog_elements', request_method=('GET', 'POST'), |
|
254 | 251 | renderer='rhodecode:templates/changelog/changelog_elements.mako', |
|
255 | 252 | xhr=True) |
|
256 | 253 | def repo_changelog_elements(self): |
|
257 | 254 | c = self.load_default_context() |
|
258 | 255 | chunk_size = 20 |
|
259 | 256 | |
|
260 | 257 | def wrap_for_error(err): |
|
261 | 258 | html = '<tr>' \ |
|
262 | 259 | '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \ |
|
263 | 260 | '</tr>'.format(err) |
|
264 | 261 | return Response(html) |
|
265 | 262 | |
|
266 | 263 | c.branch_name = branch_name = self.request.GET.get('branch') or '' |
|
267 | 264 | c.book_name = book_name = self.request.GET.get('bookmark') or '' |
|
268 | 265 | |
|
269 | 266 | c.selected_name = branch_name or book_name |
|
270 | 267 | if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all: |
|
271 | 268 | return wrap_for_error( |
|
272 | 269 | safe_str('Branch: {} is not valid'.format(branch_name))) |
|
273 | 270 | |
|
274 | 271 | pre_load = ['author', 'branch', 'date', 'message', 'parents'] |
|
275 | 272 | collection = self.rhodecode_vcs_repo.get_commits( |
|
276 | 273 | branch_name=branch_name, pre_load=pre_load) |
|
277 | 274 | |
|
278 | 275 | p = safe_int(self.request.GET.get('page', 1), 1) |
|
279 | 276 | try: |
|
280 | 277 | self._load_changelog_data( |
|
281 | 278 | c, collection, p, chunk_size, dynamic=True) |
|
282 | 279 | except EmptyRepositoryError as e: |
|
283 | 280 | return wrap_for_error(safe_str(e)) |
|
284 | 281 | except (RepositoryError, CommitDoesNotExistError, Exception) as e: |
|
285 | 282 | log.exception('Failed to fetch commits') |
|
286 | 283 | return wrap_for_error(safe_str(e)) |
|
287 | 284 | |
|
288 | 285 | prev_data = None |
|
289 | 286 | next_data = None |
|
290 | 287 | |
|
291 | 288 | prev_graph = json.loads(self.request.POST.get('graph', '')) |
|
292 | 289 | |
|
293 | 290 | if self.request.GET.get('chunk') == 'prev': |
|
294 | 291 | next_data = prev_graph |
|
295 | 292 | elif self.request.GET.get('chunk') == 'next': |
|
296 | 293 | prev_data = prev_graph |
|
297 | 294 | |
|
298 | 295 | c.graph_data, c.graph_commits = self._graph( |
|
299 | 296 | self.rhodecode_vcs_repo, c.pagination, |
|
300 | 297 | prev_data=prev_data, next_data=next_data) |
|
301 | 298 | |
|
302 | 299 | return self._get_template_context(c) |
@@ -1,557 +1,554 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 | import logging |
|
23 | 23 | import collections |
|
24 | 24 | |
|
25 | 25 | from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound |
|
26 | 26 | from pyramid.view import view_config |
|
27 | 27 | from pyramid.renderers import render |
|
28 | 28 | from pyramid.response import Response |
|
29 | 29 | |
|
30 | 30 | from rhodecode.apps._base import RepoAppView |
|
31 | 31 | |
|
32 | 32 | from rhodecode.lib import diffs, codeblocks |
|
33 | 33 | from rhodecode.lib.auth import ( |
|
34 | 34 | LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired) |
|
35 | 35 | |
|
36 | 36 | from rhodecode.lib.compat import OrderedDict |
|
37 | 37 | from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError |
|
38 | 38 | import rhodecode.lib.helpers as h |
|
39 | 39 | from rhodecode.lib.utils2 import safe_unicode |
|
40 | 40 | from rhodecode.lib.vcs.backends.base import EmptyCommit |
|
41 | 41 | from rhodecode.lib.vcs.exceptions import ( |
|
42 | 42 | RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError) |
|
43 | 43 | from rhodecode.model.db import ChangesetComment, ChangesetStatus |
|
44 | 44 | from rhodecode.model.changeset_status import ChangesetStatusModel |
|
45 | 45 | from rhodecode.model.comment import CommentsModel |
|
46 | 46 | from rhodecode.model.meta import Session |
|
47 | 47 | |
|
48 | 48 | |
|
49 | 49 | log = logging.getLogger(__name__) |
|
50 | 50 | |
|
51 | 51 | |
|
52 | 52 | def _update_with_GET(params, request): |
|
53 | 53 | for k in ['diff1', 'diff2', 'diff']: |
|
54 | 54 | params[k] += request.GET.getall(k) |
|
55 | 55 | |
|
56 | 56 | |
|
57 | 57 | def get_ignore_ws(fid, request): |
|
58 | 58 | ig_ws_global = request.GET.get('ignorews') |
|
59 | 59 | ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid)) |
|
60 | 60 | if ig_ws: |
|
61 | 61 | try: |
|
62 | 62 | return int(ig_ws[0].split(':')[-1]) |
|
63 | 63 | except Exception: |
|
64 | 64 | pass |
|
65 | 65 | return ig_ws_global |
|
66 | 66 | |
|
67 | 67 | |
|
68 | 68 | def _ignorews_url(request, fileid=None): |
|
69 | 69 | _ = request.translate |
|
70 | 70 | fileid = str(fileid) if fileid else None |
|
71 | 71 | params = collections.defaultdict(list) |
|
72 | 72 | _update_with_GET(params, request) |
|
73 | 73 | label = _('Show whitespace') |
|
74 | 74 | tooltiplbl = _('Show whitespace for all diffs') |
|
75 | 75 | ig_ws = get_ignore_ws(fileid, request) |
|
76 | 76 | ln_ctx = get_line_ctx(fileid, request) |
|
77 | 77 | |
|
78 | 78 | if ig_ws is None: |
|
79 | 79 | params['ignorews'] += [1] |
|
80 | 80 | label = _('Ignore whitespace') |
|
81 | 81 | tooltiplbl = _('Ignore whitespace for all diffs') |
|
82 | 82 | ctx_key = 'context' |
|
83 | 83 | ctx_val = ln_ctx |
|
84 | 84 | |
|
85 | 85 | # if we have passed in ln_ctx pass it along to our params |
|
86 | 86 | if ln_ctx: |
|
87 | 87 | params[ctx_key] += [ctx_val] |
|
88 | 88 | |
|
89 | 89 | if fileid: |
|
90 | 90 | params['anchor'] = 'a_' + fileid |
|
91 | 91 | return h.link_to(label, request.current_route_path(_query=params), |
|
92 | 92 | title=tooltiplbl, class_='tooltip') |
|
93 | 93 | |
|
94 | 94 | |
|
95 | 95 | def get_line_ctx(fid, request): |
|
96 | 96 | ln_ctx_global = request.GET.get('context') |
|
97 | 97 | if fid: |
|
98 | 98 | ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid)) |
|
99 | 99 | else: |
|
100 | 100 | _ln_ctx = filter(lambda k: k.startswith('C'), request.GET) |
|
101 | 101 | ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global |
|
102 | 102 | if ln_ctx: |
|
103 | 103 | ln_ctx = [ln_ctx] |
|
104 | 104 | |
|
105 | 105 | if ln_ctx: |
|
106 | 106 | retval = ln_ctx[0].split(':')[-1] |
|
107 | 107 | else: |
|
108 | 108 | retval = ln_ctx_global |
|
109 | 109 | |
|
110 | 110 | try: |
|
111 | 111 | return int(retval) |
|
112 | 112 | except Exception: |
|
113 | 113 | return 3 |
|
114 | 114 | |
|
115 | 115 | |
|
116 | 116 | def _context_url(request, fileid=None): |
|
117 | 117 | """ |
|
118 | 118 | Generates a url for context lines. |
|
119 | 119 | |
|
120 | 120 | :param fileid: |
|
121 | 121 | """ |
|
122 | 122 | |
|
123 | 123 | _ = request.translate |
|
124 | 124 | fileid = str(fileid) if fileid else None |
|
125 | 125 | ig_ws = get_ignore_ws(fileid, request) |
|
126 | 126 | ln_ctx = (get_line_ctx(fileid, request) or 3) * 2 |
|
127 | 127 | |
|
128 | 128 | params = collections.defaultdict(list) |
|
129 | 129 | _update_with_GET(params, request) |
|
130 | 130 | |
|
131 | 131 | if ln_ctx > 0: |
|
132 | 132 | params['context'] += [ln_ctx] |
|
133 | 133 | |
|
134 | 134 | if ig_ws: |
|
135 | 135 | ig_ws_key = 'ignorews' |
|
136 | 136 | ig_ws_val = 1 |
|
137 | 137 | params[ig_ws_key] += [ig_ws_val] |
|
138 | 138 | |
|
139 | 139 | lbl = _('Increase context') |
|
140 | 140 | tooltiplbl = _('Increase context for all diffs') |
|
141 | 141 | |
|
142 | 142 | if fileid: |
|
143 | 143 | params['anchor'] = 'a_' + fileid |
|
144 | 144 | return h.link_to(lbl, request.current_route_path(_query=params), |
|
145 | 145 | title=tooltiplbl, class_='tooltip') |
|
146 | 146 | |
|
147 | 147 | |
|
148 | 148 | class RepoCommitsView(RepoAppView): |
|
149 | 149 | def load_default_context(self): |
|
150 | 150 | c = self._get_local_tmpl_context(include_app_defaults=True) |
|
151 | ||
|
152 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
153 | c.repo_info = self.db_repo | |
|
154 | 151 | c.rhodecode_repo = self.rhodecode_vcs_repo |
|
155 | 152 | |
|
156 | 153 | self._register_global_c(c) |
|
157 | 154 | return c |
|
158 | 155 | |
|
159 | 156 | def _commit(self, commit_id_range, method): |
|
160 | 157 | _ = self.request.translate |
|
161 | 158 | c = self.load_default_context() |
|
162 | 159 | c.ignorews_url = _ignorews_url |
|
163 | 160 | c.context_url = _context_url |
|
164 | 161 | c.fulldiff = self.request.GET.get('fulldiff') |
|
165 | 162 | |
|
166 | 163 | # fetch global flags of ignore ws or context lines |
|
167 | 164 | context_lcl = get_line_ctx('', self.request) |
|
168 | 165 | ign_whitespace_lcl = get_ignore_ws('', self.request) |
|
169 | 166 | |
|
170 | 167 | # diff_limit will cut off the whole diff if the limit is applied |
|
171 | 168 | # otherwise it will just hide the big files from the front-end |
|
172 | 169 | diff_limit = c.visual.cut_off_limit_diff |
|
173 | 170 | file_limit = c.visual.cut_off_limit_file |
|
174 | 171 | |
|
175 | 172 | # get ranges of commit ids if preset |
|
176 | 173 | commit_range = commit_id_range.split('...')[:2] |
|
177 | 174 | |
|
178 | 175 | try: |
|
179 | 176 | pre_load = ['affected_files', 'author', 'branch', 'date', |
|
180 | 177 | 'message', 'parents'] |
|
181 | 178 | |
|
182 | 179 | if len(commit_range) == 2: |
|
183 | 180 | commits = self.rhodecode_vcs_repo.get_commits( |
|
184 | 181 | start_id=commit_range[0], end_id=commit_range[1], |
|
185 | 182 | pre_load=pre_load) |
|
186 | 183 | commits = list(commits) |
|
187 | 184 | else: |
|
188 | 185 | commits = [self.rhodecode_vcs_repo.get_commit( |
|
189 | 186 | commit_id=commit_id_range, pre_load=pre_load)] |
|
190 | 187 | |
|
191 | 188 | c.commit_ranges = commits |
|
192 | 189 | if not c.commit_ranges: |
|
193 | 190 | raise RepositoryError( |
|
194 | 191 | 'The commit range returned an empty result') |
|
195 | 192 | except CommitDoesNotExistError: |
|
196 | 193 | msg = _('No such commit exists for this repository') |
|
197 | 194 | h.flash(msg, category='error') |
|
198 | 195 | raise HTTPNotFound() |
|
199 | 196 | except Exception: |
|
200 | 197 | log.exception("General failure") |
|
201 | 198 | raise HTTPNotFound() |
|
202 | 199 | |
|
203 | 200 | c.changes = OrderedDict() |
|
204 | 201 | c.lines_added = 0 |
|
205 | 202 | c.lines_deleted = 0 |
|
206 | 203 | |
|
207 | 204 | # auto collapse if we have more than limit |
|
208 | 205 | collapse_limit = diffs.DiffProcessor._collapse_commits_over |
|
209 | 206 | c.collapse_all_commits = len(c.commit_ranges) > collapse_limit |
|
210 | 207 | |
|
211 | 208 | c.commit_statuses = ChangesetStatus.STATUSES |
|
212 | 209 | c.inline_comments = [] |
|
213 | 210 | c.files = [] |
|
214 | 211 | |
|
215 | 212 | c.statuses = [] |
|
216 | 213 | c.comments = [] |
|
217 | 214 | c.unresolved_comments = [] |
|
218 | 215 | if len(c.commit_ranges) == 1: |
|
219 | 216 | commit = c.commit_ranges[0] |
|
220 | 217 | c.comments = CommentsModel().get_comments( |
|
221 | 218 | self.db_repo.repo_id, |
|
222 | 219 | revision=commit.raw_id) |
|
223 | 220 | c.statuses.append(ChangesetStatusModel().get_status( |
|
224 | 221 | self.db_repo.repo_id, commit.raw_id)) |
|
225 | 222 | # comments from PR |
|
226 | 223 | statuses = ChangesetStatusModel().get_statuses( |
|
227 | 224 | self.db_repo.repo_id, commit.raw_id, |
|
228 | 225 | with_revisions=True) |
|
229 | 226 | prs = set(st.pull_request for st in statuses |
|
230 | 227 | if st.pull_request is not None) |
|
231 | 228 | # from associated statuses, check the pull requests, and |
|
232 | 229 | # show comments from them |
|
233 | 230 | for pr in prs: |
|
234 | 231 | c.comments.extend(pr.comments) |
|
235 | 232 | |
|
236 | 233 | c.unresolved_comments = CommentsModel()\ |
|
237 | 234 | .get_commit_unresolved_todos(commit.raw_id) |
|
238 | 235 | |
|
239 | 236 | diff = None |
|
240 | 237 | # Iterate over ranges (default commit view is always one commit) |
|
241 | 238 | for commit in c.commit_ranges: |
|
242 | 239 | c.changes[commit.raw_id] = [] |
|
243 | 240 | |
|
244 | 241 | commit2 = commit |
|
245 | 242 | commit1 = commit.parents[0] if commit.parents else EmptyCommit() |
|
246 | 243 | |
|
247 | 244 | _diff = self.rhodecode_vcs_repo.get_diff( |
|
248 | 245 | commit1, commit2, |
|
249 | 246 | ignore_whitespace=ign_whitespace_lcl, context=context_lcl) |
|
250 | 247 | diff_processor = diffs.DiffProcessor( |
|
251 | 248 | _diff, format='newdiff', diff_limit=diff_limit, |
|
252 | 249 | file_limit=file_limit, show_full_diff=c.fulldiff) |
|
253 | 250 | |
|
254 | 251 | commit_changes = OrderedDict() |
|
255 | 252 | if method == 'show': |
|
256 | 253 | _parsed = diff_processor.prepare() |
|
257 | 254 | c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer) |
|
258 | 255 | |
|
259 | 256 | _parsed = diff_processor.prepare() |
|
260 | 257 | |
|
261 | 258 | def _node_getter(commit): |
|
262 | 259 | def get_node(fname): |
|
263 | 260 | try: |
|
264 | 261 | return commit.get_node(fname) |
|
265 | 262 | except NodeDoesNotExistError: |
|
266 | 263 | return None |
|
267 | 264 | return get_node |
|
268 | 265 | |
|
269 | 266 | inline_comments = CommentsModel().get_inline_comments( |
|
270 | 267 | self.db_repo.repo_id, revision=commit.raw_id) |
|
271 | 268 | c.inline_cnt = CommentsModel().get_inline_comments_count( |
|
272 | 269 | inline_comments) |
|
273 | 270 | |
|
274 | 271 | diffset = codeblocks.DiffSet( |
|
275 | 272 | repo_name=self.db_repo_name, |
|
276 | 273 | source_node_getter=_node_getter(commit1), |
|
277 | 274 | target_node_getter=_node_getter(commit2), |
|
278 | 275 | comments=inline_comments) |
|
279 | 276 | diffset = diffset.render_patchset( |
|
280 | 277 | _parsed, commit1.raw_id, commit2.raw_id) |
|
281 | 278 | |
|
282 | 279 | c.changes[commit.raw_id] = diffset |
|
283 | 280 | else: |
|
284 | 281 | # downloads/raw we only need RAW diff nothing else |
|
285 | 282 | diff = diff_processor.as_raw() |
|
286 | 283 | c.changes[commit.raw_id] = [None, None, None, None, diff, None, None] |
|
287 | 284 | |
|
288 | 285 | # sort comments by how they were generated |
|
289 | 286 | c.comments = sorted(c.comments, key=lambda x: x.comment_id) |
|
290 | 287 | |
|
291 | 288 | if len(c.commit_ranges) == 1: |
|
292 | 289 | c.commit = c.commit_ranges[0] |
|
293 | 290 | c.parent_tmpl = ''.join( |
|
294 | 291 | '# Parent %s\n' % x.raw_id for x in c.commit.parents) |
|
295 | 292 | |
|
296 | 293 | if method == 'download': |
|
297 | 294 | response = Response(diff) |
|
298 | 295 | response.content_type = 'text/plain' |
|
299 | 296 | response.content_disposition = ( |
|
300 | 297 | 'attachment; filename=%s.diff' % commit_id_range[:12]) |
|
301 | 298 | return response |
|
302 | 299 | elif method == 'patch': |
|
303 | 300 | c.diff = safe_unicode(diff) |
|
304 | 301 | patch = render( |
|
305 | 302 | 'rhodecode:templates/changeset/patch_changeset.mako', |
|
306 | 303 | self._get_template_context(c), self.request) |
|
307 | 304 | response = Response(patch) |
|
308 | 305 | response.content_type = 'text/plain' |
|
309 | 306 | return response |
|
310 | 307 | elif method == 'raw': |
|
311 | 308 | response = Response(diff) |
|
312 | 309 | response.content_type = 'text/plain' |
|
313 | 310 | return response |
|
314 | 311 | elif method == 'show': |
|
315 | 312 | if len(c.commit_ranges) == 1: |
|
316 | 313 | html = render( |
|
317 | 314 | 'rhodecode:templates/changeset/changeset.mako', |
|
318 | 315 | self._get_template_context(c), self.request) |
|
319 | 316 | return Response(html) |
|
320 | 317 | else: |
|
321 | 318 | c.ancestor = None |
|
322 | 319 | c.target_repo = self.db_repo |
|
323 | 320 | html = render( |
|
324 | 321 | 'rhodecode:templates/changeset/changeset_range.mako', |
|
325 | 322 | self._get_template_context(c), self.request) |
|
326 | 323 | return Response(html) |
|
327 | 324 | |
|
328 | 325 | raise HTTPBadRequest() |
|
329 | 326 | |
|
330 | 327 | @LoginRequired() |
|
331 | 328 | @HasRepoPermissionAnyDecorator( |
|
332 | 329 | 'repository.read', 'repository.write', 'repository.admin') |
|
333 | 330 | @view_config( |
|
334 | 331 | route_name='repo_commit', request_method='GET', |
|
335 | 332 | renderer=None) |
|
336 | 333 | def repo_commit_show(self): |
|
337 | 334 | commit_id = self.request.matchdict['commit_id'] |
|
338 | 335 | return self._commit(commit_id, method='show') |
|
339 | 336 | |
|
340 | 337 | @LoginRequired() |
|
341 | 338 | @HasRepoPermissionAnyDecorator( |
|
342 | 339 | 'repository.read', 'repository.write', 'repository.admin') |
|
343 | 340 | @view_config( |
|
344 | 341 | route_name='repo_commit_raw', request_method='GET', |
|
345 | 342 | renderer=None) |
|
346 | 343 | @view_config( |
|
347 | 344 | route_name='repo_commit_raw_deprecated', request_method='GET', |
|
348 | 345 | renderer=None) |
|
349 | 346 | def repo_commit_raw(self): |
|
350 | 347 | commit_id = self.request.matchdict['commit_id'] |
|
351 | 348 | return self._commit(commit_id, method='raw') |
|
352 | 349 | |
|
353 | 350 | @LoginRequired() |
|
354 | 351 | @HasRepoPermissionAnyDecorator( |
|
355 | 352 | 'repository.read', 'repository.write', 'repository.admin') |
|
356 | 353 | @view_config( |
|
357 | 354 | route_name='repo_commit_patch', request_method='GET', |
|
358 | 355 | renderer=None) |
|
359 | 356 | def repo_commit_patch(self): |
|
360 | 357 | commit_id = self.request.matchdict['commit_id'] |
|
361 | 358 | return self._commit(commit_id, method='patch') |
|
362 | 359 | |
|
363 | 360 | @LoginRequired() |
|
364 | 361 | @HasRepoPermissionAnyDecorator( |
|
365 | 362 | 'repository.read', 'repository.write', 'repository.admin') |
|
366 | 363 | @view_config( |
|
367 | 364 | route_name='repo_commit_download', request_method='GET', |
|
368 | 365 | renderer=None) |
|
369 | 366 | def repo_commit_download(self): |
|
370 | 367 | commit_id = self.request.matchdict['commit_id'] |
|
371 | 368 | return self._commit(commit_id, method='download') |
|
372 | 369 | |
|
373 | 370 | @LoginRequired() |
|
374 | 371 | @NotAnonymous() |
|
375 | 372 | @HasRepoPermissionAnyDecorator( |
|
376 | 373 | 'repository.read', 'repository.write', 'repository.admin') |
|
377 | 374 | @CSRFRequired() |
|
378 | 375 | @view_config( |
|
379 | 376 | route_name='repo_commit_comment_create', request_method='POST', |
|
380 | 377 | renderer='json_ext') |
|
381 | 378 | def repo_commit_comment_create(self): |
|
382 | 379 | _ = self.request.translate |
|
383 | 380 | commit_id = self.request.matchdict['commit_id'] |
|
384 | 381 | |
|
385 | 382 | c = self.load_default_context() |
|
386 | 383 | status = self.request.POST.get('changeset_status', None) |
|
387 | 384 | text = self.request.POST.get('text') |
|
388 | 385 | comment_type = self.request.POST.get('comment_type') |
|
389 | 386 | resolves_comment_id = self.request.POST.get('resolves_comment_id', None) |
|
390 | 387 | |
|
391 | 388 | if status: |
|
392 | 389 | text = text or (_('Status change %(transition_icon)s %(status)s') |
|
393 | 390 | % {'transition_icon': '>', |
|
394 | 391 | 'status': ChangesetStatus.get_status_lbl(status)}) |
|
395 | 392 | |
|
396 | 393 | multi_commit_ids = [] |
|
397 | 394 | for _commit_id in self.request.POST.get('commit_ids', '').split(','): |
|
398 | 395 | if _commit_id not in ['', None, EmptyCommit.raw_id]: |
|
399 | 396 | if _commit_id not in multi_commit_ids: |
|
400 | 397 | multi_commit_ids.append(_commit_id) |
|
401 | 398 | |
|
402 | 399 | commit_ids = multi_commit_ids or [commit_id] |
|
403 | 400 | |
|
404 | 401 | comment = None |
|
405 | 402 | for current_id in filter(None, commit_ids): |
|
406 | 403 | comment = CommentsModel().create( |
|
407 | 404 | text=text, |
|
408 | 405 | repo=self.db_repo.repo_id, |
|
409 | 406 | user=self._rhodecode_db_user.user_id, |
|
410 | 407 | commit_id=current_id, |
|
411 | 408 | f_path=self.request.POST.get('f_path'), |
|
412 | 409 | line_no=self.request.POST.get('line'), |
|
413 | 410 | status_change=(ChangesetStatus.get_status_lbl(status) |
|
414 | 411 | if status else None), |
|
415 | 412 | status_change_type=status, |
|
416 | 413 | comment_type=comment_type, |
|
417 | 414 | resolves_comment_id=resolves_comment_id |
|
418 | 415 | ) |
|
419 | 416 | |
|
420 | 417 | # get status if set ! |
|
421 | 418 | if status: |
|
422 | 419 | # if latest status was from pull request and it's closed |
|
423 | 420 | # disallow changing status ! |
|
424 | 421 | # dont_allow_on_closed_pull_request = True ! |
|
425 | 422 | |
|
426 | 423 | try: |
|
427 | 424 | ChangesetStatusModel().set_status( |
|
428 | 425 | self.db_repo.repo_id, |
|
429 | 426 | status, |
|
430 | 427 | self._rhodecode_db_user.user_id, |
|
431 | 428 | comment, |
|
432 | 429 | revision=current_id, |
|
433 | 430 | dont_allow_on_closed_pull_request=True |
|
434 | 431 | ) |
|
435 | 432 | except StatusChangeOnClosedPullRequestError: |
|
436 | 433 | msg = _('Changing the status of a commit associated with ' |
|
437 | 434 | 'a closed pull request is not allowed') |
|
438 | 435 | log.exception(msg) |
|
439 | 436 | h.flash(msg, category='warning') |
|
440 | 437 | raise HTTPFound(h.route_path( |
|
441 | 438 | 'repo_commit', repo_name=self.db_repo_name, |
|
442 | 439 | commit_id=current_id)) |
|
443 | 440 | |
|
444 | 441 | # finalize, commit and redirect |
|
445 | 442 | Session().commit() |
|
446 | 443 | |
|
447 | 444 | data = { |
|
448 | 445 | 'target_id': h.safeid(h.safe_unicode( |
|
449 | 446 | self.request.POST.get('f_path'))), |
|
450 | 447 | } |
|
451 | 448 | if comment: |
|
452 | 449 | c.co = comment |
|
453 | 450 | rendered_comment = render( |
|
454 | 451 | 'rhodecode:templates/changeset/changeset_comment_block.mako', |
|
455 | 452 | self._get_template_context(c), self.request) |
|
456 | 453 | |
|
457 | 454 | data.update(comment.get_dict()) |
|
458 | 455 | data.update({'rendered_text': rendered_comment}) |
|
459 | 456 | |
|
460 | 457 | return data |
|
461 | 458 | |
|
462 | 459 | @LoginRequired() |
|
463 | 460 | @NotAnonymous() |
|
464 | 461 | @HasRepoPermissionAnyDecorator( |
|
465 | 462 | 'repository.read', 'repository.write', 'repository.admin') |
|
466 | 463 | @CSRFRequired() |
|
467 | 464 | @view_config( |
|
468 | 465 | route_name='repo_commit_comment_preview', request_method='POST', |
|
469 | 466 | renderer='string', xhr=True) |
|
470 | 467 | def repo_commit_comment_preview(self): |
|
471 | 468 | # Technically a CSRF token is not needed as no state changes with this |
|
472 | 469 | # call. However, as this is a POST is better to have it, so automated |
|
473 | 470 | # tools don't flag it as potential CSRF. |
|
474 | 471 | # Post is required because the payload could be bigger than the maximum |
|
475 | 472 | # allowed by GET. |
|
476 | 473 | |
|
477 | 474 | text = self.request.POST.get('text') |
|
478 | 475 | renderer = self.request.POST.get('renderer') or 'rst' |
|
479 | 476 | if text: |
|
480 | 477 | return h.render(text, renderer=renderer, mentions=True) |
|
481 | 478 | return '' |
|
482 | 479 | |
|
483 | 480 | @LoginRequired() |
|
484 | 481 | @NotAnonymous() |
|
485 | 482 | @HasRepoPermissionAnyDecorator( |
|
486 | 483 | 'repository.read', 'repository.write', 'repository.admin') |
|
487 | 484 | @CSRFRequired() |
|
488 | 485 | @view_config( |
|
489 | 486 | route_name='repo_commit_comment_delete', request_method='POST', |
|
490 | 487 | renderer='json_ext') |
|
491 | 488 | def repo_commit_comment_delete(self): |
|
492 | 489 | commit_id = self.request.matchdict['commit_id'] |
|
493 | 490 | comment_id = self.request.matchdict['comment_id'] |
|
494 | 491 | |
|
495 | 492 | comment = ChangesetComment.get_or_404(comment_id) |
|
496 | 493 | if not comment: |
|
497 | 494 | log.debug('Comment with id:%s not found, skipping', comment_id) |
|
498 | 495 | # comment already deleted in another call probably |
|
499 | 496 | return True |
|
500 | 497 | |
|
501 | 498 | is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name) |
|
502 | 499 | super_admin = h.HasPermissionAny('hg.admin')() |
|
503 | 500 | comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id) |
|
504 | 501 | is_repo_comment = comment.repo.repo_name == self.db_repo_name |
|
505 | 502 | comment_repo_admin = is_repo_admin and is_repo_comment |
|
506 | 503 | |
|
507 | 504 | if super_admin or comment_owner or comment_repo_admin: |
|
508 | 505 | CommentsModel().delete(comment=comment, user=self._rhodecode_db_user) |
|
509 | 506 | Session().commit() |
|
510 | 507 | return True |
|
511 | 508 | else: |
|
512 | 509 | log.warning('No permissions for user %s to delete comment_id: %s', |
|
513 | 510 | self._rhodecode_db_user, comment_id) |
|
514 | 511 | raise HTTPNotFound() |
|
515 | 512 | |
|
516 | 513 | @LoginRequired() |
|
517 | 514 | @HasRepoPermissionAnyDecorator( |
|
518 | 515 | 'repository.read', 'repository.write', 'repository.admin') |
|
519 | 516 | @view_config( |
|
520 | 517 | route_name='repo_commit_data', request_method='GET', |
|
521 | 518 | renderer='json_ext', xhr=True) |
|
522 | 519 | def repo_commit_data(self): |
|
523 | 520 | commit_id = self.request.matchdict['commit_id'] |
|
524 | 521 | self.load_default_context() |
|
525 | 522 | |
|
526 | 523 | try: |
|
527 | 524 | return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id) |
|
528 | 525 | except CommitDoesNotExistError as e: |
|
529 | 526 | return EmptyCommit(message=str(e)) |
|
530 | 527 | |
|
531 | 528 | @LoginRequired() |
|
532 | 529 | @HasRepoPermissionAnyDecorator( |
|
533 | 530 | 'repository.read', 'repository.write', 'repository.admin') |
|
534 | 531 | @view_config( |
|
535 | 532 | route_name='repo_commit_children', request_method='GET', |
|
536 | 533 | renderer='json_ext', xhr=True) |
|
537 | 534 | def repo_commit_children(self): |
|
538 | 535 | commit_id = self.request.matchdict['commit_id'] |
|
539 | 536 | self.load_default_context() |
|
540 | 537 | |
|
541 | 538 | commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id) |
|
542 | 539 | result = {"results": commit.children} |
|
543 | 540 | return result |
|
544 | 541 | |
|
545 | 542 | @LoginRequired() |
|
546 | 543 | @HasRepoPermissionAnyDecorator( |
|
547 | 544 | 'repository.read', 'repository.write', 'repository.admin') |
|
548 | 545 | @view_config( |
|
549 | 546 | route_name='repo_commit_parents', request_method='GET', |
|
550 | 547 | renderer='json_ext') |
|
551 | 548 | def repo_commit_parents(self): |
|
552 | 549 | commit_id = self.request.matchdict['commit_id'] |
|
553 | 550 | self.load_default_context() |
|
554 | 551 | |
|
555 | 552 | commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id) |
|
556 | 553 | result = {"results": commit.parents} |
|
557 | 554 | return result |
@@ -1,324 +1,322 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2012-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 | import logging |
|
23 | 23 | |
|
24 | 24 | from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound |
|
25 | 25 | from pyramid.view import view_config |
|
26 | 26 | from pyramid.renderers import render |
|
27 | 27 | from pyramid.response import Response |
|
28 | 28 | |
|
29 | 29 | from rhodecode.apps._base import RepoAppView |
|
30 | 30 | from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name |
|
31 | 31 | from rhodecode.lib import helpers as h |
|
32 | 32 | from rhodecode.lib import diffs, codeblocks |
|
33 | 33 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator |
|
34 | 34 | from rhodecode.lib.utils import safe_str |
|
35 | 35 | from rhodecode.lib.utils2 import safe_unicode, str2bool |
|
36 | 36 | from rhodecode.lib.vcs.exceptions import ( |
|
37 | 37 | EmptyRepositoryError, RepositoryError, RepositoryRequirementError, |
|
38 | 38 | NodeDoesNotExistError) |
|
39 | 39 | from rhodecode.model.db import Repository, ChangesetStatus |
|
40 | 40 | |
|
41 | 41 | log = logging.getLogger(__name__) |
|
42 | 42 | |
|
43 | 43 | |
|
44 | 44 | class RepoCompareView(RepoAppView): |
|
45 | 45 | def load_default_context(self): |
|
46 | 46 | c = self._get_local_tmpl_context(include_app_defaults=True) |
|
47 | 47 | |
|
48 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
49 | c.repo_info = self.db_repo | |
|
50 | 48 | c.rhodecode_repo = self.rhodecode_vcs_repo |
|
51 | 49 | |
|
52 | 50 | self._register_global_c(c) |
|
53 | 51 | return c |
|
54 | 52 | |
|
55 | 53 | def _get_commit_or_redirect( |
|
56 | 54 | self, ref, ref_type, repo, redirect_after=True, partial=False): |
|
57 | 55 | """ |
|
58 | 56 | This is a safe way to get a commit. If an error occurs it |
|
59 | 57 | redirects to a commit with a proper message. If partial is set |
|
60 | 58 | then it does not do redirect raise and throws an exception instead. |
|
61 | 59 | """ |
|
62 | 60 | _ = self.request.translate |
|
63 | 61 | try: |
|
64 | 62 | return get_commit_from_ref_name(repo, safe_str(ref), ref_type) |
|
65 | 63 | except EmptyRepositoryError: |
|
66 | 64 | if not redirect_after: |
|
67 | 65 | return repo.scm_instance().EMPTY_COMMIT |
|
68 | 66 | h.flash(h.literal(_('There are no commits yet')), |
|
69 | 67 | category='warning') |
|
70 | 68 | if not partial: |
|
71 | 69 | raise HTTPFound( |
|
72 | 70 | h.route_path('repo_summary', repo_name=repo.repo_name)) |
|
73 | 71 | raise HTTPBadRequest() |
|
74 | 72 | |
|
75 | 73 | except RepositoryError as e: |
|
76 | 74 | log.exception(safe_str(e)) |
|
77 | 75 | h.flash(safe_str(h.escape(e)), category='warning') |
|
78 | 76 | if not partial: |
|
79 | 77 | raise HTTPFound( |
|
80 | 78 | h.route_path('repo_summary', repo_name=repo.repo_name)) |
|
81 | 79 | raise HTTPBadRequest() |
|
82 | 80 | |
|
83 | 81 | @LoginRequired() |
|
84 | 82 | @HasRepoPermissionAnyDecorator( |
|
85 | 83 | 'repository.read', 'repository.write', 'repository.admin') |
|
86 | 84 | @view_config( |
|
87 | 85 | route_name='repo_compare_select', request_method='GET', |
|
88 | 86 | renderer='rhodecode:templates/compare/compare_diff.mako') |
|
89 | 87 | def compare_select(self): |
|
90 | 88 | _ = self.request.translate |
|
91 | 89 | c = self.load_default_context() |
|
92 | 90 | |
|
93 | 91 | source_repo = self.db_repo_name |
|
94 | 92 | target_repo = self.request.GET.get('target_repo', source_repo) |
|
95 | 93 | c.source_repo = Repository.get_by_repo_name(source_repo) |
|
96 | 94 | c.target_repo = Repository.get_by_repo_name(target_repo) |
|
97 | 95 | |
|
98 | 96 | if c.source_repo is None or c.target_repo is None: |
|
99 | 97 | raise HTTPNotFound() |
|
100 | 98 | |
|
101 | 99 | c.compare_home = True |
|
102 | 100 | c.commit_ranges = [] |
|
103 | 101 | c.collapse_all_commits = False |
|
104 | 102 | c.diffset = None |
|
105 | 103 | c.limited_diff = False |
|
106 | 104 | c.source_ref = c.target_ref = _('Select commit') |
|
107 | 105 | c.source_ref_type = "" |
|
108 | 106 | c.target_ref_type = "" |
|
109 | 107 | c.commit_statuses = ChangesetStatus.STATUSES |
|
110 | 108 | c.preview_mode = False |
|
111 | 109 | c.file_path = None |
|
112 | 110 | |
|
113 | 111 | return self._get_template_context(c) |
|
114 | 112 | |
|
115 | 113 | @LoginRequired() |
|
116 | 114 | @HasRepoPermissionAnyDecorator( |
|
117 | 115 | 'repository.read', 'repository.write', 'repository.admin') |
|
118 | 116 | @view_config( |
|
119 | 117 | route_name='repo_compare', request_method='GET', |
|
120 | 118 | renderer=None) |
|
121 | 119 | def compare(self): |
|
122 | 120 | _ = self.request.translate |
|
123 | 121 | c = self.load_default_context() |
|
124 | 122 | |
|
125 | 123 | source_ref_type = self.request.matchdict['source_ref_type'] |
|
126 | 124 | source_ref = self.request.matchdict['source_ref'] |
|
127 | 125 | target_ref_type = self.request.matchdict['target_ref_type'] |
|
128 | 126 | target_ref = self.request.matchdict['target_ref'] |
|
129 | 127 | |
|
130 | 128 | # source_ref will be evaluated in source_repo |
|
131 | 129 | source_repo_name = self.db_repo_name |
|
132 | 130 | source_path, source_id = parse_path_ref(source_ref) |
|
133 | 131 | |
|
134 | 132 | # target_ref will be evaluated in target_repo |
|
135 | 133 | target_repo_name = self.request.GET.get('target_repo', source_repo_name) |
|
136 | 134 | target_path, target_id = parse_path_ref( |
|
137 | 135 | target_ref, default_path=self.request.GET.get('f_path', '')) |
|
138 | 136 | |
|
139 | 137 | # if merge is True |
|
140 | 138 | # Show what changes since the shared ancestor commit of target/source |
|
141 | 139 | # the source would get if it was merged with target. Only commits |
|
142 | 140 | # which are in target but not in source will be shown. |
|
143 | 141 | merge = str2bool(self.request.GET.get('merge')) |
|
144 | 142 | # if merge is False |
|
145 | 143 | # Show a raw diff of source/target refs even if no ancestor exists |
|
146 | 144 | |
|
147 | 145 | # c.fulldiff disables cut_off_limit |
|
148 | 146 | c.fulldiff = str2bool(self.request.GET.get('fulldiff')) |
|
149 | 147 | |
|
150 | 148 | c.file_path = target_path |
|
151 | 149 | c.commit_statuses = ChangesetStatus.STATUSES |
|
152 | 150 | |
|
153 | 151 | # if partial, returns just compare_commits.html (commits log) |
|
154 | 152 | partial = self.request.is_xhr |
|
155 | 153 | |
|
156 | 154 | # swap url for compare_diff page |
|
157 | 155 | c.swap_url = h.route_path( |
|
158 | 156 | 'repo_compare', |
|
159 | 157 | repo_name=target_repo_name, |
|
160 | 158 | source_ref_type=target_ref_type, |
|
161 | 159 | source_ref=target_ref, |
|
162 | 160 | target_repo=source_repo_name, |
|
163 | 161 | target_ref_type=source_ref_type, |
|
164 | 162 | target_ref=source_ref, |
|
165 | 163 | _query=dict(merge=merge and '1' or '', f_path=target_path)) |
|
166 | 164 | |
|
167 | 165 | source_repo = Repository.get_by_repo_name(source_repo_name) |
|
168 | 166 | target_repo = Repository.get_by_repo_name(target_repo_name) |
|
169 | 167 | |
|
170 | 168 | if source_repo is None: |
|
171 | 169 | log.error('Could not find the source repo: {}' |
|
172 | 170 | .format(source_repo_name)) |
|
173 | 171 | h.flash(_('Could not find the source repo: `{}`') |
|
174 | 172 | .format(h.escape(source_repo_name)), category='error') |
|
175 | 173 | raise HTTPFound( |
|
176 | 174 | h.route_path('repo_compare_select', repo_name=self.db_repo_name)) |
|
177 | 175 | |
|
178 | 176 | if target_repo is None: |
|
179 | 177 | log.error('Could not find the target repo: {}' |
|
180 | 178 | .format(source_repo_name)) |
|
181 | 179 | h.flash(_('Could not find the target repo: `{}`') |
|
182 | 180 | .format(h.escape(target_repo_name)), category='error') |
|
183 | 181 | raise HTTPFound( |
|
184 | 182 | h.route_path('repo_compare_select', repo_name=self.db_repo_name)) |
|
185 | 183 | |
|
186 | 184 | source_scm = source_repo.scm_instance() |
|
187 | 185 | target_scm = target_repo.scm_instance() |
|
188 | 186 | |
|
189 | 187 | source_alias = source_scm.alias |
|
190 | 188 | target_alias = target_scm.alias |
|
191 | 189 | if source_alias != target_alias: |
|
192 | 190 | msg = _('The comparison of two different kinds of remote repos ' |
|
193 | 191 | 'is not available') |
|
194 | 192 | log.error(msg) |
|
195 | 193 | h.flash(msg, category='error') |
|
196 | 194 | raise HTTPFound( |
|
197 | 195 | h.route_path('repo_compare_select', repo_name=self.db_repo_name)) |
|
198 | 196 | |
|
199 | 197 | source_commit = self._get_commit_or_redirect( |
|
200 | 198 | ref=source_id, ref_type=source_ref_type, repo=source_repo, |
|
201 | 199 | partial=partial) |
|
202 | 200 | target_commit = self._get_commit_or_redirect( |
|
203 | 201 | ref=target_id, ref_type=target_ref_type, repo=target_repo, |
|
204 | 202 | partial=partial) |
|
205 | 203 | |
|
206 | 204 | c.compare_home = False |
|
207 | 205 | c.source_repo = source_repo |
|
208 | 206 | c.target_repo = target_repo |
|
209 | 207 | c.source_ref = source_ref |
|
210 | 208 | c.target_ref = target_ref |
|
211 | 209 | c.source_ref_type = source_ref_type |
|
212 | 210 | c.target_ref_type = target_ref_type |
|
213 | 211 | |
|
214 | 212 | pre_load = ["author", "branch", "date", "message"] |
|
215 | 213 | c.ancestor = None |
|
216 | 214 | |
|
217 | 215 | if c.file_path: |
|
218 | 216 | if source_commit == target_commit: |
|
219 | 217 | c.commit_ranges = [] |
|
220 | 218 | else: |
|
221 | 219 | c.commit_ranges = [target_commit] |
|
222 | 220 | else: |
|
223 | 221 | try: |
|
224 | 222 | c.commit_ranges = source_scm.compare( |
|
225 | 223 | source_commit.raw_id, target_commit.raw_id, |
|
226 | 224 | target_scm, merge, pre_load=pre_load) |
|
227 | 225 | if merge: |
|
228 | 226 | c.ancestor = source_scm.get_common_ancestor( |
|
229 | 227 | source_commit.raw_id, target_commit.raw_id, target_scm) |
|
230 | 228 | except RepositoryRequirementError: |
|
231 | 229 | msg = _('Could not compare repos with different ' |
|
232 | 230 | 'large file settings') |
|
233 | 231 | log.error(msg) |
|
234 | 232 | if partial: |
|
235 | 233 | return Response(msg) |
|
236 | 234 | h.flash(msg, category='error') |
|
237 | 235 | raise HTTPFound( |
|
238 | 236 | h.route_path('repo_compare_select', |
|
239 | 237 | repo_name=self.db_repo_name)) |
|
240 | 238 | |
|
241 | 239 | c.statuses = self.db_repo.statuses( |
|
242 | 240 | [x.raw_id for x in c.commit_ranges]) |
|
243 | 241 | |
|
244 | 242 | # auto collapse if we have more than limit |
|
245 | 243 | collapse_limit = diffs.DiffProcessor._collapse_commits_over |
|
246 | 244 | c.collapse_all_commits = len(c.commit_ranges) > collapse_limit |
|
247 | 245 | |
|
248 | 246 | if partial: # for PR ajax commits loader |
|
249 | 247 | if not c.ancestor: |
|
250 | 248 | return Response('') # cannot merge if there is no ancestor |
|
251 | 249 | |
|
252 | 250 | html = render( |
|
253 | 251 | 'rhodecode:templates/compare/compare_commits.mako', |
|
254 | 252 | self._get_template_context(c), self.request) |
|
255 | 253 | return Response(html) |
|
256 | 254 | |
|
257 | 255 | if c.ancestor: |
|
258 | 256 | # case we want a simple diff without incoming commits, |
|
259 | 257 | # previewing what will be merged. |
|
260 | 258 | # Make the diff on target repo (which is known to have target_ref) |
|
261 | 259 | log.debug('Using ancestor %s as source_ref instead of %s' |
|
262 | 260 | % (c.ancestor, source_ref)) |
|
263 | 261 | source_repo = target_repo |
|
264 | 262 | source_commit = target_repo.get_commit(commit_id=c.ancestor) |
|
265 | 263 | |
|
266 | 264 | # diff_limit will cut off the whole diff if the limit is applied |
|
267 | 265 | # otherwise it will just hide the big files from the front-end |
|
268 | 266 | diff_limit = c.visual.cut_off_limit_diff |
|
269 | 267 | file_limit = c.visual.cut_off_limit_file |
|
270 | 268 | |
|
271 | 269 | log.debug('calculating diff between ' |
|
272 | 270 | 'source_ref:%s and target_ref:%s for repo `%s`', |
|
273 | 271 | source_commit, target_commit, |
|
274 | 272 | safe_unicode(source_repo.scm_instance().path)) |
|
275 | 273 | |
|
276 | 274 | if source_commit.repository != target_commit.repository: |
|
277 | 275 | msg = _( |
|
278 | 276 | "Repositories unrelated. " |
|
279 | 277 | "Cannot compare commit %(commit1)s from repository %(repo1)s " |
|
280 | 278 | "with commit %(commit2)s from repository %(repo2)s.") % { |
|
281 | 279 | 'commit1': h.show_id(source_commit), |
|
282 | 280 | 'repo1': source_repo.repo_name, |
|
283 | 281 | 'commit2': h.show_id(target_commit), |
|
284 | 282 | 'repo2': target_repo.repo_name, |
|
285 | 283 | } |
|
286 | 284 | h.flash(msg, category='error') |
|
287 | 285 | raise HTTPFound( |
|
288 | 286 | h.route_path('repo_compare_select', |
|
289 | 287 | repo_name=self.db_repo_name)) |
|
290 | 288 | |
|
291 | 289 | txt_diff = source_repo.scm_instance().get_diff( |
|
292 | 290 | commit1=source_commit, commit2=target_commit, |
|
293 | 291 | path=target_path, path1=source_path) |
|
294 | 292 | |
|
295 | 293 | diff_processor = diffs.DiffProcessor( |
|
296 | 294 | txt_diff, format='newdiff', diff_limit=diff_limit, |
|
297 | 295 | file_limit=file_limit, show_full_diff=c.fulldiff) |
|
298 | 296 | _parsed = diff_processor.prepare() |
|
299 | 297 | |
|
300 | 298 | def _node_getter(commit): |
|
301 | 299 | """ Returns a function that returns a node for a commit or None """ |
|
302 | 300 | def get_node(fname): |
|
303 | 301 | try: |
|
304 | 302 | return commit.get_node(fname) |
|
305 | 303 | except NodeDoesNotExistError: |
|
306 | 304 | return None |
|
307 | 305 | return get_node |
|
308 | 306 | |
|
309 | 307 | diffset = codeblocks.DiffSet( |
|
310 | 308 | repo_name=source_repo.repo_name, |
|
311 | 309 | source_node_getter=_node_getter(source_commit), |
|
312 | 310 | target_node_getter=_node_getter(target_commit), |
|
313 | 311 | ) |
|
314 | 312 | c.diffset = diffset.render_patchset( |
|
315 | 313 | _parsed, source_ref, target_ref) |
|
316 | 314 | |
|
317 | 315 | c.preview_mode = merge |
|
318 | 316 | c.source_commit = source_commit |
|
319 | 317 | c.target_commit = target_commit |
|
320 | 318 | |
|
321 | 319 | html = render( |
|
322 | 320 | 'rhodecode:templates/compare/compare_diff.mako', |
|
323 | 321 | self._get_template_context(c), self.request) |
|
324 | 322 | return Response(html) No newline at end of file |
@@ -1,207 +1,204 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2017-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 pytz |
|
22 | 22 | import logging |
|
23 | 23 | |
|
24 | 24 | from beaker.cache import cache_region |
|
25 | 25 | from pyramid.view import view_config |
|
26 | 26 | from pyramid.response import Response |
|
27 | 27 | from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed |
|
28 | 28 | |
|
29 | 29 | from rhodecode.apps._base import RepoAppView |
|
30 | 30 | from rhodecode.lib import audit_logger |
|
31 | 31 | from rhodecode.lib import helpers as h |
|
32 | 32 | from rhodecode.lib.auth import ( |
|
33 | 33 | LoginRequired, HasRepoPermissionAnyDecorator) |
|
34 | 34 | from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer |
|
35 | 35 | from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe |
|
36 | 36 | from rhodecode.model.db import UserApiKeys, CacheKey |
|
37 | 37 | |
|
38 | 38 | log = logging.getLogger(__name__) |
|
39 | 39 | |
|
40 | 40 | |
|
41 | 41 | class RepoFeedView(RepoAppView): |
|
42 | 42 | def load_default_context(self): |
|
43 | 43 | c = self._get_local_tmpl_context() |
|
44 | 44 | |
|
45 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
46 | c.repo_info = self.db_repo | |
|
47 | ||
|
48 | 45 | self._register_global_c(c) |
|
49 | 46 | self._load_defaults() |
|
50 | 47 | return c |
|
51 | 48 | |
|
52 | 49 | def _get_config(self): |
|
53 | 50 | import rhodecode |
|
54 | 51 | config = rhodecode.CONFIG |
|
55 | 52 | |
|
56 | 53 | return { |
|
57 | 54 | 'language': 'en-us', |
|
58 | 55 | 'feed_ttl': '5', # TTL of feed, |
|
59 | 56 | 'feed_include_diff': |
|
60 | 57 | str2bool(config.get('rss_include_diff', False)), |
|
61 | 58 | 'feed_items_per_page': |
|
62 | 59 | safe_int(config.get('rss_items_per_page', 20)), |
|
63 | 60 | 'feed_diff_limit': |
|
64 | 61 | # we need to protect from parsing huge diffs here other way |
|
65 | 62 | # we can kill the server |
|
66 | 63 | safe_int(config.get('rss_cut_off_limit', 32 * 1024)), |
|
67 | 64 | } |
|
68 | 65 | |
|
69 | 66 | def _load_defaults(self): |
|
70 | 67 | _ = self.request.translate |
|
71 | 68 | config = self._get_config() |
|
72 | 69 | # common values for feeds |
|
73 | 70 | self.description = _('Changes on %s repository') |
|
74 | 71 | self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s') |
|
75 | 72 | self.language = config["language"] |
|
76 | 73 | self.ttl = config["feed_ttl"] |
|
77 | 74 | self.feed_include_diff = config['feed_include_diff'] |
|
78 | 75 | self.feed_diff_limit = config['feed_diff_limit'] |
|
79 | 76 | self.feed_items_per_page = config['feed_items_per_page'] |
|
80 | 77 | |
|
81 | 78 | def _changes(self, commit): |
|
82 | 79 | diff_processor = DiffProcessor( |
|
83 | 80 | commit.diff(), diff_limit=self.feed_diff_limit) |
|
84 | 81 | _parsed = diff_processor.prepare(inline_diff=False) |
|
85 | 82 | limited_diff = isinstance(_parsed, LimitedDiffContainer) |
|
86 | 83 | |
|
87 | 84 | return _parsed, limited_diff |
|
88 | 85 | |
|
89 | 86 | def _get_title(self, commit): |
|
90 | 87 | return h.shorter(commit.message, 160) |
|
91 | 88 | |
|
92 | 89 | def _get_description(self, commit): |
|
93 | 90 | _renderer = self.request.get_partial_renderer( |
|
94 | 91 | 'feed/atom_feed_entry.mako') |
|
95 | 92 | parsed_diff, limited_diff = self._changes(commit) |
|
96 | 93 | return _renderer( |
|
97 | 94 | 'body', |
|
98 | 95 | commit=commit, |
|
99 | 96 | parsed_diff=parsed_diff, |
|
100 | 97 | limited_diff=limited_diff, |
|
101 | 98 | feed_include_diff=self.feed_include_diff, |
|
102 | 99 | ) |
|
103 | 100 | |
|
104 | 101 | def _set_timezone(self, date, tzinfo=pytz.utc): |
|
105 | 102 | if not getattr(date, "tzinfo", None): |
|
106 | 103 | date.replace(tzinfo=tzinfo) |
|
107 | 104 | return date |
|
108 | 105 | |
|
109 | 106 | def _get_commits(self): |
|
110 | 107 | return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:]) |
|
111 | 108 | |
|
112 | 109 | def uid(self, repo_id, commit_id): |
|
113 | 110 | return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id)) |
|
114 | 111 | |
|
115 | 112 | @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED]) |
|
116 | 113 | @HasRepoPermissionAnyDecorator( |
|
117 | 114 | 'repository.read', 'repository.write', 'repository.admin') |
|
118 | 115 | @view_config( |
|
119 | 116 | route_name='atom_feed_home', request_method='GET', |
|
120 | 117 | renderer=None) |
|
121 | 118 | def atom(self): |
|
122 | 119 | """ |
|
123 | 120 | Produce an atom-1.0 feed via feedgenerator module |
|
124 | 121 | """ |
|
125 | 122 | self.load_default_context() |
|
126 | 123 | |
|
127 | 124 | @cache_region('long_term') |
|
128 | 125 | def _generate_feed(cache_key): |
|
129 | 126 | feed = Atom1Feed( |
|
130 | 127 | title=self.title % self.db_repo_name, |
|
131 | 128 | link=h.route_url('repo_summary', repo_name=self.db_repo_name), |
|
132 | 129 | description=self.description % self.db_repo_name, |
|
133 | 130 | language=self.language, |
|
134 | 131 | ttl=self.ttl |
|
135 | 132 | ) |
|
136 | 133 | |
|
137 | 134 | for commit in reversed(self._get_commits()): |
|
138 | 135 | date = self._set_timezone(commit.date) |
|
139 | 136 | feed.add_item( |
|
140 | 137 | unique_id=self.uid(self.db_repo.repo_id, commit.raw_id), |
|
141 | 138 | title=self._get_title(commit), |
|
142 | 139 | author_name=commit.author, |
|
143 | 140 | description=self._get_description(commit), |
|
144 | 141 | link=h.route_url( |
|
145 | 142 | 'repo_commit', repo_name=self.db_repo_name, |
|
146 | 143 | commit_id=commit.raw_id), |
|
147 | 144 | pubdate=date,) |
|
148 | 145 | |
|
149 | 146 | return feed.mime_type, feed.writeString('utf-8') |
|
150 | 147 | |
|
151 | 148 | invalidator_context = CacheKey.repo_context_cache( |
|
152 | 149 | _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM) |
|
153 | 150 | |
|
154 | 151 | with invalidator_context as context: |
|
155 | 152 | context.invalidate() |
|
156 | 153 | mime_type, feed = context.compute() |
|
157 | 154 | |
|
158 | 155 | response = Response(feed) |
|
159 | 156 | response.content_type = mime_type |
|
160 | 157 | return response |
|
161 | 158 | |
|
162 | 159 | @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED]) |
|
163 | 160 | @HasRepoPermissionAnyDecorator( |
|
164 | 161 | 'repository.read', 'repository.write', 'repository.admin') |
|
165 | 162 | @view_config( |
|
166 | 163 | route_name='rss_feed_home', request_method='GET', |
|
167 | 164 | renderer=None) |
|
168 | 165 | def rss(self): |
|
169 | 166 | """ |
|
170 | 167 | Produce an rss2 feed via feedgenerator module |
|
171 | 168 | """ |
|
172 | 169 | self.load_default_context() |
|
173 | 170 | |
|
174 | 171 | @cache_region('long_term') |
|
175 | 172 | def _generate_feed(cache_key): |
|
176 | 173 | feed = Rss201rev2Feed( |
|
177 | 174 | title=self.title % self.db_repo_name, |
|
178 | 175 | link=h.route_url('repo_summary', repo_name=self.db_repo_name), |
|
179 | 176 | description=self.description % self.db_repo_name, |
|
180 | 177 | language=self.language, |
|
181 | 178 | ttl=self.ttl |
|
182 | 179 | ) |
|
183 | 180 | |
|
184 | 181 | for commit in reversed(self._get_commits()): |
|
185 | 182 | date = self._set_timezone(commit.date) |
|
186 | 183 | feed.add_item( |
|
187 | 184 | unique_id=self.uid(self.db_repo.repo_id, commit.raw_id), |
|
188 | 185 | title=self._get_title(commit), |
|
189 | 186 | author_name=commit.author, |
|
190 | 187 | description=self._get_description(commit), |
|
191 | 188 | link=h.route_url( |
|
192 | 189 | 'repo_commit', repo_name=self.db_repo_name, |
|
193 | 190 | commit_id=commit.raw_id), |
|
194 | 191 | pubdate=date,) |
|
195 | 192 | |
|
196 | 193 | return feed.mime_type, feed.writeString('utf-8') |
|
197 | 194 | |
|
198 | 195 | invalidator_context = CacheKey.repo_context_cache( |
|
199 | 196 | _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_RSS) |
|
200 | 197 | |
|
201 | 198 | with invalidator_context as context: |
|
202 | 199 | context.invalidate() |
|
203 | 200 | mime_type, feed = context.compute() |
|
204 | 201 | |
|
205 | 202 | response = Response(feed) |
|
206 | 203 | response.content_type = mime_type |
|
207 | 204 | return response |
@@ -1,1284 +1,1282 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 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
85 | c.repo_info = self.db_repo | |
|
86 | 84 | c.rhodecode_repo = self.rhodecode_vcs_repo |
|
87 | 85 | |
|
88 | 86 | self._register_global_c(c) |
|
89 | 87 | return c |
|
90 | 88 | |
|
91 | 89 | def _ensure_not_locked(self): |
|
92 | 90 | _ = self.request.translate |
|
93 | 91 | |
|
94 | 92 | repo = self.db_repo |
|
95 | 93 | if repo.enable_locking and repo.locked[0]: |
|
96 | 94 | h.flash(_('This repository has been locked by %s on %s') |
|
97 | 95 | % (h.person_by_id(repo.locked[0]), |
|
98 | 96 | h.format_date(h.time_to_datetime(repo.locked[1]))), |
|
99 | 97 | 'warning') |
|
100 | 98 | files_url = h.route_path( |
|
101 | 99 | 'repo_files:default_path', |
|
102 | 100 | repo_name=self.db_repo_name, commit_id='tip') |
|
103 | 101 | raise HTTPFound(files_url) |
|
104 | 102 | |
|
105 | 103 | def _get_commit_and_path(self): |
|
106 | 104 | default_commit_id = self.db_repo.landing_rev[1] |
|
107 | 105 | default_f_path = '/' |
|
108 | 106 | |
|
109 | 107 | commit_id = self.request.matchdict.get( |
|
110 | 108 | 'commit_id', default_commit_id) |
|
111 | 109 | f_path = self._get_f_path(self.request.matchdict, default_f_path) |
|
112 | 110 | return commit_id, f_path |
|
113 | 111 | |
|
114 | 112 | def _get_default_encoding(self, c): |
|
115 | 113 | enc_list = getattr(c, 'default_encodings', []) |
|
116 | 114 | return enc_list[0] if enc_list else 'UTF-8' |
|
117 | 115 | |
|
118 | 116 | def _get_commit_or_redirect(self, commit_id, redirect_after=True): |
|
119 | 117 | """ |
|
120 | 118 | This is a safe way to get commit. If an error occurs it redirects to |
|
121 | 119 | tip with proper message |
|
122 | 120 | |
|
123 | 121 | :param commit_id: id of commit to fetch |
|
124 | 122 | :param redirect_after: toggle redirection |
|
125 | 123 | """ |
|
126 | 124 | _ = self.request.translate |
|
127 | 125 | |
|
128 | 126 | try: |
|
129 | 127 | return self.rhodecode_vcs_repo.get_commit(commit_id) |
|
130 | 128 | except EmptyRepositoryError: |
|
131 | 129 | if not redirect_after: |
|
132 | 130 | return None |
|
133 | 131 | |
|
134 | 132 | _url = h.route_path( |
|
135 | 133 | 'repo_files_add_file', |
|
136 | 134 | repo_name=self.db_repo_name, commit_id=0, f_path='', |
|
137 | 135 | _anchor='edit') |
|
138 | 136 | |
|
139 | 137 | if h.HasRepoPermissionAny( |
|
140 | 138 | 'repository.write', 'repository.admin')(self.db_repo_name): |
|
141 | 139 | add_new = h.link_to( |
|
142 | 140 | _('Click here to add a new file.'), _url, class_="alert-link") |
|
143 | 141 | else: |
|
144 | 142 | add_new = "" |
|
145 | 143 | |
|
146 | 144 | h.flash(h.literal( |
|
147 | 145 | _('There are no files yet. %s') % add_new), category='warning') |
|
148 | 146 | raise HTTPFound( |
|
149 | 147 | h.route_path('repo_summary', repo_name=self.db_repo_name)) |
|
150 | 148 | |
|
151 | 149 | except (CommitDoesNotExistError, LookupError): |
|
152 | 150 | msg = _('No such commit exists for this repository') |
|
153 | 151 | h.flash(msg, category='error') |
|
154 | 152 | raise HTTPNotFound() |
|
155 | 153 | except RepositoryError as e: |
|
156 | 154 | h.flash(safe_str(h.escape(e)), category='error') |
|
157 | 155 | raise HTTPNotFound() |
|
158 | 156 | |
|
159 | 157 | def _get_filenode_or_redirect(self, commit_obj, path): |
|
160 | 158 | """ |
|
161 | 159 | Returns file_node, if error occurs or given path is directory, |
|
162 | 160 | it'll redirect to top level path |
|
163 | 161 | """ |
|
164 | 162 | _ = self.request.translate |
|
165 | 163 | |
|
166 | 164 | try: |
|
167 | 165 | file_node = commit_obj.get_node(path) |
|
168 | 166 | if file_node.is_dir(): |
|
169 | 167 | raise RepositoryError('The given path is a directory') |
|
170 | 168 | except CommitDoesNotExistError: |
|
171 | 169 | log.exception('No such commit exists for this repository') |
|
172 | 170 | h.flash(_('No such commit exists for this repository'), category='error') |
|
173 | 171 | raise HTTPNotFound() |
|
174 | 172 | except RepositoryError as e: |
|
175 | 173 | log.warning('Repository error while fetching ' |
|
176 | 174 | 'filenode `%s`. Err:%s', path, e) |
|
177 | 175 | h.flash(safe_str(h.escape(e)), category='error') |
|
178 | 176 | raise HTTPNotFound() |
|
179 | 177 | |
|
180 | 178 | return file_node |
|
181 | 179 | |
|
182 | 180 | def _is_valid_head(self, commit_id, repo): |
|
183 | 181 | # check if commit is a branch identifier- basically we cannot |
|
184 | 182 | # create multiple heads via file editing |
|
185 | 183 | valid_heads = repo.branches.keys() + repo.branches.values() |
|
186 | 184 | |
|
187 | 185 | if h.is_svn(repo) and not repo.is_empty(): |
|
188 | 186 | # Note: Subversion only has one head, we add it here in case there |
|
189 | 187 | # is no branch matched. |
|
190 | 188 | valid_heads.append(repo.get_commit(commit_idx=-1).raw_id) |
|
191 | 189 | |
|
192 | 190 | # check if commit is a branch name or branch hash |
|
193 | 191 | return commit_id in valid_heads |
|
194 | 192 | |
|
195 | 193 | def _get_tree_cache_manager(self, namespace_type): |
|
196 | 194 | _namespace = caches.get_repo_namespace_key( |
|
197 | 195 | namespace_type, self.db_repo_name) |
|
198 | 196 | return caches.get_cache_manager('repo_cache_long', _namespace) |
|
199 | 197 | |
|
200 | 198 | def _get_tree_at_commit( |
|
201 | 199 | self, c, commit_id, f_path, full_load=False, force=False): |
|
202 | 200 | def _cached_tree(): |
|
203 | 201 | log.debug('Generating cached file tree for %s, %s, %s', |
|
204 | 202 | self.db_repo_name, commit_id, f_path) |
|
205 | 203 | |
|
206 | 204 | c.full_load = full_load |
|
207 | 205 | return render( |
|
208 | 206 | 'rhodecode:templates/files/files_browser_tree.mako', |
|
209 | 207 | self._get_template_context(c), self.request) |
|
210 | 208 | |
|
211 | 209 | cache_manager = self._get_tree_cache_manager(caches.FILE_TREE) |
|
212 | 210 | |
|
213 | 211 | cache_key = caches.compute_key_from_params( |
|
214 | 212 | self.db_repo_name, commit_id, f_path) |
|
215 | 213 | |
|
216 | 214 | if force: |
|
217 | 215 | # we want to force recompute of caches |
|
218 | 216 | cache_manager.remove_value(cache_key) |
|
219 | 217 | |
|
220 | 218 | return cache_manager.get(cache_key, createfunc=_cached_tree) |
|
221 | 219 | |
|
222 | 220 | def _get_archive_spec(self, fname): |
|
223 | 221 | log.debug('Detecting archive spec for: `%s`', fname) |
|
224 | 222 | |
|
225 | 223 | fileformat = None |
|
226 | 224 | ext = None |
|
227 | 225 | content_type = None |
|
228 | 226 | for a_type, ext_data in settings.ARCHIVE_SPECS.items(): |
|
229 | 227 | content_type, extension = ext_data |
|
230 | 228 | |
|
231 | 229 | if fname.endswith(extension): |
|
232 | 230 | fileformat = a_type |
|
233 | 231 | log.debug('archive is of type: %s', fileformat) |
|
234 | 232 | ext = extension |
|
235 | 233 | break |
|
236 | 234 | |
|
237 | 235 | if not fileformat: |
|
238 | 236 | raise ValueError() |
|
239 | 237 | |
|
240 | 238 | # left over part of whole fname is the commit |
|
241 | 239 | commit_id = fname[:-len(ext)] |
|
242 | 240 | |
|
243 | 241 | return commit_id, ext, fileformat, content_type |
|
244 | 242 | |
|
245 | 243 | @LoginRequired() |
|
246 | 244 | @HasRepoPermissionAnyDecorator( |
|
247 | 245 | 'repository.read', 'repository.write', 'repository.admin') |
|
248 | 246 | @view_config( |
|
249 | 247 | route_name='repo_archivefile', request_method='GET', |
|
250 | 248 | renderer=None) |
|
251 | 249 | def repo_archivefile(self): |
|
252 | 250 | # archive cache config |
|
253 | 251 | from rhodecode import CONFIG |
|
254 | 252 | _ = self.request.translate |
|
255 | 253 | self.load_default_context() |
|
256 | 254 | |
|
257 | 255 | fname = self.request.matchdict['fname'] |
|
258 | 256 | subrepos = self.request.GET.get('subrepos') == 'true' |
|
259 | 257 | |
|
260 | 258 | if not self.db_repo.enable_downloads: |
|
261 | 259 | return Response(_('Downloads disabled')) |
|
262 | 260 | |
|
263 | 261 | try: |
|
264 | 262 | commit_id, ext, fileformat, content_type = \ |
|
265 | 263 | self._get_archive_spec(fname) |
|
266 | 264 | except ValueError: |
|
267 | 265 | return Response(_('Unknown archive type for: `{}`').format(fname)) |
|
268 | 266 | |
|
269 | 267 | try: |
|
270 | 268 | commit = self.rhodecode_vcs_repo.get_commit(commit_id) |
|
271 | 269 | except CommitDoesNotExistError: |
|
272 | 270 | return Response(_('Unknown commit_id %s') % commit_id) |
|
273 | 271 | except EmptyRepositoryError: |
|
274 | 272 | return Response(_('Empty repository')) |
|
275 | 273 | |
|
276 | 274 | archive_name = '%s-%s%s%s' % ( |
|
277 | 275 | safe_str(self.db_repo_name.replace('/', '_')), |
|
278 | 276 | '-sub' if subrepos else '', |
|
279 | 277 | safe_str(commit.short_id), ext) |
|
280 | 278 | |
|
281 | 279 | use_cached_archive = False |
|
282 | 280 | archive_cache_enabled = CONFIG.get( |
|
283 | 281 | 'archive_cache_dir') and not self.request.GET.get('no_cache') |
|
284 | 282 | |
|
285 | 283 | if archive_cache_enabled: |
|
286 | 284 | # check if we it's ok to write |
|
287 | 285 | if not os.path.isdir(CONFIG['archive_cache_dir']): |
|
288 | 286 | os.makedirs(CONFIG['archive_cache_dir']) |
|
289 | 287 | cached_archive_path = os.path.join( |
|
290 | 288 | CONFIG['archive_cache_dir'], archive_name) |
|
291 | 289 | if os.path.isfile(cached_archive_path): |
|
292 | 290 | log.debug('Found cached archive in %s', cached_archive_path) |
|
293 | 291 | fd, archive = None, cached_archive_path |
|
294 | 292 | use_cached_archive = True |
|
295 | 293 | else: |
|
296 | 294 | log.debug('Archive %s is not yet cached', archive_name) |
|
297 | 295 | |
|
298 | 296 | if not use_cached_archive: |
|
299 | 297 | # generate new archive |
|
300 | 298 | fd, archive = tempfile.mkstemp() |
|
301 | 299 | log.debug('Creating new temp archive in %s', archive) |
|
302 | 300 | try: |
|
303 | 301 | commit.archive_repo(archive, kind=fileformat, subrepos=subrepos) |
|
304 | 302 | except ImproperArchiveTypeError: |
|
305 | 303 | return _('Unknown archive type') |
|
306 | 304 | if archive_cache_enabled: |
|
307 | 305 | # if we generated the archive and we have cache enabled |
|
308 | 306 | # let's use this for future |
|
309 | 307 | log.debug('Storing new archive in %s', cached_archive_path) |
|
310 | 308 | shutil.move(archive, cached_archive_path) |
|
311 | 309 | archive = cached_archive_path |
|
312 | 310 | |
|
313 | 311 | # store download action |
|
314 | 312 | audit_logger.store_web( |
|
315 | 313 | 'repo.archive.download', action_data={ |
|
316 | 314 | 'user_agent': self.request.user_agent, |
|
317 | 315 | 'archive_name': archive_name, |
|
318 | 316 | 'archive_spec': fname, |
|
319 | 317 | 'archive_cached': use_cached_archive}, |
|
320 | 318 | user=self._rhodecode_user, |
|
321 | 319 | repo=self.db_repo, |
|
322 | 320 | commit=True |
|
323 | 321 | ) |
|
324 | 322 | |
|
325 | 323 | def get_chunked_archive(archive): |
|
326 | 324 | with open(archive, 'rb') as stream: |
|
327 | 325 | while True: |
|
328 | 326 | data = stream.read(16 * 1024) |
|
329 | 327 | if not data: |
|
330 | 328 | if fd: # fd means we used temporary file |
|
331 | 329 | os.close(fd) |
|
332 | 330 | if not archive_cache_enabled: |
|
333 | 331 | log.debug('Destroying temp archive %s', archive) |
|
334 | 332 | os.remove(archive) |
|
335 | 333 | break |
|
336 | 334 | yield data |
|
337 | 335 | |
|
338 | 336 | response = Response(app_iter=get_chunked_archive(archive)) |
|
339 | 337 | response.content_disposition = str( |
|
340 | 338 | 'attachment; filename=%s' % archive_name) |
|
341 | 339 | response.content_type = str(content_type) |
|
342 | 340 | |
|
343 | 341 | return response |
|
344 | 342 | |
|
345 | 343 | def _get_file_node(self, commit_id, f_path): |
|
346 | 344 | if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]: |
|
347 | 345 | commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id) |
|
348 | 346 | try: |
|
349 | 347 | node = commit.get_node(f_path) |
|
350 | 348 | if node.is_dir(): |
|
351 | 349 | raise NodeError('%s path is a %s not a file' |
|
352 | 350 | % (node, type(node))) |
|
353 | 351 | except NodeDoesNotExistError: |
|
354 | 352 | commit = EmptyCommit( |
|
355 | 353 | commit_id=commit_id, |
|
356 | 354 | idx=commit.idx, |
|
357 | 355 | repo=commit.repository, |
|
358 | 356 | alias=commit.repository.alias, |
|
359 | 357 | message=commit.message, |
|
360 | 358 | author=commit.author, |
|
361 | 359 | date=commit.date) |
|
362 | 360 | node = FileNode(f_path, '', commit=commit) |
|
363 | 361 | else: |
|
364 | 362 | commit = EmptyCommit( |
|
365 | 363 | repo=self.rhodecode_vcs_repo, |
|
366 | 364 | alias=self.rhodecode_vcs_repo.alias) |
|
367 | 365 | node = FileNode(f_path, '', commit=commit) |
|
368 | 366 | return node |
|
369 | 367 | |
|
370 | 368 | @LoginRequired() |
|
371 | 369 | @HasRepoPermissionAnyDecorator( |
|
372 | 370 | 'repository.read', 'repository.write', 'repository.admin') |
|
373 | 371 | @view_config( |
|
374 | 372 | route_name='repo_files_diff', request_method='GET', |
|
375 | 373 | renderer=None) |
|
376 | 374 | def repo_files_diff(self): |
|
377 | 375 | c = self.load_default_context() |
|
378 | 376 | f_path = self._get_f_path(self.request.matchdict) |
|
379 | 377 | diff1 = self.request.GET.get('diff1', '') |
|
380 | 378 | diff2 = self.request.GET.get('diff2', '') |
|
381 | 379 | |
|
382 | 380 | path1, diff1 = parse_path_ref(diff1, default_path=f_path) |
|
383 | 381 | |
|
384 | 382 | ignore_whitespace = str2bool(self.request.GET.get('ignorews')) |
|
385 | 383 | line_context = self.request.GET.get('context', 3) |
|
386 | 384 | |
|
387 | 385 | if not any((diff1, diff2)): |
|
388 | 386 | h.flash( |
|
389 | 387 | 'Need query parameter "diff1" or "diff2" to generate a diff.', |
|
390 | 388 | category='error') |
|
391 | 389 | raise HTTPBadRequest() |
|
392 | 390 | |
|
393 | 391 | c.action = self.request.GET.get('diff') |
|
394 | 392 | if c.action not in ['download', 'raw']: |
|
395 | 393 | compare_url = h.route_path( |
|
396 | 394 | 'repo_compare', |
|
397 | 395 | repo_name=self.db_repo_name, |
|
398 | 396 | source_ref_type='rev', |
|
399 | 397 | source_ref=diff1, |
|
400 | 398 | target_repo=self.db_repo_name, |
|
401 | 399 | target_ref_type='rev', |
|
402 | 400 | target_ref=diff2, |
|
403 | 401 | _query=dict(f_path=f_path)) |
|
404 | 402 | # redirect to new view if we render diff |
|
405 | 403 | raise HTTPFound(compare_url) |
|
406 | 404 | |
|
407 | 405 | try: |
|
408 | 406 | node1 = self._get_file_node(diff1, path1) |
|
409 | 407 | node2 = self._get_file_node(diff2, f_path) |
|
410 | 408 | except (RepositoryError, NodeError): |
|
411 | 409 | log.exception("Exception while trying to get node from repository") |
|
412 | 410 | raise HTTPFound( |
|
413 | 411 | h.route_path('repo_files', repo_name=self.db_repo_name, |
|
414 | 412 | commit_id='tip', f_path=f_path)) |
|
415 | 413 | |
|
416 | 414 | if all(isinstance(node.commit, EmptyCommit) |
|
417 | 415 | for node in (node1, node2)): |
|
418 | 416 | raise HTTPNotFound() |
|
419 | 417 | |
|
420 | 418 | c.commit_1 = node1.commit |
|
421 | 419 | c.commit_2 = node2.commit |
|
422 | 420 | |
|
423 | 421 | if c.action == 'download': |
|
424 | 422 | _diff = diffs.get_gitdiff(node1, node2, |
|
425 | 423 | ignore_whitespace=ignore_whitespace, |
|
426 | 424 | context=line_context) |
|
427 | 425 | diff = diffs.DiffProcessor(_diff, format='gitdiff') |
|
428 | 426 | |
|
429 | 427 | response = Response(diff.as_raw()) |
|
430 | 428 | response.content_type = 'text/plain' |
|
431 | 429 | response.content_disposition = ( |
|
432 | 430 | 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2) |
|
433 | 431 | ) |
|
434 | 432 | charset = self._get_default_encoding(c) |
|
435 | 433 | if charset: |
|
436 | 434 | response.charset = charset |
|
437 | 435 | return response |
|
438 | 436 | |
|
439 | 437 | elif c.action == 'raw': |
|
440 | 438 | _diff = diffs.get_gitdiff(node1, node2, |
|
441 | 439 | ignore_whitespace=ignore_whitespace, |
|
442 | 440 | context=line_context) |
|
443 | 441 | diff = diffs.DiffProcessor(_diff, format='gitdiff') |
|
444 | 442 | |
|
445 | 443 | response = Response(diff.as_raw()) |
|
446 | 444 | response.content_type = 'text/plain' |
|
447 | 445 | charset = self._get_default_encoding(c) |
|
448 | 446 | if charset: |
|
449 | 447 | response.charset = charset |
|
450 | 448 | return response |
|
451 | 449 | |
|
452 | 450 | # in case we ever end up here |
|
453 | 451 | raise HTTPNotFound() |
|
454 | 452 | |
|
455 | 453 | @LoginRequired() |
|
456 | 454 | @HasRepoPermissionAnyDecorator( |
|
457 | 455 | 'repository.read', 'repository.write', 'repository.admin') |
|
458 | 456 | @view_config( |
|
459 | 457 | route_name='repo_files_diff_2way_redirect', request_method='GET', |
|
460 | 458 | renderer=None) |
|
461 | 459 | def repo_files_diff_2way_redirect(self): |
|
462 | 460 | """ |
|
463 | 461 | Kept only to make OLD links work |
|
464 | 462 | """ |
|
465 | 463 | f_path = self._get_f_path(self.request.matchdict) |
|
466 | 464 | diff1 = self.request.GET.get('diff1', '') |
|
467 | 465 | diff2 = self.request.GET.get('diff2', '') |
|
468 | 466 | |
|
469 | 467 | if not any((diff1, diff2)): |
|
470 | 468 | h.flash( |
|
471 | 469 | 'Need query parameter "diff1" or "diff2" to generate a diff.', |
|
472 | 470 | category='error') |
|
473 | 471 | raise HTTPBadRequest() |
|
474 | 472 | |
|
475 | 473 | compare_url = h.route_path( |
|
476 | 474 | 'repo_compare', |
|
477 | 475 | repo_name=self.db_repo_name, |
|
478 | 476 | source_ref_type='rev', |
|
479 | 477 | source_ref=diff1, |
|
480 | 478 | target_ref_type='rev', |
|
481 | 479 | target_ref=diff2, |
|
482 | 480 | _query=dict(f_path=f_path, diffmode='sideside', |
|
483 | 481 | target_repo=self.db_repo_name,)) |
|
484 | 482 | raise HTTPFound(compare_url) |
|
485 | 483 | |
|
486 | 484 | @LoginRequired() |
|
487 | 485 | @HasRepoPermissionAnyDecorator( |
|
488 | 486 | 'repository.read', 'repository.write', 'repository.admin') |
|
489 | 487 | @view_config( |
|
490 | 488 | route_name='repo_files', request_method='GET', |
|
491 | 489 | renderer=None) |
|
492 | 490 | @view_config( |
|
493 | 491 | route_name='repo_files:default_path', request_method='GET', |
|
494 | 492 | renderer=None) |
|
495 | 493 | @view_config( |
|
496 | 494 | route_name='repo_files:default_commit', request_method='GET', |
|
497 | 495 | renderer=None) |
|
498 | 496 | @view_config( |
|
499 | 497 | route_name='repo_files:rendered', request_method='GET', |
|
500 | 498 | renderer=None) |
|
501 | 499 | @view_config( |
|
502 | 500 | route_name='repo_files:annotated', request_method='GET', |
|
503 | 501 | renderer=None) |
|
504 | 502 | def repo_files(self): |
|
505 | 503 | c = self.load_default_context() |
|
506 | 504 | |
|
507 | 505 | view_name = getattr(self.request.matched_route, 'name', None) |
|
508 | 506 | |
|
509 | 507 | c.annotate = view_name == 'repo_files:annotated' |
|
510 | 508 | # default is false, but .rst/.md files later are auto rendered, we can |
|
511 | 509 | # overwrite auto rendering by setting this GET flag |
|
512 | 510 | c.renderer = view_name == 'repo_files:rendered' or \ |
|
513 | 511 | not self.request.GET.get('no-render', False) |
|
514 | 512 | |
|
515 | 513 | # redirect to given commit_id from form if given |
|
516 | 514 | get_commit_id = self.request.GET.get('at_rev', None) |
|
517 | 515 | if get_commit_id: |
|
518 | 516 | self._get_commit_or_redirect(get_commit_id) |
|
519 | 517 | |
|
520 | 518 | commit_id, f_path = self._get_commit_and_path() |
|
521 | 519 | c.commit = self._get_commit_or_redirect(commit_id) |
|
522 | 520 | c.branch = self.request.GET.get('branch', None) |
|
523 | 521 | c.f_path = f_path |
|
524 | 522 | |
|
525 | 523 | # prev link |
|
526 | 524 | try: |
|
527 | 525 | prev_commit = c.commit.prev(c.branch) |
|
528 | 526 | c.prev_commit = prev_commit |
|
529 | 527 | c.url_prev = h.route_path( |
|
530 | 528 | 'repo_files', repo_name=self.db_repo_name, |
|
531 | 529 | commit_id=prev_commit.raw_id, f_path=f_path) |
|
532 | 530 | if c.branch: |
|
533 | 531 | c.url_prev += '?branch=%s' % c.branch |
|
534 | 532 | except (CommitDoesNotExistError, VCSError): |
|
535 | 533 | c.url_prev = '#' |
|
536 | 534 | c.prev_commit = EmptyCommit() |
|
537 | 535 | |
|
538 | 536 | # next link |
|
539 | 537 | try: |
|
540 | 538 | next_commit = c.commit.next(c.branch) |
|
541 | 539 | c.next_commit = next_commit |
|
542 | 540 | c.url_next = h.route_path( |
|
543 | 541 | 'repo_files', repo_name=self.db_repo_name, |
|
544 | 542 | commit_id=next_commit.raw_id, f_path=f_path) |
|
545 | 543 | if c.branch: |
|
546 | 544 | c.url_next += '?branch=%s' % c.branch |
|
547 | 545 | except (CommitDoesNotExistError, VCSError): |
|
548 | 546 | c.url_next = '#' |
|
549 | 547 | c.next_commit = EmptyCommit() |
|
550 | 548 | |
|
551 | 549 | # files or dirs |
|
552 | 550 | try: |
|
553 | 551 | c.file = c.commit.get_node(f_path) |
|
554 | 552 | c.file_author = True |
|
555 | 553 | c.file_tree = '' |
|
556 | 554 | |
|
557 | 555 | # load file content |
|
558 | 556 | if c.file.is_file(): |
|
559 | 557 | c.lf_node = c.file.get_largefile_node() |
|
560 | 558 | |
|
561 | 559 | c.file_source_page = 'true' |
|
562 | 560 | c.file_last_commit = c.file.last_commit |
|
563 | 561 | if c.file.size < c.visual.cut_off_limit_diff: |
|
564 | 562 | if c.annotate: # annotation has precedence over renderer |
|
565 | 563 | c.annotated_lines = filenode_as_annotated_lines_tokens( |
|
566 | 564 | c.file |
|
567 | 565 | ) |
|
568 | 566 | else: |
|
569 | 567 | c.renderer = ( |
|
570 | 568 | c.renderer and h.renderer_from_filename(c.file.path) |
|
571 | 569 | ) |
|
572 | 570 | if not c.renderer: |
|
573 | 571 | c.lines = filenode_as_lines_tokens(c.file) |
|
574 | 572 | |
|
575 | 573 | c.on_branch_head = self._is_valid_head( |
|
576 | 574 | commit_id, self.rhodecode_vcs_repo) |
|
577 | 575 | |
|
578 | 576 | branch = c.commit.branch if ( |
|
579 | 577 | c.commit.branch and '/' not in c.commit.branch) else None |
|
580 | 578 | c.branch_or_raw_id = branch or c.commit.raw_id |
|
581 | 579 | c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id) |
|
582 | 580 | |
|
583 | 581 | author = c.file_last_commit.author |
|
584 | 582 | c.authors = [[ |
|
585 | 583 | h.email(author), |
|
586 | 584 | h.person(author, 'username_or_name_or_email'), |
|
587 | 585 | 1 |
|
588 | 586 | ]] |
|
589 | 587 | |
|
590 | 588 | else: # load tree content at path |
|
591 | 589 | c.file_source_page = 'false' |
|
592 | 590 | c.authors = [] |
|
593 | 591 | # this loads a simple tree without metadata to speed things up |
|
594 | 592 | # later via ajax we call repo_nodetree_full and fetch whole |
|
595 | 593 | c.file_tree = self._get_tree_at_commit( |
|
596 | 594 | c, c.commit.raw_id, f_path) |
|
597 | 595 | |
|
598 | 596 | except RepositoryError as e: |
|
599 | 597 | h.flash(safe_str(h.escape(e)), category='error') |
|
600 | 598 | raise HTTPNotFound() |
|
601 | 599 | |
|
602 | 600 | if self.request.environ.get('HTTP_X_PJAX'): |
|
603 | 601 | html = render('rhodecode:templates/files/files_pjax.mako', |
|
604 | 602 | self._get_template_context(c), self.request) |
|
605 | 603 | else: |
|
606 | 604 | html = render('rhodecode:templates/files/files.mako', |
|
607 | 605 | self._get_template_context(c), self.request) |
|
608 | 606 | return Response(html) |
|
609 | 607 | |
|
610 | 608 | @HasRepoPermissionAnyDecorator( |
|
611 | 609 | 'repository.read', 'repository.write', 'repository.admin') |
|
612 | 610 | @view_config( |
|
613 | 611 | route_name='repo_files:annotated_previous', request_method='GET', |
|
614 | 612 | renderer=None) |
|
615 | 613 | def repo_files_annotated_previous(self): |
|
616 | 614 | self.load_default_context() |
|
617 | 615 | |
|
618 | 616 | commit_id, f_path = self._get_commit_and_path() |
|
619 | 617 | commit = self._get_commit_or_redirect(commit_id) |
|
620 | 618 | prev_commit_id = commit.raw_id |
|
621 | 619 | line_anchor = self.request.GET.get('line_anchor') |
|
622 | 620 | is_file = False |
|
623 | 621 | try: |
|
624 | 622 | _file = commit.get_node(f_path) |
|
625 | 623 | is_file = _file.is_file() |
|
626 | 624 | except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError): |
|
627 | 625 | pass |
|
628 | 626 | |
|
629 | 627 | if is_file: |
|
630 | 628 | history = commit.get_file_history(f_path) |
|
631 | 629 | prev_commit_id = history[1].raw_id \ |
|
632 | 630 | if len(history) > 1 else prev_commit_id |
|
633 | 631 | prev_url = h.route_path( |
|
634 | 632 | 'repo_files:annotated', repo_name=self.db_repo_name, |
|
635 | 633 | commit_id=prev_commit_id, f_path=f_path, |
|
636 | 634 | _anchor='L{}'.format(line_anchor)) |
|
637 | 635 | |
|
638 | 636 | raise HTTPFound(prev_url) |
|
639 | 637 | |
|
640 | 638 | @LoginRequired() |
|
641 | 639 | @HasRepoPermissionAnyDecorator( |
|
642 | 640 | 'repository.read', 'repository.write', 'repository.admin') |
|
643 | 641 | @view_config( |
|
644 | 642 | route_name='repo_nodetree_full', request_method='GET', |
|
645 | 643 | renderer=None, xhr=True) |
|
646 | 644 | @view_config( |
|
647 | 645 | route_name='repo_nodetree_full:default_path', request_method='GET', |
|
648 | 646 | renderer=None, xhr=True) |
|
649 | 647 | def repo_nodetree_full(self): |
|
650 | 648 | """ |
|
651 | 649 | Returns rendered html of file tree that contains commit date, |
|
652 | 650 | author, commit_id for the specified combination of |
|
653 | 651 | repo, commit_id and file path |
|
654 | 652 | """ |
|
655 | 653 | c = self.load_default_context() |
|
656 | 654 | |
|
657 | 655 | commit_id, f_path = self._get_commit_and_path() |
|
658 | 656 | commit = self._get_commit_or_redirect(commit_id) |
|
659 | 657 | try: |
|
660 | 658 | dir_node = commit.get_node(f_path) |
|
661 | 659 | except RepositoryError as e: |
|
662 | 660 | return Response('error: {}'.format(safe_str(e))) |
|
663 | 661 | |
|
664 | 662 | if dir_node.is_file(): |
|
665 | 663 | return Response('') |
|
666 | 664 | |
|
667 | 665 | c.file = dir_node |
|
668 | 666 | c.commit = commit |
|
669 | 667 | |
|
670 | 668 | # using force=True here, make a little trick. We flush the cache and |
|
671 | 669 | # compute it using the same key as without previous full_load, so now |
|
672 | 670 | # the fully loaded tree is now returned instead of partial, |
|
673 | 671 | # and we store this in caches |
|
674 | 672 | html = self._get_tree_at_commit( |
|
675 | 673 | c, commit.raw_id, dir_node.path, full_load=True, force=True) |
|
676 | 674 | |
|
677 | 675 | return Response(html) |
|
678 | 676 | |
|
679 | 677 | def _get_attachement_disposition(self, f_path): |
|
680 | 678 | return 'attachment; filename=%s' % \ |
|
681 | 679 | safe_str(f_path.split(Repository.NAME_SEP)[-1]) |
|
682 | 680 | |
|
683 | 681 | @LoginRequired() |
|
684 | 682 | @HasRepoPermissionAnyDecorator( |
|
685 | 683 | 'repository.read', 'repository.write', 'repository.admin') |
|
686 | 684 | @view_config( |
|
687 | 685 | route_name='repo_file_raw', request_method='GET', |
|
688 | 686 | renderer=None) |
|
689 | 687 | def repo_file_raw(self): |
|
690 | 688 | """ |
|
691 | 689 | Action for show as raw, some mimetypes are "rendered", |
|
692 | 690 | those include images, icons. |
|
693 | 691 | """ |
|
694 | 692 | c = self.load_default_context() |
|
695 | 693 | |
|
696 | 694 | commit_id, f_path = self._get_commit_and_path() |
|
697 | 695 | commit = self._get_commit_or_redirect(commit_id) |
|
698 | 696 | file_node = self._get_filenode_or_redirect(commit, f_path) |
|
699 | 697 | |
|
700 | 698 | raw_mimetype_mapping = { |
|
701 | 699 | # map original mimetype to a mimetype used for "show as raw" |
|
702 | 700 | # you can also provide a content-disposition to override the |
|
703 | 701 | # default "attachment" disposition. |
|
704 | 702 | # orig_type: (new_type, new_dispo) |
|
705 | 703 | |
|
706 | 704 | # show images inline: |
|
707 | 705 | # Do not re-add SVG: it is unsafe and permits XSS attacks. One can |
|
708 | 706 | # for example render an SVG with javascript inside or even render |
|
709 | 707 | # HTML. |
|
710 | 708 | 'image/x-icon': ('image/x-icon', 'inline'), |
|
711 | 709 | 'image/png': ('image/png', 'inline'), |
|
712 | 710 | 'image/gif': ('image/gif', 'inline'), |
|
713 | 711 | 'image/jpeg': ('image/jpeg', 'inline'), |
|
714 | 712 | 'application/pdf': ('application/pdf', 'inline'), |
|
715 | 713 | } |
|
716 | 714 | |
|
717 | 715 | mimetype = file_node.mimetype |
|
718 | 716 | try: |
|
719 | 717 | mimetype, disposition = raw_mimetype_mapping[mimetype] |
|
720 | 718 | except KeyError: |
|
721 | 719 | # we don't know anything special about this, handle it safely |
|
722 | 720 | if file_node.is_binary: |
|
723 | 721 | # do same as download raw for binary files |
|
724 | 722 | mimetype, disposition = 'application/octet-stream', 'attachment' |
|
725 | 723 | else: |
|
726 | 724 | # do not just use the original mimetype, but force text/plain, |
|
727 | 725 | # otherwise it would serve text/html and that might be unsafe. |
|
728 | 726 | # Note: underlying vcs library fakes text/plain mimetype if the |
|
729 | 727 | # mimetype can not be determined and it thinks it is not |
|
730 | 728 | # binary.This might lead to erroneous text display in some |
|
731 | 729 | # cases, but helps in other cases, like with text files |
|
732 | 730 | # without extension. |
|
733 | 731 | mimetype, disposition = 'text/plain', 'inline' |
|
734 | 732 | |
|
735 | 733 | if disposition == 'attachment': |
|
736 | 734 | disposition = self._get_attachement_disposition(f_path) |
|
737 | 735 | |
|
738 | 736 | def stream_node(): |
|
739 | 737 | yield file_node.raw_bytes |
|
740 | 738 | |
|
741 | 739 | response = Response(app_iter=stream_node()) |
|
742 | 740 | response.content_disposition = disposition |
|
743 | 741 | response.content_type = mimetype |
|
744 | 742 | |
|
745 | 743 | charset = self._get_default_encoding(c) |
|
746 | 744 | if charset: |
|
747 | 745 | response.charset = charset |
|
748 | 746 | |
|
749 | 747 | return response |
|
750 | 748 | |
|
751 | 749 | @LoginRequired() |
|
752 | 750 | @HasRepoPermissionAnyDecorator( |
|
753 | 751 | 'repository.read', 'repository.write', 'repository.admin') |
|
754 | 752 | @view_config( |
|
755 | 753 | route_name='repo_file_download', request_method='GET', |
|
756 | 754 | renderer=None) |
|
757 | 755 | @view_config( |
|
758 | 756 | route_name='repo_file_download:legacy', request_method='GET', |
|
759 | 757 | renderer=None) |
|
760 | 758 | def repo_file_download(self): |
|
761 | 759 | c = self.load_default_context() |
|
762 | 760 | |
|
763 | 761 | commit_id, f_path = self._get_commit_and_path() |
|
764 | 762 | commit = self._get_commit_or_redirect(commit_id) |
|
765 | 763 | file_node = self._get_filenode_or_redirect(commit, f_path) |
|
766 | 764 | |
|
767 | 765 | if self.request.GET.get('lf'): |
|
768 | 766 | # only if lf get flag is passed, we download this file |
|
769 | 767 | # as LFS/Largefile |
|
770 | 768 | lf_node = file_node.get_largefile_node() |
|
771 | 769 | if lf_node: |
|
772 | 770 | # overwrite our pointer with the REAL large-file |
|
773 | 771 | file_node = lf_node |
|
774 | 772 | |
|
775 | 773 | disposition = self._get_attachement_disposition(f_path) |
|
776 | 774 | |
|
777 | 775 | def stream_node(): |
|
778 | 776 | yield file_node.raw_bytes |
|
779 | 777 | |
|
780 | 778 | response = Response(app_iter=stream_node()) |
|
781 | 779 | response.content_disposition = disposition |
|
782 | 780 | response.content_type = file_node.mimetype |
|
783 | 781 | |
|
784 | 782 | charset = self._get_default_encoding(c) |
|
785 | 783 | if charset: |
|
786 | 784 | response.charset = charset |
|
787 | 785 | |
|
788 | 786 | return response |
|
789 | 787 | |
|
790 | 788 | def _get_nodelist_at_commit(self, repo_name, commit_id, f_path): |
|
791 | 789 | def _cached_nodes(): |
|
792 | 790 | log.debug('Generating cached nodelist for %s, %s, %s', |
|
793 | 791 | repo_name, commit_id, f_path) |
|
794 | 792 | _d, _f = ScmModel().get_nodes( |
|
795 | 793 | repo_name, commit_id, f_path, flat=False) |
|
796 | 794 | return _d + _f |
|
797 | 795 | |
|
798 | 796 | cache_manager = self._get_tree_cache_manager(caches.FILE_SEARCH_TREE_META) |
|
799 | 797 | |
|
800 | 798 | cache_key = caches.compute_key_from_params( |
|
801 | 799 | repo_name, commit_id, f_path) |
|
802 | 800 | return cache_manager.get(cache_key, createfunc=_cached_nodes) |
|
803 | 801 | |
|
804 | 802 | @LoginRequired() |
|
805 | 803 | @HasRepoPermissionAnyDecorator( |
|
806 | 804 | 'repository.read', 'repository.write', 'repository.admin') |
|
807 | 805 | @view_config( |
|
808 | 806 | route_name='repo_files_nodelist', request_method='GET', |
|
809 | 807 | renderer='json_ext', xhr=True) |
|
810 | 808 | def repo_nodelist(self): |
|
811 | 809 | self.load_default_context() |
|
812 | 810 | |
|
813 | 811 | commit_id, f_path = self._get_commit_and_path() |
|
814 | 812 | commit = self._get_commit_or_redirect(commit_id) |
|
815 | 813 | |
|
816 | 814 | metadata = self._get_nodelist_at_commit( |
|
817 | 815 | self.db_repo_name, commit.raw_id, f_path) |
|
818 | 816 | return {'nodes': metadata} |
|
819 | 817 | |
|
820 | 818 | def _create_references( |
|
821 | 819 | self, branches_or_tags, symbolic_reference, f_path): |
|
822 | 820 | items = [] |
|
823 | 821 | for name, commit_id in branches_or_tags.items(): |
|
824 | 822 | sym_ref = symbolic_reference(commit_id, name, f_path) |
|
825 | 823 | items.append((sym_ref, name)) |
|
826 | 824 | return items |
|
827 | 825 | |
|
828 | 826 | def _symbolic_reference(self, commit_id, name, f_path): |
|
829 | 827 | return commit_id |
|
830 | 828 | |
|
831 | 829 | def _symbolic_reference_svn(self, commit_id, name, f_path): |
|
832 | 830 | new_f_path = vcspath.join(name, f_path) |
|
833 | 831 | return u'%s@%s' % (new_f_path, commit_id) |
|
834 | 832 | |
|
835 | 833 | def _get_node_history(self, commit_obj, f_path, commits=None): |
|
836 | 834 | """ |
|
837 | 835 | get commit history for given node |
|
838 | 836 | |
|
839 | 837 | :param commit_obj: commit to calculate history |
|
840 | 838 | :param f_path: path for node to calculate history for |
|
841 | 839 | :param commits: if passed don't calculate history and take |
|
842 | 840 | commits defined in this list |
|
843 | 841 | """ |
|
844 | 842 | _ = self.request.translate |
|
845 | 843 | |
|
846 | 844 | # calculate history based on tip |
|
847 | 845 | tip = self.rhodecode_vcs_repo.get_commit() |
|
848 | 846 | if commits is None: |
|
849 | 847 | pre_load = ["author", "branch"] |
|
850 | 848 | try: |
|
851 | 849 | commits = tip.get_file_history(f_path, pre_load=pre_load) |
|
852 | 850 | except (NodeDoesNotExistError, CommitError): |
|
853 | 851 | # this node is not present at tip! |
|
854 | 852 | commits = commit_obj.get_file_history(f_path, pre_load=pre_load) |
|
855 | 853 | |
|
856 | 854 | history = [] |
|
857 | 855 | commits_group = ([], _("Changesets")) |
|
858 | 856 | for commit in commits: |
|
859 | 857 | branch = ' (%s)' % commit.branch if commit.branch else '' |
|
860 | 858 | n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch) |
|
861 | 859 | commits_group[0].append((commit.raw_id, n_desc,)) |
|
862 | 860 | history.append(commits_group) |
|
863 | 861 | |
|
864 | 862 | symbolic_reference = self._symbolic_reference |
|
865 | 863 | |
|
866 | 864 | if self.rhodecode_vcs_repo.alias == 'svn': |
|
867 | 865 | adjusted_f_path = RepoFilesView.adjust_file_path_for_svn( |
|
868 | 866 | f_path, self.rhodecode_vcs_repo) |
|
869 | 867 | if adjusted_f_path != f_path: |
|
870 | 868 | log.debug( |
|
871 | 869 | 'Recognized svn tag or branch in file "%s", using svn ' |
|
872 | 870 | 'specific symbolic references', f_path) |
|
873 | 871 | f_path = adjusted_f_path |
|
874 | 872 | symbolic_reference = self._symbolic_reference_svn |
|
875 | 873 | |
|
876 | 874 | branches = self._create_references( |
|
877 | 875 | self.rhodecode_vcs_repo.branches, symbolic_reference, f_path) |
|
878 | 876 | branches_group = (branches, _("Branches")) |
|
879 | 877 | |
|
880 | 878 | tags = self._create_references( |
|
881 | 879 | self.rhodecode_vcs_repo.tags, symbolic_reference, f_path) |
|
882 | 880 | tags_group = (tags, _("Tags")) |
|
883 | 881 | |
|
884 | 882 | history.append(branches_group) |
|
885 | 883 | history.append(tags_group) |
|
886 | 884 | |
|
887 | 885 | return history, commits |
|
888 | 886 | |
|
889 | 887 | @LoginRequired() |
|
890 | 888 | @HasRepoPermissionAnyDecorator( |
|
891 | 889 | 'repository.read', 'repository.write', 'repository.admin') |
|
892 | 890 | @view_config( |
|
893 | 891 | route_name='repo_file_history', request_method='GET', |
|
894 | 892 | renderer='json_ext') |
|
895 | 893 | def repo_file_history(self): |
|
896 | 894 | self.load_default_context() |
|
897 | 895 | |
|
898 | 896 | commit_id, f_path = self._get_commit_and_path() |
|
899 | 897 | commit = self._get_commit_or_redirect(commit_id) |
|
900 | 898 | file_node = self._get_filenode_or_redirect(commit, f_path) |
|
901 | 899 | |
|
902 | 900 | if file_node.is_file(): |
|
903 | 901 | file_history, _hist = self._get_node_history(commit, f_path) |
|
904 | 902 | |
|
905 | 903 | res = [] |
|
906 | 904 | for obj in file_history: |
|
907 | 905 | res.append({ |
|
908 | 906 | 'text': obj[1], |
|
909 | 907 | 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]] |
|
910 | 908 | }) |
|
911 | 909 | |
|
912 | 910 | data = { |
|
913 | 911 | 'more': False, |
|
914 | 912 | 'results': res |
|
915 | 913 | } |
|
916 | 914 | return data |
|
917 | 915 | |
|
918 | 916 | log.warning('Cannot fetch history for directory') |
|
919 | 917 | raise HTTPBadRequest() |
|
920 | 918 | |
|
921 | 919 | @LoginRequired() |
|
922 | 920 | @HasRepoPermissionAnyDecorator( |
|
923 | 921 | 'repository.read', 'repository.write', 'repository.admin') |
|
924 | 922 | @view_config( |
|
925 | 923 | route_name='repo_file_authors', request_method='GET', |
|
926 | 924 | renderer='rhodecode:templates/files/file_authors_box.mako') |
|
927 | 925 | def repo_file_authors(self): |
|
928 | 926 | c = self.load_default_context() |
|
929 | 927 | |
|
930 | 928 | commit_id, f_path = self._get_commit_and_path() |
|
931 | 929 | commit = self._get_commit_or_redirect(commit_id) |
|
932 | 930 | file_node = self._get_filenode_or_redirect(commit, f_path) |
|
933 | 931 | |
|
934 | 932 | if not file_node.is_file(): |
|
935 | 933 | raise HTTPBadRequest() |
|
936 | 934 | |
|
937 | 935 | c.file_last_commit = file_node.last_commit |
|
938 | 936 | if self.request.GET.get('annotate') == '1': |
|
939 | 937 | # use _hist from annotation if annotation mode is on |
|
940 | 938 | commit_ids = set(x[1] for x in file_node.annotate) |
|
941 | 939 | _hist = ( |
|
942 | 940 | self.rhodecode_vcs_repo.get_commit(commit_id) |
|
943 | 941 | for commit_id in commit_ids) |
|
944 | 942 | else: |
|
945 | 943 | _f_history, _hist = self._get_node_history(commit, f_path) |
|
946 | 944 | c.file_author = False |
|
947 | 945 | |
|
948 | 946 | unique = collections.OrderedDict() |
|
949 | 947 | for commit in _hist: |
|
950 | 948 | author = commit.author |
|
951 | 949 | if author not in unique: |
|
952 | 950 | unique[commit.author] = [ |
|
953 | 951 | h.email(author), |
|
954 | 952 | h.person(author, 'username_or_name_or_email'), |
|
955 | 953 | 1 # counter |
|
956 | 954 | ] |
|
957 | 955 | |
|
958 | 956 | else: |
|
959 | 957 | # increase counter |
|
960 | 958 | unique[commit.author][2] += 1 |
|
961 | 959 | |
|
962 | 960 | c.authors = [val for val in unique.values()] |
|
963 | 961 | |
|
964 | 962 | return self._get_template_context(c) |
|
965 | 963 | |
|
966 | 964 | @LoginRequired() |
|
967 | 965 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
968 | 966 | @view_config( |
|
969 | 967 | route_name='repo_files_remove_file', request_method='GET', |
|
970 | 968 | renderer='rhodecode:templates/files/files_delete.mako') |
|
971 | 969 | def repo_files_remove_file(self): |
|
972 | 970 | _ = self.request.translate |
|
973 | 971 | c = self.load_default_context() |
|
974 | 972 | commit_id, f_path = self._get_commit_and_path() |
|
975 | 973 | |
|
976 | 974 | self._ensure_not_locked() |
|
977 | 975 | |
|
978 | 976 | if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo): |
|
979 | 977 | h.flash(_('You can only delete files with commit ' |
|
980 | 978 | 'being a valid branch '), category='warning') |
|
981 | 979 | raise HTTPFound( |
|
982 | 980 | h.route_path('repo_files', |
|
983 | 981 | repo_name=self.db_repo_name, commit_id='tip', |
|
984 | 982 | f_path=f_path)) |
|
985 | 983 | |
|
986 | 984 | c.commit = self._get_commit_or_redirect(commit_id) |
|
987 | 985 | c.file = self._get_filenode_or_redirect(c.commit, f_path) |
|
988 | 986 | |
|
989 | 987 | c.default_message = _( |
|
990 | 988 | 'Deleted file {} via RhodeCode Enterprise').format(f_path) |
|
991 | 989 | c.f_path = f_path |
|
992 | 990 | |
|
993 | 991 | return self._get_template_context(c) |
|
994 | 992 | |
|
995 | 993 | @LoginRequired() |
|
996 | 994 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
997 | 995 | @CSRFRequired() |
|
998 | 996 | @view_config( |
|
999 | 997 | route_name='repo_files_delete_file', request_method='POST', |
|
1000 | 998 | renderer=None) |
|
1001 | 999 | def repo_files_delete_file(self): |
|
1002 | 1000 | _ = self.request.translate |
|
1003 | 1001 | |
|
1004 | 1002 | c = self.load_default_context() |
|
1005 | 1003 | commit_id, f_path = self._get_commit_and_path() |
|
1006 | 1004 | |
|
1007 | 1005 | self._ensure_not_locked() |
|
1008 | 1006 | |
|
1009 | 1007 | if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo): |
|
1010 | 1008 | h.flash(_('You can only delete files with commit ' |
|
1011 | 1009 | 'being a valid branch '), category='warning') |
|
1012 | 1010 | raise HTTPFound( |
|
1013 | 1011 | h.route_path('repo_files', |
|
1014 | 1012 | repo_name=self.db_repo_name, commit_id='tip', |
|
1015 | 1013 | f_path=f_path)) |
|
1016 | 1014 | |
|
1017 | 1015 | c.commit = self._get_commit_or_redirect(commit_id) |
|
1018 | 1016 | c.file = self._get_filenode_or_redirect(c.commit, f_path) |
|
1019 | 1017 | |
|
1020 | 1018 | c.default_message = _( |
|
1021 | 1019 | 'Deleted file {} via RhodeCode Enterprise').format(f_path) |
|
1022 | 1020 | c.f_path = f_path |
|
1023 | 1021 | node_path = f_path |
|
1024 | 1022 | author = self._rhodecode_db_user.full_contact |
|
1025 | 1023 | message = self.request.POST.get('message') or c.default_message |
|
1026 | 1024 | try: |
|
1027 | 1025 | nodes = { |
|
1028 | 1026 | node_path: { |
|
1029 | 1027 | 'content': '' |
|
1030 | 1028 | } |
|
1031 | 1029 | } |
|
1032 | 1030 | ScmModel().delete_nodes( |
|
1033 | 1031 | user=self._rhodecode_db_user.user_id, repo=self.db_repo, |
|
1034 | 1032 | message=message, |
|
1035 | 1033 | nodes=nodes, |
|
1036 | 1034 | parent_commit=c.commit, |
|
1037 | 1035 | author=author, |
|
1038 | 1036 | ) |
|
1039 | 1037 | |
|
1040 | 1038 | h.flash( |
|
1041 | 1039 | _('Successfully deleted file `{}`').format( |
|
1042 | 1040 | h.escape(f_path)), category='success') |
|
1043 | 1041 | except Exception: |
|
1044 | 1042 | log.exception('Error during commit operation') |
|
1045 | 1043 | h.flash(_('Error occurred during commit'), category='error') |
|
1046 | 1044 | raise HTTPFound( |
|
1047 | 1045 | h.route_path('repo_commit', repo_name=self.db_repo_name, |
|
1048 | 1046 | commit_id='tip')) |
|
1049 | 1047 | |
|
1050 | 1048 | @LoginRequired() |
|
1051 | 1049 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
1052 | 1050 | @view_config( |
|
1053 | 1051 | route_name='repo_files_edit_file', request_method='GET', |
|
1054 | 1052 | renderer='rhodecode:templates/files/files_edit.mako') |
|
1055 | 1053 | def repo_files_edit_file(self): |
|
1056 | 1054 | _ = self.request.translate |
|
1057 | 1055 | c = self.load_default_context() |
|
1058 | 1056 | commit_id, f_path = self._get_commit_and_path() |
|
1059 | 1057 | |
|
1060 | 1058 | self._ensure_not_locked() |
|
1061 | 1059 | |
|
1062 | 1060 | if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo): |
|
1063 | 1061 | h.flash(_('You can only edit files with commit ' |
|
1064 | 1062 | 'being a valid branch '), category='warning') |
|
1065 | 1063 | raise HTTPFound( |
|
1066 | 1064 | h.route_path('repo_files', |
|
1067 | 1065 | repo_name=self.db_repo_name, commit_id='tip', |
|
1068 | 1066 | f_path=f_path)) |
|
1069 | 1067 | |
|
1070 | 1068 | c.commit = self._get_commit_or_redirect(commit_id) |
|
1071 | 1069 | c.file = self._get_filenode_or_redirect(c.commit, f_path) |
|
1072 | 1070 | |
|
1073 | 1071 | if c.file.is_binary: |
|
1074 | 1072 | files_url = h.route_path( |
|
1075 | 1073 | 'repo_files', |
|
1076 | 1074 | repo_name=self.db_repo_name, |
|
1077 | 1075 | commit_id=c.commit.raw_id, f_path=f_path) |
|
1078 | 1076 | raise HTTPFound(files_url) |
|
1079 | 1077 | |
|
1080 | 1078 | c.default_message = _( |
|
1081 | 1079 | 'Edited file {} via RhodeCode Enterprise').format(f_path) |
|
1082 | 1080 | c.f_path = f_path |
|
1083 | 1081 | |
|
1084 | 1082 | return self._get_template_context(c) |
|
1085 | 1083 | |
|
1086 | 1084 | @LoginRequired() |
|
1087 | 1085 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
1088 | 1086 | @CSRFRequired() |
|
1089 | 1087 | @view_config( |
|
1090 | 1088 | route_name='repo_files_update_file', request_method='POST', |
|
1091 | 1089 | renderer=None) |
|
1092 | 1090 | def repo_files_update_file(self): |
|
1093 | 1091 | _ = self.request.translate |
|
1094 | 1092 | c = self.load_default_context() |
|
1095 | 1093 | commit_id, f_path = self._get_commit_and_path() |
|
1096 | 1094 | |
|
1097 | 1095 | self._ensure_not_locked() |
|
1098 | 1096 | |
|
1099 | 1097 | if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo): |
|
1100 | 1098 | h.flash(_('You can only edit files with commit ' |
|
1101 | 1099 | 'being a valid branch '), category='warning') |
|
1102 | 1100 | raise HTTPFound( |
|
1103 | 1101 | h.route_path('repo_files', |
|
1104 | 1102 | repo_name=self.db_repo_name, commit_id='tip', |
|
1105 | 1103 | f_path=f_path)) |
|
1106 | 1104 | |
|
1107 | 1105 | c.commit = self._get_commit_or_redirect(commit_id) |
|
1108 | 1106 | c.file = self._get_filenode_or_redirect(c.commit, f_path) |
|
1109 | 1107 | |
|
1110 | 1108 | if c.file.is_binary: |
|
1111 | 1109 | raise HTTPFound( |
|
1112 | 1110 | h.route_path('repo_files', |
|
1113 | 1111 | repo_name=self.db_repo_name, |
|
1114 | 1112 | commit_id=c.commit.raw_id, |
|
1115 | 1113 | f_path=f_path)) |
|
1116 | 1114 | |
|
1117 | 1115 | c.default_message = _( |
|
1118 | 1116 | 'Edited file {} via RhodeCode Enterprise').format(f_path) |
|
1119 | 1117 | c.f_path = f_path |
|
1120 | 1118 | old_content = c.file.content |
|
1121 | 1119 | sl = old_content.splitlines(1) |
|
1122 | 1120 | first_line = sl[0] if sl else '' |
|
1123 | 1121 | |
|
1124 | 1122 | r_post = self.request.POST |
|
1125 | 1123 | # modes: 0 - Unix, 1 - Mac, 2 - DOS |
|
1126 | 1124 | mode = detect_mode(first_line, 0) |
|
1127 | 1125 | content = convert_line_endings(r_post.get('content', ''), mode) |
|
1128 | 1126 | |
|
1129 | 1127 | message = r_post.get('message') or c.default_message |
|
1130 | 1128 | org_f_path = c.file.unicode_path |
|
1131 | 1129 | filename = r_post['filename'] |
|
1132 | 1130 | org_filename = c.file.name |
|
1133 | 1131 | |
|
1134 | 1132 | if content == old_content and filename == org_filename: |
|
1135 | 1133 | h.flash(_('No changes'), category='warning') |
|
1136 | 1134 | raise HTTPFound( |
|
1137 | 1135 | h.route_path('repo_commit', repo_name=self.db_repo_name, |
|
1138 | 1136 | commit_id='tip')) |
|
1139 | 1137 | try: |
|
1140 | 1138 | mapping = { |
|
1141 | 1139 | org_f_path: { |
|
1142 | 1140 | 'org_filename': org_f_path, |
|
1143 | 1141 | 'filename': os.path.join(c.file.dir_path, filename), |
|
1144 | 1142 | 'content': content, |
|
1145 | 1143 | 'lexer': '', |
|
1146 | 1144 | 'op': 'mod', |
|
1147 | 1145 | } |
|
1148 | 1146 | } |
|
1149 | 1147 | |
|
1150 | 1148 | ScmModel().update_nodes( |
|
1151 | 1149 | user=self._rhodecode_db_user.user_id, |
|
1152 | 1150 | repo=self.db_repo, |
|
1153 | 1151 | message=message, |
|
1154 | 1152 | nodes=mapping, |
|
1155 | 1153 | parent_commit=c.commit, |
|
1156 | 1154 | ) |
|
1157 | 1155 | |
|
1158 | 1156 | h.flash( |
|
1159 | 1157 | _('Successfully committed changes to file `{}`').format( |
|
1160 | 1158 | h.escape(f_path)), category='success') |
|
1161 | 1159 | except Exception: |
|
1162 | 1160 | log.exception('Error occurred during commit') |
|
1163 | 1161 | h.flash(_('Error occurred during commit'), category='error') |
|
1164 | 1162 | raise HTTPFound( |
|
1165 | 1163 | h.route_path('repo_commit', repo_name=self.db_repo_name, |
|
1166 | 1164 | commit_id='tip')) |
|
1167 | 1165 | |
|
1168 | 1166 | @LoginRequired() |
|
1169 | 1167 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
1170 | 1168 | @view_config( |
|
1171 | 1169 | route_name='repo_files_add_file', request_method='GET', |
|
1172 | 1170 | renderer='rhodecode:templates/files/files_add.mako') |
|
1173 | 1171 | def repo_files_add_file(self): |
|
1174 | 1172 | _ = self.request.translate |
|
1175 | 1173 | c = self.load_default_context() |
|
1176 | 1174 | commit_id, f_path = self._get_commit_and_path() |
|
1177 | 1175 | |
|
1178 | 1176 | self._ensure_not_locked() |
|
1179 | 1177 | |
|
1180 | 1178 | c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False) |
|
1181 | 1179 | if c.commit is None: |
|
1182 | 1180 | c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias) |
|
1183 | 1181 | c.default_message = (_('Added file via RhodeCode Enterprise')) |
|
1184 | 1182 | c.f_path = f_path.lstrip('/') # ensure not relative path |
|
1185 | 1183 | |
|
1186 | 1184 | return self._get_template_context(c) |
|
1187 | 1185 | |
|
1188 | 1186 | @LoginRequired() |
|
1189 | 1187 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
1190 | 1188 | @CSRFRequired() |
|
1191 | 1189 | @view_config( |
|
1192 | 1190 | route_name='repo_files_create_file', request_method='POST', |
|
1193 | 1191 | renderer=None) |
|
1194 | 1192 | def repo_files_create_file(self): |
|
1195 | 1193 | _ = self.request.translate |
|
1196 | 1194 | c = self.load_default_context() |
|
1197 | 1195 | commit_id, f_path = self._get_commit_and_path() |
|
1198 | 1196 | |
|
1199 | 1197 | self._ensure_not_locked() |
|
1200 | 1198 | |
|
1201 | 1199 | r_post = self.request.POST |
|
1202 | 1200 | |
|
1203 | 1201 | c.commit = self._get_commit_or_redirect( |
|
1204 | 1202 | commit_id, redirect_after=False) |
|
1205 | 1203 | if c.commit is None: |
|
1206 | 1204 | c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias) |
|
1207 | 1205 | c.default_message = (_('Added file via RhodeCode Enterprise')) |
|
1208 | 1206 | c.f_path = f_path |
|
1209 | 1207 | unix_mode = 0 |
|
1210 | 1208 | content = convert_line_endings(r_post.get('content', ''), unix_mode) |
|
1211 | 1209 | |
|
1212 | 1210 | message = r_post.get('message') or c.default_message |
|
1213 | 1211 | filename = r_post.get('filename') |
|
1214 | 1212 | location = r_post.get('location', '') # dir location |
|
1215 | 1213 | file_obj = r_post.get('upload_file', None) |
|
1216 | 1214 | |
|
1217 | 1215 | if file_obj is not None and hasattr(file_obj, 'filename'): |
|
1218 | 1216 | filename = r_post.get('filename_upload') |
|
1219 | 1217 | content = file_obj.file |
|
1220 | 1218 | |
|
1221 | 1219 | if hasattr(content, 'file'): |
|
1222 | 1220 | # non posix systems store real file under file attr |
|
1223 | 1221 | content = content.file |
|
1224 | 1222 | |
|
1225 | 1223 | if self.rhodecode_vcs_repo.is_empty: |
|
1226 | 1224 | default_redirect_url = h.route_path( |
|
1227 | 1225 | 'repo_summary', repo_name=self.db_repo_name) |
|
1228 | 1226 | else: |
|
1229 | 1227 | default_redirect_url = h.route_path( |
|
1230 | 1228 | 'repo_commit', repo_name=self.db_repo_name, commit_id='tip') |
|
1231 | 1229 | |
|
1232 | 1230 | # If there's no commit, redirect to repo summary |
|
1233 | 1231 | if type(c.commit) is EmptyCommit: |
|
1234 | 1232 | redirect_url = h.route_path( |
|
1235 | 1233 | 'repo_summary', repo_name=self.db_repo_name) |
|
1236 | 1234 | else: |
|
1237 | 1235 | redirect_url = default_redirect_url |
|
1238 | 1236 | |
|
1239 | 1237 | if not filename: |
|
1240 | 1238 | h.flash(_('No filename'), category='warning') |
|
1241 | 1239 | raise HTTPFound(redirect_url) |
|
1242 | 1240 | |
|
1243 | 1241 | # extract the location from filename, |
|
1244 | 1242 | # allows using foo/bar.txt syntax to create subdirectories |
|
1245 | 1243 | subdir_loc = filename.rsplit('/', 1) |
|
1246 | 1244 | if len(subdir_loc) == 2: |
|
1247 | 1245 | location = os.path.join(location, subdir_loc[0]) |
|
1248 | 1246 | |
|
1249 | 1247 | # strip all crap out of file, just leave the basename |
|
1250 | 1248 | filename = os.path.basename(filename) |
|
1251 | 1249 | node_path = os.path.join(location, filename) |
|
1252 | 1250 | author = self._rhodecode_db_user.full_contact |
|
1253 | 1251 | |
|
1254 | 1252 | try: |
|
1255 | 1253 | nodes = { |
|
1256 | 1254 | node_path: { |
|
1257 | 1255 | 'content': content |
|
1258 | 1256 | } |
|
1259 | 1257 | } |
|
1260 | 1258 | ScmModel().create_nodes( |
|
1261 | 1259 | user=self._rhodecode_db_user.user_id, |
|
1262 | 1260 | repo=self.db_repo, |
|
1263 | 1261 | message=message, |
|
1264 | 1262 | nodes=nodes, |
|
1265 | 1263 | parent_commit=c.commit, |
|
1266 | 1264 | author=author, |
|
1267 | 1265 | ) |
|
1268 | 1266 | |
|
1269 | 1267 | h.flash( |
|
1270 | 1268 | _('Successfully committed new file `{}`').format( |
|
1271 | 1269 | h.escape(node_path)), category='success') |
|
1272 | 1270 | except NonRelativePathError: |
|
1273 | 1271 | log.exception('Non Relative path found') |
|
1274 | 1272 | h.flash(_( |
|
1275 | 1273 | 'The location specified must be a relative path and must not ' |
|
1276 | 1274 | 'contain .. in the path'), category='warning') |
|
1277 | 1275 | raise HTTPFound(default_redirect_url) |
|
1278 | 1276 | except (NodeError, NodeAlreadyExistsError) as e: |
|
1279 | 1277 | h.flash(_(h.escape(e)), category='error') |
|
1280 | 1278 | except Exception: |
|
1281 | 1279 | log.exception('Error occurred during commit') |
|
1282 | 1280 | h.flash(_('Error occurred during commit'), category='error') |
|
1283 | 1281 | |
|
1284 | 1282 | raise HTTPFound(default_redirect_url) |
@@ -1,256 +1,253 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | import datetime |
|
23 | 23 | import formencode |
|
24 | 24 | import formencode.htmlfill |
|
25 | 25 | |
|
26 | 26 | from pyramid.httpexceptions import HTTPFound |
|
27 | 27 | from pyramid.view import view_config |
|
28 | 28 | from pyramid.renderers import render |
|
29 | 29 | from pyramid.response import Response |
|
30 | 30 | |
|
31 | 31 | from rhodecode.apps._base import RepoAppView, DataGridAppView |
|
32 | 32 | from rhodecode.lib.auth import ( |
|
33 | 33 | LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, |
|
34 | 34 | HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired) |
|
35 | 35 | import rhodecode.lib.helpers as h |
|
36 | 36 | from rhodecode.model.db import coalesce, or_, Repository, RepoGroup |
|
37 | 37 | from rhodecode.model.repo import RepoModel |
|
38 | 38 | from rhodecode.model.forms import RepoForkForm |
|
39 | 39 | from rhodecode.model.scm import ScmModel, RepoGroupList |
|
40 | 40 | from rhodecode.lib.utils2 import safe_int, safe_unicode |
|
41 | 41 | |
|
42 | 42 | log = logging.getLogger(__name__) |
|
43 | 43 | |
|
44 | 44 | |
|
45 | 45 | class RepoForksView(RepoAppView, DataGridAppView): |
|
46 | 46 | |
|
47 | 47 | def load_default_context(self): |
|
48 | 48 | c = self._get_local_tmpl_context(include_app_defaults=True) |
|
49 | ||
|
50 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
51 | c.repo_info = self.db_repo | |
|
52 | 49 | c.rhodecode_repo = self.rhodecode_vcs_repo |
|
53 | 50 | |
|
54 | 51 | acl_groups = RepoGroupList( |
|
55 | 52 | RepoGroup.query().all(), |
|
56 | 53 | perm_set=['group.write', 'group.admin']) |
|
57 | 54 | c.repo_groups = RepoGroup.groups_choices(groups=acl_groups) |
|
58 | 55 | c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups) |
|
59 | 56 | choices, c.landing_revs = ScmModel().get_repo_landing_revs() |
|
60 | 57 | c.landing_revs_choices = choices |
|
61 | 58 | c.personal_repo_group = c.rhodecode_user.personal_repo_group |
|
62 | 59 | |
|
63 | 60 | self._register_global_c(c) |
|
64 | 61 | return c |
|
65 | 62 | |
|
66 | 63 | @LoginRequired() |
|
67 | 64 | @HasRepoPermissionAnyDecorator( |
|
68 | 65 | 'repository.read', 'repository.write', 'repository.admin') |
|
69 | 66 | @view_config( |
|
70 | 67 | route_name='repo_forks_show_all', request_method='GET', |
|
71 | 68 | renderer='rhodecode:templates/forks/forks.mako') |
|
72 | 69 | def repo_forks_show_all(self): |
|
73 | 70 | c = self.load_default_context() |
|
74 | 71 | return self._get_template_context(c) |
|
75 | 72 | |
|
76 | 73 | @LoginRequired() |
|
77 | 74 | @HasRepoPermissionAnyDecorator( |
|
78 | 75 | 'repository.read', 'repository.write', 'repository.admin') |
|
79 | 76 | @view_config( |
|
80 | 77 | route_name='repo_forks_data', request_method='GET', |
|
81 | 78 | renderer='json_ext', xhr=True) |
|
82 | 79 | def repo_forks_data(self): |
|
83 | 80 | _ = self.request.translate |
|
84 | 81 | column_map = { |
|
85 | 82 | 'fork_name': 'repo_name', |
|
86 | 83 | 'fork_date': 'created_on', |
|
87 | 84 | 'last_activity': 'updated_on' |
|
88 | 85 | } |
|
89 | 86 | draw, start, limit = self._extract_chunk(self.request) |
|
90 | 87 | search_q, order_by, order_dir = self._extract_ordering( |
|
91 | 88 | self.request, column_map=column_map) |
|
92 | 89 | |
|
93 | 90 | acl_check = HasRepoPermissionAny( |
|
94 | 91 | 'repository.read', 'repository.write', 'repository.admin') |
|
95 | 92 | repo_id = self.db_repo.repo_id |
|
96 | 93 | allowed_ids = [] |
|
97 | 94 | for f in Repository.query().filter(Repository.fork_id == repo_id): |
|
98 | 95 | if acl_check(f.repo_name, 'get forks check'): |
|
99 | 96 | allowed_ids.append(f.repo_id) |
|
100 | 97 | |
|
101 | 98 | forks_data_total_count = Repository.query()\ |
|
102 | 99 | .filter(Repository.fork_id == repo_id)\ |
|
103 | 100 | .filter(Repository.repo_id.in_(allowed_ids))\ |
|
104 | 101 | .count() |
|
105 | 102 | |
|
106 | 103 | # json generate |
|
107 | 104 | base_q = Repository.query()\ |
|
108 | 105 | .filter(Repository.fork_id == repo_id)\ |
|
109 | 106 | .filter(Repository.repo_id.in_(allowed_ids))\ |
|
110 | 107 | |
|
111 | 108 | if search_q: |
|
112 | 109 | like_expression = u'%{}%'.format(safe_unicode(search_q)) |
|
113 | 110 | base_q = base_q.filter(or_( |
|
114 | 111 | Repository.repo_name.ilike(like_expression), |
|
115 | 112 | Repository.description.ilike(like_expression), |
|
116 | 113 | )) |
|
117 | 114 | |
|
118 | 115 | forks_data_total_filtered_count = base_q.count() |
|
119 | 116 | |
|
120 | 117 | sort_col = getattr(Repository, order_by, None) |
|
121 | 118 | if sort_col: |
|
122 | 119 | if order_dir == 'asc': |
|
123 | 120 | # handle null values properly to order by NULL last |
|
124 | 121 | if order_by in ['last_activity']: |
|
125 | 122 | sort_col = coalesce(sort_col, datetime.date.max) |
|
126 | 123 | sort_col = sort_col.asc() |
|
127 | 124 | else: |
|
128 | 125 | # handle null values properly to order by NULL last |
|
129 | 126 | if order_by in ['last_activity']: |
|
130 | 127 | sort_col = coalesce(sort_col, datetime.date.min) |
|
131 | 128 | sort_col = sort_col.desc() |
|
132 | 129 | |
|
133 | 130 | base_q = base_q.order_by(sort_col) |
|
134 | 131 | base_q = base_q.offset(start).limit(limit) |
|
135 | 132 | |
|
136 | 133 | fork_list = base_q.all() |
|
137 | 134 | |
|
138 | 135 | def fork_actions(fork): |
|
139 | 136 | url_link = h.route_path( |
|
140 | 137 | 'repo_compare', |
|
141 | 138 | repo_name=fork.repo_name, |
|
142 | 139 | source_ref_type=self.db_repo.landing_rev[0], |
|
143 | 140 | source_ref=self.db_repo.landing_rev[1], |
|
144 | 141 | target_ref_type=self.db_repo.landing_rev[0], |
|
145 | 142 | target_ref=self.db_repo.landing_rev[1], |
|
146 | 143 | _query=dict(merge=1, target_repo=f.repo_name)) |
|
147 | 144 | return h.link_to(_('Compare fork'), url_link, class_='btn-link') |
|
148 | 145 | |
|
149 | 146 | def fork_name(fork): |
|
150 | 147 | return h.link_to(fork.repo_name, |
|
151 | 148 | h.route_path('repo_summary', repo_name=fork.repo_name)) |
|
152 | 149 | |
|
153 | 150 | forks_data = [] |
|
154 | 151 | for fork in fork_list: |
|
155 | 152 | forks_data.append({ |
|
156 | 153 | "username": h.gravatar_with_user(self.request, fork.user.username), |
|
157 | 154 | "fork_name": fork_name(fork), |
|
158 | 155 | "description": fork.description, |
|
159 | 156 | "fork_date": h.age_component(fork.created_on, time_is_local=True), |
|
160 | 157 | "last_activity": h.format_date(fork.updated_on), |
|
161 | 158 | "action": fork_actions(fork), |
|
162 | 159 | }) |
|
163 | 160 | |
|
164 | 161 | data = ({ |
|
165 | 162 | 'draw': draw, |
|
166 | 163 | 'data': forks_data, |
|
167 | 164 | 'recordsTotal': forks_data_total_count, |
|
168 | 165 | 'recordsFiltered': forks_data_total_filtered_count, |
|
169 | 166 | }) |
|
170 | 167 | |
|
171 | 168 | return data |
|
172 | 169 | |
|
173 | 170 | @LoginRequired() |
|
174 | 171 | @NotAnonymous() |
|
175 | 172 | @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') |
|
176 | 173 | @HasRepoPermissionAnyDecorator( |
|
177 | 174 | 'repository.read', 'repository.write', 'repository.admin') |
|
178 | 175 | @view_config( |
|
179 | 176 | route_name='repo_fork_new', request_method='GET', |
|
180 | 177 | renderer='rhodecode:templates/forks/forks.mako') |
|
181 | 178 | def repo_fork_new(self): |
|
182 | 179 | c = self.load_default_context() |
|
183 | 180 | |
|
184 | 181 | defaults = RepoModel()._get_defaults(self.db_repo_name) |
|
185 | 182 | # alter the description to indicate a fork |
|
186 | 183 | defaults['description'] = ( |
|
187 | 184 | 'fork of repository: %s \n%s' % ( |
|
188 | 185 | defaults['repo_name'], defaults['description'])) |
|
189 | 186 | # add suffix to fork |
|
190 | 187 | defaults['repo_name'] = '%s-fork' % defaults['repo_name'] |
|
191 | 188 | |
|
192 | 189 | data = render('rhodecode:templates/forks/fork.mako', |
|
193 | 190 | self._get_template_context(c), self.request) |
|
194 | 191 | html = formencode.htmlfill.render( |
|
195 | 192 | data, |
|
196 | 193 | defaults=defaults, |
|
197 | 194 | encoding="UTF-8", |
|
198 | 195 | force_defaults=False |
|
199 | 196 | ) |
|
200 | 197 | return Response(html) |
|
201 | 198 | |
|
202 | 199 | @LoginRequired() |
|
203 | 200 | @NotAnonymous() |
|
204 | 201 | @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') |
|
205 | 202 | @HasRepoPermissionAnyDecorator( |
|
206 | 203 | 'repository.read', 'repository.write', 'repository.admin') |
|
207 | 204 | @CSRFRequired() |
|
208 | 205 | @view_config( |
|
209 | 206 | route_name='repo_fork_create', request_method='POST', |
|
210 | 207 | renderer='rhodecode:templates/forks/fork.mako') |
|
211 | 208 | def repo_fork_create(self): |
|
212 | 209 | _ = self.request.translate |
|
213 | 210 | c = self.load_default_context() |
|
214 | 211 | |
|
215 | 212 | _form = RepoForkForm(old_data={'repo_type': self.db_repo.repo_type}, |
|
216 | 213 | repo_groups=c.repo_groups_choices, |
|
217 | 214 | landing_revs=c.landing_revs_choices)() |
|
218 | 215 | form_result = {} |
|
219 | 216 | task_id = None |
|
220 | 217 | try: |
|
221 | 218 | form_result = _form.to_python(dict(self.request.POST)) |
|
222 | 219 | # create fork is done sometimes async on celery, db transaction |
|
223 | 220 | # management is handled there. |
|
224 | 221 | task = RepoModel().create_fork( |
|
225 | 222 | form_result, c.rhodecode_user.user_id) |
|
226 | 223 | from celery.result import BaseAsyncResult |
|
227 | 224 | if isinstance(task, BaseAsyncResult): |
|
228 | 225 | task_id = task.task_id |
|
229 | 226 | except formencode.Invalid as errors: |
|
230 |
c.r |
|
|
227 | c.rhodecode_db_repo = self.db_repo | |
|
231 | 228 | |
|
232 | 229 | data = render('rhodecode:templates/forks/fork.mako', |
|
233 | 230 | self._get_template_context(c), self.request) |
|
234 | 231 | html = formencode.htmlfill.render( |
|
235 | 232 | data, |
|
236 | 233 | defaults=errors.value, |
|
237 | 234 | errors=errors.error_dict or {}, |
|
238 | 235 | prefix_error=False, |
|
239 | 236 | encoding="UTF-8", |
|
240 | 237 | force_defaults=False |
|
241 | 238 | ) |
|
242 | 239 | return Response(html) |
|
243 | 240 | except Exception: |
|
244 | 241 | log.exception( |
|
245 | 242 | u'Exception while trying to fork the repository %s', |
|
246 | 243 | self.db_repo_name) |
|
247 | 244 | msg = ( |
|
248 | 245 | _('An error occurred during repository forking %s') % ( |
|
249 | 246 | self.db_repo_name, )) |
|
250 | 247 | h.flash(msg, category='error') |
|
251 | 248 | |
|
252 | 249 | repo_name = form_result.get('repo_name_full', self.db_repo_name) |
|
253 | 250 | raise HTTPFound( |
|
254 | 251 | h.route_path('repo_creating', |
|
255 | 252 | repo_name=repo_name, |
|
256 | 253 | _query=dict(task_id=task_id))) |
@@ -1,67 +1,64 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | |
|
23 | 23 | from pyramid.view import view_config |
|
24 | 24 | |
|
25 | 25 | from rhodecode.apps._base import RepoAppView |
|
26 | 26 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator |
|
27 | 27 | from rhodecode.lib import repo_maintenance |
|
28 | 28 | |
|
29 | 29 | log = logging.getLogger(__name__) |
|
30 | 30 | |
|
31 | 31 | |
|
32 | 32 | class RepoMaintenanceView(RepoAppView): |
|
33 | 33 | def load_default_context(self): |
|
34 | 34 | c = self._get_local_tmpl_context() |
|
35 | 35 | |
|
36 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
37 | c.repo_info = self.db_repo | |
|
38 | ||
|
39 | 36 | self._register_global_c(c) |
|
40 | 37 | return c |
|
41 | 38 | |
|
42 | 39 | @LoginRequired() |
|
43 | 40 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
44 | 41 | @view_config( |
|
45 | 42 | route_name='edit_repo_maintenance', request_method='GET', |
|
46 | 43 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
47 | 44 | def repo_maintenance(self): |
|
48 | 45 | c = self.load_default_context() |
|
49 | 46 | c.active = 'maintenance' |
|
50 | 47 | maintenance = repo_maintenance.RepoMaintenance() |
|
51 | 48 | c.executable_tasks = maintenance.get_tasks_for_repo(self.db_repo) |
|
52 | 49 | return self._get_template_context(c) |
|
53 | 50 | |
|
54 | 51 | @LoginRequired() |
|
55 | 52 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
56 | 53 | @view_config( |
|
57 | 54 | route_name='edit_repo_maintenance_execute', request_method='GET', |
|
58 | 55 | renderer='json', xhr=True) |
|
59 | 56 | def repo_maintenance_execute(self): |
|
60 | 57 | c = self.load_default_context() |
|
61 | 58 | c.active = 'maintenance' |
|
62 | 59 | _ = self.request.translate |
|
63 | 60 | |
|
64 | 61 | maintenance = repo_maintenance.RepoMaintenance() |
|
65 | 62 | executed_types = maintenance.execute(self.db_repo) |
|
66 | 63 | |
|
67 | 64 | return executed_types |
@@ -1,92 +1,89 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | |
|
23 | 23 | from pyramid.httpexceptions import HTTPFound |
|
24 | 24 | from pyramid.view import view_config |
|
25 | 25 | |
|
26 | 26 | from rhodecode.apps._base import RepoAppView |
|
27 | 27 | from rhodecode.lib import helpers as h |
|
28 | 28 | from rhodecode.lib import audit_logger |
|
29 | 29 | from rhodecode.lib.auth import ( |
|
30 | 30 | LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired) |
|
31 | 31 | from rhodecode.model.forms import RepoPermsForm |
|
32 | 32 | from rhodecode.model.meta import Session |
|
33 | 33 | from rhodecode.model.repo import RepoModel |
|
34 | 34 | |
|
35 | 35 | log = logging.getLogger(__name__) |
|
36 | 36 | |
|
37 | 37 | |
|
38 | 38 | class RepoSettingsPermissionsView(RepoAppView): |
|
39 | 39 | |
|
40 | 40 | def load_default_context(self): |
|
41 | 41 | c = self._get_local_tmpl_context() |
|
42 | 42 | |
|
43 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
44 | c.repo_info = self.db_repo | |
|
45 | ||
|
46 | 43 | self._register_global_c(c) |
|
47 | 44 | return c |
|
48 | 45 | |
|
49 | 46 | @LoginRequired() |
|
50 | 47 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
51 | 48 | @view_config( |
|
52 | 49 | route_name='edit_repo_perms', request_method='GET', |
|
53 | 50 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
54 | 51 | def edit_permissions(self): |
|
55 | 52 | c = self.load_default_context() |
|
56 | 53 | c.active = 'permissions' |
|
57 | 54 | return self._get_template_context(c) |
|
58 | 55 | |
|
59 | 56 | @LoginRequired() |
|
60 | 57 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
61 | 58 | @CSRFRequired() |
|
62 | 59 | @view_config( |
|
63 | 60 | route_name='edit_repo_perms', request_method='POST', |
|
64 | 61 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
65 | 62 | def edit_permissions_update(self): |
|
66 | 63 | _ = self.request.translate |
|
67 | 64 | c = self.load_default_context() |
|
68 | 65 | c.active = 'permissions' |
|
69 | 66 | data = self.request.POST |
|
70 | 67 | # store private flag outside of HTML to verify if we can modify |
|
71 | 68 | # default user permissions, prevents submission of FAKE post data |
|
72 | 69 | # into the form for private repos |
|
73 | 70 | data['repo_private'] = self.db_repo.private |
|
74 | 71 | form = RepoPermsForm()().to_python(data) |
|
75 | 72 | changes = RepoModel().update_permissions( |
|
76 | 73 | self.db_repo_name, form['perm_additions'], form['perm_updates'], |
|
77 | 74 | form['perm_deletions']) |
|
78 | 75 | |
|
79 | 76 | action_data = { |
|
80 | 77 | 'added': changes['added'], |
|
81 | 78 | 'updated': changes['updated'], |
|
82 | 79 | 'deleted': changes['deleted'], |
|
83 | 80 | } |
|
84 | 81 | audit_logger.store_web( |
|
85 | 82 | 'repo.edit.permissions', action_data=action_data, |
|
86 | 83 | user=self._rhodecode_user, repo=self.db_repo) |
|
87 | 84 | |
|
88 | 85 | Session().commit() |
|
89 | 86 | h.flash(_('Repository permissions updated'), category='success') |
|
90 | 87 | |
|
91 | 88 | raise HTTPFound( |
|
92 | 89 | h.route_path('edit_repo_perms', repo_name=self.db_repo_name)) |
@@ -1,1194 +1,1192 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | import collections |
|
23 | 23 | |
|
24 | 24 | import formencode |
|
25 | 25 | import formencode.htmlfill |
|
26 | 26 | import peppercorn |
|
27 | 27 | from pyramid.httpexceptions import ( |
|
28 | 28 | HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest) |
|
29 | 29 | from pyramid.view import view_config |
|
30 | 30 | from pyramid.renderers import render |
|
31 | 31 | |
|
32 | 32 | from rhodecode import events |
|
33 | 33 | from rhodecode.apps._base import RepoAppView, DataGridAppView |
|
34 | 34 | |
|
35 | 35 | from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream |
|
36 | 36 | from rhodecode.lib.base import vcs_operation_context |
|
37 | 37 | from rhodecode.lib.ext_json import json |
|
38 | 38 | from rhodecode.lib.auth import ( |
|
39 | 39 | LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired) |
|
40 | 40 | from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode |
|
41 | 41 | from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason |
|
42 | 42 | from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError, |
|
43 | 43 | RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError) |
|
44 | 44 | from rhodecode.model.changeset_status import ChangesetStatusModel |
|
45 | 45 | from rhodecode.model.comment import CommentsModel |
|
46 | 46 | from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion, |
|
47 | 47 | ChangesetComment, ChangesetStatus, Repository) |
|
48 | 48 | from rhodecode.model.forms import PullRequestForm |
|
49 | 49 | from rhodecode.model.meta import Session |
|
50 | 50 | from rhodecode.model.pull_request import PullRequestModel, MergeCheck |
|
51 | 51 | from rhodecode.model.scm import ScmModel |
|
52 | 52 | |
|
53 | 53 | log = logging.getLogger(__name__) |
|
54 | 54 | |
|
55 | 55 | |
|
56 | 56 | class RepoPullRequestsView(RepoAppView, DataGridAppView): |
|
57 | 57 | |
|
58 | 58 | def load_default_context(self): |
|
59 | 59 | c = self._get_local_tmpl_context(include_app_defaults=True) |
|
60 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
61 | c.repo_info = self.db_repo | |
|
62 | 60 | c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED |
|
63 | 61 | c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED |
|
64 | 62 | self._register_global_c(c) |
|
65 | 63 | return c |
|
66 | 64 | |
|
67 | 65 | def _get_pull_requests_list( |
|
68 | 66 | self, repo_name, source, filter_type, opened_by, statuses): |
|
69 | 67 | |
|
70 | 68 | draw, start, limit = self._extract_chunk(self.request) |
|
71 | 69 | search_q, order_by, order_dir = self._extract_ordering(self.request) |
|
72 | 70 | _render = self.request.get_partial_renderer( |
|
73 | 71 | 'data_table/_dt_elements.mako') |
|
74 | 72 | |
|
75 | 73 | # pagination |
|
76 | 74 | |
|
77 | 75 | if filter_type == 'awaiting_review': |
|
78 | 76 | pull_requests = PullRequestModel().get_awaiting_review( |
|
79 | 77 | repo_name, source=source, opened_by=opened_by, |
|
80 | 78 | statuses=statuses, offset=start, length=limit, |
|
81 | 79 | order_by=order_by, order_dir=order_dir) |
|
82 | 80 | pull_requests_total_count = PullRequestModel().count_awaiting_review( |
|
83 | 81 | repo_name, source=source, statuses=statuses, |
|
84 | 82 | opened_by=opened_by) |
|
85 | 83 | elif filter_type == 'awaiting_my_review': |
|
86 | 84 | pull_requests = PullRequestModel().get_awaiting_my_review( |
|
87 | 85 | repo_name, source=source, opened_by=opened_by, |
|
88 | 86 | user_id=self._rhodecode_user.user_id, statuses=statuses, |
|
89 | 87 | offset=start, length=limit, order_by=order_by, |
|
90 | 88 | order_dir=order_dir) |
|
91 | 89 | pull_requests_total_count = PullRequestModel().count_awaiting_my_review( |
|
92 | 90 | repo_name, source=source, user_id=self._rhodecode_user.user_id, |
|
93 | 91 | statuses=statuses, opened_by=opened_by) |
|
94 | 92 | else: |
|
95 | 93 | pull_requests = PullRequestModel().get_all( |
|
96 | 94 | repo_name, source=source, opened_by=opened_by, |
|
97 | 95 | statuses=statuses, offset=start, length=limit, |
|
98 | 96 | order_by=order_by, order_dir=order_dir) |
|
99 | 97 | pull_requests_total_count = PullRequestModel().count_all( |
|
100 | 98 | repo_name, source=source, statuses=statuses, |
|
101 | 99 | opened_by=opened_by) |
|
102 | 100 | |
|
103 | 101 | data = [] |
|
104 | 102 | comments_model = CommentsModel() |
|
105 | 103 | for pr in pull_requests: |
|
106 | 104 | comments = comments_model.get_all_comments( |
|
107 | 105 | self.db_repo.repo_id, pull_request=pr) |
|
108 | 106 | |
|
109 | 107 | data.append({ |
|
110 | 108 | 'name': _render('pullrequest_name', |
|
111 | 109 | pr.pull_request_id, pr.target_repo.repo_name), |
|
112 | 110 | 'name_raw': pr.pull_request_id, |
|
113 | 111 | 'status': _render('pullrequest_status', |
|
114 | 112 | pr.calculated_review_status()), |
|
115 | 113 | 'title': _render( |
|
116 | 114 | 'pullrequest_title', pr.title, pr.description), |
|
117 | 115 | 'description': h.escape(pr.description), |
|
118 | 116 | 'updated_on': _render('pullrequest_updated_on', |
|
119 | 117 | h.datetime_to_time(pr.updated_on)), |
|
120 | 118 | 'updated_on_raw': h.datetime_to_time(pr.updated_on), |
|
121 | 119 | 'created_on': _render('pullrequest_updated_on', |
|
122 | 120 | h.datetime_to_time(pr.created_on)), |
|
123 | 121 | 'created_on_raw': h.datetime_to_time(pr.created_on), |
|
124 | 122 | 'author': _render('pullrequest_author', |
|
125 | 123 | pr.author.full_contact, ), |
|
126 | 124 | 'author_raw': pr.author.full_name, |
|
127 | 125 | 'comments': _render('pullrequest_comments', len(comments)), |
|
128 | 126 | 'comments_raw': len(comments), |
|
129 | 127 | 'closed': pr.is_closed(), |
|
130 | 128 | }) |
|
131 | 129 | |
|
132 | 130 | data = ({ |
|
133 | 131 | 'draw': draw, |
|
134 | 132 | 'data': data, |
|
135 | 133 | 'recordsTotal': pull_requests_total_count, |
|
136 | 134 | 'recordsFiltered': pull_requests_total_count, |
|
137 | 135 | }) |
|
138 | 136 | return data |
|
139 | 137 | |
|
140 | 138 | @LoginRequired() |
|
141 | 139 | @HasRepoPermissionAnyDecorator( |
|
142 | 140 | 'repository.read', 'repository.write', 'repository.admin') |
|
143 | 141 | @view_config( |
|
144 | 142 | route_name='pullrequest_show_all', request_method='GET', |
|
145 | 143 | renderer='rhodecode:templates/pullrequests/pullrequests.mako') |
|
146 | 144 | def pull_request_list(self): |
|
147 | 145 | c = self.load_default_context() |
|
148 | 146 | |
|
149 | 147 | req_get = self.request.GET |
|
150 | 148 | c.source = str2bool(req_get.get('source')) |
|
151 | 149 | c.closed = str2bool(req_get.get('closed')) |
|
152 | 150 | c.my = str2bool(req_get.get('my')) |
|
153 | 151 | c.awaiting_review = str2bool(req_get.get('awaiting_review')) |
|
154 | 152 | c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review')) |
|
155 | 153 | |
|
156 | 154 | c.active = 'open' |
|
157 | 155 | if c.my: |
|
158 | 156 | c.active = 'my' |
|
159 | 157 | if c.closed: |
|
160 | 158 | c.active = 'closed' |
|
161 | 159 | if c.awaiting_review and not c.source: |
|
162 | 160 | c.active = 'awaiting' |
|
163 | 161 | if c.source and not c.awaiting_review: |
|
164 | 162 | c.active = 'source' |
|
165 | 163 | if c.awaiting_my_review: |
|
166 | 164 | c.active = 'awaiting_my' |
|
167 | 165 | |
|
168 | 166 | return self._get_template_context(c) |
|
169 | 167 | |
|
170 | 168 | @LoginRequired() |
|
171 | 169 | @HasRepoPermissionAnyDecorator( |
|
172 | 170 | 'repository.read', 'repository.write', 'repository.admin') |
|
173 | 171 | @view_config( |
|
174 | 172 | route_name='pullrequest_show_all_data', request_method='GET', |
|
175 | 173 | renderer='json_ext', xhr=True) |
|
176 | 174 | def pull_request_list_data(self): |
|
177 | 175 | |
|
178 | 176 | # additional filters |
|
179 | 177 | req_get = self.request.GET |
|
180 | 178 | source = str2bool(req_get.get('source')) |
|
181 | 179 | closed = str2bool(req_get.get('closed')) |
|
182 | 180 | my = str2bool(req_get.get('my')) |
|
183 | 181 | awaiting_review = str2bool(req_get.get('awaiting_review')) |
|
184 | 182 | awaiting_my_review = str2bool(req_get.get('awaiting_my_review')) |
|
185 | 183 | |
|
186 | 184 | filter_type = 'awaiting_review' if awaiting_review \ |
|
187 | 185 | else 'awaiting_my_review' if awaiting_my_review \ |
|
188 | 186 | else None |
|
189 | 187 | |
|
190 | 188 | opened_by = None |
|
191 | 189 | if my: |
|
192 | 190 | opened_by = [self._rhodecode_user.user_id] |
|
193 | 191 | |
|
194 | 192 | statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN] |
|
195 | 193 | if closed: |
|
196 | 194 | statuses = [PullRequest.STATUS_CLOSED] |
|
197 | 195 | |
|
198 | 196 | data = self._get_pull_requests_list( |
|
199 | 197 | repo_name=self.db_repo_name, source=source, |
|
200 | 198 | filter_type=filter_type, opened_by=opened_by, statuses=statuses) |
|
201 | 199 | |
|
202 | 200 | return data |
|
203 | 201 | |
|
204 | 202 | def _get_pr_version(self, pull_request_id, version=None): |
|
205 | 203 | at_version = None |
|
206 | 204 | |
|
207 | 205 | if version and version == 'latest': |
|
208 | 206 | pull_request_ver = PullRequest.get(pull_request_id) |
|
209 | 207 | pull_request_obj = pull_request_ver |
|
210 | 208 | _org_pull_request_obj = pull_request_obj |
|
211 | 209 | at_version = 'latest' |
|
212 | 210 | elif version: |
|
213 | 211 | pull_request_ver = PullRequestVersion.get_or_404(version) |
|
214 | 212 | pull_request_obj = pull_request_ver |
|
215 | 213 | _org_pull_request_obj = pull_request_ver.pull_request |
|
216 | 214 | at_version = pull_request_ver.pull_request_version_id |
|
217 | 215 | else: |
|
218 | 216 | _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404( |
|
219 | 217 | pull_request_id) |
|
220 | 218 | |
|
221 | 219 | pull_request_display_obj = PullRequest.get_pr_display_object( |
|
222 | 220 | pull_request_obj, _org_pull_request_obj) |
|
223 | 221 | |
|
224 | 222 | return _org_pull_request_obj, pull_request_obj, \ |
|
225 | 223 | pull_request_display_obj, at_version |
|
226 | 224 | |
|
227 | 225 | def _get_diffset(self, source_repo_name, source_repo, |
|
228 | 226 | source_ref_id, target_ref_id, |
|
229 | 227 | target_commit, source_commit, diff_limit, fulldiff, |
|
230 | 228 | file_limit, display_inline_comments): |
|
231 | 229 | |
|
232 | 230 | vcs_diff = PullRequestModel().get_diff( |
|
233 | 231 | source_repo, source_ref_id, target_ref_id) |
|
234 | 232 | |
|
235 | 233 | diff_processor = diffs.DiffProcessor( |
|
236 | 234 | vcs_diff, format='newdiff', diff_limit=diff_limit, |
|
237 | 235 | file_limit=file_limit, show_full_diff=fulldiff) |
|
238 | 236 | |
|
239 | 237 | _parsed = diff_processor.prepare() |
|
240 | 238 | |
|
241 | 239 | def _node_getter(commit): |
|
242 | 240 | def get_node(fname): |
|
243 | 241 | try: |
|
244 | 242 | return commit.get_node(fname) |
|
245 | 243 | except NodeDoesNotExistError: |
|
246 | 244 | return None |
|
247 | 245 | |
|
248 | 246 | return get_node |
|
249 | 247 | |
|
250 | 248 | diffset = codeblocks.DiffSet( |
|
251 | 249 | repo_name=self.db_repo_name, |
|
252 | 250 | source_repo_name=source_repo_name, |
|
253 | 251 | source_node_getter=_node_getter(target_commit), |
|
254 | 252 | target_node_getter=_node_getter(source_commit), |
|
255 | 253 | comments=display_inline_comments |
|
256 | 254 | ) |
|
257 | 255 | diffset = diffset.render_patchset( |
|
258 | 256 | _parsed, target_commit.raw_id, source_commit.raw_id) |
|
259 | 257 | |
|
260 | 258 | return diffset |
|
261 | 259 | |
|
262 | 260 | @LoginRequired() |
|
263 | 261 | @HasRepoPermissionAnyDecorator( |
|
264 | 262 | 'repository.read', 'repository.write', 'repository.admin') |
|
265 | 263 | @view_config( |
|
266 | 264 | route_name='pullrequest_show', request_method='GET', |
|
267 | 265 | renderer='rhodecode:templates/pullrequests/pullrequest_show.mako') |
|
268 | 266 | def pull_request_show(self): |
|
269 | 267 | pull_request_id = self.request.matchdict['pull_request_id'] |
|
270 | 268 | |
|
271 | 269 | c = self.load_default_context() |
|
272 | 270 | |
|
273 | 271 | version = self.request.GET.get('version') |
|
274 | 272 | from_version = self.request.GET.get('from_version') or version |
|
275 | 273 | merge_checks = self.request.GET.get('merge_checks') |
|
276 | 274 | c.fulldiff = str2bool(self.request.GET.get('fulldiff')) |
|
277 | 275 | |
|
278 | 276 | (pull_request_latest, |
|
279 | 277 | pull_request_at_ver, |
|
280 | 278 | pull_request_display_obj, |
|
281 | 279 | at_version) = self._get_pr_version( |
|
282 | 280 | pull_request_id, version=version) |
|
283 | 281 | pr_closed = pull_request_latest.is_closed() |
|
284 | 282 | |
|
285 | 283 | if pr_closed and (version or from_version): |
|
286 | 284 | # not allow to browse versions |
|
287 | 285 | raise HTTPFound(h.route_path( |
|
288 | 286 | 'pullrequest_show', repo_name=self.db_repo_name, |
|
289 | 287 | pull_request_id=pull_request_id)) |
|
290 | 288 | |
|
291 | 289 | versions = pull_request_display_obj.versions() |
|
292 | 290 | |
|
293 | 291 | c.at_version = at_version |
|
294 | 292 | c.at_version_num = (at_version |
|
295 | 293 | if at_version and at_version != 'latest' |
|
296 | 294 | else None) |
|
297 | 295 | c.at_version_pos = ChangesetComment.get_index_from_version( |
|
298 | 296 | c.at_version_num, versions) |
|
299 | 297 | |
|
300 | 298 | (prev_pull_request_latest, |
|
301 | 299 | prev_pull_request_at_ver, |
|
302 | 300 | prev_pull_request_display_obj, |
|
303 | 301 | prev_at_version) = self._get_pr_version( |
|
304 | 302 | pull_request_id, version=from_version) |
|
305 | 303 | |
|
306 | 304 | c.from_version = prev_at_version |
|
307 | 305 | c.from_version_num = (prev_at_version |
|
308 | 306 | if prev_at_version and prev_at_version != 'latest' |
|
309 | 307 | else None) |
|
310 | 308 | c.from_version_pos = ChangesetComment.get_index_from_version( |
|
311 | 309 | c.from_version_num, versions) |
|
312 | 310 | |
|
313 | 311 | # define if we're in COMPARE mode or VIEW at version mode |
|
314 | 312 | compare = at_version != prev_at_version |
|
315 | 313 | |
|
316 | 314 | # pull_requests repo_name we opened it against |
|
317 | 315 | # ie. target_repo must match |
|
318 | 316 | if self.db_repo_name != pull_request_at_ver.target_repo.repo_name: |
|
319 | 317 | raise HTTPNotFound() |
|
320 | 318 | |
|
321 | 319 | c.shadow_clone_url = PullRequestModel().get_shadow_clone_url( |
|
322 | 320 | pull_request_at_ver) |
|
323 | 321 | |
|
324 | 322 | c.pull_request = pull_request_display_obj |
|
325 | 323 | c.pull_request_latest = pull_request_latest |
|
326 | 324 | |
|
327 | 325 | if compare or (at_version and not at_version == 'latest'): |
|
328 | 326 | c.allowed_to_change_status = False |
|
329 | 327 | c.allowed_to_update = False |
|
330 | 328 | c.allowed_to_merge = False |
|
331 | 329 | c.allowed_to_delete = False |
|
332 | 330 | c.allowed_to_comment = False |
|
333 | 331 | c.allowed_to_close = False |
|
334 | 332 | else: |
|
335 | 333 | can_change_status = PullRequestModel().check_user_change_status( |
|
336 | 334 | pull_request_at_ver, self._rhodecode_user) |
|
337 | 335 | c.allowed_to_change_status = can_change_status and not pr_closed |
|
338 | 336 | |
|
339 | 337 | c.allowed_to_update = PullRequestModel().check_user_update( |
|
340 | 338 | pull_request_latest, self._rhodecode_user) and not pr_closed |
|
341 | 339 | c.allowed_to_merge = PullRequestModel().check_user_merge( |
|
342 | 340 | pull_request_latest, self._rhodecode_user) and not pr_closed |
|
343 | 341 | c.allowed_to_delete = PullRequestModel().check_user_delete( |
|
344 | 342 | pull_request_latest, self._rhodecode_user) and not pr_closed |
|
345 | 343 | c.allowed_to_comment = not pr_closed |
|
346 | 344 | c.allowed_to_close = c.allowed_to_merge and not pr_closed |
|
347 | 345 | |
|
348 | 346 | c.forbid_adding_reviewers = False |
|
349 | 347 | c.forbid_author_to_review = False |
|
350 | 348 | c.forbid_commit_author_to_review = False |
|
351 | 349 | |
|
352 | 350 | if pull_request_latest.reviewer_data and \ |
|
353 | 351 | 'rules' in pull_request_latest.reviewer_data: |
|
354 | 352 | rules = pull_request_latest.reviewer_data['rules'] or {} |
|
355 | 353 | try: |
|
356 | 354 | c.forbid_adding_reviewers = rules.get( |
|
357 | 355 | 'forbid_adding_reviewers') |
|
358 | 356 | c.forbid_author_to_review = rules.get( |
|
359 | 357 | 'forbid_author_to_review') |
|
360 | 358 | c.forbid_commit_author_to_review = rules.get( |
|
361 | 359 | 'forbid_commit_author_to_review') |
|
362 | 360 | except Exception: |
|
363 | 361 | pass |
|
364 | 362 | |
|
365 | 363 | # check merge capabilities |
|
366 | 364 | _merge_check = MergeCheck.validate( |
|
367 | 365 | pull_request_latest, user=self._rhodecode_user) |
|
368 | 366 | c.pr_merge_errors = _merge_check.error_details |
|
369 | 367 | c.pr_merge_possible = not _merge_check.failed |
|
370 | 368 | c.pr_merge_message = _merge_check.merge_msg |
|
371 | 369 | |
|
372 | 370 | c.pr_merge_info = MergeCheck.get_merge_conditions(pull_request_latest) |
|
373 | 371 | |
|
374 | 372 | c.pull_request_review_status = _merge_check.review_status |
|
375 | 373 | if merge_checks: |
|
376 | 374 | self.request.override_renderer = \ |
|
377 | 375 | 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako' |
|
378 | 376 | return self._get_template_context(c) |
|
379 | 377 | |
|
380 | 378 | comments_model = CommentsModel() |
|
381 | 379 | |
|
382 | 380 | # reviewers and statuses |
|
383 | 381 | c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses() |
|
384 | 382 | allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers] |
|
385 | 383 | |
|
386 | 384 | # GENERAL COMMENTS with versions # |
|
387 | 385 | q = comments_model._all_general_comments_of_pull_request(pull_request_latest) |
|
388 | 386 | q = q.order_by(ChangesetComment.comment_id.asc()) |
|
389 | 387 | general_comments = q |
|
390 | 388 | |
|
391 | 389 | # pick comments we want to render at current version |
|
392 | 390 | c.comment_versions = comments_model.aggregate_comments( |
|
393 | 391 | general_comments, versions, c.at_version_num) |
|
394 | 392 | c.comments = c.comment_versions[c.at_version_num]['until'] |
|
395 | 393 | |
|
396 | 394 | # INLINE COMMENTS with versions # |
|
397 | 395 | q = comments_model._all_inline_comments_of_pull_request(pull_request_latest) |
|
398 | 396 | q = q.order_by(ChangesetComment.comment_id.asc()) |
|
399 | 397 | inline_comments = q |
|
400 | 398 | |
|
401 | 399 | c.inline_versions = comments_model.aggregate_comments( |
|
402 | 400 | inline_comments, versions, c.at_version_num, inline=True) |
|
403 | 401 | |
|
404 | 402 | # inject latest version |
|
405 | 403 | latest_ver = PullRequest.get_pr_display_object( |
|
406 | 404 | pull_request_latest, pull_request_latest) |
|
407 | 405 | |
|
408 | 406 | c.versions = versions + [latest_ver] |
|
409 | 407 | |
|
410 | 408 | # if we use version, then do not show later comments |
|
411 | 409 | # than current version |
|
412 | 410 | display_inline_comments = collections.defaultdict( |
|
413 | 411 | lambda: collections.defaultdict(list)) |
|
414 | 412 | for co in inline_comments: |
|
415 | 413 | if c.at_version_num: |
|
416 | 414 | # pick comments that are at least UPTO given version, so we |
|
417 | 415 | # don't render comments for higher version |
|
418 | 416 | should_render = co.pull_request_version_id and \ |
|
419 | 417 | co.pull_request_version_id <= c.at_version_num |
|
420 | 418 | else: |
|
421 | 419 | # showing all, for 'latest' |
|
422 | 420 | should_render = True |
|
423 | 421 | |
|
424 | 422 | if should_render: |
|
425 | 423 | display_inline_comments[co.f_path][co.line_no].append(co) |
|
426 | 424 | |
|
427 | 425 | # load diff data into template context, if we use compare mode then |
|
428 | 426 | # diff is calculated based on changes between versions of PR |
|
429 | 427 | |
|
430 | 428 | source_repo = pull_request_at_ver.source_repo |
|
431 | 429 | source_ref_id = pull_request_at_ver.source_ref_parts.commit_id |
|
432 | 430 | |
|
433 | 431 | target_repo = pull_request_at_ver.target_repo |
|
434 | 432 | target_ref_id = pull_request_at_ver.target_ref_parts.commit_id |
|
435 | 433 | |
|
436 | 434 | if compare: |
|
437 | 435 | # in compare switch the diff base to latest commit from prev version |
|
438 | 436 | target_ref_id = prev_pull_request_display_obj.revisions[0] |
|
439 | 437 | |
|
440 | 438 | # despite opening commits for bookmarks/branches/tags, we always |
|
441 | 439 | # convert this to rev to prevent changes after bookmark or branch change |
|
442 | 440 | c.source_ref_type = 'rev' |
|
443 | 441 | c.source_ref = source_ref_id |
|
444 | 442 | |
|
445 | 443 | c.target_ref_type = 'rev' |
|
446 | 444 | c.target_ref = target_ref_id |
|
447 | 445 | |
|
448 | 446 | c.source_repo = source_repo |
|
449 | 447 | c.target_repo = target_repo |
|
450 | 448 | |
|
451 | 449 | c.commit_ranges = [] |
|
452 | 450 | source_commit = EmptyCommit() |
|
453 | 451 | target_commit = EmptyCommit() |
|
454 | 452 | c.missing_requirements = False |
|
455 | 453 | |
|
456 | 454 | source_scm = source_repo.scm_instance() |
|
457 | 455 | target_scm = target_repo.scm_instance() |
|
458 | 456 | |
|
459 | 457 | # try first shadow repo, fallback to regular repo |
|
460 | 458 | try: |
|
461 | 459 | commits_source_repo = pull_request_latest.get_shadow_repo() |
|
462 | 460 | except Exception: |
|
463 | 461 | log.debug('Failed to get shadow repo', exc_info=True) |
|
464 | 462 | commits_source_repo = source_scm |
|
465 | 463 | |
|
466 | 464 | c.commits_source_repo = commits_source_repo |
|
467 | 465 | commit_cache = {} |
|
468 | 466 | try: |
|
469 | 467 | pre_load = ["author", "branch", "date", "message"] |
|
470 | 468 | show_revs = pull_request_at_ver.revisions |
|
471 | 469 | for rev in show_revs: |
|
472 | 470 | comm = commits_source_repo.get_commit( |
|
473 | 471 | commit_id=rev, pre_load=pre_load) |
|
474 | 472 | c.commit_ranges.append(comm) |
|
475 | 473 | commit_cache[comm.raw_id] = comm |
|
476 | 474 | |
|
477 | 475 | # Order here matters, we first need to get target, and then |
|
478 | 476 | # the source |
|
479 | 477 | target_commit = commits_source_repo.get_commit( |
|
480 | 478 | commit_id=safe_str(target_ref_id)) |
|
481 | 479 | |
|
482 | 480 | source_commit = commits_source_repo.get_commit( |
|
483 | 481 | commit_id=safe_str(source_ref_id)) |
|
484 | 482 | |
|
485 | 483 | except CommitDoesNotExistError: |
|
486 | 484 | log.warning( |
|
487 | 485 | 'Failed to get commit from `{}` repo'.format( |
|
488 | 486 | commits_source_repo), exc_info=True) |
|
489 | 487 | except RepositoryRequirementError: |
|
490 | 488 | log.warning( |
|
491 | 489 | 'Failed to get all required data from repo', exc_info=True) |
|
492 | 490 | c.missing_requirements = True |
|
493 | 491 | |
|
494 | 492 | c.ancestor = None # set it to None, to hide it from PR view |
|
495 | 493 | |
|
496 | 494 | try: |
|
497 | 495 | ancestor_id = source_scm.get_common_ancestor( |
|
498 | 496 | source_commit.raw_id, target_commit.raw_id, target_scm) |
|
499 | 497 | c.ancestor_commit = source_scm.get_commit(ancestor_id) |
|
500 | 498 | except Exception: |
|
501 | 499 | c.ancestor_commit = None |
|
502 | 500 | |
|
503 | 501 | c.statuses = source_repo.statuses( |
|
504 | 502 | [x.raw_id for x in c.commit_ranges]) |
|
505 | 503 | |
|
506 | 504 | # auto collapse if we have more than limit |
|
507 | 505 | collapse_limit = diffs.DiffProcessor._collapse_commits_over |
|
508 | 506 | c.collapse_all_commits = len(c.commit_ranges) > collapse_limit |
|
509 | 507 | c.compare_mode = compare |
|
510 | 508 | |
|
511 | 509 | # diff_limit is the old behavior, will cut off the whole diff |
|
512 | 510 | # if the limit is applied otherwise will just hide the |
|
513 | 511 | # big files from the front-end |
|
514 | 512 | diff_limit = c.visual.cut_off_limit_diff |
|
515 | 513 | file_limit = c.visual.cut_off_limit_file |
|
516 | 514 | |
|
517 | 515 | c.missing_commits = False |
|
518 | 516 | if (c.missing_requirements |
|
519 | 517 | or isinstance(source_commit, EmptyCommit) |
|
520 | 518 | or source_commit == target_commit): |
|
521 | 519 | |
|
522 | 520 | c.missing_commits = True |
|
523 | 521 | else: |
|
524 | 522 | |
|
525 | 523 | c.diffset = self._get_diffset( |
|
526 | 524 | c.source_repo.repo_name, commits_source_repo, |
|
527 | 525 | source_ref_id, target_ref_id, |
|
528 | 526 | target_commit, source_commit, |
|
529 | 527 | diff_limit, c.fulldiff, file_limit, display_inline_comments) |
|
530 | 528 | |
|
531 | 529 | c.limited_diff = c.diffset.limited_diff |
|
532 | 530 | |
|
533 | 531 | # calculate removed files that are bound to comments |
|
534 | 532 | comment_deleted_files = [ |
|
535 | 533 | fname for fname in display_inline_comments |
|
536 | 534 | if fname not in c.diffset.file_stats] |
|
537 | 535 | |
|
538 | 536 | c.deleted_files_comments = collections.defaultdict(dict) |
|
539 | 537 | for fname, per_line_comments in display_inline_comments.items(): |
|
540 | 538 | if fname in comment_deleted_files: |
|
541 | 539 | c.deleted_files_comments[fname]['stats'] = 0 |
|
542 | 540 | c.deleted_files_comments[fname]['comments'] = list() |
|
543 | 541 | for lno, comments in per_line_comments.items(): |
|
544 | 542 | c.deleted_files_comments[fname]['comments'].extend( |
|
545 | 543 | comments) |
|
546 | 544 | |
|
547 | 545 | # this is a hack to properly display links, when creating PR, the |
|
548 | 546 | # compare view and others uses different notation, and |
|
549 | 547 | # compare_commits.mako renders links based on the target_repo. |
|
550 | 548 | # We need to swap that here to generate it properly on the html side |
|
551 | 549 | c.target_repo = c.source_repo |
|
552 | 550 | |
|
553 | 551 | c.commit_statuses = ChangesetStatus.STATUSES |
|
554 | 552 | |
|
555 | 553 | c.show_version_changes = not pr_closed |
|
556 | 554 | if c.show_version_changes: |
|
557 | 555 | cur_obj = pull_request_at_ver |
|
558 | 556 | prev_obj = prev_pull_request_at_ver |
|
559 | 557 | |
|
560 | 558 | old_commit_ids = prev_obj.revisions |
|
561 | 559 | new_commit_ids = cur_obj.revisions |
|
562 | 560 | commit_changes = PullRequestModel()._calculate_commit_id_changes( |
|
563 | 561 | old_commit_ids, new_commit_ids) |
|
564 | 562 | c.commit_changes_summary = commit_changes |
|
565 | 563 | |
|
566 | 564 | # calculate the diff for commits between versions |
|
567 | 565 | c.commit_changes = [] |
|
568 | 566 | mark = lambda cs, fw: list( |
|
569 | 567 | h.itertools.izip_longest([], cs, fillvalue=fw)) |
|
570 | 568 | for c_type, raw_id in mark(commit_changes.added, 'a') \ |
|
571 | 569 | + mark(commit_changes.removed, 'r') \ |
|
572 | 570 | + mark(commit_changes.common, 'c'): |
|
573 | 571 | |
|
574 | 572 | if raw_id in commit_cache: |
|
575 | 573 | commit = commit_cache[raw_id] |
|
576 | 574 | else: |
|
577 | 575 | try: |
|
578 | 576 | commit = commits_source_repo.get_commit(raw_id) |
|
579 | 577 | except CommitDoesNotExistError: |
|
580 | 578 | # in case we fail extracting still use "dummy" commit |
|
581 | 579 | # for display in commit diff |
|
582 | 580 | commit = h.AttributeDict( |
|
583 | 581 | {'raw_id': raw_id, |
|
584 | 582 | 'message': 'EMPTY or MISSING COMMIT'}) |
|
585 | 583 | c.commit_changes.append([c_type, commit]) |
|
586 | 584 | |
|
587 | 585 | # current user review statuses for each version |
|
588 | 586 | c.review_versions = {} |
|
589 | 587 | if self._rhodecode_user.user_id in allowed_reviewers: |
|
590 | 588 | for co in general_comments: |
|
591 | 589 | if co.author.user_id == self._rhodecode_user.user_id: |
|
592 | 590 | # each comment has a status change |
|
593 | 591 | status = co.status_change |
|
594 | 592 | if status: |
|
595 | 593 | _ver_pr = status[0].comment.pull_request_version_id |
|
596 | 594 | c.review_versions[_ver_pr] = status[0] |
|
597 | 595 | |
|
598 | 596 | return self._get_template_context(c) |
|
599 | 597 | |
|
600 | 598 | def assure_not_empty_repo(self): |
|
601 | 599 | _ = self.request.translate |
|
602 | 600 | |
|
603 | 601 | try: |
|
604 | 602 | self.db_repo.scm_instance().get_commit() |
|
605 | 603 | except EmptyRepositoryError: |
|
606 | 604 | h.flash(h.literal(_('There are no commits yet')), |
|
607 | 605 | category='warning') |
|
608 | 606 | raise HTTPFound( |
|
609 | 607 | h.route_path('repo_summary', repo_name=self.db_repo.repo_name)) |
|
610 | 608 | |
|
611 | 609 | @LoginRequired() |
|
612 | 610 | @NotAnonymous() |
|
613 | 611 | @HasRepoPermissionAnyDecorator( |
|
614 | 612 | 'repository.read', 'repository.write', 'repository.admin') |
|
615 | 613 | @view_config( |
|
616 | 614 | route_name='pullrequest_new', request_method='GET', |
|
617 | 615 | renderer='rhodecode:templates/pullrequests/pullrequest.mako') |
|
618 | 616 | def pull_request_new(self): |
|
619 | 617 | _ = self.request.translate |
|
620 | 618 | c = self.load_default_context() |
|
621 | 619 | |
|
622 | 620 | self.assure_not_empty_repo() |
|
623 | 621 | source_repo = self.db_repo |
|
624 | 622 | |
|
625 | 623 | commit_id = self.request.GET.get('commit') |
|
626 | 624 | branch_ref = self.request.GET.get('branch') |
|
627 | 625 | bookmark_ref = self.request.GET.get('bookmark') |
|
628 | 626 | |
|
629 | 627 | try: |
|
630 | 628 | source_repo_data = PullRequestModel().generate_repo_data( |
|
631 | 629 | source_repo, commit_id=commit_id, |
|
632 | 630 | branch=branch_ref, bookmark=bookmark_ref) |
|
633 | 631 | except CommitDoesNotExistError as e: |
|
634 | 632 | log.exception(e) |
|
635 | 633 | h.flash(_('Commit does not exist'), 'error') |
|
636 | 634 | raise HTTPFound( |
|
637 | 635 | h.route_path('pullrequest_new', repo_name=source_repo.repo_name)) |
|
638 | 636 | |
|
639 | 637 | default_target_repo = source_repo |
|
640 | 638 | |
|
641 | 639 | if source_repo.parent: |
|
642 | 640 | parent_vcs_obj = source_repo.parent.scm_instance() |
|
643 | 641 | if parent_vcs_obj and not parent_vcs_obj.is_empty(): |
|
644 | 642 | # change default if we have a parent repo |
|
645 | 643 | default_target_repo = source_repo.parent |
|
646 | 644 | |
|
647 | 645 | target_repo_data = PullRequestModel().generate_repo_data( |
|
648 | 646 | default_target_repo) |
|
649 | 647 | |
|
650 | 648 | selected_source_ref = source_repo_data['refs']['selected_ref'] |
|
651 | 649 | |
|
652 | 650 | title_source_ref = selected_source_ref.split(':', 2)[1] |
|
653 | 651 | c.default_title = PullRequestModel().generate_pullrequest_title( |
|
654 | 652 | source=source_repo.repo_name, |
|
655 | 653 | source_ref=title_source_ref, |
|
656 | 654 | target=default_target_repo.repo_name |
|
657 | 655 | ) |
|
658 | 656 | |
|
659 | 657 | c.default_repo_data = { |
|
660 | 658 | 'source_repo_name': source_repo.repo_name, |
|
661 | 659 | 'source_refs_json': json.dumps(source_repo_data), |
|
662 | 660 | 'target_repo_name': default_target_repo.repo_name, |
|
663 | 661 | 'target_refs_json': json.dumps(target_repo_data), |
|
664 | 662 | } |
|
665 | 663 | c.default_source_ref = selected_source_ref |
|
666 | 664 | |
|
667 | 665 | return self._get_template_context(c) |
|
668 | 666 | |
|
669 | 667 | @LoginRequired() |
|
670 | 668 | @NotAnonymous() |
|
671 | 669 | @HasRepoPermissionAnyDecorator( |
|
672 | 670 | 'repository.read', 'repository.write', 'repository.admin') |
|
673 | 671 | @view_config( |
|
674 | 672 | route_name='pullrequest_repo_refs', request_method='GET', |
|
675 | 673 | renderer='json_ext', xhr=True) |
|
676 | 674 | def pull_request_repo_refs(self): |
|
677 | 675 | target_repo_name = self.request.matchdict['target_repo_name'] |
|
678 | 676 | repo = Repository.get_by_repo_name(target_repo_name) |
|
679 | 677 | if not repo: |
|
680 | 678 | raise HTTPNotFound() |
|
681 | 679 | return PullRequestModel().generate_repo_data(repo) |
|
682 | 680 | |
|
683 | 681 | @LoginRequired() |
|
684 | 682 | @NotAnonymous() |
|
685 | 683 | @HasRepoPermissionAnyDecorator( |
|
686 | 684 | 'repository.read', 'repository.write', 'repository.admin') |
|
687 | 685 | @view_config( |
|
688 | 686 | route_name='pullrequest_repo_destinations', request_method='GET', |
|
689 | 687 | renderer='json_ext', xhr=True) |
|
690 | 688 | def pull_request_repo_destinations(self): |
|
691 | 689 | _ = self.request.translate |
|
692 | 690 | filter_query = self.request.GET.get('query') |
|
693 | 691 | |
|
694 | 692 | query = Repository.query() \ |
|
695 | 693 | .order_by(func.length(Repository.repo_name)) \ |
|
696 | 694 | .filter( |
|
697 | 695 | or_(Repository.repo_name == self.db_repo.repo_name, |
|
698 | 696 | Repository.fork_id == self.db_repo.repo_id)) |
|
699 | 697 | |
|
700 | 698 | if filter_query: |
|
701 | 699 | ilike_expression = u'%{}%'.format(safe_unicode(filter_query)) |
|
702 | 700 | query = query.filter( |
|
703 | 701 | Repository.repo_name.ilike(ilike_expression)) |
|
704 | 702 | |
|
705 | 703 | add_parent = False |
|
706 | 704 | if self.db_repo.parent: |
|
707 | 705 | if filter_query in self.db_repo.parent.repo_name: |
|
708 | 706 | parent_vcs_obj = self.db_repo.parent.scm_instance() |
|
709 | 707 | if parent_vcs_obj and not parent_vcs_obj.is_empty(): |
|
710 | 708 | add_parent = True |
|
711 | 709 | |
|
712 | 710 | limit = 20 - 1 if add_parent else 20 |
|
713 | 711 | all_repos = query.limit(limit).all() |
|
714 | 712 | if add_parent: |
|
715 | 713 | all_repos += [self.db_repo.parent] |
|
716 | 714 | |
|
717 | 715 | repos = [] |
|
718 | 716 | for obj in ScmModel().get_repos(all_repos): |
|
719 | 717 | repos.append({ |
|
720 | 718 | 'id': obj['name'], |
|
721 | 719 | 'text': obj['name'], |
|
722 | 720 | 'type': 'repo', |
|
723 | 721 | 'obj': obj['dbrepo'] |
|
724 | 722 | }) |
|
725 | 723 | |
|
726 | 724 | data = { |
|
727 | 725 | 'more': False, |
|
728 | 726 | 'results': [{ |
|
729 | 727 | 'text': _('Repositories'), |
|
730 | 728 | 'children': repos |
|
731 | 729 | }] if repos else [] |
|
732 | 730 | } |
|
733 | 731 | return data |
|
734 | 732 | |
|
735 | 733 | @LoginRequired() |
|
736 | 734 | @NotAnonymous() |
|
737 | 735 | @HasRepoPermissionAnyDecorator( |
|
738 | 736 | 'repository.read', 'repository.write', 'repository.admin') |
|
739 | 737 | @CSRFRequired() |
|
740 | 738 | @view_config( |
|
741 | 739 | route_name='pullrequest_create', request_method='POST', |
|
742 | 740 | renderer=None) |
|
743 | 741 | def pull_request_create(self): |
|
744 | 742 | _ = self.request.translate |
|
745 | 743 | self.assure_not_empty_repo() |
|
746 | 744 | |
|
747 | 745 | controls = peppercorn.parse(self.request.POST.items()) |
|
748 | 746 | |
|
749 | 747 | try: |
|
750 | 748 | _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls) |
|
751 | 749 | except formencode.Invalid as errors: |
|
752 | 750 | if errors.error_dict.get('revisions'): |
|
753 | 751 | msg = 'Revisions: %s' % errors.error_dict['revisions'] |
|
754 | 752 | elif errors.error_dict.get('pullrequest_title'): |
|
755 | 753 | msg = _('Pull request requires a title with min. 3 chars') |
|
756 | 754 | else: |
|
757 | 755 | msg = _('Error creating pull request: {}').format(errors) |
|
758 | 756 | log.exception(msg) |
|
759 | 757 | h.flash(msg, 'error') |
|
760 | 758 | |
|
761 | 759 | # would rather just go back to form ... |
|
762 | 760 | raise HTTPFound( |
|
763 | 761 | h.route_path('pullrequest_new', repo_name=self.db_repo_name)) |
|
764 | 762 | |
|
765 | 763 | source_repo = _form['source_repo'] |
|
766 | 764 | source_ref = _form['source_ref'] |
|
767 | 765 | target_repo = _form['target_repo'] |
|
768 | 766 | target_ref = _form['target_ref'] |
|
769 | 767 | commit_ids = _form['revisions'][::-1] |
|
770 | 768 | |
|
771 | 769 | # find the ancestor for this pr |
|
772 | 770 | source_db_repo = Repository.get_by_repo_name(_form['source_repo']) |
|
773 | 771 | target_db_repo = Repository.get_by_repo_name(_form['target_repo']) |
|
774 | 772 | |
|
775 | 773 | source_scm = source_db_repo.scm_instance() |
|
776 | 774 | target_scm = target_db_repo.scm_instance() |
|
777 | 775 | |
|
778 | 776 | source_commit = source_scm.get_commit(source_ref.split(':')[-1]) |
|
779 | 777 | target_commit = target_scm.get_commit(target_ref.split(':')[-1]) |
|
780 | 778 | |
|
781 | 779 | ancestor = source_scm.get_common_ancestor( |
|
782 | 780 | source_commit.raw_id, target_commit.raw_id, target_scm) |
|
783 | 781 | |
|
784 | 782 | target_ref_type, target_ref_name, __ = _form['target_ref'].split(':') |
|
785 | 783 | target_ref = ':'.join((target_ref_type, target_ref_name, ancestor)) |
|
786 | 784 | |
|
787 | 785 | pullrequest_title = _form['pullrequest_title'] |
|
788 | 786 | title_source_ref = source_ref.split(':', 2)[1] |
|
789 | 787 | if not pullrequest_title: |
|
790 | 788 | pullrequest_title = PullRequestModel().generate_pullrequest_title( |
|
791 | 789 | source=source_repo, |
|
792 | 790 | source_ref=title_source_ref, |
|
793 | 791 | target=target_repo |
|
794 | 792 | ) |
|
795 | 793 | |
|
796 | 794 | description = _form['pullrequest_desc'] |
|
797 | 795 | |
|
798 | 796 | get_default_reviewers_data, validate_default_reviewers = \ |
|
799 | 797 | PullRequestModel().get_reviewer_functions() |
|
800 | 798 | |
|
801 | 799 | # recalculate reviewers logic, to make sure we can validate this |
|
802 | 800 | reviewer_rules = get_default_reviewers_data( |
|
803 | 801 | self._rhodecode_db_user, source_db_repo, |
|
804 | 802 | source_commit, target_db_repo, target_commit) |
|
805 | 803 | |
|
806 | 804 | given_reviewers = _form['review_members'] |
|
807 | 805 | reviewers = validate_default_reviewers(given_reviewers, reviewer_rules) |
|
808 | 806 | |
|
809 | 807 | try: |
|
810 | 808 | pull_request = PullRequestModel().create( |
|
811 | 809 | self._rhodecode_user.user_id, source_repo, source_ref, target_repo, |
|
812 | 810 | target_ref, commit_ids, reviewers, pullrequest_title, |
|
813 | 811 | description, reviewer_rules |
|
814 | 812 | ) |
|
815 | 813 | Session().commit() |
|
816 | 814 | h.flash(_('Successfully opened new pull request'), |
|
817 | 815 | category='success') |
|
818 | 816 | except Exception: |
|
819 | 817 | msg = _('Error occurred during creation of this pull request.') |
|
820 | 818 | log.exception(msg) |
|
821 | 819 | h.flash(msg, category='error') |
|
822 | 820 | |
|
823 | 821 | # copy the args back to redirect |
|
824 | 822 | org_query = self.request.GET.mixed() |
|
825 | 823 | raise HTTPFound( |
|
826 | 824 | h.route_path('pullrequest_new', repo_name=self.db_repo_name, |
|
827 | 825 | _query=org_query)) |
|
828 | 826 | |
|
829 | 827 | raise HTTPFound( |
|
830 | 828 | h.route_path('pullrequest_show', repo_name=target_repo, |
|
831 | 829 | pull_request_id=pull_request.pull_request_id)) |
|
832 | 830 | |
|
833 | 831 | @LoginRequired() |
|
834 | 832 | @NotAnonymous() |
|
835 | 833 | @HasRepoPermissionAnyDecorator( |
|
836 | 834 | 'repository.read', 'repository.write', 'repository.admin') |
|
837 | 835 | @CSRFRequired() |
|
838 | 836 | @view_config( |
|
839 | 837 | route_name='pullrequest_update', request_method='POST', |
|
840 | 838 | renderer='json_ext') |
|
841 | 839 | def pull_request_update(self): |
|
842 | 840 | pull_request = PullRequest.get_or_404( |
|
843 | 841 | self.request.matchdict['pull_request_id']) |
|
844 | 842 | |
|
845 | 843 | # only owner or admin can update it |
|
846 | 844 | allowed_to_update = PullRequestModel().check_user_update( |
|
847 | 845 | pull_request, self._rhodecode_user) |
|
848 | 846 | if allowed_to_update: |
|
849 | 847 | controls = peppercorn.parse(self.request.POST.items()) |
|
850 | 848 | |
|
851 | 849 | if 'review_members' in controls: |
|
852 | 850 | self._update_reviewers( |
|
853 | 851 | pull_request, controls['review_members'], |
|
854 | 852 | pull_request.reviewer_data) |
|
855 | 853 | elif str2bool(self.request.POST.get('update_commits', 'false')): |
|
856 | 854 | self._update_commits(pull_request) |
|
857 | 855 | elif str2bool(self.request.POST.get('edit_pull_request', 'false')): |
|
858 | 856 | self._edit_pull_request(pull_request) |
|
859 | 857 | else: |
|
860 | 858 | raise HTTPBadRequest() |
|
861 | 859 | return True |
|
862 | 860 | raise HTTPForbidden() |
|
863 | 861 | |
|
864 | 862 | def _edit_pull_request(self, pull_request): |
|
865 | 863 | _ = self.request.translate |
|
866 | 864 | try: |
|
867 | 865 | PullRequestModel().edit( |
|
868 | 866 | pull_request, self.request.POST.get('title'), |
|
869 | 867 | self.request.POST.get('description'), self._rhodecode_user) |
|
870 | 868 | except ValueError: |
|
871 | 869 | msg = _(u'Cannot update closed pull requests.') |
|
872 | 870 | h.flash(msg, category='error') |
|
873 | 871 | return |
|
874 | 872 | else: |
|
875 | 873 | Session().commit() |
|
876 | 874 | |
|
877 | 875 | msg = _(u'Pull request title & description updated.') |
|
878 | 876 | h.flash(msg, category='success') |
|
879 | 877 | return |
|
880 | 878 | |
|
881 | 879 | def _update_commits(self, pull_request): |
|
882 | 880 | _ = self.request.translate |
|
883 | 881 | resp = PullRequestModel().update_commits(pull_request) |
|
884 | 882 | |
|
885 | 883 | if resp.executed: |
|
886 | 884 | |
|
887 | 885 | if resp.target_changed and resp.source_changed: |
|
888 | 886 | changed = 'target and source repositories' |
|
889 | 887 | elif resp.target_changed and not resp.source_changed: |
|
890 | 888 | changed = 'target repository' |
|
891 | 889 | elif not resp.target_changed and resp.source_changed: |
|
892 | 890 | changed = 'source repository' |
|
893 | 891 | else: |
|
894 | 892 | changed = 'nothing' |
|
895 | 893 | |
|
896 | 894 | msg = _( |
|
897 | 895 | u'Pull request updated to "{source_commit_id}" with ' |
|
898 | 896 | u'{count_added} added, {count_removed} removed commits. ' |
|
899 | 897 | u'Source of changes: {change_source}') |
|
900 | 898 | msg = msg.format( |
|
901 | 899 | source_commit_id=pull_request.source_ref_parts.commit_id, |
|
902 | 900 | count_added=len(resp.changes.added), |
|
903 | 901 | count_removed=len(resp.changes.removed), |
|
904 | 902 | change_source=changed) |
|
905 | 903 | h.flash(msg, category='success') |
|
906 | 904 | |
|
907 | 905 | channel = '/repo${}$/pr/{}'.format( |
|
908 | 906 | pull_request.target_repo.repo_name, |
|
909 | 907 | pull_request.pull_request_id) |
|
910 | 908 | message = msg + ( |
|
911 | 909 | ' - <a onclick="window.location.reload()">' |
|
912 | 910 | '<strong>{}</strong></a>'.format(_('Reload page'))) |
|
913 | 911 | channelstream.post_message( |
|
914 | 912 | channel, message, self._rhodecode_user.username, |
|
915 | 913 | registry=self.request.registry) |
|
916 | 914 | else: |
|
917 | 915 | msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason] |
|
918 | 916 | warning_reasons = [ |
|
919 | 917 | UpdateFailureReason.NO_CHANGE, |
|
920 | 918 | UpdateFailureReason.WRONG_REF_TYPE, |
|
921 | 919 | ] |
|
922 | 920 | category = 'warning' if resp.reason in warning_reasons else 'error' |
|
923 | 921 | h.flash(msg, category=category) |
|
924 | 922 | |
|
925 | 923 | @LoginRequired() |
|
926 | 924 | @NotAnonymous() |
|
927 | 925 | @HasRepoPermissionAnyDecorator( |
|
928 | 926 | 'repository.read', 'repository.write', 'repository.admin') |
|
929 | 927 | @CSRFRequired() |
|
930 | 928 | @view_config( |
|
931 | 929 | route_name='pullrequest_merge', request_method='POST', |
|
932 | 930 | renderer='json_ext') |
|
933 | 931 | def pull_request_merge(self): |
|
934 | 932 | """ |
|
935 | 933 | Merge will perform a server-side merge of the specified |
|
936 | 934 | pull request, if the pull request is approved and mergeable. |
|
937 | 935 | After successful merging, the pull request is automatically |
|
938 | 936 | closed, with a relevant comment. |
|
939 | 937 | """ |
|
940 | 938 | pull_request = PullRequest.get_or_404( |
|
941 | 939 | self.request.matchdict['pull_request_id']) |
|
942 | 940 | |
|
943 | 941 | check = MergeCheck.validate(pull_request, self._rhodecode_db_user) |
|
944 | 942 | merge_possible = not check.failed |
|
945 | 943 | |
|
946 | 944 | for err_type, error_msg in check.errors: |
|
947 | 945 | h.flash(error_msg, category=err_type) |
|
948 | 946 | |
|
949 | 947 | if merge_possible: |
|
950 | 948 | log.debug("Pre-conditions checked, trying to merge.") |
|
951 | 949 | extras = vcs_operation_context( |
|
952 | 950 | self.request.environ, repo_name=pull_request.target_repo.repo_name, |
|
953 | 951 | username=self._rhodecode_db_user.username, action='push', |
|
954 | 952 | scm=pull_request.target_repo.repo_type) |
|
955 | 953 | self._merge_pull_request( |
|
956 | 954 | pull_request, self._rhodecode_db_user, extras) |
|
957 | 955 | else: |
|
958 | 956 | log.debug("Pre-conditions failed, NOT merging.") |
|
959 | 957 | |
|
960 | 958 | raise HTTPFound( |
|
961 | 959 | h.route_path('pullrequest_show', |
|
962 | 960 | repo_name=pull_request.target_repo.repo_name, |
|
963 | 961 | pull_request_id=pull_request.pull_request_id)) |
|
964 | 962 | |
|
965 | 963 | def _merge_pull_request(self, pull_request, user, extras): |
|
966 | 964 | _ = self.request.translate |
|
967 | 965 | merge_resp = PullRequestModel().merge(pull_request, user, extras=extras) |
|
968 | 966 | |
|
969 | 967 | if merge_resp.executed: |
|
970 | 968 | log.debug("The merge was successful, closing the pull request.") |
|
971 | 969 | PullRequestModel().close_pull_request( |
|
972 | 970 | pull_request.pull_request_id, user) |
|
973 | 971 | Session().commit() |
|
974 | 972 | msg = _('Pull request was successfully merged and closed.') |
|
975 | 973 | h.flash(msg, category='success') |
|
976 | 974 | else: |
|
977 | 975 | log.debug( |
|
978 | 976 | "The merge was not successful. Merge response: %s", |
|
979 | 977 | merge_resp) |
|
980 | 978 | msg = PullRequestModel().merge_status_message( |
|
981 | 979 | merge_resp.failure_reason) |
|
982 | 980 | h.flash(msg, category='error') |
|
983 | 981 | |
|
984 | 982 | def _update_reviewers(self, pull_request, review_members, reviewer_rules): |
|
985 | 983 | _ = self.request.translate |
|
986 | 984 | get_default_reviewers_data, validate_default_reviewers = \ |
|
987 | 985 | PullRequestModel().get_reviewer_functions() |
|
988 | 986 | |
|
989 | 987 | try: |
|
990 | 988 | reviewers = validate_default_reviewers(review_members, reviewer_rules) |
|
991 | 989 | except ValueError as e: |
|
992 | 990 | log.error('Reviewers Validation: {}'.format(e)) |
|
993 | 991 | h.flash(e, category='error') |
|
994 | 992 | return |
|
995 | 993 | |
|
996 | 994 | PullRequestModel().update_reviewers( |
|
997 | 995 | pull_request, reviewers, self._rhodecode_user) |
|
998 | 996 | h.flash(_('Pull request reviewers updated.'), category='success') |
|
999 | 997 | Session().commit() |
|
1000 | 998 | |
|
1001 | 999 | @LoginRequired() |
|
1002 | 1000 | @NotAnonymous() |
|
1003 | 1001 | @HasRepoPermissionAnyDecorator( |
|
1004 | 1002 | 'repository.read', 'repository.write', 'repository.admin') |
|
1005 | 1003 | @CSRFRequired() |
|
1006 | 1004 | @view_config( |
|
1007 | 1005 | route_name='pullrequest_delete', request_method='POST', |
|
1008 | 1006 | renderer='json_ext') |
|
1009 | 1007 | def pull_request_delete(self): |
|
1010 | 1008 | _ = self.request.translate |
|
1011 | 1009 | |
|
1012 | 1010 | pull_request = PullRequest.get_or_404( |
|
1013 | 1011 | self.request.matchdict['pull_request_id']) |
|
1014 | 1012 | |
|
1015 | 1013 | pr_closed = pull_request.is_closed() |
|
1016 | 1014 | allowed_to_delete = PullRequestModel().check_user_delete( |
|
1017 | 1015 | pull_request, self._rhodecode_user) and not pr_closed |
|
1018 | 1016 | |
|
1019 | 1017 | # only owner can delete it ! |
|
1020 | 1018 | if allowed_to_delete: |
|
1021 | 1019 | PullRequestModel().delete(pull_request, self._rhodecode_user) |
|
1022 | 1020 | Session().commit() |
|
1023 | 1021 | h.flash(_('Successfully deleted pull request'), |
|
1024 | 1022 | category='success') |
|
1025 | 1023 | raise HTTPFound(h.route_path('pullrequest_show_all', |
|
1026 | 1024 | repo_name=self.db_repo_name)) |
|
1027 | 1025 | |
|
1028 | 1026 | log.warning('user %s tried to delete pull request without access', |
|
1029 | 1027 | self._rhodecode_user) |
|
1030 | 1028 | raise HTTPNotFound() |
|
1031 | 1029 | |
|
1032 | 1030 | @LoginRequired() |
|
1033 | 1031 | @NotAnonymous() |
|
1034 | 1032 | @HasRepoPermissionAnyDecorator( |
|
1035 | 1033 | 'repository.read', 'repository.write', 'repository.admin') |
|
1036 | 1034 | @CSRFRequired() |
|
1037 | 1035 | @view_config( |
|
1038 | 1036 | route_name='pullrequest_comment_create', request_method='POST', |
|
1039 | 1037 | renderer='json_ext') |
|
1040 | 1038 | def pull_request_comment_create(self): |
|
1041 | 1039 | _ = self.request.translate |
|
1042 | 1040 | |
|
1043 | 1041 | pull_request = PullRequest.get_or_404( |
|
1044 | 1042 | self.request.matchdict['pull_request_id']) |
|
1045 | 1043 | pull_request_id = pull_request.pull_request_id |
|
1046 | 1044 | |
|
1047 | 1045 | if pull_request.is_closed(): |
|
1048 | 1046 | log.debug('comment: forbidden because pull request is closed') |
|
1049 | 1047 | raise HTTPForbidden() |
|
1050 | 1048 | |
|
1051 | 1049 | c = self.load_default_context() |
|
1052 | 1050 | |
|
1053 | 1051 | status = self.request.POST.get('changeset_status', None) |
|
1054 | 1052 | text = self.request.POST.get('text') |
|
1055 | 1053 | comment_type = self.request.POST.get('comment_type') |
|
1056 | 1054 | resolves_comment_id = self.request.POST.get('resolves_comment_id', None) |
|
1057 | 1055 | close_pull_request = self.request.POST.get('close_pull_request') |
|
1058 | 1056 | |
|
1059 | 1057 | # the logic here should work like following, if we submit close |
|
1060 | 1058 | # pr comment, use `close_pull_request_with_comment` function |
|
1061 | 1059 | # else handle regular comment logic |
|
1062 | 1060 | |
|
1063 | 1061 | if close_pull_request: |
|
1064 | 1062 | # only owner or admin or person with write permissions |
|
1065 | 1063 | allowed_to_close = PullRequestModel().check_user_update( |
|
1066 | 1064 | pull_request, self._rhodecode_user) |
|
1067 | 1065 | if not allowed_to_close: |
|
1068 | 1066 | log.debug('comment: forbidden because not allowed to close ' |
|
1069 | 1067 | 'pull request %s', pull_request_id) |
|
1070 | 1068 | raise HTTPForbidden() |
|
1071 | 1069 | comment, status = PullRequestModel().close_pull_request_with_comment( |
|
1072 | 1070 | pull_request, self._rhodecode_user, self.db_repo, message=text) |
|
1073 | 1071 | Session().flush() |
|
1074 | 1072 | events.trigger( |
|
1075 | 1073 | events.PullRequestCommentEvent(pull_request, comment)) |
|
1076 | 1074 | |
|
1077 | 1075 | else: |
|
1078 | 1076 | # regular comment case, could be inline, or one with status. |
|
1079 | 1077 | # for that one we check also permissions |
|
1080 | 1078 | |
|
1081 | 1079 | allowed_to_change_status = PullRequestModel().check_user_change_status( |
|
1082 | 1080 | pull_request, self._rhodecode_user) |
|
1083 | 1081 | |
|
1084 | 1082 | if status and allowed_to_change_status: |
|
1085 | 1083 | message = (_('Status change %(transition_icon)s %(status)s') |
|
1086 | 1084 | % {'transition_icon': '>', |
|
1087 | 1085 | 'status': ChangesetStatus.get_status_lbl(status)}) |
|
1088 | 1086 | text = text or message |
|
1089 | 1087 | |
|
1090 | 1088 | comment = CommentsModel().create( |
|
1091 | 1089 | text=text, |
|
1092 | 1090 | repo=self.db_repo.repo_id, |
|
1093 | 1091 | user=self._rhodecode_user.user_id, |
|
1094 | 1092 | pull_request=pull_request, |
|
1095 | 1093 | f_path=self.request.POST.get('f_path'), |
|
1096 | 1094 | line_no=self.request.POST.get('line'), |
|
1097 | 1095 | status_change=(ChangesetStatus.get_status_lbl(status) |
|
1098 | 1096 | if status and allowed_to_change_status else None), |
|
1099 | 1097 | status_change_type=(status |
|
1100 | 1098 | if status and allowed_to_change_status else None), |
|
1101 | 1099 | comment_type=comment_type, |
|
1102 | 1100 | resolves_comment_id=resolves_comment_id |
|
1103 | 1101 | ) |
|
1104 | 1102 | |
|
1105 | 1103 | if allowed_to_change_status: |
|
1106 | 1104 | # calculate old status before we change it |
|
1107 | 1105 | old_calculated_status = pull_request.calculated_review_status() |
|
1108 | 1106 | |
|
1109 | 1107 | # get status if set ! |
|
1110 | 1108 | if status: |
|
1111 | 1109 | ChangesetStatusModel().set_status( |
|
1112 | 1110 | self.db_repo.repo_id, |
|
1113 | 1111 | status, |
|
1114 | 1112 | self._rhodecode_user.user_id, |
|
1115 | 1113 | comment, |
|
1116 | 1114 | pull_request=pull_request |
|
1117 | 1115 | ) |
|
1118 | 1116 | |
|
1119 | 1117 | Session().flush() |
|
1120 | 1118 | events.trigger( |
|
1121 | 1119 | events.PullRequestCommentEvent(pull_request, comment)) |
|
1122 | 1120 | |
|
1123 | 1121 | # we now calculate the status of pull request, and based on that |
|
1124 | 1122 | # calculation we set the commits status |
|
1125 | 1123 | calculated_status = pull_request.calculated_review_status() |
|
1126 | 1124 | if old_calculated_status != calculated_status: |
|
1127 | 1125 | PullRequestModel()._trigger_pull_request_hook( |
|
1128 | 1126 | pull_request, self._rhodecode_user, 'review_status_change') |
|
1129 | 1127 | |
|
1130 | 1128 | Session().commit() |
|
1131 | 1129 | |
|
1132 | 1130 | data = { |
|
1133 | 1131 | 'target_id': h.safeid(h.safe_unicode( |
|
1134 | 1132 | self.request.POST.get('f_path'))), |
|
1135 | 1133 | } |
|
1136 | 1134 | if comment: |
|
1137 | 1135 | c.co = comment |
|
1138 | 1136 | rendered_comment = render( |
|
1139 | 1137 | 'rhodecode:templates/changeset/changeset_comment_block.mako', |
|
1140 | 1138 | self._get_template_context(c), self.request) |
|
1141 | 1139 | |
|
1142 | 1140 | data.update(comment.get_dict()) |
|
1143 | 1141 | data.update({'rendered_text': rendered_comment}) |
|
1144 | 1142 | |
|
1145 | 1143 | return data |
|
1146 | 1144 | |
|
1147 | 1145 | @LoginRequired() |
|
1148 | 1146 | @NotAnonymous() |
|
1149 | 1147 | @HasRepoPermissionAnyDecorator( |
|
1150 | 1148 | 'repository.read', 'repository.write', 'repository.admin') |
|
1151 | 1149 | @CSRFRequired() |
|
1152 | 1150 | @view_config( |
|
1153 | 1151 | route_name='pullrequest_comment_delete', request_method='POST', |
|
1154 | 1152 | renderer='json_ext') |
|
1155 | 1153 | def pull_request_comment_delete(self): |
|
1156 | 1154 | pull_request = PullRequest.get_or_404( |
|
1157 | 1155 | self.request.matchdict['pull_request_id']) |
|
1158 | 1156 | |
|
1159 | 1157 | comment = ChangesetComment.get_or_404( |
|
1160 | 1158 | self.request.matchdict['comment_id']) |
|
1161 | 1159 | comment_id = comment.comment_id |
|
1162 | 1160 | |
|
1163 | 1161 | if pull_request.is_closed(): |
|
1164 | 1162 | log.debug('comment: forbidden because pull request is closed') |
|
1165 | 1163 | raise HTTPForbidden() |
|
1166 | 1164 | |
|
1167 | 1165 | if not comment: |
|
1168 | 1166 | log.debug('Comment with id:%s not found, skipping', comment_id) |
|
1169 | 1167 | # comment already deleted in another call probably |
|
1170 | 1168 | return True |
|
1171 | 1169 | |
|
1172 | 1170 | if comment.pull_request.is_closed(): |
|
1173 | 1171 | # don't allow deleting comments on closed pull request |
|
1174 | 1172 | raise HTTPForbidden() |
|
1175 | 1173 | |
|
1176 | 1174 | is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name) |
|
1177 | 1175 | super_admin = h.HasPermissionAny('hg.admin')() |
|
1178 | 1176 | comment_owner = comment.author.user_id == self._rhodecode_user.user_id |
|
1179 | 1177 | is_repo_comment = comment.repo.repo_name == self.db_repo_name |
|
1180 | 1178 | comment_repo_admin = is_repo_admin and is_repo_comment |
|
1181 | 1179 | |
|
1182 | 1180 | if super_admin or comment_owner or comment_repo_admin: |
|
1183 | 1181 | old_calculated_status = comment.pull_request.calculated_review_status() |
|
1184 | 1182 | CommentsModel().delete(comment=comment, user=self._rhodecode_user) |
|
1185 | 1183 | Session().commit() |
|
1186 | 1184 | calculated_status = comment.pull_request.calculated_review_status() |
|
1187 | 1185 | if old_calculated_status != calculated_status: |
|
1188 | 1186 | PullRequestModel()._trigger_pull_request_hook( |
|
1189 | 1187 | comment.pull_request, self._rhodecode_user, 'review_status_change') |
|
1190 | 1188 | return True |
|
1191 | 1189 | else: |
|
1192 | 1190 | log.warning('No permissions for user %s to delete comment_id: %s', |
|
1193 | 1191 | self._rhodecode_db_user, comment_id) |
|
1194 | 1192 | raise HTTPNotFound() |
@@ -1,64 +1,61 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2016-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | |
|
23 | 23 | from pyramid.view import view_config |
|
24 | 24 | |
|
25 | 25 | from rhodecode.apps._base import RepoAppView |
|
26 | 26 | from rhodecode.apps.repository.utils import get_default_reviewers_data |
|
27 | 27 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator |
|
28 | 28 | |
|
29 | 29 | log = logging.getLogger(__name__) |
|
30 | 30 | |
|
31 | 31 | |
|
32 | 32 | class RepoReviewRulesView(RepoAppView): |
|
33 | 33 | def load_default_context(self): |
|
34 | 34 | c = self._get_local_tmpl_context() |
|
35 | 35 | |
|
36 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
37 | c.repo_info = self.db_repo | |
|
38 | ||
|
39 | 36 | self._register_global_c(c) |
|
40 | 37 | return c |
|
41 | 38 | |
|
42 | 39 | @LoginRequired() |
|
43 | 40 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
44 | 41 | @view_config( |
|
45 | 42 | route_name='repo_reviewers', request_method='GET', |
|
46 | 43 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
47 | 44 | def repo_review_rules(self): |
|
48 | 45 | c = self.load_default_context() |
|
49 | 46 | c.active = 'reviewers' |
|
50 | 47 | |
|
51 | 48 | return self._get_template_context(c) |
|
52 | 49 | |
|
53 | 50 | @LoginRequired() |
|
54 | 51 | @HasRepoPermissionAnyDecorator( |
|
55 | 52 | 'repository.read', 'repository.write', 'repository.admin') |
|
56 | 53 | @view_config( |
|
57 | 54 | route_name='repo_default_reviewers_data', request_method='GET', |
|
58 | 55 | renderer='json_ext') |
|
59 | 56 | def repo_default_reviewers_data(self): |
|
60 | 57 | review_data = get_default_reviewers_data( |
|
61 | 58 | self.db_repo.user, None, None, None, None) |
|
62 | 59 | return review_data |
|
63 | 60 | |
|
64 | 61 |
@@ -1,254 +1,251 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | |
|
23 | 23 | import deform |
|
24 | 24 | from pyramid.httpexceptions import HTTPFound |
|
25 | 25 | from pyramid.view import view_config |
|
26 | 26 | |
|
27 | 27 | from rhodecode.apps._base import RepoAppView |
|
28 | 28 | from rhodecode.forms import RcForm |
|
29 | 29 | from rhodecode.lib import helpers as h |
|
30 | 30 | from rhodecode.lib import audit_logger |
|
31 | 31 | from rhodecode.lib.auth import ( |
|
32 | 32 | LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired) |
|
33 | 33 | from rhodecode.model.db import RepositoryField, RepoGroup, Repository |
|
34 | 34 | from rhodecode.model.meta import Session |
|
35 | 35 | from rhodecode.model.repo import RepoModel |
|
36 | 36 | from rhodecode.model.scm import RepoGroupList, ScmModel |
|
37 | 37 | from rhodecode.model.validation_schema.schemas import repo_schema |
|
38 | 38 | |
|
39 | 39 | log = logging.getLogger(__name__) |
|
40 | 40 | |
|
41 | 41 | |
|
42 | 42 | class RepoSettingsView(RepoAppView): |
|
43 | 43 | |
|
44 | 44 | def load_default_context(self): |
|
45 | 45 | c = self._get_local_tmpl_context() |
|
46 | 46 | |
|
47 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
48 | c.repo_info = self.db_repo | |
|
49 | ||
|
50 | 47 | acl_groups = RepoGroupList( |
|
51 | 48 | RepoGroup.query().all(), |
|
52 | 49 | perm_set=['group.write', 'group.admin']) |
|
53 | 50 | c.repo_groups = RepoGroup.groups_choices(groups=acl_groups) |
|
54 | 51 | c.repo_groups_choices = map(lambda k: k[0], c.repo_groups) |
|
55 | 52 | |
|
56 | 53 | # in case someone no longer have a group.write access to a repository |
|
57 | 54 | # pre fill the list with this entry, we don't care if this is the same |
|
58 | 55 | # but it will allow saving repo data properly. |
|
59 | 56 | repo_group = self.db_repo.group |
|
60 | 57 | if repo_group and repo_group.group_id not in c.repo_groups_choices: |
|
61 | 58 | c.repo_groups_choices.append(repo_group.group_id) |
|
62 | 59 | c.repo_groups.append(RepoGroup._generate_choice(repo_group)) |
|
63 | 60 | |
|
64 | 61 | if c.repository_requirements_missing or self.rhodecode_vcs_repo is None: |
|
65 | 62 | # we might be in missing requirement state, so we load things |
|
66 | 63 | # without touching scm_instance() |
|
67 | 64 | c.landing_revs_choices, c.landing_revs = \ |
|
68 | 65 | ScmModel().get_repo_landing_revs() |
|
69 | 66 | else: |
|
70 | 67 | c.landing_revs_choices, c.landing_revs = \ |
|
71 | 68 | ScmModel().get_repo_landing_revs(self.db_repo) |
|
72 | 69 | |
|
73 | 70 | c.personal_repo_group = c.auth_user.personal_repo_group |
|
74 | 71 | c.repo_fields = RepositoryField.query()\ |
|
75 | 72 | .filter(RepositoryField.repository == self.db_repo).all() |
|
76 | 73 | |
|
77 | 74 | self._register_global_c(c) |
|
78 | 75 | return c |
|
79 | 76 | |
|
80 | 77 | def _get_schema(self, c, old_values=None): |
|
81 | 78 | return repo_schema.RepoSettingsSchema().bind( |
|
82 | 79 | repo_type=self.db_repo.repo_type, |
|
83 | 80 | repo_type_options=[self.db_repo.repo_type], |
|
84 | 81 | repo_ref_options=c.landing_revs_choices, |
|
85 | 82 | repo_ref_items=c.landing_revs, |
|
86 | 83 | repo_repo_group_options=c.repo_groups_choices, |
|
87 | 84 | repo_repo_group_items=c.repo_groups, |
|
88 | 85 | # user caller |
|
89 | 86 | user=self._rhodecode_user, |
|
90 | 87 | old_values=old_values |
|
91 | 88 | ) |
|
92 | 89 | |
|
93 | 90 | @LoginRequired() |
|
94 | 91 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
95 | 92 | @view_config( |
|
96 | 93 | route_name='edit_repo', request_method='GET', |
|
97 | 94 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
98 | 95 | def edit_settings(self): |
|
99 | 96 | c = self.load_default_context() |
|
100 | 97 | c.active = 'settings' |
|
101 | 98 | |
|
102 | 99 | defaults = RepoModel()._get_defaults(self.db_repo_name) |
|
103 | 100 | defaults['repo_owner'] = defaults['user'] |
|
104 | 101 | defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev'] |
|
105 | 102 | |
|
106 | 103 | schema = self._get_schema(c) |
|
107 | 104 | c.form = RcForm(schema, appstruct=defaults) |
|
108 | 105 | return self._get_template_context(c) |
|
109 | 106 | |
|
110 | 107 | @LoginRequired() |
|
111 | 108 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
112 | 109 | @CSRFRequired() |
|
113 | 110 | @view_config( |
|
114 | 111 | route_name='edit_repo', request_method='POST', |
|
115 | 112 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
116 | 113 | def edit_settings_update(self): |
|
117 | 114 | _ = self.request.translate |
|
118 | 115 | c = self.load_default_context() |
|
119 | 116 | c.active = 'settings' |
|
120 | 117 | old_repo_name = self.db_repo_name |
|
121 | 118 | |
|
122 | 119 | old_values = self.db_repo.get_api_data() |
|
123 | 120 | schema = self._get_schema(c, old_values=old_values) |
|
124 | 121 | |
|
125 | 122 | c.form = RcForm(schema) |
|
126 | 123 | pstruct = self.request.POST.items() |
|
127 | 124 | pstruct.append(('repo_type', self.db_repo.repo_type)) |
|
128 | 125 | try: |
|
129 | 126 | schema_data = c.form.validate(pstruct) |
|
130 | 127 | except deform.ValidationFailure as err_form: |
|
131 | 128 | return self._get_template_context(c) |
|
132 | 129 | |
|
133 | 130 | # data is now VALID, proceed with updates |
|
134 | 131 | # save validated data back into the updates dict |
|
135 | 132 | validated_updates = dict( |
|
136 | 133 | repo_name=schema_data['repo_group']['repo_name_without_group'], |
|
137 | 134 | repo_group=schema_data['repo_group']['repo_group_id'], |
|
138 | 135 | |
|
139 | 136 | user=schema_data['repo_owner'], |
|
140 | 137 | repo_description=schema_data['repo_description'], |
|
141 | 138 | repo_private=schema_data['repo_private'], |
|
142 | 139 | clone_uri=schema_data['repo_clone_uri'], |
|
143 | 140 | repo_landing_rev=schema_data['repo_landing_commit_ref'], |
|
144 | 141 | repo_enable_statistics=schema_data['repo_enable_statistics'], |
|
145 | 142 | repo_enable_locking=schema_data['repo_enable_locking'], |
|
146 | 143 | repo_enable_downloads=schema_data['repo_enable_downloads'], |
|
147 | 144 | ) |
|
148 | 145 | # detect if CLONE URI changed, if we get OLD means we keep old values |
|
149 | 146 | if schema_data['repo_clone_uri_change'] == 'OLD': |
|
150 | 147 | validated_updates['clone_uri'] = self.db_repo.clone_uri |
|
151 | 148 | |
|
152 | 149 | # use the new full name for redirect |
|
153 | 150 | new_repo_name = schema_data['repo_group']['repo_name_with_group'] |
|
154 | 151 | |
|
155 | 152 | # save extra fields into our validated data |
|
156 | 153 | for key, value in pstruct: |
|
157 | 154 | if key.startswith(RepositoryField.PREFIX): |
|
158 | 155 | validated_updates[key] = value |
|
159 | 156 | |
|
160 | 157 | try: |
|
161 | 158 | RepoModel().update(self.db_repo, **validated_updates) |
|
162 | 159 | ScmModel().mark_for_invalidation(new_repo_name) |
|
163 | 160 | |
|
164 | 161 | audit_logger.store_web( |
|
165 | 162 | 'repo.edit', action_data={'old_data': old_values}, |
|
166 | 163 | user=self._rhodecode_user, repo=self.db_repo) |
|
167 | 164 | |
|
168 | 165 | Session().commit() |
|
169 | 166 | |
|
170 | 167 | h.flash(_('Repository {} updated successfully').format( |
|
171 | 168 | old_repo_name), category='success') |
|
172 | 169 | except Exception: |
|
173 | 170 | log.exception("Exception during update of repository") |
|
174 | 171 | h.flash(_('Error occurred during update of repository {}').format( |
|
175 | 172 | old_repo_name), category='error') |
|
176 | 173 | |
|
177 | 174 | raise HTTPFound( |
|
178 | 175 | h.route_path('edit_repo', repo_name=new_repo_name)) |
|
179 | 176 | |
|
180 | 177 | @LoginRequired() |
|
181 | 178 | @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') |
|
182 | 179 | @view_config( |
|
183 | 180 | route_name='repo_edit_toggle_locking', request_method='GET', |
|
184 | 181 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
185 | 182 | def toggle_locking(self): |
|
186 | 183 | """ |
|
187 | 184 | Toggle locking of repository by simple GET call to url |
|
188 | 185 | """ |
|
189 | 186 | _ = self.request.translate |
|
190 | 187 | repo = self.db_repo |
|
191 | 188 | |
|
192 | 189 | try: |
|
193 | 190 | if repo.enable_locking: |
|
194 | 191 | if repo.locked[0]: |
|
195 | 192 | Repository.unlock(repo) |
|
196 | 193 | action = _('Unlocked') |
|
197 | 194 | else: |
|
198 | 195 | Repository.lock( |
|
199 | 196 | repo, self._rhodecode_user.user_id, |
|
200 | 197 | lock_reason=Repository.LOCK_WEB) |
|
201 | 198 | action = _('Locked') |
|
202 | 199 | |
|
203 | 200 | h.flash(_('Repository has been %s') % action, |
|
204 | 201 | category='success') |
|
205 | 202 | except Exception: |
|
206 | 203 | log.exception("Exception during unlocking") |
|
207 | 204 | h.flash(_('An error occurred during unlocking'), |
|
208 | 205 | category='error') |
|
209 | 206 | raise HTTPFound( |
|
210 | 207 | h.route_path('repo_summary', repo_name=self.db_repo_name)) |
|
211 | 208 | |
|
212 | 209 | @LoginRequired() |
|
213 | 210 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
214 | 211 | @view_config( |
|
215 | 212 | route_name='edit_repo_statistics', request_method='GET', |
|
216 | 213 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
217 | 214 | def edit_statistics_form(self): |
|
218 | 215 | c = self.load_default_context() |
|
219 | 216 | |
|
220 | 217 | if self.db_repo.stats: |
|
221 | 218 | # this is on what revision we ended up so we add +1 for count |
|
222 | 219 | last_rev = self.db_repo.stats.stat_on_revision + 1 |
|
223 | 220 | else: |
|
224 | 221 | last_rev = 0 |
|
225 | 222 | |
|
226 | 223 | c.active = 'statistics' |
|
227 | 224 | c.stats_revision = last_rev |
|
228 | 225 | c.repo_last_rev = self.rhodecode_vcs_repo.count() |
|
229 | 226 | |
|
230 | 227 | if last_rev == 0 or c.repo_last_rev == 0: |
|
231 | 228 | c.stats_percentage = 0 |
|
232 | 229 | else: |
|
233 | 230 | c.stats_percentage = '%.2f' % ( |
|
234 | 231 | (float((last_rev)) / c.repo_last_rev) * 100) |
|
235 | 232 | return self._get_template_context(c) |
|
236 | 233 | |
|
237 | 234 | @LoginRequired() |
|
238 | 235 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
239 | 236 | @CSRFRequired() |
|
240 | 237 | @view_config( |
|
241 | 238 | route_name='edit_repo_statistics_reset', request_method='POST', |
|
242 | 239 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
243 | 240 | def repo_statistics_reset(self): |
|
244 | 241 | _ = self.request.translate |
|
245 | 242 | |
|
246 | 243 | try: |
|
247 | 244 | RepoModel().delete_stats(self.db_repo_name) |
|
248 | 245 | Session().commit() |
|
249 | 246 | except Exception: |
|
250 | 247 | log.exception('Edit statistics failure') |
|
251 | 248 | h.flash(_('An error occurred during deletion of repository stats'), |
|
252 | 249 | category='error') |
|
253 | 250 | raise HTTPFound( |
|
254 | 251 | h.route_path('edit_repo_statistics', repo_name=self.db_repo_name)) |
@@ -1,226 +1,223 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | |
|
23 | 23 | from pyramid.view import view_config |
|
24 | 24 | from pyramid.httpexceptions import HTTPFound |
|
25 | 25 | |
|
26 | 26 | from rhodecode.apps._base import RepoAppView |
|
27 | 27 | from rhodecode.lib import helpers as h |
|
28 | 28 | from rhodecode.lib import audit_logger |
|
29 | 29 | from rhodecode.lib.auth import ( |
|
30 | 30 | LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired) |
|
31 | 31 | from rhodecode.lib.exceptions import AttachedForksError |
|
32 | 32 | from rhodecode.lib.utils2 import safe_int |
|
33 | 33 | from rhodecode.lib.vcs import RepositoryError |
|
34 | 34 | from rhodecode.model.db import Session, UserFollowing, User, Repository |
|
35 | 35 | from rhodecode.model.repo import RepoModel |
|
36 | 36 | from rhodecode.model.scm import ScmModel |
|
37 | 37 | |
|
38 | 38 | log = logging.getLogger(__name__) |
|
39 | 39 | |
|
40 | 40 | |
|
41 | 41 | class RepoSettingsView(RepoAppView): |
|
42 | 42 | |
|
43 | 43 | def load_default_context(self): |
|
44 | 44 | c = self._get_local_tmpl_context() |
|
45 | 45 | |
|
46 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
47 | c.repo_info = self.db_repo | |
|
48 | ||
|
49 | 46 | self._register_global_c(c) |
|
50 | 47 | return c |
|
51 | 48 | |
|
52 | 49 | @LoginRequired() |
|
53 | 50 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
54 | 51 | @view_config( |
|
55 | 52 | route_name='edit_repo_advanced', request_method='GET', |
|
56 | 53 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
57 | 54 | def edit_advanced(self): |
|
58 | 55 | c = self.load_default_context() |
|
59 | 56 | c.active = 'advanced' |
|
60 | 57 | |
|
61 | 58 | c.default_user_id = User.get_default_user().user_id |
|
62 | 59 | c.in_public_journal = UserFollowing.query() \ |
|
63 | 60 | .filter(UserFollowing.user_id == c.default_user_id) \ |
|
64 | 61 | .filter(UserFollowing.follows_repository == self.db_repo).scalar() |
|
65 | 62 | |
|
66 | 63 | c.has_origin_repo_read_perm = False |
|
67 | 64 | if self.db_repo.fork: |
|
68 | 65 | c.has_origin_repo_read_perm = h.HasRepoPermissionAny( |
|
69 | 66 | 'repository.write', 'repository.read', 'repository.admin')( |
|
70 | 67 | self.db_repo.fork.repo_name, 'repo set as fork page') |
|
71 | 68 | |
|
72 | 69 | return self._get_template_context(c) |
|
73 | 70 | |
|
74 | 71 | @LoginRequired() |
|
75 | 72 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
76 | 73 | @CSRFRequired() |
|
77 | 74 | @view_config( |
|
78 | 75 | route_name='edit_repo_advanced_delete', request_method='POST', |
|
79 | 76 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
80 | 77 | def edit_advanced_delete(self): |
|
81 | 78 | """ |
|
82 | 79 | Deletes the repository, or shows warnings if deletion is not possible |
|
83 | 80 | because of attached forks or other errors. |
|
84 | 81 | """ |
|
85 | 82 | _ = self.request.translate |
|
86 | 83 | handle_forks = self.request.POST.get('forks', None) |
|
87 | 84 | |
|
88 | 85 | try: |
|
89 | 86 | _forks = self.db_repo.forks.count() |
|
90 | 87 | if _forks and handle_forks: |
|
91 | 88 | if handle_forks == 'detach_forks': |
|
92 | 89 | handle_forks = 'detach' |
|
93 | 90 | h.flash(_('Detached %s forks') % _forks, category='success') |
|
94 | 91 | elif handle_forks == 'delete_forks': |
|
95 | 92 | handle_forks = 'delete' |
|
96 | 93 | h.flash(_('Deleted %s forks') % _forks, category='success') |
|
97 | 94 | |
|
98 | 95 | old_data = self.db_repo.get_api_data() |
|
99 | 96 | RepoModel().delete(self.db_repo, forks=handle_forks) |
|
100 | 97 | |
|
101 | 98 | repo = audit_logger.RepoWrap(repo_id=None, |
|
102 | 99 | repo_name=self.db_repo.repo_name) |
|
103 | 100 | audit_logger.store_web( |
|
104 | 101 | 'repo.delete', action_data={'old_data': old_data}, |
|
105 | 102 | user=self._rhodecode_user, repo=repo) |
|
106 | 103 | |
|
107 | 104 | ScmModel().mark_for_invalidation(self.db_repo_name, delete=True) |
|
108 | 105 | h.flash( |
|
109 | 106 | _('Deleted repository `%s`') % self.db_repo_name, |
|
110 | 107 | category='success') |
|
111 | 108 | Session().commit() |
|
112 | 109 | except AttachedForksError: |
|
113 | 110 | repo_advanced_url = h.route_path( |
|
114 | 111 | 'edit_repo_advanced', repo_name=self.db_repo_name, |
|
115 | 112 | _anchor='advanced-delete') |
|
116 | 113 | delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url) |
|
117 | 114 | h.flash(_('Cannot delete `{repo}` it still contains attached forks. ' |
|
118 | 115 | 'Try using {delete_or_detach} option.') |
|
119 | 116 | .format(repo=self.db_repo_name, delete_or_detach=delete_anchor), |
|
120 | 117 | category='warning') |
|
121 | 118 | |
|
122 | 119 | # redirect to advanced for forks handle action ? |
|
123 | 120 | raise HTTPFound(repo_advanced_url) |
|
124 | 121 | |
|
125 | 122 | except Exception: |
|
126 | 123 | log.exception("Exception during deletion of repository") |
|
127 | 124 | h.flash(_('An error occurred during deletion of `%s`') |
|
128 | 125 | % self.db_repo_name, category='error') |
|
129 | 126 | # redirect to advanced for more deletion options |
|
130 | 127 | raise HTTPFound( |
|
131 | 128 | h.route_path('edit_repo_advanced', repo_name=self.db_repo_name), |
|
132 | 129 | _anchor='advanced-delete') |
|
133 | 130 | |
|
134 | 131 | raise HTTPFound(h.route_path('home')) |
|
135 | 132 | |
|
136 | 133 | @LoginRequired() |
|
137 | 134 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
138 | 135 | @CSRFRequired() |
|
139 | 136 | @view_config( |
|
140 | 137 | route_name='edit_repo_advanced_journal', request_method='POST', |
|
141 | 138 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
142 | 139 | def edit_advanced_journal(self): |
|
143 | 140 | """ |
|
144 | 141 | Set's this repository to be visible in public journal, |
|
145 | 142 | in other words making default user to follow this repo |
|
146 | 143 | """ |
|
147 | 144 | _ = self.request.translate |
|
148 | 145 | |
|
149 | 146 | try: |
|
150 | 147 | user_id = User.get_default_user().user_id |
|
151 | 148 | ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id) |
|
152 | 149 | h.flash(_('Updated repository visibility in public journal'), |
|
153 | 150 | category='success') |
|
154 | 151 | Session().commit() |
|
155 | 152 | except Exception: |
|
156 | 153 | h.flash(_('An error occurred during setting this ' |
|
157 | 154 | 'repository in public journal'), |
|
158 | 155 | category='error') |
|
159 | 156 | |
|
160 | 157 | raise HTTPFound( |
|
161 | 158 | h.route_path('edit_repo_advanced', repo_name=self.db_repo_name)) |
|
162 | 159 | |
|
163 | 160 | @LoginRequired() |
|
164 | 161 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
165 | 162 | @CSRFRequired() |
|
166 | 163 | @view_config( |
|
167 | 164 | route_name='edit_repo_advanced_fork', request_method='POST', |
|
168 | 165 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
169 | 166 | def edit_advanced_fork(self): |
|
170 | 167 | """ |
|
171 | 168 | Mark given repository as a fork of another |
|
172 | 169 | """ |
|
173 | 170 | _ = self.request.translate |
|
174 | 171 | |
|
175 | 172 | new_fork_id = self.request.POST.get('id_fork_of') |
|
176 | 173 | try: |
|
177 | 174 | |
|
178 | 175 | if new_fork_id and not new_fork_id.isdigit(): |
|
179 | 176 | log.error('Given fork id %s is not an INT', new_fork_id) |
|
180 | 177 | |
|
181 | 178 | fork_id = safe_int(new_fork_id) |
|
182 | 179 | repo = ScmModel().mark_as_fork( |
|
183 | 180 | self.db_repo_name, fork_id, self._rhodecode_user.user_id) |
|
184 | 181 | fork = repo.fork.repo_name if repo.fork else _('Nothing') |
|
185 | 182 | Session().commit() |
|
186 | 183 | h.flash(_('Marked repo %s as fork of %s') % (self.db_repo_name, fork), |
|
187 | 184 | category='success') |
|
188 | 185 | except RepositoryError as e: |
|
189 | 186 | log.exception("Repository Error occurred") |
|
190 | 187 | h.flash(str(e), category='error') |
|
191 | 188 | except Exception as e: |
|
192 | 189 | log.exception("Exception while editing fork") |
|
193 | 190 | h.flash(_('An error occurred during this operation'), |
|
194 | 191 | category='error') |
|
195 | 192 | |
|
196 | 193 | raise HTTPFound( |
|
197 | 194 | h.route_path('edit_repo_advanced', repo_name=self.db_repo_name)) |
|
198 | 195 | |
|
199 | 196 | @LoginRequired() |
|
200 | 197 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
201 | 198 | @CSRFRequired() |
|
202 | 199 | @view_config( |
|
203 | 200 | route_name='edit_repo_advanced_locking', request_method='POST', |
|
204 | 201 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
205 | 202 | def edit_advanced_locking(self): |
|
206 | 203 | """ |
|
207 | 204 | Toggle locking of repository |
|
208 | 205 | """ |
|
209 | 206 | _ = self.request.translate |
|
210 | 207 | set_lock = self.request.POST.get('set_lock') |
|
211 | 208 | set_unlock = self.request.POST.get('set_unlock') |
|
212 | 209 | |
|
213 | 210 | try: |
|
214 | 211 | if set_lock: |
|
215 | 212 | Repository.lock(self.db_repo, self._rhodecode_user.user_id, |
|
216 | 213 | lock_reason=Repository.LOCK_WEB) |
|
217 | 214 | h.flash(_('Locked repository'), category='success') |
|
218 | 215 | elif set_unlock: |
|
219 | 216 | Repository.unlock(self.db_repo) |
|
220 | 217 | h.flash(_('Unlocked repository'), category='success') |
|
221 | 218 | except Exception as e: |
|
222 | 219 | log.exception("Exception during unlocking") |
|
223 | 220 | h.flash(_('An error occurred during unlocking'), category='error') |
|
224 | 221 | |
|
225 | 222 | raise HTTPFound( |
|
226 | 223 | h.route_path('edit_repo_advanced', repo_name=self.db_repo_name)) |
@@ -1,114 +1,111 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2017-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | |
|
23 | 23 | import formencode |
|
24 | 24 | import formencode.htmlfill |
|
25 | 25 | |
|
26 | 26 | from pyramid.httpexceptions import HTTPFound |
|
27 | 27 | from pyramid.view import view_config |
|
28 | 28 | |
|
29 | 29 | from rhodecode.apps._base import RepoAppView |
|
30 | 30 | from rhodecode.lib import audit_logger |
|
31 | 31 | from rhodecode.lib import helpers as h |
|
32 | 32 | from rhodecode.lib.auth import ( |
|
33 | 33 | LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired) |
|
34 | 34 | from rhodecode.model.db import RepositoryField |
|
35 | 35 | from rhodecode.model.forms import RepoFieldForm |
|
36 | 36 | from rhodecode.model.meta import Session |
|
37 | 37 | from rhodecode.model.repo import RepoModel |
|
38 | 38 | |
|
39 | 39 | log = logging.getLogger(__name__) |
|
40 | 40 | |
|
41 | 41 | |
|
42 | 42 | class RepoSettingsFieldsView(RepoAppView): |
|
43 | 43 | def load_default_context(self): |
|
44 | 44 | c = self._get_local_tmpl_context() |
|
45 | 45 | |
|
46 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
47 | c.repo_info = self.db_repo | |
|
48 | ||
|
49 | 46 | self._register_global_c(c) |
|
50 | 47 | return c |
|
51 | 48 | |
|
52 | 49 | @LoginRequired() |
|
53 | 50 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
54 | 51 | @view_config( |
|
55 | 52 | route_name='edit_repo_fields', request_method='GET', |
|
56 | 53 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
57 | 54 | def repo_field_edit(self): |
|
58 | 55 | c = self.load_default_context() |
|
59 | 56 | |
|
60 | 57 | c.active = 'fields' |
|
61 | 58 | c.repo_fields = RepositoryField.query() \ |
|
62 | 59 | .filter(RepositoryField.repository == self.db_repo).all() |
|
63 | 60 | |
|
64 | 61 | return self._get_template_context(c) |
|
65 | 62 | |
|
66 | 63 | @LoginRequired() |
|
67 | 64 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
68 | 65 | @CSRFRequired() |
|
69 | 66 | @view_config( |
|
70 | 67 | route_name='edit_repo_fields_create', request_method='POST', |
|
71 | 68 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
72 | 69 | def repo_field_create(self): |
|
73 | 70 | _ = self.request.translate |
|
74 | 71 | |
|
75 | 72 | try: |
|
76 | 73 | form_result = RepoFieldForm()().to_python(dict(self.request.POST)) |
|
77 | 74 | RepoModel().add_repo_field( |
|
78 | 75 | self.db_repo_name, |
|
79 | 76 | form_result['new_field_key'], |
|
80 | 77 | field_type=form_result['new_field_type'], |
|
81 | 78 | field_value=form_result['new_field_value'], |
|
82 | 79 | field_label=form_result['new_field_label'], |
|
83 | 80 | field_desc=form_result['new_field_desc']) |
|
84 | 81 | |
|
85 | 82 | Session().commit() |
|
86 | 83 | except Exception as e: |
|
87 | 84 | log.exception("Exception creating field") |
|
88 | 85 | msg = _('An error occurred during creation of field') |
|
89 | 86 | if isinstance(e, formencode.Invalid): |
|
90 | 87 | msg += ". " + e.msg |
|
91 | 88 | h.flash(msg, category='error') |
|
92 | 89 | |
|
93 | 90 | raise HTTPFound( |
|
94 | 91 | h.route_path('edit_repo_fields', repo_name=self.db_repo_name)) |
|
95 | 92 | |
|
96 | 93 | @LoginRequired() |
|
97 | 94 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
98 | 95 | @CSRFRequired() |
|
99 | 96 | @view_config( |
|
100 | 97 | route_name='edit_repo_fields_delete', request_method='POST', |
|
101 | 98 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
102 | 99 | def repo_field_delete(self): |
|
103 | 100 | _ = self.request.translate |
|
104 | 101 | field = RepositoryField.get_or_404(self.request.matchdict['field_id']) |
|
105 | 102 | try: |
|
106 | 103 | RepoModel().delete_repo_field(self.db_repo_name, field.field_key) |
|
107 | 104 | Session().commit() |
|
108 | 105 | except Exception: |
|
109 | 106 | log.exception('Exception during removal of field') |
|
110 | 107 | msg = _('An error occurred during removal of field') |
|
111 | 108 | h.flash(msg, category='error') |
|
112 | 109 | |
|
113 | 110 | raise HTTPFound( |
|
114 | 111 | h.route_path('edit_repo_fields', repo_name=self.db_repo_name)) |
@@ -1,129 +1,126 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2017-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | |
|
23 | 23 | from pyramid.httpexceptions import HTTPFound |
|
24 | 24 | from pyramid.view import view_config |
|
25 | 25 | |
|
26 | 26 | from rhodecode.apps._base import RepoAppView |
|
27 | 27 | from rhodecode.lib import audit_logger |
|
28 | 28 | from rhodecode.lib import helpers as h |
|
29 | 29 | from rhodecode.lib.auth import ( |
|
30 | 30 | LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired) |
|
31 | 31 | from rhodecode.model.forms import IssueTrackerPatternsForm |
|
32 | 32 | from rhodecode.model.meta import Session |
|
33 | 33 | from rhodecode.model.settings import IssueTrackerSettingsModel |
|
34 | 34 | |
|
35 | 35 | log = logging.getLogger(__name__) |
|
36 | 36 | |
|
37 | 37 | |
|
38 | 38 | class RepoSettingsIssueTrackersView(RepoAppView): |
|
39 | 39 | def load_default_context(self): |
|
40 | 40 | c = self._get_local_tmpl_context() |
|
41 | 41 | |
|
42 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
43 | c.repo_info = self.db_repo | |
|
44 | ||
|
45 | 42 | self._register_global_c(c) |
|
46 | 43 | return c |
|
47 | 44 | |
|
48 | 45 | @LoginRequired() |
|
49 | 46 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
50 | 47 | @view_config( |
|
51 | 48 | route_name='edit_repo_issuetracker', request_method='GET', |
|
52 | 49 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
53 | 50 | def repo_issuetracker(self): |
|
54 | 51 | c = self.load_default_context() |
|
55 | 52 | c.active = 'issuetracker' |
|
56 | 53 | c.data = 'data' |
|
57 | 54 | |
|
58 | 55 | c.settings_model = IssueTrackerSettingsModel(repo=self.db_repo) |
|
59 | 56 | c.global_patterns = c.settings_model.get_global_settings() |
|
60 | 57 | c.repo_patterns = c.settings_model.get_repo_settings() |
|
61 | 58 | |
|
62 | 59 | return self._get_template_context(c) |
|
63 | 60 | |
|
64 | 61 | @LoginRequired() |
|
65 | 62 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
66 | 63 | @CSRFRequired() |
|
67 | 64 | @view_config( |
|
68 | 65 | route_name='edit_repo_issuetracker_test', request_method='POST', |
|
69 | 66 | xhr=True, renderer='string') |
|
70 | 67 | def repo_issuetracker_test(self): |
|
71 | 68 | return h.urlify_commit_message( |
|
72 | 69 | self.request.POST.get('test_text', ''), |
|
73 | 70 | self.db_repo_name) |
|
74 | 71 | |
|
75 | 72 | @LoginRequired() |
|
76 | 73 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
77 | 74 | @CSRFRequired() |
|
78 | 75 | @view_config( |
|
79 | 76 | route_name='edit_repo_issuetracker_delete', request_method='POST', |
|
80 | 77 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
81 | 78 | def repo_issuetracker_delete(self): |
|
82 | 79 | _ = self.request.translate |
|
83 | 80 | uid = self.request.POST.get('uid') |
|
84 | 81 | repo_settings = IssueTrackerSettingsModel(repo=self.db_repo_name) |
|
85 | 82 | try: |
|
86 | 83 | repo_settings.delete_entries(uid) |
|
87 | 84 | except Exception: |
|
88 | 85 | h.flash(_('Error occurred during deleting issue tracker entry'), |
|
89 | 86 | category='error') |
|
90 | 87 | else: |
|
91 | 88 | h.flash(_('Removed issue tracker entry'), category='success') |
|
92 | 89 | raise HTTPFound( |
|
93 | 90 | h.route_path('edit_repo_issuetracker', repo_name=self.db_repo_name)) |
|
94 | 91 | |
|
95 | 92 | def _update_patterns(self, form, repo_settings): |
|
96 | 93 | for uid in form['delete_patterns']: |
|
97 | 94 | repo_settings.delete_entries(uid) |
|
98 | 95 | |
|
99 | 96 | for pattern_data in form['patterns']: |
|
100 | 97 | for setting_key, pattern, type_ in pattern_data: |
|
101 | 98 | sett = repo_settings.create_or_update_setting( |
|
102 | 99 | setting_key, pattern.strip(), type_) |
|
103 | 100 | Session().add(sett) |
|
104 | 101 | |
|
105 | 102 | Session().commit() |
|
106 | 103 | |
|
107 | 104 | @LoginRequired() |
|
108 | 105 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
109 | 106 | @CSRFRequired() |
|
110 | 107 | @view_config( |
|
111 | 108 | route_name='edit_repo_issuetracker_update', request_method='POST', |
|
112 | 109 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
113 | 110 | def repo_issuetracker_update(self): |
|
114 | 111 | _ = self.request.translate |
|
115 | 112 | # Save inheritance |
|
116 | 113 | repo_settings = IssueTrackerSettingsModel(repo=self.db_repo_name) |
|
117 | 114 | inherited = ( |
|
118 | 115 | self.request.POST.get('inherit_global_issuetracker') == "inherited") |
|
119 | 116 | repo_settings.inherit_global_settings = inherited |
|
120 | 117 | Session().commit() |
|
121 | 118 | |
|
122 | 119 | form = IssueTrackerPatternsForm()().to_python(self.request.POST) |
|
123 | 120 | if form: |
|
124 | 121 | self._update_patterns(form, repo_settings) |
|
125 | 122 | |
|
126 | 123 | h.flash(_('Updated issue tracker entries'), category='success') |
|
127 | 124 | raise HTTPFound( |
|
128 | 125 | h.route_path('edit_repo_issuetracker', repo_name=self.db_repo_name)) |
|
129 | 126 |
@@ -1,75 +1,72 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2017-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | |
|
23 | 23 | from pyramid.httpexceptions import HTTPFound |
|
24 | 24 | from pyramid.view import view_config |
|
25 | 25 | |
|
26 | 26 | from rhodecode.apps._base import RepoAppView |
|
27 | 27 | from rhodecode.lib import helpers as h |
|
28 | 28 | from rhodecode.lib.auth import ( |
|
29 | 29 | LoginRequired, CSRFRequired, HasRepoPermissionAnyDecorator) |
|
30 | 30 | from rhodecode.model.scm import ScmModel |
|
31 | 31 | |
|
32 | 32 | log = logging.getLogger(__name__) |
|
33 | 33 | |
|
34 | 34 | |
|
35 | 35 | class RepoSettingsRemoteView(RepoAppView): |
|
36 | 36 | def load_default_context(self): |
|
37 | 37 | c = self._get_local_tmpl_context() |
|
38 | 38 | |
|
39 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
40 | c.repo_info = self.db_repo | |
|
41 | ||
|
42 | 39 | self._register_global_c(c) |
|
43 | 40 | return c |
|
44 | 41 | |
|
45 | 42 | @LoginRequired() |
|
46 | 43 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
47 | 44 | @view_config( |
|
48 | 45 | route_name='edit_repo_remote', request_method='GET', |
|
49 | 46 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
50 | 47 | def repo_remote_edit_form(self): |
|
51 | 48 | c = self.load_default_context() |
|
52 | 49 | c.active = 'remote' |
|
53 | 50 | |
|
54 | 51 | return self._get_template_context(c) |
|
55 | 52 | |
|
56 | 53 | @LoginRequired() |
|
57 | 54 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
58 | 55 | @CSRFRequired() |
|
59 | 56 | @view_config( |
|
60 | 57 | route_name='edit_repo_remote_pull', request_method='POST', |
|
61 | 58 | renderer=None) |
|
62 | 59 | def repo_remote_pull_changes(self): |
|
63 | 60 | _ = self.request.translate |
|
64 | 61 | self.load_default_context() |
|
65 | 62 | |
|
66 | 63 | try: |
|
67 | 64 | ScmModel().pull_changes( |
|
68 | 65 | self.db_repo_name, self._rhodecode_user.username) |
|
69 | 66 | h.flash(_('Pulled from remote location'), category='success') |
|
70 | 67 | except Exception: |
|
71 | 68 | log.exception("Exception during pull from remote") |
|
72 | 69 | h.flash(_('An error occurred during pull from remote location'), |
|
73 | 70 | category='error') |
|
74 | 71 | raise HTTPFound( |
|
75 | 72 | h.route_path('edit_repo_remote', repo_name=self.db_repo_name)) |
@@ -1,173 +1,170 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2017-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | |
|
23 | 23 | import formencode |
|
24 | 24 | import formencode.htmlfill |
|
25 | 25 | from pyramid.httpexceptions import HTTPFound, HTTPBadRequest |
|
26 | 26 | from pyramid.response import Response |
|
27 | 27 | from pyramid.renderers import render |
|
28 | 28 | from pyramid.view import view_config |
|
29 | 29 | |
|
30 | 30 | from rhodecode.apps._base import RepoAppView |
|
31 | 31 | from rhodecode.lib import audit_logger |
|
32 | 32 | from rhodecode.lib import helpers as h |
|
33 | 33 | from rhodecode.lib.auth import ( |
|
34 | 34 | LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired) |
|
35 | 35 | from rhodecode.model.forms import RepoVcsSettingsForm |
|
36 | 36 | from rhodecode.model.meta import Session |
|
37 | 37 | from rhodecode.model.settings import VcsSettingsModel, SettingNotFound |
|
38 | 38 | |
|
39 | 39 | log = logging.getLogger(__name__) |
|
40 | 40 | |
|
41 | 41 | |
|
42 | 42 | class RepoSettingsVcsView(RepoAppView): |
|
43 | 43 | def load_default_context(self): |
|
44 | 44 | c = self._get_local_tmpl_context() |
|
45 | 45 | |
|
46 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
47 | c.repo_info = self.db_repo | |
|
48 | ||
|
49 | 46 | self._register_global_c(c) |
|
50 | 47 | return c |
|
51 | 48 | |
|
52 | 49 | def _vcs_form_defaults(self, repo_name): |
|
53 | 50 | model = VcsSettingsModel(repo=repo_name) |
|
54 | 51 | global_defaults = model.get_global_settings() |
|
55 | 52 | |
|
56 | 53 | repo_defaults = {} |
|
57 | 54 | repo_defaults.update(global_defaults) |
|
58 | 55 | repo_defaults.update(model.get_repo_settings()) |
|
59 | 56 | |
|
60 | 57 | global_defaults = { |
|
61 | 58 | '{}_inherited'.format(k): global_defaults[k] |
|
62 | 59 | for k in global_defaults} |
|
63 | 60 | |
|
64 | 61 | defaults = { |
|
65 | 62 | 'inherit_global_settings': model.inherit_global_settings |
|
66 | 63 | } |
|
67 | 64 | defaults.update(global_defaults) |
|
68 | 65 | defaults.update(repo_defaults) |
|
69 | 66 | defaults.update({ |
|
70 | 67 | 'new_svn_branch': '', |
|
71 | 68 | 'new_svn_tag': '', |
|
72 | 69 | }) |
|
73 | 70 | return defaults |
|
74 | 71 | |
|
75 | 72 | @LoginRequired() |
|
76 | 73 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
77 | 74 | @view_config( |
|
78 | 75 | route_name='edit_repo_vcs', request_method='GET', |
|
79 | 76 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
80 | 77 | def repo_vcs_settings(self): |
|
81 | 78 | c = self.load_default_context() |
|
82 | 79 | model = VcsSettingsModel(repo=self.db_repo_name) |
|
83 | 80 | |
|
84 | 81 | c.active = 'vcs' |
|
85 | 82 | c.global_svn_branch_patterns = model.get_global_svn_branch_patterns() |
|
86 | 83 | c.global_svn_tag_patterns = model.get_global_svn_tag_patterns() |
|
87 | 84 | c.svn_branch_patterns = model.get_repo_svn_branch_patterns() |
|
88 | 85 | c.svn_tag_patterns = model.get_repo_svn_tag_patterns() |
|
89 | 86 | |
|
90 | 87 | defaults = self._vcs_form_defaults(self.db_repo_name) |
|
91 | 88 | c.inherit_global_settings = defaults['inherit_global_settings'] |
|
92 | 89 | |
|
93 | 90 | data = render('rhodecode:templates/admin/repos/repo_edit.mako', |
|
94 | 91 | self._get_template_context(c), self.request) |
|
95 | 92 | html = formencode.htmlfill.render( |
|
96 | 93 | data, |
|
97 | 94 | defaults=defaults, |
|
98 | 95 | encoding="UTF-8", |
|
99 | 96 | force_defaults=False |
|
100 | 97 | ) |
|
101 | 98 | return Response(html) |
|
102 | 99 | |
|
103 | 100 | @LoginRequired() |
|
104 | 101 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
105 | 102 | @CSRFRequired() |
|
106 | 103 | @view_config( |
|
107 | 104 | route_name='edit_repo_vcs_update', request_method='POST', |
|
108 | 105 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
109 | 106 | def repo_settings_vcs_update(self): |
|
110 | 107 | _ = self.request.translate |
|
111 | 108 | c = self.load_default_context() |
|
112 | 109 | c.active = 'vcs' |
|
113 | 110 | |
|
114 | 111 | model = VcsSettingsModel(repo=self.db_repo_name) |
|
115 | 112 | c.global_svn_branch_patterns = model.get_global_svn_branch_patterns() |
|
116 | 113 | c.global_svn_tag_patterns = model.get_global_svn_tag_patterns() |
|
117 | 114 | c.svn_branch_patterns = model.get_repo_svn_branch_patterns() |
|
118 | 115 | c.svn_tag_patterns = model.get_repo_svn_tag_patterns() |
|
119 | 116 | |
|
120 | 117 | defaults = self._vcs_form_defaults(self.db_repo_name) |
|
121 | 118 | c.inherit_global_settings = defaults['inherit_global_settings'] |
|
122 | 119 | |
|
123 | 120 | application_form = RepoVcsSettingsForm(self.db_repo_name)() |
|
124 | 121 | try: |
|
125 | 122 | form_result = application_form.to_python(dict(self.request.POST)) |
|
126 | 123 | except formencode.Invalid as errors: |
|
127 | 124 | h.flash(_("Some form inputs contain invalid data."), |
|
128 | 125 | category='error') |
|
129 | 126 | |
|
130 | 127 | data = render('rhodecode:templates/admin/repos/repo_edit.mako', |
|
131 | 128 | self._get_template_context(c), self.request) |
|
132 | 129 | html = formencode.htmlfill.render( |
|
133 | 130 | data, |
|
134 | 131 | defaults=errors.value, |
|
135 | 132 | errors=errors.error_dict or {}, |
|
136 | 133 | encoding="UTF-8", |
|
137 | 134 | force_defaults=False |
|
138 | 135 | ) |
|
139 | 136 | return Response(html) |
|
140 | 137 | |
|
141 | 138 | try: |
|
142 | 139 | inherit_global_settings = form_result['inherit_global_settings'] |
|
143 | 140 | model.create_or_update_repo_settings( |
|
144 | 141 | form_result, inherit_global_settings=inherit_global_settings) |
|
145 | 142 | Session().commit() |
|
146 | 143 | h.flash(_('Updated VCS settings'), category='success') |
|
147 | 144 | except Exception: |
|
148 | 145 | log.exception("Exception while updating settings") |
|
149 | 146 | h.flash( |
|
150 | 147 | _('Error occurred during updating repository VCS settings'), |
|
151 | 148 | category='error') |
|
152 | 149 | |
|
153 | 150 | raise HTTPFound( |
|
154 | 151 | h.route_path('edit_repo_vcs', repo_name=self.db_repo_name)) |
|
155 | 152 | |
|
156 | 153 | @LoginRequired() |
|
157 | 154 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
158 | 155 | @CSRFRequired() |
|
159 | 156 | @view_config( |
|
160 | 157 | route_name='edit_repo_vcs_svn_pattern_delete', request_method='POST', |
|
161 | 158 | renderer='json_ext', xhr=True) |
|
162 | 159 | def repo_settings_delete_svn_pattern(self): |
|
163 | 160 | self.load_default_context() |
|
164 | 161 | delete_pattern_id = self.request.POST.get('delete_svn_pattern') |
|
165 | 162 | model = VcsSettingsModel(repo=self.db_repo_name) |
|
166 | 163 | try: |
|
167 | 164 | model.delete_repo_svn_pattern(delete_pattern_id) |
|
168 | 165 | except SettingNotFound: |
|
169 | 166 | log.exception('Failed to delete SVN pattern') |
|
170 | 167 | raise HTTPBadRequest() |
|
171 | 168 | |
|
172 | 169 | Session().commit() |
|
173 | 170 | return True |
@@ -1,116 +1,113 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2017-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | from pyramid.view import view_config |
|
23 | 23 | |
|
24 | 24 | from rhodecode.apps._base import RepoAppView |
|
25 | 25 | from rhodecode.lib import audit_logger |
|
26 | 26 | from rhodecode.lib import helpers as h |
|
27 | 27 | from rhodecode.lib.auth import ( |
|
28 | 28 | LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired) |
|
29 | 29 | from rhodecode.lib.ext_json import json |
|
30 | 30 | |
|
31 | 31 | log = logging.getLogger(__name__) |
|
32 | 32 | |
|
33 | 33 | |
|
34 | 34 | class StripView(RepoAppView): |
|
35 | 35 | def load_default_context(self): |
|
36 | 36 | c = self._get_local_tmpl_context() |
|
37 | 37 | |
|
38 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
39 | c.repo_info = self.db_repo | |
|
40 | ||
|
41 | 38 | self._register_global_c(c) |
|
42 | 39 | return c |
|
43 | 40 | |
|
44 | 41 | @LoginRequired() |
|
45 | 42 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
46 | 43 | @view_config( |
|
47 | 44 | route_name='edit_repo_strip', request_method='GET', |
|
48 | 45 | renderer='rhodecode:templates/admin/repos/repo_edit.mako') |
|
49 | 46 | def strip(self): |
|
50 | 47 | c = self.load_default_context() |
|
51 | 48 | c.active = 'strip' |
|
52 | 49 | c.strip_limit = 10 |
|
53 | 50 | |
|
54 | 51 | return self._get_template_context(c) |
|
55 | 52 | |
|
56 | 53 | @LoginRequired() |
|
57 | 54 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
58 | 55 | @CSRFRequired() |
|
59 | 56 | @view_config( |
|
60 | 57 | route_name='strip_check', request_method='POST', |
|
61 | 58 | renderer='json', xhr=True) |
|
62 | 59 | def strip_check(self): |
|
63 | 60 | from rhodecode.lib.vcs.backends.base import EmptyCommit |
|
64 | 61 | data = {} |
|
65 | 62 | rp = self.request.POST |
|
66 | 63 | for i in range(1, 11): |
|
67 | 64 | chset = 'changeset_id-%d' % (i,) |
|
68 | 65 | check = rp.get(chset) |
|
69 | 66 | |
|
70 | 67 | if check: |
|
71 | 68 | data[i] = self.db_repo.get_changeset(rp[chset]) |
|
72 | 69 | if isinstance(data[i], EmptyCommit): |
|
73 | 70 | data[i] = {'rev': None, 'commit': h.escape(rp[chset])} |
|
74 | 71 | else: |
|
75 | 72 | data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch, |
|
76 | 73 | 'author': data[i].author, |
|
77 | 74 | 'comment': data[i].message} |
|
78 | 75 | else: |
|
79 | 76 | break |
|
80 | 77 | return data |
|
81 | 78 | |
|
82 | 79 | @LoginRequired() |
|
83 | 80 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
84 | 81 | @CSRFRequired() |
|
85 | 82 | @view_config( |
|
86 | 83 | route_name='strip_execute', request_method='POST', |
|
87 | 84 | renderer='json', xhr=True) |
|
88 | 85 | def strip_execute(self): |
|
89 | 86 | from rhodecode.model.scm import ScmModel |
|
90 | 87 | |
|
91 | 88 | c = self.load_default_context() |
|
92 | 89 | user = self._rhodecode_user |
|
93 | 90 | rp = self.request.POST |
|
94 | 91 | data = {} |
|
95 | 92 | for idx in rp: |
|
96 | 93 | commit = json.loads(rp[idx]) |
|
97 | 94 | # If someone put two times the same branch |
|
98 | 95 | if commit['branch'] in data.keys(): |
|
99 | 96 | continue |
|
100 | 97 | try: |
|
101 | 98 | ScmModel().strip( |
|
102 | 99 | repo=self.db_repo, |
|
103 | 100 | commit_id=commit['rev'], branch=commit['branch']) |
|
104 | 101 | log.info('Stripped commit %s from repo `%s` by %s' % ( |
|
105 | 102 | commit['rev'], self.db_repo_name, user)) |
|
106 | 103 | data[commit['rev']] = True |
|
107 | 104 | |
|
108 | 105 | audit_logger.store_web( |
|
109 | 106 | 'repo.commit.strip', action_data={'commit_id': commit['rev']}, |
|
110 | 107 | repo=self.db_repo, user=self._rhodecode_user, commit=True) |
|
111 | 108 | |
|
112 | 109 | except Exception as e: |
|
113 | 110 | data[commit['rev']] = False |
|
114 | 111 | log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s' % ( |
|
115 | 112 | commit['rev'], self.db_repo_name, user, e.message)) |
|
116 | 113 | return data |
@@ -1,372 +1,370 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-2017 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | import string |
|
23 | 23 | |
|
24 | 24 | from pyramid.view import view_config |
|
25 | 25 | from beaker.cache import cache_region |
|
26 | 26 | |
|
27 | 27 | from rhodecode.controllers import utils |
|
28 | 28 | from rhodecode.apps._base import RepoAppView |
|
29 | 29 | from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP) |
|
30 | 30 | from rhodecode.lib import caches, helpers as h |
|
31 | 31 | from rhodecode.lib.helpers import RepoPage |
|
32 | 32 | from rhodecode.lib.utils2 import safe_str, safe_int |
|
33 | 33 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator |
|
34 | 34 | from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links |
|
35 | 35 | from rhodecode.lib.ext_json import json |
|
36 | 36 | from rhodecode.lib.vcs.backends.base import EmptyCommit |
|
37 | 37 | from rhodecode.lib.vcs.exceptions import CommitError, EmptyRepositoryError |
|
38 | 38 | from rhodecode.model.db import Statistics, CacheKey, User |
|
39 | 39 | from rhodecode.model.meta import Session |
|
40 | 40 | from rhodecode.model.repo import ReadmeFinder |
|
41 | 41 | from rhodecode.model.scm import ScmModel |
|
42 | 42 | |
|
43 | 43 | log = logging.getLogger(__name__) |
|
44 | 44 | |
|
45 | 45 | |
|
46 | 46 | class RepoSummaryView(RepoAppView): |
|
47 | 47 | |
|
48 | 48 | def load_default_context(self): |
|
49 | 49 | c = self._get_local_tmpl_context(include_app_defaults=True) |
|
50 | 50 | |
|
51 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
52 | c.repo_info = self.db_repo | |
|
53 | 51 | c.rhodecode_repo = None |
|
54 | 52 | if not c.repository_requirements_missing: |
|
55 | 53 | c.rhodecode_repo = self.rhodecode_vcs_repo |
|
56 | 54 | |
|
57 | 55 | self._register_global_c(c) |
|
58 | 56 | return c |
|
59 | 57 | |
|
60 | 58 | def _get_readme_data(self, db_repo, default_renderer): |
|
61 | 59 | repo_name = db_repo.repo_name |
|
62 | 60 | log.debug('Looking for README file') |
|
63 | 61 | |
|
64 | 62 | @cache_region('long_term') |
|
65 | 63 | def _generate_readme(cache_key): |
|
66 | 64 | readme_data = None |
|
67 | 65 | readme_node = None |
|
68 | 66 | readme_filename = None |
|
69 | 67 | commit = self._get_landing_commit_or_none(db_repo) |
|
70 | 68 | if commit: |
|
71 | 69 | log.debug("Searching for a README file.") |
|
72 | 70 | readme_node = ReadmeFinder(default_renderer).search(commit) |
|
73 | 71 | if readme_node: |
|
74 | 72 | relative_urls = { |
|
75 | 73 | 'raw': h.route_path( |
|
76 | 74 | 'repo_file_raw', repo_name=repo_name, |
|
77 | 75 | commit_id=commit.raw_id, f_path=readme_node.path), |
|
78 | 76 | 'standard': h.route_path( |
|
79 | 77 | 'repo_files', repo_name=repo_name, |
|
80 | 78 | commit_id=commit.raw_id, f_path=readme_node.path), |
|
81 | 79 | } |
|
82 | 80 | readme_data = self._render_readme_or_none( |
|
83 | 81 | commit, readme_node, relative_urls) |
|
84 | 82 | readme_filename = readme_node.path |
|
85 | 83 | return readme_data, readme_filename |
|
86 | 84 | |
|
87 | 85 | invalidator_context = CacheKey.repo_context_cache( |
|
88 | 86 | _generate_readme, repo_name, CacheKey.CACHE_TYPE_README) |
|
89 | 87 | |
|
90 | 88 | with invalidator_context as context: |
|
91 | 89 | context.invalidate() |
|
92 | 90 | computed = context.compute() |
|
93 | 91 | |
|
94 | 92 | return computed |
|
95 | 93 | |
|
96 | 94 | def _get_landing_commit_or_none(self, db_repo): |
|
97 | 95 | log.debug("Getting the landing commit.") |
|
98 | 96 | try: |
|
99 | 97 | commit = db_repo.get_landing_commit() |
|
100 | 98 | if not isinstance(commit, EmptyCommit): |
|
101 | 99 | return commit |
|
102 | 100 | else: |
|
103 | 101 | log.debug("Repository is empty, no README to render.") |
|
104 | 102 | except CommitError: |
|
105 | 103 | log.exception( |
|
106 | 104 | "Problem getting commit when trying to render the README.") |
|
107 | 105 | |
|
108 | 106 | def _render_readme_or_none(self, commit, readme_node, relative_urls): |
|
109 | 107 | log.debug( |
|
110 | 108 | 'Found README file `%s` rendering...', readme_node.path) |
|
111 | 109 | renderer = MarkupRenderer() |
|
112 | 110 | try: |
|
113 | 111 | html_source = renderer.render( |
|
114 | 112 | readme_node.content, filename=readme_node.path) |
|
115 | 113 | if relative_urls: |
|
116 | 114 | return relative_links(html_source, relative_urls) |
|
117 | 115 | return html_source |
|
118 | 116 | except Exception: |
|
119 | 117 | log.exception( |
|
120 | 118 | "Exception while trying to render the README") |
|
121 | 119 | |
|
122 | 120 | def _load_commits_context(self, c): |
|
123 | 121 | p = safe_int(self.request.GET.get('page'), 1) |
|
124 | 122 | size = safe_int(self.request.GET.get('size'), 10) |
|
125 | 123 | |
|
126 | 124 | def url_generator(**kw): |
|
127 | 125 | query_params = { |
|
128 | 126 | 'size': size |
|
129 | 127 | } |
|
130 | 128 | query_params.update(kw) |
|
131 | 129 | return h.route_path( |
|
132 | 130 | 'repo_summary_commits', |
|
133 | 131 | repo_name=c.rhodecode_db_repo.repo_name, _query=query_params) |
|
134 | 132 | |
|
135 | 133 | pre_load = ['author', 'branch', 'date', 'message'] |
|
136 | 134 | try: |
|
137 | 135 | collection = self.rhodecode_vcs_repo.get_commits(pre_load=pre_load) |
|
138 | 136 | except EmptyRepositoryError: |
|
139 | 137 | collection = self.rhodecode_vcs_repo |
|
140 | 138 | |
|
141 | 139 | c.repo_commits = RepoPage( |
|
142 | 140 | collection, page=p, items_per_page=size, url=url_generator) |
|
143 | 141 | page_ids = [x.raw_id for x in c.repo_commits] |
|
144 | 142 | c.comments = self.db_repo.get_comments(page_ids) |
|
145 | 143 | c.statuses = self.db_repo.statuses(page_ids) |
|
146 | 144 | |
|
147 | 145 | @LoginRequired() |
|
148 | 146 | @HasRepoPermissionAnyDecorator( |
|
149 | 147 | 'repository.read', 'repository.write', 'repository.admin') |
|
150 | 148 | @view_config( |
|
151 | 149 | route_name='repo_summary_commits', request_method='GET', |
|
152 | 150 | renderer='rhodecode:templates/summary/summary_commits.mako') |
|
153 | 151 | def summary_commits(self): |
|
154 | 152 | c = self.load_default_context() |
|
155 | 153 | self._load_commits_context(c) |
|
156 | 154 | return self._get_template_context(c) |
|
157 | 155 | |
|
158 | 156 | @LoginRequired() |
|
159 | 157 | @HasRepoPermissionAnyDecorator( |
|
160 | 158 | 'repository.read', 'repository.write', 'repository.admin') |
|
161 | 159 | @view_config( |
|
162 | 160 | route_name='repo_summary', request_method='GET', |
|
163 | 161 | renderer='rhodecode:templates/summary/summary.mako') |
|
164 | 162 | @view_config( |
|
165 | 163 | route_name='repo_summary_slash', request_method='GET', |
|
166 | 164 | renderer='rhodecode:templates/summary/summary.mako') |
|
167 | 165 | @view_config( |
|
168 | 166 | route_name='repo_summary_explicit', request_method='GET', |
|
169 | 167 | renderer='rhodecode:templates/summary/summary.mako') |
|
170 | 168 | def summary(self): |
|
171 | 169 | c = self.load_default_context() |
|
172 | 170 | |
|
173 | 171 | # Prepare the clone URL |
|
174 | 172 | username = '' |
|
175 | 173 | if self._rhodecode_user.username != User.DEFAULT_USER: |
|
176 | 174 | username = safe_str(self._rhodecode_user.username) |
|
177 | 175 | |
|
178 | 176 | _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl |
|
179 | 177 | if '{repo}' in _def_clone_uri: |
|
180 | 178 | _def_clone_uri_by_id = _def_clone_uri.replace( |
|
181 | 179 | '{repo}', '_{repoid}') |
|
182 | 180 | elif '{repoid}' in _def_clone_uri: |
|
183 | 181 | _def_clone_uri_by_id = _def_clone_uri.replace( |
|
184 | 182 | '_{repoid}', '{repo}') |
|
185 | 183 | |
|
186 | 184 | c.clone_repo_url = self.db_repo.clone_url( |
|
187 | 185 | user=username, uri_tmpl=_def_clone_uri) |
|
188 | 186 | c.clone_repo_url_id = self.db_repo.clone_url( |
|
189 | 187 | user=username, uri_tmpl=_def_clone_uri_by_id) |
|
190 | 188 | |
|
191 | 189 | # If enabled, get statistics data |
|
192 | 190 | |
|
193 | 191 | c.show_stats = bool(self.db_repo.enable_statistics) |
|
194 | 192 | |
|
195 | 193 | stats = Session().query(Statistics) \ |
|
196 | 194 | .filter(Statistics.repository == self.db_repo) \ |
|
197 | 195 | .scalar() |
|
198 | 196 | |
|
199 | 197 | c.stats_percentage = 0 |
|
200 | 198 | |
|
201 | 199 | if stats and stats.languages: |
|
202 | 200 | c.no_data = False is self.db_repo.enable_statistics |
|
203 | 201 | lang_stats_d = json.loads(stats.languages) |
|
204 | 202 | |
|
205 | 203 | # Sort first by decreasing count and second by the file extension, |
|
206 | 204 | # so we have a consistent output. |
|
207 | 205 | lang_stats_items = sorted(lang_stats_d.iteritems(), |
|
208 | 206 | key=lambda k: (-k[1], k[0]))[:10] |
|
209 | 207 | lang_stats = [(x, {"count": y, |
|
210 | 208 | "desc": LANGUAGES_EXTENSIONS_MAP.get(x)}) |
|
211 | 209 | for x, y in lang_stats_items] |
|
212 | 210 | |
|
213 | 211 | c.trending_languages = json.dumps(lang_stats) |
|
214 | 212 | else: |
|
215 | 213 | c.no_data = True |
|
216 | 214 | c.trending_languages = json.dumps({}) |
|
217 | 215 | |
|
218 | 216 | scm_model = ScmModel() |
|
219 | 217 | c.enable_downloads = self.db_repo.enable_downloads |
|
220 | 218 | c.repository_followers = scm_model.get_followers(self.db_repo) |
|
221 | 219 | c.repository_forks = scm_model.get_forks(self.db_repo) |
|
222 | 220 | c.repository_is_user_following = scm_model.is_following_repo( |
|
223 | 221 | self.db_repo_name, self._rhodecode_user.user_id) |
|
224 | 222 | |
|
225 | 223 | # first interaction with the VCS instance after here... |
|
226 | 224 | if c.repository_requirements_missing: |
|
227 | 225 | self.request.override_renderer = \ |
|
228 | 226 | 'rhodecode:templates/summary/missing_requirements.mako' |
|
229 | 227 | return self._get_template_context(c) |
|
230 | 228 | |
|
231 | 229 | c.readme_data, c.readme_file = \ |
|
232 | 230 | self._get_readme_data(self.db_repo, c.visual.default_renderer) |
|
233 | 231 | |
|
234 | 232 | # loads the summary commits template context |
|
235 | 233 | self._load_commits_context(c) |
|
236 | 234 | |
|
237 | 235 | return self._get_template_context(c) |
|
238 | 236 | |
|
239 | 237 | def get_request_commit_id(self): |
|
240 | 238 | return self.request.matchdict['commit_id'] |
|
241 | 239 | |
|
242 | 240 | @LoginRequired() |
|
243 | 241 | @HasRepoPermissionAnyDecorator( |
|
244 | 242 | 'repository.read', 'repository.write', 'repository.admin') |
|
245 | 243 | @view_config( |
|
246 | 244 | route_name='repo_stats', request_method='GET', |
|
247 | 245 | renderer='json_ext') |
|
248 | 246 | def repo_stats(self): |
|
249 | 247 | commit_id = self.get_request_commit_id() |
|
250 | 248 | |
|
251 | 249 | _namespace = caches.get_repo_namespace_key( |
|
252 | 250 | caches.SUMMARY_STATS, self.db_repo_name) |
|
253 | 251 | show_stats = bool(self.db_repo.enable_statistics) |
|
254 | 252 | cache_manager = caches.get_cache_manager( |
|
255 | 253 | 'repo_cache_long', _namespace) |
|
256 | 254 | _cache_key = caches.compute_key_from_params( |
|
257 | 255 | self.db_repo_name, commit_id, show_stats) |
|
258 | 256 | |
|
259 | 257 | def compute_stats(): |
|
260 | 258 | code_stats = {} |
|
261 | 259 | size = 0 |
|
262 | 260 | try: |
|
263 | 261 | scm_instance = self.db_repo.scm_instance() |
|
264 | 262 | commit = scm_instance.get_commit(commit_id) |
|
265 | 263 | |
|
266 | 264 | for node in commit.get_filenodes_generator(): |
|
267 | 265 | size += node.size |
|
268 | 266 | if not show_stats: |
|
269 | 267 | continue |
|
270 | 268 | ext = string.lower(node.extension) |
|
271 | 269 | ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext) |
|
272 | 270 | if ext_info: |
|
273 | 271 | if ext in code_stats: |
|
274 | 272 | code_stats[ext]['count'] += 1 |
|
275 | 273 | else: |
|
276 | 274 | code_stats[ext] = {"count": 1, "desc": ext_info} |
|
277 | 275 | except EmptyRepositoryError: |
|
278 | 276 | pass |
|
279 | 277 | return {'size': h.format_byte_size_binary(size), |
|
280 | 278 | 'code_stats': code_stats} |
|
281 | 279 | |
|
282 | 280 | stats = cache_manager.get(_cache_key, createfunc=compute_stats) |
|
283 | 281 | return stats |
|
284 | 282 | |
|
285 | 283 | @LoginRequired() |
|
286 | 284 | @HasRepoPermissionAnyDecorator( |
|
287 | 285 | 'repository.read', 'repository.write', 'repository.admin') |
|
288 | 286 | @view_config( |
|
289 | 287 | route_name='repo_refs_data', request_method='GET', |
|
290 | 288 | renderer='json_ext') |
|
291 | 289 | def repo_refs_data(self): |
|
292 | 290 | _ = self.request.translate |
|
293 | 291 | self.load_default_context() |
|
294 | 292 | |
|
295 | 293 | repo = self.rhodecode_vcs_repo |
|
296 | 294 | refs_to_create = [ |
|
297 | 295 | (_("Branch"), repo.branches, 'branch'), |
|
298 | 296 | (_("Tag"), repo.tags, 'tag'), |
|
299 | 297 | (_("Bookmark"), repo.bookmarks, 'book'), |
|
300 | 298 | ] |
|
301 | 299 | res = self._create_reference_data( |
|
302 | 300 | repo, self.db_repo_name, refs_to_create) |
|
303 | 301 | data = { |
|
304 | 302 | 'more': False, |
|
305 | 303 | 'results': res |
|
306 | 304 | } |
|
307 | 305 | return data |
|
308 | 306 | |
|
309 | 307 | @LoginRequired() |
|
310 | 308 | @HasRepoPermissionAnyDecorator( |
|
311 | 309 | 'repository.read', 'repository.write', 'repository.admin') |
|
312 | 310 | @view_config( |
|
313 | 311 | route_name='repo_refs_changelog_data', request_method='GET', |
|
314 | 312 | renderer='json_ext') |
|
315 | 313 | def repo_refs_changelog_data(self): |
|
316 | 314 | _ = self.request.translate |
|
317 | 315 | self.load_default_context() |
|
318 | 316 | |
|
319 | 317 | repo = self.rhodecode_vcs_repo |
|
320 | 318 | |
|
321 | 319 | refs_to_create = [ |
|
322 | 320 | (_("Branches"), repo.branches, 'branch'), |
|
323 | 321 | (_("Closed branches"), repo.branches_closed, 'branch_closed'), |
|
324 | 322 | # TODO: enable when vcs can handle bookmarks filters |
|
325 | 323 | # (_("Bookmarks"), repo.bookmarks, "book"), |
|
326 | 324 | ] |
|
327 | 325 | res = self._create_reference_data( |
|
328 | 326 | repo, self.db_repo_name, refs_to_create) |
|
329 | 327 | data = { |
|
330 | 328 | 'more': False, |
|
331 | 329 | 'results': res |
|
332 | 330 | } |
|
333 | 331 | return data |
|
334 | 332 | |
|
335 | 333 | def _create_reference_data(self, repo, full_repo_name, refs_to_create): |
|
336 | 334 | format_ref_id = utils.get_format_ref_id(repo) |
|
337 | 335 | |
|
338 | 336 | result = [] |
|
339 | 337 | for title, refs, ref_type in refs_to_create: |
|
340 | 338 | if refs: |
|
341 | 339 | result.append({ |
|
342 | 340 | 'text': title, |
|
343 | 341 | 'children': self._create_reference_items( |
|
344 | 342 | repo, full_repo_name, refs, ref_type, |
|
345 | 343 | format_ref_id), |
|
346 | 344 | }) |
|
347 | 345 | return result |
|
348 | 346 | |
|
349 | 347 | def _create_reference_items(self, repo, full_repo_name, refs, ref_type, |
|
350 | 348 | format_ref_id): |
|
351 | 349 | result = [] |
|
352 | 350 | is_svn = h.is_svn(repo) |
|
353 | 351 | for ref_name, raw_id in refs.iteritems(): |
|
354 | 352 | files_url = self._create_files_url( |
|
355 | 353 | repo, full_repo_name, ref_name, raw_id, is_svn) |
|
356 | 354 | result.append({ |
|
357 | 355 | 'text': ref_name, |
|
358 | 356 | 'id': format_ref_id(ref_name, raw_id), |
|
359 | 357 | 'raw_id': raw_id, |
|
360 | 358 | 'type': ref_type, |
|
361 | 359 | 'files_url': files_url, |
|
362 | 360 | }) |
|
363 | 361 | return result |
|
364 | 362 | |
|
365 | 363 | def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn): |
|
366 | 364 | use_commit_id = '/' in ref_name or is_svn |
|
367 | 365 | return h.route_path( |
|
368 | 366 | 'repo_files', |
|
369 | 367 | repo_name=full_repo_name, |
|
370 | 368 | f_path=ref_name if is_svn else '', |
|
371 | 369 | commit_id=raw_id if use_commit_id else ref_name, |
|
372 | 370 | _query=dict(at=ref_name)) |
@@ -1,455 +1,454 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2012-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 deform |
|
22 | 22 | import logging |
|
23 | 23 | import peppercorn |
|
24 | 24 | import webhelpers.paginate |
|
25 | 25 | |
|
26 | 26 | from pyramid.httpexceptions import HTTPFound, HTTPForbidden |
|
27 | 27 | |
|
28 | 28 | from rhodecode.apps._base import BaseAppView |
|
29 | 29 | from rhodecode.integrations import integration_type_registry |
|
30 | 30 | from rhodecode.apps.admin.navigation import navigation_list |
|
31 | 31 | from rhodecode.lib.auth import ( |
|
32 | 32 | LoginRequired, CSRFRequired, HasPermissionAnyDecorator, |
|
33 | 33 | HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator) |
|
34 | 34 | from rhodecode.lib.utils2 import safe_int |
|
35 | 35 | from rhodecode.lib.helpers import Page |
|
36 | 36 | from rhodecode.model.db import Repository, RepoGroup, Session, Integration |
|
37 | 37 | from rhodecode.model.scm import ScmModel |
|
38 | 38 | from rhodecode.model.integration import IntegrationModel |
|
39 | 39 | from rhodecode.model.validation_schema.schemas.integration_schema import ( |
|
40 | 40 | make_integration_schema, IntegrationScopeType) |
|
41 | 41 | |
|
42 | 42 | log = logging.getLogger(__name__) |
|
43 | 43 | |
|
44 | 44 | |
|
45 | 45 | class IntegrationSettingsViewBase(BaseAppView): |
|
46 | 46 | """ |
|
47 | 47 | Base Integration settings view used by both repo / global settings |
|
48 | 48 | """ |
|
49 | 49 | |
|
50 | 50 | def __init__(self, context, request): |
|
51 | 51 | super(IntegrationSettingsViewBase, self).__init__(context, request) |
|
52 | 52 | self._load_view_context() |
|
53 | 53 | |
|
54 | 54 | def _load_view_context(self): |
|
55 | 55 | """ |
|
56 | 56 | This avoids boilerplate for repo/global+list/edit+views/templates |
|
57 | 57 | by doing all possible contexts at the same time however it should |
|
58 | 58 | be split up into separate functions once more "contexts" exist |
|
59 | 59 | """ |
|
60 | 60 | |
|
61 | 61 | self.IntegrationType = None |
|
62 | 62 | self.repo = None |
|
63 | 63 | self.repo_group = None |
|
64 | 64 | self.integration = None |
|
65 | 65 | self.integrations = {} |
|
66 | 66 | |
|
67 | 67 | request = self.request |
|
68 | 68 | |
|
69 | 69 | if 'repo_name' in request.matchdict: # in repo settings context |
|
70 | 70 | repo_name = request.matchdict['repo_name'] |
|
71 | 71 | self.repo = Repository.get_by_repo_name(repo_name) |
|
72 | 72 | |
|
73 | 73 | if 'repo_group_name' in request.matchdict: # in group settings context |
|
74 | 74 | repo_group_name = request.matchdict['repo_group_name'] |
|
75 | 75 | self.repo_group = RepoGroup.get_by_group_name(repo_group_name) |
|
76 | 76 | |
|
77 | 77 | if 'integration' in request.matchdict: # integration type context |
|
78 | 78 | integration_type = request.matchdict['integration'] |
|
79 | 79 | self.IntegrationType = integration_type_registry[integration_type] |
|
80 | 80 | |
|
81 | 81 | if 'integration_id' in request.matchdict: # single integration context |
|
82 | 82 | integration_id = request.matchdict['integration_id'] |
|
83 | 83 | self.integration = Integration.get(integration_id) |
|
84 | 84 | |
|
85 | 85 | # extra perms check just in case |
|
86 | 86 | if not self._has_perms_for_integration(self.integration): |
|
87 | 87 | raise HTTPForbidden() |
|
88 | 88 | |
|
89 | 89 | self.settings = self.integration and self.integration.settings or {} |
|
90 | 90 | self.admin_view = not (self.repo or self.repo_group) |
|
91 | 91 | |
|
92 | 92 | def _has_perms_for_integration(self, integration): |
|
93 | 93 | perms = self.request.user.permissions |
|
94 | 94 | |
|
95 | 95 | if 'hg.admin' in perms['global']: |
|
96 | 96 | return True |
|
97 | 97 | |
|
98 | 98 | if integration.repo: |
|
99 | 99 | return perms['repositories'].get( |
|
100 | 100 | integration.repo.repo_name) == 'repository.admin' |
|
101 | 101 | |
|
102 | 102 | if integration.repo_group: |
|
103 | 103 | return perms['repositories_groups'].get( |
|
104 | 104 | integration.repo_group.group_name) == 'group.admin' |
|
105 | 105 | |
|
106 | 106 | return False |
|
107 | 107 | |
|
108 | 108 | def _get_local_tmpl_context(self, include_app_defaults=False): |
|
109 | 109 | _ = self.request.translate |
|
110 | 110 | c = super(IntegrationSettingsViewBase, self)._get_local_tmpl_context( |
|
111 | 111 | include_app_defaults=include_app_defaults) |
|
112 | 112 | |
|
113 | 113 | c.active = 'integrations' |
|
114 | 114 | |
|
115 | 115 | return c |
|
116 | 116 | |
|
117 | 117 | def _form_schema(self): |
|
118 | 118 | schema = make_integration_schema(IntegrationType=self.IntegrationType, |
|
119 | 119 | settings=self.settings) |
|
120 | 120 | |
|
121 | 121 | # returns a clone, important if mutating the schema later |
|
122 | 122 | return schema.bind( |
|
123 | 123 | permissions=self.request.user.permissions, |
|
124 | 124 | no_scope=not self.admin_view) |
|
125 | 125 | |
|
126 | 126 | def _form_defaults(self): |
|
127 | 127 | _ = self.request.translate |
|
128 | 128 | defaults = {} |
|
129 | 129 | |
|
130 | 130 | if self.integration: |
|
131 | 131 | defaults['settings'] = self.integration.settings or {} |
|
132 | 132 | defaults['options'] = { |
|
133 | 133 | 'name': self.integration.name, |
|
134 | 134 | 'enabled': self.integration.enabled, |
|
135 | 135 | 'scope': { |
|
136 | 136 | 'repo': self.integration.repo, |
|
137 | 137 | 'repo_group': self.integration.repo_group, |
|
138 | 138 | 'child_repos_only': self.integration.child_repos_only, |
|
139 | 139 | }, |
|
140 | 140 | } |
|
141 | 141 | else: |
|
142 | 142 | if self.repo: |
|
143 | 143 | scope = _('{repo_name} repository').format( |
|
144 | 144 | repo_name=self.repo.repo_name) |
|
145 | 145 | elif self.repo_group: |
|
146 | 146 | scope = _('{repo_group_name} repo group').format( |
|
147 | 147 | repo_group_name=self.repo_group.group_name) |
|
148 | 148 | else: |
|
149 | 149 | scope = _('Global') |
|
150 | 150 | |
|
151 | 151 | defaults['options'] = { |
|
152 | 152 | 'enabled': True, |
|
153 | 153 | 'name': _('{name} integration').format( |
|
154 | 154 | name=self.IntegrationType.display_name), |
|
155 | 155 | } |
|
156 | 156 | defaults['options']['scope'] = { |
|
157 | 157 | 'repo': self.repo, |
|
158 | 158 | 'repo_group': self.repo_group, |
|
159 | 159 | } |
|
160 | 160 | |
|
161 | 161 | return defaults |
|
162 | 162 | |
|
163 | 163 | def _delete_integration(self, integration): |
|
164 | 164 | _ = self.request.translate |
|
165 | 165 | Session().delete(integration) |
|
166 | 166 | Session().commit() |
|
167 | 167 | self.request.session.flash( |
|
168 | 168 | _('Integration {integration_name} deleted successfully.').format( |
|
169 | 169 | integration_name=integration.name), |
|
170 | 170 | queue='success') |
|
171 | 171 | |
|
172 | 172 | if self.repo: |
|
173 | 173 | redirect_to = self.request.route_path( |
|
174 | 174 | 'repo_integrations_home', repo_name=self.repo.repo_name) |
|
175 | 175 | elif self.repo_group: |
|
176 | 176 | redirect_to = self.request.route_path( |
|
177 | 177 | 'repo_group_integrations_home', |
|
178 | 178 | repo_group_name=self.repo_group.group_name) |
|
179 | 179 | else: |
|
180 | 180 | redirect_to = self.request.route_path('global_integrations_home') |
|
181 | 181 | raise HTTPFound(redirect_to) |
|
182 | 182 | |
|
183 | 183 | def _integration_list(self): |
|
184 | 184 | """ List integrations """ |
|
185 | 185 | |
|
186 | 186 | c = self.load_default_context() |
|
187 | 187 | if self.repo: |
|
188 | 188 | scope = self.repo |
|
189 | 189 | elif self.repo_group: |
|
190 | 190 | scope = self.repo_group |
|
191 | 191 | else: |
|
192 | 192 | scope = 'all' |
|
193 | 193 | |
|
194 | 194 | integrations = [] |
|
195 | 195 | |
|
196 | 196 | for IntType, integration in IntegrationModel().get_integrations( |
|
197 | 197 | scope=scope, IntegrationType=self.IntegrationType): |
|
198 | 198 | |
|
199 | 199 | # extra permissions check *just in case* |
|
200 | 200 | if not self._has_perms_for_integration(integration): |
|
201 | 201 | continue |
|
202 | 202 | |
|
203 | 203 | integrations.append((IntType, integration)) |
|
204 | 204 | |
|
205 | 205 | sort_arg = self.request.GET.get('sort', 'name:asc') |
|
206 | 206 | if ':' in sort_arg: |
|
207 | 207 | sort_field, sort_dir = sort_arg.split(':') |
|
208 | 208 | else: |
|
209 | 209 | sort_field = sort_arg, 'asc' |
|
210 | 210 | |
|
211 | 211 | assert sort_field in ('name', 'integration_type', 'enabled', 'scope') |
|
212 | 212 | |
|
213 | 213 | integrations.sort( |
|
214 | 214 | key=lambda x: getattr(x[1], sort_field), |
|
215 | 215 | reverse=(sort_dir == 'desc')) |
|
216 | 216 | |
|
217 | 217 | page_url = webhelpers.paginate.PageURL( |
|
218 | 218 | self.request.path, self.request.GET) |
|
219 | 219 | page = safe_int(self.request.GET.get('page', 1), 1) |
|
220 | 220 | |
|
221 | 221 | integrations = Page( |
|
222 | 222 | integrations, page=page, items_per_page=10, url=page_url) |
|
223 | 223 | |
|
224 | 224 | c.rev_sort_dir = sort_dir != 'desc' and 'desc' or 'asc' |
|
225 | 225 | |
|
226 | 226 | c.current_IntegrationType = self.IntegrationType |
|
227 | 227 | c.integrations_list = integrations |
|
228 | 228 | c.available_integrations = integration_type_registry |
|
229 | 229 | |
|
230 | 230 | return self._get_template_context(c) |
|
231 | 231 | |
|
232 | 232 | def _settings_get(self, defaults=None, form=None): |
|
233 | 233 | """ |
|
234 | 234 | View that displays the integration settings as a form. |
|
235 | 235 | """ |
|
236 | 236 | c = self.load_default_context() |
|
237 | 237 | |
|
238 | 238 | defaults = defaults or self._form_defaults() |
|
239 | 239 | schema = self._form_schema() |
|
240 | 240 | |
|
241 | 241 | if self.integration: |
|
242 | 242 | buttons = ('submit', 'delete') |
|
243 | 243 | else: |
|
244 | 244 | buttons = ('submit',) |
|
245 | 245 | |
|
246 | 246 | form = form or deform.Form(schema, appstruct=defaults, buttons=buttons) |
|
247 | 247 | |
|
248 | 248 | c.form = form |
|
249 | 249 | c.current_IntegrationType = self.IntegrationType |
|
250 | 250 | c.integration = self.integration |
|
251 | 251 | |
|
252 | 252 | return self._get_template_context(c) |
|
253 | 253 | |
|
254 | 254 | def _settings_post(self): |
|
255 | 255 | """ |
|
256 | 256 | View that validates and stores the integration settings. |
|
257 | 257 | """ |
|
258 | 258 | _ = self.request.translate |
|
259 | 259 | |
|
260 | 260 | controls = self.request.POST.items() |
|
261 | 261 | pstruct = peppercorn.parse(controls) |
|
262 | 262 | |
|
263 | 263 | if self.integration and pstruct.get('delete'): |
|
264 | 264 | return self._delete_integration(self.integration) |
|
265 | 265 | |
|
266 | 266 | schema = self._form_schema() |
|
267 | 267 | |
|
268 | 268 | skip_settings_validation = False |
|
269 | 269 | if self.integration and 'enabled' not in pstruct.get('options', {}): |
|
270 | 270 | skip_settings_validation = True |
|
271 | 271 | schema['settings'].validator = None |
|
272 | 272 | for field in schema['settings'].children: |
|
273 | 273 | field.validator = None |
|
274 | 274 | field.missing = '' |
|
275 | 275 | |
|
276 | 276 | if self.integration: |
|
277 | 277 | buttons = ('submit', 'delete') |
|
278 | 278 | else: |
|
279 | 279 | buttons = ('submit',) |
|
280 | 280 | |
|
281 | 281 | form = deform.Form(schema, buttons=buttons) |
|
282 | 282 | |
|
283 | 283 | if not self.admin_view: |
|
284 | 284 | # scope is read only field in these cases, and has to be added |
|
285 | 285 | options = pstruct.setdefault('options', {}) |
|
286 | 286 | if 'scope' not in options: |
|
287 | 287 | options['scope'] = IntegrationScopeType().serialize(None, { |
|
288 | 288 | 'repo': self.repo, |
|
289 | 289 | 'repo_group': self.repo_group, |
|
290 | 290 | }) |
|
291 | 291 | |
|
292 | 292 | try: |
|
293 | 293 | valid_data = form.validate_pstruct(pstruct) |
|
294 | 294 | except deform.ValidationFailure as e: |
|
295 | 295 | self.request.session.flash( |
|
296 | 296 | _('Errors exist when saving integration settings. ' |
|
297 | 297 | 'Please check the form inputs.'), |
|
298 | 298 | queue='error') |
|
299 | 299 | return self._settings_get(form=e) |
|
300 | 300 | |
|
301 | 301 | if not self.integration: |
|
302 | 302 | self.integration = Integration() |
|
303 | 303 | self.integration.integration_type = self.IntegrationType.key |
|
304 | 304 | Session().add(self.integration) |
|
305 | 305 | |
|
306 | 306 | scope = valid_data['options']['scope'] |
|
307 | 307 | |
|
308 | 308 | IntegrationModel().update_integration(self.integration, |
|
309 | 309 | name=valid_data['options']['name'], |
|
310 | 310 | enabled=valid_data['options']['enabled'], |
|
311 | 311 | settings=valid_data['settings'], |
|
312 | 312 | repo=scope['repo'], |
|
313 | 313 | repo_group=scope['repo_group'], |
|
314 | 314 | child_repos_only=scope['child_repos_only'], |
|
315 | 315 | ) |
|
316 | 316 | |
|
317 | 317 | self.integration.settings = valid_data['settings'] |
|
318 | 318 | Session().commit() |
|
319 | 319 | # Display success message and redirect. |
|
320 | 320 | self.request.session.flash( |
|
321 | 321 | _('Integration {integration_name} updated successfully.').format( |
|
322 | 322 | integration_name=self.IntegrationType.display_name), |
|
323 | 323 | queue='success') |
|
324 | 324 | |
|
325 | 325 | # if integration scope changes, we must redirect to the right place |
|
326 | 326 | # keeping in mind if the original view was for /repo/ or /_admin/ |
|
327 | 327 | admin_view = not (self.repo or self.repo_group) |
|
328 | 328 | |
|
329 | 329 | if self.integration.repo and not admin_view: |
|
330 | 330 | redirect_to = self.request.route_path( |
|
331 | 331 | 'repo_integrations_edit', |
|
332 | 332 | repo_name=self.integration.repo.repo_name, |
|
333 | 333 | integration=self.integration.integration_type, |
|
334 | 334 | integration_id=self.integration.integration_id) |
|
335 | 335 | elif self.integration.repo_group and not admin_view: |
|
336 | 336 | redirect_to = self.request.route_path( |
|
337 | 337 | 'repo_group_integrations_edit', |
|
338 | 338 | repo_group_name=self.integration.repo_group.group_name, |
|
339 | 339 | integration=self.integration.integration_type, |
|
340 | 340 | integration_id=self.integration.integration_id) |
|
341 | 341 | else: |
|
342 | 342 | redirect_to = self.request.route_path( |
|
343 | 343 | 'global_integrations_edit', |
|
344 | 344 | integration=self.integration.integration_type, |
|
345 | 345 | integration_id=self.integration.integration_id) |
|
346 | 346 | |
|
347 | 347 | return HTTPFound(redirect_to) |
|
348 | 348 | |
|
349 | 349 | def _new_integration(self): |
|
350 | 350 | c = self.load_default_context() |
|
351 | 351 | c.available_integrations = integration_type_registry |
|
352 | 352 | return self._get_template_context(c) |
|
353 | 353 | |
|
354 | 354 | def load_default_context(self): |
|
355 | 355 | raise NotImplementedError() |
|
356 | 356 | |
|
357 | 357 | |
|
358 | 358 | class GlobalIntegrationsView(IntegrationSettingsViewBase): |
|
359 | 359 | def load_default_context(self): |
|
360 | 360 | c = self._get_local_tmpl_context() |
|
361 | 361 | c.repo = self.repo |
|
362 | 362 | c.repo_group = self.repo_group |
|
363 | 363 | c.navlist = navigation_list(self.request) |
|
364 | 364 | self._register_global_c(c) |
|
365 | 365 | return c |
|
366 | 366 | |
|
367 | 367 | @LoginRequired() |
|
368 | 368 | @HasPermissionAnyDecorator('hg.admin') |
|
369 | 369 | def integration_list(self): |
|
370 | 370 | return self._integration_list() |
|
371 | 371 | |
|
372 | 372 | @LoginRequired() |
|
373 | 373 | @HasPermissionAnyDecorator('hg.admin') |
|
374 | 374 | def settings_get(self): |
|
375 | 375 | return self._settings_get() |
|
376 | 376 | |
|
377 | 377 | @LoginRequired() |
|
378 | 378 | @HasPermissionAnyDecorator('hg.admin') |
|
379 | 379 | @CSRFRequired() |
|
380 | 380 | def settings_post(self): |
|
381 | 381 | return self._settings_post() |
|
382 | 382 | |
|
383 | 383 | @LoginRequired() |
|
384 | 384 | @HasPermissionAnyDecorator('hg.admin') |
|
385 | 385 | def new_integration(self): |
|
386 | 386 | return self._new_integration() |
|
387 | 387 | |
|
388 | 388 | |
|
389 | 389 | class RepoIntegrationsView(IntegrationSettingsViewBase): |
|
390 | 390 | def load_default_context(self): |
|
391 | 391 | c = self._get_local_tmpl_context() |
|
392 | 392 | |
|
393 | 393 | c.repo = self.repo |
|
394 | 394 | c.repo_group = self.repo_group |
|
395 | 395 | |
|
396 | # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead | |
|
397 | c.repo_info = self.db_repo = self.repo | |
|
396 | self.db_repo = self.repo | |
|
398 | 397 | c.rhodecode_db_repo = self.repo |
|
399 | 398 | c.repo_name = self.db_repo.repo_name |
|
400 | 399 | c.repository_pull_requests = ScmModel().get_pull_requests(self.repo) |
|
401 | 400 | |
|
402 | 401 | self._register_global_c(c) |
|
403 | 402 | return c |
|
404 | 403 | |
|
405 | 404 | @LoginRequired() |
|
406 | 405 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
407 | 406 | def integration_list(self): |
|
408 | 407 | return self._integration_list() |
|
409 | 408 | |
|
410 | 409 | @LoginRequired() |
|
411 | 410 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
412 | 411 | def settings_get(self): |
|
413 | 412 | return self._settings_get() |
|
414 | 413 | |
|
415 | 414 | @LoginRequired() |
|
416 | 415 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
417 | 416 | @CSRFRequired() |
|
418 | 417 | def settings_post(self): |
|
419 | 418 | return self._settings_post() |
|
420 | 419 | |
|
421 | 420 | @LoginRequired() |
|
422 | 421 | @HasRepoPermissionAnyDecorator('repository.admin') |
|
423 | 422 | def new_integration(self): |
|
424 | 423 | return self._new_integration() |
|
425 | 424 | |
|
426 | 425 | |
|
427 | 426 | class RepoGroupIntegrationsView(IntegrationSettingsViewBase): |
|
428 | 427 | def load_default_context(self): |
|
429 | 428 | c = self._get_local_tmpl_context() |
|
430 | 429 | c.repo = self.repo |
|
431 | 430 | c.repo_group = self.repo_group |
|
432 | 431 | c.navlist = navigation_list(self.request) |
|
433 | 432 | self._register_global_c(c) |
|
434 | 433 | return c |
|
435 | 434 | |
|
436 | 435 | @LoginRequired() |
|
437 | 436 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
438 | 437 | def integration_list(self): |
|
439 | 438 | return self._integration_list() |
|
440 | 439 | |
|
441 | 440 | @LoginRequired() |
|
442 | 441 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
443 | 442 | def settings_get(self): |
|
444 | 443 | return self._settings_get() |
|
445 | 444 | |
|
446 | 445 | @LoginRequired() |
|
447 | 446 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
448 | 447 | @CSRFRequired() |
|
449 | 448 | def settings_post(self): |
|
450 | 449 | return self._settings_post() |
|
451 | 450 | |
|
452 | 451 | @LoginRequired() |
|
453 | 452 | @HasRepoGroupPermissionAnyDecorator('group.admin') |
|
454 | 453 | def new_integration(self): |
|
455 | 454 | return self._new_integration() |
@@ -1,99 +1,99 b'' | |||
|
1 | 1 | ## -*- coding: utf-8 -*- |
|
2 | 2 | ## |
|
3 | 3 | ## See also repo_settings.html |
|
4 | 4 | ## |
|
5 | 5 | <%inherit file="/base/base.mako"/> |
|
6 | 6 | |
|
7 | 7 | <%def name="title()"> |
|
8 |
${_('%s repository settings') % c.r |
|
|
8 | ${_('%s repository settings') % c.rhodecode_db_repo.repo_name} | |
|
9 | 9 | %if c.rhodecode_name: |
|
10 | 10 | · ${h.branding(c.rhodecode_name)} |
|
11 | 11 | %endif |
|
12 | 12 | </%def> |
|
13 | 13 | |
|
14 | 14 | <%def name="breadcrumbs_links()"> |
|
15 | 15 | ${_('Settings')} |
|
16 | 16 | </%def> |
|
17 | 17 | |
|
18 | 18 | <%def name="menu_bar_nav()"> |
|
19 | 19 | ${self.menu_items(active='repositories')} |
|
20 | 20 | </%def> |
|
21 | 21 | |
|
22 | 22 | <%def name="menu_bar_subnav()"> |
|
23 | 23 | ${self.repo_menu(active='options')} |
|
24 | 24 | </%def> |
|
25 | 25 | |
|
26 | 26 | <%def name="main_content()"> |
|
27 | 27 | % if hasattr(c, 'repo_edit_template'): |
|
28 | 28 | <%include file="${c.repo_edit_template}"/> |
|
29 | 29 | % else: |
|
30 | 30 | <%include file="/admin/repos/repo_edit_${c.active}.mako"/> |
|
31 | 31 | % endif |
|
32 | 32 | </%def> |
|
33 | 33 | |
|
34 | 34 | |
|
35 | 35 | <%def name="main()"> |
|
36 | 36 | <div class="box"> |
|
37 | 37 | <div class="title"> |
|
38 | 38 | ${self.repo_page_title(c.rhodecode_db_repo)} |
|
39 | 39 | ${self.breadcrumbs()} |
|
40 | 40 | </div> |
|
41 | 41 | |
|
42 | 42 | <div class="sidebar-col-wrapper scw-small"> |
|
43 | 43 | <div class="sidebar"> |
|
44 | 44 | <ul class="nav nav-pills nav-stacked"> |
|
45 | 45 | <li class="${'active' if c.active=='settings' else ''}"> |
|
46 | 46 | <a href="${h.route_path('edit_repo', repo_name=c.repo_name)}">${_('Settings')}</a> |
|
47 | 47 | </li> |
|
48 | 48 | <li class="${'active' if c.active=='permissions' else ''}"> |
|
49 | 49 | <a href="${h.route_path('edit_repo_perms', repo_name=c.repo_name)}">${_('Permissions')}</a> |
|
50 | 50 | </li> |
|
51 | 51 | <li class="${'active' if c.active=='advanced' else ''}"> |
|
52 | 52 | <a href="${h.route_path('edit_repo_advanced', repo_name=c.repo_name)}">${_('Advanced')}</a> |
|
53 | 53 | </li> |
|
54 | 54 | <li class="${'active' if c.active=='vcs' else ''}"> |
|
55 | 55 | <a href="${h.route_path('edit_repo_vcs', repo_name=c.repo_name)}">${_('VCS')}</a> |
|
56 | 56 | </li> |
|
57 | 57 | <li class="${'active' if c.active=='fields' else ''}"> |
|
58 | 58 | <a href="${h.route_path('edit_repo_fields', repo_name=c.repo_name)}">${_('Extra Fields')}</a> |
|
59 | 59 | </li> |
|
60 | 60 | <li class="${'active' if c.active=='issuetracker' else ''}"> |
|
61 | 61 | <a href="${h.route_path('edit_repo_issuetracker', repo_name=c.repo_name)}">${_('Issue Tracker')}</a> |
|
62 | 62 | </li> |
|
63 | 63 | <li class="${'active' if c.active=='caches' else ''}"> |
|
64 | 64 | <a href="${h.route_path('edit_repo_caches', repo_name=c.repo_name)}">${_('Caches')}</a> |
|
65 | 65 | </li> |
|
66 |
%if c.r |
|
|
66 | %if c.rhodecode_db_repo.repo_type != 'svn': | |
|
67 | 67 | <li class="${'active' if c.active=='remote' else ''}"> |
|
68 | 68 | <a href="${h.route_path('edit_repo_remote', repo_name=c.repo_name)}">${_('Remote')}</a> |
|
69 | 69 | </li> |
|
70 | 70 | %endif |
|
71 | 71 | <li class="${'active' if c.active=='statistics' else ''}"> |
|
72 | 72 | <a href="${h.route_path('edit_repo_statistics', repo_name=c.repo_name)}">${_('Statistics')}</a> |
|
73 | 73 | </li> |
|
74 | 74 | <li class="${'active' if c.active=='integrations' else ''}"> |
|
75 | 75 | <a href="${h.route_path('repo_integrations_home', repo_name=c.repo_name)}">${_('Integrations')}</a> |
|
76 | 76 | </li> |
|
77 |
%if c.r |
|
|
77 | %if c.rhodecode_db_repo.repo_type != 'svn': | |
|
78 | 78 | <li class="${'active' if c.active=='reviewers' else ''}"> |
|
79 | 79 | <a href="${h.route_path('repo_reviewers', repo_name=c.repo_name)}">${_('Reviewer Rules')}</a> |
|
80 | 80 | </li> |
|
81 | 81 | %endif |
|
82 | 82 | <li class="${'active' if c.active=='maintenance' else ''}"> |
|
83 | 83 | <a href="${h.route_path('edit_repo_maintenance', repo_name=c.repo_name)}">${_('Maintenance')}</a> |
|
84 | 84 | </li> |
|
85 | 85 | <li class="${'active' if c.active=='strip' else ''}"> |
|
86 | 86 | <a href="${h.route_path('edit_repo_strip', repo_name=c.repo_name)}">${_('Strip')}</a> |
|
87 | 87 | </li> |
|
88 | 88 | |
|
89 | 89 | </ul> |
|
90 | 90 | </div> |
|
91 | 91 | |
|
92 | 92 | <div class="main-content-full-width"> |
|
93 | 93 | ${self.main_content()} |
|
94 | 94 | </div> |
|
95 | 95 | |
|
96 | 96 | </div> |
|
97 | 97 | </div> |
|
98 | 98 | |
|
99 | 99 | </%def> No newline at end of file |
@@ -1,210 +1,210 b'' | |||
|
1 | 1 | <%namespace name="base" file="/base/base.mako"/> |
|
2 | 2 | |
|
3 | 3 | <% |
|
4 | 4 | elems = [ |
|
5 |
(_('Owner'), lambda:base.gravatar_with_user(c.r |
|
|
6 |
(_('Created on'), h.format_date(c.r |
|
|
7 |
(_('Updated on'), h.format_date(c.r |
|
|
8 |
(_('Cached Commit id'), lambda: h.link_to(c.r |
|
|
5 | (_('Owner'), lambda:base.gravatar_with_user(c.rhodecode_db_repo.user.email), '', ''), | |
|
6 | (_('Created on'), h.format_date(c.rhodecode_db_repo.created_on), '', ''), | |
|
7 | (_('Updated on'), h.format_date(c.rhodecode_db_repo.updated_on), '', ''), | |
|
8 | (_('Cached Commit id'), lambda: h.link_to(c.rhodecode_db_repo.changeset_cache.get('short_id'), h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.changeset_cache.get('raw_id'))), '', ''), | |
|
9 | 9 | ] |
|
10 | 10 | %> |
|
11 | 11 | |
|
12 | 12 | <div class="panel panel-default"> |
|
13 | 13 | <div class="panel-heading" id="advanced-info" > |
|
14 |
<h3 class="panel-title">${_('Repository: %s') % c.r |
|
|
14 | <h3 class="panel-title">${_('Repository: %s') % c.rhodecode_db_repo.repo_name} <a class="permalink" href="#advanced-info"> ¶</a></h3> | |
|
15 | 15 | </div> |
|
16 | 16 | <div class="panel-body"> |
|
17 | 17 | ${base.dt_info_panel(elems)} |
|
18 | 18 | </div> |
|
19 | 19 | </div> |
|
20 | 20 | |
|
21 | 21 | |
|
22 | 22 | <div class="panel panel-default"> |
|
23 | 23 | <div class="panel-heading" id="advanced-fork"> |
|
24 | 24 | <h3 class="panel-title">${_('Fork Reference')} <a class="permalink" href="#advanced-fork"> ¶</a></h3> |
|
25 | 25 | </div> |
|
26 | 26 | <div class="panel-body"> |
|
27 |
${h.secure_form(h.route_path('edit_repo_advanced_fork', repo_name=c.r |
|
|
27 | ${h.secure_form(h.route_path('edit_repo_advanced_fork', repo_name=c.rhodecode_db_repo.repo_name), method='POST', request=request)} | |
|
28 | 28 | |
|
29 |
% if c.r |
|
|
30 |
<div class="panel-body-title-text">${h.literal(_('This repository is a fork of %(repo_link)s') % {'repo_link': h.link_to_if(c.has_origin_repo_read_perm,c.r |
|
|
29 | % if c.rhodecode_db_repo.fork: | |
|
30 | <div class="panel-body-title-text">${h.literal(_('This repository is a fork of %(repo_link)s') % {'repo_link': h.link_to_if(c.has_origin_repo_read_perm,c.rhodecode_db_repo.fork.repo_name, h.route_path('repo_summary', repo_name=c.rhodecode_db_repo.fork.repo_name))})} | |
|
31 | 31 | | <button class="btn btn-link btn-danger" type="submit">Remove fork reference</button></div> |
|
32 | 32 | % endif |
|
33 | 33 | |
|
34 | 34 | <div class="field"> |
|
35 | 35 | ${h.hidden('id_fork_of')} |
|
36 |
${h.submit('set_as_fork_%s' % c.r |
|
|
36 | ${h.submit('set_as_fork_%s' % c.rhodecode_db_repo.repo_name,_('Set'),class_="btn btn-small",)} | |
|
37 | 37 | </div> |
|
38 | 38 | <div class="field"> |
|
39 | 39 | <span class="help-block">${_('Manually set this repository as a fork of another from the list')}</span> |
|
40 | 40 | </div> |
|
41 | 41 | ${h.end_form()} |
|
42 | 42 | </div> |
|
43 | 43 | </div> |
|
44 | 44 | |
|
45 | 45 | |
|
46 | 46 | <div class="panel panel-default"> |
|
47 | 47 | <div class="panel-heading" id="advanced-journal"> |
|
48 | 48 | <h3 class="panel-title">${_('Public Journal Visibility')} <a class="permalink" href="#advanced-journal"> ¶</a></h3> |
|
49 | 49 | </div> |
|
50 | 50 | <div class="panel-body"> |
|
51 |
${h.secure_form(h.route_path('edit_repo_advanced_journal', repo_name=c.r |
|
|
51 | ${h.secure_form(h.route_path('edit_repo_advanced_journal', repo_name=c.rhodecode_db_repo.repo_name), method='POST', request=request)} | |
|
52 | 52 | <div class="field"> |
|
53 | 53 | %if c.in_public_journal: |
|
54 | 54 | <button class="btn btn-small" type="submit"> |
|
55 | 55 | ${_('Remove from Public Journal')} |
|
56 | 56 | </button> |
|
57 | 57 | %else: |
|
58 | 58 | <button class="btn btn-small" type="submit"> |
|
59 | 59 | ${_('Add to Public Journal')} |
|
60 | 60 | </button> |
|
61 | 61 | %endif |
|
62 | 62 | </div> |
|
63 | 63 | <div class="field" > |
|
64 | 64 | <span class="help-block">${_('All actions made on this repository will be visible to everyone following the public journal.')}</span> |
|
65 | 65 | </div> |
|
66 | 66 | ${h.end_form()} |
|
67 | 67 | </div> |
|
68 | 68 | </div> |
|
69 | 69 | |
|
70 | 70 | |
|
71 | 71 | <div class="panel panel-default"> |
|
72 | 72 | <div class="panel-heading" id="advanced-locking"> |
|
73 | 73 | <h3 class="panel-title">${_('Locking state')} <a class="permalink" href="#advanced-locking"> ¶</a></h3> |
|
74 | 74 | </div> |
|
75 | 75 | <div class="panel-body"> |
|
76 |
${h.secure_form(h.route_path('edit_repo_advanced_locking', repo_name=c.r |
|
|
76 | ${h.secure_form(h.route_path('edit_repo_advanced_locking', repo_name=c.rhodecode_db_repo.repo_name), method='POST', request=request)} | |
|
77 | 77 | |
|
78 |
%if c.r |
|
|
79 |
<div class="panel-body-title-text">${'Locked by %s on %s. Lock reason: %s' % (h.person_by_id(c.r |
|
|
80 |
h.format_date(h. time_to_datetime(c.r |
|
|
78 | %if c.rhodecode_db_repo.locked[0]: | |
|
79 | <div class="panel-body-title-text">${'Locked by %s on %s. Lock reason: %s' % (h.person_by_id(c.rhodecode_db_repo.locked[0]), | |
|
80 | h.format_date(h. time_to_datetime(c.rhodecode_db_repo.locked[1])), c.rhodecode_db_repo.locked[2])}</div> | |
|
81 | 81 | %else: |
|
82 | 82 | <div class="panel-body-title-text">${_('This Repository is not currently locked.')}</div> |
|
83 | 83 | %endif |
|
84 | 84 | |
|
85 | 85 | <div class="field" > |
|
86 |
%if c.r |
|
|
86 | %if c.rhodecode_db_repo.locked[0]: | |
|
87 | 87 | ${h.hidden('set_unlock', '1')} |
|
88 | 88 | <button class="btn btn-small" type="submit" |
|
89 | 89 | onclick="return confirm('${_('Confirm to unlock repository.')}');"> |
|
90 | 90 | <i class="icon-unlock"></i> |
|
91 | 91 | ${_('Unlock repository')} |
|
92 | 92 | </button> |
|
93 | 93 | %else: |
|
94 | 94 | ${h.hidden('set_lock', '1')} |
|
95 | 95 | <button class="btn btn-small" type="submit" |
|
96 | 96 | onclick="return confirm('${_('Confirm to lock repository.')}');"> |
|
97 | 97 | <i class="icon-lock"></i> |
|
98 | 98 | ${_('Lock Repository')} |
|
99 | 99 | </button> |
|
100 | 100 | %endif |
|
101 | 101 | </div> |
|
102 | 102 | <div class="field" > |
|
103 | 103 | <span class="help-block"> |
|
104 | 104 | ${_('Force repository locking. This only works when anonymous access is disabled. Pulling from the repository locks the repository to that user until the same user pushes to that repository again.')} |
|
105 | 105 | </span> |
|
106 | 106 | </div> |
|
107 | 107 | ${h.end_form()} |
|
108 | 108 | </div> |
|
109 | 109 | </div> |
|
110 | 110 | |
|
111 | 111 | <div class="panel panel-danger"> |
|
112 | 112 | <div class="panel-heading" id="advanced-delete"> |
|
113 | 113 | <h3 class="panel-title">${_('Delete repository')} <a class="permalink" href="#advanced-delete"> ¶</a></h3> |
|
114 | 114 | </div> |
|
115 | 115 | <div class="panel-body"> |
|
116 | 116 | ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=c.repo_name), method='POST', request=request)} |
|
117 | 117 | <table class="display"> |
|
118 | 118 | <tr> |
|
119 | 119 | <td> |
|
120 |
${_ungettext('This repository has %s fork.', 'This repository has %s forks.', c.r |
|
|
120 | ${_ungettext('This repository has %s fork.', 'This repository has %s forks.', c.rhodecode_db_repo.forks.count()) % c.rhodecode_db_repo.forks.count()} | |
|
121 | 121 | </td> |
|
122 | 122 | <td> |
|
123 |
%if c.r |
|
|
123 | %if c.rhodecode_db_repo.forks.count(): | |
|
124 | 124 | <input type="radio" name="forks" value="detach_forks" checked="checked"/> <label for="forks">${_('Detach forks')}</label> |
|
125 | 125 | %endif |
|
126 | 126 | </td> |
|
127 | 127 | <td> |
|
128 |
%if c.r |
|
|
128 | %if c.rhodecode_db_repo.forks.count(): | |
|
129 | 129 | <input type="radio" name="forks" value="delete_forks"/> <label for="forks">${_('Delete forks')}</label> |
|
130 | 130 | %endif |
|
131 | 131 | </td> |
|
132 | 132 | </tr> |
|
133 | 133 | </table> |
|
134 | 134 | <div style="margin: 0 0 20px 0" class="fake-space"></div> |
|
135 | 135 | |
|
136 | 136 | <div class="field"> |
|
137 | 137 | <button class="btn btn-small btn-danger" type="submit" |
|
138 | 138 | onclick="return confirm('${_('Confirm to delete this repository: %s') % c.repo_name}');"> |
|
139 | 139 | <i class="icon-remove-sign"></i> |
|
140 | 140 | ${_('Delete This Repository')} |
|
141 | 141 | </button> |
|
142 | 142 | </div> |
|
143 | 143 | <div class="field"> |
|
144 | 144 | <span class="help-block"> |
|
145 | 145 | ${_('This repository will be renamed in a special way in order to make it inaccessible to RhodeCode Enterprise and its VCS systems. If you need to fully delete it from the file system, please do it manually, or with rhodecode-cleanup-repos command available in rhodecode-tools.')} |
|
146 | 146 | </span> |
|
147 | 147 | </div> |
|
148 | 148 | |
|
149 | 149 | ${h.end_form()} |
|
150 | 150 | </div> |
|
151 | 151 | </div> |
|
152 | 152 | |
|
153 | 153 | |
|
154 | 154 | <script> |
|
155 | 155 | |
|
156 |
var currentRepoId = ${c.r |
|
|
156 | var currentRepoId = ${c.rhodecode_db_repo.repo_id}; | |
|
157 | 157 | |
|
158 | 158 | var repoTypeFilter = function(data) { |
|
159 | 159 | var results = []; |
|
160 | 160 | |
|
161 | 161 | if (!data.results[0]) { |
|
162 | 162 | return data |
|
163 | 163 | } |
|
164 | 164 | |
|
165 | 165 | $.each(data.results[0].children, function() { |
|
166 | 166 | // filter out the SAME repo, it cannot be used as fork of itself |
|
167 | 167 | if (this.obj.repo_id != currentRepoId) { |
|
168 | 168 | this.id = this.obj.repo_id; |
|
169 | 169 | results.push(this) |
|
170 | 170 | } |
|
171 | 171 | }); |
|
172 | 172 | data.results[0].children = results; |
|
173 | 173 | return data; |
|
174 | 174 | }; |
|
175 | 175 | |
|
176 | 176 | $("#id_fork_of").select2({ |
|
177 | 177 | cachedDataSource: {}, |
|
178 | 178 | minimumInputLength: 2, |
|
179 |
placeholder: "${_('Change repository') if c.r |
|
|
179 | placeholder: "${_('Change repository') if c.rhodecode_db_repo.fork else _('Pick repository')}", | |
|
180 | 180 | dropdownAutoWidth: true, |
|
181 | 181 | containerCssClass: "drop-menu", |
|
182 | 182 | dropdownCssClass: "drop-menu-dropdown", |
|
183 | 183 | formatResult: formatResult, |
|
184 | 184 | query: $.debounce(250, function(query){ |
|
185 | 185 | self = this; |
|
186 | 186 | var cacheKey = query.term; |
|
187 | 187 | var cachedData = self.cachedDataSource[cacheKey]; |
|
188 | 188 | |
|
189 | 189 | if (cachedData) { |
|
190 | 190 | query.callback({results: cachedData.results}); |
|
191 | 191 | } else { |
|
192 | 192 | $.ajax({ |
|
193 | 193 | url: pyroutes.url('repo_list_data'), |
|
194 |
data: {'query': query.term, repo_type: '${c.r |
|
|
194 | data: {'query': query.term, repo_type: '${c.rhodecode_db_repo.repo_type}'}, | |
|
195 | 195 | dataType: 'json', |
|
196 | 196 | type: 'GET', |
|
197 | 197 | success: function(data) { |
|
198 | 198 | data = repoTypeFilter(data); |
|
199 | 199 | self.cachedDataSource[cacheKey] = data; |
|
200 | 200 | query.callback({results: data.results}); |
|
201 | 201 | }, |
|
202 | 202 | error: function(data, textStatus, errorThrown) { |
|
203 | 203 | alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText)); |
|
204 | 204 | } |
|
205 | 205 | }) |
|
206 | 206 | } |
|
207 | 207 | }) |
|
208 | 208 | }); |
|
209 | 209 | </script> |
|
210 | 210 |
@@ -1,53 +1,53 b'' | |||
|
1 | 1 | <div class="panel panel-default"> |
|
2 | 2 | <div class="panel-heading"> |
|
3 | 3 | <h3 class="panel-title">${_('Invalidate Cache for Repository')}</h3> |
|
4 | 4 | </div> |
|
5 | 5 | <div class="panel-body"> |
|
6 | 6 | |
|
7 | 7 | <h4>${_('Manually invalidate the repository cache. On the next access a repository cache will be recreated.')}</h4> |
|
8 | 8 | |
|
9 | 9 | <p> |
|
10 | 10 | ${_('Cache purge can be automated by such api call. Can be called periodically in crontab etc.')} |
|
11 | 11 | <br/> |
|
12 | 12 | <code> |
|
13 |
${h.api_call_example(method='invalidate_cache', args={"repoid": c.r |
|
|
13 | ${h.api_call_example(method='invalidate_cache', args={"repoid": c.rhodecode_db_repo.repo_name})} | |
|
14 | 14 | </code> |
|
15 | 15 | </p> |
|
16 | 16 | |
|
17 | 17 | ${h.secure_form(h.route_path('edit_repo_caches', repo_name=c.repo_name), method='POST', request=request)} |
|
18 | 18 | <div class="form"> |
|
19 | 19 | <div class="fields"> |
|
20 |
${h.submit('reset_cache_%s' % c.r |
|
|
20 | ${h.submit('reset_cache_%s' % c.rhodecode_db_repo.repo_name,_('Invalidate repository cache'),class_="btn btn-small",onclick="return confirm('"+_('Confirm to invalidate repository cache')+"');")} | |
|
21 | 21 | </div> |
|
22 | 22 | </div> |
|
23 | 23 | ${h.end_form()} |
|
24 | 24 | |
|
25 | 25 | </div> |
|
26 | 26 | </div> |
|
27 | 27 | |
|
28 | 28 | |
|
29 | 29 | <div class="panel panel-default"> |
|
30 | 30 | <div class="panel-heading"> |
|
31 | 31 | <h3 class="panel-title"> |
|
32 |
${(_ungettext('List of repository caches (%(count)s entry)', 'List of repository caches (%(count)s entries)' ,len(c.r |
|
|
32 | ${(_ungettext('List of repository caches (%(count)s entry)', 'List of repository caches (%(count)s entries)' ,len(c.rhodecode_db_repo.cache_keys)) % {'count': len(c.rhodecode_db_repo.cache_keys)})} | |
|
33 | 33 | </h3> |
|
34 | 34 | </div> |
|
35 | 35 | <div class="panel-body"> |
|
36 | 36 | <div class="field" > |
|
37 | 37 | <table class="rctable edit_cache"> |
|
38 | 38 | <tr> |
|
39 | 39 | <th>${_('Prefix')}</th> |
|
40 | 40 | <th>${_('Key')}</th> |
|
41 | 41 | <th>${_('Active')}</th> |
|
42 | 42 | </tr> |
|
43 |
%for cache in c.r |
|
|
43 | %for cache in c.rhodecode_db_repo.cache_keys: | |
|
44 | 44 | <tr> |
|
45 | 45 | <td class="td-prefix">${cache.get_prefix() or '-'}</td> |
|
46 | 46 | <td class="td-cachekey">${cache.cache_key}</td> |
|
47 | 47 | <td class="td-active">${h.bool2icon(cache.cache_active)}</td> |
|
48 | 48 | </tr> |
|
49 | 49 | %endfor |
|
50 | 50 | </table> |
|
51 | 51 | </div> |
|
52 | 52 | </div> |
|
53 | 53 | </div> |
@@ -1,79 +1,79 b'' | |||
|
1 | 1 | <div class="panel panel-default"> |
|
2 | 2 | <div class="panel-heading"> |
|
3 | 3 | <h3 class="panel-title">${_('Custom extra fields for this repository')}</h3> |
|
4 | 4 | </div> |
|
5 | 5 | <div class="panel-body"> |
|
6 | 6 | %if c.visual.repository_fields: |
|
7 | 7 | %if c.repo_fields: |
|
8 | 8 | <div class="emails_wrap"> |
|
9 | 9 | <table class="rctable edit_fields"> |
|
10 | 10 | <th>${_('Label')}</th> |
|
11 | 11 | <th>${_('Key')}</th> |
|
12 | 12 | <th>${_('Type')}</th> |
|
13 | 13 | <th>${_('Action')}</th> |
|
14 | 14 | |
|
15 | 15 | %for field in c.repo_fields: |
|
16 | 16 | <tr> |
|
17 | 17 | <td class="td-tags">${field.field_label}</td> |
|
18 | 18 | <td class="td-hash">${field.field_key}</td> |
|
19 | 19 | <td class="td-type">${field.field_type}</td> |
|
20 | 20 | <td class="td-action"> |
|
21 |
${h.secure_form(h.route_path('edit_repo_fields_delete', repo_name=c.r |
|
|
21 | ${h.secure_form(h.route_path('edit_repo_fields_delete', repo_name=c.rhodecode_db_repo.repo_name, field_id=field.repo_field_id), method='POST', request=request)} | |
|
22 | 22 | ${h.hidden('del_repo_field',field.repo_field_id)} |
|
23 | 23 | <button class="btn btn-link btn-danger" type="submit" |
|
24 | 24 | onclick="return confirm('${_('Confirm to delete this field: %s') % field.field_key}');"> |
|
25 | 25 | ${_('Delete')} |
|
26 | 26 | </button> |
|
27 | 27 | ${h.end_form()} |
|
28 | 28 | </td> |
|
29 | 29 | </tr> |
|
30 | 30 | %endfor |
|
31 | 31 | </table> |
|
32 | 32 | </div> |
|
33 | 33 | %endif |
|
34 | 34 | ${h.secure_form(h.route_path('edit_repo_fields_create', repo_name=c.repo_name), method='POST', request=request)} |
|
35 | 35 | <div class="form"> |
|
36 | 36 | <!-- fields --> |
|
37 | 37 | <div class="fields"> |
|
38 | 38 | <div class="field"> |
|
39 | 39 | <div class="label"> |
|
40 | 40 | <label for="new_field_key">${_('New Field Key')}:</label> |
|
41 | 41 | </div> |
|
42 | 42 | <div class="input"> |
|
43 | 43 | ${h.text('new_field_key', class_='medium')} |
|
44 | 44 | </div> |
|
45 | 45 | </div> |
|
46 | 46 | <div class="field"> |
|
47 | 47 | <div class="label"> |
|
48 | 48 | <label for="new_field_label">${_('New Field Label')}:</label> |
|
49 | 49 | </div> |
|
50 | 50 | <div class="input"> |
|
51 | 51 | ${h.text('new_field_label', class_='medium', placeholder=_('Enter short label'))} |
|
52 | 52 | </div> |
|
53 | 53 | </div> |
|
54 | 54 | |
|
55 | 55 | <div class="field"> |
|
56 | 56 | <div class="label"> |
|
57 | 57 | <label for="new_field_desc">${_('New Field Description')}:</label> |
|
58 | 58 | </div> |
|
59 | 59 | <div class="input"> |
|
60 | 60 | ${h.text('new_field_desc', class_='medium', placeholder=_('Enter a full description for the field'))} |
|
61 | 61 | </div> |
|
62 | 62 | </div> |
|
63 | 63 | |
|
64 | 64 | <div class="buttons"> |
|
65 | 65 | ${h.submit('save',_('Add'),class_="btn")} |
|
66 | 66 | ${h.reset('reset',_('Reset'),class_="btn")} |
|
67 | 67 | </div> |
|
68 | 68 | </div> |
|
69 | 69 | </div> |
|
70 | 70 | ${h.end_form()} |
|
71 | 71 | %else: |
|
72 | 72 | <h2> |
|
73 | 73 | ${_('Extra fields are disabled. You can enable them from the Admin/Settings/Visual page.')} |
|
74 | 74 | </h2> |
|
75 | 75 | %endif |
|
76 | 76 | </div> |
|
77 | 77 | </div> |
|
78 | 78 | |
|
79 | 79 |
@@ -1,109 +1,109 b'' | |||
|
1 | 1 | <%namespace name="its" file="/base/issue_tracker_settings.mako"/> |
|
2 | 2 | |
|
3 | 3 | <div id="repo_issue_tracker" class="${'inherited' if c.settings_model.inherit_global_settings else ''}"> |
|
4 | 4 | ${h.secure_form(h.route_path('edit_repo_issuetracker_update', repo_name=c.repo_name), id="inherit-form", method='POST', request=request)} |
|
5 | 5 | <div class="panel panel-default panel-body"> |
|
6 | 6 | <div class="fields"> |
|
7 | 7 | <div class="field"> |
|
8 | 8 | <div class="label label-checkbox"> |
|
9 | 9 | <label for="inherit_default_permissions">${_('Inherit from global settings')}:</label> |
|
10 | 10 | </div> |
|
11 | 11 | <div class="checkboxes"> |
|
12 | 12 | ${h.checkbox('inherit_global_issuetracker', value='inherited', checked=c.settings_model.inherit_global_settings)} |
|
13 | 13 | <span class="help-block"> |
|
14 | 14 | ${h.literal(_('Select to inherit global patterns for issue tracker.'))} |
|
15 | 15 | </span> |
|
16 | 16 | </div> |
|
17 | 17 | </div> |
|
18 | 18 | </div> |
|
19 | 19 | </div> |
|
20 | 20 | |
|
21 | 21 | <div id="inherit_overlay"> |
|
22 | 22 | <div class="panel panel-default"> |
|
23 | 23 | <div class="panel-heading"> |
|
24 | 24 | <h3 class="panel-title">${_('Inherited Issue Tracker Patterns')}</h3> |
|
25 | 25 | </div> |
|
26 | 26 | <div class="panel-body"> |
|
27 | 27 | <table class="rctable issuetracker readonly"> |
|
28 | 28 | <tr> |
|
29 | 29 | <th>${_('Description')}</th> |
|
30 | 30 | <th>${_('Pattern')}</th> |
|
31 | 31 | <th>${_('Url')}</th> |
|
32 | 32 | <th>${_('Prefix')}</th> |
|
33 | 33 | <th ></th> |
|
34 | 34 | </tr> |
|
35 | 35 | %for uid, entry in c.global_patterns.items(): |
|
36 | 36 | <tr id="${uid}"> |
|
37 | 37 | <td class="td-description issuetracker_desc"> |
|
38 | 38 | <span class="entry"> |
|
39 | 39 | ${entry.desc} |
|
40 | 40 | </span> |
|
41 | 41 | </td> |
|
42 | 42 | <td class="td-regex issuetracker_pat"> |
|
43 | 43 | <span class="entry"> |
|
44 | 44 | ${entry.pat} |
|
45 | 45 | </span> |
|
46 | 46 | </td> |
|
47 | 47 | <td class="td-url issuetracker_url"> |
|
48 | 48 | <span class="entry"> |
|
49 | 49 | ${entry.url} |
|
50 | 50 | </span> |
|
51 | 51 | </td> |
|
52 | 52 | <td class="td-prefix issuetracker_pref"> |
|
53 | 53 | <span class="entry"> |
|
54 | 54 | ${entry.pref} |
|
55 | 55 | </span> |
|
56 | 56 | </td> |
|
57 | 57 | <td class="td-action"> |
|
58 | 58 | </td> |
|
59 | 59 | </tr> |
|
60 | 60 | %endfor |
|
61 | 61 | |
|
62 | 62 | </table> |
|
63 | 63 | </div> |
|
64 | 64 | </div> |
|
65 | 65 | </div> |
|
66 | 66 | |
|
67 | 67 | <div id="custom_overlay"> |
|
68 | 68 | <div class="panel panel-default"> |
|
69 | 69 | <div class="panel-heading"> |
|
70 | 70 | <h3 class="panel-title">${_('Issue Tracker / Wiki Patterns')}</h3> |
|
71 | 71 | </div> |
|
72 | 72 | <div class="panel-body"> |
|
73 | 73 | ${its.issue_tracker_settings_table( |
|
74 | 74 | patterns=c.repo_patterns.items(), |
|
75 |
form_url=h.route_path('edit_repo_issuetracker', repo_name=c.r |
|
|
76 |
delete_url=h.route_path('edit_repo_issuetracker_delete', repo_name=c.r |
|
|
75 | form_url=h.route_path('edit_repo_issuetracker', repo_name=c.rhodecode_db_repo.repo_name), | |
|
76 | delete_url=h.route_path('edit_repo_issuetracker_delete', repo_name=c.rhodecode_db_repo.repo_name) | |
|
77 | 77 | )} |
|
78 | 78 | <div class="buttons"> |
|
79 | 79 | <button type="submit" class="btn btn-primary save-inheritance" id="save">${_('Save')}</button> |
|
80 | 80 | <button type="reset" class="btn reset-inheritance">${_('Reset')}</button> |
|
81 | 81 | </div> |
|
82 | 82 | </div> |
|
83 | 83 | </div> |
|
84 | 84 | </div> |
|
85 | 85 | |
|
86 | 86 | |
|
87 | 87 | ${h.end_form()} |
|
88 | 88 | |
|
89 | 89 | <div class="panel panel-default"> |
|
90 | 90 | <div class="panel-heading"> |
|
91 | 91 | <h3 class="panel-title">${_('Test Patterns')}</h3> |
|
92 | 92 | </div> |
|
93 | 93 | <div class="panel-body"> |
|
94 | 94 | ${its.issue_tracker_new_row()} |
|
95 |
${its.issue_tracker_settings_test(test_url=h.route_path('edit_repo_issuetracker_test', repo_name=c.r |
|
|
95 | ${its.issue_tracker_settings_test(test_url=h.route_path('edit_repo_issuetracker_test', repo_name=c.rhodecode_db_repo.repo_name))} | |
|
96 | 96 | </div> |
|
97 | 97 | </div> |
|
98 | 98 | |
|
99 | 99 | </div> |
|
100 | 100 | |
|
101 | 101 | <script> |
|
102 | 102 | $('#inherit_global_issuetracker').on('change', function(e){ |
|
103 | 103 | $('#repo_issue_tracker').toggleClass('inherited',this.checked); |
|
104 | 104 | }); |
|
105 | 105 | |
|
106 | 106 | $('.reset-inheritance').on('click', function(e){ |
|
107 | 107 | $('#inherit_global_issuetracker').prop('checked', false).change(); |
|
108 | 108 | }); |
|
109 | 109 | </script> |
@@ -1,66 +1,66 b'' | |||
|
1 | 1 | <div class="panel panel-default"> |
|
2 | 2 | <div class="panel-heading"> |
|
3 | 3 | <h3 class="panel-title">${_('Maintenance')}</h3> |
|
4 | 4 | </div> |
|
5 | 5 | <div class="panel-body"> |
|
6 | 6 | |
|
7 | 7 | % if c.executable_tasks: |
|
8 | 8 | <h4>${_('Perform maintenance tasks for this repo')}</h4> |
|
9 | 9 | |
|
10 | 10 | <span>${_('Following tasks will be performed')}:</span> |
|
11 | 11 | <ol> |
|
12 | 12 | % for task in c.executable_tasks: |
|
13 | 13 | <li>${task}</li> |
|
14 | 14 | % endfor |
|
15 | 15 | </ol> |
|
16 | 16 | <p> |
|
17 | 17 | ${_('Maintenance can be automated by such api call. Can be called periodically in crontab etc.')} |
|
18 | 18 | <br/> |
|
19 | 19 | <code> |
|
20 |
${h.api_call_example(method='maintenance', args={"repoid": c.r |
|
|
20 | ${h.api_call_example(method='maintenance', args={"repoid": c.rhodecode_db_repo.repo_name})} | |
|
21 | 21 | </code> |
|
22 | 22 | </p> |
|
23 | 23 | |
|
24 | 24 | % else: |
|
25 | 25 | <h4>${_('No maintenance tasks for this repo available')}</h4> |
|
26 | 26 | % endif |
|
27 | 27 | |
|
28 | 28 | <div id="results" style="display:none; padding: 10px 0px;"></div> |
|
29 | 29 | |
|
30 | 30 | % if c.executable_tasks: |
|
31 | 31 | <div class="form"> |
|
32 | 32 | <div class="fields"> |
|
33 | 33 | <button class="btn btn-small btn-primary" onclick="executeTask();return false"> |
|
34 | 34 | ${_('Run Maintenance')} |
|
35 | 35 | </button> |
|
36 | 36 | </div> |
|
37 | 37 | </div> |
|
38 | 38 | % endif |
|
39 | 39 | |
|
40 | 40 | </div> |
|
41 | 41 | </div> |
|
42 | 42 | |
|
43 | 43 | |
|
44 | 44 | <script> |
|
45 | 45 | |
|
46 | 46 | executeTask = function() { |
|
47 | 47 | var btn = $(this); |
|
48 | 48 | $('#results').show(); |
|
49 | 49 | $('#results').html('<h4>${_('Performing Maintenance')}...</h4>'); |
|
50 | 50 | |
|
51 | 51 | btn.attr('disabled', 'disabled'); |
|
52 | 52 | btn.addClass('disabled'); |
|
53 | 53 | |
|
54 |
var url = "${h.route_path('edit_repo_maintenance_execute', repo_name=c.r |
|
|
54 | var url = "${h.route_path('edit_repo_maintenance_execute', repo_name=c.rhodecode_db_repo.repo_name)}"; | |
|
55 | 55 | var success = function (data) { |
|
56 | 56 | var displayHtml = $('<pre></pre>'); |
|
57 | 57 | |
|
58 | 58 | $(displayHtml).append(data); |
|
59 | 59 | $('#results').html(displayHtml); |
|
60 | 60 | btn.removeAttr('disabled'); |
|
61 | 61 | btn.removeClass('disabled'); |
|
62 | 62 | }; |
|
63 | 63 | ajaxGET(url, success, null); |
|
64 | 64 | |
|
65 | 65 | } |
|
66 | 66 | </script> |
@@ -1,123 +1,123 b'' | |||
|
1 | 1 | <%namespace name="base" file="/base/base.mako"/> |
|
2 | 2 | |
|
3 | 3 | <div class="panel panel-default"> |
|
4 | 4 | <div class="panel-heading"> |
|
5 | 5 | <h3 class="panel-title">${_('Repository Permissions')}</h3> |
|
6 | 6 | </div> |
|
7 | 7 | <div class="panel-body"> |
|
8 | 8 | ${h.secure_form(h.route_path('edit_repo_perms', repo_name=c.repo_name), method='POST', request=request)} |
|
9 | 9 | <table id="permissions_manage" class="rctable permissions"> |
|
10 | 10 | <tr> |
|
11 | 11 | <th class="td-radio">${_('None')}</th> |
|
12 | 12 | <th class="td-radio">${_('Read')}</th> |
|
13 | 13 | <th class="td-radio">${_('Write')}</th> |
|
14 | 14 | <th class="td-radio">${_('Admin')}</th> |
|
15 | 15 | <th class="td-owner">${_('User/User Group')}</th> |
|
16 | 16 | <th></th> |
|
17 | 17 | </tr> |
|
18 | 18 | ## USERS |
|
19 |
%for _user in c.r |
|
|
19 | %for _user in c.rhodecode_db_repo.permissions(): | |
|
20 | 20 | %if getattr(_user, 'admin_row', None) or getattr(_user, 'owner_row', None): |
|
21 | 21 | <tr class="perm_admin_row"> |
|
22 | 22 | <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.none', disabled="disabled")}</td> |
|
23 | 23 | <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.read', disabled="disabled")}</td> |
|
24 | 24 | <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.write', disabled="disabled")}</td> |
|
25 | 25 | <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.admin', 'repository.admin', disabled="disabled")}</td> |
|
26 | 26 | <td class="td-user"> |
|
27 | 27 | ${base.gravatar(_user.email, 16)} |
|
28 | 28 | ${h.link_to_user(_user.username)} |
|
29 | 29 | %if getattr(_user, 'admin_row', None): |
|
30 | 30 | (${_('super admin')}) |
|
31 | 31 | %endif |
|
32 | 32 | %if getattr(_user, 'owner_row', None): |
|
33 | 33 | (${_('owner')}) |
|
34 | 34 | %endif |
|
35 | 35 | </td> |
|
36 | 36 | <td></td> |
|
37 | 37 | </tr> |
|
38 |
%elif _user.username == h.DEFAULT_USER and c.r |
|
|
38 | %elif _user.username == h.DEFAULT_USER and c.rhodecode_db_repo.private: | |
|
39 | 39 | <tr> |
|
40 | 40 | <td colspan="4"> |
|
41 | 41 | <span class="private_repo_msg"> |
|
42 | 42 | <strong title="${h.tooltip(_user.permission)}">${_('private repository')}</strong> |
|
43 | 43 | </span> |
|
44 | 44 | </td> |
|
45 | 45 | <td class="private_repo_msg"> |
|
46 | 46 | ${base.gravatar(h.DEFAULT_USER_EMAIL, 16)} |
|
47 | 47 | ${h.DEFAULT_USER} - ${_('only users/user groups explicitly added here will have access')}</td> |
|
48 | 48 | <td></td> |
|
49 | 49 | </tr> |
|
50 | 50 | %else: |
|
51 | 51 | <tr> |
|
52 | 52 | <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.none', checked=_user.permission=='repository.none')}</td> |
|
53 | 53 | <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.read', checked=_user.permission=='repository.read')}</td> |
|
54 | 54 | <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.write', checked=_user.permission=='repository.write')}</td> |
|
55 | 55 | <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.admin', checked=_user.permission=='repository.admin')}</td> |
|
56 | 56 | <td class="td-user"> |
|
57 | 57 | ${base.gravatar(_user.email, 16)} |
|
58 | 58 | <span class="user"> |
|
59 | 59 | % if _user.username == h.DEFAULT_USER: |
|
60 | 60 | ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span> |
|
61 | 61 | % else: |
|
62 | 62 | ${h.link_to_user(_user.username)} |
|
63 | 63 | % endif |
|
64 | 64 | </span> |
|
65 | 65 | </td> |
|
66 | 66 | <td class="td-action"> |
|
67 | 67 | %if _user.username != h.DEFAULT_USER: |
|
68 | 68 | <span class="btn btn-link btn-danger revoke_perm" |
|
69 | 69 | member="${_user.user_id}" member_type="user"> |
|
70 | 70 | <i class="icon-remove"></i> ${_('Revoke')} |
|
71 | 71 | </span> |
|
72 | 72 | %endif |
|
73 | 73 | </td> |
|
74 | 74 | </tr> |
|
75 | 75 | %endif |
|
76 | 76 | %endfor |
|
77 | 77 | |
|
78 | 78 | ## USER GROUPS |
|
79 |
%for _user_group in c.r |
|
|
79 | %for _user_group in c.rhodecode_db_repo.permission_user_groups(): | |
|
80 | 80 | <tr> |
|
81 | 81 | <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.none', checked=_user_group.permission=='repository.none')}</td> |
|
82 | 82 | <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.read', checked=_user_group.permission=='repository.read')}</td> |
|
83 | 83 | <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.write', checked=_user_group.permission=='repository.write')}</td> |
|
84 | 84 | <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.admin', checked=_user_group.permission=='repository.admin')}</td> |
|
85 | 85 | <td class="td-componentname"> |
|
86 | 86 | <i class="icon-group" ></i> |
|
87 | 87 | %if h.HasPermissionAny('hg.admin')(): |
|
88 | 88 | <a href="${h.route_path('edit_user_group',user_group_id=_user_group.users_group_id)}"> |
|
89 | 89 | ${_user_group.users_group_name} |
|
90 | 90 | </a> |
|
91 | 91 | %else: |
|
92 | 92 | ${_user_group.users_group_name} |
|
93 | 93 | %endif |
|
94 | 94 | </td> |
|
95 | 95 | <td class="td-action"> |
|
96 | 96 | <span class="btn btn-link btn-danger revoke_perm" |
|
97 | 97 | member="${_user_group.users_group_id}" member_type="user_group"> |
|
98 | 98 | <i class="icon-remove"></i> ${_('Revoke')} |
|
99 | 99 | </span> |
|
100 | 100 | </td> |
|
101 | 101 | </tr> |
|
102 | 102 | %endfor |
|
103 | 103 | <tr class="new_members" id="add_perm_input"></tr> |
|
104 | 104 | </table> |
|
105 | 105 | <div id="add_perm" class="link"> |
|
106 | 106 | ${_('Add new')} |
|
107 | 107 | </div> |
|
108 | 108 | <div class="buttons"> |
|
109 | 109 | ${h.submit('save',_('Save'),class_="btn btn-primary")} |
|
110 | 110 | ${h.reset('reset',_('Reset'),class_="btn btn-danger")} |
|
111 | 111 | </div> |
|
112 | 112 | ${h.end_form()} |
|
113 | 113 | </div> |
|
114 | 114 | </div> |
|
115 | 115 | |
|
116 | 116 | <script type="text/javascript"> |
|
117 | 117 | $('#add_perm').on('click', function(e){ |
|
118 | 118 | addNewPermInput($(this), 'repository'); |
|
119 | 119 | }); |
|
120 | 120 | $('.revoke_perm').on('click', function(e){ |
|
121 | 121 | markRevokePermInput($(this), 'repository'); |
|
122 | 122 | }); |
|
123 | 123 | </script> |
@@ -1,40 +1,40 b'' | |||
|
1 | 1 | <div class="panel panel-default"> |
|
2 | 2 | <div class="panel-heading"> |
|
3 | 3 | <h3 class="panel-title">${_('Remote url')}</h3> |
|
4 | 4 | </div> |
|
5 | 5 | <div class="panel-body"> |
|
6 | 6 | |
|
7 | 7 | <h4>${_('Manually pull changes from external repository.')}</h4> |
|
8 | 8 | |
|
9 |
%if c.r |
|
|
9 | %if c.rhodecode_db_repo.clone_uri: | |
|
10 | 10 | |
|
11 | 11 | ${_('Remote mirror url')}: |
|
12 |
<a href="${c.r |
|
|
12 | <a href="${c.rhodecode_db_repo.clone_uri}">${c.rhodecode_db_repo.clone_uri_hidden}</a> | |
|
13 | 13 | |
|
14 | 14 | <p> |
|
15 | 15 | ${_('Pull can be automated by such api call. Can be called periodically in crontab etc.')} |
|
16 | 16 | <br/> |
|
17 | 17 | <code> |
|
18 |
${h.api_call_example(method='pull', args={"repoid": c.r |
|
|
18 | ${h.api_call_example(method='pull', args={"repoid": c.rhodecode_db_repo.repo_name})} | |
|
19 | 19 | </code> |
|
20 | 20 | </p> |
|
21 | 21 | |
|
22 | 22 | ${h.secure_form(h.route_path('edit_repo_remote_pull', repo_name=c.repo_name), method='POST', request=request)} |
|
23 | 23 | <div class="form"> |
|
24 | 24 | <div class="fields"> |
|
25 |
${h.submit('remote_pull_%s' % c.r |
|
|
25 | ${h.submit('remote_pull_%s' % c.rhodecode_db_repo.repo_name,_('Pull changes from remote location'),class_="btn btn-small",onclick="return confirm('"+_('Confirm to pull changes from remote side')+"');")} | |
|
26 | 26 | </div> |
|
27 | 27 | </div> |
|
28 | 28 | ${h.end_form()} |
|
29 | 29 | %else: |
|
30 | 30 | |
|
31 | 31 | ${_('This repository does not have any remote mirror url set.')} |
|
32 |
<a href="${h.route_path('edit_repo', repo_name=c.r |
|
|
32 | <a href="${h.route_path('edit_repo', repo_name=c.rhodecode_db_repo.repo_name)}">${_('Set remote url.')}</a> | |
|
33 | 33 | <br/> |
|
34 | 34 | <br/> |
|
35 | 35 | <button class="btn disabled" type="submit" disabled="disabled"> |
|
36 | 36 | ${_('Pull changes from remote location')} |
|
37 | 37 | </button> |
|
38 | 38 | %endif |
|
39 | 39 | </div> |
|
40 | 40 | </div> |
@@ -1,22 +1,22 b'' | |||
|
1 | 1 | <div class="panel panel-default"> |
|
2 | 2 | <div class="panel-heading"> |
|
3 | 3 | <h3 class="panel-title">${_('Repository statistics')}</h3> |
|
4 | 4 | </div> |
|
5 | 5 | <div class="panel-body"> |
|
6 |
${h.secure_form(h.route_path('edit_repo_statistics_reset', repo_name=c.r |
|
|
6 | ${h.secure_form(h.route_path('edit_repo_statistics_reset', repo_name=c.rhodecode_db_repo.repo_name), method='POST', request=request)} | |
|
7 | 7 | <div class="form"> |
|
8 | 8 | <div class="fields"> |
|
9 | 9 | <div class="field" > |
|
10 | 10 | <dl class="dl-horizontal settings"> |
|
11 | 11 | <dt>${_('Processed commits')}:</dt><dd>${c.stats_revision}/${c.repo_last_rev}</dd> |
|
12 | 12 | <dt>${_('Processed progress')}:</dt><dd>${c.stats_percentage}%</dd> |
|
13 | 13 | </dl> |
|
14 | 14 | </div> |
|
15 |
${h.submit('reset_stats_%s' % c.r |
|
|
15 | ${h.submit('reset_stats_%s' % c.rhodecode_db_repo.repo_name,_('Reset statistics'),class_="btn btn-small",onclick="return confirm('"+_('Confirm to remove current statistics')+"');")} | |
|
16 | 16 | </div> |
|
17 | 17 | </div> |
|
18 | 18 | ${h.end_form()} |
|
19 | 19 | </div> |
|
20 | 20 | </div> |
|
21 | 21 | |
|
22 | 22 |
@@ -1,197 +1,197 b'' | |||
|
1 | 1 | <div class="panel panel-default"> |
|
2 | 2 | <div class="panel-heading"> |
|
3 | 3 | <h3 class="panel-title">${_('Strip commits from repository')}</h3> |
|
4 | 4 | </div> |
|
5 | 5 | <div class="panel-body"> |
|
6 |
%if c.r |
|
|
6 | %if c.rhodecode_db_repo.repo_type != 'svn': | |
|
7 | 7 | <h4>${_('Please provide up to %d commits commits to strip') % c.strip_limit}</h4> |
|
8 | 8 | <p> |
|
9 | 9 | ${_('In the first step commits will be verified for existance in the repository')}. </br> |
|
10 | 10 | ${_('In the second step, correct commits will be available for stripping')}. |
|
11 | 11 | </p> |
|
12 |
${h.secure_form(h.route_path('strip_check', repo_name=c.r |
|
|
12 | ${h.secure_form(h.route_path('strip_check', repo_name=c.rhodecode_db_repo.repo_name), method='POST', request=request)} | |
|
13 | 13 | <div id="change_body" class="field"> |
|
14 | 14 | <div id="box-1" class="inputx locked_input"> |
|
15 | 15 | <input class="text" id="changeset_id-1" name="changeset_id-1" size="59" |
|
16 | 16 | placeholder="${_('Enter full 40 character commit sha')}" type="text" value=""> |
|
17 | 17 | <div id="plus_icon-1" class="btn btn-default plus_input_button" onclick="addNew(1);return false"> |
|
18 | 18 | <i class="icon-plus">${_('Add another commit')}</i> |
|
19 | 19 | </div> |
|
20 | 20 | </div> |
|
21 | 21 | </div> |
|
22 | 22 | |
|
23 | 23 | <div id="results" style="display:none; padding: 10px 0px;"></div> |
|
24 | 24 | |
|
25 | 25 | <div class="buttons"> |
|
26 | 26 | <button id="strip_action" class="btn btn-small btn-primary" onclick="checkCommits();return false"> |
|
27 | 27 | ${_('Check commits')} |
|
28 | 28 | </button> |
|
29 | 29 | </div> |
|
30 | 30 | |
|
31 | 31 | ${h.end_form()} |
|
32 | 32 | %else: |
|
33 | 33 | <h4>${_('Sorry this functionality is not available for SVN repository')}</h4> |
|
34 | 34 | %endif |
|
35 | 35 | </div> |
|
36 | 36 | </div> |
|
37 | 37 | |
|
38 | 38 | |
|
39 | 39 | <script> |
|
40 | 40 | var plus_leaf = 1; |
|
41 | 41 | |
|
42 | 42 | addNew = function(number){ |
|
43 | 43 | if (number >= ${c.strip_limit}){ |
|
44 | 44 | return; |
|
45 | 45 | } |
|
46 | 46 | var minus = '<i class="icon-minus">${_('Remove')}</i>'; |
|
47 | 47 | $('#plus_icon-'+number).detach(); |
|
48 | 48 | number++; |
|
49 | 49 | |
|
50 | 50 | var input = '<div id="box-'+number+'" class="inputx locked_input">'+ |
|
51 | 51 | '<input class="text" id="changeset_id-'+number+'" name="changeset_id-'+number+'" size="59" type="text" value=""' + |
|
52 | 52 | 'placeholder="${_('Enter full 40 character commit sha')}">'+ |
|
53 | 53 | '<div id="plus_icon-'+number+'" class="btn btn-default plus_input_button" onclick="addNew('+number+');return false">'+ |
|
54 | 54 | '<i class="icon-plus">${_('Add another commit')}</i>'+ |
|
55 | 55 | '</div>'+ |
|
56 | 56 | '<div id="minus_icon-'+number+'" class="btn btn-default minus_input_button" onclick="delOld('+(number)+');return false">'+ |
|
57 | 57 | minus + |
|
58 | 58 | '</div>' + |
|
59 | 59 | '</div>'; |
|
60 | 60 | $('#change_body').append(input); |
|
61 | 61 | plus_leaf++; |
|
62 | 62 | }; |
|
63 | 63 | |
|
64 | 64 | reIndex = function(number){ |
|
65 | 65 | for(var i=number;i<=plus_leaf;i++){ |
|
66 | 66 | var check = $('#box-'+i); |
|
67 | 67 | if (check.length == 0){ |
|
68 | 68 | var change = $('#box-'+(i+1)); |
|
69 | 69 | change.attr('id','box-'+i); |
|
70 | 70 | var plus = $('#plus_icon-'+(i+1)); |
|
71 | 71 | |
|
72 | 72 | if (plus.length != 0){ |
|
73 | 73 | plus.attr('id','plus_icon-'+i); |
|
74 | 74 | plus.attr('onclick','addNew('+i+');return false'); |
|
75 | 75 | plus_leaf--; |
|
76 | 76 | } |
|
77 | 77 | var minus = $('#minus_icon-'+(i+1)); |
|
78 | 78 | |
|
79 | 79 | minus.attr('id','minus_icon-'+i); |
|
80 | 80 | |
|
81 | 81 | minus.attr('onclick','delOld('+i+');re' + |
|
82 | 82 | 'turn false'); |
|
83 | 83 | var input = $('input#changeset_id-'+(i+1)); |
|
84 | 84 | input.attr('name','changeset_id-'+i); |
|
85 | 85 | input.attr('id','changeset_id-'+i); |
|
86 | 86 | } |
|
87 | 87 | } |
|
88 | 88 | }; |
|
89 | 89 | |
|
90 | 90 | delOld = function(number){ |
|
91 | 91 | $('#box-'+number).remove(); |
|
92 | 92 | number = number - 1; |
|
93 | 93 | var box = $('#box-'+number); |
|
94 | 94 | var plus = '<div id="plus_icon-'+number+'" class="btn btn-default plus_input_button" onclick="addNew('+number +');return false">'+ |
|
95 | 95 | '<i id="i_plus_icon-'+number+'" class="icon-plus">${_('Add another commit')}</i></div>'; |
|
96 | 96 | var minus = $('#minus_icon-'+number); |
|
97 | 97 | if(number +1 == plus_leaf){ |
|
98 | 98 | minus.detach(); |
|
99 | 99 | box.append(plus); |
|
100 | 100 | box.append(minus); |
|
101 | 101 | plus_leaf --; |
|
102 | 102 | } |
|
103 | 103 | reIndex(number+1); |
|
104 | 104 | |
|
105 | 105 | }; |
|
106 | 106 | |
|
107 | 107 | var resultData = { |
|
108 | 108 | 'csrf_token': CSRF_TOKEN |
|
109 | 109 | }; |
|
110 | 110 | |
|
111 | 111 | checkCommits = function() { |
|
112 | 112 | var postData = $('form').serialize(); |
|
113 | 113 | $('#results').show(); |
|
114 | 114 | $('#results').html('<h4>${_('Checking commits')}...</h4>'); |
|
115 |
var url = "${h.route_path('strip_check', repo_name=c.r |
|
|
115 | var url = "${h.route_path('strip_check', repo_name=c.rhodecode_db_repo.repo_name)}"; | |
|
116 | 116 | var btn = $('#strip_action'); |
|
117 | 117 | btn.attr('disabled', 'disabled'); |
|
118 | 118 | btn.addClass('disabled'); |
|
119 | 119 | |
|
120 | 120 | var success = function (data) { |
|
121 | 121 | resultData = { |
|
122 | 122 | 'csrf_token': CSRF_TOKEN |
|
123 | 123 | }; |
|
124 | 124 | var i = 0; |
|
125 | 125 | var result = '<ol>'; |
|
126 | 126 | $.each(data, function(index, value){ |
|
127 | 127 | i= index; |
|
128 | 128 | var box = $('#box-'+index); |
|
129 | 129 | if (value.rev){ |
|
130 | 130 | resultData[index] = JSON.stringify(value); |
|
131 | 131 | |
|
132 | 132 | var verifiedHtml = ( |
|
133 | 133 | '<li style="line-height:1.2em">' + |
|
134 | 134 | '<code>{0}</code>' + |
|
135 | 135 | '{1}' + |
|
136 | 136 | '<div style="white-space:pre">' + |
|
137 | 137 | 'author: {2}\n' + |
|
138 | 138 | 'description: {3}' + |
|
139 | 139 | '</div>' + |
|
140 | 140 | '</li>').format( |
|
141 | 141 | value.rev, |
|
142 | 142 | "${_(' commit verified positive')}", |
|
143 | 143 | value.author, value.comment |
|
144 | 144 | ); |
|
145 | 145 | result += verifiedHtml; |
|
146 | 146 | } |
|
147 | 147 | else { |
|
148 | 148 | var verifiedHtml = ( |
|
149 | 149 | '<li style="line-height:1.2em">' + |
|
150 | 150 | '<code><strike>{0}</strike></code>' + |
|
151 | 151 | '{1}' + |
|
152 | 152 | '</li>').format( |
|
153 | 153 | value.commit, |
|
154 | 154 | "${_(' commit verified negative')}" |
|
155 | 155 | ); |
|
156 | 156 | result += verifiedHtml; |
|
157 | 157 | } |
|
158 | 158 | box.remove(); |
|
159 | 159 | }); |
|
160 | 160 | result += '</ol>'; |
|
161 | 161 | var box = $('#box-'+(parseInt(i)+1)); |
|
162 | 162 | box.remove(); |
|
163 | 163 | $('#results').html(result); |
|
164 | 164 | }; |
|
165 | 165 | |
|
166 | 166 | btn.html('Strip'); |
|
167 | 167 | btn.removeAttr('disabled'); |
|
168 | 168 | btn.removeClass('disabled'); |
|
169 | 169 | btn.attr('onclick','strip();return false;'); |
|
170 | 170 | ajaxPOST(url, postData, success, null); |
|
171 | 171 | }; |
|
172 | 172 | |
|
173 | 173 | strip = function() { |
|
174 |
var url = "${h.route_path('strip_execute', repo_name=c.r |
|
|
174 | var url = "${h.route_path('strip_execute', repo_name=c.rhodecode_db_repo.repo_name)}"; | |
|
175 | 175 | var success = function(data) { |
|
176 | 176 | var result = '<h4>Strip executed</h4><ol>'; |
|
177 | 177 | $.each(data, function(index, value){ |
|
178 | 178 | if(data[index]) { |
|
179 | 179 | result += '<li><code>' +index+ '</code> ${_(' commit striped successfully')}' + '</li>'; |
|
180 | 180 | } |
|
181 | 181 | else { |
|
182 | 182 | result += '<li><code>' +index+ '</code> ${_(' commit strip failed')}' + '</li>'; |
|
183 | 183 | } |
|
184 | 184 | }); |
|
185 | 185 | if ($.isEmptyObject(data)) { |
|
186 | 186 | result += '<li>Nothing done...</li>' |
|
187 | 187 | } |
|
188 | 188 | result += '</ol>'; |
|
189 | 189 | $('#results').html(result); |
|
190 | 190 | |
|
191 | 191 | }; |
|
192 | 192 | ajaxPOST(url, resultData, success, null); |
|
193 | 193 | var btn = $('#strip_action'); |
|
194 | 194 | btn.remove(); |
|
195 | 195 | |
|
196 | 196 | }; |
|
197 | 197 | </script> |
@@ -1,73 +1,73 b'' | |||
|
1 | 1 | <%namespace name="vcss" file="/base/vcs_settings.mako"/> |
|
2 | 2 | |
|
3 | 3 | <div id="repo_vcs_settings" class="${'inherited' if c.inherit_global_settings else ''}"> |
|
4 |
${h.secure_form(h.route_path('edit_repo_vcs_update', repo_name=c.r |
|
|
4 | ${h.secure_form(h.route_path('edit_repo_vcs_update', repo_name=c.rhodecode_db_repo.repo_name), method='POST', request=request)} | |
|
5 | 5 | <div class="form panel panel-default"> |
|
6 | 6 | <div class="fields panel-body"> |
|
7 | 7 | <div class="field"> |
|
8 | 8 | <div class="label label-checkbox"> |
|
9 | 9 | <label for="inherit_global_settings">${_('Inherit from global settings')}:</label> |
|
10 | 10 | </div> |
|
11 | 11 | <div class="checkboxes"> |
|
12 | 12 | ${h.checkbox('inherit_global_settings',value=True)} |
|
13 | 13 | <span class="help-block">${h.literal(_('Select to inherit global vcs settings.'))}</span> |
|
14 | 14 | </div> |
|
15 | 15 | </div> |
|
16 | 16 | </div> |
|
17 | 17 | </div> |
|
18 | 18 | |
|
19 | 19 | <div id="inherit_overlay_vcs_default"> |
|
20 | 20 | <div> |
|
21 | 21 | ${vcss.vcs_settings_fields( |
|
22 | 22 | suffix='_inherited', |
|
23 | 23 | svn_tag_patterns=c.global_svn_tag_patterns, |
|
24 | 24 | svn_branch_patterns=c.global_svn_branch_patterns, |
|
25 |
repo_type=c.r |
|
|
25 | repo_type=c.rhodecode_db_repo.repo_type, | |
|
26 | 26 | disabled='disabled' |
|
27 | 27 | )} |
|
28 | 28 | </div> |
|
29 | 29 | </div> |
|
30 | 30 | |
|
31 | 31 | <div id="inherit_overlay_vcs_custom"> |
|
32 | 32 | <div> |
|
33 | 33 | ${vcss.vcs_settings_fields( |
|
34 | 34 | suffix='', |
|
35 | 35 | svn_tag_patterns=c.svn_tag_patterns, |
|
36 | 36 | svn_branch_patterns=c.svn_branch_patterns, |
|
37 |
repo_type=c.r |
|
|
37 | repo_type=c.rhodecode_db_repo.repo_type | |
|
38 | 38 | )} |
|
39 | 39 | </div> |
|
40 | 40 | </div> |
|
41 | 41 | |
|
42 | 42 | <div class="buttons"> |
|
43 | 43 | ${h.submit('save',_('Save settings'),class_="btn")} |
|
44 | 44 | ${h.reset('reset',_('Reset'),class_="btn")} |
|
45 | 45 | </div> |
|
46 | 46 | |
|
47 | 47 | ${h.end_form()} |
|
48 | 48 | </div> |
|
49 | 49 | |
|
50 | 50 | <script type="text/javascript"> |
|
51 | 51 | |
|
52 | 52 | function ajaxDeletePattern(pattern_id, field_id) { |
|
53 |
var sUrl = "${h.route_path('edit_repo_vcs_svn_pattern_delete', repo_name=c.r |
|
|
53 | var sUrl = "${h.route_path('edit_repo_vcs_svn_pattern_delete', repo_name=c.rhodecode_db_repo.repo_name)}"; | |
|
54 | 54 | var callback = function (o) { |
|
55 | 55 | var elem = $("#"+field_id); |
|
56 | 56 | elem.remove(); |
|
57 | 57 | }; |
|
58 | 58 | var postData = { |
|
59 | 59 | 'delete_svn_pattern': pattern_id, |
|
60 | 60 | 'csrf_token': CSRF_TOKEN |
|
61 | 61 | }; |
|
62 | 62 | var request = $.post(sUrl, postData) |
|
63 | 63 | .done(callback) |
|
64 | 64 | .fail(function (data, textStatus, errorThrown) { |
|
65 | 65 | alert("Error while deleting hooks.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(this)[0].url)); |
|
66 | 66 | }); |
|
67 | 67 | } |
|
68 | 68 | |
|
69 | 69 | $('#inherit_global_settings').on('change', function(e){ |
|
70 | 70 | $('#repo_vcs_settings').toggleClass('inherited', this.checked); |
|
71 | 71 | }); |
|
72 | 72 | |
|
73 | 73 | </script> |
@@ -1,129 +1,129 b'' | |||
|
1 | 1 | ## -*- coding: utf-8 -*- |
|
2 | 2 | <%inherit file="/base/base.mako"/> |
|
3 | 3 | |
|
4 | 4 | <%def name="title()"> |
|
5 | 5 | ${_('Fork repository %s') % c.repo_name} |
|
6 | 6 | %if c.rhodecode_name: |
|
7 | 7 | · ${h.branding(c.rhodecode_name)} |
|
8 | 8 | %endif |
|
9 | 9 | </%def> |
|
10 | 10 | |
|
11 | 11 | <%def name="breadcrumbs_links()"> |
|
12 | 12 | ${_('New Fork')} |
|
13 | 13 | </%def> |
|
14 | 14 | |
|
15 | 15 | <%def name="menu_bar_nav()"> |
|
16 | 16 | ${self.menu_items(active='repositories')} |
|
17 | 17 | </%def> |
|
18 | 18 | |
|
19 | 19 | <%def name="menu_bar_subnav()"> |
|
20 | 20 | ${self.repo_menu(active='options')} |
|
21 | 21 | </%def> |
|
22 | 22 | |
|
23 | 23 | <%def name="main()"> |
|
24 | 24 | <div class="box"> |
|
25 | 25 | <div class="title"> |
|
26 | 26 | ${self.repo_page_title(c.rhodecode_db_repo)} |
|
27 | 27 | ${self.breadcrumbs()} |
|
28 | 28 | </div> |
|
29 | 29 | |
|
30 |
${h.secure_form(h.route_path('repo_fork_create',repo_name=c.r |
|
|
30 | ${h.secure_form(h.route_path('repo_fork_create',repo_name=c.rhodecode_db_repo.repo_name), method='POST', request=request)} | |
|
31 | 31 | <div class="form"> |
|
32 | 32 | <!-- fields --> |
|
33 | 33 | <div class="fields"> |
|
34 | 34 | |
|
35 | 35 | <div class="field"> |
|
36 | 36 | <div class="label"> |
|
37 | 37 | <label for="repo_name">${_('Fork name')}:</label> |
|
38 | 38 | </div> |
|
39 | 39 | <div class="input"> |
|
40 | 40 | ${h.text('repo_name', class_="medium")} |
|
41 |
${h.hidden('repo_type',c.r |
|
|
42 |
${h.hidden('fork_parent_id',c.r |
|
|
41 | ${h.hidden('repo_type',c.rhodecode_db_repo.repo_type)} | |
|
42 | ${h.hidden('fork_parent_id',c.rhodecode_db_repo.repo_id)} | |
|
43 | 43 | </div> |
|
44 | 44 | </div> |
|
45 | 45 | |
|
46 | 46 | <div class="field"> |
|
47 | 47 | <div class="label label-textarea"> |
|
48 | 48 | <label for="description">${_('Description')}:</label> |
|
49 | 49 | </div> |
|
50 | 50 | <div class="textarea-repo textarea text-area editor"> |
|
51 | 51 | ${h.textarea('description')} |
|
52 | 52 | <span class="help-block">${_('Keep it short and to the point. Use a README file for longer descriptions.')}</span> |
|
53 | 53 | </div> |
|
54 | 54 | </div> |
|
55 | 55 | |
|
56 | 56 | <div class="field"> |
|
57 | 57 | <div class="label"> |
|
58 | 58 | <label for="repo_group">${_('Repository group')}:</label> |
|
59 | 59 | </div> |
|
60 | 60 | <div class="select"> |
|
61 | 61 | ${h.select('repo_group','',c.repo_groups,class_="medium")} |
|
62 | 62 | % if c.personal_repo_group: |
|
63 | 63 | <a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}"> |
|
64 | 64 | ${_('Select my personal group (%(repo_group_name)s)') % {'repo_group_name': c.personal_repo_group.group_name}} |
|
65 | 65 | </a> |
|
66 | 66 | % endif |
|
67 | 67 | <span class="help-block">${_('Optionally select a group to put this repository into.')}</span> |
|
68 | 68 | </div> |
|
69 | 69 | </div> |
|
70 | 70 | |
|
71 | 71 | <div class="field"> |
|
72 | 72 | <div class="label"> |
|
73 | 73 | <label for="landing_rev">${_('Landing commit')}:</label> |
|
74 | 74 | </div> |
|
75 | 75 | <div class="select"> |
|
76 | 76 | ${h.select('landing_rev','',c.landing_revs,class_="medium")} |
|
77 | 77 | <span class="help-block">${_('Default commit for files page, downloads, whoosh and readme')}</span> |
|
78 | 78 | </div> |
|
79 | 79 | </div> |
|
80 | 80 | |
|
81 | 81 | <div class="field"> |
|
82 | 82 | <div class="label label-checkbox"> |
|
83 | 83 | <label for="private">${_('Private')}:</label> |
|
84 | 84 | </div> |
|
85 | 85 | <div class="checkboxes"> |
|
86 | 86 | ${h.checkbox('private',value="True")} |
|
87 | 87 | <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span> |
|
88 | 88 | </div> |
|
89 | 89 | </div> |
|
90 | 90 | |
|
91 | 91 | <div class="field"> |
|
92 | 92 | <div class="label label-checkbox"> |
|
93 | 93 | <label for="private">${_('Copy permissions')}:</label> |
|
94 | 94 | </div> |
|
95 | 95 | <div class="checkboxes"> |
|
96 | 96 | ${h.checkbox('copy_permissions',value="True", checked="checked")} |
|
97 | 97 | <span class="help-block">${_('Copy permissions from forked repository')}</span> |
|
98 | 98 | </div> |
|
99 | 99 | </div> |
|
100 | 100 | |
|
101 | 101 | <div class="buttons"> |
|
102 | 102 | ${h.submit('',_('Fork this Repository'),class_="btn")} |
|
103 | 103 | </div> |
|
104 | 104 | </div> |
|
105 | 105 | </div> |
|
106 | 106 | ${h.end_form()} |
|
107 | 107 | </div> |
|
108 | 108 | <script> |
|
109 | 109 | $(document).ready(function(){ |
|
110 | 110 | $("#repo_group").select2({ |
|
111 | 111 | 'dropdownAutoWidth': true, |
|
112 | 112 | 'containerCssClass': "drop-menu", |
|
113 | 113 | 'dropdownCssClass': "drop-menu-dropdown", |
|
114 | 114 | 'width': "resolve" |
|
115 | 115 | }); |
|
116 | 116 | $("#landing_rev").select2({ |
|
117 | 117 | 'containerCssClass': "drop-menu", |
|
118 | 118 | 'dropdownCssClass': "drop-menu-dropdown", |
|
119 | 119 | 'minimumResultsForSearch': -1 |
|
120 | 120 | }); |
|
121 | 121 | $('#repo_name').focus(); |
|
122 | 122 | |
|
123 | 123 | $('#select_my_group').on('click', function(e){ |
|
124 | 124 | e.preventDefault(); |
|
125 | 125 | $("#repo_group").val($(this).data('personalGroupId')).trigger("change"); |
|
126 | 126 | }) |
|
127 | 127 | }) |
|
128 | 128 | </script> |
|
129 | 129 | </%def> |
General Comments 0
You need to be logged in to leave comments.
Login now