##// END OF EJS Templates
whitespace cleanup
marcink -
r3394:fe2bb88b beta
parent child Browse files
Show More
@@ -1,479 +1,479
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.pullrequests
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 pull requests controller for rhodecode for initializing pull requests
7 7
8 8 :created_on: May 7, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26 import traceback
27 27 import formencode
28 28
29 29 from webob.exc import HTTPNotFound, HTTPForbidden
30 30 from collections import defaultdict
31 31 from itertools import groupby
32 32
33 33 from pylons import request, response, session, tmpl_context as c, url
34 34 from pylons.controllers.util import abort, redirect
35 35 from pylons.i18n.translation import _
36 36
37 37 from rhodecode.lib.compat import json
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
40 40 NotAnonymous
41 41 from rhodecode.lib import helpers as h
42 42 from rhodecode.lib import diffs
43 43 from rhodecode.lib.utils import action_logger, jsonify
44 44 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
45 45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 46 from rhodecode.lib.diffs import LimitedDiffContainer
47 47 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
48 48 ChangesetComment
49 49 from rhodecode.model.pull_request import PullRequestModel
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.repo import RepoModel
52 52 from rhodecode.model.comment import ChangesetCommentsModel
53 53 from rhodecode.model.changeset_status import ChangesetStatusModel
54 54 from rhodecode.model.forms import PullRequestForm
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 class PullrequestsController(BaseRepoController):
60 60
61 61 @LoginRequired()
62 62 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
63 63 'repository.admin')
64 64 def __before__(self):
65 65 super(PullrequestsController, self).__before__()
66 66 repo_model = RepoModel()
67 67 c.users_array = repo_model.get_users_js()
68 68 c.users_groups_array = repo_model.get_users_groups_js()
69 69
70 70 def _get_repo_refs(self, repo):
71 71 hist_l = []
72 72
73 73 branches_group = ([('branch:%s:%s' % (k, v), k) for
74 74 k, v in repo.branches.iteritems()], _("Branches"))
75 75 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
76 76 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
77 77 tags_group = ([('tag:%s:%s' % (k, v), k) for
78 78 k, v in repo.tags.iteritems()
79 79 if k != 'tip'], _("Tags"))
80 80
81 81 tip = repo.tags['tip']
82 82 tipref = 'tag:tip:%s' % tip
83 83 colontip = ':' + tip
84 84 tips = [x[1] for x in branches_group[0] + bookmarks_group[0] + tags_group[0]
85 85 if x[0].endswith(colontip)]
86 86 tags_group[0].append((tipref, 'tip (%s)' % ', '.join(tips)))
87 87
88 88 hist_l.append(bookmarks_group)
89 89 hist_l.append(branches_group)
90 90 hist_l.append(tags_group)
91 91
92 92 return hist_l, tipref
93 93
94 94 def _get_is_allowed_change_status(self, pull_request):
95 95 owner = self.rhodecode_user.user_id == pull_request.user_id
96 96 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
97 97 pull_request.reviewers]
98 98 return (self.rhodecode_user.admin or owner or reviewer)
99 99
100 100 def show_all(self, repo_name):
101 101 c.pull_requests = PullRequestModel().get_all(repo_name)
102 102 c.repo_name = repo_name
103 103 return render('/pullrequests/pullrequest_show_all.html')
104 104
105 105 @NotAnonymous()
106 106 def index(self):
107 107 org_repo = c.rhodecode_db_repo
108 108
109 109 if org_repo.scm_instance.alias != 'hg':
110 110 log.error('Review not available for GIT REPOS')
111 111 raise HTTPNotFound
112 112
113 113 try:
114 114 org_repo.scm_instance.get_changeset()
115 115 except EmptyRepositoryError, e:
116 116 h.flash(h.literal(_('There are no changesets yet')),
117 117 category='warning')
118 118 redirect(url('summary_home', repo_name=org_repo.repo_name))
119 119
120 120 other_repos_info = {}
121 121
122 122 c.org_repos = []
123 123 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
124 124 c.default_org_repo = org_repo.repo_name
125 125 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance)
126 126
127 127 c.other_repos = []
128 128 # add org repo to other so we can open pull request against itself
129 129 c.other_repos.extend(c.org_repos)
130 130 c.default_other_repo = org_repo.repo_name
131 131 c.default_other_refs, c.default_other_ref = self._get_repo_refs(org_repo.scm_instance)
132 132 usr_data = lambda usr: dict(user_id=usr.user_id,
133 133 username=usr.username,
134 134 firstname=usr.firstname,
135 135 lastname=usr.lastname,
136 136 gravatar_link=h.gravatar_url(usr.email, 14))
137 137 other_repos_info[org_repo.repo_name] = {
138 138 'user': usr_data(org_repo.user),
139 139 'description': org_repo.description,
140 140 'revs': h.select('other_ref', c.default_other_ref,
141 141 c.default_other_refs, class_='refs')
142 142 }
143 143
144 # gather forks and add to this list ... even though it is rare to
144 # gather forks and add to this list ... even though it is rare to
145 145 # request forks to pull their parent
146 146 for fork in org_repo.forks:
147 147 c.other_repos.append((fork.repo_name, fork.repo_name))
148 148 refs, default_ref = self._get_repo_refs(fork.scm_instance)
149 149 other_repos_info[fork.repo_name] = {
150 150 'user': usr_data(fork.user),
151 151 'description': fork.description,
152 152 'revs': h.select('other_ref', default_ref, refs, class_='refs')
153 153 }
154 154
155 155 # add parents of this fork also, but only if it's not empty
156 156 if org_repo.parent and org_repo.parent.scm_instance.revisions:
157 157 c.default_other_repo = org_repo.parent.repo_name
158 158 c.default_other_refs, c.default_other_ref = self._get_repo_refs(org_repo.parent.scm_instance)
159 159 c.other_repos.append((org_repo.parent.repo_name, org_repo.parent.repo_name))
160 160 other_repos_info[org_repo.parent.repo_name] = {
161 161 'user': usr_data(org_repo.parent.user),
162 162 'description': org_repo.parent.description,
163 163 'revs': h.select('other_ref', c.default_other_ref,
164 164 c.default_other_refs, class_='refs')
165 165 }
166 166
167 167 c.other_repos_info = json.dumps(other_repos_info)
168 168 # other repo owner
169 169 c.review_members = []
170 170 return render('/pullrequests/pullrequest.html')
171 171
172 172 @NotAnonymous()
173 173 def create(self, repo_name):
174 174 repo = RepoModel()._get_repo(repo_name)
175 175 try:
176 176 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
177 177 except formencode.Invalid, errors:
178 178 log.error(traceback.format_exc())
179 179 if errors.error_dict.get('revisions'):
180 180 msg = 'Revisions: %s' % errors.error_dict['revisions']
181 181 elif errors.error_dict.get('pullrequest_title'):
182 182 msg = _('Pull request requires a title with min. 3 chars')
183 183 else:
184 184 msg = _('error during creation of pull request')
185 185
186 186 h.flash(msg, 'error')
187 187 return redirect(url('pullrequest_home', repo_name=repo_name))
188 188
189 189 org_repo = _form['org_repo']
190 190 org_ref = _form['org_ref']
191 191 other_repo = _form['other_repo']
192 192 other_ref = _form['other_ref']
193 193 revisions = _form['revisions']
194 194 reviewers = _form['review_members']
195 195
196 196 # if we have cherry picked pull request we don't care what is in
197 197 # org_ref/other_ref
198 198 rev_start = request.POST.get('rev_start')
199 199 rev_end = request.POST.get('rev_end')
200 200
201 201 if rev_start and rev_end:
202 202 # this is swapped to simulate that rev_end is a revision from
203 203 # parent of the fork
204 204 org_ref = 'rev:%s:%s' % (rev_end, rev_end)
205 205 other_ref = 'rev:%s:%s' % (rev_start, rev_start)
206 206
207 207 title = _form['pullrequest_title']
208 208 description = _form['pullrequest_desc']
209 209
210 210 try:
211 211 pull_request = PullRequestModel().create(
212 212 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
213 213 other_ref, revisions, reviewers, title, description
214 214 )
215 215 Session().commit()
216 216 h.flash(_('Successfully opened new pull request'),
217 217 category='success')
218 218 except Exception:
219 219 h.flash(_('Error occurred during sending pull request'),
220 220 category='error')
221 221 log.error(traceback.format_exc())
222 222 return redirect(url('pullrequest_home', repo_name=repo_name))
223 223
224 224 return redirect(url('pullrequest_show', repo_name=other_repo,
225 225 pull_request_id=pull_request.pull_request_id))
226 226
227 227 @NotAnonymous()
228 228 @jsonify
229 229 def update(self, repo_name, pull_request_id):
230 230 pull_request = PullRequest.get_or_404(pull_request_id)
231 231 if pull_request.is_closed():
232 232 raise HTTPForbidden()
233 233 #only owner or admin can update it
234 234 owner = pull_request.author.user_id == c.rhodecode_user.user_id
235 235 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
236 236 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
237 237 request.POST.get('reviewers_ids', '').split(',')))
238 238
239 239 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
240 240 Session().commit()
241 241 return True
242 242 raise HTTPForbidden()
243 243
244 244 @NotAnonymous()
245 245 @jsonify
246 246 def delete(self, repo_name, pull_request_id):
247 247 pull_request = PullRequest.get_or_404(pull_request_id)
248 248 #only owner can delete it !
249 249 if pull_request.author.user_id == c.rhodecode_user.user_id:
250 250 PullRequestModel().delete(pull_request)
251 251 Session().commit()
252 252 h.flash(_('Successfully deleted pull request'),
253 253 category='success')
254 254 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
255 255 raise HTTPForbidden()
256 256
257 257 def _load_compare_data(self, pull_request, enable_comments=True):
258 258 """
259 259 Load context data needed for generating compare diff
260 260
261 261 :param pull_request:
262 262 :type pull_request:
263 263 """
264 264 rev_start = request.GET.get('rev_start')
265 265 rev_end = request.GET.get('rev_end')
266 266
267 267 org_repo = pull_request.org_repo
268 268 (org_ref_type,
269 269 org_ref_name,
270 270 org_ref_rev) = pull_request.org_ref.split(':')
271 271
272 272 other_repo = org_repo
273 273 (other_ref_type,
274 274 other_ref_name,
275 275 other_ref_rev) = pull_request.other_ref.split(':')
276 276
277 277 # despite opening revisions for bookmarks/branches/tags, we always
278 278 # convert this to rev to prevent changes after book or branch change
279 279 org_ref = ('rev', org_ref_rev)
280 280 other_ref = ('rev', other_ref_rev)
281 281
282 282 c.org_repo = org_repo
283 283 c.other_repo = other_repo
284 284
285 285 c.fulldiff = fulldiff = request.GET.get('fulldiff')
286 286
287 287 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
288 288
289 289 other_ref = ('rev', getattr(c.cs_ranges[0].parents[0]
290 290 if c.cs_ranges[0].parents
291 291 else EmptyChangeset(), 'raw_id'))
292 292
293 293 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
294 294 # defines that we need hidden inputs with changesets
295 295 c.as_form = request.GET.get('as_form', False)
296 296
297 297 c.org_ref = org_ref[1]
298 298 c.org_ref_type = org_ref[0]
299 299 c.other_ref = other_ref[1]
300 300 c.other_ref_type = other_ref[0]
301 301
302 302 diff_limit = self.cut_off_limit if not fulldiff else None
303 303
304 304 #we swap org/other ref since we run a simple diff on one repo
305 305 _diff = diffs.differ(org_repo, other_ref, other_repo, org_ref)
306 306
307 307 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
308 308 diff_limit=diff_limit)
309 309 _parsed = diff_processor.prepare()
310 310
311 311 c.limited_diff = False
312 312 if isinstance(_parsed, LimitedDiffContainer):
313 313 c.limited_diff = True
314 314
315 315 c.files = []
316 316 c.changes = {}
317 317 c.lines_added = 0
318 318 c.lines_deleted = 0
319 319 for f in _parsed:
320 320 st = f['stats']
321 321 if st[0] != 'b':
322 322 c.lines_added += st[0]
323 323 c.lines_deleted += st[1]
324 324 fid = h.FID('', f['filename'])
325 325 c.files.append([fid, f['operation'], f['filename'], f['stats']])
326 326 diff = diff_processor.as_html(enable_comments=enable_comments,
327 327 parsed_lines=[f])
328 328 c.changes[fid] = [f['operation'], f['filename'], diff]
329 329
330 330 def show(self, repo_name, pull_request_id):
331 331 repo_model = RepoModel()
332 332 c.users_array = repo_model.get_users_js()
333 333 c.users_groups_array = repo_model.get_users_groups_js()
334 334 c.pull_request = PullRequest.get_or_404(pull_request_id)
335 335 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
336 336 cc_model = ChangesetCommentsModel()
337 337 cs_model = ChangesetStatusModel()
338 338 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
339 339 pull_request=c.pull_request,
340 340 with_revisions=True)
341 341
342 342 cs_statuses = defaultdict(list)
343 343 for st in _cs_statuses:
344 344 cs_statuses[st.author.username] += [st]
345 345
346 346 c.pull_request_reviewers = []
347 347 c.pull_request_pending_reviewers = []
348 348 for o in c.pull_request.reviewers:
349 349 st = cs_statuses.get(o.user.username, None)
350 350 if st:
351 351 sorter = lambda k: k.version
352 352 st = [(x, list(y)[0])
353 353 for x, y in (groupby(sorted(st, key=sorter), sorter))]
354 354 else:
355 355 c.pull_request_pending_reviewers.append(o.user)
356 356 c.pull_request_reviewers.append([o.user, st])
357 357
358 358 # pull_requests repo_name we opened it against
359 359 # ie. other_repo must match
360 360 if repo_name != c.pull_request.other_repo.repo_name:
361 361 raise HTTPNotFound
362 362
363 363 # load compare data into template context
364 364 enable_comments = not c.pull_request.is_closed()
365 365 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
366 366
367 367 # inline comments
368 368 c.inline_cnt = 0
369 369 c.inline_comments = cc_model.get_inline_comments(
370 370 c.rhodecode_db_repo.repo_id,
371 371 pull_request=pull_request_id)
372 372 # count inline comments
373 373 for __, lines in c.inline_comments:
374 374 for comments in lines.values():
375 375 c.inline_cnt += len(comments)
376 376 # comments
377 377 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
378 378 pull_request=pull_request_id)
379 379
380 380 try:
381 381 cur_status = c.statuses[c.pull_request.revisions[0]][0]
382 382 except:
383 383 log.error(traceback.format_exc())
384 384 cur_status = 'undefined'
385 385 if c.pull_request.is_closed() and 0:
386 386 c.current_changeset_status = cur_status
387 387 else:
388 388 # changeset(pull-request) status calulation based on reviewers
389 389 c.current_changeset_status = cs_model.calculate_status(
390 390 c.pull_request_reviewers,
391 391 )
392 392 c.changeset_statuses = ChangesetStatus.STATUSES
393 393
394 394 return render('/pullrequests/pullrequest_show.html')
395 395
396 396 @NotAnonymous()
397 397 @jsonify
398 398 def comment(self, repo_name, pull_request_id):
399 399 pull_request = PullRequest.get_or_404(pull_request_id)
400 400 if pull_request.is_closed():
401 401 raise HTTPForbidden()
402 402
403 403 status = request.POST.get('changeset_status')
404 404 change_status = request.POST.get('change_changeset_status')
405 405 text = request.POST.get('text')
406 406
407 407 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
408 408 if status and change_status and allowed_to_change_status:
409 409 text = text or (_('Status change -> %s')
410 410 % ChangesetStatus.get_status_lbl(status))
411 411 comm = ChangesetCommentsModel().create(
412 412 text=text,
413 413 repo=c.rhodecode_db_repo.repo_id,
414 414 user=c.rhodecode_user.user_id,
415 415 pull_request=pull_request_id,
416 416 f_path=request.POST.get('f_path'),
417 417 line_no=request.POST.get('line'),
418 418 status_change=(ChangesetStatus.get_status_lbl(status)
419 419 if status and change_status and allowed_to_change_status else None)
420 420 )
421 421
422 422 action_logger(self.rhodecode_user,
423 423 'user_commented_pull_request:%s' % pull_request_id,
424 424 c.rhodecode_db_repo, self.ip_addr, self.sa)
425 425
426 426 if allowed_to_change_status:
427 427 # get status if set !
428 428 if status and change_status:
429 429 ChangesetStatusModel().set_status(
430 430 c.rhodecode_db_repo.repo_id,
431 431 status,
432 432 c.rhodecode_user.user_id,
433 433 comm,
434 434 pull_request=pull_request_id
435 435 )
436 436
437 437 if request.POST.get('save_close'):
438 438 if status in ['rejected', 'approved']:
439 439 PullRequestModel().close_pull_request(pull_request_id)
440 440 action_logger(self.rhodecode_user,
441 441 'user_closed_pull_request:%s' % pull_request_id,
442 442 c.rhodecode_db_repo, self.ip_addr, self.sa)
443 443 else:
444 444 h.flash(_('Closing pull request on other statuses than '
445 445 'rejected or approved forbidden'),
446 446 category='warning')
447 447
448 448 Session().commit()
449 449
450 450 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
451 451 return redirect(h.url('pullrequest_show', repo_name=repo_name,
452 452 pull_request_id=pull_request_id))
453 453
454 454 data = {
455 455 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
456 456 }
457 457 if comm:
458 458 c.co = comm
459 459 data.update(comm.get_dict())
460 460 data.update({'rendered_text':
461 461 render('changeset/changeset_comment_block.html')})
462 462
463 463 return data
464 464
465 465 @NotAnonymous()
466 466 @jsonify
467 467 def delete_comment(self, repo_name, comment_id):
468 468 co = ChangesetComment.get(comment_id)
469 469 if co.pull_request.is_closed():
470 470 #don't allow deleting comments on closed pull request
471 471 raise HTTPForbidden()
472 472
473 473 owner = co.author.user_id == c.rhodecode_user.user_id
474 474 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
475 475 ChangesetCommentsModel().delete(comment=co)
476 476 Session().commit()
477 477 return True
478 478 else:
479 479 raise HTTPForbidden()
@@ -1,544 +1,544
1 1 import re
2 2 from itertools import chain
3 3 from dulwich import objects
4 4 from subprocess import Popen, PIPE
5 5 import rhodecode
6 6 from rhodecode.lib.vcs.conf import settings
7 7 from rhodecode.lib.vcs.exceptions import RepositoryError
8 8 from rhodecode.lib.vcs.exceptions import ChangesetError
9 9 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
10 10 from rhodecode.lib.vcs.exceptions import VCSError
11 11 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
12 12 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
13 13 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
14 14 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
15 15 RemovedFileNode, SubModuleNode, ChangedFileNodesGenerator,\
16 16 AddedFileNodesGenerator, RemovedFileNodesGenerator
17 17 from rhodecode.lib.vcs.utils import safe_unicode
18 18 from rhodecode.lib.vcs.utils import date_fromtimestamp
19 19 from rhodecode.lib.vcs.utils.lazy import LazyProperty
20 20
21 21
22 22 class GitChangeset(BaseChangeset):
23 23 """
24 24 Represents state of the repository at single revision.
25 25 """
26 26
27 27 def __init__(self, repository, revision):
28 28 self._stat_modes = {}
29 29 self.repository = repository
30 30
31 31 try:
32 32 commit = self.repository._repo.get_object(revision)
33 33 if isinstance(commit, objects.Tag):
34 34 revision = commit.object[1]
35 35 commit = self.repository._repo.get_object(commit.object[1])
36 36 except KeyError:
37 37 raise RepositoryError("Cannot get object with id %s" % revision)
38 38 self.raw_id = revision
39 39 self.id = self.raw_id
40 40 self.short_id = self.raw_id[:12]
41 41 self._commit = commit
42 42
43 43 self._tree_id = commit.tree
44 44 self._commiter_property = 'committer'
45 45 self._author_property = 'author'
46 46 self._date_property = 'commit_time'
47 47 self._date_tz_property = 'commit_timezone'
48 48 self.revision = repository.revisions.index(revision)
49 49
50 50 self.message = safe_unicode(commit.message)
51 51
52 52 self.nodes = {}
53 53 self._paths = {}
54 54
55 55 @LazyProperty
56 56 def commiter(self):
57 57 return safe_unicode(getattr(self._commit, self._commiter_property))
58 58
59 59 @LazyProperty
60 60 def author(self):
61 61 return safe_unicode(getattr(self._commit, self._author_property))
62 62
63 63 @LazyProperty
64 64 def date(self):
65 65 return date_fromtimestamp(getattr(self._commit, self._date_property),
66 66 getattr(self._commit, self._date_tz_property))
67 67
68 68 @LazyProperty
69 69 def _timestamp(self):
70 70 return getattr(self._commit, self._date_property)
71 71
72 72 @LazyProperty
73 73 def status(self):
74 74 """
75 75 Returns modified, added, removed, deleted files for current changeset
76 76 """
77 77 return self.changed, self.added, self.removed
78 78
79 79 @LazyProperty
80 80 def tags(self):
81 81 _tags = []
82 82 for tname, tsha in self.repository.tags.iteritems():
83 83 if tsha == self.raw_id:
84 84 _tags.append(tname)
85 85 return _tags
86 86
87 87 @LazyProperty
88 88 def branch(self):
89 89
90 90 heads = self.repository._heads(reverse=False)
91 91
92 92 ref = heads.get(self.raw_id)
93 93 if ref:
94 94 return safe_unicode(ref)
95 95
96 96 def _fix_path(self, path):
97 97 """
98 98 Paths are stored without trailing slash so we need to get rid off it if
99 99 needed.
100 100 """
101 101 if path.endswith('/'):
102 102 path = path.rstrip('/')
103 103 return path
104 104
105 105 def _get_id_for_path(self, path):
106 106
107 107 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
108 108 if not path in self._paths:
109 109 path = path.strip('/')
110 110 # set root tree
111 111 tree = self.repository._repo[self._tree_id]
112 112 if path == '':
113 113 self._paths[''] = tree.id
114 114 return tree.id
115 115 splitted = path.split('/')
116 116 dirs, name = splitted[:-1], splitted[-1]
117 117 curdir = ''
118 118
119 119 # initially extract things from root dir
120 120 for item, stat, id in tree.iteritems():
121 121 if curdir:
122 122 name = '/'.join((curdir, item))
123 123 else:
124 124 name = item
125 125 self._paths[name] = id
126 126 self._stat_modes[name] = stat
127 127
128 128 for dir in dirs:
129 129 if curdir:
130 130 curdir = '/'.join((curdir, dir))
131 131 else:
132 132 curdir = dir
133 133 dir_id = None
134 134 for item, stat, id in tree.iteritems():
135 135 if dir == item:
136 136 dir_id = id
137 137 if dir_id:
138 138 # Update tree
139 139 tree = self.repository._repo[dir_id]
140 140 if not isinstance(tree, objects.Tree):
141 141 raise ChangesetError('%s is not a directory' % curdir)
142 142 else:
143 143 raise ChangesetError('%s have not been found' % curdir)
144 144
145 145 # cache all items from the given traversed tree
146 146 for item, stat, id in tree.iteritems():
147 147 if curdir:
148 148 name = '/'.join((curdir, item))
149 149 else:
150 150 name = item
151 151 self._paths[name] = id
152 152 self._stat_modes[name] = stat
153 153 if not path in self._paths:
154 154 raise NodeDoesNotExistError("There is no file nor directory "
155 155 "at the given path %r at revision %r"
156 156 % (path, self.short_id))
157 157 return self._paths[path]
158 158
159 159 def _get_kind(self, path):
160 160 obj = self.repository._repo[self._get_id_for_path(path)]
161 161 if isinstance(obj, objects.Blob):
162 162 return NodeKind.FILE
163 163 elif isinstance(obj, objects.Tree):
164 164 return NodeKind.DIR
165 165
166 166 def _get_filectx(self, path):
167 167 path = self._fix_path(path)
168 168 if self._get_kind(path) != NodeKind.FILE:
169 169 raise ChangesetError("File does not exist for revision %r at "
170 170 " %r" % (self.raw_id, path))
171 171 return path
172 172
173 173 def _get_file_nodes(self):
174 174 return chain(*(t[2] for t in self.walk()))
175 175
176 176 @LazyProperty
177 177 def parents(self):
178 178 """
179 179 Returns list of parents changesets.
180 180 """
181 181 return [self.repository.get_changeset(parent)
182 182 for parent in self._commit.parents]
183 183
184 184 @LazyProperty
185 185 def children(self):
186 186 """
187 187 Returns list of children changesets.
188 188 """
189 189 so, se = self.repository.run_git_command(
190 190 "rev-list --all --children | grep '^%s'" % self.raw_id
191 191 )
192 192
193 193 children = []
194 194 for l in so.splitlines():
195 195 childs = l.split(' ')[1:]
196 196 children.extend(childs)
197 197 return [self.repository.get_changeset(cs) for cs in children]
198 198
199 199 def next(self, branch=None):
200 200
201 201 if branch and self.branch != branch:
202 202 raise VCSError('Branch option used on changeset not belonging '
203 203 'to that branch')
204 204
205 205 def _next(changeset, branch):
206 206 try:
207 207 next_ = changeset.revision + 1
208 208 next_rev = changeset.repository.revisions[next_]
209 209 except IndexError:
210 210 raise ChangesetDoesNotExistError
211 211 cs = changeset.repository.get_changeset(next_rev)
212 212
213 213 if branch and branch != cs.branch:
214 214 return _next(cs, branch)
215 215
216 216 return cs
217 217
218 218 return _next(self, branch)
219 219
220 220 def prev(self, branch=None):
221 221 if branch and self.branch != branch:
222 222 raise VCSError('Branch option used on changeset not belonging '
223 223 'to that branch')
224 224
225 225 def _prev(changeset, branch):
226 226 try:
227 227 prev_ = changeset.revision - 1
228 228 if prev_ < 0:
229 229 raise IndexError
230 230 prev_rev = changeset.repository.revisions[prev_]
231 231 except IndexError:
232 232 raise ChangesetDoesNotExistError
233 233
234 234 cs = changeset.repository.get_changeset(prev_rev)
235 235
236 236 if branch and branch != cs.branch:
237 237 return _prev(cs, branch)
238 238
239 239 return cs
240 240
241 241 return _prev(self, branch)
242 242
243 243 def diff(self, ignore_whitespace=True, context=3):
244 244 rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET
245 245 rev2 = self
246 246 return ''.join(self.repository.get_diff(rev1, rev2,
247 247 ignore_whitespace=ignore_whitespace,
248 248 context=context))
249 249
250 250 def get_file_mode(self, path):
251 251 """
252 252 Returns stat mode of the file at the given ``path``.
253 253 """
254 254 # ensure path is traversed
255 255 self._get_id_for_path(path)
256 256 return self._stat_modes[path]
257 257
258 258 def get_file_content(self, path):
259 259 """
260 260 Returns content of the file at given ``path``.
261 261 """
262 262 id = self._get_id_for_path(path)
263 263 blob = self.repository._repo[id]
264 264 return blob.as_pretty_string()
265 265
266 266 def get_file_size(self, path):
267 267 """
268 268 Returns size of the file at given ``path``.
269 269 """
270 270 id = self._get_id_for_path(path)
271 271 blob = self.repository._repo[id]
272 272 return blob.raw_length()
273 273
274 274 def get_file_changeset(self, path):
275 275 """
276 276 Returns last commit of the file at the given ``path``.
277 277 """
278 278 node = self.get_node(path)
279 279 return node.history[0]
280 280
281 281 def get_file_history(self, path):
282 282 """
283 283 Returns history of file as reversed list of ``Changeset`` objects for
284 284 which file at given ``path`` has been modified.
285 285
286 286 TODO: This function now uses os underlying 'git' and 'grep' commands
287 287 which is generally not good. Should be replaced with algorithm
288 288 iterating commits.
289 289 """
290 290 self._get_filectx(path)
291 291
292 292 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
293 293 self.id, path
294 294 )
295 295 so, se = self.repository.run_git_command(cmd)
296 296 ids = re.findall(r'[0-9a-fA-F]{40}', so)
297 297 return [self.repository.get_changeset(id) for id in ids]
298 298
299 299 def get_file_history_2(self, path):
300 300 """
301 301 Returns history of file as reversed list of ``Changeset`` objects for
302 302 which file at given ``path`` has been modified.
303 303
304 304 """
305 305 self._get_filectx(path)
306 306 from dulwich.walk import Walker
307 307 include = [self.id]
308 308 walker = Walker(self.repository._repo.object_store, include,
309 309 paths=[path], max_entries=1)
310 310 return [self.repository.get_changeset(sha)
311 311 for sha in (x.commit.id for x in walker)]
312 312
313 313 def get_file_annotate(self, path):
314 314 """
315 315 Returns a generator of four element tuples with
316 316 lineno, sha, changeset lazy loader and line
317 317
318 318 TODO: This function now uses os underlying 'git' command which is
319 319 generally not good. Should be replaced with algorithm iterating
320 320 commits.
321 321 """
322 322 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
323 323 # -l ==> outputs long shas (and we need all 40 characters)
324 324 # --root ==> doesn't put '^' character for bounderies
325 325 # -r sha ==> blames for the given revision
326 326 so, se = self.repository.run_git_command(cmd)
327 327
328 328 for i, blame_line in enumerate(so.split('\n')[:-1]):
329 329 ln_no = i + 1
330 330 sha, line = re.split(r' ', blame_line, 1)
331 331 yield (ln_no, sha, lambda: self.repository.get_changeset(sha), line)
332 332
333 333 def fill_archive(self, stream=None, kind='tgz', prefix=None,
334 334 subrepos=False):
335 335 """
336 336 Fills up given stream.
337 337
338 338 :param stream: file like object.
339 339 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
340 340 Default: ``tgz``.
341 341 :param prefix: name of root directory in archive.
342 342 Default is repository name and changeset's raw_id joined with dash
343 343 (``repo-tip.<KIND>``).
344 344 :param subrepos: include subrepos in this archive.
345 345
346 346 :raise ImproperArchiveTypeError: If given kind is wrong.
347 347 :raise VcsError: If given stream is None
348 348
349 349 """
350 350 allowed_kinds = settings.ARCHIVE_SPECS.keys()
351 351 if kind not in allowed_kinds:
352 352 raise ImproperArchiveTypeError('Archive kind not supported use one'
353 353 'of %s', allowed_kinds)
354 354
355 355 if prefix is None:
356 356 prefix = '%s-%s' % (self.repository.name, self.short_id)
357 357 elif prefix.startswith('/'):
358 358 raise VCSError("Prefix cannot start with leading slash")
359 359 elif prefix.strip() == '':
360 360 raise VCSError("Prefix cannot be empty")
361 361
362 362 if kind == 'zip':
363 363 frmt = 'zip'
364 364 else:
365 365 frmt = 'tar'
366 366 _git_path = rhodecode.CONFIG.get('git_path', 'git')
367 cmd = '%s archive --format=%s --prefix=%s/ %s' % (_git_path,
367 cmd = '%s archive --format=%s --prefix=%s/ %s' % (_git_path,
368 368 frmt, prefix, self.raw_id)
369 369 if kind == 'tgz':
370 370 cmd += ' | gzip -9'
371 371 elif kind == 'tbz2':
372 372 cmd += ' | bzip2 -9'
373 373
374 374 if stream is None:
375 375 raise VCSError('You need to pass in a valid stream for filling'
376 376 ' with archival data')
377 377 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
378 378 cwd=self.repository.path)
379 379
380 380 buffer_size = 1024 * 8
381 381 chunk = popen.stdout.read(buffer_size)
382 382 while chunk:
383 383 stream.write(chunk)
384 384 chunk = popen.stdout.read(buffer_size)
385 385 # Make sure all descriptors would be read
386 386 popen.communicate()
387 387
388 388 def get_nodes(self, path):
389 389 if self._get_kind(path) != NodeKind.DIR:
390 390 raise ChangesetError("Directory does not exist for revision %r at "
391 391 " %r" % (self.revision, path))
392 392 path = self._fix_path(path)
393 393 id = self._get_id_for_path(path)
394 394 tree = self.repository._repo[id]
395 395 dirnodes = []
396 396 filenodes = []
397 397 als = self.repository.alias
398 398 for name, stat, id in tree.iteritems():
399 399 if objects.S_ISGITLINK(stat):
400 400 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
401 401 alias=als))
402 402 continue
403 403
404 404 obj = self.repository._repo.get_object(id)
405 405 if path != '':
406 406 obj_path = '/'.join((path, name))
407 407 else:
408 408 obj_path = name
409 409 if obj_path not in self._stat_modes:
410 410 self._stat_modes[obj_path] = stat
411 411 if isinstance(obj, objects.Tree):
412 412 dirnodes.append(DirNode(obj_path, changeset=self))
413 413 elif isinstance(obj, objects.Blob):
414 414 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
415 415 else:
416 416 raise ChangesetError("Requested object should be Tree "
417 417 "or Blob, is %r" % type(obj))
418 418 nodes = dirnodes + filenodes
419 419 for node in nodes:
420 420 if not node.path in self.nodes:
421 421 self.nodes[node.path] = node
422 422 nodes.sort()
423 423 return nodes
424 424
425 425 def get_node(self, path):
426 426 if isinstance(path, unicode):
427 427 path = path.encode('utf-8')
428 428 path = self._fix_path(path)
429 429 if not path in self.nodes:
430 430 try:
431 431 id_ = self._get_id_for_path(path)
432 432 except ChangesetError:
433 433 raise NodeDoesNotExistError("Cannot find one of parents' "
434 434 "directories for a given path: %s" % path)
435 435
436 436 _GL = lambda m: m and objects.S_ISGITLINK(m)
437 437 if _GL(self._stat_modes.get(path)):
438 438 node = SubModuleNode(path, url=None, changeset=id_,
439 439 alias=self.repository.alias)
440 440 else:
441 441 obj = self.repository._repo.get_object(id_)
442 442
443 443 if isinstance(obj, objects.Tree):
444 444 if path == '':
445 445 node = RootNode(changeset=self)
446 446 else:
447 447 node = DirNode(path, changeset=self)
448 448 node._tree = obj
449 449 elif isinstance(obj, objects.Blob):
450 450 node = FileNode(path, changeset=self)
451 451 node._blob = obj
452 452 else:
453 453 raise NodeDoesNotExistError("There is no file nor directory "
454 454 "at the given path %r at revision %r"
455 455 % (path, self.short_id))
456 456 # cache node
457 457 self.nodes[path] = node
458 458 return self.nodes[path]
459 459
460 460 @LazyProperty
461 461 def affected_files(self):
462 462 """
463 463 Get's a fast accessible file changes for given changeset
464 464 """
465 465 a, m, d = self._changes_cache
466 466 return list(a.union(m).union(d))
467 467
468 468 @LazyProperty
469 469 def _diff_name_status(self):
470 470 output = []
471 471 for parent in self.parents:
472 472 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id,
473 473 self.raw_id)
474 474 so, se = self.repository.run_git_command(cmd)
475 475 output.append(so.strip())
476 476 return '\n'.join(output)
477 477
478 478 @LazyProperty
479 479 def _changes_cache(self):
480 480 added = set()
481 481 modified = set()
482 482 deleted = set()
483 483 _r = self.repository._repo
484 484
485 485 parents = self.parents
486 486 if not self.parents:
487 487 parents = [EmptyChangeset()]
488 488 for parent in parents:
489 489 if isinstance(parent, EmptyChangeset):
490 490 oid = None
491 491 else:
492 492 oid = _r[parent.raw_id].tree
493 493 changes = _r.object_store.tree_changes(oid, _r[self.raw_id].tree)
494 494 for (oldpath, newpath), (_, _), (_, _) in changes:
495 495 if newpath and oldpath:
496 496 modified.add(newpath)
497 497 elif newpath and not oldpath:
498 498 added.add(newpath)
499 499 elif not newpath and oldpath:
500 500 deleted.add(oldpath)
501 501 return added, modified, deleted
502 502
503 503 def _get_paths_for_status(self, status):
504 504 """
505 505 Returns sorted list of paths for given ``status``.
506 506
507 507 :param status: one of: *added*, *modified* or *deleted*
508 508 """
509 509 a, m, d = self._changes_cache
510 510 return sorted({
511 511 'added': list(a),
512 512 'modified': list(m),
513 513 'deleted': list(d)}[status]
514 514 )
515 515
516 516 @LazyProperty
517 517 def added(self):
518 518 """
519 519 Returns list of added ``FileNode`` objects.
520 520 """
521 521 if not self.parents:
522 522 return list(self._get_file_nodes())
523 523 return AddedFileNodesGenerator([n for n in
524 524 self._get_paths_for_status('added')], self)
525 525
526 526 @LazyProperty
527 527 def changed(self):
528 528 """
529 529 Returns list of modified ``FileNode`` objects.
530 530 """
531 531 if not self.parents:
532 532 return []
533 533 return ChangedFileNodesGenerator([n for n in
534 534 self._get_paths_for_status('modified')], self)
535 535
536 536 @LazyProperty
537 537 def removed(self):
538 538 """
539 539 Returns list of removed ``FileNode`` objects.
540 540 """
541 541 if not self.parents:
542 542 return []
543 543 return RemovedFileNodesGenerator([n for n in
544 544 self._get_paths_for_status('deleted')], self)
@@ -1,373 +1,373
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Edit repository')} ${c.repo_info.repo_name} - ${c.rhodecode_name}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 ${h.link_to(_(u'Home'),h.url('/'))}
10 10 &raquo;
11 11 ${h.repo_link(c.rhodecode_db_repo.groups_and_repo)}
12 12 &raquo;
13 13 ${_('edit')}
14 14 </%def>
15 15
16 16 <%def name="page_nav()">
17 17 ${self.menu('options')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21 <div class="box box-left">
22 22 <!-- box / title -->
23 23 <div class="title">
24 24 ${self.breadcrumbs()}
25 25 </div>
26 26 ${h.form(url('repo', repo_name=c.repo_info.repo_name),method='put')}
27 27 <div class="form">
28 28 <!-- fields -->
29 29 <div class="fields">
30 30 <div class="field">
31 31 <div class="label">
32 32 <label for="repo_name">${_('Name')}:</label>
33 33 </div>
34 34 <div class="input">
35 35 ${h.text('repo_name',class_="medium")}
36 36 </div>
37 37 </div>
38 38 <div class="field">
39 39 <div class="label">
40 40 <label for="clone_uri">${_('Clone uri')}:</label>
41 41 </div>
42 42 <div class="input">
43 43 ${h.text('clone_uri',class_="medium")}
44 44 <span class="help-block">${_('Optional http[s] url from which repository should be cloned.')}</span>
45 45 </div>
46 46 </div>
47 47 <div class="field">
48 48 <div class="label">
49 49 <label for="repo_group">${_('Repository group')}:</label>
50 50 </div>
51 51 <div class="input">
52 52 ${h.select('repo_group','',c.repo_groups,class_="medium")}
53 53 <span class="help-block">${_('Optional select a group to put this repository into.')}</span>
54 54 </div>
55 55 </div>
56 56 <div class="field">
57 57 <div class="label">
58 58 <label for="repo_type">${_('Type')}:</label>
59 59 </div>
60 60 <div class="input">
61 61 ${h.select('repo_type','hg',c.backends,class_="medium")}
62 62 </div>
63 63 </div>
64 64 <div class="field">
65 65 <div class="label">
66 66 <label for="repo_landing_rev">${_('Landing revision')}:</label>
67 67 </div>
68 68 <div class="input">
69 69 ${h.select('repo_landing_rev','',c.landing_revs,class_="medium")}
70 70 <span class="help-block">${_('Default revision for files page, downloads, whoosh and readme')}</span>
71 71 </div>
72 72 </div>
73 73 <div class="field">
74 74 <div class="label label-textarea">
75 75 <label for="repo_description">${_('Description')}:</label>
76 76 </div>
77 77 <div class="textarea text-area editor">
78 78 ${h.textarea('repo_description')}
79 79 <span class="help-block">${_('Keep it short and to the point. Use a README file for longer descriptions.')}</span>
80 80 </div>
81 81 </div>
82 82
83 83 <div class="field">
84 84 <div class="label label-checkbox">
85 85 <label for="repo_private">${_('Private repository')}:</label>
86 86 </div>
87 87 <div class="checkboxes">
88 88 ${h.checkbox('repo_private',value="True")}
89 89 <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
90 90 </div>
91 91 </div>
92 92 <div class="field">
93 93 <div class="label label-checkbox">
94 94 <label for="repo_enable_statistics">${_('Enable statistics')}:</label>
95 95 </div>
96 96 <div class="checkboxes">
97 97 ${h.checkbox('repo_enable_statistics',value="True")}
98 98 <span class="help-block">${_('Enable statistics window on summary page.')}</span>
99 99 </div>
100 100 </div>
101 101 <div class="field">
102 102 <div class="label label-checkbox">
103 103 <label for="repo_enable_downloads">${_('Enable downloads')}:</label>
104 104 </div>
105 105 <div class="checkboxes">
106 106 ${h.checkbox('repo_enable_downloads',value="True")}
107 107 <span class="help-block">${_('Enable download menu on summary page.')}</span>
108 108 </div>
109 109 </div>
110 110 <div class="field">
111 111 <div class="label label-checkbox">
112 112 <label for="repo_enable_locking">${_('Enable locking')}:</label>
113 113 </div>
114 114 <div class="checkboxes">
115 115 ${h.checkbox('repo_enable_locking',value="True")}
116 116 <span class="help-block">${_('Enable lock-by-pulling on repository.')}</span>
117 117 </div>
118 118 </div>
119 119 <div class="field">
120 120 <div class="label">
121 121 <label for="user">${_('Owner')}:</label>
122 122 </div>
123 123 <div class="input input-medium ac">
124 124 <div class="perm_ac">
125 125 ${h.text('user',class_='yui-ac-input')}
126 126 <span class="help-block">${_('Change owner of this repository.')}</span>
127 127 <div id="owner_container"></div>
128 128 </div>
129 129 </div>
130 130 </div>
131 131 %if c.visual.repository_fields:
132 132 ## EXTRA FIELDS
133 133 %for field in c.repo_fields:
134 134 <div class="field">
135 135 <div class="label">
136 136 <label for="${field.field_key_prefixed}">${field.field_label} (${field.field_key}):</label>
137 137 </div>
138 138 <div class="input input-medium">
139 139 ${h.text(field.field_key_prefixed, field.field_value, class_='medium')}
140 140 %if field.field_desc:
141 141 <span class="help-block">${field.field_desc}</span>
142 142 %endif
143 143 </div>
144 144 </div>
145 145 %endfor
146 146 %endif
147 147 <div class="field">
148 148 <div class="label">
149 149 <label for="input">${_('Permissions')}:</label>
150 150 </div>
151 151 <div class="input">
152 152 <%include file="repo_edit_perms.html"/>
153 153 </div>
154 154
155 155 <div class="buttons">
156 156 ${h.submit('save',_('Save'),class_="ui-btn large")}
157 157 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
158 158 </div>
159 159 </div>
160 160 </div>
161 161 </div>
162 162 ${h.end_form()}
163 163 </div>
164 164
165 165 <div class="box box-right">
166 166 <div class="title">
167 167 <h5>${_('Administration')}</h5>
168 168 </div>
169 169
170 170 <h3>${_('Statistics')}</h3>
171 171 ${h.form(url('repo_stats', repo_name=c.repo_info.repo_name),method='delete')}
172 172 <div class="form">
173 173 <div class="fields">
174 174 ${h.submit('reset_stats_%s' % c.repo_info.repo_name,_('Reset current statistics'),class_="ui-btn",onclick="return confirm('"+_('Confirm to remove current statistics')+"');")}
175 175 <div class="field" style="border:none;color:#888">
176 176 <ul>
177 177 <li>${_('Fetched to rev')}: ${c.stats_revision}/${c.repo_last_rev}</li>
178 178 <li>${_('Stats gathered')}: ${c.stats_percentage}%</li>
179 179 </ul>
180 180 </div>
181 181 </div>
182 182 </div>
183 183 ${h.end_form()}
184 184
185 185 %if c.repo_info.clone_uri:
186 186 <h3>${_('Remote')}</h3>
187 187 ${h.form(url('repo_pull', repo_name=c.repo_info.repo_name),method='put')}
188 188 <div class="form">
189 189 <div class="fields">
190 190 ${h.submit('remote_pull_%s' % c.repo_info.repo_name,_('Pull changes from remote location'),class_="ui-btn",onclick="return confirm('"+_('Confirm to pull changes from remote side')+"');")}
191 191 <div class="field" style="border:none">
192 192 <ul>
193 193 <li><a href="${c.repo_info.clone_uri}">${c.repo_info.clone_uri}</a></li>
194 194 </ul>
195 195 </div>
196 196 </div>
197 197 </div>
198 198 ${h.end_form()}
199 199 %endif
200 200
201 201 <h3>${_('Cache')}</h3>
202 202 ${h.form(url('repo_cache', repo_name=c.repo_info.repo_name),method='delete')}
203 203 <div class="form">
204 204 <div class="fields">
205 205 ${h.submit('reset_cache_%s' % c.repo_info.repo_name,_('Invalidate repository cache'),class_="ui-btn",onclick="return confirm('"+_('Confirm to invalidate repository cache')+"');")}
206 206 <div class="field" style="border:none;color:#888">
207 207 <ul>
208 208 <li>${_('Manually invalidate cache for this repository. On first access repository will be cached again')}
209 209 </li>
210 210 </ul>
211 211 </div>
212 212 <div class="field" style="border:none;">
213 213 ${_('List of cached values')}
214 214 <table>
215 215 <tr>
216 216 <th>${_('Prefix')}</th>
217 217 <th>${_('Key')}</th>
218 218 <th>${_('Active')}</th>
219 219 </tr>
220 220 %for cache in c.repo_info.cache_keys:
221 221 <tr>
222 222 <td>${cache.prefix or '-'}</td>
223 223 <td>${cache.cache_key}</td>
224 224 <td>${h.bool2icon(cache.cache_active)}</td>
225 225 </tr>
226 226 %endfor
227 227 </table>
228 228 </div>
229 229 </div>
230 230 </div>
231 231 ${h.end_form()}
232 232
233 233 <h3>${_('Public journal')}</h3>
234 234 ${h.form(url('repo_public_journal', repo_name=c.repo_info.repo_name),method='put')}
235 235 <div class="form">
236 236 ${h.hidden('auth_token',str(h.get_token()))}
237 237 <div class="field">
238 238 %if c.in_public_journal:
239 239 ${h.submit('set_public_%s' % c.repo_info.repo_name,_('Remove from public journal'),class_="ui-btn")}
240 240 %else:
241 241 ${h.submit('set_public_%s' % c.repo_info.repo_name,_('Add to public journal'),class_="ui-btn")}
242 242 %endif
243 243 </div>
244 244 <div class="field" style="border:none;color:#888">
245 245 <ul>
246 246 <li>${_('All actions made on this repository will be accessible to everyone in public journal')}
247 247 </li>
248 248 </ul>
249 249 </div>
250 250 </div>
251 251 ${h.end_form()}
252 252
253 253 <h3>${_('Locking')}</h3>
254 254 ${h.form(url('repo_locking', repo_name=c.repo_info.repo_name),method='put')}
255 255 <div class="form">
256 256 <div class="fields">
257 257 %if c.repo_info.locked[0]:
258 258 ${h.submit('set_unlock' ,_('Unlock locked repo'),class_="ui-btn",onclick="return confirm('"+_('Confirm to unlock repository')+"');")}
259 259 ${'Locked by %s on %s' % (h.person_by_id(c.repo_info.locked[0]),h.fmt_date(h.time_to_datetime(c.repo_info.locked[1])))}
260 260 %else:
261 261 ${h.submit('set_lock',_('lock repo'),class_="ui-btn",onclick="return confirm('"+_('Confirm to lock repository')+"');")}
262 262 ${_('Repository is not locked')}
263 263 %endif
264 264 </div>
265 265 <div class="field" style="border:none;color:#888">
266 266 <ul>
267 267 <li>${_('Force locking on repository. Works only when anonymous access is disabled')}
268 268 </li>
269 269 </ul>
270 270 </div>
271 271 </div>
272 272 ${h.end_form()}
273 273
274 274 <h3>${_('Set as fork of')}</h3>
275 275 ${h.form(url('repo_as_fork', repo_name=c.repo_info.repo_name),method='put')}
276 276 <div class="form">
277 277 <div class="fields">
278 278 ${h.select('id_fork_of','',c.repos_list,class_="medium")}
279 279 ${h.submit('set_as_fork_%s' % c.repo_info.repo_name,_('set'),class_="ui-btn",)}
280 280 </div>
281 281 <div class="field" style="border:none;color:#888">
282 282 <ul>
283 283 <li>${_('''Manually set this repository as a fork of another from the list''')}</li>
284 284 </ul>
285 285 </div>
286 286 </div>
287 287 ${h.end_form()}
288 288
289 289 <h3>${_('Delete')}</h3>
290 290 ${h.form(url('repo', repo_name=c.repo_info.repo_name),method='delete')}
291 291 <div class="form">
292 292 <div class="fields">
293 293 ${h.submit('remove_%s' % c.repo_info.repo_name,_('Remove this repository'),class_="ui-btn red",onclick="return confirm('"+_('Confirm to delete this repository')+"');")}
294 294 %if c.repo_info.forks.count():
295 - ${ungettext('this repository has %s fork', 'this repository has %s forks', c.repo_info.forks.count()) % c.repo_info.forks.count()}
295 - ${ungettext('this repository has %s fork', 'this repository has %s forks', c.repo_info.forks.count()) % c.repo_info.forks.count()}
296 296 <input type="radio" name="forks" value="detach_forks" checked="checked"/> <label for="forks">${_('Detach forks')}</label>
297 297 <input type="radio" name="forks" value="delete_forks" /> <label for="forks">${_('Delete forks')}</label>
298 %endif
298 %endif
299 299 </div>
300 300 <div class="field" style="border:none;color:#888">
301 301 <ul>
302 302 <li>${_('This repository will be renamed in a special way in order to be unaccesible for RhodeCode and VCS systems. If you need to fully delete it from file system please do it manually')}</li>
303 303 </ul>
304 304 </div>
305 305 </div>
306 306 ${h.end_form()}
307 307 </div>
308 308
309 309 ##TODO: this should be controlled by the VISUAL setting
310 310 %if c.visual.repository_fields:
311 311 <div class="box box-left" style="clear:left">
312 312 <!-- box / title -->
313 313 <div class="title">
314 314 <h5>${_('Extra fields')}</h5>
315 315 </div>
316 316
317 317 <div class="emails_wrap">
318 318 <table class="noborder">
319 319 %for field in c.repo_fields:
320 320 <tr>
321 321 <td>${field.field_label} (${field.field_key})</td>
322 322 <td>${field.field_type}</td>
323 323 <td>
324 324 ${h.form(url('delete_repo_fields', repo_name=c.repo_info.repo_name, field_id=field.repo_field_id),method='delete')}
325 325 ${h.submit('remove_%s' % field.repo_field_id, _('delete'), id="remove_field_%s" % field.repo_field_id,
326 326 class_="delete_icon action_button", onclick="return confirm('"+_('Confirm to delete this field: %s') % field.field_key+"');")}
327 327 ${h.end_form()}
328 328 </td>
329 329 </tr>
330 330 %endfor
331 331 </table>
332 332 </div>
333 333
334 334 ${h.form(url('create_repo_fields', repo_name=c.repo_info.repo_name),method='put')}
335 335 <div class="form">
336 336 <!-- fields -->
337 337 <div class="fields">
338 338 <div class="field">
339 339 <div class="label">
340 340 <label for="new_field_key">${_('New field key')}:</label>
341 341 </div>
342 342 <div class="input">
343 343 ${h.text('new_field_key', class_='small')}
344 344 </div>
345 345 </div>
346 346 <div class="field">
347 347 <div class="label">
348 348 <label for="new_field_label">${_('New field label')}:</label>
349 349 </div>
350 350 <div class="input">
351 351 ${h.text('new_field_label', class_='small', placeholder=_('Enter short label'))}
352 352 </div>
353 353 </div>
354 354
355 355 <div class="field">
356 356 <div class="label">
357 357 <label for="new_field_desc">${_('New field description')}:</label>
358 358 </div>
359 359 <div class="input">
360 360 ${h.text('new_field_desc', class_='small', placeholder=_('Enter description of a field'))}
361 361 </div>
362 362 </div>
363 363
364 364 <div class="buttons">
365 365 ${h.submit('save',_('Add'),class_="ui-btn large")}
366 366 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
367 367 </div>
368 368 </div>
369 369 </div>
370 370 ${h.end_form()}
371 371 </div>
372 372 %endif
373 373 </%def>
@@ -1,49 +1,49
1 1
2 2 <div class="pullrequests_section_head">${_('Opened by me')}</div>
3 3 <ul>
4 4 %if c.my_pull_requests:
5 5 %for pull_request in c.my_pull_requests:
6 6 <li>
7 7 <div style="height: 12px">
8 8 <div style="float:left">
9 9 %if pull_request.is_closed():
10 10 <img src="${h.url('/images/icons/lock_go.png')}" title="${_('Closed')}"/>
11 11 %endif
12 <img src="${h.url('/images/icons/flag_status_%s.png' % str(pull_request.last_review_status))}" />
12 <img src="${h.url('/images/icons/flag_status_%s.png' % str(pull_request.last_review_status))}" />
13 13 <a href="${h.url('pullrequest_show',repo_name=pull_request.other_repo.repo_name,pull_request_id=pull_request.pull_request_id)}">
14 14 ${_('Pull request #%s opened on %s') % (pull_request.pull_request_id, h.fmt_date(pull_request.created_on))}
15 15 </a>
16 16 </div>
17 17 <div style="float:left;margin-top: -5px">
18 18 ${h.form(url('pullrequest_delete', repo_name=pull_request.other_repo.repo_name, pull_request_id=pull_request.pull_request_id),method='delete')}
19 19 ${h.submit('remove_%s' % pull_request.pull_request_id,'',class_="delete_icon action_button",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
20 20 ${h.end_form()}
21 21 </div>
22 22 </div>
23 23 </li>
24 24 %endfor
25 25 %else:
26 26 <li><span class="empty_data">${_('Nothing here yet')}</span></li>
27 27 %endif
28 28 </ul>
29 29
30 30 <div class="pullrequests_section_head" style="clear:both">${_('I participate in')}</div>
31 31 <ul>
32 32 %if c.participate_in_pull_requests:
33 33 %for pull_request in c.participate_in_pull_requests:
34 34 <li>
35 35 <div style="height: 12px">
36 36 %if pull_request.is_closed():
37 37 <img src="${h.url('/images/icons/lock_go.png')}" title="${_('Closed')}"/>
38 38 %endif
39 <img src="${h.url('/images/icons/flag_status_%s.png' % str(pull_request.last_review_status))}" />
39 <img src="${h.url('/images/icons/flag_status_%s.png' % str(pull_request.last_review_status))}" />
40 40 <a href="${h.url('pullrequest_show',repo_name=pull_request.other_repo.repo_name,pull_request_id=pull_request.pull_request_id)}">
41 41 ${_('Pull request #%s opened by %s on %s') % (pull_request.pull_request_id, pull_request.author.full_name, h.fmt_date(pull_request.created_on))}
42 42 </a>
43 43 </div>
44 44 </li>
45 45 %endfor
46 46 %else:
47 47 <li><span class="empty_data">${_('Nothing here yet')}</span></li>
48 48 %endif
49 49 </ul>
@@ -1,390 +1,390
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.html"/>
3 3
4 4 <!-- HEADER -->
5 5 <div id="header-dd"></div>
6 6 <div id="header">
7 7 <div id="header-inner" class="title">
8 8 <div id="logo">
9 9 <h1><a href="${h.url('home')}">${c.rhodecode_name}</a></h1>
10 10 </div>
11 11 <!-- MENU -->
12 12 ${self.page_nav()}
13 13 <!-- END MENU -->
14 14 ${self.body()}
15 15 </div>
16 16 </div>
17 17 <!-- END HEADER -->
18 18
19 19 <!-- CONTENT -->
20 20 <div id="content">
21 21 <div class="flash_msg">
22 22 <% messages = h.flash.pop_messages() %>
23 23 % if messages:
24 24 <ul id="flash-messages">
25 25 % for message in messages:
26 26 <li class="${message.category}_msg">${message}</li>
27 27 % endfor
28 28 </ul>
29 29 % endif
30 30 </div>
31 31 <div id="main">
32 32 ${next.main()}
33 33 </div>
34 34 </div>
35 35 <!-- END CONTENT -->
36 36
37 37 <!-- FOOTER -->
38 38 <div id="footer">
39 39 <div id="footer-inner" class="title">
40 40 <div>
41 41 <p class="footer-link">
42 42 <a href="${h.url('bugtracker')}">${_('Submit a bug')}</a>
43 43 </p>
44 44 <p class="footer-link-right">
45 45 <a href="${h.url('rhodecode_official')}">RhodeCode${'-%s' % c.rhodecode_instanceid if c.rhodecode_instanceid else ''}</a>
46 46 ${c.rhodecode_version} &copy; 2010-${h.datetime.today().year} by Marcin Kuzminski
47 47 </p>
48 48 </div>
49 49 </div>
50 50 </div>
51 51 <!-- END FOOTER -->
52 52
53 53 ### MAKO DEFS ###
54 54 <%def name="page_nav()">
55 55 ${self.menu()}
56 56 </%def>
57 57
58 58 <%def name="breadcrumbs()">
59 59 <div class="breadcrumbs">
60 60 ${self.breadcrumbs_links()}
61 61 </div>
62 62 </%def>
63 63
64 64 <%def name="usermenu()">
65 65 ## USER MENU
66 66 <li>
67 67 <a class="menu_link" id="quick_login_link">
68 68 <span class="icon" style="padding:5px 5px 0px 5px">
69 69 <img src="${h.gravatar_url(c.rhodecode_user.email,20)}" alt="avatar">
70 70 </span>
71 71 %if c.rhodecode_user.username != 'default':
72 72 <span class="menu_link_user">${c.rhodecode_user.username}</span>
73 73 %if c.unread_notifications != 0:
74 74 <span class="menu_link_notifications">${c.unread_notifications}</span>
75 75 %endif
76 76 %else:
77 77 <span>${_('Not logged in')}</span>
78 78 %endif
79 79 </a>
80 80
81 81 <div class="user-menu">
82 82 <div id="quick_login">
83 83 %if c.rhodecode_user.username == 'default':
84 84 <h4>${_('Login to your account')}</h4>
85 85 ${h.form(h.url('login_home',came_from=h.url.current()))}
86 86 <div class="form">
87 87 <div class="fields">
88 88 <div class="field">
89 89 <div class="label">
90 90 <label for="username">${_('Username')}:</label>
91 91 </div>
92 92 <div class="input">
93 93 ${h.text('username',class_='focus',size=40)}
94 94 </div>
95 95
96 96 </div>
97 97 <div class="field">
98 98 <div class="label">
99 99 <label for="password">${_('Password')}:</label>
100 100 </div>
101 101 <div class="input">
102 102 ${h.password('password',class_='focus',size=40)}
103 103 </div>
104 104
105 105 </div>
106 106 <div class="buttons">
107 107 <div class="password_forgoten">${h.link_to(_('Forgot password ?'),h.url('reset_password'))}</div>
108 108 <div class="register">
109 109 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
110 110 ${h.link_to(_("Don't have an account ?"),h.url('register'))}
111 111 %endif
112 112 </div>
113 113 <div class="submit">
114 114 ${h.submit('sign_in',_('Log In'),class_="ui-btn xsmall")}
115 115 </div>
116 116 </div>
117 117 </div>
118 118 </div>
119 119 ${h.end_form()}
120 120 %else:
121 121 <div class="links_left">
122 122 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
123 123 <div class="email">${c.rhodecode_user.email}</div>
124 124 <div class="big_gravatar"><img alt="gravatar" src="${h.gravatar_url(c.rhodecode_user.email,48)}" /></div>
125 125 <div class="notifications"><a href="${h.url('notifications')}">${_('Notifications')}</a></div>
126 126 <div class="unread"><a href="${h.url('notifications')}">${_('Unread')}: ${c.unread_notifications}</a></div>
127 127 </div>
128 128 <div class="links_right">
129 129 <ol class="links">
130 130 <li>${h.link_to(_(u'Home'),h.url('home'))}</li>
131 131 <li>${h.link_to(_(u'Journal'),h.url('journal'))}</li>
132 132 <li>${h.link_to(_(u'My account'),h.url('admin_settings_my_account'))}</li>
133 133 <li class="logout">${h.link_to(_(u'Log Out'),h.url('logout_home'))}</li>
134 134 </ol>
135 135 </div>
136 136 %endif
137 137 </div>
138 138 </div>
139 139
140 140 </li>
141 141 </%def>
142 142
143 143 <%def name="menu(current=None)">
144 144 <%
145 145 def is_current(selected):
146 146 if selected == current:
147 147 return h.literal('class="current"')
148 148 %>
149 149 <ul id="quick">
150 150 <!-- repo switcher -->
151 151 <li ${is_current('home')}>
152 152 <a class="menu_link" id="repo_switcher" title="${_('Switch repository')}" href="${h.url('home')}">
153 153 <span class="icon">
154 154 <img src="${h.url('/images/icons/database.png')}" alt="${_('Products')}" />
155 155 </span>
156 156 <span>${_('Repositories')}</span>
157 157 </a>
158 158 <ul id="repo_switcher_list" class="repo_switcher">
159 159 <li>
160 160 <a href="#">${_('loading...')}</a>
161 161 </li>
162 162 </ul>
163 163 </li>
164 164 ## we render this menu only not for those pages
165 165 %if current not in ['home','admin', 'search', 'journal']:
166 166 ##REGULAR MENU
167 167 <li ${is_current('summary')}>
168 168 <a class="menu_link" title="${_('Summary page')}" href="${h.url('summary_home',repo_name=c.repo_name)}">
169 169 <span class="icon">
170 170 <img src="${h.url('/images/icons/clipboard_16.png')}" alt="${_('Summary')}" />
171 171 </span>
172 172 <span>${_('Summary')}</span>
173 173 </a>
174 174 </li>
175 175 <li ${is_current('changelog')}>
176 176 <a class="menu_link" title="${_('Changeset list')}" href="${h.url('changelog_home',repo_name=c.repo_name)}">
177 177 <span class="icon">
178 178 <img src="${h.url('/images/icons/time.png')}" alt="${_('Changelog')}" />
179 179 </span>
180 180 <span>${_('Changelog')}</span>
181 181 </a>
182 182 </li>
183 183 <li ${is_current('switch_to')}>
184 184 <a class="menu_link" id="branch_tag_switcher" title="${_('Switch to')}" href="#">
185 185 <span class="icon">
186 186 <img src="${h.url('/images/icons/arrow_switch.png')}" alt="${_('Switch to')}" />
187 187 </span>
188 188 <span>${_('Switch to')}</span>
189 189 </a>
190 190 <ul id="switch_to_list" class="switch_to">
191 191 <li><a href="#">${_('loading...')}</a></li>
192 192 </ul>
193 193 </li>
194 194 <li ${is_current('files')}>
195 195 <a class="menu_link" title="${_('Show repository content')}" href="${h.url('files_home',repo_name=c.repo_name)}">
196 196 <span class="icon">
197 197 <img src="${h.url('/images/icons/file.png')}" alt="${_('Files')}" />
198 198 </span>
199 199 <span>${_('Files')}</span>
200 200 </a>
201 201 </li>
202 202 <li ${is_current('options')}>
203 203 <a class="menu_link" title="${_('Options')}" href="#">
204 204 <span class="icon">
205 205 <img src="${h.url('/images/icons/table_gear.png')}" alt="${_('Admin')}" />
206 206 </span>
207 207 <span>${_('Options')}</span>
208 208 </a>
209 209 <ul>
210 210 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
211 211 %if h.HasPermissionAll('hg.admin')('access settings on repository'):
212 212 <li>${h.link_to(_('repository settings'),h.url('edit_repo',repo_name=c.repo_name),class_='settings')}</li>
213 213 %else:
214 214 <li>${h.link_to(_('repository settings'),h.url('repo_settings_home',repo_name=c.repo_name),class_='settings')}</li>
215 215 %endif
216 216 %endif
217 217
218 218 <li>${h.link_to(_('fork'),h.url('repo_fork_home',repo_name=c.repo_name),class_='fork')}</li>
219 219 %if h.is_hg(c.rhodecode_repo):
220 220 <li>${h.link_to(_('open new pull request'),h.url('pullrequest_home',repo_name=c.repo_name),class_='pull_request')}</li>
221 221 %endif
222 222 %if c.rhodecode_db_repo.fork:
223 223 <li>${h.link_to(_('compare fork'),h.url('compare_url',repo_name=c.rhodecode_db_repo.fork.repo_name,org_ref_type='branch',org_ref='default',other_repo=c.repo_name,other_ref_type='branch',other_ref=request.GET.get('branch') or 'default'),class_='compare_request')}</li>
224 224 %endif
225 225 <li>${h.link_to(_('lightweight changelog'),h.url('shortlog_home',repo_name=c.repo_name),class_='shortlog')}</li>
226 226 <li>${h.link_to(_('search'),h.url('search_repo',repo_name=c.repo_name),class_='search')}</li>
227 227
228 228 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
229 229 %if c.rhodecode_db_repo.locked[0]:
230 230 <li>${h.link_to(_('unlock'), h.url('toggle_locking',repo_name=c.repo_name),class_='locking_del')}</li>
231 231 %else:
232 232 <li>${h.link_to(_('lock'), h.url('toggle_locking',repo_name=c.repo_name),class_='locking_add')}</li>
233 233 %endif
234 234 %endif
235 235
236 236 % if h.HasPermissionAll('hg.admin')('access admin main page'):
237 237 <li>
238 238 ${h.link_to(_('admin'),h.url('admin_home'),class_='admin')}
239 239 <%def name="admin_menu()">
240 240 <ul>
241 241 <li>${h.link_to(_('admin journal'),h.url('admin_home'),class_='journal')}</li>
242 242 <li>${h.link_to(_('repositories'),h.url('repos'),class_='repos')}</li>
243 243 <li>${h.link_to(_('repositories groups'),h.url('repos_groups'),class_='repos_groups')}</li>
244 244 <li>${h.link_to(_('users'),h.url('users'),class_='users')}</li>
245 245 <li>${h.link_to(_('users groups'),h.url('users_groups'),class_='groups')}</li>
246 246 <li>${h.link_to(_('permissions'),h.url('edit_permission',id='default'),class_='permissions')}</li>
247 247 <li>${h.link_to(_('ldap'),h.url('ldap_home'),class_='ldap')}</li>
248 248 <li>${h.link_to(_('defaults'),h.url('defaults'),class_='defaults')}</li>
249 249 <li class="last">${h.link_to(_('settings'),h.url('admin_settings'),class_='settings')}</li>
250 250 </ul>
251 251 </%def>
252 252 ## ADMIN MENU
253 253 ${admin_menu()}
254 254 </li>
255 255 ## if you're a admin of any groups, show admin menu for it
256 256 % elif c.rhodecode_user.groups_admin:
257 257 <li>
258 258 ${h.link_to(_('admin'),h.url('admin_home'),class_='admin')}
259 259 <%def name="admin_menu_simple()">
260 260 <ul>
261 261 <li>${h.link_to(_('repositories groups'),h.url('repos_groups'),class_='repos_groups')}</li>
262 262 </ul>
263 263 </%def>
264 264 ## ADMIN MENU
265 265 ${admin_menu_simple()}
266 266 </li>
267 267 % endif
268 268 </ul>
269 269 </li>
270 270 <li>
271 271 <a class="menu_link" title="${_('Followers')}" href="${h.url('repo_followers_home',repo_name=c.repo_name)}">
272 272 <span class="icon_short">
273 273 <img src="${h.url('/images/icons/heart.png')}" alt="${_('Followers')}" />
274 274 </span>
275 275 <span id="current_followers_count" class="short">${c.repository_followers}</span>
276 276 </a>
277 277 </li>
278 278 <li>
279 279 <a class="menu_link" title="${_('Forks')}" href="${h.url('repo_forks_home',repo_name=c.repo_name)}">
280 280 <span class="icon_short">
281 281 <img src="${h.url('/images/icons/arrow_divide.png')}" alt="${_('Forks')}" />
282 282 </span>
283 283 <span class="short">${c.repository_forks}</span>
284 284 </a>
285 285 </li>
286 286 <li>
287 287 <a class="menu_link" title="${_('Pull requests')}" href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}">
288 288 <span class="icon_short">
289 289 <img src="${h.url('/images/icons/arrow_join.png')}" alt="${_('Pull requests')}" />
290 290 </span>
291 291 <span class="short">${c.repository_pull_requests}</span>
292 292 </a>
293 293 </li>
294 294 ${usermenu()}
295 295 <script type="text/javascript">
296 296 YUE.on('branch_tag_switcher','mouseover',function(){
297 297 var loaded = YUD.hasClass('branch_tag_switcher','loaded');
298 298 if(!loaded){
299 299 YUD.addClass('branch_tag_switcher','loaded');
300 300 ypjax("${h.url('branch_tag_switcher',repo_name=c.repo_name)}",'switch_to_list',
301 301 function(o){},
302 302 function(o){YUD.removeClass('branch_tag_switcher','loaded');}
303 303 ,null);
304 304 }
305 305 return false;
306 306 });
307 307 </script>
308 308 %else:
309 309 ##ROOT MENU
310 310 %if c.rhodecode_user.username != 'default':
311 311 <li ${is_current('journal')}>
312 312 <a class="menu_link" title="${_('Show recent activity')}" href="${h.url('journal')}">
313 313 <span class="icon">
314 314 <img src="${h.url('/images/icons/book.png')}" alt="${_('Journal')}" />
315 315 </span>
316 316 <span>${_('Journal')}</span>
317 317 </a>
318 318 </li>
319 319 %else:
320 320 <li ${is_current('journal')}>
321 321 <a class="menu_link" title="${_('Public journal')}" href="${h.url('public_journal')}">
322 322 <span class="icon">
323 323 <img src="${h.url('/images/icons/book.png')}" alt="${_('Public journal')}" />
324 324 </span>
325 325 <span>${_('Public journal')}</span>
326 326 </a>
327 327 </li>
328 328 %endif
329 329 <li ${is_current('search')}>
330 330 <a class="menu_link" title="${_('Search in repositories')}" href="${h.url('search')}">
331 331 <span class="icon">
332 332 <img src="${h.url('/images/icons/search_16.png')}" alt="${_('Search')}" />
333 333 </span>
334 334 <span>${_('Search')}</span>
335 335 </a>
336 336 </li>
337 337 % if h.HasPermissionAll('hg.admin')('access admin main page'):
338 338 <li ${is_current('admin')}>
339 339 <a class="menu_link" title="${_('Admin')}" href="${h.url('admin_home')}">
340 340 <span class="icon">
341 341 <img src="${h.url('/images/icons/cog_edit.png')}" alt="${_('Admin')}" />
342 342 </span>
343 343 <span>${_('Admin')}</span>
344 344 </a>
345 345 ${admin_menu()}
346 346 </li>
347 347 % elif c.rhodecode_user.groups_admin:
348 348 <li ${is_current('admin')}>
349 349 <a class="menu_link" title="${_('Admin')}" href="${h.url('admin_home')}">
350 350 <span class="icon">
351 351 <img src="${h.url('/images/icons/cog_edit.png')}" alt="${_('Admin')}" />
352 352 </span>
353 353 <span>${_('Admin')}</span>
354 354 </a>
355 355 ${admin_menu_simple()}
356 </li>
356 </li>
357 357 % endif
358 358 ${usermenu()}
359 359 %endif
360 360 <script type="text/javascript">
361 361 YUE.on('repo_switcher','mouseover',function(){
362 362 var target = 'q_filter_rs';
363 363 var qfilter_activate = function(){
364 364 var nodes = YUQ('ul#repo_switcher_list li a.repo_name');
365 365 var func = function(node){
366 366 return node.parentNode;
367 367 }
368 368 q_filter(target,nodes,func);
369 369 }
370 370
371 371 var loaded = YUD.hasClass('repo_switcher','loaded');
372 372 if(!loaded){
373 373 YUD.addClass('repo_switcher','loaded');
374 374 ypjax("${h.url('repo_switcher')}",'repo_switcher_list',
375 375 function(o){qfilter_activate();YUD.get(target).focus()},
376 376 function(o){YUD.removeClass('repo_switcher','loaded');}
377 377 ,null);
378 378 }else{
379 379 YUD.get(target).focus();
380 380 }
381 381 return false;
382 382 });
383 383
384 384 YUE.on('header-dd', 'click',function(e){
385 385 YUD.addClass('header-inner', 'hover');
386 386 YUD.addClass('content', 'hover');
387 387 });
388 388
389 389 </script>
390 390 </%def>
@@ -1,111 +1,111
1 1 ## -*- coding: utf-8 -*-
2 2 <!DOCTYPE html>
3 3 <html xmlns="http://www.w3.org/1999/xhtml">
4 4 <head>
5 5 <title>${self.title()}</title>
6 6 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
7 7 <meta name="robots" content="index, nofollow"/>
8 8 <link rel="icon" href="${h.url('/images/icons/database_gear.png')}" type="image/png" />
9 9
10 10 ## CSS ###
11 11 <%def name="css()">
12 12 <link rel="stylesheet" type="text/css" href="${h.url('/css/style.css', ver=c.rhodecode_version)}" media="screen"/>
13 13 <link rel="stylesheet" type="text/css" href="${h.url('/css/pygments.css', ver=c.rhodecode_version)}"/>
14 14 ## EXTRA FOR CSS
15 15 ${self.css_extra()}
16 16 </%def>
17 17 <%def name="css_extra()">
18 18 </%def>
19 19
20 20 ${self.css()}
21 21
22 22 %if c.ga_code:
23 23 <!-- Analytics -->
24 24 <script type="text/javascript">
25 25 var _gaq = _gaq || [];
26 26 _gaq.push(['_setAccount', '${c.ga_code}']);
27 27 _gaq.push(['_trackPageview']);
28 28
29 29 (function() {
30 30 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
31 31 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
32 32 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
33 33 })();
34 34 </script>
35 35 %endif
36 36
37 37 ## JAVASCRIPT ##
38 38 <%def name="js()">
39 39 <script type="text/javascript">
40 40 //JS translations map
41 41 var TRANSLATION_MAP = {
42 42 'add another comment':'${_("add another comment")}',
43 43 'Stop following this repository':"${_('Stop following this repository')}",
44 44 'Start following this repository':"${_('Start following this repository')}",
45 45 'Group':"${_('Group')}",
46 46 'members':"${_('members')}",
47 47 'loading...':"${_('loading...')}",
48 48 'search truncated': "${_('search truncated')}",
49 49 'no matching files': "${_('no matching files')}",
50 50 'Open new pull request': "${_('Open new pull request')}",
51 51 'Open new pull request for selected changesets': "${_('Open new pull request for selected changesets')}",
52 52 'Show selected changes __S -> __E': "${_('Show selected changes __S -> __E')}",
53 53 'Selection link': "${_('Selection link')}",
54 54 };
55 55 var _TM = TRANSLATION_MAP;
56 56
57 57 var TOGGLE_FOLLOW_URL = "${h.url('toggle_following')}";
58 58
59 59 </script>
60 60 <script type="text/javascript" src="${h.url('/js/yui.2.9.js', ver=c.rhodecode_version)}"></script>
61 61 <!--[if lt IE 9]>
62 62 <script language="javascript" type="text/javascript" src="${h.url('/js/excanvas.min.js')}"></script>
63 63 <![endif]-->
64 64 <script type="text/javascript" src="${h.url('/js/yui.flot.js', ver=c.rhodecode_version)}"></script>
65 65 <script type="text/javascript" src="${h.url('/js/native.history.js', ver=c.rhodecode_version)}"></script>
66 66 <script type="text/javascript" src="${h.url('/js/pyroutes_map.js', ver=c.rhodecode_version)}"></script>
67 67 <script type="text/javascript" src="${h.url('/js/rhodecode.js', ver=c.rhodecode_version)}"></script>
68 68 ## EXTRA FOR JS
69 69 ${self.js_extra()}
70 70 <script type="text/javascript">
71 71 (function(window,undefined){
72 72 // Prepare
73 73 var History = window.History; // Note: We are using a capital H instead of a lower h
74 74 if ( !History.enabled ) {
75 75 // History.js is disabled for this browser.
76 76 // This is because we can optionally choose to support HTML4 browsers or not.
77 77 return false;
78 78 }
79 79 })(window);
80 80
81 81 YUE.onDOMReady(function(){
82 82 tooltip_activate();
83 83 show_more_event();
84 84 show_changeset_tooltip();
85 85 // routes registration
86 86 pyroutes.register('toggle_following', "${h.url('toggle_following')}");
87 87 pyroutes.register('changeset_info', "${h.url('changeset_info', repo_name='%(repo_name)s', revision='%(revision)s')}", ['repo_name', 'revision']);
88 pyroutes.register('repo_size', "${h.url('repo_size', repo_name='%(repo_name)s')}", ['repo_name']);
88 pyroutes.register('repo_size', "${h.url('repo_size', repo_name='%(repo_name)s')}", ['repo_name']);
89 89 })
90 90 </script>
91 91 </%def>
92 92 <%def name="js_extra()"></%def>
93 93 ${self.js()}
94 94 <%def name="head_extra()"></%def>
95 95 ${self.head_extra()}
96 96 </head>
97 97 <body id="body">
98 98 ## IE hacks
99 99 <!--[if IE 7]>
100 100 <script>YUD.addClass(document.body,'ie7')</script>
101 101 <![endif]-->
102 102 <!--[if IE 8]>
103 103 <script>YUD.addClass(document.body,'ie8')</script>
104 104 <![endif]-->
105 105 <!--[if IE 9]>
106 106 <script>YUD.addClass(document.body,'ie9')</script>
107 107 <![endif]-->
108 108
109 109 ${next.body()}
110 110 </body>
111 111 </html>
@@ -1,340 +1,340
1 1 <%page args="parent" />
2 2 <div class="box">
3 3 <!-- box / title -->
4 4 <div class="title">
5 5 <h5>
6 6 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" value="${_('quick filter...')}"/> ${parent.breadcrumbs()} <span id="repo_count">0</span> ${_('repositories')}
7 7 </h5>
8 8 %if c.rhodecode_user.username != 'default':
9 9 <ul class="links">
10 10 %if h.HasPermissionAny('hg.admin','hg.create.repository')() or h.HasReposGroupPermissionAny('group.write', 'group.admin')(c.group.group_name if c.group else None):
11 11 <li>
12 12 %if c.group:
13 13 <span>${h.link_to(_('Add repository'),h.url('admin_settings_create_repository',parent_group=c.group.group_id))}</span>
14 14 %if h.HasPermissionAny('hg.admin')() or h.HasReposGroupPermissionAny('group.admin')(c.group.group_name):
15 15 <span>${h.link_to(_(u'Add group'),h.url('new_repos_group', parent_group=c.group.group_id))}</span>
16 %endif
16 %endif
17 17 %else:
18 18 <span>${h.link_to(_('Add repository'),h.url('admin_settings_create_repository'))}</span>
19 19 %if h.HasPermissionAny('hg.admin')():
20 20 <span>${h.link_to(_(u'Add group'),h.url('new_repos_group'))}</span>
21 21 %endif
22 22 %endif
23 23 </li>
24 24 %endif
25 25 %if c.group and h.HasReposGroupPermissionAny('group.admin')(c.group.group_name):
26 26 <li>
27 27 <span>${h.link_to(_('Edit group'),h.url('edit_repos_group',group_name=c.group.group_name), title=_('You have admin right to this group, and can edit it'))}</span>
28 28 </li>
29 29 %endif
30 30 </ul>
31 31 %endif
32 32 </div>
33 33 <!-- end box / title -->
34 34 <div class="table">
35 35 % if c.groups:
36 36 <div id='groups_list_wrap' class="yui-skin-sam">
37 37 <table id="groups_list">
38 38 <thead>
39 39 <tr>
40 40 <th class="left"><a href="#">${_('Group name')}</a></th>
41 41 <th class="left"><a href="#">${_('Description')}</a></th>
42 42 ##<th class="left"><a href="#">${_('Number of repositories')}</a></th>
43 43 </tr>
44 44 </thead>
45 45
46 46 ## REPO GROUPS
47 47 % for gr in c.groups:
48 48 <tr>
49 49 <td>
50 50 <div style="white-space: nowrap">
51 51 <img class="icon" alt="${_('Repositories group')}" src="${h.url('/images/icons/database_link.png')}"/>
52 52 ${h.link_to(gr.name,url('repos_group_home',group_name=gr.group_name))}
53 53 </div>
54 54 </td>
55 55 %if c.visual.stylify_metatags:
56 56 <td>${h.urlify_text(h.desc_stylize(gr.group_description))}</td>
57 57 %else:
58 58 <td>${gr.group_description}</td>
59 59 %endif
60 60 ## this is commented out since for multi nested repos can be HEAVY!
61 61 ## in number of executed queries during traversing uncomment at will
62 62 ##<td><b>${gr.repositories_recursive_count}</b></td>
63 63 </tr>
64 64 % endfor
65 65 </table>
66 66 </div>
67 67 <div id="group-user-paginator" style="padding: 0px 0px 0px 0px"></div>
68 68 <div style="height: 20px"></div>
69 69 % endif
70 70 <div id="welcome" style="display:none;text-align:center">
71 71 <h1><a href="${h.url('home')}">${c.rhodecode_name} ${c.rhodecode_version}</a></h1>
72 72 </div>
73 73 <%cnt=0%>
74 74 <%namespace name="dt" file="/data_table/_dt_elements.html"/>
75 75 % if c.visual.lightweight_dashboard is False:
76 76 ## old full detailed version
77 77 <div id='repos_list_wrap' class="yui-skin-sam">
78 78 <table id="repos_list">
79 79 <thead>
80 80 <tr>
81 81 <th class="left"></th>
82 82 <th class="left">${_('Name')}</th>
83 83 <th class="left">${_('Description')}</th>
84 84 <th class="left">${_('Last change')}</th>
85 85 <th class="left">${_('Tip')}</th>
86 86 <th class="left">${_('Owner')}</th>
87 87 <th class="left">${_('Atom')}</th>
88 88 </tr>
89 89 </thead>
90 90 <tbody>
91 91 %for cnt,repo in enumerate(c.repos_list):
92 92 <tr class="parity${(cnt+1)%2}">
93 93 ##QUICK MENU
94 94 <td class="quick_repo_menu">
95 95 ${dt.quick_menu(repo['name'])}
96 96 </td>
97 97 ##REPO NAME AND ICONS
98 98 <td class="reponame">
99 99 ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],h.AttributeDict(repo['dbrepo_fork']),pageargs.get('short_repo_names'))}
100 100 </td>
101 101 ##DESCRIPTION
102 102 <td><span class="tooltip" title="${h.tooltip(repo['description'])}">
103 103 %if c.visual.stylify_metatags:
104 104 ${h.urlify_text(h.desc_stylize(h.truncate(repo['description'],60)))}</span>
105 105 %else:
106 106 ${h.truncate(repo['description'],60)}</span>
107 107 %endif
108 108 </td>
109 109 ##LAST CHANGE DATE
110 110 <td>
111 111 ${dt.last_change(repo['last_change'])}
112 112 </td>
113 113 ##LAST REVISION
114 114 <td>
115 115 ${dt.revision(repo['name'],repo['rev'],repo['tip'],repo['author'],repo['last_msg'])}
116 116 </td>
117 117 ##
118 118 <td title="${repo['contact']}">${h.person(repo['contact'])}</td>
119 119 <td>
120 120 ${dt.atom(repo['name'])}
121 121 </td>
122 122 </tr>
123 123 %endfor
124 124 </tbody>
125 125 </table>
126 126 </div>
127 127 % else:
128 128 ## lightweight version
129 129 <div class="yui-skin-sam" id="repos_list_wrap"></div>
130 130 <div id="user-paginator" style="padding: 0px 0px 0px 0px"></div>
131 131 % endif
132 132 </div>
133 133 </div>
134 134 % if c.visual.lightweight_dashboard is False:
135 135 <script>
136 136 YUD.get('repo_count').innerHTML = ${cnt+1 if cnt else 0};
137 137
138 138 // groups table sorting
139 139 var myColumnDefs = [
140 140 {key:"name",label:"${_('Group name')}",sortable:true,
141 141 sortOptions: { sortFunction: groupNameSort }},
142 142 {key:"desc",label:"${_('Description')}",sortable:true},
143 143 ];
144 144
145 145 var myDataSource = new YAHOO.util.DataSource(YUD.get("groups_list"));
146 146
147 147 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
148 148 myDataSource.responseSchema = {
149 149 fields: [
150 150 {key:"name"},
151 151 {key:"desc"},
152 152 ]
153 153 };
154 154
155 155 var myDataTable = new YAHOO.widget.DataTable("groups_list_wrap", myColumnDefs, myDataSource,{
156 156 sortedBy:{key:"name",dir:"asc"},
157 157 paginator: new YAHOO.widget.Paginator({
158 158 rowsPerPage: 50,
159 159 alwaysVisible: false,
160 160 template : "{PreviousPageLink} {FirstPageLink} {PageLinks} {LastPageLink} {NextPageLink}",
161 161 pageLinks: 5,
162 162 containerClass: 'pagination-wh',
163 163 currentPageClass: 'pager_curpage',
164 164 pageLinkClass: 'pager_link',
165 165 nextPageLinkLabel: '&gt;',
166 166 previousPageLinkLabel: '&lt;',
167 167 firstPageLinkLabel: '&lt;&lt;',
168 168 lastPageLinkLabel: '&gt;&gt;',
169 169 containers:['group-user-paginator']
170 170 }),
171 171 MSG_SORTASC:"${_('Click to sort ascending')}",
172 172 MSG_SORTDESC:"${_('Click to sort descending')}"
173 173 });
174 174
175 175 // main table sorting
176 176 var myColumnDefs = [
177 177 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
178 178 {key:"name",label:"${_('Name')}",sortable:true,
179 179 sortOptions: { sortFunction: nameSort }},
180 180 {key:"desc",label:"${_('Description')}",sortable:true},
181 181 {key:"last_change",label:"${_('Last Change')}",sortable:true,
182 182 sortOptions: { sortFunction: ageSort }},
183 183 {key:"tip",label:"${_('Tip')}",sortable:true,
184 184 sortOptions: { sortFunction: revisionSort }},
185 185 {key:"owner",label:"${_('Owner')}",sortable:true},
186 186 {key:"atom",label:"",sortable:false},
187 187 ];
188 188
189 189 var myDataSource = new YAHOO.util.DataSource(YUD.get("repos_list"));
190 190
191 191 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
192 192
193 193 myDataSource.responseSchema = {
194 194 fields: [
195 195 {key:"menu"},
196 196 //{key:"raw_name"},
197 197 {key:"name"},
198 198 {key:"desc"},
199 199 {key:"last_change"},
200 200 {key:"tip"},
201 201 {key:"owner"},
202 202 {key:"atom"},
203 203 ]
204 204 };
205 205
206 206 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,
207 207 {
208 208 sortedBy:{key:"name",dir:"asc"},
209 209 MSG_SORTASC:"${_('Click to sort ascending')}",
210 210 MSG_SORTDESC:"${_('Click to sort descending')}",
211 211 MSG_EMPTY:"${_('No records found.')}",
212 212 MSG_ERROR:"${_('Data error.')}",
213 213 MSG_LOADING:"${_('Loading...')}",
214 214 }
215 215 );
216 216 myDataTable.subscribe('postRenderEvent',function(oArgs) {
217 217 tooltip_activate();
218 218 quick_repo_menu();
219 219 var func = function(node){
220 220 return node.parentNode.parentNode.parentNode.parentNode;
221 221 }
222 222 q_filter('q_filter',YUQ('div.table tr td a.repo_name'),func);
223 223 });
224 224
225 225 </script>
226 226 % else:
227 227 <script>
228 228 var data = ${c.data|n};
229 229 var myDataSource = new YAHOO.util.DataSource(data);
230 230 myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
231 231
232 232 myDataSource.responseSchema = {
233 233 resultsList: "records",
234 234 fields: [
235 235 {key:"menu"},
236 236 {key:"raw_name"},
237 237 {key:"name"},
238 238 {key:"desc"},
239 239 {key:"last_change"},
240 240 {key:"last_changeset"},
241 241 {key:"owner"},
242 242 {key:"atom"},
243 243 ]
244 244 };
245 245 myDataSource.doBeforeCallback = function(req,raw,res,cb) {
246 246 // This is the filter function
247 247 var data = res.results || [],
248 248 filtered = [],
249 249 i,l;
250 250
251 251 if (req) {
252 252 req = req.toLowerCase();
253 253 for (i = 0; i<data.length; i++) {
254 254 var pos = data[i].raw_name.toLowerCase().indexOf(req)
255 255 if (pos != -1) {
256 256 filtered.push(data[i]);
257 257 }
258 258 }
259 259 res.results = filtered;
260 260 }
261 261 YUD.get('repo_count').innerHTML = res.results.length;
262 262 return res;
263 263 }
264 264
265 265 // main table sorting
266 266 var myColumnDefs = [
267 267 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
268 268 {key:"name",label:"${_('Name')}",sortable:true,
269 269 sortOptions: { sortFunction: nameSort }},
270 270 {key:"desc",label:"${_('Description')}",sortable:true},
271 271 {key:"last_change",label:"${_('Last Change')}",sortable:true,
272 272 sortOptions: { sortFunction: ageSort }},
273 273 {key:"last_changeset",label:"${_('Tip')}",sortable:true,
274 274 sortOptions: { sortFunction: revisionSort }},
275 275 {key:"owner",label:"${_('Owner')}",sortable:true},
276 276 {key:"atom",label:"",sortable:false},
277 277 ];
278 278
279 279 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,{
280 280 sortedBy:{key:"name",dir:"asc"},
281 281 paginator: new YAHOO.widget.Paginator({
282 282 rowsPerPage: ${c.visual.lightweight_dashboard_items},
283 283 alwaysVisible: false,
284 284 template : "{PreviousPageLink} {FirstPageLink} {PageLinks} {LastPageLink} {NextPageLink}",
285 285 pageLinks: 5,
286 286 containerClass: 'pagination-wh',
287 287 currentPageClass: 'pager_curpage',
288 288 pageLinkClass: 'pager_link',
289 289 nextPageLinkLabel: '&gt;',
290 290 previousPageLinkLabel: '&lt;',
291 291 firstPageLinkLabel: '&lt;&lt;',
292 292 lastPageLinkLabel: '&gt;&gt;',
293 293 containers:['user-paginator']
294 294 }),
295 295
296 296 MSG_SORTASC:"${_('Click to sort ascending')}",
297 297 MSG_SORTDESC:"${_('Click to sort descending')}",
298 298 MSG_EMPTY:"${_('No records found.')}",
299 299 MSG_ERROR:"${_('Data error.')}",
300 300 MSG_LOADING:"${_('Loading...')}",
301 301 }
302 302 );
303 303 myDataTable.subscribe('postRenderEvent',function(oArgs) {
304 304 tooltip_activate();
305 305 quick_repo_menu();
306 306 });
307 307
308 308 var filterTimeout = null;
309 309
310 310 updateFilter = function () {
311 311 // Reset timeout
312 312 filterTimeout = null;
313 313
314 314 // Reset sort
315 315 var state = myDataTable.getState();
316 316 state.sortedBy = {key:'name', dir:YAHOO.widget.DataTable.CLASS_ASC};
317 317
318 318 // Get filtered data
319 319 myDataSource.sendRequest(YUD.get('q_filter').value,{
320 320 success : myDataTable.onDataReturnInitializeTable,
321 321 failure : myDataTable.onDataReturnInitializeTable,
322 322 scope : myDataTable,
323 323 argument: state
324 324 });
325 325
326 326 };
327 327 YUE.on('q_filter','click',function(){
328 328 if(!YUD.hasClass('q_filter', 'loaded')){
329 329 YUD.get('q_filter').value = '';
330 330 //TODO: load here full list later to do search within groups
331 331 YUD.addClass('q_filter', 'loaded');
332 332 }
333 333 });
334 334
335 335 YUE.on('q_filter','keyup',function (e) {
336 336 clearTimeout(filterTimeout);
337 337 filterTimeout = setTimeout(updateFilter,600);
338 338 });
339 339 </script>
340 340 % endif
@@ -1,243 +1,243
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('Pull request #%s') % c.pull_request.pull_request_id}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${h.link_to(_(u'Home'),h.url('/'))}
9 9 &raquo;
10 10 ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
11 11 &raquo;
12 12 ${_('Pull request #%s') % c.pull_request.pull_request_id}
13 13 </%def>
14 14
15 15 <%def name="main()">
16 16
17 17 <div class="box">
18 18 <!-- box / title -->
19 19 <div class="title">
20 20 ${self.breadcrumbs()}
21 21 </div>
22 22 %if c.pull_request.is_closed():
23 23 <div style="padding:10px; font-size:22px;width:100%;text-align: center; color:#88D882">${_('Closed %s') % (h.age(c.pull_request.updated_on))} ${_('with status %s') % h.changeset_status_lbl(c.current_changeset_status)}</div>
24 24 %endif
25 25 <h3>${_('Title')}: ${c.pull_request.title}</h3>
26 26
27 27 <div class="form">
28 28 <div id="summary" class="fields">
29 29 <div class="field">
30 30 <div class="label-summary">
31 31 <label>${_('Review status')}:</label>
32 32 </div>
33 33 <div class="input">
34 34 <div class="changeset-status-container" style="float:none;clear:both">
35 35 %if c.current_changeset_status:
36 36 <div title="${_('Pull request status')}" class="changeset-status-lbl">${h.changeset_status_lbl(c.current_changeset_status)}</div>
37 37 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
38 38 %endif
39 39 </div>
40 40 </div>
41 41 </div>
42 42 <div class="field">
43 43 <div class="label-summary">
44 44 <label>${_('Still not reviewed by')}:</label>
45 45 </div>
46 46 <div class="input">
47 47 % if len(c.pull_request_pending_reviewers) > 0:
48 48 <div class="tooltip" title="${h.tooltip(','.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
49 49 %else:
50 50 <div>${_('pull request was reviewed by all reviewers')}</div>
51 51 %endif
52 52 </div>
53 53 </div>
54 54 <div class="field">
55 55 <div class="label-summary">
56 56 <label>${_('Origin repository')}:</label>
57 57 </div>
58 58 <div class="input">
59 59 <div>
60 60 ##%if h.is_hg(c.pull_request.org_repo):
61 61 ## <img class="icon" title="${_('Mercurial repository')}" alt="${_('Mercurial repository')}" src="${h.url('/images/icons/hgicon.png')}"/>
62 62 ##%elif h.is_git(c.pull_request.org_repo):
63 63 ## <img class="icon" title="${_('Git repository')}" alt="${_('Git repository')}" src="${h.url('/images/icons/giticon.png')}"/>
64 64 ##%endif
65 65 <span class="spantag">${c.pull_request.org_ref_parts[0]}: ${c.pull_request.org_ref_parts[1]}</span>
66 66 <span><a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name)}">${c.pull_request.org_repo.clone_url()}</a></span>
67 67 </div>
68 68 </div>
69 69 </div>
70 70 <div class="field">
71 71 <div class="label-summary">
72 72 <label>${_('Summary')}:</label>
73 73 </div>
74 74 <div class="input">
75 75 <div style="white-space:pre-wrap">${h.literal(c.pull_request.description)}</div>
76 76 </div>
77 77 </div>
78 78 <div class="field">
79 79 <div class="label-summary">
80 80 <label>${_('Created on')}:</label>
81 81 </div>
82 82 <div class="input">
83 83 <div>${h.fmt_date(c.pull_request.created_on)}</div>
84 84 </div>
85 85 </div>
86 86 </div>
87 87 </div>
88 88
89 89 <div style="overflow: auto;">
90 90 ##DIFF
91 91 <div class="table" style="float:left;clear:none">
92 92 <div id="body" class="diffblock">
93 93 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
94 94 </div>
95 95 <div id="changeset_compare_view_content">
96 96 ##CS
97 97 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
98 98 <%include file="/compare/compare_cs.html" />
99 99
100 100 ## FILES
101 101 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
102 102
103 103 % if c.limited_diff:
104 104 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
105 105 % else:
106 106 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
107 107 %endif
108 108
109 109 </div>
110 110 <div class="cs_files">
111 111 %if not c.files:
112 112 <span class="empty_data">${_('No files')}</span>
113 113 %endif
114 114 %for fid, change, f, stat in c.files:
115 115 <div class="cs_${change}">
116 116 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
117 117 <div class="changes">${h.fancy_file_stats(stat)}</div>
118 118 </div>
119 119 %endfor
120 120 </div>
121 121 % if c.limited_diff:
122 122 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("confirm to show potentially huge diff")}')">${_('Show full diff')}</a></h5>
123 123 % endif
124 124 </div>
125 125 </div>
126 126 ## REVIEWERS
127 127 <div style="float:left; border-left:1px dashed #eee">
128 128 <h4>${_('Pull request reviewers')}</h4>
129 129 <div id="reviewers" style="padding:0px 0px 5px 10px">
130 130 ## members goes here !
131 131 <div class="group_members_wrap" style="min-height:45px">
132 132 <ul id="review_members" class="group_members">
133 133 %for member,status in c.pull_request_reviewers:
134 134 <li id="reviewer_${member.user_id}">
135 135 <div class="reviewers_member">
136 136 <div style="float:left;padding:0px 3px 0px 0px" class="tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
137 137 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
138 138 </div>
139 139 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
140 140 <div style="float:left">${member.full_name} (${_('owner') if c.pull_request.user_id == member.user_id else _('reviewer')})</div>
141 141 <input type="hidden" value="${member.user_id}" name="review_members" />
142 142 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.user_id == c.rhodecode_user.user_id):
143 143 <span class="delete_icon action_button" onclick="removeReviewMember(${member.user_id})"></span>
144 144 %endif
145 145 </div>
146 146 </li>
147 147 %endfor
148 148 </ul>
149 149 </div>
150 150 %if not c.pull_request.is_closed():
151 151 <div class='ac'>
152 152 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id:
153 153 <div class="reviewer_ac">
154 154 ${h.text('user', class_='yui-ac-input')}
155 155 <span class="help-block">${_('Add or remove reviewer to this pull request.')}</span>
156 156 <div id="reviewers_container"></div>
157 157 </div>
158 158 <div style="padding:0px 10px">
159 159 <span id="update_pull_request" class="ui-btn xsmall">${_('save changes')}</span>
160 160 </div>
161 161 %endif
162 162 </div>
163 163 %endif
164 164 </div>
165 165 </div>
166 166 </div>
167 167 <script>
168 168 var _USERS_AC_DATA = ${c.users_array|n};
169 169 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
170 170 // TODO: switch this to pyroutes
171 171 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
172 172 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
173 173
174 174 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
175 175 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
176 176 pyroutes.register('pullrequest_update', "${url('pullrequest_update',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
177
177
178 178 </script>
179 179
180 180 ## diff block
181 181 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
182 182 %for fid, change, f, stat in c.files:
183 183 ${diff_block.diff_block_simple([c.changes[fid]])}
184 184 %endfor
185 185 % if c.limited_diff:
186 186 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("confirm to show potentially huge diff")}')">${_('Show full diff')}</a></h4>
187 187 % endif
188 188
189 189
190 190 ## template for inline comment form
191 191 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
192 192 ${comment.comment_inline_form()}
193 193
194 194 ## render comments and inlines
195 195 ${comment.generate_comments(include_pr=True)}
196 196
197 197 % if not c.pull_request.is_closed():
198 198 ## main comment form and it status
199 199 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
200 200 pull_request_id=c.pull_request.pull_request_id),
201 201 c.current_changeset_status,
202 202 close_btn=True, change_status=c.allowed_to_change_status)}
203 203 %endif
204 204
205 205 <script type="text/javascript">
206 206 YUE.onDOMReady(function(){
207 207 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
208 208
209 209 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
210 210 var show = 'none';
211 211 var target = e.currentTarget;
212 212 if(target.checked){
213 213 var show = ''
214 214 }
215 215 var boxid = YUD.getAttribute(target,'id_for');
216 216 var comments = YUQ('#{0} .inline-comments'.format(boxid));
217 217 for(c in comments){
218 218 YUD.setStyle(comments[c],'display',show);
219 219 }
220 220 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
221 221 for(c in btns){
222 222 YUD.setStyle(btns[c],'display',show);
223 223 }
224 224 })
225 225
226 226 YUE.on(YUQ('.line'),'click',function(e){
227 227 var tr = e.currentTarget;
228 228 injectInlineForm(tr);
229 229 });
230 230
231 231 // inject comments into they proper positions
232 232 var file_comments = YUQ('.inline-comment-placeholder');
233 233 renderInlineComments(file_comments);
234 234
235 235 YUE.on(YUD.get('update_pull_request'),'click',function(e){
236 236 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
237 237 })
238 238 })
239 239 </script>
240 240
241 241 </div>
242 242
243 243 </%def>
@@ -1,254 +1,254
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.tests.test_libs
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6
7 7 Package for testing various lib/helper functions in rhodecode
8 8
9 9 :created_on: Jun 9, 2011
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 from __future__ import with_statement
26 26 import unittest
27 27 import datetime
28 28 import hashlib
29 29 import mock
30 30 from rhodecode.tests import *
31 31
32 32 proto = 'http'
33 33 TEST_URLS = [
34 34 ('%s://127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
35 35 '%s://127.0.0.1' % proto),
36 36 ('%s://marcink@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
37 37 '%s://127.0.0.1' % proto),
38 38 ('%s://marcink:pass@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
39 39 '%s://127.0.0.1' % proto),
40 40 ('%s://127.0.0.1:8080' % proto, ['%s://' % proto, '127.0.0.1', '8080'],
41 41 '%s://127.0.0.1:8080' % proto),
42 42 ('%s://domain.org' % proto, ['%s://' % proto, 'domain.org'],
43 43 '%s://domain.org' % proto),
44 44 ('%s://user:pass@domain.org:8080' % proto, ['%s://' % proto, 'domain.org',
45 45 '8080'],
46 46 '%s://domain.org:8080' % proto),
47 47 ]
48 48
49 49 proto = 'https'
50 50 TEST_URLS += [
51 51 ('%s://127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
52 52 '%s://127.0.0.1' % proto),
53 53 ('%s://marcink@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
54 54 '%s://127.0.0.1' % proto),
55 55 ('%s://marcink:pass@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
56 56 '%s://127.0.0.1' % proto),
57 57 ('%s://127.0.0.1:8080' % proto, ['%s://' % proto, '127.0.0.1', '8080'],
58 58 '%s://127.0.0.1:8080' % proto),
59 59 ('%s://domain.org' % proto, ['%s://' % proto, 'domain.org'],
60 60 '%s://domain.org' % proto),
61 61 ('%s://user:pass@domain.org:8080' % proto, ['%s://' % proto, 'domain.org',
62 62 '8080'],
63 63 '%s://domain.org:8080' % proto),
64 64 ]
65 65
66 66
67 67 class TestLibs(unittest.TestCase):
68 68
69 69 @parameterized.expand(TEST_URLS)
70 70 def test_uri_filter(self, test_url, expected, expected_creds):
71 71 from rhodecode.lib.utils2 import uri_filter
72 72 self.assertEqual(uri_filter(test_url), expected)
73 73
74 74 @parameterized.expand(TEST_URLS)
75 75 def test_credentials_filter(self, test_url, expected, expected_creds):
76 76 from rhodecode.lib.utils2 import credentials_filter
77 77 self.assertEqual(credentials_filter(test_url), expected_creds)
78 78
79 79 @parameterized.expand([('t', True),
80 80 ('true', True),
81 81 ('y', True),
82 82 ('yes', True),
83 83 ('on', True),
84 84 ('1', True),
85 85 ('Y', True),
86 86 ('yeS', True),
87 87 ('Y', True),
88 88 ('TRUE', True),
89 89 ('T', True),
90 90 ('False', False),
91 91 ('F', False),
92 92 ('FALSE', False),
93 93 ('0', False),
94 94 ('-1', False),
95 95 ('', False)
96 96 ])
97 97 def test_str2bool(self, str_bool, expected):
98 98 from rhodecode.lib.utils2 import str2bool
99 99 self.assertEqual(str2bool(str_bool), expected)
100 100
101 101 def test_mention_extractor(self):
102 102 from rhodecode.lib.utils2 import extract_mentioned_users
103 103 sample = (
104 104 "@first hi there @marcink here's my email marcin@email.com "
105 105 "@lukaszb check @one_more22 it pls @ ttwelve @D[] @one@two@three "
106 106 "@MARCIN @maRCiN @2one_more22 @john please see this http://org.pl "
107 107 "@marian.user just do it @marco-polo and next extract @marco_polo "
108 108 "user.dot hej ! not-needed maril@domain.org"
109 109 )
110 110
111 111 s = sorted([
112 112 'first', 'marcink', 'lukaszb', 'one_more22', 'MARCIN', 'maRCiN', 'john',
113 113 'marian.user', 'marco-polo', 'marco_polo'
114 114 ], key=lambda k: k.lower())
115 115 self.assertEqual(s, extract_mentioned_users(sample))
116 116
117 117 def test_age(self):
118 118 from rhodecode.lib.utils2 import age
119 119 from dateutil import relativedelta
120 120 n = datetime.datetime.now()
121 121 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
122 122
123 123 self.assertEqual(age(n), u'just now')
124 124 self.assertEqual(age(n + delt(seconds=-1)), u'1 second ago')
125 125 self.assertEqual(age(n + delt(seconds=-60 * 2)), u'2 minutes ago')
126 126 self.assertEqual(age(n + delt(hours=-1)), u'1 hour ago')
127 127 self.assertEqual(age(n + delt(hours=-24)), u'1 day ago')
128 128 self.assertEqual(age(n + delt(hours=-24 * 5)), u'5 days ago')
129 129 self.assertEqual(age(n + delt(months=-1)), u'1 month ago')
130 130 self.assertEqual(age(n + delt(months=-1, days=-2)), u'1 month and 2 days ago')
131 131 self.assertEqual(age(n + delt(years=-1, months=-1)), u'1 year and 1 month ago')
132 132
133 133 def test_age_in_future(self):
134 134 from rhodecode.lib.utils2 import age
135 135 from dateutil import relativedelta
136 136 n = datetime.datetime.now()
137 137 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
138 138
139 139 self.assertEqual(age(n), u'just now')
140 140 self.assertEqual(age(n + delt(seconds=1)), u'in 1 second')
141 141 self.assertEqual(age(n + delt(seconds=60 * 2)), u'in 2 minutes')
142 142 self.assertEqual(age(n + delt(hours=1)), u'in 1 hour')
143 143 self.assertEqual(age(n + delt(hours=24)), u'in 1 day')
144 144 self.assertEqual(age(n + delt(hours=24 * 5)), u'in 5 days')
145 145 self.assertEqual(age(n + delt(months=1)), u'in 1 month')
146 146 self.assertEqual(age(n + delt(months=1, days=1)), u'in 1 month and 1 day')
147 147 self.assertEqual(age(n + delt(years=1, months=1)), u'in 1 year and 1 month')
148 148
149 149 def test_tag_exctrator(self):
150 150 sample = (
151 151 "hello pta[tag] gog [[]] [[] sda ero[or]d [me =>>< sa]"
152 152 "[requires] [stale] [see<>=>] [see => http://url.com]"
153 153 "[requires => url] [lang => python] [just a tag]"
154 154 "[,d] [ => ULR ] [obsolete] [desc]]"
155 155 )
156 156 from rhodecode.lib.helpers import desc_stylize
157 157 res = desc_stylize(sample)
158 158 self.assertTrue('<div class="metatag" tag="tag">tag</div>' in res)
159 159 self.assertTrue('<div class="metatag" tag="obsolete">obsolete</div>' in res)
160 160 self.assertTrue('<div class="metatag" tag="stale">stale</div>' in res)
161 161 self.assertTrue('<div class="metatag" tag="lang">python</div>' in res)
162 162 self.assertTrue('<div class="metatag" tag="requires">requires =&gt; <a href="/url">url</a></div>' in res)
163 163 self.assertTrue('<div class="metatag" tag="tag">tag</div>' in res)
164 164
165 165 def test_alternative_gravatar(self):
166 166 from rhodecode.lib.helpers import gravatar_url
167 167 _md5 = lambda s: hashlib.md5(s).hexdigest()
168 168
169 169 def fake_conf(**kwargs):
170 170 from pylons import config
171 171 config['app_conf'] = {}
172 172 config['app_conf']['use_gravatar'] = True
173 173 config['app_conf'].update(kwargs)
174 174 return config
175 175
176 176 class fake_url():
177 177 @classmethod
178 178 def current(cls, *args, **kwargs):
179 179 return 'https://server.com'
180 180
181 181 with mock.patch('pylons.url', fake_url):
182 182 fake = fake_conf(alternative_gravatar_url='http://test.com/{email}')
183 183 with mock.patch('pylons.config', fake):
184 184 from pylons import url
185 185 assert url.current() == 'https://server.com'
186 186 grav = gravatar_url(email_address='test@foo.com', size=24)
187 187 assert grav == 'http://test.com/test@foo.com'
188 188
189 189 fake = fake_conf(alternative_gravatar_url='http://test.com/{email}')
190 190 with mock.patch('pylons.config', fake):
191 191 grav = gravatar_url(email_address='test@foo.com', size=24)
192 192 assert grav == 'http://test.com/test@foo.com'
193 193
194 194 fake = fake_conf(alternative_gravatar_url='http://test.com/{md5email}')
195 195 with mock.patch('pylons.config', fake):
196 196 em = 'test@foo.com'
197 197 grav = gravatar_url(email_address=em, size=24)
198 198 assert grav == 'http://test.com/%s' % (_md5(em))
199 199
200 200 fake = fake_conf(alternative_gravatar_url='http://test.com/{md5email}/{size}')
201 201 with mock.patch('pylons.config', fake):
202 202 em = 'test@foo.com'
203 203 grav = gravatar_url(email_address=em, size=24)
204 204 assert grav == 'http://test.com/%s/%s' % (_md5(em), 24)
205 205
206 206 fake = fake_conf(alternative_gravatar_url='{scheme}://{netloc}/{md5email}/{size}')
207 207 with mock.patch('pylons.config', fake):
208 208 em = 'test@foo.com'
209 209 grav = gravatar_url(email_address=em, size=24)
210 210 assert grav == 'https://server.com/%s/%s' % (_md5(em), 24)
211 211
212 212 @parameterized.expand([
213 213 ("",
214 214 ""),
215 215 ("git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
216 216 "git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68"),
217 217 ("from rev 000000000000",
218 218 "from rev url[000000000000]"),
219 219 ("from rev 000000000000123123 also rev 000000000000",
220 220 "from rev url[000000000000123123] also rev url[000000000000]"),
221 221 ("this should-000 00",
222 222 "this should-000 00"),
223 223 ("longtextffffffffff rev 123123123123",
224 224 "longtextffffffffff rev url[123123123123]"),
225 225 ("rev ffffffffffffffffffffffffffffffffffffffffffffffffff",
226 226 "rev ffffffffffffffffffffffffffffffffffffffffffffffffff"),
227 227 ("ffffffffffff some text traalaa",
228 228 "url[ffffffffffff] some text traalaa"),
229 229 ("""Multi line
230 123123123123
230 123123123123
231 231 some text 123123123123""",
232 232 """Multi line
233 url[123123123123]
233 url[123123123123]
234 234 some text url[123123123123]""")
235 235 ])
236 236 def test_urlify_changesets(self, sample, expected):
237 237 import re
238 238
239 239 def fake_url(self, *args, **kwargs):
240 240 return '/some-url'
241 241
242 242 #quickly change expected url[] into a link
243 243 URL_PAT = re.compile(r'(?:url\[)(.+?)(?:\])')
244 244
245 245 def url_func(match_obj):
246 246 _url = match_obj.groups()[0]
247 247 tmpl = """<a class="revision-link" href="/some-url">%s</a>"""
248 248 return tmpl % _url
249 249
250 250 expected = URL_PAT.sub(url_func, expected)
251 251
252 252 with mock.patch('pylons.url', fake_url):
253 253 from rhodecode.lib.helpers import urlify_changesets
254 254 self.assertEqual(urlify_changesets(sample, 'repo_name'), expected)
General Comments 0
You need to be logged in to leave comments. Login now