##// END OF EJS Templates
white space cleanup
marcink -
r2478:8eab8111 beta
parent child Browse files
Show More
@@ -1,428 +1,428 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changeset
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 changeset controller for pylons showoing changes beetween
7 7 revisions
8 8
9 9 :created_on: Apr 25, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import logging
27 27 import traceback
28 28 from collections import defaultdict
29 29 from webob.exc import HTTPForbidden
30 30
31 31 from pylons import tmpl_context as c, url, request, response
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from pylons.decorators import jsonify
35 35
36 36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \
37 37 ChangesetDoesNotExistError
38 38 from rhodecode.lib.vcs.nodes import FileNode
39 39
40 40 import rhodecode.lib.helpers as h
41 41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
42 42 from rhodecode.lib.base import BaseRepoController, render
43 43 from rhodecode.lib.utils import EmptyChangeset, action_logger
44 44 from rhodecode.lib.compat import OrderedDict
45 45 from rhodecode.lib import diffs
46 46 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 47 from rhodecode.model.comment import ChangesetCommentsModel
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.meta import Session
50 50 from rhodecode.lib.diffs import wrapped_diff
51 51 from rhodecode.model.repo import RepoModel
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 def _update_with_GET(params, GET):
57 57 for k in ['diff1', 'diff2', 'diff']:
58 58 params[k] += GET.getall(k)
59 59
60 60
61 61 def anchor_url(revision, path, GET):
62 62 fid = h.FID(revision, path)
63 63 return h.url.current(anchor=fid, **dict(GET))
64 64
65 65
66 66 def get_ignore_ws(fid, GET):
67 67 ig_ws_global = GET.get('ignorews')
68 68 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
69 69 if ig_ws:
70 70 try:
71 71 return int(ig_ws[0].split(':')[-1])
72 72 except:
73 73 pass
74 74 return ig_ws_global
75 75
76 76
77 77 def _ignorews_url(GET, fileid=None):
78 78 fileid = str(fileid) if fileid else None
79 79 params = defaultdict(list)
80 80 _update_with_GET(params, GET)
81 81 lbl = _('show white space')
82 82 ig_ws = get_ignore_ws(fileid, GET)
83 83 ln_ctx = get_line_ctx(fileid, GET)
84 84 # global option
85 85 if fileid is None:
86 86 if ig_ws is None:
87 87 params['ignorews'] += [1]
88 88 lbl = _('ignore white space')
89 89 ctx_key = 'context'
90 90 ctx_val = ln_ctx
91 91 # per file options
92 92 else:
93 93 if ig_ws is None:
94 94 params[fileid] += ['WS:1']
95 95 lbl = _('ignore white space')
96 96
97 97 ctx_key = fileid
98 98 ctx_val = 'C:%s' % ln_ctx
99 99 # if we have passed in ln_ctx pass it along to our params
100 100 if ln_ctx:
101 101 params[ctx_key] += [ctx_val]
102 102
103 103 params['anchor'] = fileid
104 104 img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon')
105 105 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
106 106
107 107
108 108 def get_line_ctx(fid, GET):
109 109 ln_ctx_global = GET.get('context')
110 110 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
111 111
112 112 if ln_ctx:
113 113 retval = ln_ctx[0].split(':')[-1]
114 114 else:
115 115 retval = ln_ctx_global
116 116
117 117 try:
118 118 return int(retval)
119 119 except:
120 120 return
121 121
122 122
123 123 def _context_url(GET, fileid=None):
124 124 """
125 125 Generates url for context lines
126 126
127 127 :param fileid:
128 128 """
129 129
130 130 fileid = str(fileid) if fileid else None
131 131 ig_ws = get_ignore_ws(fileid, GET)
132 132 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
133 133
134 134 params = defaultdict(list)
135 135 _update_with_GET(params, GET)
136 136
137 137 # global option
138 138 if fileid is None:
139 139 if ln_ctx > 0:
140 140 params['context'] += [ln_ctx]
141 141
142 142 if ig_ws:
143 143 ig_ws_key = 'ignorews'
144 144 ig_ws_val = 1
145 145
146 146 # per file option
147 147 else:
148 148 params[fileid] += ['C:%s' % ln_ctx]
149 149 ig_ws_key = fileid
150 150 ig_ws_val = 'WS:%s' % 1
151 151
152 152 if ig_ws:
153 153 params[ig_ws_key] += [ig_ws_val]
154 154
155 155 lbl = _('%s line context') % ln_ctx
156 156
157 157 params['anchor'] = fileid
158 158 img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon')
159 159 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
160 160
161 161
162 162 class ChangesetController(BaseRepoController):
163 163
164 164 @LoginRequired()
165 165 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
166 166 'repository.admin')
167 167 def __before__(self):
168 168 super(ChangesetController, self).__before__()
169 169 c.affected_files_cut_off = 60
170 170 repo_model = RepoModel()
171 171 c.users_array = repo_model.get_users_js()
172 172 c.users_groups_array = repo_model.get_users_groups_js()
173 173
174 174 def index(self, revision):
175 175
176 176 c.anchor_url = anchor_url
177 177 c.ignorews_url = _ignorews_url
178 178 c.context_url = _context_url
179 179 limit_off = request.GET.get('fulldiff')
180 180 #get ranges of revisions if preset
181 181 rev_range = revision.split('...')[:2]
182 182 enable_comments = True
183 183 try:
184 184 if len(rev_range) == 2:
185 185 enable_comments = False
186 186 rev_start = rev_range[0]
187 187 rev_end = rev_range[1]
188 188 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
189 189 end=rev_end)
190 190 else:
191 191 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
192 192
193 193 c.cs_ranges = list(rev_ranges)
194 194 if not c.cs_ranges:
195 195 raise RepositoryError('Changeset range returned empty result')
196 196
197 197 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
198 198 log.error(traceback.format_exc())
199 199 h.flash(str(e), category='warning')
200 200 return redirect(url('home'))
201 201
202 202 c.changes = OrderedDict()
203 203
204 204 c.lines_added = 0 # count of lines added
205 205 c.lines_deleted = 0 # count of lines removes
206 206
207 207 cumulative_diff = 0
208 208 c.cut_off = False # defines if cut off limit is reached
209 209 c.changeset_statuses = ChangesetStatus.STATUSES
210 210 c.comments = []
211 211 c.statuses = []
212 212 c.inline_comments = []
213 213 c.inline_cnt = 0
214 214 # Iterate over ranges (default changeset view is always one changeset)
215 215 for changeset in c.cs_ranges:
216 216
217 217 c.statuses.extend([ChangesetStatusModel()\
218 218 .get_status(c.rhodecode_db_repo.repo_id,
219 219 changeset.raw_id)])
220 220
221 221 c.comments.extend(ChangesetCommentsModel()\
222 222 .get_comments(c.rhodecode_db_repo.repo_id,
223 223 revision=changeset.raw_id))
224 224 inlines = ChangesetCommentsModel()\
225 225 .get_inline_comments(c.rhodecode_db_repo.repo_id,
226 226 revision=changeset.raw_id)
227 227 c.inline_comments.extend(inlines)
228 228 c.changes[changeset.raw_id] = []
229 229 try:
230 230 changeset_parent = changeset.parents[0]
231 231 except IndexError:
232 232 changeset_parent = None
233 233
234 234 #==================================================================
235 235 # ADDED FILES
236 236 #==================================================================
237 237 for node in changeset.added:
238 238 fid = h.FID(revision, node.path)
239 239 line_context_lcl = get_line_ctx(fid, request.GET)
240 240 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
241 241 lim = self.cut_off_limit
242 242 if cumulative_diff > self.cut_off_limit:
243 243 lim = -1 if limit_off is None else None
244 244 size, cs1, cs2, diff, st = wrapped_diff(
245 245 filenode_old=None,
246 246 filenode_new=node,
247 247 cut_off_limit=lim,
248 248 ignore_whitespace=ign_whitespace_lcl,
249 249 line_context=line_context_lcl,
250 250 enable_comments=enable_comments
251 251 )
252 252 cumulative_diff += size
253 253 c.lines_added += st[0]
254 254 c.lines_deleted += st[1]
255 255 c.changes[changeset.raw_id].append(
256 256 ('added', node, diff, cs1, cs2, st)
257 257 )
258 258
259 259 #==================================================================
260 260 # CHANGED FILES
261 261 #==================================================================
262 262 for node in changeset.changed:
263 263 try:
264 264 filenode_old = changeset_parent.get_node(node.path)
265 265 except ChangesetError:
266 266 log.warning('Unable to fetch parent node for diff')
267 267 filenode_old = FileNode(node.path, '', EmptyChangeset())
268 268
269 269 fid = h.FID(revision, node.path)
270 270 line_context_lcl = get_line_ctx(fid, request.GET)
271 271 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
272 272 lim = self.cut_off_limit
273 273 if cumulative_diff > self.cut_off_limit:
274 274 lim = -1 if limit_off is None else None
275 275 size, cs1, cs2, diff, st = wrapped_diff(
276 276 filenode_old=filenode_old,
277 277 filenode_new=node,
278 278 cut_off_limit=lim,
279 279 ignore_whitespace=ign_whitespace_lcl,
280 280 line_context=line_context_lcl,
281 281 enable_comments=enable_comments
282 282 )
283 283 cumulative_diff += size
284 284 c.lines_added += st[0]
285 285 c.lines_deleted += st[1]
286 286 c.changes[changeset.raw_id].append(
287 287 ('changed', node, diff, cs1, cs2, st)
288 288 )
289 289 #==================================================================
290 290 # REMOVED FILES
291 291 #==================================================================
292 292 for node in changeset.removed:
293 293 c.changes[changeset.raw_id].append(
294 294 ('removed', node, None, None, None, (0, 0))
295 295 )
296 296
297 297 # count inline comments
298 298 for __, lines in c.inline_comments:
299 299 for comments in lines.values():
300 300 c.inline_cnt += len(comments)
301 301
302 302 if len(c.cs_ranges) == 1:
303 303 c.changeset = c.cs_ranges[0]
304 304 c.changes = c.changes[c.changeset.raw_id]
305 305
306 306 return render('changeset/changeset.html')
307 307 else:
308 308 return render('changeset/changeset_range.html')
309 309
310 310 def raw_changeset(self, revision):
311 311
312 312 method = request.GET.get('diff', 'show')
313 313 ignore_whitespace = request.GET.get('ignorews') == '1'
314 314 line_context = request.GET.get('context', 3)
315 315 try:
316 316 c.scm_type = c.rhodecode_repo.alias
317 317 c.changeset = c.rhodecode_repo.get_changeset(revision)
318 318 except RepositoryError:
319 319 log.error(traceback.format_exc())
320 320 return redirect(url('home'))
321 321 else:
322 322 try:
323 323 c.changeset_parent = c.changeset.parents[0]
324 324 except IndexError:
325 325 c.changeset_parent = None
326 326 c.changes = []
327 327
328 328 for node in c.changeset.added:
329 329 filenode_old = FileNode(node.path, '')
330 330 if filenode_old.is_binary or node.is_binary:
331 331 diff = _('binary file') + '\n'
332 332 else:
333 333 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
334 334 ignore_whitespace=ignore_whitespace,
335 335 context=line_context)
336 336 diff = diffs.DiffProcessor(f_gitdiff,
337 337 format='gitdiff').raw_diff()
338 338
339 339 cs1 = None
340 340 cs2 = node.changeset.raw_id
341 341 c.changes.append(('added', node, diff, cs1, cs2))
342 342
343 343 for node in c.changeset.changed:
344 344 filenode_old = c.changeset_parent.get_node(node.path)
345 345 if filenode_old.is_binary or node.is_binary:
346 346 diff = _('binary file')
347 347 else:
348 348 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
349 349 ignore_whitespace=ignore_whitespace,
350 350 context=line_context)
351 351 diff = diffs.DiffProcessor(f_gitdiff,
352 352 format='gitdiff').raw_diff()
353 353
354 354 cs1 = filenode_old.changeset.raw_id
355 355 cs2 = node.changeset.raw_id
356 356 c.changes.append(('changed', node, diff, cs1, cs2))
357 357
358 358 response.content_type = 'text/plain'
359 359
360 360 if method == 'download':
361 361 response.content_disposition = 'attachment; filename=%s.patch' \
362 362 % revision
363 363
364 364 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
365 365 for x in c.changeset.parents])
366 366
367 367 c.diffs = ''
368 368 for x in c.changes:
369 369 c.diffs += x[2]
370 370
371 371 return render('changeset/raw_changeset.html')
372 372
373 373 @jsonify
374 374 def comment(self, repo_name, revision):
375 375 status = request.POST.get('changeset_status')
376 376 change_status = request.POST.get('change_changeset_status')
377 377
378 378 comm = ChangesetCommentsModel().create(
379 379 text=request.POST.get('text'),
380 380 repo_id=c.rhodecode_db_repo.repo_id,
381 381 user_id=c.rhodecode_user.user_id,
382 382 revision=revision,
383 383 f_path=request.POST.get('f_path'),
384 384 line_no=request.POST.get('line'),
385 status_change=(ChangesetStatus.get_status_lbl(status)
385 status_change=(ChangesetStatus.get_status_lbl(status)
386 386 if status and change_status else None)
387 387 )
388 388
389 389 # get status if set !
390 390 if status and change_status:
391 391 ChangesetStatusModel().set_status(
392 392 c.rhodecode_db_repo.repo_id,
393 393 status,
394 394 c.rhodecode_user.user_id,
395 395 comm,
396 396 revision=revision,
397 397 )
398 398 action_logger(self.rhodecode_user,
399 399 'user_commented_revision:%s' % revision,
400 400 c.rhodecode_db_repo, self.ip_addr, self.sa)
401 401
402 402 Session.commit()
403 403
404 404 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
405 405 return redirect(h.url('changeset_home', repo_name=repo_name,
406 406 revision=revision))
407 407
408 408 data = {
409 409 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
410 410 }
411 411 if comm:
412 412 c.co = comm
413 413 data.update(comm.get_dict())
414 414 data.update({'rendered_text':
415 415 render('changeset/changeset_comment_block.html')})
416 416
417 417 return data
418 418
419 419 @jsonify
420 420 def delete_comment(self, repo_name, comment_id):
421 421 co = ChangesetComment.get(comment_id)
422 422 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
423 423 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
424 424 ChangesetCommentsModel().delete(comment=co)
425 425 Session.commit()
426 426 return True
427 427 else:
428 428 raise HTTPForbidden()
@@ -1,277 +1,277 b''
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
28 28 from webob.exc import HTTPNotFound
29 29
30 30 from pylons import request, response, session, tmpl_context as c, url
31 31 from pylons.controllers.util import abort, redirect
32 32 from pylons.i18n.translation import _
33 33 from pylons.decorators import jsonify
34 34
35 35 from rhodecode.lib.base import BaseRepoController, render
36 36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib import diffs
39 39 from rhodecode.lib.utils import action_logger
40 40 from rhodecode.model.db import User, PullRequest, ChangesetStatus
41 41 from rhodecode.model.pull_request import PullRequestModel
42 42 from rhodecode.model.meta import Session
43 43 from rhodecode.model.repo import RepoModel
44 44 from rhodecode.model.comment import ChangesetCommentsModel
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class PullrequestsController(BaseRepoController):
51 51
52 52 @LoginRequired()
53 53 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
54 54 'repository.admin')
55 55 def __before__(self):
56 56 super(PullrequestsController, self).__before__()
57 57
58 58 def _get_repo_refs(self, repo):
59 59 hist_l = []
60 60
61 61 branches_group = ([('branch:%s:%s' % (k, v), k) for
62 62 k, v in repo.branches.iteritems()], _("Branches"))
63 63 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
64 64 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
65 tags_group = ([('tag:%s:%s' % (k, v), k) for
65 tags_group = ([('tag:%s:%s' % (k, v), k) for
66 66 k, v in repo.tags.iteritems()], _("Tags"))
67 67
68 68 hist_l.append(bookmarks_group)
69 69 hist_l.append(branches_group)
70 70 hist_l.append(tags_group)
71 71
72 72 return hist_l
73 73
74 74 def show_all(self, repo_name):
75 75 c.pull_requests = PullRequestModel().get_all(repo_name)
76 76 c.repo_name = repo_name
77 77 return render('/pullrequests/pullrequest_show_all.html')
78 78
79 79 def index(self):
80 80 org_repo = c.rhodecode_db_repo
81 81
82 82 if org_repo.scm_instance.alias != 'hg':
83 83 log.error('Review not available for GIT REPOS')
84 84 raise HTTPNotFound
85 85
86 86 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
87 87 c.org_repos = []
88 88 c.other_repos = []
89 89 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
90 90 org_repo.user.username, c.repo_name))
91 91 )
92 92
93 93 c.other_refs = c.org_refs
94 94 c.other_repos.extend(c.org_repos)
95 95 c.default_pull_request = org_repo.repo_name
96 96 #gather forks and add to this list
97 97 for fork in org_repo.forks:
98 98 c.other_repos.append((fork.repo_name, '%s/%s' % (
99 99 fork.user.username, fork.repo_name))
100 100 )
101 101 #add parents of this fork also
102 102 if org_repo.parent:
103 103 c.default_pull_request = org_repo.parent.repo_name
104 104 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
105 105 org_repo.parent.user.username,
106 106 org_repo.parent.repo_name))
107 107 )
108 108
109 109 c.review_members = []
110 110 c.available_members = []
111 111 for u in User.query().filter(User.username != 'default').all():
112 112 uname = u.username
113 113 if org_repo.user == u:
114 114 uname = _('%s (owner)' % u.username)
115 115 # auto add owner to pull-request recipients
116 116 c.review_members.append([u.user_id, uname])
117 117 c.available_members.append([u.user_id, uname])
118 118 return render('/pullrequests/pullrequest.html')
119 119
120 120 def create(self, repo_name):
121 121 req_p = request.POST
122 122 org_repo = req_p['org_repo']
123 123 org_ref = req_p['org_ref']
124 124 other_repo = req_p['other_repo']
125 125 other_ref = req_p['other_ref']
126 126 revisions = req_p.getall('revisions')
127 127 reviewers = req_p.getall('review_members')
128 128 #TODO: wrap this into a FORM !!!
129 129
130 130 title = req_p['pullrequest_title']
131 131 description = req_p['pullrequest_desc']
132 132
133 133 try:
134 134 model = PullRequestModel()
135 135 model.create(self.rhodecode_user.user_id, org_repo,
136 136 org_ref, other_repo, other_ref, revisions,
137 137 reviewers, title, description)
138 138 Session.commit()
139 139 h.flash(_('Pull request send'), category='success')
140 140 except Exception:
141 141 raise
142 142 h.flash(_('Error occured during sending pull request'),
143 143 category='error')
144 144 log.error(traceback.format_exc())
145 145
146 146 return redirect(url('changelog_home', repo_name=repo_name))
147 147
148 148 def _load_compare_data(self, pull_request):
149 149 """
150 150 Load context data needed for generating compare diff
151 151
152 152 :param pull_request:
153 153 :type pull_request:
154 154 """
155 155
156 156 org_repo = pull_request.org_repo
157 157 org_ref_type, org_ref_, org_ref = pull_request.org_ref.split(':')
158 158 other_repo = pull_request.other_repo
159 159 other_ref_type, other_ref, other_ref_ = pull_request.other_ref.split(':')
160 160
161 161 org_ref = (org_ref_type, org_ref)
162 162 other_ref = (other_ref_type, other_ref)
163 163
164 164 c.org_repo = org_repo
165 165 c.other_repo = other_repo
166 166
167 167 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
168 168 org_repo, org_ref, other_repo, other_ref
169 169 )
170 170
171 171 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
172 172 c.cs_ranges])
173 173 # defines that we need hidden inputs with changesets
174 174 c.as_form = request.GET.get('as_form', False)
175 175
176 176 c.org_ref = org_ref[1]
177 177 c.other_ref = other_ref[1]
178 178 # diff needs to have swapped org with other to generate proper diff
179 179 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
180 180 discovery_data)
181 181 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
182 182 _parsed = diff_processor.prepare()
183 183
184 184 c.files = []
185 185 c.changes = {}
186 186
187 187 for f in _parsed:
188 188 fid = h.FID('', f['filename'])
189 189 c.files.append([fid, f['operation'], f['filename'], f['stats']])
190 190 diff = diff_processor.as_html(enable_comments=False, diff_lines=[f])
191 191 c.changes[fid] = [f['operation'], f['filename'], diff]
192 192
193 193 def show(self, repo_name, pull_request_id):
194 194 repo_model = RepoModel()
195 195 c.users_array = repo_model.get_users_js()
196 196 c.users_groups_array = repo_model.get_users_groups_js()
197 197 c.pull_request = PullRequest.get(pull_request_id)
198 198
199 199 # valid ID
200 200 if not c.pull_request:
201 201 raise HTTPNotFound
202 202
203 203 # pull_requests repo_name we opened it against
204 204 # ie. other_repo must match
205 205 if repo_name != c.pull_request.other_repo.repo_name:
206 206 raise HTTPNotFound
207 207
208 208 # load compare data into template context
209 209 self._load_compare_data(c.pull_request)
210 210
211 211 # inline comments
212 212 c.inline_cnt = 0
213 213 c.inline_comments = ChangesetCommentsModel()\
214 214 .get_inline_comments(c.rhodecode_db_repo.repo_id,
215 215 pull_request=pull_request_id)
216 216 # count inline comments
217 217 for __, lines in c.inline_comments:
218 218 for comments in lines.values():
219 219 c.inline_cnt += len(comments)
220 220 # comments
221 221 c.comments = ChangesetCommentsModel()\
222 222 .get_comments(c.rhodecode_db_repo.repo_id,
223 223 pull_request=pull_request_id)
224 224
225 225 # changeset(pull-request) status
226 226 c.current_changeset_status = ChangesetStatusModel()\
227 227 .get_status(c.pull_request.org_repo,
228 228 pull_request=c.pull_request)
229 229 c.changeset_statuses = ChangesetStatus.STATUSES
230 230 return render('/pullrequests/pullrequest_show.html')
231 231
232 232 @jsonify
233 233 def comment(self, repo_name, pull_request_id):
234 234
235 235 status = request.POST.get('changeset_status')
236 236 change_status = request.POST.get('change_changeset_status')
237 237
238 238 comm = ChangesetCommentsModel().create(
239 239 text=request.POST.get('text'),
240 240 repo_id=c.rhodecode_db_repo.repo_id,
241 241 user_id=c.rhodecode_user.user_id,
242 242 pull_request=pull_request_id,
243 243 f_path=request.POST.get('f_path'),
244 244 line_no=request.POST.get('line'),
245 status_change=(ChangesetStatus.get_status_lbl(status)
245 status_change=(ChangesetStatus.get_status_lbl(status)
246 246 if status and change_status else None)
247 247 )
248 248
249 249 # get status if set !
250 250 if status and change_status:
251 251 ChangesetStatusModel().set_status(
252 252 c.rhodecode_db_repo.repo_id,
253 253 status,
254 254 c.rhodecode_user.user_id,
255 255 comm,
256 256 pull_request=pull_request_id
257 257 )
258 258 action_logger(self.rhodecode_user,
259 259 'user_commented_pull_request:%s' % pull_request_id,
260 260 c.rhodecode_db_repo, self.ip_addr, self.sa)
261 261
262 262 Session.commit()
263 263
264 264 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
265 265 return redirect(h.url('pullrequest_show', repo_name=repo_name,
266 266 pull_request_id=pull_request_id))
267 267
268 268 data = {
269 269 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
270 270 }
271 271 if comm:
272 272 c.co = comm
273 273 data.update(comm.get_dict())
274 274 data.update({'rendered_text':
275 275 render('changeset/changeset_comment_block.html')})
276 276
277 277 return data
@@ -1,219 +1,219 b''
1 1 """The base Controller API
2 2
3 3 Provides the BaseController class for subclassing.
4 4 """
5 5 import logging
6 6 import time
7 7 import traceback
8 8
9 9 from paste.auth.basic import AuthBasicAuthenticator
10 10 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden
11 11 from paste.httpheaders import WWW_AUTHENTICATE
12 12
13 13 from pylons import config, tmpl_context as c, request, session, url
14 14 from pylons.controllers import WSGIController
15 15 from pylons.controllers.util import redirect
16 16 from pylons.templating import render_mako as render
17 17
18 18 from rhodecode import __version__, BACKENDS
19 19
20 20 from rhodecode.lib.utils2 import str2bool, safe_unicode
21 21 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
22 22 HasPermissionAnyMiddleware, CookieStoreWrapper
23 23 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
24 24 from rhodecode.model import meta
25 25
26 26 from rhodecode.model.db import Repository
27 27 from rhodecode.model.notification import NotificationModel
28 28 from rhodecode.model.scm import ScmModel
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 def _get_ip_addr(environ):
34 34 proxy_key = 'HTTP_X_REAL_IP'
35 35 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
36 36 def_key = 'REMOTE_ADDR'
37 37
38 38 return environ.get(proxy_key2,
39 39 environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
40 40 )
41 41
42 42
43 43 class BasicAuth(AuthBasicAuthenticator):
44 44
45 45 def __init__(self, realm, authfunc, auth_http_code=None):
46 46 self.realm = realm
47 47 self.authfunc = authfunc
48 48 self._rc_auth_http_code = auth_http_code
49 49
50 50 def build_authentication(self):
51 51 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
52 52 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
53 53 # return 403 if alternative http return code is specified in
54 54 # RhodeCode config
55 55 return HTTPForbidden(headers=head)
56 56 return HTTPUnauthorized(headers=head)
57 57
58 58
59 59 class BaseVCSController(object):
60 60
61 61 def __init__(self, application, config):
62 62 self.application = application
63 63 self.config = config
64 64 # base path of repo locations
65 65 self.basepath = self.config['base_path']
66 66 #authenticate this mercurial request using authfunc
67 67 self.authenticate = BasicAuth('', authfunc,
68 68 config.get('auth_ret_code'))
69 69 self.ipaddr = '0.0.0.0'
70 70
71 71 def _handle_request(self, environ, start_response):
72 72 raise NotImplementedError()
73 73
74 74 def _get_by_id(self, repo_name):
75 75 """
76 76 Get's a special pattern _<ID> from clone url and tries to replace it
77 77 with a repository_name for support of _<ID> non changable urls
78 78
79 79 :param repo_name:
80 80 """
81 81 try:
82 82 data = repo_name.split('/')
83 83 if len(data) >= 2:
84 84 by_id = data[1].split('_')
85 85 if len(by_id) == 2 and by_id[1].isdigit():
86 86 _repo_name = Repository.get(by_id[1]).repo_name
87 87 data[1] = _repo_name
88 88 except:
89 89 log.debug('Failed to extract repo_name from id %s' % (
90 90 traceback.format_exc()
91 91 )
92 92 )
93 93
94 94 return '/'.join(data)
95 95
96 96 def _invalidate_cache(self, repo_name):
97 97 """
98 98 Set's cache for this repository for invalidation on next access
99 99
100 100 :param repo_name: full repo name, also a cache key
101 101 """
102 102 invalidate_cache('get_repo_cached_%s' % repo_name)
103 103
104 104 def _check_permission(self, action, user, repo_name):
105 105 """
106 106 Checks permissions using action (push/pull) user and repository
107 107 name
108 108
109 109 :param action: push or pull action
110 110 :param user: user instance
111 111 :param repo_name: repository name
112 112 """
113 113 if action == 'push':
114 114 if not HasPermissionAnyMiddleware('repository.write',
115 115 'repository.admin')(user,
116 116 repo_name):
117 117 return False
118 118
119 119 else:
120 120 #any other action need at least read permission
121 121 if not HasPermissionAnyMiddleware('repository.read',
122 122 'repository.write',
123 123 'repository.admin')(user,
124 124 repo_name):
125 125 return False
126 126
127 127 return True
128 128
129 129 def _get_ip_addr(self, environ):
130 130 return _get_ip_addr(environ)
131 131
132 132 def __call__(self, environ, start_response):
133 133 start = time.time()
134 134 try:
135 135 return self._handle_request(environ, start_response)
136 136 finally:
137 137 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
138 138 log.debug('Request time: %.3fs' % (time.time() - start))
139 139 meta.Session.remove()
140 140
141 141
142 142 class BaseController(WSGIController):
143 143
144 144 def __before__(self):
145 145 c.rhodecode_version = __version__
146 146 c.rhodecode_instanceid = config.get('instance_id')
147 147 c.rhodecode_name = config.get('rhodecode_title')
148 148 c.use_gravatar = str2bool(config.get('use_gravatar'))
149 149 c.ga_code = config.get('rhodecode_ga_code')
150 150 c.repo_name = get_repo_slug(request)
151 151 c.backends = BACKENDS.keys()
152 152 c.unread_notifications = NotificationModel()\
153 153 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
154 154 self.cut_off_limit = int(config.get('cut_off_limit'))
155 155
156 156 self.sa = meta.Session
157 157 self.scm_model = ScmModel(self.sa)
158 158 self.ip_addr = ''
159 159
160 160 def __call__(self, environ, start_response):
161 161 """Invoke the Controller"""
162 162 # WSGIController.__call__ dispatches to the Controller method
163 163 # the request is routed to. This routing information is
164 164 # available in environ['pylons.routes_dict']
165 165 start = time.time()
166 166 try:
167 167 self.ip_addr = _get_ip_addr(environ)
168 168 # make sure that we update permissions each time we call controller
169 169 api_key = request.GET.get('api_key')
170 170 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
171 171 user_id = cookie_store.get('user_id', None)
172 172 username = get_container_username(environ, config)
173 173 auth_user = AuthUser(user_id, api_key, username)
174 174 request.user = auth_user
175 175 self.rhodecode_user = c.rhodecode_user = auth_user
176 176 if not self.rhodecode_user.is_authenticated and \
177 177 self.rhodecode_user.user_id is not None:
178 178 self.rhodecode_user.set_authenticated(
179 179 cookie_store.get('is_authenticated')
180 180 )
181 181 log.info('User: %s accessed %s' % (
182 182 auth_user, safe_unicode(environ.get('PATH_INFO')))
183 183 )
184 184 return WSGIController.__call__(self, environ, start_response)
185 185 finally:
186 186 log.info('Request to %s time: %.3fs' % (
187 187 safe_unicode(environ.get('PATH_INFO')), time.time() - start)
188 188 )
189 189 meta.Session.remove()
190 190
191 191
192 192 class BaseRepoController(BaseController):
193 193 """
194 194 Base class for controllers responsible for loading all needed data for
195 195 repository loaded items are
196 196
197 197 c.rhodecode_repo: instance of scm repository
198 198 c.rhodecode_db_repo: instance of db
199 199 c.repository_followers: number of followers
200 200 c.repository_forks: number of forks
201 201 """
202 202
203 203 def __before__(self):
204 204 super(BaseRepoController, self).__before__()
205 205 if c.repo_name:
206 206
207 207 dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
208 208 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
209 209
210 210 if c.rhodecode_repo is None:
211 211 log.error('%s this repository is present in database but it '
212 212 'cannot be created as an scm instance', c.repo_name)
213 213
214 214 redirect(url('home'))
215 215
216 216 # some globals counter for menu
217 217 c.repository_followers = self.scm_model.get_followers(dbr)
218 218 c.repository_forks = self.scm_model.get_forks(dbr)
219 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr) No newline at end of file
219 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
@@ -1,28 +1,28 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db_1_4_0
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode <=1.4.X
7 7
8 8 :created_on: Apr 08, 2010
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
26 26 #TODO: replace that will db.py content after 1.5 Release
27 27
28 from rhodecode.model.db import * No newline at end of file
28 from rhodecode.model.db import *
@@ -1,627 +1,627 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.diffs
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Set of diffing helpers, previously part of vcs
7 7
8 8
9 9 :created_on: Dec 4, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :original copyright: 2007-2008 by Armin Ronacher
13 13 :license: GPLv3, see COPYING for more details.
14 14 """
15 15 # This program is free software: you can redistribute it and/or modify
16 16 # it under the terms of the GNU General Public License as published by
17 17 # the Free Software Foundation, either version 3 of the License, or
18 18 # (at your option) any later version.
19 19 #
20 20 # This program is distributed in the hope that it will be useful,
21 21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 23 # GNU General Public License for more details.
24 24 #
25 25 # You should have received a copy of the GNU General Public License
26 26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 27
28 28 import re
29 29 import io
30 30 import difflib
31 31 import markupsafe
32 32
33 33 from itertools import tee, imap
34 34
35 35 from mercurial import patch
36 36 from mercurial.mdiff import diffopts
37 37 from mercurial.bundlerepo import bundlerepository
38 38 from mercurial import localrepo
39 39
40 40 from pylons.i18n.translation import _
41 41
42 42 from rhodecode.lib.vcs.exceptions import VCSError
43 43 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
44 44 from rhodecode.lib.helpers import escape
45 45 from rhodecode.lib.utils import EmptyChangeset, make_ui
46 46
47 47
48 48 def wrap_to_table(str_):
49 49 return '''<table class="code-difftable">
50 50 <tr class="line no-comment">
51 51 <td class="lineno new"></td>
52 52 <td class="code no-comment"><pre>%s</pre></td>
53 53 </tr>
54 54 </table>''' % str_
55 55
56 56
57 57 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
58 58 ignore_whitespace=True, line_context=3,
59 59 enable_comments=False):
60 60 """
61 61 returns a wrapped diff into a table, checks for cut_off_limit and presents
62 62 proper message
63 63 """
64 64
65 65 if filenode_old is None:
66 66 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
67 67
68 68 if filenode_old.is_binary or filenode_new.is_binary:
69 69 diff = wrap_to_table(_('binary file'))
70 70 stats = (0, 0)
71 71 size = 0
72 72
73 73 elif cut_off_limit != -1 and (cut_off_limit is None or
74 74 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
75 75
76 76 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
77 77 ignore_whitespace=ignore_whitespace,
78 78 context=line_context)
79 79 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
80 80
81 81 diff = diff_processor.as_html(enable_comments=enable_comments)
82 82 stats = diff_processor.stat()
83 83 size = len(diff or '')
84 84 else:
85 85 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
86 86 'diff menu to display this diff'))
87 87 stats = (0, 0)
88 88 size = 0
89 89 if not diff:
90 90 submodules = filter(lambda o: isinstance(o, SubModuleNode),
91 91 [filenode_new, filenode_old])
92 92 if submodules:
93 93 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
94 94 else:
95 95 diff = wrap_to_table(_('No changes detected'))
96 96
97 97 cs1 = filenode_old.changeset.raw_id
98 98 cs2 = filenode_new.changeset.raw_id
99 99
100 100 return size, cs1, cs2, diff, stats
101 101
102 102
103 103 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
104 104 """
105 105 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
106 106
107 107 :param ignore_whitespace: ignore whitespaces in diff
108 108 """
109 109 # make sure we pass in default context
110 110 context = context or 3
111 111 submodules = filter(lambda o: isinstance(o, SubModuleNode),
112 112 [filenode_new, filenode_old])
113 113 if submodules:
114 114 return ''
115 115
116 116 for filenode in (filenode_old, filenode_new):
117 117 if not isinstance(filenode, FileNode):
118 118 raise VCSError("Given object should be FileNode object, not %s"
119 119 % filenode.__class__)
120 120
121 121 repo = filenode_new.changeset.repository
122 122 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
123 123 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
124 124
125 125 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
126 126 ignore_whitespace, context)
127 127 return vcs_gitdiff
128 128
129 129
130 130 class DiffProcessor(object):
131 131 """
132 132 Give it a unified diff and it returns a list of the files that were
133 133 mentioned in the diff together with a dict of meta information that
134 134 can be used to render it in a HTML template.
135 135 """
136 136 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
137 137
138 138 def __init__(self, diff, differ='diff', format='gitdiff'):
139 139 """
140 140 :param diff: a text in diff format or generator
141 141 :param format: format of diff passed, `udiff` or `gitdiff`
142 142 """
143 143 if isinstance(diff, basestring):
144 144 diff = [diff]
145 145
146 146 self.__udiff = diff
147 147 self.__format = format
148 148 self.adds = 0
149 149 self.removes = 0
150 150
151 151 if isinstance(self.__udiff, basestring):
152 152 self.lines = iter(self.__udiff.splitlines(1))
153 153
154 154 elif self.__format == 'gitdiff':
155 155 udiff_copy = self.copy_iterator()
156 156 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
157 157 else:
158 158 udiff_copy = self.copy_iterator()
159 159 self.lines = imap(self.escaper, udiff_copy)
160 160
161 161 # Select a differ.
162 162 if differ == 'difflib':
163 163 self.differ = self._highlight_line_difflib
164 164 else:
165 165 self.differ = self._highlight_line_udiff
166 166
167 167 def escaper(self, string):
168 168 return markupsafe.escape(string)
169 169
170 170 def copy_iterator(self):
171 171 """
172 172 make a fresh copy of generator, we should not iterate thru
173 173 an original as it's needed for repeating operations on
174 174 this instance of DiffProcessor
175 175 """
176 176 self.__udiff, iterator_copy = tee(self.__udiff)
177 177 return iterator_copy
178 178
179 179 def _extract_rev(self, line1, line2):
180 180 """
181 181 Extract the operation (A/M/D), filename and revision hint from a line.
182 182 """
183 183
184 184 try:
185 185 if line1.startswith('--- ') and line2.startswith('+++ '):
186 186 l1 = line1[4:].split(None, 1)
187 187 old_filename = (l1[0].replace('a/', '', 1)
188 188 if len(l1) >= 1 else None)
189 189 old_rev = l1[1] if len(l1) == 2 else 'old'
190 190
191 191 l2 = line2[4:].split(None, 1)
192 192 new_filename = (l2[0].replace('b/', '', 1)
193 193 if len(l1) >= 1 else None)
194 194 new_rev = l2[1] if len(l2) == 2 else 'new'
195 195
196 196 filename = (old_filename
197 197 if old_filename != '/dev/null' else new_filename)
198 198
199 199 operation = 'D' if new_filename == '/dev/null' else None
200 200 if not operation:
201 201 operation = 'M' if old_filename != '/dev/null' else 'A'
202 202
203 203 return operation, filename, new_rev, old_rev
204 204 except (ValueError, IndexError):
205 205 pass
206 206
207 207 return None, None, None, None
208 208
209 209 def _parse_gitdiff(self, diffiterator):
210 210 def line_decoder(l):
211 211 if l.startswith('+') and not l.startswith('+++'):
212 212 self.adds += 1
213 213 elif l.startswith('-') and not l.startswith('---'):
214 214 self.removes += 1
215 215 return l.decode('utf8', 'replace')
216 216
217 217 output = list(diffiterator)
218 218 size = len(output)
219 219
220 220 if size == 2:
221 221 l = []
222 222 l.extend([output[0]])
223 223 l.extend(output[1].splitlines(1))
224 224 return map(line_decoder, l)
225 225 elif size == 1:
226 226 return map(line_decoder, output[0].splitlines(1))
227 227 elif size == 0:
228 228 return []
229 229
230 230 raise Exception('wrong size of diff %s' % size)
231 231
232 232 def _highlight_line_difflib(self, line, next_):
233 233 """
234 234 Highlight inline changes in both lines.
235 235 """
236 236
237 237 if line['action'] == 'del':
238 238 old, new = line, next_
239 239 else:
240 240 old, new = next_, line
241 241
242 242 oldwords = re.split(r'(\W)', old['line'])
243 243 newwords = re.split(r'(\W)', new['line'])
244 244
245 245 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
246 246
247 247 oldfragments, newfragments = [], []
248 248 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
249 249 oldfrag = ''.join(oldwords[i1:i2])
250 250 newfrag = ''.join(newwords[j1:j2])
251 251 if tag != 'equal':
252 252 if oldfrag:
253 253 oldfrag = '<del>%s</del>' % oldfrag
254 254 if newfrag:
255 255 newfrag = '<ins>%s</ins>' % newfrag
256 256 oldfragments.append(oldfrag)
257 257 newfragments.append(newfrag)
258 258
259 259 old['line'] = "".join(oldfragments)
260 260 new['line'] = "".join(newfragments)
261 261
262 262 def _highlight_line_udiff(self, line, next_):
263 263 """
264 264 Highlight inline changes in both lines.
265 265 """
266 266 start = 0
267 267 limit = min(len(line['line']), len(next_['line']))
268 268 while start < limit and line['line'][start] == next_['line'][start]:
269 269 start += 1
270 270 end = -1
271 271 limit -= start
272 272 while -end <= limit and line['line'][end] == next_['line'][end]:
273 273 end -= 1
274 274 end += 1
275 275 if start or end:
276 276 def do(l):
277 277 last = end + len(l['line'])
278 278 if l['action'] == 'add':
279 279 tag = 'ins'
280 280 else:
281 281 tag = 'del'
282 282 l['line'] = '%s<%s>%s</%s>%s' % (
283 283 l['line'][:start],
284 284 tag,
285 285 l['line'][start:last],
286 286 tag,
287 287 l['line'][last:]
288 288 )
289 289 do(line)
290 290 do(next_)
291 291
292 292 def _parse_udiff(self, inline_diff=True):
293 293 """
294 294 Parse the diff an return data for the template.
295 295 """
296 296 lineiter = self.lines
297 297 files = []
298 298 try:
299 299 line = lineiter.next()
300 300 while 1:
301 301 # continue until we found the old file
302 302 if not line.startswith('--- '):
303 303 line = lineiter.next()
304 304 continue
305 305
306 306 chunks = []
307 307 stats = [0, 0]
308 308 operation, filename, old_rev, new_rev = \
309 309 self._extract_rev(line, lineiter.next())
310 310 files.append({
311 311 'filename': filename,
312 312 'old_revision': old_rev,
313 313 'new_revision': new_rev,
314 314 'chunks': chunks,
315 315 'operation': operation,
316 316 'stats': stats,
317 317 })
318 318
319 319 line = lineiter.next()
320 320 while line:
321 321 match = self._chunk_re.match(line)
322 322 if not match:
323 323 break
324 324
325 325 lines = []
326 326 chunks.append(lines)
327 327
328 328 old_line, old_end, new_line, new_end = \
329 329 [int(x or 1) for x in match.groups()[:-1]]
330 330 old_line -= 1
331 331 new_line -= 1
332 332 gr = match.groups()
333 333 context = len(gr) == 5
334 334 old_end += old_line
335 335 new_end += new_line
336 336
337 337 if context:
338 338 # skip context only if it's first line
339 339 if int(gr[0]) > 1:
340 340 lines.append({
341 341 'old_lineno': '...',
342 342 'new_lineno': '...',
343 343 'action': 'context',
344 344 'line': line,
345 345 })
346 346
347 347 line = lineiter.next()
348 348 while old_line < old_end or new_line < new_end:
349 349 if line:
350 350 command, line = line[0], line[1:]
351 351 else:
352 352 command = ' '
353 353 affects_old = affects_new = False
354 354
355 355 # ignore those if we don't expect them
356 356 if command in '#@':
357 357 continue
358 358 elif command == '+':
359 359 affects_new = True
360 360 action = 'add'
361 361 stats[0] += 1
362 362 elif command == '-':
363 363 affects_old = True
364 364 action = 'del'
365 365 stats[1] += 1
366 366 else:
367 367 affects_old = affects_new = True
368 368 action = 'unmod'
369 369
370 370 if line.find('No newline at end of file') != -1:
371 371 lines.append({
372 372 'old_lineno': '...',
373 373 'new_lineno': '...',
374 374 'action': 'context',
375 375 'line': line
376 376 })
377 377
378 378 else:
379 379 old_line += affects_old
380 380 new_line += affects_new
381 381 lines.append({
382 382 'old_lineno': affects_old and old_line or '',
383 383 'new_lineno': affects_new and new_line or '',
384 384 'action': action,
385 385 'line': line
386 386 })
387 387
388 388 line = lineiter.next()
389 389
390 390 except StopIteration:
391 391 pass
392 392
393 393 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
394 394 if inline_diff is False:
395 395 return sorted(files, key=sorter)
396 396
397 397 # highlight inline changes
398 398 for diff_data in files:
399 399 for chunk in diff_data['chunks']:
400 400 lineiter = iter(chunk)
401 401 try:
402 402 while 1:
403 403 line = lineiter.next()
404 404 if line['action'] != 'unmod':
405 405 nextline = lineiter.next()
406 406 if nextline['action'] in ['unmod', 'context'] or \
407 407 nextline['action'] == line['action']:
408 408 continue
409 409 self.differ(line, nextline)
410 410 except StopIteration:
411 411 pass
412 412
413 413 return sorted(files, key=sorter)
414 414
415 415 def prepare(self, inline_diff=True):
416 416 """
417 417 Prepare the passed udiff for HTML rendering. It'l return a list
418 418 of dicts
419 419 """
420 420 return self._parse_udiff(inline_diff=inline_diff)
421 421
422 422 def _safe_id(self, idstring):
423 423 """Make a string safe for including in an id attribute.
424 424
425 425 The HTML spec says that id attributes 'must begin with
426 426 a letter ([A-Za-z]) and may be followed by any number
427 427 of letters, digits ([0-9]), hyphens ("-"), underscores
428 428 ("_"), colons (":"), and periods (".")'. These regexps
429 429 are slightly over-zealous, in that they remove colons
430 430 and periods unnecessarily.
431 431
432 432 Whitespace is transformed into underscores, and then
433 433 anything which is not a hyphen or a character that
434 434 matches \w (alphanumerics and underscore) is removed.
435 435
436 436 """
437 437 # Transform all whitespace to underscore
438 438 idstring = re.sub(r'\s', "_", '%s' % idstring)
439 439 # Remove everything that is not a hyphen or a member of \w
440 440 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
441 441 return idstring
442 442
443 443 def raw_diff(self):
444 444 """
445 445 Returns raw string as udiff
446 446 """
447 447 udiff_copy = self.copy_iterator()
448 448 if self.__format == 'gitdiff':
449 449 udiff_copy = self._parse_gitdiff(udiff_copy)
450 450 return u''.join(udiff_copy)
451 451
452 452 def as_html(self, table_class='code-difftable', line_class='line',
453 453 new_lineno_class='lineno old', old_lineno_class='lineno new',
454 454 code_class='code', enable_comments=False, diff_lines=None):
455 455 """
456 456 Return given diff as html table with customized css classes
457 457 """
458 458 def _link_to_if(condition, label, url):
459 459 """
460 460 Generates a link if condition is meet or just the label if not.
461 461 """
462 462
463 463 if condition:
464 464 return '''<a href="%(url)s">%(label)s</a>''' % {
465 465 'url': url,
466 466 'label': label
467 467 }
468 468 else:
469 469 return label
470 470 if diff_lines is None:
471 471 diff_lines = self.prepare()
472 472 _html_empty = True
473 473 _html = []
474 474 _html.append('''<table class="%(table_class)s">\n''' % {
475 475 'table_class': table_class
476 476 })
477 477 for diff in diff_lines:
478 478 for line in diff['chunks']:
479 479 _html_empty = False
480 480 for change in line:
481 481 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
482 482 'lc': line_class,
483 483 'action': change['action']
484 484 })
485 485 anchor_old_id = ''
486 486 anchor_new_id = ''
487 487 anchor_old = "%(filename)s_o%(oldline_no)s" % {
488 488 'filename': self._safe_id(diff['filename']),
489 489 'oldline_no': change['old_lineno']
490 490 }
491 491 anchor_new = "%(filename)s_n%(oldline_no)s" % {
492 492 'filename': self._safe_id(diff['filename']),
493 493 'oldline_no': change['new_lineno']
494 494 }
495 495 cond_old = (change['old_lineno'] != '...' and
496 496 change['old_lineno'])
497 497 cond_new = (change['new_lineno'] != '...' and
498 498 change['new_lineno'])
499 499 if cond_old:
500 500 anchor_old_id = 'id="%s"' % anchor_old
501 501 if cond_new:
502 502 anchor_new_id = 'id="%s"' % anchor_new
503 503 ###########################################################
504 504 # OLD LINE NUMBER
505 505 ###########################################################
506 506 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
507 507 'a_id': anchor_old_id,
508 508 'olc': old_lineno_class
509 509 })
510 510
511 511 _html.append('''%(link)s''' % {
512 512 'link': _link_to_if(True, change['old_lineno'],
513 513 '#%s' % anchor_old)
514 514 })
515 515 _html.append('''</td>\n''')
516 516 ###########################################################
517 517 # NEW LINE NUMBER
518 518 ###########################################################
519 519
520 520 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
521 521 'a_id': anchor_new_id,
522 522 'nlc': new_lineno_class
523 523 })
524 524
525 525 _html.append('''%(link)s''' % {
526 526 'link': _link_to_if(True, change['new_lineno'],
527 527 '#%s' % anchor_new)
528 528 })
529 529 _html.append('''</td>\n''')
530 530 ###########################################################
531 531 # CODE
532 532 ###########################################################
533 533 comments = '' if enable_comments else 'no-comment'
534 534 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
535 535 'cc': code_class,
536 536 'inc': comments
537 537 })
538 538 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
539 539 'code': change['line']
540 540 })
541 541 _html.append('''\t</td>''')
542 542 _html.append('''\n</tr>\n''')
543 543 _html.append('''</table>''')
544 544 if _html_empty:
545 545 return None
546 546 return ''.join(_html)
547 547
548 548 def stat(self):
549 549 """
550 550 Returns tuple of added, and removed lines for this instance
551 551 """
552 552 return self.adds, self.removes
553 553
554 554
555 555 class InMemoryBundleRepo(bundlerepository):
556 556 def __init__(self, ui, path, bundlestream):
557 557 self._tempparent = None
558 558 localrepo.localrepository.__init__(self, ui, path)
559 559 self.ui.setconfig('phases', 'publish', False)
560 560
561 561 self.bundle = bundlestream
562 562
563 563 # dict with the mapping 'filename' -> position in the bundle
564 564 self.bundlefilespos = {}
565 565
566 566
567 567 def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None):
568 568 """
569 General differ between branches, bookmarks or separate but releated
569 General differ between branches, bookmarks or separate but releated
570 570 repositories
571 571
572 572 :param org_repo:
573 573 :type org_repo:
574 574 :param org_ref:
575 575 :type org_ref:
576 576 :param other_repo:
577 577 :type other_repo:
578 578 :param other_ref:
579 579 :type other_ref:
580 580 """
581 581
582 582 bundlerepo = None
583 583 ignore_whitespace = False
584 584 context = 3
585 585 org_repo = org_repo.scm_instance._repo
586 586 other_repo = other_repo.scm_instance._repo
587 587 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
588 588 org_ref = org_ref[1]
589 589 other_ref = other_ref[1]
590 590
591 591 if org_repo != other_repo:
592 592
593 593 common, incoming, rheads = discovery_data
594 594
595 595 # create a bundle (uncompressed if other repo is not local)
596 596 if other_repo.capable('getbundle') and incoming:
597 597 # disable repo hooks here since it's just bundle !
598 598 # patch and reset hooks section of UI config to not run any
599 599 # hooks on fetching archives with subrepos
600 600 for k, _ in other_repo.ui.configitems('hooks'):
601 601 other_repo.ui.setconfig('hooks', k, None)
602 602
603 603 unbundle = other_repo.getbundle('incoming', common=common,
604 604 heads=rheads)
605 605
606 606 buf = io.BytesIO()
607 607 while True:
608 608 chunk = unbundle._stream.read(1024 * 4)
609 609 if not chunk:
610 610 break
611 611 buf.write(chunk)
612 612
613 613 buf.seek(0)
614 614 # replace chunked _stream with data that can do tell() and seek()
615 615 unbundle._stream = buf
616 616
617 617 ui = make_ui('db')
618 618 bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
619 619 bundlestream=unbundle)
620 620
621 621 return ''.join(patch.diff(bundlerepo or org_repo,
622 622 node1=org_repo[org_ref].node(),
623 623 node2=other_repo[other_ref].node(),
624 624 opts=opts))
625 625 else:
626 626 return ''.join(patch.diff(org_repo, node1=org_ref, node2=other_ref,
627 627 opts=opts))
@@ -1,613 +1,613 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.nodes
4 4 ~~~~~~~~~
5 5
6 6 Module holding everything related to vcs nodes.
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11 import os
12 12 import stat
13 13 import posixpath
14 14 import mimetypes
15 15
16 16 from pygments import lexers
17 17
18 18 from rhodecode.lib.vcs.utils.lazy import LazyProperty
19 19 from rhodecode.lib.vcs.utils import safe_unicode, safe_str
20 20 from rhodecode.lib.vcs.exceptions import NodeError
21 21 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
22 22 from rhodecode.lib.vcs.backends.base import EmptyChangeset
23 23
24 24
25 25 class NodeKind:
26 26 SUBMODULE = -1
27 27 DIR = 1
28 28 FILE = 2
29 29
30 30
31 31 class NodeState:
32 32 ADDED = u'added'
33 33 CHANGED = u'changed'
34 34 NOT_CHANGED = u'not changed'
35 35 REMOVED = u'removed'
36 36
37 37
38 38 class NodeGeneratorBase(object):
39 39 """
40 40 Base class for removed added and changed filenodes, it's a lazy generator
41 41 class that will create filenodes only on iteration or call
42 42
43 43 The len method doesn't need to create filenodes at all
44 44 """
45 45
46 46 def __init__(self, current_paths, cs):
47 47 self.cs = cs
48 48 self.current_paths = current_paths
49 49
50 50 def __call__(self):
51 51 return [n for n in self]
52 52
53 53 def __getslice__(self, i, j):
54 54 for p in self.current_paths[i:j]:
55 55 yield self.cs.get_node(p)
56 56
57 57 def __len__(self):
58 58 return len(self.current_paths)
59 59
60 60 def __iter__(self):
61 61 for p in self.current_paths:
62 62 yield self.cs.get_node(p)
63 63
64 64
65 65 class AddedFileNodesGenerator(NodeGeneratorBase):
66 66 """
67 67 Class holding Added files for current changeset
68 68 """
69 69 pass
70 70
71 71
72 72 class ChangedFileNodesGenerator(NodeGeneratorBase):
73 73 """
74 74 Class holding Changed files for current changeset
75 75 """
76 76 pass
77 77
78 78
79 79 class RemovedFileNodesGenerator(NodeGeneratorBase):
80 80 """
81 81 Class holding removed files for current changeset
82 82 """
83 83 def __iter__(self):
84 84 for p in self.current_paths:
85 85 yield RemovedFileNode(path=p)
86 86
87 87 def __getslice__(self, i, j):
88 88 for p in self.current_paths[i:j]:
89 89 yield RemovedFileNode(path=p)
90 90
91 91
92 92 class Node(object):
93 93 """
94 94 Simplest class representing file or directory on repository. SCM backends
95 95 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
96 96 directly.
97 97
98 98 Node's ``path`` cannot start with slash as we operate on *relative* paths
99 99 only. Moreover, every single node is identified by the ``path`` attribute,
100 100 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
101 101 """
102 102
103 103 def __init__(self, path, kind):
104 104 if path.startswith('/'):
105 105 raise NodeError("Cannot initialize Node objects with slash at "
106 106 "the beginning as only relative paths are supported")
107 107 self.path = path.rstrip('/')
108 108 if path == '' and kind != NodeKind.DIR:
109 109 raise NodeError("Only DirNode and its subclasses may be "
110 110 "initialized with empty path")
111 111 self.kind = kind
112 112 #self.dirs, self.files = [], []
113 113 if self.is_root() and not self.is_dir():
114 114 raise NodeError("Root node cannot be FILE kind")
115 115
116 116 @LazyProperty
117 117 def parent(self):
118 118 parent_path = self.get_parent_path()
119 119 if parent_path:
120 120 if self.changeset:
121 121 return self.changeset.get_node(parent_path)
122 122 return DirNode(parent_path)
123 123 return None
124 124
125 125 @LazyProperty
126 126 def unicode_path(self):
127 127 return safe_unicode(self.path)
128 128
129 129 @LazyProperty
130 130 def name(self):
131 131 """
132 132 Returns name of the node so if its path
133 133 then only last part is returned.
134 134 """
135 135 return safe_unicode(self.path.rstrip('/').split('/')[-1])
136 136
137 137 def _get_kind(self):
138 138 return self._kind
139 139
140 140 def _set_kind(self, kind):
141 141 if hasattr(self, '_kind'):
142 142 raise NodeError("Cannot change node's kind")
143 143 else:
144 144 self._kind = kind
145 145 # Post setter check (path's trailing slash)
146 146 if self.path.endswith('/'):
147 147 raise NodeError("Node's path cannot end with slash")
148 148
149 149 kind = property(_get_kind, _set_kind)
150 150
151 151 def __cmp__(self, other):
152 152 """
153 153 Comparator using name of the node, needed for quick list sorting.
154 154 """
155 155 kind_cmp = cmp(self.kind, other.kind)
156 156 if kind_cmp:
157 157 return kind_cmp
158 158 return cmp(self.name, other.name)
159 159
160 160 def __eq__(self, other):
161 161 for attr in ['name', 'path', 'kind']:
162 162 if getattr(self, attr) != getattr(other, attr):
163 163 return False
164 164 if self.is_file():
165 165 if self.content != other.content:
166 166 return False
167 167 else:
168 168 # For DirNode's check without entering each dir
169 169 self_nodes_paths = list(sorted(n.path for n in self.nodes))
170 170 other_nodes_paths = list(sorted(n.path for n in self.nodes))
171 171 if self_nodes_paths != other_nodes_paths:
172 172 return False
173 173 return True
174 174
175 175 def __nq__(self, other):
176 176 return not self.__eq__(other)
177 177
178 178 def __repr__(self):
179 179 return '<%s %r>' % (self.__class__.__name__, self.path)
180 180
181 181 def __str__(self):
182 182 return self.__repr__()
183 183
184 184 def __unicode__(self):
185 185 return self.name
186 186
187 187 def get_parent_path(self):
188 188 """
189 189 Returns node's parent path or empty string if node is root.
190 190 """
191 191 if self.is_root():
192 192 return ''
193 193 return posixpath.dirname(self.path.rstrip('/')) + '/'
194 194
195 195 def is_file(self):
196 196 """
197 197 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
198 198 otherwise.
199 199 """
200 200 return self.kind == NodeKind.FILE
201 201
202 202 def is_dir(self):
203 203 """
204 204 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
205 205 otherwise.
206 206 """
207 207 return self.kind == NodeKind.DIR
208 208
209 209 def is_root(self):
210 210 """
211 211 Returns ``True`` if node is a root node and ``False`` otherwise.
212 212 """
213 213 return self.kind == NodeKind.DIR and self.path == ''
214 214
215 215 def is_submodule(self):
216 216 """
217 217 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
218 218 otherwise.
219 219 """
220 220 return self.kind == NodeKind.SUBMODULE
221 221
222 222 @LazyProperty
223 223 def added(self):
224 224 return self.state is NodeState.ADDED
225 225
226 226 @LazyProperty
227 227 def changed(self):
228 228 return self.state is NodeState.CHANGED
229 229
230 230 @LazyProperty
231 231 def not_changed(self):
232 232 return self.state is NodeState.NOT_CHANGED
233 233
234 234 @LazyProperty
235 235 def removed(self):
236 236 return self.state is NodeState.REMOVED
237 237
238 238
239 239 class FileNode(Node):
240 240 """
241 241 Class representing file nodes.
242 242
243 243 :attribute: path: path to the node, relative to repostiory's root
244 244 :attribute: content: if given arbitrary sets content of the file
245 245 :attribute: changeset: if given, first time content is accessed, callback
246 246 :attribute: mode: octal stat mode for a node. Default is 0100644.
247 247 """
248 248
249 249 def __init__(self, path, content=None, changeset=None, mode=None):
250 250 """
251 251 Only one of ``content`` and ``changeset`` may be given. Passing both
252 252 would raise ``NodeError`` exception.
253 253
254 254 :param path: relative path to the node
255 255 :param content: content may be passed to constructor
256 256 :param changeset: if given, will use it to lazily fetch content
257 257 :param mode: octal representation of ST_MODE (i.e. 0100644)
258 258 """
259 259
260 260 if content and changeset:
261 261 raise NodeError("Cannot use both content and changeset")
262 262 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
263 263 self.changeset = changeset
264 264 self._content = content
265 265 self._mode = mode or 0100644
266 266
267 267 @LazyProperty
268 268 def mode(self):
269 269 """
270 270 Returns lazily mode of the FileNode. If ``changeset`` is not set, would
271 271 use value given at initialization or 0100644 (default).
272 272 """
273 273 if self.changeset:
274 274 mode = self.changeset.get_file_mode(self.path)
275 275 else:
276 276 mode = self._mode
277 277 return mode
278 278
279 279 @property
280 280 def content(self):
281 281 """
282 282 Returns lazily content of the FileNode. If possible, would try to
283 283 decode content from UTF-8.
284 284 """
285 285 if self.changeset:
286 286 content = self.changeset.get_file_content(self.path)
287 287 else:
288 288 content = self._content
289 289
290 290 if bool(content and '\0' in content):
291 291 return content
292 292 return safe_unicode(content)
293 293
294 294 @LazyProperty
295 295 def size(self):
296 296 if self.changeset:
297 297 return self.changeset.get_file_size(self.path)
298 298 raise NodeError("Cannot retrieve size of the file without related "
299 299 "changeset attribute")
300 300
301 301 @LazyProperty
302 302 def message(self):
303 303 if self.changeset:
304 304 return self.last_changeset.message
305 305 raise NodeError("Cannot retrieve message of the file without related "
306 306 "changeset attribute")
307 307
308 308 @LazyProperty
309 309 def last_changeset(self):
310 310 if self.changeset:
311 311 return self.changeset.get_file_changeset(self.path)
312 312 raise NodeError("Cannot retrieve last changeset of the file without "
313 313 "related changeset attribute")
314 314
315 315 def get_mimetype(self):
316 316 """
317 317 Mimetype is calculated based on the file's content. If ``_mimetype``
318 318 attribute is available, it will be returned (backends which store
319 319 mimetypes or can easily recognize them, should set this private
320 320 attribute to indicate that type should *NOT* be calculated).
321 321 """
322 322 if hasattr(self, '_mimetype'):
323 323 if (isinstance(self._mimetype, (tuple, list,)) and
324 324 len(self._mimetype) == 2):
325 325 return self._mimetype
326 326 else:
327 327 raise NodeError('given _mimetype attribute must be an 2 '
328 328 'element list or tuple')
329 329
330 330 mtype, encoding = mimetypes.guess_type(self.name)
331 331
332 332 if mtype is None:
333 333 if self.is_binary:
334 334 mtype = 'application/octet-stream'
335 335 encoding = None
336 336 else:
337 337 mtype = 'text/plain'
338 338 encoding = None
339 339 return mtype, encoding
340 340
341 341 @LazyProperty
342 342 def mimetype(self):
343 343 """
344 344 Wrapper around full mimetype info. It returns only type of fetched
345 345 mimetype without the encoding part. use get_mimetype function to fetch
346 346 full set of (type,encoding)
347 347 """
348 348 return self.get_mimetype()[0]
349 349
350 350 @LazyProperty
351 351 def mimetype_main(self):
352 352 return self.mimetype.split('/')[0]
353 353
354 354 @LazyProperty
355 355 def lexer(self):
356 356 """
357 357 Returns pygment's lexer class. Would try to guess lexer taking file's
358 358 content, name and mimetype.
359 359 """
360 360 try:
361 361 lexer = lexers.guess_lexer_for_filename(self.name, self.content)
362 362 except lexers.ClassNotFound:
363 363 lexer = lexers.TextLexer()
364 364 # returns first alias
365 365 return lexer
366 366
367 367 @LazyProperty
368 368 def lexer_alias(self):
369 369 """
370 370 Returns first alias of the lexer guessed for this file.
371 371 """
372 372 return self.lexer.aliases[0]
373 373
374 374 @LazyProperty
375 375 def history(self):
376 376 """
377 377 Returns a list of changeset for this file in which the file was changed
378 378 """
379 379 if self.changeset is None:
380 380 raise NodeError('Unable to get changeset for this FileNode')
381 381 return self.changeset.get_file_history(self.path)
382 382
383 383 @LazyProperty
384 384 def annotate(self):
385 385 """
386 386 Returns a list of three element tuples with lineno,changeset and line
387 387 """
388 388 if self.changeset is None:
389 389 raise NodeError('Unable to get changeset for this FileNode')
390 390 return self.changeset.get_file_annotate(self.path)
391 391
392 392 @LazyProperty
393 393 def state(self):
394 394 if not self.changeset:
395 395 raise NodeError("Cannot check state of the node if it's not "
396 396 "linked with changeset")
397 397 elif self.path in (node.path for node in self.changeset.added):
398 398 return NodeState.ADDED
399 399 elif self.path in (node.path for node in self.changeset.changed):
400 400 return NodeState.CHANGED
401 401 else:
402 402 return NodeState.NOT_CHANGED
403 403
404 404 @property
405 405 def is_binary(self):
406 406 """
407 407 Returns True if file has binary content.
408 408 """
409 409 _bin = '\0' in self.content
410 410 return _bin
411 411
412 412 @LazyProperty
413 413 def extension(self):
414 414 """Returns filenode extension"""
415 415 return self.name.split('.')[-1]
416 416
417 417 def is_executable(self):
418 418 """
419 419 Returns ``True`` if file has executable flag turned on.
420 420 """
421 421 return bool(self.mode & stat.S_IXUSR)
422 422
423 423 def __repr__(self):
424 424 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
425 425 getattr(self.changeset, 'short_id', ''))
426 426
427 427
428 428 class RemovedFileNode(FileNode):
429 429 """
430 430 Dummy FileNode class - trying to access any public attribute except path,
431 431 name, kind or state (or methods/attributes checking those two) would raise
432 432 RemovedFileNodeError.
433 433 """
434 434 ALLOWED_ATTRIBUTES = [
435 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
435 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
436 436 'added', 'changed', 'not_changed', 'removed'
437 437 ]
438 438
439 439 def __init__(self, path):
440 440 """
441 441 :param path: relative path to the node
442 442 """
443 443 super(RemovedFileNode, self).__init__(path=path)
444 444
445 445 def __getattribute__(self, attr):
446 446 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
447 447 return super(RemovedFileNode, self).__getattribute__(attr)
448 448 raise RemovedFileNodeError("Cannot access attribute %s on "
449 449 "RemovedFileNode" % attr)
450 450
451 451 @LazyProperty
452 452 def state(self):
453 453 return NodeState.REMOVED
454 454
455 455
456 456 class DirNode(Node):
457 457 """
458 458 DirNode stores list of files and directories within this node.
459 459 Nodes may be used standalone but within repository context they
460 460 lazily fetch data within same repositorty's changeset.
461 461 """
462 462
463 463 def __init__(self, path, nodes=(), changeset=None):
464 464 """
465 465 Only one of ``nodes`` and ``changeset`` may be given. Passing both
466 466 would raise ``NodeError`` exception.
467 467
468 468 :param path: relative path to the node
469 469 :param nodes: content may be passed to constructor
470 470 :param changeset: if given, will use it to lazily fetch content
471 471 :param size: always 0 for ``DirNode``
472 472 """
473 473 if nodes and changeset:
474 474 raise NodeError("Cannot use both nodes and changeset")
475 475 super(DirNode, self).__init__(path, NodeKind.DIR)
476 476 self.changeset = changeset
477 477 self._nodes = nodes
478 478
479 479 @LazyProperty
480 480 def content(self):
481 481 raise NodeError("%s represents a dir and has no ``content`` attribute"
482 482 % self)
483 483
484 484 @LazyProperty
485 485 def nodes(self):
486 486 if self.changeset:
487 487 nodes = self.changeset.get_nodes(self.path)
488 488 else:
489 489 nodes = self._nodes
490 490 self._nodes_dict = dict((node.path, node) for node in nodes)
491 491 return sorted(nodes)
492 492
493 493 @LazyProperty
494 494 def files(self):
495 495 return sorted((node for node in self.nodes if node.is_file()))
496 496
497 497 @LazyProperty
498 498 def dirs(self):
499 499 return sorted((node for node in self.nodes if node.is_dir()))
500 500
501 501 def __iter__(self):
502 502 for node in self.nodes:
503 503 yield node
504 504
505 505 def get_node(self, path):
506 506 """
507 507 Returns node from within this particular ``DirNode``, so it is now
508 508 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
509 509 'docs'. In order to access deeper nodes one must fetch nodes between
510 510 them first - this would work::
511 511
512 512 docs = root.get_node('docs')
513 513 docs.get_node('api').get_node('index.rst')
514 514
515 515 :param: path - relative to the current node
516 516
517 517 .. note::
518 518 To access lazily (as in example above) node have to be initialized
519 519 with related changeset object - without it node is out of
520 520 context and may know nothing about anything else than nearest
521 521 (located at same level) nodes.
522 522 """
523 523 try:
524 524 path = path.rstrip('/')
525 525 if path == '':
526 526 raise NodeError("Cannot retrieve node without path")
527 527 self.nodes # access nodes first in order to set _nodes_dict
528 528 paths = path.split('/')
529 529 if len(paths) == 1:
530 530 if not self.is_root():
531 531 path = '/'.join((self.path, paths[0]))
532 532 else:
533 533 path = paths[0]
534 534 return self._nodes_dict[path]
535 535 elif len(paths) > 1:
536 536 if self.changeset is None:
537 537 raise NodeError("Cannot access deeper "
538 538 "nodes without changeset")
539 539 else:
540 540 path1, path2 = paths[0], '/'.join(paths[1:])
541 541 return self.get_node(path1).get_node(path2)
542 542 else:
543 543 raise KeyError
544 544 except KeyError:
545 545 raise NodeError("Node does not exist at %s" % path)
546 546
547 547 @LazyProperty
548 548 def state(self):
549 549 raise NodeError("Cannot access state of DirNode")
550 550
551 551 @LazyProperty
552 552 def size(self):
553 553 size = 0
554 554 for root, dirs, files in self.changeset.walk(self.path):
555 555 for f in files:
556 556 size += f.size
557 557
558 558 return size
559 559
560 560 def __repr__(self):
561 561 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
562 562 getattr(self.changeset, 'short_id', ''))
563 563
564 564
565 565 class RootNode(DirNode):
566 566 """
567 567 DirNode being the root node of the repository.
568 568 """
569 569
570 570 def __init__(self, nodes=(), changeset=None):
571 571 super(RootNode, self).__init__(path='', nodes=nodes,
572 572 changeset=changeset)
573 573
574 574 def __repr__(self):
575 575 return '<%s>' % self.__class__.__name__
576 576
577 577
578 578 class SubModuleNode(Node):
579 579 """
580 580 represents a SubModule of Git or SubRepo of Mercurial
581 581 """
582 582 is_binary = False
583 583 size = 0
584 584
585 585 def __init__(self, name, url=None, changeset=None, alias=None):
586 586 self.path = name
587 587 self.kind = NodeKind.SUBMODULE
588 588 self.alias = alias
589 589 # we have to use emptyChangeset here since this can point to svn/git/hg
590 590 # submodules we cannot get from repository
591 591 self.changeset = EmptyChangeset(str(changeset), alias=alias)
592 592 self.url = url or self._extract_submodule_url()
593 593
594 594 def __repr__(self):
595 595 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
596 596 getattr(self.changeset, 'short_id', ''))
597 597
598 598 def _extract_submodule_url(self):
599 599 if self.alias == 'git':
600 600 #TODO: find a way to parse gits submodule file and extract the
601 601 # linking URL
602 602 return self.path
603 603 if self.alias == 'hg':
604 604 return self.path
605 605
606 606 @LazyProperty
607 607 def name(self):
608 608 """
609 609 Returns name of the node so if its path
610 610 then only last part is returned.
611 611 """
612 612 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
613 613 return u'%s @ %s' % (org, self.changeset.short_id)
@@ -1,15 +1,15 b''
1 1 """
2 2 Mercurial libs compatibility
3 3 """
4 4
5 5 from mercurial import archival, merge as hg_merge, patch, ui
6 6 from mercurial.commands import clone, nullid, pull
7 7 from mercurial.context import memctx, memfilectx
8 8 from mercurial.error import RepoError, RepoLookupError, Abort
9 9 from mercurial.hgweb.common import get_contact
10 10 from mercurial.localrepo import localrepository
11 11 from mercurial.match import match
12 12 from mercurial.mdiff import diffopts
13 13 from mercurial.node import hex
14 14 from mercurial.encoding import tolocal
15 from mercurial import discovery No newline at end of file
15 from mercurial import discovery
@@ -1,139 +1,139 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.changeset_status
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6
7 7 :created_on: Apr 30, 2012
8 8 :author: marcink
9 9 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
10 10 :license: GPLv3, see COPYING for more details.
11 11 """
12 12 # This program is free software: you can redistribute it and/or modify
13 13 # it under the terms of the GNU General Public License as published by
14 14 # the Free Software Foundation, either version 3 of the License, or
15 15 # (at your option) any later version.
16 16 #
17 17 # This program is distributed in the hope that it will be useful,
18 18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 20 # GNU General Public License for more details.
21 21 #
22 22 # You should have received a copy of the GNU General Public License
23 23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 24
25 25
26 26 import logging
27 27
28 28 from rhodecode.model import BaseModel
29 29 from rhodecode.model.db import ChangesetStatus, PullRequest
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class ChangesetStatusModel(BaseModel):
35 35
36 36 def __get_changeset_status(self, changeset_status):
37 37 return self._get_instance(ChangesetStatus, changeset_status)
38 38
39 39 def __get_pull_request(self, pull_request):
40 40 return self._get_instance(PullRequest, pull_request)
41 41
42 42 def get_status(self, repo, revision=None, pull_request=None):
43 43 """
44 44 Returns latest status of changeset for given revision or for given
45 45 pull request. Statuses are versioned inside a table itself and
46 46 version == 0 is always the current one
47 47
48 48 :param repo:
49 49 :type repo:
50 50 :param revision: 40char hash or None
51 51 :type revision: str
52 52 :param pull_request: pull_request reference
53 53 :type:
54 54 """
55 55 repo = self._get_repo(repo)
56 56
57 57 q = ChangesetStatus.query()\
58 58 .filter(ChangesetStatus.repo == repo)\
59 59 .filter(ChangesetStatus.version == 0)
60 60
61 61 if revision:
62 62 q = q.filter(ChangesetStatus.revision == revision)
63 63 elif pull_request:
64 64 pull_request = self.__get_pull_request(pull_request)
65 65 q = q.filter(ChangesetStatus.pull_request == pull_request)
66 66 else:
67 67 raise Exception('Please specify revision or pull_request')
68 68
69 69 # need to use first here since there can be multiple statuses
70 70 # returned from pull_request
71 71 status = q.first()
72 72 status = status.status if status else status
73 73 st = status or ChangesetStatus.DEFAULT
74 74 return str(st)
75 75
76 76 def set_status(self, repo, status, user, comment, revision=None,
77 77 pull_request=None):
78 78 """
79 79 Creates new status for changeset or updates the old ones bumping their
80 80 version, leaving the current status at
81 81
82 82 :param repo:
83 83 :type repo:
84 84 :param revision:
85 85 :type revision:
86 86 :param status:
87 87 :type status:
88 88 :param user:
89 89 :type user:
90 90 :param comment:
91 91 :type comment:
92 92 """
93 93 repo = self._get_repo(repo)
94 94
95 95 q = ChangesetStatus.query()
96 96
97 97 if revision:
98 98 q = q.filter(ChangesetStatus.repo == repo)
99 99 q = q.filter(ChangesetStatus.revision == revision)
100 100 elif pull_request:
101 101 pull_request = self.__get_pull_request(pull_request)
102 102 q = q.filter(ChangesetStatus.repo == pull_request.org_repo)
103 103 q = q.filter(ChangesetStatus.pull_request == pull_request)
104 104 cur_statuses = q.all()
105 105
106 106 if cur_statuses:
107 107 for st in cur_statuses:
108 108 st.version += 1
109 109 self.sa.add(st)
110 110
111 111 def _create_status(user, repo, status, comment, revision, pull_request):
112 112 new_status = ChangesetStatus()
113 113 new_status.author = self._get_user(user)
114 114 new_status.repo = self._get_repo(repo)
115 115 new_status.status = status
116 116 new_status.comment = comment
117 117 new_status.revision = revision
118 118 new_status.pull_request = pull_request
119 119 return new_status
120 120
121 121 if revision:
122 122 new_status = _create_status(user=user, repo=repo, status=status,
123 comment=comment, revision=revision,
123 comment=comment, revision=revision,
124 124 pull_request=None)
125 125 self.sa.add(new_status)
126 126 return new_status
127 127 elif pull_request:
128 128 #pull request can have more than one revision associated to it
129 129 #we need to create new version for each one
130 130 new_statuses = []
131 131 repo = pull_request.org_repo
132 132 for rev in pull_request.revisions:
133 133 new_status = _create_status(user=user, repo=repo,
134 134 status=status, comment=comment,
135 135 revision=rev,
136 136 pull_request=pull_request)
137 137 new_statuses.append(new_status)
138 138 self.sa.add(new_status)
139 139 return new_statuses
@@ -1,211 +1,211 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.comment
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 comments model for RhodeCode
7 7
8 8 :created_on: Nov 11, 2011
9 9 :author: marcink
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
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons.i18n.translation import _
30 30 from sqlalchemy.util.compat import defaultdict
31 31
32 32 from rhodecode.lib.utils2 import extract_mentioned_users, safe_unicode
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.model import BaseModel
35 35 from rhodecode.model.db import ChangesetComment, User, Repository, \
36 36 Notification, PullRequest
37 37 from rhodecode.model.notification import NotificationModel
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class ChangesetCommentsModel(BaseModel):
43 43
44 44 def __get_changeset_comment(self, changeset_comment):
45 45 return self._get_instance(ChangesetComment, changeset_comment)
46 46
47 47 def __get_pull_request(self, pull_request):
48 48 return self._get_instance(PullRequest, pull_request)
49 49
50 50 def _extract_mentions(self, s):
51 51 user_objects = []
52 52 for username in extract_mentioned_users(s):
53 53 user_obj = User.get_by_username(username, case_insensitive=True)
54 54 if user_obj:
55 55 user_objects.append(user_obj)
56 56 return user_objects
57 57
58 58 def create(self, text, repo_id, user_id, revision=None, pull_request=None,
59 59 f_path=None, line_no=None, status_change=None):
60 60 """
61 61 Creates new comment for changeset or pull request.
62 IF status_change is not none this comment is associated with a
62 IF status_change is not none this comment is associated with a
63 63 status change of changeset or changesets associated with pull request
64 64
65 65 :param text:
66 66 :param repo_id:
67 67 :param user_id:
68 68 :param revision:
69 69 :param pull_request:
70 70 :param f_path:
71 71 :param line_no:
72 72 :param status_change:
73 73 """
74 74 if not text:
75 75 return
76 76
77 77 repo = Repository.get(repo_id)
78 78 comment = ChangesetComment()
79 79 comment.repo = repo
80 80 comment.user_id = user_id
81 81 comment.text = text
82 82 comment.f_path = f_path
83 83 comment.line_no = line_no
84 84
85 85 if revision:
86 86 cs = repo.scm_instance.get_changeset(revision)
87 87 desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256))
88 88 author_email = cs.author_email
89 89 comment.revision = revision
90 90 elif pull_request:
91 91 pull_request = self.__get_pull_request(pull_request)
92 92 comment.pull_request = pull_request
93 93 desc = ''
94 94 else:
95 95 raise Exception('Please specify revision or pull_request_id')
96 96
97 97 self.sa.add(comment)
98 98 self.sa.flush()
99 99
100 100 # make notification
101 101 line = ''
102 102 body = text
103 103
104 104 #changeset
105 105 if revision:
106 106 if line_no:
107 107 line = _('on line %s') % line_no
108 108 subj = safe_unicode(
109 109 h.link_to('Re commit: %(commit_desc)s %(line)s' % \
110 110 {'commit_desc': desc, 'line': line},
111 111 h.url('changeset_home', repo_name=repo.repo_name,
112 112 revision=revision,
113 113 anchor='comment-%s' % comment.comment_id,
114 114 qualified=True,
115 115 )
116 116 )
117 117 )
118 118 notification_type = Notification.TYPE_CHANGESET_COMMENT
119 119 # get the current participants of this changeset
120 120 recipients = ChangesetComment.get_users(revision=revision)
121 121 # add changeset author if it's in rhodecode system
122 122 recipients += [User.get_by_email(author_email)]
123 123 #pull request
124 124 elif pull_request:
125 125 #TODO: make this something usefull
126 126 subj = 'commented on pull request something...'
127 127 notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
128 128 # get the current participants of this pull request
129 129 recipients = ChangesetComment.get_users(pull_request_id=
130 130 pull_request.pull_request_id)
131 131 # add pull request author
132 132 recipients += [pull_request.author]
133 133
134 134 # create notification objects, and emails
135 135 NotificationModel().create(
136 136 created_by=user_id, subject=subj, body=body,
137 137 recipients=recipients, type_=notification_type,
138 138 email_kwargs={'status_change': status_change}
139 139 )
140 140
141 141 mention_recipients = set(self._extract_mentions(body))\
142 142 .difference(recipients)
143 143 if mention_recipients:
144 144 subj = _('[Mention]') + ' ' + subj
145 145 NotificationModel().create(
146 146 created_by=user_id, subject=subj, body=body,
147 147 recipients=mention_recipients,
148 148 type_=notification_type,
149 149 email_kwargs={'status_change': status_change}
150 150 )
151 151
152 152 return comment
153 153
154 154 def delete(self, comment):
155 155 """
156 156 Deletes given comment
157 157
158 158 :param comment_id:
159 159 """
160 160 comment = self.__get_changeset_comment(comment)
161 161 self.sa.delete(comment)
162 162
163 163 return comment
164 164
165 165 def get_comments(self, repo_id, revision=None, pull_request=None):
166 166 """
167 167 Get's main comments based on revision or pull_request_id
168 168
169 169 :param repo_id:
170 170 :type repo_id:
171 171 :param revision:
172 172 :type revision:
173 173 :param pull_request:
174 174 :type pull_request:
175 175 """
176 176
177 177 q = ChangesetComment.query()\
178 178 .filter(ChangesetComment.repo_id == repo_id)\
179 179 .filter(ChangesetComment.line_no == None)\
180 180 .filter(ChangesetComment.f_path == None)
181 181 if revision:
182 182 q = q.filter(ChangesetComment.revision == revision)
183 183 elif pull_request:
184 184 pull_request = self.__get_pull_request(pull_request)
185 185 q = q.filter(ChangesetComment.pull_request == pull_request)
186 186 else:
187 187 raise Exception('Please specify revision or pull_request')
188 188 return q.all()
189 189
190 190 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
191 191 q = self.sa.query(ChangesetComment)\
192 192 .filter(ChangesetComment.repo_id == repo_id)\
193 193 .filter(ChangesetComment.line_no != None)\
194 194 .filter(ChangesetComment.f_path != None)\
195 195 .order_by(ChangesetComment.comment_id.asc())\
196 196
197 197 if revision:
198 198 q = q.filter(ChangesetComment.revision == revision)
199 199 elif pull_request:
200 200 pull_request = self.__get_pull_request(pull_request)
201 201 q = q.filter(ChangesetComment.pull_request == pull_request)
202 202 else:
203 203 raise Exception('Please specify revision or pull_request_id')
204 204
205 205 comments = q.all()
206 206
207 207 paths = defaultdict(lambda: defaultdict(list))
208 208
209 209 for co in comments:
210 210 paths[co.f_path][co.line_no].append(co)
211 211 return paths.items()
@@ -1,613 +1,613 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.user
4 4 ~~~~~~~~~~~~~~~~~~~~
5 5
6 6 users model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
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
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons import url
30 30 from pylons.i18n.translation import _
31 31
32 32 from rhodecode.lib.utils2 import safe_unicode, generate_api_key
33 33 from rhodecode.lib.caching_query import FromCache
34 34
35 35 from rhodecode.model import BaseModel
36 36 from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \
37 37 UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \
38 38 Notification, RepoGroup, UserRepoGroupToPerm, UsersGroupRepoGroupToPerm, \
39 39 UserEmailMap
40 40 from rhodecode.lib.exceptions import DefaultUserException, \
41 41 UserOwnsReposException
42 42
43 43 from sqlalchemy.exc import DatabaseError
44 44
45 45 from sqlalchemy.orm import joinedload
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 PERM_WEIGHTS = {
51 51 'repository.none': 0,
52 52 'repository.read': 1,
53 53 'repository.write': 3,
54 54 'repository.admin': 4,
55 55 'group.none': 0,
56 56 'group.read': 1,
57 57 'group.write': 3,
58 58 'group.admin': 4,
59 59 }
60 60
61 61
62 62 class UserModel(BaseModel):
63 63
64 64 def get(self, user_id, cache=False):
65 65 user = self.sa.query(User)
66 66 if cache:
67 67 user = user.options(FromCache("sql_cache_short",
68 68 "get_user_%s" % user_id))
69 69 return user.get(user_id)
70 70
71 71 def get_user(self, user):
72 72 return self._get_user(user)
73 73
74 74 def get_by_username(self, username, cache=False, case_insensitive=False):
75 75
76 76 if case_insensitive:
77 77 user = self.sa.query(User).filter(User.username.ilike(username))
78 78 else:
79 79 user = self.sa.query(User)\
80 80 .filter(User.username == username)
81 81 if cache:
82 82 user = user.options(FromCache("sql_cache_short",
83 83 "get_user_%s" % username))
84 84 return user.scalar()
85 85
86 86 def get_by_api_key(self, api_key, cache=False):
87 87 return User.get_by_api_key(api_key, cache)
88 88
89 89 def create(self, form_data):
90 90 from rhodecode.lib.auth import get_crypt_password
91 91 try:
92 92 new_user = User()
93 93 for k, v in form_data.items():
94 94 if k == 'password':
95 95 v = get_crypt_password(v)
96 96 setattr(new_user, k, v)
97 97
98 98 new_user.api_key = generate_api_key(form_data['username'])
99 99 self.sa.add(new_user)
100 100 return new_user
101 101 except:
102 102 log.error(traceback.format_exc())
103 103 raise
104 104
105 105 def create_or_update(self, username, password, email, name, lastname,
106 106 active=True, admin=False, ldap_dn=None):
107 107 """
108 108 Creates a new instance if not found, or updates current one
109 109
110 110 :param username:
111 111 :param password:
112 112 :param email:
113 113 :param active:
114 114 :param name:
115 115 :param lastname:
116 116 :param active:
117 117 :param admin:
118 118 :param ldap_dn:
119 119 """
120 120
121 121 from rhodecode.lib.auth import get_crypt_password
122 122
123 123 log.debug('Checking for %s account in RhodeCode database' % username)
124 124 user = User.get_by_username(username, case_insensitive=True)
125 125 if user is None:
126 126 log.debug('creating new user %s' % username)
127 127 new_user = User()
128 128 else:
129 129 log.debug('updating user %s' % username)
130 130 new_user = user
131 131
132 132 try:
133 133 new_user.username = username
134 134 new_user.admin = admin
135 135 new_user.password = get_crypt_password(password)
136 136 new_user.api_key = generate_api_key(username)
137 137 new_user.email = email
138 138 new_user.active = active
139 139 new_user.ldap_dn = safe_unicode(ldap_dn) if ldap_dn else None
140 140 new_user.name = name
141 141 new_user.lastname = lastname
142 142 self.sa.add(new_user)
143 143 return new_user
144 144 except (DatabaseError,):
145 145 log.error(traceback.format_exc())
146 146 raise
147 147
148 148 def create_for_container_auth(self, username, attrs):
149 149 """
150 150 Creates the given user if it's not already in the database
151 151
152 152 :param username:
153 153 :param attrs:
154 154 """
155 155 if self.get_by_username(username, case_insensitive=True) is None:
156 156
157 157 # autogenerate email for container account without one
158 158 generate_email = lambda usr: '%s@container_auth.account' % usr
159 159
160 160 try:
161 161 new_user = User()
162 162 new_user.username = username
163 163 new_user.password = None
164 164 new_user.api_key = generate_api_key(username)
165 165 new_user.email = attrs['email']
166 166 new_user.active = attrs.get('active', True)
167 167 new_user.name = attrs['name'] or generate_email(username)
168 168 new_user.lastname = attrs['lastname']
169 169
170 170 self.sa.add(new_user)
171 171 return new_user
172 172 except (DatabaseError,):
173 173 log.error(traceback.format_exc())
174 174 self.sa.rollback()
175 175 raise
176 176 log.debug('User %s already exists. Skipping creation of account'
177 177 ' for container auth.', username)
178 178 return None
179 179
180 180 def create_ldap(self, username, password, user_dn, attrs):
181 181 """
182 182 Checks if user is in database, if not creates this user marked
183 183 as ldap user
184 184
185 185 :param username:
186 186 :param password:
187 187 :param user_dn:
188 188 :param attrs:
189 189 """
190 190 from rhodecode.lib.auth import get_crypt_password
191 191 log.debug('Checking for such ldap account in RhodeCode database')
192 192 if self.get_by_username(username, case_insensitive=True) is None:
193 193
194 194 # autogenerate email for ldap account without one
195 195 generate_email = lambda usr: '%s@ldap.account' % usr
196 196
197 197 try:
198 198 new_user = User()
199 199 username = username.lower()
200 200 # add ldap account always lowercase
201 201 new_user.username = username
202 202 new_user.password = get_crypt_password(password)
203 203 new_user.api_key = generate_api_key(username)
204 204 new_user.email = attrs['email'] or generate_email(username)
205 205 new_user.active = attrs.get('active', True)
206 206 new_user.ldap_dn = safe_unicode(user_dn)
207 207 new_user.name = attrs['name']
208 208 new_user.lastname = attrs['lastname']
209 209
210 210 self.sa.add(new_user)
211 211 return new_user
212 212 except (DatabaseError,):
213 213 log.error(traceback.format_exc())
214 214 self.sa.rollback()
215 215 raise
216 216 log.debug('this %s user exists skipping creation of ldap account',
217 217 username)
218 218 return None
219 219
220 220 def create_registration(self, form_data):
221 221 from rhodecode.model.notification import NotificationModel
222 222
223 223 try:
224 224 form_data['admin'] = False
225 225 new_user = self.create(form_data)
226 226
227 227 self.sa.add(new_user)
228 228 self.sa.flush()
229 229
230 230 # notification to admins
231 231 subject = _('new user registration')
232 232 body = ('New user registration\n'
233 233 '---------------------\n'
234 234 '- Username: %s\n'
235 235 '- Full Name: %s\n'
236 236 '- Email: %s\n')
237 237 body = body % (new_user.username, new_user.full_name,
238 238 new_user.email)
239 239 edit_url = url('edit_user', id=new_user.user_id, qualified=True)
240 240 kw = {'registered_user_url': edit_url}
241 241 NotificationModel().create(created_by=new_user, subject=subject,
242 242 body=body, recipients=None,
243 243 type_=Notification.TYPE_REGISTRATION,
244 244 email_kwargs=kw)
245 245
246 246 except:
247 247 log.error(traceback.format_exc())
248 248 raise
249 249
250 250 def update(self, user_id, form_data):
251 251 try:
252 252 user = self.get(user_id, cache=False)
253 253 if user.username == 'default':
254 254 raise DefaultUserException(
255 255 _("You can't Edit this user since it's"
256 256 " crucial for entire application"))
257 257
258 258 for k, v in form_data.items():
259 259 if k == 'new_password' and v != '':
260 260 user.password = v
261 261 user.api_key = generate_api_key(user.username)
262 262 else:
263 263 setattr(user, k, v)
264 264
265 265 self.sa.add(user)
266 266 except:
267 267 log.error(traceback.format_exc())
268 268 raise
269 269
270 270 def update_my_account(self, user_id, form_data):
271 271 from rhodecode.lib.auth import get_crypt_password
272 272 try:
273 273 user = self.get(user_id, cache=False)
274 274 if user.username == 'default':
275 275 raise DefaultUserException(
276 276 _("You can't Edit this user since it's"
277 277 " crucial for entire application")
278 278 )
279 279 for k, v in form_data.items():
280 280 if k == 'new_password' and v != '':
281 281 user.password = get_crypt_password(v)
282 282 user.api_key = generate_api_key(user.username)
283 283 else:
284 284 if k not in ['admin', 'active']:
285 285 setattr(user, k, v)
286 286
287 287 self.sa.add(user)
288 288 except:
289 289 log.error(traceback.format_exc())
290 290 raise
291 291
292 292 def delete(self, user):
293 293 user = self._get_user(user)
294 294
295 295 try:
296 296 if user.username == 'default':
297 297 raise DefaultUserException(
298 298 _(u"You can't remove this user since it's"
299 299 " crucial for entire application")
300 300 )
301 301 if user.repositories:
302 302 repos = [x.repo_name for x in user.repositories]
303 303 raise UserOwnsReposException(
304 304 _(u'user "%s" still owns %s repositories and cannot be '
305 305 'removed. Switch owners or remove those repositories. %s')
306 306 % (user.username, len(repos), ', '.join(repos))
307 307 )
308 308 self.sa.delete(user)
309 309 except:
310 310 log.error(traceback.format_exc())
311 311 raise
312 312
313 313 def reset_password_link(self, data):
314 314 from rhodecode.lib.celerylib import tasks, run_task
315 315 run_task(tasks.send_password_link, data['email'])
316 316
317 317 def reset_password(self, data):
318 318 from rhodecode.lib.celerylib import tasks, run_task
319 319 run_task(tasks.reset_user_password, data['email'])
320 320
321 321 def fill_data(self, auth_user, user_id=None, api_key=None):
322 322 """
323 323 Fetches auth_user by user_id,or api_key if present.
324 324 Fills auth_user attributes with those taken from database.
325 325 Additionally set's is_authenitated if lookup fails
326 326 present in database
327 327
328 328 :param auth_user: instance of user to set attributes
329 329 :param user_id: user id to fetch by
330 330 :param api_key: api key to fetch by
331 331 """
332 332 if user_id is None and api_key is None:
333 333 raise Exception('You need to pass user_id or api_key')
334 334
335 335 try:
336 336 if api_key:
337 337 dbuser = self.get_by_api_key(api_key)
338 338 else:
339 339 dbuser = self.get(user_id)
340 340
341 341 if dbuser is not None and dbuser.active:
342 342 log.debug('filling %s data' % dbuser)
343 343 for k, v in dbuser.get_dict().items():
344 344 setattr(auth_user, k, v)
345 345 else:
346 346 return False
347 347
348 348 except:
349 349 log.error(traceback.format_exc())
350 350 auth_user.is_authenticated = False
351 351 return False
352 352
353 353 return True
354 354
355 355 def fill_perms(self, user):
356 356 """
357 357 Fills user permission attribute with permissions taken from database
358 358 works for permissions given for repositories, and for permissions that
359 359 are granted to groups
360 360
361 361 :param user: user instance to fill his perms
362 362 """
363 363 RK = 'repositories'
364 364 GK = 'repositories_groups'
365 365 GLOBAL = 'global'
366 366 user.permissions[RK] = {}
367 367 user.permissions[GK] = {}
368 368 user.permissions[GLOBAL] = set()
369 369
370 370 #======================================================================
371 371 # fetch default permissions
372 372 #======================================================================
373 373 default_user = User.get_by_username('default', cache=True)
374 374 default_user_id = default_user.user_id
375 375
376 376 default_repo_perms = Permission.get_default_perms(default_user_id)
377 377 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
378 378
379 379 if user.is_admin:
380 380 #==================================================================
381 381 # admin user have all default rights for repositories
382 382 # and groups set to admin
383 383 #==================================================================
384 384 user.permissions[GLOBAL].add('hg.admin')
385 385
386 386 # repositories
387 387 for perm in default_repo_perms:
388 388 r_k = perm.UserRepoToPerm.repository.repo_name
389 389 p = 'repository.admin'
390 390 user.permissions[RK][r_k] = p
391 391
392 392 # repositories groups
393 393 for perm in default_repo_groups_perms:
394 394 rg_k = perm.UserRepoGroupToPerm.group.group_name
395 395 p = 'group.admin'
396 396 user.permissions[GK][rg_k] = p
397 397 return user
398 398
399 399 #==================================================================
400 400 # set default permissions first for repositories and groups
401 401 #==================================================================
402 402 uid = user.user_id
403 403
404 404 # default global permissions
405 405 default_global_perms = self.sa.query(UserToPerm)\
406 406 .filter(UserToPerm.user_id == default_user_id)
407 407
408 408 for perm in default_global_perms:
409 409 user.permissions[GLOBAL].add(perm.permission.permission_name)
410 410
411 411 # defaults for repositories, taken from default user
412 412 for perm in default_repo_perms:
413 413 r_k = perm.UserRepoToPerm.repository.repo_name
414 414 if perm.Repository.private and not (perm.Repository.user_id == uid):
415 415 # disable defaults for private repos,
416 416 p = 'repository.none'
417 417 elif perm.Repository.user_id == uid:
418 418 # set admin if owner
419 419 p = 'repository.admin'
420 420 else:
421 421 p = perm.Permission.permission_name
422 422
423 423 user.permissions[RK][r_k] = p
424 424
425 425 # defaults for repositories groups taken from default user permission
426 426 # on given group
427 427 for perm in default_repo_groups_perms:
428 428 rg_k = perm.UserRepoGroupToPerm.group.group_name
429 429 p = perm.Permission.permission_name
430 430 user.permissions[GK][rg_k] = p
431 431
432 432 #==================================================================
433 433 # overwrite defaults with user permissions if any found
434 434 #==================================================================
435 435
436 436 # user global permissions
437 437 user_perms = self.sa.query(UserToPerm)\
438 438 .options(joinedload(UserToPerm.permission))\
439 439 .filter(UserToPerm.user_id == uid).all()
440 440
441 441 for perm in user_perms:
442 442 user.permissions[GLOBAL].add(perm.permission.permission_name)
443 443
444 444 # user explicit permissions for repositories
445 445 user_repo_perms = \
446 446 self.sa.query(UserRepoToPerm, Permission, Repository)\
447 447 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
448 448 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
449 449 .filter(UserRepoToPerm.user_id == uid)\
450 450 .all()
451 451
452 452 for perm in user_repo_perms:
453 453 # set admin if owner
454 454 r_k = perm.UserRepoToPerm.repository.repo_name
455 455 if perm.Repository.user_id == uid:
456 456 p = 'repository.admin'
457 457 else:
458 458 p = perm.Permission.permission_name
459 459 user.permissions[RK][r_k] = p
460 460
461 461 # USER GROUP
462 462 #==================================================================
463 463 # check if user is part of user groups for this repository and
464 464 # fill in (or replace with higher) permissions
465 465 #==================================================================
466 466
467 467 # users group global
468 468 user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
469 469 .options(joinedload(UsersGroupToPerm.permission))\
470 470 .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
471 471 UsersGroupMember.users_group_id))\
472 472 .filter(UsersGroupMember.user_id == uid).all()
473 473
474 474 for perm in user_perms_from_users_groups:
475 475 user.permissions[GLOBAL].add(perm.permission.permission_name)
476 476
477 477 # users group for repositories permissions
478 478 user_repo_perms_from_users_groups = \
479 479 self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\
480 480 .join((Repository, UsersGroupRepoToPerm.repository_id == Repository.repo_id))\
481 481 .join((Permission, UsersGroupRepoToPerm.permission_id == Permission.permission_id))\
482 482 .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == UsersGroupMember.users_group_id))\
483 483 .filter(UsersGroupMember.user_id == uid)\
484 484 .all()
485 485
486 486 for perm in user_repo_perms_from_users_groups:
487 487 r_k = perm.UsersGroupRepoToPerm.repository.repo_name
488 488 p = perm.Permission.permission_name
489 489 cur_perm = user.permissions[RK][r_k]
490 490 # overwrite permission only if it's greater than permission
491 491 # given from other sources
492 492 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
493 493 user.permissions[RK][r_k] = p
494 494
495 495 # REPO GROUP
496 496 #==================================================================
497 497 # get access for this user for repos group and override defaults
498 498 #==================================================================
499 499
500 500 # user explicit permissions for repository
501 501 user_repo_groups_perms = \
502 502 self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\
503 503 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
504 504 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
505 505 .filter(UserRepoGroupToPerm.user_id == uid)\
506 506 .all()
507 507
508 508 for perm in user_repo_groups_perms:
509 509 rg_k = perm.UserRepoGroupToPerm.group.group_name
510 510 p = perm.Permission.permission_name
511 511 cur_perm = user.permissions[GK][rg_k]
512 512 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
513 513 user.permissions[GK][rg_k] = p
514 514
515 515 # REPO GROUP + USER GROUP
516 516 #==================================================================
517 517 # check if user is part of user groups for this repo group and
518 518 # fill in (or replace with higher) permissions
519 519 #==================================================================
520 520
521 521 # users group for repositories permissions
522 522 user_repo_group_perms_from_users_groups = \
523 523 self.sa.query(UsersGroupRepoGroupToPerm, Permission, RepoGroup)\
524 524 .join((RepoGroup, UsersGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
525 525 .join((Permission, UsersGroupRepoGroupToPerm.permission_id == Permission.permission_id))\
526 526 .join((UsersGroupMember, UsersGroupRepoGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\
527 527 .filter(UsersGroupMember.user_id == uid)\
528 528 .all()
529 529
530 530 for perm in user_repo_group_perms_from_users_groups:
531 531 g_k = perm.UsersGroupRepoGroupToPerm.group.group_name
532 532 p = perm.Permission.permission_name
533 533 cur_perm = user.permissions[GK][g_k]
534 534 # overwrite permission only if it's greater than permission
535 535 # given from other sources
536 536 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
537 537 user.permissions[GK][g_k] = p
538 538
539 539 return user
540 540
541 541 def has_perm(self, user, perm):
542 542 if not isinstance(perm, Permission):
543 543 raise Exception('perm needs to be an instance of Permission class '
544 544 'got %s instead' % type(perm))
545 545
546 546 user = self._get_user(user)
547 547
548 548 return UserToPerm.query().filter(UserToPerm.user == user)\
549 549 .filter(UserToPerm.permission == perm).scalar() is not None
550 550
551 551 def grant_perm(self, user, perm):
552 552 """
553 553 Grant user global permissions
554 554
555 555 :param user:
556 556 :param perm:
557 557 """
558 558 user = self._get_user(user)
559 559 perm = self._get_perm(perm)
560 560 # if this permission is already granted skip it
561 561 _perm = UserToPerm.query()\
562 562 .filter(UserToPerm.user == user)\
563 563 .filter(UserToPerm.permission == perm)\
564 564 .scalar()
565 565 if _perm:
566 566 return
567 567 new = UserToPerm()
568 568 new.user = user
569 569 new.permission = perm
570 570 self.sa.add(new)
571 571
572 572 def revoke_perm(self, user, perm):
573 573 """
574 574 Revoke users global permissions
575 575
576 576 :param user:
577 577 :param perm:
578 578 """
579 579 user = self._get_user(user)
580 580 perm = self._get_perm(perm)
581 581
582 582 obj = UserToPerm.query()\
583 583 .filter(UserToPerm.user == user)\
584 584 .filter(UserToPerm.permission == perm)\
585 585 .scalar()
586 586 if obj:
587 587 self.sa.delete(obj)
588 588
589 589 def add_extra_email(self, user, email):
590 590 """
591 591 Adds email address to UserEmailMap
592 592
593 593 :param user:
594 594 :param email:
595 595 """
596 596 user = self._get_user(user)
597 597 obj = UserEmailMap()
598 598 obj.user = user
599 599 obj.email = email
600 600 self.sa.add(obj)
601 601 return obj
602 602
603 603 def delete_extra_email(self, user, email_id):
604 604 """
605 605 Removes email address from UserEmailMap
606 606
607 607 :param user:
608 608 :param email_id:
609 609 """
610 610 user = self._get_user(user)
611 611 obj = UserEmailMap.query().get(email_id)
612 612 if obj:
613 self.sa.delete(obj) No newline at end of file
613 self.sa.delete(obj)
@@ -1,251 +1,251 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Edit user')} ${c.user.username} - ${c.rhodecode_name}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 ${h.link_to(_('Admin'),h.url('admin_home'))}
10 10 &raquo;
11 11 ${h.link_to(_('Users'),h.url('users'))}
12 12 &raquo;
13 13 ${_('edit')} "${c.user.username}"
14 14 </%def>
15 15
16 16 <%def name="page_nav()">
17 17 ${self.menu('admin')}
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 <!-- end box / title -->
27 27 ${h.form(url('update_user', id=c.user.user_id),method='put')}
28 28 <div class="form">
29 29 <div class="field">
30 30 <div class="gravatar_box">
31 31 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(c.user.email)}"/></div>
32 32 <p>
33 33 %if c.use_gravatar:
34 34 <strong>${_('Change your avatar at')} <a href="http://gravatar.com">gravatar.com</a></strong>
35 35 <br/>${_('Using')} ${c.user.email}
36 36 %else:
37 37 <br/>${c.user.email}
38 38 %endif
39 39 </div>
40 40 </div>
41 41 <div class="field">
42 42 <div class="label">
43 43 <label>${_('API key')}</label> ${c.user.api_key}
44 44 </div>
45 45 </div>
46 46
47 47 <div class="fields">
48 48 <div class="field">
49 49 <div class="label">
50 50 <label for="username">${_('Username')}:</label>
51 51 </div>
52 52 <div class="input">
53 53 ${h.text('username',class_='medium')}
54 54 </div>
55 55 </div>
56 56
57 57 <div class="field">
58 58 <div class="label">
59 59 <label for="ldap_dn">${_('LDAP DN')}:</label>
60 60 </div>
61 61 <div class="input">
62 62 ${h.text('ldap_dn',class_='medium disabled',readonly="readonly")}
63 63 </div>
64 64 </div>
65 65
66 66 <div class="field">
67 67 <div class="label">
68 68 <label for="new_password">${_('New password')}:</label>
69 69 </div>
70 70 <div class="input">
71 71 ${h.password('new_password',class_='medium',autocomplete="off")}
72 72 </div>
73 73 </div>
74 74
75 75 <div class="field">
76 76 <div class="label">
77 77 <label for="password_confirmation">${_('New password confirmation')}:</label>
78 78 </div>
79 79 <div class="input">
80 80 ${h.password('password_confirmation',class_="medium",autocomplete="off")}
81 81 </div>
82 82 </div>
83 83
84 84 <div class="field">
85 85 <div class="label">
86 86 <label for="name">${_('First Name')}:</label>
87 87 </div>
88 88 <div class="input">
89 89 ${h.text('name',class_='medium')}
90 90 </div>
91 91 </div>
92 92
93 93 <div class="field">
94 94 <div class="label">
95 95 <label for="lastname">${_('Last Name')}:</label>
96 96 </div>
97 97 <div class="input">
98 98 ${h.text('lastname',class_='medium')}
99 99 </div>
100 100 </div>
101 101
102 102 <div class="field">
103 103 <div class="label">
104 104 <label for="email">${_('Email')}:</label>
105 105 </div>
106 106 <div class="input">
107 107 ${h.text('email',class_='medium')}
108 108 </div>
109 109 </div>
110 110
111 111 <div class="field">
112 112 <div class="label label-checkbox">
113 113 <label for="active">${_('Active')}:</label>
114 114 </div>
115 115 <div class="checkboxes">
116 116 ${h.checkbox('active',value=True)}
117 117 </div>
118 118 </div>
119 119
120 120 <div class="field">
121 121 <div class="label label-checkbox">
122 122 <label for="admin">${_('Admin')}:</label>
123 123 </div>
124 124 <div class="checkboxes">
125 125 ${h.checkbox('admin',value=True)}
126 126 </div>
127 127 </div>
128 128 <div class="buttons">
129 129 ${h.submit('save',_('Save'),class_="ui-button")}
130 130 ${h.reset('reset',_('Reset'),class_="ui-button")}
131 131 </div>
132 132 </div>
133 133 </div>
134 134 ${h.end_form()}
135 135 </div>
136 136 <div class="box box-right">
137 137 <!-- box / title -->
138 138 <div class="title">
139 139 <h5>${_('Permissions')}</h5>
140 140 </div>
141 141 ${h.form(url('user_perm', id=c.user.user_id),method='put')}
142 142 <div class="form">
143 143 <!-- fields -->
144 144 <div class="fields">
145 145 <div class="field">
146 146 <div class="label label-checkbox">
147 147 <label for="create_repo_perm">${_('Create repositories')}:</label>
148 148 </div>
149 149 <div class="checkboxes">
150 150 ${h.checkbox('create_repo_perm',value=True)}
151 151 </div>
152 152 </div>
153 153 <div class="buttons">
154 154 ${h.submit('save',_('Save'),class_="ui-button")}
155 155 ${h.reset('reset',_('Reset'),class_="ui-button")}
156 156 </div>
157 157 </div>
158 158 </div>
159 159 ${h.end_form()}
160 160
161 161 ## permissions overview
162 162 <div id="perms" class="table">
163 163 %for section in sorted(c.perm_user.permissions.keys()):
164 164 <div class="perms_section_head">${section.replace("_"," ").capitalize()}</div>
165 165
166 166 <div id='tbl_list_wrap_${section}' class="yui-skin-sam">
167 167 <table id="tbl_list_${section}">
168 168 <thead>
169 169 <tr>
170 170 <th class="left">${_('Name')}</th>
171 171 <th class="left">${_('Permission')}</th>
172 172 </thead>
173 173 <tbody>
174 174 %for k in c.perm_user.permissions[section]:
175 175 <%
176 176 if section != 'global':
177 177 section_perm = c.perm_user.permissions[section].get(k)
178 178 _perm = section_perm.split('.')[-1]
179 179 else:
180 180 _perm = section_perm = None
181 181 %>
182 182 <tr>
183 183 <td>
184 184 %if section == 'repositories':
185 185 <a href="${h.url('summary_home',repo_name=k)}">${k}</a>
186 186 %elif section == 'repositories_groups':
187 187 <a href="${h.url('repos_group_home',group_name=k)}">${k}</a>
188 188 %else:
189 189 ${k}
190 190 %endif
191 191 </td>
192 192 <td>
193 193 %if section == 'global':
194 194 ${h.bool2icon(True)}
195 195 %else:
196 196 <span class="perm_tag ${_perm}">${section_perm}</span>
197 197 %endif
198 198 </td>
199 199 </tr>
200 200 %endfor
201 201 </tbody>
202 202 </table>
203 203 </div>
204 204 %endfor
205 205 </div>
206 206 </div>
207 207 <div class="box box-right">
208 208 <!-- box / title -->
209 209 <div class="title">
210 210 <h5>${_('Email addresses')}</h5>
211 211 </div>
212
212
213 213 <div class="emails_wrap">
214 214 <table class="noborder">
215 215 %for em in c.user_email_map:
216 216 <tr>
217 217 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(em.user.email,16)}"/> </div></td>
218 218 <td><div class="email">${em.email}</div></td>
219 219 <td>
220 220 ${h.form(url('user_emails_delete', id=c.user.user_id),method='delete')}
221 221 ${h.hidden('del_email',em.email_id)}
222 222 ${h.submit('remove_',_('delete'),id="remove_email_%s" % em.email_id,
223 223 class_="delete_icon action_button", onclick="return confirm('"+_('Confirm to delete this email: %s') % em.email+"');")}
224 ${h.end_form()}
224 ${h.end_form()}
225 225 </td>
226 226 </tr>
227 227 %endfor
228 228 </table>
229 229 </div>
230
230
231 231 ${h.form(url('user_emails', id=c.user.user_id),method='put')}
232 232 <div class="form">
233 233 <!-- fields -->
234 234 <div class="fields">
235 235 <div class="field">
236 236 <div class="label">
237 237 <label for="email">${_('New email address')}:</label>
238 238 </div>
239 239 <div class="input">
240 240 ${h.text('new_email', class_='medium')}
241 241 </div>
242 </div>
242 </div>
243 243 <div class="buttons">
244 244 ${h.submit('save',_('Add'),class_="ui-button")}
245 245 ${h.reset('reset',_('Reset'),class_="ui-button")}
246 246 </div>
247 247 </div>
248 248 </div>
249 249 ${h.end_form()}
250 250 </div>
251 251 </%def>
@@ -1,94 +1,94 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Branches') % c.repo_name} - ${c.rhodecode_name}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 <input class="q_filter_box" id="q_filter_branches" size="15" type="text" name="filter" value="${_('quick filter...')}"/>
10 10 ${h.link_to(u'Home',h.url('/'))}
11 11 &raquo;
12 12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 13 &raquo;
14 14 ${_('branches')}
15 15 </%def>
16 16
17 17 <%def name="page_nav()">
18 18 ${self.menu('branches')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <div class="box">
23 23 <!-- box / title -->
24 24 <div class="title">
25 25 ${self.breadcrumbs()}
26 26 </div>
27 27 <!-- end box / title -->
28 28 %if c.repo_branches:
29 29 <div class="info_box" id="compare_branches" style="clear: both;padding: 10px 19px;vertical-align: right;text-align: right;"><a href="#" class="ui-btn small">${_('Compare branches')}</a></div>
30 30 %endif
31 31 <div class="table">
32 32 <%include file='branches_data.html'/>
33 33 </div>
34 34 </div>
35 35 <script type="text/javascript">
36 36 YUE.on('compare_branches','click',function(e){
37 37 YUE.preventDefault(e);
38 38 var org = YUQ('input[name=compare_org]:checked')[0];
39 39 var other = YUQ('input[name=compare_other]:checked')[0];
40 40
41 41 if(org && other){
42 42 var compare_url = "${h.url('compare_url',repo_name=c.repo_name,org_ref_type='branch',org_ref='__ORG__',other_ref_type='branch',other_ref='__OTHER__')}";
43 43 var u = compare_url.replace('__ORG__',org.value)
44 44 .replace('__OTHER__',other.value);
45 45 window.location=u;
46 46 }
47
47
48 48 })
49 49 // main table sorting
50 50 var myColumnDefs = [
51 51 {key:"name",label:"${_('Name')}",sortable:true},
52 52 {key:"date",label:"${_('Date')}",sortable:true,
53 53 sortOptions: { sortFunction: dateSort }},
54 54 {key:"author",label:"${_('Author')}",sortable:true},
55 55 {key:"revision",label:"${_('Revision')}",sortable:true,
56 56 sortOptions: { sortFunction: revisionSort }},
57 57 {key:"compare",label:"${_('Compare')}",sortable:false,},
58 58 ];
59 59
60 60 var myDataSource = new YAHOO.util.DataSource(YUD.get("branches_data"));
61 61
62 62 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
63 63
64 64 myDataSource.responseSchema = {
65 65 fields: [
66 66 {key:"name"},
67 67 {key:"date"},
68 68 {key:"author"},
69 69 {key:"revision"},
70 70 {key:"compare"},
71 71 ]
72 72 };
73 73
74 74 var myDataTable = new YAHOO.widget.DataTable("table_wrap", myColumnDefs, myDataSource,
75 75 {
76 76 sortedBy:{key:"name",dir:"asc"},
77 77 MSG_SORTASC:"${_('Click to sort ascending')}",
78 78 MSG_SORTDESC:"${_('Click to sort descending')}",
79 79 MSG_EMPTY:"${_('No records found.')}",
80 80 MSG_ERROR:"${_('Data error.')}",
81 81 MSG_LOADING:"${_('Loading...')}",
82 82 }
83 83 );
84 84 myDataTable.subscribe('postRenderEvent',function(oArgs) {
85 85 tooltip_activate();
86 86 var func = function(node){
87 87 return node.parentNode.parentNode.parentNode.parentNode.parentNode;
88 88 }
89 89 q_filter('q_filter_branches',YUQ('div.table tr td .logtags .branchtag a'),func);
90 90 });
91 91
92 92 </script>
93 93
94 94 </%def>
@@ -1,155 +1,155 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 4 ## ${comment.comment_block(co)}
5 5 ##
6 6 <%def name="comment_block(co)">
7 7 <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
8 8 <div class="comment-wrapp">
9 9 <div class="meta">
10 10 <div style="float:left"> <img src="${h.gravatar_url(co.author.email, 20)}" /> </div>
11 11 <div class="user">
12 12 ${co.author.username}
13 13 </div>
14 14 <div class="date">
15 15 ${h.age(co.modified_at)}
16 16 </div>
17 17 %if co.status_change:
18 18 <div style="float:left" class="changeset-status-container">
19 19 <div style="float:left;padding:0px 2px 0px 2px"><span style="font-size: 18px;">&rsaquo;</span></div>
20 20 <div title="${_('Changeset status')}" class="changeset-status-lbl"> ${co.status_change.status_lbl}</div>
21 <div class="changeset-status-ico"><img src="${h.url(str('/images/icons/flag_status_%s.png' % co.status_change.status))}" /></div>
21 <div class="changeset-status-ico"><img src="${h.url(str('/images/icons/flag_status_%s.png' % co.status_change.status))}" /></div>
22 22 </div>
23 %endif
23 %endif
24 24 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or co.author.user_id == c.rhodecode_user.user_id:
25 25 <div class="buttons">
26 26 <span onClick="deleteComment(${co.comment_id})" class="delete-comment ui-btn">${_('Delete')}</span>
27 27 </div>
28 28 %endif
29 29 </div>
30 30 <div class="text">
31 31 ${h.rst_w_mentions(co.text)|n}
32 32 </div>
33 33 </div>
34 34 </div>
35 35 </%def>
36 36
37 37
38 38 <%def name="comment_inline_form(changeset)">
39 39 <div id='comment-inline-form-template' style="display:none">
40 40 <div class="comment-inline-form ac">
41 41 %if c.rhodecode_user.username != 'default':
42 42 <div class="overlay"><div class="overlay-text">${_('Submitting...')}</div></div>
43 43 ${h.form(h.url('changeset_comment', repo_name=c.repo_name, revision=changeset.raw_id),class_='inline-form')}
44 44 <div class="clearfix">
45 45 <div class="comment-help">${_('Commenting on line {1}.')}
46 46 ${(_('Comments parsed using %s syntax with %s support.') % (
47 47 ('<a href="%s">RST</a>' % h.url('rst_help')),
48 48 ('<span style="color:#003367" class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
49 49 )
50 50 )|n
51 51 }
52 52 </div>
53 53 <div class="mentions-container" id="mentions_container_{1}"></div>
54 54 <textarea id="text_{1}" name="text" class="yui-ac-input"></textarea>
55 55 </div>
56 56 <div class="comment-button">
57 57 <input type="hidden" name="f_path" value="{0}">
58 58 <input type="hidden" name="line" value="{1}">
59 59 ${h.submit('save', _('Comment'), class_='ui-btn save-inline-form')}
60 60 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
61 61 </div>
62 62 ${h.end_form()}
63 63 %else:
64 64 ${h.form('')}
65 65 <div class="clearfix">
66 66 <div class="comment-help">
67 67 ${_('You need to be logged in to comment.')} <a href="${h.url('login_home',came_from=h.url.current())}">${_('Login now')}</a>
68 68 </div>
69 69 </div>
70 70 <div class="comment-button">
71 71 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
72 72 </div>
73 73 ${h.end_form()}
74 74 %endif
75 75 </div>
76 76 </div>
77 77 </%def>
78 78
79 79
80 80 ## generates inlines taken from c.comments var
81 81 <%def name="inlines()">
82 82 <div class="comments-number">${ungettext("%d comment", "%d comments", len(c.comments)) % len(c.comments)} ${ungettext("(%d inline)", "(%d inline)", c.inline_cnt) % c.inline_cnt}</div>
83 83 %for path, lines in c.inline_comments:
84 84 % for line,comments in lines.iteritems():
85 85 <div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
86 86 %for co in comments:
87 87 ${comment_block(co)}
88 88 %endfor
89 89 </div>
90 90 %endfor
91 91 %endfor
92 92
93 93 </%def>
94 94
95 95 ## MAIN COMMENT FORM
96 96 <%def name="comments(post_url, cur_status)">
97 97
98 98 <div class="comments">
99 99 <div id="inline-comments-container">
100 100 ## generate inlines for this changeset
101 101 ${inlines()}
102 102 </div>
103 103
104 104 %for co in c.comments:
105 105 <div id="comment-tr-${co.comment_id}">
106 106 ${comment_block(co)}
107 107 </div>
108 108 %endfor
109 109 %if c.rhodecode_user.username != 'default':
110 110 <div class="comment-form ac">
111 111 ${h.form(post_url)}
112 112 <strong>${_('Leave a comment')}</strong>
113 113 <div class="clearfix">
114 114 <div class="comment-help">
115 115 ${(_('Comments parsed using %s syntax with %s support.') % (('<a href="%s">RST</a>' % h.url('rst_help')),
116 116 '<span style="color:#003367" class="tooltip" title="%s">@mention</span>' %
117 117 _('Use @username inside this text to send notification to this RhodeCode user')))|n}
118 118 | <span class="tooltip" title="${_('Check this to change current status of code-review for this changeset')}"> ${_('change status')}
119 119 <input style="vertical-align: bottom;margin-bottom:-2px" id="show_changeset_status_box" type="checkbox" name="change_changeset_status" />
120 </span>
120 </span>
121 121 </div>
122 122 <div id="status_block_container" class="status-block" style="display:none">
123 123 %for status,lbl in c.changeset_statuses:
124 124 <div class="">
125 125 <img src="${h.url('/images/icons/flag_status_%s.png' % status)}" /> <input ${'checked="checked"' if status == cur_status else ''}" type="radio" name="changeset_status" value="${status}"> <label>${lbl}</label>
126 </div>
126 </div>
127 127 %endfor
128 </div>
128 </div>
129 129 <div class="mentions-container" id="mentions_container"></div>
130 130 ${h.textarea('text')}
131 131 </div>
132 132 <div class="comment-button">
133 133 ${h.submit('save', _('Comment'), class_='ui-button')}
134 134 </div>
135 135 ${h.end_form()}
136 136 </div>
137 137 %endif
138 138 </div>
139 139 <script>
140 140 YUE.onDOMReady(function () {
141 141 MentionsAutoComplete('text', 'mentions_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
142
142
143 143 // changeset status box listener
144 144 YUE.on(YUD.get('show_changeset_status_box'),'change',function(e){
145 145 if(e.currentTarget.checked){
146 YUD.setStyle('status_block_container','display','');
146 YUD.setStyle('status_block_container','display','');
147 147 }
148 148 else{
149 149 YUD.setStyle('status_block_container','display','none');
150 150 }
151 151 })
152
152
153 153 });
154 154 </script>
155 155 </%def>
@@ -1,61 +1,61 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ##usage:
3 3 ## <%namespace name="diff_block" file="/changeset/diff_block.html"/>
4 4 ## ${diff_block.diff_block(change)}
5 5 ##
6 6 <%def name="diff_block(change)">
7 7
8 8 %for op,filenode,diff,cs1,cs2,stat in change:
9 9 %if op !='removed':
10 10 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}_target" style="clear:both;margin-top:25px"></div>
11 11 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}" class="diffblock margined comm">
12 12 <div class="code-header">
13 13 <div class="changeset_header">
14 14 <div class="changeset_file">
15 15 ${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name,
16 16 revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))}
17 17 </div>
18 18 <div class="diff-actions">
19 19 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}" class="tooltip" title="${h.tooltip(_('diff'))}"><img class="icon" src="${h.url('/images/icons/page_white_go.png')}"/></a>
20 20 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='raw')}" class="tooltip" title="${h.tooltip(_('raw diff'))}"><img class="icon" src="${h.url('/images/icons/page_white.png')}"/></a>
21 21 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='download')}" class="tooltip" title="${h.tooltip(_('download diff'))}"><img class="icon" src="${h.url('/images/icons/page_white_get.png')}"/></a>
22 22 ${c.ignorews_url(request.GET, h.FID(filenode.changeset.raw_id,filenode.path))}
23 23 ${c.context_url(request.GET, h.FID(filenode.changeset.raw_id,filenode.path))}
24 24 </div>
25 25 <span style="float:right;margin-top:-3px">
26 26 <label>
27 27 ${_('show inline comments')}
28 28 ${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=h.FID(filenode.changeset.raw_id,filenode.path))}
29 29 </label>
30 30 </span>
31 31 </div>
32 32 </div>
33 33 <div class="code-body">
34 34 <div class="full_f_path" path="${h.safe_unicode(filenode.path)}"></div>
35 35 ${diff|n}
36 36 </div>
37 37 </div>
38 38 %endif
39 39 %endfor
40 40
41 41 </%def>
42 42
43 43 <%def name="diff_block_simple(change)">
44 44
45 45 %for op,filenode_path,diff in change:
46 46 <div id="${h.FID('',filenode_path)}_target" style="clear:both;margin-top:25px"></div>
47 <div id="${h.FID('',filenode_path)}" class="diffblock margined comm">
47 <div id="${h.FID('',filenode_path)}" class="diffblock margined comm">
48 48 <div class="code-header">
49 49 <div class="changeset_header">
50 50 <div class="changeset_file">
51 51 <a href="#">${h.safe_unicode(filenode_path)}</a>
52 52 </div>
53 53 </div>
54 54 </div>
55 55 <div class="code-body">
56 56 <div class="full_f_path" path="${h.safe_unicode(filenode_path)}"></div>
57 57 ${diff|n}
58 58 </div>
59 59 </div>
60 60 %endfor
61 </%def> No newline at end of file
61 </%def>
@@ -1,12 +1,12 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="main.html"/>
3 3
4 4 <h4>${subject}</h4>
5 5
6 6 ${body}
7 7
8 8 % if status_change is not None:
9 9 <div>
10 10 New status -> ${status_change}
11 </div>
12 % endif No newline at end of file
11 </div>
12 % endif
@@ -1,83 +1,83 b''
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
23 23 <h3>${_('Title')}: ${c.pull_request.title}</h3>
24 24 <div class="changeset-status-container" style="float:left;padding:0px 20px 20px 20px">
25 25 %if c.current_changeset_status:
26 26 <div title="${_('Changeset status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.current_changeset_status)}]</div>
27 27 <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
28 28 %endif
29 </div>
29 </div>
30 30 <div style="padding:4px">
31 31 <div>${h.fmt_date(c.pull_request.created_on)}</div>
32 32 </div>
33
33
34 34 ##DIFF
35
35
36 36 <div class="table">
37 37 <div id="body" class="diffblock">
38 38 <div style="white-space:pre-wrap;padding:5px">${h.literal(c.pull_request.description)}</div>
39 39 </div>
40 40 <div id="changeset_compare_view_content">
41 41 ##CS
42 42 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Incoming changesets')}</div>
43 43 <%include file="/compare/compare_cs.html" />
44 44
45 45 ## FILES
46 46 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
47 47 <div class="cs_files">
48 48 %for fid, change, f, stat in c.files:
49 49 <div class="cs_${change}">
50 50 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
51 51 <div class="changes">${h.fancy_file_stats(stat)}</div>
52 52 </div>
53 53 %endfor
54 54 </div>
55 55 </div>
56 56 </div>
57 57 <script>
58 58 var _USERS_AC_DATA = ${c.users_array|n};
59 59 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
60 60 </script>
61 61
62 62 ## diff block
63 63 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
64 64 %for fid, change, f, stat in c.files:
65 65 ${diff_block.diff_block_simple([c.changes[fid]])}
66 66 %endfor
67 67
68 68 ## template for inline comment form
69 69 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
70 70 ##${comment.comment_inline_form(c.changeset)}
71 71
72 72 ## render comments main comments form and it status
73 73 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id),
74 74 c.current_changeset_status)}
75
75
76 76 </div>
77 77
78 78 <script type="text/javascript">
79 79
80 80
81 81 </script>
82 82
83 83 </%def>
@@ -1,31 +1,31 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('All pull requests')}
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 ${_('All pull requests')}
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
23 23 %for pr in c.pull_requests:
24 24 <a href="${h.url('pullrequest_show',repo_name=c.repo_name,pull_request_id=pr.pull_request_id)}">#${pr.pull_request_id}</a>
25 25 %endfor
26
26
27 27 </div>
28 28
29 29 <script type="text/javascript"></script>
30 30
31 31 </%def>
@@ -1,160 +1,160 b''
1 1 """Pylons application test package
2 2
3 3 This package assumes the Pylons environment is already loaded, such as
4 4 when this script is imported from the `nosetests --with-pylons=test.ini`
5 5 command.
6 6
7 7 This module initializes the application via ``websetup`` (`paster
8 8 setup-app`) and provides the base testing objects.
9 9 """
10 10 import os
11 11 import time
12 12 import logging
13 13 import datetime
14 14 import hashlib
15 15 import tempfile
16 16 from os.path import join as jn
17 17
18 18 from unittest import TestCase
19 19 from tempfile import _RandomNameSequence
20 20
21 21 from paste.deploy import loadapp
22 22 from paste.script.appinstall import SetupCommand
23 23 from pylons import config, url
24 24 from routes.util import URLGenerator
25 25 from webtest import TestApp
26 26
27 27 from rhodecode import is_windows
28 28 from rhodecode.model.meta import Session
29 29 from rhodecode.model.db import User
30 30 from rhodecode.tests.nose_parametrized import parameterized
31
31
32 32 import pylons.test
33 33
34 34
35 35 os.environ['TZ'] = 'UTC'
36 36 if not is_windows:
37 37 time.tzset()
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41 __all__ = [
42 42 'parameterized', 'environ', 'url', 'get_new_dir', 'TestController',
43 43 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO',
44 44 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_REGULAR_LOGIN',
45 45 'TEST_USER_REGULAR_PASS', 'TEST_USER_REGULAR_EMAIL',
46 46 'TEST_USER_REGULAR2_LOGIN', 'TEST_USER_REGULAR2_PASS',
47 47 'TEST_USER_REGULAR2_EMAIL', 'TEST_HG_REPO', 'TEST_HG_REPO_CLONE',
48 48 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO', 'TEST_GIT_REPO_CLONE',
49 49 'TEST_GIT_REPO_PULL', 'HG_REMOTE_REPO', 'GIT_REMOTE_REPO', 'SCM_TESTS',
50 50 ]
51 51
52 52 # Invoke websetup with the current config file
53 53 # SetupCommand('setup-app').run([config_file])
54 54
55 55 ##RUNNING DESIRED TESTS
56 56 # nosetests -x rhodecode.tests.functional.test_admin_settings:TestSettingsController.test_my_account
57 57 # nosetests --pdb --pdb-failures
58 58 # nosetests --with-coverage --cover-package=rhodecode.model.validators rhodecode.tests.test_validators
59 59 environ = {}
60 60
61 61 #SOME GLOBALS FOR TESTS
62 62
63 63 TESTS_TMP_PATH = jn('/', 'tmp', 'rc_test_%s' % _RandomNameSequence().next())
64 64 TEST_USER_ADMIN_LOGIN = 'test_admin'
65 65 TEST_USER_ADMIN_PASS = 'test12'
66 66 TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com'
67 67
68 68 TEST_USER_REGULAR_LOGIN = 'test_regular'
69 69 TEST_USER_REGULAR_PASS = 'test12'
70 70 TEST_USER_REGULAR_EMAIL = 'test_regular@mail.com'
71 71
72 72 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
73 73 TEST_USER_REGULAR2_PASS = 'test12'
74 74 TEST_USER_REGULAR2_EMAIL = 'test_regular2@mail.com'
75 75
76 76 HG_REPO = 'vcs_test_hg'
77 77 GIT_REPO = 'vcs_test_git'
78 78
79 79 NEW_HG_REPO = 'vcs_test_hg_new'
80 80 NEW_GIT_REPO = 'vcs_test_git_new'
81 81
82 82 HG_FORK = 'vcs_test_hg_fork'
83 83 GIT_FORK = 'vcs_test_git_fork'
84 84
85 85 ## VCS
86 86 SCM_TESTS = ['hg', 'git']
87 87 uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple())))
88 88
89 89 GIT_REMOTE_REPO = 'git://github.com/codeinn/vcs.git'
90 90
91 91 TEST_GIT_REPO = jn(TESTS_TMP_PATH, GIT_REPO)
92 92 TEST_GIT_REPO_CLONE = jn(TESTS_TMP_PATH, 'vcsgitclone%s' % uniq_suffix)
93 93 TEST_GIT_REPO_PULL = jn(TESTS_TMP_PATH, 'vcsgitpull%s' % uniq_suffix)
94 94
95 95
96 96 HG_REMOTE_REPO = 'http://bitbucket.org/marcinkuzminski/vcs'
97 97
98 98 TEST_HG_REPO = jn(TESTS_TMP_PATH, HG_REPO)
99 99 TEST_HG_REPO_CLONE = jn(TESTS_TMP_PATH, 'vcshgclone%s' % uniq_suffix)
100 100 TEST_HG_REPO_PULL = jn(TESTS_TMP_PATH, 'vcshgpull%s' % uniq_suffix)
101 101
102 102 TEST_DIR = tempfile.gettempdir()
103 103 TEST_REPO_PREFIX = 'vcs-test'
104 104
105 105 # cached repos if any !
106 106 # comment out to get some other repos from bb or github
107 107 GIT_REMOTE_REPO = jn(TESTS_TMP_PATH, GIT_REPO)
108 108 HG_REMOTE_REPO = jn(TESTS_TMP_PATH, HG_REPO)
109 109
110 110
111 111 def get_new_dir(title):
112 112 """
113 113 Returns always new directory path.
114 114 """
115 115 from rhodecode.tests.vcs.utils import get_normalized_path
116 116 name = TEST_REPO_PREFIX
117 117 if title:
118 118 name = '-'.join((name, title))
119 119 hex = hashlib.sha1(str(time.time())).hexdigest()
120 120 name = '-'.join((name, hex))
121 121 path = os.path.join(TEST_DIR, name)
122 122 return get_normalized_path(path)
123 123
124 124
125 125 class TestController(TestCase):
126 126
127 127 def __init__(self, *args, **kwargs):
128 128 wsgiapp = pylons.test.pylonsapp
129 129 config = wsgiapp.config
130 130
131 131 self.app = TestApp(wsgiapp)
132 132 url._push_object(URLGenerator(config['routes.map'], environ))
133 133 self.Session = Session
134 134 self.index_location = config['app_conf']['index_dir']
135 135 TestCase.__init__(self, *args, **kwargs)
136 136
137 137 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
138 138 password=TEST_USER_ADMIN_PASS):
139 139 self._logged_username = username
140 140 response = self.app.post(url(controller='login', action='index'),
141 141 {'username': username,
142 142 'password': password})
143 143
144 144 if 'invalid user name' in response.body:
145 145 self.fail('could not login using %s %s' % (username, password))
146 146
147 147 self.assertEqual(response.status, '302 Found')
148 148 ses = response.session['rhodecode_user']
149 149 self.assertEqual(ses.get('username'), username)
150 150 response = response.follow()
151 151 self.assertEqual(ses.get('is_authenticated'), True)
152 152
153 153 return response.session['rhodecode_user']
154 154
155 155 def _get_logged_user(self):
156 156 return User.get_by_username(self._logged_username)
157 157
158 158 def checkSessionFlash(self, response, msg):
159 159 self.assertTrue('flash' in response.session)
160 160 self.assertTrue(msg in response.session['flash'][0][1])
@@ -1,52 +1,52 b''
1 1 from rhodecode.tests import *
2 2
3 3
4 4 class TestCompareController(TestController):
5 5
6 6 def test_index_tag(self):
7 7 self.log_user()
8 8 tag1='0.1.3'
9 9 tag2='0.1.2'
10 10 response = self.app.get(url(controller='compare', action='index',
11 11 repo_name=HG_REPO,
12 12 org_ref_type="tag",
13 13 org_ref=tag1,
14 14 other_ref_type="tag",
15 15 other_ref=tag2,
16 16 ))
17 17 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, tag1, HG_REPO, tag2))
18 18 ## outgoing changesets between tags
19 19 response.mustcontain('''<a href="/%s/changeset/17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">r120:17544fbfcd33</a>''' % HG_REPO)
20 20 response.mustcontain('''<a href="/%s/changeset/36e0fc9d2808c5022a24f49d6658330383ed8666">r119:36e0fc9d2808</a>''' % HG_REPO)
21 21 response.mustcontain('''<a href="/%s/changeset/bb1a3ab98cc45cb934a77dcabf87a5a598b59e97">r118:bb1a3ab98cc4</a>''' % HG_REPO)
22 22 response.mustcontain('''<a href="/%s/changeset/41fda979f02fda216374bf8edac4e83f69e7581c">r117:41fda979f02f</a>''' % HG_REPO)
23 23 response.mustcontain('''<a href="/%s/changeset/9749bfbfc0d2eba208d7947de266303b67c87cda">r116:9749bfbfc0d2</a>''' % HG_REPO)
24 24 response.mustcontain('''<a href="/%s/changeset/70d4cef8a37657ee4cf5aabb3bd9f68879769816">r115:70d4cef8a376</a>''' % HG_REPO)
25 25 response.mustcontain('''<a href="/%s/changeset/c5ddebc06eaaba3010c2d66ea6ec9d074eb0f678">r112:c5ddebc06eaa</a>''' % HG_REPO)
26
26
27 27 ## files diff
28 28 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--1c5cf9e91c12">docs/api/utils/index.rst</a></div>''' % (HG_REPO, tag1, tag2))
29 29 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--e3305437df55">test_and_report.sh</a></div>''' % (HG_REPO, tag1, tag2))
30 30 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--c8e92ef85cd1">.hgignore</a></div>''' % (HG_REPO, tag1, tag2))
31 31 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--6e08b694d687">.hgtags</a></div>''' % (HG_REPO, tag1, tag2))
32 32 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2c14b00f3393">docs/api/index.rst</a></div>''' % (HG_REPO, tag1, tag2))
33 33 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--430ccbc82bdf">vcs/__init__.py</a></div>''' % (HG_REPO, tag1, tag2))
34 34 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--9c390eb52cd6">vcs/backends/hg.py</a></div>''' % (HG_REPO, tag1, tag2))
35 35 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--ebb592c595c0">vcs/utils/__init__.py</a></div>''' % (HG_REPO, tag1, tag2))
36 36 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--7abc741b5052">vcs/utils/annotate.py</a></div>''' % (HG_REPO, tag1, tag2))
37 37 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2ef0ef106c56">vcs/utils/diffs.py</a></div>''' % (HG_REPO, tag1, tag2))
38 38 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--3150cb87d4b7">vcs/utils/lazy.py</a></div>''' % (HG_REPO, tag1, tag2))
39 39
40 40 def test_index_branch(self):
41 41 self.log_user()
42 42 response = self.app.get(url(controller='compare', action='index',
43 43 repo_name=HG_REPO,
44 44 org_ref_type="branch",
45 45 org_ref='default',
46 46 other_ref_type="branch",
47 47 other_ref='default',
48 48 ))
49 49
50 50 response.mustcontain('%s@default -> %s@default' % (HG_REPO, HG_REPO))
51 51 # branch are equal
52 52 response.mustcontain('<tr><td>No changesets</td></tr>')
General Comments 0
You need to be logged in to leave comments. Login now