##// END OF EJS Templates
comments: add comments type into comments.
marcink -
r1324:efd94f49 default
parent child Browse files
Show More
@@ -0,0 +1,30 b''
1 import logging
2
3 from sqlalchemy import Column, MetaData, Integer, Unicode, ForeignKey
4
5 from rhodecode.lib.dbmigrate.versions import _reset_base
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_5_0_0 as db
17
18 # add comment type and link to resolve by id
19 comment_table = db.ChangesetComment.__table__
20 col1 = Column('comment_type', Unicode(128), nullable=True)
21 col1.create(table=comment_table)
22
23 col1 = Column('resolved_comment_id', Integer(),
24 ForeignKey('changeset_comments.comment_id'), nullable=True)
25 col1.create(table=comment_table)
26
27
28 def downgrade(migrate_engine):
29 meta = MetaData()
30 meta.bind = migrate_engine
@@ -0,0 +1,70 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22
23 import colander
24
25 from rhodecode.translation import _
26 from rhodecode.model.validation_schema import preparers
27 from rhodecode.model.validation_schema import types
28
29
30 @colander.deferred
31 def deferred_lifetime_validator(node, kw):
32 options = kw.get('lifetime_options', [])
33 return colander.All(
34 colander.Range(min=-1, max=60 * 24 * 30 * 12),
35 colander.OneOf([x for x in options]))
36
37
38 def unique_gist_validator(node, value):
39 from rhodecode.model.db import Gist
40 existing = Gist.get_by_access_id(value)
41 if existing:
42 msg = _(u'Gist with name {} already exists').format(value)
43 raise colander.Invalid(node, msg)
44
45
46 def filename_validator(node, value):
47 if value != os.path.basename(value):
48 msg = _(u'Filename {} cannot be inside a directory').format(value)
49 raise colander.Invalid(node, msg)
50
51
52 comment_types = ['note', 'todo']
53
54
55 class CommentSchema(colander.MappingSchema):
56 from rhodecode.model.db import ChangesetComment
57
58 comment_body = colander.SchemaNode(colander.String())
59 comment_type = colander.SchemaNode(
60 colander.String(),
61 validator=colander.OneOf(ChangesetComment.COMMENT_TYPES))
62
63 comment_file = colander.SchemaNode(colander.String(), missing=None)
64 comment_line = colander.SchemaNode(colander.String(), missing=None)
65 status_change = colander.SchemaNode(colander.String(), missing=None)
66 renderer_type = colander.SchemaNode(colander.String())
67
68 # do those ?
69 user = colander.SchemaNode(types.StrOrIntType())
70 repo = colander.SchemaNode(types.StrOrIntType())
@@ -1,63 +1,63 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22
23 23 RhodeCode, a web based repository management software
24 24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 25 """
26 26
27 27 import os
28 28 import sys
29 29 import platform
30 30
31 31 VERSION = tuple(open(os.path.join(
32 32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33 33
34 34 BACKENDS = {
35 35 'hg': 'Mercurial repository',
36 36 'git': 'Git repository',
37 37 'svn': 'Subversion repository',
38 38 }
39 39
40 40 CELERY_ENABLED = False
41 41 CELERY_EAGER = False
42 42
43 43 # link to config for pylons
44 44 CONFIG = {}
45 45
46 46 # Populated with the settings dictionary from application init in
47 47 # rhodecode.conf.environment.load_pyramid_environment
48 48 PYRAMID_SETTINGS = {}
49 49
50 50 # Linked module for extensions
51 51 EXTENSIONS = {}
52 52
53 53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 63 # defines current db version for migrations
54 __dbversion__ = 64 # defines current db version for migrations
55 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
58 58 __url__ = 'https://code.rhodecode.com'
59 59
60 60 is_windows = __platform__ in ['Windows']
61 61 is_unix = not is_windows
62 62 is_test = False
63 63 disable_error_handler = False
@@ -1,470 +1,473 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 commit controller for RhodeCode showing changes between commits
23 23 """
24 24
25 25 import logging
26 26
27 27 from collections import defaultdict
28 28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29 29
30 30 from pylons import tmpl_context as c, request, response
31 31 from pylons.i18n.translation import _
32 32 from pylons.controllers.util import redirect
33 33
34 34 from rhodecode.lib import auth
35 35 from rhodecode.lib import diffs, codeblocks
36 36 from rhodecode.lib.auth import (
37 37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 41 import rhodecode.lib.helpers as h
42 42 from rhodecode.lib.utils import action_logger, jsonify
43 43 from rhodecode.lib.utils2 import safe_unicode
44 44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.comment import CommentsModel
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.repo import RepoModel
52 52
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 def _update_with_GET(params, GET):
58 58 for k in ['diff1', 'diff2', 'diff']:
59 59 params[k] += GET.getall(k)
60 60
61 61
62 62 def get_ignore_ws(fid, GET):
63 63 ig_ws_global = GET.get('ignorews')
64 64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 65 if ig_ws:
66 66 try:
67 67 return int(ig_ws[0].split(':')[-1])
68 68 except Exception:
69 69 pass
70 70 return ig_ws_global
71 71
72 72
73 73 def _ignorews_url(GET, fileid=None):
74 74 fileid = str(fileid) if fileid else None
75 75 params = defaultdict(list)
76 76 _update_with_GET(params, GET)
77 77 label = _('Show whitespace')
78 78 tooltiplbl = _('Show whitespace for all diffs')
79 79 ig_ws = get_ignore_ws(fileid, GET)
80 80 ln_ctx = get_line_ctx(fileid, GET)
81 81
82 82 if ig_ws is None:
83 83 params['ignorews'] += [1]
84 84 label = _('Ignore whitespace')
85 85 tooltiplbl = _('Ignore whitespace for all diffs')
86 86 ctx_key = 'context'
87 87 ctx_val = ln_ctx
88 88
89 89 # if we have passed in ln_ctx pass it along to our params
90 90 if ln_ctx:
91 91 params[ctx_key] += [ctx_val]
92 92
93 93 if fileid:
94 94 params['anchor'] = 'a_' + fileid
95 95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96 96
97 97
98 98 def get_line_ctx(fid, GET):
99 99 ln_ctx_global = GET.get('context')
100 100 if fid:
101 101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 102 else:
103 103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 105 if ln_ctx:
106 106 ln_ctx = [ln_ctx]
107 107
108 108 if ln_ctx:
109 109 retval = ln_ctx[0].split(':')[-1]
110 110 else:
111 111 retval = ln_ctx_global
112 112
113 113 try:
114 114 return int(retval)
115 115 except Exception:
116 116 return 3
117 117
118 118
119 119 def _context_url(GET, fileid=None):
120 120 """
121 121 Generates a url for context lines.
122 122
123 123 :param fileid:
124 124 """
125 125
126 126 fileid = str(fileid) if fileid else None
127 127 ig_ws = get_ignore_ws(fileid, GET)
128 128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129 129
130 130 params = defaultdict(list)
131 131 _update_with_GET(params, GET)
132 132
133 133 if ln_ctx > 0:
134 134 params['context'] += [ln_ctx]
135 135
136 136 if ig_ws:
137 137 ig_ws_key = 'ignorews'
138 138 ig_ws_val = 1
139 139 params[ig_ws_key] += [ig_ws_val]
140 140
141 141 lbl = _('Increase context')
142 142 tooltiplbl = _('Increase context for all diffs')
143 143
144 144 if fileid:
145 145 params['anchor'] = 'a_' + fileid
146 146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147 147
148 148
149 149 class ChangesetController(BaseRepoController):
150 150
151 151 def __before__(self):
152 152 super(ChangesetController, self).__before__()
153 153 c.affected_files_cut_off = 60
154 154
155 155 def _index(self, commit_id_range, method):
156 156 c.ignorews_url = _ignorews_url
157 157 c.context_url = _context_url
158 158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159 159
160 160 # fetch global flags of ignore ws or context lines
161 161 context_lcl = get_line_ctx('', request.GET)
162 162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163 163
164 164 # diff_limit will cut off the whole diff if the limit is applied
165 165 # otherwise it will just hide the big files from the front-end
166 166 diff_limit = self.cut_off_limit_diff
167 167 file_limit = self.cut_off_limit_file
168 168
169 169 # get ranges of commit ids if preset
170 170 commit_range = commit_id_range.split('...')[:2]
171 171
172 172 try:
173 173 pre_load = ['affected_files', 'author', 'branch', 'date',
174 174 'message', 'parents']
175 175
176 176 if len(commit_range) == 2:
177 177 commits = c.rhodecode_repo.get_commits(
178 178 start_id=commit_range[0], end_id=commit_range[1],
179 179 pre_load=pre_load)
180 180 commits = list(commits)
181 181 else:
182 182 commits = [c.rhodecode_repo.get_commit(
183 183 commit_id=commit_id_range, pre_load=pre_load)]
184 184
185 185 c.commit_ranges = commits
186 186 if not c.commit_ranges:
187 187 raise RepositoryError(
188 188 'The commit range returned an empty result')
189 189 except CommitDoesNotExistError:
190 190 msg = _('No such commit exists for this repository')
191 191 h.flash(msg, category='error')
192 192 raise HTTPNotFound()
193 193 except Exception:
194 194 log.exception("General failure")
195 195 raise HTTPNotFound()
196 196
197 197 c.changes = OrderedDict()
198 198 c.lines_added = 0
199 199 c.lines_deleted = 0
200 200
201 201 # auto collapse if we have more than limit
202 202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
203 203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
204 204
205 205 c.commit_statuses = ChangesetStatus.STATUSES
206 206 c.inline_comments = []
207 207 c.files = []
208 208
209 209 c.statuses = []
210 210 c.comments = []
211 211 if len(c.commit_ranges) == 1:
212 212 commit = c.commit_ranges[0]
213 213 c.comments = CommentsModel().get_comments(
214 214 c.rhodecode_db_repo.repo_id,
215 215 revision=commit.raw_id)
216 216 c.statuses.append(ChangesetStatusModel().get_status(
217 217 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 218 # comments from PR
219 219 statuses = ChangesetStatusModel().get_statuses(
220 220 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 221 with_revisions=True)
222 222 prs = set(st.pull_request for st in statuses
223 223 if st.pull_request is not None)
224 224 # from associated statuses, check the pull requests, and
225 225 # show comments from them
226 226 for pr in prs:
227 227 c.comments.extend(pr.comments)
228 228
229 229 # Iterate over ranges (default commit view is always one commit)
230 230 for commit in c.commit_ranges:
231 231 c.changes[commit.raw_id] = []
232 232
233 233 commit2 = commit
234 234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
235 235
236 236 _diff = c.rhodecode_repo.get_diff(
237 237 commit1, commit2,
238 238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
239 239 diff_processor = diffs.DiffProcessor(
240 240 _diff, format='newdiff', diff_limit=diff_limit,
241 241 file_limit=file_limit, show_full_diff=fulldiff)
242 242
243 243 commit_changes = OrderedDict()
244 244 if method == 'show':
245 245 _parsed = diff_processor.prepare()
246 246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
247 247
248 248 _parsed = diff_processor.prepare()
249 249
250 250 def _node_getter(commit):
251 251 def get_node(fname):
252 252 try:
253 253 return commit.get_node(fname)
254 254 except NodeDoesNotExistError:
255 255 return None
256 256 return get_node
257 257
258 258 inline_comments = CommentsModel().get_inline_comments(
259 259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
260 260 c.inline_cnt = CommentsModel().get_inline_comments_count(
261 261 inline_comments)
262 262
263 263 diffset = codeblocks.DiffSet(
264 264 repo_name=c.repo_name,
265 265 source_node_getter=_node_getter(commit1),
266 266 target_node_getter=_node_getter(commit2),
267 267 comments=inline_comments
268 268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
269 269 c.changes[commit.raw_id] = diffset
270 270 else:
271 271 # downloads/raw we only need RAW diff nothing else
272 272 diff = diff_processor.as_raw()
273 273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
274 274
275 275 # sort comments by how they were generated
276 276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
277 277
278 278
279 279 if len(c.commit_ranges) == 1:
280 280 c.commit = c.commit_ranges[0]
281 281 c.parent_tmpl = ''.join(
282 282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
283 283 if method == 'download':
284 284 response.content_type = 'text/plain'
285 285 response.content_disposition = (
286 286 'attachment; filename=%s.diff' % commit_id_range[:12])
287 287 return diff
288 288 elif method == 'patch':
289 289 response.content_type = 'text/plain'
290 290 c.diff = safe_unicode(diff)
291 291 return render('changeset/patch_changeset.mako')
292 292 elif method == 'raw':
293 293 response.content_type = 'text/plain'
294 294 return diff
295 295 elif method == 'show':
296 296 if len(c.commit_ranges) == 1:
297 297 return render('changeset/changeset.mako')
298 298 else:
299 299 c.ancestor = None
300 300 c.target_repo = c.rhodecode_db_repo
301 301 return render('changeset/changeset_range.mako')
302 302
303 303 @LoginRequired()
304 304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
305 305 'repository.admin')
306 306 def index(self, revision, method='show'):
307 307 return self._index(revision, method=method)
308 308
309 309 @LoginRequired()
310 310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 311 'repository.admin')
312 312 def changeset_raw(self, revision):
313 313 return self._index(revision, method='raw')
314 314
315 315 @LoginRequired()
316 316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
317 317 'repository.admin')
318 318 def changeset_patch(self, revision):
319 319 return self._index(revision, method='patch')
320 320
321 321 @LoginRequired()
322 322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
323 323 'repository.admin')
324 324 def changeset_download(self, revision):
325 325 return self._index(revision, method='download')
326 326
327 327 @LoginRequired()
328 328 @NotAnonymous()
329 329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
330 330 'repository.admin')
331 331 @auth.CSRFRequired()
332 332 @jsonify
333 333 def comment(self, repo_name, revision):
334 334 commit_id = revision
335 335 status = request.POST.get('changeset_status', None)
336 336 text = request.POST.get('text')
337 comment_type = request.POST.get('comment_type')
338
337 339 if status:
338 340 text = text or (_('Status change %(transition_icon)s %(status)s')
339 341 % {'transition_icon': '>',
340 342 'status': ChangesetStatus.get_status_lbl(status)})
341 343
342 344 multi_commit_ids = filter(
343 345 lambda s: s not in ['', None],
344 346 request.POST.get('commit_ids', '').split(','),)
345 347
346 348 commit_ids = multi_commit_ids or [commit_id]
347 349 comment = None
348 350 for current_id in filter(None, commit_ids):
349 351 c.co = comment = CommentsModel().create(
350 352 text=text,
351 353 repo=c.rhodecode_db_repo.repo_id,
352 354 user=c.rhodecode_user.user_id,
353 355 commit_id=current_id,
354 356 f_path=request.POST.get('f_path'),
355 357 line_no=request.POST.get('line'),
356 358 status_change=(ChangesetStatus.get_status_lbl(status)
357 359 if status else None),
358 status_change_type=status
360 status_change_type=status,
361 comment_type=comment_type
359 362 )
360 363 c.inline_comment = True if comment.line_no else False
361 364
362 365 # get status if set !
363 366 if status:
364 367 # if latest status was from pull request and it's closed
365 368 # disallow changing status !
366 369 # dont_allow_on_closed_pull_request = True !
367 370
368 371 try:
369 372 ChangesetStatusModel().set_status(
370 373 c.rhodecode_db_repo.repo_id,
371 374 status,
372 375 c.rhodecode_user.user_id,
373 376 comment,
374 377 revision=current_id,
375 378 dont_allow_on_closed_pull_request=True
376 379 )
377 380 except StatusChangeOnClosedPullRequestError:
378 381 msg = _('Changing the status of a commit associated with '
379 382 'a closed pull request is not allowed')
380 383 log.exception(msg)
381 384 h.flash(msg, category='warning')
382 385 return redirect(h.url(
383 386 'changeset_home', repo_name=repo_name,
384 387 revision=current_id))
385 388
386 389 # finalize, commit and redirect
387 390 Session().commit()
388 391
389 392 data = {
390 393 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
391 394 }
392 395 if comment:
393 396 data.update(comment.get_dict())
394 397 data.update({'rendered_text':
395 398 render('changeset/changeset_comment_block.mako')})
396 399
397 400 return data
398 401
399 402 @LoginRequired()
400 403 @NotAnonymous()
401 404 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
402 405 'repository.admin')
403 406 @auth.CSRFRequired()
404 407 def preview_comment(self):
405 408 # Technically a CSRF token is not needed as no state changes with this
406 409 # call. However, as this is a POST is better to have it, so automated
407 410 # tools don't flag it as potential CSRF.
408 411 # Post is required because the payload could be bigger than the maximum
409 412 # allowed by GET.
410 413 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
411 414 raise HTTPBadRequest()
412 415 text = request.POST.get('text')
413 416 renderer = request.POST.get('renderer') or 'rst'
414 417 if text:
415 418 return h.render(text, renderer=renderer, mentions=True)
416 419 return ''
417 420
418 421 @LoginRequired()
419 422 @NotAnonymous()
420 423 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
421 424 'repository.admin')
422 425 @auth.CSRFRequired()
423 426 @jsonify
424 427 def delete_comment(self, repo_name, comment_id):
425 428 comment = ChangesetComment.get(comment_id)
426 429 owner = (comment.author.user_id == c.rhodecode_user.user_id)
427 430 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
428 431 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
429 432 CommentsModel().delete(comment=comment)
430 433 Session().commit()
431 434 return True
432 435 else:
433 436 raise HTTPForbidden()
434 437
435 438 @LoginRequired()
436 439 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
437 440 'repository.admin')
438 441 @jsonify
439 442 def changeset_info(self, repo_name, revision):
440 443 if request.is_xhr:
441 444 try:
442 445 return c.rhodecode_repo.get_commit(commit_id=revision)
443 446 except CommitDoesNotExistError as e:
444 447 return EmptyCommit(message=str(e))
445 448 else:
446 449 raise HTTPBadRequest()
447 450
448 451 @LoginRequired()
449 452 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
450 453 'repository.admin')
451 454 @jsonify
452 455 def changeset_children(self, repo_name, revision):
453 456 if request.is_xhr:
454 457 commit = c.rhodecode_repo.get_commit(commit_id=revision)
455 458 result = {"results": commit.children}
456 459 return result
457 460 else:
458 461 raise HTTPBadRequest()
459 462
460 463 @LoginRequired()
461 464 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
462 465 'repository.admin')
463 466 @jsonify
464 467 def changeset_parents(self, repo_name, revision):
465 468 if request.is_xhr:
466 469 commit = c.rhodecode_repo.get_commit(commit_id=revision)
467 470 result = {"results": commit.parents}
468 471 return result
469 472 else:
470 473 raise HTTPBadRequest()
@@ -1,1024 +1,1026 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24 import types
25 25
26 26 import peppercorn
27 27 import formencode
28 28 import logging
29 29 import collections
30 30
31 31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 32 from pylons import request, tmpl_context as c, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from pyramid.threadlocal import get_current_registry
36 36 from sqlalchemy.sql import func
37 37 from sqlalchemy.sql.expression import or_
38 38
39 39 from rhodecode import events
40 40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 41 from rhodecode.lib.ext_json import json
42 42 from rhodecode.lib.base import (
43 43 BaseRepoController, render, vcs_operation_context)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 46 HasAcceptedRepoType, XHRRequired)
47 47 from rhodecode.lib.channelstream import channelstream_request
48 48 from rhodecode.lib.utils import jsonify
49 49 from rhodecode.lib.utils2 import (
50 50 safe_int, safe_str, str2bool, safe_unicode)
51 51 from rhodecode.lib.vcs.backends.base import (
52 52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 55 NodeDoesNotExistError)
56 56
57 57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 58 from rhodecode.model.comment import CommentsModel
59 59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 60 Repository, PullRequestVersion)
61 61 from rhodecode.model.forms import PullRequestForm
62 62 from rhodecode.model.meta import Session
63 63 from rhodecode.model.pull_request import PullRequestModel
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 class PullrequestsController(BaseRepoController):
69 69 def __before__(self):
70 70 super(PullrequestsController, self).__before__()
71 71
72 72 def _load_compare_data(self, pull_request, inline_comments):
73 73 """
74 74 Load context data needed for generating compare diff
75 75
76 76 :param pull_request: object related to the request
77 77 :param enable_comments: flag to determine if comments are included
78 78 """
79 79 source_repo = pull_request.source_repo
80 80 source_ref_id = pull_request.source_ref_parts.commit_id
81 81
82 82 target_repo = pull_request.target_repo
83 83 target_ref_id = pull_request.target_ref_parts.commit_id
84 84
85 85 # despite opening commits for bookmarks/branches/tags, we always
86 86 # convert this to rev to prevent changes after bookmark or branch change
87 87 c.source_ref_type = 'rev'
88 88 c.source_ref = source_ref_id
89 89
90 90 c.target_ref_type = 'rev'
91 91 c.target_ref = target_ref_id
92 92
93 93 c.source_repo = source_repo
94 94 c.target_repo = target_repo
95 95
96 96 c.fulldiff = bool(request.GET.get('fulldiff'))
97 97
98 98 # diff_limit is the old behavior, will cut off the whole diff
99 99 # if the limit is applied otherwise will just hide the
100 100 # big files from the front-end
101 101 diff_limit = self.cut_off_limit_diff
102 102 file_limit = self.cut_off_limit_file
103 103
104 104 pre_load = ["author", "branch", "date", "message"]
105 105
106 106 c.commit_ranges = []
107 107 source_commit = EmptyCommit()
108 108 target_commit = EmptyCommit()
109 109 c.missing_requirements = False
110 110 try:
111 111 c.commit_ranges = [
112 112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 113 for rev in pull_request.revisions]
114 114
115 115 c.statuses = source_repo.statuses(
116 116 [x.raw_id for x in c.commit_ranges])
117 117
118 118 target_commit = source_repo.get_commit(
119 119 commit_id=safe_str(target_ref_id))
120 120 source_commit = source_repo.get_commit(
121 121 commit_id=safe_str(source_ref_id))
122 122 except RepositoryRequirementError:
123 123 c.missing_requirements = True
124 124
125 125 # auto collapse if we have more than limit
126 126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128 128
129 129 c.changes = {}
130 130 c.missing_commits = False
131 131 if (c.missing_requirements or
132 132 isinstance(source_commit, EmptyCommit) or
133 133 source_commit == target_commit):
134 134 _parsed = []
135 135 c.missing_commits = True
136 136 else:
137 137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 138 diff_processor = diffs.DiffProcessor(
139 139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 140 file_limit=file_limit, show_full_diff=c.fulldiff)
141 141
142 142 _parsed = diff_processor.prepare()
143 143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144 144
145 145 included_files = {}
146 146 for f in _parsed:
147 147 included_files[f['filename']] = f['stats']
148 148
149 149 c.deleted_files = [fname for fname in inline_comments if
150 150 fname not in included_files]
151 151
152 152 c.deleted_files_comments = collections.defaultdict(dict)
153 153 for fname, per_line_comments in inline_comments.items():
154 154 if fname in c.deleted_files:
155 155 c.deleted_files_comments[fname]['stats'] = 0
156 156 c.deleted_files_comments[fname]['comments'] = list()
157 157 for lno, comments in per_line_comments.items():
158 158 c.deleted_files_comments[fname]['comments'].extend(comments)
159 159
160 160 def _node_getter(commit):
161 161 def get_node(fname):
162 162 try:
163 163 return commit.get_node(fname)
164 164 except NodeDoesNotExistError:
165 165 return None
166 166 return get_node
167 167
168 168 c.diffset = codeblocks.DiffSet(
169 169 repo_name=c.repo_name,
170 170 source_repo_name=c.source_repo.repo_name,
171 171 source_node_getter=_node_getter(target_commit),
172 172 target_node_getter=_node_getter(source_commit),
173 173 comments=inline_comments
174 174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175 175
176 176 def _extract_ordering(self, request):
177 177 column_index = safe_int(request.GET.get('order[0][column]'))
178 178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 179 order_by = request.GET.get(
180 180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 181 return order_by, order_dir
182 182
183 183 @LoginRequired()
184 184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 185 'repository.admin')
186 186 @HasAcceptedRepoType('git', 'hg')
187 187 def show_all(self, repo_name):
188 188 # filter types
189 189 c.active = 'open'
190 190 c.source = str2bool(request.GET.get('source'))
191 191 c.closed = str2bool(request.GET.get('closed'))
192 192 c.my = str2bool(request.GET.get('my'))
193 193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 195 c.repo_name = repo_name
196 196
197 197 opened_by = None
198 198 if c.my:
199 199 c.active = 'my'
200 200 opened_by = [c.rhodecode_user.user_id]
201 201
202 202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 203 if c.closed:
204 204 c.active = 'closed'
205 205 statuses = [PullRequest.STATUS_CLOSED]
206 206
207 207 if c.awaiting_review and not c.source:
208 208 c.active = 'awaiting'
209 209 if c.source and not c.awaiting_review:
210 210 c.active = 'source'
211 211 if c.awaiting_my_review:
212 212 c.active = 'awaiting_my'
213 213
214 214 data = self._get_pull_requests_list(
215 215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 216 if not request.is_xhr:
217 217 c.data = json.dumps(data['data'])
218 218 c.records_total = data['recordsTotal']
219 219 return render('/pullrequests/pullrequests.mako')
220 220 else:
221 221 return json.dumps(data)
222 222
223 223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 224 # pagination
225 225 start = safe_int(request.GET.get('start'), 0)
226 226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 227 order_by, order_dir = self._extract_ordering(request)
228 228
229 229 if c.awaiting_review:
230 230 pull_requests = PullRequestModel().get_awaiting_review(
231 231 repo_name, source=c.source, opened_by=opened_by,
232 232 statuses=statuses, offset=start, length=length,
233 233 order_by=order_by, order_dir=order_dir)
234 234 pull_requests_total_count = PullRequestModel(
235 235 ).count_awaiting_review(
236 236 repo_name, source=c.source, statuses=statuses,
237 237 opened_by=opened_by)
238 238 elif c.awaiting_my_review:
239 239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 240 repo_name, source=c.source, opened_by=opened_by,
241 241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 242 offset=start, length=length, order_by=order_by,
243 243 order_dir=order_dir)
244 244 pull_requests_total_count = PullRequestModel(
245 245 ).count_awaiting_my_review(
246 246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 247 statuses=statuses, opened_by=opened_by)
248 248 else:
249 249 pull_requests = PullRequestModel().get_all(
250 250 repo_name, source=c.source, opened_by=opened_by,
251 251 statuses=statuses, offset=start, length=length,
252 252 order_by=order_by, order_dir=order_dir)
253 253 pull_requests_total_count = PullRequestModel().count_all(
254 254 repo_name, source=c.source, statuses=statuses,
255 255 opened_by=opened_by)
256 256
257 257 from rhodecode.lib.utils import PartialRenderer
258 258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 259 data = []
260 260 for pr in pull_requests:
261 261 comments = CommentsModel().get_all_comments(
262 262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263 263
264 264 data.append({
265 265 'name': _render('pullrequest_name',
266 266 pr.pull_request_id, pr.target_repo.repo_name),
267 267 'name_raw': pr.pull_request_id,
268 268 'status': _render('pullrequest_status',
269 269 pr.calculated_review_status()),
270 270 'title': _render(
271 271 'pullrequest_title', pr.title, pr.description),
272 272 'description': h.escape(pr.description),
273 273 'updated_on': _render('pullrequest_updated_on',
274 274 h.datetime_to_time(pr.updated_on)),
275 275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 276 'created_on': _render('pullrequest_updated_on',
277 277 h.datetime_to_time(pr.created_on)),
278 278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 279 'author': _render('pullrequest_author',
280 280 pr.author.full_contact, ),
281 281 'author_raw': pr.author.full_name,
282 282 'comments': _render('pullrequest_comments', len(comments)),
283 283 'comments_raw': len(comments),
284 284 'closed': pr.is_closed(),
285 285 })
286 286 # json used to render the grid
287 287 data = ({
288 288 'data': data,
289 289 'recordsTotal': pull_requests_total_count,
290 290 'recordsFiltered': pull_requests_total_count,
291 291 })
292 292 return data
293 293
294 294 @LoginRequired()
295 295 @NotAnonymous()
296 296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 297 'repository.admin')
298 298 @HasAcceptedRepoType('git', 'hg')
299 299 def index(self):
300 300 source_repo = c.rhodecode_db_repo
301 301
302 302 try:
303 303 source_repo.scm_instance().get_commit()
304 304 except EmptyRepositoryError:
305 305 h.flash(h.literal(_('There are no commits yet')),
306 306 category='warning')
307 307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308 308
309 309 commit_id = request.GET.get('commit')
310 310 branch_ref = request.GET.get('branch')
311 311 bookmark_ref = request.GET.get('bookmark')
312 312
313 313 try:
314 314 source_repo_data = PullRequestModel().generate_repo_data(
315 315 source_repo, commit_id=commit_id,
316 316 branch=branch_ref, bookmark=bookmark_ref)
317 317 except CommitDoesNotExistError as e:
318 318 log.exception(e)
319 319 h.flash(_('Commit does not exist'), 'error')
320 320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321 321
322 322 default_target_repo = source_repo
323 323
324 324 if source_repo.parent:
325 325 parent_vcs_obj = source_repo.parent.scm_instance()
326 326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 327 # change default if we have a parent repo
328 328 default_target_repo = source_repo.parent
329 329
330 330 target_repo_data = PullRequestModel().generate_repo_data(
331 331 default_target_repo)
332 332
333 333 selected_source_ref = source_repo_data['refs']['selected_ref']
334 334
335 335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 337 source=source_repo.repo_name,
338 338 source_ref=title_source_ref,
339 339 target=default_target_repo.repo_name
340 340 )
341 341
342 342 c.default_repo_data = {
343 343 'source_repo_name': source_repo.repo_name,
344 344 'source_refs_json': json.dumps(source_repo_data),
345 345 'target_repo_name': default_target_repo.repo_name,
346 346 'target_refs_json': json.dumps(target_repo_data),
347 347 }
348 348 c.default_source_ref = selected_source_ref
349 349
350 350 return render('/pullrequests/pullrequest.mako')
351 351
352 352 @LoginRequired()
353 353 @NotAnonymous()
354 354 @XHRRequired()
355 355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 356 'repository.admin')
357 357 @jsonify
358 358 def get_repo_refs(self, repo_name, target_repo_name):
359 359 repo = Repository.get_by_repo_name(target_repo_name)
360 360 if not repo:
361 361 raise HTTPNotFound
362 362 return PullRequestModel().generate_repo_data(repo)
363 363
364 364 @LoginRequired()
365 365 @NotAnonymous()
366 366 @XHRRequired()
367 367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 368 'repository.admin')
369 369 @jsonify
370 370 def get_repo_destinations(self, repo_name):
371 371 repo = Repository.get_by_repo_name(repo_name)
372 372 if not repo:
373 373 raise HTTPNotFound
374 374 filter_query = request.GET.get('query')
375 375
376 376 query = Repository.query() \
377 377 .order_by(func.length(Repository.repo_name)) \
378 378 .filter(or_(
379 379 Repository.repo_name == repo.repo_name,
380 380 Repository.fork_id == repo.repo_id))
381 381
382 382 if filter_query:
383 383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 384 query = query.filter(
385 385 Repository.repo_name.ilike(ilike_expression))
386 386
387 387 add_parent = False
388 388 if repo.parent:
389 389 if filter_query in repo.parent.repo_name:
390 390 parent_vcs_obj = repo.parent.scm_instance()
391 391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 392 add_parent = True
393 393
394 394 limit = 20 - 1 if add_parent else 20
395 395 all_repos = query.limit(limit).all()
396 396 if add_parent:
397 397 all_repos += [repo.parent]
398 398
399 399 repos = []
400 400 for obj in self.scm_model.get_repos(all_repos):
401 401 repos.append({
402 402 'id': obj['name'],
403 403 'text': obj['name'],
404 404 'type': 'repo',
405 405 'obj': obj['dbrepo']
406 406 })
407 407
408 408 data = {
409 409 'more': False,
410 410 'results': [{
411 411 'text': _('Repositories'),
412 412 'children': repos
413 413 }] if repos else []
414 414 }
415 415 return data
416 416
417 417 @LoginRequired()
418 418 @NotAnonymous()
419 419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 420 'repository.admin')
421 421 @HasAcceptedRepoType('git', 'hg')
422 422 @auth.CSRFRequired()
423 423 def create(self, repo_name):
424 424 repo = Repository.get_by_repo_name(repo_name)
425 425 if not repo:
426 426 raise HTTPNotFound
427 427
428 428 controls = peppercorn.parse(request.POST.items())
429 429
430 430 try:
431 431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 432 except formencode.Invalid as errors:
433 433 if errors.error_dict.get('revisions'):
434 434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 435 elif errors.error_dict.get('pullrequest_title'):
436 436 msg = _('Pull request requires a title with min. 3 chars')
437 437 else:
438 438 msg = _('Error creating pull request: {}').format(errors)
439 439 log.exception(msg)
440 440 h.flash(msg, 'error')
441 441
442 442 # would rather just go back to form ...
443 443 return redirect(url('pullrequest_home', repo_name=repo_name))
444 444
445 445 source_repo = _form['source_repo']
446 446 source_ref = _form['source_ref']
447 447 target_repo = _form['target_repo']
448 448 target_ref = _form['target_ref']
449 449 commit_ids = _form['revisions'][::-1]
450 450 reviewers = [
451 451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452 452
453 453 # find the ancestor for this pr
454 454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456 456
457 457 source_scm = source_db_repo.scm_instance()
458 458 target_scm = target_db_repo.scm_instance()
459 459
460 460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462 462
463 463 ancestor = source_scm.get_common_ancestor(
464 464 source_commit.raw_id, target_commit.raw_id, target_scm)
465 465
466 466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468 468
469 469 pullrequest_title = _form['pullrequest_title']
470 470 title_source_ref = source_ref.split(':', 2)[1]
471 471 if not pullrequest_title:
472 472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 473 source=source_repo,
474 474 source_ref=title_source_ref,
475 475 target=target_repo
476 476 )
477 477
478 478 description = _form['pullrequest_desc']
479 479 try:
480 480 pull_request = PullRequestModel().create(
481 481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 482 target_ref, commit_ids, reviewers, pullrequest_title,
483 483 description
484 484 )
485 485 Session().commit()
486 486 h.flash(_('Successfully opened new pull request'),
487 487 category='success')
488 488 except Exception as e:
489 489 msg = _('Error occurred during sending pull request')
490 490 log.exception(msg)
491 491 h.flash(msg, category='error')
492 492 return redirect(url('pullrequest_home', repo_name=repo_name))
493 493
494 494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 495 pull_request_id=pull_request.pull_request_id))
496 496
497 497 @LoginRequired()
498 498 @NotAnonymous()
499 499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 500 'repository.admin')
501 501 @auth.CSRFRequired()
502 502 @jsonify
503 503 def update(self, repo_name, pull_request_id):
504 504 pull_request_id = safe_int(pull_request_id)
505 505 pull_request = PullRequest.get_or_404(pull_request_id)
506 506 # only owner or admin can update it
507 507 allowed_to_update = PullRequestModel().check_user_update(
508 508 pull_request, c.rhodecode_user)
509 509 if allowed_to_update:
510 510 controls = peppercorn.parse(request.POST.items())
511 511
512 512 if 'review_members' in controls:
513 513 self._update_reviewers(
514 514 pull_request_id, controls['review_members'])
515 515 elif str2bool(request.POST.get('update_commits', 'false')):
516 516 self._update_commits(pull_request)
517 517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 518 self._reject_close(pull_request)
519 519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 520 self._edit_pull_request(pull_request)
521 521 else:
522 522 raise HTTPBadRequest()
523 523 return True
524 524 raise HTTPForbidden()
525 525
526 526 def _edit_pull_request(self, pull_request):
527 527 try:
528 528 PullRequestModel().edit(
529 529 pull_request, request.POST.get('title'),
530 530 request.POST.get('description'))
531 531 except ValueError:
532 532 msg = _(u'Cannot update closed pull requests.')
533 533 h.flash(msg, category='error')
534 534 return
535 535 else:
536 536 Session().commit()
537 537
538 538 msg = _(u'Pull request title & description updated.')
539 539 h.flash(msg, category='success')
540 540 return
541 541
542 542 def _update_commits(self, pull_request):
543 543 resp = PullRequestModel().update_commits(pull_request)
544 544
545 545 if resp.executed:
546 546 msg = _(
547 547 u'Pull request updated to "{source_commit_id}" with '
548 548 u'{count_added} added, {count_removed} removed commits.')
549 549 msg = msg.format(
550 550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 551 count_added=len(resp.changes.added),
552 552 count_removed=len(resp.changes.removed))
553 553 h.flash(msg, category='success')
554 554
555 555 registry = get_current_registry()
556 556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 558 if channelstream_config.get('enabled'):
559 559 message = msg + (
560 560 ' - <a onclick="window.location.reload()">'
561 561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 562 channel = '/repo${}$/pr/{}'.format(
563 563 pull_request.target_repo.repo_name,
564 564 pull_request.pull_request_id
565 565 )
566 566 payload = {
567 567 'type': 'message',
568 568 'user': 'system',
569 569 'exclude_users': [request.user.username],
570 570 'channel': channel,
571 571 'message': {
572 572 'message': message,
573 573 'level': 'success',
574 574 'topic': '/notifications'
575 575 }
576 576 }
577 577 channelstream_request(
578 578 channelstream_config, [payload], '/message',
579 579 raise_exc=False)
580 580 else:
581 581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 582 warning_reasons = [
583 583 UpdateFailureReason.NO_CHANGE,
584 584 UpdateFailureReason.WRONG_REF_TPYE,
585 585 ]
586 586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 587 h.flash(msg, category=category)
588 588
589 589 @auth.CSRFRequired()
590 590 @LoginRequired()
591 591 @NotAnonymous()
592 592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 593 'repository.admin')
594 594 def merge(self, repo_name, pull_request_id):
595 595 """
596 596 POST /{repo_name}/pull-request/{pull_request_id}
597 597
598 598 Merge will perform a server-side merge of the specified
599 599 pull request, if the pull request is approved and mergeable.
600 600 After succesfull merging, the pull request is automatically
601 601 closed, with a relevant comment.
602 602 """
603 603 pull_request_id = safe_int(pull_request_id)
604 604 pull_request = PullRequest.get_or_404(pull_request_id)
605 605 user = c.rhodecode_user
606 606
607 607 if self._meets_merge_pre_conditions(pull_request, user):
608 608 log.debug("Pre-conditions checked, trying to merge.")
609 609 extras = vcs_operation_context(
610 610 request.environ, repo_name=pull_request.target_repo.repo_name,
611 611 username=user.username, action='push',
612 612 scm=pull_request.target_repo.repo_type)
613 613 self._merge_pull_request(pull_request, user, extras)
614 614
615 615 return redirect(url(
616 616 'pullrequest_show',
617 617 repo_name=pull_request.target_repo.repo_name,
618 618 pull_request_id=pull_request.pull_request_id))
619 619
620 620 def _meets_merge_pre_conditions(self, pull_request, user):
621 621 if not PullRequestModel().check_user_merge(pull_request, user):
622 622 raise HTTPForbidden()
623 623
624 624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 625 if not merge_status:
626 626 log.debug("Cannot merge, not mergeable.")
627 627 h.flash(msg, category='error')
628 628 return False
629 629
630 630 if (pull_request.calculated_review_status()
631 631 is not ChangesetStatus.STATUS_APPROVED):
632 632 log.debug("Cannot merge, approval is pending.")
633 633 msg = _('Pull request reviewer approval is pending.')
634 634 h.flash(msg, category='error')
635 635 return False
636 636 return True
637 637
638 638 def _merge_pull_request(self, pull_request, user, extras):
639 639 merge_resp = PullRequestModel().merge(
640 640 pull_request, user, extras=extras)
641 641
642 642 if merge_resp.executed:
643 643 log.debug("The merge was successful, closing the pull request.")
644 644 PullRequestModel().close_pull_request(
645 645 pull_request.pull_request_id, user)
646 646 Session().commit()
647 647 msg = _('Pull request was successfully merged and closed.')
648 648 h.flash(msg, category='success')
649 649 else:
650 650 log.debug(
651 651 "The merge was not successful. Merge response: %s",
652 652 merge_resp)
653 653 msg = PullRequestModel().merge_status_message(
654 654 merge_resp.failure_reason)
655 655 h.flash(msg, category='error')
656 656
657 657 def _update_reviewers(self, pull_request_id, review_members):
658 658 reviewers = [
659 659 (int(r['user_id']), r['reasons']) for r in review_members]
660 660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 661 Session().commit()
662 662
663 663 def _reject_close(self, pull_request):
664 664 if pull_request.is_closed():
665 665 raise HTTPForbidden()
666 666
667 667 PullRequestModel().close_pull_request_with_comment(
668 668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 669 Session().commit()
670 670
671 671 @LoginRequired()
672 672 @NotAnonymous()
673 673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 674 'repository.admin')
675 675 @auth.CSRFRequired()
676 676 @jsonify
677 677 def delete(self, repo_name, pull_request_id):
678 678 pull_request_id = safe_int(pull_request_id)
679 679 pull_request = PullRequest.get_or_404(pull_request_id)
680 680 # only owner can delete it !
681 681 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 682 PullRequestModel().delete(pull_request)
683 683 Session().commit()
684 684 h.flash(_('Successfully deleted pull request'),
685 685 category='success')
686 686 return redirect(url('my_account_pullrequests'))
687 687 raise HTTPForbidden()
688 688
689 689 def _get_pr_version(self, pull_request_id, version=None):
690 690 pull_request_id = safe_int(pull_request_id)
691 691 at_version = None
692 692
693 693 if version and version == 'latest':
694 694 pull_request_ver = PullRequest.get(pull_request_id)
695 695 pull_request_obj = pull_request_ver
696 696 _org_pull_request_obj = pull_request_obj
697 697 at_version = 'latest'
698 698 elif version:
699 699 pull_request_ver = PullRequestVersion.get_or_404(version)
700 700 pull_request_obj = pull_request_ver
701 701 _org_pull_request_obj = pull_request_ver.pull_request
702 702 at_version = pull_request_ver.pull_request_version_id
703 703 else:
704 704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
705 705
706 706 pull_request_display_obj = PullRequest.get_pr_display_object(
707 707 pull_request_obj, _org_pull_request_obj)
708 708 return _org_pull_request_obj, pull_request_obj, \
709 709 pull_request_display_obj, at_version
710 710
711 711 def _get_pr_version_changes(self, version, pull_request_latest):
712 712 """
713 713 Generate changes commits, and diff data based on the current pr version
714 714 """
715 715
716 716 #TODO(marcink): save those changes as JSON metadata for chaching later.
717 717
718 718 # fake the version to add the "initial" state object
719 719 pull_request_initial = PullRequest.get_pr_display_object(
720 720 pull_request_latest, pull_request_latest,
721 721 internal_methods=['get_commit', 'versions'])
722 722 pull_request_initial.revisions = []
723 723 pull_request_initial.source_repo.get_commit = types.MethodType(
724 724 lambda *a, **k: EmptyCommit(), pull_request_initial)
725 725 pull_request_initial.source_repo.scm_instance = types.MethodType(
726 726 lambda *a, **k: EmptyRepository(), pull_request_initial)
727 727
728 728 _changes_versions = [pull_request_latest] + \
729 729 list(reversed(c.versions)) + \
730 730 [pull_request_initial]
731 731
732 732 if version == 'latest':
733 733 index = 0
734 734 else:
735 735 for pos, prver in enumerate(_changes_versions):
736 736 ver = getattr(prver, 'pull_request_version_id', -1)
737 737 if ver == safe_int(version):
738 738 index = pos
739 739 break
740 740 else:
741 741 index = 0
742 742
743 743 cur_obj = _changes_versions[index]
744 744 prev_obj = _changes_versions[index + 1]
745 745
746 746 old_commit_ids = set(prev_obj.revisions)
747 747 new_commit_ids = set(cur_obj.revisions)
748 748
749 749 changes = PullRequestModel()._calculate_commit_id_changes(
750 750 old_commit_ids, new_commit_ids)
751 751
752 752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
753 753 cur_obj, prev_obj)
754 754 file_changes = PullRequestModel()._calculate_file_changes(
755 755 old_diff_data, new_diff_data)
756 756 return changes, file_changes
757 757
758 758 @LoginRequired()
759 759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 760 'repository.admin')
761 761 def show(self, repo_name, pull_request_id):
762 762 pull_request_id = safe_int(pull_request_id)
763 763 version = request.GET.get('version')
764 764
765 765 (pull_request_latest,
766 766 pull_request_at_ver,
767 767 pull_request_display_obj,
768 768 at_version) = self._get_pr_version(pull_request_id, version=version)
769 769
770 770 c.template_context['pull_request_data']['pull_request_id'] = \
771 771 pull_request_id
772 772
773 773 # pull_requests repo_name we opened it against
774 774 # ie. target_repo must match
775 775 if repo_name != pull_request_at_ver.target_repo.repo_name:
776 776 raise HTTPNotFound
777 777
778 778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
779 779 pull_request_at_ver)
780 780
781 781 pr_closed = pull_request_latest.is_closed()
782 782 if at_version and not at_version == 'latest':
783 783 c.allowed_to_change_status = False
784 784 c.allowed_to_update = False
785 785 c.allowed_to_merge = False
786 786 c.allowed_to_delete = False
787 787 c.allowed_to_comment = False
788 788 else:
789 789 c.allowed_to_change_status = PullRequestModel(). \
790 790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
791 791 c.allowed_to_update = PullRequestModel().check_user_update(
792 792 pull_request_latest, c.rhodecode_user) and not pr_closed
793 793 c.allowed_to_merge = PullRequestModel().check_user_merge(
794 794 pull_request_latest, c.rhodecode_user) and not pr_closed
795 795 c.allowed_to_delete = PullRequestModel().check_user_delete(
796 796 pull_request_latest, c.rhodecode_user) and not pr_closed
797 797 c.allowed_to_comment = not pr_closed
798 798
799 799 cc_model = CommentsModel()
800 800
801 801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
802 802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
803 803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
804 804 pull_request_at_ver)
805 805 c.approval_msg = None
806 806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
807 807 c.approval_msg = _('Reviewer approval is pending.')
808 808 c.pr_merge_status = False
809 809
810 810 # inline comments
811 811 inline_comments = cc_model.get_inline_comments(
812 812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
813 813
814 814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
815 815 inline_comments, version=at_version, include_aggregates=True)
816 816
817 817 c.versions = pull_request_display_obj.versions()
818 818 c.at_version_num = at_version if at_version and at_version != 'latest' else None
819 819 c.at_version_pos = ChangesetComment.get_index_from_version(
820 820 c.at_version_num, c.versions)
821 821
822 822 is_outdated = lambda co: \
823 823 not c.at_version_num \
824 824 or co.pull_request_version_id <= c.at_version_num
825 825
826 826 # inline_comments_until_version
827 827 if c.at_version_num:
828 828 # if we use version, then do not show later comments
829 829 # than current version
830 830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
831 831 for fname, per_line_comments in inline_comments.iteritems():
832 832 for lno, comments in per_line_comments.iteritems():
833 833 for co in comments:
834 834 if co.pull_request_version_id and is_outdated(co):
835 835 paths[co.f_path][co.line_no].append(co)
836 836 inline_comments = paths
837 837
838 838 # outdated comments
839 839 c.outdated_cnt = 0
840 840 if CommentsModel.use_outdated_comments(pull_request_latest):
841 841 outdated_comments = cc_model.get_outdated_comments(
842 842 c.rhodecode_db_repo.repo_id,
843 843 pull_request=pull_request_at_ver)
844 844
845 845 # Count outdated comments and check for deleted files
846 846 is_outdated = lambda co: \
847 847 not c.at_version_num \
848 848 or co.pull_request_version_id < c.at_version_num
849 849 for file_name, lines in outdated_comments.iteritems():
850 850 for comments in lines.values():
851 851 comments = [comm for comm in comments if is_outdated(comm)]
852 852 c.outdated_cnt += len(comments)
853 853
854 854 # load compare data into template context
855 855 self._load_compare_data(pull_request_at_ver, inline_comments)
856 856
857 857 # this is a hack to properly display links, when creating PR, the
858 858 # compare view and others uses different notation, and
859 859 # compare_commits.mako renders links based on the target_repo.
860 860 # We need to swap that here to generate it properly on the html side
861 861 c.target_repo = c.source_repo
862 862
863 863 # general comments
864 864 c.comments = cc_model.get_comments(
865 865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
866 866
867 867 if c.allowed_to_update:
868 868 force_close = ('forced_closed', _('Close Pull Request'))
869 869 statuses = ChangesetStatus.STATUSES + [force_close]
870 870 else:
871 871 statuses = ChangesetStatus.STATUSES
872 872 c.commit_statuses = statuses
873 873
874 874 c.ancestor = None # TODO: add ancestor here
875 875 c.pull_request = pull_request_display_obj
876 876 c.pull_request_latest = pull_request_latest
877 877 c.at_version = at_version
878 878
879 879 c.changes = None
880 880 c.file_changes = None
881 881
882 882 c.show_version_changes = 1 # control flag, not used yet
883 883
884 884 if at_version and c.show_version_changes:
885 885 c.changes, c.file_changes = self._get_pr_version_changes(
886 886 version, pull_request_latest)
887 887
888 888 return render('/pullrequests/pullrequest_show.mako')
889 889
890 890 @LoginRequired()
891 891 @NotAnonymous()
892 892 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
893 893 'repository.admin')
894 894 @auth.CSRFRequired()
895 895 @jsonify
896 896 def comment(self, repo_name, pull_request_id):
897 897 pull_request_id = safe_int(pull_request_id)
898 898 pull_request = PullRequest.get_or_404(pull_request_id)
899 899 if pull_request.is_closed():
900 900 raise HTTPForbidden()
901 901
902 902 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
903 903 # as a changeset status, still we want to send it in one value.
904 904 status = request.POST.get('changeset_status', None)
905 905 text = request.POST.get('text')
906 comment_type = request.POST.get('comment_type')
906 907 if status and '_closed' in status:
907 908 close_pr = True
908 909 status = status.replace('_closed', '')
909 910 else:
910 911 close_pr = False
911 912
912 913 forced = (status == 'forced')
913 914 if forced:
914 915 status = 'rejected'
915 916
916 917 allowed_to_change_status = PullRequestModel().check_user_change_status(
917 918 pull_request, c.rhodecode_user)
918 919
919 920 if status and allowed_to_change_status:
920 921 message = (_('Status change %(transition_icon)s %(status)s')
921 922 % {'transition_icon': '>',
922 923 'status': ChangesetStatus.get_status_lbl(status)})
923 924 if close_pr:
924 925 message = _('Closing with') + ' ' + message
925 926 text = text or message
926 927 comm = CommentsModel().create(
927 928 text=text,
928 929 repo=c.rhodecode_db_repo.repo_id,
929 930 user=c.rhodecode_user.user_id,
930 931 pull_request=pull_request_id,
931 932 f_path=request.POST.get('f_path'),
932 933 line_no=request.POST.get('line'),
933 934 status_change=(ChangesetStatus.get_status_lbl(status)
934 935 if status and allowed_to_change_status else None),
935 936 status_change_type=(status
936 937 if status and allowed_to_change_status else None),
937 closing_pr=close_pr
938 closing_pr=close_pr,
939 comment_type=comment_type
938 940 )
939 941
940 942 if allowed_to_change_status:
941 943 old_calculated_status = pull_request.calculated_review_status()
942 944 # get status if set !
943 945 if status:
944 946 ChangesetStatusModel().set_status(
945 947 c.rhodecode_db_repo.repo_id,
946 948 status,
947 949 c.rhodecode_user.user_id,
948 950 comm,
949 951 pull_request=pull_request_id
950 952 )
951 953
952 954 Session().flush()
953 955 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
954 956 # we now calculate the status of pull request, and based on that
955 957 # calculation we set the commits status
956 958 calculated_status = pull_request.calculated_review_status()
957 959 if old_calculated_status != calculated_status:
958 960 PullRequestModel()._trigger_pull_request_hook(
959 961 pull_request, c.rhodecode_user, 'review_status_change')
960 962
961 963 calculated_status_lbl = ChangesetStatus.get_status_lbl(
962 964 calculated_status)
963 965
964 966 if close_pr:
965 967 status_completed = (
966 968 calculated_status in [ChangesetStatus.STATUS_APPROVED,
967 969 ChangesetStatus.STATUS_REJECTED])
968 970 if forced or status_completed:
969 971 PullRequestModel().close_pull_request(
970 972 pull_request_id, c.rhodecode_user)
971 973 else:
972 974 h.flash(_('Closing pull request on other statuses than '
973 975 'rejected or approved is forbidden. '
974 976 'Calculated status from all reviewers '
975 977 'is currently: %s') % calculated_status_lbl,
976 978 category='warning')
977 979
978 980 Session().commit()
979 981
980 982 if not request.is_xhr:
981 983 return redirect(h.url('pullrequest_show', repo_name=repo_name,
982 984 pull_request_id=pull_request_id))
983 985
984 986 data = {
985 987 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
986 988 }
987 989 if comm:
988 990 c.co = comm
989 991 c.inline_comment = True if comm.line_no else False
990 992 data.update(comm.get_dict())
991 993 data.update({'rendered_text':
992 994 render('changeset/changeset_comment_block.mako')})
993 995
994 996 return data
995 997
996 998 @LoginRequired()
997 999 @NotAnonymous()
998 1000 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
999 1001 'repository.admin')
1000 1002 @auth.CSRFRequired()
1001 1003 @jsonify
1002 1004 def delete_comment(self, repo_name, comment_id):
1003 1005 return self._delete_comment(comment_id)
1004 1006
1005 1007 def _delete_comment(self, comment_id):
1006 1008 comment_id = safe_int(comment_id)
1007 1009 co = ChangesetComment.get_or_404(comment_id)
1008 1010 if co.pull_request.is_closed():
1009 1011 # don't allow deleting comments on closed pull request
1010 1012 raise HTTPForbidden()
1011 1013
1012 1014 is_owner = co.author.user_id == c.rhodecode_user.user_id
1013 1015 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1014 1016 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1015 1017 old_calculated_status = co.pull_request.calculated_review_status()
1016 1018 CommentsModel().delete(comment=co)
1017 1019 Session().commit()
1018 1020 calculated_status = co.pull_request.calculated_review_status()
1019 1021 if old_calculated_status != calculated_status:
1020 1022 PullRequestModel()._trigger_pull_request_hook(
1021 1023 co.pull_request, c.rhodecode_user, 'review_status_change')
1022 1024 return True
1023 1025 else:
1024 1026 raise HTTPForbidden()
@@ -1,596 +1,597 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import ipaddress
31 31 import pyramid.threadlocal
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36 from pylons import config, tmpl_context as c, request, session, url
37 37 from pylons.controllers import WSGIController
38 38 from pylons.controllers.util import redirect
39 39 from pylons.i18n import translation
40 40 # marcink: don't remove this import
41 41 from pylons.templating import render_mako as render # noqa
42 42 from pylons.i18n.translation import _
43 43 from webob.exc import HTTPFound
44 44
45 45
46 46 import rhodecode
47 47 from rhodecode.authentication.base import VCS_TYPE
48 48 from rhodecode.lib import auth, utils2
49 49 from rhodecode.lib import helpers as h
50 50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 51 from rhodecode.lib.exceptions import UserCreationError
52 52 from rhodecode.lib.utils import (
53 53 get_repo_slug, set_rhodecode_config, password_changed,
54 54 get_enabled_hook_classes)
55 55 from rhodecode.lib.utils2 import (
56 56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 58 from rhodecode.model import meta
59 from rhodecode.model.db import Repository, User
59 from rhodecode.model.db import Repository, User, ChangesetComment
60 60 from rhodecode.model.notification import NotificationModel
61 61 from rhodecode.model.scm import ScmModel
62 62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63 63
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 def _filter_proxy(ip):
69 69 """
70 70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 71 ips. Those comma separated IPs are passed from various proxies in the
72 72 chain of request processing. The left-most being the original client.
73 73 We only care about the first IP which came from the org. client.
74 74
75 75 :param ip: ip string from headers
76 76 """
77 77 if ',' in ip:
78 78 _ips = ip.split(',')
79 79 _first_ip = _ips[0].strip()
80 80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 81 return _first_ip
82 82 return ip
83 83
84 84
85 85 def _filter_port(ip):
86 86 """
87 87 Removes a port from ip, there are 4 main cases to handle here.
88 88 - ipv4 eg. 127.0.0.1
89 89 - ipv6 eg. ::1
90 90 - ipv4+port eg. 127.0.0.1:8080
91 91 - ipv6+port eg. [::1]:8080
92 92
93 93 :param ip:
94 94 """
95 95 def is_ipv6(ip_addr):
96 96 if hasattr(socket, 'inet_pton'):
97 97 try:
98 98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 99 except socket.error:
100 100 return False
101 101 else:
102 102 # fallback to ipaddress
103 103 try:
104 104 ipaddress.IPv6Address(ip_addr)
105 105 except Exception:
106 106 return False
107 107 return True
108 108
109 109 if ':' not in ip: # must be ipv4 pure ip
110 110 return ip
111 111
112 112 if '[' in ip and ']' in ip: # ipv6 with port
113 113 return ip.split(']')[0][1:].lower()
114 114
115 115 # must be ipv6 or ipv4 with port
116 116 if is_ipv6(ip):
117 117 return ip
118 118 else:
119 119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 120 return ip
121 121
122 122
123 123 def get_ip_addr(environ):
124 124 proxy_key = 'HTTP_X_REAL_IP'
125 125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 126 def_key = 'REMOTE_ADDR'
127 127 _filters = lambda x: _filter_port(_filter_proxy(x))
128 128
129 129 ip = environ.get(proxy_key)
130 130 if ip:
131 131 return _filters(ip)
132 132
133 133 ip = environ.get(proxy_key2)
134 134 if ip:
135 135 return _filters(ip)
136 136
137 137 ip = environ.get(def_key, '0.0.0.0')
138 138 return _filters(ip)
139 139
140 140
141 141 def get_server_ip_addr(environ, log_errors=True):
142 142 hostname = environ.get('SERVER_NAME')
143 143 try:
144 144 return socket.gethostbyname(hostname)
145 145 except Exception as e:
146 146 if log_errors:
147 147 # in some cases this lookup is not possible, and we don't want to
148 148 # make it an exception in logs
149 149 log.exception('Could not retrieve server ip address: %s', e)
150 150 return hostname
151 151
152 152
153 153 def get_server_port(environ):
154 154 return environ.get('SERVER_PORT')
155 155
156 156
157 157 def get_access_path(environ):
158 158 path = environ.get('PATH_INFO')
159 159 org_req = environ.get('pylons.original_request')
160 160 if org_req:
161 161 path = org_req.environ.get('PATH_INFO')
162 162 return path
163 163
164 164
165 165 def vcs_operation_context(
166 166 environ, repo_name, username, action, scm, check_locking=True,
167 167 is_shadow_repo=False):
168 168 """
169 169 Generate the context for a vcs operation, e.g. push or pull.
170 170
171 171 This context is passed over the layers so that hooks triggered by the
172 172 vcs operation know details like the user, the user's IP address etc.
173 173
174 174 :param check_locking: Allows to switch of the computation of the locking
175 175 data. This serves mainly the need of the simplevcs middleware to be
176 176 able to disable this for certain operations.
177 177
178 178 """
179 179 # Tri-state value: False: unlock, None: nothing, True: lock
180 180 make_lock = None
181 181 locked_by = [None, None, None]
182 182 is_anonymous = username == User.DEFAULT_USER
183 183 if not is_anonymous and check_locking:
184 184 log.debug('Checking locking on repository "%s"', repo_name)
185 185 user = User.get_by_username(username)
186 186 repo = Repository.get_by_repo_name(repo_name)
187 187 make_lock, __, locked_by = repo.get_locking_state(
188 188 action, user.user_id)
189 189
190 190 settings_model = VcsSettingsModel(repo=repo_name)
191 191 ui_settings = settings_model.get_ui_settings()
192 192
193 193 extras = {
194 194 'ip': get_ip_addr(environ),
195 195 'username': username,
196 196 'action': action,
197 197 'repository': repo_name,
198 198 'scm': scm,
199 199 'config': rhodecode.CONFIG['__file__'],
200 200 'make_lock': make_lock,
201 201 'locked_by': locked_by,
202 202 'server_url': utils2.get_server_url(environ),
203 203 'hooks': get_enabled_hook_classes(ui_settings),
204 204 'is_shadow_repo': is_shadow_repo,
205 205 }
206 206 return extras
207 207
208 208
209 209 class BasicAuth(AuthBasicAuthenticator):
210 210
211 211 def __init__(self, realm, authfunc, registry, auth_http_code=None,
212 212 initial_call_detection=False):
213 213 self.realm = realm
214 214 self.initial_call = initial_call_detection
215 215 self.authfunc = authfunc
216 216 self.registry = registry
217 217 self._rc_auth_http_code = auth_http_code
218 218
219 219 def _get_response_from_code(self, http_code):
220 220 try:
221 221 return get_exception(safe_int(http_code))
222 222 except Exception:
223 223 log.exception('Failed to fetch response for code %s' % http_code)
224 224 return HTTPForbidden
225 225
226 226 def build_authentication(self):
227 227 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
228 228 if self._rc_auth_http_code and not self.initial_call:
229 229 # return alternative HTTP code if alternative http return code
230 230 # is specified in RhodeCode config, but ONLY if it's not the
231 231 # FIRST call
232 232 custom_response_klass = self._get_response_from_code(
233 233 self._rc_auth_http_code)
234 234 return custom_response_klass(headers=head)
235 235 return HTTPUnauthorized(headers=head)
236 236
237 237 def authenticate(self, environ):
238 238 authorization = AUTHORIZATION(environ)
239 239 if not authorization:
240 240 return self.build_authentication()
241 241 (authmeth, auth) = authorization.split(' ', 1)
242 242 if 'basic' != authmeth.lower():
243 243 return self.build_authentication()
244 244 auth = auth.strip().decode('base64')
245 245 _parts = auth.split(':', 1)
246 246 if len(_parts) == 2:
247 247 username, password = _parts
248 248 if self.authfunc(
249 249 username, password, environ, VCS_TYPE,
250 250 registry=self.registry):
251 251 return username
252 252 if username and password:
253 253 # we mark that we actually executed authentication once, at
254 254 # that point we can use the alternative auth code
255 255 self.initial_call = False
256 256
257 257 return self.build_authentication()
258 258
259 259 __call__ = authenticate
260 260
261 261
262 262 def attach_context_attributes(context, request):
263 263 """
264 264 Attach variables into template context called `c`, please note that
265 265 request could be pylons or pyramid request in here.
266 266 """
267 267 rc_config = SettingsModel().get_all_settings(cache=True)
268 268
269 269 context.rhodecode_version = rhodecode.__version__
270 270 context.rhodecode_edition = config.get('rhodecode.edition')
271 271 # unique secret + version does not leak the version but keep consistency
272 272 context.rhodecode_version_hash = md5(
273 273 config.get('beaker.session.secret', '') +
274 274 rhodecode.__version__)[:8]
275 275
276 276 # Default language set for the incoming request
277 277 context.language = translation.get_lang()[0]
278 278
279 279 # Visual options
280 280 context.visual = AttributeDict({})
281 281
282 282 # DB stored Visual Items
283 283 context.visual.show_public_icon = str2bool(
284 284 rc_config.get('rhodecode_show_public_icon'))
285 285 context.visual.show_private_icon = str2bool(
286 286 rc_config.get('rhodecode_show_private_icon'))
287 287 context.visual.stylify_metatags = str2bool(
288 288 rc_config.get('rhodecode_stylify_metatags'))
289 289 context.visual.dashboard_items = safe_int(
290 290 rc_config.get('rhodecode_dashboard_items', 100))
291 291 context.visual.admin_grid_items = safe_int(
292 292 rc_config.get('rhodecode_admin_grid_items', 100))
293 293 context.visual.repository_fields = str2bool(
294 294 rc_config.get('rhodecode_repository_fields'))
295 295 context.visual.show_version = str2bool(
296 296 rc_config.get('rhodecode_show_version'))
297 297 context.visual.use_gravatar = str2bool(
298 298 rc_config.get('rhodecode_use_gravatar'))
299 299 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
300 300 context.visual.default_renderer = rc_config.get(
301 301 'rhodecode_markup_renderer', 'rst')
302 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
302 303 context.visual.rhodecode_support_url = \
303 304 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
304 305
305 306 context.pre_code = rc_config.get('rhodecode_pre_code')
306 307 context.post_code = rc_config.get('rhodecode_post_code')
307 308 context.rhodecode_name = rc_config.get('rhodecode_title')
308 309 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
309 310 # if we have specified default_encoding in the request, it has more
310 311 # priority
311 312 if request.GET.get('default_encoding'):
312 313 context.default_encodings.insert(0, request.GET.get('default_encoding'))
313 314 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
314 315
315 316 # INI stored
316 317 context.labs_active = str2bool(
317 318 config.get('labs_settings_active', 'false'))
318 319 context.visual.allow_repo_location_change = str2bool(
319 320 config.get('allow_repo_location_change', True))
320 321 context.visual.allow_custom_hooks_settings = str2bool(
321 322 config.get('allow_custom_hooks_settings', True))
322 323 context.debug_style = str2bool(config.get('debug_style', False))
323 324
324 325 context.rhodecode_instanceid = config.get('instance_id')
325 326
326 327 # AppEnlight
327 328 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
328 329 context.appenlight_api_public_key = config.get(
329 330 'appenlight.api_public_key', '')
330 331 context.appenlight_server_url = config.get('appenlight.server_url', '')
331 332
332 333 # JS template context
333 334 context.template_context = {
334 335 'repo_name': None,
335 336 'repo_type': None,
336 337 'repo_landing_commit': None,
337 338 'rhodecode_user': {
338 339 'username': None,
339 340 'email': None,
340 341 'notification_status': False
341 342 },
342 343 'visual': {
343 344 'default_renderer': None
344 345 },
345 346 'commit_data': {
346 347 'commit_id': None
347 348 },
348 349 'pull_request_data': {'pull_request_id': None},
349 350 'timeago': {
350 351 'refresh_time': 120 * 1000,
351 352 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
352 353 },
353 354 'pylons_dispatch': {
354 355 # 'controller': request.environ['pylons.routes_dict']['controller'],
355 356 # 'action': request.environ['pylons.routes_dict']['action'],
356 357 },
357 358 'pyramid_dispatch': {
358 359
359 360 },
360 361 'extra': {'plugins': {}}
361 362 }
362 363 # END CONFIG VARS
363 364
364 365 # TODO: This dosn't work when called from pylons compatibility tween.
365 366 # Fix this and remove it from base controller.
366 367 # context.repo_name = get_repo_slug(request) # can be empty
367 368
368 369 diffmode = 'sideside'
369 370 if request.GET.get('diffmode'):
370 371 if request.GET['diffmode'] == 'unified':
371 372 diffmode = 'unified'
372 373 elif request.session.get('diffmode'):
373 374 diffmode = request.session['diffmode']
374 375
375 376 context.diffmode = diffmode
376 377
377 378 if request.session.get('diffmode') != diffmode:
378 379 request.session['diffmode'] = diffmode
379 380
380 381 context.csrf_token = auth.get_csrf_token()
381 382 context.backends = rhodecode.BACKENDS.keys()
382 383 context.backends.sort()
383 384 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
384 385 context.rhodecode_user.user_id)
385 386
386 387 context.pyramid_request = pyramid.threadlocal.get_current_request()
387 388
388 389
389 390 def get_auth_user(environ):
390 391 ip_addr = get_ip_addr(environ)
391 392 # make sure that we update permissions each time we call controller
392 393 _auth_token = (request.GET.get('auth_token', '') or
393 394 request.GET.get('api_key', ''))
394 395
395 396 if _auth_token:
396 397 # when using API_KEY we assume user exists, and
397 398 # doesn't need auth based on cookies.
398 399 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
399 400 authenticated = False
400 401 else:
401 402 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
402 403 try:
403 404 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
404 405 ip_addr=ip_addr)
405 406 except UserCreationError as e:
406 407 h.flash(e, 'error')
407 408 # container auth or other auth functions that create users
408 409 # on the fly can throw this exception signaling that there's
409 410 # issue with user creation, explanation should be provided
410 411 # in Exception itself. We then create a simple blank
411 412 # AuthUser
412 413 auth_user = AuthUser(ip_addr=ip_addr)
413 414
414 415 if password_changed(auth_user, session):
415 416 session.invalidate()
416 417 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
417 418 auth_user = AuthUser(ip_addr=ip_addr)
418 419
419 420 authenticated = cookie_store.get('is_authenticated')
420 421
421 422 if not auth_user.is_authenticated and auth_user.is_user_object:
422 423 # user is not authenticated and not empty
423 424 auth_user.set_authenticated(authenticated)
424 425
425 426 return auth_user
426 427
427 428
428 429 class BaseController(WSGIController):
429 430
430 431 def __before__(self):
431 432 """
432 433 __before__ is called before controller methods and after __call__
433 434 """
434 435 # on each call propagate settings calls into global settings.
435 436 set_rhodecode_config(config)
436 437 attach_context_attributes(c, request)
437 438
438 439 # TODO: Remove this when fixed in attach_context_attributes()
439 440 c.repo_name = get_repo_slug(request) # can be empty
440 441
441 442 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
442 443 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
443 444 self.sa = meta.Session
444 445 self.scm_model = ScmModel(self.sa)
445 446
446 447 # set user language
447 448 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
448 449 if user_lang:
449 450 translation.set_lang(user_lang)
450 451 log.debug('set language to %s for user %s',
451 452 user_lang, self._rhodecode_user)
452 453
453 454 def _dispatch_redirect(self, with_url, environ, start_response):
454 455 resp = HTTPFound(with_url)
455 456 environ['SCRIPT_NAME'] = '' # handle prefix middleware
456 457 environ['PATH_INFO'] = with_url
457 458 return resp(environ, start_response)
458 459
459 460 def __call__(self, environ, start_response):
460 461 """Invoke the Controller"""
461 462 # WSGIController.__call__ dispatches to the Controller method
462 463 # the request is routed to. This routing information is
463 464 # available in environ['pylons.routes_dict']
464 465 from rhodecode.lib import helpers as h
465 466
466 467 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
467 468 if environ.get('debugtoolbar.wants_pylons_context', False):
468 469 environ['debugtoolbar.pylons_context'] = c._current_obj()
469 470
470 471 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
471 472 environ['pylons.routes_dict']['action']])
472 473
473 474 self.rc_config = SettingsModel().get_all_settings(cache=True)
474 475 self.ip_addr = get_ip_addr(environ)
475 476
476 477 # The rhodecode auth user is looked up and passed through the
477 478 # environ by the pylons compatibility tween in pyramid.
478 479 # So we can just grab it from there.
479 480 auth_user = environ['rc_auth_user']
480 481
481 482 # set globals for auth user
482 483 request.user = auth_user
483 484 c.rhodecode_user = self._rhodecode_user = auth_user
484 485
485 486 log.info('IP: %s User: %s accessed %s [%s]' % (
486 487 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
487 488 _route_name)
488 489 )
489 490
490 491 # TODO: Maybe this should be move to pyramid to cover all views.
491 492 # check user attributes for password change flag
492 493 user_obj = auth_user.get_instance()
493 494 if user_obj and user_obj.user_data.get('force_password_change'):
494 495 h.flash('You are required to change your password', 'warning',
495 496 ignore_duplicate=True)
496 497
497 498 skip_user_check_urls = [
498 499 'error.document', 'login.logout', 'login.index',
499 500 'admin/my_account.my_account_password',
500 501 'admin/my_account.my_account_password_update'
501 502 ]
502 503 if _route_name not in skip_user_check_urls:
503 504 return self._dispatch_redirect(
504 505 url('my_account_password'), environ, start_response)
505 506
506 507 return WSGIController.__call__(self, environ, start_response)
507 508
508 509
509 510 class BaseRepoController(BaseController):
510 511 """
511 512 Base class for controllers responsible for loading all needed data for
512 513 repository loaded items are
513 514
514 515 c.rhodecode_repo: instance of scm repository
515 516 c.rhodecode_db_repo: instance of db
516 517 c.repository_requirements_missing: shows that repository specific data
517 518 could not be displayed due to the missing requirements
518 519 c.repository_pull_requests: show number of open pull requests
519 520 """
520 521
521 522 def __before__(self):
522 523 super(BaseRepoController, self).__before__()
523 524 if c.repo_name: # extracted from routes
524 525 db_repo = Repository.get_by_repo_name(c.repo_name)
525 526 if not db_repo:
526 527 return
527 528
528 529 log.debug(
529 530 'Found repository in database %s with state `%s`',
530 531 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
531 532 route = getattr(request.environ.get('routes.route'), 'name', '')
532 533
533 534 # allow to delete repos that are somehow damages in filesystem
534 535 if route in ['delete_repo']:
535 536 return
536 537
537 538 if db_repo.repo_state in [Repository.STATE_PENDING]:
538 539 if route in ['repo_creating_home']:
539 540 return
540 541 check_url = url('repo_creating_home', repo_name=c.repo_name)
541 542 return redirect(check_url)
542 543
543 544 self.rhodecode_db_repo = db_repo
544 545
545 546 missing_requirements = False
546 547 try:
547 548 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
548 549 except RepositoryRequirementError as e:
549 550 missing_requirements = True
550 551 self._handle_missing_requirements(e)
551 552
552 553 if self.rhodecode_repo is None and not missing_requirements:
553 554 log.error('%s this repository is present in database but it '
554 555 'cannot be created as an scm instance', c.repo_name)
555 556
556 557 h.flash(_(
557 558 "The repository at %(repo_name)s cannot be located.") %
558 559 {'repo_name': c.repo_name},
559 560 category='error', ignore_duplicate=True)
560 561 redirect(url('home'))
561 562
562 563 # update last change according to VCS data
563 564 if not missing_requirements:
564 565 commit = db_repo.get_commit(
565 566 pre_load=["author", "date", "message", "parents"])
566 567 db_repo.update_commit_cache(commit)
567 568
568 569 # Prepare context
569 570 c.rhodecode_db_repo = db_repo
570 571 c.rhodecode_repo = self.rhodecode_repo
571 572 c.repository_requirements_missing = missing_requirements
572 573
573 574 self._update_global_counters(self.scm_model, db_repo)
574 575
575 576 def _update_global_counters(self, scm_model, db_repo):
576 577 """
577 578 Base variables that are exposed to every page of repository
578 579 """
579 580 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
580 581
581 582 def _handle_missing_requirements(self, error):
582 583 self.rhodecode_repo = None
583 584 log.error(
584 585 'Requirements are missing for repository %s: %s',
585 586 c.repo_name, error.message)
586 587
587 588 summary_url = url('summary_home', repo_name=c.repo_name)
588 589 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
589 590 settings_update_url = url('repo', repo_name=c.repo_name)
590 591 path = request.path
591 592 should_redirect = (
592 593 path not in (summary_url, settings_update_url)
593 594 and '/settings' not in path or path == statistics_url
594 595 )
595 596 if should_redirect:
596 597 redirect(summary_url)
@@ -1,530 +1,549 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 comments model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import collections
28 28
29 29 from datetime import datetime
30 30
31 31 from pylons.i18n.translation import _
32 32 from pyramid.threadlocal import get_current_registry
33 33 from sqlalchemy.sql.expression import null
34 34 from sqlalchemy.sql.functions import coalesce
35 35
36 36 from rhodecode.lib import helpers as h, diffs
37 37 from rhodecode.lib.channelstream import channelstream_request
38 38 from rhodecode.lib.utils import action_logger
39 39 from rhodecode.lib.utils2 import extract_mentioned_users
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.db import (
42 42 ChangesetComment, User, Notification, PullRequest)
43 43 from rhodecode.model.notification import NotificationModel
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.settings import VcsSettingsModel
46 46 from rhodecode.model.notification import EmailNotificationModel
47 from rhodecode.model.validation_schema.schemas import comment_schema
48
47 49
48 50 log = logging.getLogger(__name__)
49 51
50 52
51 53 class CommentsModel(BaseModel):
52 54
53 55 cls = ChangesetComment
54 56
55 57 DIFF_CONTEXT_BEFORE = 3
56 58 DIFF_CONTEXT_AFTER = 3
57 59
58 60 def __get_commit_comment(self, changeset_comment):
59 61 return self._get_instance(ChangesetComment, changeset_comment)
60 62
61 63 def __get_pull_request(self, pull_request):
62 64 return self._get_instance(PullRequest, pull_request)
63 65
64 66 def _extract_mentions(self, s):
65 67 user_objects = []
66 68 for username in extract_mentioned_users(s):
67 69 user_obj = User.get_by_username(username, case_insensitive=True)
68 70 if user_obj:
69 71 user_objects.append(user_obj)
70 72 return user_objects
71 73
72 74 def _get_renderer(self, global_renderer='rst'):
73 75 try:
74 76 # try reading from visual context
75 77 from pylons import tmpl_context
76 78 global_renderer = tmpl_context.visual.default_renderer
77 79 except AttributeError:
78 80 log.debug("Renderer not set, falling back "
79 81 "to default renderer '%s'", global_renderer)
80 82 except Exception:
81 83 log.error(traceback.format_exc())
82 84 return global_renderer
83 85
84 86 def create(self, text, repo, user, commit_id=None, pull_request=None,
85 87 f_path=None, line_no=None, status_change=None, comment_type=None,
86 88 status_change_type=None, closing_pr=False,
87 89 send_email=True, renderer=None):
88 90 """
89 91 Creates new comment for commit or pull request.
90 92 IF status_change is not none this comment is associated with a
91 93 status change of commit or commit associated with pull request
92 94
93 95 :param text:
94 96 :param repo:
95 97 :param user:
96 98 :param commit_id:
97 99 :param pull_request:
98 100 :param f_path:
99 101 :param line_no:
100 102 :param status_change: Label for status change
101 103 :param comment_type: Type of comment
102 104 :param status_change_type: type of status change
103 105 :param closing_pr:
104 106 :param send_email:
105 107 :param renderer: pick renderer for this comment
106 108 """
107 109 if not text:
108 110 log.warning('Missing text for comment, skipping...')
109 111 return
110 112
111 113 if not renderer:
112 114 renderer = self._get_renderer()
113 115
114 repo = self._get_repo(repo)
115 user = self._get_user(user)
116
117 schema = comment_schema.CommentSchema()
118 validated_kwargs = schema.deserialize(dict(
119 comment_body=text,
120 comment_type=comment_type,
121 comment_file=f_path,
122 comment_line=line_no,
123 renderer_type=renderer,
124 status_change=status_change,
125
126 repo=repo,
127 user=user,
128 ))
129
130 repo = self._get_repo(validated_kwargs['repo'])
131 user = self._get_user(validated_kwargs['user'])
132
116 133 comment = ChangesetComment()
117 comment.renderer = renderer
134 comment.renderer = validated_kwargs['renderer_type']
135 comment.text = validated_kwargs['comment_body']
136 comment.f_path = validated_kwargs['comment_file']
137 comment.line_no = validated_kwargs['comment_line']
138 comment.comment_type = validated_kwargs['comment_type']
139
118 140 comment.repo = repo
119 141 comment.author = user
120 comment.text = text
121 comment.f_path = f_path
122 comment.line_no = line_no
123 142
124 143 pull_request_id = pull_request
125 144
126 145 commit_obj = None
127 146 pull_request_obj = None
128 147
129 148 if commit_id:
130 149 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
131 150 # do a lookup, so we don't pass something bad here
132 151 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
133 152 comment.revision = commit_obj.raw_id
134 153
135 154 elif pull_request_id:
136 155 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
137 156 pull_request_obj = self.__get_pull_request(pull_request_id)
138 157 comment.pull_request = pull_request_obj
139 158 else:
140 159 raise Exception('Please specify commit or pull_request_id')
141 160
142 161 Session().add(comment)
143 162 Session().flush()
144 163 kwargs = {
145 164 'user': user,
146 165 'renderer_type': renderer,
147 166 'repo_name': repo.repo_name,
148 167 'status_change': status_change,
149 168 'status_change_type': status_change_type,
150 169 'comment_body': text,
151 170 'comment_file': f_path,
152 171 'comment_line': line_no,
153 172 }
154 173
155 174 if commit_obj:
156 175 recipients = ChangesetComment.get_users(
157 176 revision=commit_obj.raw_id)
158 177 # add commit author if it's in RhodeCode system
159 178 cs_author = User.get_from_cs_author(commit_obj.author)
160 179 if not cs_author:
161 180 # use repo owner if we cannot extract the author correctly
162 181 cs_author = repo.user
163 182 recipients += [cs_author]
164 183
165 184 commit_comment_url = self.get_url(comment)
166 185
167 186 target_repo_url = h.link_to(
168 187 repo.repo_name,
169 188 h.url('summary_home',
170 189 repo_name=repo.repo_name, qualified=True))
171 190
172 191 # commit specifics
173 192 kwargs.update({
174 193 'commit': commit_obj,
175 194 'commit_message': commit_obj.message,
176 195 'commit_target_repo': target_repo_url,
177 196 'commit_comment_url': commit_comment_url,
178 197 })
179 198
180 199 elif pull_request_obj:
181 200 # get the current participants of this pull request
182 201 recipients = ChangesetComment.get_users(
183 202 pull_request_id=pull_request_obj.pull_request_id)
184 203 # add pull request author
185 204 recipients += [pull_request_obj.author]
186 205
187 206 # add the reviewers to notification
188 207 recipients += [x.user for x in pull_request_obj.reviewers]
189 208
190 209 pr_target_repo = pull_request_obj.target_repo
191 210 pr_source_repo = pull_request_obj.source_repo
192 211
193 212 pr_comment_url = h.url(
194 213 'pullrequest_show',
195 214 repo_name=pr_target_repo.repo_name,
196 215 pull_request_id=pull_request_obj.pull_request_id,
197 216 anchor='comment-%s' % comment.comment_id,
198 217 qualified=True,)
199 218
200 219 # set some variables for email notification
201 220 pr_target_repo_url = h.url(
202 221 'summary_home', repo_name=pr_target_repo.repo_name,
203 222 qualified=True)
204 223
205 224 pr_source_repo_url = h.url(
206 225 'summary_home', repo_name=pr_source_repo.repo_name,
207 226 qualified=True)
208 227
209 228 # pull request specifics
210 229 kwargs.update({
211 230 'pull_request': pull_request_obj,
212 231 'pr_id': pull_request_obj.pull_request_id,
213 232 'pr_target_repo': pr_target_repo,
214 233 'pr_target_repo_url': pr_target_repo_url,
215 234 'pr_source_repo': pr_source_repo,
216 235 'pr_source_repo_url': pr_source_repo_url,
217 236 'pr_comment_url': pr_comment_url,
218 237 'pr_closing': closing_pr,
219 238 })
220 239 if send_email:
221 240 # pre-generate the subject for notification itself
222 241 (subject,
223 242 _h, _e, # we don't care about those
224 243 body_plaintext) = EmailNotificationModel().render_email(
225 244 notification_type, **kwargs)
226 245
227 246 mention_recipients = set(
228 247 self._extract_mentions(text)).difference(recipients)
229 248
230 249 # create notification objects, and emails
231 250 NotificationModel().create(
232 251 created_by=user,
233 252 notification_subject=subject,
234 253 notification_body=body_plaintext,
235 254 notification_type=notification_type,
236 255 recipients=recipients,
237 256 mention_recipients=mention_recipients,
238 257 email_kwargs=kwargs,
239 258 )
240 259
241 260 action = (
242 261 'user_commented_pull_request:{}'.format(
243 262 comment.pull_request.pull_request_id)
244 263 if comment.pull_request
245 264 else 'user_commented_revision:{}'.format(comment.revision)
246 265 )
247 266 action_logger(user, action, comment.repo)
248 267
249 268 registry = get_current_registry()
250 269 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
251 270 channelstream_config = rhodecode_plugins.get('channelstream', {})
252 271 msg_url = ''
253 272 if commit_obj:
254 273 msg_url = commit_comment_url
255 274 repo_name = repo.repo_name
256 275 elif pull_request_obj:
257 276 msg_url = pr_comment_url
258 277 repo_name = pr_target_repo.repo_name
259 278
260 279 if channelstream_config.get('enabled'):
261 280 message = '<strong>{}</strong> {} - ' \
262 281 '<a onclick="window.location=\'{}\';' \
263 282 'window.location.reload()">' \
264 283 '<strong>{}</strong></a>'
265 284 message = message.format(
266 285 user.username, _('made a comment'), msg_url,
267 286 _('Show it now'))
268 287 channel = '/repo${}$/pr/{}'.format(
269 288 repo_name,
270 289 pull_request_id
271 290 )
272 291 payload = {
273 292 'type': 'message',
274 293 'timestamp': datetime.utcnow(),
275 294 'user': 'system',
276 295 'exclude_users': [user.username],
277 296 'channel': channel,
278 297 'message': {
279 298 'message': message,
280 299 'level': 'info',
281 300 'topic': '/notifications'
282 301 }
283 302 }
284 303 channelstream_request(channelstream_config, [payload],
285 304 '/message', raise_exc=False)
286 305
287 306 return comment
288 307
289 308 def delete(self, comment):
290 309 """
291 310 Deletes given comment
292 311
293 312 :param comment_id:
294 313 """
295 314 comment = self.__get_commit_comment(comment)
296 315 Session().delete(comment)
297 316
298 317 return comment
299 318
300 319 def get_all_comments(self, repo_id, revision=None, pull_request=None):
301 320 q = ChangesetComment.query()\
302 321 .filter(ChangesetComment.repo_id == repo_id)
303 322 if revision:
304 323 q = q.filter(ChangesetComment.revision == revision)
305 324 elif pull_request:
306 325 pull_request = self.__get_pull_request(pull_request)
307 326 q = q.filter(ChangesetComment.pull_request == pull_request)
308 327 else:
309 328 raise Exception('Please specify commit or pull_request')
310 329 q = q.order_by(ChangesetComment.created_on)
311 330 return q.all()
312 331
313 332 def get_url(self, comment):
314 333 comment = self.__get_commit_comment(comment)
315 334 if comment.pull_request:
316 335 return h.url(
317 336 'pullrequest_show',
318 337 repo_name=comment.pull_request.target_repo.repo_name,
319 338 pull_request_id=comment.pull_request.pull_request_id,
320 339 anchor='comment-%s' % comment.comment_id,
321 340 qualified=True,)
322 341 else:
323 342 return h.url(
324 343 'changeset_home',
325 344 repo_name=comment.repo.repo_name,
326 345 revision=comment.revision,
327 346 anchor='comment-%s' % comment.comment_id,
328 347 qualified=True,)
329 348
330 349 def get_comments(self, repo_id, revision=None, pull_request=None):
331 350 """
332 351 Gets main comments based on revision or pull_request_id
333 352
334 353 :param repo_id:
335 354 :param revision:
336 355 :param pull_request:
337 356 """
338 357
339 358 q = ChangesetComment.query()\
340 359 .filter(ChangesetComment.repo_id == repo_id)\
341 360 .filter(ChangesetComment.line_no == None)\
342 361 .filter(ChangesetComment.f_path == None)
343 362 if revision:
344 363 q = q.filter(ChangesetComment.revision == revision)
345 364 elif pull_request:
346 365 pull_request = self.__get_pull_request(pull_request)
347 366 q = q.filter(ChangesetComment.pull_request == pull_request)
348 367 else:
349 368 raise Exception('Please specify commit or pull_request')
350 369 q = q.order_by(ChangesetComment.created_on)
351 370 return q.all()
352 371
353 372 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
354 373 q = self._get_inline_comments_query(repo_id, revision, pull_request)
355 374 return self._group_comments_by_path_and_line_number(q)
356 375
357 376 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
358 377 version=None, include_aggregates=False):
359 378 version_aggregates = collections.defaultdict(list)
360 379 inline_cnt = 0
361 380 for fname, per_line_comments in inline_comments.iteritems():
362 381 for lno, comments in per_line_comments.iteritems():
363 382 for comm in comments:
364 383 version_aggregates[comm.pull_request_version_id].append(comm)
365 384 if not comm.outdated_at_version(version) and skip_outdated:
366 385 inline_cnt += 1
367 386
368 387 if include_aggregates:
369 388 return inline_cnt, version_aggregates
370 389 return inline_cnt
371 390
372 391 def get_outdated_comments(self, repo_id, pull_request):
373 392 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
374 393 # of a pull request.
375 394 q = self._all_inline_comments_of_pull_request(pull_request)
376 395 q = q.filter(
377 396 ChangesetComment.display_state ==
378 397 ChangesetComment.COMMENT_OUTDATED
379 398 ).order_by(ChangesetComment.comment_id.asc())
380 399
381 400 return self._group_comments_by_path_and_line_number(q)
382 401
383 402 def _get_inline_comments_query(self, repo_id, revision, pull_request):
384 403 # TODO: johbo: Split this into two methods: One for PR and one for
385 404 # commit.
386 405 if revision:
387 406 q = Session().query(ChangesetComment).filter(
388 407 ChangesetComment.repo_id == repo_id,
389 408 ChangesetComment.line_no != null(),
390 409 ChangesetComment.f_path != null(),
391 410 ChangesetComment.revision == revision)
392 411
393 412 elif pull_request:
394 413 pull_request = self.__get_pull_request(pull_request)
395 414 if not CommentsModel.use_outdated_comments(pull_request):
396 415 q = self._visible_inline_comments_of_pull_request(pull_request)
397 416 else:
398 417 q = self._all_inline_comments_of_pull_request(pull_request)
399 418
400 419 else:
401 420 raise Exception('Please specify commit or pull_request_id')
402 421 q = q.order_by(ChangesetComment.comment_id.asc())
403 422 return q
404 423
405 424 def _group_comments_by_path_and_line_number(self, q):
406 425 comments = q.all()
407 426 paths = collections.defaultdict(lambda: collections.defaultdict(list))
408 427 for co in comments:
409 428 paths[co.f_path][co.line_no].append(co)
410 429 return paths
411 430
412 431 @classmethod
413 432 def needed_extra_diff_context(cls):
414 433 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
415 434
416 435 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
417 436 if not CommentsModel.use_outdated_comments(pull_request):
418 437 return
419 438
420 439 comments = self._visible_inline_comments_of_pull_request(pull_request)
421 440 comments_to_outdate = comments.all()
422 441
423 442 for comment in comments_to_outdate:
424 443 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
425 444
426 445 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
427 446 diff_line = _parse_comment_line_number(comment.line_no)
428 447
429 448 try:
430 449 old_context = old_diff_proc.get_context_of_line(
431 450 path=comment.f_path, diff_line=diff_line)
432 451 new_context = new_diff_proc.get_context_of_line(
433 452 path=comment.f_path, diff_line=diff_line)
434 453 except (diffs.LineNotInDiffException,
435 454 diffs.FileNotInDiffException):
436 455 comment.display_state = ChangesetComment.COMMENT_OUTDATED
437 456 return
438 457
439 458 if old_context == new_context:
440 459 return
441 460
442 461 if self._should_relocate_diff_line(diff_line):
443 462 new_diff_lines = new_diff_proc.find_context(
444 463 path=comment.f_path, context=old_context,
445 464 offset=self.DIFF_CONTEXT_BEFORE)
446 465 if not new_diff_lines:
447 466 comment.display_state = ChangesetComment.COMMENT_OUTDATED
448 467 else:
449 468 new_diff_line = self._choose_closest_diff_line(
450 469 diff_line, new_diff_lines)
451 470 comment.line_no = _diff_to_comment_line_number(new_diff_line)
452 471 else:
453 472 comment.display_state = ChangesetComment.COMMENT_OUTDATED
454 473
455 474 def _should_relocate_diff_line(self, diff_line):
456 475 """
457 476 Checks if relocation shall be tried for the given `diff_line`.
458 477
459 478 If a comment points into the first lines, then we can have a situation
460 479 that after an update another line has been added on top. In this case
461 480 we would find the context still and move the comment around. This
462 481 would be wrong.
463 482 """
464 483 should_relocate = (
465 484 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
466 485 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
467 486 return should_relocate
468 487
469 488 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
470 489 candidate = new_diff_lines[0]
471 490 best_delta = _diff_line_delta(diff_line, candidate)
472 491 for new_diff_line in new_diff_lines[1:]:
473 492 delta = _diff_line_delta(diff_line, new_diff_line)
474 493 if delta < best_delta:
475 494 candidate = new_diff_line
476 495 best_delta = delta
477 496 return candidate
478 497
479 498 def _visible_inline_comments_of_pull_request(self, pull_request):
480 499 comments = self._all_inline_comments_of_pull_request(pull_request)
481 500 comments = comments.filter(
482 501 coalesce(ChangesetComment.display_state, '') !=
483 502 ChangesetComment.COMMENT_OUTDATED)
484 503 return comments
485 504
486 505 def _all_inline_comments_of_pull_request(self, pull_request):
487 506 comments = Session().query(ChangesetComment)\
488 507 .filter(ChangesetComment.line_no != None)\
489 508 .filter(ChangesetComment.f_path != None)\
490 509 .filter(ChangesetComment.pull_request == pull_request)
491 510 return comments
492 511
493 512 @staticmethod
494 513 def use_outdated_comments(pull_request):
495 514 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
496 515 settings = settings_model.get_general_settings()
497 516 return settings.get('rhodecode_use_outdated_comments', False)
498 517
499 518
500 519 def _parse_comment_line_number(line_no):
501 520 """
502 521 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
503 522 """
504 523 old_line = None
505 524 new_line = None
506 525 if line_no.startswith('o'):
507 526 old_line = int(line_no[1:])
508 527 elif line_no.startswith('n'):
509 528 new_line = int(line_no[1:])
510 529 else:
511 530 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
512 531 return diffs.DiffLineNumber(old_line, new_line)
513 532
514 533
515 534 def _diff_to_comment_line_number(diff_line):
516 535 if diff_line.new is not None:
517 536 return u'n{}'.format(diff_line.new)
518 537 elif diff_line.old is not None:
519 538 return u'o{}'.format(diff_line.old)
520 539 return u''
521 540
522 541
523 542 def _diff_line_delta(a, b):
524 543 if None not in (a.new, b.new):
525 544 return abs(a.new - b.new)
526 545 elif None not in (a.old, b.old):
527 546 return abs(a.old - b.old)
528 547 else:
529 548 raise ValueError(
530 549 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,3823 +1,3829 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from beaker.cache import cache_region
45 45 from webob.exc import HTTPNotFound
46 46 from zope.cachedescriptors.property import Lazy as LazyProperty
47 47
48 48 from pylons import url
49 49 from pylons.i18n.translation import lazy_ugettext as _
50 50
51 51 from rhodecode.lib.vcs import get_vcs_instance
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 53 from rhodecode.lib.utils2 import (
54 54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 56 glob2re, StrictAttributeDict)
57 57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 58 from rhodecode.lib.ext_json import json
59 59 from rhodecode.lib.caching_query import FromCache
60 60 from rhodecode.lib.encrypt import AESCipher
61 61
62 62 from rhodecode.model.meta import Base, Session
63 63
64 64 URL_SEP = '/'
65 65 log = logging.getLogger(__name__)
66 66
67 67 # =============================================================================
68 68 # BASE CLASSES
69 69 # =============================================================================
70 70
71 71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 72 # beaker.session.secret if first is not set.
73 73 # and initialized at environment.py
74 74 ENCRYPTION_KEY = None
75 75
76 76 # used to sort permissions by types, '#' used here is not allowed to be in
77 77 # usernames, and it's very early in sorted string.printable table.
78 78 PERMISSION_TYPE_SORT = {
79 79 'admin': '####',
80 80 'write': '###',
81 81 'read': '##',
82 82 'none': '#',
83 83 }
84 84
85 85
86 86 def display_sort(obj):
87 87 """
88 88 Sort function used to sort permissions in .permissions() function of
89 89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 90 of all other resources
91 91 """
92 92
93 93 if obj.username == User.DEFAULT_USER:
94 94 return '#####'
95 95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 96 return prefix + obj.username
97 97
98 98
99 99 def _hash_key(k):
100 100 return md5_safe(k)
101 101
102 102
103 103 class EncryptedTextValue(TypeDecorator):
104 104 """
105 105 Special column for encrypted long text data, use like::
106 106
107 107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108 108
109 109 This column is intelligent so if value is in unencrypted form it return
110 110 unencrypted form, but on save it always encrypts
111 111 """
112 112 impl = Text
113 113
114 114 def process_bind_param(self, value, dialect):
115 115 if not value:
116 116 return value
117 117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 118 # protect against double encrypting if someone manually starts
119 119 # doing
120 120 raise ValueError('value needs to be in unencrypted format, ie. '
121 121 'not starting with enc$aes')
122 122 return 'enc$aes_hmac$%s' % AESCipher(
123 123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124 124
125 125 def process_result_value(self, value, dialect):
126 126 import rhodecode
127 127
128 128 if not value:
129 129 return value
130 130
131 131 parts = value.split('$', 3)
132 132 if not len(parts) == 3:
133 133 # probably not encrypted values
134 134 return value
135 135 else:
136 136 if parts[0] != 'enc':
137 137 # parts ok but without our header ?
138 138 return value
139 139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 140 'rhodecode.encrypted_values.strict') or True)
141 141 # at that stage we know it's our encryption
142 142 if parts[1] == 'aes':
143 143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 144 elif parts[1] == 'aes_hmac':
145 145 decrypted_data = AESCipher(
146 146 ENCRYPTION_KEY, hmac=True,
147 147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 148 else:
149 149 raise ValueError(
150 150 'Encryption type part is wrong, must be `aes` '
151 151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 152 return decrypted_data
153 153
154 154
155 155 class BaseModel(object):
156 156 """
157 157 Base Model for all classes
158 158 """
159 159
160 160 @classmethod
161 161 def _get_keys(cls):
162 162 """return column names for this model """
163 163 return class_mapper(cls).c.keys()
164 164
165 165 def get_dict(self):
166 166 """
167 167 return dict with keys and values corresponding
168 168 to this model data """
169 169
170 170 d = {}
171 171 for k in self._get_keys():
172 172 d[k] = getattr(self, k)
173 173
174 174 # also use __json__() if present to get additional fields
175 175 _json_attr = getattr(self, '__json__', None)
176 176 if _json_attr:
177 177 # update with attributes from __json__
178 178 if callable(_json_attr):
179 179 _json_attr = _json_attr()
180 180 for k, val in _json_attr.iteritems():
181 181 d[k] = val
182 182 return d
183 183
184 184 def get_appstruct(self):
185 185 """return list with keys and values tuples corresponding
186 186 to this model data """
187 187
188 188 l = []
189 189 for k in self._get_keys():
190 190 l.append((k, getattr(self, k),))
191 191 return l
192 192
193 193 def populate_obj(self, populate_dict):
194 194 """populate model with data from given populate_dict"""
195 195
196 196 for k in self._get_keys():
197 197 if k in populate_dict:
198 198 setattr(self, k, populate_dict[k])
199 199
200 200 @classmethod
201 201 def query(cls):
202 202 return Session().query(cls)
203 203
204 204 @classmethod
205 205 def get(cls, id_):
206 206 if id_:
207 207 return cls.query().get(id_)
208 208
209 209 @classmethod
210 210 def get_or_404(cls, id_):
211 211 try:
212 212 id_ = int(id_)
213 213 except (TypeError, ValueError):
214 214 raise HTTPNotFound
215 215
216 216 res = cls.query().get(id_)
217 217 if not res:
218 218 raise HTTPNotFound
219 219 return res
220 220
221 221 @classmethod
222 222 def getAll(cls):
223 223 # deprecated and left for backward compatibility
224 224 return cls.get_all()
225 225
226 226 @classmethod
227 227 def get_all(cls):
228 228 return cls.query().all()
229 229
230 230 @classmethod
231 231 def delete(cls, id_):
232 232 obj = cls.query().get(id_)
233 233 Session().delete(obj)
234 234
235 235 @classmethod
236 236 def identity_cache(cls, session, attr_name, value):
237 237 exist_in_session = []
238 238 for (item_cls, pkey), instance in session.identity_map.items():
239 239 if cls == item_cls and getattr(instance, attr_name) == value:
240 240 exist_in_session.append(instance)
241 241 if exist_in_session:
242 242 if len(exist_in_session) == 1:
243 243 return exist_in_session[0]
244 244 log.exception(
245 245 'multiple objects with attr %s and '
246 246 'value %s found with same name: %r',
247 247 attr_name, value, exist_in_session)
248 248
249 249 def __repr__(self):
250 250 if hasattr(self, '__unicode__'):
251 251 # python repr needs to return str
252 252 try:
253 253 return safe_str(self.__unicode__())
254 254 except UnicodeDecodeError:
255 255 pass
256 256 return '<DB:%s>' % (self.__class__.__name__)
257 257
258 258
259 259 class RhodeCodeSetting(Base, BaseModel):
260 260 __tablename__ = 'rhodecode_settings'
261 261 __table_args__ = (
262 262 UniqueConstraint('app_settings_name'),
263 263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 265 )
266 266
267 267 SETTINGS_TYPES = {
268 268 'str': safe_str,
269 269 'int': safe_int,
270 270 'unicode': safe_unicode,
271 271 'bool': str2bool,
272 272 'list': functools.partial(aslist, sep=',')
273 273 }
274 274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 275 GLOBAL_CONF_KEY = 'app_settings'
276 276
277 277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281 281
282 282 def __init__(self, key='', val='', type='unicode'):
283 283 self.app_settings_name = key
284 284 self.app_settings_type = type
285 285 self.app_settings_value = val
286 286
287 287 @validates('_app_settings_value')
288 288 def validate_settings_value(self, key, val):
289 289 assert type(val) == unicode
290 290 return val
291 291
292 292 @hybrid_property
293 293 def app_settings_value(self):
294 294 v = self._app_settings_value
295 295 _type = self.app_settings_type
296 296 if _type:
297 297 _type = self.app_settings_type.split('.')[0]
298 298 # decode the encrypted value
299 299 if 'encrypted' in self.app_settings_type:
300 300 cipher = EncryptedTextValue()
301 301 v = safe_unicode(cipher.process_result_value(v, None))
302 302
303 303 converter = self.SETTINGS_TYPES.get(_type) or \
304 304 self.SETTINGS_TYPES['unicode']
305 305 return converter(v)
306 306
307 307 @app_settings_value.setter
308 308 def app_settings_value(self, val):
309 309 """
310 310 Setter that will always make sure we use unicode in app_settings_value
311 311
312 312 :param val:
313 313 """
314 314 val = safe_unicode(val)
315 315 # encode the encrypted value
316 316 if 'encrypted' in self.app_settings_type:
317 317 cipher = EncryptedTextValue()
318 318 val = safe_unicode(cipher.process_bind_param(val, None))
319 319 self._app_settings_value = val
320 320
321 321 @hybrid_property
322 322 def app_settings_type(self):
323 323 return self._app_settings_type
324 324
325 325 @app_settings_type.setter
326 326 def app_settings_type(self, val):
327 327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 328 raise Exception('type must be one of %s got %s'
329 329 % (self.SETTINGS_TYPES.keys(), val))
330 330 self._app_settings_type = val
331 331
332 332 def __unicode__(self):
333 333 return u"<%s('%s:%s[%s]')>" % (
334 334 self.__class__.__name__,
335 335 self.app_settings_name, self.app_settings_value,
336 336 self.app_settings_type
337 337 )
338 338
339 339
340 340 class RhodeCodeUi(Base, BaseModel):
341 341 __tablename__ = 'rhodecode_ui'
342 342 __table_args__ = (
343 343 UniqueConstraint('ui_key'),
344 344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 346 )
347 347
348 348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 349 # HG
350 350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 351 HOOK_PULL = 'outgoing.pull_logger'
352 352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 353 HOOK_PUSH = 'changegroup.push_logger'
354 354
355 355 # TODO: johbo: Unify way how hooks are configured for git and hg,
356 356 # git part is currently hardcoded.
357 357
358 358 # SVN PATTERNS
359 359 SVN_BRANCH_ID = 'vcs_svn_branch'
360 360 SVN_TAG_ID = 'vcs_svn_tag'
361 361
362 362 ui_id = Column(
363 363 "ui_id", Integer(), nullable=False, unique=True, default=None,
364 364 primary_key=True)
365 365 ui_section = Column(
366 366 "ui_section", String(255), nullable=True, unique=None, default=None)
367 367 ui_key = Column(
368 368 "ui_key", String(255), nullable=True, unique=None, default=None)
369 369 ui_value = Column(
370 370 "ui_value", String(255), nullable=True, unique=None, default=None)
371 371 ui_active = Column(
372 372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
373 373
374 374 def __repr__(self):
375 375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
376 376 self.ui_key, self.ui_value)
377 377
378 378
379 379 class RepoRhodeCodeSetting(Base, BaseModel):
380 380 __tablename__ = 'repo_rhodecode_settings'
381 381 __table_args__ = (
382 382 UniqueConstraint(
383 383 'app_settings_name', 'repository_id',
384 384 name='uq_repo_rhodecode_setting_name_repo_id'),
385 385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
386 386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
387 387 )
388 388
389 389 repository_id = Column(
390 390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
391 391 nullable=False)
392 392 app_settings_id = Column(
393 393 "app_settings_id", Integer(), nullable=False, unique=True,
394 394 default=None, primary_key=True)
395 395 app_settings_name = Column(
396 396 "app_settings_name", String(255), nullable=True, unique=None,
397 397 default=None)
398 398 _app_settings_value = Column(
399 399 "app_settings_value", String(4096), nullable=True, unique=None,
400 400 default=None)
401 401 _app_settings_type = Column(
402 402 "app_settings_type", String(255), nullable=True, unique=None,
403 403 default=None)
404 404
405 405 repository = relationship('Repository')
406 406
407 407 def __init__(self, repository_id, key='', val='', type='unicode'):
408 408 self.repository_id = repository_id
409 409 self.app_settings_name = key
410 410 self.app_settings_type = type
411 411 self.app_settings_value = val
412 412
413 413 @validates('_app_settings_value')
414 414 def validate_settings_value(self, key, val):
415 415 assert type(val) == unicode
416 416 return val
417 417
418 418 @hybrid_property
419 419 def app_settings_value(self):
420 420 v = self._app_settings_value
421 421 type_ = self.app_settings_type
422 422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
423 423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
424 424 return converter(v)
425 425
426 426 @app_settings_value.setter
427 427 def app_settings_value(self, val):
428 428 """
429 429 Setter that will always make sure we use unicode in app_settings_value
430 430
431 431 :param val:
432 432 """
433 433 self._app_settings_value = safe_unicode(val)
434 434
435 435 @hybrid_property
436 436 def app_settings_type(self):
437 437 return self._app_settings_type
438 438
439 439 @app_settings_type.setter
440 440 def app_settings_type(self, val):
441 441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
442 442 if val not in SETTINGS_TYPES:
443 443 raise Exception('type must be one of %s got %s'
444 444 % (SETTINGS_TYPES.keys(), val))
445 445 self._app_settings_type = val
446 446
447 447 def __unicode__(self):
448 448 return u"<%s('%s:%s:%s[%s]')>" % (
449 449 self.__class__.__name__, self.repository.repo_name,
450 450 self.app_settings_name, self.app_settings_value,
451 451 self.app_settings_type
452 452 )
453 453
454 454
455 455 class RepoRhodeCodeUi(Base, BaseModel):
456 456 __tablename__ = 'repo_rhodecode_ui'
457 457 __table_args__ = (
458 458 UniqueConstraint(
459 459 'repository_id', 'ui_section', 'ui_key',
460 460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
461 461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
462 462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
463 463 )
464 464
465 465 repository_id = Column(
466 466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 467 nullable=False)
468 468 ui_id = Column(
469 469 "ui_id", Integer(), nullable=False, unique=True, default=None,
470 470 primary_key=True)
471 471 ui_section = Column(
472 472 "ui_section", String(255), nullable=True, unique=None, default=None)
473 473 ui_key = Column(
474 474 "ui_key", String(255), nullable=True, unique=None, default=None)
475 475 ui_value = Column(
476 476 "ui_value", String(255), nullable=True, unique=None, default=None)
477 477 ui_active = Column(
478 478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
479 479
480 480 repository = relationship('Repository')
481 481
482 482 def __repr__(self):
483 483 return '<%s[%s:%s]%s=>%s]>' % (
484 484 self.__class__.__name__, self.repository.repo_name,
485 485 self.ui_section, self.ui_key, self.ui_value)
486 486
487 487
488 488 class User(Base, BaseModel):
489 489 __tablename__ = 'users'
490 490 __table_args__ = (
491 491 UniqueConstraint('username'), UniqueConstraint('email'),
492 492 Index('u_username_idx', 'username'),
493 493 Index('u_email_idx', 'email'),
494 494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
495 495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
496 496 )
497 497 DEFAULT_USER = 'default'
498 498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
499 499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
500 500
501 501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
502 502 username = Column("username", String(255), nullable=True, unique=None, default=None)
503 503 password = Column("password", String(255), nullable=True, unique=None, default=None)
504 504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
505 505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
506 506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
507 507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
508 508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
509 509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
510 510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
511 511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
512 512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
513 513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
514 514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
515 515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
516 516
517 517 user_log = relationship('UserLog')
518 518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
519 519
520 520 repositories = relationship('Repository')
521 521 repository_groups = relationship('RepoGroup')
522 522 user_groups = relationship('UserGroup')
523 523
524 524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
525 525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
526 526
527 527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
528 528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
529 529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
530 530
531 531 group_member = relationship('UserGroupMember', cascade='all')
532 532
533 533 notifications = relationship('UserNotification', cascade='all')
534 534 # notifications assigned to this user
535 535 user_created_notifications = relationship('Notification', cascade='all')
536 536 # comments created by this user
537 537 user_comments = relationship('ChangesetComment', cascade='all')
538 538 # user profile extra info
539 539 user_emails = relationship('UserEmailMap', cascade='all')
540 540 user_ip_map = relationship('UserIpMap', cascade='all')
541 541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
542 542 # gists
543 543 user_gists = relationship('Gist', cascade='all')
544 544 # user pull requests
545 545 user_pull_requests = relationship('PullRequest', cascade='all')
546 546 # external identities
547 547 extenal_identities = relationship(
548 548 'ExternalIdentity',
549 549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
550 550 cascade='all')
551 551
552 552 def __unicode__(self):
553 553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
554 554 self.user_id, self.username)
555 555
556 556 @hybrid_property
557 557 def email(self):
558 558 return self._email
559 559
560 560 @email.setter
561 561 def email(self, val):
562 562 self._email = val.lower() if val else None
563 563
564 564 @property
565 565 def firstname(self):
566 566 # alias for future
567 567 return self.name
568 568
569 569 @property
570 570 def emails(self):
571 571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
572 572 return [self.email] + [x.email for x in other]
573 573
574 574 @property
575 575 def auth_tokens(self):
576 576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
577 577
578 578 @property
579 579 def extra_auth_tokens(self):
580 580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
581 581
582 582 @property
583 583 def feed_token(self):
584 584 feed_tokens = UserApiKeys.query()\
585 585 .filter(UserApiKeys.user == self)\
586 586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
587 587 .all()
588 588 if feed_tokens:
589 589 return feed_tokens[0].api_key
590 590 else:
591 591 # use the main token so we don't end up with nothing...
592 592 return self.api_key
593 593
594 594 @classmethod
595 595 def extra_valid_auth_tokens(cls, user, role=None):
596 596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
597 597 .filter(or_(UserApiKeys.expires == -1,
598 598 UserApiKeys.expires >= time.time()))
599 599 if role:
600 600 tokens = tokens.filter(or_(UserApiKeys.role == role,
601 601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
602 602 return tokens.all()
603 603
604 604 @property
605 605 def builtin_token_roles(self):
606 606 return map(UserApiKeys._get_role_name, [
607 607 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
608 608 ])
609 609
610 610 @property
611 611 def ip_addresses(self):
612 612 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
613 613 return [x.ip_addr for x in ret]
614 614
615 615 @property
616 616 def username_and_name(self):
617 617 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
618 618
619 619 @property
620 620 def username_or_name_or_email(self):
621 621 full_name = self.full_name if self.full_name is not ' ' else None
622 622 return self.username or full_name or self.email
623 623
624 624 @property
625 625 def full_name(self):
626 626 return '%s %s' % (self.firstname, self.lastname)
627 627
628 628 @property
629 629 def full_name_or_username(self):
630 630 return ('%s %s' % (self.firstname, self.lastname)
631 631 if (self.firstname and self.lastname) else self.username)
632 632
633 633 @property
634 634 def full_contact(self):
635 635 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
636 636
637 637 @property
638 638 def short_contact(self):
639 639 return '%s %s' % (self.firstname, self.lastname)
640 640
641 641 @property
642 642 def is_admin(self):
643 643 return self.admin
644 644
645 645 @property
646 646 def AuthUser(self):
647 647 """
648 648 Returns instance of AuthUser for this user
649 649 """
650 650 from rhodecode.lib.auth import AuthUser
651 651 return AuthUser(user_id=self.user_id, api_key=self.api_key,
652 652 username=self.username)
653 653
654 654 @hybrid_property
655 655 def user_data(self):
656 656 if not self._user_data:
657 657 return {}
658 658
659 659 try:
660 660 return json.loads(self._user_data)
661 661 except TypeError:
662 662 return {}
663 663
664 664 @user_data.setter
665 665 def user_data(self, val):
666 666 if not isinstance(val, dict):
667 667 raise Exception('user_data must be dict, got %s' % type(val))
668 668 try:
669 669 self._user_data = json.dumps(val)
670 670 except Exception:
671 671 log.error(traceback.format_exc())
672 672
673 673 @classmethod
674 674 def get_by_username(cls, username, case_insensitive=False,
675 675 cache=False, identity_cache=False):
676 676 session = Session()
677 677
678 678 if case_insensitive:
679 679 q = cls.query().filter(
680 680 func.lower(cls.username) == func.lower(username))
681 681 else:
682 682 q = cls.query().filter(cls.username == username)
683 683
684 684 if cache:
685 685 if identity_cache:
686 686 val = cls.identity_cache(session, 'username', username)
687 687 if val:
688 688 return val
689 689 else:
690 690 q = q.options(
691 691 FromCache("sql_cache_short",
692 692 "get_user_by_name_%s" % _hash_key(username)))
693 693
694 694 return q.scalar()
695 695
696 696 @classmethod
697 697 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
698 698 q = cls.query().filter(cls.api_key == auth_token)
699 699
700 700 if cache:
701 701 q = q.options(FromCache("sql_cache_short",
702 702 "get_auth_token_%s" % auth_token))
703 703 res = q.scalar()
704 704
705 705 if fallback and not res:
706 706 #fallback to additional keys
707 707 _res = UserApiKeys.query()\
708 708 .filter(UserApiKeys.api_key == auth_token)\
709 709 .filter(or_(UserApiKeys.expires == -1,
710 710 UserApiKeys.expires >= time.time()))\
711 711 .first()
712 712 if _res:
713 713 res = _res.user
714 714 return res
715 715
716 716 @classmethod
717 717 def get_by_email(cls, email, case_insensitive=False, cache=False):
718 718
719 719 if case_insensitive:
720 720 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
721 721
722 722 else:
723 723 q = cls.query().filter(cls.email == email)
724 724
725 725 if cache:
726 726 q = q.options(FromCache("sql_cache_short",
727 727 "get_email_key_%s" % _hash_key(email)))
728 728
729 729 ret = q.scalar()
730 730 if ret is None:
731 731 q = UserEmailMap.query()
732 732 # try fetching in alternate email map
733 733 if case_insensitive:
734 734 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
735 735 else:
736 736 q = q.filter(UserEmailMap.email == email)
737 737 q = q.options(joinedload(UserEmailMap.user))
738 738 if cache:
739 739 q = q.options(FromCache("sql_cache_short",
740 740 "get_email_map_key_%s" % email))
741 741 ret = getattr(q.scalar(), 'user', None)
742 742
743 743 return ret
744 744
745 745 @classmethod
746 746 def get_from_cs_author(cls, author):
747 747 """
748 748 Tries to get User objects out of commit author string
749 749
750 750 :param author:
751 751 """
752 752 from rhodecode.lib.helpers import email, author_name
753 753 # Valid email in the attribute passed, see if they're in the system
754 754 _email = email(author)
755 755 if _email:
756 756 user = cls.get_by_email(_email, case_insensitive=True)
757 757 if user:
758 758 return user
759 759 # Maybe we can match by username?
760 760 _author = author_name(author)
761 761 user = cls.get_by_username(_author, case_insensitive=True)
762 762 if user:
763 763 return user
764 764
765 765 def update_userdata(self, **kwargs):
766 766 usr = self
767 767 old = usr.user_data
768 768 old.update(**kwargs)
769 769 usr.user_data = old
770 770 Session().add(usr)
771 771 log.debug('updated userdata with ', kwargs)
772 772
773 773 def update_lastlogin(self):
774 774 """Update user lastlogin"""
775 775 self.last_login = datetime.datetime.now()
776 776 Session().add(self)
777 777 log.debug('updated user %s lastlogin', self.username)
778 778
779 779 def update_lastactivity(self):
780 780 """Update user lastactivity"""
781 781 usr = self
782 782 old = usr.user_data
783 783 old.update({'last_activity': time.time()})
784 784 usr.user_data = old
785 785 Session().add(usr)
786 786 log.debug('updated user %s lastactivity', usr.username)
787 787
788 788 def update_password(self, new_password, change_api_key=False):
789 789 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
790 790
791 791 self.password = get_crypt_password(new_password)
792 792 if change_api_key:
793 793 self.api_key = generate_auth_token(self.username)
794 794 Session().add(self)
795 795
796 796 @classmethod
797 797 def get_first_super_admin(cls):
798 798 user = User.query().filter(User.admin == true()).first()
799 799 if user is None:
800 800 raise Exception('FATAL: Missing administrative account!')
801 801 return user
802 802
803 803 @classmethod
804 804 def get_all_super_admins(cls):
805 805 """
806 806 Returns all admin accounts sorted by username
807 807 """
808 808 return User.query().filter(User.admin == true())\
809 809 .order_by(User.username.asc()).all()
810 810
811 811 @classmethod
812 812 def get_default_user(cls, cache=False):
813 813 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
814 814 if user is None:
815 815 raise Exception('FATAL: Missing default account!')
816 816 return user
817 817
818 818 def _get_default_perms(self, user, suffix=''):
819 819 from rhodecode.model.permission import PermissionModel
820 820 return PermissionModel().get_default_perms(user.user_perms, suffix)
821 821
822 822 def get_default_perms(self, suffix=''):
823 823 return self._get_default_perms(self, suffix)
824 824
825 825 def get_api_data(self, include_secrets=False, details='full'):
826 826 """
827 827 Common function for generating user related data for API
828 828
829 829 :param include_secrets: By default secrets in the API data will be replaced
830 830 by a placeholder value to prevent exposing this data by accident. In case
831 831 this data shall be exposed, set this flag to ``True``.
832 832
833 833 :param details: details can be 'basic|full' basic gives only a subset of
834 834 the available user information that includes user_id, name and emails.
835 835 """
836 836 user = self
837 837 user_data = self.user_data
838 838 data = {
839 839 'user_id': user.user_id,
840 840 'username': user.username,
841 841 'firstname': user.name,
842 842 'lastname': user.lastname,
843 843 'email': user.email,
844 844 'emails': user.emails,
845 845 }
846 846 if details == 'basic':
847 847 return data
848 848
849 849 api_key_length = 40
850 850 api_key_replacement = '*' * api_key_length
851 851
852 852 extras = {
853 853 'api_key': api_key_replacement,
854 854 'api_keys': [api_key_replacement],
855 855 'active': user.active,
856 856 'admin': user.admin,
857 857 'extern_type': user.extern_type,
858 858 'extern_name': user.extern_name,
859 859 'last_login': user.last_login,
860 860 'ip_addresses': user.ip_addresses,
861 861 'language': user_data.get('language')
862 862 }
863 863 data.update(extras)
864 864
865 865 if include_secrets:
866 866 data['api_key'] = user.api_key
867 867 data['api_keys'] = user.auth_tokens
868 868 return data
869 869
870 870 def __json__(self):
871 871 data = {
872 872 'full_name': self.full_name,
873 873 'full_name_or_username': self.full_name_or_username,
874 874 'short_contact': self.short_contact,
875 875 'full_contact': self.full_contact,
876 876 }
877 877 data.update(self.get_api_data())
878 878 return data
879 879
880 880
881 881 class UserApiKeys(Base, BaseModel):
882 882 __tablename__ = 'user_api_keys'
883 883 __table_args__ = (
884 884 Index('uak_api_key_idx', 'api_key'),
885 885 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
886 886 UniqueConstraint('api_key'),
887 887 {'extend_existing': True, 'mysql_engine': 'InnoDB',
888 888 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
889 889 )
890 890 __mapper_args__ = {}
891 891
892 892 # ApiKey role
893 893 ROLE_ALL = 'token_role_all'
894 894 ROLE_HTTP = 'token_role_http'
895 895 ROLE_VCS = 'token_role_vcs'
896 896 ROLE_API = 'token_role_api'
897 897 ROLE_FEED = 'token_role_feed'
898 898 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
899 899
900 900 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
901 901 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
902 902 api_key = Column("api_key", String(255), nullable=False, unique=True)
903 903 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
904 904 expires = Column('expires', Float(53), nullable=False)
905 905 role = Column('role', String(255), nullable=True)
906 906 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
907 907
908 908 user = relationship('User', lazy='joined')
909 909
910 910 @classmethod
911 911 def _get_role_name(cls, role):
912 912 return {
913 913 cls.ROLE_ALL: _('all'),
914 914 cls.ROLE_HTTP: _('http/web interface'),
915 915 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
916 916 cls.ROLE_API: _('api calls'),
917 917 cls.ROLE_FEED: _('feed access'),
918 918 }.get(role, role)
919 919
920 920 @property
921 921 def expired(self):
922 922 if self.expires == -1:
923 923 return False
924 924 return time.time() > self.expires
925 925
926 926 @property
927 927 def role_humanized(self):
928 928 return self._get_role_name(self.role)
929 929
930 930
931 931 class UserEmailMap(Base, BaseModel):
932 932 __tablename__ = 'user_email_map'
933 933 __table_args__ = (
934 934 Index('uem_email_idx', 'email'),
935 935 UniqueConstraint('email'),
936 936 {'extend_existing': True, 'mysql_engine': 'InnoDB',
937 937 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
938 938 )
939 939 __mapper_args__ = {}
940 940
941 941 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
942 942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
943 943 _email = Column("email", String(255), nullable=True, unique=False, default=None)
944 944 user = relationship('User', lazy='joined')
945 945
946 946 @validates('_email')
947 947 def validate_email(self, key, email):
948 948 # check if this email is not main one
949 949 main_email = Session().query(User).filter(User.email == email).scalar()
950 950 if main_email is not None:
951 951 raise AttributeError('email %s is present is user table' % email)
952 952 return email
953 953
954 954 @hybrid_property
955 955 def email(self):
956 956 return self._email
957 957
958 958 @email.setter
959 959 def email(self, val):
960 960 self._email = val.lower() if val else None
961 961
962 962
963 963 class UserIpMap(Base, BaseModel):
964 964 __tablename__ = 'user_ip_map'
965 965 __table_args__ = (
966 966 UniqueConstraint('user_id', 'ip_addr'),
967 967 {'extend_existing': True, 'mysql_engine': 'InnoDB',
968 968 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
969 969 )
970 970 __mapper_args__ = {}
971 971
972 972 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
973 973 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
974 974 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
975 975 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
976 976 description = Column("description", String(10000), nullable=True, unique=None, default=None)
977 977 user = relationship('User', lazy='joined')
978 978
979 979 @classmethod
980 980 def _get_ip_range(cls, ip_addr):
981 981 net = ipaddress.ip_network(ip_addr, strict=False)
982 982 return [str(net.network_address), str(net.broadcast_address)]
983 983
984 984 def __json__(self):
985 985 return {
986 986 'ip_addr': self.ip_addr,
987 987 'ip_range': self._get_ip_range(self.ip_addr),
988 988 }
989 989
990 990 def __unicode__(self):
991 991 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
992 992 self.user_id, self.ip_addr)
993 993
994 994 class UserLog(Base, BaseModel):
995 995 __tablename__ = 'user_logs'
996 996 __table_args__ = (
997 997 {'extend_existing': True, 'mysql_engine': 'InnoDB',
998 998 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
999 999 )
1000 1000 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1001 1001 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1002 1002 username = Column("username", String(255), nullable=True, unique=None, default=None)
1003 1003 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1004 1004 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1005 1005 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1006 1006 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1007 1007 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1008 1008
1009 1009 def __unicode__(self):
1010 1010 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1011 1011 self.repository_name,
1012 1012 self.action)
1013 1013
1014 1014 @property
1015 1015 def action_as_day(self):
1016 1016 return datetime.date(*self.action_date.timetuple()[:3])
1017 1017
1018 1018 user = relationship('User')
1019 1019 repository = relationship('Repository', cascade='')
1020 1020
1021 1021
1022 1022 class UserGroup(Base, BaseModel):
1023 1023 __tablename__ = 'users_groups'
1024 1024 __table_args__ = (
1025 1025 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1026 1026 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1027 1027 )
1028 1028
1029 1029 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1030 1030 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1031 1031 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1032 1032 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1033 1033 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1034 1034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1035 1035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1036 1036 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1037 1037
1038 1038 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1039 1039 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1040 1040 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1041 1041 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1042 1042 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1043 1043 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1044 1044
1045 1045 user = relationship('User')
1046 1046
1047 1047 @hybrid_property
1048 1048 def group_data(self):
1049 1049 if not self._group_data:
1050 1050 return {}
1051 1051
1052 1052 try:
1053 1053 return json.loads(self._group_data)
1054 1054 except TypeError:
1055 1055 return {}
1056 1056
1057 1057 @group_data.setter
1058 1058 def group_data(self, val):
1059 1059 try:
1060 1060 self._group_data = json.dumps(val)
1061 1061 except Exception:
1062 1062 log.error(traceback.format_exc())
1063 1063
1064 1064 def __unicode__(self):
1065 1065 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1066 1066 self.users_group_id,
1067 1067 self.users_group_name)
1068 1068
1069 1069 @classmethod
1070 1070 def get_by_group_name(cls, group_name, cache=False,
1071 1071 case_insensitive=False):
1072 1072 if case_insensitive:
1073 1073 q = cls.query().filter(func.lower(cls.users_group_name) ==
1074 1074 func.lower(group_name))
1075 1075
1076 1076 else:
1077 1077 q = cls.query().filter(cls.users_group_name == group_name)
1078 1078 if cache:
1079 1079 q = q.options(FromCache(
1080 1080 "sql_cache_short",
1081 1081 "get_group_%s" % _hash_key(group_name)))
1082 1082 return q.scalar()
1083 1083
1084 1084 @classmethod
1085 1085 def get(cls, user_group_id, cache=False):
1086 1086 user_group = cls.query()
1087 1087 if cache:
1088 1088 user_group = user_group.options(FromCache("sql_cache_short",
1089 1089 "get_users_group_%s" % user_group_id))
1090 1090 return user_group.get(user_group_id)
1091 1091
1092 1092 def permissions(self, with_admins=True, with_owner=True):
1093 1093 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1094 1094 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1095 1095 joinedload(UserUserGroupToPerm.user),
1096 1096 joinedload(UserUserGroupToPerm.permission),)
1097 1097
1098 1098 # get owners and admins and permissions. We do a trick of re-writing
1099 1099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1100 1100 # has a global reference and changing one object propagates to all
1101 1101 # others. This means if admin is also an owner admin_row that change
1102 1102 # would propagate to both objects
1103 1103 perm_rows = []
1104 1104 for _usr in q.all():
1105 1105 usr = AttributeDict(_usr.user.get_dict())
1106 1106 usr.permission = _usr.permission.permission_name
1107 1107 perm_rows.append(usr)
1108 1108
1109 1109 # filter the perm rows by 'default' first and then sort them by
1110 1110 # admin,write,read,none permissions sorted again alphabetically in
1111 1111 # each group
1112 1112 perm_rows = sorted(perm_rows, key=display_sort)
1113 1113
1114 1114 _admin_perm = 'usergroup.admin'
1115 1115 owner_row = []
1116 1116 if with_owner:
1117 1117 usr = AttributeDict(self.user.get_dict())
1118 1118 usr.owner_row = True
1119 1119 usr.permission = _admin_perm
1120 1120 owner_row.append(usr)
1121 1121
1122 1122 super_admin_rows = []
1123 1123 if with_admins:
1124 1124 for usr in User.get_all_super_admins():
1125 1125 # if this admin is also owner, don't double the record
1126 1126 if usr.user_id == owner_row[0].user_id:
1127 1127 owner_row[0].admin_row = True
1128 1128 else:
1129 1129 usr = AttributeDict(usr.get_dict())
1130 1130 usr.admin_row = True
1131 1131 usr.permission = _admin_perm
1132 1132 super_admin_rows.append(usr)
1133 1133
1134 1134 return super_admin_rows + owner_row + perm_rows
1135 1135
1136 1136 def permission_user_groups(self):
1137 1137 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1138 1138 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1139 1139 joinedload(UserGroupUserGroupToPerm.target_user_group),
1140 1140 joinedload(UserGroupUserGroupToPerm.permission),)
1141 1141
1142 1142 perm_rows = []
1143 1143 for _user_group in q.all():
1144 1144 usr = AttributeDict(_user_group.user_group.get_dict())
1145 1145 usr.permission = _user_group.permission.permission_name
1146 1146 perm_rows.append(usr)
1147 1147
1148 1148 return perm_rows
1149 1149
1150 1150 def _get_default_perms(self, user_group, suffix=''):
1151 1151 from rhodecode.model.permission import PermissionModel
1152 1152 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1153 1153
1154 1154 def get_default_perms(self, suffix=''):
1155 1155 return self._get_default_perms(self, suffix)
1156 1156
1157 1157 def get_api_data(self, with_group_members=True, include_secrets=False):
1158 1158 """
1159 1159 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1160 1160 basically forwarded.
1161 1161
1162 1162 """
1163 1163 user_group = self
1164 1164
1165 1165 data = {
1166 1166 'users_group_id': user_group.users_group_id,
1167 1167 'group_name': user_group.users_group_name,
1168 1168 'group_description': user_group.user_group_description,
1169 1169 'active': user_group.users_group_active,
1170 1170 'owner': user_group.user.username,
1171 1171 }
1172 1172 if with_group_members:
1173 1173 users = []
1174 1174 for user in user_group.members:
1175 1175 user = user.user
1176 1176 users.append(user.get_api_data(include_secrets=include_secrets))
1177 1177 data['users'] = users
1178 1178
1179 1179 return data
1180 1180
1181 1181
1182 1182 class UserGroupMember(Base, BaseModel):
1183 1183 __tablename__ = 'users_groups_members'
1184 1184 __table_args__ = (
1185 1185 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1186 1186 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1187 1187 )
1188 1188
1189 1189 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1190 1190 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1191 1191 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1192 1192
1193 1193 user = relationship('User', lazy='joined')
1194 1194 users_group = relationship('UserGroup')
1195 1195
1196 1196 def __init__(self, gr_id='', u_id=''):
1197 1197 self.users_group_id = gr_id
1198 1198 self.user_id = u_id
1199 1199
1200 1200
1201 1201 class RepositoryField(Base, BaseModel):
1202 1202 __tablename__ = 'repositories_fields'
1203 1203 __table_args__ = (
1204 1204 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1205 1205 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1206 1206 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1207 1207 )
1208 1208 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1209 1209
1210 1210 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1211 1211 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1212 1212 field_key = Column("field_key", String(250))
1213 1213 field_label = Column("field_label", String(1024), nullable=False)
1214 1214 field_value = Column("field_value", String(10000), nullable=False)
1215 1215 field_desc = Column("field_desc", String(1024), nullable=False)
1216 1216 field_type = Column("field_type", String(255), nullable=False, unique=None)
1217 1217 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1218 1218
1219 1219 repository = relationship('Repository')
1220 1220
1221 1221 @property
1222 1222 def field_key_prefixed(self):
1223 1223 return 'ex_%s' % self.field_key
1224 1224
1225 1225 @classmethod
1226 1226 def un_prefix_key(cls, key):
1227 1227 if key.startswith(cls.PREFIX):
1228 1228 return key[len(cls.PREFIX):]
1229 1229 return key
1230 1230
1231 1231 @classmethod
1232 1232 def get_by_key_name(cls, key, repo):
1233 1233 row = cls.query()\
1234 1234 .filter(cls.repository == repo)\
1235 1235 .filter(cls.field_key == key).scalar()
1236 1236 return row
1237 1237
1238 1238
1239 1239 class Repository(Base, BaseModel):
1240 1240 __tablename__ = 'repositories'
1241 1241 __table_args__ = (
1242 1242 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1243 1243 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1244 1244 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1245 1245 )
1246 1246 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1247 1247 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1248 1248
1249 1249 STATE_CREATED = 'repo_state_created'
1250 1250 STATE_PENDING = 'repo_state_pending'
1251 1251 STATE_ERROR = 'repo_state_error'
1252 1252
1253 1253 LOCK_AUTOMATIC = 'lock_auto'
1254 1254 LOCK_API = 'lock_api'
1255 1255 LOCK_WEB = 'lock_web'
1256 1256 LOCK_PULL = 'lock_pull'
1257 1257
1258 1258 NAME_SEP = URL_SEP
1259 1259
1260 1260 repo_id = Column(
1261 1261 "repo_id", Integer(), nullable=False, unique=True, default=None,
1262 1262 primary_key=True)
1263 1263 _repo_name = Column(
1264 1264 "repo_name", Text(), nullable=False, default=None)
1265 1265 _repo_name_hash = Column(
1266 1266 "repo_name_hash", String(255), nullable=False, unique=True)
1267 1267 repo_state = Column("repo_state", String(255), nullable=True)
1268 1268
1269 1269 clone_uri = Column(
1270 1270 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1271 1271 default=None)
1272 1272 repo_type = Column(
1273 1273 "repo_type", String(255), nullable=False, unique=False, default=None)
1274 1274 user_id = Column(
1275 1275 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1276 1276 unique=False, default=None)
1277 1277 private = Column(
1278 1278 "private", Boolean(), nullable=True, unique=None, default=None)
1279 1279 enable_statistics = Column(
1280 1280 "statistics", Boolean(), nullable=True, unique=None, default=True)
1281 1281 enable_downloads = Column(
1282 1282 "downloads", Boolean(), nullable=True, unique=None, default=True)
1283 1283 description = Column(
1284 1284 "description", String(10000), nullable=True, unique=None, default=None)
1285 1285 created_on = Column(
1286 1286 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1287 1287 default=datetime.datetime.now)
1288 1288 updated_on = Column(
1289 1289 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1290 1290 default=datetime.datetime.now)
1291 1291 _landing_revision = Column(
1292 1292 "landing_revision", String(255), nullable=False, unique=False,
1293 1293 default=None)
1294 1294 enable_locking = Column(
1295 1295 "enable_locking", Boolean(), nullable=False, unique=None,
1296 1296 default=False)
1297 1297 _locked = Column(
1298 1298 "locked", String(255), nullable=True, unique=False, default=None)
1299 1299 _changeset_cache = Column(
1300 1300 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1301 1301
1302 1302 fork_id = Column(
1303 1303 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1304 1304 nullable=True, unique=False, default=None)
1305 1305 group_id = Column(
1306 1306 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1307 1307 unique=False, default=None)
1308 1308
1309 1309 user = relationship('User', lazy='joined')
1310 1310 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1311 1311 group = relationship('RepoGroup', lazy='joined')
1312 1312 repo_to_perm = relationship(
1313 1313 'UserRepoToPerm', cascade='all',
1314 1314 order_by='UserRepoToPerm.repo_to_perm_id')
1315 1315 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1316 1316 stats = relationship('Statistics', cascade='all', uselist=False)
1317 1317
1318 1318 followers = relationship(
1319 1319 'UserFollowing',
1320 1320 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1321 1321 cascade='all')
1322 1322 extra_fields = relationship(
1323 1323 'RepositoryField', cascade="all, delete, delete-orphan")
1324 1324 logs = relationship('UserLog')
1325 1325 comments = relationship(
1326 1326 'ChangesetComment', cascade="all, delete, delete-orphan")
1327 1327 pull_requests_source = relationship(
1328 1328 'PullRequest',
1329 1329 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1330 1330 cascade="all, delete, delete-orphan")
1331 1331 pull_requests_target = relationship(
1332 1332 'PullRequest',
1333 1333 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1334 1334 cascade="all, delete, delete-orphan")
1335 1335 ui = relationship('RepoRhodeCodeUi', cascade="all")
1336 1336 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1337 1337 integrations = relationship('Integration',
1338 1338 cascade="all, delete, delete-orphan")
1339 1339
1340 1340 def __unicode__(self):
1341 1341 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1342 1342 safe_unicode(self.repo_name))
1343 1343
1344 1344 @hybrid_property
1345 1345 def landing_rev(self):
1346 1346 # always should return [rev_type, rev]
1347 1347 if self._landing_revision:
1348 1348 _rev_info = self._landing_revision.split(':')
1349 1349 if len(_rev_info) < 2:
1350 1350 _rev_info.insert(0, 'rev')
1351 1351 return [_rev_info[0], _rev_info[1]]
1352 1352 return [None, None]
1353 1353
1354 1354 @landing_rev.setter
1355 1355 def landing_rev(self, val):
1356 1356 if ':' not in val:
1357 1357 raise ValueError('value must be delimited with `:` and consist '
1358 1358 'of <rev_type>:<rev>, got %s instead' % val)
1359 1359 self._landing_revision = val
1360 1360
1361 1361 @hybrid_property
1362 1362 def locked(self):
1363 1363 if self._locked:
1364 1364 user_id, timelocked, reason = self._locked.split(':')
1365 1365 lock_values = int(user_id), timelocked, reason
1366 1366 else:
1367 1367 lock_values = [None, None, None]
1368 1368 return lock_values
1369 1369
1370 1370 @locked.setter
1371 1371 def locked(self, val):
1372 1372 if val and isinstance(val, (list, tuple)):
1373 1373 self._locked = ':'.join(map(str, val))
1374 1374 else:
1375 1375 self._locked = None
1376 1376
1377 1377 @hybrid_property
1378 1378 def changeset_cache(self):
1379 1379 from rhodecode.lib.vcs.backends.base import EmptyCommit
1380 1380 dummy = EmptyCommit().__json__()
1381 1381 if not self._changeset_cache:
1382 1382 return dummy
1383 1383 try:
1384 1384 return json.loads(self._changeset_cache)
1385 1385 except TypeError:
1386 1386 return dummy
1387 1387 except Exception:
1388 1388 log.error(traceback.format_exc())
1389 1389 return dummy
1390 1390
1391 1391 @changeset_cache.setter
1392 1392 def changeset_cache(self, val):
1393 1393 try:
1394 1394 self._changeset_cache = json.dumps(val)
1395 1395 except Exception:
1396 1396 log.error(traceback.format_exc())
1397 1397
1398 1398 @hybrid_property
1399 1399 def repo_name(self):
1400 1400 return self._repo_name
1401 1401
1402 1402 @repo_name.setter
1403 1403 def repo_name(self, value):
1404 1404 self._repo_name = value
1405 1405 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1406 1406
1407 1407 @classmethod
1408 1408 def normalize_repo_name(cls, repo_name):
1409 1409 """
1410 1410 Normalizes os specific repo_name to the format internally stored inside
1411 1411 database using URL_SEP
1412 1412
1413 1413 :param cls:
1414 1414 :param repo_name:
1415 1415 """
1416 1416 return cls.NAME_SEP.join(repo_name.split(os.sep))
1417 1417
1418 1418 @classmethod
1419 1419 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1420 1420 session = Session()
1421 1421 q = session.query(cls).filter(cls.repo_name == repo_name)
1422 1422
1423 1423 if cache:
1424 1424 if identity_cache:
1425 1425 val = cls.identity_cache(session, 'repo_name', repo_name)
1426 1426 if val:
1427 1427 return val
1428 1428 else:
1429 1429 q = q.options(
1430 1430 FromCache("sql_cache_short",
1431 1431 "get_repo_by_name_%s" % _hash_key(repo_name)))
1432 1432
1433 1433 return q.scalar()
1434 1434
1435 1435 @classmethod
1436 1436 def get_by_full_path(cls, repo_full_path):
1437 1437 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1438 1438 repo_name = cls.normalize_repo_name(repo_name)
1439 1439 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1440 1440
1441 1441 @classmethod
1442 1442 def get_repo_forks(cls, repo_id):
1443 1443 return cls.query().filter(Repository.fork_id == repo_id)
1444 1444
1445 1445 @classmethod
1446 1446 def base_path(cls):
1447 1447 """
1448 1448 Returns base path when all repos are stored
1449 1449
1450 1450 :param cls:
1451 1451 """
1452 1452 q = Session().query(RhodeCodeUi)\
1453 1453 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1454 1454 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1455 1455 return q.one().ui_value
1456 1456
1457 1457 @classmethod
1458 1458 def is_valid(cls, repo_name):
1459 1459 """
1460 1460 returns True if given repo name is a valid filesystem repository
1461 1461
1462 1462 :param cls:
1463 1463 :param repo_name:
1464 1464 """
1465 1465 from rhodecode.lib.utils import is_valid_repo
1466 1466
1467 1467 return is_valid_repo(repo_name, cls.base_path())
1468 1468
1469 1469 @classmethod
1470 1470 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1471 1471 case_insensitive=True):
1472 1472 q = Repository.query()
1473 1473
1474 1474 if not isinstance(user_id, Optional):
1475 1475 q = q.filter(Repository.user_id == user_id)
1476 1476
1477 1477 if not isinstance(group_id, Optional):
1478 1478 q = q.filter(Repository.group_id == group_id)
1479 1479
1480 1480 if case_insensitive:
1481 1481 q = q.order_by(func.lower(Repository.repo_name))
1482 1482 else:
1483 1483 q = q.order_by(Repository.repo_name)
1484 1484 return q.all()
1485 1485
1486 1486 @property
1487 1487 def forks(self):
1488 1488 """
1489 1489 Return forks of this repo
1490 1490 """
1491 1491 return Repository.get_repo_forks(self.repo_id)
1492 1492
1493 1493 @property
1494 1494 def parent(self):
1495 1495 """
1496 1496 Returns fork parent
1497 1497 """
1498 1498 return self.fork
1499 1499
1500 1500 @property
1501 1501 def just_name(self):
1502 1502 return self.repo_name.split(self.NAME_SEP)[-1]
1503 1503
1504 1504 @property
1505 1505 def groups_with_parents(self):
1506 1506 groups = []
1507 1507 if self.group is None:
1508 1508 return groups
1509 1509
1510 1510 cur_gr = self.group
1511 1511 groups.insert(0, cur_gr)
1512 1512 while 1:
1513 1513 gr = getattr(cur_gr, 'parent_group', None)
1514 1514 cur_gr = cur_gr.parent_group
1515 1515 if gr is None:
1516 1516 break
1517 1517 groups.insert(0, gr)
1518 1518
1519 1519 return groups
1520 1520
1521 1521 @property
1522 1522 def groups_and_repo(self):
1523 1523 return self.groups_with_parents, self
1524 1524
1525 1525 @LazyProperty
1526 1526 def repo_path(self):
1527 1527 """
1528 1528 Returns base full path for that repository means where it actually
1529 1529 exists on a filesystem
1530 1530 """
1531 1531 q = Session().query(RhodeCodeUi).filter(
1532 1532 RhodeCodeUi.ui_key == self.NAME_SEP)
1533 1533 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1534 1534 return q.one().ui_value
1535 1535
1536 1536 @property
1537 1537 def repo_full_path(self):
1538 1538 p = [self.repo_path]
1539 1539 # we need to split the name by / since this is how we store the
1540 1540 # names in the database, but that eventually needs to be converted
1541 1541 # into a valid system path
1542 1542 p += self.repo_name.split(self.NAME_SEP)
1543 1543 return os.path.join(*map(safe_unicode, p))
1544 1544
1545 1545 @property
1546 1546 def cache_keys(self):
1547 1547 """
1548 1548 Returns associated cache keys for that repo
1549 1549 """
1550 1550 return CacheKey.query()\
1551 1551 .filter(CacheKey.cache_args == self.repo_name)\
1552 1552 .order_by(CacheKey.cache_key)\
1553 1553 .all()
1554 1554
1555 1555 def get_new_name(self, repo_name):
1556 1556 """
1557 1557 returns new full repository name based on assigned group and new new
1558 1558
1559 1559 :param group_name:
1560 1560 """
1561 1561 path_prefix = self.group.full_path_splitted if self.group else []
1562 1562 return self.NAME_SEP.join(path_prefix + [repo_name])
1563 1563
1564 1564 @property
1565 1565 def _config(self):
1566 1566 """
1567 1567 Returns db based config object.
1568 1568 """
1569 1569 from rhodecode.lib.utils import make_db_config
1570 1570 return make_db_config(clear_session=False, repo=self)
1571 1571
1572 1572 def permissions(self, with_admins=True, with_owner=True):
1573 1573 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1574 1574 q = q.options(joinedload(UserRepoToPerm.repository),
1575 1575 joinedload(UserRepoToPerm.user),
1576 1576 joinedload(UserRepoToPerm.permission),)
1577 1577
1578 1578 # get owners and admins and permissions. We do a trick of re-writing
1579 1579 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1580 1580 # has a global reference and changing one object propagates to all
1581 1581 # others. This means if admin is also an owner admin_row that change
1582 1582 # would propagate to both objects
1583 1583 perm_rows = []
1584 1584 for _usr in q.all():
1585 1585 usr = AttributeDict(_usr.user.get_dict())
1586 1586 usr.permission = _usr.permission.permission_name
1587 1587 perm_rows.append(usr)
1588 1588
1589 1589 # filter the perm rows by 'default' first and then sort them by
1590 1590 # admin,write,read,none permissions sorted again alphabetically in
1591 1591 # each group
1592 1592 perm_rows = sorted(perm_rows, key=display_sort)
1593 1593
1594 1594 _admin_perm = 'repository.admin'
1595 1595 owner_row = []
1596 1596 if with_owner:
1597 1597 usr = AttributeDict(self.user.get_dict())
1598 1598 usr.owner_row = True
1599 1599 usr.permission = _admin_perm
1600 1600 owner_row.append(usr)
1601 1601
1602 1602 super_admin_rows = []
1603 1603 if with_admins:
1604 1604 for usr in User.get_all_super_admins():
1605 1605 # if this admin is also owner, don't double the record
1606 1606 if usr.user_id == owner_row[0].user_id:
1607 1607 owner_row[0].admin_row = True
1608 1608 else:
1609 1609 usr = AttributeDict(usr.get_dict())
1610 1610 usr.admin_row = True
1611 1611 usr.permission = _admin_perm
1612 1612 super_admin_rows.append(usr)
1613 1613
1614 1614 return super_admin_rows + owner_row + perm_rows
1615 1615
1616 1616 def permission_user_groups(self):
1617 1617 q = UserGroupRepoToPerm.query().filter(
1618 1618 UserGroupRepoToPerm.repository == self)
1619 1619 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1620 1620 joinedload(UserGroupRepoToPerm.users_group),
1621 1621 joinedload(UserGroupRepoToPerm.permission),)
1622 1622
1623 1623 perm_rows = []
1624 1624 for _user_group in q.all():
1625 1625 usr = AttributeDict(_user_group.users_group.get_dict())
1626 1626 usr.permission = _user_group.permission.permission_name
1627 1627 perm_rows.append(usr)
1628 1628
1629 1629 return perm_rows
1630 1630
1631 1631 def get_api_data(self, include_secrets=False):
1632 1632 """
1633 1633 Common function for generating repo api data
1634 1634
1635 1635 :param include_secrets: See :meth:`User.get_api_data`.
1636 1636
1637 1637 """
1638 1638 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1639 1639 # move this methods on models level.
1640 1640 from rhodecode.model.settings import SettingsModel
1641 1641
1642 1642 repo = self
1643 1643 _user_id, _time, _reason = self.locked
1644 1644
1645 1645 data = {
1646 1646 'repo_id': repo.repo_id,
1647 1647 'repo_name': repo.repo_name,
1648 1648 'repo_type': repo.repo_type,
1649 1649 'clone_uri': repo.clone_uri or '',
1650 1650 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1651 1651 'private': repo.private,
1652 1652 'created_on': repo.created_on,
1653 1653 'description': repo.description,
1654 1654 'landing_rev': repo.landing_rev,
1655 1655 'owner': repo.user.username,
1656 1656 'fork_of': repo.fork.repo_name if repo.fork else None,
1657 1657 'enable_statistics': repo.enable_statistics,
1658 1658 'enable_locking': repo.enable_locking,
1659 1659 'enable_downloads': repo.enable_downloads,
1660 1660 'last_changeset': repo.changeset_cache,
1661 1661 'locked_by': User.get(_user_id).get_api_data(
1662 1662 include_secrets=include_secrets) if _user_id else None,
1663 1663 'locked_date': time_to_datetime(_time) if _time else None,
1664 1664 'lock_reason': _reason if _reason else None,
1665 1665 }
1666 1666
1667 1667 # TODO: mikhail: should be per-repo settings here
1668 1668 rc_config = SettingsModel().get_all_settings()
1669 1669 repository_fields = str2bool(
1670 1670 rc_config.get('rhodecode_repository_fields'))
1671 1671 if repository_fields:
1672 1672 for f in self.extra_fields:
1673 1673 data[f.field_key_prefixed] = f.field_value
1674 1674
1675 1675 return data
1676 1676
1677 1677 @classmethod
1678 1678 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1679 1679 if not lock_time:
1680 1680 lock_time = time.time()
1681 1681 if not lock_reason:
1682 1682 lock_reason = cls.LOCK_AUTOMATIC
1683 1683 repo.locked = [user_id, lock_time, lock_reason]
1684 1684 Session().add(repo)
1685 1685 Session().commit()
1686 1686
1687 1687 @classmethod
1688 1688 def unlock(cls, repo):
1689 1689 repo.locked = None
1690 1690 Session().add(repo)
1691 1691 Session().commit()
1692 1692
1693 1693 @classmethod
1694 1694 def getlock(cls, repo):
1695 1695 return repo.locked
1696 1696
1697 1697 def is_user_lock(self, user_id):
1698 1698 if self.lock[0]:
1699 1699 lock_user_id = safe_int(self.lock[0])
1700 1700 user_id = safe_int(user_id)
1701 1701 # both are ints, and they are equal
1702 1702 return all([lock_user_id, user_id]) and lock_user_id == user_id
1703 1703
1704 1704 return False
1705 1705
1706 1706 def get_locking_state(self, action, user_id, only_when_enabled=True):
1707 1707 """
1708 1708 Checks locking on this repository, if locking is enabled and lock is
1709 1709 present returns a tuple of make_lock, locked, locked_by.
1710 1710 make_lock can have 3 states None (do nothing) True, make lock
1711 1711 False release lock, This value is later propagated to hooks, which
1712 1712 do the locking. Think about this as signals passed to hooks what to do.
1713 1713
1714 1714 """
1715 1715 # TODO: johbo: This is part of the business logic and should be moved
1716 1716 # into the RepositoryModel.
1717 1717
1718 1718 if action not in ('push', 'pull'):
1719 1719 raise ValueError("Invalid action value: %s" % repr(action))
1720 1720
1721 1721 # defines if locked error should be thrown to user
1722 1722 currently_locked = False
1723 1723 # defines if new lock should be made, tri-state
1724 1724 make_lock = None
1725 1725 repo = self
1726 1726 user = User.get(user_id)
1727 1727
1728 1728 lock_info = repo.locked
1729 1729
1730 1730 if repo and (repo.enable_locking or not only_when_enabled):
1731 1731 if action == 'push':
1732 1732 # check if it's already locked !, if it is compare users
1733 1733 locked_by_user_id = lock_info[0]
1734 1734 if user.user_id == locked_by_user_id:
1735 1735 log.debug(
1736 1736 'Got `push` action from user %s, now unlocking', user)
1737 1737 # unlock if we have push from user who locked
1738 1738 make_lock = False
1739 1739 else:
1740 1740 # we're not the same user who locked, ban with
1741 1741 # code defined in settings (default is 423 HTTP Locked) !
1742 1742 log.debug('Repo %s is currently locked by %s', repo, user)
1743 1743 currently_locked = True
1744 1744 elif action == 'pull':
1745 1745 # [0] user [1] date
1746 1746 if lock_info[0] and lock_info[1]:
1747 1747 log.debug('Repo %s is currently locked by %s', repo, user)
1748 1748 currently_locked = True
1749 1749 else:
1750 1750 log.debug('Setting lock on repo %s by %s', repo, user)
1751 1751 make_lock = True
1752 1752
1753 1753 else:
1754 1754 log.debug('Repository %s do not have locking enabled', repo)
1755 1755
1756 1756 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1757 1757 make_lock, currently_locked, lock_info)
1758 1758
1759 1759 from rhodecode.lib.auth import HasRepoPermissionAny
1760 1760 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1761 1761 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1762 1762 # if we don't have at least write permission we cannot make a lock
1763 1763 log.debug('lock state reset back to FALSE due to lack '
1764 1764 'of at least read permission')
1765 1765 make_lock = False
1766 1766
1767 1767 return make_lock, currently_locked, lock_info
1768 1768
1769 1769 @property
1770 1770 def last_db_change(self):
1771 1771 return self.updated_on
1772 1772
1773 1773 @property
1774 1774 def clone_uri_hidden(self):
1775 1775 clone_uri = self.clone_uri
1776 1776 if clone_uri:
1777 1777 import urlobject
1778 1778 url_obj = urlobject.URLObject(clone_uri)
1779 1779 if url_obj.password:
1780 1780 clone_uri = url_obj.with_password('*****')
1781 1781 return clone_uri
1782 1782
1783 1783 def clone_url(self, **override):
1784 1784 qualified_home_url = url('home', qualified=True)
1785 1785
1786 1786 uri_tmpl = None
1787 1787 if 'with_id' in override:
1788 1788 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1789 1789 del override['with_id']
1790 1790
1791 1791 if 'uri_tmpl' in override:
1792 1792 uri_tmpl = override['uri_tmpl']
1793 1793 del override['uri_tmpl']
1794 1794
1795 1795 # we didn't override our tmpl from **overrides
1796 1796 if not uri_tmpl:
1797 1797 uri_tmpl = self.DEFAULT_CLONE_URI
1798 1798 try:
1799 1799 from pylons import tmpl_context as c
1800 1800 uri_tmpl = c.clone_uri_tmpl
1801 1801 except Exception:
1802 1802 # in any case if we call this outside of request context,
1803 1803 # ie, not having tmpl_context set up
1804 1804 pass
1805 1805
1806 1806 return get_clone_url(uri_tmpl=uri_tmpl,
1807 1807 qualifed_home_url=qualified_home_url,
1808 1808 repo_name=self.repo_name,
1809 1809 repo_id=self.repo_id, **override)
1810 1810
1811 1811 def set_state(self, state):
1812 1812 self.repo_state = state
1813 1813 Session().add(self)
1814 1814 #==========================================================================
1815 1815 # SCM PROPERTIES
1816 1816 #==========================================================================
1817 1817
1818 1818 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1819 1819 return get_commit_safe(
1820 1820 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1821 1821
1822 1822 def get_changeset(self, rev=None, pre_load=None):
1823 1823 warnings.warn("Use get_commit", DeprecationWarning)
1824 1824 commit_id = None
1825 1825 commit_idx = None
1826 1826 if isinstance(rev, basestring):
1827 1827 commit_id = rev
1828 1828 else:
1829 1829 commit_idx = rev
1830 1830 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1831 1831 pre_load=pre_load)
1832 1832
1833 1833 def get_landing_commit(self):
1834 1834 """
1835 1835 Returns landing commit, or if that doesn't exist returns the tip
1836 1836 """
1837 1837 _rev_type, _rev = self.landing_rev
1838 1838 commit = self.get_commit(_rev)
1839 1839 if isinstance(commit, EmptyCommit):
1840 1840 return self.get_commit()
1841 1841 return commit
1842 1842
1843 1843 def update_commit_cache(self, cs_cache=None, config=None):
1844 1844 """
1845 1845 Update cache of last changeset for repository, keys should be::
1846 1846
1847 1847 short_id
1848 1848 raw_id
1849 1849 revision
1850 1850 parents
1851 1851 message
1852 1852 date
1853 1853 author
1854 1854
1855 1855 :param cs_cache:
1856 1856 """
1857 1857 from rhodecode.lib.vcs.backends.base import BaseChangeset
1858 1858 if cs_cache is None:
1859 1859 # use no-cache version here
1860 1860 scm_repo = self.scm_instance(cache=False, config=config)
1861 1861 if scm_repo:
1862 1862 cs_cache = scm_repo.get_commit(
1863 1863 pre_load=["author", "date", "message", "parents"])
1864 1864 else:
1865 1865 cs_cache = EmptyCommit()
1866 1866
1867 1867 if isinstance(cs_cache, BaseChangeset):
1868 1868 cs_cache = cs_cache.__json__()
1869 1869
1870 1870 def is_outdated(new_cs_cache):
1871 1871 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1872 1872 new_cs_cache['revision'] != self.changeset_cache['revision']):
1873 1873 return True
1874 1874 return False
1875 1875
1876 1876 # check if we have maybe already latest cached revision
1877 1877 if is_outdated(cs_cache) or not self.changeset_cache:
1878 1878 _default = datetime.datetime.fromtimestamp(0)
1879 1879 last_change = cs_cache.get('date') or _default
1880 1880 log.debug('updated repo %s with new cs cache %s',
1881 1881 self.repo_name, cs_cache)
1882 1882 self.updated_on = last_change
1883 1883 self.changeset_cache = cs_cache
1884 1884 Session().add(self)
1885 1885 Session().commit()
1886 1886 else:
1887 1887 log.debug('Skipping update_commit_cache for repo:`%s` '
1888 1888 'commit already with latest changes', self.repo_name)
1889 1889
1890 1890 @property
1891 1891 def tip(self):
1892 1892 return self.get_commit('tip')
1893 1893
1894 1894 @property
1895 1895 def author(self):
1896 1896 return self.tip.author
1897 1897
1898 1898 @property
1899 1899 def last_change(self):
1900 1900 return self.scm_instance().last_change
1901 1901
1902 1902 def get_comments(self, revisions=None):
1903 1903 """
1904 1904 Returns comments for this repository grouped by revisions
1905 1905
1906 1906 :param revisions: filter query by revisions only
1907 1907 """
1908 1908 cmts = ChangesetComment.query()\
1909 1909 .filter(ChangesetComment.repo == self)
1910 1910 if revisions:
1911 1911 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1912 1912 grouped = collections.defaultdict(list)
1913 1913 for cmt in cmts.all():
1914 1914 grouped[cmt.revision].append(cmt)
1915 1915 return grouped
1916 1916
1917 1917 def statuses(self, revisions=None):
1918 1918 """
1919 1919 Returns statuses for this repository
1920 1920
1921 1921 :param revisions: list of revisions to get statuses for
1922 1922 """
1923 1923 statuses = ChangesetStatus.query()\
1924 1924 .filter(ChangesetStatus.repo == self)\
1925 1925 .filter(ChangesetStatus.version == 0)
1926 1926
1927 1927 if revisions:
1928 1928 # Try doing the filtering in chunks to avoid hitting limits
1929 1929 size = 500
1930 1930 status_results = []
1931 1931 for chunk in xrange(0, len(revisions), size):
1932 1932 status_results += statuses.filter(
1933 1933 ChangesetStatus.revision.in_(
1934 1934 revisions[chunk: chunk+size])
1935 1935 ).all()
1936 1936 else:
1937 1937 status_results = statuses.all()
1938 1938
1939 1939 grouped = {}
1940 1940
1941 1941 # maybe we have open new pullrequest without a status?
1942 1942 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1943 1943 status_lbl = ChangesetStatus.get_status_lbl(stat)
1944 1944 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1945 1945 for rev in pr.revisions:
1946 1946 pr_id = pr.pull_request_id
1947 1947 pr_repo = pr.target_repo.repo_name
1948 1948 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1949 1949
1950 1950 for stat in status_results:
1951 1951 pr_id = pr_repo = None
1952 1952 if stat.pull_request:
1953 1953 pr_id = stat.pull_request.pull_request_id
1954 1954 pr_repo = stat.pull_request.target_repo.repo_name
1955 1955 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1956 1956 pr_id, pr_repo]
1957 1957 return grouped
1958 1958
1959 1959 # ==========================================================================
1960 1960 # SCM CACHE INSTANCE
1961 1961 # ==========================================================================
1962 1962
1963 1963 def scm_instance(self, **kwargs):
1964 1964 import rhodecode
1965 1965
1966 1966 # Passing a config will not hit the cache currently only used
1967 1967 # for repo2dbmapper
1968 1968 config = kwargs.pop('config', None)
1969 1969 cache = kwargs.pop('cache', None)
1970 1970 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1971 1971 # if cache is NOT defined use default global, else we have a full
1972 1972 # control over cache behaviour
1973 1973 if cache is None and full_cache and not config:
1974 1974 return self._get_instance_cached()
1975 1975 return self._get_instance(cache=bool(cache), config=config)
1976 1976
1977 1977 def _get_instance_cached(self):
1978 1978 @cache_region('long_term')
1979 1979 def _get_repo(cache_key):
1980 1980 return self._get_instance()
1981 1981
1982 1982 invalidator_context = CacheKey.repo_context_cache(
1983 1983 _get_repo, self.repo_name, None, thread_scoped=True)
1984 1984
1985 1985 with invalidator_context as context:
1986 1986 context.invalidate()
1987 1987 repo = context.compute()
1988 1988
1989 1989 return repo
1990 1990
1991 1991 def _get_instance(self, cache=True, config=None):
1992 1992 config = config or self._config
1993 1993 custom_wire = {
1994 1994 'cache': cache # controls the vcs.remote cache
1995 1995 }
1996 1996 repo = get_vcs_instance(
1997 1997 repo_path=safe_str(self.repo_full_path),
1998 1998 config=config,
1999 1999 with_wire=custom_wire,
2000 2000 create=False,
2001 2001 _vcs_alias=self.repo_type)
2002 2002
2003 2003 return repo
2004 2004
2005 2005 def __json__(self):
2006 2006 return {'landing_rev': self.landing_rev}
2007 2007
2008 2008 def get_dict(self):
2009 2009
2010 2010 # Since we transformed `repo_name` to a hybrid property, we need to
2011 2011 # keep compatibility with the code which uses `repo_name` field.
2012 2012
2013 2013 result = super(Repository, self).get_dict()
2014 2014 result['repo_name'] = result.pop('_repo_name', None)
2015 2015 return result
2016 2016
2017 2017
2018 2018 class RepoGroup(Base, BaseModel):
2019 2019 __tablename__ = 'groups'
2020 2020 __table_args__ = (
2021 2021 UniqueConstraint('group_name', 'group_parent_id'),
2022 2022 CheckConstraint('group_id != group_parent_id'),
2023 2023 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2024 2024 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2025 2025 )
2026 2026 __mapper_args__ = {'order_by': 'group_name'}
2027 2027
2028 2028 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2029 2029
2030 2030 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2031 2031 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2032 2032 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2033 2033 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2034 2034 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2035 2035 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2036 2036 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2037 2037 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2038 2038
2039 2039 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2040 2040 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2041 2041 parent_group = relationship('RepoGroup', remote_side=group_id)
2042 2042 user = relationship('User')
2043 2043 integrations = relationship('Integration',
2044 2044 cascade="all, delete, delete-orphan")
2045 2045
2046 2046 def __init__(self, group_name='', parent_group=None):
2047 2047 self.group_name = group_name
2048 2048 self.parent_group = parent_group
2049 2049
2050 2050 def __unicode__(self):
2051 2051 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2052 2052 self.group_name)
2053 2053
2054 2054 @classmethod
2055 2055 def _generate_choice(cls, repo_group):
2056 2056 from webhelpers.html import literal as _literal
2057 2057 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2058 2058 return repo_group.group_id, _name(repo_group.full_path_splitted)
2059 2059
2060 2060 @classmethod
2061 2061 def groups_choices(cls, groups=None, show_empty_group=True):
2062 2062 if not groups:
2063 2063 groups = cls.query().all()
2064 2064
2065 2065 repo_groups = []
2066 2066 if show_empty_group:
2067 2067 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2068 2068
2069 2069 repo_groups.extend([cls._generate_choice(x) for x in groups])
2070 2070
2071 2071 repo_groups = sorted(
2072 2072 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2073 2073 return repo_groups
2074 2074
2075 2075 @classmethod
2076 2076 def url_sep(cls):
2077 2077 return URL_SEP
2078 2078
2079 2079 @classmethod
2080 2080 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2081 2081 if case_insensitive:
2082 2082 gr = cls.query().filter(func.lower(cls.group_name)
2083 2083 == func.lower(group_name))
2084 2084 else:
2085 2085 gr = cls.query().filter(cls.group_name == group_name)
2086 2086 if cache:
2087 2087 gr = gr.options(FromCache(
2088 2088 "sql_cache_short",
2089 2089 "get_group_%s" % _hash_key(group_name)))
2090 2090 return gr.scalar()
2091 2091
2092 2092 @classmethod
2093 2093 def get_user_personal_repo_group(cls, user_id):
2094 2094 user = User.get(user_id)
2095 2095 return cls.query()\
2096 2096 .filter(cls.personal == true())\
2097 2097 .filter(cls.user == user).scalar()
2098 2098
2099 2099 @classmethod
2100 2100 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2101 2101 case_insensitive=True):
2102 2102 q = RepoGroup.query()
2103 2103
2104 2104 if not isinstance(user_id, Optional):
2105 2105 q = q.filter(RepoGroup.user_id == user_id)
2106 2106
2107 2107 if not isinstance(group_id, Optional):
2108 2108 q = q.filter(RepoGroup.group_parent_id == group_id)
2109 2109
2110 2110 if case_insensitive:
2111 2111 q = q.order_by(func.lower(RepoGroup.group_name))
2112 2112 else:
2113 2113 q = q.order_by(RepoGroup.group_name)
2114 2114 return q.all()
2115 2115
2116 2116 @property
2117 2117 def parents(self):
2118 2118 parents_recursion_limit = 10
2119 2119 groups = []
2120 2120 if self.parent_group is None:
2121 2121 return groups
2122 2122 cur_gr = self.parent_group
2123 2123 groups.insert(0, cur_gr)
2124 2124 cnt = 0
2125 2125 while 1:
2126 2126 cnt += 1
2127 2127 gr = getattr(cur_gr, 'parent_group', None)
2128 2128 cur_gr = cur_gr.parent_group
2129 2129 if gr is None:
2130 2130 break
2131 2131 if cnt == parents_recursion_limit:
2132 2132 # this will prevent accidental infinit loops
2133 2133 log.error(('more than %s parents found for group %s, stopping '
2134 2134 'recursive parent fetching' % (parents_recursion_limit, self)))
2135 2135 break
2136 2136
2137 2137 groups.insert(0, gr)
2138 2138 return groups
2139 2139
2140 2140 @property
2141 2141 def children(self):
2142 2142 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2143 2143
2144 2144 @property
2145 2145 def name(self):
2146 2146 return self.group_name.split(RepoGroup.url_sep())[-1]
2147 2147
2148 2148 @property
2149 2149 def full_path(self):
2150 2150 return self.group_name
2151 2151
2152 2152 @property
2153 2153 def full_path_splitted(self):
2154 2154 return self.group_name.split(RepoGroup.url_sep())
2155 2155
2156 2156 @property
2157 2157 def repositories(self):
2158 2158 return Repository.query()\
2159 2159 .filter(Repository.group == self)\
2160 2160 .order_by(Repository.repo_name)
2161 2161
2162 2162 @property
2163 2163 def repositories_recursive_count(self):
2164 2164 cnt = self.repositories.count()
2165 2165
2166 2166 def children_count(group):
2167 2167 cnt = 0
2168 2168 for child in group.children:
2169 2169 cnt += child.repositories.count()
2170 2170 cnt += children_count(child)
2171 2171 return cnt
2172 2172
2173 2173 return cnt + children_count(self)
2174 2174
2175 2175 def _recursive_objects(self, include_repos=True):
2176 2176 all_ = []
2177 2177
2178 2178 def _get_members(root_gr):
2179 2179 if include_repos:
2180 2180 for r in root_gr.repositories:
2181 2181 all_.append(r)
2182 2182 childs = root_gr.children.all()
2183 2183 if childs:
2184 2184 for gr in childs:
2185 2185 all_.append(gr)
2186 2186 _get_members(gr)
2187 2187
2188 2188 _get_members(self)
2189 2189 return [self] + all_
2190 2190
2191 2191 def recursive_groups_and_repos(self):
2192 2192 """
2193 2193 Recursive return all groups, with repositories in those groups
2194 2194 """
2195 2195 return self._recursive_objects()
2196 2196
2197 2197 def recursive_groups(self):
2198 2198 """
2199 2199 Returns all children groups for this group including children of children
2200 2200 """
2201 2201 return self._recursive_objects(include_repos=False)
2202 2202
2203 2203 def get_new_name(self, group_name):
2204 2204 """
2205 2205 returns new full group name based on parent and new name
2206 2206
2207 2207 :param group_name:
2208 2208 """
2209 2209 path_prefix = (self.parent_group.full_path_splitted if
2210 2210 self.parent_group else [])
2211 2211 return RepoGroup.url_sep().join(path_prefix + [group_name])
2212 2212
2213 2213 def permissions(self, with_admins=True, with_owner=True):
2214 2214 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2215 2215 q = q.options(joinedload(UserRepoGroupToPerm.group),
2216 2216 joinedload(UserRepoGroupToPerm.user),
2217 2217 joinedload(UserRepoGroupToPerm.permission),)
2218 2218
2219 2219 # get owners and admins and permissions. We do a trick of re-writing
2220 2220 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2221 2221 # has a global reference and changing one object propagates to all
2222 2222 # others. This means if admin is also an owner admin_row that change
2223 2223 # would propagate to both objects
2224 2224 perm_rows = []
2225 2225 for _usr in q.all():
2226 2226 usr = AttributeDict(_usr.user.get_dict())
2227 2227 usr.permission = _usr.permission.permission_name
2228 2228 perm_rows.append(usr)
2229 2229
2230 2230 # filter the perm rows by 'default' first and then sort them by
2231 2231 # admin,write,read,none permissions sorted again alphabetically in
2232 2232 # each group
2233 2233 perm_rows = sorted(perm_rows, key=display_sort)
2234 2234
2235 2235 _admin_perm = 'group.admin'
2236 2236 owner_row = []
2237 2237 if with_owner:
2238 2238 usr = AttributeDict(self.user.get_dict())
2239 2239 usr.owner_row = True
2240 2240 usr.permission = _admin_perm
2241 2241 owner_row.append(usr)
2242 2242
2243 2243 super_admin_rows = []
2244 2244 if with_admins:
2245 2245 for usr in User.get_all_super_admins():
2246 2246 # if this admin is also owner, don't double the record
2247 2247 if usr.user_id == owner_row[0].user_id:
2248 2248 owner_row[0].admin_row = True
2249 2249 else:
2250 2250 usr = AttributeDict(usr.get_dict())
2251 2251 usr.admin_row = True
2252 2252 usr.permission = _admin_perm
2253 2253 super_admin_rows.append(usr)
2254 2254
2255 2255 return super_admin_rows + owner_row + perm_rows
2256 2256
2257 2257 def permission_user_groups(self):
2258 2258 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2259 2259 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2260 2260 joinedload(UserGroupRepoGroupToPerm.users_group),
2261 2261 joinedload(UserGroupRepoGroupToPerm.permission),)
2262 2262
2263 2263 perm_rows = []
2264 2264 for _user_group in q.all():
2265 2265 usr = AttributeDict(_user_group.users_group.get_dict())
2266 2266 usr.permission = _user_group.permission.permission_name
2267 2267 perm_rows.append(usr)
2268 2268
2269 2269 return perm_rows
2270 2270
2271 2271 def get_api_data(self):
2272 2272 """
2273 2273 Common function for generating api data
2274 2274
2275 2275 """
2276 2276 group = self
2277 2277 data = {
2278 2278 'group_id': group.group_id,
2279 2279 'group_name': group.group_name,
2280 2280 'group_description': group.group_description,
2281 2281 'parent_group': group.parent_group.group_name if group.parent_group else None,
2282 2282 'repositories': [x.repo_name for x in group.repositories],
2283 2283 'owner': group.user.username,
2284 2284 }
2285 2285 return data
2286 2286
2287 2287
2288 2288 class Permission(Base, BaseModel):
2289 2289 __tablename__ = 'permissions'
2290 2290 __table_args__ = (
2291 2291 Index('p_perm_name_idx', 'permission_name'),
2292 2292 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2293 2293 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2294 2294 )
2295 2295 PERMS = [
2296 2296 ('hg.admin', _('RhodeCode Super Administrator')),
2297 2297
2298 2298 ('repository.none', _('Repository no access')),
2299 2299 ('repository.read', _('Repository read access')),
2300 2300 ('repository.write', _('Repository write access')),
2301 2301 ('repository.admin', _('Repository admin access')),
2302 2302
2303 2303 ('group.none', _('Repository group no access')),
2304 2304 ('group.read', _('Repository group read access')),
2305 2305 ('group.write', _('Repository group write access')),
2306 2306 ('group.admin', _('Repository group admin access')),
2307 2307
2308 2308 ('usergroup.none', _('User group no access')),
2309 2309 ('usergroup.read', _('User group read access')),
2310 2310 ('usergroup.write', _('User group write access')),
2311 2311 ('usergroup.admin', _('User group admin access')),
2312 2312
2313 2313 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2314 2314 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2315 2315
2316 2316 ('hg.usergroup.create.false', _('User Group creation disabled')),
2317 2317 ('hg.usergroup.create.true', _('User Group creation enabled')),
2318 2318
2319 2319 ('hg.create.none', _('Repository creation disabled')),
2320 2320 ('hg.create.repository', _('Repository creation enabled')),
2321 2321 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2322 2322 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2323 2323
2324 2324 ('hg.fork.none', _('Repository forking disabled')),
2325 2325 ('hg.fork.repository', _('Repository forking enabled')),
2326 2326
2327 2327 ('hg.register.none', _('Registration disabled')),
2328 2328 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2329 2329 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2330 2330
2331 2331 ('hg.password_reset.enabled', _('Password reset enabled')),
2332 2332 ('hg.password_reset.hidden', _('Password reset hidden')),
2333 2333 ('hg.password_reset.disabled', _('Password reset disabled')),
2334 2334
2335 2335 ('hg.extern_activate.manual', _('Manual activation of external account')),
2336 2336 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2337 2337
2338 2338 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2339 2339 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2340 2340 ]
2341 2341
2342 2342 # definition of system default permissions for DEFAULT user
2343 2343 DEFAULT_USER_PERMISSIONS = [
2344 2344 'repository.read',
2345 2345 'group.read',
2346 2346 'usergroup.read',
2347 2347 'hg.create.repository',
2348 2348 'hg.repogroup.create.false',
2349 2349 'hg.usergroup.create.false',
2350 2350 'hg.create.write_on_repogroup.true',
2351 2351 'hg.fork.repository',
2352 2352 'hg.register.manual_activate',
2353 2353 'hg.password_reset.enabled',
2354 2354 'hg.extern_activate.auto',
2355 2355 'hg.inherit_default_perms.true',
2356 2356 ]
2357 2357
2358 2358 # defines which permissions are more important higher the more important
2359 2359 # Weight defines which permissions are more important.
2360 2360 # The higher number the more important.
2361 2361 PERM_WEIGHTS = {
2362 2362 'repository.none': 0,
2363 2363 'repository.read': 1,
2364 2364 'repository.write': 3,
2365 2365 'repository.admin': 4,
2366 2366
2367 2367 'group.none': 0,
2368 2368 'group.read': 1,
2369 2369 'group.write': 3,
2370 2370 'group.admin': 4,
2371 2371
2372 2372 'usergroup.none': 0,
2373 2373 'usergroup.read': 1,
2374 2374 'usergroup.write': 3,
2375 2375 'usergroup.admin': 4,
2376 2376
2377 2377 'hg.repogroup.create.false': 0,
2378 2378 'hg.repogroup.create.true': 1,
2379 2379
2380 2380 'hg.usergroup.create.false': 0,
2381 2381 'hg.usergroup.create.true': 1,
2382 2382
2383 2383 'hg.fork.none': 0,
2384 2384 'hg.fork.repository': 1,
2385 2385 'hg.create.none': 0,
2386 2386 'hg.create.repository': 1
2387 2387 }
2388 2388
2389 2389 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2390 2390 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2391 2391 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2392 2392
2393 2393 def __unicode__(self):
2394 2394 return u"<%s('%s:%s')>" % (
2395 2395 self.__class__.__name__, self.permission_id, self.permission_name
2396 2396 )
2397 2397
2398 2398 @classmethod
2399 2399 def get_by_key(cls, key):
2400 2400 return cls.query().filter(cls.permission_name == key).scalar()
2401 2401
2402 2402 @classmethod
2403 2403 def get_default_repo_perms(cls, user_id, repo_id=None):
2404 2404 q = Session().query(UserRepoToPerm, Repository, Permission)\
2405 2405 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2406 2406 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2407 2407 .filter(UserRepoToPerm.user_id == user_id)
2408 2408 if repo_id:
2409 2409 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2410 2410 return q.all()
2411 2411
2412 2412 @classmethod
2413 2413 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2414 2414 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2415 2415 .join(
2416 2416 Permission,
2417 2417 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2418 2418 .join(
2419 2419 Repository,
2420 2420 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2421 2421 .join(
2422 2422 UserGroup,
2423 2423 UserGroupRepoToPerm.users_group_id ==
2424 2424 UserGroup.users_group_id)\
2425 2425 .join(
2426 2426 UserGroupMember,
2427 2427 UserGroupRepoToPerm.users_group_id ==
2428 2428 UserGroupMember.users_group_id)\
2429 2429 .filter(
2430 2430 UserGroupMember.user_id == user_id,
2431 2431 UserGroup.users_group_active == true())
2432 2432 if repo_id:
2433 2433 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2434 2434 return q.all()
2435 2435
2436 2436 @classmethod
2437 2437 def get_default_group_perms(cls, user_id, repo_group_id=None):
2438 2438 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2439 2439 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2440 2440 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2441 2441 .filter(UserRepoGroupToPerm.user_id == user_id)
2442 2442 if repo_group_id:
2443 2443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2444 2444 return q.all()
2445 2445
2446 2446 @classmethod
2447 2447 def get_default_group_perms_from_user_group(
2448 2448 cls, user_id, repo_group_id=None):
2449 2449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2450 2450 .join(
2451 2451 Permission,
2452 2452 UserGroupRepoGroupToPerm.permission_id ==
2453 2453 Permission.permission_id)\
2454 2454 .join(
2455 2455 RepoGroup,
2456 2456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2457 2457 .join(
2458 2458 UserGroup,
2459 2459 UserGroupRepoGroupToPerm.users_group_id ==
2460 2460 UserGroup.users_group_id)\
2461 2461 .join(
2462 2462 UserGroupMember,
2463 2463 UserGroupRepoGroupToPerm.users_group_id ==
2464 2464 UserGroupMember.users_group_id)\
2465 2465 .filter(
2466 2466 UserGroupMember.user_id == user_id,
2467 2467 UserGroup.users_group_active == true())
2468 2468 if repo_group_id:
2469 2469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2470 2470 return q.all()
2471 2471
2472 2472 @classmethod
2473 2473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2474 2474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2475 2475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2476 2476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2477 2477 .filter(UserUserGroupToPerm.user_id == user_id)
2478 2478 if user_group_id:
2479 2479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2480 2480 return q.all()
2481 2481
2482 2482 @classmethod
2483 2483 def get_default_user_group_perms_from_user_group(
2484 2484 cls, user_id, user_group_id=None):
2485 2485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2486 2486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2487 2487 .join(
2488 2488 Permission,
2489 2489 UserGroupUserGroupToPerm.permission_id ==
2490 2490 Permission.permission_id)\
2491 2491 .join(
2492 2492 TargetUserGroup,
2493 2493 UserGroupUserGroupToPerm.target_user_group_id ==
2494 2494 TargetUserGroup.users_group_id)\
2495 2495 .join(
2496 2496 UserGroup,
2497 2497 UserGroupUserGroupToPerm.user_group_id ==
2498 2498 UserGroup.users_group_id)\
2499 2499 .join(
2500 2500 UserGroupMember,
2501 2501 UserGroupUserGroupToPerm.user_group_id ==
2502 2502 UserGroupMember.users_group_id)\
2503 2503 .filter(
2504 2504 UserGroupMember.user_id == user_id,
2505 2505 UserGroup.users_group_active == true())
2506 2506 if user_group_id:
2507 2507 q = q.filter(
2508 2508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2509 2509
2510 2510 return q.all()
2511 2511
2512 2512
2513 2513 class UserRepoToPerm(Base, BaseModel):
2514 2514 __tablename__ = 'repo_to_perm'
2515 2515 __table_args__ = (
2516 2516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2517 2517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2518 2518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2519 2519 )
2520 2520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2521 2521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2522 2522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2523 2523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2524 2524
2525 2525 user = relationship('User')
2526 2526 repository = relationship('Repository')
2527 2527 permission = relationship('Permission')
2528 2528
2529 2529 @classmethod
2530 2530 def create(cls, user, repository, permission):
2531 2531 n = cls()
2532 2532 n.user = user
2533 2533 n.repository = repository
2534 2534 n.permission = permission
2535 2535 Session().add(n)
2536 2536 return n
2537 2537
2538 2538 def __unicode__(self):
2539 2539 return u'<%s => %s >' % (self.user, self.repository)
2540 2540
2541 2541
2542 2542 class UserUserGroupToPerm(Base, BaseModel):
2543 2543 __tablename__ = 'user_user_group_to_perm'
2544 2544 __table_args__ = (
2545 2545 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2546 2546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2547 2547 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2548 2548 )
2549 2549 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2550 2550 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2551 2551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2552 2552 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2553 2553
2554 2554 user = relationship('User')
2555 2555 user_group = relationship('UserGroup')
2556 2556 permission = relationship('Permission')
2557 2557
2558 2558 @classmethod
2559 2559 def create(cls, user, user_group, permission):
2560 2560 n = cls()
2561 2561 n.user = user
2562 2562 n.user_group = user_group
2563 2563 n.permission = permission
2564 2564 Session().add(n)
2565 2565 return n
2566 2566
2567 2567 def __unicode__(self):
2568 2568 return u'<%s => %s >' % (self.user, self.user_group)
2569 2569
2570 2570
2571 2571 class UserToPerm(Base, BaseModel):
2572 2572 __tablename__ = 'user_to_perm'
2573 2573 __table_args__ = (
2574 2574 UniqueConstraint('user_id', 'permission_id'),
2575 2575 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2576 2576 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2577 2577 )
2578 2578 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2579 2579 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2580 2580 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2581 2581
2582 2582 user = relationship('User')
2583 2583 permission = relationship('Permission', lazy='joined')
2584 2584
2585 2585 def __unicode__(self):
2586 2586 return u'<%s => %s >' % (self.user, self.permission)
2587 2587
2588 2588
2589 2589 class UserGroupRepoToPerm(Base, BaseModel):
2590 2590 __tablename__ = 'users_group_repo_to_perm'
2591 2591 __table_args__ = (
2592 2592 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2593 2593 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2594 2594 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2595 2595 )
2596 2596 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2597 2597 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2598 2598 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2599 2599 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2600 2600
2601 2601 users_group = relationship('UserGroup')
2602 2602 permission = relationship('Permission')
2603 2603 repository = relationship('Repository')
2604 2604
2605 2605 @classmethod
2606 2606 def create(cls, users_group, repository, permission):
2607 2607 n = cls()
2608 2608 n.users_group = users_group
2609 2609 n.repository = repository
2610 2610 n.permission = permission
2611 2611 Session().add(n)
2612 2612 return n
2613 2613
2614 2614 def __unicode__(self):
2615 2615 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2616 2616
2617 2617
2618 2618 class UserGroupUserGroupToPerm(Base, BaseModel):
2619 2619 __tablename__ = 'user_group_user_group_to_perm'
2620 2620 __table_args__ = (
2621 2621 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2622 2622 CheckConstraint('target_user_group_id != user_group_id'),
2623 2623 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2624 2624 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2625 2625 )
2626 2626 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2627 2627 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2628 2628 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2629 2629 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2630 2630
2631 2631 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2632 2632 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2633 2633 permission = relationship('Permission')
2634 2634
2635 2635 @classmethod
2636 2636 def create(cls, target_user_group, user_group, permission):
2637 2637 n = cls()
2638 2638 n.target_user_group = target_user_group
2639 2639 n.user_group = user_group
2640 2640 n.permission = permission
2641 2641 Session().add(n)
2642 2642 return n
2643 2643
2644 2644 def __unicode__(self):
2645 2645 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2646 2646
2647 2647
2648 2648 class UserGroupToPerm(Base, BaseModel):
2649 2649 __tablename__ = 'users_group_to_perm'
2650 2650 __table_args__ = (
2651 2651 UniqueConstraint('users_group_id', 'permission_id',),
2652 2652 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2653 2653 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2654 2654 )
2655 2655 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2656 2656 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2657 2657 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2658 2658
2659 2659 users_group = relationship('UserGroup')
2660 2660 permission = relationship('Permission')
2661 2661
2662 2662
2663 2663 class UserRepoGroupToPerm(Base, BaseModel):
2664 2664 __tablename__ = 'user_repo_group_to_perm'
2665 2665 __table_args__ = (
2666 2666 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2667 2667 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2668 2668 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2669 2669 )
2670 2670
2671 2671 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2672 2672 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2673 2673 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2674 2674 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2675 2675
2676 2676 user = relationship('User')
2677 2677 group = relationship('RepoGroup')
2678 2678 permission = relationship('Permission')
2679 2679
2680 2680 @classmethod
2681 2681 def create(cls, user, repository_group, permission):
2682 2682 n = cls()
2683 2683 n.user = user
2684 2684 n.group = repository_group
2685 2685 n.permission = permission
2686 2686 Session().add(n)
2687 2687 return n
2688 2688
2689 2689
2690 2690 class UserGroupRepoGroupToPerm(Base, BaseModel):
2691 2691 __tablename__ = 'users_group_repo_group_to_perm'
2692 2692 __table_args__ = (
2693 2693 UniqueConstraint('users_group_id', 'group_id'),
2694 2694 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2695 2695 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2696 2696 )
2697 2697
2698 2698 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2699 2699 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2700 2700 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2701 2701 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2702 2702
2703 2703 users_group = relationship('UserGroup')
2704 2704 permission = relationship('Permission')
2705 2705 group = relationship('RepoGroup')
2706 2706
2707 2707 @classmethod
2708 2708 def create(cls, user_group, repository_group, permission):
2709 2709 n = cls()
2710 2710 n.users_group = user_group
2711 2711 n.group = repository_group
2712 2712 n.permission = permission
2713 2713 Session().add(n)
2714 2714 return n
2715 2715
2716 2716 def __unicode__(self):
2717 2717 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2718 2718
2719 2719
2720 2720 class Statistics(Base, BaseModel):
2721 2721 __tablename__ = 'statistics'
2722 2722 __table_args__ = (
2723 2723 UniqueConstraint('repository_id'),
2724 2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2725 2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2726 2726 )
2727 2727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2728 2728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2729 2729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2730 2730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2731 2731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2732 2732 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2733 2733
2734 2734 repository = relationship('Repository', single_parent=True)
2735 2735
2736 2736
2737 2737 class UserFollowing(Base, BaseModel):
2738 2738 __tablename__ = 'user_followings'
2739 2739 __table_args__ = (
2740 2740 UniqueConstraint('user_id', 'follows_repository_id'),
2741 2741 UniqueConstraint('user_id', 'follows_user_id'),
2742 2742 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2743 2743 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2744 2744 )
2745 2745
2746 2746 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2747 2747 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2748 2748 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2749 2749 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2750 2750 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2751 2751
2752 2752 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2753 2753
2754 2754 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2755 2755 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2756 2756
2757 2757 @classmethod
2758 2758 def get_repo_followers(cls, repo_id):
2759 2759 return cls.query().filter(cls.follows_repo_id == repo_id)
2760 2760
2761 2761
2762 2762 class CacheKey(Base, BaseModel):
2763 2763 __tablename__ = 'cache_invalidation'
2764 2764 __table_args__ = (
2765 2765 UniqueConstraint('cache_key'),
2766 2766 Index('key_idx', 'cache_key'),
2767 2767 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2768 2768 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2769 2769 )
2770 2770 CACHE_TYPE_ATOM = 'ATOM'
2771 2771 CACHE_TYPE_RSS = 'RSS'
2772 2772 CACHE_TYPE_README = 'README'
2773 2773
2774 2774 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2775 2775 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2776 2776 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2777 2777 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2778 2778
2779 2779 def __init__(self, cache_key, cache_args=''):
2780 2780 self.cache_key = cache_key
2781 2781 self.cache_args = cache_args
2782 2782 self.cache_active = False
2783 2783
2784 2784 def __unicode__(self):
2785 2785 return u"<%s('%s:%s[%s]')>" % (
2786 2786 self.__class__.__name__,
2787 2787 self.cache_id, self.cache_key, self.cache_active)
2788 2788
2789 2789 def _cache_key_partition(self):
2790 2790 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2791 2791 return prefix, repo_name, suffix
2792 2792
2793 2793 def get_prefix(self):
2794 2794 """
2795 2795 Try to extract prefix from existing cache key. The key could consist
2796 2796 of prefix, repo_name, suffix
2797 2797 """
2798 2798 # this returns prefix, repo_name, suffix
2799 2799 return self._cache_key_partition()[0]
2800 2800
2801 2801 def get_suffix(self):
2802 2802 """
2803 2803 get suffix that might have been used in _get_cache_key to
2804 2804 generate self.cache_key. Only used for informational purposes
2805 2805 in repo_edit.mako.
2806 2806 """
2807 2807 # prefix, repo_name, suffix
2808 2808 return self._cache_key_partition()[2]
2809 2809
2810 2810 @classmethod
2811 2811 def delete_all_cache(cls):
2812 2812 """
2813 2813 Delete all cache keys from database.
2814 2814 Should only be run when all instances are down and all entries
2815 2815 thus stale.
2816 2816 """
2817 2817 cls.query().delete()
2818 2818 Session().commit()
2819 2819
2820 2820 @classmethod
2821 2821 def get_cache_key(cls, repo_name, cache_type):
2822 2822 """
2823 2823
2824 2824 Generate a cache key for this process of RhodeCode instance.
2825 2825 Prefix most likely will be process id or maybe explicitly set
2826 2826 instance_id from .ini file.
2827 2827 """
2828 2828 import rhodecode
2829 2829 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2830 2830
2831 2831 repo_as_unicode = safe_unicode(repo_name)
2832 2832 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2833 2833 if cache_type else repo_as_unicode
2834 2834
2835 2835 return u'{}{}'.format(prefix, key)
2836 2836
2837 2837 @classmethod
2838 2838 def set_invalidate(cls, repo_name, delete=False):
2839 2839 """
2840 2840 Mark all caches of a repo as invalid in the database.
2841 2841 """
2842 2842
2843 2843 try:
2844 2844 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2845 2845 if delete:
2846 2846 log.debug('cache objects deleted for repo %s',
2847 2847 safe_str(repo_name))
2848 2848 qry.delete()
2849 2849 else:
2850 2850 log.debug('cache objects marked as invalid for repo %s',
2851 2851 safe_str(repo_name))
2852 2852 qry.update({"cache_active": False})
2853 2853
2854 2854 Session().commit()
2855 2855 except Exception:
2856 2856 log.exception(
2857 2857 'Cache key invalidation failed for repository %s',
2858 2858 safe_str(repo_name))
2859 2859 Session().rollback()
2860 2860
2861 2861 @classmethod
2862 2862 def get_active_cache(cls, cache_key):
2863 2863 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2864 2864 if inv_obj:
2865 2865 return inv_obj
2866 2866 return None
2867 2867
2868 2868 @classmethod
2869 2869 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2870 2870 thread_scoped=False):
2871 2871 """
2872 2872 @cache_region('long_term')
2873 2873 def _heavy_calculation(cache_key):
2874 2874 return 'result'
2875 2875
2876 2876 cache_context = CacheKey.repo_context_cache(
2877 2877 _heavy_calculation, repo_name, cache_type)
2878 2878
2879 2879 with cache_context as context:
2880 2880 context.invalidate()
2881 2881 computed = context.compute()
2882 2882
2883 2883 assert computed == 'result'
2884 2884 """
2885 2885 from rhodecode.lib import caches
2886 2886 return caches.InvalidationContext(
2887 2887 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2888 2888
2889 2889
2890 2890 class ChangesetComment(Base, BaseModel):
2891 2891 __tablename__ = 'changeset_comments'
2892 2892 __table_args__ = (
2893 2893 Index('cc_revision_idx', 'revision'),
2894 2894 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2895 2895 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2896 2896 )
2897 2897
2898 2898 COMMENT_OUTDATED = u'comment_outdated'
2899 COMMENT_TYPE_NOTE = u'note'
2900 COMMENT_TYPE_TODO = u'todo'
2901 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2899 2902
2900 2903 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2901 2904 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2902 2905 revision = Column('revision', String(40), nullable=True)
2903 2906 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2904 2907 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2905 2908 line_no = Column('line_no', Unicode(10), nullable=True)
2906 2909 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2907 2910 f_path = Column('f_path', Unicode(1000), nullable=True)
2908 2911 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2909 2912 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2910 2913 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2911 2914 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2912 2915 renderer = Column('renderer', Unicode(64), nullable=True)
2913 2916 display_state = Column('display_state', Unicode(128), nullable=True)
2914 2917
2918 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
2919 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
2920 resolved_comment = relationship('ChangesetComment', remote_side=comment_id)
2915 2921 author = relationship('User', lazy='joined')
2916 2922 repo = relationship('Repository')
2917 2923 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2918 2924 pull_request = relationship('PullRequest', lazy='joined')
2919 2925 pull_request_version = relationship('PullRequestVersion')
2920 2926
2921 2927 @classmethod
2922 2928 def get_users(cls, revision=None, pull_request_id=None):
2923 2929 """
2924 2930 Returns user associated with this ChangesetComment. ie those
2925 2931 who actually commented
2926 2932
2927 2933 :param cls:
2928 2934 :param revision:
2929 2935 """
2930 2936 q = Session().query(User)\
2931 2937 .join(ChangesetComment.author)
2932 2938 if revision:
2933 2939 q = q.filter(cls.revision == revision)
2934 2940 elif pull_request_id:
2935 2941 q = q.filter(cls.pull_request_id == pull_request_id)
2936 2942 return q.all()
2937 2943
2938 2944 @classmethod
2939 2945 def get_index_from_version(cls, pr_version, versions):
2940 2946 num_versions = [x.pull_request_version_id for x in versions]
2941 2947 try:
2942 2948 return num_versions.index(pr_version) +1
2943 2949 except (IndexError, ValueError):
2944 2950 return
2945 2951
2946 2952 @property
2947 2953 def outdated(self):
2948 2954 return self.display_state == self.COMMENT_OUTDATED
2949 2955
2950 2956 def outdated_at_version(self, version):
2951 2957 """
2952 2958 Checks if comment is outdated for given pull request version
2953 2959 """
2954 2960 return self.outdated and self.pull_request_version_id != version
2955 2961
2956 2962 def get_index_version(self, versions):
2957 2963 return self.get_index_from_version(
2958 2964 self.pull_request_version_id, versions)
2959 2965
2960 2966 def render(self, mentions=False):
2961 2967 from rhodecode.lib import helpers as h
2962 2968 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2963 2969
2964 2970 def __repr__(self):
2965 2971 if self.comment_id:
2966 2972 return '<DB:ChangesetComment #%s>' % self.comment_id
2967 2973 else:
2968 2974 return '<DB:ChangesetComment at %#x>' % id(self)
2969 2975
2970 2976
2971 2977 class ChangesetStatus(Base, BaseModel):
2972 2978 __tablename__ = 'changeset_statuses'
2973 2979 __table_args__ = (
2974 2980 Index('cs_revision_idx', 'revision'),
2975 2981 Index('cs_version_idx', 'version'),
2976 2982 UniqueConstraint('repo_id', 'revision', 'version'),
2977 2983 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2978 2984 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2979 2985 )
2980 2986 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2981 2987 STATUS_APPROVED = 'approved'
2982 2988 STATUS_REJECTED = 'rejected'
2983 2989 STATUS_UNDER_REVIEW = 'under_review'
2984 2990
2985 2991 STATUSES = [
2986 2992 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2987 2993 (STATUS_APPROVED, _("Approved")),
2988 2994 (STATUS_REJECTED, _("Rejected")),
2989 2995 (STATUS_UNDER_REVIEW, _("Under Review")),
2990 2996 ]
2991 2997
2992 2998 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2993 2999 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2994 3000 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2995 3001 revision = Column('revision', String(40), nullable=False)
2996 3002 status = Column('status', String(128), nullable=False, default=DEFAULT)
2997 3003 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2998 3004 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2999 3005 version = Column('version', Integer(), nullable=False, default=0)
3000 3006 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3001 3007
3002 3008 author = relationship('User', lazy='joined')
3003 3009 repo = relationship('Repository')
3004 3010 comment = relationship('ChangesetComment', lazy='joined')
3005 3011 pull_request = relationship('PullRequest', lazy='joined')
3006 3012
3007 3013 def __unicode__(self):
3008 3014 return u"<%s('%s[%s]:%s')>" % (
3009 3015 self.__class__.__name__,
3010 3016 self.status, self.version, self.author
3011 3017 )
3012 3018
3013 3019 @classmethod
3014 3020 def get_status_lbl(cls, value):
3015 3021 return dict(cls.STATUSES).get(value)
3016 3022
3017 3023 @property
3018 3024 def status_lbl(self):
3019 3025 return ChangesetStatus.get_status_lbl(self.status)
3020 3026
3021 3027
3022 3028 class _PullRequestBase(BaseModel):
3023 3029 """
3024 3030 Common attributes of pull request and version entries.
3025 3031 """
3026 3032
3027 3033 # .status values
3028 3034 STATUS_NEW = u'new'
3029 3035 STATUS_OPEN = u'open'
3030 3036 STATUS_CLOSED = u'closed'
3031 3037
3032 3038 title = Column('title', Unicode(255), nullable=True)
3033 3039 description = Column(
3034 3040 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3035 3041 nullable=True)
3036 3042 # new/open/closed status of pull request (not approve/reject/etc)
3037 3043 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3038 3044 created_on = Column(
3039 3045 'created_on', DateTime(timezone=False), nullable=False,
3040 3046 default=datetime.datetime.now)
3041 3047 updated_on = Column(
3042 3048 'updated_on', DateTime(timezone=False), nullable=False,
3043 3049 default=datetime.datetime.now)
3044 3050
3045 3051 @declared_attr
3046 3052 def user_id(cls):
3047 3053 return Column(
3048 3054 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3049 3055 unique=None)
3050 3056
3051 3057 # 500 revisions max
3052 3058 _revisions = Column(
3053 3059 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3054 3060
3055 3061 @declared_attr
3056 3062 def source_repo_id(cls):
3057 3063 # TODO: dan: rename column to source_repo_id
3058 3064 return Column(
3059 3065 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3060 3066 nullable=False)
3061 3067
3062 3068 source_ref = Column('org_ref', Unicode(255), nullable=False)
3063 3069
3064 3070 @declared_attr
3065 3071 def target_repo_id(cls):
3066 3072 # TODO: dan: rename column to target_repo_id
3067 3073 return Column(
3068 3074 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3069 3075 nullable=False)
3070 3076
3071 3077 target_ref = Column('other_ref', Unicode(255), nullable=False)
3072 3078 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3073 3079
3074 3080 # TODO: dan: rename column to last_merge_source_rev
3075 3081 _last_merge_source_rev = Column(
3076 3082 'last_merge_org_rev', String(40), nullable=True)
3077 3083 # TODO: dan: rename column to last_merge_target_rev
3078 3084 _last_merge_target_rev = Column(
3079 3085 'last_merge_other_rev', String(40), nullable=True)
3080 3086 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3081 3087 merge_rev = Column('merge_rev', String(40), nullable=True)
3082 3088
3083 3089 @hybrid_property
3084 3090 def revisions(self):
3085 3091 return self._revisions.split(':') if self._revisions else []
3086 3092
3087 3093 @revisions.setter
3088 3094 def revisions(self, val):
3089 3095 self._revisions = ':'.join(val)
3090 3096
3091 3097 @declared_attr
3092 3098 def author(cls):
3093 3099 return relationship('User', lazy='joined')
3094 3100
3095 3101 @declared_attr
3096 3102 def source_repo(cls):
3097 3103 return relationship(
3098 3104 'Repository',
3099 3105 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3100 3106
3101 3107 @property
3102 3108 def source_ref_parts(self):
3103 3109 return self.unicode_to_reference(self.source_ref)
3104 3110
3105 3111 @declared_attr
3106 3112 def target_repo(cls):
3107 3113 return relationship(
3108 3114 'Repository',
3109 3115 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3110 3116
3111 3117 @property
3112 3118 def target_ref_parts(self):
3113 3119 return self.unicode_to_reference(self.target_ref)
3114 3120
3115 3121 @property
3116 3122 def shadow_merge_ref(self):
3117 3123 return self.unicode_to_reference(self._shadow_merge_ref)
3118 3124
3119 3125 @shadow_merge_ref.setter
3120 3126 def shadow_merge_ref(self, ref):
3121 3127 self._shadow_merge_ref = self.reference_to_unicode(ref)
3122 3128
3123 3129 def unicode_to_reference(self, raw):
3124 3130 """
3125 3131 Convert a unicode (or string) to a reference object.
3126 3132 If unicode evaluates to False it returns None.
3127 3133 """
3128 3134 if raw:
3129 3135 refs = raw.split(':')
3130 3136 return Reference(*refs)
3131 3137 else:
3132 3138 return None
3133 3139
3134 3140 def reference_to_unicode(self, ref):
3135 3141 """
3136 3142 Convert a reference object to unicode.
3137 3143 If reference is None it returns None.
3138 3144 """
3139 3145 if ref:
3140 3146 return u':'.join(ref)
3141 3147 else:
3142 3148 return None
3143 3149
3144 3150 def get_api_data(self):
3145 3151 from rhodecode.model.pull_request import PullRequestModel
3146 3152 pull_request = self
3147 3153 merge_status = PullRequestModel().merge_status(pull_request)
3148 3154
3149 3155 pull_request_url = url(
3150 3156 'pullrequest_show', repo_name=self.target_repo.repo_name,
3151 3157 pull_request_id=self.pull_request_id, qualified=True)
3152 3158
3153 3159 merge_data = {
3154 3160 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3155 3161 'reference': (
3156 3162 pull_request.shadow_merge_ref._asdict()
3157 3163 if pull_request.shadow_merge_ref else None),
3158 3164 }
3159 3165
3160 3166 data = {
3161 3167 'pull_request_id': pull_request.pull_request_id,
3162 3168 'url': pull_request_url,
3163 3169 'title': pull_request.title,
3164 3170 'description': pull_request.description,
3165 3171 'status': pull_request.status,
3166 3172 'created_on': pull_request.created_on,
3167 3173 'updated_on': pull_request.updated_on,
3168 3174 'commit_ids': pull_request.revisions,
3169 3175 'review_status': pull_request.calculated_review_status(),
3170 3176 'mergeable': {
3171 3177 'status': merge_status[0],
3172 3178 'message': unicode(merge_status[1]),
3173 3179 },
3174 3180 'source': {
3175 3181 'clone_url': pull_request.source_repo.clone_url(),
3176 3182 'repository': pull_request.source_repo.repo_name,
3177 3183 'reference': {
3178 3184 'name': pull_request.source_ref_parts.name,
3179 3185 'type': pull_request.source_ref_parts.type,
3180 3186 'commit_id': pull_request.source_ref_parts.commit_id,
3181 3187 },
3182 3188 },
3183 3189 'target': {
3184 3190 'clone_url': pull_request.target_repo.clone_url(),
3185 3191 'repository': pull_request.target_repo.repo_name,
3186 3192 'reference': {
3187 3193 'name': pull_request.target_ref_parts.name,
3188 3194 'type': pull_request.target_ref_parts.type,
3189 3195 'commit_id': pull_request.target_ref_parts.commit_id,
3190 3196 },
3191 3197 },
3192 3198 'merge': merge_data,
3193 3199 'author': pull_request.author.get_api_data(include_secrets=False,
3194 3200 details='basic'),
3195 3201 'reviewers': [
3196 3202 {
3197 3203 'user': reviewer.get_api_data(include_secrets=False,
3198 3204 details='basic'),
3199 3205 'reasons': reasons,
3200 3206 'review_status': st[0][1].status if st else 'not_reviewed',
3201 3207 }
3202 3208 for reviewer, reasons, st in pull_request.reviewers_statuses()
3203 3209 ]
3204 3210 }
3205 3211
3206 3212 return data
3207 3213
3208 3214
3209 3215 class PullRequest(Base, _PullRequestBase):
3210 3216 __tablename__ = 'pull_requests'
3211 3217 __table_args__ = (
3212 3218 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3213 3219 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3214 3220 )
3215 3221
3216 3222 pull_request_id = Column(
3217 3223 'pull_request_id', Integer(), nullable=False, primary_key=True)
3218 3224
3219 3225 def __repr__(self):
3220 3226 if self.pull_request_id:
3221 3227 return '<DB:PullRequest #%s>' % self.pull_request_id
3222 3228 else:
3223 3229 return '<DB:PullRequest at %#x>' % id(self)
3224 3230
3225 3231 reviewers = relationship('PullRequestReviewers',
3226 3232 cascade="all, delete, delete-orphan")
3227 3233 statuses = relationship('ChangesetStatus')
3228 3234 comments = relationship('ChangesetComment',
3229 3235 cascade="all, delete, delete-orphan")
3230 3236 versions = relationship('PullRequestVersion',
3231 3237 cascade="all, delete, delete-orphan",
3232 3238 lazy='dynamic')
3233 3239
3234 3240
3235 3241 @classmethod
3236 3242 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3237 3243 internal_methods=None):
3238 3244
3239 3245 class PullRequestDisplay(object):
3240 3246 """
3241 3247 Special object wrapper for showing PullRequest data via Versions
3242 3248 It mimics PR object as close as possible. This is read only object
3243 3249 just for display
3244 3250 """
3245 3251
3246 3252 def __init__(self, attrs, internal=None):
3247 3253 self.attrs = attrs
3248 3254 # internal have priority over the given ones via attrs
3249 3255 self.internal = internal or ['versions']
3250 3256
3251 3257 def __getattr__(self, item):
3252 3258 if item in self.internal:
3253 3259 return getattr(self, item)
3254 3260 try:
3255 3261 return self.attrs[item]
3256 3262 except KeyError:
3257 3263 raise AttributeError(
3258 3264 '%s object has no attribute %s' % (self, item))
3259 3265
3260 3266 def __repr__(self):
3261 3267 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3262 3268
3263 3269 def versions(self):
3264 3270 return pull_request_obj.versions.order_by(
3265 3271 PullRequestVersion.pull_request_version_id).all()
3266 3272
3267 3273 def is_closed(self):
3268 3274 return pull_request_obj.is_closed()
3269 3275
3270 3276 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3271 3277
3272 3278 attrs.author = StrictAttributeDict(
3273 3279 pull_request_obj.author.get_api_data())
3274 3280 if pull_request_obj.target_repo:
3275 3281 attrs.target_repo = StrictAttributeDict(
3276 3282 pull_request_obj.target_repo.get_api_data())
3277 3283 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3278 3284
3279 3285 if pull_request_obj.source_repo:
3280 3286 attrs.source_repo = StrictAttributeDict(
3281 3287 pull_request_obj.source_repo.get_api_data())
3282 3288 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3283 3289
3284 3290 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3285 3291 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3286 3292 attrs.revisions = pull_request_obj.revisions
3287 3293
3288 3294 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3289 3295
3290 3296 return PullRequestDisplay(attrs, internal=internal_methods)
3291 3297
3292 3298 def is_closed(self):
3293 3299 return self.status == self.STATUS_CLOSED
3294 3300
3295 3301 def __json__(self):
3296 3302 return {
3297 3303 'revisions': self.revisions,
3298 3304 }
3299 3305
3300 3306 def calculated_review_status(self):
3301 3307 from rhodecode.model.changeset_status import ChangesetStatusModel
3302 3308 return ChangesetStatusModel().calculated_review_status(self)
3303 3309
3304 3310 def reviewers_statuses(self):
3305 3311 from rhodecode.model.changeset_status import ChangesetStatusModel
3306 3312 return ChangesetStatusModel().reviewers_statuses(self)
3307 3313
3308 3314
3309 3315 class PullRequestVersion(Base, _PullRequestBase):
3310 3316 __tablename__ = 'pull_request_versions'
3311 3317 __table_args__ = (
3312 3318 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3313 3319 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3314 3320 )
3315 3321
3316 3322 pull_request_version_id = Column(
3317 3323 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3318 3324 pull_request_id = Column(
3319 3325 'pull_request_id', Integer(),
3320 3326 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3321 3327 pull_request = relationship('PullRequest')
3322 3328
3323 3329 def __repr__(self):
3324 3330 if self.pull_request_version_id:
3325 3331 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3326 3332 else:
3327 3333 return '<DB:PullRequestVersion at %#x>' % id(self)
3328 3334
3329 3335 @property
3330 3336 def reviewers(self):
3331 3337 return self.pull_request.reviewers
3332 3338
3333 3339 @property
3334 3340 def versions(self):
3335 3341 return self.pull_request.versions
3336 3342
3337 3343 def is_closed(self):
3338 3344 # calculate from original
3339 3345 return self.pull_request.status == self.STATUS_CLOSED
3340 3346
3341 3347 def calculated_review_status(self):
3342 3348 return self.pull_request.calculated_review_status()
3343 3349
3344 3350 def reviewers_statuses(self):
3345 3351 return self.pull_request.reviewers_statuses()
3346 3352
3347 3353
3348 3354 class PullRequestReviewers(Base, BaseModel):
3349 3355 __tablename__ = 'pull_request_reviewers'
3350 3356 __table_args__ = (
3351 3357 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3352 3358 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3353 3359 )
3354 3360
3355 3361 def __init__(self, user=None, pull_request=None, reasons=None):
3356 3362 self.user = user
3357 3363 self.pull_request = pull_request
3358 3364 self.reasons = reasons or []
3359 3365
3360 3366 @hybrid_property
3361 3367 def reasons(self):
3362 3368 if not self._reasons:
3363 3369 return []
3364 3370 return self._reasons
3365 3371
3366 3372 @reasons.setter
3367 3373 def reasons(self, val):
3368 3374 val = val or []
3369 3375 if any(not isinstance(x, basestring) for x in val):
3370 3376 raise Exception('invalid reasons type, must be list of strings')
3371 3377 self._reasons = val
3372 3378
3373 3379 pull_requests_reviewers_id = Column(
3374 3380 'pull_requests_reviewers_id', Integer(), nullable=False,
3375 3381 primary_key=True)
3376 3382 pull_request_id = Column(
3377 3383 "pull_request_id", Integer(),
3378 3384 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3379 3385 user_id = Column(
3380 3386 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3381 3387 _reasons = Column(
3382 3388 'reason', MutationList.as_mutable(
3383 3389 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3384 3390
3385 3391 user = relationship('User')
3386 3392 pull_request = relationship('PullRequest')
3387 3393
3388 3394
3389 3395 class Notification(Base, BaseModel):
3390 3396 __tablename__ = 'notifications'
3391 3397 __table_args__ = (
3392 3398 Index('notification_type_idx', 'type'),
3393 3399 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3394 3400 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3395 3401 )
3396 3402
3397 3403 TYPE_CHANGESET_COMMENT = u'cs_comment'
3398 3404 TYPE_MESSAGE = u'message'
3399 3405 TYPE_MENTION = u'mention'
3400 3406 TYPE_REGISTRATION = u'registration'
3401 3407 TYPE_PULL_REQUEST = u'pull_request'
3402 3408 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3403 3409
3404 3410 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3405 3411 subject = Column('subject', Unicode(512), nullable=True)
3406 3412 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3407 3413 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3408 3414 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3409 3415 type_ = Column('type', Unicode(255))
3410 3416
3411 3417 created_by_user = relationship('User')
3412 3418 notifications_to_users = relationship('UserNotification', lazy='joined',
3413 3419 cascade="all, delete, delete-orphan")
3414 3420
3415 3421 @property
3416 3422 def recipients(self):
3417 3423 return [x.user for x in UserNotification.query()\
3418 3424 .filter(UserNotification.notification == self)\
3419 3425 .order_by(UserNotification.user_id.asc()).all()]
3420 3426
3421 3427 @classmethod
3422 3428 def create(cls, created_by, subject, body, recipients, type_=None):
3423 3429 if type_ is None:
3424 3430 type_ = Notification.TYPE_MESSAGE
3425 3431
3426 3432 notification = cls()
3427 3433 notification.created_by_user = created_by
3428 3434 notification.subject = subject
3429 3435 notification.body = body
3430 3436 notification.type_ = type_
3431 3437 notification.created_on = datetime.datetime.now()
3432 3438
3433 3439 for u in recipients:
3434 3440 assoc = UserNotification()
3435 3441 assoc.notification = notification
3436 3442
3437 3443 # if created_by is inside recipients mark his notification
3438 3444 # as read
3439 3445 if u.user_id == created_by.user_id:
3440 3446 assoc.read = True
3441 3447
3442 3448 u.notifications.append(assoc)
3443 3449 Session().add(notification)
3444 3450
3445 3451 return notification
3446 3452
3447 3453 @property
3448 3454 def description(self):
3449 3455 from rhodecode.model.notification import NotificationModel
3450 3456 return NotificationModel().make_description(self)
3451 3457
3452 3458
3453 3459 class UserNotification(Base, BaseModel):
3454 3460 __tablename__ = 'user_to_notification'
3455 3461 __table_args__ = (
3456 3462 UniqueConstraint('user_id', 'notification_id'),
3457 3463 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3458 3464 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3459 3465 )
3460 3466 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3461 3467 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3462 3468 read = Column('read', Boolean, default=False)
3463 3469 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3464 3470
3465 3471 user = relationship('User', lazy="joined")
3466 3472 notification = relationship('Notification', lazy="joined",
3467 3473 order_by=lambda: Notification.created_on.desc(),)
3468 3474
3469 3475 def mark_as_read(self):
3470 3476 self.read = True
3471 3477 Session().add(self)
3472 3478
3473 3479
3474 3480 class Gist(Base, BaseModel):
3475 3481 __tablename__ = 'gists'
3476 3482 __table_args__ = (
3477 3483 Index('g_gist_access_id_idx', 'gist_access_id'),
3478 3484 Index('g_created_on_idx', 'created_on'),
3479 3485 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3480 3486 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3481 3487 )
3482 3488 GIST_PUBLIC = u'public'
3483 3489 GIST_PRIVATE = u'private'
3484 3490 DEFAULT_FILENAME = u'gistfile1.txt'
3485 3491
3486 3492 ACL_LEVEL_PUBLIC = u'acl_public'
3487 3493 ACL_LEVEL_PRIVATE = u'acl_private'
3488 3494
3489 3495 gist_id = Column('gist_id', Integer(), primary_key=True)
3490 3496 gist_access_id = Column('gist_access_id', Unicode(250))
3491 3497 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3492 3498 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3493 3499 gist_expires = Column('gist_expires', Float(53), nullable=False)
3494 3500 gist_type = Column('gist_type', Unicode(128), nullable=False)
3495 3501 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3496 3502 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3497 3503 acl_level = Column('acl_level', Unicode(128), nullable=True)
3498 3504
3499 3505 owner = relationship('User')
3500 3506
3501 3507 def __repr__(self):
3502 3508 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3503 3509
3504 3510 @classmethod
3505 3511 def get_or_404(cls, id_):
3506 3512 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3507 3513 if not res:
3508 3514 raise HTTPNotFound
3509 3515 return res
3510 3516
3511 3517 @classmethod
3512 3518 def get_by_access_id(cls, gist_access_id):
3513 3519 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3514 3520
3515 3521 def gist_url(self):
3516 3522 import rhodecode
3517 3523 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3518 3524 if alias_url:
3519 3525 return alias_url.replace('{gistid}', self.gist_access_id)
3520 3526
3521 3527 return url('gist', gist_id=self.gist_access_id, qualified=True)
3522 3528
3523 3529 @classmethod
3524 3530 def base_path(cls):
3525 3531 """
3526 3532 Returns base path when all gists are stored
3527 3533
3528 3534 :param cls:
3529 3535 """
3530 3536 from rhodecode.model.gist import GIST_STORE_LOC
3531 3537 q = Session().query(RhodeCodeUi)\
3532 3538 .filter(RhodeCodeUi.ui_key == URL_SEP)
3533 3539 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3534 3540 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3535 3541
3536 3542 def get_api_data(self):
3537 3543 """
3538 3544 Common function for generating gist related data for API
3539 3545 """
3540 3546 gist = self
3541 3547 data = {
3542 3548 'gist_id': gist.gist_id,
3543 3549 'type': gist.gist_type,
3544 3550 'access_id': gist.gist_access_id,
3545 3551 'description': gist.gist_description,
3546 3552 'url': gist.gist_url(),
3547 3553 'expires': gist.gist_expires,
3548 3554 'created_on': gist.created_on,
3549 3555 'modified_at': gist.modified_at,
3550 3556 'content': None,
3551 3557 'acl_level': gist.acl_level,
3552 3558 }
3553 3559 return data
3554 3560
3555 3561 def __json__(self):
3556 3562 data = dict(
3557 3563 )
3558 3564 data.update(self.get_api_data())
3559 3565 return data
3560 3566 # SCM functions
3561 3567
3562 3568 def scm_instance(self, **kwargs):
3563 3569 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3564 3570 return get_vcs_instance(
3565 3571 repo_path=safe_str(full_repo_path), create=False)
3566 3572
3567 3573
3568 3574 class ExternalIdentity(Base, BaseModel):
3569 3575 __tablename__ = 'external_identities'
3570 3576 __table_args__ = (
3571 3577 Index('local_user_id_idx', 'local_user_id'),
3572 3578 Index('external_id_idx', 'external_id'),
3573 3579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3574 3580 'mysql_charset': 'utf8'})
3575 3581
3576 3582 external_id = Column('external_id', Unicode(255), default=u'',
3577 3583 primary_key=True)
3578 3584 external_username = Column('external_username', Unicode(1024), default=u'')
3579 3585 local_user_id = Column('local_user_id', Integer(),
3580 3586 ForeignKey('users.user_id'), primary_key=True)
3581 3587 provider_name = Column('provider_name', Unicode(255), default=u'',
3582 3588 primary_key=True)
3583 3589 access_token = Column('access_token', String(1024), default=u'')
3584 3590 alt_token = Column('alt_token', String(1024), default=u'')
3585 3591 token_secret = Column('token_secret', String(1024), default=u'')
3586 3592
3587 3593 @classmethod
3588 3594 def by_external_id_and_provider(cls, external_id, provider_name,
3589 3595 local_user_id=None):
3590 3596 """
3591 3597 Returns ExternalIdentity instance based on search params
3592 3598
3593 3599 :param external_id:
3594 3600 :param provider_name:
3595 3601 :return: ExternalIdentity
3596 3602 """
3597 3603 query = cls.query()
3598 3604 query = query.filter(cls.external_id == external_id)
3599 3605 query = query.filter(cls.provider_name == provider_name)
3600 3606 if local_user_id:
3601 3607 query = query.filter(cls.local_user_id == local_user_id)
3602 3608 return query.first()
3603 3609
3604 3610 @classmethod
3605 3611 def user_by_external_id_and_provider(cls, external_id, provider_name):
3606 3612 """
3607 3613 Returns User instance based on search params
3608 3614
3609 3615 :param external_id:
3610 3616 :param provider_name:
3611 3617 :return: User
3612 3618 """
3613 3619 query = User.query()
3614 3620 query = query.filter(cls.external_id == external_id)
3615 3621 query = query.filter(cls.provider_name == provider_name)
3616 3622 query = query.filter(User.user_id == cls.local_user_id)
3617 3623 return query.first()
3618 3624
3619 3625 @classmethod
3620 3626 def by_local_user_id(cls, local_user_id):
3621 3627 """
3622 3628 Returns all tokens for user
3623 3629
3624 3630 :param local_user_id:
3625 3631 :return: ExternalIdentity
3626 3632 """
3627 3633 query = cls.query()
3628 3634 query = query.filter(cls.local_user_id == local_user_id)
3629 3635 return query
3630 3636
3631 3637
3632 3638 class Integration(Base, BaseModel):
3633 3639 __tablename__ = 'integrations'
3634 3640 __table_args__ = (
3635 3641 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3636 3642 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3637 3643 )
3638 3644
3639 3645 integration_id = Column('integration_id', Integer(), primary_key=True)
3640 3646 integration_type = Column('integration_type', String(255))
3641 3647 enabled = Column('enabled', Boolean(), nullable=False)
3642 3648 name = Column('name', String(255), nullable=False)
3643 3649 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3644 3650 default=False)
3645 3651
3646 3652 settings = Column(
3647 3653 'settings_json', MutationObj.as_mutable(
3648 3654 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3649 3655 repo_id = Column(
3650 3656 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3651 3657 nullable=True, unique=None, default=None)
3652 3658 repo = relationship('Repository', lazy='joined')
3653 3659
3654 3660 repo_group_id = Column(
3655 3661 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3656 3662 nullable=True, unique=None, default=None)
3657 3663 repo_group = relationship('RepoGroup', lazy='joined')
3658 3664
3659 3665 @property
3660 3666 def scope(self):
3661 3667 if self.repo:
3662 3668 return repr(self.repo)
3663 3669 if self.repo_group:
3664 3670 if self.child_repos_only:
3665 3671 return repr(self.repo_group) + ' (child repos only)'
3666 3672 else:
3667 3673 return repr(self.repo_group) + ' (recursive)'
3668 3674 if self.child_repos_only:
3669 3675 return 'root_repos'
3670 3676 return 'global'
3671 3677
3672 3678 def __repr__(self):
3673 3679 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3674 3680
3675 3681
3676 3682 class RepoReviewRuleUser(Base, BaseModel):
3677 3683 __tablename__ = 'repo_review_rules_users'
3678 3684 __table_args__ = (
3679 3685 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3680 3686 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3681 3687 )
3682 3688 repo_review_rule_user_id = Column(
3683 3689 'repo_review_rule_user_id', Integer(), primary_key=True)
3684 3690 repo_review_rule_id = Column("repo_review_rule_id",
3685 3691 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3686 3692 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3687 3693 nullable=False)
3688 3694 user = relationship('User')
3689 3695
3690 3696
3691 3697 class RepoReviewRuleUserGroup(Base, BaseModel):
3692 3698 __tablename__ = 'repo_review_rules_users_groups'
3693 3699 __table_args__ = (
3694 3700 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3695 3701 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3696 3702 )
3697 3703 repo_review_rule_users_group_id = Column(
3698 3704 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3699 3705 repo_review_rule_id = Column("repo_review_rule_id",
3700 3706 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3701 3707 users_group_id = Column("users_group_id", Integer(),
3702 3708 ForeignKey('users_groups.users_group_id'), nullable=False)
3703 3709 users_group = relationship('UserGroup')
3704 3710
3705 3711
3706 3712 class RepoReviewRule(Base, BaseModel):
3707 3713 __tablename__ = 'repo_review_rules'
3708 3714 __table_args__ = (
3709 3715 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3710 3716 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3711 3717 )
3712 3718
3713 3719 repo_review_rule_id = Column(
3714 3720 'repo_review_rule_id', Integer(), primary_key=True)
3715 3721 repo_id = Column(
3716 3722 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3717 3723 repo = relationship('Repository', backref='review_rules')
3718 3724
3719 3725 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3720 3726 default=u'*') # glob
3721 3727 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3722 3728 default=u'*') # glob
3723 3729
3724 3730 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3725 3731 nullable=False, default=False)
3726 3732 rule_users = relationship('RepoReviewRuleUser')
3727 3733 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3728 3734
3729 3735 @hybrid_property
3730 3736 def branch_pattern(self):
3731 3737 return self._branch_pattern or '*'
3732 3738
3733 3739 def _validate_glob(self, value):
3734 3740 re.compile('^' + glob2re(value) + '$')
3735 3741
3736 3742 @branch_pattern.setter
3737 3743 def branch_pattern(self, value):
3738 3744 self._validate_glob(value)
3739 3745 self._branch_pattern = value or '*'
3740 3746
3741 3747 @hybrid_property
3742 3748 def file_pattern(self):
3743 3749 return self._file_pattern or '*'
3744 3750
3745 3751 @file_pattern.setter
3746 3752 def file_pattern(self, value):
3747 3753 self._validate_glob(value)
3748 3754 self._file_pattern = value or '*'
3749 3755
3750 3756 def matches(self, branch, files_changed):
3751 3757 """
3752 3758 Check if this review rule matches a branch/files in a pull request
3753 3759
3754 3760 :param branch: branch name for the commit
3755 3761 :param files_changed: list of file paths changed in the pull request
3756 3762 """
3757 3763
3758 3764 branch = branch or ''
3759 3765 files_changed = files_changed or []
3760 3766
3761 3767 branch_matches = True
3762 3768 if branch:
3763 3769 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3764 3770 branch_matches = bool(branch_regex.search(branch))
3765 3771
3766 3772 files_matches = True
3767 3773 if self.file_pattern != '*':
3768 3774 files_matches = False
3769 3775 file_regex = re.compile(glob2re(self.file_pattern))
3770 3776 for filename in files_changed:
3771 3777 if file_regex.search(filename):
3772 3778 files_matches = True
3773 3779 break
3774 3780
3775 3781 return branch_matches and files_matches
3776 3782
3777 3783 @property
3778 3784 def review_users(self):
3779 3785 """ Returns the users which this rule applies to """
3780 3786
3781 3787 users = set()
3782 3788 users |= set([
3783 3789 rule_user.user for rule_user in self.rule_users
3784 3790 if rule_user.user.active])
3785 3791 users |= set(
3786 3792 member.user
3787 3793 for rule_user_group in self.rule_user_groups
3788 3794 for member in rule_user_group.users_group.members
3789 3795 if member.user.active
3790 3796 )
3791 3797 return users
3792 3798
3793 3799 def __repr__(self):
3794 3800 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3795 3801 self.repo_review_rule_id, self.repo)
3796 3802
3797 3803
3798 3804 class DbMigrateVersion(Base, BaseModel):
3799 3805 __tablename__ = 'db_migrate_version'
3800 3806 __table_args__ = (
3801 3807 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3802 3808 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3803 3809 )
3804 3810 repository_id = Column('repository_id', String(250), primary_key=True)
3805 3811 repository_path = Column('repository_path', Text)
3806 3812 version = Column('version', Integer)
3807 3813
3808 3814
3809 3815 class DbSession(Base, BaseModel):
3810 3816 __tablename__ = 'db_session'
3811 3817 __table_args__ = (
3812 3818 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3813 3819 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3814 3820 )
3815 3821
3816 3822 def __repr__(self):
3817 3823 return '<DB:DbSession({})>'.format(self.id)
3818 3824
3819 3825 id = Column('id', Integer())
3820 3826 namespace = Column('namespace', String(255), primary_key=True)
3821 3827 accessed = Column('accessed', DateTime, nullable=False)
3822 3828 created = Column('created', DateTime, nullable=False)
3823 3829 data = Column('data', PickleType, nullable=False)
@@ -1,188 +1,196 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22
23 23 import colander
24 24 from rhodecode.model.validation_schema import preparers
25 25 from rhodecode.model.db import User, UserGroup
26 26
27 27
28 28 class _RootLocation(object):
29 29 pass
30 30
31 31 RootLocation = _RootLocation()
32 32
33 33
34 34 def _normalize(seperator, path):
35 35
36 36 if not path:
37 37 return ''
38 38 elif path is colander.null:
39 39 return colander.null
40 40
41 41 parts = path.split(seperator)
42 42
43 43 def bad_parts(value):
44 44 if not value:
45 45 return False
46 46 if re.match(r'^[.]+$', value):
47 47 return False
48 48
49 49 return True
50 50
51 51 def slugify(value):
52 52 value = preparers.slugify_preparer(value)
53 53 value = re.sub(r'[.]{2,}', '.', value)
54 54 return value
55 55
56 56 clean_parts = [slugify(item) for item in parts if item]
57 57 path = filter(bad_parts, clean_parts)
58 58 return seperator.join(path)
59 59
60 60
61 61 class RepoNameType(colander.String):
62 62 SEPARATOR = '/'
63 63
64 64 def deserialize(self, node, cstruct):
65 65 result = super(RepoNameType, self).deserialize(node, cstruct)
66 66 if cstruct is colander.null:
67 67 return colander.null
68 68 return self._normalize(result)
69 69
70 70 def _normalize(self, path):
71 71 return _normalize(self.SEPARATOR, path)
72 72
73 73
74 74 class GroupNameType(colander.String):
75 75 SEPARATOR = '/'
76 76
77 77 def deserialize(self, node, cstruct):
78 78 if cstruct is RootLocation:
79 79 return cstruct
80 80
81 81 result = super(GroupNameType, self).deserialize(node, cstruct)
82 82 if cstruct is colander.null:
83 83 return colander.null
84 84 return self._normalize(result)
85 85
86 86 def _normalize(self, path):
87 87 return _normalize(self.SEPARATOR, path)
88 88
89 89
90 90 class StringBooleanType(colander.String):
91 91 true_values = ['true', 't', 'yes', 'y', 'on', '1']
92 92 false_values = ['false', 'f', 'no', 'n', 'off', '0']
93 93
94 94 def serialize(self, node, appstruct):
95 95 if appstruct is colander.null:
96 96 return colander.null
97 97 if not isinstance(appstruct, bool):
98 98 raise colander.Invalid(node, '%r is not a boolean' % appstruct)
99 99
100 100 return appstruct and 'true' or 'false'
101 101
102 102 def deserialize(self, node, cstruct):
103 103 if cstruct is colander.null:
104 104 return colander.null
105 105
106 106 if isinstance(cstruct, bool):
107 107 return cstruct
108 108
109 109 if not isinstance(cstruct, basestring):
110 110 raise colander.Invalid(node, '%r is not a string' % cstruct)
111 111
112 112 value = cstruct.lower()
113 113 if value in self.true_values:
114 114 return True
115 115 elif value in self.false_values:
116 116 return False
117 117 else:
118 118 raise colander.Invalid(
119 119 node, '{} value cannot be translated to bool'.format(value))
120 120
121 121
122 122 class UserOrUserGroupType(colander.SchemaType):
123 123 """ colander Schema type for valid rhodecode user and/or usergroup """
124 124 scopes = ('user', 'usergroup')
125 125
126 126 def __init__(self):
127 127 self.users = 'user' in self.scopes
128 128 self.usergroups = 'usergroup' in self.scopes
129 129
130 130 def serialize(self, node, appstruct):
131 131 if appstruct is colander.null:
132 132 return colander.null
133 133
134 134 if self.users:
135 135 if isinstance(appstruct, User):
136 136 if self.usergroups:
137 137 return 'user:%s' % appstruct.username
138 138 return appstruct.username
139 139
140 140 if self.usergroups:
141 141 if isinstance(appstruct, UserGroup):
142 142 if self.users:
143 143 return 'usergroup:%s' % appstruct.users_group_name
144 144 return appstruct.users_group_name
145 145
146 146 raise colander.Invalid(
147 147 node, '%s is not a valid %s' % (appstruct, ' or '.join(self.scopes)))
148 148
149 149 def deserialize(self, node, cstruct):
150 150 if cstruct is colander.null:
151 151 return colander.null
152 152
153 153 user, usergroup = None, None
154 154 if self.users:
155 155 if cstruct.startswith('user:'):
156 156 user = User.get_by_username(cstruct.split(':')[1])
157 157 else:
158 158 user = User.get_by_username(cstruct)
159 159
160 160 if self.usergroups:
161 161 if cstruct.startswith('usergroup:'):
162 162 usergroup = UserGroup.get_by_group_name(cstruct.split(':')[1])
163 163 else:
164 164 usergroup = UserGroup.get_by_group_name(cstruct)
165 165
166 166 if self.users and self.usergroups:
167 167 if user and usergroup:
168 168 raise colander.Invalid(node, (
169 169 '%s is both a user and usergroup, specify which '
170 170 'one was wanted by prepending user: or usergroup: to the '
171 171 'name') % cstruct)
172 172
173 173 if self.users and user:
174 174 return user
175 175
176 176 if self.usergroups and usergroup:
177 177 return usergroup
178 178
179 179 raise colander.Invalid(
180 180 node, '%s is not a valid %s' % (cstruct, ' or '.join(self.scopes)))
181 181
182 182
183 183 class UserType(UserOrUserGroupType):
184 184 scopes = ('user',)
185 185
186 186
187 187 class UserGroupType(UserOrUserGroupType):
188 188 scopes = ('usergroup',)
189
190
191 class StrOrIntType(colander.String):
192 def deserialize(self, node, cstruct):
193 if isinstance(node, basestring):
194 return super(StrOrIntType, self).deserialize(node, cstruct)
195 else:
196 return colander.Integer().deserialize(node, cstruct)
@@ -1,1198 +1,1143 b''
1 1 // Default styles
2 2
3 3 .diff-collapse {
4 4 margin: @padding 0;
5 5 text-align: right;
6 6 }
7 7
8 8 .diff-container {
9 9 margin-bottom: @space;
10 10
11 11 .diffblock {
12 12 margin-bottom: @space;
13 13 }
14 14
15 15 &.hidden {
16 16 display: none;
17 17 overflow: hidden;
18 18 }
19 19 }
20 20
21 21 .compare_view_files {
22 22
23 23 .diff-container {
24 24
25 25 .diffblock {
26 26 margin-bottom: 0;
27 27 }
28 28 }
29 29 }
30 30
31 31 div.diffblock .sidebyside {
32 32 background: #ffffff;
33 33 }
34 34
35 35 div.diffblock {
36 36 overflow-x: auto;
37 37 overflow-y: hidden;
38 38 clear: both;
39 39 padding: 0px;
40 40 background: @grey6;
41 41 border: @border-thickness solid @grey5;
42 42 -webkit-border-radius: @border-radius @border-radius 0px 0px;
43 43 border-radius: @border-radius @border-radius 0px 0px;
44 44
45 45
46 46 .comments-number {
47 47 float: right;
48 48 }
49 49
50 50 // BEGIN CODE-HEADER STYLES
51 51
52 52 .code-header {
53 53 background: @grey6;
54 54 padding: 10px 0 10px 0;
55 55 height: auto;
56 56 width: 100%;
57 57
58 58 .hash {
59 59 float: left;
60 60 padding: 2px 0 0 2px;
61 61 }
62 62
63 63 .date {
64 64 float: left;
65 65 text-transform: uppercase;
66 66 padding: 4px 0px 0px 2px;
67 67 }
68 68
69 69 div {
70 70 margin-left: 4px;
71 71 }
72 72
73 73 div.compare_header {
74 74 min-height: 40px;
75 75 margin: 0;
76 76 padding: 0 @padding;
77 77
78 78 .drop-menu {
79 79 float:left;
80 80 display: block;
81 81 margin:0 0 @padding 0;
82 82 }
83 83
84 84 .compare-label {
85 85 float: left;
86 86 clear: both;
87 87 display: inline-block;
88 88 min-width: 5em;
89 89 margin: 0;
90 90 padding: @button-padding @button-padding @button-padding 0;
91 91 font-family: @text-semibold;
92 92 }
93 93
94 94 .compare-buttons {
95 95 float: left;
96 96 margin: 0;
97 97 padding: 0 0 @padding;
98 98
99 99 .btn {
100 100 margin: 0 @padding 0 0;
101 101 }
102 102 }
103 103 }
104 104
105 105 }
106 106
107 107 .parents {
108 108 float: left;
109 109 width: 100px;
110 110 font-weight: 400;
111 111 vertical-align: middle;
112 112 padding: 0px 2px 0px 2px;
113 113 background-color: @grey6;
114 114
115 115 #parent_link {
116 116 margin: 00px 2px;
117 117
118 118 &.double {
119 119 margin: 0px 2px;
120 120 }
121 121
122 122 &.disabled{
123 123 margin-right: @padding;
124 124 }
125 125 }
126 126 }
127 127
128 128 .children {
129 129 float: right;
130 130 width: 100px;
131 131 font-weight: 400;
132 132 vertical-align: middle;
133 133 text-align: right;
134 134 padding: 0px 2px 0px 2px;
135 135 background-color: @grey6;
136 136
137 137 #child_link {
138 138 margin: 0px 2px;
139 139
140 140 &.double {
141 141 margin: 0px 2px;
142 142 }
143 143
144 144 &.disabled{
145 145 margin-right: @padding;
146 146 }
147 147 }
148 148 }
149 149
150 150 .changeset_header {
151 151 height: 16px;
152 152
153 153 & > div{
154 154 margin-right: @padding;
155 155 }
156 156 }
157 157
158 158 .changeset_file {
159 159 text-align: left;
160 160 float: left;
161 161 padding: 0;
162 162
163 163 a{
164 164 display: inline-block;
165 165 margin-right: 0.5em;
166 166 }
167 167
168 168 #selected_mode{
169 169 margin-left: 0;
170 170 }
171 171 }
172 172
173 173 .diff-menu-wrapper {
174 174 float: left;
175 175 }
176 176
177 177 .diff-menu {
178 178 position: absolute;
179 179 background: none repeat scroll 0 0 #FFFFFF;
180 180 border-color: #003367 @grey3 @grey3;
181 181 border-right: 1px solid @grey3;
182 182 border-style: solid solid solid;
183 183 border-width: @border-thickness;
184 184 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
185 185 margin-top: 5px;
186 186 margin-left: 1px;
187 187 }
188 188
189 189 .diff-actions, .editor-actions {
190 190 float: left;
191 191
192 192 input{
193 193 margin: 0 0.5em 0 0;
194 194 }
195 195 }
196 196
197 197 // END CODE-HEADER STYLES
198 198
199 199 // BEGIN CODE-BODY STYLES
200 200
201 201 .code-body {
202 202 background: white;
203 203 padding: 0;
204 204 background-color: #ffffff;
205 205 position: relative;
206 206 max-width: none;
207 207 box-sizing: border-box;
208 208 // TODO: johbo: Parent has overflow: auto, this forces the child here
209 209 // to have the intended size and to scroll. Should be simplified.
210 210 width: 100%;
211 211 overflow-x: auto;
212 212 }
213 213
214 214 pre.raw {
215 215 background: white;
216 216 color: @grey1;
217 217 }
218 218 // END CODE-BODY STYLES
219 219
220 220 }
221 221
222 222
223 223 table.code-difftable {
224 224 border-collapse: collapse;
225 225 width: 99%;
226 226 border-radius: 0px !important;
227 227
228 228 td {
229 229 padding: 0 !important;
230 230 background: none !important;
231 231 border: 0 !important;
232 232 }
233 233
234 234 .context {
235 235 background: none repeat scroll 0 0 #DDE7EF;
236 236 }
237 237
238 238 .add {
239 239 background: none repeat scroll 0 0 #DDFFDD;
240 240
241 241 ins {
242 242 background: none repeat scroll 0 0 #AAFFAA;
243 243 text-decoration: none;
244 244 }
245 245 }
246 246
247 247 .del {
248 248 background: none repeat scroll 0 0 #FFDDDD;
249 249
250 250 del {
251 251 background: none repeat scroll 0 0 #FFAAAA;
252 252 text-decoration: none;
253 253 }
254 254 }
255 255
256 256 /** LINE NUMBERS **/
257 257 .lineno {
258 258 padding-left: 2px !important;
259 259 padding-right: 2px;
260 260 text-align: right;
261 261 width: 32px;
262 262 -moz-user-select: none;
263 263 -webkit-user-select: none;
264 264 border-right: @border-thickness solid @grey5 !important;
265 265 border-left: 0px solid #CCC !important;
266 266 border-top: 0px solid #CCC !important;
267 267 border-bottom: none !important;
268 268
269 269 a {
270 270 &:extend(pre);
271 271 text-align: right;
272 272 padding-right: 2px;
273 273 cursor: pointer;
274 274 display: block;
275 275 width: 32px;
276 276 }
277 277 }
278 278
279 279 .context {
280 280 cursor: auto;
281 281 &:extend(pre);
282 282 }
283 283
284 284 .lineno-inline {
285 285 background: none repeat scroll 0 0 #FFF !important;
286 286 padding-left: 2px;
287 287 padding-right: 2px;
288 288 text-align: right;
289 289 width: 30px;
290 290 -moz-user-select: none;
291 291 -webkit-user-select: none;
292 292 }
293 293
294 294 /** CODE **/
295 295 .code {
296 296 display: block;
297 297 width: 100%;
298 298
299 299 td {
300 300 margin: 0;
301 301 padding: 0;
302 302 }
303 303
304 304 pre {
305 305 margin: 0;
306 306 padding: 0;
307 307 margin-left: .5em;
308 308 }
309 309 }
310 310 }
311 311
312 312
313 313 // Comments
314 314
315 315 div.comment:target {
316 316 border-left: 6px solid @comment-highlight-color !important;
317 317 padding-left: 3px;
318 318 margin-left: -9px;
319 319 }
320 320
321 321 //TODO: anderson: can't get an absolute number out of anything, so had to put the
322 322 //current values that might change. But to make it clear I put as a calculation
323 323 @comment-max-width: 1065px;
324 324 @pr-extra-margin: 34px;
325 325 @pr-border-spacing: 4px;
326 326 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
327 327
328 328 // Pull Request
329 329 .cs_files .code-difftable {
330 330 border: @border-thickness solid @grey5; //borders only on PRs
331 331
332 332 .comment-inline-form,
333 333 div.comment {
334 334 width: @pr-comment-width;
335 335 }
336 336 }
337 337
338 338 // Changeset
339 339 .code-difftable {
340 340 .comment-inline-form,
341 341 div.comment {
342 342 width: @comment-max-width;
343 343 }
344 344 }
345 345
346 346 //Style page
347 347 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
348 348 #style-page .code-difftable{
349 349 .comment-inline-form,
350 350 div.comment {
351 351 width: @comment-max-width - @style-extra-margin;
352 352 }
353 353 }
354 354
355 355 #context-bar > h2 {
356 356 font-size: 20px;
357 357 }
358 358
359 359 #context-bar > h2> a {
360 360 font-size: 20px;
361 361 }
362 362 // end of defaults
363 363
364 364 .file_diff_buttons {
365 365 padding: 0 0 @padding;
366 366
367 367 .drop-menu {
368 368 float: left;
369 369 margin: 0 @padding 0 0;
370 370 }
371 371 .btn {
372 372 margin: 0 @padding 0 0;
373 373 }
374 374 }
375 375
376 376 .code-body.textarea.editor {
377 377 max-width: none;
378 378 padding: 15px;
379 379 }
380 380
381 381 td.injected_diff{
382 382 max-width: 1178px;
383 383 overflow-x: auto;
384 384 overflow-y: hidden;
385 385
386 386 div.diff-container,
387 387 div.diffblock{
388 388 max-width: 100%;
389 389 }
390 390
391 391 div.code-body {
392 392 max-width: 1124px;
393 393 overflow-x: auto;
394 394 overflow-y: hidden;
395 395 padding: 0;
396 396 }
397 397 div.diffblock {
398 398 border: none;
399 399 }
400 400
401 401 &.inline-form {
402 402 width: 99%
403 403 }
404 404 }
405 405
406 406
407 407 table.code-difftable {
408 408 width: 100%;
409 409 }
410 410
411 411 /** PYGMENTS COLORING **/
412 412 div.codeblock {
413 413
414 414 // TODO: johbo: Added interim to get rid of the margin around
415 415 // Select2 widgets. This needs further cleanup.
416 416 margin-top: @padding;
417 417
418 418 overflow: auto;
419 419 padding: 0px;
420 420 border: @border-thickness solid @grey5;
421 421 background: @grey6;
422 422 .border-radius(@border-radius);
423 423
424 424 #remove_gist {
425 425 float: right;
426 426 }
427 427
428 428 .author {
429 429 clear: both;
430 430 vertical-align: middle;
431 431 font-family: @text-bold;
432 432 }
433 433
434 434 .btn-mini {
435 435 float: left;
436 436 margin: 0 5px 0 0;
437 437 }
438 438
439 439 .code-header {
440 440 padding: @padding;
441 441 border-bottom: @border-thickness solid @grey5;
442 442
443 443 .rc-user {
444 444 min-width: 0;
445 445 margin-right: .5em;
446 446 }
447 447
448 448 .stats {
449 449 clear: both;
450 450 margin: 0 0 @padding 0;
451 451 padding: 0;
452 452 .left {
453 453 float: left;
454 454 clear: left;
455 455 max-width: 75%;
456 456 margin: 0 0 @padding 0;
457 457
458 458 &.item {
459 459 margin-right: @padding;
460 460 &.last { border-right: none; }
461 461 }
462 462 }
463 463 .buttons { float: right; }
464 464 .author {
465 465 height: 25px; margin-left: 15px; font-weight: bold;
466 466 }
467 467 }
468 468
469 469 .commit {
470 470 margin: 5px 0 0 26px;
471 471 font-weight: normal;
472 472 white-space: pre-wrap;
473 473 }
474 474 }
475 475
476 476 .message {
477 477 position: relative;
478 478 margin: @padding;
479 479
480 480 .codeblock-label {
481 481 margin: 0 0 1em 0;
482 482 }
483 483 }
484 484
485 485 .code-body {
486 486 padding: @padding;
487 487 background-color: #ffffff;
488 488 min-width: 100%;
489 489 box-sizing: border-box;
490 490 // TODO: johbo: Parent has overflow: auto, this forces the child here
491 491 // to have the intended size and to scroll. Should be simplified.
492 492 width: 100%;
493 493 overflow-x: auto;
494 494 }
495 495 }
496 496
497 497 .code-highlighttable,
498 498 div.codeblock {
499 499
500 500 &.readme {
501 501 background-color: white;
502 502 }
503 503
504 504 .markdown-block table {
505 505 border-collapse: collapse;
506 506
507 507 th,
508 508 td {
509 509 padding: .5em;
510 510 border: @border-thickness solid @border-default-color;
511 511 }
512 512 }
513 513
514 514 table {
515 515 border: 0px;
516 516 margin: 0;
517 517 letter-spacing: normal;
518 518
519 519
520 520 td {
521 521 border: 0px;
522 522 vertical-align: top;
523 523 }
524 524 }
525 525 }
526 526
527 527 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
528 528 div.search-code-body {
529 529 background-color: #ffffff; padding: 5px 0 5px 10px;
530 530 pre {
531 531 .match { background-color: #faffa6;}
532 532 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
533 533 }
534 534 .code-highlighttable {
535 535 border-collapse: collapse;
536 536
537 537 tr:hover {
538 538 background: #fafafa;
539 539 }
540 540 td.code {
541 541 padding-left: 10px;
542 542 }
543 543 td.line {
544 544 border-right: 1px solid #ccc !important;
545 545 padding-right: 10px;
546 546 text-align: right;
547 547 font-family: "Lucida Console",Monaco,monospace;
548 548 span {
549 549 white-space: pre-wrap;
550 550 color: #666666;
551 551 }
552 552 }
553 553 }
554 554 }
555 555
556 556 div.annotatediv { margin-left: 2px; margin-right: 4px; }
557 557 .code-highlight {
558 558 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
559 559 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
560 560 pre div:target {background-color: @comment-highlight-color !important;}
561 561 }
562 562
563 563 .linenos a { text-decoration: none; }
564 564
565 565 .CodeMirror-selected { background: @rchighlightblue; }
566 566 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
567 567 .CodeMirror ::selection { background: @rchighlightblue; }
568 568 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
569 569
570 570 .code { display: block; border:0px !important; }
571 571 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
572 572 .codehilite {
573 573 .hll { background-color: #ffffcc }
574 574 .c { color: #408080; font-style: italic } /* Comment */
575 575 .err, .codehilite .err { border: @border-thickness solid #FF0000 } /* Error */
576 576 .k { color: #008000; font-weight: bold } /* Keyword */
577 577 .o { color: #666666 } /* Operator */
578 578 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
579 579 .cp { color: #BC7A00 } /* Comment.Preproc */
580 580 .c1 { color: #408080; font-style: italic } /* Comment.Single */
581 581 .cs { color: #408080; font-style: italic } /* Comment.Special */
582 582 .gd { color: #A00000 } /* Generic.Deleted */
583 583 .ge { font-style: italic } /* Generic.Emph */
584 584 .gr { color: #FF0000 } /* Generic.Error */
585 585 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
586 586 .gi { color: #00A000 } /* Generic.Inserted */
587 587 .go { color: #808080 } /* Generic.Output */
588 588 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
589 589 .gs { font-weight: bold } /* Generic.Strong */
590 590 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
591 591 .gt { color: #0040D0 } /* Generic.Traceback */
592 592 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
593 593 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
594 594 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
595 595 .kp { color: #008000 } /* Keyword.Pseudo */
596 596 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
597 597 .kt { color: #B00040 } /* Keyword.Type */
598 598 .m { color: #666666 } /* Literal.Number */
599 599 .s { color: #BA2121 } /* Literal.String */
600 600 .na { color: #7D9029 } /* Name.Attribute */
601 601 .nb { color: #008000 } /* Name.Builtin */
602 602 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
603 603 .no { color: #880000 } /* Name.Constant */
604 604 .nd { color: #AA22FF } /* Name.Decorator */
605 605 .ni { color: #999999; font-weight: bold } /* Name.Entity */
606 606 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
607 607 .nf { color: #0000FF } /* Name.Function */
608 608 .nl { color: #A0A000 } /* Name.Label */
609 609 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
610 610 .nt { color: #008000; font-weight: bold } /* Name.Tag */
611 611 .nv { color: #19177C } /* Name.Variable */
612 612 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
613 613 .w { color: #bbbbbb } /* Text.Whitespace */
614 614 .mf { color: #666666 } /* Literal.Number.Float */
615 615 .mh { color: #666666 } /* Literal.Number.Hex */
616 616 .mi { color: #666666 } /* Literal.Number.Integer */
617 617 .mo { color: #666666 } /* Literal.Number.Oct */
618 618 .sb { color: #BA2121 } /* Literal.String.Backtick */
619 619 .sc { color: #BA2121 } /* Literal.String.Char */
620 620 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
621 621 .s2 { color: #BA2121 } /* Literal.String.Double */
622 622 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
623 623 .sh { color: #BA2121 } /* Literal.String.Heredoc */
624 624 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
625 625 .sx { color: #008000 } /* Literal.String.Other */
626 626 .sr { color: #BB6688 } /* Literal.String.Regex */
627 627 .s1 { color: #BA2121 } /* Literal.String.Single */
628 628 .ss { color: #19177C } /* Literal.String.Symbol */
629 629 .bp { color: #008000 } /* Name.Builtin.Pseudo */
630 630 .vc { color: #19177C } /* Name.Variable.Class */
631 631 .vg { color: #19177C } /* Name.Variable.Global */
632 632 .vi { color: #19177C } /* Name.Variable.Instance */
633 633 .il { color: #666666 } /* Literal.Number.Integer.Long */
634 634 }
635 635
636 636 /* customized pre blocks for markdown/rst */
637 637 pre.literal-block, .codehilite pre{
638 638 padding: @padding;
639 639 border: 1px solid @grey6;
640 640 .border-radius(@border-radius);
641 641 background-color: @grey7;
642 642 }
643 643
644 644
645 645 /* START NEW CODE BLOCK CSS */
646 646
647 647 @cb-line-height: 18px;
648 648 @cb-line-code-padding: 10px;
649 649 @cb-text-padding: 5px;
650 650
651 651 @pill-padding: 2px 7px;
652 652
653 653 input.filediff-collapse-state {
654 654 display: none;
655 655
656 656 &:checked + .filediff { /* file diff is collapsed */
657 657 .cb {
658 658 display: none
659 659 }
660 660 .filediff-collapse-indicator {
661 661 width: 0;
662 662 height: 0;
663 663 border-style: solid;
664 664 border-width: 6.5px 0 6.5px 11.3px;
665 665 border-color: transparent transparent transparent #ccc;
666 666 }
667 667 .filediff-menu {
668 668 display: none;
669 669 }
670 670 margin: 10px 0 0 0;
671 671 }
672 672
673 673 &+ .filediff { /* file diff is expanded */
674 674 .filediff-collapse-indicator {
675 675 width: 0;
676 676 height: 0;
677 677 border-style: solid;
678 678 border-width: 11.3px 6.5px 0 6.5px;
679 679 border-color: #ccc transparent transparent transparent;
680 680 }
681 681 .filediff-menu {
682 682 display: block;
683 683 }
684 684 margin: 10px 0;
685 685 &:nth-child(2) {
686 686 margin: 0;
687 687 }
688 688 }
689 689 }
690 690 .cs_files {
691 691 clear: both;
692 692 }
693 693
694 694 .diffset-menu {
695 695 margin-bottom: 20px;
696 696 }
697 697 .diffset {
698 698 margin: 20px auto;
699 699 .diffset-heading {
700 700 border: 1px solid @grey5;
701 701 margin-bottom: -1px;
702 702 // margin-top: 20px;
703 703 h2 {
704 704 margin: 0;
705 705 line-height: 38px;
706 706 padding-left: 10px;
707 707 }
708 708 .btn {
709 709 margin: 0;
710 710 }
711 711 background: @grey6;
712 712 display: block;
713 713 padding: 5px;
714 714 }
715 715 .diffset-heading-warning {
716 716 background: @alert3-inner;
717 717 border: 1px solid @alert3;
718 718 }
719 719 &.diffset-comments-disabled {
720 720 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
721 721 display: none !important;
722 722 }
723 723 }
724 724 }
725 725
726 726 .pill {
727 727 display: block;
728 728 float: left;
729 729 padding: @pill-padding;
730 730 }
731 731 .pill-group {
732 732 .pill {
733 733 opacity: .8;
734 734 &:first-child {
735 735 border-radius: @border-radius 0 0 @border-radius;
736 736 }
737 737 &:last-child {
738 738 border-radius: 0 @border-radius @border-radius 0;
739 739 }
740 740 &:only-child {
741 741 border-radius: @border-radius;
742 742 }
743 743 }
744 744 }
745 745
746 746 /* Main comments*/
747 747 #comments {
748 748 .comment-selected {
749 749 border-left: 6px solid @comment-highlight-color;
750 750 padding-left: 3px;
751 751 margin-left: -9px;
752 752 }
753 753 }
754 754
755 755 .filediff {
756 756 border: 1px solid @grey5;
757 757
758 758 /* START OVERRIDES */
759 759 .code-highlight {
760 760 border: none; // TODO: remove this border from the global
761 761 // .code-highlight, it doesn't belong there
762 762 }
763 763 label {
764 764 margin: 0; // TODO: remove this margin definition from global label
765 765 // it doesn't belong there - if margin on labels
766 766 // are needed for a form they should be defined
767 767 // in the form's class
768 768 }
769 769 /* END OVERRIDES */
770 770
771 771 * {
772 772 box-sizing: border-box;
773 773 }
774 774 .filediff-anchor {
775 775 visibility: hidden;
776 776 }
777 777 &:hover {
778 778 .filediff-anchor {
779 779 visibility: visible;
780 780 }
781 781 }
782 782
783 783 .filediff-collapse-indicator {
784 784 border-style: solid;
785 785 float: left;
786 786 margin: 4px 0px 0 0;
787 787 cursor: pointer;
788 788 }
789 789
790 790 .filediff-heading {
791 791 background: @grey7;
792 792 cursor: pointer;
793 793 display: block;
794 794 padding: 5px 10px;
795 795 }
796 796 .filediff-heading:after {
797 797 content: "";
798 798 display: table;
799 799 clear: both;
800 800 }
801 801 .filediff-heading:hover {
802 802 background: #e1e9f4 !important;
803 803 }
804 804
805 805 .filediff-menu {
806 806 float: right;
807 807 text-align: right;
808 808 padding: 5px 5px 5px 0px;
809 809
810 810 &> a,
811 811 &> span {
812 812 padding: 1px;
813 813 }
814 814 }
815 815
816 816 .pill {
817 817 &[op="name"] {
818 818 background: none;
819 819 color: @grey2;
820 820 opacity: 1;
821 821 color: white;
822 822 }
823 823 &[op="limited"] {
824 824 background: @grey2;
825 825 color: white;
826 826 }
827 827 &[op="binary"] {
828 828 background: @color7;
829 829 color: white;
830 830 }
831 831 &[op="modified"] {
832 832 background: @alert1;
833 833 color: white;
834 834 }
835 835 &[op="renamed"] {
836 836 background: @color4;
837 837 color: white;
838 838 }
839 839 &[op="mode"] {
840 840 background: @grey3;
841 841 color: white;
842 842 }
843 843 &[op="symlink"] {
844 844 background: @color8;
845 845 color: white;
846 846 }
847 847
848 848 &[op="added"] { /* added lines */
849 849 background: @alert1;
850 850 color: white;
851 851 }
852 852 &[op="deleted"] { /* deleted lines */
853 853 background: @alert2;
854 854 color: white;
855 855 }
856 856
857 857 &[op="created"] { /* created file */
858 858 background: @alert1;
859 859 color: white;
860 860 }
861 861 &[op="removed"] { /* deleted file */
862 862 background: @color5;
863 863 color: white;
864 864 }
865 865 }
866 866
867 867 .filediff-collapse-button, .filediff-expand-button {
868 868 cursor: pointer;
869 869 }
870 870 .filediff-collapse-button {
871 871 display: inline;
872 872 }
873 873 .filediff-expand-button {
874 874 display: none;
875 875 }
876 876 .filediff-collapsed .filediff-collapse-button {
877 877 display: none;
878 878 }
879 879 .filediff-collapsed .filediff-expand-button {
880 880 display: inline;
881 881 }
882 882
883 @comment-padding: 5px;
884
885 883 /**** COMMENTS ****/
886 884
887 885 .filediff-menu {
888 886 .show-comment-button {
889 887 display: none;
890 888 }
891 889 }
892 890 &.hide-comments {
893 891 .inline-comments {
894 892 display: none;
895 893 }
896 894 .filediff-menu {
897 895 .show-comment-button {
898 896 display: inline;
899 897 }
900 898 .hide-comment-button {
901 899 display: none;
902 900 }
903 901 }
904 902 }
905 903
906 904 .hide-line-comments {
907 905 .inline-comments {
908 906 display: none;
909 907 }
910 908 }
911 909
912 .inline-comments {
913 border-radius: @border-radius;
914 .comment {
915 margin: 0;
916 border-radius: @border-radius;
917 }
918 .comment-outdated {
919 opacity: 0.5;
920 }
921
922 .comment-inline {
923 background: white;
924 padding: (@comment-padding + 3px) @comment-padding;
925 border: @comment-padding solid @grey6;
926
927 .text {
928 border: none;
929 }
930 .meta {
931 border-bottom: 1px solid @grey6;
932 padding-bottom: 10px;
933 }
934 }
935 .comment-selected {
936 border-left: 6px solid @comment-highlight-color;
937 }
938 .comment-inline-form {
939 padding: @comment-padding;
940 display: none;
941 }
942 .cb-comment-add-button {
943 margin: @comment-padding;
944 }
945 /* hide add comment button when form is open */
946 .comment-inline-form-open ~ .cb-comment-add-button {
947 display: none;
948 }
949 .comment-inline-form-open {
950 display: block;
951 }
952 /* hide add comment button when form but no comments */
953 .comment-inline-form:first-child + .cb-comment-add-button {
954 display: none;
955 }
956 /* hide add comment button when no comments or form */
957 .cb-comment-add-button:first-child {
958 display: none;
959 }
960 /* hide add comment button when only comment is being deleted */
961 .comment-deleting:first-child + .cb-comment-add-button {
962 display: none;
963 }
964 }
965 910 /**** END COMMENTS ****/
966 911
967 912 }
968 913
969 914 .filediff-outdated {
970 915 padding: 8px 0;
971 916
972 917 .filediff-heading {
973 918 opacity: .5;
974 919 }
975 920 }
976 921
977 922 table.cb {
978 923 width: 100%;
979 924 border-collapse: collapse;
980 925
981 926 .cb-text {
982 927 padding: @cb-text-padding;
983 928 }
984 929 .cb-hunk {
985 930 padding: @cb-text-padding;
986 931 }
987 932 .cb-expand {
988 933 display: none;
989 934 }
990 935 .cb-collapse {
991 936 display: inline;
992 937 }
993 938 &.cb-collapsed {
994 939 .cb-line {
995 940 display: none;
996 941 }
997 942 .cb-expand {
998 943 display: inline;
999 944 }
1000 945 .cb-collapse {
1001 946 display: none;
1002 947 }
1003 948 }
1004 949
1005 950 /* intentionally general selector since .cb-line-selected must override it
1006 951 and they both use !important since the td itself may have a random color
1007 952 generated by annotation blocks. TLDR: if you change it, make sure
1008 953 annotated block selection and line selection in file view still work */
1009 954 .cb-line-fresh .cb-content {
1010 955 background: white !important;
1011 956 }
1012 957 .cb-warning {
1013 958 background: #fff4dd;
1014 959 }
1015 960
1016 961 &.cb-diff-sideside {
1017 962 td {
1018 963 &.cb-content {
1019 964 width: 50%;
1020 965 }
1021 966 }
1022 967 }
1023 968
1024 969 tr {
1025 970 &.cb-annotate {
1026 971 border-top: 1px solid #eee;
1027 972
1028 973 &+ .cb-line {
1029 974 border-top: 1px solid #eee;
1030 975 }
1031 976
1032 977 &:first-child {
1033 978 border-top: none;
1034 979 &+ .cb-line {
1035 980 border-top: none;
1036 981 }
1037 982 }
1038 983 }
1039 984
1040 985 &.cb-hunk {
1041 986 font-family: @font-family-monospace;
1042 987 color: rgba(0, 0, 0, 0.3);
1043 988
1044 989 td {
1045 990 &:first-child {
1046 991 background: #edf2f9;
1047 992 }
1048 993 &:last-child {
1049 994 background: #f4f7fb;
1050 995 }
1051 996 }
1052 997 }
1053 998 }
1054 999
1055 1000
1056 1001 td {
1057 1002 vertical-align: top;
1058 1003 padding: 0;
1059 1004
1060 1005 &.cb-content {
1061 1006 font-size: 12.35px;
1062 1007
1063 1008 &.cb-line-selected .cb-code {
1064 1009 background: @comment-highlight-color !important;
1065 1010 }
1066 1011
1067 1012 span.cb-code {
1068 1013 line-height: @cb-line-height;
1069 1014 padding-left: @cb-line-code-padding;
1070 1015 padding-right: @cb-line-code-padding;
1071 1016 display: block;
1072 1017 white-space: pre-wrap;
1073 1018 font-family: @font-family-monospace;
1074 1019 word-break: break-word;
1075 1020 .nonl {
1076 1021 color: @color5;
1077 1022 }
1078 1023 }
1079 1024
1080 1025 &> button.cb-comment-box-opener {
1081 1026
1082 1027 padding: 2px 2px 1px 3px;
1083 1028 margin-left: -6px;
1084 1029 margin-top: -1px;
1085 1030
1086 1031 border-radius: @border-radius;
1087 1032 position: absolute;
1088 1033 display: none;
1089 1034 }
1090 1035 .cb-comment {
1091 1036 margin-top: 10px;
1092 1037 white-space: normal;
1093 1038 }
1094 1039 }
1095 1040 &:hover {
1096 1041 button.cb-comment-box-opener {
1097 1042 display: block;
1098 1043 }
1099 1044 &+ td button.cb-comment-box-opener {
1100 1045 display: block
1101 1046 }
1102 1047 }
1103 1048
1104 1049 &.cb-data {
1105 1050 text-align: right;
1106 1051 width: 30px;
1107 1052 font-family: @font-family-monospace;
1108 1053
1109 1054 .icon-comment {
1110 1055 cursor: pointer;
1111 1056 }
1112 1057 &.cb-line-selected > div {
1113 1058 display: block;
1114 1059 background: @comment-highlight-color !important;
1115 1060 line-height: @cb-line-height;
1116 1061 color: rgba(0, 0, 0, 0.3);
1117 1062 }
1118 1063 }
1119 1064
1120 1065 &.cb-lineno {
1121 1066 padding: 0;
1122 1067 width: 50px;
1123 1068 color: rgba(0, 0, 0, 0.3);
1124 1069 text-align: right;
1125 1070 border-right: 1px solid #eee;
1126 1071 font-family: @font-family-monospace;
1127 1072
1128 1073 a::before {
1129 1074 content: attr(data-line-no);
1130 1075 }
1131 1076 &.cb-line-selected a {
1132 1077 background: @comment-highlight-color !important;
1133 1078 }
1134 1079
1135 1080 a {
1136 1081 display: block;
1137 1082 padding-right: @cb-line-code-padding;
1138 1083 padding-left: @cb-line-code-padding;
1139 1084 line-height: @cb-line-height;
1140 1085 color: rgba(0, 0, 0, 0.3);
1141 1086 }
1142 1087 }
1143 1088
1144 1089 &.cb-empty {
1145 1090 background: @grey7;
1146 1091 }
1147 1092
1148 1093 ins {
1149 1094 color: black;
1150 1095 background: #a6f3a6;
1151 1096 text-decoration: none;
1152 1097 }
1153 1098 del {
1154 1099 color: black;
1155 1100 background: #f8cbcb;
1156 1101 text-decoration: none;
1157 1102 }
1158 1103 &.cb-addition {
1159 1104 background: #ecffec;
1160 1105
1161 1106 &.blob-lineno {
1162 1107 background: #ddffdd;
1163 1108 }
1164 1109 }
1165 1110 &.cb-deletion {
1166 1111 background: #ffecec;
1167 1112
1168 1113 &.blob-lineno {
1169 1114 background: #ffdddd;
1170 1115 }
1171 1116 }
1172 1117
1173 1118 &.cb-annotate-info {
1174 1119 width: 320px;
1175 1120 min-width: 320px;
1176 1121 max-width: 320px;
1177 1122 padding: 5px 2px;
1178 1123 font-size: 13px;
1179 1124
1180 1125 strong.cb-annotate-message {
1181 1126 padding: 5px 0;
1182 1127 white-space: pre-line;
1183 1128 display: inline-block;
1184 1129 }
1185 1130 .rc-user {
1186 1131 float: none;
1187 1132 padding: 0 6px 0 17px;
1188 1133 min-width: auto;
1189 1134 min-height: auto;
1190 1135 }
1191 1136 }
1192 1137
1193 1138 &.cb-annotate-revision {
1194 1139 cursor: pointer;
1195 1140 text-align: right;
1196 1141 }
1197 1142 }
1198 1143 }
@@ -1,432 +1,527 b''
1 1 // comments.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5
6 6 // Comments
7 7 .comments {
8 8 width: 100%;
9 9 }
10 10
11 11 tr.inline-comments div {
12 12 max-width: 100%;
13 13
14 14 p {
15 15 white-space: normal;
16 16 }
17 17
18 18 code, pre, .code, dd {
19 19 overflow-x: auto;
20 20 width: 1062px;
21 21 }
22 22
23 23 dd {
24 24 width: auto;
25 25 }
26 26 }
27 27
28 28 #injected_page_comments {
29 29 .comment-previous-link,
30 30 .comment-next-link,
31 31 .comment-links-divider {
32 32 display: none;
33 33 }
34 34 }
35 35
36 36 .add-comment {
37 37 margin-bottom: 10px;
38 38 }
39 39 .hide-comment-button .add-comment {
40 40 display: none;
41 41 }
42 42
43 43 .comment-bubble {
44 44 color: @grey4;
45 45 margin-top: 4px;
46 46 margin-right: 30px;
47 47 visibility: hidden;
48 48 }
49 49
50 .comment-label {
51 float: left;
52
53 padding: 0.4em 0.4em;
54 margin: 2px 5px 0px -10px;
55 display: inline-block;
56 min-height: 0;
57
58 text-align: center;
59 font-size: 10px;
60 line-height: .8em;
61
62 font-family: @text-italic;
63 background: #fff none;
64 color: @grey4;
65 border: 1px solid @grey4;
66 white-space: nowrap;
67
68 text-transform: uppercase;
69
70 &.todo {
71 color: @color5;
72 font-family: @text-bold-italic;
73 }
74 }
75
76
50 77 .comment {
51 78
52 79 &.comment-general {
53 80 border: 1px solid @grey5;
54 81 padding: 5px 5px 5px 5px;
55 82 }
56 83
57 84 margin: @padding 0;
58 85 padding: 4px 0 0 0;
59 86 line-height: 1em;
60 87
61 88 .rc-user {
62 89 min-width: 0;
63 margin: -2px .5em 0 0;
90 margin: 0px .5em 0 0;
91
92 .user {
93 display: inline;
94 }
64 95 }
65 96
66 97 .meta {
67 98 position: relative;
68 99 width: 100%;
69 margin: 0 0 .5em 0;
70 100 border-bottom: 1px solid @grey5;
71 padding: 8px 0px;
101 margin: -5px 0px;
102 line-height: 24px;
72 103
73 104 &:hover .permalink {
74 105 visibility: visible;
75 106 color: @rcblue;
76 107 }
77 108 }
78 109
79 110 .author,
80 111 .date {
81 112 display: inline;
82 113
83 114 &:after {
84 115 content: ' | ';
85 116 color: @grey5;
86 117 }
87 118 }
88 119
89 120 .author-general img {
90 top: -3px;
121 top: 3px;
91 122 }
92 123 .author-inline img {
93 top: -3px;
124 top: 3px;
94 125 }
95 126
96 127 .status-change,
97 128 .permalink,
98 129 .changeset-status-lbl {
99 130 display: inline;
100 131 }
101 132
102 133 .permalink {
103 134 visibility: hidden;
104 135 }
105 136
106 137 .comment-links-divider {
107 138 display: inline;
108 139 }
109 140
110 141 .comment-links-block {
111 142 float:right;
112 143 text-align: right;
113 144 min-width: 85px;
114 145
115 146 [class^="icon-"]:before,
116 147 [class*=" icon-"]:before {
117 148 margin-left: 0;
118 149 margin-right: 0;
119 150 }
120 151 }
121 152
122 153 .comment-previous-link {
123 154 display: inline-block;
124 155
125 156 .arrow_comment_link{
126 157 cursor: pointer;
127 158 i {
128 159 font-size:10px;
129 160 }
130 161 }
131 162 .arrow_comment_link.disabled {
132 163 cursor: default;
133 164 color: @grey5;
134 165 }
135 166 }
136 167
137 168 .comment-next-link {
138 169 display: inline-block;
139 170
140 171 .arrow_comment_link{
141 172 cursor: pointer;
142 173 i {
143 174 font-size:10px;
144 175 }
145 176 }
146 177 .arrow_comment_link.disabled {
147 178 cursor: default;
148 179 color: @grey5;
149 180 }
150 181 }
151 182
152 183 .flag_status {
153 184 display: inline-block;
154 185 margin: -2px .5em 0 .25em
155 186 }
156 187
157 188 .delete-comment {
158 189 display: inline-block;
159 190 color: @rcblue;
160 191
161 192 &:hover {
162 193 cursor: pointer;
163 194 }
164 195 }
165 196
166 197
167 198 .text {
168 199 clear: both;
169 200 .border-radius(@border-radius);
170 201 .box-sizing(border-box);
171 202
172 203 .markdown-block p,
173 204 .rst-block p {
174 205 margin: .5em 0 !important;
175 206 // TODO: lisa: This is needed because of other rst !important rules :[
176 207 }
177 208 }
178 209
179 210 .pr-version {
180 211 float: left;
181 212 margin: 0px 4px;
182 213 }
183 214 .pr-version-inline {
184 215 float: left;
185 margin: 1px 4px;
216 margin: 0px 4px;
186 217 }
187 218 .pr-version-num {
188 219 font-size: 10px;
189 220 }
190 221
191 222 }
192 223
224 @comment-padding: 5px;
225
226 .inline-comments {
227 border-radius: @border-radius;
228 .comment {
229 margin: 0;
230 border-radius: @border-radius;
231 }
232 .comment-outdated {
233 opacity: 0.5;
234 }
235
236 .comment-inline {
237 background: white;
238 padding: @comment-padding @comment-padding;
239 border: @comment-padding solid @grey6;
240
241 .text {
242 border: none;
243 }
244 .meta {
245 border-bottom: 1px solid @grey6;
246 margin: -5px 0px;
247 line-height: 24px;
248 }
249 }
250 .comment-selected {
251 border-left: 6px solid @comment-highlight-color;
252 }
253 .comment-inline-form {
254 padding: @comment-padding;
255 display: none;
256 }
257 .cb-comment-add-button {
258 margin: @comment-padding;
259 }
260 /* hide add comment button when form is open */
261 .comment-inline-form-open ~ .cb-comment-add-button {
262 display: none;
263 }
264 .comment-inline-form-open {
265 display: block;
266 }
267 /* hide add comment button when form but no comments */
268 .comment-inline-form:first-child + .cb-comment-add-button {
269 display: none;
270 }
271 /* hide add comment button when no comments or form */
272 .cb-comment-add-button:first-child {
273 display: none;
274 }
275 /* hide add comment button when only comment is being deleted */
276 .comment-deleting:first-child + .cb-comment-add-button {
277 display: none;
278 }
279 }
280
281
193 282 .show-outdated-comments {
194 283 display: inline;
195 284 color: @rcblue;
196 285 }
197 286
198 287 // Comment Form
199 288 div.comment-form {
200 289 margin-top: 20px;
201 290 }
202 291
203 292 .comment-form strong {
204 293 display: block;
205 294 margin-bottom: 15px;
206 295 }
207 296
208 297 .comment-form textarea {
209 298 width: 100%;
210 299 height: 100px;
211 300 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
212 301 }
213 302
214 303 form.comment-form {
215 304 margin-top: 10px;
216 305 margin-left: 10px;
217 306 }
218 307
219 308 .comment-inline-form .comment-block-ta,
220 309 .comment-form .comment-block-ta,
221 310 .comment-form .preview-box {
222 311 .border-radius(@border-radius);
223 312 .box-sizing(border-box);
224 313 background-color: white;
225 314 }
226 315
227 316 .comment-form-submit {
228 317 margin-top: 5px;
229 318 margin-left: 525px;
230 319 }
231 320
232 321 .file-comments {
233 322 display: none;
234 323 }
235 324
236 325 .comment-form .preview-box.unloaded,
237 326 .comment-inline-form .preview-box.unloaded {
238 327 height: 50px;
239 328 text-align: center;
240 329 padding: 20px;
241 330 background-color: white;
242 331 }
243 332
244 333 .comment-footer {
245 334 position: relative;
246 335 width: 100%;
247 336 min-height: 42px;
248 337
249 338 .status_box,
250 339 .cancel-button {
251 340 float: left;
252 341 display: inline-block;
253 342 }
254 343
255 344 .action-buttons {
256 345 float: right;
257 346 display: inline-block;
258 347 }
259 348 }
260 349
261 350 .comment-form {
262 351
263 352 .comment {
264 353 margin-left: 10px;
265 354 }
266 355
267 356 .comment-help {
268 357 color: @grey4;
269 358 padding: 5px 0 5px 0;
270 359 }
271 360
272 361 .comment-title {
273 362 padding: 5px 0 5px 0;
274 363 }
275 364
276 365 .comment-button {
277 366 display: inline-block;
278 367 }
279 368
280 369 .comment-button .comment-button-input {
281 370 margin-right: 0;
282 371 }
283 372
284 373 .comment-footer {
285 374 margin-bottom: 110px;
286 375 margin-top: 10px;
287 376 }
288 377 }
289 378
290 379
291 380 .comment-form-login {
292 381 .comment-help {
293 382 padding: 0.9em; //same as the button
294 383 }
295 384
296 385 div.clearfix {
297 386 clear: both;
298 387 width: 100%;
299 388 display: block;
300 389 }
301 390 }
302 391
392 .comment-type {
393 margin: 0px;
394 border-radius: inherit;
395 border-color: @grey6;
396 }
397
303 398 .preview-box {
304 399 min-height: 105px;
305 400 margin-bottom: 15px;
306 401 background-color: white;
307 402 .border-radius(@border-radius);
308 403 .box-sizing(border-box);
309 404 }
310 405
311 406 .add-another-button {
312 407 margin-left: 10px;
313 408 margin-top: 10px;
314 409 margin-bottom: 10px;
315 410 }
316 411
317 412 .comment .buttons {
318 413 float: right;
319 414 margin: -1px 0px 0px 0px;
320 415 }
321 416
322 417 // Inline Comment Form
323 418 .injected_diff .comment-inline-form,
324 419 .comment-inline-form {
325 420 background-color: white;
326 421 margin-top: 10px;
327 422 margin-bottom: 20px;
328 423 }
329 424
330 425 .inline-form {
331 426 padding: 10px 7px;
332 427 }
333 428
334 429 .inline-form div {
335 430 max-width: 100%;
336 431 }
337 432
338 433 .overlay {
339 434 display: none;
340 435 position: absolute;
341 436 width: 100%;
342 437 text-align: center;
343 438 vertical-align: middle;
344 439 font-size: 16px;
345 440 background: none repeat scroll 0 0 white;
346 441
347 442 &.submitting {
348 443 display: block;
349 444 opacity: 0.5;
350 445 z-index: 100;
351 446 }
352 447 }
353 448 .comment-inline-form .overlay.submitting .overlay-text {
354 449 margin-top: 5%;
355 450 }
356 451
357 452 .comment-inline-form .clearfix,
358 453 .comment-form .clearfix {
359 454 .border-radius(@border-radius);
360 455 margin: 0px;
361 456 }
362 457
363 458 .comment-inline-form .comment-footer {
364 459 margin: 10px 0px 0px 0px;
365 460 }
366 461
367 462 .hide-inline-form-button {
368 463 margin-left: 5px;
369 464 }
370 465 .comment-button .hide-inline-form {
371 466 background: white;
372 467 }
373 468
374 469 .comment-area {
375 470 padding: 8px 12px;
376 471 border: 1px solid @grey5;
377 472 .border-radius(@border-radius);
378 473 }
379 474
380 475 .comment-area-header .nav-links {
381 476 display: flex;
382 477 flex-flow: row wrap;
383 478 -webkit-flex-flow: row wrap;
384 479 width: 100%;
385 480 }
386 481
387 482 .comment-area-footer {
388 483 display: flex;
389 484 }
390 485
391 486 .comment-footer .toolbar {
392 487
393 488 }
394 489
395 490 .nav-links {
396 491 padding: 0;
397 492 margin: 0;
398 493 list-style: none;
399 494 height: auto;
400 495 border-bottom: 1px solid @grey5;
401 496 }
402 497 .nav-links li {
403 498 display: inline-block;
404 499 }
405 500 .nav-links li:before {
406 501 content: "";
407 502 }
408 503 .nav-links li a.disabled {
409 504 cursor: not-allowed;
410 505 }
411 506
412 507 .nav-links li.active a {
413 508 border-bottom: 2px solid @rcblue;
414 509 color: #000;
415 510 font-weight: 600;
416 511 }
417 512 .nav-links li a {
418 513 display: inline-block;
419 514 padding: 0px 10px 5px 10px;
420 515 margin-bottom: -1px;
421 516 font-size: 14px;
422 517 line-height: 28px;
423 518 color: #8f8f8f;
424 519 border-bottom: 2px solid transparent;
425 520 }
426 521
427 522 .toolbar-text {
428 523 float: left;
429 524 margin: -5px 0px 0px 0px;
430 525 font-size: 12px;
431 526 }
432 527
@@ -1,2222 +1,2223 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 29
30 30 //--- BASE ------------------//
31 31 .noscript-error {
32 32 top: 0;
33 33 left: 0;
34 34 width: 100%;
35 35 z-index: 101;
36 36 text-align: center;
37 37 font-family: @text-semibold;
38 38 font-size: 120%;
39 39 color: white;
40 40 background-color: @alert2;
41 41 padding: 5px 0 5px 0;
42 42 }
43 43
44 44 html {
45 45 display: table;
46 46 height: 100%;
47 47 width: 100%;
48 48 }
49 49
50 50 body {
51 51 display: table-cell;
52 52 width: 100%;
53 53 }
54 54
55 55 //--- LAYOUT ------------------//
56 56
57 57 .hidden{
58 58 display: none !important;
59 59 }
60 60
61 61 .box{
62 62 float: left;
63 63 width: 100%;
64 64 }
65 65
66 66 .browser-header {
67 67 clear: both;
68 68 }
69 69 .main {
70 70 clear: both;
71 71 padding:0 0 @pagepadding;
72 72 height: auto;
73 73
74 74 &:after { //clearfix
75 75 content:"";
76 76 clear:both;
77 77 width:100%;
78 78 display:block;
79 79 }
80 80 }
81 81
82 82 .action-link{
83 83 margin-left: @padding;
84 84 padding-left: @padding;
85 85 border-left: @border-thickness solid @border-default-color;
86 86 }
87 87
88 88 input + .action-link, .action-link.first{
89 89 border-left: none;
90 90 }
91 91
92 92 .action-link.last{
93 93 margin-right: @padding;
94 94 padding-right: @padding;
95 95 }
96 96
97 97 .action-link.active,
98 98 .action-link.active a{
99 99 color: @grey4;
100 100 }
101 101
102 102 ul.simple-list{
103 103 list-style: none;
104 104 margin: 0;
105 105 padding: 0;
106 106 }
107 107
108 108 .main-content {
109 109 padding-bottom: @pagepadding;
110 110 }
111 111
112 112 .wide-mode-wrapper {
113 113 max-width:4000px !important;
114 114 }
115 115
116 116 .wrapper {
117 117 position: relative;
118 118 max-width: @wrapper-maxwidth;
119 119 margin: 0 auto;
120 120 }
121 121
122 122 #content {
123 123 clear: both;
124 124 padding: 0 @contentpadding;
125 125 }
126 126
127 127 .advanced-settings-fields{
128 128 input{
129 129 margin-left: @textmargin;
130 130 margin-right: @padding/2;
131 131 }
132 132 }
133 133
134 134 .cs_files_title {
135 135 margin: @pagepadding 0 0;
136 136 }
137 137
138 138 input.inline[type="file"] {
139 139 display: inline;
140 140 }
141 141
142 142 .error_page {
143 143 margin: 10% auto;
144 144
145 145 h1 {
146 146 color: @grey2;
147 147 }
148 148
149 149 .alert {
150 150 margin: @padding 0;
151 151 }
152 152
153 153 .error-branding {
154 154 font-family: @text-semibold;
155 155 color: @grey4;
156 156 }
157 157
158 158 .error_message {
159 159 font-family: @text-regular;
160 160 }
161 161
162 162 .sidebar {
163 163 min-height: 275px;
164 164 margin: 0;
165 165 padding: 0 0 @sidebarpadding @sidebarpadding;
166 166 border: none;
167 167 }
168 168
169 169 .main-content {
170 170 position: relative;
171 171 margin: 0 @sidebarpadding @sidebarpadding;
172 172 padding: 0 0 0 @sidebarpadding;
173 173 border-left: @border-thickness solid @grey5;
174 174
175 175 @media (max-width:767px) {
176 176 clear: both;
177 177 width: 100%;
178 178 margin: 0;
179 179 border: none;
180 180 }
181 181 }
182 182
183 183 .inner-column {
184 184 float: left;
185 185 width: 29.75%;
186 186 min-height: 150px;
187 187 margin: @sidebarpadding 2% 0 0;
188 188 padding: 0 2% 0 0;
189 189 border-right: @border-thickness solid @grey5;
190 190
191 191 @media (max-width:767px) {
192 192 clear: both;
193 193 width: 100%;
194 194 border: none;
195 195 }
196 196
197 197 ul {
198 198 padding-left: 1.25em;
199 199 }
200 200
201 201 &:last-child {
202 202 margin: @sidebarpadding 0 0;
203 203 border: none;
204 204 }
205 205
206 206 h4 {
207 207 margin: 0 0 @padding;
208 208 font-family: @text-semibold;
209 209 }
210 210 }
211 211 }
212 212 .error-page-logo {
213 213 width: 130px;
214 214 height: 160px;
215 215 }
216 216
217 217 // HEADER
218 218 .header {
219 219
220 220 // TODO: johbo: Fix login pages, so that they work without a min-height
221 221 // for the header and then remove the min-height. I chose a smaller value
222 222 // intentionally here to avoid rendering issues in the main navigation.
223 223 min-height: 49px;
224 224
225 225 position: relative;
226 226 vertical-align: bottom;
227 227 padding: 0 @header-padding;
228 228 background-color: @grey2;
229 229 color: @grey5;
230 230
231 231 .title {
232 232 overflow: visible;
233 233 }
234 234
235 235 &:before,
236 236 &:after {
237 237 content: "";
238 238 clear: both;
239 239 width: 100%;
240 240 }
241 241
242 242 // TODO: johbo: Avoids breaking "Repositories" chooser
243 243 .select2-container .select2-choice .select2-arrow {
244 244 display: none;
245 245 }
246 246 }
247 247
248 248 #header-inner {
249 249 &.title {
250 250 margin: 0;
251 251 }
252 252 &:before,
253 253 &:after {
254 254 content: "";
255 255 clear: both;
256 256 }
257 257 }
258 258
259 259 // Gists
260 260 #files_data {
261 261 clear: both; //for firefox
262 262 }
263 263 #gistid {
264 264 margin-right: @padding;
265 265 }
266 266
267 267 // Global Settings Editor
268 268 .textarea.editor {
269 269 float: left;
270 270 position: relative;
271 271 max-width: @texteditor-width;
272 272
273 273 select {
274 274 position: absolute;
275 275 top:10px;
276 276 right:0;
277 277 }
278 278
279 279 .CodeMirror {
280 280 margin: 0;
281 281 }
282 282
283 283 .help-block {
284 284 margin: 0 0 @padding;
285 285 padding:.5em;
286 286 background-color: @grey6;
287 287 }
288 288 }
289 289
290 290 ul.auth_plugins {
291 291 margin: @padding 0 @padding @legend-width;
292 292 padding: 0;
293 293
294 294 li {
295 295 margin-bottom: @padding;
296 296 line-height: 1em;
297 297 list-style-type: none;
298 298
299 299 .auth_buttons .btn {
300 300 margin-right: @padding;
301 301 }
302 302
303 303 &:before { content: none; }
304 304 }
305 305 }
306 306
307 307
308 308 // My Account PR list
309 309
310 310 #show_closed {
311 311 margin: 0 1em 0 0;
312 312 }
313 313
314 314 .pullrequestlist {
315 315 .closed {
316 316 background-color: @grey6;
317 317 }
318 318 .td-status {
319 319 padding-left: .5em;
320 320 }
321 321 .log-container .truncate {
322 322 height: 2.75em;
323 323 white-space: pre-line;
324 324 }
325 325 table.rctable .user {
326 326 padding-left: 0;
327 327 }
328 328 table.rctable {
329 329 td.td-description,
330 330 .rc-user {
331 331 min-width: auto;
332 332 }
333 333 }
334 334 }
335 335
336 336 // Pull Requests
337 337
338 338 .pullrequests_section_head {
339 339 display: block;
340 340 clear: both;
341 341 margin: @padding 0;
342 342 font-family: @text-bold;
343 343 }
344 344
345 345 .pr-origininfo, .pr-targetinfo {
346 346 position: relative;
347 347
348 348 .tag {
349 349 display: inline-block;
350 350 margin: 0 1em .5em 0;
351 351 }
352 352
353 353 .clone-url {
354 354 display: inline-block;
355 355 margin: 0 0 .5em 0;
356 356 padding: 0;
357 357 line-height: 1.2em;
358 358 }
359 359 }
360 360
361 361 .pr-pullinfo {
362 362 clear: both;
363 363 margin: .5em 0;
364 364 }
365 365
366 366 #pr-title-input {
367 367 width: 72%;
368 368 font-size: 1em;
369 369 font-family: @text-bold;
370 370 margin: 0;
371 371 padding: 0 0 0 @padding/4;
372 372 line-height: 1.7em;
373 373 color: @text-color;
374 374 letter-spacing: .02em;
375 375 }
376 376
377 377 #pullrequest_title {
378 378 width: 100%;
379 379 box-sizing: border-box;
380 380 }
381 381
382 382 #pr_open_message {
383 383 border: @border-thickness solid #fff;
384 384 border-radius: @border-radius;
385 385 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
386 386 text-align: right;
387 387 overflow: hidden;
388 388 }
389 389
390 390 .pr-submit-button {
391 391 float: right;
392 392 margin: 0 0 0 5px;
393 393 }
394 394
395 395 .pr-spacing-container {
396 396 padding: 20px;
397 397 clear: both
398 398 }
399 399
400 400 #pr-description-input {
401 401 margin-bottom: 0;
402 402 }
403 403
404 404 .pr-description-label {
405 405 vertical-align: top;
406 406 }
407 407
408 408 .perms_section_head {
409 409 min-width: 625px;
410 410
411 411 h2 {
412 412 margin-bottom: 0;
413 413 }
414 414
415 415 .label-checkbox {
416 416 float: left;
417 417 }
418 418
419 419 &.field {
420 420 margin: @space 0 @padding;
421 421 }
422 422
423 423 &:first-child.field {
424 424 margin-top: 0;
425 425
426 426 .label {
427 427 margin-top: 0;
428 428 padding-top: 0;
429 429 }
430 430
431 431 .radios {
432 432 padding-top: 0;
433 433 }
434 434 }
435 435
436 436 .radios {
437 437 float: right;
438 438 position: relative;
439 439 width: 405px;
440 440 }
441 441 }
442 442
443 443 //--- MODULES ------------------//
444 444
445 445
446 446 // Server Announcement
447 447 #server-announcement {
448 448 width: 95%;
449 449 margin: @padding auto;
450 450 padding: @padding;
451 451 border-width: 2px;
452 452 border-style: solid;
453 453 .border-radius(2px);
454 454 font-family: @text-bold;
455 455
456 456 &.info { border-color: @alert4; background-color: @alert4-inner; }
457 457 &.warning { border-color: @alert3; background-color: @alert3-inner; }
458 458 &.error { border-color: @alert2; background-color: @alert2-inner; }
459 459 &.success { border-color: @alert1; background-color: @alert1-inner; }
460 460 &.neutral { border-color: @grey3; background-color: @grey6; }
461 461 }
462 462
463 463 // Fixed Sidebar Column
464 464 .sidebar-col-wrapper {
465 465 padding-left: @sidebar-all-width;
466 466
467 467 .sidebar {
468 468 width: @sidebar-width;
469 469 margin-left: -@sidebar-all-width;
470 470 }
471 471 }
472 472
473 473 .sidebar-col-wrapper.scw-small {
474 474 padding-left: @sidebar-small-all-width;
475 475
476 476 .sidebar {
477 477 width: @sidebar-small-width;
478 478 margin-left: -@sidebar-small-all-width;
479 479 }
480 480 }
481 481
482 482
483 483 // FOOTER
484 484 #footer {
485 485 padding: 0;
486 486 text-align: center;
487 487 vertical-align: middle;
488 488 color: @grey2;
489 489 background-color: @grey6;
490 490
491 491 p {
492 492 margin: 0;
493 493 padding: 1em;
494 494 line-height: 1em;
495 495 }
496 496
497 497 .server-instance { //server instance
498 498 display: none;
499 499 }
500 500
501 501 .title {
502 502 float: none;
503 503 margin: 0 auto;
504 504 }
505 505 }
506 506
507 507 button.close {
508 508 padding: 0;
509 509 cursor: pointer;
510 510 background: transparent;
511 511 border: 0;
512 512 .box-shadow(none);
513 513 -webkit-appearance: none;
514 514 }
515 515
516 516 .close {
517 517 float: right;
518 518 font-size: 21px;
519 519 font-family: @text-bootstrap;
520 520 line-height: 1em;
521 521 font-weight: bold;
522 522 color: @grey2;
523 523
524 524 &:hover,
525 525 &:focus {
526 526 color: @grey1;
527 527 text-decoration: none;
528 528 cursor: pointer;
529 529 }
530 530 }
531 531
532 532 // GRID
533 533 .sorting,
534 534 .sorting_desc,
535 535 .sorting_asc {
536 536 cursor: pointer;
537 537 }
538 538 .sorting_desc:after {
539 539 content: "\00A0\25B2";
540 540 font-size: .75em;
541 541 }
542 542 .sorting_asc:after {
543 543 content: "\00A0\25BC";
544 544 font-size: .68em;
545 545 }
546 546
547 547
548 548 .user_auth_tokens {
549 549
550 550 &.truncate {
551 551 white-space: nowrap;
552 552 overflow: hidden;
553 553 text-overflow: ellipsis;
554 554 }
555 555
556 556 .fields .field .input {
557 557 margin: 0;
558 558 }
559 559
560 560 input#description {
561 561 width: 100px;
562 562 margin: 0;
563 563 }
564 564
565 565 .drop-menu {
566 566 // TODO: johbo: Remove this, should work out of the box when
567 567 // having multiple inputs inline
568 568 margin: 0 0 0 5px;
569 569 }
570 570 }
571 571 #user_list_table {
572 572 .closed {
573 573 background-color: @grey6;
574 574 }
575 575 }
576 576
577 577
578 578 input {
579 579 &.disabled {
580 580 opacity: .5;
581 581 }
582 582 }
583 583
584 584 // remove extra padding in firefox
585 585 input::-moz-focus-inner { border:0; padding:0 }
586 586
587 587 .adjacent input {
588 588 margin-bottom: @padding;
589 589 }
590 590
591 591 .permissions_boxes {
592 592 display: block;
593 593 }
594 594
595 595 //TODO: lisa: this should be in tables
596 596 .show_more_col {
597 597 width: 20px;
598 598 }
599 599
600 600 //FORMS
601 601
602 602 .medium-inline,
603 603 input#description.medium-inline {
604 604 display: inline;
605 605 width: @medium-inline-input-width;
606 606 min-width: 100px;
607 607 }
608 608
609 609 select {
610 610 //reset
611 611 -webkit-appearance: none;
612 612 -moz-appearance: none;
613 613
614 614 display: inline-block;
615 615 height: 28px;
616 616 width: auto;
617 617 margin: 0 @padding @padding 0;
618 618 padding: 0 18px 0 8px;
619 619 line-height:1em;
620 620 font-size: @basefontsize;
621 621 border: @border-thickness solid @rcblue;
622 622 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
623 623 color: @rcblue;
624 624
625 625 &:after {
626 626 content: "\00A0\25BE";
627 627 }
628 628
629 629 &:focus {
630 630 outline: none;
631 631 }
632 632 }
633 633
634 634 option {
635 635 &:focus {
636 636 outline: none;
637 637 }
638 638 }
639 639
640 640 input,
641 641 textarea {
642 642 padding: @input-padding;
643 643 border: @input-border-thickness solid @border-highlight-color;
644 644 .border-radius (@border-radius);
645 645 font-family: @text-light;
646 646 font-size: @basefontsize;
647 647
648 648 &.input-sm {
649 649 padding: 5px;
650 650 }
651 651
652 652 &#description {
653 653 min-width: @input-description-minwidth;
654 654 min-height: 1em;
655 655 padding: 10px;
656 656 }
657 657 }
658 658
659 659 .field-sm {
660 660 input,
661 661 textarea {
662 662 padding: 5px;
663 663 }
664 664 }
665 665
666 666 textarea {
667 667 display: block;
668 668 clear: both;
669 669 width: 100%;
670 670 min-height: 100px;
671 671 margin-bottom: @padding;
672 672 .box-sizing(border-box);
673 673 overflow: auto;
674 674 }
675 675
676 676 label {
677 677 font-family: @text-light;
678 678 }
679 679
680 680 // GRAVATARS
681 681 // centers gravatar on username to the right
682 682
683 683 .gravatar {
684 684 display: inline;
685 685 min-width: 16px;
686 686 min-height: 16px;
687 687 margin: -5px 0;
688 688 padding: 0;
689 689 line-height: 1em;
690 690 border: 1px solid @grey4;
691 box-sizing: content-box;
691 692
692 693 &.gravatar-large {
693 694 margin: -0.5em .25em -0.5em 0;
694 695 }
695 696
696 697 & + .user {
697 698 display: inline;
698 699 margin: 0;
699 700 padding: 0 0 0 .17em;
700 701 line-height: 1em;
701 702 }
702 703 }
703 704
704 705 .user-inline-data {
705 706 display: inline-block;
706 707 float: left;
707 708 padding-left: .5em;
708 709 line-height: 1.3em;
709 710 }
710 711
711 712 .rc-user { // gravatar + user wrapper
712 713 float: left;
713 714 position: relative;
714 715 min-width: 100px;
715 716 max-width: 200px;
716 717 min-height: (@gravatar-size + @border-thickness * 2); // account for border
717 718 display: block;
718 719 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
719 720
720 721
721 722 .gravatar {
722 723 display: block;
723 724 position: absolute;
724 725 top: 0;
725 726 left: 0;
726 727 min-width: @gravatar-size;
727 728 min-height: @gravatar-size;
728 729 margin: 0;
729 730 }
730 731
731 732 .user {
732 733 display: block;
733 734 max-width: 175px;
734 735 padding-top: 2px;
735 736 overflow: hidden;
736 737 text-overflow: ellipsis;
737 738 }
738 739 }
739 740
740 741 .gist-gravatar,
741 742 .journal_container {
742 743 .gravatar-large {
743 744 margin: 0 .5em -10px 0;
744 745 }
745 746 }
746 747
747 748
748 749 // ADMIN SETTINGS
749 750
750 751 // Tag Patterns
751 752 .tag_patterns {
752 753 .tag_input {
753 754 margin-bottom: @padding;
754 755 }
755 756 }
756 757
757 758 .locked_input {
758 759 position: relative;
759 760
760 761 input {
761 762 display: inline;
762 763 margin-top: 3px;
763 764 }
764 765
765 766 br {
766 767 display: none;
767 768 }
768 769
769 770 .error-message {
770 771 float: left;
771 772 width: 100%;
772 773 }
773 774
774 775 .lock_input_button {
775 776 display: inline;
776 777 }
777 778
778 779 .help-block {
779 780 clear: both;
780 781 }
781 782 }
782 783
783 784 // Notifications
784 785
785 786 .notifications_buttons {
786 787 margin: 0 0 @space 0;
787 788 padding: 0;
788 789
789 790 .btn {
790 791 display: inline-block;
791 792 }
792 793 }
793 794
794 795 .notification-list {
795 796
796 797 div {
797 798 display: inline-block;
798 799 vertical-align: middle;
799 800 }
800 801
801 802 .container {
802 803 display: block;
803 804 margin: 0 0 @padding 0;
804 805 }
805 806
806 807 .delete-notifications {
807 808 margin-left: @padding;
808 809 text-align: right;
809 810 cursor: pointer;
810 811 }
811 812
812 813 .read-notifications {
813 814 margin-left: @padding/2;
814 815 text-align: right;
815 816 width: 35px;
816 817 cursor: pointer;
817 818 }
818 819
819 820 .icon-minus-sign {
820 821 color: @alert2;
821 822 }
822 823
823 824 .icon-ok-sign {
824 825 color: @alert1;
825 826 }
826 827 }
827 828
828 829 .user_settings {
829 830 float: left;
830 831 clear: both;
831 832 display: block;
832 833 width: 100%;
833 834
834 835 .gravatar_box {
835 836 margin-bottom: @padding;
836 837
837 838 &:after {
838 839 content: " ";
839 840 clear: both;
840 841 width: 100%;
841 842 }
842 843 }
843 844
844 845 .fields .field {
845 846 clear: both;
846 847 }
847 848 }
848 849
849 850 .advanced_settings {
850 851 margin-bottom: @space;
851 852
852 853 .help-block {
853 854 margin-left: 0;
854 855 }
855 856
856 857 button + .help-block {
857 858 margin-top: @padding;
858 859 }
859 860 }
860 861
861 862 // admin settings radio buttons and labels
862 863 .label-2 {
863 864 float: left;
864 865 width: @label2-width;
865 866
866 867 label {
867 868 color: @grey1;
868 869 }
869 870 }
870 871 .checkboxes {
871 872 float: left;
872 873 width: @checkboxes-width;
873 874 margin-bottom: @padding;
874 875
875 876 .checkbox {
876 877 width: 100%;
877 878
878 879 label {
879 880 margin: 0;
880 881 padding: 0;
881 882 }
882 883 }
883 884
884 885 .checkbox + .checkbox {
885 886 display: inline-block;
886 887 }
887 888
888 889 label {
889 890 margin-right: 1em;
890 891 }
891 892 }
892 893
893 894 // CHANGELOG
894 895 .container_header {
895 896 float: left;
896 897 display: block;
897 898 width: 100%;
898 899 margin: @padding 0 @padding;
899 900
900 901 #filter_changelog {
901 902 float: left;
902 903 margin-right: @padding;
903 904 }
904 905
905 906 .breadcrumbs_light {
906 907 display: inline-block;
907 908 }
908 909 }
909 910
910 911 .info_box {
911 912 float: right;
912 913 }
913 914
914 915
915 916 #graph_nodes {
916 917 padding-top: 43px;
917 918 }
918 919
919 920 #graph_content{
920 921
921 922 // adjust for table headers so that graph renders properly
922 923 // #graph_nodes padding - table cell padding
923 924 padding-top: (@space - (@basefontsize * 2.4));
924 925
925 926 &.graph_full_width {
926 927 width: 100%;
927 928 max-width: 100%;
928 929 }
929 930 }
930 931
931 932 #graph {
932 933 .flag_status {
933 934 margin: 0;
934 935 }
935 936
936 937 .pagination-left {
937 938 float: left;
938 939 clear: both;
939 940 }
940 941
941 942 .log-container {
942 943 max-width: 345px;
943 944
944 945 .message{
945 946 max-width: 340px;
946 947 }
947 948 }
948 949
949 950 .graph-col-wrapper {
950 951 padding-left: 110px;
951 952
952 953 #graph_nodes {
953 954 width: 100px;
954 955 margin-left: -110px;
955 956 float: left;
956 957 clear: left;
957 958 }
958 959 }
959 960 }
960 961
961 962 #filter_changelog {
962 963 float: left;
963 964 }
964 965
965 966
966 967 //--- THEME ------------------//
967 968
968 969 #logo {
969 970 float: left;
970 971 margin: 9px 0 0 0;
971 972
972 973 .header {
973 974 background-color: transparent;
974 975 }
975 976
976 977 a {
977 978 display: inline-block;
978 979 }
979 980
980 981 img {
981 982 height:30px;
982 983 }
983 984 }
984 985
985 986 .logo-wrapper {
986 987 float:left;
987 988 }
988 989
989 990 .branding{
990 991 float: left;
991 992 padding: 9px 2px;
992 993 line-height: 1em;
993 994 font-size: @navigation-fontsize;
994 995 }
995 996
996 997 img {
997 998 border: none;
998 999 outline: none;
999 1000 }
1000 1001 user-profile-header
1001 1002 label {
1002 1003
1003 1004 input[type="checkbox"] {
1004 1005 margin-right: 1em;
1005 1006 }
1006 1007 input[type="radio"] {
1007 1008 margin-right: 1em;
1008 1009 }
1009 1010 }
1010 1011
1011 1012 .flag_status {
1012 1013 margin: 2px 8px 6px 2px;
1013 1014 &.under_review {
1014 1015 .circle(5px, @alert3);
1015 1016 }
1016 1017 &.approved {
1017 1018 .circle(5px, @alert1);
1018 1019 }
1019 1020 &.rejected,
1020 1021 &.forced_closed{
1021 1022 .circle(5px, @alert2);
1022 1023 }
1023 1024 &.not_reviewed {
1024 1025 .circle(5px, @grey5);
1025 1026 }
1026 1027 }
1027 1028
1028 1029 .flag_status_comment_box {
1029 1030 margin: 5px 6px 0px 2px;
1030 1031 }
1031 1032 .test_pattern_preview {
1032 1033 margin: @space 0;
1033 1034
1034 1035 p {
1035 1036 margin-bottom: 0;
1036 1037 border-bottom: @border-thickness solid @border-default-color;
1037 1038 color: @grey3;
1038 1039 }
1039 1040
1040 1041 .btn {
1041 1042 margin-bottom: @padding;
1042 1043 }
1043 1044 }
1044 1045 #test_pattern_result {
1045 1046 display: none;
1046 1047 &:extend(pre);
1047 1048 padding: .9em;
1048 1049 color: @grey3;
1049 1050 background-color: @grey7;
1050 1051 border-right: @border-thickness solid @border-default-color;
1051 1052 border-bottom: @border-thickness solid @border-default-color;
1052 1053 border-left: @border-thickness solid @border-default-color;
1053 1054 }
1054 1055
1055 1056 #repo_vcs_settings {
1056 1057 #inherit_overlay_vcs_default {
1057 1058 display: none;
1058 1059 }
1059 1060 #inherit_overlay_vcs_custom {
1060 1061 display: custom;
1061 1062 }
1062 1063 &.inherited {
1063 1064 #inherit_overlay_vcs_default {
1064 1065 display: block;
1065 1066 }
1066 1067 #inherit_overlay_vcs_custom {
1067 1068 display: none;
1068 1069 }
1069 1070 }
1070 1071 }
1071 1072
1072 1073 .issue-tracker-link {
1073 1074 color: @rcblue;
1074 1075 }
1075 1076
1076 1077 // Issue Tracker Table Show/Hide
1077 1078 #repo_issue_tracker {
1078 1079 #inherit_overlay {
1079 1080 display: none;
1080 1081 }
1081 1082 #custom_overlay {
1082 1083 display: custom;
1083 1084 }
1084 1085 &.inherited {
1085 1086 #inherit_overlay {
1086 1087 display: block;
1087 1088 }
1088 1089 #custom_overlay {
1089 1090 display: none;
1090 1091 }
1091 1092 }
1092 1093 }
1093 1094 table.issuetracker {
1094 1095 &.readonly {
1095 1096 tr, td {
1096 1097 color: @grey3;
1097 1098 }
1098 1099 }
1099 1100 .edit {
1100 1101 display: none;
1101 1102 }
1102 1103 .editopen {
1103 1104 .edit {
1104 1105 display: inline;
1105 1106 }
1106 1107 .entry {
1107 1108 display: none;
1108 1109 }
1109 1110 }
1110 1111 tr td.td-action {
1111 1112 min-width: 117px;
1112 1113 }
1113 1114 td input {
1114 1115 max-width: none;
1115 1116 min-width: 30px;
1116 1117 width: 80%;
1117 1118 }
1118 1119 .issuetracker_pref input {
1119 1120 width: 40%;
1120 1121 }
1121 1122 input.edit_issuetracker_update {
1122 1123 margin-right: 0;
1123 1124 width: auto;
1124 1125 }
1125 1126 }
1126 1127
1127 1128 table.integrations {
1128 1129 .td-icon {
1129 1130 width: 20px;
1130 1131 .integration-icon {
1131 1132 height: 20px;
1132 1133 width: 20px;
1133 1134 }
1134 1135 }
1135 1136 }
1136 1137
1137 1138 .integrations {
1138 1139 a.integration-box {
1139 1140 color: @text-color;
1140 1141 &:hover {
1141 1142 .panel {
1142 1143 background: #fbfbfb;
1143 1144 }
1144 1145 }
1145 1146 .integration-icon {
1146 1147 width: 30px;
1147 1148 height: 30px;
1148 1149 margin-right: 20px;
1149 1150 float: left;
1150 1151 }
1151 1152
1152 1153 .panel-body {
1153 1154 padding: 10px;
1154 1155 }
1155 1156 .panel {
1156 1157 margin-bottom: 10px;
1157 1158 }
1158 1159 h2 {
1159 1160 display: inline-block;
1160 1161 margin: 0;
1161 1162 min-width: 140px;
1162 1163 }
1163 1164 }
1164 1165 }
1165 1166
1166 1167 //Permissions Settings
1167 1168 #add_perm {
1168 1169 margin: 0 0 @padding;
1169 1170 cursor: pointer;
1170 1171 }
1171 1172
1172 1173 .perm_ac {
1173 1174 input {
1174 1175 width: 95%;
1175 1176 }
1176 1177 }
1177 1178
1178 1179 .autocomplete-suggestions {
1179 1180 width: auto !important; // overrides autocomplete.js
1180 1181 margin: 0;
1181 1182 border: @border-thickness solid @rcblue;
1182 1183 border-radius: @border-radius;
1183 1184 color: @rcblue;
1184 1185 background-color: white;
1185 1186 }
1186 1187 .autocomplete-selected {
1187 1188 background: #F0F0F0;
1188 1189 }
1189 1190 .ac-container-wrap {
1190 1191 margin: 0;
1191 1192 padding: 8px;
1192 1193 border-bottom: @border-thickness solid @rclightblue;
1193 1194 list-style-type: none;
1194 1195 cursor: pointer;
1195 1196
1196 1197 &:hover {
1197 1198 background-color: @rclightblue;
1198 1199 }
1199 1200
1200 1201 img {
1201 1202 height: @gravatar-size;
1202 1203 width: @gravatar-size;
1203 1204 margin-right: 1em;
1204 1205 }
1205 1206
1206 1207 strong {
1207 1208 font-weight: normal;
1208 1209 }
1209 1210 }
1210 1211
1211 1212 // Settings Dropdown
1212 1213 .user-menu .container {
1213 1214 padding: 0 4px;
1214 1215 margin: 0;
1215 1216 }
1216 1217
1217 1218 .user-menu .gravatar {
1218 1219 cursor: pointer;
1219 1220 }
1220 1221
1221 1222 .codeblock {
1222 1223 margin-bottom: @padding;
1223 1224 clear: both;
1224 1225
1225 1226 .stats{
1226 1227 overflow: hidden;
1227 1228 }
1228 1229
1229 1230 .message{
1230 1231 textarea{
1231 1232 margin: 0;
1232 1233 }
1233 1234 }
1234 1235
1235 1236 .code-header {
1236 1237 .stats {
1237 1238 line-height: 2em;
1238 1239
1239 1240 .revision_id {
1240 1241 margin-left: 0;
1241 1242 }
1242 1243 .buttons {
1243 1244 padding-right: 0;
1244 1245 }
1245 1246 }
1246 1247
1247 1248 .item{
1248 1249 margin-right: 0.5em;
1249 1250 }
1250 1251 }
1251 1252
1252 1253 #editor_container{
1253 1254 position: relative;
1254 1255 margin: @padding;
1255 1256 }
1256 1257 }
1257 1258
1258 1259 #file_history_container {
1259 1260 display: none;
1260 1261 }
1261 1262
1262 1263 .file-history-inner {
1263 1264 margin-bottom: 10px;
1264 1265 }
1265 1266
1266 1267 // Pull Requests
1267 1268 .summary-details {
1268 1269 width: 72%;
1269 1270 }
1270 1271 .pr-summary {
1271 1272 border-bottom: @border-thickness solid @grey5;
1272 1273 margin-bottom: @space;
1273 1274 }
1274 1275 .reviewers-title {
1275 1276 width: 25%;
1276 1277 min-width: 200px;
1277 1278 }
1278 1279 .reviewers {
1279 1280 width: 25%;
1280 1281 min-width: 200px;
1281 1282 }
1282 1283 .reviewers ul li {
1283 1284 position: relative;
1284 1285 width: 100%;
1285 1286 margin-bottom: 8px;
1286 1287 }
1287 1288 .reviewers_member {
1288 1289 width: 100%;
1289 1290 overflow: auto;
1290 1291 }
1291 1292 .reviewer_reason {
1292 1293 padding-left: 20px;
1293 1294 }
1294 1295 .reviewer_status {
1295 1296 display: inline-block;
1296 1297 vertical-align: top;
1297 1298 width: 7%;
1298 1299 min-width: 20px;
1299 1300 height: 1.2em;
1300 1301 margin-top: 3px;
1301 1302 line-height: 1em;
1302 1303 }
1303 1304
1304 1305 .reviewer_name {
1305 1306 display: inline-block;
1306 1307 max-width: 83%;
1307 1308 padding-right: 20px;
1308 1309 vertical-align: middle;
1309 1310 line-height: 1;
1310 1311
1311 1312 .rc-user {
1312 1313 min-width: 0;
1313 1314 margin: -2px 1em 0 0;
1314 1315 }
1315 1316
1316 1317 .reviewer {
1317 1318 float: left;
1318 1319 }
1319 1320
1320 1321 &.to-delete {
1321 1322 .user,
1322 1323 .reviewer {
1323 1324 text-decoration: line-through;
1324 1325 }
1325 1326 }
1326 1327 }
1327 1328
1328 1329 .reviewer_member_remove {
1329 1330 position: absolute;
1330 1331 right: 0;
1331 1332 top: 0;
1332 1333 width: 16px;
1333 1334 margin-bottom: 10px;
1334 1335 padding: 0;
1335 1336 color: black;
1336 1337 }
1337 1338 .reviewer_member_status {
1338 1339 margin-top: 5px;
1339 1340 }
1340 1341 .pr-summary #summary{
1341 1342 width: 100%;
1342 1343 }
1343 1344 .pr-summary .action_button:hover {
1344 1345 border: 0;
1345 1346 cursor: pointer;
1346 1347 }
1347 1348 .pr-details-title {
1348 1349 padding-bottom: 8px;
1349 1350 border-bottom: @border-thickness solid @grey5;
1350 1351
1351 1352 .action_button.disabled {
1352 1353 color: @grey4;
1353 1354 cursor: inherit;
1354 1355 }
1355 1356 .action_button {
1356 1357 color: @rcblue;
1357 1358 }
1358 1359 }
1359 1360 .pr-details-content {
1360 1361 margin-top: @textmargin;
1361 1362 margin-bottom: @textmargin;
1362 1363 }
1363 1364 .pr-description {
1364 1365 white-space:pre-wrap;
1365 1366 }
1366 1367 .group_members {
1367 1368 margin-top: 0;
1368 1369 padding: 0;
1369 1370 list-style: outside none none;
1370 1371
1371 1372 img {
1372 1373 height: @gravatar-size;
1373 1374 width: @gravatar-size;
1374 1375 margin-right: .5em;
1375 1376 margin-left: 3px;
1376 1377 }
1377 1378
1378 1379 .to-delete {
1379 1380 .user {
1380 1381 text-decoration: line-through;
1381 1382 }
1382 1383 }
1383 1384 }
1384 1385
1385 1386 .compare_view_commits_title {
1386 1387 .disabled {
1387 1388 cursor: inherit;
1388 1389 &:hover{
1389 1390 background-color: inherit;
1390 1391 color: inherit;
1391 1392 }
1392 1393 }
1393 1394 }
1394 1395
1395 1396 // new entry in group_members
1396 1397 .td-author-new-entry {
1397 1398 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1398 1399 }
1399 1400
1400 1401 .usergroup_member_remove {
1401 1402 width: 16px;
1402 1403 margin-bottom: 10px;
1403 1404 padding: 0;
1404 1405 color: black !important;
1405 1406 cursor: pointer;
1406 1407 }
1407 1408
1408 1409 .reviewer_ac .ac-input {
1409 1410 width: 92%;
1410 1411 margin-bottom: 1em;
1411 1412 }
1412 1413
1413 1414 .compare_view_commits tr{
1414 1415 height: 20px;
1415 1416 }
1416 1417 .compare_view_commits td {
1417 1418 vertical-align: top;
1418 1419 padding-top: 10px;
1419 1420 }
1420 1421 .compare_view_commits .author {
1421 1422 margin-left: 5px;
1422 1423 }
1423 1424
1424 1425 .compare_view_files {
1425 1426 width: 100%;
1426 1427
1427 1428 td {
1428 1429 vertical-align: middle;
1429 1430 }
1430 1431 }
1431 1432
1432 1433 .compare_view_filepath {
1433 1434 color: @grey1;
1434 1435 }
1435 1436
1436 1437 .show_more {
1437 1438 display: inline-block;
1438 1439 position: relative;
1439 1440 vertical-align: middle;
1440 1441 width: 4px;
1441 1442 height: @basefontsize;
1442 1443
1443 1444 &:after {
1444 1445 content: "\00A0\25BE";
1445 1446 display: inline-block;
1446 1447 width:10px;
1447 1448 line-height: 5px;
1448 1449 font-size: 12px;
1449 1450 cursor: pointer;
1450 1451 }
1451 1452 }
1452 1453
1453 1454 .journal_more .show_more {
1454 1455 display: inline;
1455 1456
1456 1457 &:after {
1457 1458 content: none;
1458 1459 }
1459 1460 }
1460 1461
1461 1462 .open .show_more:after,
1462 1463 .select2-dropdown-open .show_more:after {
1463 1464 .rotate(180deg);
1464 1465 margin-left: 4px;
1465 1466 }
1466 1467
1467 1468
1468 1469 .compare_view_commits .collapse_commit:after {
1469 1470 cursor: pointer;
1470 1471 content: "\00A0\25B4";
1471 1472 margin-left: -3px;
1472 1473 font-size: 17px;
1473 1474 color: @grey4;
1474 1475 }
1475 1476
1476 1477 .diff_links {
1477 1478 margin-left: 8px;
1478 1479 }
1479 1480
1480 1481 div.ancestor {
1481 1482 margin: -30px 0px;
1482 1483 }
1483 1484
1484 1485 .cs_icon_td input[type="checkbox"] {
1485 1486 display: none;
1486 1487 }
1487 1488
1488 1489 .cs_icon_td .expand_file_icon:after {
1489 1490 cursor: pointer;
1490 1491 content: "\00A0\25B6";
1491 1492 font-size: 12px;
1492 1493 color: @grey4;
1493 1494 }
1494 1495
1495 1496 .cs_icon_td .collapse_file_icon:after {
1496 1497 cursor: pointer;
1497 1498 content: "\00A0\25BC";
1498 1499 font-size: 12px;
1499 1500 color: @grey4;
1500 1501 }
1501 1502
1502 1503 /*new binary
1503 1504 NEW_FILENODE = 1
1504 1505 DEL_FILENODE = 2
1505 1506 MOD_FILENODE = 3
1506 1507 RENAMED_FILENODE = 4
1507 1508 COPIED_FILENODE = 5
1508 1509 CHMOD_FILENODE = 6
1509 1510 BIN_FILENODE = 7
1510 1511 */
1511 1512 .cs_files_expand {
1512 1513 font-size: @basefontsize + 5px;
1513 1514 line-height: 1.8em;
1514 1515 float: right;
1515 1516 }
1516 1517
1517 1518 .cs_files_expand span{
1518 1519 color: @rcblue;
1519 1520 cursor: pointer;
1520 1521 }
1521 1522 .cs_files {
1522 1523 clear: both;
1523 1524 padding-bottom: @padding;
1524 1525
1525 1526 .cur_cs {
1526 1527 margin: 10px 2px;
1527 1528 font-weight: bold;
1528 1529 }
1529 1530
1530 1531 .node {
1531 1532 float: left;
1532 1533 }
1533 1534
1534 1535 .changes {
1535 1536 float: right;
1536 1537 color: white;
1537 1538 font-size: @basefontsize - 4px;
1538 1539 margin-top: 4px;
1539 1540 opacity: 0.6;
1540 1541 filter: Alpha(opacity=60); /* IE8 and earlier */
1541 1542
1542 1543 .added {
1543 1544 background-color: @alert1;
1544 1545 float: left;
1545 1546 text-align: center;
1546 1547 }
1547 1548
1548 1549 .deleted {
1549 1550 background-color: @alert2;
1550 1551 float: left;
1551 1552 text-align: center;
1552 1553 }
1553 1554
1554 1555 .bin {
1555 1556 background-color: @alert1;
1556 1557 text-align: center;
1557 1558 }
1558 1559
1559 1560 /*new binary*/
1560 1561 .bin.bin1 {
1561 1562 background-color: @alert1;
1562 1563 text-align: center;
1563 1564 }
1564 1565
1565 1566 /*deleted binary*/
1566 1567 .bin.bin2 {
1567 1568 background-color: @alert2;
1568 1569 text-align: center;
1569 1570 }
1570 1571
1571 1572 /*mod binary*/
1572 1573 .bin.bin3 {
1573 1574 background-color: @grey2;
1574 1575 text-align: center;
1575 1576 }
1576 1577
1577 1578 /*rename file*/
1578 1579 .bin.bin4 {
1579 1580 background-color: @alert4;
1580 1581 text-align: center;
1581 1582 }
1582 1583
1583 1584 /*copied file*/
1584 1585 .bin.bin5 {
1585 1586 background-color: @alert4;
1586 1587 text-align: center;
1587 1588 }
1588 1589
1589 1590 /*chmod file*/
1590 1591 .bin.bin6 {
1591 1592 background-color: @grey2;
1592 1593 text-align: center;
1593 1594 }
1594 1595 }
1595 1596 }
1596 1597
1597 1598 .cs_files .cs_added, .cs_files .cs_A,
1598 1599 .cs_files .cs_added, .cs_files .cs_M,
1599 1600 .cs_files .cs_added, .cs_files .cs_D {
1600 1601 height: 16px;
1601 1602 padding-right: 10px;
1602 1603 margin-top: 7px;
1603 1604 text-align: left;
1604 1605 }
1605 1606
1606 1607 .cs_icon_td {
1607 1608 min-width: 16px;
1608 1609 width: 16px;
1609 1610 }
1610 1611
1611 1612 .pull-request-merge {
1612 1613 padding: 10px 0;
1613 1614 margin-top: 10px;
1614 1615 margin-bottom: 20px;
1615 1616 }
1616 1617
1617 1618 .pull-request-merge .pull-request-wrap {
1618 1619 height: 25px;
1619 1620 padding: 5px 0;
1620 1621 }
1621 1622
1622 1623 .pull-request-merge span {
1623 1624 margin-right: 10px;
1624 1625 }
1625 1626
1626 1627 .pr-versions {
1627 1628 position: relative;
1628 1629 top: 6px;
1629 1630 }
1630 1631
1631 1632 #close_pull_request {
1632 1633 margin-right: 0px;
1633 1634 }
1634 1635
1635 1636 .empty_data {
1636 1637 color: @grey4;
1637 1638 }
1638 1639
1639 1640 #changeset_compare_view_content {
1640 1641 margin-bottom: @space;
1641 1642 clear: both;
1642 1643 width: 100%;
1643 1644 box-sizing: border-box;
1644 1645 .border-radius(@border-radius);
1645 1646
1646 1647 .help-block {
1647 1648 margin: @padding 0;
1648 1649 color: @text-color;
1649 1650 }
1650 1651
1651 1652 .empty_data {
1652 1653 margin: @padding 0;
1653 1654 }
1654 1655
1655 1656 .alert {
1656 1657 margin-bottom: @space;
1657 1658 }
1658 1659 }
1659 1660
1660 1661 .table_disp {
1661 1662 .status {
1662 1663 width: auto;
1663 1664
1664 1665 .flag_status {
1665 1666 float: left;
1666 1667 }
1667 1668 }
1668 1669 }
1669 1670
1670 1671 .status_box_menu {
1671 1672 margin: 0;
1672 1673 }
1673 1674
1674 1675 .notification-table{
1675 1676 margin-bottom: @space;
1676 1677 display: table;
1677 1678 width: 100%;
1678 1679
1679 1680 .container{
1680 1681 display: table-row;
1681 1682
1682 1683 .notification-header{
1683 1684 border-bottom: @border-thickness solid @border-default-color;
1684 1685 }
1685 1686
1686 1687 .notification-subject{
1687 1688 display: table-cell;
1688 1689 }
1689 1690 }
1690 1691 }
1691 1692
1692 1693 // Notifications
1693 1694 .notification-header{
1694 1695 display: table;
1695 1696 width: 100%;
1696 1697 padding: floor(@basefontsize/2) 0;
1697 1698 line-height: 1em;
1698 1699
1699 1700 .desc, .delete-notifications, .read-notifications{
1700 1701 display: table-cell;
1701 1702 text-align: left;
1702 1703 }
1703 1704
1704 1705 .desc{
1705 1706 width: 1163px;
1706 1707 }
1707 1708
1708 1709 .delete-notifications, .read-notifications{
1709 1710 width: 35px;
1710 1711 min-width: 35px; //fixes when only one button is displayed
1711 1712 }
1712 1713 }
1713 1714
1714 1715 .notification-body {
1715 1716 .markdown-block,
1716 1717 .rst-block {
1717 1718 padding: @padding 0;
1718 1719 }
1719 1720
1720 1721 .notification-subject {
1721 1722 padding: @textmargin 0;
1722 1723 border-bottom: @border-thickness solid @border-default-color;
1723 1724 }
1724 1725 }
1725 1726
1726 1727
1727 1728 .notifications_buttons{
1728 1729 float: right;
1729 1730 }
1730 1731
1731 1732 #notification-status{
1732 1733 display: inline;
1733 1734 }
1734 1735
1735 1736 // Repositories
1736 1737
1737 1738 #summary.fields{
1738 1739 display: table;
1739 1740
1740 1741 .field{
1741 1742 display: table-row;
1742 1743
1743 1744 .label-summary{
1744 1745 display: table-cell;
1745 1746 min-width: @label-summary-minwidth;
1746 1747 padding-top: @padding/2;
1747 1748 padding-bottom: @padding/2;
1748 1749 padding-right: @padding/2;
1749 1750 }
1750 1751
1751 1752 .input{
1752 1753 display: table-cell;
1753 1754 padding: @padding/2;
1754 1755
1755 1756 input{
1756 1757 min-width: 29em;
1757 1758 padding: @padding/4;
1758 1759 }
1759 1760 }
1760 1761 .statistics, .downloads{
1761 1762 .disabled{
1762 1763 color: @grey4;
1763 1764 }
1764 1765 }
1765 1766 }
1766 1767 }
1767 1768
1768 1769 #summary{
1769 1770 width: 70%;
1770 1771 }
1771 1772
1772 1773
1773 1774 // Journal
1774 1775 .journal.title {
1775 1776 h5 {
1776 1777 float: left;
1777 1778 margin: 0;
1778 1779 width: 70%;
1779 1780 }
1780 1781
1781 1782 ul {
1782 1783 float: right;
1783 1784 display: inline-block;
1784 1785 margin: 0;
1785 1786 width: 30%;
1786 1787 text-align: right;
1787 1788
1788 1789 li {
1789 1790 display: inline;
1790 1791 font-size: @journal-fontsize;
1791 1792 line-height: 1em;
1792 1793
1793 1794 &:before { content: none; }
1794 1795 }
1795 1796 }
1796 1797 }
1797 1798
1798 1799 .filterexample {
1799 1800 position: absolute;
1800 1801 top: 95px;
1801 1802 left: @contentpadding;
1802 1803 color: @rcblue;
1803 1804 font-size: 11px;
1804 1805 font-family: @text-regular;
1805 1806 cursor: help;
1806 1807
1807 1808 &:hover {
1808 1809 color: @rcdarkblue;
1809 1810 }
1810 1811
1811 1812 @media (max-width:768px) {
1812 1813 position: relative;
1813 1814 top: auto;
1814 1815 left: auto;
1815 1816 display: block;
1816 1817 }
1817 1818 }
1818 1819
1819 1820
1820 1821 #journal{
1821 1822 margin-bottom: @space;
1822 1823
1823 1824 .journal_day{
1824 1825 margin-bottom: @textmargin/2;
1825 1826 padding-bottom: @textmargin/2;
1826 1827 font-size: @journal-fontsize;
1827 1828 border-bottom: @border-thickness solid @border-default-color;
1828 1829 }
1829 1830
1830 1831 .journal_container{
1831 1832 margin-bottom: @space;
1832 1833
1833 1834 .journal_user{
1834 1835 display: inline-block;
1835 1836 }
1836 1837 .journal_action_container{
1837 1838 display: block;
1838 1839 margin-top: @textmargin;
1839 1840
1840 1841 div{
1841 1842 display: inline;
1842 1843 }
1843 1844
1844 1845 div.journal_action_params{
1845 1846 display: block;
1846 1847 }
1847 1848
1848 1849 div.journal_repo:after{
1849 1850 content: "\A";
1850 1851 white-space: pre;
1851 1852 }
1852 1853
1853 1854 div.date{
1854 1855 display: block;
1855 1856 margin-bottom: @textmargin;
1856 1857 }
1857 1858 }
1858 1859 }
1859 1860 }
1860 1861
1861 1862 // Files
1862 1863 .edit-file-title {
1863 1864 border-bottom: @border-thickness solid @border-default-color;
1864 1865
1865 1866 .breadcrumbs {
1866 1867 margin-bottom: 0;
1867 1868 }
1868 1869 }
1869 1870
1870 1871 .edit-file-fieldset {
1871 1872 margin-top: @sidebarpadding;
1872 1873
1873 1874 .fieldset {
1874 1875 .left-label {
1875 1876 width: 13%;
1876 1877 }
1877 1878 .right-content {
1878 1879 width: 87%;
1879 1880 max-width: 100%;
1880 1881 }
1881 1882 .filename-label {
1882 1883 margin-top: 13px;
1883 1884 }
1884 1885 .commit-message-label {
1885 1886 margin-top: 4px;
1886 1887 }
1887 1888 .file-upload-input {
1888 1889 input {
1889 1890 display: none;
1890 1891 }
1891 1892 }
1892 1893 p {
1893 1894 margin-top: 5px;
1894 1895 }
1895 1896
1896 1897 }
1897 1898 .custom-path-link {
1898 1899 margin-left: 5px;
1899 1900 }
1900 1901 #commit {
1901 1902 resize: vertical;
1902 1903 }
1903 1904 }
1904 1905
1905 1906 .delete-file-preview {
1906 1907 max-height: 250px;
1907 1908 }
1908 1909
1909 1910 .new-file,
1910 1911 #filter_activate,
1911 1912 #filter_deactivate {
1912 1913 float: left;
1913 1914 margin: 0 0 0 15px;
1914 1915 }
1915 1916
1916 1917 h3.files_location{
1917 1918 line-height: 2.4em;
1918 1919 }
1919 1920
1920 1921 .browser-nav {
1921 1922 display: table;
1922 1923 margin-bottom: @space;
1923 1924
1924 1925
1925 1926 .info_box {
1926 1927 display: inline-table;
1927 1928 height: 2.5em;
1928 1929
1929 1930 .browser-cur-rev, .info_box_elem {
1930 1931 display: table-cell;
1931 1932 vertical-align: middle;
1932 1933 }
1933 1934
1934 1935 .info_box_elem {
1935 1936 border-top: @border-thickness solid @rcblue;
1936 1937 border-bottom: @border-thickness solid @rcblue;
1937 1938
1938 1939 #at_rev, a {
1939 1940 padding: 0.6em 0.9em;
1940 1941 margin: 0;
1941 1942 .box-shadow(none);
1942 1943 border: 0;
1943 1944 height: 12px;
1944 1945 }
1945 1946
1946 1947 input#at_rev {
1947 1948 max-width: 50px;
1948 1949 text-align: right;
1949 1950 }
1950 1951
1951 1952 &.previous {
1952 1953 border: @border-thickness solid @rcblue;
1953 1954 .disabled {
1954 1955 color: @grey4;
1955 1956 cursor: not-allowed;
1956 1957 }
1957 1958 }
1958 1959
1959 1960 &.next {
1960 1961 border: @border-thickness solid @rcblue;
1961 1962 .disabled {
1962 1963 color: @grey4;
1963 1964 cursor: not-allowed;
1964 1965 }
1965 1966 }
1966 1967 }
1967 1968
1968 1969 .browser-cur-rev {
1969 1970
1970 1971 span{
1971 1972 margin: 0;
1972 1973 color: @rcblue;
1973 1974 height: 12px;
1974 1975 display: inline-block;
1975 1976 padding: 0.7em 1em ;
1976 1977 border: @border-thickness solid @rcblue;
1977 1978 margin-right: @padding;
1978 1979 }
1979 1980 }
1980 1981 }
1981 1982
1982 1983 .search_activate {
1983 1984 display: table-cell;
1984 1985 vertical-align: middle;
1985 1986
1986 1987 input, label{
1987 1988 margin: 0;
1988 1989 padding: 0;
1989 1990 }
1990 1991
1991 1992 input{
1992 1993 margin-left: @textmargin;
1993 1994 }
1994 1995
1995 1996 }
1996 1997 }
1997 1998
1998 1999 .browser-cur-rev{
1999 2000 margin-bottom: @textmargin;
2000 2001 }
2001 2002
2002 2003 #node_filter_box_loading{
2003 2004 .info_text;
2004 2005 }
2005 2006
2006 2007 .browser-search {
2007 2008 margin: -25px 0px 5px 0px;
2008 2009 }
2009 2010
2010 2011 .node-filter {
2011 2012 font-size: @repo-title-fontsize;
2012 2013 padding: 4px 0px 0px 0px;
2013 2014
2014 2015 .node-filter-path {
2015 2016 float: left;
2016 2017 color: @grey4;
2017 2018 }
2018 2019 .node-filter-input {
2019 2020 float: left;
2020 2021 margin: -2px 0px 0px 2px;
2021 2022 input {
2022 2023 padding: 2px;
2023 2024 border: none;
2024 2025 font-size: @repo-title-fontsize;
2025 2026 }
2026 2027 }
2027 2028 }
2028 2029
2029 2030
2030 2031 .browser-result{
2031 2032 td a{
2032 2033 margin-left: 0.5em;
2033 2034 display: inline-block;
2034 2035
2035 2036 em{
2036 2037 font-family: @text-bold;
2037 2038 }
2038 2039 }
2039 2040 }
2040 2041
2041 2042 .browser-highlight{
2042 2043 background-color: @grey5-alpha;
2043 2044 }
2044 2045
2045 2046
2046 2047 // Search
2047 2048
2048 2049 .search-form{
2049 2050 #q {
2050 2051 width: @search-form-width;
2051 2052 }
2052 2053 .fields{
2053 2054 margin: 0 0 @space;
2054 2055 }
2055 2056
2056 2057 label{
2057 2058 display: inline-block;
2058 2059 margin-right: @textmargin;
2059 2060 padding-top: 0.25em;
2060 2061 }
2061 2062
2062 2063
2063 2064 .results{
2064 2065 clear: both;
2065 2066 margin: 0 0 @padding;
2066 2067 }
2067 2068 }
2068 2069
2069 2070 div.search-feedback-items {
2070 2071 display: inline-block;
2071 2072 padding:0px 0px 0px 96px;
2072 2073 }
2073 2074
2074 2075 div.search-code-body {
2075 2076 background-color: #ffffff; padding: 5px 0 5px 10px;
2076 2077 pre {
2077 2078 .match { background-color: #faffa6;}
2078 2079 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2079 2080 }
2080 2081 }
2081 2082
2082 2083 .expand_commit.search {
2083 2084 .show_more.open {
2084 2085 height: auto;
2085 2086 max-height: none;
2086 2087 }
2087 2088 }
2088 2089
2089 2090 .search-results {
2090 2091
2091 2092 h2 {
2092 2093 margin-bottom: 0;
2093 2094 }
2094 2095 .codeblock {
2095 2096 border: none;
2096 2097 background: transparent;
2097 2098 }
2098 2099
2099 2100 .codeblock-header {
2100 2101 border: none;
2101 2102 background: transparent;
2102 2103 }
2103 2104
2104 2105 .code-body {
2105 2106 border: @border-thickness solid @border-default-color;
2106 2107 .border-radius(@border-radius);
2107 2108 }
2108 2109
2109 2110 .td-commit {
2110 2111 &:extend(pre);
2111 2112 border-bottom: @border-thickness solid @border-default-color;
2112 2113 }
2113 2114
2114 2115 .message {
2115 2116 height: auto;
2116 2117 max-width: 350px;
2117 2118 white-space: normal;
2118 2119 text-overflow: initial;
2119 2120 overflow: visible;
2120 2121
2121 2122 .match { background-color: #faffa6;}
2122 2123 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2123 2124 }
2124 2125
2125 2126 }
2126 2127
2127 2128 table.rctable td.td-search-results div {
2128 2129 max-width: 100%;
2129 2130 }
2130 2131
2131 2132 #tip-box, .tip-box{
2132 2133 padding: @menupadding/2;
2133 2134 display: block;
2134 2135 border: @border-thickness solid @border-highlight-color;
2135 2136 .border-radius(@border-radius);
2136 2137 background-color: white;
2137 2138 z-index: 99;
2138 2139 white-space: pre-wrap;
2139 2140 }
2140 2141
2141 2142 #linktt {
2142 2143 width: 79px;
2143 2144 }
2144 2145
2145 2146 #help_kb .modal-content{
2146 2147 max-width: 750px;
2147 2148 margin: 10% auto;
2148 2149
2149 2150 table{
2150 2151 td,th{
2151 2152 border-bottom: none;
2152 2153 line-height: 2.5em;
2153 2154 }
2154 2155 th{
2155 2156 padding-bottom: @textmargin/2;
2156 2157 }
2157 2158 td.keys{
2158 2159 text-align: center;
2159 2160 }
2160 2161 }
2161 2162
2162 2163 .block-left{
2163 2164 width: 45%;
2164 2165 margin-right: 5%;
2165 2166 }
2166 2167 .modal-footer{
2167 2168 clear: both;
2168 2169 }
2169 2170 .key.tag{
2170 2171 padding: 0.5em;
2171 2172 background-color: @rcblue;
2172 2173 color: white;
2173 2174 border-color: @rcblue;
2174 2175 .box-shadow(none);
2175 2176 }
2176 2177 }
2177 2178
2178 2179
2179 2180
2180 2181 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2181 2182
2182 2183 @import 'statistics-graph';
2183 2184 @import 'tables';
2184 2185 @import 'forms';
2185 2186 @import 'diff';
2186 2187 @import 'summary';
2187 2188 @import 'navigation';
2188 2189
2189 2190 //--- SHOW/HIDE SECTIONS --//
2190 2191
2191 2192 .btn-collapse {
2192 2193 float: right;
2193 2194 text-align: right;
2194 2195 font-family: @text-light;
2195 2196 font-size: @basefontsize;
2196 2197 cursor: pointer;
2197 2198 border: none;
2198 2199 color: @rcblue;
2199 2200 }
2200 2201
2201 2202 table.rctable,
2202 2203 table.dataTable {
2203 2204 .btn-collapse {
2204 2205 float: right;
2205 2206 text-align: right;
2206 2207 }
2207 2208 }
2208 2209
2209 2210
2210 2211 // TODO: johbo: Fix for IE10, this avoids that we see a border
2211 2212 // and padding around checkboxes and radio boxes. Move to the right place,
2212 2213 // or better: Remove this once we did the form refactoring.
2213 2214 input[type=checkbox],
2214 2215 input[type=radio] {
2215 2216 padding: 0;
2216 2217 border: none;
2217 2218 }
2218 2219
2219 2220 .toggle-ajax-spinner{
2220 2221 height: 16px;
2221 2222 width: 16px;
2222 2223 }
@@ -1,658 +1,666 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 // returns a node from given html;
29 29 var fromHTML = function(html){
30 30 var _html = document.createElement('element');
31 31 _html.innerHTML = html;
32 32 return _html;
33 33 };
34 34
35 35 var tableTr = function(cls, body){
36 36 var _el = document.createElement('div');
37 37 var _body = $(body).attr('id');
38 38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
39 39 var id = 'comment-tr-{0}'.format(comment_id);
40 40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
41 41 '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+
42 42 '<td></td>'+
43 43 '<td></td>'+
44 44 '<td></td>'+
45 45 '<td>{2}</td>'+
46 46 '</tr></tbody></table>').format(id, cls, body);
47 47 $(_el).html(_html);
48 48 return _el.children[0].children[0].children[0];
49 49 };
50 50
51 51 function bindDeleteCommentButtons() {
52 52 $('.delete-comment').one('click', function() {
53 53 var comment_id = $(this).data("comment-id");
54 54
55 55 if (comment_id){
56 56 deleteComment(comment_id);
57 57 }
58 58 });
59 59 }
60 60
61 61 var deleteComment = function(comment_id) {
62 62 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
63 63 var postData = {
64 64 '_method': 'delete',
65 65 'csrf_token': CSRF_TOKEN
66 66 };
67 67
68 68 var success = function(o) {
69 69 window.location.reload();
70 70 };
71 71 ajaxPOST(url, postData, success);
72 72 };
73 73
74 74
75 75 var bindToggleButtons = function() {
76 76 $('.comment-toggle').on('click', function() {
77 77 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
78 78 });
79 79 };
80 80
81 81 var linkifyComments = function(comments) {
82 82 /* TODO: dan: remove this - it should no longer needed */
83 83 for (var i = 0; i < comments.length; i++) {
84 84 var comment_id = $(comments[i]).data('comment-id');
85 85 var prev_comment_id = $(comments[i - 1]).data('comment-id');
86 86 var next_comment_id = $(comments[i + 1]).data('comment-id');
87 87
88 88 // place next/prev links
89 89 if (prev_comment_id) {
90 90 $('#prev_c_' + comment_id).show();
91 91 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
92 92 'href', '#comment-' + prev_comment_id).removeClass('disabled');
93 93 }
94 94 if (next_comment_id) {
95 95 $('#next_c_' + comment_id).show();
96 96 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
97 97 'href', '#comment-' + next_comment_id).removeClass('disabled');
98 98 }
99 99 // place a first link to the total counter
100 100 if (i === 0) {
101 101 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
102 102 }
103 103 }
104 104
105 105 };
106 106
107 107
108 108 /* Comment form for main and inline comments */
109 109 var CommentForm = (function() {
110 110 "use strict";
111 111
112 112 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
113 113
114 114 this.withLineNo = function(selector) {
115 115 var lineNo = this.lineNo;
116 116 if (lineNo === undefined) {
117 117 return selector
118 118 } else {
119 119 return selector + '_' + lineNo;
120 120 }
121 121 };
122 122
123 123 this.commitId = commitId;
124 124 this.pullRequestId = pullRequestId;
125 125 this.lineNo = lineNo;
126 126 this.initAutocompleteActions = initAutocompleteActions;
127 127
128 128 this.previewButton = this.withLineNo('#preview-btn');
129 129 this.previewContainer = this.withLineNo('#preview-container');
130 130
131 131 this.previewBoxSelector = this.withLineNo('#preview-box');
132 132
133 133 this.editButton = this.withLineNo('#edit-btn');
134 134 this.editContainer = this.withLineNo('#edit-container');
135 this.cancelButton = this.withLineNo('#cancel-btn');
136 this.commentType = this.withLineNo('#comment_type');
135 137
136 this.cancelButton = this.withLineNo('#cancel-btn');
138 this.cmBox = this.withLineNo('#text');
139 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
137 140
138 141 this.statusChange = '#change_status';
139 this.cmBox = this.withLineNo('#text');
140 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
141 142
142 143 this.submitForm = formElement;
143 144 this.submitButton = $(this.submitForm).find('input[type="submit"]');
144 145 this.submitButtonText = this.submitButton.val();
145 146
146 147 this.previewUrl = pyroutes.url('changeset_comment_preview',
147 148 {'repo_name': templateContext.repo_name});
148 149
149 150 // based on commitId, or pullReuqestId decide where do we submit
150 151 // out data
151 152 if (this.commitId){
152 153 this.submitUrl = pyroutes.url('changeset_comment',
153 154 {'repo_name': templateContext.repo_name,
154 155 'revision': this.commitId});
155 156
156 157 } else if (this.pullRequestId) {
157 158 this.submitUrl = pyroutes.url('pullrequest_comment',
158 159 {'repo_name': templateContext.repo_name,
159 160 'pull_request_id': this.pullRequestId});
160 161
161 162 } else {
162 163 throw new Error(
163 164 'CommentForm requires pullRequestId, or commitId to be specified.')
164 165 }
165 166
166 167 this.getCmInstance = function(){
167 168 return this.cm
168 169 };
169 170
170 171 var self = this;
171 172
172 173 this.getCommentStatus = function() {
173 174 return $(this.submitForm).find(this.statusChange).val();
174 175 };
175
176 this.getCommentType = function() {
177 return $(this.submitForm).find(this.commentType).val();
178 };
176 179 this.isAllowedToSubmit = function() {
177 180 return !$(this.submitButton).prop('disabled');
178 181 };
179 182
180 183 this.initStatusChangeSelector = function(){
181 184 var formatChangeStatus = function(state, escapeMarkup) {
182 185 var originalOption = state.element;
183 186 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
184 187 '<span>' + escapeMarkup(state.text) + '</span>';
185 188 };
186 189 var formatResult = function(result, container, query, escapeMarkup) {
187 190 return formatChangeStatus(result, escapeMarkup);
188 191 };
189 192
190 193 var formatSelection = function(data, container, escapeMarkup) {
191 194 return formatChangeStatus(data, escapeMarkup);
192 195 };
193 196
194 197 $(this.submitForm).find(this.statusChange).select2({
195 198 placeholder: _gettext('Status Review'),
196 199 formatResult: formatResult,
197 200 formatSelection: formatSelection,
198 201 containerCssClass: "drop-menu status_box_menu",
199 202 dropdownCssClass: "drop-menu-dropdown",
200 203 dropdownAutoWidth: true,
201 204 minimumResultsForSearch: -1
202 205 });
203 206 $(this.submitForm).find(this.statusChange).on('change', function() {
204 207 var status = self.getCommentStatus();
205 208 if (status && !self.lineNo) {
206 209 $(self.submitButton).prop('disabled', false);
207 210 }
208 211 //todo, fix this name
209 212 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
210 213 self.cm.setOption('placeholder', placeholderText);
211 214 })
212 215 };
213 216
214 217 // reset the comment form into it's original state
215 218 this.resetCommentFormState = function(content) {
216 219 content = content || '';
217 220
218 221 $(this.editContainer).show();
219 222 $(this.editButton).parent().addClass('active');
220 223
221 224 $(this.previewContainer).hide();
222 225 $(this.previewButton).parent().removeClass('active');
223 226
224 227 this.setActionButtonsDisabled(true);
225 228 self.cm.setValue(content);
226 229 self.cm.setOption("readOnly", false);
227 230 };
228 231
229 232 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
230 233 failHandler = failHandler || function() {};
231 234 var postData = toQueryString(postData);
232 235 var request = $.ajax({
233 236 url: url,
234 237 type: 'POST',
235 238 data: postData,
236 239 headers: {'X-PARTIAL-XHR': true}
237 240 })
238 241 .done(function(data) {
239 242 successHandler(data);
240 243 })
241 244 .fail(function(data, textStatus, errorThrown){
242 245 alert(
243 246 "Error while submitting comment.\n" +
244 247 "Error code {0} ({1}).".format(data.status, data.statusText));
245 248 failHandler()
246 249 });
247 250 return request;
248 251 };
249 252
250 253 // overwrite a submitHandler, we need to do it for inline comments
251 254 this.setHandleFormSubmit = function(callback) {
252 255 this.handleFormSubmit = callback;
253 256 };
254 257
255 258 // default handler for for submit for main comments
256 259 this.handleFormSubmit = function() {
257 260 var text = self.cm.getValue();
258 261 var status = self.getCommentStatus();
262 var commentType = self.getCommentType();
259 263
260 264 if (text === "" && !status) {
261 265 return;
262 266 }
263 267
264 268 var excludeCancelBtn = false;
265 269 var submitEvent = true;
266 270 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
267 271 self.cm.setOption("readOnly", true);
268 272 var postData = {
269 273 'text': text,
270 274 'changeset_status': status,
275 'comment_type': commentType,
271 276 'csrf_token': CSRF_TOKEN
272 277 };
273 278
274 279 var submitSuccessCallback = function(o) {
275 280 if (status) {
276 281 location.reload(true);
277 282 } else {
278 283 $('#injected_page_comments').append(o.rendered_text);
279 284 self.resetCommentFormState();
280 285 bindDeleteCommentButtons();
281 286 timeagoActivate();
282 287 }
283 288 };
284 289 var submitFailCallback = function(){
285 290 self.resetCommentFormState(text)
286 291 };
287 292 self.submitAjaxPOST(
288 293 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
289 294 };
290 295
291 296 this.previewSuccessCallback = function(o) {
292 297 $(self.previewBoxSelector).html(o);
293 298 $(self.previewBoxSelector).removeClass('unloaded');
294 299
295 300 // swap buttons, making preview active
296 301 $(self.previewButton).parent().addClass('active');
297 302 $(self.editButton).parent().removeClass('active');
298 303
299 304 // unlock buttons
300 305 self.setActionButtonsDisabled(false);
301 306 };
302 307
303 308 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
304 309 excludeCancelBtn = excludeCancelBtn || false;
305 310 submitEvent = submitEvent || false;
306 311
307 312 $(this.editButton).prop('disabled', state);
308 313 $(this.previewButton).prop('disabled', state);
309 314
310 315 if (!excludeCancelBtn) {
311 316 $(this.cancelButton).prop('disabled', state);
312 317 }
313 318
314 319 var submitState = state;
315 320 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
316 321 // if the value of commit review status is set, we allow
317 322 // submit button, but only on Main form, lineNo means inline
318 323 submitState = false
319 324 }
320 325 $(this.submitButton).prop('disabled', submitState);
321 326 if (submitEvent) {
322 327 $(this.submitButton).val(_gettext('Submitting...'));
323 328 } else {
324 329 $(this.submitButton).val(this.submitButtonText);
325 330 }
326 331
327 332 };
328 333
329 334 // lock preview/edit/submit buttons on load, but exclude cancel button
330 335 var excludeCancelBtn = true;
331 336 this.setActionButtonsDisabled(true, excludeCancelBtn);
332 337
333 338 // anonymous users don't have access to initialized CM instance
334 339 if (this.cm !== undefined){
335 340 this.cm.on('change', function(cMirror) {
336 341 if (cMirror.getValue() === "") {
337 342 self.setActionButtonsDisabled(true, excludeCancelBtn)
338 343 } else {
339 344 self.setActionButtonsDisabled(false, excludeCancelBtn)
340 345 }
341 346 });
342 347 }
343 348
344 349 $(this.editButton).on('click', function(e) {
345 350 e.preventDefault();
346 351
347 352 $(self.previewButton).parent().removeClass('active');
348 353 $(self.previewContainer).hide();
349 354
350 355 $(self.editButton).parent().addClass('active');
351 356 $(self.editContainer).show();
352 357
353 358 });
354 359
355 360 $(this.previewButton).on('click', function(e) {
356 361 e.preventDefault();
357 362 var text = self.cm.getValue();
358 363
359 364 if (text === "") {
360 365 return;
361 366 }
362 367
363 368 var postData = {
364 369 'text': text,
365 370 'renderer': DEFAULT_RENDERER,
366 371 'csrf_token': CSRF_TOKEN
367 372 };
368 373
369 374 // lock ALL buttons on preview
370 375 self.setActionButtonsDisabled(true);
371 376
372 377 $(self.previewBoxSelector).addClass('unloaded');
373 378 $(self.previewBoxSelector).html(_gettext('Loading ...'));
374 379
375 380 $(self.editContainer).hide();
376 381 $(self.previewContainer).show();
377 382
378 383 // by default we reset state of comment preserving the text
379 384 var previewFailCallback = function(){
380 385 self.resetCommentFormState(text)
381 386 };
382 387 self.submitAjaxPOST(
383 388 self.previewUrl, postData, self.previewSuccessCallback,
384 389 previewFailCallback);
385 390
386 391 $(self.previewButton).parent().addClass('active');
387 392 $(self.editButton).parent().removeClass('active');
388 393 });
389 394
390 395 $(this.submitForm).submit(function(e) {
391 396 e.preventDefault();
392 397 var allowedToSubmit = self.isAllowedToSubmit();
393 398 if (!allowedToSubmit){
394 399 return false;
395 400 }
396 401 self.handleFormSubmit();
397 402 });
398 403
399 404 }
400 405
401 406 return CommentForm;
402 407 })();
403 408
404 409 var CommentsController = function() { /* comments controller */
405 410 var self = this;
406 411
407 412 this.cancelComment = function(node) {
408 413 var $node = $(node);
409 414 var $td = $node.closest('td');
410 415 $node.closest('.comment-inline-form').removeClass('comment-inline-form-open');
411 416 return false;
412 417 };
413 418
414 419 this.getLineNumber = function(node) {
415 420 var $node = $(node);
416 421 return $node.closest('td').attr('data-line-number');
417 422 };
418 423
419 424 this.scrollToComment = function(node, offset, outdated) {
420 425 var outdated = outdated || false;
421 426 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
422 427
423 428 if (!node) {
424 429 node = $('.comment-selected');
425 430 if (!node.length) {
426 431 node = $('comment-current')
427 432 }
428 433 }
429 434 $comment = $(node).closest(klass);
430 435 $comments = $(klass);
431 436
432 437 $('.comment-selected').removeClass('comment-selected');
433 438
434 439 var nextIdx = $(klass).index($comment) + offset;
435 440 if (nextIdx >= $comments.length) {
436 441 nextIdx = 0;
437 442 }
438 443 var $next = $(klass).eq(nextIdx);
439 444 var $cb = $next.closest('.cb');
440 445 $cb.removeClass('cb-collapsed');
441 446
442 447 var $filediffCollapseState = $cb.closest('.filediff').prev();
443 448 $filediffCollapseState.prop('checked', false);
444 449 $next.addClass('comment-selected');
445 450 scrollToElement($next);
446 451 return false;
447 452 };
448 453
449 454 this.nextComment = function(node) {
450 455 return self.scrollToComment(node, 1);
451 456 };
452 457
453 458 this.prevComment = function(node) {
454 459 return self.scrollToComment(node, -1);
455 460 };
456 461
457 462 this.nextOutdatedComment = function(node) {
458 463 return self.scrollToComment(node, 1, true);
459 464 };
460 465
461 466 this.prevOutdatedComment = function(node) {
462 467 return self.scrollToComment(node, -1, true);
463 468 };
464 469
465 470 this.deleteComment = function(node) {
466 471 if (!confirm(_gettext('Delete this comment?'))) {
467 472 return false;
468 473 }
469 474 var $node = $(node);
470 475 var $td = $node.closest('td');
471 476 var $comment = $node.closest('.comment');
472 477 var comment_id = $comment.attr('data-comment-id');
473 478 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
474 479 var postData = {
475 480 '_method': 'delete',
476 481 'csrf_token': CSRF_TOKEN
477 482 };
478 483
479 484 $comment.addClass('comment-deleting');
480 485 $comment.hide('fast');
481 486
482 487 var success = function(response) {
483 488 $comment.remove();
484 489 return false;
485 490 };
486 491 var failure = function(data, textStatus, xhr) {
487 492 alert("error processing request: " + textStatus);
488 493 $comment.show('fast');
489 494 $comment.removeClass('comment-deleting');
490 495 return false;
491 496 };
492 497 ajaxPOST(url, postData, success, failure);
493 498 };
494 499
495 500 this.toggleWideMode = function (node) {
496 501 if ($('#content').hasClass('wrapper')) {
497 502 $('#content').removeClass("wrapper");
498 503 $('#content').addClass("wide-mode-wrapper");
499 504 $(node).addClass('btn-success');
500 505 } else {
501 506 $('#content').removeClass("wide-mode-wrapper");
502 507 $('#content').addClass("wrapper");
503 508 $(node).removeClass('btn-success');
504 509 }
505 510 return false;
506 511 };
507 512
508 513 this.toggleComments = function(node, show) {
509 514 var $filediff = $(node).closest('.filediff');
510 515 if (show === true) {
511 516 $filediff.removeClass('hide-comments');
512 517 } else if (show === false) {
513 518 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
514 519 $filediff.addClass('hide-comments');
515 520 } else {
516 521 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
517 522 $filediff.toggleClass('hide-comments');
518 523 }
519 524 return false;
520 525 };
521 526
522 527 this.toggleLineComments = function(node) {
523 528 self.toggleComments(node, true);
524 529 var $node = $(node);
525 530 $node.closest('tr').toggleClass('hide-line-comments');
526 531 };
527 532
528 533 this.createComment = function(node) {
529 534 var $node = $(node);
530 535 var $td = $node.closest('td');
531 536 var $form = $td.find('.comment-inline-form');
532 537
533 538 if (!$form.length) {
534 539 var tmpl = $('#cb-comment-inline-form-template').html();
535 540 var $filediff = $node.closest('.filediff');
536 541 $filediff.removeClass('hide-comments');
537 542 var f_path = $filediff.attr('data-f-path');
538 543 var lineno = self.getLineNumber(node);
544
539 545 tmpl = tmpl.format(f_path, lineno);
540 546 $form = $(tmpl);
541 547
542 548 var $comments = $td.find('.inline-comments');
543 549 if (!$comments.length) {
544 550 $comments = $(
545 551 $('#cb-comments-inline-container-template').html());
546 552 $td.append($comments);
547 553 }
548 554
549 555 $td.find('.cb-comment-add-button').before($form);
550 556
551 557 var pullRequestId = templateContext.pull_request_data.pull_request_id;
552 558 var commitId = templateContext.commit_data.commit_id;
553 559 var _form = $form[0];
554 560 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
555 561 var cm = commentForm.getCmInstance();
556 562
557 563 // set a CUSTOM submit handler for inline comments.
558 564 commentForm.setHandleFormSubmit(function(o) {
559 565 var text = commentForm.cm.getValue();
566 var commentType = commentForm.getCommentType();
560 567
561 568 if (text === "") {
562 569 return;
563 570 }
564 571
565 572 if (lineno === undefined) {
566 573 alert('missing line !');
567 574 return;
568 575 }
569 576 if (f_path === undefined) {
570 577 alert('missing file path !');
571 578 return;
572 579 }
573 580
574 581 var excludeCancelBtn = false;
575 582 var submitEvent = true;
576 583 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
577 584 commentForm.cm.setOption("readOnly", true);
578 585 var postData = {
579 586 'text': text,
580 587 'f_path': f_path,
581 588 'line': lineno,
589 'comment_type': commentType,
582 590 'csrf_token': CSRF_TOKEN
583 591 };
584 592 var submitSuccessCallback = function(json_data) {
585 593 $form.remove();
586 594 try {
587 595 var html = json_data.rendered_text;
588 596 var lineno = json_data.line_no;
589 597 var target_id = json_data.target_id;
590 598
591 599 $comments.find('.cb-comment-add-button').before(html);
592 600
593 601 } catch (e) {
594 602 console.error(e);
595 603 }
596 604
597 605 // re trigger the linkification of next/prev navigation
598 606 linkifyComments($('.inline-comment-injected'));
599 607 timeagoActivate();
600 608 bindDeleteCommentButtons();
601 609 commentForm.setActionButtonsDisabled(false);
602 610
603 611 };
604 612 var submitFailCallback = function(){
605 613 commentForm.resetCommentFormState(text)
606 614 };
607 615 commentForm.submitAjaxPOST(
608 616 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
609 617 });
610 618
611 619 setTimeout(function() {
612 620 // callbacks
613 621 if (cm !== undefined) {
614 622 cm.setOption('placeholder', _gettext('Leave a comment on line {0}.').format(lineno));
615 623 cm.focus();
616 624 cm.refresh();
617 625 }
618 626 }, 10);
619 627
620 628 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
621 629 form: _form,
622 630 parent: $td[0],
623 631 lineno: lineno,
624 632 f_path: f_path}
625 633 );
626 634 }
627 635
628 636 $form.addClass('comment-inline-form-open');
629 637 };
630 638
631 639 this.renderInlineComments = function(file_comments) {
632 640 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
633 641
634 642 for (var i = 0; i < file_comments.length; i++) {
635 643 var box = file_comments[i];
636 644
637 645 var target_id = $(box).attr('target_id');
638 646
639 647 // actually comments with line numbers
640 648 var comments = box.children;
641 649
642 650 for (var j = 0; j < comments.length; j++) {
643 651 var data = {
644 652 'rendered_text': comments[j].outerHTML,
645 653 'line_no': $(comments[j]).attr('line'),
646 654 'target_id': target_id
647 655 };
648 656 }
649 657 }
650 658
651 659 // since order of injection is random, we're now re-iterating
652 660 // from correct order and filling in links
653 661 linkifyComments($('.inline-comment-injected'));
654 662 bindDeleteCommentButtons();
655 663 firefoxAnchorFix();
656 664 };
657 665
658 666 };
@@ -1,299 +1,310 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.mako"/>
7 7
8 8 <%def name="comment_block(comment, inline=False)">
9 9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
10 10 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
11 11
12 12 <div class="comment
13 13 ${'comment-inline' if inline else 'comment-general'}
14 14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
15 15 id="comment-${comment.comment_id}"
16 16 line="${comment.line_no}"
17 17 data-comment-id="${comment.comment_id}"
18 18 style="${'display: none;' if outdated_at_ver else ''}">
19 19
20 20 <div class="meta">
21 <div class="comment-type-label tooltip">
22 <div class="comment-label ${comment.comment_type or 'note'}">
23 ${comment.comment_type or 'note'}
24 </div>
25 </div>
26
21 27 <div class="author ${'author-inline' if inline else 'author-general'}">
22 ${base.gravatar_with_user(comment.author.email, 20)}
28 ${base.gravatar_with_user(comment.author.email, 16)}
23 29 </div>
24 30 <div class="date">
25 31 ${h.age_component(comment.modified_at, time_is_local=True)}
26 32 </div>
27 33 % if inline:
28 34 <span></span>
29 35 % else:
30 36 <div class="status-change">
31 37 % if comment.pull_request:
32 38 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
33 39 % if comment.status_change:
34 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
40 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
35 41 % else:
36 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
42 ${_('pull request #%s') % comment.pull_request.pull_request_id}
37 43 % endif
38 44 </a>
39 45 % else:
40 46 % if comment.status_change:
41 47 ${_('Status change on commit')}:
42 % else:
43 ${_('Comment on commit')}
44 48 % endif
45 49 % endif
46 50 </div>
47 51 % endif
48 52
49 53 % if comment.status_change:
50 54 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
51 55 <div title="${_('Commit status')}" class="changeset-status-lbl">
52 56 ${comment.status_change[0].status_lbl}
53 57 </div>
54 58 % endif
55 59
56 60 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
57 61
58 62 <div class="comment-links-block">
59 63
60 64 % if inline:
61 65 % if outdated_at_ver:
62 66 <div class="pr-version-inline">
63 67 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
64 68 <code class="pr-version-num">
65 69 outdated ${'v{}'.format(pr_index_ver)}
66 70 </code>
67 71 </a>
68 72 </div>
69 73 |
70 74 % endif
71 75 % else:
72 76 % if comment.pull_request_version_id and pr_index_ver:
73 77 |
74 78 <div class="pr-version">
75 79 % if comment.outdated:
76 80 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
77 81 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
78 82 </a>
79 83 % else:
80 84 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
81 85 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
82 86 <code class="pr-version-num">
83 87 ${'v{}'.format(pr_index_ver)}
84 88 </code>
85 89 </a>
86 90 </div>
87 91 % endif
88 92 </div>
89 93 % endif
90 94 % endif
91 95
92 96 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
93 97 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
94 98 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
95 99 ## permissions to delete
96 100 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
97 101 ## TODO: dan: add edit comment here
98 102 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
99 103 %else:
100 104 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
101 105 %endif
102 106 %else:
103 107 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
104 108 %endif
105 109
106 110 %if not outdated_at_ver:
107 111 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
108 112 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
109 113 %endif
110 114
111 115 </div>
112 116 </div>
113 117 <div class="text">
114 118 ${comment.render(mentions=True)|n}
115 119 </div>
116 120
117 121 </div>
118 122 </%def>
119 123 ## generate main comments
120 124 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
121 125 <div id="comments">
122 126 %for comment in c.comments:
123 127 <div id="comment-tr-${comment.comment_id}">
124 128 ## only render comments that are not from pull request, or from
125 129 ## pull request and a status change
126 130 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
127 131 ${comment_block(comment)}
128 132 %endif
129 133 </div>
130 134 %endfor
131 135 ## to anchor ajax comments
132 136 <div id="injected_page_comments"></div>
133 137 </div>
134 138 </%def>
135 139
136 140 ## MAIN COMMENT FORM
137 141 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
138 142
139 143 %if is_compare:
140 144 <% form_id = "comments_form_compare" %>
141 145 %else:
142 146 <% form_id = "comments_form" %>
143 147 %endif
144 148
145 149
146 150 %if is_pull_request:
147 151 <div class="pull-request-merge">
148 152 %if c.allowed_to_merge:
149 153 <div class="pull-request-wrap">
150 154 <div class="pull-right">
151 155 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
152 156 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
153 157 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
154 158 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
155 159 ${h.end_form()}
156 160 </div>
157 161 </div>
158 162 %else:
159 163 <div class="pull-request-wrap">
160 164 <div class="pull-right">
161 165 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
162 166 </div>
163 167 </div>
164 168 %endif
165 169 </div>
166 170 %endif
167 171 <div class="comments">
168 172 <%
169 173 if is_pull_request:
170 174 placeholder = _('Leave a comment on this Pull Request.')
171 175 elif is_compare:
172 176 placeholder = _('Leave a comment on all commits in this range.')
173 177 else:
174 178 placeholder = _('Leave a comment on this Commit.')
175 179 %>
176 180 % if c.rhodecode_user.username != h.DEFAULT_USER:
177 181 <div class="comment-form ac">
178 182 ${h.secure_form(post_url, id_=form_id)}
179 183 <div class="comment-area">
180 184 <div class="comment-area-header">
181 185 <ul class="nav-links clearfix">
182 186 <li class="active">
183 187 <a href="#edit-btn" tabindex="-1" id="edit-btn">${_('Write')}</a>
184 188 </li>
185 189 <li class="">
186 190 <a href="#preview-btn" tabindex="-1" id="preview-btn">${_('Preview')}</a>
187 191 </li>
192 <li class="pull-right">
193 <select class="comment-type" id="comment_type" name="comment_type">
194 % for val in c.visual.comment_types:
195 <option value="${val}">${val.upper()}</option>
196 % endfor
197 </select>
198 </li>
188 199 </ul>
189 200 </div>
190 201
191 202 <div class="comment-area-write" style="display: block;">
192 203 <div id="edit-container">
193 204 <textarea id="text" name="text" class="comment-block-ta ac-input"></textarea>
194 205 </div>
195 206 <div id="preview-container" class="clearfix" style="display: none;">
196 207 <div id="preview-box" class="preview-box"></div>
197 208 </div>
198 209 </div>
199 210
200 211 <div class="comment-area-footer">
201 212 <div class="toolbar">
202 213 <div class="toolbar-text">
203 214 ${(_('Comments parsed using %s syntax with %s support.') % (
204 215 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
205 216 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
206 217 )
207 218 )|n}
208 219 </div>
209 220 </div>
210 221 </div>
211 222 </div>
212 223
213 224 <div id="comment_form_extras">
214 225 %if form_extras and isinstance(form_extras, (list, tuple)):
215 226 % for form_ex_el in form_extras:
216 227 ${form_ex_el|n}
217 228 % endfor
218 229 %endif
219 230 </div>
220 231 <div class="comment-footer">
221 232 %if change_status:
222 233 <div class="status_box">
223 234 <select id="change_status" name="changeset_status">
224 235 <option></option> # Placeholder
225 236 %for status,lbl in c.commit_statuses:
226 237 <option value="${status}" data-status="${status}">${lbl}</option>
227 238 %if is_pull_request and change_status and status in ('approved', 'rejected'):
228 239 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
229 240 %endif
230 241 %endfor
231 242 </select>
232 243 </div>
233 244 %endif
234 245 <div class="action-buttons">
235 246 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
236 247 </div>
237 248 </div>
238 249 ${h.end_form()}
239 250 </div>
240 251 % else:
241 252 <div class="comment-form ac">
242 253
243 254 <div class="comment-area">
244 255 <div class="comment-area-header">
245 256 <ul class="nav-links clearfix">
246 257 <li class="active">
247 258 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
248 259 </li>
249 260 <li class="">
250 261 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
251 262 </li>
252 263 </ul>
253 264 </div>
254 265
255 266 <div class="comment-area-write" style="display: block;">
256 267 <div id="edit-container">
257 268 <div style="padding: 40px 0">
258 269 ${_('You need to be logged in to leave comments.')}
259 270 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
260 271 </div>
261 272 </div>
262 273 <div id="preview-container" class="clearfix" style="display: none;">
263 274 <div id="preview-box" class="preview-box"></div>
264 275 </div>
265 276 </div>
266 277
267 278 <div class="comment-area-footer">
268 279 <div class="toolbar">
269 280 <div class="toolbar-text">
270 281 </div>
271 282 </div>
272 283 </div>
273 284 </div>
274 285
275 286 <div class="comment-footer">
276 287 </div>
277 288
278 289 </div>
279 290 % endif
280 291
281 292 </div>
282 293
283 294 <script>
284 295 // init active elements of commentForm
285 296 var commitId = templateContext.commit_data.commit_id;
286 297 var pullRequestId = templateContext.pull_request_data.pull_request_id;
287 298 var lineNo;
288 299
289 300 var mainCommentForm = new CommentForm(
290 301 "#${form_id}", commitId, pullRequestId, lineNo, true);
291 302
292 303 if (mainCommentForm.cm){
293 304 mainCommentForm.cm.setOption('placeholder', "${placeholder}");
294 305 }
295 306
296 307 mainCommentForm.initStatusChangeSelector();
297 308 bindToggleButtons();
298 309 </script>
299 310 </%def>
@@ -1,711 +1,718 b''
1 1 <%def name="diff_line_anchor(filename, line, type)"><%
2 2 return '%s_%s_%i' % (h.safeid(filename), type, line)
3 3 %></%def>
4 4
5 5 <%def name="action_class(action)">
6 6 <%
7 7 return {
8 8 '-': 'cb-deletion',
9 9 '+': 'cb-addition',
10 10 ' ': 'cb-context',
11 11 }.get(action, 'cb-empty')
12 12 %>
13 13 </%def>
14 14
15 15 <%def name="op_class(op_id)">
16 16 <%
17 17 return {
18 18 DEL_FILENODE: 'deletion', # file deleted
19 19 BIN_FILENODE: 'warning' # binary diff hidden
20 20 }.get(op_id, 'addition')
21 21 %>
22 22 </%def>
23 23
24 24 <%def name="link_for(**kw)">
25 25 <%
26 26 new_args = request.GET.mixed()
27 27 new_args.update(kw)
28 28 return h.url('', **new_args)
29 29 %>
30 30 </%def>
31 31
32 32 <%def name="render_diffset(diffset, commit=None,
33 33
34 34 # collapse all file diff entries when there are more than this amount of files in the diff
35 35 collapse_when_files_over=20,
36 36
37 37 # collapse lines in the diff when more than this amount of lines changed in the file diff
38 38 lines_changed_limit=500,
39 39
40 40 # add a ruler at to the output
41 41 ruler_at_chars=0,
42 42
43 43 # show inline comments
44 44 use_comments=False,
45 45
46 46 # disable new comments
47 47 disable_new_comments=False,
48 48
49 49 # special file-comments that were deleted in previous versions
50 50 # it's used for showing outdated comments for deleted files in a PR
51 51 deleted_files_comments=None
52 52
53 53 )">
54 54
55 55 %if use_comments:
56 56 <div id="cb-comments-inline-container-template" class="js-template">
57 57 ${inline_comments_container([])}
58 58 </div>
59 59 <div class="js-template" id="cb-comment-inline-form-template">
60 60 <div class="comment-inline-form ac">
61 61
62 62 %if c.rhodecode_user.username != h.DEFAULT_USER:
63 63 ${h.form('#', method='get')}
64 64 <div class="comment-area">
65 65 <div class="comment-area-header">
66 66 <ul class="nav-links clearfix">
67 67 <li class="active">
68 68 <a href="#edit-btn" tabindex="-1" id="edit-btn_{1}">${_('Write')}</a>
69 69 </li>
70 70 <li class="">
71 71 <a href="#preview-btn" tabindex="-1" id="preview-btn_{1}">${_('Preview')}</a>
72 72 </li>
73 <li class="pull-right">
74 <select class="comment-type" id="comment_type_{1}" name="comment_type">
75 % for val in c.visual.comment_types:
76 <option value="${val}">${val.upper()}</option>
77 % endfor
78 </select>
79 </li>
73 80 </ul>
74 81 </div>
75 82
76 83 <div class="comment-area-write" style="display: block;">
77 84 <div id="edit-container_{1}">
78 85 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
79 86 </div>
80 87 <div id="preview-container_{1}" class="clearfix" style="display: none;">
81 88 <div id="preview-box_{1}" class="preview-box"></div>
82 89 </div>
83 90 </div>
84 91
85 92 <div class="comment-area-footer">
86 93 <div class="toolbar">
87 94 <div class="toolbar-text">
88 95 ${(_('Comments parsed using %s syntax with %s support.') % (
89 96 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
90 97 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
91 98 )
92 99 )|n}
93 100 </div>
94 101 </div>
95 102 </div>
96 103 </div>
97 104
98 105 <div class="comment-footer">
99 106 <div class="action-buttons">
100 107 <input type="hidden" name="f_path" value="{0}">
101 108 <input type="hidden" name="line" value="{1}">
102 109 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
103 110 ${_('Cancel')}
104 111 </button>
105 112 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
106 113 </div>
107 114 ${h.end_form()}
108 115 </div>
109 116 %else:
110 117 ${h.form('', class_='inline-form comment-form-login', method='get')}
111 118 <div class="pull-left">
112 119 <div class="comment-help pull-right">
113 120 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
114 121 </div>
115 122 </div>
116 123 <div class="comment-button pull-right">
117 124 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
118 125 ${_('Cancel')}
119 126 </button>
120 127 </div>
121 128 <div class="clearfix"></div>
122 129 ${h.end_form()}
123 130 %endif
124 131 </div>
125 132 </div>
126 133
127 134 %endif
128 135 <%
129 136 collapse_all = len(diffset.files) > collapse_when_files_over
130 137 %>
131 138
132 139 %if c.diffmode == 'sideside':
133 140 <style>
134 141 .wrapper {
135 142 max-width: 1600px !important;
136 143 }
137 144 </style>
138 145 %endif
139 146
140 147 %if ruler_at_chars:
141 148 <style>
142 149 .diff table.cb .cb-content:after {
143 150 content: "";
144 151 border-left: 1px solid blue;
145 152 position: absolute;
146 153 top: 0;
147 154 height: 18px;
148 155 opacity: .2;
149 156 z-index: 10;
150 157 //## +5 to account for diff action (+/-)
151 158 left: ${ruler_at_chars + 5}ch;
152 159 </style>
153 160 %endif
154 161
155 162 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
156 163 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
157 164 %if commit:
158 165 <div class="pull-right">
159 166 <a class="btn tooltip" title="${_('Browse Files at revision {}').format(commit.raw_id)}" href="${h.url('files_home',repo_name=diffset.repo_name, revision=commit.raw_id, f_path='')}">
160 167 ${_('Browse Files')}
161 168 </a>
162 169 </div>
163 170 %endif
164 171 <h2 class="clearinner">
165 172 %if commit:
166 173 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
167 174 ${h.age_component(commit.date)} -
168 175 %endif
169 176 %if diffset.limited_diff:
170 177 ${_('The requested commit is too big and content was truncated.')}
171 178
172 179 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
173 180 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
174 181 %else:
175 182 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
176 183 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
177 184 %endif
178 185
179 186 <% at_ver = getattr(c, 'at_version_pos', None) %>
180 187 % if at_ver:
181 188 <div class="pull-right">
182 189 ${_('Showing changes at version %d') % at_ver}
183 190 </div>
184 191 % endif
185 192
186 193 </h2>
187 194 </div>
188 195
189 196 %if not diffset.files:
190 197 <p class="empty_data">${_('No files')}</p>
191 198 %endif
192 199
193 200 <div class="filediffs">
194 201 %for i, filediff in enumerate(diffset.files):
195 202
196 203 <%
197 204 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
198 205 over_lines_changed_limit = lines_changed > lines_changed_limit
199 206 %>
200 207 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
201 208 <div
202 209 class="filediff"
203 210 data-f-path="${filediff['patch']['filename']}"
204 211 id="a_${h.FID('', filediff['patch']['filename'])}">
205 212 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
206 213 <div class="filediff-collapse-indicator"></div>
207 214 ${diff_ops(filediff)}
208 215 </label>
209 216 ${diff_menu(filediff, use_comments=use_comments)}
210 217 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
211 218 %if not filediff.hunks:
212 219 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
213 220 <tr>
214 221 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
215 222 %if op_id == DEL_FILENODE:
216 223 ${_('File was deleted')}
217 224 %elif op_id == BIN_FILENODE:
218 225 ${_('Binary file hidden')}
219 226 %else:
220 227 ${op_text}
221 228 %endif
222 229 </td>
223 230 </tr>
224 231 %endfor
225 232 %endif
226 233 %if filediff.patch['is_limited_diff']:
227 234 <tr class="cb-warning cb-collapser">
228 235 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
229 236 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
230 237 </td>
231 238 </tr>
232 239 %else:
233 240 %if over_lines_changed_limit:
234 241 <tr class="cb-warning cb-collapser">
235 242 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
236 243 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
237 244 <a href="#" class="cb-expand"
238 245 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
239 246 </a>
240 247 <a href="#" class="cb-collapse"
241 248 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
242 249 </a>
243 250 </td>
244 251 </tr>
245 252 %endif
246 253 %endif
247 254
248 255 %for hunk in filediff.hunks:
249 256 <tr class="cb-hunk">
250 257 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
251 258 ## TODO: dan: add ajax loading of more context here
252 259 ## <a href="#">
253 260 <i class="icon-more"></i>
254 261 ## </a>
255 262 </td>
256 263 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
257 264 @@
258 265 -${hunk.source_start},${hunk.source_length}
259 266 +${hunk.target_start},${hunk.target_length}
260 267 ${hunk.section_header}
261 268 </td>
262 269 </tr>
263 270 %if c.diffmode == 'unified':
264 271 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
265 272 %elif c.diffmode == 'sideside':
266 273 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
267 274 %else:
268 275 <tr class="cb-line">
269 276 <td>unknown diff mode</td>
270 277 </tr>
271 278 %endif
272 279 %endfor
273 280
274 281 ## outdated comments that do not fit into currently displayed lines
275 282 % for lineno, comments in filediff.left_comments.items():
276 283
277 284 %if c.diffmode == 'unified':
278 285 <tr class="cb-line">
279 286 <td class="cb-data cb-context"></td>
280 287 <td class="cb-lineno cb-context"></td>
281 288 <td class="cb-lineno cb-context"></td>
282 289 <td class="cb-content cb-context">
283 290 ${inline_comments_container(comments)}
284 291 </td>
285 292 </tr>
286 293 %elif c.diffmode == 'sideside':
287 294 <tr class="cb-line">
288 295 <td class="cb-data cb-context"></td>
289 296 <td class="cb-lineno cb-context"></td>
290 297 <td class="cb-content cb-context"></td>
291 298
292 299 <td class="cb-data cb-context"></td>
293 300 <td class="cb-lineno cb-context"></td>
294 301 <td class="cb-content cb-context">
295 302 ${inline_comments_container(comments)}
296 303 </td>
297 304 </tr>
298 305 %endif
299 306
300 307 % endfor
301 308
302 309 </table>
303 310 </div>
304 311 %endfor
305 312
306 313 ## outdated comments that are made for a file that has been deleted
307 314 % for filename, comments_dict in (deleted_files_comments or {}).items():
308 315
309 316 <div class="filediffs filediff-outdated" style="display: none">
310 317 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
311 318 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
312 319 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
313 320 <div class="filediff-collapse-indicator"></div>
314 321 <span class="pill">
315 322 ## file was deleted
316 323 <strong>${filename}</strong>
317 324 </span>
318 325 <span class="pill-group" style="float: left">
319 326 ## file op, doesn't need translation
320 327 <span class="pill" op="removed">removed in this version</span>
321 328 </span>
322 329 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
323 330 <span class="pill-group" style="float: right">
324 331 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
325 332 </span>
326 333 </label>
327 334
328 335 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
329 336 <tr>
330 337 % if c.diffmode == 'unified':
331 338 <td></td>
332 339 %endif
333 340
334 341 <td></td>
335 342 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
336 343 ${_('File was deleted in this version, and outdated comments were made on it')}
337 344 </td>
338 345 </tr>
339 346 %if c.diffmode == 'unified':
340 347 <tr class="cb-line">
341 348 <td class="cb-data cb-context"></td>
342 349 <td class="cb-lineno cb-context"></td>
343 350 <td class="cb-lineno cb-context"></td>
344 351 <td class="cb-content cb-context">
345 352 ${inline_comments_container(comments_dict['comments'])}
346 353 </td>
347 354 </tr>
348 355 %elif c.diffmode == 'sideside':
349 356 <tr class="cb-line">
350 357 <td class="cb-data cb-context"></td>
351 358 <td class="cb-lineno cb-context"></td>
352 359 <td class="cb-content cb-context"></td>
353 360
354 361 <td class="cb-data cb-context"></td>
355 362 <td class="cb-lineno cb-context"></td>
356 363 <td class="cb-content cb-context">
357 364 ${inline_comments_container(comments_dict['comments'])}
358 365 </td>
359 366 </tr>
360 367 %endif
361 368 </table>
362 369 </div>
363 370 </div>
364 371 % endfor
365 372
366 373 </div>
367 374 </div>
368 375 </%def>
369 376
370 377 <%def name="diff_ops(filediff)">
371 378 <%
372 379 stats = filediff['patch']['stats']
373 380 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
374 381 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
375 382 %>
376 383 <span class="pill">
377 384 %if filediff.source_file_path and filediff.target_file_path:
378 385 %if filediff.source_file_path != filediff.target_file_path:
379 386 ## file was renamed
380 387 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
381 388 %else:
382 389 ## file was modified
383 390 <strong>${filediff.source_file_path}</strong>
384 391 %endif
385 392 %else:
386 393 %if filediff.source_file_path:
387 394 ## file was deleted
388 395 <strong>${filediff.source_file_path}</strong>
389 396 %else:
390 397 ## file was added
391 398 <strong>${filediff.target_file_path}</strong>
392 399 %endif
393 400 %endif
394 401 </span>
395 402 <span class="pill-group" style="float: left">
396 403 %if filediff.patch['is_limited_diff']:
397 404 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
398 405 %endif
399 406 %if RENAMED_FILENODE in stats['ops']:
400 407 <span class="pill" op="renamed">renamed</span>
401 408 %endif
402 409
403 410 %if NEW_FILENODE in stats['ops']:
404 411 <span class="pill" op="created">created</span>
405 412 %if filediff['target_mode'].startswith('120'):
406 413 <span class="pill" op="symlink">symlink</span>
407 414 %else:
408 415 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
409 416 %endif
410 417 %endif
411 418
412 419 %if DEL_FILENODE in stats['ops']:
413 420 <span class="pill" op="removed">removed</span>
414 421 %endif
415 422
416 423 %if CHMOD_FILENODE in stats['ops']:
417 424 <span class="pill" op="mode">
418 425 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
419 426 </span>
420 427 %endif
421 428 </span>
422 429
423 430 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
424 431
425 432 <span class="pill-group" style="float: right">
426 433 %if BIN_FILENODE in stats['ops']:
427 434 <span class="pill" op="binary">binary</span>
428 435 %if MOD_FILENODE in stats['ops']:
429 436 <span class="pill" op="modified">modified</span>
430 437 %endif
431 438 %endif
432 439 %if stats['added']:
433 440 <span class="pill" op="added">+${stats['added']}</span>
434 441 %endif
435 442 %if stats['deleted']:
436 443 <span class="pill" op="deleted">-${stats['deleted']}</span>
437 444 %endif
438 445 </span>
439 446
440 447 </%def>
441 448
442 449 <%def name="nice_mode(filemode)">
443 450 ${filemode.startswith('100') and filemode[3:] or filemode}
444 451 </%def>
445 452
446 453 <%def name="diff_menu(filediff, use_comments=False)">
447 454 <div class="filediff-menu">
448 455 %if filediff.diffset.source_ref:
449 456 %if filediff.patch['operation'] in ['D', 'M']:
450 457 <a
451 458 class="tooltip"
452 459 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
453 460 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
454 461 >
455 462 ${_('Show file before')}
456 463 </a> |
457 464 %else:
458 465 <span
459 466 class="tooltip"
460 467 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
461 468 >
462 469 ${_('Show file before')}
463 470 </span> |
464 471 %endif
465 472 %if filediff.patch['operation'] in ['A', 'M']:
466 473 <a
467 474 class="tooltip"
468 475 href="${h.url('files_home',repo_name=filediff.diffset.source_repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
469 476 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
470 477 >
471 478 ${_('Show file after')}
472 479 </a> |
473 480 %else:
474 481 <span
475 482 class="tooltip"
476 483 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
477 484 >
478 485 ${_('Show file after')}
479 486 </span> |
480 487 %endif
481 488 <a
482 489 class="tooltip"
483 490 title="${h.tooltip(_('Raw diff'))}"
484 491 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw')}"
485 492 >
486 493 ${_('Raw diff')}
487 494 </a> |
488 495 <a
489 496 class="tooltip"
490 497 title="${h.tooltip(_('Download diff'))}"
491 498 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download')}"
492 499 >
493 500 ${_('Download diff')}
494 501 </a>
495 502 % if use_comments:
496 503 |
497 504 % endif
498 505
499 506 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
500 507 %if hasattr(c, 'ignorews_url'):
501 508 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
502 509 %endif
503 510 %if hasattr(c, 'context_url'):
504 511 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
505 512 %endif
506 513
507 514 %if use_comments:
508 515 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
509 516 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
510 517 </a>
511 518 %endif
512 519 %endif
513 520 </div>
514 521 </%def>
515 522
516 523
517 524 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
518 525 <%def name="inline_comments_container(comments)">
519 526 <div class="inline-comments">
520 527 %for comment in comments:
521 528 ${commentblock.comment_block(comment, inline=True)}
522 529 %endfor
523 530
524 531 % if comments and comments[-1].outdated:
525 532 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
526 533 style="display: none;}">
527 534 ${_('Add another comment')}
528 535 </span>
529 536 % else:
530 537 <span onclick="return Rhodecode.comments.createComment(this)"
531 538 class="btn btn-secondary cb-comment-add-button">
532 539 ${_('Add another comment')}
533 540 </span>
534 541 % endif
535 542
536 543 </div>
537 544 </%def>
538 545
539 546
540 547 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
541 548 %for i, line in enumerate(hunk.sideside):
542 549 <%
543 550 old_line_anchor, new_line_anchor = None, None
544 551 if line.original.lineno:
545 552 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
546 553 if line.modified.lineno:
547 554 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
548 555 %>
549 556
550 557 <tr class="cb-line">
551 558 <td class="cb-data ${action_class(line.original.action)}"
552 559 data-line-number="${line.original.lineno}"
553 560 >
554 561 <div>
555 562 %if line.original.comments:
556 563 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
557 564 %endif
558 565 </div>
559 566 </td>
560 567 <td class="cb-lineno ${action_class(line.original.action)}"
561 568 data-line-number="${line.original.lineno}"
562 569 %if old_line_anchor:
563 570 id="${old_line_anchor}"
564 571 %endif
565 572 >
566 573 %if line.original.lineno:
567 574 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
568 575 %endif
569 576 </td>
570 577 <td class="cb-content ${action_class(line.original.action)}"
571 578 data-line-number="o${line.original.lineno}"
572 579 >
573 580 %if use_comments and line.original.lineno:
574 581 ${render_add_comment_button()}
575 582 %endif
576 583 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
577 584 %if use_comments and line.original.lineno and line.original.comments:
578 585 ${inline_comments_container(line.original.comments)}
579 586 %endif
580 587 </td>
581 588 <td class="cb-data ${action_class(line.modified.action)}"
582 589 data-line-number="${line.modified.lineno}"
583 590 >
584 591 <div>
585 592 %if line.modified.comments:
586 593 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
587 594 %endif
588 595 </div>
589 596 </td>
590 597 <td class="cb-lineno ${action_class(line.modified.action)}"
591 598 data-line-number="${line.modified.lineno}"
592 599 %if new_line_anchor:
593 600 id="${new_line_anchor}"
594 601 %endif
595 602 >
596 603 %if line.modified.lineno:
597 604 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
598 605 %endif
599 606 </td>
600 607 <td class="cb-content ${action_class(line.modified.action)}"
601 608 data-line-number="n${line.modified.lineno}"
602 609 >
603 610 %if use_comments and line.modified.lineno:
604 611 ${render_add_comment_button()}
605 612 %endif
606 613 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
607 614 %if use_comments and line.modified.lineno and line.modified.comments:
608 615 ${inline_comments_container(line.modified.comments)}
609 616 %endif
610 617 </td>
611 618 </tr>
612 619 %endfor
613 620 </%def>
614 621
615 622
616 623 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
617 624 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
618 625 <%
619 626 old_line_anchor, new_line_anchor = None, None
620 627 if old_line_no:
621 628 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
622 629 if new_line_no:
623 630 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
624 631 %>
625 632 <tr class="cb-line">
626 633 <td class="cb-data ${action_class(action)}">
627 634 <div>
628 635 %if comments:
629 636 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
630 637 %endif
631 638 </div>
632 639 </td>
633 640 <td class="cb-lineno ${action_class(action)}"
634 641 data-line-number="${old_line_no}"
635 642 %if old_line_anchor:
636 643 id="${old_line_anchor}"
637 644 %endif
638 645 >
639 646 %if old_line_anchor:
640 647 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
641 648 %endif
642 649 </td>
643 650 <td class="cb-lineno ${action_class(action)}"
644 651 data-line-number="${new_line_no}"
645 652 %if new_line_anchor:
646 653 id="${new_line_anchor}"
647 654 %endif
648 655 >
649 656 %if new_line_anchor:
650 657 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
651 658 %endif
652 659 </td>
653 660 <td class="cb-content ${action_class(action)}"
654 661 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
655 662 >
656 663 %if use_comments:
657 664 ${render_add_comment_button()}
658 665 %endif
659 666 <span class="cb-code">${action} ${content or '' | n}</span>
660 667 %if use_comments and comments:
661 668 ${inline_comments_container(comments)}
662 669 %endif
663 670 </td>
664 671 </tr>
665 672 %endfor
666 673 </%def>
667 674
668 675 <%def name="render_add_comment_button()">
669 676 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
670 677 <span><i class="icon-comment"></i></span>
671 678 </button>
672 679 </%def>
673 680
674 681 <%def name="render_diffset_menu()">
675 682
676 683 <div class="diffset-menu clearinner">
677 684 <div class="pull-right">
678 685 <div class="btn-group">
679 686
680 687 <a
681 688 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
682 689 title="${_('View side by side')}"
683 690 href="${h.url_replace(diffmode='sideside')}">
684 691 <span>${_('Side by Side')}</span>
685 692 </a>
686 693 <a
687 694 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
688 695 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
689 696 <span>${_('Unified')}</span>
690 697 </a>
691 698 </div>
692 699 </div>
693 700
694 701 <div class="pull-left">
695 702 <div class="btn-group">
696 703 <a
697 704 class="btn"
698 705 href="#"
699 706 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
700 707 <a
701 708 class="btn"
702 709 href="#"
703 710 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
704 711 <a
705 712 class="btn"
706 713 href="#"
707 714 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
708 715 </div>
709 716 </div>
710 717 </div>
711 718 </%def>
General Comments 0
You need to be logged in to leave comments. Login now