##// 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 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22
22
23 RhodeCode, a web based repository management software
23 RhodeCode, a web based repository management software
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 """
25 """
26
26
27 import os
27 import os
28 import sys
28 import sys
29 import platform
29 import platform
30
30
31 VERSION = tuple(open(os.path.join(
31 VERSION = tuple(open(os.path.join(
32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33
33
34 BACKENDS = {
34 BACKENDS = {
35 'hg': 'Mercurial repository',
35 'hg': 'Mercurial repository',
36 'git': 'Git repository',
36 'git': 'Git repository',
37 'svn': 'Subversion repository',
37 'svn': 'Subversion repository',
38 }
38 }
39
39
40 CELERY_ENABLED = False
40 CELERY_ENABLED = False
41 CELERY_EAGER = False
41 CELERY_EAGER = False
42
42
43 # link to config for pylons
43 # link to config for pylons
44 CONFIG = {}
44 CONFIG = {}
45
45
46 # Populated with the settings dictionary from application init in
46 # Populated with the settings dictionary from application init in
47 # rhodecode.conf.environment.load_pyramid_environment
47 # rhodecode.conf.environment.load_pyramid_environment
48 PYRAMID_SETTINGS = {}
48 PYRAMID_SETTINGS = {}
49
49
50 # Linked module for extensions
50 # Linked module for extensions
51 EXTENSIONS = {}
51 EXTENSIONS = {}
52
52
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
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 __platform__ = platform.system()
55 __platform__ = platform.system()
56 __license__ = 'AGPLv3, and Commercial License'
56 __license__ = 'AGPLv3, and Commercial License'
57 __author__ = 'RhodeCode GmbH'
57 __author__ = 'RhodeCode GmbH'
58 __url__ = 'https://code.rhodecode.com'
58 __url__ = 'https://code.rhodecode.com'
59
59
60 is_windows = __platform__ in ['Windows']
60 is_windows = __platform__ in ['Windows']
61 is_unix = not is_windows
61 is_unix = not is_windows
62 is_test = False
62 is_test = False
63 disable_error_handler = False
63 disable_error_handler = False
@@ -1,470 +1,473 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 commit controller for RhodeCode showing changes between commits
22 commit controller for RhodeCode showing changes between commits
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from collections import defaultdict
27 from collections import defaultdict
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29
29
30 from pylons import tmpl_context as c, request, response
30 from pylons import tmpl_context as c, request, response
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33
33
34 from rhodecode.lib import auth
34 from rhodecode.lib import auth
35 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib import diffs, codeblocks
36 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 import rhodecode.lib.helpers as h
41 import rhodecode.lib.helpers as h
42 from rhodecode.lib.utils import action_logger, jsonify
42 from rhodecode.lib.utils import action_logger, jsonify
43 from rhodecode.lib.utils2 import safe_unicode
43 from rhodecode.lib.utils2 import safe_unicode
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def _update_with_GET(params, GET):
57 def _update_with_GET(params, GET):
58 for k in ['diff1', 'diff2', 'diff']:
58 for k in ['diff1', 'diff2', 'diff']:
59 params[k] += GET.getall(k)
59 params[k] += GET.getall(k)
60
60
61
61
62 def get_ignore_ws(fid, GET):
62 def get_ignore_ws(fid, GET):
63 ig_ws_global = GET.get('ignorews')
63 ig_ws_global = GET.get('ignorews')
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 if ig_ws:
65 if ig_ws:
66 try:
66 try:
67 return int(ig_ws[0].split(':')[-1])
67 return int(ig_ws[0].split(':')[-1])
68 except Exception:
68 except Exception:
69 pass
69 pass
70 return ig_ws_global
70 return ig_ws_global
71
71
72
72
73 def _ignorews_url(GET, fileid=None):
73 def _ignorews_url(GET, fileid=None):
74 fileid = str(fileid) if fileid else None
74 fileid = str(fileid) if fileid else None
75 params = defaultdict(list)
75 params = defaultdict(list)
76 _update_with_GET(params, GET)
76 _update_with_GET(params, GET)
77 label = _('Show whitespace')
77 label = _('Show whitespace')
78 tooltiplbl = _('Show whitespace for all diffs')
78 tooltiplbl = _('Show whitespace for all diffs')
79 ig_ws = get_ignore_ws(fileid, GET)
79 ig_ws = get_ignore_ws(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
81
81
82 if ig_ws is None:
82 if ig_ws is None:
83 params['ignorews'] += [1]
83 params['ignorews'] += [1]
84 label = _('Ignore whitespace')
84 label = _('Ignore whitespace')
85 tooltiplbl = _('Ignore whitespace for all diffs')
85 tooltiplbl = _('Ignore whitespace for all diffs')
86 ctx_key = 'context'
86 ctx_key = 'context'
87 ctx_val = ln_ctx
87 ctx_val = ln_ctx
88
88
89 # if we have passed in ln_ctx pass it along to our params
89 # if we have passed in ln_ctx pass it along to our params
90 if ln_ctx:
90 if ln_ctx:
91 params[ctx_key] += [ctx_val]
91 params[ctx_key] += [ctx_val]
92
92
93 if fileid:
93 if fileid:
94 params['anchor'] = 'a_' + fileid
94 params['anchor'] = 'a_' + fileid
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96
96
97
97
98 def get_line_ctx(fid, GET):
98 def get_line_ctx(fid, GET):
99 ln_ctx_global = GET.get('context')
99 ln_ctx_global = GET.get('context')
100 if fid:
100 if fid:
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 else:
102 else:
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 if ln_ctx:
105 if ln_ctx:
106 ln_ctx = [ln_ctx]
106 ln_ctx = [ln_ctx]
107
107
108 if ln_ctx:
108 if ln_ctx:
109 retval = ln_ctx[0].split(':')[-1]
109 retval = ln_ctx[0].split(':')[-1]
110 else:
110 else:
111 retval = ln_ctx_global
111 retval = ln_ctx_global
112
112
113 try:
113 try:
114 return int(retval)
114 return int(retval)
115 except Exception:
115 except Exception:
116 return 3
116 return 3
117
117
118
118
119 def _context_url(GET, fileid=None):
119 def _context_url(GET, fileid=None):
120 """
120 """
121 Generates a url for context lines.
121 Generates a url for context lines.
122
122
123 :param fileid:
123 :param fileid:
124 """
124 """
125
125
126 fileid = str(fileid) if fileid else None
126 fileid = str(fileid) if fileid else None
127 ig_ws = get_ignore_ws(fileid, GET)
127 ig_ws = get_ignore_ws(fileid, GET)
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129
129
130 params = defaultdict(list)
130 params = defaultdict(list)
131 _update_with_GET(params, GET)
131 _update_with_GET(params, GET)
132
132
133 if ln_ctx > 0:
133 if ln_ctx > 0:
134 params['context'] += [ln_ctx]
134 params['context'] += [ln_ctx]
135
135
136 if ig_ws:
136 if ig_ws:
137 ig_ws_key = 'ignorews'
137 ig_ws_key = 'ignorews'
138 ig_ws_val = 1
138 ig_ws_val = 1
139 params[ig_ws_key] += [ig_ws_val]
139 params[ig_ws_key] += [ig_ws_val]
140
140
141 lbl = _('Increase context')
141 lbl = _('Increase context')
142 tooltiplbl = _('Increase context for all diffs')
142 tooltiplbl = _('Increase context for all diffs')
143
143
144 if fileid:
144 if fileid:
145 params['anchor'] = 'a_' + fileid
145 params['anchor'] = 'a_' + fileid
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147
147
148
148
149 class ChangesetController(BaseRepoController):
149 class ChangesetController(BaseRepoController):
150
150
151 def __before__(self):
151 def __before__(self):
152 super(ChangesetController, self).__before__()
152 super(ChangesetController, self).__before__()
153 c.affected_files_cut_off = 60
153 c.affected_files_cut_off = 60
154
154
155 def _index(self, commit_id_range, method):
155 def _index(self, commit_id_range, method):
156 c.ignorews_url = _ignorews_url
156 c.ignorews_url = _ignorews_url
157 c.context_url = _context_url
157 c.context_url = _context_url
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159
159
160 # fetch global flags of ignore ws or context lines
160 # fetch global flags of ignore ws or context lines
161 context_lcl = get_line_ctx('', request.GET)
161 context_lcl = get_line_ctx('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163
163
164 # diff_limit will cut off the whole diff if the limit is applied
164 # diff_limit will cut off the whole diff if the limit is applied
165 # otherwise it will just hide the big files from the front-end
165 # otherwise it will just hide the big files from the front-end
166 diff_limit = self.cut_off_limit_diff
166 diff_limit = self.cut_off_limit_diff
167 file_limit = self.cut_off_limit_file
167 file_limit = self.cut_off_limit_file
168
168
169 # get ranges of commit ids if preset
169 # get ranges of commit ids if preset
170 commit_range = commit_id_range.split('...')[:2]
170 commit_range = commit_id_range.split('...')[:2]
171
171
172 try:
172 try:
173 pre_load = ['affected_files', 'author', 'branch', 'date',
173 pre_load = ['affected_files', 'author', 'branch', 'date',
174 'message', 'parents']
174 'message', 'parents']
175
175
176 if len(commit_range) == 2:
176 if len(commit_range) == 2:
177 commits = c.rhodecode_repo.get_commits(
177 commits = c.rhodecode_repo.get_commits(
178 start_id=commit_range[0], end_id=commit_range[1],
178 start_id=commit_range[0], end_id=commit_range[1],
179 pre_load=pre_load)
179 pre_load=pre_load)
180 commits = list(commits)
180 commits = list(commits)
181 else:
181 else:
182 commits = [c.rhodecode_repo.get_commit(
182 commits = [c.rhodecode_repo.get_commit(
183 commit_id=commit_id_range, pre_load=pre_load)]
183 commit_id=commit_id_range, pre_load=pre_load)]
184
184
185 c.commit_ranges = commits
185 c.commit_ranges = commits
186 if not c.commit_ranges:
186 if not c.commit_ranges:
187 raise RepositoryError(
187 raise RepositoryError(
188 'The commit range returned an empty result')
188 'The commit range returned an empty result')
189 except CommitDoesNotExistError:
189 except CommitDoesNotExistError:
190 msg = _('No such commit exists for this repository')
190 msg = _('No such commit exists for this repository')
191 h.flash(msg, category='error')
191 h.flash(msg, category='error')
192 raise HTTPNotFound()
192 raise HTTPNotFound()
193 except Exception:
193 except Exception:
194 log.exception("General failure")
194 log.exception("General failure")
195 raise HTTPNotFound()
195 raise HTTPNotFound()
196
196
197 c.changes = OrderedDict()
197 c.changes = OrderedDict()
198 c.lines_added = 0
198 c.lines_added = 0
199 c.lines_deleted = 0
199 c.lines_deleted = 0
200
200
201 # auto collapse if we have more than limit
201 # auto collapse if we have more than limit
202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
204
204
205 c.commit_statuses = ChangesetStatus.STATUSES
205 c.commit_statuses = ChangesetStatus.STATUSES
206 c.inline_comments = []
206 c.inline_comments = []
207 c.files = []
207 c.files = []
208
208
209 c.statuses = []
209 c.statuses = []
210 c.comments = []
210 c.comments = []
211 if len(c.commit_ranges) == 1:
211 if len(c.commit_ranges) == 1:
212 commit = c.commit_ranges[0]
212 commit = c.commit_ranges[0]
213 c.comments = CommentsModel().get_comments(
213 c.comments = CommentsModel().get_comments(
214 c.rhodecode_db_repo.repo_id,
214 c.rhodecode_db_repo.repo_id,
215 revision=commit.raw_id)
215 revision=commit.raw_id)
216 c.statuses.append(ChangesetStatusModel().get_status(
216 c.statuses.append(ChangesetStatusModel().get_status(
217 c.rhodecode_db_repo.repo_id, commit.raw_id))
217 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 # comments from PR
218 # comments from PR
219 statuses = ChangesetStatusModel().get_statuses(
219 statuses = ChangesetStatusModel().get_statuses(
220 c.rhodecode_db_repo.repo_id, commit.raw_id,
220 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 with_revisions=True)
221 with_revisions=True)
222 prs = set(st.pull_request for st in statuses
222 prs = set(st.pull_request for st in statuses
223 if st.pull_request is not None)
223 if st.pull_request is not None)
224 # from associated statuses, check the pull requests, and
224 # from associated statuses, check the pull requests, and
225 # show comments from them
225 # show comments from them
226 for pr in prs:
226 for pr in prs:
227 c.comments.extend(pr.comments)
227 c.comments.extend(pr.comments)
228
228
229 # Iterate over ranges (default commit view is always one commit)
229 # Iterate over ranges (default commit view is always one commit)
230 for commit in c.commit_ranges:
230 for commit in c.commit_ranges:
231 c.changes[commit.raw_id] = []
231 c.changes[commit.raw_id] = []
232
232
233 commit2 = commit
233 commit2 = commit
234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
235
235
236 _diff = c.rhodecode_repo.get_diff(
236 _diff = c.rhodecode_repo.get_diff(
237 commit1, commit2,
237 commit1, commit2,
238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
239 diff_processor = diffs.DiffProcessor(
239 diff_processor = diffs.DiffProcessor(
240 _diff, format='newdiff', diff_limit=diff_limit,
240 _diff, format='newdiff', diff_limit=diff_limit,
241 file_limit=file_limit, show_full_diff=fulldiff)
241 file_limit=file_limit, show_full_diff=fulldiff)
242
242
243 commit_changes = OrderedDict()
243 commit_changes = OrderedDict()
244 if method == 'show':
244 if method == 'show':
245 _parsed = diff_processor.prepare()
245 _parsed = diff_processor.prepare()
246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
247
247
248 _parsed = diff_processor.prepare()
248 _parsed = diff_processor.prepare()
249
249
250 def _node_getter(commit):
250 def _node_getter(commit):
251 def get_node(fname):
251 def get_node(fname):
252 try:
252 try:
253 return commit.get_node(fname)
253 return commit.get_node(fname)
254 except NodeDoesNotExistError:
254 except NodeDoesNotExistError:
255 return None
255 return None
256 return get_node
256 return get_node
257
257
258 inline_comments = CommentsModel().get_inline_comments(
258 inline_comments = CommentsModel().get_inline_comments(
259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
260 c.inline_cnt = CommentsModel().get_inline_comments_count(
260 c.inline_cnt = CommentsModel().get_inline_comments_count(
261 inline_comments)
261 inline_comments)
262
262
263 diffset = codeblocks.DiffSet(
263 diffset = codeblocks.DiffSet(
264 repo_name=c.repo_name,
264 repo_name=c.repo_name,
265 source_node_getter=_node_getter(commit1),
265 source_node_getter=_node_getter(commit1),
266 target_node_getter=_node_getter(commit2),
266 target_node_getter=_node_getter(commit2),
267 comments=inline_comments
267 comments=inline_comments
268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
269 c.changes[commit.raw_id] = diffset
269 c.changes[commit.raw_id] = diffset
270 else:
270 else:
271 # downloads/raw we only need RAW diff nothing else
271 # downloads/raw we only need RAW diff nothing else
272 diff = diff_processor.as_raw()
272 diff = diff_processor.as_raw()
273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
274
274
275 # sort comments by how they were generated
275 # sort comments by how they were generated
276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
277
277
278
278
279 if len(c.commit_ranges) == 1:
279 if len(c.commit_ranges) == 1:
280 c.commit = c.commit_ranges[0]
280 c.commit = c.commit_ranges[0]
281 c.parent_tmpl = ''.join(
281 c.parent_tmpl = ''.join(
282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
283 if method == 'download':
283 if method == 'download':
284 response.content_type = 'text/plain'
284 response.content_type = 'text/plain'
285 response.content_disposition = (
285 response.content_disposition = (
286 'attachment; filename=%s.diff' % commit_id_range[:12])
286 'attachment; filename=%s.diff' % commit_id_range[:12])
287 return diff
287 return diff
288 elif method == 'patch':
288 elif method == 'patch':
289 response.content_type = 'text/plain'
289 response.content_type = 'text/plain'
290 c.diff = safe_unicode(diff)
290 c.diff = safe_unicode(diff)
291 return render('changeset/patch_changeset.mako')
291 return render('changeset/patch_changeset.mako')
292 elif method == 'raw':
292 elif method == 'raw':
293 response.content_type = 'text/plain'
293 response.content_type = 'text/plain'
294 return diff
294 return diff
295 elif method == 'show':
295 elif method == 'show':
296 if len(c.commit_ranges) == 1:
296 if len(c.commit_ranges) == 1:
297 return render('changeset/changeset.mako')
297 return render('changeset/changeset.mako')
298 else:
298 else:
299 c.ancestor = None
299 c.ancestor = None
300 c.target_repo = c.rhodecode_db_repo
300 c.target_repo = c.rhodecode_db_repo
301 return render('changeset/changeset_range.mako')
301 return render('changeset/changeset_range.mako')
302
302
303 @LoginRequired()
303 @LoginRequired()
304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
305 'repository.admin')
305 'repository.admin')
306 def index(self, revision, method='show'):
306 def index(self, revision, method='show'):
307 return self._index(revision, method=method)
307 return self._index(revision, method=method)
308
308
309 @LoginRequired()
309 @LoginRequired()
310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 'repository.admin')
311 'repository.admin')
312 def changeset_raw(self, revision):
312 def changeset_raw(self, revision):
313 return self._index(revision, method='raw')
313 return self._index(revision, method='raw')
314
314
315 @LoginRequired()
315 @LoginRequired()
316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
317 'repository.admin')
317 'repository.admin')
318 def changeset_patch(self, revision):
318 def changeset_patch(self, revision):
319 return self._index(revision, method='patch')
319 return self._index(revision, method='patch')
320
320
321 @LoginRequired()
321 @LoginRequired()
322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
323 'repository.admin')
323 'repository.admin')
324 def changeset_download(self, revision):
324 def changeset_download(self, revision):
325 return self._index(revision, method='download')
325 return self._index(revision, method='download')
326
326
327 @LoginRequired()
327 @LoginRequired()
328 @NotAnonymous()
328 @NotAnonymous()
329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
330 'repository.admin')
330 'repository.admin')
331 @auth.CSRFRequired()
331 @auth.CSRFRequired()
332 @jsonify
332 @jsonify
333 def comment(self, repo_name, revision):
333 def comment(self, repo_name, revision):
334 commit_id = revision
334 commit_id = revision
335 status = request.POST.get('changeset_status', None)
335 status = request.POST.get('changeset_status', None)
336 text = request.POST.get('text')
336 text = request.POST.get('text')
337 comment_type = request.POST.get('comment_type')
338
337 if status:
339 if status:
338 text = text or (_('Status change %(transition_icon)s %(status)s')
340 text = text or (_('Status change %(transition_icon)s %(status)s')
339 % {'transition_icon': '>',
341 % {'transition_icon': '>',
340 'status': ChangesetStatus.get_status_lbl(status)})
342 'status': ChangesetStatus.get_status_lbl(status)})
341
343
342 multi_commit_ids = filter(
344 multi_commit_ids = filter(
343 lambda s: s not in ['', None],
345 lambda s: s not in ['', None],
344 request.POST.get('commit_ids', '').split(','),)
346 request.POST.get('commit_ids', '').split(','),)
345
347
346 commit_ids = multi_commit_ids or [commit_id]
348 commit_ids = multi_commit_ids or [commit_id]
347 comment = None
349 comment = None
348 for current_id in filter(None, commit_ids):
350 for current_id in filter(None, commit_ids):
349 c.co = comment = CommentsModel().create(
351 c.co = comment = CommentsModel().create(
350 text=text,
352 text=text,
351 repo=c.rhodecode_db_repo.repo_id,
353 repo=c.rhodecode_db_repo.repo_id,
352 user=c.rhodecode_user.user_id,
354 user=c.rhodecode_user.user_id,
353 commit_id=current_id,
355 commit_id=current_id,
354 f_path=request.POST.get('f_path'),
356 f_path=request.POST.get('f_path'),
355 line_no=request.POST.get('line'),
357 line_no=request.POST.get('line'),
356 status_change=(ChangesetStatus.get_status_lbl(status)
358 status_change=(ChangesetStatus.get_status_lbl(status)
357 if status else None),
359 if status else None),
358 status_change_type=status
360 status_change_type=status,
361 comment_type=comment_type
359 )
362 )
360 c.inline_comment = True if comment.line_no else False
363 c.inline_comment = True if comment.line_no else False
361
364
362 # get status if set !
365 # get status if set !
363 if status:
366 if status:
364 # if latest status was from pull request and it's closed
367 # if latest status was from pull request and it's closed
365 # disallow changing status !
368 # disallow changing status !
366 # dont_allow_on_closed_pull_request = True !
369 # dont_allow_on_closed_pull_request = True !
367
370
368 try:
371 try:
369 ChangesetStatusModel().set_status(
372 ChangesetStatusModel().set_status(
370 c.rhodecode_db_repo.repo_id,
373 c.rhodecode_db_repo.repo_id,
371 status,
374 status,
372 c.rhodecode_user.user_id,
375 c.rhodecode_user.user_id,
373 comment,
376 comment,
374 revision=current_id,
377 revision=current_id,
375 dont_allow_on_closed_pull_request=True
378 dont_allow_on_closed_pull_request=True
376 )
379 )
377 except StatusChangeOnClosedPullRequestError:
380 except StatusChangeOnClosedPullRequestError:
378 msg = _('Changing the status of a commit associated with '
381 msg = _('Changing the status of a commit associated with '
379 'a closed pull request is not allowed')
382 'a closed pull request is not allowed')
380 log.exception(msg)
383 log.exception(msg)
381 h.flash(msg, category='warning')
384 h.flash(msg, category='warning')
382 return redirect(h.url(
385 return redirect(h.url(
383 'changeset_home', repo_name=repo_name,
386 'changeset_home', repo_name=repo_name,
384 revision=current_id))
387 revision=current_id))
385
388
386 # finalize, commit and redirect
389 # finalize, commit and redirect
387 Session().commit()
390 Session().commit()
388
391
389 data = {
392 data = {
390 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
393 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
391 }
394 }
392 if comment:
395 if comment:
393 data.update(comment.get_dict())
396 data.update(comment.get_dict())
394 data.update({'rendered_text':
397 data.update({'rendered_text':
395 render('changeset/changeset_comment_block.mako')})
398 render('changeset/changeset_comment_block.mako')})
396
399
397 return data
400 return data
398
401
399 @LoginRequired()
402 @LoginRequired()
400 @NotAnonymous()
403 @NotAnonymous()
401 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
404 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
402 'repository.admin')
405 'repository.admin')
403 @auth.CSRFRequired()
406 @auth.CSRFRequired()
404 def preview_comment(self):
407 def preview_comment(self):
405 # Technically a CSRF token is not needed as no state changes with this
408 # Technically a CSRF token is not needed as no state changes with this
406 # call. However, as this is a POST is better to have it, so automated
409 # call. However, as this is a POST is better to have it, so automated
407 # tools don't flag it as potential CSRF.
410 # tools don't flag it as potential CSRF.
408 # Post is required because the payload could be bigger than the maximum
411 # Post is required because the payload could be bigger than the maximum
409 # allowed by GET.
412 # allowed by GET.
410 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
413 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
411 raise HTTPBadRequest()
414 raise HTTPBadRequest()
412 text = request.POST.get('text')
415 text = request.POST.get('text')
413 renderer = request.POST.get('renderer') or 'rst'
416 renderer = request.POST.get('renderer') or 'rst'
414 if text:
417 if text:
415 return h.render(text, renderer=renderer, mentions=True)
418 return h.render(text, renderer=renderer, mentions=True)
416 return ''
419 return ''
417
420
418 @LoginRequired()
421 @LoginRequired()
419 @NotAnonymous()
422 @NotAnonymous()
420 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
423 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
421 'repository.admin')
424 'repository.admin')
422 @auth.CSRFRequired()
425 @auth.CSRFRequired()
423 @jsonify
426 @jsonify
424 def delete_comment(self, repo_name, comment_id):
427 def delete_comment(self, repo_name, comment_id):
425 comment = ChangesetComment.get(comment_id)
428 comment = ChangesetComment.get(comment_id)
426 owner = (comment.author.user_id == c.rhodecode_user.user_id)
429 owner = (comment.author.user_id == c.rhodecode_user.user_id)
427 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
430 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
428 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
431 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
429 CommentsModel().delete(comment=comment)
432 CommentsModel().delete(comment=comment)
430 Session().commit()
433 Session().commit()
431 return True
434 return True
432 else:
435 else:
433 raise HTTPForbidden()
436 raise HTTPForbidden()
434
437
435 @LoginRequired()
438 @LoginRequired()
436 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
439 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
437 'repository.admin')
440 'repository.admin')
438 @jsonify
441 @jsonify
439 def changeset_info(self, repo_name, revision):
442 def changeset_info(self, repo_name, revision):
440 if request.is_xhr:
443 if request.is_xhr:
441 try:
444 try:
442 return c.rhodecode_repo.get_commit(commit_id=revision)
445 return c.rhodecode_repo.get_commit(commit_id=revision)
443 except CommitDoesNotExistError as e:
446 except CommitDoesNotExistError as e:
444 return EmptyCommit(message=str(e))
447 return EmptyCommit(message=str(e))
445 else:
448 else:
446 raise HTTPBadRequest()
449 raise HTTPBadRequest()
447
450
448 @LoginRequired()
451 @LoginRequired()
449 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
452 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
450 'repository.admin')
453 'repository.admin')
451 @jsonify
454 @jsonify
452 def changeset_children(self, repo_name, revision):
455 def changeset_children(self, repo_name, revision):
453 if request.is_xhr:
456 if request.is_xhr:
454 commit = c.rhodecode_repo.get_commit(commit_id=revision)
457 commit = c.rhodecode_repo.get_commit(commit_id=revision)
455 result = {"results": commit.children}
458 result = {"results": commit.children}
456 return result
459 return result
457 else:
460 else:
458 raise HTTPBadRequest()
461 raise HTTPBadRequest()
459
462
460 @LoginRequired()
463 @LoginRequired()
461 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
464 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
462 'repository.admin')
465 'repository.admin')
463 @jsonify
466 @jsonify
464 def changeset_parents(self, repo_name, revision):
467 def changeset_parents(self, repo_name, revision):
465 if request.is_xhr:
468 if request.is_xhr:
466 commit = c.rhodecode_repo.get_commit(commit_id=revision)
469 commit = c.rhodecode_repo.get_commit(commit_id=revision)
467 result = {"results": commit.parents}
470 result = {"results": commit.parents}
468 return result
471 return result
469 else:
472 else:
470 raise HTTPBadRequest()
473 raise HTTPBadRequest()
@@ -1,1024 +1,1026 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel
63 from rhodecode.model.pull_request import PullRequestModel
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69 def __before__(self):
69 def __before__(self):
70 super(PullrequestsController, self).__before__()
70 super(PullrequestsController, self).__before__()
71
71
72 def _load_compare_data(self, pull_request, inline_comments):
72 def _load_compare_data(self, pull_request, inline_comments):
73 """
73 """
74 Load context data needed for generating compare diff
74 Load context data needed for generating compare diff
75
75
76 :param pull_request: object related to the request
76 :param pull_request: object related to the request
77 :param enable_comments: flag to determine if comments are included
77 :param enable_comments: flag to determine if comments are included
78 """
78 """
79 source_repo = pull_request.source_repo
79 source_repo = pull_request.source_repo
80 source_ref_id = pull_request.source_ref_parts.commit_id
80 source_ref_id = pull_request.source_ref_parts.commit_id
81
81
82 target_repo = pull_request.target_repo
82 target_repo = pull_request.target_repo
83 target_ref_id = pull_request.target_ref_parts.commit_id
83 target_ref_id = pull_request.target_ref_parts.commit_id
84
84
85 # despite opening commits for bookmarks/branches/tags, we always
85 # despite opening commits for bookmarks/branches/tags, we always
86 # convert this to rev to prevent changes after bookmark or branch change
86 # convert this to rev to prevent changes after bookmark or branch change
87 c.source_ref_type = 'rev'
87 c.source_ref_type = 'rev'
88 c.source_ref = source_ref_id
88 c.source_ref = source_ref_id
89
89
90 c.target_ref_type = 'rev'
90 c.target_ref_type = 'rev'
91 c.target_ref = target_ref_id
91 c.target_ref = target_ref_id
92
92
93 c.source_repo = source_repo
93 c.source_repo = source_repo
94 c.target_repo = target_repo
94 c.target_repo = target_repo
95
95
96 c.fulldiff = bool(request.GET.get('fulldiff'))
96 c.fulldiff = bool(request.GET.get('fulldiff'))
97
97
98 # diff_limit is the old behavior, will cut off the whole diff
98 # diff_limit is the old behavior, will cut off the whole diff
99 # if the limit is applied otherwise will just hide the
99 # if the limit is applied otherwise will just hide the
100 # big files from the front-end
100 # big files from the front-end
101 diff_limit = self.cut_off_limit_diff
101 diff_limit = self.cut_off_limit_diff
102 file_limit = self.cut_off_limit_file
102 file_limit = self.cut_off_limit_file
103
103
104 pre_load = ["author", "branch", "date", "message"]
104 pre_load = ["author", "branch", "date", "message"]
105
105
106 c.commit_ranges = []
106 c.commit_ranges = []
107 source_commit = EmptyCommit()
107 source_commit = EmptyCommit()
108 target_commit = EmptyCommit()
108 target_commit = EmptyCommit()
109 c.missing_requirements = False
109 c.missing_requirements = False
110 try:
110 try:
111 c.commit_ranges = [
111 c.commit_ranges = [
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 for rev in pull_request.revisions]
113 for rev in pull_request.revisions]
114
114
115 c.statuses = source_repo.statuses(
115 c.statuses = source_repo.statuses(
116 [x.raw_id for x in c.commit_ranges])
116 [x.raw_id for x in c.commit_ranges])
117
117
118 target_commit = source_repo.get_commit(
118 target_commit = source_repo.get_commit(
119 commit_id=safe_str(target_ref_id))
119 commit_id=safe_str(target_ref_id))
120 source_commit = source_repo.get_commit(
120 source_commit = source_repo.get_commit(
121 commit_id=safe_str(source_ref_id))
121 commit_id=safe_str(source_ref_id))
122 except RepositoryRequirementError:
122 except RepositoryRequirementError:
123 c.missing_requirements = True
123 c.missing_requirements = True
124
124
125 # auto collapse if we have more than limit
125 # auto collapse if we have more than limit
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128
128
129 c.changes = {}
129 c.changes = {}
130 c.missing_commits = False
130 c.missing_commits = False
131 if (c.missing_requirements or
131 if (c.missing_requirements or
132 isinstance(source_commit, EmptyCommit) or
132 isinstance(source_commit, EmptyCommit) or
133 source_commit == target_commit):
133 source_commit == target_commit):
134 _parsed = []
134 _parsed = []
135 c.missing_commits = True
135 c.missing_commits = True
136 else:
136 else:
137 vcs_diff = PullRequestModel().get_diff(pull_request)
137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 diff_processor = diffs.DiffProcessor(
138 diff_processor = diffs.DiffProcessor(
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 file_limit=file_limit, show_full_diff=c.fulldiff)
140 file_limit=file_limit, show_full_diff=c.fulldiff)
141
141
142 _parsed = diff_processor.prepare()
142 _parsed = diff_processor.prepare()
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144
144
145 included_files = {}
145 included_files = {}
146 for f in _parsed:
146 for f in _parsed:
147 included_files[f['filename']] = f['stats']
147 included_files[f['filename']] = f['stats']
148
148
149 c.deleted_files = [fname for fname in inline_comments if
149 c.deleted_files = [fname for fname in inline_comments if
150 fname not in included_files]
150 fname not in included_files]
151
151
152 c.deleted_files_comments = collections.defaultdict(dict)
152 c.deleted_files_comments = collections.defaultdict(dict)
153 for fname, per_line_comments in inline_comments.items():
153 for fname, per_line_comments in inline_comments.items():
154 if fname in c.deleted_files:
154 if fname in c.deleted_files:
155 c.deleted_files_comments[fname]['stats'] = 0
155 c.deleted_files_comments[fname]['stats'] = 0
156 c.deleted_files_comments[fname]['comments'] = list()
156 c.deleted_files_comments[fname]['comments'] = list()
157 for lno, comments in per_line_comments.items():
157 for lno, comments in per_line_comments.items():
158 c.deleted_files_comments[fname]['comments'].extend(comments)
158 c.deleted_files_comments[fname]['comments'].extend(comments)
159
159
160 def _node_getter(commit):
160 def _node_getter(commit):
161 def get_node(fname):
161 def get_node(fname):
162 try:
162 try:
163 return commit.get_node(fname)
163 return commit.get_node(fname)
164 except NodeDoesNotExistError:
164 except NodeDoesNotExistError:
165 return None
165 return None
166 return get_node
166 return get_node
167
167
168 c.diffset = codeblocks.DiffSet(
168 c.diffset = codeblocks.DiffSet(
169 repo_name=c.repo_name,
169 repo_name=c.repo_name,
170 source_repo_name=c.source_repo.repo_name,
170 source_repo_name=c.source_repo.repo_name,
171 source_node_getter=_node_getter(target_commit),
171 source_node_getter=_node_getter(target_commit),
172 target_node_getter=_node_getter(source_commit),
172 target_node_getter=_node_getter(source_commit),
173 comments=inline_comments
173 comments=inline_comments
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175
175
176 def _extract_ordering(self, request):
176 def _extract_ordering(self, request):
177 column_index = safe_int(request.GET.get('order[0][column]'))
177 column_index = safe_int(request.GET.get('order[0][column]'))
178 order_dir = request.GET.get('order[0][dir]', 'desc')
178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 order_by = request.GET.get(
179 order_by = request.GET.get(
180 'columns[%s][data][sort]' % column_index, 'name_raw')
180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 return order_by, order_dir
181 return order_by, order_dir
182
182
183 @LoginRequired()
183 @LoginRequired()
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 'repository.admin')
185 'repository.admin')
186 @HasAcceptedRepoType('git', 'hg')
186 @HasAcceptedRepoType('git', 'hg')
187 def show_all(self, repo_name):
187 def show_all(self, repo_name):
188 # filter types
188 # filter types
189 c.active = 'open'
189 c.active = 'open'
190 c.source = str2bool(request.GET.get('source'))
190 c.source = str2bool(request.GET.get('source'))
191 c.closed = str2bool(request.GET.get('closed'))
191 c.closed = str2bool(request.GET.get('closed'))
192 c.my = str2bool(request.GET.get('my'))
192 c.my = str2bool(request.GET.get('my'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 c.repo_name = repo_name
195 c.repo_name = repo_name
196
196
197 opened_by = None
197 opened_by = None
198 if c.my:
198 if c.my:
199 c.active = 'my'
199 c.active = 'my'
200 opened_by = [c.rhodecode_user.user_id]
200 opened_by = [c.rhodecode_user.user_id]
201
201
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 if c.closed:
203 if c.closed:
204 c.active = 'closed'
204 c.active = 'closed'
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 if c.awaiting_review and not c.source:
207 if c.awaiting_review and not c.source:
208 c.active = 'awaiting'
208 c.active = 'awaiting'
209 if c.source and not c.awaiting_review:
209 if c.source and not c.awaiting_review:
210 c.active = 'source'
210 c.active = 'source'
211 if c.awaiting_my_review:
211 if c.awaiting_my_review:
212 c.active = 'awaiting_my'
212 c.active = 'awaiting_my'
213
213
214 data = self._get_pull_requests_list(
214 data = self._get_pull_requests_list(
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 if not request.is_xhr:
216 if not request.is_xhr:
217 c.data = json.dumps(data['data'])
217 c.data = json.dumps(data['data'])
218 c.records_total = data['recordsTotal']
218 c.records_total = data['recordsTotal']
219 return render('/pullrequests/pullrequests.mako')
219 return render('/pullrequests/pullrequests.mako')
220 else:
220 else:
221 return json.dumps(data)
221 return json.dumps(data)
222
222
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 # pagination
224 # pagination
225 start = safe_int(request.GET.get('start'), 0)
225 start = safe_int(request.GET.get('start'), 0)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 order_by, order_dir = self._extract_ordering(request)
227 order_by, order_dir = self._extract_ordering(request)
228
228
229 if c.awaiting_review:
229 if c.awaiting_review:
230 pull_requests = PullRequestModel().get_awaiting_review(
230 pull_requests = PullRequestModel().get_awaiting_review(
231 repo_name, source=c.source, opened_by=opened_by,
231 repo_name, source=c.source, opened_by=opened_by,
232 statuses=statuses, offset=start, length=length,
232 statuses=statuses, offset=start, length=length,
233 order_by=order_by, order_dir=order_dir)
233 order_by=order_by, order_dir=order_dir)
234 pull_requests_total_count = PullRequestModel(
234 pull_requests_total_count = PullRequestModel(
235 ).count_awaiting_review(
235 ).count_awaiting_review(
236 repo_name, source=c.source, statuses=statuses,
236 repo_name, source=c.source, statuses=statuses,
237 opened_by=opened_by)
237 opened_by=opened_by)
238 elif c.awaiting_my_review:
238 elif c.awaiting_my_review:
239 pull_requests = PullRequestModel().get_awaiting_my_review(
239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 repo_name, source=c.source, opened_by=opened_by,
240 repo_name, source=c.source, opened_by=opened_by,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 offset=start, length=length, order_by=order_by,
242 offset=start, length=length, order_by=order_by,
243 order_dir=order_dir)
243 order_dir=order_dir)
244 pull_requests_total_count = PullRequestModel(
244 pull_requests_total_count = PullRequestModel(
245 ).count_awaiting_my_review(
245 ).count_awaiting_my_review(
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 statuses=statuses, opened_by=opened_by)
247 statuses=statuses, opened_by=opened_by)
248 else:
248 else:
249 pull_requests = PullRequestModel().get_all(
249 pull_requests = PullRequestModel().get_all(
250 repo_name, source=c.source, opened_by=opened_by,
250 repo_name, source=c.source, opened_by=opened_by,
251 statuses=statuses, offset=start, length=length,
251 statuses=statuses, offset=start, length=length,
252 order_by=order_by, order_dir=order_dir)
252 order_by=order_by, order_dir=order_dir)
253 pull_requests_total_count = PullRequestModel().count_all(
253 pull_requests_total_count = PullRequestModel().count_all(
254 repo_name, source=c.source, statuses=statuses,
254 repo_name, source=c.source, statuses=statuses,
255 opened_by=opened_by)
255 opened_by=opened_by)
256
256
257 from rhodecode.lib.utils import PartialRenderer
257 from rhodecode.lib.utils import PartialRenderer
258 _render = PartialRenderer('data_table/_dt_elements.mako')
258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 data = []
259 data = []
260 for pr in pull_requests:
260 for pr in pull_requests:
261 comments = CommentsModel().get_all_comments(
261 comments = CommentsModel().get_all_comments(
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263
263
264 data.append({
264 data.append({
265 'name': _render('pullrequest_name',
265 'name': _render('pullrequest_name',
266 pr.pull_request_id, pr.target_repo.repo_name),
266 pr.pull_request_id, pr.target_repo.repo_name),
267 'name_raw': pr.pull_request_id,
267 'name_raw': pr.pull_request_id,
268 'status': _render('pullrequest_status',
268 'status': _render('pullrequest_status',
269 pr.calculated_review_status()),
269 pr.calculated_review_status()),
270 'title': _render(
270 'title': _render(
271 'pullrequest_title', pr.title, pr.description),
271 'pullrequest_title', pr.title, pr.description),
272 'description': h.escape(pr.description),
272 'description': h.escape(pr.description),
273 'updated_on': _render('pullrequest_updated_on',
273 'updated_on': _render('pullrequest_updated_on',
274 h.datetime_to_time(pr.updated_on)),
274 h.datetime_to_time(pr.updated_on)),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 'created_on': _render('pullrequest_updated_on',
276 'created_on': _render('pullrequest_updated_on',
277 h.datetime_to_time(pr.created_on)),
277 h.datetime_to_time(pr.created_on)),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 'author': _render('pullrequest_author',
279 'author': _render('pullrequest_author',
280 pr.author.full_contact, ),
280 pr.author.full_contact, ),
281 'author_raw': pr.author.full_name,
281 'author_raw': pr.author.full_name,
282 'comments': _render('pullrequest_comments', len(comments)),
282 'comments': _render('pullrequest_comments', len(comments)),
283 'comments_raw': len(comments),
283 'comments_raw': len(comments),
284 'closed': pr.is_closed(),
284 'closed': pr.is_closed(),
285 })
285 })
286 # json used to render the grid
286 # json used to render the grid
287 data = ({
287 data = ({
288 'data': data,
288 'data': data,
289 'recordsTotal': pull_requests_total_count,
289 'recordsTotal': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
291 })
291 })
292 return data
292 return data
293
293
294 @LoginRequired()
294 @LoginRequired()
295 @NotAnonymous()
295 @NotAnonymous()
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 'repository.admin')
297 'repository.admin')
298 @HasAcceptedRepoType('git', 'hg')
298 @HasAcceptedRepoType('git', 'hg')
299 def index(self):
299 def index(self):
300 source_repo = c.rhodecode_db_repo
300 source_repo = c.rhodecode_db_repo
301
301
302 try:
302 try:
303 source_repo.scm_instance().get_commit()
303 source_repo.scm_instance().get_commit()
304 except EmptyRepositoryError:
304 except EmptyRepositoryError:
305 h.flash(h.literal(_('There are no commits yet')),
305 h.flash(h.literal(_('There are no commits yet')),
306 category='warning')
306 category='warning')
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308
308
309 commit_id = request.GET.get('commit')
309 commit_id = request.GET.get('commit')
310 branch_ref = request.GET.get('branch')
310 branch_ref = request.GET.get('branch')
311 bookmark_ref = request.GET.get('bookmark')
311 bookmark_ref = request.GET.get('bookmark')
312
312
313 try:
313 try:
314 source_repo_data = PullRequestModel().generate_repo_data(
314 source_repo_data = PullRequestModel().generate_repo_data(
315 source_repo, commit_id=commit_id,
315 source_repo, commit_id=commit_id,
316 branch=branch_ref, bookmark=bookmark_ref)
316 branch=branch_ref, bookmark=bookmark_ref)
317 except CommitDoesNotExistError as e:
317 except CommitDoesNotExistError as e:
318 log.exception(e)
318 log.exception(e)
319 h.flash(_('Commit does not exist'), 'error')
319 h.flash(_('Commit does not exist'), 'error')
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321
321
322 default_target_repo = source_repo
322 default_target_repo = source_repo
323
323
324 if source_repo.parent:
324 if source_repo.parent:
325 parent_vcs_obj = source_repo.parent.scm_instance()
325 parent_vcs_obj = source_repo.parent.scm_instance()
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 # change default if we have a parent repo
327 # change default if we have a parent repo
328 default_target_repo = source_repo.parent
328 default_target_repo = source_repo.parent
329
329
330 target_repo_data = PullRequestModel().generate_repo_data(
330 target_repo_data = PullRequestModel().generate_repo_data(
331 default_target_repo)
331 default_target_repo)
332
332
333 selected_source_ref = source_repo_data['refs']['selected_ref']
333 selected_source_ref = source_repo_data['refs']['selected_ref']
334
334
335 title_source_ref = selected_source_ref.split(':', 2)[1]
335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 c.default_title = PullRequestModel().generate_pullrequest_title(
336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 source=source_repo.repo_name,
337 source=source_repo.repo_name,
338 source_ref=title_source_ref,
338 source_ref=title_source_ref,
339 target=default_target_repo.repo_name
339 target=default_target_repo.repo_name
340 )
340 )
341
341
342 c.default_repo_data = {
342 c.default_repo_data = {
343 'source_repo_name': source_repo.repo_name,
343 'source_repo_name': source_repo.repo_name,
344 'source_refs_json': json.dumps(source_repo_data),
344 'source_refs_json': json.dumps(source_repo_data),
345 'target_repo_name': default_target_repo.repo_name,
345 'target_repo_name': default_target_repo.repo_name,
346 'target_refs_json': json.dumps(target_repo_data),
346 'target_refs_json': json.dumps(target_repo_data),
347 }
347 }
348 c.default_source_ref = selected_source_ref
348 c.default_source_ref = selected_source_ref
349
349
350 return render('/pullrequests/pullrequest.mako')
350 return render('/pullrequests/pullrequest.mako')
351
351
352 @LoginRequired()
352 @LoginRequired()
353 @NotAnonymous()
353 @NotAnonymous()
354 @XHRRequired()
354 @XHRRequired()
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 'repository.admin')
356 'repository.admin')
357 @jsonify
357 @jsonify
358 def get_repo_refs(self, repo_name, target_repo_name):
358 def get_repo_refs(self, repo_name, target_repo_name):
359 repo = Repository.get_by_repo_name(target_repo_name)
359 repo = Repository.get_by_repo_name(target_repo_name)
360 if not repo:
360 if not repo:
361 raise HTTPNotFound
361 raise HTTPNotFound
362 return PullRequestModel().generate_repo_data(repo)
362 return PullRequestModel().generate_repo_data(repo)
363
363
364 @LoginRequired()
364 @LoginRequired()
365 @NotAnonymous()
365 @NotAnonymous()
366 @XHRRequired()
366 @XHRRequired()
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 'repository.admin')
368 'repository.admin')
369 @jsonify
369 @jsonify
370 def get_repo_destinations(self, repo_name):
370 def get_repo_destinations(self, repo_name):
371 repo = Repository.get_by_repo_name(repo_name)
371 repo = Repository.get_by_repo_name(repo_name)
372 if not repo:
372 if not repo:
373 raise HTTPNotFound
373 raise HTTPNotFound
374 filter_query = request.GET.get('query')
374 filter_query = request.GET.get('query')
375
375
376 query = Repository.query() \
376 query = Repository.query() \
377 .order_by(func.length(Repository.repo_name)) \
377 .order_by(func.length(Repository.repo_name)) \
378 .filter(or_(
378 .filter(or_(
379 Repository.repo_name == repo.repo_name,
379 Repository.repo_name == repo.repo_name,
380 Repository.fork_id == repo.repo_id))
380 Repository.fork_id == repo.repo_id))
381
381
382 if filter_query:
382 if filter_query:
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 query = query.filter(
384 query = query.filter(
385 Repository.repo_name.ilike(ilike_expression))
385 Repository.repo_name.ilike(ilike_expression))
386
386
387 add_parent = False
387 add_parent = False
388 if repo.parent:
388 if repo.parent:
389 if filter_query in repo.parent.repo_name:
389 if filter_query in repo.parent.repo_name:
390 parent_vcs_obj = repo.parent.scm_instance()
390 parent_vcs_obj = repo.parent.scm_instance()
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 add_parent = True
392 add_parent = True
393
393
394 limit = 20 - 1 if add_parent else 20
394 limit = 20 - 1 if add_parent else 20
395 all_repos = query.limit(limit).all()
395 all_repos = query.limit(limit).all()
396 if add_parent:
396 if add_parent:
397 all_repos += [repo.parent]
397 all_repos += [repo.parent]
398
398
399 repos = []
399 repos = []
400 for obj in self.scm_model.get_repos(all_repos):
400 for obj in self.scm_model.get_repos(all_repos):
401 repos.append({
401 repos.append({
402 'id': obj['name'],
402 'id': obj['name'],
403 'text': obj['name'],
403 'text': obj['name'],
404 'type': 'repo',
404 'type': 'repo',
405 'obj': obj['dbrepo']
405 'obj': obj['dbrepo']
406 })
406 })
407
407
408 data = {
408 data = {
409 'more': False,
409 'more': False,
410 'results': [{
410 'results': [{
411 'text': _('Repositories'),
411 'text': _('Repositories'),
412 'children': repos
412 'children': repos
413 }] if repos else []
413 }] if repos else []
414 }
414 }
415 return data
415 return data
416
416
417 @LoginRequired()
417 @LoginRequired()
418 @NotAnonymous()
418 @NotAnonymous()
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 'repository.admin')
420 'repository.admin')
421 @HasAcceptedRepoType('git', 'hg')
421 @HasAcceptedRepoType('git', 'hg')
422 @auth.CSRFRequired()
422 @auth.CSRFRequired()
423 def create(self, repo_name):
423 def create(self, repo_name):
424 repo = Repository.get_by_repo_name(repo_name)
424 repo = Repository.get_by_repo_name(repo_name)
425 if not repo:
425 if not repo:
426 raise HTTPNotFound
426 raise HTTPNotFound
427
427
428 controls = peppercorn.parse(request.POST.items())
428 controls = peppercorn.parse(request.POST.items())
429
429
430 try:
430 try:
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 except formencode.Invalid as errors:
432 except formencode.Invalid as errors:
433 if errors.error_dict.get('revisions'):
433 if errors.error_dict.get('revisions'):
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 elif errors.error_dict.get('pullrequest_title'):
435 elif errors.error_dict.get('pullrequest_title'):
436 msg = _('Pull request requires a title with min. 3 chars')
436 msg = _('Pull request requires a title with min. 3 chars')
437 else:
437 else:
438 msg = _('Error creating pull request: {}').format(errors)
438 msg = _('Error creating pull request: {}').format(errors)
439 log.exception(msg)
439 log.exception(msg)
440 h.flash(msg, 'error')
440 h.flash(msg, 'error')
441
441
442 # would rather just go back to form ...
442 # would rather just go back to form ...
443 return redirect(url('pullrequest_home', repo_name=repo_name))
443 return redirect(url('pullrequest_home', repo_name=repo_name))
444
444
445 source_repo = _form['source_repo']
445 source_repo = _form['source_repo']
446 source_ref = _form['source_ref']
446 source_ref = _form['source_ref']
447 target_repo = _form['target_repo']
447 target_repo = _form['target_repo']
448 target_ref = _form['target_ref']
448 target_ref = _form['target_ref']
449 commit_ids = _form['revisions'][::-1]
449 commit_ids = _form['revisions'][::-1]
450 reviewers = [
450 reviewers = [
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452
452
453 # find the ancestor for this pr
453 # find the ancestor for this pr
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456
456
457 source_scm = source_db_repo.scm_instance()
457 source_scm = source_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
459
459
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462
462
463 ancestor = source_scm.get_common_ancestor(
463 ancestor = source_scm.get_common_ancestor(
464 source_commit.raw_id, target_commit.raw_id, target_scm)
464 source_commit.raw_id, target_commit.raw_id, target_scm)
465
465
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468
468
469 pullrequest_title = _form['pullrequest_title']
469 pullrequest_title = _form['pullrequest_title']
470 title_source_ref = source_ref.split(':', 2)[1]
470 title_source_ref = source_ref.split(':', 2)[1]
471 if not pullrequest_title:
471 if not pullrequest_title:
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 source=source_repo,
473 source=source_repo,
474 source_ref=title_source_ref,
474 source_ref=title_source_ref,
475 target=target_repo
475 target=target_repo
476 )
476 )
477
477
478 description = _form['pullrequest_desc']
478 description = _form['pullrequest_desc']
479 try:
479 try:
480 pull_request = PullRequestModel().create(
480 pull_request = PullRequestModel().create(
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 target_ref, commit_ids, reviewers, pullrequest_title,
482 target_ref, commit_ids, reviewers, pullrequest_title,
483 description
483 description
484 )
484 )
485 Session().commit()
485 Session().commit()
486 h.flash(_('Successfully opened new pull request'),
486 h.flash(_('Successfully opened new pull request'),
487 category='success')
487 category='success')
488 except Exception as e:
488 except Exception as e:
489 msg = _('Error occurred during sending pull request')
489 msg = _('Error occurred during sending pull request')
490 log.exception(msg)
490 log.exception(msg)
491 h.flash(msg, category='error')
491 h.flash(msg, category='error')
492 return redirect(url('pullrequest_home', repo_name=repo_name))
492 return redirect(url('pullrequest_home', repo_name=repo_name))
493
493
494 return redirect(url('pullrequest_show', repo_name=target_repo,
494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 pull_request_id=pull_request.pull_request_id))
495 pull_request_id=pull_request.pull_request_id))
496
496
497 @LoginRequired()
497 @LoginRequired()
498 @NotAnonymous()
498 @NotAnonymous()
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 'repository.admin')
500 'repository.admin')
501 @auth.CSRFRequired()
501 @auth.CSRFRequired()
502 @jsonify
502 @jsonify
503 def update(self, repo_name, pull_request_id):
503 def update(self, repo_name, pull_request_id):
504 pull_request_id = safe_int(pull_request_id)
504 pull_request_id = safe_int(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
506 # only owner or admin can update it
506 # only owner or admin can update it
507 allowed_to_update = PullRequestModel().check_user_update(
507 allowed_to_update = PullRequestModel().check_user_update(
508 pull_request, c.rhodecode_user)
508 pull_request, c.rhodecode_user)
509 if allowed_to_update:
509 if allowed_to_update:
510 controls = peppercorn.parse(request.POST.items())
510 controls = peppercorn.parse(request.POST.items())
511
511
512 if 'review_members' in controls:
512 if 'review_members' in controls:
513 self._update_reviewers(
513 self._update_reviewers(
514 pull_request_id, controls['review_members'])
514 pull_request_id, controls['review_members'])
515 elif str2bool(request.POST.get('update_commits', 'false')):
515 elif str2bool(request.POST.get('update_commits', 'false')):
516 self._update_commits(pull_request)
516 self._update_commits(pull_request)
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 self._reject_close(pull_request)
518 self._reject_close(pull_request)
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 self._edit_pull_request(pull_request)
520 self._edit_pull_request(pull_request)
521 else:
521 else:
522 raise HTTPBadRequest()
522 raise HTTPBadRequest()
523 return True
523 return True
524 raise HTTPForbidden()
524 raise HTTPForbidden()
525
525
526 def _edit_pull_request(self, pull_request):
526 def _edit_pull_request(self, pull_request):
527 try:
527 try:
528 PullRequestModel().edit(
528 PullRequestModel().edit(
529 pull_request, request.POST.get('title'),
529 pull_request, request.POST.get('title'),
530 request.POST.get('description'))
530 request.POST.get('description'))
531 except ValueError:
531 except ValueError:
532 msg = _(u'Cannot update closed pull requests.')
532 msg = _(u'Cannot update closed pull requests.')
533 h.flash(msg, category='error')
533 h.flash(msg, category='error')
534 return
534 return
535 else:
535 else:
536 Session().commit()
536 Session().commit()
537
537
538 msg = _(u'Pull request title & description updated.')
538 msg = _(u'Pull request title & description updated.')
539 h.flash(msg, category='success')
539 h.flash(msg, category='success')
540 return
540 return
541
541
542 def _update_commits(self, pull_request):
542 def _update_commits(self, pull_request):
543 resp = PullRequestModel().update_commits(pull_request)
543 resp = PullRequestModel().update_commits(pull_request)
544
544
545 if resp.executed:
545 if resp.executed:
546 msg = _(
546 msg = _(
547 u'Pull request updated to "{source_commit_id}" with '
547 u'Pull request updated to "{source_commit_id}" with '
548 u'{count_added} added, {count_removed} removed commits.')
548 u'{count_added} added, {count_removed} removed commits.')
549 msg = msg.format(
549 msg = msg.format(
550 source_commit_id=pull_request.source_ref_parts.commit_id,
550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 count_added=len(resp.changes.added),
551 count_added=len(resp.changes.added),
552 count_removed=len(resp.changes.removed))
552 count_removed=len(resp.changes.removed))
553 h.flash(msg, category='success')
553 h.flash(msg, category='success')
554
554
555 registry = get_current_registry()
555 registry = get_current_registry()
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 if channelstream_config.get('enabled'):
558 if channelstream_config.get('enabled'):
559 message = msg + (
559 message = msg + (
560 ' - <a onclick="window.location.reload()">'
560 ' - <a onclick="window.location.reload()">'
561 '<strong>{}</strong></a>'.format(_('Reload page')))
561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 channel = '/repo${}$/pr/{}'.format(
562 channel = '/repo${}$/pr/{}'.format(
563 pull_request.target_repo.repo_name,
563 pull_request.target_repo.repo_name,
564 pull_request.pull_request_id
564 pull_request.pull_request_id
565 )
565 )
566 payload = {
566 payload = {
567 'type': 'message',
567 'type': 'message',
568 'user': 'system',
568 'user': 'system',
569 'exclude_users': [request.user.username],
569 'exclude_users': [request.user.username],
570 'channel': channel,
570 'channel': channel,
571 'message': {
571 'message': {
572 'message': message,
572 'message': message,
573 'level': 'success',
573 'level': 'success',
574 'topic': '/notifications'
574 'topic': '/notifications'
575 }
575 }
576 }
576 }
577 channelstream_request(
577 channelstream_request(
578 channelstream_config, [payload], '/message',
578 channelstream_config, [payload], '/message',
579 raise_exc=False)
579 raise_exc=False)
580 else:
580 else:
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 warning_reasons = [
582 warning_reasons = [
583 UpdateFailureReason.NO_CHANGE,
583 UpdateFailureReason.NO_CHANGE,
584 UpdateFailureReason.WRONG_REF_TPYE,
584 UpdateFailureReason.WRONG_REF_TPYE,
585 ]
585 ]
586 category = 'warning' if resp.reason in warning_reasons else 'error'
586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 h.flash(msg, category=category)
587 h.flash(msg, category=category)
588
588
589 @auth.CSRFRequired()
589 @auth.CSRFRequired()
590 @LoginRequired()
590 @LoginRequired()
591 @NotAnonymous()
591 @NotAnonymous()
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 'repository.admin')
593 'repository.admin')
594 def merge(self, repo_name, pull_request_id):
594 def merge(self, repo_name, pull_request_id):
595 """
595 """
596 POST /{repo_name}/pull-request/{pull_request_id}
596 POST /{repo_name}/pull-request/{pull_request_id}
597
597
598 Merge will perform a server-side merge of the specified
598 Merge will perform a server-side merge of the specified
599 pull request, if the pull request is approved and mergeable.
599 pull request, if the pull request is approved and mergeable.
600 After succesfull merging, the pull request is automatically
600 After succesfull merging, the pull request is automatically
601 closed, with a relevant comment.
601 closed, with a relevant comment.
602 """
602 """
603 pull_request_id = safe_int(pull_request_id)
603 pull_request_id = safe_int(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
605 user = c.rhodecode_user
605 user = c.rhodecode_user
606
606
607 if self._meets_merge_pre_conditions(pull_request, user):
607 if self._meets_merge_pre_conditions(pull_request, user):
608 log.debug("Pre-conditions checked, trying to merge.")
608 log.debug("Pre-conditions checked, trying to merge.")
609 extras = vcs_operation_context(
609 extras = vcs_operation_context(
610 request.environ, repo_name=pull_request.target_repo.repo_name,
610 request.environ, repo_name=pull_request.target_repo.repo_name,
611 username=user.username, action='push',
611 username=user.username, action='push',
612 scm=pull_request.target_repo.repo_type)
612 scm=pull_request.target_repo.repo_type)
613 self._merge_pull_request(pull_request, user, extras)
613 self._merge_pull_request(pull_request, user, extras)
614
614
615 return redirect(url(
615 return redirect(url(
616 'pullrequest_show',
616 'pullrequest_show',
617 repo_name=pull_request.target_repo.repo_name,
617 repo_name=pull_request.target_repo.repo_name,
618 pull_request_id=pull_request.pull_request_id))
618 pull_request_id=pull_request.pull_request_id))
619
619
620 def _meets_merge_pre_conditions(self, pull_request, user):
620 def _meets_merge_pre_conditions(self, pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
622 raise HTTPForbidden()
622 raise HTTPForbidden()
623
623
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 if not merge_status:
625 if not merge_status:
626 log.debug("Cannot merge, not mergeable.")
626 log.debug("Cannot merge, not mergeable.")
627 h.flash(msg, category='error')
627 h.flash(msg, category='error')
628 return False
628 return False
629
629
630 if (pull_request.calculated_review_status()
630 if (pull_request.calculated_review_status()
631 is not ChangesetStatus.STATUS_APPROVED):
631 is not ChangesetStatus.STATUS_APPROVED):
632 log.debug("Cannot merge, approval is pending.")
632 log.debug("Cannot merge, approval is pending.")
633 msg = _('Pull request reviewer approval is pending.')
633 msg = _('Pull request reviewer approval is pending.')
634 h.flash(msg, category='error')
634 h.flash(msg, category='error')
635 return False
635 return False
636 return True
636 return True
637
637
638 def _merge_pull_request(self, pull_request, user, extras):
638 def _merge_pull_request(self, pull_request, user, extras):
639 merge_resp = PullRequestModel().merge(
639 merge_resp = PullRequestModel().merge(
640 pull_request, user, extras=extras)
640 pull_request, user, extras=extras)
641
641
642 if merge_resp.executed:
642 if merge_resp.executed:
643 log.debug("The merge was successful, closing the pull request.")
643 log.debug("The merge was successful, closing the pull request.")
644 PullRequestModel().close_pull_request(
644 PullRequestModel().close_pull_request(
645 pull_request.pull_request_id, user)
645 pull_request.pull_request_id, user)
646 Session().commit()
646 Session().commit()
647 msg = _('Pull request was successfully merged and closed.')
647 msg = _('Pull request was successfully merged and closed.')
648 h.flash(msg, category='success')
648 h.flash(msg, category='success')
649 else:
649 else:
650 log.debug(
650 log.debug(
651 "The merge was not successful. Merge response: %s",
651 "The merge was not successful. Merge response: %s",
652 merge_resp)
652 merge_resp)
653 msg = PullRequestModel().merge_status_message(
653 msg = PullRequestModel().merge_status_message(
654 merge_resp.failure_reason)
654 merge_resp.failure_reason)
655 h.flash(msg, category='error')
655 h.flash(msg, category='error')
656
656
657 def _update_reviewers(self, pull_request_id, review_members):
657 def _update_reviewers(self, pull_request_id, review_members):
658 reviewers = [
658 reviewers = [
659 (int(r['user_id']), r['reasons']) for r in review_members]
659 (int(r['user_id']), r['reasons']) for r in review_members]
660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 Session().commit()
661 Session().commit()
662
662
663 def _reject_close(self, pull_request):
663 def _reject_close(self, pull_request):
664 if pull_request.is_closed():
664 if pull_request.is_closed():
665 raise HTTPForbidden()
665 raise HTTPForbidden()
666
666
667 PullRequestModel().close_pull_request_with_comment(
667 PullRequestModel().close_pull_request_with_comment(
668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 Session().commit()
669 Session().commit()
670
670
671 @LoginRequired()
671 @LoginRequired()
672 @NotAnonymous()
672 @NotAnonymous()
673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 'repository.admin')
674 'repository.admin')
675 @auth.CSRFRequired()
675 @auth.CSRFRequired()
676 @jsonify
676 @jsonify
677 def delete(self, repo_name, pull_request_id):
677 def delete(self, repo_name, pull_request_id):
678 pull_request_id = safe_int(pull_request_id)
678 pull_request_id = safe_int(pull_request_id)
679 pull_request = PullRequest.get_or_404(pull_request_id)
679 pull_request = PullRequest.get_or_404(pull_request_id)
680 # only owner can delete it !
680 # only owner can delete it !
681 if pull_request.author.user_id == c.rhodecode_user.user_id:
681 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 PullRequestModel().delete(pull_request)
682 PullRequestModel().delete(pull_request)
683 Session().commit()
683 Session().commit()
684 h.flash(_('Successfully deleted pull request'),
684 h.flash(_('Successfully deleted pull request'),
685 category='success')
685 category='success')
686 return redirect(url('my_account_pullrequests'))
686 return redirect(url('my_account_pullrequests'))
687 raise HTTPForbidden()
687 raise HTTPForbidden()
688
688
689 def _get_pr_version(self, pull_request_id, version=None):
689 def _get_pr_version(self, pull_request_id, version=None):
690 pull_request_id = safe_int(pull_request_id)
690 pull_request_id = safe_int(pull_request_id)
691 at_version = None
691 at_version = None
692
692
693 if version and version == 'latest':
693 if version and version == 'latest':
694 pull_request_ver = PullRequest.get(pull_request_id)
694 pull_request_ver = PullRequest.get(pull_request_id)
695 pull_request_obj = pull_request_ver
695 pull_request_obj = pull_request_ver
696 _org_pull_request_obj = pull_request_obj
696 _org_pull_request_obj = pull_request_obj
697 at_version = 'latest'
697 at_version = 'latest'
698 elif version:
698 elif version:
699 pull_request_ver = PullRequestVersion.get_or_404(version)
699 pull_request_ver = PullRequestVersion.get_or_404(version)
700 pull_request_obj = pull_request_ver
700 pull_request_obj = pull_request_ver
701 _org_pull_request_obj = pull_request_ver.pull_request
701 _org_pull_request_obj = pull_request_ver.pull_request
702 at_version = pull_request_ver.pull_request_version_id
702 at_version = pull_request_ver.pull_request_version_id
703 else:
703 else:
704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
705
705
706 pull_request_display_obj = PullRequest.get_pr_display_object(
706 pull_request_display_obj = PullRequest.get_pr_display_object(
707 pull_request_obj, _org_pull_request_obj)
707 pull_request_obj, _org_pull_request_obj)
708 return _org_pull_request_obj, pull_request_obj, \
708 return _org_pull_request_obj, pull_request_obj, \
709 pull_request_display_obj, at_version
709 pull_request_display_obj, at_version
710
710
711 def _get_pr_version_changes(self, version, pull_request_latest):
711 def _get_pr_version_changes(self, version, pull_request_latest):
712 """
712 """
713 Generate changes commits, and diff data based on the current pr version
713 Generate changes commits, and diff data based on the current pr version
714 """
714 """
715
715
716 #TODO(marcink): save those changes as JSON metadata for chaching later.
716 #TODO(marcink): save those changes as JSON metadata for chaching later.
717
717
718 # fake the version to add the "initial" state object
718 # fake the version to add the "initial" state object
719 pull_request_initial = PullRequest.get_pr_display_object(
719 pull_request_initial = PullRequest.get_pr_display_object(
720 pull_request_latest, pull_request_latest,
720 pull_request_latest, pull_request_latest,
721 internal_methods=['get_commit', 'versions'])
721 internal_methods=['get_commit', 'versions'])
722 pull_request_initial.revisions = []
722 pull_request_initial.revisions = []
723 pull_request_initial.source_repo.get_commit = types.MethodType(
723 pull_request_initial.source_repo.get_commit = types.MethodType(
724 lambda *a, **k: EmptyCommit(), pull_request_initial)
724 lambda *a, **k: EmptyCommit(), pull_request_initial)
725 pull_request_initial.source_repo.scm_instance = types.MethodType(
725 pull_request_initial.source_repo.scm_instance = types.MethodType(
726 lambda *a, **k: EmptyRepository(), pull_request_initial)
726 lambda *a, **k: EmptyRepository(), pull_request_initial)
727
727
728 _changes_versions = [pull_request_latest] + \
728 _changes_versions = [pull_request_latest] + \
729 list(reversed(c.versions)) + \
729 list(reversed(c.versions)) + \
730 [pull_request_initial]
730 [pull_request_initial]
731
731
732 if version == 'latest':
732 if version == 'latest':
733 index = 0
733 index = 0
734 else:
734 else:
735 for pos, prver in enumerate(_changes_versions):
735 for pos, prver in enumerate(_changes_versions):
736 ver = getattr(prver, 'pull_request_version_id', -1)
736 ver = getattr(prver, 'pull_request_version_id', -1)
737 if ver == safe_int(version):
737 if ver == safe_int(version):
738 index = pos
738 index = pos
739 break
739 break
740 else:
740 else:
741 index = 0
741 index = 0
742
742
743 cur_obj = _changes_versions[index]
743 cur_obj = _changes_versions[index]
744 prev_obj = _changes_versions[index + 1]
744 prev_obj = _changes_versions[index + 1]
745
745
746 old_commit_ids = set(prev_obj.revisions)
746 old_commit_ids = set(prev_obj.revisions)
747 new_commit_ids = set(cur_obj.revisions)
747 new_commit_ids = set(cur_obj.revisions)
748
748
749 changes = PullRequestModel()._calculate_commit_id_changes(
749 changes = PullRequestModel()._calculate_commit_id_changes(
750 old_commit_ids, new_commit_ids)
750 old_commit_ids, new_commit_ids)
751
751
752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
753 cur_obj, prev_obj)
753 cur_obj, prev_obj)
754 file_changes = PullRequestModel()._calculate_file_changes(
754 file_changes = PullRequestModel()._calculate_file_changes(
755 old_diff_data, new_diff_data)
755 old_diff_data, new_diff_data)
756 return changes, file_changes
756 return changes, file_changes
757
757
758 @LoginRequired()
758 @LoginRequired()
759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 'repository.admin')
760 'repository.admin')
761 def show(self, repo_name, pull_request_id):
761 def show(self, repo_name, pull_request_id):
762 pull_request_id = safe_int(pull_request_id)
762 pull_request_id = safe_int(pull_request_id)
763 version = request.GET.get('version')
763 version = request.GET.get('version')
764
764
765 (pull_request_latest,
765 (pull_request_latest,
766 pull_request_at_ver,
766 pull_request_at_ver,
767 pull_request_display_obj,
767 pull_request_display_obj,
768 at_version) = self._get_pr_version(pull_request_id, version=version)
768 at_version) = self._get_pr_version(pull_request_id, version=version)
769
769
770 c.template_context['pull_request_data']['pull_request_id'] = \
770 c.template_context['pull_request_data']['pull_request_id'] = \
771 pull_request_id
771 pull_request_id
772
772
773 # pull_requests repo_name we opened it against
773 # pull_requests repo_name we opened it against
774 # ie. target_repo must match
774 # ie. target_repo must match
775 if repo_name != pull_request_at_ver.target_repo.repo_name:
775 if repo_name != pull_request_at_ver.target_repo.repo_name:
776 raise HTTPNotFound
776 raise HTTPNotFound
777
777
778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
779 pull_request_at_ver)
779 pull_request_at_ver)
780
780
781 pr_closed = pull_request_latest.is_closed()
781 pr_closed = pull_request_latest.is_closed()
782 if at_version and not at_version == 'latest':
782 if at_version and not at_version == 'latest':
783 c.allowed_to_change_status = False
783 c.allowed_to_change_status = False
784 c.allowed_to_update = False
784 c.allowed_to_update = False
785 c.allowed_to_merge = False
785 c.allowed_to_merge = False
786 c.allowed_to_delete = False
786 c.allowed_to_delete = False
787 c.allowed_to_comment = False
787 c.allowed_to_comment = False
788 else:
788 else:
789 c.allowed_to_change_status = PullRequestModel(). \
789 c.allowed_to_change_status = PullRequestModel(). \
790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
791 c.allowed_to_update = PullRequestModel().check_user_update(
791 c.allowed_to_update = PullRequestModel().check_user_update(
792 pull_request_latest, c.rhodecode_user) and not pr_closed
792 pull_request_latest, c.rhodecode_user) and not pr_closed
793 c.allowed_to_merge = PullRequestModel().check_user_merge(
793 c.allowed_to_merge = PullRequestModel().check_user_merge(
794 pull_request_latest, c.rhodecode_user) and not pr_closed
794 pull_request_latest, c.rhodecode_user) and not pr_closed
795 c.allowed_to_delete = PullRequestModel().check_user_delete(
795 c.allowed_to_delete = PullRequestModel().check_user_delete(
796 pull_request_latest, c.rhodecode_user) and not pr_closed
796 pull_request_latest, c.rhodecode_user) and not pr_closed
797 c.allowed_to_comment = not pr_closed
797 c.allowed_to_comment = not pr_closed
798
798
799 cc_model = CommentsModel()
799 cc_model = CommentsModel()
800
800
801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
804 pull_request_at_ver)
804 pull_request_at_ver)
805 c.approval_msg = None
805 c.approval_msg = None
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
807 c.approval_msg = _('Reviewer approval is pending.')
807 c.approval_msg = _('Reviewer approval is pending.')
808 c.pr_merge_status = False
808 c.pr_merge_status = False
809
809
810 # inline comments
810 # inline comments
811 inline_comments = cc_model.get_inline_comments(
811 inline_comments = cc_model.get_inline_comments(
812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
813
813
814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
815 inline_comments, version=at_version, include_aggregates=True)
815 inline_comments, version=at_version, include_aggregates=True)
816
816
817 c.versions = pull_request_display_obj.versions()
817 c.versions = pull_request_display_obj.versions()
818 c.at_version_num = at_version if at_version and at_version != 'latest' else None
818 c.at_version_num = at_version if at_version and at_version != 'latest' else None
819 c.at_version_pos = ChangesetComment.get_index_from_version(
819 c.at_version_pos = ChangesetComment.get_index_from_version(
820 c.at_version_num, c.versions)
820 c.at_version_num, c.versions)
821
821
822 is_outdated = lambda co: \
822 is_outdated = lambda co: \
823 not c.at_version_num \
823 not c.at_version_num \
824 or co.pull_request_version_id <= c.at_version_num
824 or co.pull_request_version_id <= c.at_version_num
825
825
826 # inline_comments_until_version
826 # inline_comments_until_version
827 if c.at_version_num:
827 if c.at_version_num:
828 # if we use version, then do not show later comments
828 # if we use version, then do not show later comments
829 # than current version
829 # than current version
830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
831 for fname, per_line_comments in inline_comments.iteritems():
831 for fname, per_line_comments in inline_comments.iteritems():
832 for lno, comments in per_line_comments.iteritems():
832 for lno, comments in per_line_comments.iteritems():
833 for co in comments:
833 for co in comments:
834 if co.pull_request_version_id and is_outdated(co):
834 if co.pull_request_version_id and is_outdated(co):
835 paths[co.f_path][co.line_no].append(co)
835 paths[co.f_path][co.line_no].append(co)
836 inline_comments = paths
836 inline_comments = paths
837
837
838 # outdated comments
838 # outdated comments
839 c.outdated_cnt = 0
839 c.outdated_cnt = 0
840 if CommentsModel.use_outdated_comments(pull_request_latest):
840 if CommentsModel.use_outdated_comments(pull_request_latest):
841 outdated_comments = cc_model.get_outdated_comments(
841 outdated_comments = cc_model.get_outdated_comments(
842 c.rhodecode_db_repo.repo_id,
842 c.rhodecode_db_repo.repo_id,
843 pull_request=pull_request_at_ver)
843 pull_request=pull_request_at_ver)
844
844
845 # Count outdated comments and check for deleted files
845 # Count outdated comments and check for deleted files
846 is_outdated = lambda co: \
846 is_outdated = lambda co: \
847 not c.at_version_num \
847 not c.at_version_num \
848 or co.pull_request_version_id < c.at_version_num
848 or co.pull_request_version_id < c.at_version_num
849 for file_name, lines in outdated_comments.iteritems():
849 for file_name, lines in outdated_comments.iteritems():
850 for comments in lines.values():
850 for comments in lines.values():
851 comments = [comm for comm in comments if is_outdated(comm)]
851 comments = [comm for comm in comments if is_outdated(comm)]
852 c.outdated_cnt += len(comments)
852 c.outdated_cnt += len(comments)
853
853
854 # load compare data into template context
854 # load compare data into template context
855 self._load_compare_data(pull_request_at_ver, inline_comments)
855 self._load_compare_data(pull_request_at_ver, inline_comments)
856
856
857 # this is a hack to properly display links, when creating PR, the
857 # this is a hack to properly display links, when creating PR, the
858 # compare view and others uses different notation, and
858 # compare view and others uses different notation, and
859 # compare_commits.mako renders links based on the target_repo.
859 # compare_commits.mako renders links based on the target_repo.
860 # We need to swap that here to generate it properly on the html side
860 # We need to swap that here to generate it properly on the html side
861 c.target_repo = c.source_repo
861 c.target_repo = c.source_repo
862
862
863 # general comments
863 # general comments
864 c.comments = cc_model.get_comments(
864 c.comments = cc_model.get_comments(
865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
866
866
867 if c.allowed_to_update:
867 if c.allowed_to_update:
868 force_close = ('forced_closed', _('Close Pull Request'))
868 force_close = ('forced_closed', _('Close Pull Request'))
869 statuses = ChangesetStatus.STATUSES + [force_close]
869 statuses = ChangesetStatus.STATUSES + [force_close]
870 else:
870 else:
871 statuses = ChangesetStatus.STATUSES
871 statuses = ChangesetStatus.STATUSES
872 c.commit_statuses = statuses
872 c.commit_statuses = statuses
873
873
874 c.ancestor = None # TODO: add ancestor here
874 c.ancestor = None # TODO: add ancestor here
875 c.pull_request = pull_request_display_obj
875 c.pull_request = pull_request_display_obj
876 c.pull_request_latest = pull_request_latest
876 c.pull_request_latest = pull_request_latest
877 c.at_version = at_version
877 c.at_version = at_version
878
878
879 c.changes = None
879 c.changes = None
880 c.file_changes = None
880 c.file_changes = None
881
881
882 c.show_version_changes = 1 # control flag, not used yet
882 c.show_version_changes = 1 # control flag, not used yet
883
883
884 if at_version and c.show_version_changes:
884 if at_version and c.show_version_changes:
885 c.changes, c.file_changes = self._get_pr_version_changes(
885 c.changes, c.file_changes = self._get_pr_version_changes(
886 version, pull_request_latest)
886 version, pull_request_latest)
887
887
888 return render('/pullrequests/pullrequest_show.mako')
888 return render('/pullrequests/pullrequest_show.mako')
889
889
890 @LoginRequired()
890 @LoginRequired()
891 @NotAnonymous()
891 @NotAnonymous()
892 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
892 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
893 'repository.admin')
893 'repository.admin')
894 @auth.CSRFRequired()
894 @auth.CSRFRequired()
895 @jsonify
895 @jsonify
896 def comment(self, repo_name, pull_request_id):
896 def comment(self, repo_name, pull_request_id):
897 pull_request_id = safe_int(pull_request_id)
897 pull_request_id = safe_int(pull_request_id)
898 pull_request = PullRequest.get_or_404(pull_request_id)
898 pull_request = PullRequest.get_or_404(pull_request_id)
899 if pull_request.is_closed():
899 if pull_request.is_closed():
900 raise HTTPForbidden()
900 raise HTTPForbidden()
901
901
902 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
902 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
903 # as a changeset status, still we want to send it in one value.
903 # as a changeset status, still we want to send it in one value.
904 status = request.POST.get('changeset_status', None)
904 status = request.POST.get('changeset_status', None)
905 text = request.POST.get('text')
905 text = request.POST.get('text')
906 comment_type = request.POST.get('comment_type')
906 if status and '_closed' in status:
907 if status and '_closed' in status:
907 close_pr = True
908 close_pr = True
908 status = status.replace('_closed', '')
909 status = status.replace('_closed', '')
909 else:
910 else:
910 close_pr = False
911 close_pr = False
911
912
912 forced = (status == 'forced')
913 forced = (status == 'forced')
913 if forced:
914 if forced:
914 status = 'rejected'
915 status = 'rejected'
915
916
916 allowed_to_change_status = PullRequestModel().check_user_change_status(
917 allowed_to_change_status = PullRequestModel().check_user_change_status(
917 pull_request, c.rhodecode_user)
918 pull_request, c.rhodecode_user)
918
919
919 if status and allowed_to_change_status:
920 if status and allowed_to_change_status:
920 message = (_('Status change %(transition_icon)s %(status)s')
921 message = (_('Status change %(transition_icon)s %(status)s')
921 % {'transition_icon': '>',
922 % {'transition_icon': '>',
922 'status': ChangesetStatus.get_status_lbl(status)})
923 'status': ChangesetStatus.get_status_lbl(status)})
923 if close_pr:
924 if close_pr:
924 message = _('Closing with') + ' ' + message
925 message = _('Closing with') + ' ' + message
925 text = text or message
926 text = text or message
926 comm = CommentsModel().create(
927 comm = CommentsModel().create(
927 text=text,
928 text=text,
928 repo=c.rhodecode_db_repo.repo_id,
929 repo=c.rhodecode_db_repo.repo_id,
929 user=c.rhodecode_user.user_id,
930 user=c.rhodecode_user.user_id,
930 pull_request=pull_request_id,
931 pull_request=pull_request_id,
931 f_path=request.POST.get('f_path'),
932 f_path=request.POST.get('f_path'),
932 line_no=request.POST.get('line'),
933 line_no=request.POST.get('line'),
933 status_change=(ChangesetStatus.get_status_lbl(status)
934 status_change=(ChangesetStatus.get_status_lbl(status)
934 if status and allowed_to_change_status else None),
935 if status and allowed_to_change_status else None),
935 status_change_type=(status
936 status_change_type=(status
936 if status and allowed_to_change_status else None),
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 if allowed_to_change_status:
942 if allowed_to_change_status:
941 old_calculated_status = pull_request.calculated_review_status()
943 old_calculated_status = pull_request.calculated_review_status()
942 # get status if set !
944 # get status if set !
943 if status:
945 if status:
944 ChangesetStatusModel().set_status(
946 ChangesetStatusModel().set_status(
945 c.rhodecode_db_repo.repo_id,
947 c.rhodecode_db_repo.repo_id,
946 status,
948 status,
947 c.rhodecode_user.user_id,
949 c.rhodecode_user.user_id,
948 comm,
950 comm,
949 pull_request=pull_request_id
951 pull_request=pull_request_id
950 )
952 )
951
953
952 Session().flush()
954 Session().flush()
953 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
955 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
954 # we now calculate the status of pull request, and based on that
956 # we now calculate the status of pull request, and based on that
955 # calculation we set the commits status
957 # calculation we set the commits status
956 calculated_status = pull_request.calculated_review_status()
958 calculated_status = pull_request.calculated_review_status()
957 if old_calculated_status != calculated_status:
959 if old_calculated_status != calculated_status:
958 PullRequestModel()._trigger_pull_request_hook(
960 PullRequestModel()._trigger_pull_request_hook(
959 pull_request, c.rhodecode_user, 'review_status_change')
961 pull_request, c.rhodecode_user, 'review_status_change')
960
962
961 calculated_status_lbl = ChangesetStatus.get_status_lbl(
963 calculated_status_lbl = ChangesetStatus.get_status_lbl(
962 calculated_status)
964 calculated_status)
963
965
964 if close_pr:
966 if close_pr:
965 status_completed = (
967 status_completed = (
966 calculated_status in [ChangesetStatus.STATUS_APPROVED,
968 calculated_status in [ChangesetStatus.STATUS_APPROVED,
967 ChangesetStatus.STATUS_REJECTED])
969 ChangesetStatus.STATUS_REJECTED])
968 if forced or status_completed:
970 if forced or status_completed:
969 PullRequestModel().close_pull_request(
971 PullRequestModel().close_pull_request(
970 pull_request_id, c.rhodecode_user)
972 pull_request_id, c.rhodecode_user)
971 else:
973 else:
972 h.flash(_('Closing pull request on other statuses than '
974 h.flash(_('Closing pull request on other statuses than '
973 'rejected or approved is forbidden. '
975 'rejected or approved is forbidden. '
974 'Calculated status from all reviewers '
976 'Calculated status from all reviewers '
975 'is currently: %s') % calculated_status_lbl,
977 'is currently: %s') % calculated_status_lbl,
976 category='warning')
978 category='warning')
977
979
978 Session().commit()
980 Session().commit()
979
981
980 if not request.is_xhr:
982 if not request.is_xhr:
981 return redirect(h.url('pullrequest_show', repo_name=repo_name,
983 return redirect(h.url('pullrequest_show', repo_name=repo_name,
982 pull_request_id=pull_request_id))
984 pull_request_id=pull_request_id))
983
985
984 data = {
986 data = {
985 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
987 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
986 }
988 }
987 if comm:
989 if comm:
988 c.co = comm
990 c.co = comm
989 c.inline_comment = True if comm.line_no else False
991 c.inline_comment = True if comm.line_no else False
990 data.update(comm.get_dict())
992 data.update(comm.get_dict())
991 data.update({'rendered_text':
993 data.update({'rendered_text':
992 render('changeset/changeset_comment_block.mako')})
994 render('changeset/changeset_comment_block.mako')})
993
995
994 return data
996 return data
995
997
996 @LoginRequired()
998 @LoginRequired()
997 @NotAnonymous()
999 @NotAnonymous()
998 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1000 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
999 'repository.admin')
1001 'repository.admin')
1000 @auth.CSRFRequired()
1002 @auth.CSRFRequired()
1001 @jsonify
1003 @jsonify
1002 def delete_comment(self, repo_name, comment_id):
1004 def delete_comment(self, repo_name, comment_id):
1003 return self._delete_comment(comment_id)
1005 return self._delete_comment(comment_id)
1004
1006
1005 def _delete_comment(self, comment_id):
1007 def _delete_comment(self, comment_id):
1006 comment_id = safe_int(comment_id)
1008 comment_id = safe_int(comment_id)
1007 co = ChangesetComment.get_or_404(comment_id)
1009 co = ChangesetComment.get_or_404(comment_id)
1008 if co.pull_request.is_closed():
1010 if co.pull_request.is_closed():
1009 # don't allow deleting comments on closed pull request
1011 # don't allow deleting comments on closed pull request
1010 raise HTTPForbidden()
1012 raise HTTPForbidden()
1011
1013
1012 is_owner = co.author.user_id == c.rhodecode_user.user_id
1014 is_owner = co.author.user_id == c.rhodecode_user.user_id
1013 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1015 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1014 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1016 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1015 old_calculated_status = co.pull_request.calculated_review_status()
1017 old_calculated_status = co.pull_request.calculated_review_status()
1016 CommentsModel().delete(comment=co)
1018 CommentsModel().delete(comment=co)
1017 Session().commit()
1019 Session().commit()
1018 calculated_status = co.pull_request.calculated_review_status()
1020 calculated_status = co.pull_request.calculated_review_status()
1019 if old_calculated_status != calculated_status:
1021 if old_calculated_status != calculated_status:
1020 PullRequestModel()._trigger_pull_request_hook(
1022 PullRequestModel()._trigger_pull_request_hook(
1021 co.pull_request, c.rhodecode_user, 'review_status_change')
1023 co.pull_request, c.rhodecode_user, 'review_status_change')
1022 return True
1024 return True
1023 else:
1025 else:
1024 raise HTTPForbidden()
1026 raise HTTPForbidden()
@@ -1,596 +1,597 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import ipaddress
30 import ipaddress
31 import pyramid.threadlocal
31 import pyramid.threadlocal
32
32
33 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from pylons import config, tmpl_context as c, request, session, url
36 from pylons import config, tmpl_context as c, request, session, url
37 from pylons.controllers import WSGIController
37 from pylons.controllers import WSGIController
38 from pylons.controllers.util import redirect
38 from pylons.controllers.util import redirect
39 from pylons.i18n import translation
39 from pylons.i18n import translation
40 # marcink: don't remove this import
40 # marcink: don't remove this import
41 from pylons.templating import render_mako as render # noqa
41 from pylons.templating import render_mako as render # noqa
42 from pylons.i18n.translation import _
42 from pylons.i18n.translation import _
43 from webob.exc import HTTPFound
43 from webob.exc import HTTPFound
44
44
45
45
46 import rhodecode
46 import rhodecode
47 from rhodecode.authentication.base import VCS_TYPE
47 from rhodecode.authentication.base import VCS_TYPE
48 from rhodecode.lib import auth, utils2
48 from rhodecode.lib import auth, utils2
49 from rhodecode.lib import helpers as h
49 from rhodecode.lib import helpers as h
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 from rhodecode.lib.exceptions import UserCreationError
51 from rhodecode.lib.exceptions import UserCreationError
52 from rhodecode.lib.utils import (
52 from rhodecode.lib.utils import (
53 get_repo_slug, set_rhodecode_config, password_changed,
53 get_repo_slug, set_rhodecode_config, password_changed,
54 get_enabled_hook_classes)
54 get_enabled_hook_classes)
55 from rhodecode.lib.utils2 import (
55 from rhodecode.lib.utils2 import (
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 from rhodecode.model import meta
58 from rhodecode.model import meta
59 from rhodecode.model.db import Repository, User
59 from rhodecode.model.db import Repository, User, ChangesetComment
60 from rhodecode.model.notification import NotificationModel
60 from rhodecode.model.notification import NotificationModel
61 from rhodecode.model.scm import ScmModel
61 from rhodecode.model.scm import ScmModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63
63
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 def _filter_proxy(ip):
68 def _filter_proxy(ip):
69 """
69 """
70 Passed in IP addresses in HEADERS can be in a special format of multiple
70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 ips. Those comma separated IPs are passed from various proxies in the
71 ips. Those comma separated IPs are passed from various proxies in the
72 chain of request processing. The left-most being the original client.
72 chain of request processing. The left-most being the original client.
73 We only care about the first IP which came from the org. client.
73 We only care about the first IP which came from the org. client.
74
74
75 :param ip: ip string from headers
75 :param ip: ip string from headers
76 """
76 """
77 if ',' in ip:
77 if ',' in ip:
78 _ips = ip.split(',')
78 _ips = ip.split(',')
79 _first_ip = _ips[0].strip()
79 _first_ip = _ips[0].strip()
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 return _first_ip
81 return _first_ip
82 return ip
82 return ip
83
83
84
84
85 def _filter_port(ip):
85 def _filter_port(ip):
86 """
86 """
87 Removes a port from ip, there are 4 main cases to handle here.
87 Removes a port from ip, there are 4 main cases to handle here.
88 - ipv4 eg. 127.0.0.1
88 - ipv4 eg. 127.0.0.1
89 - ipv6 eg. ::1
89 - ipv6 eg. ::1
90 - ipv4+port eg. 127.0.0.1:8080
90 - ipv4+port eg. 127.0.0.1:8080
91 - ipv6+port eg. [::1]:8080
91 - ipv6+port eg. [::1]:8080
92
92
93 :param ip:
93 :param ip:
94 """
94 """
95 def is_ipv6(ip_addr):
95 def is_ipv6(ip_addr):
96 if hasattr(socket, 'inet_pton'):
96 if hasattr(socket, 'inet_pton'):
97 try:
97 try:
98 socket.inet_pton(socket.AF_INET6, ip_addr)
98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 except socket.error:
99 except socket.error:
100 return False
100 return False
101 else:
101 else:
102 # fallback to ipaddress
102 # fallback to ipaddress
103 try:
103 try:
104 ipaddress.IPv6Address(ip_addr)
104 ipaddress.IPv6Address(ip_addr)
105 except Exception:
105 except Exception:
106 return False
106 return False
107 return True
107 return True
108
108
109 if ':' not in ip: # must be ipv4 pure ip
109 if ':' not in ip: # must be ipv4 pure ip
110 return ip
110 return ip
111
111
112 if '[' in ip and ']' in ip: # ipv6 with port
112 if '[' in ip and ']' in ip: # ipv6 with port
113 return ip.split(']')[0][1:].lower()
113 return ip.split(']')[0][1:].lower()
114
114
115 # must be ipv6 or ipv4 with port
115 # must be ipv6 or ipv4 with port
116 if is_ipv6(ip):
116 if is_ipv6(ip):
117 return ip
117 return ip
118 else:
118 else:
119 ip, _port = ip.split(':')[:2] # means ipv4+port
119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 return ip
120 return ip
121
121
122
122
123 def get_ip_addr(environ):
123 def get_ip_addr(environ):
124 proxy_key = 'HTTP_X_REAL_IP'
124 proxy_key = 'HTTP_X_REAL_IP'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 def_key = 'REMOTE_ADDR'
126 def_key = 'REMOTE_ADDR'
127 _filters = lambda x: _filter_port(_filter_proxy(x))
127 _filters = lambda x: _filter_port(_filter_proxy(x))
128
128
129 ip = environ.get(proxy_key)
129 ip = environ.get(proxy_key)
130 if ip:
130 if ip:
131 return _filters(ip)
131 return _filters(ip)
132
132
133 ip = environ.get(proxy_key2)
133 ip = environ.get(proxy_key2)
134 if ip:
134 if ip:
135 return _filters(ip)
135 return _filters(ip)
136
136
137 ip = environ.get(def_key, '0.0.0.0')
137 ip = environ.get(def_key, '0.0.0.0')
138 return _filters(ip)
138 return _filters(ip)
139
139
140
140
141 def get_server_ip_addr(environ, log_errors=True):
141 def get_server_ip_addr(environ, log_errors=True):
142 hostname = environ.get('SERVER_NAME')
142 hostname = environ.get('SERVER_NAME')
143 try:
143 try:
144 return socket.gethostbyname(hostname)
144 return socket.gethostbyname(hostname)
145 except Exception as e:
145 except Exception as e:
146 if log_errors:
146 if log_errors:
147 # in some cases this lookup is not possible, and we don't want to
147 # in some cases this lookup is not possible, and we don't want to
148 # make it an exception in logs
148 # make it an exception in logs
149 log.exception('Could not retrieve server ip address: %s', e)
149 log.exception('Could not retrieve server ip address: %s', e)
150 return hostname
150 return hostname
151
151
152
152
153 def get_server_port(environ):
153 def get_server_port(environ):
154 return environ.get('SERVER_PORT')
154 return environ.get('SERVER_PORT')
155
155
156
156
157 def get_access_path(environ):
157 def get_access_path(environ):
158 path = environ.get('PATH_INFO')
158 path = environ.get('PATH_INFO')
159 org_req = environ.get('pylons.original_request')
159 org_req = environ.get('pylons.original_request')
160 if org_req:
160 if org_req:
161 path = org_req.environ.get('PATH_INFO')
161 path = org_req.environ.get('PATH_INFO')
162 return path
162 return path
163
163
164
164
165 def vcs_operation_context(
165 def vcs_operation_context(
166 environ, repo_name, username, action, scm, check_locking=True,
166 environ, repo_name, username, action, scm, check_locking=True,
167 is_shadow_repo=False):
167 is_shadow_repo=False):
168 """
168 """
169 Generate the context for a vcs operation, e.g. push or pull.
169 Generate the context for a vcs operation, e.g. push or pull.
170
170
171 This context is passed over the layers so that hooks triggered by the
171 This context is passed over the layers so that hooks triggered by the
172 vcs operation know details like the user, the user's IP address etc.
172 vcs operation know details like the user, the user's IP address etc.
173
173
174 :param check_locking: Allows to switch of the computation of the locking
174 :param check_locking: Allows to switch of the computation of the locking
175 data. This serves mainly the need of the simplevcs middleware to be
175 data. This serves mainly the need of the simplevcs middleware to be
176 able to disable this for certain operations.
176 able to disable this for certain operations.
177
177
178 """
178 """
179 # Tri-state value: False: unlock, None: nothing, True: lock
179 # Tri-state value: False: unlock, None: nothing, True: lock
180 make_lock = None
180 make_lock = None
181 locked_by = [None, None, None]
181 locked_by = [None, None, None]
182 is_anonymous = username == User.DEFAULT_USER
182 is_anonymous = username == User.DEFAULT_USER
183 if not is_anonymous and check_locking:
183 if not is_anonymous and check_locking:
184 log.debug('Checking locking on repository "%s"', repo_name)
184 log.debug('Checking locking on repository "%s"', repo_name)
185 user = User.get_by_username(username)
185 user = User.get_by_username(username)
186 repo = Repository.get_by_repo_name(repo_name)
186 repo = Repository.get_by_repo_name(repo_name)
187 make_lock, __, locked_by = repo.get_locking_state(
187 make_lock, __, locked_by = repo.get_locking_state(
188 action, user.user_id)
188 action, user.user_id)
189
189
190 settings_model = VcsSettingsModel(repo=repo_name)
190 settings_model = VcsSettingsModel(repo=repo_name)
191 ui_settings = settings_model.get_ui_settings()
191 ui_settings = settings_model.get_ui_settings()
192
192
193 extras = {
193 extras = {
194 'ip': get_ip_addr(environ),
194 'ip': get_ip_addr(environ),
195 'username': username,
195 'username': username,
196 'action': action,
196 'action': action,
197 'repository': repo_name,
197 'repository': repo_name,
198 'scm': scm,
198 'scm': scm,
199 'config': rhodecode.CONFIG['__file__'],
199 'config': rhodecode.CONFIG['__file__'],
200 'make_lock': make_lock,
200 'make_lock': make_lock,
201 'locked_by': locked_by,
201 'locked_by': locked_by,
202 'server_url': utils2.get_server_url(environ),
202 'server_url': utils2.get_server_url(environ),
203 'hooks': get_enabled_hook_classes(ui_settings),
203 'hooks': get_enabled_hook_classes(ui_settings),
204 'is_shadow_repo': is_shadow_repo,
204 'is_shadow_repo': is_shadow_repo,
205 }
205 }
206 return extras
206 return extras
207
207
208
208
209 class BasicAuth(AuthBasicAuthenticator):
209 class BasicAuth(AuthBasicAuthenticator):
210
210
211 def __init__(self, realm, authfunc, registry, auth_http_code=None,
211 def __init__(self, realm, authfunc, registry, auth_http_code=None,
212 initial_call_detection=False):
212 initial_call_detection=False):
213 self.realm = realm
213 self.realm = realm
214 self.initial_call = initial_call_detection
214 self.initial_call = initial_call_detection
215 self.authfunc = authfunc
215 self.authfunc = authfunc
216 self.registry = registry
216 self.registry = registry
217 self._rc_auth_http_code = auth_http_code
217 self._rc_auth_http_code = auth_http_code
218
218
219 def _get_response_from_code(self, http_code):
219 def _get_response_from_code(self, http_code):
220 try:
220 try:
221 return get_exception(safe_int(http_code))
221 return get_exception(safe_int(http_code))
222 except Exception:
222 except Exception:
223 log.exception('Failed to fetch response for code %s' % http_code)
223 log.exception('Failed to fetch response for code %s' % http_code)
224 return HTTPForbidden
224 return HTTPForbidden
225
225
226 def build_authentication(self):
226 def build_authentication(self):
227 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
227 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
228 if self._rc_auth_http_code and not self.initial_call:
228 if self._rc_auth_http_code and not self.initial_call:
229 # return alternative HTTP code if alternative http return code
229 # return alternative HTTP code if alternative http return code
230 # is specified in RhodeCode config, but ONLY if it's not the
230 # is specified in RhodeCode config, but ONLY if it's not the
231 # FIRST call
231 # FIRST call
232 custom_response_klass = self._get_response_from_code(
232 custom_response_klass = self._get_response_from_code(
233 self._rc_auth_http_code)
233 self._rc_auth_http_code)
234 return custom_response_klass(headers=head)
234 return custom_response_klass(headers=head)
235 return HTTPUnauthorized(headers=head)
235 return HTTPUnauthorized(headers=head)
236
236
237 def authenticate(self, environ):
237 def authenticate(self, environ):
238 authorization = AUTHORIZATION(environ)
238 authorization = AUTHORIZATION(environ)
239 if not authorization:
239 if not authorization:
240 return self.build_authentication()
240 return self.build_authentication()
241 (authmeth, auth) = authorization.split(' ', 1)
241 (authmeth, auth) = authorization.split(' ', 1)
242 if 'basic' != authmeth.lower():
242 if 'basic' != authmeth.lower():
243 return self.build_authentication()
243 return self.build_authentication()
244 auth = auth.strip().decode('base64')
244 auth = auth.strip().decode('base64')
245 _parts = auth.split(':', 1)
245 _parts = auth.split(':', 1)
246 if len(_parts) == 2:
246 if len(_parts) == 2:
247 username, password = _parts
247 username, password = _parts
248 if self.authfunc(
248 if self.authfunc(
249 username, password, environ, VCS_TYPE,
249 username, password, environ, VCS_TYPE,
250 registry=self.registry):
250 registry=self.registry):
251 return username
251 return username
252 if username and password:
252 if username and password:
253 # we mark that we actually executed authentication once, at
253 # we mark that we actually executed authentication once, at
254 # that point we can use the alternative auth code
254 # that point we can use the alternative auth code
255 self.initial_call = False
255 self.initial_call = False
256
256
257 return self.build_authentication()
257 return self.build_authentication()
258
258
259 __call__ = authenticate
259 __call__ = authenticate
260
260
261
261
262 def attach_context_attributes(context, request):
262 def attach_context_attributes(context, request):
263 """
263 """
264 Attach variables into template context called `c`, please note that
264 Attach variables into template context called `c`, please note that
265 request could be pylons or pyramid request in here.
265 request could be pylons or pyramid request in here.
266 """
266 """
267 rc_config = SettingsModel().get_all_settings(cache=True)
267 rc_config = SettingsModel().get_all_settings(cache=True)
268
268
269 context.rhodecode_version = rhodecode.__version__
269 context.rhodecode_version = rhodecode.__version__
270 context.rhodecode_edition = config.get('rhodecode.edition')
270 context.rhodecode_edition = config.get('rhodecode.edition')
271 # unique secret + version does not leak the version but keep consistency
271 # unique secret + version does not leak the version but keep consistency
272 context.rhodecode_version_hash = md5(
272 context.rhodecode_version_hash = md5(
273 config.get('beaker.session.secret', '') +
273 config.get('beaker.session.secret', '') +
274 rhodecode.__version__)[:8]
274 rhodecode.__version__)[:8]
275
275
276 # Default language set for the incoming request
276 # Default language set for the incoming request
277 context.language = translation.get_lang()[0]
277 context.language = translation.get_lang()[0]
278
278
279 # Visual options
279 # Visual options
280 context.visual = AttributeDict({})
280 context.visual = AttributeDict({})
281
281
282 # DB stored Visual Items
282 # DB stored Visual Items
283 context.visual.show_public_icon = str2bool(
283 context.visual.show_public_icon = str2bool(
284 rc_config.get('rhodecode_show_public_icon'))
284 rc_config.get('rhodecode_show_public_icon'))
285 context.visual.show_private_icon = str2bool(
285 context.visual.show_private_icon = str2bool(
286 rc_config.get('rhodecode_show_private_icon'))
286 rc_config.get('rhodecode_show_private_icon'))
287 context.visual.stylify_metatags = str2bool(
287 context.visual.stylify_metatags = str2bool(
288 rc_config.get('rhodecode_stylify_metatags'))
288 rc_config.get('rhodecode_stylify_metatags'))
289 context.visual.dashboard_items = safe_int(
289 context.visual.dashboard_items = safe_int(
290 rc_config.get('rhodecode_dashboard_items', 100))
290 rc_config.get('rhodecode_dashboard_items', 100))
291 context.visual.admin_grid_items = safe_int(
291 context.visual.admin_grid_items = safe_int(
292 rc_config.get('rhodecode_admin_grid_items', 100))
292 rc_config.get('rhodecode_admin_grid_items', 100))
293 context.visual.repository_fields = str2bool(
293 context.visual.repository_fields = str2bool(
294 rc_config.get('rhodecode_repository_fields'))
294 rc_config.get('rhodecode_repository_fields'))
295 context.visual.show_version = str2bool(
295 context.visual.show_version = str2bool(
296 rc_config.get('rhodecode_show_version'))
296 rc_config.get('rhodecode_show_version'))
297 context.visual.use_gravatar = str2bool(
297 context.visual.use_gravatar = str2bool(
298 rc_config.get('rhodecode_use_gravatar'))
298 rc_config.get('rhodecode_use_gravatar'))
299 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
299 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
300 context.visual.default_renderer = rc_config.get(
300 context.visual.default_renderer = rc_config.get(
301 'rhodecode_markup_renderer', 'rst')
301 'rhodecode_markup_renderer', 'rst')
302 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
302 context.visual.rhodecode_support_url = \
303 context.visual.rhodecode_support_url = \
303 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
304 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
304
305
305 context.pre_code = rc_config.get('rhodecode_pre_code')
306 context.pre_code = rc_config.get('rhodecode_pre_code')
306 context.post_code = rc_config.get('rhodecode_post_code')
307 context.post_code = rc_config.get('rhodecode_post_code')
307 context.rhodecode_name = rc_config.get('rhodecode_title')
308 context.rhodecode_name = rc_config.get('rhodecode_title')
308 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
309 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
309 # if we have specified default_encoding in the request, it has more
310 # if we have specified default_encoding in the request, it has more
310 # priority
311 # priority
311 if request.GET.get('default_encoding'):
312 if request.GET.get('default_encoding'):
312 context.default_encodings.insert(0, request.GET.get('default_encoding'))
313 context.default_encodings.insert(0, request.GET.get('default_encoding'))
313 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
314 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
314
315
315 # INI stored
316 # INI stored
316 context.labs_active = str2bool(
317 context.labs_active = str2bool(
317 config.get('labs_settings_active', 'false'))
318 config.get('labs_settings_active', 'false'))
318 context.visual.allow_repo_location_change = str2bool(
319 context.visual.allow_repo_location_change = str2bool(
319 config.get('allow_repo_location_change', True))
320 config.get('allow_repo_location_change', True))
320 context.visual.allow_custom_hooks_settings = str2bool(
321 context.visual.allow_custom_hooks_settings = str2bool(
321 config.get('allow_custom_hooks_settings', True))
322 config.get('allow_custom_hooks_settings', True))
322 context.debug_style = str2bool(config.get('debug_style', False))
323 context.debug_style = str2bool(config.get('debug_style', False))
323
324
324 context.rhodecode_instanceid = config.get('instance_id')
325 context.rhodecode_instanceid = config.get('instance_id')
325
326
326 # AppEnlight
327 # AppEnlight
327 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
328 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
328 context.appenlight_api_public_key = config.get(
329 context.appenlight_api_public_key = config.get(
329 'appenlight.api_public_key', '')
330 'appenlight.api_public_key', '')
330 context.appenlight_server_url = config.get('appenlight.server_url', '')
331 context.appenlight_server_url = config.get('appenlight.server_url', '')
331
332
332 # JS template context
333 # JS template context
333 context.template_context = {
334 context.template_context = {
334 'repo_name': None,
335 'repo_name': None,
335 'repo_type': None,
336 'repo_type': None,
336 'repo_landing_commit': None,
337 'repo_landing_commit': None,
337 'rhodecode_user': {
338 'rhodecode_user': {
338 'username': None,
339 'username': None,
339 'email': None,
340 'email': None,
340 'notification_status': False
341 'notification_status': False
341 },
342 },
342 'visual': {
343 'visual': {
343 'default_renderer': None
344 'default_renderer': None
344 },
345 },
345 'commit_data': {
346 'commit_data': {
346 'commit_id': None
347 'commit_id': None
347 },
348 },
348 'pull_request_data': {'pull_request_id': None},
349 'pull_request_data': {'pull_request_id': None},
349 'timeago': {
350 'timeago': {
350 'refresh_time': 120 * 1000,
351 'refresh_time': 120 * 1000,
351 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
352 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
352 },
353 },
353 'pylons_dispatch': {
354 'pylons_dispatch': {
354 # 'controller': request.environ['pylons.routes_dict']['controller'],
355 # 'controller': request.environ['pylons.routes_dict']['controller'],
355 # 'action': request.environ['pylons.routes_dict']['action'],
356 # 'action': request.environ['pylons.routes_dict']['action'],
356 },
357 },
357 'pyramid_dispatch': {
358 'pyramid_dispatch': {
358
359
359 },
360 },
360 'extra': {'plugins': {}}
361 'extra': {'plugins': {}}
361 }
362 }
362 # END CONFIG VARS
363 # END CONFIG VARS
363
364
364 # TODO: This dosn't work when called from pylons compatibility tween.
365 # TODO: This dosn't work when called from pylons compatibility tween.
365 # Fix this and remove it from base controller.
366 # Fix this and remove it from base controller.
366 # context.repo_name = get_repo_slug(request) # can be empty
367 # context.repo_name = get_repo_slug(request) # can be empty
367
368
368 diffmode = 'sideside'
369 diffmode = 'sideside'
369 if request.GET.get('diffmode'):
370 if request.GET.get('diffmode'):
370 if request.GET['diffmode'] == 'unified':
371 if request.GET['diffmode'] == 'unified':
371 diffmode = 'unified'
372 diffmode = 'unified'
372 elif request.session.get('diffmode'):
373 elif request.session.get('diffmode'):
373 diffmode = request.session['diffmode']
374 diffmode = request.session['diffmode']
374
375
375 context.diffmode = diffmode
376 context.diffmode = diffmode
376
377
377 if request.session.get('diffmode') != diffmode:
378 if request.session.get('diffmode') != diffmode:
378 request.session['diffmode'] = diffmode
379 request.session['diffmode'] = diffmode
379
380
380 context.csrf_token = auth.get_csrf_token()
381 context.csrf_token = auth.get_csrf_token()
381 context.backends = rhodecode.BACKENDS.keys()
382 context.backends = rhodecode.BACKENDS.keys()
382 context.backends.sort()
383 context.backends.sort()
383 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
384 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
384 context.rhodecode_user.user_id)
385 context.rhodecode_user.user_id)
385
386
386 context.pyramid_request = pyramid.threadlocal.get_current_request()
387 context.pyramid_request = pyramid.threadlocal.get_current_request()
387
388
388
389
389 def get_auth_user(environ):
390 def get_auth_user(environ):
390 ip_addr = get_ip_addr(environ)
391 ip_addr = get_ip_addr(environ)
391 # make sure that we update permissions each time we call controller
392 # make sure that we update permissions each time we call controller
392 _auth_token = (request.GET.get('auth_token', '') or
393 _auth_token = (request.GET.get('auth_token', '') or
393 request.GET.get('api_key', ''))
394 request.GET.get('api_key', ''))
394
395
395 if _auth_token:
396 if _auth_token:
396 # when using API_KEY we assume user exists, and
397 # when using API_KEY we assume user exists, and
397 # doesn't need auth based on cookies.
398 # doesn't need auth based on cookies.
398 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
399 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
399 authenticated = False
400 authenticated = False
400 else:
401 else:
401 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
402 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
402 try:
403 try:
403 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
404 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
404 ip_addr=ip_addr)
405 ip_addr=ip_addr)
405 except UserCreationError as e:
406 except UserCreationError as e:
406 h.flash(e, 'error')
407 h.flash(e, 'error')
407 # container auth or other auth functions that create users
408 # container auth or other auth functions that create users
408 # on the fly can throw this exception signaling that there's
409 # on the fly can throw this exception signaling that there's
409 # issue with user creation, explanation should be provided
410 # issue with user creation, explanation should be provided
410 # in Exception itself. We then create a simple blank
411 # in Exception itself. We then create a simple blank
411 # AuthUser
412 # AuthUser
412 auth_user = AuthUser(ip_addr=ip_addr)
413 auth_user = AuthUser(ip_addr=ip_addr)
413
414
414 if password_changed(auth_user, session):
415 if password_changed(auth_user, session):
415 session.invalidate()
416 session.invalidate()
416 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
417 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
417 auth_user = AuthUser(ip_addr=ip_addr)
418 auth_user = AuthUser(ip_addr=ip_addr)
418
419
419 authenticated = cookie_store.get('is_authenticated')
420 authenticated = cookie_store.get('is_authenticated')
420
421
421 if not auth_user.is_authenticated and auth_user.is_user_object:
422 if not auth_user.is_authenticated and auth_user.is_user_object:
422 # user is not authenticated and not empty
423 # user is not authenticated and not empty
423 auth_user.set_authenticated(authenticated)
424 auth_user.set_authenticated(authenticated)
424
425
425 return auth_user
426 return auth_user
426
427
427
428
428 class BaseController(WSGIController):
429 class BaseController(WSGIController):
429
430
430 def __before__(self):
431 def __before__(self):
431 """
432 """
432 __before__ is called before controller methods and after __call__
433 __before__ is called before controller methods and after __call__
433 """
434 """
434 # on each call propagate settings calls into global settings.
435 # on each call propagate settings calls into global settings.
435 set_rhodecode_config(config)
436 set_rhodecode_config(config)
436 attach_context_attributes(c, request)
437 attach_context_attributes(c, request)
437
438
438 # TODO: Remove this when fixed in attach_context_attributes()
439 # TODO: Remove this when fixed in attach_context_attributes()
439 c.repo_name = get_repo_slug(request) # can be empty
440 c.repo_name = get_repo_slug(request) # can be empty
440
441
441 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
442 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
442 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
443 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
443 self.sa = meta.Session
444 self.sa = meta.Session
444 self.scm_model = ScmModel(self.sa)
445 self.scm_model = ScmModel(self.sa)
445
446
446 # set user language
447 # set user language
447 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
448 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
448 if user_lang:
449 if user_lang:
449 translation.set_lang(user_lang)
450 translation.set_lang(user_lang)
450 log.debug('set language to %s for user %s',
451 log.debug('set language to %s for user %s',
451 user_lang, self._rhodecode_user)
452 user_lang, self._rhodecode_user)
452
453
453 def _dispatch_redirect(self, with_url, environ, start_response):
454 def _dispatch_redirect(self, with_url, environ, start_response):
454 resp = HTTPFound(with_url)
455 resp = HTTPFound(with_url)
455 environ['SCRIPT_NAME'] = '' # handle prefix middleware
456 environ['SCRIPT_NAME'] = '' # handle prefix middleware
456 environ['PATH_INFO'] = with_url
457 environ['PATH_INFO'] = with_url
457 return resp(environ, start_response)
458 return resp(environ, start_response)
458
459
459 def __call__(self, environ, start_response):
460 def __call__(self, environ, start_response):
460 """Invoke the Controller"""
461 """Invoke the Controller"""
461 # WSGIController.__call__ dispatches to the Controller method
462 # WSGIController.__call__ dispatches to the Controller method
462 # the request is routed to. This routing information is
463 # the request is routed to. This routing information is
463 # available in environ['pylons.routes_dict']
464 # available in environ['pylons.routes_dict']
464 from rhodecode.lib import helpers as h
465 from rhodecode.lib import helpers as h
465
466
466 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
467 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
467 if environ.get('debugtoolbar.wants_pylons_context', False):
468 if environ.get('debugtoolbar.wants_pylons_context', False):
468 environ['debugtoolbar.pylons_context'] = c._current_obj()
469 environ['debugtoolbar.pylons_context'] = c._current_obj()
469
470
470 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
471 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
471 environ['pylons.routes_dict']['action']])
472 environ['pylons.routes_dict']['action']])
472
473
473 self.rc_config = SettingsModel().get_all_settings(cache=True)
474 self.rc_config = SettingsModel().get_all_settings(cache=True)
474 self.ip_addr = get_ip_addr(environ)
475 self.ip_addr = get_ip_addr(environ)
475
476
476 # The rhodecode auth user is looked up and passed through the
477 # The rhodecode auth user is looked up and passed through the
477 # environ by the pylons compatibility tween in pyramid.
478 # environ by the pylons compatibility tween in pyramid.
478 # So we can just grab it from there.
479 # So we can just grab it from there.
479 auth_user = environ['rc_auth_user']
480 auth_user = environ['rc_auth_user']
480
481
481 # set globals for auth user
482 # set globals for auth user
482 request.user = auth_user
483 request.user = auth_user
483 c.rhodecode_user = self._rhodecode_user = auth_user
484 c.rhodecode_user = self._rhodecode_user = auth_user
484
485
485 log.info('IP: %s User: %s accessed %s [%s]' % (
486 log.info('IP: %s User: %s accessed %s [%s]' % (
486 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
487 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
487 _route_name)
488 _route_name)
488 )
489 )
489
490
490 # TODO: Maybe this should be move to pyramid to cover all views.
491 # TODO: Maybe this should be move to pyramid to cover all views.
491 # check user attributes for password change flag
492 # check user attributes for password change flag
492 user_obj = auth_user.get_instance()
493 user_obj = auth_user.get_instance()
493 if user_obj and user_obj.user_data.get('force_password_change'):
494 if user_obj and user_obj.user_data.get('force_password_change'):
494 h.flash('You are required to change your password', 'warning',
495 h.flash('You are required to change your password', 'warning',
495 ignore_duplicate=True)
496 ignore_duplicate=True)
496
497
497 skip_user_check_urls = [
498 skip_user_check_urls = [
498 'error.document', 'login.logout', 'login.index',
499 'error.document', 'login.logout', 'login.index',
499 'admin/my_account.my_account_password',
500 'admin/my_account.my_account_password',
500 'admin/my_account.my_account_password_update'
501 'admin/my_account.my_account_password_update'
501 ]
502 ]
502 if _route_name not in skip_user_check_urls:
503 if _route_name not in skip_user_check_urls:
503 return self._dispatch_redirect(
504 return self._dispatch_redirect(
504 url('my_account_password'), environ, start_response)
505 url('my_account_password'), environ, start_response)
505
506
506 return WSGIController.__call__(self, environ, start_response)
507 return WSGIController.__call__(self, environ, start_response)
507
508
508
509
509 class BaseRepoController(BaseController):
510 class BaseRepoController(BaseController):
510 """
511 """
511 Base class for controllers responsible for loading all needed data for
512 Base class for controllers responsible for loading all needed data for
512 repository loaded items are
513 repository loaded items are
513
514
514 c.rhodecode_repo: instance of scm repository
515 c.rhodecode_repo: instance of scm repository
515 c.rhodecode_db_repo: instance of db
516 c.rhodecode_db_repo: instance of db
516 c.repository_requirements_missing: shows that repository specific data
517 c.repository_requirements_missing: shows that repository specific data
517 could not be displayed due to the missing requirements
518 could not be displayed due to the missing requirements
518 c.repository_pull_requests: show number of open pull requests
519 c.repository_pull_requests: show number of open pull requests
519 """
520 """
520
521
521 def __before__(self):
522 def __before__(self):
522 super(BaseRepoController, self).__before__()
523 super(BaseRepoController, self).__before__()
523 if c.repo_name: # extracted from routes
524 if c.repo_name: # extracted from routes
524 db_repo = Repository.get_by_repo_name(c.repo_name)
525 db_repo = Repository.get_by_repo_name(c.repo_name)
525 if not db_repo:
526 if not db_repo:
526 return
527 return
527
528
528 log.debug(
529 log.debug(
529 'Found repository in database %s with state `%s`',
530 'Found repository in database %s with state `%s`',
530 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
531 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
531 route = getattr(request.environ.get('routes.route'), 'name', '')
532 route = getattr(request.environ.get('routes.route'), 'name', '')
532
533
533 # allow to delete repos that are somehow damages in filesystem
534 # allow to delete repos that are somehow damages in filesystem
534 if route in ['delete_repo']:
535 if route in ['delete_repo']:
535 return
536 return
536
537
537 if db_repo.repo_state in [Repository.STATE_PENDING]:
538 if db_repo.repo_state in [Repository.STATE_PENDING]:
538 if route in ['repo_creating_home']:
539 if route in ['repo_creating_home']:
539 return
540 return
540 check_url = url('repo_creating_home', repo_name=c.repo_name)
541 check_url = url('repo_creating_home', repo_name=c.repo_name)
541 return redirect(check_url)
542 return redirect(check_url)
542
543
543 self.rhodecode_db_repo = db_repo
544 self.rhodecode_db_repo = db_repo
544
545
545 missing_requirements = False
546 missing_requirements = False
546 try:
547 try:
547 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
548 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
548 except RepositoryRequirementError as e:
549 except RepositoryRequirementError as e:
549 missing_requirements = True
550 missing_requirements = True
550 self._handle_missing_requirements(e)
551 self._handle_missing_requirements(e)
551
552
552 if self.rhodecode_repo is None and not missing_requirements:
553 if self.rhodecode_repo is None and not missing_requirements:
553 log.error('%s this repository is present in database but it '
554 log.error('%s this repository is present in database but it '
554 'cannot be created as an scm instance', c.repo_name)
555 'cannot be created as an scm instance', c.repo_name)
555
556
556 h.flash(_(
557 h.flash(_(
557 "The repository at %(repo_name)s cannot be located.") %
558 "The repository at %(repo_name)s cannot be located.") %
558 {'repo_name': c.repo_name},
559 {'repo_name': c.repo_name},
559 category='error', ignore_duplicate=True)
560 category='error', ignore_duplicate=True)
560 redirect(url('home'))
561 redirect(url('home'))
561
562
562 # update last change according to VCS data
563 # update last change according to VCS data
563 if not missing_requirements:
564 if not missing_requirements:
564 commit = db_repo.get_commit(
565 commit = db_repo.get_commit(
565 pre_load=["author", "date", "message", "parents"])
566 pre_load=["author", "date", "message", "parents"])
566 db_repo.update_commit_cache(commit)
567 db_repo.update_commit_cache(commit)
567
568
568 # Prepare context
569 # Prepare context
569 c.rhodecode_db_repo = db_repo
570 c.rhodecode_db_repo = db_repo
570 c.rhodecode_repo = self.rhodecode_repo
571 c.rhodecode_repo = self.rhodecode_repo
571 c.repository_requirements_missing = missing_requirements
572 c.repository_requirements_missing = missing_requirements
572
573
573 self._update_global_counters(self.scm_model, db_repo)
574 self._update_global_counters(self.scm_model, db_repo)
574
575
575 def _update_global_counters(self, scm_model, db_repo):
576 def _update_global_counters(self, scm_model, db_repo):
576 """
577 """
577 Base variables that are exposed to every page of repository
578 Base variables that are exposed to every page of repository
578 """
579 """
579 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
580 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
580
581
581 def _handle_missing_requirements(self, error):
582 def _handle_missing_requirements(self, error):
582 self.rhodecode_repo = None
583 self.rhodecode_repo = None
583 log.error(
584 log.error(
584 'Requirements are missing for repository %s: %s',
585 'Requirements are missing for repository %s: %s',
585 c.repo_name, error.message)
586 c.repo_name, error.message)
586
587
587 summary_url = url('summary_home', repo_name=c.repo_name)
588 summary_url = url('summary_home', repo_name=c.repo_name)
588 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
589 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
589 settings_update_url = url('repo', repo_name=c.repo_name)
590 settings_update_url = url('repo', repo_name=c.repo_name)
590 path = request.path
591 path = request.path
591 should_redirect = (
592 should_redirect = (
592 path not in (summary_url, settings_update_url)
593 path not in (summary_url, settings_update_url)
593 and '/settings' not in path or path == statistics_url
594 and '/settings' not in path or path == statistics_url
594 )
595 )
595 if should_redirect:
596 if should_redirect:
596 redirect(summary_url)
597 redirect(summary_url)
@@ -1,530 +1,549 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from datetime import datetime
29 from datetime import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry
32 from pyramid.threadlocal import get_current_registry
33 from sqlalchemy.sql.expression import null
33 from sqlalchemy.sql.expression import null
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs
36 from rhodecode.lib import helpers as h, diffs
37 from rhodecode.lib.channelstream import channelstream_request
37 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.utils import action_logger
38 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils2 import extract_mentioned_users
39 from rhodecode.lib.utils2 import extract_mentioned_users
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 ChangesetComment, User, Notification, PullRequest)
42 ChangesetComment, User, Notification, PullRequest)
43 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.notification import NotificationModel
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import VcsSettingsModel
45 from rhodecode.model.settings import VcsSettingsModel
46 from rhodecode.model.notification import EmailNotificationModel
46 from rhodecode.model.notification import EmailNotificationModel
47 from rhodecode.model.validation_schema.schemas import comment_schema
48
47
49
48 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
49
51
50
52
51 class CommentsModel(BaseModel):
53 class CommentsModel(BaseModel):
52
54
53 cls = ChangesetComment
55 cls = ChangesetComment
54
56
55 DIFF_CONTEXT_BEFORE = 3
57 DIFF_CONTEXT_BEFORE = 3
56 DIFF_CONTEXT_AFTER = 3
58 DIFF_CONTEXT_AFTER = 3
57
59
58 def __get_commit_comment(self, changeset_comment):
60 def __get_commit_comment(self, changeset_comment):
59 return self._get_instance(ChangesetComment, changeset_comment)
61 return self._get_instance(ChangesetComment, changeset_comment)
60
62
61 def __get_pull_request(self, pull_request):
63 def __get_pull_request(self, pull_request):
62 return self._get_instance(PullRequest, pull_request)
64 return self._get_instance(PullRequest, pull_request)
63
65
64 def _extract_mentions(self, s):
66 def _extract_mentions(self, s):
65 user_objects = []
67 user_objects = []
66 for username in extract_mentioned_users(s):
68 for username in extract_mentioned_users(s):
67 user_obj = User.get_by_username(username, case_insensitive=True)
69 user_obj = User.get_by_username(username, case_insensitive=True)
68 if user_obj:
70 if user_obj:
69 user_objects.append(user_obj)
71 user_objects.append(user_obj)
70 return user_objects
72 return user_objects
71
73
72 def _get_renderer(self, global_renderer='rst'):
74 def _get_renderer(self, global_renderer='rst'):
73 try:
75 try:
74 # try reading from visual context
76 # try reading from visual context
75 from pylons import tmpl_context
77 from pylons import tmpl_context
76 global_renderer = tmpl_context.visual.default_renderer
78 global_renderer = tmpl_context.visual.default_renderer
77 except AttributeError:
79 except AttributeError:
78 log.debug("Renderer not set, falling back "
80 log.debug("Renderer not set, falling back "
79 "to default renderer '%s'", global_renderer)
81 "to default renderer '%s'", global_renderer)
80 except Exception:
82 except Exception:
81 log.error(traceback.format_exc())
83 log.error(traceback.format_exc())
82 return global_renderer
84 return global_renderer
83
85
84 def create(self, text, repo, user, commit_id=None, pull_request=None,
86 def create(self, text, repo, user, commit_id=None, pull_request=None,
85 f_path=None, line_no=None, status_change=None, comment_type=None,
87 f_path=None, line_no=None, status_change=None, comment_type=None,
86 status_change_type=None, closing_pr=False,
88 status_change_type=None, closing_pr=False,
87 send_email=True, renderer=None):
89 send_email=True, renderer=None):
88 """
90 """
89 Creates new comment for commit or pull request.
91 Creates new comment for commit or pull request.
90 IF status_change is not none this comment is associated with a
92 IF status_change is not none this comment is associated with a
91 status change of commit or commit associated with pull request
93 status change of commit or commit associated with pull request
92
94
93 :param text:
95 :param text:
94 :param repo:
96 :param repo:
95 :param user:
97 :param user:
96 :param commit_id:
98 :param commit_id:
97 :param pull_request:
99 :param pull_request:
98 :param f_path:
100 :param f_path:
99 :param line_no:
101 :param line_no:
100 :param status_change: Label for status change
102 :param status_change: Label for status change
101 :param comment_type: Type of comment
103 :param comment_type: Type of comment
102 :param status_change_type: type of status change
104 :param status_change_type: type of status change
103 :param closing_pr:
105 :param closing_pr:
104 :param send_email:
106 :param send_email:
105 :param renderer: pick renderer for this comment
107 :param renderer: pick renderer for this comment
106 """
108 """
107 if not text:
109 if not text:
108 log.warning('Missing text for comment, skipping...')
110 log.warning('Missing text for comment, skipping...')
109 return
111 return
110
112
111 if not renderer:
113 if not renderer:
112 renderer = self._get_renderer()
114 renderer = self._get_renderer()
113
115
114 repo = self._get_repo(repo)
116
115 user = self._get_user(user)
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 comment = ChangesetComment()
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 comment.repo = repo
140 comment.repo = repo
119 comment.author = user
141 comment.author = user
120 comment.text = text
121 comment.f_path = f_path
122 comment.line_no = line_no
123
142
124 pull_request_id = pull_request
143 pull_request_id = pull_request
125
144
126 commit_obj = None
145 commit_obj = None
127 pull_request_obj = None
146 pull_request_obj = None
128
147
129 if commit_id:
148 if commit_id:
130 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
149 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
131 # do a lookup, so we don't pass something bad here
150 # do a lookup, so we don't pass something bad here
132 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
151 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
133 comment.revision = commit_obj.raw_id
152 comment.revision = commit_obj.raw_id
134
153
135 elif pull_request_id:
154 elif pull_request_id:
136 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
155 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
137 pull_request_obj = self.__get_pull_request(pull_request_id)
156 pull_request_obj = self.__get_pull_request(pull_request_id)
138 comment.pull_request = pull_request_obj
157 comment.pull_request = pull_request_obj
139 else:
158 else:
140 raise Exception('Please specify commit or pull_request_id')
159 raise Exception('Please specify commit or pull_request_id')
141
160
142 Session().add(comment)
161 Session().add(comment)
143 Session().flush()
162 Session().flush()
144 kwargs = {
163 kwargs = {
145 'user': user,
164 'user': user,
146 'renderer_type': renderer,
165 'renderer_type': renderer,
147 'repo_name': repo.repo_name,
166 'repo_name': repo.repo_name,
148 'status_change': status_change,
167 'status_change': status_change,
149 'status_change_type': status_change_type,
168 'status_change_type': status_change_type,
150 'comment_body': text,
169 'comment_body': text,
151 'comment_file': f_path,
170 'comment_file': f_path,
152 'comment_line': line_no,
171 'comment_line': line_no,
153 }
172 }
154
173
155 if commit_obj:
174 if commit_obj:
156 recipients = ChangesetComment.get_users(
175 recipients = ChangesetComment.get_users(
157 revision=commit_obj.raw_id)
176 revision=commit_obj.raw_id)
158 # add commit author if it's in RhodeCode system
177 # add commit author if it's in RhodeCode system
159 cs_author = User.get_from_cs_author(commit_obj.author)
178 cs_author = User.get_from_cs_author(commit_obj.author)
160 if not cs_author:
179 if not cs_author:
161 # use repo owner if we cannot extract the author correctly
180 # use repo owner if we cannot extract the author correctly
162 cs_author = repo.user
181 cs_author = repo.user
163 recipients += [cs_author]
182 recipients += [cs_author]
164
183
165 commit_comment_url = self.get_url(comment)
184 commit_comment_url = self.get_url(comment)
166
185
167 target_repo_url = h.link_to(
186 target_repo_url = h.link_to(
168 repo.repo_name,
187 repo.repo_name,
169 h.url('summary_home',
188 h.url('summary_home',
170 repo_name=repo.repo_name, qualified=True))
189 repo_name=repo.repo_name, qualified=True))
171
190
172 # commit specifics
191 # commit specifics
173 kwargs.update({
192 kwargs.update({
174 'commit': commit_obj,
193 'commit': commit_obj,
175 'commit_message': commit_obj.message,
194 'commit_message': commit_obj.message,
176 'commit_target_repo': target_repo_url,
195 'commit_target_repo': target_repo_url,
177 'commit_comment_url': commit_comment_url,
196 'commit_comment_url': commit_comment_url,
178 })
197 })
179
198
180 elif pull_request_obj:
199 elif pull_request_obj:
181 # get the current participants of this pull request
200 # get the current participants of this pull request
182 recipients = ChangesetComment.get_users(
201 recipients = ChangesetComment.get_users(
183 pull_request_id=pull_request_obj.pull_request_id)
202 pull_request_id=pull_request_obj.pull_request_id)
184 # add pull request author
203 # add pull request author
185 recipients += [pull_request_obj.author]
204 recipients += [pull_request_obj.author]
186
205
187 # add the reviewers to notification
206 # add the reviewers to notification
188 recipients += [x.user for x in pull_request_obj.reviewers]
207 recipients += [x.user for x in pull_request_obj.reviewers]
189
208
190 pr_target_repo = pull_request_obj.target_repo
209 pr_target_repo = pull_request_obj.target_repo
191 pr_source_repo = pull_request_obj.source_repo
210 pr_source_repo = pull_request_obj.source_repo
192
211
193 pr_comment_url = h.url(
212 pr_comment_url = h.url(
194 'pullrequest_show',
213 'pullrequest_show',
195 repo_name=pr_target_repo.repo_name,
214 repo_name=pr_target_repo.repo_name,
196 pull_request_id=pull_request_obj.pull_request_id,
215 pull_request_id=pull_request_obj.pull_request_id,
197 anchor='comment-%s' % comment.comment_id,
216 anchor='comment-%s' % comment.comment_id,
198 qualified=True,)
217 qualified=True,)
199
218
200 # set some variables for email notification
219 # set some variables for email notification
201 pr_target_repo_url = h.url(
220 pr_target_repo_url = h.url(
202 'summary_home', repo_name=pr_target_repo.repo_name,
221 'summary_home', repo_name=pr_target_repo.repo_name,
203 qualified=True)
222 qualified=True)
204
223
205 pr_source_repo_url = h.url(
224 pr_source_repo_url = h.url(
206 'summary_home', repo_name=pr_source_repo.repo_name,
225 'summary_home', repo_name=pr_source_repo.repo_name,
207 qualified=True)
226 qualified=True)
208
227
209 # pull request specifics
228 # pull request specifics
210 kwargs.update({
229 kwargs.update({
211 'pull_request': pull_request_obj,
230 'pull_request': pull_request_obj,
212 'pr_id': pull_request_obj.pull_request_id,
231 'pr_id': pull_request_obj.pull_request_id,
213 'pr_target_repo': pr_target_repo,
232 'pr_target_repo': pr_target_repo,
214 'pr_target_repo_url': pr_target_repo_url,
233 'pr_target_repo_url': pr_target_repo_url,
215 'pr_source_repo': pr_source_repo,
234 'pr_source_repo': pr_source_repo,
216 'pr_source_repo_url': pr_source_repo_url,
235 'pr_source_repo_url': pr_source_repo_url,
217 'pr_comment_url': pr_comment_url,
236 'pr_comment_url': pr_comment_url,
218 'pr_closing': closing_pr,
237 'pr_closing': closing_pr,
219 })
238 })
220 if send_email:
239 if send_email:
221 # pre-generate the subject for notification itself
240 # pre-generate the subject for notification itself
222 (subject,
241 (subject,
223 _h, _e, # we don't care about those
242 _h, _e, # we don't care about those
224 body_plaintext) = EmailNotificationModel().render_email(
243 body_plaintext) = EmailNotificationModel().render_email(
225 notification_type, **kwargs)
244 notification_type, **kwargs)
226
245
227 mention_recipients = set(
246 mention_recipients = set(
228 self._extract_mentions(text)).difference(recipients)
247 self._extract_mentions(text)).difference(recipients)
229
248
230 # create notification objects, and emails
249 # create notification objects, and emails
231 NotificationModel().create(
250 NotificationModel().create(
232 created_by=user,
251 created_by=user,
233 notification_subject=subject,
252 notification_subject=subject,
234 notification_body=body_plaintext,
253 notification_body=body_plaintext,
235 notification_type=notification_type,
254 notification_type=notification_type,
236 recipients=recipients,
255 recipients=recipients,
237 mention_recipients=mention_recipients,
256 mention_recipients=mention_recipients,
238 email_kwargs=kwargs,
257 email_kwargs=kwargs,
239 )
258 )
240
259
241 action = (
260 action = (
242 'user_commented_pull_request:{}'.format(
261 'user_commented_pull_request:{}'.format(
243 comment.pull_request.pull_request_id)
262 comment.pull_request.pull_request_id)
244 if comment.pull_request
263 if comment.pull_request
245 else 'user_commented_revision:{}'.format(comment.revision)
264 else 'user_commented_revision:{}'.format(comment.revision)
246 )
265 )
247 action_logger(user, action, comment.repo)
266 action_logger(user, action, comment.repo)
248
267
249 registry = get_current_registry()
268 registry = get_current_registry()
250 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
269 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
251 channelstream_config = rhodecode_plugins.get('channelstream', {})
270 channelstream_config = rhodecode_plugins.get('channelstream', {})
252 msg_url = ''
271 msg_url = ''
253 if commit_obj:
272 if commit_obj:
254 msg_url = commit_comment_url
273 msg_url = commit_comment_url
255 repo_name = repo.repo_name
274 repo_name = repo.repo_name
256 elif pull_request_obj:
275 elif pull_request_obj:
257 msg_url = pr_comment_url
276 msg_url = pr_comment_url
258 repo_name = pr_target_repo.repo_name
277 repo_name = pr_target_repo.repo_name
259
278
260 if channelstream_config.get('enabled'):
279 if channelstream_config.get('enabled'):
261 message = '<strong>{}</strong> {} - ' \
280 message = '<strong>{}</strong> {} - ' \
262 '<a onclick="window.location=\'{}\';' \
281 '<a onclick="window.location=\'{}\';' \
263 'window.location.reload()">' \
282 'window.location.reload()">' \
264 '<strong>{}</strong></a>'
283 '<strong>{}</strong></a>'
265 message = message.format(
284 message = message.format(
266 user.username, _('made a comment'), msg_url,
285 user.username, _('made a comment'), msg_url,
267 _('Show it now'))
286 _('Show it now'))
268 channel = '/repo${}$/pr/{}'.format(
287 channel = '/repo${}$/pr/{}'.format(
269 repo_name,
288 repo_name,
270 pull_request_id
289 pull_request_id
271 )
290 )
272 payload = {
291 payload = {
273 'type': 'message',
292 'type': 'message',
274 'timestamp': datetime.utcnow(),
293 'timestamp': datetime.utcnow(),
275 'user': 'system',
294 'user': 'system',
276 'exclude_users': [user.username],
295 'exclude_users': [user.username],
277 'channel': channel,
296 'channel': channel,
278 'message': {
297 'message': {
279 'message': message,
298 'message': message,
280 'level': 'info',
299 'level': 'info',
281 'topic': '/notifications'
300 'topic': '/notifications'
282 }
301 }
283 }
302 }
284 channelstream_request(channelstream_config, [payload],
303 channelstream_request(channelstream_config, [payload],
285 '/message', raise_exc=False)
304 '/message', raise_exc=False)
286
305
287 return comment
306 return comment
288
307
289 def delete(self, comment):
308 def delete(self, comment):
290 """
309 """
291 Deletes given comment
310 Deletes given comment
292
311
293 :param comment_id:
312 :param comment_id:
294 """
313 """
295 comment = self.__get_commit_comment(comment)
314 comment = self.__get_commit_comment(comment)
296 Session().delete(comment)
315 Session().delete(comment)
297
316
298 return comment
317 return comment
299
318
300 def get_all_comments(self, repo_id, revision=None, pull_request=None):
319 def get_all_comments(self, repo_id, revision=None, pull_request=None):
301 q = ChangesetComment.query()\
320 q = ChangesetComment.query()\
302 .filter(ChangesetComment.repo_id == repo_id)
321 .filter(ChangesetComment.repo_id == repo_id)
303 if revision:
322 if revision:
304 q = q.filter(ChangesetComment.revision == revision)
323 q = q.filter(ChangesetComment.revision == revision)
305 elif pull_request:
324 elif pull_request:
306 pull_request = self.__get_pull_request(pull_request)
325 pull_request = self.__get_pull_request(pull_request)
307 q = q.filter(ChangesetComment.pull_request == pull_request)
326 q = q.filter(ChangesetComment.pull_request == pull_request)
308 else:
327 else:
309 raise Exception('Please specify commit or pull_request')
328 raise Exception('Please specify commit or pull_request')
310 q = q.order_by(ChangesetComment.created_on)
329 q = q.order_by(ChangesetComment.created_on)
311 return q.all()
330 return q.all()
312
331
313 def get_url(self, comment):
332 def get_url(self, comment):
314 comment = self.__get_commit_comment(comment)
333 comment = self.__get_commit_comment(comment)
315 if comment.pull_request:
334 if comment.pull_request:
316 return h.url(
335 return h.url(
317 'pullrequest_show',
336 'pullrequest_show',
318 repo_name=comment.pull_request.target_repo.repo_name,
337 repo_name=comment.pull_request.target_repo.repo_name,
319 pull_request_id=comment.pull_request.pull_request_id,
338 pull_request_id=comment.pull_request.pull_request_id,
320 anchor='comment-%s' % comment.comment_id,
339 anchor='comment-%s' % comment.comment_id,
321 qualified=True,)
340 qualified=True,)
322 else:
341 else:
323 return h.url(
342 return h.url(
324 'changeset_home',
343 'changeset_home',
325 repo_name=comment.repo.repo_name,
344 repo_name=comment.repo.repo_name,
326 revision=comment.revision,
345 revision=comment.revision,
327 anchor='comment-%s' % comment.comment_id,
346 anchor='comment-%s' % comment.comment_id,
328 qualified=True,)
347 qualified=True,)
329
348
330 def get_comments(self, repo_id, revision=None, pull_request=None):
349 def get_comments(self, repo_id, revision=None, pull_request=None):
331 """
350 """
332 Gets main comments based on revision or pull_request_id
351 Gets main comments based on revision or pull_request_id
333
352
334 :param repo_id:
353 :param repo_id:
335 :param revision:
354 :param revision:
336 :param pull_request:
355 :param pull_request:
337 """
356 """
338
357
339 q = ChangesetComment.query()\
358 q = ChangesetComment.query()\
340 .filter(ChangesetComment.repo_id == repo_id)\
359 .filter(ChangesetComment.repo_id == repo_id)\
341 .filter(ChangesetComment.line_no == None)\
360 .filter(ChangesetComment.line_no == None)\
342 .filter(ChangesetComment.f_path == None)
361 .filter(ChangesetComment.f_path == None)
343 if revision:
362 if revision:
344 q = q.filter(ChangesetComment.revision == revision)
363 q = q.filter(ChangesetComment.revision == revision)
345 elif pull_request:
364 elif pull_request:
346 pull_request = self.__get_pull_request(pull_request)
365 pull_request = self.__get_pull_request(pull_request)
347 q = q.filter(ChangesetComment.pull_request == pull_request)
366 q = q.filter(ChangesetComment.pull_request == pull_request)
348 else:
367 else:
349 raise Exception('Please specify commit or pull_request')
368 raise Exception('Please specify commit or pull_request')
350 q = q.order_by(ChangesetComment.created_on)
369 q = q.order_by(ChangesetComment.created_on)
351 return q.all()
370 return q.all()
352
371
353 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
372 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
354 q = self._get_inline_comments_query(repo_id, revision, pull_request)
373 q = self._get_inline_comments_query(repo_id, revision, pull_request)
355 return self._group_comments_by_path_and_line_number(q)
374 return self._group_comments_by_path_and_line_number(q)
356
375
357 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
376 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
358 version=None, include_aggregates=False):
377 version=None, include_aggregates=False):
359 version_aggregates = collections.defaultdict(list)
378 version_aggregates = collections.defaultdict(list)
360 inline_cnt = 0
379 inline_cnt = 0
361 for fname, per_line_comments in inline_comments.iteritems():
380 for fname, per_line_comments in inline_comments.iteritems():
362 for lno, comments in per_line_comments.iteritems():
381 for lno, comments in per_line_comments.iteritems():
363 for comm in comments:
382 for comm in comments:
364 version_aggregates[comm.pull_request_version_id].append(comm)
383 version_aggregates[comm.pull_request_version_id].append(comm)
365 if not comm.outdated_at_version(version) and skip_outdated:
384 if not comm.outdated_at_version(version) and skip_outdated:
366 inline_cnt += 1
385 inline_cnt += 1
367
386
368 if include_aggregates:
387 if include_aggregates:
369 return inline_cnt, version_aggregates
388 return inline_cnt, version_aggregates
370 return inline_cnt
389 return inline_cnt
371
390
372 def get_outdated_comments(self, repo_id, pull_request):
391 def get_outdated_comments(self, repo_id, pull_request):
373 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
392 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
374 # of a pull request.
393 # of a pull request.
375 q = self._all_inline_comments_of_pull_request(pull_request)
394 q = self._all_inline_comments_of_pull_request(pull_request)
376 q = q.filter(
395 q = q.filter(
377 ChangesetComment.display_state ==
396 ChangesetComment.display_state ==
378 ChangesetComment.COMMENT_OUTDATED
397 ChangesetComment.COMMENT_OUTDATED
379 ).order_by(ChangesetComment.comment_id.asc())
398 ).order_by(ChangesetComment.comment_id.asc())
380
399
381 return self._group_comments_by_path_and_line_number(q)
400 return self._group_comments_by_path_and_line_number(q)
382
401
383 def _get_inline_comments_query(self, repo_id, revision, pull_request):
402 def _get_inline_comments_query(self, repo_id, revision, pull_request):
384 # TODO: johbo: Split this into two methods: One for PR and one for
403 # TODO: johbo: Split this into two methods: One for PR and one for
385 # commit.
404 # commit.
386 if revision:
405 if revision:
387 q = Session().query(ChangesetComment).filter(
406 q = Session().query(ChangesetComment).filter(
388 ChangesetComment.repo_id == repo_id,
407 ChangesetComment.repo_id == repo_id,
389 ChangesetComment.line_no != null(),
408 ChangesetComment.line_no != null(),
390 ChangesetComment.f_path != null(),
409 ChangesetComment.f_path != null(),
391 ChangesetComment.revision == revision)
410 ChangesetComment.revision == revision)
392
411
393 elif pull_request:
412 elif pull_request:
394 pull_request = self.__get_pull_request(pull_request)
413 pull_request = self.__get_pull_request(pull_request)
395 if not CommentsModel.use_outdated_comments(pull_request):
414 if not CommentsModel.use_outdated_comments(pull_request):
396 q = self._visible_inline_comments_of_pull_request(pull_request)
415 q = self._visible_inline_comments_of_pull_request(pull_request)
397 else:
416 else:
398 q = self._all_inline_comments_of_pull_request(pull_request)
417 q = self._all_inline_comments_of_pull_request(pull_request)
399
418
400 else:
419 else:
401 raise Exception('Please specify commit or pull_request_id')
420 raise Exception('Please specify commit or pull_request_id')
402 q = q.order_by(ChangesetComment.comment_id.asc())
421 q = q.order_by(ChangesetComment.comment_id.asc())
403 return q
422 return q
404
423
405 def _group_comments_by_path_and_line_number(self, q):
424 def _group_comments_by_path_and_line_number(self, q):
406 comments = q.all()
425 comments = q.all()
407 paths = collections.defaultdict(lambda: collections.defaultdict(list))
426 paths = collections.defaultdict(lambda: collections.defaultdict(list))
408 for co in comments:
427 for co in comments:
409 paths[co.f_path][co.line_no].append(co)
428 paths[co.f_path][co.line_no].append(co)
410 return paths
429 return paths
411
430
412 @classmethod
431 @classmethod
413 def needed_extra_diff_context(cls):
432 def needed_extra_diff_context(cls):
414 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
433 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
415
434
416 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
435 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
417 if not CommentsModel.use_outdated_comments(pull_request):
436 if not CommentsModel.use_outdated_comments(pull_request):
418 return
437 return
419
438
420 comments = self._visible_inline_comments_of_pull_request(pull_request)
439 comments = self._visible_inline_comments_of_pull_request(pull_request)
421 comments_to_outdate = comments.all()
440 comments_to_outdate = comments.all()
422
441
423 for comment in comments_to_outdate:
442 for comment in comments_to_outdate:
424 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
443 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
425
444
426 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
445 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
427 diff_line = _parse_comment_line_number(comment.line_no)
446 diff_line = _parse_comment_line_number(comment.line_no)
428
447
429 try:
448 try:
430 old_context = old_diff_proc.get_context_of_line(
449 old_context = old_diff_proc.get_context_of_line(
431 path=comment.f_path, diff_line=diff_line)
450 path=comment.f_path, diff_line=diff_line)
432 new_context = new_diff_proc.get_context_of_line(
451 new_context = new_diff_proc.get_context_of_line(
433 path=comment.f_path, diff_line=diff_line)
452 path=comment.f_path, diff_line=diff_line)
434 except (diffs.LineNotInDiffException,
453 except (diffs.LineNotInDiffException,
435 diffs.FileNotInDiffException):
454 diffs.FileNotInDiffException):
436 comment.display_state = ChangesetComment.COMMENT_OUTDATED
455 comment.display_state = ChangesetComment.COMMENT_OUTDATED
437 return
456 return
438
457
439 if old_context == new_context:
458 if old_context == new_context:
440 return
459 return
441
460
442 if self._should_relocate_diff_line(diff_line):
461 if self._should_relocate_diff_line(diff_line):
443 new_diff_lines = new_diff_proc.find_context(
462 new_diff_lines = new_diff_proc.find_context(
444 path=comment.f_path, context=old_context,
463 path=comment.f_path, context=old_context,
445 offset=self.DIFF_CONTEXT_BEFORE)
464 offset=self.DIFF_CONTEXT_BEFORE)
446 if not new_diff_lines:
465 if not new_diff_lines:
447 comment.display_state = ChangesetComment.COMMENT_OUTDATED
466 comment.display_state = ChangesetComment.COMMENT_OUTDATED
448 else:
467 else:
449 new_diff_line = self._choose_closest_diff_line(
468 new_diff_line = self._choose_closest_diff_line(
450 diff_line, new_diff_lines)
469 diff_line, new_diff_lines)
451 comment.line_no = _diff_to_comment_line_number(new_diff_line)
470 comment.line_no = _diff_to_comment_line_number(new_diff_line)
452 else:
471 else:
453 comment.display_state = ChangesetComment.COMMENT_OUTDATED
472 comment.display_state = ChangesetComment.COMMENT_OUTDATED
454
473
455 def _should_relocate_diff_line(self, diff_line):
474 def _should_relocate_diff_line(self, diff_line):
456 """
475 """
457 Checks if relocation shall be tried for the given `diff_line`.
476 Checks if relocation shall be tried for the given `diff_line`.
458
477
459 If a comment points into the first lines, then we can have a situation
478 If a comment points into the first lines, then we can have a situation
460 that after an update another line has been added on top. In this case
479 that after an update another line has been added on top. In this case
461 we would find the context still and move the comment around. This
480 we would find the context still and move the comment around. This
462 would be wrong.
481 would be wrong.
463 """
482 """
464 should_relocate = (
483 should_relocate = (
465 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
484 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
466 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
485 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
467 return should_relocate
486 return should_relocate
468
487
469 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
488 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
470 candidate = new_diff_lines[0]
489 candidate = new_diff_lines[0]
471 best_delta = _diff_line_delta(diff_line, candidate)
490 best_delta = _diff_line_delta(diff_line, candidate)
472 for new_diff_line in new_diff_lines[1:]:
491 for new_diff_line in new_diff_lines[1:]:
473 delta = _diff_line_delta(diff_line, new_diff_line)
492 delta = _diff_line_delta(diff_line, new_diff_line)
474 if delta < best_delta:
493 if delta < best_delta:
475 candidate = new_diff_line
494 candidate = new_diff_line
476 best_delta = delta
495 best_delta = delta
477 return candidate
496 return candidate
478
497
479 def _visible_inline_comments_of_pull_request(self, pull_request):
498 def _visible_inline_comments_of_pull_request(self, pull_request):
480 comments = self._all_inline_comments_of_pull_request(pull_request)
499 comments = self._all_inline_comments_of_pull_request(pull_request)
481 comments = comments.filter(
500 comments = comments.filter(
482 coalesce(ChangesetComment.display_state, '') !=
501 coalesce(ChangesetComment.display_state, '') !=
483 ChangesetComment.COMMENT_OUTDATED)
502 ChangesetComment.COMMENT_OUTDATED)
484 return comments
503 return comments
485
504
486 def _all_inline_comments_of_pull_request(self, pull_request):
505 def _all_inline_comments_of_pull_request(self, pull_request):
487 comments = Session().query(ChangesetComment)\
506 comments = Session().query(ChangesetComment)\
488 .filter(ChangesetComment.line_no != None)\
507 .filter(ChangesetComment.line_no != None)\
489 .filter(ChangesetComment.f_path != None)\
508 .filter(ChangesetComment.f_path != None)\
490 .filter(ChangesetComment.pull_request == pull_request)
509 .filter(ChangesetComment.pull_request == pull_request)
491 return comments
510 return comments
492
511
493 @staticmethod
512 @staticmethod
494 def use_outdated_comments(pull_request):
513 def use_outdated_comments(pull_request):
495 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
514 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
496 settings = settings_model.get_general_settings()
515 settings = settings_model.get_general_settings()
497 return settings.get('rhodecode_use_outdated_comments', False)
516 return settings.get('rhodecode_use_outdated_comments', False)
498
517
499
518
500 def _parse_comment_line_number(line_no):
519 def _parse_comment_line_number(line_no):
501 """
520 """
502 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
521 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
503 """
522 """
504 old_line = None
523 old_line = None
505 new_line = None
524 new_line = None
506 if line_no.startswith('o'):
525 if line_no.startswith('o'):
507 old_line = int(line_no[1:])
526 old_line = int(line_no[1:])
508 elif line_no.startswith('n'):
527 elif line_no.startswith('n'):
509 new_line = int(line_no[1:])
528 new_line = int(line_no[1:])
510 else:
529 else:
511 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
530 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
512 return diffs.DiffLineNumber(old_line, new_line)
531 return diffs.DiffLineNumber(old_line, new_line)
513
532
514
533
515 def _diff_to_comment_line_number(diff_line):
534 def _diff_to_comment_line_number(diff_line):
516 if diff_line.new is not None:
535 if diff_line.new is not None:
517 return u'n{}'.format(diff_line.new)
536 return u'n{}'.format(diff_line.new)
518 elif diff_line.old is not None:
537 elif diff_line.old is not None:
519 return u'o{}'.format(diff_line.old)
538 return u'o{}'.format(diff_line.old)
520 return u''
539 return u''
521
540
522
541
523 def _diff_line_delta(a, b):
542 def _diff_line_delta(a, b):
524 if None not in (a.new, b.new):
543 if None not in (a.new, b.new):
525 return abs(a.new - b.new)
544 return abs(a.new - b.new)
526 elif None not in (a.old, b.old):
545 elif None not in (a.old, b.old):
527 return abs(a.old - b.old)
546 return abs(a.old - b.old)
528 else:
547 else:
529 raise ValueError(
548 raise ValueError(
530 "Cannot compute delta between {} and {}".format(a, b))
549 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,3823 +1,3829 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37
37
38 from sqlalchemy import *
38 from sqlalchemy import *
39 from sqlalchemy.ext.declarative import declared_attr
39 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.hybrid import hybrid_property
40 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.orm import (
41 from sqlalchemy.orm import (
42 relationship, joinedload, class_mapper, validates, aliased)
42 relationship, joinedload, class_mapper, validates, aliased)
43 from sqlalchemy.sql.expression import true
43 from sqlalchemy.sql.expression import true
44 from beaker.cache import cache_region
44 from beaker.cache import cache_region
45 from webob.exc import HTTPNotFound
45 from webob.exc import HTTPNotFound
46 from zope.cachedescriptors.property import Lazy as LazyProperty
46 from zope.cachedescriptors.property import Lazy as LazyProperty
47
47
48 from pylons import url
48 from pylons import url
49 from pylons.i18n.translation import lazy_ugettext as _
49 from pylons.i18n.translation import lazy_ugettext as _
50
50
51 from rhodecode.lib.vcs import get_vcs_instance
51 from rhodecode.lib.vcs import get_vcs_instance
52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 from rhodecode.lib.utils2 import (
53 from rhodecode.lib.utils2 import (
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 glob2re, StrictAttributeDict)
56 glob2re, StrictAttributeDict)
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 from rhodecode.lib.ext_json import json
58 from rhodecode.lib.ext_json import json
59 from rhodecode.lib.caching_query import FromCache
59 from rhodecode.lib.caching_query import FromCache
60 from rhodecode.lib.encrypt import AESCipher
60 from rhodecode.lib.encrypt import AESCipher
61
61
62 from rhodecode.model.meta import Base, Session
62 from rhodecode.model.meta import Base, Session
63
63
64 URL_SEP = '/'
64 URL_SEP = '/'
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67 # =============================================================================
67 # =============================================================================
68 # BASE CLASSES
68 # BASE CLASSES
69 # =============================================================================
69 # =============================================================================
70
70
71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 # beaker.session.secret if first is not set.
72 # beaker.session.secret if first is not set.
73 # and initialized at environment.py
73 # and initialized at environment.py
74 ENCRYPTION_KEY = None
74 ENCRYPTION_KEY = None
75
75
76 # used to sort permissions by types, '#' used here is not allowed to be in
76 # used to sort permissions by types, '#' used here is not allowed to be in
77 # usernames, and it's very early in sorted string.printable table.
77 # usernames, and it's very early in sorted string.printable table.
78 PERMISSION_TYPE_SORT = {
78 PERMISSION_TYPE_SORT = {
79 'admin': '####',
79 'admin': '####',
80 'write': '###',
80 'write': '###',
81 'read': '##',
81 'read': '##',
82 'none': '#',
82 'none': '#',
83 }
83 }
84
84
85
85
86 def display_sort(obj):
86 def display_sort(obj):
87 """
87 """
88 Sort function used to sort permissions in .permissions() function of
88 Sort function used to sort permissions in .permissions() function of
89 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 of all other resources
90 of all other resources
91 """
91 """
92
92
93 if obj.username == User.DEFAULT_USER:
93 if obj.username == User.DEFAULT_USER:
94 return '#####'
94 return '#####'
95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 return prefix + obj.username
96 return prefix + obj.username
97
97
98
98
99 def _hash_key(k):
99 def _hash_key(k):
100 return md5_safe(k)
100 return md5_safe(k)
101
101
102
102
103 class EncryptedTextValue(TypeDecorator):
103 class EncryptedTextValue(TypeDecorator):
104 """
104 """
105 Special column for encrypted long text data, use like::
105 Special column for encrypted long text data, use like::
106
106
107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108
108
109 This column is intelligent so if value is in unencrypted form it return
109 This column is intelligent so if value is in unencrypted form it return
110 unencrypted form, but on save it always encrypts
110 unencrypted form, but on save it always encrypts
111 """
111 """
112 impl = Text
112 impl = Text
113
113
114 def process_bind_param(self, value, dialect):
114 def process_bind_param(self, value, dialect):
115 if not value:
115 if not value:
116 return value
116 return value
117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 # protect against double encrypting if someone manually starts
118 # protect against double encrypting if someone manually starts
119 # doing
119 # doing
120 raise ValueError('value needs to be in unencrypted format, ie. '
120 raise ValueError('value needs to be in unencrypted format, ie. '
121 'not starting with enc$aes')
121 'not starting with enc$aes')
122 return 'enc$aes_hmac$%s' % AESCipher(
122 return 'enc$aes_hmac$%s' % AESCipher(
123 ENCRYPTION_KEY, hmac=True).encrypt(value)
123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124
124
125 def process_result_value(self, value, dialect):
125 def process_result_value(self, value, dialect):
126 import rhodecode
126 import rhodecode
127
127
128 if not value:
128 if not value:
129 return value
129 return value
130
130
131 parts = value.split('$', 3)
131 parts = value.split('$', 3)
132 if not len(parts) == 3:
132 if not len(parts) == 3:
133 # probably not encrypted values
133 # probably not encrypted values
134 return value
134 return value
135 else:
135 else:
136 if parts[0] != 'enc':
136 if parts[0] != 'enc':
137 # parts ok but without our header ?
137 # parts ok but without our header ?
138 return value
138 return value
139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 'rhodecode.encrypted_values.strict') or True)
140 'rhodecode.encrypted_values.strict') or True)
141 # at that stage we know it's our encryption
141 # at that stage we know it's our encryption
142 if parts[1] == 'aes':
142 if parts[1] == 'aes':
143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 elif parts[1] == 'aes_hmac':
144 elif parts[1] == 'aes_hmac':
145 decrypted_data = AESCipher(
145 decrypted_data = AESCipher(
146 ENCRYPTION_KEY, hmac=True,
146 ENCRYPTION_KEY, hmac=True,
147 strict_verification=enc_strict_mode).decrypt(parts[2])
147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 else:
148 else:
149 raise ValueError(
149 raise ValueError(
150 'Encryption type part is wrong, must be `aes` '
150 'Encryption type part is wrong, must be `aes` '
151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 return decrypted_data
152 return decrypted_data
153
153
154
154
155 class BaseModel(object):
155 class BaseModel(object):
156 """
156 """
157 Base Model for all classes
157 Base Model for all classes
158 """
158 """
159
159
160 @classmethod
160 @classmethod
161 def _get_keys(cls):
161 def _get_keys(cls):
162 """return column names for this model """
162 """return column names for this model """
163 return class_mapper(cls).c.keys()
163 return class_mapper(cls).c.keys()
164
164
165 def get_dict(self):
165 def get_dict(self):
166 """
166 """
167 return dict with keys and values corresponding
167 return dict with keys and values corresponding
168 to this model data """
168 to this model data """
169
169
170 d = {}
170 d = {}
171 for k in self._get_keys():
171 for k in self._get_keys():
172 d[k] = getattr(self, k)
172 d[k] = getattr(self, k)
173
173
174 # also use __json__() if present to get additional fields
174 # also use __json__() if present to get additional fields
175 _json_attr = getattr(self, '__json__', None)
175 _json_attr = getattr(self, '__json__', None)
176 if _json_attr:
176 if _json_attr:
177 # update with attributes from __json__
177 # update with attributes from __json__
178 if callable(_json_attr):
178 if callable(_json_attr):
179 _json_attr = _json_attr()
179 _json_attr = _json_attr()
180 for k, val in _json_attr.iteritems():
180 for k, val in _json_attr.iteritems():
181 d[k] = val
181 d[k] = val
182 return d
182 return d
183
183
184 def get_appstruct(self):
184 def get_appstruct(self):
185 """return list with keys and values tuples corresponding
185 """return list with keys and values tuples corresponding
186 to this model data """
186 to this model data """
187
187
188 l = []
188 l = []
189 for k in self._get_keys():
189 for k in self._get_keys():
190 l.append((k, getattr(self, k),))
190 l.append((k, getattr(self, k),))
191 return l
191 return l
192
192
193 def populate_obj(self, populate_dict):
193 def populate_obj(self, populate_dict):
194 """populate model with data from given populate_dict"""
194 """populate model with data from given populate_dict"""
195
195
196 for k in self._get_keys():
196 for k in self._get_keys():
197 if k in populate_dict:
197 if k in populate_dict:
198 setattr(self, k, populate_dict[k])
198 setattr(self, k, populate_dict[k])
199
199
200 @classmethod
200 @classmethod
201 def query(cls):
201 def query(cls):
202 return Session().query(cls)
202 return Session().query(cls)
203
203
204 @classmethod
204 @classmethod
205 def get(cls, id_):
205 def get(cls, id_):
206 if id_:
206 if id_:
207 return cls.query().get(id_)
207 return cls.query().get(id_)
208
208
209 @classmethod
209 @classmethod
210 def get_or_404(cls, id_):
210 def get_or_404(cls, id_):
211 try:
211 try:
212 id_ = int(id_)
212 id_ = int(id_)
213 except (TypeError, ValueError):
213 except (TypeError, ValueError):
214 raise HTTPNotFound
214 raise HTTPNotFound
215
215
216 res = cls.query().get(id_)
216 res = cls.query().get(id_)
217 if not res:
217 if not res:
218 raise HTTPNotFound
218 raise HTTPNotFound
219 return res
219 return res
220
220
221 @classmethod
221 @classmethod
222 def getAll(cls):
222 def getAll(cls):
223 # deprecated and left for backward compatibility
223 # deprecated and left for backward compatibility
224 return cls.get_all()
224 return cls.get_all()
225
225
226 @classmethod
226 @classmethod
227 def get_all(cls):
227 def get_all(cls):
228 return cls.query().all()
228 return cls.query().all()
229
229
230 @classmethod
230 @classmethod
231 def delete(cls, id_):
231 def delete(cls, id_):
232 obj = cls.query().get(id_)
232 obj = cls.query().get(id_)
233 Session().delete(obj)
233 Session().delete(obj)
234
234
235 @classmethod
235 @classmethod
236 def identity_cache(cls, session, attr_name, value):
236 def identity_cache(cls, session, attr_name, value):
237 exist_in_session = []
237 exist_in_session = []
238 for (item_cls, pkey), instance in session.identity_map.items():
238 for (item_cls, pkey), instance in session.identity_map.items():
239 if cls == item_cls and getattr(instance, attr_name) == value:
239 if cls == item_cls and getattr(instance, attr_name) == value:
240 exist_in_session.append(instance)
240 exist_in_session.append(instance)
241 if exist_in_session:
241 if exist_in_session:
242 if len(exist_in_session) == 1:
242 if len(exist_in_session) == 1:
243 return exist_in_session[0]
243 return exist_in_session[0]
244 log.exception(
244 log.exception(
245 'multiple objects with attr %s and '
245 'multiple objects with attr %s and '
246 'value %s found with same name: %r',
246 'value %s found with same name: %r',
247 attr_name, value, exist_in_session)
247 attr_name, value, exist_in_session)
248
248
249 def __repr__(self):
249 def __repr__(self):
250 if hasattr(self, '__unicode__'):
250 if hasattr(self, '__unicode__'):
251 # python repr needs to return str
251 # python repr needs to return str
252 try:
252 try:
253 return safe_str(self.__unicode__())
253 return safe_str(self.__unicode__())
254 except UnicodeDecodeError:
254 except UnicodeDecodeError:
255 pass
255 pass
256 return '<DB:%s>' % (self.__class__.__name__)
256 return '<DB:%s>' % (self.__class__.__name__)
257
257
258
258
259 class RhodeCodeSetting(Base, BaseModel):
259 class RhodeCodeSetting(Base, BaseModel):
260 __tablename__ = 'rhodecode_settings'
260 __tablename__ = 'rhodecode_settings'
261 __table_args__ = (
261 __table_args__ = (
262 UniqueConstraint('app_settings_name'),
262 UniqueConstraint('app_settings_name'),
263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 )
265 )
266
266
267 SETTINGS_TYPES = {
267 SETTINGS_TYPES = {
268 'str': safe_str,
268 'str': safe_str,
269 'int': safe_int,
269 'int': safe_int,
270 'unicode': safe_unicode,
270 'unicode': safe_unicode,
271 'bool': str2bool,
271 'bool': str2bool,
272 'list': functools.partial(aslist, sep=',')
272 'list': functools.partial(aslist, sep=',')
273 }
273 }
274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 GLOBAL_CONF_KEY = 'app_settings'
275 GLOBAL_CONF_KEY = 'app_settings'
276
276
277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281
281
282 def __init__(self, key='', val='', type='unicode'):
282 def __init__(self, key='', val='', type='unicode'):
283 self.app_settings_name = key
283 self.app_settings_name = key
284 self.app_settings_type = type
284 self.app_settings_type = type
285 self.app_settings_value = val
285 self.app_settings_value = val
286
286
287 @validates('_app_settings_value')
287 @validates('_app_settings_value')
288 def validate_settings_value(self, key, val):
288 def validate_settings_value(self, key, val):
289 assert type(val) == unicode
289 assert type(val) == unicode
290 return val
290 return val
291
291
292 @hybrid_property
292 @hybrid_property
293 def app_settings_value(self):
293 def app_settings_value(self):
294 v = self._app_settings_value
294 v = self._app_settings_value
295 _type = self.app_settings_type
295 _type = self.app_settings_type
296 if _type:
296 if _type:
297 _type = self.app_settings_type.split('.')[0]
297 _type = self.app_settings_type.split('.')[0]
298 # decode the encrypted value
298 # decode the encrypted value
299 if 'encrypted' in self.app_settings_type:
299 if 'encrypted' in self.app_settings_type:
300 cipher = EncryptedTextValue()
300 cipher = EncryptedTextValue()
301 v = safe_unicode(cipher.process_result_value(v, None))
301 v = safe_unicode(cipher.process_result_value(v, None))
302
302
303 converter = self.SETTINGS_TYPES.get(_type) or \
303 converter = self.SETTINGS_TYPES.get(_type) or \
304 self.SETTINGS_TYPES['unicode']
304 self.SETTINGS_TYPES['unicode']
305 return converter(v)
305 return converter(v)
306
306
307 @app_settings_value.setter
307 @app_settings_value.setter
308 def app_settings_value(self, val):
308 def app_settings_value(self, val):
309 """
309 """
310 Setter that will always make sure we use unicode in app_settings_value
310 Setter that will always make sure we use unicode in app_settings_value
311
311
312 :param val:
312 :param val:
313 """
313 """
314 val = safe_unicode(val)
314 val = safe_unicode(val)
315 # encode the encrypted value
315 # encode the encrypted value
316 if 'encrypted' in self.app_settings_type:
316 if 'encrypted' in self.app_settings_type:
317 cipher = EncryptedTextValue()
317 cipher = EncryptedTextValue()
318 val = safe_unicode(cipher.process_bind_param(val, None))
318 val = safe_unicode(cipher.process_bind_param(val, None))
319 self._app_settings_value = val
319 self._app_settings_value = val
320
320
321 @hybrid_property
321 @hybrid_property
322 def app_settings_type(self):
322 def app_settings_type(self):
323 return self._app_settings_type
323 return self._app_settings_type
324
324
325 @app_settings_type.setter
325 @app_settings_type.setter
326 def app_settings_type(self, val):
326 def app_settings_type(self, val):
327 if val.split('.')[0] not in self.SETTINGS_TYPES:
327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 raise Exception('type must be one of %s got %s'
328 raise Exception('type must be one of %s got %s'
329 % (self.SETTINGS_TYPES.keys(), val))
329 % (self.SETTINGS_TYPES.keys(), val))
330 self._app_settings_type = val
330 self._app_settings_type = val
331
331
332 def __unicode__(self):
332 def __unicode__(self):
333 return u"<%s('%s:%s[%s]')>" % (
333 return u"<%s('%s:%s[%s]')>" % (
334 self.__class__.__name__,
334 self.__class__.__name__,
335 self.app_settings_name, self.app_settings_value,
335 self.app_settings_name, self.app_settings_value,
336 self.app_settings_type
336 self.app_settings_type
337 )
337 )
338
338
339
339
340 class RhodeCodeUi(Base, BaseModel):
340 class RhodeCodeUi(Base, BaseModel):
341 __tablename__ = 'rhodecode_ui'
341 __tablename__ = 'rhodecode_ui'
342 __table_args__ = (
342 __table_args__ = (
343 UniqueConstraint('ui_key'),
343 UniqueConstraint('ui_key'),
344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 )
346 )
347
347
348 HOOK_REPO_SIZE = 'changegroup.repo_size'
348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 # HG
349 # HG
350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 HOOK_PULL = 'outgoing.pull_logger'
351 HOOK_PULL = 'outgoing.pull_logger'
352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 HOOK_PUSH = 'changegroup.push_logger'
353 HOOK_PUSH = 'changegroup.push_logger'
354
354
355 # TODO: johbo: Unify way how hooks are configured for git and hg,
355 # TODO: johbo: Unify way how hooks are configured for git and hg,
356 # git part is currently hardcoded.
356 # git part is currently hardcoded.
357
357
358 # SVN PATTERNS
358 # SVN PATTERNS
359 SVN_BRANCH_ID = 'vcs_svn_branch'
359 SVN_BRANCH_ID = 'vcs_svn_branch'
360 SVN_TAG_ID = 'vcs_svn_tag'
360 SVN_TAG_ID = 'vcs_svn_tag'
361
361
362 ui_id = Column(
362 ui_id = Column(
363 "ui_id", Integer(), nullable=False, unique=True, default=None,
363 "ui_id", Integer(), nullable=False, unique=True, default=None,
364 primary_key=True)
364 primary_key=True)
365 ui_section = Column(
365 ui_section = Column(
366 "ui_section", String(255), nullable=True, unique=None, default=None)
366 "ui_section", String(255), nullable=True, unique=None, default=None)
367 ui_key = Column(
367 ui_key = Column(
368 "ui_key", String(255), nullable=True, unique=None, default=None)
368 "ui_key", String(255), nullable=True, unique=None, default=None)
369 ui_value = Column(
369 ui_value = Column(
370 "ui_value", String(255), nullable=True, unique=None, default=None)
370 "ui_value", String(255), nullable=True, unique=None, default=None)
371 ui_active = Column(
371 ui_active = Column(
372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
373
373
374 def __repr__(self):
374 def __repr__(self):
375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
376 self.ui_key, self.ui_value)
376 self.ui_key, self.ui_value)
377
377
378
378
379 class RepoRhodeCodeSetting(Base, BaseModel):
379 class RepoRhodeCodeSetting(Base, BaseModel):
380 __tablename__ = 'repo_rhodecode_settings'
380 __tablename__ = 'repo_rhodecode_settings'
381 __table_args__ = (
381 __table_args__ = (
382 UniqueConstraint(
382 UniqueConstraint(
383 'app_settings_name', 'repository_id',
383 'app_settings_name', 'repository_id',
384 name='uq_repo_rhodecode_setting_name_repo_id'),
384 name='uq_repo_rhodecode_setting_name_repo_id'),
385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
387 )
387 )
388
388
389 repository_id = Column(
389 repository_id = Column(
390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
391 nullable=False)
391 nullable=False)
392 app_settings_id = Column(
392 app_settings_id = Column(
393 "app_settings_id", Integer(), nullable=False, unique=True,
393 "app_settings_id", Integer(), nullable=False, unique=True,
394 default=None, primary_key=True)
394 default=None, primary_key=True)
395 app_settings_name = Column(
395 app_settings_name = Column(
396 "app_settings_name", String(255), nullable=True, unique=None,
396 "app_settings_name", String(255), nullable=True, unique=None,
397 default=None)
397 default=None)
398 _app_settings_value = Column(
398 _app_settings_value = Column(
399 "app_settings_value", String(4096), nullable=True, unique=None,
399 "app_settings_value", String(4096), nullable=True, unique=None,
400 default=None)
400 default=None)
401 _app_settings_type = Column(
401 _app_settings_type = Column(
402 "app_settings_type", String(255), nullable=True, unique=None,
402 "app_settings_type", String(255), nullable=True, unique=None,
403 default=None)
403 default=None)
404
404
405 repository = relationship('Repository')
405 repository = relationship('Repository')
406
406
407 def __init__(self, repository_id, key='', val='', type='unicode'):
407 def __init__(self, repository_id, key='', val='', type='unicode'):
408 self.repository_id = repository_id
408 self.repository_id = repository_id
409 self.app_settings_name = key
409 self.app_settings_name = key
410 self.app_settings_type = type
410 self.app_settings_type = type
411 self.app_settings_value = val
411 self.app_settings_value = val
412
412
413 @validates('_app_settings_value')
413 @validates('_app_settings_value')
414 def validate_settings_value(self, key, val):
414 def validate_settings_value(self, key, val):
415 assert type(val) == unicode
415 assert type(val) == unicode
416 return val
416 return val
417
417
418 @hybrid_property
418 @hybrid_property
419 def app_settings_value(self):
419 def app_settings_value(self):
420 v = self._app_settings_value
420 v = self._app_settings_value
421 type_ = self.app_settings_type
421 type_ = self.app_settings_type
422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
424 return converter(v)
424 return converter(v)
425
425
426 @app_settings_value.setter
426 @app_settings_value.setter
427 def app_settings_value(self, val):
427 def app_settings_value(self, val):
428 """
428 """
429 Setter that will always make sure we use unicode in app_settings_value
429 Setter that will always make sure we use unicode in app_settings_value
430
430
431 :param val:
431 :param val:
432 """
432 """
433 self._app_settings_value = safe_unicode(val)
433 self._app_settings_value = safe_unicode(val)
434
434
435 @hybrid_property
435 @hybrid_property
436 def app_settings_type(self):
436 def app_settings_type(self):
437 return self._app_settings_type
437 return self._app_settings_type
438
438
439 @app_settings_type.setter
439 @app_settings_type.setter
440 def app_settings_type(self, val):
440 def app_settings_type(self, val):
441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
442 if val not in SETTINGS_TYPES:
442 if val not in SETTINGS_TYPES:
443 raise Exception('type must be one of %s got %s'
443 raise Exception('type must be one of %s got %s'
444 % (SETTINGS_TYPES.keys(), val))
444 % (SETTINGS_TYPES.keys(), val))
445 self._app_settings_type = val
445 self._app_settings_type = val
446
446
447 def __unicode__(self):
447 def __unicode__(self):
448 return u"<%s('%s:%s:%s[%s]')>" % (
448 return u"<%s('%s:%s:%s[%s]')>" % (
449 self.__class__.__name__, self.repository.repo_name,
449 self.__class__.__name__, self.repository.repo_name,
450 self.app_settings_name, self.app_settings_value,
450 self.app_settings_name, self.app_settings_value,
451 self.app_settings_type
451 self.app_settings_type
452 )
452 )
453
453
454
454
455 class RepoRhodeCodeUi(Base, BaseModel):
455 class RepoRhodeCodeUi(Base, BaseModel):
456 __tablename__ = 'repo_rhodecode_ui'
456 __tablename__ = 'repo_rhodecode_ui'
457 __table_args__ = (
457 __table_args__ = (
458 UniqueConstraint(
458 UniqueConstraint(
459 'repository_id', 'ui_section', 'ui_key',
459 'repository_id', 'ui_section', 'ui_key',
460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
463 )
463 )
464
464
465 repository_id = Column(
465 repository_id = Column(
466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 nullable=False)
467 nullable=False)
468 ui_id = Column(
468 ui_id = Column(
469 "ui_id", Integer(), nullable=False, unique=True, default=None,
469 "ui_id", Integer(), nullable=False, unique=True, default=None,
470 primary_key=True)
470 primary_key=True)
471 ui_section = Column(
471 ui_section = Column(
472 "ui_section", String(255), nullable=True, unique=None, default=None)
472 "ui_section", String(255), nullable=True, unique=None, default=None)
473 ui_key = Column(
473 ui_key = Column(
474 "ui_key", String(255), nullable=True, unique=None, default=None)
474 "ui_key", String(255), nullable=True, unique=None, default=None)
475 ui_value = Column(
475 ui_value = Column(
476 "ui_value", String(255), nullable=True, unique=None, default=None)
476 "ui_value", String(255), nullable=True, unique=None, default=None)
477 ui_active = Column(
477 ui_active = Column(
478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
479
479
480 repository = relationship('Repository')
480 repository = relationship('Repository')
481
481
482 def __repr__(self):
482 def __repr__(self):
483 return '<%s[%s:%s]%s=>%s]>' % (
483 return '<%s[%s:%s]%s=>%s]>' % (
484 self.__class__.__name__, self.repository.repo_name,
484 self.__class__.__name__, self.repository.repo_name,
485 self.ui_section, self.ui_key, self.ui_value)
485 self.ui_section, self.ui_key, self.ui_value)
486
486
487
487
488 class User(Base, BaseModel):
488 class User(Base, BaseModel):
489 __tablename__ = 'users'
489 __tablename__ = 'users'
490 __table_args__ = (
490 __table_args__ = (
491 UniqueConstraint('username'), UniqueConstraint('email'),
491 UniqueConstraint('username'), UniqueConstraint('email'),
492 Index('u_username_idx', 'username'),
492 Index('u_username_idx', 'username'),
493 Index('u_email_idx', 'email'),
493 Index('u_email_idx', 'email'),
494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
496 )
496 )
497 DEFAULT_USER = 'default'
497 DEFAULT_USER = 'default'
498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
500
500
501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
502 username = Column("username", String(255), nullable=True, unique=None, default=None)
502 username = Column("username", String(255), nullable=True, unique=None, default=None)
503 password = Column("password", String(255), nullable=True, unique=None, default=None)
503 password = Column("password", String(255), nullable=True, unique=None, default=None)
504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
516
516
517 user_log = relationship('UserLog')
517 user_log = relationship('UserLog')
518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
519
519
520 repositories = relationship('Repository')
520 repositories = relationship('Repository')
521 repository_groups = relationship('RepoGroup')
521 repository_groups = relationship('RepoGroup')
522 user_groups = relationship('UserGroup')
522 user_groups = relationship('UserGroup')
523
523
524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
526
526
527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
530
530
531 group_member = relationship('UserGroupMember', cascade='all')
531 group_member = relationship('UserGroupMember', cascade='all')
532
532
533 notifications = relationship('UserNotification', cascade='all')
533 notifications = relationship('UserNotification', cascade='all')
534 # notifications assigned to this user
534 # notifications assigned to this user
535 user_created_notifications = relationship('Notification', cascade='all')
535 user_created_notifications = relationship('Notification', cascade='all')
536 # comments created by this user
536 # comments created by this user
537 user_comments = relationship('ChangesetComment', cascade='all')
537 user_comments = relationship('ChangesetComment', cascade='all')
538 # user profile extra info
538 # user profile extra info
539 user_emails = relationship('UserEmailMap', cascade='all')
539 user_emails = relationship('UserEmailMap', cascade='all')
540 user_ip_map = relationship('UserIpMap', cascade='all')
540 user_ip_map = relationship('UserIpMap', cascade='all')
541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
542 # gists
542 # gists
543 user_gists = relationship('Gist', cascade='all')
543 user_gists = relationship('Gist', cascade='all')
544 # user pull requests
544 # user pull requests
545 user_pull_requests = relationship('PullRequest', cascade='all')
545 user_pull_requests = relationship('PullRequest', cascade='all')
546 # external identities
546 # external identities
547 extenal_identities = relationship(
547 extenal_identities = relationship(
548 'ExternalIdentity',
548 'ExternalIdentity',
549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
550 cascade='all')
550 cascade='all')
551
551
552 def __unicode__(self):
552 def __unicode__(self):
553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
554 self.user_id, self.username)
554 self.user_id, self.username)
555
555
556 @hybrid_property
556 @hybrid_property
557 def email(self):
557 def email(self):
558 return self._email
558 return self._email
559
559
560 @email.setter
560 @email.setter
561 def email(self, val):
561 def email(self, val):
562 self._email = val.lower() if val else None
562 self._email = val.lower() if val else None
563
563
564 @property
564 @property
565 def firstname(self):
565 def firstname(self):
566 # alias for future
566 # alias for future
567 return self.name
567 return self.name
568
568
569 @property
569 @property
570 def emails(self):
570 def emails(self):
571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
572 return [self.email] + [x.email for x in other]
572 return [self.email] + [x.email for x in other]
573
573
574 @property
574 @property
575 def auth_tokens(self):
575 def auth_tokens(self):
576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
577
577
578 @property
578 @property
579 def extra_auth_tokens(self):
579 def extra_auth_tokens(self):
580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
581
581
582 @property
582 @property
583 def feed_token(self):
583 def feed_token(self):
584 feed_tokens = UserApiKeys.query()\
584 feed_tokens = UserApiKeys.query()\
585 .filter(UserApiKeys.user == self)\
585 .filter(UserApiKeys.user == self)\
586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
587 .all()
587 .all()
588 if feed_tokens:
588 if feed_tokens:
589 return feed_tokens[0].api_key
589 return feed_tokens[0].api_key
590 else:
590 else:
591 # use the main token so we don't end up with nothing...
591 # use the main token so we don't end up with nothing...
592 return self.api_key
592 return self.api_key
593
593
594 @classmethod
594 @classmethod
595 def extra_valid_auth_tokens(cls, user, role=None):
595 def extra_valid_auth_tokens(cls, user, role=None):
596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
597 .filter(or_(UserApiKeys.expires == -1,
597 .filter(or_(UserApiKeys.expires == -1,
598 UserApiKeys.expires >= time.time()))
598 UserApiKeys.expires >= time.time()))
599 if role:
599 if role:
600 tokens = tokens.filter(or_(UserApiKeys.role == role,
600 tokens = tokens.filter(or_(UserApiKeys.role == role,
601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
602 return tokens.all()
602 return tokens.all()
603
603
604 @property
604 @property
605 def builtin_token_roles(self):
605 def builtin_token_roles(self):
606 return map(UserApiKeys._get_role_name, [
606 return map(UserApiKeys._get_role_name, [
607 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
607 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
608 ])
608 ])
609
609
610 @property
610 @property
611 def ip_addresses(self):
611 def ip_addresses(self):
612 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
612 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
613 return [x.ip_addr for x in ret]
613 return [x.ip_addr for x in ret]
614
614
615 @property
615 @property
616 def username_and_name(self):
616 def username_and_name(self):
617 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
617 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
618
618
619 @property
619 @property
620 def username_or_name_or_email(self):
620 def username_or_name_or_email(self):
621 full_name = self.full_name if self.full_name is not ' ' else None
621 full_name = self.full_name if self.full_name is not ' ' else None
622 return self.username or full_name or self.email
622 return self.username or full_name or self.email
623
623
624 @property
624 @property
625 def full_name(self):
625 def full_name(self):
626 return '%s %s' % (self.firstname, self.lastname)
626 return '%s %s' % (self.firstname, self.lastname)
627
627
628 @property
628 @property
629 def full_name_or_username(self):
629 def full_name_or_username(self):
630 return ('%s %s' % (self.firstname, self.lastname)
630 return ('%s %s' % (self.firstname, self.lastname)
631 if (self.firstname and self.lastname) else self.username)
631 if (self.firstname and self.lastname) else self.username)
632
632
633 @property
633 @property
634 def full_contact(self):
634 def full_contact(self):
635 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
635 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
636
636
637 @property
637 @property
638 def short_contact(self):
638 def short_contact(self):
639 return '%s %s' % (self.firstname, self.lastname)
639 return '%s %s' % (self.firstname, self.lastname)
640
640
641 @property
641 @property
642 def is_admin(self):
642 def is_admin(self):
643 return self.admin
643 return self.admin
644
644
645 @property
645 @property
646 def AuthUser(self):
646 def AuthUser(self):
647 """
647 """
648 Returns instance of AuthUser for this user
648 Returns instance of AuthUser for this user
649 """
649 """
650 from rhodecode.lib.auth import AuthUser
650 from rhodecode.lib.auth import AuthUser
651 return AuthUser(user_id=self.user_id, api_key=self.api_key,
651 return AuthUser(user_id=self.user_id, api_key=self.api_key,
652 username=self.username)
652 username=self.username)
653
653
654 @hybrid_property
654 @hybrid_property
655 def user_data(self):
655 def user_data(self):
656 if not self._user_data:
656 if not self._user_data:
657 return {}
657 return {}
658
658
659 try:
659 try:
660 return json.loads(self._user_data)
660 return json.loads(self._user_data)
661 except TypeError:
661 except TypeError:
662 return {}
662 return {}
663
663
664 @user_data.setter
664 @user_data.setter
665 def user_data(self, val):
665 def user_data(self, val):
666 if not isinstance(val, dict):
666 if not isinstance(val, dict):
667 raise Exception('user_data must be dict, got %s' % type(val))
667 raise Exception('user_data must be dict, got %s' % type(val))
668 try:
668 try:
669 self._user_data = json.dumps(val)
669 self._user_data = json.dumps(val)
670 except Exception:
670 except Exception:
671 log.error(traceback.format_exc())
671 log.error(traceback.format_exc())
672
672
673 @classmethod
673 @classmethod
674 def get_by_username(cls, username, case_insensitive=False,
674 def get_by_username(cls, username, case_insensitive=False,
675 cache=False, identity_cache=False):
675 cache=False, identity_cache=False):
676 session = Session()
676 session = Session()
677
677
678 if case_insensitive:
678 if case_insensitive:
679 q = cls.query().filter(
679 q = cls.query().filter(
680 func.lower(cls.username) == func.lower(username))
680 func.lower(cls.username) == func.lower(username))
681 else:
681 else:
682 q = cls.query().filter(cls.username == username)
682 q = cls.query().filter(cls.username == username)
683
683
684 if cache:
684 if cache:
685 if identity_cache:
685 if identity_cache:
686 val = cls.identity_cache(session, 'username', username)
686 val = cls.identity_cache(session, 'username', username)
687 if val:
687 if val:
688 return val
688 return val
689 else:
689 else:
690 q = q.options(
690 q = q.options(
691 FromCache("sql_cache_short",
691 FromCache("sql_cache_short",
692 "get_user_by_name_%s" % _hash_key(username)))
692 "get_user_by_name_%s" % _hash_key(username)))
693
693
694 return q.scalar()
694 return q.scalar()
695
695
696 @classmethod
696 @classmethod
697 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
697 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
698 q = cls.query().filter(cls.api_key == auth_token)
698 q = cls.query().filter(cls.api_key == auth_token)
699
699
700 if cache:
700 if cache:
701 q = q.options(FromCache("sql_cache_short",
701 q = q.options(FromCache("sql_cache_short",
702 "get_auth_token_%s" % auth_token))
702 "get_auth_token_%s" % auth_token))
703 res = q.scalar()
703 res = q.scalar()
704
704
705 if fallback and not res:
705 if fallback and not res:
706 #fallback to additional keys
706 #fallback to additional keys
707 _res = UserApiKeys.query()\
707 _res = UserApiKeys.query()\
708 .filter(UserApiKeys.api_key == auth_token)\
708 .filter(UserApiKeys.api_key == auth_token)\
709 .filter(or_(UserApiKeys.expires == -1,
709 .filter(or_(UserApiKeys.expires == -1,
710 UserApiKeys.expires >= time.time()))\
710 UserApiKeys.expires >= time.time()))\
711 .first()
711 .first()
712 if _res:
712 if _res:
713 res = _res.user
713 res = _res.user
714 return res
714 return res
715
715
716 @classmethod
716 @classmethod
717 def get_by_email(cls, email, case_insensitive=False, cache=False):
717 def get_by_email(cls, email, case_insensitive=False, cache=False):
718
718
719 if case_insensitive:
719 if case_insensitive:
720 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
720 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
721
721
722 else:
722 else:
723 q = cls.query().filter(cls.email == email)
723 q = cls.query().filter(cls.email == email)
724
724
725 if cache:
725 if cache:
726 q = q.options(FromCache("sql_cache_short",
726 q = q.options(FromCache("sql_cache_short",
727 "get_email_key_%s" % _hash_key(email)))
727 "get_email_key_%s" % _hash_key(email)))
728
728
729 ret = q.scalar()
729 ret = q.scalar()
730 if ret is None:
730 if ret is None:
731 q = UserEmailMap.query()
731 q = UserEmailMap.query()
732 # try fetching in alternate email map
732 # try fetching in alternate email map
733 if case_insensitive:
733 if case_insensitive:
734 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
734 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
735 else:
735 else:
736 q = q.filter(UserEmailMap.email == email)
736 q = q.filter(UserEmailMap.email == email)
737 q = q.options(joinedload(UserEmailMap.user))
737 q = q.options(joinedload(UserEmailMap.user))
738 if cache:
738 if cache:
739 q = q.options(FromCache("sql_cache_short",
739 q = q.options(FromCache("sql_cache_short",
740 "get_email_map_key_%s" % email))
740 "get_email_map_key_%s" % email))
741 ret = getattr(q.scalar(), 'user', None)
741 ret = getattr(q.scalar(), 'user', None)
742
742
743 return ret
743 return ret
744
744
745 @classmethod
745 @classmethod
746 def get_from_cs_author(cls, author):
746 def get_from_cs_author(cls, author):
747 """
747 """
748 Tries to get User objects out of commit author string
748 Tries to get User objects out of commit author string
749
749
750 :param author:
750 :param author:
751 """
751 """
752 from rhodecode.lib.helpers import email, author_name
752 from rhodecode.lib.helpers import email, author_name
753 # Valid email in the attribute passed, see if they're in the system
753 # Valid email in the attribute passed, see if they're in the system
754 _email = email(author)
754 _email = email(author)
755 if _email:
755 if _email:
756 user = cls.get_by_email(_email, case_insensitive=True)
756 user = cls.get_by_email(_email, case_insensitive=True)
757 if user:
757 if user:
758 return user
758 return user
759 # Maybe we can match by username?
759 # Maybe we can match by username?
760 _author = author_name(author)
760 _author = author_name(author)
761 user = cls.get_by_username(_author, case_insensitive=True)
761 user = cls.get_by_username(_author, case_insensitive=True)
762 if user:
762 if user:
763 return user
763 return user
764
764
765 def update_userdata(self, **kwargs):
765 def update_userdata(self, **kwargs):
766 usr = self
766 usr = self
767 old = usr.user_data
767 old = usr.user_data
768 old.update(**kwargs)
768 old.update(**kwargs)
769 usr.user_data = old
769 usr.user_data = old
770 Session().add(usr)
770 Session().add(usr)
771 log.debug('updated userdata with ', kwargs)
771 log.debug('updated userdata with ', kwargs)
772
772
773 def update_lastlogin(self):
773 def update_lastlogin(self):
774 """Update user lastlogin"""
774 """Update user lastlogin"""
775 self.last_login = datetime.datetime.now()
775 self.last_login = datetime.datetime.now()
776 Session().add(self)
776 Session().add(self)
777 log.debug('updated user %s lastlogin', self.username)
777 log.debug('updated user %s lastlogin', self.username)
778
778
779 def update_lastactivity(self):
779 def update_lastactivity(self):
780 """Update user lastactivity"""
780 """Update user lastactivity"""
781 usr = self
781 usr = self
782 old = usr.user_data
782 old = usr.user_data
783 old.update({'last_activity': time.time()})
783 old.update({'last_activity': time.time()})
784 usr.user_data = old
784 usr.user_data = old
785 Session().add(usr)
785 Session().add(usr)
786 log.debug('updated user %s lastactivity', usr.username)
786 log.debug('updated user %s lastactivity', usr.username)
787
787
788 def update_password(self, new_password, change_api_key=False):
788 def update_password(self, new_password, change_api_key=False):
789 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
789 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
790
790
791 self.password = get_crypt_password(new_password)
791 self.password = get_crypt_password(new_password)
792 if change_api_key:
792 if change_api_key:
793 self.api_key = generate_auth_token(self.username)
793 self.api_key = generate_auth_token(self.username)
794 Session().add(self)
794 Session().add(self)
795
795
796 @classmethod
796 @classmethod
797 def get_first_super_admin(cls):
797 def get_first_super_admin(cls):
798 user = User.query().filter(User.admin == true()).first()
798 user = User.query().filter(User.admin == true()).first()
799 if user is None:
799 if user is None:
800 raise Exception('FATAL: Missing administrative account!')
800 raise Exception('FATAL: Missing administrative account!')
801 return user
801 return user
802
802
803 @classmethod
803 @classmethod
804 def get_all_super_admins(cls):
804 def get_all_super_admins(cls):
805 """
805 """
806 Returns all admin accounts sorted by username
806 Returns all admin accounts sorted by username
807 """
807 """
808 return User.query().filter(User.admin == true())\
808 return User.query().filter(User.admin == true())\
809 .order_by(User.username.asc()).all()
809 .order_by(User.username.asc()).all()
810
810
811 @classmethod
811 @classmethod
812 def get_default_user(cls, cache=False):
812 def get_default_user(cls, cache=False):
813 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
813 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
814 if user is None:
814 if user is None:
815 raise Exception('FATAL: Missing default account!')
815 raise Exception('FATAL: Missing default account!')
816 return user
816 return user
817
817
818 def _get_default_perms(self, user, suffix=''):
818 def _get_default_perms(self, user, suffix=''):
819 from rhodecode.model.permission import PermissionModel
819 from rhodecode.model.permission import PermissionModel
820 return PermissionModel().get_default_perms(user.user_perms, suffix)
820 return PermissionModel().get_default_perms(user.user_perms, suffix)
821
821
822 def get_default_perms(self, suffix=''):
822 def get_default_perms(self, suffix=''):
823 return self._get_default_perms(self, suffix)
823 return self._get_default_perms(self, suffix)
824
824
825 def get_api_data(self, include_secrets=False, details='full'):
825 def get_api_data(self, include_secrets=False, details='full'):
826 """
826 """
827 Common function for generating user related data for API
827 Common function for generating user related data for API
828
828
829 :param include_secrets: By default secrets in the API data will be replaced
829 :param include_secrets: By default secrets in the API data will be replaced
830 by a placeholder value to prevent exposing this data by accident. In case
830 by a placeholder value to prevent exposing this data by accident. In case
831 this data shall be exposed, set this flag to ``True``.
831 this data shall be exposed, set this flag to ``True``.
832
832
833 :param details: details can be 'basic|full' basic gives only a subset of
833 :param details: details can be 'basic|full' basic gives only a subset of
834 the available user information that includes user_id, name and emails.
834 the available user information that includes user_id, name and emails.
835 """
835 """
836 user = self
836 user = self
837 user_data = self.user_data
837 user_data = self.user_data
838 data = {
838 data = {
839 'user_id': user.user_id,
839 'user_id': user.user_id,
840 'username': user.username,
840 'username': user.username,
841 'firstname': user.name,
841 'firstname': user.name,
842 'lastname': user.lastname,
842 'lastname': user.lastname,
843 'email': user.email,
843 'email': user.email,
844 'emails': user.emails,
844 'emails': user.emails,
845 }
845 }
846 if details == 'basic':
846 if details == 'basic':
847 return data
847 return data
848
848
849 api_key_length = 40
849 api_key_length = 40
850 api_key_replacement = '*' * api_key_length
850 api_key_replacement = '*' * api_key_length
851
851
852 extras = {
852 extras = {
853 'api_key': api_key_replacement,
853 'api_key': api_key_replacement,
854 'api_keys': [api_key_replacement],
854 'api_keys': [api_key_replacement],
855 'active': user.active,
855 'active': user.active,
856 'admin': user.admin,
856 'admin': user.admin,
857 'extern_type': user.extern_type,
857 'extern_type': user.extern_type,
858 'extern_name': user.extern_name,
858 'extern_name': user.extern_name,
859 'last_login': user.last_login,
859 'last_login': user.last_login,
860 'ip_addresses': user.ip_addresses,
860 'ip_addresses': user.ip_addresses,
861 'language': user_data.get('language')
861 'language': user_data.get('language')
862 }
862 }
863 data.update(extras)
863 data.update(extras)
864
864
865 if include_secrets:
865 if include_secrets:
866 data['api_key'] = user.api_key
866 data['api_key'] = user.api_key
867 data['api_keys'] = user.auth_tokens
867 data['api_keys'] = user.auth_tokens
868 return data
868 return data
869
869
870 def __json__(self):
870 def __json__(self):
871 data = {
871 data = {
872 'full_name': self.full_name,
872 'full_name': self.full_name,
873 'full_name_or_username': self.full_name_or_username,
873 'full_name_or_username': self.full_name_or_username,
874 'short_contact': self.short_contact,
874 'short_contact': self.short_contact,
875 'full_contact': self.full_contact,
875 'full_contact': self.full_contact,
876 }
876 }
877 data.update(self.get_api_data())
877 data.update(self.get_api_data())
878 return data
878 return data
879
879
880
880
881 class UserApiKeys(Base, BaseModel):
881 class UserApiKeys(Base, BaseModel):
882 __tablename__ = 'user_api_keys'
882 __tablename__ = 'user_api_keys'
883 __table_args__ = (
883 __table_args__ = (
884 Index('uak_api_key_idx', 'api_key'),
884 Index('uak_api_key_idx', 'api_key'),
885 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
885 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
886 UniqueConstraint('api_key'),
886 UniqueConstraint('api_key'),
887 {'extend_existing': True, 'mysql_engine': 'InnoDB',
887 {'extend_existing': True, 'mysql_engine': 'InnoDB',
888 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
888 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
889 )
889 )
890 __mapper_args__ = {}
890 __mapper_args__ = {}
891
891
892 # ApiKey role
892 # ApiKey role
893 ROLE_ALL = 'token_role_all'
893 ROLE_ALL = 'token_role_all'
894 ROLE_HTTP = 'token_role_http'
894 ROLE_HTTP = 'token_role_http'
895 ROLE_VCS = 'token_role_vcs'
895 ROLE_VCS = 'token_role_vcs'
896 ROLE_API = 'token_role_api'
896 ROLE_API = 'token_role_api'
897 ROLE_FEED = 'token_role_feed'
897 ROLE_FEED = 'token_role_feed'
898 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
898 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
899
899
900 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
900 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
901 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
901 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
902 api_key = Column("api_key", String(255), nullable=False, unique=True)
902 api_key = Column("api_key", String(255), nullable=False, unique=True)
903 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
903 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
904 expires = Column('expires', Float(53), nullable=False)
904 expires = Column('expires', Float(53), nullable=False)
905 role = Column('role', String(255), nullable=True)
905 role = Column('role', String(255), nullable=True)
906 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
906 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
907
907
908 user = relationship('User', lazy='joined')
908 user = relationship('User', lazy='joined')
909
909
910 @classmethod
910 @classmethod
911 def _get_role_name(cls, role):
911 def _get_role_name(cls, role):
912 return {
912 return {
913 cls.ROLE_ALL: _('all'),
913 cls.ROLE_ALL: _('all'),
914 cls.ROLE_HTTP: _('http/web interface'),
914 cls.ROLE_HTTP: _('http/web interface'),
915 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
915 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
916 cls.ROLE_API: _('api calls'),
916 cls.ROLE_API: _('api calls'),
917 cls.ROLE_FEED: _('feed access'),
917 cls.ROLE_FEED: _('feed access'),
918 }.get(role, role)
918 }.get(role, role)
919
919
920 @property
920 @property
921 def expired(self):
921 def expired(self):
922 if self.expires == -1:
922 if self.expires == -1:
923 return False
923 return False
924 return time.time() > self.expires
924 return time.time() > self.expires
925
925
926 @property
926 @property
927 def role_humanized(self):
927 def role_humanized(self):
928 return self._get_role_name(self.role)
928 return self._get_role_name(self.role)
929
929
930
930
931 class UserEmailMap(Base, BaseModel):
931 class UserEmailMap(Base, BaseModel):
932 __tablename__ = 'user_email_map'
932 __tablename__ = 'user_email_map'
933 __table_args__ = (
933 __table_args__ = (
934 Index('uem_email_idx', 'email'),
934 Index('uem_email_idx', 'email'),
935 UniqueConstraint('email'),
935 UniqueConstraint('email'),
936 {'extend_existing': True, 'mysql_engine': 'InnoDB',
936 {'extend_existing': True, 'mysql_engine': 'InnoDB',
937 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
937 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
938 )
938 )
939 __mapper_args__ = {}
939 __mapper_args__ = {}
940
940
941 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
941 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
943 _email = Column("email", String(255), nullable=True, unique=False, default=None)
943 _email = Column("email", String(255), nullable=True, unique=False, default=None)
944 user = relationship('User', lazy='joined')
944 user = relationship('User', lazy='joined')
945
945
946 @validates('_email')
946 @validates('_email')
947 def validate_email(self, key, email):
947 def validate_email(self, key, email):
948 # check if this email is not main one
948 # check if this email is not main one
949 main_email = Session().query(User).filter(User.email == email).scalar()
949 main_email = Session().query(User).filter(User.email == email).scalar()
950 if main_email is not None:
950 if main_email is not None:
951 raise AttributeError('email %s is present is user table' % email)
951 raise AttributeError('email %s is present is user table' % email)
952 return email
952 return email
953
953
954 @hybrid_property
954 @hybrid_property
955 def email(self):
955 def email(self):
956 return self._email
956 return self._email
957
957
958 @email.setter
958 @email.setter
959 def email(self, val):
959 def email(self, val):
960 self._email = val.lower() if val else None
960 self._email = val.lower() if val else None
961
961
962
962
963 class UserIpMap(Base, BaseModel):
963 class UserIpMap(Base, BaseModel):
964 __tablename__ = 'user_ip_map'
964 __tablename__ = 'user_ip_map'
965 __table_args__ = (
965 __table_args__ = (
966 UniqueConstraint('user_id', 'ip_addr'),
966 UniqueConstraint('user_id', 'ip_addr'),
967 {'extend_existing': True, 'mysql_engine': 'InnoDB',
967 {'extend_existing': True, 'mysql_engine': 'InnoDB',
968 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
968 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
969 )
969 )
970 __mapper_args__ = {}
970 __mapper_args__ = {}
971
971
972 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
972 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
973 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
973 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
974 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
974 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
975 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
975 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
976 description = Column("description", String(10000), nullable=True, unique=None, default=None)
976 description = Column("description", String(10000), nullable=True, unique=None, default=None)
977 user = relationship('User', lazy='joined')
977 user = relationship('User', lazy='joined')
978
978
979 @classmethod
979 @classmethod
980 def _get_ip_range(cls, ip_addr):
980 def _get_ip_range(cls, ip_addr):
981 net = ipaddress.ip_network(ip_addr, strict=False)
981 net = ipaddress.ip_network(ip_addr, strict=False)
982 return [str(net.network_address), str(net.broadcast_address)]
982 return [str(net.network_address), str(net.broadcast_address)]
983
983
984 def __json__(self):
984 def __json__(self):
985 return {
985 return {
986 'ip_addr': self.ip_addr,
986 'ip_addr': self.ip_addr,
987 'ip_range': self._get_ip_range(self.ip_addr),
987 'ip_range': self._get_ip_range(self.ip_addr),
988 }
988 }
989
989
990 def __unicode__(self):
990 def __unicode__(self):
991 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
991 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
992 self.user_id, self.ip_addr)
992 self.user_id, self.ip_addr)
993
993
994 class UserLog(Base, BaseModel):
994 class UserLog(Base, BaseModel):
995 __tablename__ = 'user_logs'
995 __tablename__ = 'user_logs'
996 __table_args__ = (
996 __table_args__ = (
997 {'extend_existing': True, 'mysql_engine': 'InnoDB',
997 {'extend_existing': True, 'mysql_engine': 'InnoDB',
998 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
998 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
999 )
999 )
1000 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1000 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1001 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1001 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1002 username = Column("username", String(255), nullable=True, unique=None, default=None)
1002 username = Column("username", String(255), nullable=True, unique=None, default=None)
1003 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1003 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1004 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1004 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1005 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1005 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1006 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1006 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1007 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1007 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1008
1008
1009 def __unicode__(self):
1009 def __unicode__(self):
1010 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1010 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1011 self.repository_name,
1011 self.repository_name,
1012 self.action)
1012 self.action)
1013
1013
1014 @property
1014 @property
1015 def action_as_day(self):
1015 def action_as_day(self):
1016 return datetime.date(*self.action_date.timetuple()[:3])
1016 return datetime.date(*self.action_date.timetuple()[:3])
1017
1017
1018 user = relationship('User')
1018 user = relationship('User')
1019 repository = relationship('Repository', cascade='')
1019 repository = relationship('Repository', cascade='')
1020
1020
1021
1021
1022 class UserGroup(Base, BaseModel):
1022 class UserGroup(Base, BaseModel):
1023 __tablename__ = 'users_groups'
1023 __tablename__ = 'users_groups'
1024 __table_args__ = (
1024 __table_args__ = (
1025 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1025 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1026 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1026 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1027 )
1027 )
1028
1028
1029 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1029 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1030 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1030 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1031 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1031 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1032 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1032 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1033 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1033 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1036 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1036 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1037
1037
1038 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1038 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1039 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1039 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1040 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1040 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1041 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1041 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1042 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1042 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1043 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1043 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1044
1044
1045 user = relationship('User')
1045 user = relationship('User')
1046
1046
1047 @hybrid_property
1047 @hybrid_property
1048 def group_data(self):
1048 def group_data(self):
1049 if not self._group_data:
1049 if not self._group_data:
1050 return {}
1050 return {}
1051
1051
1052 try:
1052 try:
1053 return json.loads(self._group_data)
1053 return json.loads(self._group_data)
1054 except TypeError:
1054 except TypeError:
1055 return {}
1055 return {}
1056
1056
1057 @group_data.setter
1057 @group_data.setter
1058 def group_data(self, val):
1058 def group_data(self, val):
1059 try:
1059 try:
1060 self._group_data = json.dumps(val)
1060 self._group_data = json.dumps(val)
1061 except Exception:
1061 except Exception:
1062 log.error(traceback.format_exc())
1062 log.error(traceback.format_exc())
1063
1063
1064 def __unicode__(self):
1064 def __unicode__(self):
1065 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1065 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1066 self.users_group_id,
1066 self.users_group_id,
1067 self.users_group_name)
1067 self.users_group_name)
1068
1068
1069 @classmethod
1069 @classmethod
1070 def get_by_group_name(cls, group_name, cache=False,
1070 def get_by_group_name(cls, group_name, cache=False,
1071 case_insensitive=False):
1071 case_insensitive=False):
1072 if case_insensitive:
1072 if case_insensitive:
1073 q = cls.query().filter(func.lower(cls.users_group_name) ==
1073 q = cls.query().filter(func.lower(cls.users_group_name) ==
1074 func.lower(group_name))
1074 func.lower(group_name))
1075
1075
1076 else:
1076 else:
1077 q = cls.query().filter(cls.users_group_name == group_name)
1077 q = cls.query().filter(cls.users_group_name == group_name)
1078 if cache:
1078 if cache:
1079 q = q.options(FromCache(
1079 q = q.options(FromCache(
1080 "sql_cache_short",
1080 "sql_cache_short",
1081 "get_group_%s" % _hash_key(group_name)))
1081 "get_group_%s" % _hash_key(group_name)))
1082 return q.scalar()
1082 return q.scalar()
1083
1083
1084 @classmethod
1084 @classmethod
1085 def get(cls, user_group_id, cache=False):
1085 def get(cls, user_group_id, cache=False):
1086 user_group = cls.query()
1086 user_group = cls.query()
1087 if cache:
1087 if cache:
1088 user_group = user_group.options(FromCache("sql_cache_short",
1088 user_group = user_group.options(FromCache("sql_cache_short",
1089 "get_users_group_%s" % user_group_id))
1089 "get_users_group_%s" % user_group_id))
1090 return user_group.get(user_group_id)
1090 return user_group.get(user_group_id)
1091
1091
1092 def permissions(self, with_admins=True, with_owner=True):
1092 def permissions(self, with_admins=True, with_owner=True):
1093 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1093 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1094 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1094 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1095 joinedload(UserUserGroupToPerm.user),
1095 joinedload(UserUserGroupToPerm.user),
1096 joinedload(UserUserGroupToPerm.permission),)
1096 joinedload(UserUserGroupToPerm.permission),)
1097
1097
1098 # get owners and admins and permissions. We do a trick of re-writing
1098 # get owners and admins and permissions. We do a trick of re-writing
1099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1100 # has a global reference and changing one object propagates to all
1100 # has a global reference and changing one object propagates to all
1101 # others. This means if admin is also an owner admin_row that change
1101 # others. This means if admin is also an owner admin_row that change
1102 # would propagate to both objects
1102 # would propagate to both objects
1103 perm_rows = []
1103 perm_rows = []
1104 for _usr in q.all():
1104 for _usr in q.all():
1105 usr = AttributeDict(_usr.user.get_dict())
1105 usr = AttributeDict(_usr.user.get_dict())
1106 usr.permission = _usr.permission.permission_name
1106 usr.permission = _usr.permission.permission_name
1107 perm_rows.append(usr)
1107 perm_rows.append(usr)
1108
1108
1109 # filter the perm rows by 'default' first and then sort them by
1109 # filter the perm rows by 'default' first and then sort them by
1110 # admin,write,read,none permissions sorted again alphabetically in
1110 # admin,write,read,none permissions sorted again alphabetically in
1111 # each group
1111 # each group
1112 perm_rows = sorted(perm_rows, key=display_sort)
1112 perm_rows = sorted(perm_rows, key=display_sort)
1113
1113
1114 _admin_perm = 'usergroup.admin'
1114 _admin_perm = 'usergroup.admin'
1115 owner_row = []
1115 owner_row = []
1116 if with_owner:
1116 if with_owner:
1117 usr = AttributeDict(self.user.get_dict())
1117 usr = AttributeDict(self.user.get_dict())
1118 usr.owner_row = True
1118 usr.owner_row = True
1119 usr.permission = _admin_perm
1119 usr.permission = _admin_perm
1120 owner_row.append(usr)
1120 owner_row.append(usr)
1121
1121
1122 super_admin_rows = []
1122 super_admin_rows = []
1123 if with_admins:
1123 if with_admins:
1124 for usr in User.get_all_super_admins():
1124 for usr in User.get_all_super_admins():
1125 # if this admin is also owner, don't double the record
1125 # if this admin is also owner, don't double the record
1126 if usr.user_id == owner_row[0].user_id:
1126 if usr.user_id == owner_row[0].user_id:
1127 owner_row[0].admin_row = True
1127 owner_row[0].admin_row = True
1128 else:
1128 else:
1129 usr = AttributeDict(usr.get_dict())
1129 usr = AttributeDict(usr.get_dict())
1130 usr.admin_row = True
1130 usr.admin_row = True
1131 usr.permission = _admin_perm
1131 usr.permission = _admin_perm
1132 super_admin_rows.append(usr)
1132 super_admin_rows.append(usr)
1133
1133
1134 return super_admin_rows + owner_row + perm_rows
1134 return super_admin_rows + owner_row + perm_rows
1135
1135
1136 def permission_user_groups(self):
1136 def permission_user_groups(self):
1137 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1137 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1138 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1138 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1139 joinedload(UserGroupUserGroupToPerm.target_user_group),
1139 joinedload(UserGroupUserGroupToPerm.target_user_group),
1140 joinedload(UserGroupUserGroupToPerm.permission),)
1140 joinedload(UserGroupUserGroupToPerm.permission),)
1141
1141
1142 perm_rows = []
1142 perm_rows = []
1143 for _user_group in q.all():
1143 for _user_group in q.all():
1144 usr = AttributeDict(_user_group.user_group.get_dict())
1144 usr = AttributeDict(_user_group.user_group.get_dict())
1145 usr.permission = _user_group.permission.permission_name
1145 usr.permission = _user_group.permission.permission_name
1146 perm_rows.append(usr)
1146 perm_rows.append(usr)
1147
1147
1148 return perm_rows
1148 return perm_rows
1149
1149
1150 def _get_default_perms(self, user_group, suffix=''):
1150 def _get_default_perms(self, user_group, suffix=''):
1151 from rhodecode.model.permission import PermissionModel
1151 from rhodecode.model.permission import PermissionModel
1152 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1152 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1153
1153
1154 def get_default_perms(self, suffix=''):
1154 def get_default_perms(self, suffix=''):
1155 return self._get_default_perms(self, suffix)
1155 return self._get_default_perms(self, suffix)
1156
1156
1157 def get_api_data(self, with_group_members=True, include_secrets=False):
1157 def get_api_data(self, with_group_members=True, include_secrets=False):
1158 """
1158 """
1159 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1159 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1160 basically forwarded.
1160 basically forwarded.
1161
1161
1162 """
1162 """
1163 user_group = self
1163 user_group = self
1164
1164
1165 data = {
1165 data = {
1166 'users_group_id': user_group.users_group_id,
1166 'users_group_id': user_group.users_group_id,
1167 'group_name': user_group.users_group_name,
1167 'group_name': user_group.users_group_name,
1168 'group_description': user_group.user_group_description,
1168 'group_description': user_group.user_group_description,
1169 'active': user_group.users_group_active,
1169 'active': user_group.users_group_active,
1170 'owner': user_group.user.username,
1170 'owner': user_group.user.username,
1171 }
1171 }
1172 if with_group_members:
1172 if with_group_members:
1173 users = []
1173 users = []
1174 for user in user_group.members:
1174 for user in user_group.members:
1175 user = user.user
1175 user = user.user
1176 users.append(user.get_api_data(include_secrets=include_secrets))
1176 users.append(user.get_api_data(include_secrets=include_secrets))
1177 data['users'] = users
1177 data['users'] = users
1178
1178
1179 return data
1179 return data
1180
1180
1181
1181
1182 class UserGroupMember(Base, BaseModel):
1182 class UserGroupMember(Base, BaseModel):
1183 __tablename__ = 'users_groups_members'
1183 __tablename__ = 'users_groups_members'
1184 __table_args__ = (
1184 __table_args__ = (
1185 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1185 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1186 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1186 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1187 )
1187 )
1188
1188
1189 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1189 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1190 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1190 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1191 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1191 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1192
1192
1193 user = relationship('User', lazy='joined')
1193 user = relationship('User', lazy='joined')
1194 users_group = relationship('UserGroup')
1194 users_group = relationship('UserGroup')
1195
1195
1196 def __init__(self, gr_id='', u_id=''):
1196 def __init__(self, gr_id='', u_id=''):
1197 self.users_group_id = gr_id
1197 self.users_group_id = gr_id
1198 self.user_id = u_id
1198 self.user_id = u_id
1199
1199
1200
1200
1201 class RepositoryField(Base, BaseModel):
1201 class RepositoryField(Base, BaseModel):
1202 __tablename__ = 'repositories_fields'
1202 __tablename__ = 'repositories_fields'
1203 __table_args__ = (
1203 __table_args__ = (
1204 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1204 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1205 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1205 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1206 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1206 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1207 )
1207 )
1208 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1208 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1209
1209
1210 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1210 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1211 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1211 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1212 field_key = Column("field_key", String(250))
1212 field_key = Column("field_key", String(250))
1213 field_label = Column("field_label", String(1024), nullable=False)
1213 field_label = Column("field_label", String(1024), nullable=False)
1214 field_value = Column("field_value", String(10000), nullable=False)
1214 field_value = Column("field_value", String(10000), nullable=False)
1215 field_desc = Column("field_desc", String(1024), nullable=False)
1215 field_desc = Column("field_desc", String(1024), nullable=False)
1216 field_type = Column("field_type", String(255), nullable=False, unique=None)
1216 field_type = Column("field_type", String(255), nullable=False, unique=None)
1217 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1217 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1218
1218
1219 repository = relationship('Repository')
1219 repository = relationship('Repository')
1220
1220
1221 @property
1221 @property
1222 def field_key_prefixed(self):
1222 def field_key_prefixed(self):
1223 return 'ex_%s' % self.field_key
1223 return 'ex_%s' % self.field_key
1224
1224
1225 @classmethod
1225 @classmethod
1226 def un_prefix_key(cls, key):
1226 def un_prefix_key(cls, key):
1227 if key.startswith(cls.PREFIX):
1227 if key.startswith(cls.PREFIX):
1228 return key[len(cls.PREFIX):]
1228 return key[len(cls.PREFIX):]
1229 return key
1229 return key
1230
1230
1231 @classmethod
1231 @classmethod
1232 def get_by_key_name(cls, key, repo):
1232 def get_by_key_name(cls, key, repo):
1233 row = cls.query()\
1233 row = cls.query()\
1234 .filter(cls.repository == repo)\
1234 .filter(cls.repository == repo)\
1235 .filter(cls.field_key == key).scalar()
1235 .filter(cls.field_key == key).scalar()
1236 return row
1236 return row
1237
1237
1238
1238
1239 class Repository(Base, BaseModel):
1239 class Repository(Base, BaseModel):
1240 __tablename__ = 'repositories'
1240 __tablename__ = 'repositories'
1241 __table_args__ = (
1241 __table_args__ = (
1242 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1242 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1243 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1243 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1244 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1244 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1245 )
1245 )
1246 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1246 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1247 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1247 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1248
1248
1249 STATE_CREATED = 'repo_state_created'
1249 STATE_CREATED = 'repo_state_created'
1250 STATE_PENDING = 'repo_state_pending'
1250 STATE_PENDING = 'repo_state_pending'
1251 STATE_ERROR = 'repo_state_error'
1251 STATE_ERROR = 'repo_state_error'
1252
1252
1253 LOCK_AUTOMATIC = 'lock_auto'
1253 LOCK_AUTOMATIC = 'lock_auto'
1254 LOCK_API = 'lock_api'
1254 LOCK_API = 'lock_api'
1255 LOCK_WEB = 'lock_web'
1255 LOCK_WEB = 'lock_web'
1256 LOCK_PULL = 'lock_pull'
1256 LOCK_PULL = 'lock_pull'
1257
1257
1258 NAME_SEP = URL_SEP
1258 NAME_SEP = URL_SEP
1259
1259
1260 repo_id = Column(
1260 repo_id = Column(
1261 "repo_id", Integer(), nullable=False, unique=True, default=None,
1261 "repo_id", Integer(), nullable=False, unique=True, default=None,
1262 primary_key=True)
1262 primary_key=True)
1263 _repo_name = Column(
1263 _repo_name = Column(
1264 "repo_name", Text(), nullable=False, default=None)
1264 "repo_name", Text(), nullable=False, default=None)
1265 _repo_name_hash = Column(
1265 _repo_name_hash = Column(
1266 "repo_name_hash", String(255), nullable=False, unique=True)
1266 "repo_name_hash", String(255), nullable=False, unique=True)
1267 repo_state = Column("repo_state", String(255), nullable=True)
1267 repo_state = Column("repo_state", String(255), nullable=True)
1268
1268
1269 clone_uri = Column(
1269 clone_uri = Column(
1270 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1270 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1271 default=None)
1271 default=None)
1272 repo_type = Column(
1272 repo_type = Column(
1273 "repo_type", String(255), nullable=False, unique=False, default=None)
1273 "repo_type", String(255), nullable=False, unique=False, default=None)
1274 user_id = Column(
1274 user_id = Column(
1275 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1275 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1276 unique=False, default=None)
1276 unique=False, default=None)
1277 private = Column(
1277 private = Column(
1278 "private", Boolean(), nullable=True, unique=None, default=None)
1278 "private", Boolean(), nullable=True, unique=None, default=None)
1279 enable_statistics = Column(
1279 enable_statistics = Column(
1280 "statistics", Boolean(), nullable=True, unique=None, default=True)
1280 "statistics", Boolean(), nullable=True, unique=None, default=True)
1281 enable_downloads = Column(
1281 enable_downloads = Column(
1282 "downloads", Boolean(), nullable=True, unique=None, default=True)
1282 "downloads", Boolean(), nullable=True, unique=None, default=True)
1283 description = Column(
1283 description = Column(
1284 "description", String(10000), nullable=True, unique=None, default=None)
1284 "description", String(10000), nullable=True, unique=None, default=None)
1285 created_on = Column(
1285 created_on = Column(
1286 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1286 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1287 default=datetime.datetime.now)
1287 default=datetime.datetime.now)
1288 updated_on = Column(
1288 updated_on = Column(
1289 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1289 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1290 default=datetime.datetime.now)
1290 default=datetime.datetime.now)
1291 _landing_revision = Column(
1291 _landing_revision = Column(
1292 "landing_revision", String(255), nullable=False, unique=False,
1292 "landing_revision", String(255), nullable=False, unique=False,
1293 default=None)
1293 default=None)
1294 enable_locking = Column(
1294 enable_locking = Column(
1295 "enable_locking", Boolean(), nullable=False, unique=None,
1295 "enable_locking", Boolean(), nullable=False, unique=None,
1296 default=False)
1296 default=False)
1297 _locked = Column(
1297 _locked = Column(
1298 "locked", String(255), nullable=True, unique=False, default=None)
1298 "locked", String(255), nullable=True, unique=False, default=None)
1299 _changeset_cache = Column(
1299 _changeset_cache = Column(
1300 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1300 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1301
1301
1302 fork_id = Column(
1302 fork_id = Column(
1303 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1303 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1304 nullable=True, unique=False, default=None)
1304 nullable=True, unique=False, default=None)
1305 group_id = Column(
1305 group_id = Column(
1306 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1306 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1307 unique=False, default=None)
1307 unique=False, default=None)
1308
1308
1309 user = relationship('User', lazy='joined')
1309 user = relationship('User', lazy='joined')
1310 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1310 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1311 group = relationship('RepoGroup', lazy='joined')
1311 group = relationship('RepoGroup', lazy='joined')
1312 repo_to_perm = relationship(
1312 repo_to_perm = relationship(
1313 'UserRepoToPerm', cascade='all',
1313 'UserRepoToPerm', cascade='all',
1314 order_by='UserRepoToPerm.repo_to_perm_id')
1314 order_by='UserRepoToPerm.repo_to_perm_id')
1315 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1315 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1316 stats = relationship('Statistics', cascade='all', uselist=False)
1316 stats = relationship('Statistics', cascade='all', uselist=False)
1317
1317
1318 followers = relationship(
1318 followers = relationship(
1319 'UserFollowing',
1319 'UserFollowing',
1320 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1320 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1321 cascade='all')
1321 cascade='all')
1322 extra_fields = relationship(
1322 extra_fields = relationship(
1323 'RepositoryField', cascade="all, delete, delete-orphan")
1323 'RepositoryField', cascade="all, delete, delete-orphan")
1324 logs = relationship('UserLog')
1324 logs = relationship('UserLog')
1325 comments = relationship(
1325 comments = relationship(
1326 'ChangesetComment', cascade="all, delete, delete-orphan")
1326 'ChangesetComment', cascade="all, delete, delete-orphan")
1327 pull_requests_source = relationship(
1327 pull_requests_source = relationship(
1328 'PullRequest',
1328 'PullRequest',
1329 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1329 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1330 cascade="all, delete, delete-orphan")
1330 cascade="all, delete, delete-orphan")
1331 pull_requests_target = relationship(
1331 pull_requests_target = relationship(
1332 'PullRequest',
1332 'PullRequest',
1333 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1333 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1334 cascade="all, delete, delete-orphan")
1334 cascade="all, delete, delete-orphan")
1335 ui = relationship('RepoRhodeCodeUi', cascade="all")
1335 ui = relationship('RepoRhodeCodeUi', cascade="all")
1336 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1336 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1337 integrations = relationship('Integration',
1337 integrations = relationship('Integration',
1338 cascade="all, delete, delete-orphan")
1338 cascade="all, delete, delete-orphan")
1339
1339
1340 def __unicode__(self):
1340 def __unicode__(self):
1341 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1341 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1342 safe_unicode(self.repo_name))
1342 safe_unicode(self.repo_name))
1343
1343
1344 @hybrid_property
1344 @hybrid_property
1345 def landing_rev(self):
1345 def landing_rev(self):
1346 # always should return [rev_type, rev]
1346 # always should return [rev_type, rev]
1347 if self._landing_revision:
1347 if self._landing_revision:
1348 _rev_info = self._landing_revision.split(':')
1348 _rev_info = self._landing_revision.split(':')
1349 if len(_rev_info) < 2:
1349 if len(_rev_info) < 2:
1350 _rev_info.insert(0, 'rev')
1350 _rev_info.insert(0, 'rev')
1351 return [_rev_info[0], _rev_info[1]]
1351 return [_rev_info[0], _rev_info[1]]
1352 return [None, None]
1352 return [None, None]
1353
1353
1354 @landing_rev.setter
1354 @landing_rev.setter
1355 def landing_rev(self, val):
1355 def landing_rev(self, val):
1356 if ':' not in val:
1356 if ':' not in val:
1357 raise ValueError('value must be delimited with `:` and consist '
1357 raise ValueError('value must be delimited with `:` and consist '
1358 'of <rev_type>:<rev>, got %s instead' % val)
1358 'of <rev_type>:<rev>, got %s instead' % val)
1359 self._landing_revision = val
1359 self._landing_revision = val
1360
1360
1361 @hybrid_property
1361 @hybrid_property
1362 def locked(self):
1362 def locked(self):
1363 if self._locked:
1363 if self._locked:
1364 user_id, timelocked, reason = self._locked.split(':')
1364 user_id, timelocked, reason = self._locked.split(':')
1365 lock_values = int(user_id), timelocked, reason
1365 lock_values = int(user_id), timelocked, reason
1366 else:
1366 else:
1367 lock_values = [None, None, None]
1367 lock_values = [None, None, None]
1368 return lock_values
1368 return lock_values
1369
1369
1370 @locked.setter
1370 @locked.setter
1371 def locked(self, val):
1371 def locked(self, val):
1372 if val and isinstance(val, (list, tuple)):
1372 if val and isinstance(val, (list, tuple)):
1373 self._locked = ':'.join(map(str, val))
1373 self._locked = ':'.join(map(str, val))
1374 else:
1374 else:
1375 self._locked = None
1375 self._locked = None
1376
1376
1377 @hybrid_property
1377 @hybrid_property
1378 def changeset_cache(self):
1378 def changeset_cache(self):
1379 from rhodecode.lib.vcs.backends.base import EmptyCommit
1379 from rhodecode.lib.vcs.backends.base import EmptyCommit
1380 dummy = EmptyCommit().__json__()
1380 dummy = EmptyCommit().__json__()
1381 if not self._changeset_cache:
1381 if not self._changeset_cache:
1382 return dummy
1382 return dummy
1383 try:
1383 try:
1384 return json.loads(self._changeset_cache)
1384 return json.loads(self._changeset_cache)
1385 except TypeError:
1385 except TypeError:
1386 return dummy
1386 return dummy
1387 except Exception:
1387 except Exception:
1388 log.error(traceback.format_exc())
1388 log.error(traceback.format_exc())
1389 return dummy
1389 return dummy
1390
1390
1391 @changeset_cache.setter
1391 @changeset_cache.setter
1392 def changeset_cache(self, val):
1392 def changeset_cache(self, val):
1393 try:
1393 try:
1394 self._changeset_cache = json.dumps(val)
1394 self._changeset_cache = json.dumps(val)
1395 except Exception:
1395 except Exception:
1396 log.error(traceback.format_exc())
1396 log.error(traceback.format_exc())
1397
1397
1398 @hybrid_property
1398 @hybrid_property
1399 def repo_name(self):
1399 def repo_name(self):
1400 return self._repo_name
1400 return self._repo_name
1401
1401
1402 @repo_name.setter
1402 @repo_name.setter
1403 def repo_name(self, value):
1403 def repo_name(self, value):
1404 self._repo_name = value
1404 self._repo_name = value
1405 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1405 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1406
1406
1407 @classmethod
1407 @classmethod
1408 def normalize_repo_name(cls, repo_name):
1408 def normalize_repo_name(cls, repo_name):
1409 """
1409 """
1410 Normalizes os specific repo_name to the format internally stored inside
1410 Normalizes os specific repo_name to the format internally stored inside
1411 database using URL_SEP
1411 database using URL_SEP
1412
1412
1413 :param cls:
1413 :param cls:
1414 :param repo_name:
1414 :param repo_name:
1415 """
1415 """
1416 return cls.NAME_SEP.join(repo_name.split(os.sep))
1416 return cls.NAME_SEP.join(repo_name.split(os.sep))
1417
1417
1418 @classmethod
1418 @classmethod
1419 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1419 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1420 session = Session()
1420 session = Session()
1421 q = session.query(cls).filter(cls.repo_name == repo_name)
1421 q = session.query(cls).filter(cls.repo_name == repo_name)
1422
1422
1423 if cache:
1423 if cache:
1424 if identity_cache:
1424 if identity_cache:
1425 val = cls.identity_cache(session, 'repo_name', repo_name)
1425 val = cls.identity_cache(session, 'repo_name', repo_name)
1426 if val:
1426 if val:
1427 return val
1427 return val
1428 else:
1428 else:
1429 q = q.options(
1429 q = q.options(
1430 FromCache("sql_cache_short",
1430 FromCache("sql_cache_short",
1431 "get_repo_by_name_%s" % _hash_key(repo_name)))
1431 "get_repo_by_name_%s" % _hash_key(repo_name)))
1432
1432
1433 return q.scalar()
1433 return q.scalar()
1434
1434
1435 @classmethod
1435 @classmethod
1436 def get_by_full_path(cls, repo_full_path):
1436 def get_by_full_path(cls, repo_full_path):
1437 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1437 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1438 repo_name = cls.normalize_repo_name(repo_name)
1438 repo_name = cls.normalize_repo_name(repo_name)
1439 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1439 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1440
1440
1441 @classmethod
1441 @classmethod
1442 def get_repo_forks(cls, repo_id):
1442 def get_repo_forks(cls, repo_id):
1443 return cls.query().filter(Repository.fork_id == repo_id)
1443 return cls.query().filter(Repository.fork_id == repo_id)
1444
1444
1445 @classmethod
1445 @classmethod
1446 def base_path(cls):
1446 def base_path(cls):
1447 """
1447 """
1448 Returns base path when all repos are stored
1448 Returns base path when all repos are stored
1449
1449
1450 :param cls:
1450 :param cls:
1451 """
1451 """
1452 q = Session().query(RhodeCodeUi)\
1452 q = Session().query(RhodeCodeUi)\
1453 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1453 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1454 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1454 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1455 return q.one().ui_value
1455 return q.one().ui_value
1456
1456
1457 @classmethod
1457 @classmethod
1458 def is_valid(cls, repo_name):
1458 def is_valid(cls, repo_name):
1459 """
1459 """
1460 returns True if given repo name is a valid filesystem repository
1460 returns True if given repo name is a valid filesystem repository
1461
1461
1462 :param cls:
1462 :param cls:
1463 :param repo_name:
1463 :param repo_name:
1464 """
1464 """
1465 from rhodecode.lib.utils import is_valid_repo
1465 from rhodecode.lib.utils import is_valid_repo
1466
1466
1467 return is_valid_repo(repo_name, cls.base_path())
1467 return is_valid_repo(repo_name, cls.base_path())
1468
1468
1469 @classmethod
1469 @classmethod
1470 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1470 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1471 case_insensitive=True):
1471 case_insensitive=True):
1472 q = Repository.query()
1472 q = Repository.query()
1473
1473
1474 if not isinstance(user_id, Optional):
1474 if not isinstance(user_id, Optional):
1475 q = q.filter(Repository.user_id == user_id)
1475 q = q.filter(Repository.user_id == user_id)
1476
1476
1477 if not isinstance(group_id, Optional):
1477 if not isinstance(group_id, Optional):
1478 q = q.filter(Repository.group_id == group_id)
1478 q = q.filter(Repository.group_id == group_id)
1479
1479
1480 if case_insensitive:
1480 if case_insensitive:
1481 q = q.order_by(func.lower(Repository.repo_name))
1481 q = q.order_by(func.lower(Repository.repo_name))
1482 else:
1482 else:
1483 q = q.order_by(Repository.repo_name)
1483 q = q.order_by(Repository.repo_name)
1484 return q.all()
1484 return q.all()
1485
1485
1486 @property
1486 @property
1487 def forks(self):
1487 def forks(self):
1488 """
1488 """
1489 Return forks of this repo
1489 Return forks of this repo
1490 """
1490 """
1491 return Repository.get_repo_forks(self.repo_id)
1491 return Repository.get_repo_forks(self.repo_id)
1492
1492
1493 @property
1493 @property
1494 def parent(self):
1494 def parent(self):
1495 """
1495 """
1496 Returns fork parent
1496 Returns fork parent
1497 """
1497 """
1498 return self.fork
1498 return self.fork
1499
1499
1500 @property
1500 @property
1501 def just_name(self):
1501 def just_name(self):
1502 return self.repo_name.split(self.NAME_SEP)[-1]
1502 return self.repo_name.split(self.NAME_SEP)[-1]
1503
1503
1504 @property
1504 @property
1505 def groups_with_parents(self):
1505 def groups_with_parents(self):
1506 groups = []
1506 groups = []
1507 if self.group is None:
1507 if self.group is None:
1508 return groups
1508 return groups
1509
1509
1510 cur_gr = self.group
1510 cur_gr = self.group
1511 groups.insert(0, cur_gr)
1511 groups.insert(0, cur_gr)
1512 while 1:
1512 while 1:
1513 gr = getattr(cur_gr, 'parent_group', None)
1513 gr = getattr(cur_gr, 'parent_group', None)
1514 cur_gr = cur_gr.parent_group
1514 cur_gr = cur_gr.parent_group
1515 if gr is None:
1515 if gr is None:
1516 break
1516 break
1517 groups.insert(0, gr)
1517 groups.insert(0, gr)
1518
1518
1519 return groups
1519 return groups
1520
1520
1521 @property
1521 @property
1522 def groups_and_repo(self):
1522 def groups_and_repo(self):
1523 return self.groups_with_parents, self
1523 return self.groups_with_parents, self
1524
1524
1525 @LazyProperty
1525 @LazyProperty
1526 def repo_path(self):
1526 def repo_path(self):
1527 """
1527 """
1528 Returns base full path for that repository means where it actually
1528 Returns base full path for that repository means where it actually
1529 exists on a filesystem
1529 exists on a filesystem
1530 """
1530 """
1531 q = Session().query(RhodeCodeUi).filter(
1531 q = Session().query(RhodeCodeUi).filter(
1532 RhodeCodeUi.ui_key == self.NAME_SEP)
1532 RhodeCodeUi.ui_key == self.NAME_SEP)
1533 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1533 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1534 return q.one().ui_value
1534 return q.one().ui_value
1535
1535
1536 @property
1536 @property
1537 def repo_full_path(self):
1537 def repo_full_path(self):
1538 p = [self.repo_path]
1538 p = [self.repo_path]
1539 # we need to split the name by / since this is how we store the
1539 # we need to split the name by / since this is how we store the
1540 # names in the database, but that eventually needs to be converted
1540 # names in the database, but that eventually needs to be converted
1541 # into a valid system path
1541 # into a valid system path
1542 p += self.repo_name.split(self.NAME_SEP)
1542 p += self.repo_name.split(self.NAME_SEP)
1543 return os.path.join(*map(safe_unicode, p))
1543 return os.path.join(*map(safe_unicode, p))
1544
1544
1545 @property
1545 @property
1546 def cache_keys(self):
1546 def cache_keys(self):
1547 """
1547 """
1548 Returns associated cache keys for that repo
1548 Returns associated cache keys for that repo
1549 """
1549 """
1550 return CacheKey.query()\
1550 return CacheKey.query()\
1551 .filter(CacheKey.cache_args == self.repo_name)\
1551 .filter(CacheKey.cache_args == self.repo_name)\
1552 .order_by(CacheKey.cache_key)\
1552 .order_by(CacheKey.cache_key)\
1553 .all()
1553 .all()
1554
1554
1555 def get_new_name(self, repo_name):
1555 def get_new_name(self, repo_name):
1556 """
1556 """
1557 returns new full repository name based on assigned group and new new
1557 returns new full repository name based on assigned group and new new
1558
1558
1559 :param group_name:
1559 :param group_name:
1560 """
1560 """
1561 path_prefix = self.group.full_path_splitted if self.group else []
1561 path_prefix = self.group.full_path_splitted if self.group else []
1562 return self.NAME_SEP.join(path_prefix + [repo_name])
1562 return self.NAME_SEP.join(path_prefix + [repo_name])
1563
1563
1564 @property
1564 @property
1565 def _config(self):
1565 def _config(self):
1566 """
1566 """
1567 Returns db based config object.
1567 Returns db based config object.
1568 """
1568 """
1569 from rhodecode.lib.utils import make_db_config
1569 from rhodecode.lib.utils import make_db_config
1570 return make_db_config(clear_session=False, repo=self)
1570 return make_db_config(clear_session=False, repo=self)
1571
1571
1572 def permissions(self, with_admins=True, with_owner=True):
1572 def permissions(self, with_admins=True, with_owner=True):
1573 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1573 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1574 q = q.options(joinedload(UserRepoToPerm.repository),
1574 q = q.options(joinedload(UserRepoToPerm.repository),
1575 joinedload(UserRepoToPerm.user),
1575 joinedload(UserRepoToPerm.user),
1576 joinedload(UserRepoToPerm.permission),)
1576 joinedload(UserRepoToPerm.permission),)
1577
1577
1578 # get owners and admins and permissions. We do a trick of re-writing
1578 # get owners and admins and permissions. We do a trick of re-writing
1579 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1579 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1580 # has a global reference and changing one object propagates to all
1580 # has a global reference and changing one object propagates to all
1581 # others. This means if admin is also an owner admin_row that change
1581 # others. This means if admin is also an owner admin_row that change
1582 # would propagate to both objects
1582 # would propagate to both objects
1583 perm_rows = []
1583 perm_rows = []
1584 for _usr in q.all():
1584 for _usr in q.all():
1585 usr = AttributeDict(_usr.user.get_dict())
1585 usr = AttributeDict(_usr.user.get_dict())
1586 usr.permission = _usr.permission.permission_name
1586 usr.permission = _usr.permission.permission_name
1587 perm_rows.append(usr)
1587 perm_rows.append(usr)
1588
1588
1589 # filter the perm rows by 'default' first and then sort them by
1589 # filter the perm rows by 'default' first and then sort them by
1590 # admin,write,read,none permissions sorted again alphabetically in
1590 # admin,write,read,none permissions sorted again alphabetically in
1591 # each group
1591 # each group
1592 perm_rows = sorted(perm_rows, key=display_sort)
1592 perm_rows = sorted(perm_rows, key=display_sort)
1593
1593
1594 _admin_perm = 'repository.admin'
1594 _admin_perm = 'repository.admin'
1595 owner_row = []
1595 owner_row = []
1596 if with_owner:
1596 if with_owner:
1597 usr = AttributeDict(self.user.get_dict())
1597 usr = AttributeDict(self.user.get_dict())
1598 usr.owner_row = True
1598 usr.owner_row = True
1599 usr.permission = _admin_perm
1599 usr.permission = _admin_perm
1600 owner_row.append(usr)
1600 owner_row.append(usr)
1601
1601
1602 super_admin_rows = []
1602 super_admin_rows = []
1603 if with_admins:
1603 if with_admins:
1604 for usr in User.get_all_super_admins():
1604 for usr in User.get_all_super_admins():
1605 # if this admin is also owner, don't double the record
1605 # if this admin is also owner, don't double the record
1606 if usr.user_id == owner_row[0].user_id:
1606 if usr.user_id == owner_row[0].user_id:
1607 owner_row[0].admin_row = True
1607 owner_row[0].admin_row = True
1608 else:
1608 else:
1609 usr = AttributeDict(usr.get_dict())
1609 usr = AttributeDict(usr.get_dict())
1610 usr.admin_row = True
1610 usr.admin_row = True
1611 usr.permission = _admin_perm
1611 usr.permission = _admin_perm
1612 super_admin_rows.append(usr)
1612 super_admin_rows.append(usr)
1613
1613
1614 return super_admin_rows + owner_row + perm_rows
1614 return super_admin_rows + owner_row + perm_rows
1615
1615
1616 def permission_user_groups(self):
1616 def permission_user_groups(self):
1617 q = UserGroupRepoToPerm.query().filter(
1617 q = UserGroupRepoToPerm.query().filter(
1618 UserGroupRepoToPerm.repository == self)
1618 UserGroupRepoToPerm.repository == self)
1619 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1619 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1620 joinedload(UserGroupRepoToPerm.users_group),
1620 joinedload(UserGroupRepoToPerm.users_group),
1621 joinedload(UserGroupRepoToPerm.permission),)
1621 joinedload(UserGroupRepoToPerm.permission),)
1622
1622
1623 perm_rows = []
1623 perm_rows = []
1624 for _user_group in q.all():
1624 for _user_group in q.all():
1625 usr = AttributeDict(_user_group.users_group.get_dict())
1625 usr = AttributeDict(_user_group.users_group.get_dict())
1626 usr.permission = _user_group.permission.permission_name
1626 usr.permission = _user_group.permission.permission_name
1627 perm_rows.append(usr)
1627 perm_rows.append(usr)
1628
1628
1629 return perm_rows
1629 return perm_rows
1630
1630
1631 def get_api_data(self, include_secrets=False):
1631 def get_api_data(self, include_secrets=False):
1632 """
1632 """
1633 Common function for generating repo api data
1633 Common function for generating repo api data
1634
1634
1635 :param include_secrets: See :meth:`User.get_api_data`.
1635 :param include_secrets: See :meth:`User.get_api_data`.
1636
1636
1637 """
1637 """
1638 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1638 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1639 # move this methods on models level.
1639 # move this methods on models level.
1640 from rhodecode.model.settings import SettingsModel
1640 from rhodecode.model.settings import SettingsModel
1641
1641
1642 repo = self
1642 repo = self
1643 _user_id, _time, _reason = self.locked
1643 _user_id, _time, _reason = self.locked
1644
1644
1645 data = {
1645 data = {
1646 'repo_id': repo.repo_id,
1646 'repo_id': repo.repo_id,
1647 'repo_name': repo.repo_name,
1647 'repo_name': repo.repo_name,
1648 'repo_type': repo.repo_type,
1648 'repo_type': repo.repo_type,
1649 'clone_uri': repo.clone_uri or '',
1649 'clone_uri': repo.clone_uri or '',
1650 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1650 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1651 'private': repo.private,
1651 'private': repo.private,
1652 'created_on': repo.created_on,
1652 'created_on': repo.created_on,
1653 'description': repo.description,
1653 'description': repo.description,
1654 'landing_rev': repo.landing_rev,
1654 'landing_rev': repo.landing_rev,
1655 'owner': repo.user.username,
1655 'owner': repo.user.username,
1656 'fork_of': repo.fork.repo_name if repo.fork else None,
1656 'fork_of': repo.fork.repo_name if repo.fork else None,
1657 'enable_statistics': repo.enable_statistics,
1657 'enable_statistics': repo.enable_statistics,
1658 'enable_locking': repo.enable_locking,
1658 'enable_locking': repo.enable_locking,
1659 'enable_downloads': repo.enable_downloads,
1659 'enable_downloads': repo.enable_downloads,
1660 'last_changeset': repo.changeset_cache,
1660 'last_changeset': repo.changeset_cache,
1661 'locked_by': User.get(_user_id).get_api_data(
1661 'locked_by': User.get(_user_id).get_api_data(
1662 include_secrets=include_secrets) if _user_id else None,
1662 include_secrets=include_secrets) if _user_id else None,
1663 'locked_date': time_to_datetime(_time) if _time else None,
1663 'locked_date': time_to_datetime(_time) if _time else None,
1664 'lock_reason': _reason if _reason else None,
1664 'lock_reason': _reason if _reason else None,
1665 }
1665 }
1666
1666
1667 # TODO: mikhail: should be per-repo settings here
1667 # TODO: mikhail: should be per-repo settings here
1668 rc_config = SettingsModel().get_all_settings()
1668 rc_config = SettingsModel().get_all_settings()
1669 repository_fields = str2bool(
1669 repository_fields = str2bool(
1670 rc_config.get('rhodecode_repository_fields'))
1670 rc_config.get('rhodecode_repository_fields'))
1671 if repository_fields:
1671 if repository_fields:
1672 for f in self.extra_fields:
1672 for f in self.extra_fields:
1673 data[f.field_key_prefixed] = f.field_value
1673 data[f.field_key_prefixed] = f.field_value
1674
1674
1675 return data
1675 return data
1676
1676
1677 @classmethod
1677 @classmethod
1678 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1678 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1679 if not lock_time:
1679 if not lock_time:
1680 lock_time = time.time()
1680 lock_time = time.time()
1681 if not lock_reason:
1681 if not lock_reason:
1682 lock_reason = cls.LOCK_AUTOMATIC
1682 lock_reason = cls.LOCK_AUTOMATIC
1683 repo.locked = [user_id, lock_time, lock_reason]
1683 repo.locked = [user_id, lock_time, lock_reason]
1684 Session().add(repo)
1684 Session().add(repo)
1685 Session().commit()
1685 Session().commit()
1686
1686
1687 @classmethod
1687 @classmethod
1688 def unlock(cls, repo):
1688 def unlock(cls, repo):
1689 repo.locked = None
1689 repo.locked = None
1690 Session().add(repo)
1690 Session().add(repo)
1691 Session().commit()
1691 Session().commit()
1692
1692
1693 @classmethod
1693 @classmethod
1694 def getlock(cls, repo):
1694 def getlock(cls, repo):
1695 return repo.locked
1695 return repo.locked
1696
1696
1697 def is_user_lock(self, user_id):
1697 def is_user_lock(self, user_id):
1698 if self.lock[0]:
1698 if self.lock[0]:
1699 lock_user_id = safe_int(self.lock[0])
1699 lock_user_id = safe_int(self.lock[0])
1700 user_id = safe_int(user_id)
1700 user_id = safe_int(user_id)
1701 # both are ints, and they are equal
1701 # both are ints, and they are equal
1702 return all([lock_user_id, user_id]) and lock_user_id == user_id
1702 return all([lock_user_id, user_id]) and lock_user_id == user_id
1703
1703
1704 return False
1704 return False
1705
1705
1706 def get_locking_state(self, action, user_id, only_when_enabled=True):
1706 def get_locking_state(self, action, user_id, only_when_enabled=True):
1707 """
1707 """
1708 Checks locking on this repository, if locking is enabled and lock is
1708 Checks locking on this repository, if locking is enabled and lock is
1709 present returns a tuple of make_lock, locked, locked_by.
1709 present returns a tuple of make_lock, locked, locked_by.
1710 make_lock can have 3 states None (do nothing) True, make lock
1710 make_lock can have 3 states None (do nothing) True, make lock
1711 False release lock, This value is later propagated to hooks, which
1711 False release lock, This value is later propagated to hooks, which
1712 do the locking. Think about this as signals passed to hooks what to do.
1712 do the locking. Think about this as signals passed to hooks what to do.
1713
1713
1714 """
1714 """
1715 # TODO: johbo: This is part of the business logic and should be moved
1715 # TODO: johbo: This is part of the business logic and should be moved
1716 # into the RepositoryModel.
1716 # into the RepositoryModel.
1717
1717
1718 if action not in ('push', 'pull'):
1718 if action not in ('push', 'pull'):
1719 raise ValueError("Invalid action value: %s" % repr(action))
1719 raise ValueError("Invalid action value: %s" % repr(action))
1720
1720
1721 # defines if locked error should be thrown to user
1721 # defines if locked error should be thrown to user
1722 currently_locked = False
1722 currently_locked = False
1723 # defines if new lock should be made, tri-state
1723 # defines if new lock should be made, tri-state
1724 make_lock = None
1724 make_lock = None
1725 repo = self
1725 repo = self
1726 user = User.get(user_id)
1726 user = User.get(user_id)
1727
1727
1728 lock_info = repo.locked
1728 lock_info = repo.locked
1729
1729
1730 if repo and (repo.enable_locking or not only_when_enabled):
1730 if repo and (repo.enable_locking or not only_when_enabled):
1731 if action == 'push':
1731 if action == 'push':
1732 # check if it's already locked !, if it is compare users
1732 # check if it's already locked !, if it is compare users
1733 locked_by_user_id = lock_info[0]
1733 locked_by_user_id = lock_info[0]
1734 if user.user_id == locked_by_user_id:
1734 if user.user_id == locked_by_user_id:
1735 log.debug(
1735 log.debug(
1736 'Got `push` action from user %s, now unlocking', user)
1736 'Got `push` action from user %s, now unlocking', user)
1737 # unlock if we have push from user who locked
1737 # unlock if we have push from user who locked
1738 make_lock = False
1738 make_lock = False
1739 else:
1739 else:
1740 # we're not the same user who locked, ban with
1740 # we're not the same user who locked, ban with
1741 # code defined in settings (default is 423 HTTP Locked) !
1741 # code defined in settings (default is 423 HTTP Locked) !
1742 log.debug('Repo %s is currently locked by %s', repo, user)
1742 log.debug('Repo %s is currently locked by %s', repo, user)
1743 currently_locked = True
1743 currently_locked = True
1744 elif action == 'pull':
1744 elif action == 'pull':
1745 # [0] user [1] date
1745 # [0] user [1] date
1746 if lock_info[0] and lock_info[1]:
1746 if lock_info[0] and lock_info[1]:
1747 log.debug('Repo %s is currently locked by %s', repo, user)
1747 log.debug('Repo %s is currently locked by %s', repo, user)
1748 currently_locked = True
1748 currently_locked = True
1749 else:
1749 else:
1750 log.debug('Setting lock on repo %s by %s', repo, user)
1750 log.debug('Setting lock on repo %s by %s', repo, user)
1751 make_lock = True
1751 make_lock = True
1752
1752
1753 else:
1753 else:
1754 log.debug('Repository %s do not have locking enabled', repo)
1754 log.debug('Repository %s do not have locking enabled', repo)
1755
1755
1756 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1756 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1757 make_lock, currently_locked, lock_info)
1757 make_lock, currently_locked, lock_info)
1758
1758
1759 from rhodecode.lib.auth import HasRepoPermissionAny
1759 from rhodecode.lib.auth import HasRepoPermissionAny
1760 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1760 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1761 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1761 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1762 # if we don't have at least write permission we cannot make a lock
1762 # if we don't have at least write permission we cannot make a lock
1763 log.debug('lock state reset back to FALSE due to lack '
1763 log.debug('lock state reset back to FALSE due to lack '
1764 'of at least read permission')
1764 'of at least read permission')
1765 make_lock = False
1765 make_lock = False
1766
1766
1767 return make_lock, currently_locked, lock_info
1767 return make_lock, currently_locked, lock_info
1768
1768
1769 @property
1769 @property
1770 def last_db_change(self):
1770 def last_db_change(self):
1771 return self.updated_on
1771 return self.updated_on
1772
1772
1773 @property
1773 @property
1774 def clone_uri_hidden(self):
1774 def clone_uri_hidden(self):
1775 clone_uri = self.clone_uri
1775 clone_uri = self.clone_uri
1776 if clone_uri:
1776 if clone_uri:
1777 import urlobject
1777 import urlobject
1778 url_obj = urlobject.URLObject(clone_uri)
1778 url_obj = urlobject.URLObject(clone_uri)
1779 if url_obj.password:
1779 if url_obj.password:
1780 clone_uri = url_obj.with_password('*****')
1780 clone_uri = url_obj.with_password('*****')
1781 return clone_uri
1781 return clone_uri
1782
1782
1783 def clone_url(self, **override):
1783 def clone_url(self, **override):
1784 qualified_home_url = url('home', qualified=True)
1784 qualified_home_url = url('home', qualified=True)
1785
1785
1786 uri_tmpl = None
1786 uri_tmpl = None
1787 if 'with_id' in override:
1787 if 'with_id' in override:
1788 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1788 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1789 del override['with_id']
1789 del override['with_id']
1790
1790
1791 if 'uri_tmpl' in override:
1791 if 'uri_tmpl' in override:
1792 uri_tmpl = override['uri_tmpl']
1792 uri_tmpl = override['uri_tmpl']
1793 del override['uri_tmpl']
1793 del override['uri_tmpl']
1794
1794
1795 # we didn't override our tmpl from **overrides
1795 # we didn't override our tmpl from **overrides
1796 if not uri_tmpl:
1796 if not uri_tmpl:
1797 uri_tmpl = self.DEFAULT_CLONE_URI
1797 uri_tmpl = self.DEFAULT_CLONE_URI
1798 try:
1798 try:
1799 from pylons import tmpl_context as c
1799 from pylons import tmpl_context as c
1800 uri_tmpl = c.clone_uri_tmpl
1800 uri_tmpl = c.clone_uri_tmpl
1801 except Exception:
1801 except Exception:
1802 # in any case if we call this outside of request context,
1802 # in any case if we call this outside of request context,
1803 # ie, not having tmpl_context set up
1803 # ie, not having tmpl_context set up
1804 pass
1804 pass
1805
1805
1806 return get_clone_url(uri_tmpl=uri_tmpl,
1806 return get_clone_url(uri_tmpl=uri_tmpl,
1807 qualifed_home_url=qualified_home_url,
1807 qualifed_home_url=qualified_home_url,
1808 repo_name=self.repo_name,
1808 repo_name=self.repo_name,
1809 repo_id=self.repo_id, **override)
1809 repo_id=self.repo_id, **override)
1810
1810
1811 def set_state(self, state):
1811 def set_state(self, state):
1812 self.repo_state = state
1812 self.repo_state = state
1813 Session().add(self)
1813 Session().add(self)
1814 #==========================================================================
1814 #==========================================================================
1815 # SCM PROPERTIES
1815 # SCM PROPERTIES
1816 #==========================================================================
1816 #==========================================================================
1817
1817
1818 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1818 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1819 return get_commit_safe(
1819 return get_commit_safe(
1820 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1820 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1821
1821
1822 def get_changeset(self, rev=None, pre_load=None):
1822 def get_changeset(self, rev=None, pre_load=None):
1823 warnings.warn("Use get_commit", DeprecationWarning)
1823 warnings.warn("Use get_commit", DeprecationWarning)
1824 commit_id = None
1824 commit_id = None
1825 commit_idx = None
1825 commit_idx = None
1826 if isinstance(rev, basestring):
1826 if isinstance(rev, basestring):
1827 commit_id = rev
1827 commit_id = rev
1828 else:
1828 else:
1829 commit_idx = rev
1829 commit_idx = rev
1830 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1830 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1831 pre_load=pre_load)
1831 pre_load=pre_load)
1832
1832
1833 def get_landing_commit(self):
1833 def get_landing_commit(self):
1834 """
1834 """
1835 Returns landing commit, or if that doesn't exist returns the tip
1835 Returns landing commit, or if that doesn't exist returns the tip
1836 """
1836 """
1837 _rev_type, _rev = self.landing_rev
1837 _rev_type, _rev = self.landing_rev
1838 commit = self.get_commit(_rev)
1838 commit = self.get_commit(_rev)
1839 if isinstance(commit, EmptyCommit):
1839 if isinstance(commit, EmptyCommit):
1840 return self.get_commit()
1840 return self.get_commit()
1841 return commit
1841 return commit
1842
1842
1843 def update_commit_cache(self, cs_cache=None, config=None):
1843 def update_commit_cache(self, cs_cache=None, config=None):
1844 """
1844 """
1845 Update cache of last changeset for repository, keys should be::
1845 Update cache of last changeset for repository, keys should be::
1846
1846
1847 short_id
1847 short_id
1848 raw_id
1848 raw_id
1849 revision
1849 revision
1850 parents
1850 parents
1851 message
1851 message
1852 date
1852 date
1853 author
1853 author
1854
1854
1855 :param cs_cache:
1855 :param cs_cache:
1856 """
1856 """
1857 from rhodecode.lib.vcs.backends.base import BaseChangeset
1857 from rhodecode.lib.vcs.backends.base import BaseChangeset
1858 if cs_cache is None:
1858 if cs_cache is None:
1859 # use no-cache version here
1859 # use no-cache version here
1860 scm_repo = self.scm_instance(cache=False, config=config)
1860 scm_repo = self.scm_instance(cache=False, config=config)
1861 if scm_repo:
1861 if scm_repo:
1862 cs_cache = scm_repo.get_commit(
1862 cs_cache = scm_repo.get_commit(
1863 pre_load=["author", "date", "message", "parents"])
1863 pre_load=["author", "date", "message", "parents"])
1864 else:
1864 else:
1865 cs_cache = EmptyCommit()
1865 cs_cache = EmptyCommit()
1866
1866
1867 if isinstance(cs_cache, BaseChangeset):
1867 if isinstance(cs_cache, BaseChangeset):
1868 cs_cache = cs_cache.__json__()
1868 cs_cache = cs_cache.__json__()
1869
1869
1870 def is_outdated(new_cs_cache):
1870 def is_outdated(new_cs_cache):
1871 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1871 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1872 new_cs_cache['revision'] != self.changeset_cache['revision']):
1872 new_cs_cache['revision'] != self.changeset_cache['revision']):
1873 return True
1873 return True
1874 return False
1874 return False
1875
1875
1876 # check if we have maybe already latest cached revision
1876 # check if we have maybe already latest cached revision
1877 if is_outdated(cs_cache) or not self.changeset_cache:
1877 if is_outdated(cs_cache) or not self.changeset_cache:
1878 _default = datetime.datetime.fromtimestamp(0)
1878 _default = datetime.datetime.fromtimestamp(0)
1879 last_change = cs_cache.get('date') or _default
1879 last_change = cs_cache.get('date') or _default
1880 log.debug('updated repo %s with new cs cache %s',
1880 log.debug('updated repo %s with new cs cache %s',
1881 self.repo_name, cs_cache)
1881 self.repo_name, cs_cache)
1882 self.updated_on = last_change
1882 self.updated_on = last_change
1883 self.changeset_cache = cs_cache
1883 self.changeset_cache = cs_cache
1884 Session().add(self)
1884 Session().add(self)
1885 Session().commit()
1885 Session().commit()
1886 else:
1886 else:
1887 log.debug('Skipping update_commit_cache for repo:`%s` '
1887 log.debug('Skipping update_commit_cache for repo:`%s` '
1888 'commit already with latest changes', self.repo_name)
1888 'commit already with latest changes', self.repo_name)
1889
1889
1890 @property
1890 @property
1891 def tip(self):
1891 def tip(self):
1892 return self.get_commit('tip')
1892 return self.get_commit('tip')
1893
1893
1894 @property
1894 @property
1895 def author(self):
1895 def author(self):
1896 return self.tip.author
1896 return self.tip.author
1897
1897
1898 @property
1898 @property
1899 def last_change(self):
1899 def last_change(self):
1900 return self.scm_instance().last_change
1900 return self.scm_instance().last_change
1901
1901
1902 def get_comments(self, revisions=None):
1902 def get_comments(self, revisions=None):
1903 """
1903 """
1904 Returns comments for this repository grouped by revisions
1904 Returns comments for this repository grouped by revisions
1905
1905
1906 :param revisions: filter query by revisions only
1906 :param revisions: filter query by revisions only
1907 """
1907 """
1908 cmts = ChangesetComment.query()\
1908 cmts = ChangesetComment.query()\
1909 .filter(ChangesetComment.repo == self)
1909 .filter(ChangesetComment.repo == self)
1910 if revisions:
1910 if revisions:
1911 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1911 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1912 grouped = collections.defaultdict(list)
1912 grouped = collections.defaultdict(list)
1913 for cmt in cmts.all():
1913 for cmt in cmts.all():
1914 grouped[cmt.revision].append(cmt)
1914 grouped[cmt.revision].append(cmt)
1915 return grouped
1915 return grouped
1916
1916
1917 def statuses(self, revisions=None):
1917 def statuses(self, revisions=None):
1918 """
1918 """
1919 Returns statuses for this repository
1919 Returns statuses for this repository
1920
1920
1921 :param revisions: list of revisions to get statuses for
1921 :param revisions: list of revisions to get statuses for
1922 """
1922 """
1923 statuses = ChangesetStatus.query()\
1923 statuses = ChangesetStatus.query()\
1924 .filter(ChangesetStatus.repo == self)\
1924 .filter(ChangesetStatus.repo == self)\
1925 .filter(ChangesetStatus.version == 0)
1925 .filter(ChangesetStatus.version == 0)
1926
1926
1927 if revisions:
1927 if revisions:
1928 # Try doing the filtering in chunks to avoid hitting limits
1928 # Try doing the filtering in chunks to avoid hitting limits
1929 size = 500
1929 size = 500
1930 status_results = []
1930 status_results = []
1931 for chunk in xrange(0, len(revisions), size):
1931 for chunk in xrange(0, len(revisions), size):
1932 status_results += statuses.filter(
1932 status_results += statuses.filter(
1933 ChangesetStatus.revision.in_(
1933 ChangesetStatus.revision.in_(
1934 revisions[chunk: chunk+size])
1934 revisions[chunk: chunk+size])
1935 ).all()
1935 ).all()
1936 else:
1936 else:
1937 status_results = statuses.all()
1937 status_results = statuses.all()
1938
1938
1939 grouped = {}
1939 grouped = {}
1940
1940
1941 # maybe we have open new pullrequest without a status?
1941 # maybe we have open new pullrequest without a status?
1942 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1942 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1943 status_lbl = ChangesetStatus.get_status_lbl(stat)
1943 status_lbl = ChangesetStatus.get_status_lbl(stat)
1944 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1944 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1945 for rev in pr.revisions:
1945 for rev in pr.revisions:
1946 pr_id = pr.pull_request_id
1946 pr_id = pr.pull_request_id
1947 pr_repo = pr.target_repo.repo_name
1947 pr_repo = pr.target_repo.repo_name
1948 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1948 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1949
1949
1950 for stat in status_results:
1950 for stat in status_results:
1951 pr_id = pr_repo = None
1951 pr_id = pr_repo = None
1952 if stat.pull_request:
1952 if stat.pull_request:
1953 pr_id = stat.pull_request.pull_request_id
1953 pr_id = stat.pull_request.pull_request_id
1954 pr_repo = stat.pull_request.target_repo.repo_name
1954 pr_repo = stat.pull_request.target_repo.repo_name
1955 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1955 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1956 pr_id, pr_repo]
1956 pr_id, pr_repo]
1957 return grouped
1957 return grouped
1958
1958
1959 # ==========================================================================
1959 # ==========================================================================
1960 # SCM CACHE INSTANCE
1960 # SCM CACHE INSTANCE
1961 # ==========================================================================
1961 # ==========================================================================
1962
1962
1963 def scm_instance(self, **kwargs):
1963 def scm_instance(self, **kwargs):
1964 import rhodecode
1964 import rhodecode
1965
1965
1966 # Passing a config will not hit the cache currently only used
1966 # Passing a config will not hit the cache currently only used
1967 # for repo2dbmapper
1967 # for repo2dbmapper
1968 config = kwargs.pop('config', None)
1968 config = kwargs.pop('config', None)
1969 cache = kwargs.pop('cache', None)
1969 cache = kwargs.pop('cache', None)
1970 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1970 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1971 # if cache is NOT defined use default global, else we have a full
1971 # if cache is NOT defined use default global, else we have a full
1972 # control over cache behaviour
1972 # control over cache behaviour
1973 if cache is None and full_cache and not config:
1973 if cache is None and full_cache and not config:
1974 return self._get_instance_cached()
1974 return self._get_instance_cached()
1975 return self._get_instance(cache=bool(cache), config=config)
1975 return self._get_instance(cache=bool(cache), config=config)
1976
1976
1977 def _get_instance_cached(self):
1977 def _get_instance_cached(self):
1978 @cache_region('long_term')
1978 @cache_region('long_term')
1979 def _get_repo(cache_key):
1979 def _get_repo(cache_key):
1980 return self._get_instance()
1980 return self._get_instance()
1981
1981
1982 invalidator_context = CacheKey.repo_context_cache(
1982 invalidator_context = CacheKey.repo_context_cache(
1983 _get_repo, self.repo_name, None, thread_scoped=True)
1983 _get_repo, self.repo_name, None, thread_scoped=True)
1984
1984
1985 with invalidator_context as context:
1985 with invalidator_context as context:
1986 context.invalidate()
1986 context.invalidate()
1987 repo = context.compute()
1987 repo = context.compute()
1988
1988
1989 return repo
1989 return repo
1990
1990
1991 def _get_instance(self, cache=True, config=None):
1991 def _get_instance(self, cache=True, config=None):
1992 config = config or self._config
1992 config = config or self._config
1993 custom_wire = {
1993 custom_wire = {
1994 'cache': cache # controls the vcs.remote cache
1994 'cache': cache # controls the vcs.remote cache
1995 }
1995 }
1996 repo = get_vcs_instance(
1996 repo = get_vcs_instance(
1997 repo_path=safe_str(self.repo_full_path),
1997 repo_path=safe_str(self.repo_full_path),
1998 config=config,
1998 config=config,
1999 with_wire=custom_wire,
1999 with_wire=custom_wire,
2000 create=False,
2000 create=False,
2001 _vcs_alias=self.repo_type)
2001 _vcs_alias=self.repo_type)
2002
2002
2003 return repo
2003 return repo
2004
2004
2005 def __json__(self):
2005 def __json__(self):
2006 return {'landing_rev': self.landing_rev}
2006 return {'landing_rev': self.landing_rev}
2007
2007
2008 def get_dict(self):
2008 def get_dict(self):
2009
2009
2010 # Since we transformed `repo_name` to a hybrid property, we need to
2010 # Since we transformed `repo_name` to a hybrid property, we need to
2011 # keep compatibility with the code which uses `repo_name` field.
2011 # keep compatibility with the code which uses `repo_name` field.
2012
2012
2013 result = super(Repository, self).get_dict()
2013 result = super(Repository, self).get_dict()
2014 result['repo_name'] = result.pop('_repo_name', None)
2014 result['repo_name'] = result.pop('_repo_name', None)
2015 return result
2015 return result
2016
2016
2017
2017
2018 class RepoGroup(Base, BaseModel):
2018 class RepoGroup(Base, BaseModel):
2019 __tablename__ = 'groups'
2019 __tablename__ = 'groups'
2020 __table_args__ = (
2020 __table_args__ = (
2021 UniqueConstraint('group_name', 'group_parent_id'),
2021 UniqueConstraint('group_name', 'group_parent_id'),
2022 CheckConstraint('group_id != group_parent_id'),
2022 CheckConstraint('group_id != group_parent_id'),
2023 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2023 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2024 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2024 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2025 )
2025 )
2026 __mapper_args__ = {'order_by': 'group_name'}
2026 __mapper_args__ = {'order_by': 'group_name'}
2027
2027
2028 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2028 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2029
2029
2030 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2030 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2031 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2031 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2032 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2032 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2033 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2033 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2034 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2034 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2035 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2035 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2036 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2036 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2037 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2037 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2038
2038
2039 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2039 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2040 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2040 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2041 parent_group = relationship('RepoGroup', remote_side=group_id)
2041 parent_group = relationship('RepoGroup', remote_side=group_id)
2042 user = relationship('User')
2042 user = relationship('User')
2043 integrations = relationship('Integration',
2043 integrations = relationship('Integration',
2044 cascade="all, delete, delete-orphan")
2044 cascade="all, delete, delete-orphan")
2045
2045
2046 def __init__(self, group_name='', parent_group=None):
2046 def __init__(self, group_name='', parent_group=None):
2047 self.group_name = group_name
2047 self.group_name = group_name
2048 self.parent_group = parent_group
2048 self.parent_group = parent_group
2049
2049
2050 def __unicode__(self):
2050 def __unicode__(self):
2051 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2051 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2052 self.group_name)
2052 self.group_name)
2053
2053
2054 @classmethod
2054 @classmethod
2055 def _generate_choice(cls, repo_group):
2055 def _generate_choice(cls, repo_group):
2056 from webhelpers.html import literal as _literal
2056 from webhelpers.html import literal as _literal
2057 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2057 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2058 return repo_group.group_id, _name(repo_group.full_path_splitted)
2058 return repo_group.group_id, _name(repo_group.full_path_splitted)
2059
2059
2060 @classmethod
2060 @classmethod
2061 def groups_choices(cls, groups=None, show_empty_group=True):
2061 def groups_choices(cls, groups=None, show_empty_group=True):
2062 if not groups:
2062 if not groups:
2063 groups = cls.query().all()
2063 groups = cls.query().all()
2064
2064
2065 repo_groups = []
2065 repo_groups = []
2066 if show_empty_group:
2066 if show_empty_group:
2067 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2067 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2068
2068
2069 repo_groups.extend([cls._generate_choice(x) for x in groups])
2069 repo_groups.extend([cls._generate_choice(x) for x in groups])
2070
2070
2071 repo_groups = sorted(
2071 repo_groups = sorted(
2072 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2072 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2073 return repo_groups
2073 return repo_groups
2074
2074
2075 @classmethod
2075 @classmethod
2076 def url_sep(cls):
2076 def url_sep(cls):
2077 return URL_SEP
2077 return URL_SEP
2078
2078
2079 @classmethod
2079 @classmethod
2080 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2080 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2081 if case_insensitive:
2081 if case_insensitive:
2082 gr = cls.query().filter(func.lower(cls.group_name)
2082 gr = cls.query().filter(func.lower(cls.group_name)
2083 == func.lower(group_name))
2083 == func.lower(group_name))
2084 else:
2084 else:
2085 gr = cls.query().filter(cls.group_name == group_name)
2085 gr = cls.query().filter(cls.group_name == group_name)
2086 if cache:
2086 if cache:
2087 gr = gr.options(FromCache(
2087 gr = gr.options(FromCache(
2088 "sql_cache_short",
2088 "sql_cache_short",
2089 "get_group_%s" % _hash_key(group_name)))
2089 "get_group_%s" % _hash_key(group_name)))
2090 return gr.scalar()
2090 return gr.scalar()
2091
2091
2092 @classmethod
2092 @classmethod
2093 def get_user_personal_repo_group(cls, user_id):
2093 def get_user_personal_repo_group(cls, user_id):
2094 user = User.get(user_id)
2094 user = User.get(user_id)
2095 return cls.query()\
2095 return cls.query()\
2096 .filter(cls.personal == true())\
2096 .filter(cls.personal == true())\
2097 .filter(cls.user == user).scalar()
2097 .filter(cls.user == user).scalar()
2098
2098
2099 @classmethod
2099 @classmethod
2100 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2100 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2101 case_insensitive=True):
2101 case_insensitive=True):
2102 q = RepoGroup.query()
2102 q = RepoGroup.query()
2103
2103
2104 if not isinstance(user_id, Optional):
2104 if not isinstance(user_id, Optional):
2105 q = q.filter(RepoGroup.user_id == user_id)
2105 q = q.filter(RepoGroup.user_id == user_id)
2106
2106
2107 if not isinstance(group_id, Optional):
2107 if not isinstance(group_id, Optional):
2108 q = q.filter(RepoGroup.group_parent_id == group_id)
2108 q = q.filter(RepoGroup.group_parent_id == group_id)
2109
2109
2110 if case_insensitive:
2110 if case_insensitive:
2111 q = q.order_by(func.lower(RepoGroup.group_name))
2111 q = q.order_by(func.lower(RepoGroup.group_name))
2112 else:
2112 else:
2113 q = q.order_by(RepoGroup.group_name)
2113 q = q.order_by(RepoGroup.group_name)
2114 return q.all()
2114 return q.all()
2115
2115
2116 @property
2116 @property
2117 def parents(self):
2117 def parents(self):
2118 parents_recursion_limit = 10
2118 parents_recursion_limit = 10
2119 groups = []
2119 groups = []
2120 if self.parent_group is None:
2120 if self.parent_group is None:
2121 return groups
2121 return groups
2122 cur_gr = self.parent_group
2122 cur_gr = self.parent_group
2123 groups.insert(0, cur_gr)
2123 groups.insert(0, cur_gr)
2124 cnt = 0
2124 cnt = 0
2125 while 1:
2125 while 1:
2126 cnt += 1
2126 cnt += 1
2127 gr = getattr(cur_gr, 'parent_group', None)
2127 gr = getattr(cur_gr, 'parent_group', None)
2128 cur_gr = cur_gr.parent_group
2128 cur_gr = cur_gr.parent_group
2129 if gr is None:
2129 if gr is None:
2130 break
2130 break
2131 if cnt == parents_recursion_limit:
2131 if cnt == parents_recursion_limit:
2132 # this will prevent accidental infinit loops
2132 # this will prevent accidental infinit loops
2133 log.error(('more than %s parents found for group %s, stopping '
2133 log.error(('more than %s parents found for group %s, stopping '
2134 'recursive parent fetching' % (parents_recursion_limit, self)))
2134 'recursive parent fetching' % (parents_recursion_limit, self)))
2135 break
2135 break
2136
2136
2137 groups.insert(0, gr)
2137 groups.insert(0, gr)
2138 return groups
2138 return groups
2139
2139
2140 @property
2140 @property
2141 def children(self):
2141 def children(self):
2142 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2142 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2143
2143
2144 @property
2144 @property
2145 def name(self):
2145 def name(self):
2146 return self.group_name.split(RepoGroup.url_sep())[-1]
2146 return self.group_name.split(RepoGroup.url_sep())[-1]
2147
2147
2148 @property
2148 @property
2149 def full_path(self):
2149 def full_path(self):
2150 return self.group_name
2150 return self.group_name
2151
2151
2152 @property
2152 @property
2153 def full_path_splitted(self):
2153 def full_path_splitted(self):
2154 return self.group_name.split(RepoGroup.url_sep())
2154 return self.group_name.split(RepoGroup.url_sep())
2155
2155
2156 @property
2156 @property
2157 def repositories(self):
2157 def repositories(self):
2158 return Repository.query()\
2158 return Repository.query()\
2159 .filter(Repository.group == self)\
2159 .filter(Repository.group == self)\
2160 .order_by(Repository.repo_name)
2160 .order_by(Repository.repo_name)
2161
2161
2162 @property
2162 @property
2163 def repositories_recursive_count(self):
2163 def repositories_recursive_count(self):
2164 cnt = self.repositories.count()
2164 cnt = self.repositories.count()
2165
2165
2166 def children_count(group):
2166 def children_count(group):
2167 cnt = 0
2167 cnt = 0
2168 for child in group.children:
2168 for child in group.children:
2169 cnt += child.repositories.count()
2169 cnt += child.repositories.count()
2170 cnt += children_count(child)
2170 cnt += children_count(child)
2171 return cnt
2171 return cnt
2172
2172
2173 return cnt + children_count(self)
2173 return cnt + children_count(self)
2174
2174
2175 def _recursive_objects(self, include_repos=True):
2175 def _recursive_objects(self, include_repos=True):
2176 all_ = []
2176 all_ = []
2177
2177
2178 def _get_members(root_gr):
2178 def _get_members(root_gr):
2179 if include_repos:
2179 if include_repos:
2180 for r in root_gr.repositories:
2180 for r in root_gr.repositories:
2181 all_.append(r)
2181 all_.append(r)
2182 childs = root_gr.children.all()
2182 childs = root_gr.children.all()
2183 if childs:
2183 if childs:
2184 for gr in childs:
2184 for gr in childs:
2185 all_.append(gr)
2185 all_.append(gr)
2186 _get_members(gr)
2186 _get_members(gr)
2187
2187
2188 _get_members(self)
2188 _get_members(self)
2189 return [self] + all_
2189 return [self] + all_
2190
2190
2191 def recursive_groups_and_repos(self):
2191 def recursive_groups_and_repos(self):
2192 """
2192 """
2193 Recursive return all groups, with repositories in those groups
2193 Recursive return all groups, with repositories in those groups
2194 """
2194 """
2195 return self._recursive_objects()
2195 return self._recursive_objects()
2196
2196
2197 def recursive_groups(self):
2197 def recursive_groups(self):
2198 """
2198 """
2199 Returns all children groups for this group including children of children
2199 Returns all children groups for this group including children of children
2200 """
2200 """
2201 return self._recursive_objects(include_repos=False)
2201 return self._recursive_objects(include_repos=False)
2202
2202
2203 def get_new_name(self, group_name):
2203 def get_new_name(self, group_name):
2204 """
2204 """
2205 returns new full group name based on parent and new name
2205 returns new full group name based on parent and new name
2206
2206
2207 :param group_name:
2207 :param group_name:
2208 """
2208 """
2209 path_prefix = (self.parent_group.full_path_splitted if
2209 path_prefix = (self.parent_group.full_path_splitted if
2210 self.parent_group else [])
2210 self.parent_group else [])
2211 return RepoGroup.url_sep().join(path_prefix + [group_name])
2211 return RepoGroup.url_sep().join(path_prefix + [group_name])
2212
2212
2213 def permissions(self, with_admins=True, with_owner=True):
2213 def permissions(self, with_admins=True, with_owner=True):
2214 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2214 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2215 q = q.options(joinedload(UserRepoGroupToPerm.group),
2215 q = q.options(joinedload(UserRepoGroupToPerm.group),
2216 joinedload(UserRepoGroupToPerm.user),
2216 joinedload(UserRepoGroupToPerm.user),
2217 joinedload(UserRepoGroupToPerm.permission),)
2217 joinedload(UserRepoGroupToPerm.permission),)
2218
2218
2219 # get owners and admins and permissions. We do a trick of re-writing
2219 # get owners and admins and permissions. We do a trick of re-writing
2220 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2220 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2221 # has a global reference and changing one object propagates to all
2221 # has a global reference and changing one object propagates to all
2222 # others. This means if admin is also an owner admin_row that change
2222 # others. This means if admin is also an owner admin_row that change
2223 # would propagate to both objects
2223 # would propagate to both objects
2224 perm_rows = []
2224 perm_rows = []
2225 for _usr in q.all():
2225 for _usr in q.all():
2226 usr = AttributeDict(_usr.user.get_dict())
2226 usr = AttributeDict(_usr.user.get_dict())
2227 usr.permission = _usr.permission.permission_name
2227 usr.permission = _usr.permission.permission_name
2228 perm_rows.append(usr)
2228 perm_rows.append(usr)
2229
2229
2230 # filter the perm rows by 'default' first and then sort them by
2230 # filter the perm rows by 'default' first and then sort them by
2231 # admin,write,read,none permissions sorted again alphabetically in
2231 # admin,write,read,none permissions sorted again alphabetically in
2232 # each group
2232 # each group
2233 perm_rows = sorted(perm_rows, key=display_sort)
2233 perm_rows = sorted(perm_rows, key=display_sort)
2234
2234
2235 _admin_perm = 'group.admin'
2235 _admin_perm = 'group.admin'
2236 owner_row = []
2236 owner_row = []
2237 if with_owner:
2237 if with_owner:
2238 usr = AttributeDict(self.user.get_dict())
2238 usr = AttributeDict(self.user.get_dict())
2239 usr.owner_row = True
2239 usr.owner_row = True
2240 usr.permission = _admin_perm
2240 usr.permission = _admin_perm
2241 owner_row.append(usr)
2241 owner_row.append(usr)
2242
2242
2243 super_admin_rows = []
2243 super_admin_rows = []
2244 if with_admins:
2244 if with_admins:
2245 for usr in User.get_all_super_admins():
2245 for usr in User.get_all_super_admins():
2246 # if this admin is also owner, don't double the record
2246 # if this admin is also owner, don't double the record
2247 if usr.user_id == owner_row[0].user_id:
2247 if usr.user_id == owner_row[0].user_id:
2248 owner_row[0].admin_row = True
2248 owner_row[0].admin_row = True
2249 else:
2249 else:
2250 usr = AttributeDict(usr.get_dict())
2250 usr = AttributeDict(usr.get_dict())
2251 usr.admin_row = True
2251 usr.admin_row = True
2252 usr.permission = _admin_perm
2252 usr.permission = _admin_perm
2253 super_admin_rows.append(usr)
2253 super_admin_rows.append(usr)
2254
2254
2255 return super_admin_rows + owner_row + perm_rows
2255 return super_admin_rows + owner_row + perm_rows
2256
2256
2257 def permission_user_groups(self):
2257 def permission_user_groups(self):
2258 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2258 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2259 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2259 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2260 joinedload(UserGroupRepoGroupToPerm.users_group),
2260 joinedload(UserGroupRepoGroupToPerm.users_group),
2261 joinedload(UserGroupRepoGroupToPerm.permission),)
2261 joinedload(UserGroupRepoGroupToPerm.permission),)
2262
2262
2263 perm_rows = []
2263 perm_rows = []
2264 for _user_group in q.all():
2264 for _user_group in q.all():
2265 usr = AttributeDict(_user_group.users_group.get_dict())
2265 usr = AttributeDict(_user_group.users_group.get_dict())
2266 usr.permission = _user_group.permission.permission_name
2266 usr.permission = _user_group.permission.permission_name
2267 perm_rows.append(usr)
2267 perm_rows.append(usr)
2268
2268
2269 return perm_rows
2269 return perm_rows
2270
2270
2271 def get_api_data(self):
2271 def get_api_data(self):
2272 """
2272 """
2273 Common function for generating api data
2273 Common function for generating api data
2274
2274
2275 """
2275 """
2276 group = self
2276 group = self
2277 data = {
2277 data = {
2278 'group_id': group.group_id,
2278 'group_id': group.group_id,
2279 'group_name': group.group_name,
2279 'group_name': group.group_name,
2280 'group_description': group.group_description,
2280 'group_description': group.group_description,
2281 'parent_group': group.parent_group.group_name if group.parent_group else None,
2281 'parent_group': group.parent_group.group_name if group.parent_group else None,
2282 'repositories': [x.repo_name for x in group.repositories],
2282 'repositories': [x.repo_name for x in group.repositories],
2283 'owner': group.user.username,
2283 'owner': group.user.username,
2284 }
2284 }
2285 return data
2285 return data
2286
2286
2287
2287
2288 class Permission(Base, BaseModel):
2288 class Permission(Base, BaseModel):
2289 __tablename__ = 'permissions'
2289 __tablename__ = 'permissions'
2290 __table_args__ = (
2290 __table_args__ = (
2291 Index('p_perm_name_idx', 'permission_name'),
2291 Index('p_perm_name_idx', 'permission_name'),
2292 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2292 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2293 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2293 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2294 )
2294 )
2295 PERMS = [
2295 PERMS = [
2296 ('hg.admin', _('RhodeCode Super Administrator')),
2296 ('hg.admin', _('RhodeCode Super Administrator')),
2297
2297
2298 ('repository.none', _('Repository no access')),
2298 ('repository.none', _('Repository no access')),
2299 ('repository.read', _('Repository read access')),
2299 ('repository.read', _('Repository read access')),
2300 ('repository.write', _('Repository write access')),
2300 ('repository.write', _('Repository write access')),
2301 ('repository.admin', _('Repository admin access')),
2301 ('repository.admin', _('Repository admin access')),
2302
2302
2303 ('group.none', _('Repository group no access')),
2303 ('group.none', _('Repository group no access')),
2304 ('group.read', _('Repository group read access')),
2304 ('group.read', _('Repository group read access')),
2305 ('group.write', _('Repository group write access')),
2305 ('group.write', _('Repository group write access')),
2306 ('group.admin', _('Repository group admin access')),
2306 ('group.admin', _('Repository group admin access')),
2307
2307
2308 ('usergroup.none', _('User group no access')),
2308 ('usergroup.none', _('User group no access')),
2309 ('usergroup.read', _('User group read access')),
2309 ('usergroup.read', _('User group read access')),
2310 ('usergroup.write', _('User group write access')),
2310 ('usergroup.write', _('User group write access')),
2311 ('usergroup.admin', _('User group admin access')),
2311 ('usergroup.admin', _('User group admin access')),
2312
2312
2313 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2313 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2314 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2314 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2315
2315
2316 ('hg.usergroup.create.false', _('User Group creation disabled')),
2316 ('hg.usergroup.create.false', _('User Group creation disabled')),
2317 ('hg.usergroup.create.true', _('User Group creation enabled')),
2317 ('hg.usergroup.create.true', _('User Group creation enabled')),
2318
2318
2319 ('hg.create.none', _('Repository creation disabled')),
2319 ('hg.create.none', _('Repository creation disabled')),
2320 ('hg.create.repository', _('Repository creation enabled')),
2320 ('hg.create.repository', _('Repository creation enabled')),
2321 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2321 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2322 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2322 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2323
2323
2324 ('hg.fork.none', _('Repository forking disabled')),
2324 ('hg.fork.none', _('Repository forking disabled')),
2325 ('hg.fork.repository', _('Repository forking enabled')),
2325 ('hg.fork.repository', _('Repository forking enabled')),
2326
2326
2327 ('hg.register.none', _('Registration disabled')),
2327 ('hg.register.none', _('Registration disabled')),
2328 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2328 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2329 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2329 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2330
2330
2331 ('hg.password_reset.enabled', _('Password reset enabled')),
2331 ('hg.password_reset.enabled', _('Password reset enabled')),
2332 ('hg.password_reset.hidden', _('Password reset hidden')),
2332 ('hg.password_reset.hidden', _('Password reset hidden')),
2333 ('hg.password_reset.disabled', _('Password reset disabled')),
2333 ('hg.password_reset.disabled', _('Password reset disabled')),
2334
2334
2335 ('hg.extern_activate.manual', _('Manual activation of external account')),
2335 ('hg.extern_activate.manual', _('Manual activation of external account')),
2336 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2336 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2337
2337
2338 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2338 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2339 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2339 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2340 ]
2340 ]
2341
2341
2342 # definition of system default permissions for DEFAULT user
2342 # definition of system default permissions for DEFAULT user
2343 DEFAULT_USER_PERMISSIONS = [
2343 DEFAULT_USER_PERMISSIONS = [
2344 'repository.read',
2344 'repository.read',
2345 'group.read',
2345 'group.read',
2346 'usergroup.read',
2346 'usergroup.read',
2347 'hg.create.repository',
2347 'hg.create.repository',
2348 'hg.repogroup.create.false',
2348 'hg.repogroup.create.false',
2349 'hg.usergroup.create.false',
2349 'hg.usergroup.create.false',
2350 'hg.create.write_on_repogroup.true',
2350 'hg.create.write_on_repogroup.true',
2351 'hg.fork.repository',
2351 'hg.fork.repository',
2352 'hg.register.manual_activate',
2352 'hg.register.manual_activate',
2353 'hg.password_reset.enabled',
2353 'hg.password_reset.enabled',
2354 'hg.extern_activate.auto',
2354 'hg.extern_activate.auto',
2355 'hg.inherit_default_perms.true',
2355 'hg.inherit_default_perms.true',
2356 ]
2356 ]
2357
2357
2358 # defines which permissions are more important higher the more important
2358 # defines which permissions are more important higher the more important
2359 # Weight defines which permissions are more important.
2359 # Weight defines which permissions are more important.
2360 # The higher number the more important.
2360 # The higher number the more important.
2361 PERM_WEIGHTS = {
2361 PERM_WEIGHTS = {
2362 'repository.none': 0,
2362 'repository.none': 0,
2363 'repository.read': 1,
2363 'repository.read': 1,
2364 'repository.write': 3,
2364 'repository.write': 3,
2365 'repository.admin': 4,
2365 'repository.admin': 4,
2366
2366
2367 'group.none': 0,
2367 'group.none': 0,
2368 'group.read': 1,
2368 'group.read': 1,
2369 'group.write': 3,
2369 'group.write': 3,
2370 'group.admin': 4,
2370 'group.admin': 4,
2371
2371
2372 'usergroup.none': 0,
2372 'usergroup.none': 0,
2373 'usergroup.read': 1,
2373 'usergroup.read': 1,
2374 'usergroup.write': 3,
2374 'usergroup.write': 3,
2375 'usergroup.admin': 4,
2375 'usergroup.admin': 4,
2376
2376
2377 'hg.repogroup.create.false': 0,
2377 'hg.repogroup.create.false': 0,
2378 'hg.repogroup.create.true': 1,
2378 'hg.repogroup.create.true': 1,
2379
2379
2380 'hg.usergroup.create.false': 0,
2380 'hg.usergroup.create.false': 0,
2381 'hg.usergroup.create.true': 1,
2381 'hg.usergroup.create.true': 1,
2382
2382
2383 'hg.fork.none': 0,
2383 'hg.fork.none': 0,
2384 'hg.fork.repository': 1,
2384 'hg.fork.repository': 1,
2385 'hg.create.none': 0,
2385 'hg.create.none': 0,
2386 'hg.create.repository': 1
2386 'hg.create.repository': 1
2387 }
2387 }
2388
2388
2389 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2389 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2390 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2390 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2391 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2391 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2392
2392
2393 def __unicode__(self):
2393 def __unicode__(self):
2394 return u"<%s('%s:%s')>" % (
2394 return u"<%s('%s:%s')>" % (
2395 self.__class__.__name__, self.permission_id, self.permission_name
2395 self.__class__.__name__, self.permission_id, self.permission_name
2396 )
2396 )
2397
2397
2398 @classmethod
2398 @classmethod
2399 def get_by_key(cls, key):
2399 def get_by_key(cls, key):
2400 return cls.query().filter(cls.permission_name == key).scalar()
2400 return cls.query().filter(cls.permission_name == key).scalar()
2401
2401
2402 @classmethod
2402 @classmethod
2403 def get_default_repo_perms(cls, user_id, repo_id=None):
2403 def get_default_repo_perms(cls, user_id, repo_id=None):
2404 q = Session().query(UserRepoToPerm, Repository, Permission)\
2404 q = Session().query(UserRepoToPerm, Repository, Permission)\
2405 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2405 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2406 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2406 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2407 .filter(UserRepoToPerm.user_id == user_id)
2407 .filter(UserRepoToPerm.user_id == user_id)
2408 if repo_id:
2408 if repo_id:
2409 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2409 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2410 return q.all()
2410 return q.all()
2411
2411
2412 @classmethod
2412 @classmethod
2413 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2413 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2414 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2414 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2415 .join(
2415 .join(
2416 Permission,
2416 Permission,
2417 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2417 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2418 .join(
2418 .join(
2419 Repository,
2419 Repository,
2420 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2420 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2421 .join(
2421 .join(
2422 UserGroup,
2422 UserGroup,
2423 UserGroupRepoToPerm.users_group_id ==
2423 UserGroupRepoToPerm.users_group_id ==
2424 UserGroup.users_group_id)\
2424 UserGroup.users_group_id)\
2425 .join(
2425 .join(
2426 UserGroupMember,
2426 UserGroupMember,
2427 UserGroupRepoToPerm.users_group_id ==
2427 UserGroupRepoToPerm.users_group_id ==
2428 UserGroupMember.users_group_id)\
2428 UserGroupMember.users_group_id)\
2429 .filter(
2429 .filter(
2430 UserGroupMember.user_id == user_id,
2430 UserGroupMember.user_id == user_id,
2431 UserGroup.users_group_active == true())
2431 UserGroup.users_group_active == true())
2432 if repo_id:
2432 if repo_id:
2433 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2433 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2434 return q.all()
2434 return q.all()
2435
2435
2436 @classmethod
2436 @classmethod
2437 def get_default_group_perms(cls, user_id, repo_group_id=None):
2437 def get_default_group_perms(cls, user_id, repo_group_id=None):
2438 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2438 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2439 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2439 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2440 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2440 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2441 .filter(UserRepoGroupToPerm.user_id == user_id)
2441 .filter(UserRepoGroupToPerm.user_id == user_id)
2442 if repo_group_id:
2442 if repo_group_id:
2443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2444 return q.all()
2444 return q.all()
2445
2445
2446 @classmethod
2446 @classmethod
2447 def get_default_group_perms_from_user_group(
2447 def get_default_group_perms_from_user_group(
2448 cls, user_id, repo_group_id=None):
2448 cls, user_id, repo_group_id=None):
2449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2450 .join(
2450 .join(
2451 Permission,
2451 Permission,
2452 UserGroupRepoGroupToPerm.permission_id ==
2452 UserGroupRepoGroupToPerm.permission_id ==
2453 Permission.permission_id)\
2453 Permission.permission_id)\
2454 .join(
2454 .join(
2455 RepoGroup,
2455 RepoGroup,
2456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2457 .join(
2457 .join(
2458 UserGroup,
2458 UserGroup,
2459 UserGroupRepoGroupToPerm.users_group_id ==
2459 UserGroupRepoGroupToPerm.users_group_id ==
2460 UserGroup.users_group_id)\
2460 UserGroup.users_group_id)\
2461 .join(
2461 .join(
2462 UserGroupMember,
2462 UserGroupMember,
2463 UserGroupRepoGroupToPerm.users_group_id ==
2463 UserGroupRepoGroupToPerm.users_group_id ==
2464 UserGroupMember.users_group_id)\
2464 UserGroupMember.users_group_id)\
2465 .filter(
2465 .filter(
2466 UserGroupMember.user_id == user_id,
2466 UserGroupMember.user_id == user_id,
2467 UserGroup.users_group_active == true())
2467 UserGroup.users_group_active == true())
2468 if repo_group_id:
2468 if repo_group_id:
2469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2470 return q.all()
2470 return q.all()
2471
2471
2472 @classmethod
2472 @classmethod
2473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2477 .filter(UserUserGroupToPerm.user_id == user_id)
2477 .filter(UserUserGroupToPerm.user_id == user_id)
2478 if user_group_id:
2478 if user_group_id:
2479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2480 return q.all()
2480 return q.all()
2481
2481
2482 @classmethod
2482 @classmethod
2483 def get_default_user_group_perms_from_user_group(
2483 def get_default_user_group_perms_from_user_group(
2484 cls, user_id, user_group_id=None):
2484 cls, user_id, user_group_id=None):
2485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2487 .join(
2487 .join(
2488 Permission,
2488 Permission,
2489 UserGroupUserGroupToPerm.permission_id ==
2489 UserGroupUserGroupToPerm.permission_id ==
2490 Permission.permission_id)\
2490 Permission.permission_id)\
2491 .join(
2491 .join(
2492 TargetUserGroup,
2492 TargetUserGroup,
2493 UserGroupUserGroupToPerm.target_user_group_id ==
2493 UserGroupUserGroupToPerm.target_user_group_id ==
2494 TargetUserGroup.users_group_id)\
2494 TargetUserGroup.users_group_id)\
2495 .join(
2495 .join(
2496 UserGroup,
2496 UserGroup,
2497 UserGroupUserGroupToPerm.user_group_id ==
2497 UserGroupUserGroupToPerm.user_group_id ==
2498 UserGroup.users_group_id)\
2498 UserGroup.users_group_id)\
2499 .join(
2499 .join(
2500 UserGroupMember,
2500 UserGroupMember,
2501 UserGroupUserGroupToPerm.user_group_id ==
2501 UserGroupUserGroupToPerm.user_group_id ==
2502 UserGroupMember.users_group_id)\
2502 UserGroupMember.users_group_id)\
2503 .filter(
2503 .filter(
2504 UserGroupMember.user_id == user_id,
2504 UserGroupMember.user_id == user_id,
2505 UserGroup.users_group_active == true())
2505 UserGroup.users_group_active == true())
2506 if user_group_id:
2506 if user_group_id:
2507 q = q.filter(
2507 q = q.filter(
2508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2509
2509
2510 return q.all()
2510 return q.all()
2511
2511
2512
2512
2513 class UserRepoToPerm(Base, BaseModel):
2513 class UserRepoToPerm(Base, BaseModel):
2514 __tablename__ = 'repo_to_perm'
2514 __tablename__ = 'repo_to_perm'
2515 __table_args__ = (
2515 __table_args__ = (
2516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2519 )
2519 )
2520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2524
2524
2525 user = relationship('User')
2525 user = relationship('User')
2526 repository = relationship('Repository')
2526 repository = relationship('Repository')
2527 permission = relationship('Permission')
2527 permission = relationship('Permission')
2528
2528
2529 @classmethod
2529 @classmethod
2530 def create(cls, user, repository, permission):
2530 def create(cls, user, repository, permission):
2531 n = cls()
2531 n = cls()
2532 n.user = user
2532 n.user = user
2533 n.repository = repository
2533 n.repository = repository
2534 n.permission = permission
2534 n.permission = permission
2535 Session().add(n)
2535 Session().add(n)
2536 return n
2536 return n
2537
2537
2538 def __unicode__(self):
2538 def __unicode__(self):
2539 return u'<%s => %s >' % (self.user, self.repository)
2539 return u'<%s => %s >' % (self.user, self.repository)
2540
2540
2541
2541
2542 class UserUserGroupToPerm(Base, BaseModel):
2542 class UserUserGroupToPerm(Base, BaseModel):
2543 __tablename__ = 'user_user_group_to_perm'
2543 __tablename__ = 'user_user_group_to_perm'
2544 __table_args__ = (
2544 __table_args__ = (
2545 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2545 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2547 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2547 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2548 )
2548 )
2549 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
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 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2550 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2552 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2552 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2553
2553
2554 user = relationship('User')
2554 user = relationship('User')
2555 user_group = relationship('UserGroup')
2555 user_group = relationship('UserGroup')
2556 permission = relationship('Permission')
2556 permission = relationship('Permission')
2557
2557
2558 @classmethod
2558 @classmethod
2559 def create(cls, user, user_group, permission):
2559 def create(cls, user, user_group, permission):
2560 n = cls()
2560 n = cls()
2561 n.user = user
2561 n.user = user
2562 n.user_group = user_group
2562 n.user_group = user_group
2563 n.permission = permission
2563 n.permission = permission
2564 Session().add(n)
2564 Session().add(n)
2565 return n
2565 return n
2566
2566
2567 def __unicode__(self):
2567 def __unicode__(self):
2568 return u'<%s => %s >' % (self.user, self.user_group)
2568 return u'<%s => %s >' % (self.user, self.user_group)
2569
2569
2570
2570
2571 class UserToPerm(Base, BaseModel):
2571 class UserToPerm(Base, BaseModel):
2572 __tablename__ = 'user_to_perm'
2572 __tablename__ = 'user_to_perm'
2573 __table_args__ = (
2573 __table_args__ = (
2574 UniqueConstraint('user_id', 'permission_id'),
2574 UniqueConstraint('user_id', 'permission_id'),
2575 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2575 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2576 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2576 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2577 )
2577 )
2578 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2578 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2579 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2579 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2580 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2580 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2581
2581
2582 user = relationship('User')
2582 user = relationship('User')
2583 permission = relationship('Permission', lazy='joined')
2583 permission = relationship('Permission', lazy='joined')
2584
2584
2585 def __unicode__(self):
2585 def __unicode__(self):
2586 return u'<%s => %s >' % (self.user, self.permission)
2586 return u'<%s => %s >' % (self.user, self.permission)
2587
2587
2588
2588
2589 class UserGroupRepoToPerm(Base, BaseModel):
2589 class UserGroupRepoToPerm(Base, BaseModel):
2590 __tablename__ = 'users_group_repo_to_perm'
2590 __tablename__ = 'users_group_repo_to_perm'
2591 __table_args__ = (
2591 __table_args__ = (
2592 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2592 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2593 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2593 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2594 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2594 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2595 )
2595 )
2596 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2596 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2597 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2597 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2598 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2598 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2599 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2599 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2600
2600
2601 users_group = relationship('UserGroup')
2601 users_group = relationship('UserGroup')
2602 permission = relationship('Permission')
2602 permission = relationship('Permission')
2603 repository = relationship('Repository')
2603 repository = relationship('Repository')
2604
2604
2605 @classmethod
2605 @classmethod
2606 def create(cls, users_group, repository, permission):
2606 def create(cls, users_group, repository, permission):
2607 n = cls()
2607 n = cls()
2608 n.users_group = users_group
2608 n.users_group = users_group
2609 n.repository = repository
2609 n.repository = repository
2610 n.permission = permission
2610 n.permission = permission
2611 Session().add(n)
2611 Session().add(n)
2612 return n
2612 return n
2613
2613
2614 def __unicode__(self):
2614 def __unicode__(self):
2615 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2615 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2616
2616
2617
2617
2618 class UserGroupUserGroupToPerm(Base, BaseModel):
2618 class UserGroupUserGroupToPerm(Base, BaseModel):
2619 __tablename__ = 'user_group_user_group_to_perm'
2619 __tablename__ = 'user_group_user_group_to_perm'
2620 __table_args__ = (
2620 __table_args__ = (
2621 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2621 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2622 CheckConstraint('target_user_group_id != user_group_id'),
2622 CheckConstraint('target_user_group_id != user_group_id'),
2623 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2623 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2624 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2624 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2625 )
2625 )
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)
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 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2627 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2628 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2628 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2629 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2629 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2630
2630
2631 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2631 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2632 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2632 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2633 permission = relationship('Permission')
2633 permission = relationship('Permission')
2634
2634
2635 @classmethod
2635 @classmethod
2636 def create(cls, target_user_group, user_group, permission):
2636 def create(cls, target_user_group, user_group, permission):
2637 n = cls()
2637 n = cls()
2638 n.target_user_group = target_user_group
2638 n.target_user_group = target_user_group
2639 n.user_group = user_group
2639 n.user_group = user_group
2640 n.permission = permission
2640 n.permission = permission
2641 Session().add(n)
2641 Session().add(n)
2642 return n
2642 return n
2643
2643
2644 def __unicode__(self):
2644 def __unicode__(self):
2645 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2645 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2646
2646
2647
2647
2648 class UserGroupToPerm(Base, BaseModel):
2648 class UserGroupToPerm(Base, BaseModel):
2649 __tablename__ = 'users_group_to_perm'
2649 __tablename__ = 'users_group_to_perm'
2650 __table_args__ = (
2650 __table_args__ = (
2651 UniqueConstraint('users_group_id', 'permission_id',),
2651 UniqueConstraint('users_group_id', 'permission_id',),
2652 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2652 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2653 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2653 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2654 )
2654 )
2655 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2655 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2656 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2656 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2657 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2657 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2658
2658
2659 users_group = relationship('UserGroup')
2659 users_group = relationship('UserGroup')
2660 permission = relationship('Permission')
2660 permission = relationship('Permission')
2661
2661
2662
2662
2663 class UserRepoGroupToPerm(Base, BaseModel):
2663 class UserRepoGroupToPerm(Base, BaseModel):
2664 __tablename__ = 'user_repo_group_to_perm'
2664 __tablename__ = 'user_repo_group_to_perm'
2665 __table_args__ = (
2665 __table_args__ = (
2666 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2666 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2667 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2667 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2668 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2668 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2669 )
2669 )
2670
2670
2671 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2671 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2672 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2672 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2673 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2673 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2674 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2674 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2675
2675
2676 user = relationship('User')
2676 user = relationship('User')
2677 group = relationship('RepoGroup')
2677 group = relationship('RepoGroup')
2678 permission = relationship('Permission')
2678 permission = relationship('Permission')
2679
2679
2680 @classmethod
2680 @classmethod
2681 def create(cls, user, repository_group, permission):
2681 def create(cls, user, repository_group, permission):
2682 n = cls()
2682 n = cls()
2683 n.user = user
2683 n.user = user
2684 n.group = repository_group
2684 n.group = repository_group
2685 n.permission = permission
2685 n.permission = permission
2686 Session().add(n)
2686 Session().add(n)
2687 return n
2687 return n
2688
2688
2689
2689
2690 class UserGroupRepoGroupToPerm(Base, BaseModel):
2690 class UserGroupRepoGroupToPerm(Base, BaseModel):
2691 __tablename__ = 'users_group_repo_group_to_perm'
2691 __tablename__ = 'users_group_repo_group_to_perm'
2692 __table_args__ = (
2692 __table_args__ = (
2693 UniqueConstraint('users_group_id', 'group_id'),
2693 UniqueConstraint('users_group_id', 'group_id'),
2694 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2694 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2695 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2695 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2696 )
2696 )
2697
2697
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)
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 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2699 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2700 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2700 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2701 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2701 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2702
2702
2703 users_group = relationship('UserGroup')
2703 users_group = relationship('UserGroup')
2704 permission = relationship('Permission')
2704 permission = relationship('Permission')
2705 group = relationship('RepoGroup')
2705 group = relationship('RepoGroup')
2706
2706
2707 @classmethod
2707 @classmethod
2708 def create(cls, user_group, repository_group, permission):
2708 def create(cls, user_group, repository_group, permission):
2709 n = cls()
2709 n = cls()
2710 n.users_group = user_group
2710 n.users_group = user_group
2711 n.group = repository_group
2711 n.group = repository_group
2712 n.permission = permission
2712 n.permission = permission
2713 Session().add(n)
2713 Session().add(n)
2714 return n
2714 return n
2715
2715
2716 def __unicode__(self):
2716 def __unicode__(self):
2717 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2717 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2718
2718
2719
2719
2720 class Statistics(Base, BaseModel):
2720 class Statistics(Base, BaseModel):
2721 __tablename__ = 'statistics'
2721 __tablename__ = 'statistics'
2722 __table_args__ = (
2722 __table_args__ = (
2723 UniqueConstraint('repository_id'),
2723 UniqueConstraint('repository_id'),
2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2726 )
2726 )
2727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2732 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2732 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2733
2733
2734 repository = relationship('Repository', single_parent=True)
2734 repository = relationship('Repository', single_parent=True)
2735
2735
2736
2736
2737 class UserFollowing(Base, BaseModel):
2737 class UserFollowing(Base, BaseModel):
2738 __tablename__ = 'user_followings'
2738 __tablename__ = 'user_followings'
2739 __table_args__ = (
2739 __table_args__ = (
2740 UniqueConstraint('user_id', 'follows_repository_id'),
2740 UniqueConstraint('user_id', 'follows_repository_id'),
2741 UniqueConstraint('user_id', 'follows_user_id'),
2741 UniqueConstraint('user_id', 'follows_user_id'),
2742 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2742 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2743 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2743 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2744 )
2744 )
2745
2745
2746 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2746 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2747 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2747 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2748 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2748 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2749 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2749 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2750 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2750 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2751
2751
2752 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2752 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2753
2753
2754 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2754 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2755 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2755 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2756
2756
2757 @classmethod
2757 @classmethod
2758 def get_repo_followers(cls, repo_id):
2758 def get_repo_followers(cls, repo_id):
2759 return cls.query().filter(cls.follows_repo_id == repo_id)
2759 return cls.query().filter(cls.follows_repo_id == repo_id)
2760
2760
2761
2761
2762 class CacheKey(Base, BaseModel):
2762 class CacheKey(Base, BaseModel):
2763 __tablename__ = 'cache_invalidation'
2763 __tablename__ = 'cache_invalidation'
2764 __table_args__ = (
2764 __table_args__ = (
2765 UniqueConstraint('cache_key'),
2765 UniqueConstraint('cache_key'),
2766 Index('key_idx', 'cache_key'),
2766 Index('key_idx', 'cache_key'),
2767 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2767 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2768 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2768 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2769 )
2769 )
2770 CACHE_TYPE_ATOM = 'ATOM'
2770 CACHE_TYPE_ATOM = 'ATOM'
2771 CACHE_TYPE_RSS = 'RSS'
2771 CACHE_TYPE_RSS = 'RSS'
2772 CACHE_TYPE_README = 'README'
2772 CACHE_TYPE_README = 'README'
2773
2773
2774 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2774 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2775 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2775 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2776 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2776 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2777 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2777 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2778
2778
2779 def __init__(self, cache_key, cache_args=''):
2779 def __init__(self, cache_key, cache_args=''):
2780 self.cache_key = cache_key
2780 self.cache_key = cache_key
2781 self.cache_args = cache_args
2781 self.cache_args = cache_args
2782 self.cache_active = False
2782 self.cache_active = False
2783
2783
2784 def __unicode__(self):
2784 def __unicode__(self):
2785 return u"<%s('%s:%s[%s]')>" % (
2785 return u"<%s('%s:%s[%s]')>" % (
2786 self.__class__.__name__,
2786 self.__class__.__name__,
2787 self.cache_id, self.cache_key, self.cache_active)
2787 self.cache_id, self.cache_key, self.cache_active)
2788
2788
2789 def _cache_key_partition(self):
2789 def _cache_key_partition(self):
2790 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2790 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2791 return prefix, repo_name, suffix
2791 return prefix, repo_name, suffix
2792
2792
2793 def get_prefix(self):
2793 def get_prefix(self):
2794 """
2794 """
2795 Try to extract prefix from existing cache key. The key could consist
2795 Try to extract prefix from existing cache key. The key could consist
2796 of prefix, repo_name, suffix
2796 of prefix, repo_name, suffix
2797 """
2797 """
2798 # this returns prefix, repo_name, suffix
2798 # this returns prefix, repo_name, suffix
2799 return self._cache_key_partition()[0]
2799 return self._cache_key_partition()[0]
2800
2800
2801 def get_suffix(self):
2801 def get_suffix(self):
2802 """
2802 """
2803 get suffix that might have been used in _get_cache_key to
2803 get suffix that might have been used in _get_cache_key to
2804 generate self.cache_key. Only used for informational purposes
2804 generate self.cache_key. Only used for informational purposes
2805 in repo_edit.mako.
2805 in repo_edit.mako.
2806 """
2806 """
2807 # prefix, repo_name, suffix
2807 # prefix, repo_name, suffix
2808 return self._cache_key_partition()[2]
2808 return self._cache_key_partition()[2]
2809
2809
2810 @classmethod
2810 @classmethod
2811 def delete_all_cache(cls):
2811 def delete_all_cache(cls):
2812 """
2812 """
2813 Delete all cache keys from database.
2813 Delete all cache keys from database.
2814 Should only be run when all instances are down and all entries
2814 Should only be run when all instances are down and all entries
2815 thus stale.
2815 thus stale.
2816 """
2816 """
2817 cls.query().delete()
2817 cls.query().delete()
2818 Session().commit()
2818 Session().commit()
2819
2819
2820 @classmethod
2820 @classmethod
2821 def get_cache_key(cls, repo_name, cache_type):
2821 def get_cache_key(cls, repo_name, cache_type):
2822 """
2822 """
2823
2823
2824 Generate a cache key for this process of RhodeCode instance.
2824 Generate a cache key for this process of RhodeCode instance.
2825 Prefix most likely will be process id or maybe explicitly set
2825 Prefix most likely will be process id or maybe explicitly set
2826 instance_id from .ini file.
2826 instance_id from .ini file.
2827 """
2827 """
2828 import rhodecode
2828 import rhodecode
2829 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2829 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2830
2830
2831 repo_as_unicode = safe_unicode(repo_name)
2831 repo_as_unicode = safe_unicode(repo_name)
2832 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2832 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2833 if cache_type else repo_as_unicode
2833 if cache_type else repo_as_unicode
2834
2834
2835 return u'{}{}'.format(prefix, key)
2835 return u'{}{}'.format(prefix, key)
2836
2836
2837 @classmethod
2837 @classmethod
2838 def set_invalidate(cls, repo_name, delete=False):
2838 def set_invalidate(cls, repo_name, delete=False):
2839 """
2839 """
2840 Mark all caches of a repo as invalid in the database.
2840 Mark all caches of a repo as invalid in the database.
2841 """
2841 """
2842
2842
2843 try:
2843 try:
2844 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2844 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2845 if delete:
2845 if delete:
2846 log.debug('cache objects deleted for repo %s',
2846 log.debug('cache objects deleted for repo %s',
2847 safe_str(repo_name))
2847 safe_str(repo_name))
2848 qry.delete()
2848 qry.delete()
2849 else:
2849 else:
2850 log.debug('cache objects marked as invalid for repo %s',
2850 log.debug('cache objects marked as invalid for repo %s',
2851 safe_str(repo_name))
2851 safe_str(repo_name))
2852 qry.update({"cache_active": False})
2852 qry.update({"cache_active": False})
2853
2853
2854 Session().commit()
2854 Session().commit()
2855 except Exception:
2855 except Exception:
2856 log.exception(
2856 log.exception(
2857 'Cache key invalidation failed for repository %s',
2857 'Cache key invalidation failed for repository %s',
2858 safe_str(repo_name))
2858 safe_str(repo_name))
2859 Session().rollback()
2859 Session().rollback()
2860
2860
2861 @classmethod
2861 @classmethod
2862 def get_active_cache(cls, cache_key):
2862 def get_active_cache(cls, cache_key):
2863 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2863 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2864 if inv_obj:
2864 if inv_obj:
2865 return inv_obj
2865 return inv_obj
2866 return None
2866 return None
2867
2867
2868 @classmethod
2868 @classmethod
2869 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2869 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2870 thread_scoped=False):
2870 thread_scoped=False):
2871 """
2871 """
2872 @cache_region('long_term')
2872 @cache_region('long_term')
2873 def _heavy_calculation(cache_key):
2873 def _heavy_calculation(cache_key):
2874 return 'result'
2874 return 'result'
2875
2875
2876 cache_context = CacheKey.repo_context_cache(
2876 cache_context = CacheKey.repo_context_cache(
2877 _heavy_calculation, repo_name, cache_type)
2877 _heavy_calculation, repo_name, cache_type)
2878
2878
2879 with cache_context as context:
2879 with cache_context as context:
2880 context.invalidate()
2880 context.invalidate()
2881 computed = context.compute()
2881 computed = context.compute()
2882
2882
2883 assert computed == 'result'
2883 assert computed == 'result'
2884 """
2884 """
2885 from rhodecode.lib import caches
2885 from rhodecode.lib import caches
2886 return caches.InvalidationContext(
2886 return caches.InvalidationContext(
2887 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2887 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2888
2888
2889
2889
2890 class ChangesetComment(Base, BaseModel):
2890 class ChangesetComment(Base, BaseModel):
2891 __tablename__ = 'changeset_comments'
2891 __tablename__ = 'changeset_comments'
2892 __table_args__ = (
2892 __table_args__ = (
2893 Index('cc_revision_idx', 'revision'),
2893 Index('cc_revision_idx', 'revision'),
2894 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2894 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2895 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2895 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2896 )
2896 )
2897
2897
2898 COMMENT_OUTDATED = u'comment_outdated'
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 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2903 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2901 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2904 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2902 revision = Column('revision', String(40), nullable=True)
2905 revision = Column('revision', String(40), nullable=True)
2903 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2906 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2904 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2907 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2905 line_no = Column('line_no', Unicode(10), nullable=True)
2908 line_no = Column('line_no', Unicode(10), nullable=True)
2906 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2909 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2907 f_path = Column('f_path', Unicode(1000), nullable=True)
2910 f_path = Column('f_path', Unicode(1000), nullable=True)
2908 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2911 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2909 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2912 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2910 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2913 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2911 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2914 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2912 renderer = Column('renderer', Unicode(64), nullable=True)
2915 renderer = Column('renderer', Unicode(64), nullable=True)
2913 display_state = Column('display_state', Unicode(128), nullable=True)
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 author = relationship('User', lazy='joined')
2921 author = relationship('User', lazy='joined')
2916 repo = relationship('Repository')
2922 repo = relationship('Repository')
2917 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2923 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2918 pull_request = relationship('PullRequest', lazy='joined')
2924 pull_request = relationship('PullRequest', lazy='joined')
2919 pull_request_version = relationship('PullRequestVersion')
2925 pull_request_version = relationship('PullRequestVersion')
2920
2926
2921 @classmethod
2927 @classmethod
2922 def get_users(cls, revision=None, pull_request_id=None):
2928 def get_users(cls, revision=None, pull_request_id=None):
2923 """
2929 """
2924 Returns user associated with this ChangesetComment. ie those
2930 Returns user associated with this ChangesetComment. ie those
2925 who actually commented
2931 who actually commented
2926
2932
2927 :param cls:
2933 :param cls:
2928 :param revision:
2934 :param revision:
2929 """
2935 """
2930 q = Session().query(User)\
2936 q = Session().query(User)\
2931 .join(ChangesetComment.author)
2937 .join(ChangesetComment.author)
2932 if revision:
2938 if revision:
2933 q = q.filter(cls.revision == revision)
2939 q = q.filter(cls.revision == revision)
2934 elif pull_request_id:
2940 elif pull_request_id:
2935 q = q.filter(cls.pull_request_id == pull_request_id)
2941 q = q.filter(cls.pull_request_id == pull_request_id)
2936 return q.all()
2942 return q.all()
2937
2943
2938 @classmethod
2944 @classmethod
2939 def get_index_from_version(cls, pr_version, versions):
2945 def get_index_from_version(cls, pr_version, versions):
2940 num_versions = [x.pull_request_version_id for x in versions]
2946 num_versions = [x.pull_request_version_id for x in versions]
2941 try:
2947 try:
2942 return num_versions.index(pr_version) +1
2948 return num_versions.index(pr_version) +1
2943 except (IndexError, ValueError):
2949 except (IndexError, ValueError):
2944 return
2950 return
2945
2951
2946 @property
2952 @property
2947 def outdated(self):
2953 def outdated(self):
2948 return self.display_state == self.COMMENT_OUTDATED
2954 return self.display_state == self.COMMENT_OUTDATED
2949
2955
2950 def outdated_at_version(self, version):
2956 def outdated_at_version(self, version):
2951 """
2957 """
2952 Checks if comment is outdated for given pull request version
2958 Checks if comment is outdated for given pull request version
2953 """
2959 """
2954 return self.outdated and self.pull_request_version_id != version
2960 return self.outdated and self.pull_request_version_id != version
2955
2961
2956 def get_index_version(self, versions):
2962 def get_index_version(self, versions):
2957 return self.get_index_from_version(
2963 return self.get_index_from_version(
2958 self.pull_request_version_id, versions)
2964 self.pull_request_version_id, versions)
2959
2965
2960 def render(self, mentions=False):
2966 def render(self, mentions=False):
2961 from rhodecode.lib import helpers as h
2967 from rhodecode.lib import helpers as h
2962 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2968 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2963
2969
2964 def __repr__(self):
2970 def __repr__(self):
2965 if self.comment_id:
2971 if self.comment_id:
2966 return '<DB:ChangesetComment #%s>' % self.comment_id
2972 return '<DB:ChangesetComment #%s>' % self.comment_id
2967 else:
2973 else:
2968 return '<DB:ChangesetComment at %#x>' % id(self)
2974 return '<DB:ChangesetComment at %#x>' % id(self)
2969
2975
2970
2976
2971 class ChangesetStatus(Base, BaseModel):
2977 class ChangesetStatus(Base, BaseModel):
2972 __tablename__ = 'changeset_statuses'
2978 __tablename__ = 'changeset_statuses'
2973 __table_args__ = (
2979 __table_args__ = (
2974 Index('cs_revision_idx', 'revision'),
2980 Index('cs_revision_idx', 'revision'),
2975 Index('cs_version_idx', 'version'),
2981 Index('cs_version_idx', 'version'),
2976 UniqueConstraint('repo_id', 'revision', 'version'),
2982 UniqueConstraint('repo_id', 'revision', 'version'),
2977 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2983 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2978 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2984 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2979 )
2985 )
2980 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2986 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2981 STATUS_APPROVED = 'approved'
2987 STATUS_APPROVED = 'approved'
2982 STATUS_REJECTED = 'rejected'
2988 STATUS_REJECTED = 'rejected'
2983 STATUS_UNDER_REVIEW = 'under_review'
2989 STATUS_UNDER_REVIEW = 'under_review'
2984
2990
2985 STATUSES = [
2991 STATUSES = [
2986 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2992 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2987 (STATUS_APPROVED, _("Approved")),
2993 (STATUS_APPROVED, _("Approved")),
2988 (STATUS_REJECTED, _("Rejected")),
2994 (STATUS_REJECTED, _("Rejected")),
2989 (STATUS_UNDER_REVIEW, _("Under Review")),
2995 (STATUS_UNDER_REVIEW, _("Under Review")),
2990 ]
2996 ]
2991
2997
2992 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2998 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2993 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2999 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2994 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3000 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2995 revision = Column('revision', String(40), nullable=False)
3001 revision = Column('revision', String(40), nullable=False)
2996 status = Column('status', String(128), nullable=False, default=DEFAULT)
3002 status = Column('status', String(128), nullable=False, default=DEFAULT)
2997 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3003 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2998 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3004 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2999 version = Column('version', Integer(), nullable=False, default=0)
3005 version = Column('version', Integer(), nullable=False, default=0)
3000 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3006 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3001
3007
3002 author = relationship('User', lazy='joined')
3008 author = relationship('User', lazy='joined')
3003 repo = relationship('Repository')
3009 repo = relationship('Repository')
3004 comment = relationship('ChangesetComment', lazy='joined')
3010 comment = relationship('ChangesetComment', lazy='joined')
3005 pull_request = relationship('PullRequest', lazy='joined')
3011 pull_request = relationship('PullRequest', lazy='joined')
3006
3012
3007 def __unicode__(self):
3013 def __unicode__(self):
3008 return u"<%s('%s[%s]:%s')>" % (
3014 return u"<%s('%s[%s]:%s')>" % (
3009 self.__class__.__name__,
3015 self.__class__.__name__,
3010 self.status, self.version, self.author
3016 self.status, self.version, self.author
3011 )
3017 )
3012
3018
3013 @classmethod
3019 @classmethod
3014 def get_status_lbl(cls, value):
3020 def get_status_lbl(cls, value):
3015 return dict(cls.STATUSES).get(value)
3021 return dict(cls.STATUSES).get(value)
3016
3022
3017 @property
3023 @property
3018 def status_lbl(self):
3024 def status_lbl(self):
3019 return ChangesetStatus.get_status_lbl(self.status)
3025 return ChangesetStatus.get_status_lbl(self.status)
3020
3026
3021
3027
3022 class _PullRequestBase(BaseModel):
3028 class _PullRequestBase(BaseModel):
3023 """
3029 """
3024 Common attributes of pull request and version entries.
3030 Common attributes of pull request and version entries.
3025 """
3031 """
3026
3032
3027 # .status values
3033 # .status values
3028 STATUS_NEW = u'new'
3034 STATUS_NEW = u'new'
3029 STATUS_OPEN = u'open'
3035 STATUS_OPEN = u'open'
3030 STATUS_CLOSED = u'closed'
3036 STATUS_CLOSED = u'closed'
3031
3037
3032 title = Column('title', Unicode(255), nullable=True)
3038 title = Column('title', Unicode(255), nullable=True)
3033 description = Column(
3039 description = Column(
3034 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3040 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3035 nullable=True)
3041 nullable=True)
3036 # new/open/closed status of pull request (not approve/reject/etc)
3042 # new/open/closed status of pull request (not approve/reject/etc)
3037 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3043 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3038 created_on = Column(
3044 created_on = Column(
3039 'created_on', DateTime(timezone=False), nullable=False,
3045 'created_on', DateTime(timezone=False), nullable=False,
3040 default=datetime.datetime.now)
3046 default=datetime.datetime.now)
3041 updated_on = Column(
3047 updated_on = Column(
3042 'updated_on', DateTime(timezone=False), nullable=False,
3048 'updated_on', DateTime(timezone=False), nullable=False,
3043 default=datetime.datetime.now)
3049 default=datetime.datetime.now)
3044
3050
3045 @declared_attr
3051 @declared_attr
3046 def user_id(cls):
3052 def user_id(cls):
3047 return Column(
3053 return Column(
3048 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3054 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3049 unique=None)
3055 unique=None)
3050
3056
3051 # 500 revisions max
3057 # 500 revisions max
3052 _revisions = Column(
3058 _revisions = Column(
3053 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3059 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3054
3060
3055 @declared_attr
3061 @declared_attr
3056 def source_repo_id(cls):
3062 def source_repo_id(cls):
3057 # TODO: dan: rename column to source_repo_id
3063 # TODO: dan: rename column to source_repo_id
3058 return Column(
3064 return Column(
3059 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3065 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3060 nullable=False)
3066 nullable=False)
3061
3067
3062 source_ref = Column('org_ref', Unicode(255), nullable=False)
3068 source_ref = Column('org_ref', Unicode(255), nullable=False)
3063
3069
3064 @declared_attr
3070 @declared_attr
3065 def target_repo_id(cls):
3071 def target_repo_id(cls):
3066 # TODO: dan: rename column to target_repo_id
3072 # TODO: dan: rename column to target_repo_id
3067 return Column(
3073 return Column(
3068 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3074 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3069 nullable=False)
3075 nullable=False)
3070
3076
3071 target_ref = Column('other_ref', Unicode(255), nullable=False)
3077 target_ref = Column('other_ref', Unicode(255), nullable=False)
3072 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3078 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3073
3079
3074 # TODO: dan: rename column to last_merge_source_rev
3080 # TODO: dan: rename column to last_merge_source_rev
3075 _last_merge_source_rev = Column(
3081 _last_merge_source_rev = Column(
3076 'last_merge_org_rev', String(40), nullable=True)
3082 'last_merge_org_rev', String(40), nullable=True)
3077 # TODO: dan: rename column to last_merge_target_rev
3083 # TODO: dan: rename column to last_merge_target_rev
3078 _last_merge_target_rev = Column(
3084 _last_merge_target_rev = Column(
3079 'last_merge_other_rev', String(40), nullable=True)
3085 'last_merge_other_rev', String(40), nullable=True)
3080 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3086 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3081 merge_rev = Column('merge_rev', String(40), nullable=True)
3087 merge_rev = Column('merge_rev', String(40), nullable=True)
3082
3088
3083 @hybrid_property
3089 @hybrid_property
3084 def revisions(self):
3090 def revisions(self):
3085 return self._revisions.split(':') if self._revisions else []
3091 return self._revisions.split(':') if self._revisions else []
3086
3092
3087 @revisions.setter
3093 @revisions.setter
3088 def revisions(self, val):
3094 def revisions(self, val):
3089 self._revisions = ':'.join(val)
3095 self._revisions = ':'.join(val)
3090
3096
3091 @declared_attr
3097 @declared_attr
3092 def author(cls):
3098 def author(cls):
3093 return relationship('User', lazy='joined')
3099 return relationship('User', lazy='joined')
3094
3100
3095 @declared_attr
3101 @declared_attr
3096 def source_repo(cls):
3102 def source_repo(cls):
3097 return relationship(
3103 return relationship(
3098 'Repository',
3104 'Repository',
3099 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3105 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3100
3106
3101 @property
3107 @property
3102 def source_ref_parts(self):
3108 def source_ref_parts(self):
3103 return self.unicode_to_reference(self.source_ref)
3109 return self.unicode_to_reference(self.source_ref)
3104
3110
3105 @declared_attr
3111 @declared_attr
3106 def target_repo(cls):
3112 def target_repo(cls):
3107 return relationship(
3113 return relationship(
3108 'Repository',
3114 'Repository',
3109 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3115 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3110
3116
3111 @property
3117 @property
3112 def target_ref_parts(self):
3118 def target_ref_parts(self):
3113 return self.unicode_to_reference(self.target_ref)
3119 return self.unicode_to_reference(self.target_ref)
3114
3120
3115 @property
3121 @property
3116 def shadow_merge_ref(self):
3122 def shadow_merge_ref(self):
3117 return self.unicode_to_reference(self._shadow_merge_ref)
3123 return self.unicode_to_reference(self._shadow_merge_ref)
3118
3124
3119 @shadow_merge_ref.setter
3125 @shadow_merge_ref.setter
3120 def shadow_merge_ref(self, ref):
3126 def shadow_merge_ref(self, ref):
3121 self._shadow_merge_ref = self.reference_to_unicode(ref)
3127 self._shadow_merge_ref = self.reference_to_unicode(ref)
3122
3128
3123 def unicode_to_reference(self, raw):
3129 def unicode_to_reference(self, raw):
3124 """
3130 """
3125 Convert a unicode (or string) to a reference object.
3131 Convert a unicode (or string) to a reference object.
3126 If unicode evaluates to False it returns None.
3132 If unicode evaluates to False it returns None.
3127 """
3133 """
3128 if raw:
3134 if raw:
3129 refs = raw.split(':')
3135 refs = raw.split(':')
3130 return Reference(*refs)
3136 return Reference(*refs)
3131 else:
3137 else:
3132 return None
3138 return None
3133
3139
3134 def reference_to_unicode(self, ref):
3140 def reference_to_unicode(self, ref):
3135 """
3141 """
3136 Convert a reference object to unicode.
3142 Convert a reference object to unicode.
3137 If reference is None it returns None.
3143 If reference is None it returns None.
3138 """
3144 """
3139 if ref:
3145 if ref:
3140 return u':'.join(ref)
3146 return u':'.join(ref)
3141 else:
3147 else:
3142 return None
3148 return None
3143
3149
3144 def get_api_data(self):
3150 def get_api_data(self):
3145 from rhodecode.model.pull_request import PullRequestModel
3151 from rhodecode.model.pull_request import PullRequestModel
3146 pull_request = self
3152 pull_request = self
3147 merge_status = PullRequestModel().merge_status(pull_request)
3153 merge_status = PullRequestModel().merge_status(pull_request)
3148
3154
3149 pull_request_url = url(
3155 pull_request_url = url(
3150 'pullrequest_show', repo_name=self.target_repo.repo_name,
3156 'pullrequest_show', repo_name=self.target_repo.repo_name,
3151 pull_request_id=self.pull_request_id, qualified=True)
3157 pull_request_id=self.pull_request_id, qualified=True)
3152
3158
3153 merge_data = {
3159 merge_data = {
3154 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3160 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3155 'reference': (
3161 'reference': (
3156 pull_request.shadow_merge_ref._asdict()
3162 pull_request.shadow_merge_ref._asdict()
3157 if pull_request.shadow_merge_ref else None),
3163 if pull_request.shadow_merge_ref else None),
3158 }
3164 }
3159
3165
3160 data = {
3166 data = {
3161 'pull_request_id': pull_request.pull_request_id,
3167 'pull_request_id': pull_request.pull_request_id,
3162 'url': pull_request_url,
3168 'url': pull_request_url,
3163 'title': pull_request.title,
3169 'title': pull_request.title,
3164 'description': pull_request.description,
3170 'description': pull_request.description,
3165 'status': pull_request.status,
3171 'status': pull_request.status,
3166 'created_on': pull_request.created_on,
3172 'created_on': pull_request.created_on,
3167 'updated_on': pull_request.updated_on,
3173 'updated_on': pull_request.updated_on,
3168 'commit_ids': pull_request.revisions,
3174 'commit_ids': pull_request.revisions,
3169 'review_status': pull_request.calculated_review_status(),
3175 'review_status': pull_request.calculated_review_status(),
3170 'mergeable': {
3176 'mergeable': {
3171 'status': merge_status[0],
3177 'status': merge_status[0],
3172 'message': unicode(merge_status[1]),
3178 'message': unicode(merge_status[1]),
3173 },
3179 },
3174 'source': {
3180 'source': {
3175 'clone_url': pull_request.source_repo.clone_url(),
3181 'clone_url': pull_request.source_repo.clone_url(),
3176 'repository': pull_request.source_repo.repo_name,
3182 'repository': pull_request.source_repo.repo_name,
3177 'reference': {
3183 'reference': {
3178 'name': pull_request.source_ref_parts.name,
3184 'name': pull_request.source_ref_parts.name,
3179 'type': pull_request.source_ref_parts.type,
3185 'type': pull_request.source_ref_parts.type,
3180 'commit_id': pull_request.source_ref_parts.commit_id,
3186 'commit_id': pull_request.source_ref_parts.commit_id,
3181 },
3187 },
3182 },
3188 },
3183 'target': {
3189 'target': {
3184 'clone_url': pull_request.target_repo.clone_url(),
3190 'clone_url': pull_request.target_repo.clone_url(),
3185 'repository': pull_request.target_repo.repo_name,
3191 'repository': pull_request.target_repo.repo_name,
3186 'reference': {
3192 'reference': {
3187 'name': pull_request.target_ref_parts.name,
3193 'name': pull_request.target_ref_parts.name,
3188 'type': pull_request.target_ref_parts.type,
3194 'type': pull_request.target_ref_parts.type,
3189 'commit_id': pull_request.target_ref_parts.commit_id,
3195 'commit_id': pull_request.target_ref_parts.commit_id,
3190 },
3196 },
3191 },
3197 },
3192 'merge': merge_data,
3198 'merge': merge_data,
3193 'author': pull_request.author.get_api_data(include_secrets=False,
3199 'author': pull_request.author.get_api_data(include_secrets=False,
3194 details='basic'),
3200 details='basic'),
3195 'reviewers': [
3201 'reviewers': [
3196 {
3202 {
3197 'user': reviewer.get_api_data(include_secrets=False,
3203 'user': reviewer.get_api_data(include_secrets=False,
3198 details='basic'),
3204 details='basic'),
3199 'reasons': reasons,
3205 'reasons': reasons,
3200 'review_status': st[0][1].status if st else 'not_reviewed',
3206 'review_status': st[0][1].status if st else 'not_reviewed',
3201 }
3207 }
3202 for reviewer, reasons, st in pull_request.reviewers_statuses()
3208 for reviewer, reasons, st in pull_request.reviewers_statuses()
3203 ]
3209 ]
3204 }
3210 }
3205
3211
3206 return data
3212 return data
3207
3213
3208
3214
3209 class PullRequest(Base, _PullRequestBase):
3215 class PullRequest(Base, _PullRequestBase):
3210 __tablename__ = 'pull_requests'
3216 __tablename__ = 'pull_requests'
3211 __table_args__ = (
3217 __table_args__ = (
3212 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3218 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3213 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3219 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3214 )
3220 )
3215
3221
3216 pull_request_id = Column(
3222 pull_request_id = Column(
3217 'pull_request_id', Integer(), nullable=False, primary_key=True)
3223 'pull_request_id', Integer(), nullable=False, primary_key=True)
3218
3224
3219 def __repr__(self):
3225 def __repr__(self):
3220 if self.pull_request_id:
3226 if self.pull_request_id:
3221 return '<DB:PullRequest #%s>' % self.pull_request_id
3227 return '<DB:PullRequest #%s>' % self.pull_request_id
3222 else:
3228 else:
3223 return '<DB:PullRequest at %#x>' % id(self)
3229 return '<DB:PullRequest at %#x>' % id(self)
3224
3230
3225 reviewers = relationship('PullRequestReviewers',
3231 reviewers = relationship('PullRequestReviewers',
3226 cascade="all, delete, delete-orphan")
3232 cascade="all, delete, delete-orphan")
3227 statuses = relationship('ChangesetStatus')
3233 statuses = relationship('ChangesetStatus')
3228 comments = relationship('ChangesetComment',
3234 comments = relationship('ChangesetComment',
3229 cascade="all, delete, delete-orphan")
3235 cascade="all, delete, delete-orphan")
3230 versions = relationship('PullRequestVersion',
3236 versions = relationship('PullRequestVersion',
3231 cascade="all, delete, delete-orphan",
3237 cascade="all, delete, delete-orphan",
3232 lazy='dynamic')
3238 lazy='dynamic')
3233
3239
3234
3240
3235 @classmethod
3241 @classmethod
3236 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3242 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3237 internal_methods=None):
3243 internal_methods=None):
3238
3244
3239 class PullRequestDisplay(object):
3245 class PullRequestDisplay(object):
3240 """
3246 """
3241 Special object wrapper for showing PullRequest data via Versions
3247 Special object wrapper for showing PullRequest data via Versions
3242 It mimics PR object as close as possible. This is read only object
3248 It mimics PR object as close as possible. This is read only object
3243 just for display
3249 just for display
3244 """
3250 """
3245
3251
3246 def __init__(self, attrs, internal=None):
3252 def __init__(self, attrs, internal=None):
3247 self.attrs = attrs
3253 self.attrs = attrs
3248 # internal have priority over the given ones via attrs
3254 # internal have priority over the given ones via attrs
3249 self.internal = internal or ['versions']
3255 self.internal = internal or ['versions']
3250
3256
3251 def __getattr__(self, item):
3257 def __getattr__(self, item):
3252 if item in self.internal:
3258 if item in self.internal:
3253 return getattr(self, item)
3259 return getattr(self, item)
3254 try:
3260 try:
3255 return self.attrs[item]
3261 return self.attrs[item]
3256 except KeyError:
3262 except KeyError:
3257 raise AttributeError(
3263 raise AttributeError(
3258 '%s object has no attribute %s' % (self, item))
3264 '%s object has no attribute %s' % (self, item))
3259
3265
3260 def __repr__(self):
3266 def __repr__(self):
3261 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3267 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3262
3268
3263 def versions(self):
3269 def versions(self):
3264 return pull_request_obj.versions.order_by(
3270 return pull_request_obj.versions.order_by(
3265 PullRequestVersion.pull_request_version_id).all()
3271 PullRequestVersion.pull_request_version_id).all()
3266
3272
3267 def is_closed(self):
3273 def is_closed(self):
3268 return pull_request_obj.is_closed()
3274 return pull_request_obj.is_closed()
3269
3275
3270 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3276 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3271
3277
3272 attrs.author = StrictAttributeDict(
3278 attrs.author = StrictAttributeDict(
3273 pull_request_obj.author.get_api_data())
3279 pull_request_obj.author.get_api_data())
3274 if pull_request_obj.target_repo:
3280 if pull_request_obj.target_repo:
3275 attrs.target_repo = StrictAttributeDict(
3281 attrs.target_repo = StrictAttributeDict(
3276 pull_request_obj.target_repo.get_api_data())
3282 pull_request_obj.target_repo.get_api_data())
3277 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3283 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3278
3284
3279 if pull_request_obj.source_repo:
3285 if pull_request_obj.source_repo:
3280 attrs.source_repo = StrictAttributeDict(
3286 attrs.source_repo = StrictAttributeDict(
3281 pull_request_obj.source_repo.get_api_data())
3287 pull_request_obj.source_repo.get_api_data())
3282 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3288 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3283
3289
3284 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3290 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3285 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3291 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3286 attrs.revisions = pull_request_obj.revisions
3292 attrs.revisions = pull_request_obj.revisions
3287
3293
3288 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3294 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3289
3295
3290 return PullRequestDisplay(attrs, internal=internal_methods)
3296 return PullRequestDisplay(attrs, internal=internal_methods)
3291
3297
3292 def is_closed(self):
3298 def is_closed(self):
3293 return self.status == self.STATUS_CLOSED
3299 return self.status == self.STATUS_CLOSED
3294
3300
3295 def __json__(self):
3301 def __json__(self):
3296 return {
3302 return {
3297 'revisions': self.revisions,
3303 'revisions': self.revisions,
3298 }
3304 }
3299
3305
3300 def calculated_review_status(self):
3306 def calculated_review_status(self):
3301 from rhodecode.model.changeset_status import ChangesetStatusModel
3307 from rhodecode.model.changeset_status import ChangesetStatusModel
3302 return ChangesetStatusModel().calculated_review_status(self)
3308 return ChangesetStatusModel().calculated_review_status(self)
3303
3309
3304 def reviewers_statuses(self):
3310 def reviewers_statuses(self):
3305 from rhodecode.model.changeset_status import ChangesetStatusModel
3311 from rhodecode.model.changeset_status import ChangesetStatusModel
3306 return ChangesetStatusModel().reviewers_statuses(self)
3312 return ChangesetStatusModel().reviewers_statuses(self)
3307
3313
3308
3314
3309 class PullRequestVersion(Base, _PullRequestBase):
3315 class PullRequestVersion(Base, _PullRequestBase):
3310 __tablename__ = 'pull_request_versions'
3316 __tablename__ = 'pull_request_versions'
3311 __table_args__ = (
3317 __table_args__ = (
3312 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3318 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3313 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3319 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3314 )
3320 )
3315
3321
3316 pull_request_version_id = Column(
3322 pull_request_version_id = Column(
3317 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3323 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3318 pull_request_id = Column(
3324 pull_request_id = Column(
3319 'pull_request_id', Integer(),
3325 'pull_request_id', Integer(),
3320 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3326 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3321 pull_request = relationship('PullRequest')
3327 pull_request = relationship('PullRequest')
3322
3328
3323 def __repr__(self):
3329 def __repr__(self):
3324 if self.pull_request_version_id:
3330 if self.pull_request_version_id:
3325 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3331 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3326 else:
3332 else:
3327 return '<DB:PullRequestVersion at %#x>' % id(self)
3333 return '<DB:PullRequestVersion at %#x>' % id(self)
3328
3334
3329 @property
3335 @property
3330 def reviewers(self):
3336 def reviewers(self):
3331 return self.pull_request.reviewers
3337 return self.pull_request.reviewers
3332
3338
3333 @property
3339 @property
3334 def versions(self):
3340 def versions(self):
3335 return self.pull_request.versions
3341 return self.pull_request.versions
3336
3342
3337 def is_closed(self):
3343 def is_closed(self):
3338 # calculate from original
3344 # calculate from original
3339 return self.pull_request.status == self.STATUS_CLOSED
3345 return self.pull_request.status == self.STATUS_CLOSED
3340
3346
3341 def calculated_review_status(self):
3347 def calculated_review_status(self):
3342 return self.pull_request.calculated_review_status()
3348 return self.pull_request.calculated_review_status()
3343
3349
3344 def reviewers_statuses(self):
3350 def reviewers_statuses(self):
3345 return self.pull_request.reviewers_statuses()
3351 return self.pull_request.reviewers_statuses()
3346
3352
3347
3353
3348 class PullRequestReviewers(Base, BaseModel):
3354 class PullRequestReviewers(Base, BaseModel):
3349 __tablename__ = 'pull_request_reviewers'
3355 __tablename__ = 'pull_request_reviewers'
3350 __table_args__ = (
3356 __table_args__ = (
3351 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3357 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3352 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3358 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3353 )
3359 )
3354
3360
3355 def __init__(self, user=None, pull_request=None, reasons=None):
3361 def __init__(self, user=None, pull_request=None, reasons=None):
3356 self.user = user
3362 self.user = user
3357 self.pull_request = pull_request
3363 self.pull_request = pull_request
3358 self.reasons = reasons or []
3364 self.reasons = reasons or []
3359
3365
3360 @hybrid_property
3366 @hybrid_property
3361 def reasons(self):
3367 def reasons(self):
3362 if not self._reasons:
3368 if not self._reasons:
3363 return []
3369 return []
3364 return self._reasons
3370 return self._reasons
3365
3371
3366 @reasons.setter
3372 @reasons.setter
3367 def reasons(self, val):
3373 def reasons(self, val):
3368 val = val or []
3374 val = val or []
3369 if any(not isinstance(x, basestring) for x in val):
3375 if any(not isinstance(x, basestring) for x in val):
3370 raise Exception('invalid reasons type, must be list of strings')
3376 raise Exception('invalid reasons type, must be list of strings')
3371 self._reasons = val
3377 self._reasons = val
3372
3378
3373 pull_requests_reviewers_id = Column(
3379 pull_requests_reviewers_id = Column(
3374 'pull_requests_reviewers_id', Integer(), nullable=False,
3380 'pull_requests_reviewers_id', Integer(), nullable=False,
3375 primary_key=True)
3381 primary_key=True)
3376 pull_request_id = Column(
3382 pull_request_id = Column(
3377 "pull_request_id", Integer(),
3383 "pull_request_id", Integer(),
3378 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3384 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3379 user_id = Column(
3385 user_id = Column(
3380 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3386 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3381 _reasons = Column(
3387 _reasons = Column(
3382 'reason', MutationList.as_mutable(
3388 'reason', MutationList.as_mutable(
3383 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3389 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3384
3390
3385 user = relationship('User')
3391 user = relationship('User')
3386 pull_request = relationship('PullRequest')
3392 pull_request = relationship('PullRequest')
3387
3393
3388
3394
3389 class Notification(Base, BaseModel):
3395 class Notification(Base, BaseModel):
3390 __tablename__ = 'notifications'
3396 __tablename__ = 'notifications'
3391 __table_args__ = (
3397 __table_args__ = (
3392 Index('notification_type_idx', 'type'),
3398 Index('notification_type_idx', 'type'),
3393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3399 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3394 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3400 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3395 )
3401 )
3396
3402
3397 TYPE_CHANGESET_COMMENT = u'cs_comment'
3403 TYPE_CHANGESET_COMMENT = u'cs_comment'
3398 TYPE_MESSAGE = u'message'
3404 TYPE_MESSAGE = u'message'
3399 TYPE_MENTION = u'mention'
3405 TYPE_MENTION = u'mention'
3400 TYPE_REGISTRATION = u'registration'
3406 TYPE_REGISTRATION = u'registration'
3401 TYPE_PULL_REQUEST = u'pull_request'
3407 TYPE_PULL_REQUEST = u'pull_request'
3402 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3408 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3403
3409
3404 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3410 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3405 subject = Column('subject', Unicode(512), nullable=True)
3411 subject = Column('subject', Unicode(512), nullable=True)
3406 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3412 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3407 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3413 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3408 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3414 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3409 type_ = Column('type', Unicode(255))
3415 type_ = Column('type', Unicode(255))
3410
3416
3411 created_by_user = relationship('User')
3417 created_by_user = relationship('User')
3412 notifications_to_users = relationship('UserNotification', lazy='joined',
3418 notifications_to_users = relationship('UserNotification', lazy='joined',
3413 cascade="all, delete, delete-orphan")
3419 cascade="all, delete, delete-orphan")
3414
3420
3415 @property
3421 @property
3416 def recipients(self):
3422 def recipients(self):
3417 return [x.user for x in UserNotification.query()\
3423 return [x.user for x in UserNotification.query()\
3418 .filter(UserNotification.notification == self)\
3424 .filter(UserNotification.notification == self)\
3419 .order_by(UserNotification.user_id.asc()).all()]
3425 .order_by(UserNotification.user_id.asc()).all()]
3420
3426
3421 @classmethod
3427 @classmethod
3422 def create(cls, created_by, subject, body, recipients, type_=None):
3428 def create(cls, created_by, subject, body, recipients, type_=None):
3423 if type_ is None:
3429 if type_ is None:
3424 type_ = Notification.TYPE_MESSAGE
3430 type_ = Notification.TYPE_MESSAGE
3425
3431
3426 notification = cls()
3432 notification = cls()
3427 notification.created_by_user = created_by
3433 notification.created_by_user = created_by
3428 notification.subject = subject
3434 notification.subject = subject
3429 notification.body = body
3435 notification.body = body
3430 notification.type_ = type_
3436 notification.type_ = type_
3431 notification.created_on = datetime.datetime.now()
3437 notification.created_on = datetime.datetime.now()
3432
3438
3433 for u in recipients:
3439 for u in recipients:
3434 assoc = UserNotification()
3440 assoc = UserNotification()
3435 assoc.notification = notification
3441 assoc.notification = notification
3436
3442
3437 # if created_by is inside recipients mark his notification
3443 # if created_by is inside recipients mark his notification
3438 # as read
3444 # as read
3439 if u.user_id == created_by.user_id:
3445 if u.user_id == created_by.user_id:
3440 assoc.read = True
3446 assoc.read = True
3441
3447
3442 u.notifications.append(assoc)
3448 u.notifications.append(assoc)
3443 Session().add(notification)
3449 Session().add(notification)
3444
3450
3445 return notification
3451 return notification
3446
3452
3447 @property
3453 @property
3448 def description(self):
3454 def description(self):
3449 from rhodecode.model.notification import NotificationModel
3455 from rhodecode.model.notification import NotificationModel
3450 return NotificationModel().make_description(self)
3456 return NotificationModel().make_description(self)
3451
3457
3452
3458
3453 class UserNotification(Base, BaseModel):
3459 class UserNotification(Base, BaseModel):
3454 __tablename__ = 'user_to_notification'
3460 __tablename__ = 'user_to_notification'
3455 __table_args__ = (
3461 __table_args__ = (
3456 UniqueConstraint('user_id', 'notification_id'),
3462 UniqueConstraint('user_id', 'notification_id'),
3457 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3463 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3458 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3464 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3459 )
3465 )
3460 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3466 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3461 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3467 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3462 read = Column('read', Boolean, default=False)
3468 read = Column('read', Boolean, default=False)
3463 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3469 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3464
3470
3465 user = relationship('User', lazy="joined")
3471 user = relationship('User', lazy="joined")
3466 notification = relationship('Notification', lazy="joined",
3472 notification = relationship('Notification', lazy="joined",
3467 order_by=lambda: Notification.created_on.desc(),)
3473 order_by=lambda: Notification.created_on.desc(),)
3468
3474
3469 def mark_as_read(self):
3475 def mark_as_read(self):
3470 self.read = True
3476 self.read = True
3471 Session().add(self)
3477 Session().add(self)
3472
3478
3473
3479
3474 class Gist(Base, BaseModel):
3480 class Gist(Base, BaseModel):
3475 __tablename__ = 'gists'
3481 __tablename__ = 'gists'
3476 __table_args__ = (
3482 __table_args__ = (
3477 Index('g_gist_access_id_idx', 'gist_access_id'),
3483 Index('g_gist_access_id_idx', 'gist_access_id'),
3478 Index('g_created_on_idx', 'created_on'),
3484 Index('g_created_on_idx', 'created_on'),
3479 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3485 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3480 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3486 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3481 )
3487 )
3482 GIST_PUBLIC = u'public'
3488 GIST_PUBLIC = u'public'
3483 GIST_PRIVATE = u'private'
3489 GIST_PRIVATE = u'private'
3484 DEFAULT_FILENAME = u'gistfile1.txt'
3490 DEFAULT_FILENAME = u'gistfile1.txt'
3485
3491
3486 ACL_LEVEL_PUBLIC = u'acl_public'
3492 ACL_LEVEL_PUBLIC = u'acl_public'
3487 ACL_LEVEL_PRIVATE = u'acl_private'
3493 ACL_LEVEL_PRIVATE = u'acl_private'
3488
3494
3489 gist_id = Column('gist_id', Integer(), primary_key=True)
3495 gist_id = Column('gist_id', Integer(), primary_key=True)
3490 gist_access_id = Column('gist_access_id', Unicode(250))
3496 gist_access_id = Column('gist_access_id', Unicode(250))
3491 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3497 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3492 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3498 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3493 gist_expires = Column('gist_expires', Float(53), nullable=False)
3499 gist_expires = Column('gist_expires', Float(53), nullable=False)
3494 gist_type = Column('gist_type', Unicode(128), nullable=False)
3500 gist_type = Column('gist_type', Unicode(128), nullable=False)
3495 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3501 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3496 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3502 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3497 acl_level = Column('acl_level', Unicode(128), nullable=True)
3503 acl_level = Column('acl_level', Unicode(128), nullable=True)
3498
3504
3499 owner = relationship('User')
3505 owner = relationship('User')
3500
3506
3501 def __repr__(self):
3507 def __repr__(self):
3502 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3508 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3503
3509
3504 @classmethod
3510 @classmethod
3505 def get_or_404(cls, id_):
3511 def get_or_404(cls, id_):
3506 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3512 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3507 if not res:
3513 if not res:
3508 raise HTTPNotFound
3514 raise HTTPNotFound
3509 return res
3515 return res
3510
3516
3511 @classmethod
3517 @classmethod
3512 def get_by_access_id(cls, gist_access_id):
3518 def get_by_access_id(cls, gist_access_id):
3513 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3519 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3514
3520
3515 def gist_url(self):
3521 def gist_url(self):
3516 import rhodecode
3522 import rhodecode
3517 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3523 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3518 if alias_url:
3524 if alias_url:
3519 return alias_url.replace('{gistid}', self.gist_access_id)
3525 return alias_url.replace('{gistid}', self.gist_access_id)
3520
3526
3521 return url('gist', gist_id=self.gist_access_id, qualified=True)
3527 return url('gist', gist_id=self.gist_access_id, qualified=True)
3522
3528
3523 @classmethod
3529 @classmethod
3524 def base_path(cls):
3530 def base_path(cls):
3525 """
3531 """
3526 Returns base path when all gists are stored
3532 Returns base path when all gists are stored
3527
3533
3528 :param cls:
3534 :param cls:
3529 """
3535 """
3530 from rhodecode.model.gist import GIST_STORE_LOC
3536 from rhodecode.model.gist import GIST_STORE_LOC
3531 q = Session().query(RhodeCodeUi)\
3537 q = Session().query(RhodeCodeUi)\
3532 .filter(RhodeCodeUi.ui_key == URL_SEP)
3538 .filter(RhodeCodeUi.ui_key == URL_SEP)
3533 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3539 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3534 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3540 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3535
3541
3536 def get_api_data(self):
3542 def get_api_data(self):
3537 """
3543 """
3538 Common function for generating gist related data for API
3544 Common function for generating gist related data for API
3539 """
3545 """
3540 gist = self
3546 gist = self
3541 data = {
3547 data = {
3542 'gist_id': gist.gist_id,
3548 'gist_id': gist.gist_id,
3543 'type': gist.gist_type,
3549 'type': gist.gist_type,
3544 'access_id': gist.gist_access_id,
3550 'access_id': gist.gist_access_id,
3545 'description': gist.gist_description,
3551 'description': gist.gist_description,
3546 'url': gist.gist_url(),
3552 'url': gist.gist_url(),
3547 'expires': gist.gist_expires,
3553 'expires': gist.gist_expires,
3548 'created_on': gist.created_on,
3554 'created_on': gist.created_on,
3549 'modified_at': gist.modified_at,
3555 'modified_at': gist.modified_at,
3550 'content': None,
3556 'content': None,
3551 'acl_level': gist.acl_level,
3557 'acl_level': gist.acl_level,
3552 }
3558 }
3553 return data
3559 return data
3554
3560
3555 def __json__(self):
3561 def __json__(self):
3556 data = dict(
3562 data = dict(
3557 )
3563 )
3558 data.update(self.get_api_data())
3564 data.update(self.get_api_data())
3559 return data
3565 return data
3560 # SCM functions
3566 # SCM functions
3561
3567
3562 def scm_instance(self, **kwargs):
3568 def scm_instance(self, **kwargs):
3563 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3569 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3564 return get_vcs_instance(
3570 return get_vcs_instance(
3565 repo_path=safe_str(full_repo_path), create=False)
3571 repo_path=safe_str(full_repo_path), create=False)
3566
3572
3567
3573
3568 class ExternalIdentity(Base, BaseModel):
3574 class ExternalIdentity(Base, BaseModel):
3569 __tablename__ = 'external_identities'
3575 __tablename__ = 'external_identities'
3570 __table_args__ = (
3576 __table_args__ = (
3571 Index('local_user_id_idx', 'local_user_id'),
3577 Index('local_user_id_idx', 'local_user_id'),
3572 Index('external_id_idx', 'external_id'),
3578 Index('external_id_idx', 'external_id'),
3573 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3574 'mysql_charset': 'utf8'})
3580 'mysql_charset': 'utf8'})
3575
3581
3576 external_id = Column('external_id', Unicode(255), default=u'',
3582 external_id = Column('external_id', Unicode(255), default=u'',
3577 primary_key=True)
3583 primary_key=True)
3578 external_username = Column('external_username', Unicode(1024), default=u'')
3584 external_username = Column('external_username', Unicode(1024), default=u'')
3579 local_user_id = Column('local_user_id', Integer(),
3585 local_user_id = Column('local_user_id', Integer(),
3580 ForeignKey('users.user_id'), primary_key=True)
3586 ForeignKey('users.user_id'), primary_key=True)
3581 provider_name = Column('provider_name', Unicode(255), default=u'',
3587 provider_name = Column('provider_name', Unicode(255), default=u'',
3582 primary_key=True)
3588 primary_key=True)
3583 access_token = Column('access_token', String(1024), default=u'')
3589 access_token = Column('access_token', String(1024), default=u'')
3584 alt_token = Column('alt_token', String(1024), default=u'')
3590 alt_token = Column('alt_token', String(1024), default=u'')
3585 token_secret = Column('token_secret', String(1024), default=u'')
3591 token_secret = Column('token_secret', String(1024), default=u'')
3586
3592
3587 @classmethod
3593 @classmethod
3588 def by_external_id_and_provider(cls, external_id, provider_name,
3594 def by_external_id_and_provider(cls, external_id, provider_name,
3589 local_user_id=None):
3595 local_user_id=None):
3590 """
3596 """
3591 Returns ExternalIdentity instance based on search params
3597 Returns ExternalIdentity instance based on search params
3592
3598
3593 :param external_id:
3599 :param external_id:
3594 :param provider_name:
3600 :param provider_name:
3595 :return: ExternalIdentity
3601 :return: ExternalIdentity
3596 """
3602 """
3597 query = cls.query()
3603 query = cls.query()
3598 query = query.filter(cls.external_id == external_id)
3604 query = query.filter(cls.external_id == external_id)
3599 query = query.filter(cls.provider_name == provider_name)
3605 query = query.filter(cls.provider_name == provider_name)
3600 if local_user_id:
3606 if local_user_id:
3601 query = query.filter(cls.local_user_id == local_user_id)
3607 query = query.filter(cls.local_user_id == local_user_id)
3602 return query.first()
3608 return query.first()
3603
3609
3604 @classmethod
3610 @classmethod
3605 def user_by_external_id_and_provider(cls, external_id, provider_name):
3611 def user_by_external_id_and_provider(cls, external_id, provider_name):
3606 """
3612 """
3607 Returns User instance based on search params
3613 Returns User instance based on search params
3608
3614
3609 :param external_id:
3615 :param external_id:
3610 :param provider_name:
3616 :param provider_name:
3611 :return: User
3617 :return: User
3612 """
3618 """
3613 query = User.query()
3619 query = User.query()
3614 query = query.filter(cls.external_id == external_id)
3620 query = query.filter(cls.external_id == external_id)
3615 query = query.filter(cls.provider_name == provider_name)
3621 query = query.filter(cls.provider_name == provider_name)
3616 query = query.filter(User.user_id == cls.local_user_id)
3622 query = query.filter(User.user_id == cls.local_user_id)
3617 return query.first()
3623 return query.first()
3618
3624
3619 @classmethod
3625 @classmethod
3620 def by_local_user_id(cls, local_user_id):
3626 def by_local_user_id(cls, local_user_id):
3621 """
3627 """
3622 Returns all tokens for user
3628 Returns all tokens for user
3623
3629
3624 :param local_user_id:
3630 :param local_user_id:
3625 :return: ExternalIdentity
3631 :return: ExternalIdentity
3626 """
3632 """
3627 query = cls.query()
3633 query = cls.query()
3628 query = query.filter(cls.local_user_id == local_user_id)
3634 query = query.filter(cls.local_user_id == local_user_id)
3629 return query
3635 return query
3630
3636
3631
3637
3632 class Integration(Base, BaseModel):
3638 class Integration(Base, BaseModel):
3633 __tablename__ = 'integrations'
3639 __tablename__ = 'integrations'
3634 __table_args__ = (
3640 __table_args__ = (
3635 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3641 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3636 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3642 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3637 )
3643 )
3638
3644
3639 integration_id = Column('integration_id', Integer(), primary_key=True)
3645 integration_id = Column('integration_id', Integer(), primary_key=True)
3640 integration_type = Column('integration_type', String(255))
3646 integration_type = Column('integration_type', String(255))
3641 enabled = Column('enabled', Boolean(), nullable=False)
3647 enabled = Column('enabled', Boolean(), nullable=False)
3642 name = Column('name', String(255), nullable=False)
3648 name = Column('name', String(255), nullable=False)
3643 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3649 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3644 default=False)
3650 default=False)
3645
3651
3646 settings = Column(
3652 settings = Column(
3647 'settings_json', MutationObj.as_mutable(
3653 'settings_json', MutationObj.as_mutable(
3648 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3654 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3649 repo_id = Column(
3655 repo_id = Column(
3650 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3656 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3651 nullable=True, unique=None, default=None)
3657 nullable=True, unique=None, default=None)
3652 repo = relationship('Repository', lazy='joined')
3658 repo = relationship('Repository', lazy='joined')
3653
3659
3654 repo_group_id = Column(
3660 repo_group_id = Column(
3655 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3661 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3656 nullable=True, unique=None, default=None)
3662 nullable=True, unique=None, default=None)
3657 repo_group = relationship('RepoGroup', lazy='joined')
3663 repo_group = relationship('RepoGroup', lazy='joined')
3658
3664
3659 @property
3665 @property
3660 def scope(self):
3666 def scope(self):
3661 if self.repo:
3667 if self.repo:
3662 return repr(self.repo)
3668 return repr(self.repo)
3663 if self.repo_group:
3669 if self.repo_group:
3664 if self.child_repos_only:
3670 if self.child_repos_only:
3665 return repr(self.repo_group) + ' (child repos only)'
3671 return repr(self.repo_group) + ' (child repos only)'
3666 else:
3672 else:
3667 return repr(self.repo_group) + ' (recursive)'
3673 return repr(self.repo_group) + ' (recursive)'
3668 if self.child_repos_only:
3674 if self.child_repos_only:
3669 return 'root_repos'
3675 return 'root_repos'
3670 return 'global'
3676 return 'global'
3671
3677
3672 def __repr__(self):
3678 def __repr__(self):
3673 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3679 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3674
3680
3675
3681
3676 class RepoReviewRuleUser(Base, BaseModel):
3682 class RepoReviewRuleUser(Base, BaseModel):
3677 __tablename__ = 'repo_review_rules_users'
3683 __tablename__ = 'repo_review_rules_users'
3678 __table_args__ = (
3684 __table_args__ = (
3679 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3685 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3680 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3686 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3681 )
3687 )
3682 repo_review_rule_user_id = Column(
3688 repo_review_rule_user_id = Column(
3683 'repo_review_rule_user_id', Integer(), primary_key=True)
3689 'repo_review_rule_user_id', Integer(), primary_key=True)
3684 repo_review_rule_id = Column("repo_review_rule_id",
3690 repo_review_rule_id = Column("repo_review_rule_id",
3685 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3691 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3686 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3692 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3687 nullable=False)
3693 nullable=False)
3688 user = relationship('User')
3694 user = relationship('User')
3689
3695
3690
3696
3691 class RepoReviewRuleUserGroup(Base, BaseModel):
3697 class RepoReviewRuleUserGroup(Base, BaseModel):
3692 __tablename__ = 'repo_review_rules_users_groups'
3698 __tablename__ = 'repo_review_rules_users_groups'
3693 __table_args__ = (
3699 __table_args__ = (
3694 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3700 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3695 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3701 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3696 )
3702 )
3697 repo_review_rule_users_group_id = Column(
3703 repo_review_rule_users_group_id = Column(
3698 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3704 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3699 repo_review_rule_id = Column("repo_review_rule_id",
3705 repo_review_rule_id = Column("repo_review_rule_id",
3700 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3706 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3701 users_group_id = Column("users_group_id", Integer(),
3707 users_group_id = Column("users_group_id", Integer(),
3702 ForeignKey('users_groups.users_group_id'), nullable=False)
3708 ForeignKey('users_groups.users_group_id'), nullable=False)
3703 users_group = relationship('UserGroup')
3709 users_group = relationship('UserGroup')
3704
3710
3705
3711
3706 class RepoReviewRule(Base, BaseModel):
3712 class RepoReviewRule(Base, BaseModel):
3707 __tablename__ = 'repo_review_rules'
3713 __tablename__ = 'repo_review_rules'
3708 __table_args__ = (
3714 __table_args__ = (
3709 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3715 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3710 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3716 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3711 )
3717 )
3712
3718
3713 repo_review_rule_id = Column(
3719 repo_review_rule_id = Column(
3714 'repo_review_rule_id', Integer(), primary_key=True)
3720 'repo_review_rule_id', Integer(), primary_key=True)
3715 repo_id = Column(
3721 repo_id = Column(
3716 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3722 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3717 repo = relationship('Repository', backref='review_rules')
3723 repo = relationship('Repository', backref='review_rules')
3718
3724
3719 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3725 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3720 default=u'*') # glob
3726 default=u'*') # glob
3721 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3727 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3722 default=u'*') # glob
3728 default=u'*') # glob
3723
3729
3724 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3730 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3725 nullable=False, default=False)
3731 nullable=False, default=False)
3726 rule_users = relationship('RepoReviewRuleUser')
3732 rule_users = relationship('RepoReviewRuleUser')
3727 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3733 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3728
3734
3729 @hybrid_property
3735 @hybrid_property
3730 def branch_pattern(self):
3736 def branch_pattern(self):
3731 return self._branch_pattern or '*'
3737 return self._branch_pattern or '*'
3732
3738
3733 def _validate_glob(self, value):
3739 def _validate_glob(self, value):
3734 re.compile('^' + glob2re(value) + '$')
3740 re.compile('^' + glob2re(value) + '$')
3735
3741
3736 @branch_pattern.setter
3742 @branch_pattern.setter
3737 def branch_pattern(self, value):
3743 def branch_pattern(self, value):
3738 self._validate_glob(value)
3744 self._validate_glob(value)
3739 self._branch_pattern = value or '*'
3745 self._branch_pattern = value or '*'
3740
3746
3741 @hybrid_property
3747 @hybrid_property
3742 def file_pattern(self):
3748 def file_pattern(self):
3743 return self._file_pattern or '*'
3749 return self._file_pattern or '*'
3744
3750
3745 @file_pattern.setter
3751 @file_pattern.setter
3746 def file_pattern(self, value):
3752 def file_pattern(self, value):
3747 self._validate_glob(value)
3753 self._validate_glob(value)
3748 self._file_pattern = value or '*'
3754 self._file_pattern = value or '*'
3749
3755
3750 def matches(self, branch, files_changed):
3756 def matches(self, branch, files_changed):
3751 """
3757 """
3752 Check if this review rule matches a branch/files in a pull request
3758 Check if this review rule matches a branch/files in a pull request
3753
3759
3754 :param branch: branch name for the commit
3760 :param branch: branch name for the commit
3755 :param files_changed: list of file paths changed in the pull request
3761 :param files_changed: list of file paths changed in the pull request
3756 """
3762 """
3757
3763
3758 branch = branch or ''
3764 branch = branch or ''
3759 files_changed = files_changed or []
3765 files_changed = files_changed or []
3760
3766
3761 branch_matches = True
3767 branch_matches = True
3762 if branch:
3768 if branch:
3763 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3769 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3764 branch_matches = bool(branch_regex.search(branch))
3770 branch_matches = bool(branch_regex.search(branch))
3765
3771
3766 files_matches = True
3772 files_matches = True
3767 if self.file_pattern != '*':
3773 if self.file_pattern != '*':
3768 files_matches = False
3774 files_matches = False
3769 file_regex = re.compile(glob2re(self.file_pattern))
3775 file_regex = re.compile(glob2re(self.file_pattern))
3770 for filename in files_changed:
3776 for filename in files_changed:
3771 if file_regex.search(filename):
3777 if file_regex.search(filename):
3772 files_matches = True
3778 files_matches = True
3773 break
3779 break
3774
3780
3775 return branch_matches and files_matches
3781 return branch_matches and files_matches
3776
3782
3777 @property
3783 @property
3778 def review_users(self):
3784 def review_users(self):
3779 """ Returns the users which this rule applies to """
3785 """ Returns the users which this rule applies to """
3780
3786
3781 users = set()
3787 users = set()
3782 users |= set([
3788 users |= set([
3783 rule_user.user for rule_user in self.rule_users
3789 rule_user.user for rule_user in self.rule_users
3784 if rule_user.user.active])
3790 if rule_user.user.active])
3785 users |= set(
3791 users |= set(
3786 member.user
3792 member.user
3787 for rule_user_group in self.rule_user_groups
3793 for rule_user_group in self.rule_user_groups
3788 for member in rule_user_group.users_group.members
3794 for member in rule_user_group.users_group.members
3789 if member.user.active
3795 if member.user.active
3790 )
3796 )
3791 return users
3797 return users
3792
3798
3793 def __repr__(self):
3799 def __repr__(self):
3794 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3800 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3795 self.repo_review_rule_id, self.repo)
3801 self.repo_review_rule_id, self.repo)
3796
3802
3797
3803
3798 class DbMigrateVersion(Base, BaseModel):
3804 class DbMigrateVersion(Base, BaseModel):
3799 __tablename__ = 'db_migrate_version'
3805 __tablename__ = 'db_migrate_version'
3800 __table_args__ = (
3806 __table_args__ = (
3801 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3807 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3802 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3808 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3803 )
3809 )
3804 repository_id = Column('repository_id', String(250), primary_key=True)
3810 repository_id = Column('repository_id', String(250), primary_key=True)
3805 repository_path = Column('repository_path', Text)
3811 repository_path = Column('repository_path', Text)
3806 version = Column('version', Integer)
3812 version = Column('version', Integer)
3807
3813
3808
3814
3809 class DbSession(Base, BaseModel):
3815 class DbSession(Base, BaseModel):
3810 __tablename__ = 'db_session'
3816 __tablename__ = 'db_session'
3811 __table_args__ = (
3817 __table_args__ = (
3812 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3818 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3813 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3819 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3814 )
3820 )
3815
3821
3816 def __repr__(self):
3822 def __repr__(self):
3817 return '<DB:DbSession({})>'.format(self.id)
3823 return '<DB:DbSession({})>'.format(self.id)
3818
3824
3819 id = Column('id', Integer())
3825 id = Column('id', Integer())
3820 namespace = Column('namespace', String(255), primary_key=True)
3826 namespace = Column('namespace', String(255), primary_key=True)
3821 accessed = Column('accessed', DateTime, nullable=False)
3827 accessed = Column('accessed', DateTime, nullable=False)
3822 created = Column('created', DateTime, nullable=False)
3828 created = Column('created', DateTime, nullable=False)
3823 data = Column('data', PickleType, nullable=False)
3829 data = Column('data', PickleType, nullable=False)
@@ -1,188 +1,196 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import re
21 import re
22
22
23 import colander
23 import colander
24 from rhodecode.model.validation_schema import preparers
24 from rhodecode.model.validation_schema import preparers
25 from rhodecode.model.db import User, UserGroup
25 from rhodecode.model.db import User, UserGroup
26
26
27
27
28 class _RootLocation(object):
28 class _RootLocation(object):
29 pass
29 pass
30
30
31 RootLocation = _RootLocation()
31 RootLocation = _RootLocation()
32
32
33
33
34 def _normalize(seperator, path):
34 def _normalize(seperator, path):
35
35
36 if not path:
36 if not path:
37 return ''
37 return ''
38 elif path is colander.null:
38 elif path is colander.null:
39 return colander.null
39 return colander.null
40
40
41 parts = path.split(seperator)
41 parts = path.split(seperator)
42
42
43 def bad_parts(value):
43 def bad_parts(value):
44 if not value:
44 if not value:
45 return False
45 return False
46 if re.match(r'^[.]+$', value):
46 if re.match(r'^[.]+$', value):
47 return False
47 return False
48
48
49 return True
49 return True
50
50
51 def slugify(value):
51 def slugify(value):
52 value = preparers.slugify_preparer(value)
52 value = preparers.slugify_preparer(value)
53 value = re.sub(r'[.]{2,}', '.', value)
53 value = re.sub(r'[.]{2,}', '.', value)
54 return value
54 return value
55
55
56 clean_parts = [slugify(item) for item in parts if item]
56 clean_parts = [slugify(item) for item in parts if item]
57 path = filter(bad_parts, clean_parts)
57 path = filter(bad_parts, clean_parts)
58 return seperator.join(path)
58 return seperator.join(path)
59
59
60
60
61 class RepoNameType(colander.String):
61 class RepoNameType(colander.String):
62 SEPARATOR = '/'
62 SEPARATOR = '/'
63
63
64 def deserialize(self, node, cstruct):
64 def deserialize(self, node, cstruct):
65 result = super(RepoNameType, self).deserialize(node, cstruct)
65 result = super(RepoNameType, self).deserialize(node, cstruct)
66 if cstruct is colander.null:
66 if cstruct is colander.null:
67 return colander.null
67 return colander.null
68 return self._normalize(result)
68 return self._normalize(result)
69
69
70 def _normalize(self, path):
70 def _normalize(self, path):
71 return _normalize(self.SEPARATOR, path)
71 return _normalize(self.SEPARATOR, path)
72
72
73
73
74 class GroupNameType(colander.String):
74 class GroupNameType(colander.String):
75 SEPARATOR = '/'
75 SEPARATOR = '/'
76
76
77 def deserialize(self, node, cstruct):
77 def deserialize(self, node, cstruct):
78 if cstruct is RootLocation:
78 if cstruct is RootLocation:
79 return cstruct
79 return cstruct
80
80
81 result = super(GroupNameType, self).deserialize(node, cstruct)
81 result = super(GroupNameType, self).deserialize(node, cstruct)
82 if cstruct is colander.null:
82 if cstruct is colander.null:
83 return colander.null
83 return colander.null
84 return self._normalize(result)
84 return self._normalize(result)
85
85
86 def _normalize(self, path):
86 def _normalize(self, path):
87 return _normalize(self.SEPARATOR, path)
87 return _normalize(self.SEPARATOR, path)
88
88
89
89
90 class StringBooleanType(colander.String):
90 class StringBooleanType(colander.String):
91 true_values = ['true', 't', 'yes', 'y', 'on', '1']
91 true_values = ['true', 't', 'yes', 'y', 'on', '1']
92 false_values = ['false', 'f', 'no', 'n', 'off', '0']
92 false_values = ['false', 'f', 'no', 'n', 'off', '0']
93
93
94 def serialize(self, node, appstruct):
94 def serialize(self, node, appstruct):
95 if appstruct is colander.null:
95 if appstruct is colander.null:
96 return colander.null
96 return colander.null
97 if not isinstance(appstruct, bool):
97 if not isinstance(appstruct, bool):
98 raise colander.Invalid(node, '%r is not a boolean' % appstruct)
98 raise colander.Invalid(node, '%r is not a boolean' % appstruct)
99
99
100 return appstruct and 'true' or 'false'
100 return appstruct and 'true' or 'false'
101
101
102 def deserialize(self, node, cstruct):
102 def deserialize(self, node, cstruct):
103 if cstruct is colander.null:
103 if cstruct is colander.null:
104 return colander.null
104 return colander.null
105
105
106 if isinstance(cstruct, bool):
106 if isinstance(cstruct, bool):
107 return cstruct
107 return cstruct
108
108
109 if not isinstance(cstruct, basestring):
109 if not isinstance(cstruct, basestring):
110 raise colander.Invalid(node, '%r is not a string' % cstruct)
110 raise colander.Invalid(node, '%r is not a string' % cstruct)
111
111
112 value = cstruct.lower()
112 value = cstruct.lower()
113 if value in self.true_values:
113 if value in self.true_values:
114 return True
114 return True
115 elif value in self.false_values:
115 elif value in self.false_values:
116 return False
116 return False
117 else:
117 else:
118 raise colander.Invalid(
118 raise colander.Invalid(
119 node, '{} value cannot be translated to bool'.format(value))
119 node, '{} value cannot be translated to bool'.format(value))
120
120
121
121
122 class UserOrUserGroupType(colander.SchemaType):
122 class UserOrUserGroupType(colander.SchemaType):
123 """ colander Schema type for valid rhodecode user and/or usergroup """
123 """ colander Schema type for valid rhodecode user and/or usergroup """
124 scopes = ('user', 'usergroup')
124 scopes = ('user', 'usergroup')
125
125
126 def __init__(self):
126 def __init__(self):
127 self.users = 'user' in self.scopes
127 self.users = 'user' in self.scopes
128 self.usergroups = 'usergroup' in self.scopes
128 self.usergroups = 'usergroup' in self.scopes
129
129
130 def serialize(self, node, appstruct):
130 def serialize(self, node, appstruct):
131 if appstruct is colander.null:
131 if appstruct is colander.null:
132 return colander.null
132 return colander.null
133
133
134 if self.users:
134 if self.users:
135 if isinstance(appstruct, User):
135 if isinstance(appstruct, User):
136 if self.usergroups:
136 if self.usergroups:
137 return 'user:%s' % appstruct.username
137 return 'user:%s' % appstruct.username
138 return appstruct.username
138 return appstruct.username
139
139
140 if self.usergroups:
140 if self.usergroups:
141 if isinstance(appstruct, UserGroup):
141 if isinstance(appstruct, UserGroup):
142 if self.users:
142 if self.users:
143 return 'usergroup:%s' % appstruct.users_group_name
143 return 'usergroup:%s' % appstruct.users_group_name
144 return appstruct.users_group_name
144 return appstruct.users_group_name
145
145
146 raise colander.Invalid(
146 raise colander.Invalid(
147 node, '%s is not a valid %s' % (appstruct, ' or '.join(self.scopes)))
147 node, '%s is not a valid %s' % (appstruct, ' or '.join(self.scopes)))
148
148
149 def deserialize(self, node, cstruct):
149 def deserialize(self, node, cstruct):
150 if cstruct is colander.null:
150 if cstruct is colander.null:
151 return colander.null
151 return colander.null
152
152
153 user, usergroup = None, None
153 user, usergroup = None, None
154 if self.users:
154 if self.users:
155 if cstruct.startswith('user:'):
155 if cstruct.startswith('user:'):
156 user = User.get_by_username(cstruct.split(':')[1])
156 user = User.get_by_username(cstruct.split(':')[1])
157 else:
157 else:
158 user = User.get_by_username(cstruct)
158 user = User.get_by_username(cstruct)
159
159
160 if self.usergroups:
160 if self.usergroups:
161 if cstruct.startswith('usergroup:'):
161 if cstruct.startswith('usergroup:'):
162 usergroup = UserGroup.get_by_group_name(cstruct.split(':')[1])
162 usergroup = UserGroup.get_by_group_name(cstruct.split(':')[1])
163 else:
163 else:
164 usergroup = UserGroup.get_by_group_name(cstruct)
164 usergroup = UserGroup.get_by_group_name(cstruct)
165
165
166 if self.users and self.usergroups:
166 if self.users and self.usergroups:
167 if user and usergroup:
167 if user and usergroup:
168 raise colander.Invalid(node, (
168 raise colander.Invalid(node, (
169 '%s is both a user and usergroup, specify which '
169 '%s is both a user and usergroup, specify which '
170 'one was wanted by prepending user: or usergroup: to the '
170 'one was wanted by prepending user: or usergroup: to the '
171 'name') % cstruct)
171 'name') % cstruct)
172
172
173 if self.users and user:
173 if self.users and user:
174 return user
174 return user
175
175
176 if self.usergroups and usergroup:
176 if self.usergroups and usergroup:
177 return usergroup
177 return usergroup
178
178
179 raise colander.Invalid(
179 raise colander.Invalid(
180 node, '%s is not a valid %s' % (cstruct, ' or '.join(self.scopes)))
180 node, '%s is not a valid %s' % (cstruct, ' or '.join(self.scopes)))
181
181
182
182
183 class UserType(UserOrUserGroupType):
183 class UserType(UserOrUserGroupType):
184 scopes = ('user',)
184 scopes = ('user',)
185
185
186
186
187 class UserGroupType(UserOrUserGroupType):
187 class UserGroupType(UserOrUserGroupType):
188 scopes = ('usergroup',)
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 // Default styles
1 // Default styles
2
2
3 .diff-collapse {
3 .diff-collapse {
4 margin: @padding 0;
4 margin: @padding 0;
5 text-align: right;
5 text-align: right;
6 }
6 }
7
7
8 .diff-container {
8 .diff-container {
9 margin-bottom: @space;
9 margin-bottom: @space;
10
10
11 .diffblock {
11 .diffblock {
12 margin-bottom: @space;
12 margin-bottom: @space;
13 }
13 }
14
14
15 &.hidden {
15 &.hidden {
16 display: none;
16 display: none;
17 overflow: hidden;
17 overflow: hidden;
18 }
18 }
19 }
19 }
20
20
21 .compare_view_files {
21 .compare_view_files {
22
22
23 .diff-container {
23 .diff-container {
24
24
25 .diffblock {
25 .diffblock {
26 margin-bottom: 0;
26 margin-bottom: 0;
27 }
27 }
28 }
28 }
29 }
29 }
30
30
31 div.diffblock .sidebyside {
31 div.diffblock .sidebyside {
32 background: #ffffff;
32 background: #ffffff;
33 }
33 }
34
34
35 div.diffblock {
35 div.diffblock {
36 overflow-x: auto;
36 overflow-x: auto;
37 overflow-y: hidden;
37 overflow-y: hidden;
38 clear: both;
38 clear: both;
39 padding: 0px;
39 padding: 0px;
40 background: @grey6;
40 background: @grey6;
41 border: @border-thickness solid @grey5;
41 border: @border-thickness solid @grey5;
42 -webkit-border-radius: @border-radius @border-radius 0px 0px;
42 -webkit-border-radius: @border-radius @border-radius 0px 0px;
43 border-radius: @border-radius @border-radius 0px 0px;
43 border-radius: @border-radius @border-radius 0px 0px;
44
44
45
45
46 .comments-number {
46 .comments-number {
47 float: right;
47 float: right;
48 }
48 }
49
49
50 // BEGIN CODE-HEADER STYLES
50 // BEGIN CODE-HEADER STYLES
51
51
52 .code-header {
52 .code-header {
53 background: @grey6;
53 background: @grey6;
54 padding: 10px 0 10px 0;
54 padding: 10px 0 10px 0;
55 height: auto;
55 height: auto;
56 width: 100%;
56 width: 100%;
57
57
58 .hash {
58 .hash {
59 float: left;
59 float: left;
60 padding: 2px 0 0 2px;
60 padding: 2px 0 0 2px;
61 }
61 }
62
62
63 .date {
63 .date {
64 float: left;
64 float: left;
65 text-transform: uppercase;
65 text-transform: uppercase;
66 padding: 4px 0px 0px 2px;
66 padding: 4px 0px 0px 2px;
67 }
67 }
68
68
69 div {
69 div {
70 margin-left: 4px;
70 margin-left: 4px;
71 }
71 }
72
72
73 div.compare_header {
73 div.compare_header {
74 min-height: 40px;
74 min-height: 40px;
75 margin: 0;
75 margin: 0;
76 padding: 0 @padding;
76 padding: 0 @padding;
77
77
78 .drop-menu {
78 .drop-menu {
79 float:left;
79 float:left;
80 display: block;
80 display: block;
81 margin:0 0 @padding 0;
81 margin:0 0 @padding 0;
82 }
82 }
83
83
84 .compare-label {
84 .compare-label {
85 float: left;
85 float: left;
86 clear: both;
86 clear: both;
87 display: inline-block;
87 display: inline-block;
88 min-width: 5em;
88 min-width: 5em;
89 margin: 0;
89 margin: 0;
90 padding: @button-padding @button-padding @button-padding 0;
90 padding: @button-padding @button-padding @button-padding 0;
91 font-family: @text-semibold;
91 font-family: @text-semibold;
92 }
92 }
93
93
94 .compare-buttons {
94 .compare-buttons {
95 float: left;
95 float: left;
96 margin: 0;
96 margin: 0;
97 padding: 0 0 @padding;
97 padding: 0 0 @padding;
98
98
99 .btn {
99 .btn {
100 margin: 0 @padding 0 0;
100 margin: 0 @padding 0 0;
101 }
101 }
102 }
102 }
103 }
103 }
104
104
105 }
105 }
106
106
107 .parents {
107 .parents {
108 float: left;
108 float: left;
109 width: 100px;
109 width: 100px;
110 font-weight: 400;
110 font-weight: 400;
111 vertical-align: middle;
111 vertical-align: middle;
112 padding: 0px 2px 0px 2px;
112 padding: 0px 2px 0px 2px;
113 background-color: @grey6;
113 background-color: @grey6;
114
114
115 #parent_link {
115 #parent_link {
116 margin: 00px 2px;
116 margin: 00px 2px;
117
117
118 &.double {
118 &.double {
119 margin: 0px 2px;
119 margin: 0px 2px;
120 }
120 }
121
121
122 &.disabled{
122 &.disabled{
123 margin-right: @padding;
123 margin-right: @padding;
124 }
124 }
125 }
125 }
126 }
126 }
127
127
128 .children {
128 .children {
129 float: right;
129 float: right;
130 width: 100px;
130 width: 100px;
131 font-weight: 400;
131 font-weight: 400;
132 vertical-align: middle;
132 vertical-align: middle;
133 text-align: right;
133 text-align: right;
134 padding: 0px 2px 0px 2px;
134 padding: 0px 2px 0px 2px;
135 background-color: @grey6;
135 background-color: @grey6;
136
136
137 #child_link {
137 #child_link {
138 margin: 0px 2px;
138 margin: 0px 2px;
139
139
140 &.double {
140 &.double {
141 margin: 0px 2px;
141 margin: 0px 2px;
142 }
142 }
143
143
144 &.disabled{
144 &.disabled{
145 margin-right: @padding;
145 margin-right: @padding;
146 }
146 }
147 }
147 }
148 }
148 }
149
149
150 .changeset_header {
150 .changeset_header {
151 height: 16px;
151 height: 16px;
152
152
153 & > div{
153 & > div{
154 margin-right: @padding;
154 margin-right: @padding;
155 }
155 }
156 }
156 }
157
157
158 .changeset_file {
158 .changeset_file {
159 text-align: left;
159 text-align: left;
160 float: left;
160 float: left;
161 padding: 0;
161 padding: 0;
162
162
163 a{
163 a{
164 display: inline-block;
164 display: inline-block;
165 margin-right: 0.5em;
165 margin-right: 0.5em;
166 }
166 }
167
167
168 #selected_mode{
168 #selected_mode{
169 margin-left: 0;
169 margin-left: 0;
170 }
170 }
171 }
171 }
172
172
173 .diff-menu-wrapper {
173 .diff-menu-wrapper {
174 float: left;
174 float: left;
175 }
175 }
176
176
177 .diff-menu {
177 .diff-menu {
178 position: absolute;
178 position: absolute;
179 background: none repeat scroll 0 0 #FFFFFF;
179 background: none repeat scroll 0 0 #FFFFFF;
180 border-color: #003367 @grey3 @grey3;
180 border-color: #003367 @grey3 @grey3;
181 border-right: 1px solid @grey3;
181 border-right: 1px solid @grey3;
182 border-style: solid solid solid;
182 border-style: solid solid solid;
183 border-width: @border-thickness;
183 border-width: @border-thickness;
184 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
184 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
185 margin-top: 5px;
185 margin-top: 5px;
186 margin-left: 1px;
186 margin-left: 1px;
187 }
187 }
188
188
189 .diff-actions, .editor-actions {
189 .diff-actions, .editor-actions {
190 float: left;
190 float: left;
191
191
192 input{
192 input{
193 margin: 0 0.5em 0 0;
193 margin: 0 0.5em 0 0;
194 }
194 }
195 }
195 }
196
196
197 // END CODE-HEADER STYLES
197 // END CODE-HEADER STYLES
198
198
199 // BEGIN CODE-BODY STYLES
199 // BEGIN CODE-BODY STYLES
200
200
201 .code-body {
201 .code-body {
202 background: white;
202 background: white;
203 padding: 0;
203 padding: 0;
204 background-color: #ffffff;
204 background-color: #ffffff;
205 position: relative;
205 position: relative;
206 max-width: none;
206 max-width: none;
207 box-sizing: border-box;
207 box-sizing: border-box;
208 // TODO: johbo: Parent has overflow: auto, this forces the child here
208 // TODO: johbo: Parent has overflow: auto, this forces the child here
209 // to have the intended size and to scroll. Should be simplified.
209 // to have the intended size and to scroll. Should be simplified.
210 width: 100%;
210 width: 100%;
211 overflow-x: auto;
211 overflow-x: auto;
212 }
212 }
213
213
214 pre.raw {
214 pre.raw {
215 background: white;
215 background: white;
216 color: @grey1;
216 color: @grey1;
217 }
217 }
218 // END CODE-BODY STYLES
218 // END CODE-BODY STYLES
219
219
220 }
220 }
221
221
222
222
223 table.code-difftable {
223 table.code-difftable {
224 border-collapse: collapse;
224 border-collapse: collapse;
225 width: 99%;
225 width: 99%;
226 border-radius: 0px !important;
226 border-radius: 0px !important;
227
227
228 td {
228 td {
229 padding: 0 !important;
229 padding: 0 !important;
230 background: none !important;
230 background: none !important;
231 border: 0 !important;
231 border: 0 !important;
232 }
232 }
233
233
234 .context {
234 .context {
235 background: none repeat scroll 0 0 #DDE7EF;
235 background: none repeat scroll 0 0 #DDE7EF;
236 }
236 }
237
237
238 .add {
238 .add {
239 background: none repeat scroll 0 0 #DDFFDD;
239 background: none repeat scroll 0 0 #DDFFDD;
240
240
241 ins {
241 ins {
242 background: none repeat scroll 0 0 #AAFFAA;
242 background: none repeat scroll 0 0 #AAFFAA;
243 text-decoration: none;
243 text-decoration: none;
244 }
244 }
245 }
245 }
246
246
247 .del {
247 .del {
248 background: none repeat scroll 0 0 #FFDDDD;
248 background: none repeat scroll 0 0 #FFDDDD;
249
249
250 del {
250 del {
251 background: none repeat scroll 0 0 #FFAAAA;
251 background: none repeat scroll 0 0 #FFAAAA;
252 text-decoration: none;
252 text-decoration: none;
253 }
253 }
254 }
254 }
255
255
256 /** LINE NUMBERS **/
256 /** LINE NUMBERS **/
257 .lineno {
257 .lineno {
258 padding-left: 2px !important;
258 padding-left: 2px !important;
259 padding-right: 2px;
259 padding-right: 2px;
260 text-align: right;
260 text-align: right;
261 width: 32px;
261 width: 32px;
262 -moz-user-select: none;
262 -moz-user-select: none;
263 -webkit-user-select: none;
263 -webkit-user-select: none;
264 border-right: @border-thickness solid @grey5 !important;
264 border-right: @border-thickness solid @grey5 !important;
265 border-left: 0px solid #CCC !important;
265 border-left: 0px solid #CCC !important;
266 border-top: 0px solid #CCC !important;
266 border-top: 0px solid #CCC !important;
267 border-bottom: none !important;
267 border-bottom: none !important;
268
268
269 a {
269 a {
270 &:extend(pre);
270 &:extend(pre);
271 text-align: right;
271 text-align: right;
272 padding-right: 2px;
272 padding-right: 2px;
273 cursor: pointer;
273 cursor: pointer;
274 display: block;
274 display: block;
275 width: 32px;
275 width: 32px;
276 }
276 }
277 }
277 }
278
278
279 .context {
279 .context {
280 cursor: auto;
280 cursor: auto;
281 &:extend(pre);
281 &:extend(pre);
282 }
282 }
283
283
284 .lineno-inline {
284 .lineno-inline {
285 background: none repeat scroll 0 0 #FFF !important;
285 background: none repeat scroll 0 0 #FFF !important;
286 padding-left: 2px;
286 padding-left: 2px;
287 padding-right: 2px;
287 padding-right: 2px;
288 text-align: right;
288 text-align: right;
289 width: 30px;
289 width: 30px;
290 -moz-user-select: none;
290 -moz-user-select: none;
291 -webkit-user-select: none;
291 -webkit-user-select: none;
292 }
292 }
293
293
294 /** CODE **/
294 /** CODE **/
295 .code {
295 .code {
296 display: block;
296 display: block;
297 width: 100%;
297 width: 100%;
298
298
299 td {
299 td {
300 margin: 0;
300 margin: 0;
301 padding: 0;
301 padding: 0;
302 }
302 }
303
303
304 pre {
304 pre {
305 margin: 0;
305 margin: 0;
306 padding: 0;
306 padding: 0;
307 margin-left: .5em;
307 margin-left: .5em;
308 }
308 }
309 }
309 }
310 }
310 }
311
311
312
312
313 // Comments
313 // Comments
314
314
315 div.comment:target {
315 div.comment:target {
316 border-left: 6px solid @comment-highlight-color !important;
316 border-left: 6px solid @comment-highlight-color !important;
317 padding-left: 3px;
317 padding-left: 3px;
318 margin-left: -9px;
318 margin-left: -9px;
319 }
319 }
320
320
321 //TODO: anderson: can't get an absolute number out of anything, so had to put the
321 //TODO: anderson: can't get an absolute number out of anything, so had to put the
322 //current values that might change. But to make it clear I put as a calculation
322 //current values that might change. But to make it clear I put as a calculation
323 @comment-max-width: 1065px;
323 @comment-max-width: 1065px;
324 @pr-extra-margin: 34px;
324 @pr-extra-margin: 34px;
325 @pr-border-spacing: 4px;
325 @pr-border-spacing: 4px;
326 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
326 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
327
327
328 // Pull Request
328 // Pull Request
329 .cs_files .code-difftable {
329 .cs_files .code-difftable {
330 border: @border-thickness solid @grey5; //borders only on PRs
330 border: @border-thickness solid @grey5; //borders only on PRs
331
331
332 .comment-inline-form,
332 .comment-inline-form,
333 div.comment {
333 div.comment {
334 width: @pr-comment-width;
334 width: @pr-comment-width;
335 }
335 }
336 }
336 }
337
337
338 // Changeset
338 // Changeset
339 .code-difftable {
339 .code-difftable {
340 .comment-inline-form,
340 .comment-inline-form,
341 div.comment {
341 div.comment {
342 width: @comment-max-width;
342 width: @comment-max-width;
343 }
343 }
344 }
344 }
345
345
346 //Style page
346 //Style page
347 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
347 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
348 #style-page .code-difftable{
348 #style-page .code-difftable{
349 .comment-inline-form,
349 .comment-inline-form,
350 div.comment {
350 div.comment {
351 width: @comment-max-width - @style-extra-margin;
351 width: @comment-max-width - @style-extra-margin;
352 }
352 }
353 }
353 }
354
354
355 #context-bar > h2 {
355 #context-bar > h2 {
356 font-size: 20px;
356 font-size: 20px;
357 }
357 }
358
358
359 #context-bar > h2> a {
359 #context-bar > h2> a {
360 font-size: 20px;
360 font-size: 20px;
361 }
361 }
362 // end of defaults
362 // end of defaults
363
363
364 .file_diff_buttons {
364 .file_diff_buttons {
365 padding: 0 0 @padding;
365 padding: 0 0 @padding;
366
366
367 .drop-menu {
367 .drop-menu {
368 float: left;
368 float: left;
369 margin: 0 @padding 0 0;
369 margin: 0 @padding 0 0;
370 }
370 }
371 .btn {
371 .btn {
372 margin: 0 @padding 0 0;
372 margin: 0 @padding 0 0;
373 }
373 }
374 }
374 }
375
375
376 .code-body.textarea.editor {
376 .code-body.textarea.editor {
377 max-width: none;
377 max-width: none;
378 padding: 15px;
378 padding: 15px;
379 }
379 }
380
380
381 td.injected_diff{
381 td.injected_diff{
382 max-width: 1178px;
382 max-width: 1178px;
383 overflow-x: auto;
383 overflow-x: auto;
384 overflow-y: hidden;
384 overflow-y: hidden;
385
385
386 div.diff-container,
386 div.diff-container,
387 div.diffblock{
387 div.diffblock{
388 max-width: 100%;
388 max-width: 100%;
389 }
389 }
390
390
391 div.code-body {
391 div.code-body {
392 max-width: 1124px;
392 max-width: 1124px;
393 overflow-x: auto;
393 overflow-x: auto;
394 overflow-y: hidden;
394 overflow-y: hidden;
395 padding: 0;
395 padding: 0;
396 }
396 }
397 div.diffblock {
397 div.diffblock {
398 border: none;
398 border: none;
399 }
399 }
400
400
401 &.inline-form {
401 &.inline-form {
402 width: 99%
402 width: 99%
403 }
403 }
404 }
404 }
405
405
406
406
407 table.code-difftable {
407 table.code-difftable {
408 width: 100%;
408 width: 100%;
409 }
409 }
410
410
411 /** PYGMENTS COLORING **/
411 /** PYGMENTS COLORING **/
412 div.codeblock {
412 div.codeblock {
413
413
414 // TODO: johbo: Added interim to get rid of the margin around
414 // TODO: johbo: Added interim to get rid of the margin around
415 // Select2 widgets. This needs further cleanup.
415 // Select2 widgets. This needs further cleanup.
416 margin-top: @padding;
416 margin-top: @padding;
417
417
418 overflow: auto;
418 overflow: auto;
419 padding: 0px;
419 padding: 0px;
420 border: @border-thickness solid @grey5;
420 border: @border-thickness solid @grey5;
421 background: @grey6;
421 background: @grey6;
422 .border-radius(@border-radius);
422 .border-radius(@border-radius);
423
423
424 #remove_gist {
424 #remove_gist {
425 float: right;
425 float: right;
426 }
426 }
427
427
428 .author {
428 .author {
429 clear: both;
429 clear: both;
430 vertical-align: middle;
430 vertical-align: middle;
431 font-family: @text-bold;
431 font-family: @text-bold;
432 }
432 }
433
433
434 .btn-mini {
434 .btn-mini {
435 float: left;
435 float: left;
436 margin: 0 5px 0 0;
436 margin: 0 5px 0 0;
437 }
437 }
438
438
439 .code-header {
439 .code-header {
440 padding: @padding;
440 padding: @padding;
441 border-bottom: @border-thickness solid @grey5;
441 border-bottom: @border-thickness solid @grey5;
442
442
443 .rc-user {
443 .rc-user {
444 min-width: 0;
444 min-width: 0;
445 margin-right: .5em;
445 margin-right: .5em;
446 }
446 }
447
447
448 .stats {
448 .stats {
449 clear: both;
449 clear: both;
450 margin: 0 0 @padding 0;
450 margin: 0 0 @padding 0;
451 padding: 0;
451 padding: 0;
452 .left {
452 .left {
453 float: left;
453 float: left;
454 clear: left;
454 clear: left;
455 max-width: 75%;
455 max-width: 75%;
456 margin: 0 0 @padding 0;
456 margin: 0 0 @padding 0;
457
457
458 &.item {
458 &.item {
459 margin-right: @padding;
459 margin-right: @padding;
460 &.last { border-right: none; }
460 &.last { border-right: none; }
461 }
461 }
462 }
462 }
463 .buttons { float: right; }
463 .buttons { float: right; }
464 .author {
464 .author {
465 height: 25px; margin-left: 15px; font-weight: bold;
465 height: 25px; margin-left: 15px; font-weight: bold;
466 }
466 }
467 }
467 }
468
468
469 .commit {
469 .commit {
470 margin: 5px 0 0 26px;
470 margin: 5px 0 0 26px;
471 font-weight: normal;
471 font-weight: normal;
472 white-space: pre-wrap;
472 white-space: pre-wrap;
473 }
473 }
474 }
474 }
475
475
476 .message {
476 .message {
477 position: relative;
477 position: relative;
478 margin: @padding;
478 margin: @padding;
479
479
480 .codeblock-label {
480 .codeblock-label {
481 margin: 0 0 1em 0;
481 margin: 0 0 1em 0;
482 }
482 }
483 }
483 }
484
484
485 .code-body {
485 .code-body {
486 padding: @padding;
486 padding: @padding;
487 background-color: #ffffff;
487 background-color: #ffffff;
488 min-width: 100%;
488 min-width: 100%;
489 box-sizing: border-box;
489 box-sizing: border-box;
490 // TODO: johbo: Parent has overflow: auto, this forces the child here
490 // TODO: johbo: Parent has overflow: auto, this forces the child here
491 // to have the intended size and to scroll. Should be simplified.
491 // to have the intended size and to scroll. Should be simplified.
492 width: 100%;
492 width: 100%;
493 overflow-x: auto;
493 overflow-x: auto;
494 }
494 }
495 }
495 }
496
496
497 .code-highlighttable,
497 .code-highlighttable,
498 div.codeblock {
498 div.codeblock {
499
499
500 &.readme {
500 &.readme {
501 background-color: white;
501 background-color: white;
502 }
502 }
503
503
504 .markdown-block table {
504 .markdown-block table {
505 border-collapse: collapse;
505 border-collapse: collapse;
506
506
507 th,
507 th,
508 td {
508 td {
509 padding: .5em;
509 padding: .5em;
510 border: @border-thickness solid @border-default-color;
510 border: @border-thickness solid @border-default-color;
511 }
511 }
512 }
512 }
513
513
514 table {
514 table {
515 border: 0px;
515 border: 0px;
516 margin: 0;
516 margin: 0;
517 letter-spacing: normal;
517 letter-spacing: normal;
518
518
519
519
520 td {
520 td {
521 border: 0px;
521 border: 0px;
522 vertical-align: top;
522 vertical-align: top;
523 }
523 }
524 }
524 }
525 }
525 }
526
526
527 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
527 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
528 div.search-code-body {
528 div.search-code-body {
529 background-color: #ffffff; padding: 5px 0 5px 10px;
529 background-color: #ffffff; padding: 5px 0 5px 10px;
530 pre {
530 pre {
531 .match { background-color: #faffa6;}
531 .match { background-color: #faffa6;}
532 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
532 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
533 }
533 }
534 .code-highlighttable {
534 .code-highlighttable {
535 border-collapse: collapse;
535 border-collapse: collapse;
536
536
537 tr:hover {
537 tr:hover {
538 background: #fafafa;
538 background: #fafafa;
539 }
539 }
540 td.code {
540 td.code {
541 padding-left: 10px;
541 padding-left: 10px;
542 }
542 }
543 td.line {
543 td.line {
544 border-right: 1px solid #ccc !important;
544 border-right: 1px solid #ccc !important;
545 padding-right: 10px;
545 padding-right: 10px;
546 text-align: right;
546 text-align: right;
547 font-family: "Lucida Console",Monaco,monospace;
547 font-family: "Lucida Console",Monaco,monospace;
548 span {
548 span {
549 white-space: pre-wrap;
549 white-space: pre-wrap;
550 color: #666666;
550 color: #666666;
551 }
551 }
552 }
552 }
553 }
553 }
554 }
554 }
555
555
556 div.annotatediv { margin-left: 2px; margin-right: 4px; }
556 div.annotatediv { margin-left: 2px; margin-right: 4px; }
557 .code-highlight {
557 .code-highlight {
558 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
558 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
559 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
559 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
560 pre div:target {background-color: @comment-highlight-color !important;}
560 pre div:target {background-color: @comment-highlight-color !important;}
561 }
561 }
562
562
563 .linenos a { text-decoration: none; }
563 .linenos a { text-decoration: none; }
564
564
565 .CodeMirror-selected { background: @rchighlightblue; }
565 .CodeMirror-selected { background: @rchighlightblue; }
566 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
566 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
567 .CodeMirror ::selection { background: @rchighlightblue; }
567 .CodeMirror ::selection { background: @rchighlightblue; }
568 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
568 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
569
569
570 .code { display: block; border:0px !important; }
570 .code { display: block; border:0px !important; }
571 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
571 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
572 .codehilite {
572 .codehilite {
573 .hll { background-color: #ffffcc }
573 .hll { background-color: #ffffcc }
574 .c { color: #408080; font-style: italic } /* Comment */
574 .c { color: #408080; font-style: italic } /* Comment */
575 .err, .codehilite .err { border: @border-thickness solid #FF0000 } /* Error */
575 .err, .codehilite .err { border: @border-thickness solid #FF0000 } /* Error */
576 .k { color: #008000; font-weight: bold } /* Keyword */
576 .k { color: #008000; font-weight: bold } /* Keyword */
577 .o { color: #666666 } /* Operator */
577 .o { color: #666666 } /* Operator */
578 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
578 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
579 .cp { color: #BC7A00 } /* Comment.Preproc */
579 .cp { color: #BC7A00 } /* Comment.Preproc */
580 .c1 { color: #408080; font-style: italic } /* Comment.Single */
580 .c1 { color: #408080; font-style: italic } /* Comment.Single */
581 .cs { color: #408080; font-style: italic } /* Comment.Special */
581 .cs { color: #408080; font-style: italic } /* Comment.Special */
582 .gd { color: #A00000 } /* Generic.Deleted */
582 .gd { color: #A00000 } /* Generic.Deleted */
583 .ge { font-style: italic } /* Generic.Emph */
583 .ge { font-style: italic } /* Generic.Emph */
584 .gr { color: #FF0000 } /* Generic.Error */
584 .gr { color: #FF0000 } /* Generic.Error */
585 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
585 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
586 .gi { color: #00A000 } /* Generic.Inserted */
586 .gi { color: #00A000 } /* Generic.Inserted */
587 .go { color: #808080 } /* Generic.Output */
587 .go { color: #808080 } /* Generic.Output */
588 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
588 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
589 .gs { font-weight: bold } /* Generic.Strong */
589 .gs { font-weight: bold } /* Generic.Strong */
590 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
590 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
591 .gt { color: #0040D0 } /* Generic.Traceback */
591 .gt { color: #0040D0 } /* Generic.Traceback */
592 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
592 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
593 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
593 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
594 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
594 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
595 .kp { color: #008000 } /* Keyword.Pseudo */
595 .kp { color: #008000 } /* Keyword.Pseudo */
596 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
596 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
597 .kt { color: #B00040 } /* Keyword.Type */
597 .kt { color: #B00040 } /* Keyword.Type */
598 .m { color: #666666 } /* Literal.Number */
598 .m { color: #666666 } /* Literal.Number */
599 .s { color: #BA2121 } /* Literal.String */
599 .s { color: #BA2121 } /* Literal.String */
600 .na { color: #7D9029 } /* Name.Attribute */
600 .na { color: #7D9029 } /* Name.Attribute */
601 .nb { color: #008000 } /* Name.Builtin */
601 .nb { color: #008000 } /* Name.Builtin */
602 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
602 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
603 .no { color: #880000 } /* Name.Constant */
603 .no { color: #880000 } /* Name.Constant */
604 .nd { color: #AA22FF } /* Name.Decorator */
604 .nd { color: #AA22FF } /* Name.Decorator */
605 .ni { color: #999999; font-weight: bold } /* Name.Entity */
605 .ni { color: #999999; font-weight: bold } /* Name.Entity */
606 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
606 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
607 .nf { color: #0000FF } /* Name.Function */
607 .nf { color: #0000FF } /* Name.Function */
608 .nl { color: #A0A000 } /* Name.Label */
608 .nl { color: #A0A000 } /* Name.Label */
609 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
609 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
610 .nt { color: #008000; font-weight: bold } /* Name.Tag */
610 .nt { color: #008000; font-weight: bold } /* Name.Tag */
611 .nv { color: #19177C } /* Name.Variable */
611 .nv { color: #19177C } /* Name.Variable */
612 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
612 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
613 .w { color: #bbbbbb } /* Text.Whitespace */
613 .w { color: #bbbbbb } /* Text.Whitespace */
614 .mf { color: #666666 } /* Literal.Number.Float */
614 .mf { color: #666666 } /* Literal.Number.Float */
615 .mh { color: #666666 } /* Literal.Number.Hex */
615 .mh { color: #666666 } /* Literal.Number.Hex */
616 .mi { color: #666666 } /* Literal.Number.Integer */
616 .mi { color: #666666 } /* Literal.Number.Integer */
617 .mo { color: #666666 } /* Literal.Number.Oct */
617 .mo { color: #666666 } /* Literal.Number.Oct */
618 .sb { color: #BA2121 } /* Literal.String.Backtick */
618 .sb { color: #BA2121 } /* Literal.String.Backtick */
619 .sc { color: #BA2121 } /* Literal.String.Char */
619 .sc { color: #BA2121 } /* Literal.String.Char */
620 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
620 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
621 .s2 { color: #BA2121 } /* Literal.String.Double */
621 .s2 { color: #BA2121 } /* Literal.String.Double */
622 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
622 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
623 .sh { color: #BA2121 } /* Literal.String.Heredoc */
623 .sh { color: #BA2121 } /* Literal.String.Heredoc */
624 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
624 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
625 .sx { color: #008000 } /* Literal.String.Other */
625 .sx { color: #008000 } /* Literal.String.Other */
626 .sr { color: #BB6688 } /* Literal.String.Regex */
626 .sr { color: #BB6688 } /* Literal.String.Regex */
627 .s1 { color: #BA2121 } /* Literal.String.Single */
627 .s1 { color: #BA2121 } /* Literal.String.Single */
628 .ss { color: #19177C } /* Literal.String.Symbol */
628 .ss { color: #19177C } /* Literal.String.Symbol */
629 .bp { color: #008000 } /* Name.Builtin.Pseudo */
629 .bp { color: #008000 } /* Name.Builtin.Pseudo */
630 .vc { color: #19177C } /* Name.Variable.Class */
630 .vc { color: #19177C } /* Name.Variable.Class */
631 .vg { color: #19177C } /* Name.Variable.Global */
631 .vg { color: #19177C } /* Name.Variable.Global */
632 .vi { color: #19177C } /* Name.Variable.Instance */
632 .vi { color: #19177C } /* Name.Variable.Instance */
633 .il { color: #666666 } /* Literal.Number.Integer.Long */
633 .il { color: #666666 } /* Literal.Number.Integer.Long */
634 }
634 }
635
635
636 /* customized pre blocks for markdown/rst */
636 /* customized pre blocks for markdown/rst */
637 pre.literal-block, .codehilite pre{
637 pre.literal-block, .codehilite pre{
638 padding: @padding;
638 padding: @padding;
639 border: 1px solid @grey6;
639 border: 1px solid @grey6;
640 .border-radius(@border-radius);
640 .border-radius(@border-radius);
641 background-color: @grey7;
641 background-color: @grey7;
642 }
642 }
643
643
644
644
645 /* START NEW CODE BLOCK CSS */
645 /* START NEW CODE BLOCK CSS */
646
646
647 @cb-line-height: 18px;
647 @cb-line-height: 18px;
648 @cb-line-code-padding: 10px;
648 @cb-line-code-padding: 10px;
649 @cb-text-padding: 5px;
649 @cb-text-padding: 5px;
650
650
651 @pill-padding: 2px 7px;
651 @pill-padding: 2px 7px;
652
652
653 input.filediff-collapse-state {
653 input.filediff-collapse-state {
654 display: none;
654 display: none;
655
655
656 &:checked + .filediff { /* file diff is collapsed */
656 &:checked + .filediff { /* file diff is collapsed */
657 .cb {
657 .cb {
658 display: none
658 display: none
659 }
659 }
660 .filediff-collapse-indicator {
660 .filediff-collapse-indicator {
661 width: 0;
661 width: 0;
662 height: 0;
662 height: 0;
663 border-style: solid;
663 border-style: solid;
664 border-width: 6.5px 0 6.5px 11.3px;
664 border-width: 6.5px 0 6.5px 11.3px;
665 border-color: transparent transparent transparent #ccc;
665 border-color: transparent transparent transparent #ccc;
666 }
666 }
667 .filediff-menu {
667 .filediff-menu {
668 display: none;
668 display: none;
669 }
669 }
670 margin: 10px 0 0 0;
670 margin: 10px 0 0 0;
671 }
671 }
672
672
673 &+ .filediff { /* file diff is expanded */
673 &+ .filediff { /* file diff is expanded */
674 .filediff-collapse-indicator {
674 .filediff-collapse-indicator {
675 width: 0;
675 width: 0;
676 height: 0;
676 height: 0;
677 border-style: solid;
677 border-style: solid;
678 border-width: 11.3px 6.5px 0 6.5px;
678 border-width: 11.3px 6.5px 0 6.5px;
679 border-color: #ccc transparent transparent transparent;
679 border-color: #ccc transparent transparent transparent;
680 }
680 }
681 .filediff-menu {
681 .filediff-menu {
682 display: block;
682 display: block;
683 }
683 }
684 margin: 10px 0;
684 margin: 10px 0;
685 &:nth-child(2) {
685 &:nth-child(2) {
686 margin: 0;
686 margin: 0;
687 }
687 }
688 }
688 }
689 }
689 }
690 .cs_files {
690 .cs_files {
691 clear: both;
691 clear: both;
692 }
692 }
693
693
694 .diffset-menu {
694 .diffset-menu {
695 margin-bottom: 20px;
695 margin-bottom: 20px;
696 }
696 }
697 .diffset {
697 .diffset {
698 margin: 20px auto;
698 margin: 20px auto;
699 .diffset-heading {
699 .diffset-heading {
700 border: 1px solid @grey5;
700 border: 1px solid @grey5;
701 margin-bottom: -1px;
701 margin-bottom: -1px;
702 // margin-top: 20px;
702 // margin-top: 20px;
703 h2 {
703 h2 {
704 margin: 0;
704 margin: 0;
705 line-height: 38px;
705 line-height: 38px;
706 padding-left: 10px;
706 padding-left: 10px;
707 }
707 }
708 .btn {
708 .btn {
709 margin: 0;
709 margin: 0;
710 }
710 }
711 background: @grey6;
711 background: @grey6;
712 display: block;
712 display: block;
713 padding: 5px;
713 padding: 5px;
714 }
714 }
715 .diffset-heading-warning {
715 .diffset-heading-warning {
716 background: @alert3-inner;
716 background: @alert3-inner;
717 border: 1px solid @alert3;
717 border: 1px solid @alert3;
718 }
718 }
719 &.diffset-comments-disabled {
719 &.diffset-comments-disabled {
720 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
720 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
721 display: none !important;
721 display: none !important;
722 }
722 }
723 }
723 }
724 }
724 }
725
725
726 .pill {
726 .pill {
727 display: block;
727 display: block;
728 float: left;
728 float: left;
729 padding: @pill-padding;
729 padding: @pill-padding;
730 }
730 }
731 .pill-group {
731 .pill-group {
732 .pill {
732 .pill {
733 opacity: .8;
733 opacity: .8;
734 &:first-child {
734 &:first-child {
735 border-radius: @border-radius 0 0 @border-radius;
735 border-radius: @border-radius 0 0 @border-radius;
736 }
736 }
737 &:last-child {
737 &:last-child {
738 border-radius: 0 @border-radius @border-radius 0;
738 border-radius: 0 @border-radius @border-radius 0;
739 }
739 }
740 &:only-child {
740 &:only-child {
741 border-radius: @border-radius;
741 border-radius: @border-radius;
742 }
742 }
743 }
743 }
744 }
744 }
745
745
746 /* Main comments*/
746 /* Main comments*/
747 #comments {
747 #comments {
748 .comment-selected {
748 .comment-selected {
749 border-left: 6px solid @comment-highlight-color;
749 border-left: 6px solid @comment-highlight-color;
750 padding-left: 3px;
750 padding-left: 3px;
751 margin-left: -9px;
751 margin-left: -9px;
752 }
752 }
753 }
753 }
754
754
755 .filediff {
755 .filediff {
756 border: 1px solid @grey5;
756 border: 1px solid @grey5;
757
757
758 /* START OVERRIDES */
758 /* START OVERRIDES */
759 .code-highlight {
759 .code-highlight {
760 border: none; // TODO: remove this border from the global
760 border: none; // TODO: remove this border from the global
761 // .code-highlight, it doesn't belong there
761 // .code-highlight, it doesn't belong there
762 }
762 }
763 label {
763 label {
764 margin: 0; // TODO: remove this margin definition from global label
764 margin: 0; // TODO: remove this margin definition from global label
765 // it doesn't belong there - if margin on labels
765 // it doesn't belong there - if margin on labels
766 // are needed for a form they should be defined
766 // are needed for a form they should be defined
767 // in the form's class
767 // in the form's class
768 }
768 }
769 /* END OVERRIDES */
769 /* END OVERRIDES */
770
770
771 * {
771 * {
772 box-sizing: border-box;
772 box-sizing: border-box;
773 }
773 }
774 .filediff-anchor {
774 .filediff-anchor {
775 visibility: hidden;
775 visibility: hidden;
776 }
776 }
777 &:hover {
777 &:hover {
778 .filediff-anchor {
778 .filediff-anchor {
779 visibility: visible;
779 visibility: visible;
780 }
780 }
781 }
781 }
782
782
783 .filediff-collapse-indicator {
783 .filediff-collapse-indicator {
784 border-style: solid;
784 border-style: solid;
785 float: left;
785 float: left;
786 margin: 4px 0px 0 0;
786 margin: 4px 0px 0 0;
787 cursor: pointer;
787 cursor: pointer;
788 }
788 }
789
789
790 .filediff-heading {
790 .filediff-heading {
791 background: @grey7;
791 background: @grey7;
792 cursor: pointer;
792 cursor: pointer;
793 display: block;
793 display: block;
794 padding: 5px 10px;
794 padding: 5px 10px;
795 }
795 }
796 .filediff-heading:after {
796 .filediff-heading:after {
797 content: "";
797 content: "";
798 display: table;
798 display: table;
799 clear: both;
799 clear: both;
800 }
800 }
801 .filediff-heading:hover {
801 .filediff-heading:hover {
802 background: #e1e9f4 !important;
802 background: #e1e9f4 !important;
803 }
803 }
804
804
805 .filediff-menu {
805 .filediff-menu {
806 float: right;
806 float: right;
807 text-align: right;
807 text-align: right;
808 padding: 5px 5px 5px 0px;
808 padding: 5px 5px 5px 0px;
809
809
810 &> a,
810 &> a,
811 &> span {
811 &> span {
812 padding: 1px;
812 padding: 1px;
813 }
813 }
814 }
814 }
815
815
816 .pill {
816 .pill {
817 &[op="name"] {
817 &[op="name"] {
818 background: none;
818 background: none;
819 color: @grey2;
819 color: @grey2;
820 opacity: 1;
820 opacity: 1;
821 color: white;
821 color: white;
822 }
822 }
823 &[op="limited"] {
823 &[op="limited"] {
824 background: @grey2;
824 background: @grey2;
825 color: white;
825 color: white;
826 }
826 }
827 &[op="binary"] {
827 &[op="binary"] {
828 background: @color7;
828 background: @color7;
829 color: white;
829 color: white;
830 }
830 }
831 &[op="modified"] {
831 &[op="modified"] {
832 background: @alert1;
832 background: @alert1;
833 color: white;
833 color: white;
834 }
834 }
835 &[op="renamed"] {
835 &[op="renamed"] {
836 background: @color4;
836 background: @color4;
837 color: white;
837 color: white;
838 }
838 }
839 &[op="mode"] {
839 &[op="mode"] {
840 background: @grey3;
840 background: @grey3;
841 color: white;
841 color: white;
842 }
842 }
843 &[op="symlink"] {
843 &[op="symlink"] {
844 background: @color8;
844 background: @color8;
845 color: white;
845 color: white;
846 }
846 }
847
847
848 &[op="added"] { /* added lines */
848 &[op="added"] { /* added lines */
849 background: @alert1;
849 background: @alert1;
850 color: white;
850 color: white;
851 }
851 }
852 &[op="deleted"] { /* deleted lines */
852 &[op="deleted"] { /* deleted lines */
853 background: @alert2;
853 background: @alert2;
854 color: white;
854 color: white;
855 }
855 }
856
856
857 &[op="created"] { /* created file */
857 &[op="created"] { /* created file */
858 background: @alert1;
858 background: @alert1;
859 color: white;
859 color: white;
860 }
860 }
861 &[op="removed"] { /* deleted file */
861 &[op="removed"] { /* deleted file */
862 background: @color5;
862 background: @color5;
863 color: white;
863 color: white;
864 }
864 }
865 }
865 }
866
866
867 .filediff-collapse-button, .filediff-expand-button {
867 .filediff-collapse-button, .filediff-expand-button {
868 cursor: pointer;
868 cursor: pointer;
869 }
869 }
870 .filediff-collapse-button {
870 .filediff-collapse-button {
871 display: inline;
871 display: inline;
872 }
872 }
873 .filediff-expand-button {
873 .filediff-expand-button {
874 display: none;
874 display: none;
875 }
875 }
876 .filediff-collapsed .filediff-collapse-button {
876 .filediff-collapsed .filediff-collapse-button {
877 display: none;
877 display: none;
878 }
878 }
879 .filediff-collapsed .filediff-expand-button {
879 .filediff-collapsed .filediff-expand-button {
880 display: inline;
880 display: inline;
881 }
881 }
882
882
883 @comment-padding: 5px;
884
885 /**** COMMENTS ****/
883 /**** COMMENTS ****/
886
884
887 .filediff-menu {
885 .filediff-menu {
888 .show-comment-button {
886 .show-comment-button {
889 display: none;
887 display: none;
890 }
888 }
891 }
889 }
892 &.hide-comments {
890 &.hide-comments {
893 .inline-comments {
891 .inline-comments {
894 display: none;
892 display: none;
895 }
893 }
896 .filediff-menu {
894 .filediff-menu {
897 .show-comment-button {
895 .show-comment-button {
898 display: inline;
896 display: inline;
899 }
897 }
900 .hide-comment-button {
898 .hide-comment-button {
901 display: none;
899 display: none;
902 }
900 }
903 }
901 }
904 }
902 }
905
903
906 .hide-line-comments {
904 .hide-line-comments {
907 .inline-comments {
905 .inline-comments {
908 display: none;
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 /**** END COMMENTS ****/
910 /**** END COMMENTS ****/
966
911
967 }
912 }
968
913
969 .filediff-outdated {
914 .filediff-outdated {
970 padding: 8px 0;
915 padding: 8px 0;
971
916
972 .filediff-heading {
917 .filediff-heading {
973 opacity: .5;
918 opacity: .5;
974 }
919 }
975 }
920 }
976
921
977 table.cb {
922 table.cb {
978 width: 100%;
923 width: 100%;
979 border-collapse: collapse;
924 border-collapse: collapse;
980
925
981 .cb-text {
926 .cb-text {
982 padding: @cb-text-padding;
927 padding: @cb-text-padding;
983 }
928 }
984 .cb-hunk {
929 .cb-hunk {
985 padding: @cb-text-padding;
930 padding: @cb-text-padding;
986 }
931 }
987 .cb-expand {
932 .cb-expand {
988 display: none;
933 display: none;
989 }
934 }
990 .cb-collapse {
935 .cb-collapse {
991 display: inline;
936 display: inline;
992 }
937 }
993 &.cb-collapsed {
938 &.cb-collapsed {
994 .cb-line {
939 .cb-line {
995 display: none;
940 display: none;
996 }
941 }
997 .cb-expand {
942 .cb-expand {
998 display: inline;
943 display: inline;
999 }
944 }
1000 .cb-collapse {
945 .cb-collapse {
1001 display: none;
946 display: none;
1002 }
947 }
1003 }
948 }
1004
949
1005 /* intentionally general selector since .cb-line-selected must override it
950 /* intentionally general selector since .cb-line-selected must override it
1006 and they both use !important since the td itself may have a random color
951 and they both use !important since the td itself may have a random color
1007 generated by annotation blocks. TLDR: if you change it, make sure
952 generated by annotation blocks. TLDR: if you change it, make sure
1008 annotated block selection and line selection in file view still work */
953 annotated block selection and line selection in file view still work */
1009 .cb-line-fresh .cb-content {
954 .cb-line-fresh .cb-content {
1010 background: white !important;
955 background: white !important;
1011 }
956 }
1012 .cb-warning {
957 .cb-warning {
1013 background: #fff4dd;
958 background: #fff4dd;
1014 }
959 }
1015
960
1016 &.cb-diff-sideside {
961 &.cb-diff-sideside {
1017 td {
962 td {
1018 &.cb-content {
963 &.cb-content {
1019 width: 50%;
964 width: 50%;
1020 }
965 }
1021 }
966 }
1022 }
967 }
1023
968
1024 tr {
969 tr {
1025 &.cb-annotate {
970 &.cb-annotate {
1026 border-top: 1px solid #eee;
971 border-top: 1px solid #eee;
1027
972
1028 &+ .cb-line {
973 &+ .cb-line {
1029 border-top: 1px solid #eee;
974 border-top: 1px solid #eee;
1030 }
975 }
1031
976
1032 &:first-child {
977 &:first-child {
1033 border-top: none;
978 border-top: none;
1034 &+ .cb-line {
979 &+ .cb-line {
1035 border-top: none;
980 border-top: none;
1036 }
981 }
1037 }
982 }
1038 }
983 }
1039
984
1040 &.cb-hunk {
985 &.cb-hunk {
1041 font-family: @font-family-monospace;
986 font-family: @font-family-monospace;
1042 color: rgba(0, 0, 0, 0.3);
987 color: rgba(0, 0, 0, 0.3);
1043
988
1044 td {
989 td {
1045 &:first-child {
990 &:first-child {
1046 background: #edf2f9;
991 background: #edf2f9;
1047 }
992 }
1048 &:last-child {
993 &:last-child {
1049 background: #f4f7fb;
994 background: #f4f7fb;
1050 }
995 }
1051 }
996 }
1052 }
997 }
1053 }
998 }
1054
999
1055
1000
1056 td {
1001 td {
1057 vertical-align: top;
1002 vertical-align: top;
1058 padding: 0;
1003 padding: 0;
1059
1004
1060 &.cb-content {
1005 &.cb-content {
1061 font-size: 12.35px;
1006 font-size: 12.35px;
1062
1007
1063 &.cb-line-selected .cb-code {
1008 &.cb-line-selected .cb-code {
1064 background: @comment-highlight-color !important;
1009 background: @comment-highlight-color !important;
1065 }
1010 }
1066
1011
1067 span.cb-code {
1012 span.cb-code {
1068 line-height: @cb-line-height;
1013 line-height: @cb-line-height;
1069 padding-left: @cb-line-code-padding;
1014 padding-left: @cb-line-code-padding;
1070 padding-right: @cb-line-code-padding;
1015 padding-right: @cb-line-code-padding;
1071 display: block;
1016 display: block;
1072 white-space: pre-wrap;
1017 white-space: pre-wrap;
1073 font-family: @font-family-monospace;
1018 font-family: @font-family-monospace;
1074 word-break: break-word;
1019 word-break: break-word;
1075 .nonl {
1020 .nonl {
1076 color: @color5;
1021 color: @color5;
1077 }
1022 }
1078 }
1023 }
1079
1024
1080 &> button.cb-comment-box-opener {
1025 &> button.cb-comment-box-opener {
1081
1026
1082 padding: 2px 2px 1px 3px;
1027 padding: 2px 2px 1px 3px;
1083 margin-left: -6px;
1028 margin-left: -6px;
1084 margin-top: -1px;
1029 margin-top: -1px;
1085
1030
1086 border-radius: @border-radius;
1031 border-radius: @border-radius;
1087 position: absolute;
1032 position: absolute;
1088 display: none;
1033 display: none;
1089 }
1034 }
1090 .cb-comment {
1035 .cb-comment {
1091 margin-top: 10px;
1036 margin-top: 10px;
1092 white-space: normal;
1037 white-space: normal;
1093 }
1038 }
1094 }
1039 }
1095 &:hover {
1040 &:hover {
1096 button.cb-comment-box-opener {
1041 button.cb-comment-box-opener {
1097 display: block;
1042 display: block;
1098 }
1043 }
1099 &+ td button.cb-comment-box-opener {
1044 &+ td button.cb-comment-box-opener {
1100 display: block
1045 display: block
1101 }
1046 }
1102 }
1047 }
1103
1048
1104 &.cb-data {
1049 &.cb-data {
1105 text-align: right;
1050 text-align: right;
1106 width: 30px;
1051 width: 30px;
1107 font-family: @font-family-monospace;
1052 font-family: @font-family-monospace;
1108
1053
1109 .icon-comment {
1054 .icon-comment {
1110 cursor: pointer;
1055 cursor: pointer;
1111 }
1056 }
1112 &.cb-line-selected > div {
1057 &.cb-line-selected > div {
1113 display: block;
1058 display: block;
1114 background: @comment-highlight-color !important;
1059 background: @comment-highlight-color !important;
1115 line-height: @cb-line-height;
1060 line-height: @cb-line-height;
1116 color: rgba(0, 0, 0, 0.3);
1061 color: rgba(0, 0, 0, 0.3);
1117 }
1062 }
1118 }
1063 }
1119
1064
1120 &.cb-lineno {
1065 &.cb-lineno {
1121 padding: 0;
1066 padding: 0;
1122 width: 50px;
1067 width: 50px;
1123 color: rgba(0, 0, 0, 0.3);
1068 color: rgba(0, 0, 0, 0.3);
1124 text-align: right;
1069 text-align: right;
1125 border-right: 1px solid #eee;
1070 border-right: 1px solid #eee;
1126 font-family: @font-family-monospace;
1071 font-family: @font-family-monospace;
1127
1072
1128 a::before {
1073 a::before {
1129 content: attr(data-line-no);
1074 content: attr(data-line-no);
1130 }
1075 }
1131 &.cb-line-selected a {
1076 &.cb-line-selected a {
1132 background: @comment-highlight-color !important;
1077 background: @comment-highlight-color !important;
1133 }
1078 }
1134
1079
1135 a {
1080 a {
1136 display: block;
1081 display: block;
1137 padding-right: @cb-line-code-padding;
1082 padding-right: @cb-line-code-padding;
1138 padding-left: @cb-line-code-padding;
1083 padding-left: @cb-line-code-padding;
1139 line-height: @cb-line-height;
1084 line-height: @cb-line-height;
1140 color: rgba(0, 0, 0, 0.3);
1085 color: rgba(0, 0, 0, 0.3);
1141 }
1086 }
1142 }
1087 }
1143
1088
1144 &.cb-empty {
1089 &.cb-empty {
1145 background: @grey7;
1090 background: @grey7;
1146 }
1091 }
1147
1092
1148 ins {
1093 ins {
1149 color: black;
1094 color: black;
1150 background: #a6f3a6;
1095 background: #a6f3a6;
1151 text-decoration: none;
1096 text-decoration: none;
1152 }
1097 }
1153 del {
1098 del {
1154 color: black;
1099 color: black;
1155 background: #f8cbcb;
1100 background: #f8cbcb;
1156 text-decoration: none;
1101 text-decoration: none;
1157 }
1102 }
1158 &.cb-addition {
1103 &.cb-addition {
1159 background: #ecffec;
1104 background: #ecffec;
1160
1105
1161 &.blob-lineno {
1106 &.blob-lineno {
1162 background: #ddffdd;
1107 background: #ddffdd;
1163 }
1108 }
1164 }
1109 }
1165 &.cb-deletion {
1110 &.cb-deletion {
1166 background: #ffecec;
1111 background: #ffecec;
1167
1112
1168 &.blob-lineno {
1113 &.blob-lineno {
1169 background: #ffdddd;
1114 background: #ffdddd;
1170 }
1115 }
1171 }
1116 }
1172
1117
1173 &.cb-annotate-info {
1118 &.cb-annotate-info {
1174 width: 320px;
1119 width: 320px;
1175 min-width: 320px;
1120 min-width: 320px;
1176 max-width: 320px;
1121 max-width: 320px;
1177 padding: 5px 2px;
1122 padding: 5px 2px;
1178 font-size: 13px;
1123 font-size: 13px;
1179
1124
1180 strong.cb-annotate-message {
1125 strong.cb-annotate-message {
1181 padding: 5px 0;
1126 padding: 5px 0;
1182 white-space: pre-line;
1127 white-space: pre-line;
1183 display: inline-block;
1128 display: inline-block;
1184 }
1129 }
1185 .rc-user {
1130 .rc-user {
1186 float: none;
1131 float: none;
1187 padding: 0 6px 0 17px;
1132 padding: 0 6px 0 17px;
1188 min-width: auto;
1133 min-width: auto;
1189 min-height: auto;
1134 min-height: auto;
1190 }
1135 }
1191 }
1136 }
1192
1137
1193 &.cb-annotate-revision {
1138 &.cb-annotate-revision {
1194 cursor: pointer;
1139 cursor: pointer;
1195 text-align: right;
1140 text-align: right;
1196 }
1141 }
1197 }
1142 }
1198 }
1143 }
@@ -1,432 +1,527 b''
1 // comments.less
1 // comments.less
2 // For use in RhodeCode applications;
2 // For use in RhodeCode applications;
3 // see style guide documentation for guidelines.
3 // see style guide documentation for guidelines.
4
4
5
5
6 // Comments
6 // Comments
7 .comments {
7 .comments {
8 width: 100%;
8 width: 100%;
9 }
9 }
10
10
11 tr.inline-comments div {
11 tr.inline-comments div {
12 max-width: 100%;
12 max-width: 100%;
13
13
14 p {
14 p {
15 white-space: normal;
15 white-space: normal;
16 }
16 }
17
17
18 code, pre, .code, dd {
18 code, pre, .code, dd {
19 overflow-x: auto;
19 overflow-x: auto;
20 width: 1062px;
20 width: 1062px;
21 }
21 }
22
22
23 dd {
23 dd {
24 width: auto;
24 width: auto;
25 }
25 }
26 }
26 }
27
27
28 #injected_page_comments {
28 #injected_page_comments {
29 .comment-previous-link,
29 .comment-previous-link,
30 .comment-next-link,
30 .comment-next-link,
31 .comment-links-divider {
31 .comment-links-divider {
32 display: none;
32 display: none;
33 }
33 }
34 }
34 }
35
35
36 .add-comment {
36 .add-comment {
37 margin-bottom: 10px;
37 margin-bottom: 10px;
38 }
38 }
39 .hide-comment-button .add-comment {
39 .hide-comment-button .add-comment {
40 display: none;
40 display: none;
41 }
41 }
42
42
43 .comment-bubble {
43 .comment-bubble {
44 color: @grey4;
44 color: @grey4;
45 margin-top: 4px;
45 margin-top: 4px;
46 margin-right: 30px;
46 margin-right: 30px;
47 visibility: hidden;
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 .comment {
77 .comment {
51
78
52 &.comment-general {
79 &.comment-general {
53 border: 1px solid @grey5;
80 border: 1px solid @grey5;
54 padding: 5px 5px 5px 5px;
81 padding: 5px 5px 5px 5px;
55 }
82 }
56
83
57 margin: @padding 0;
84 margin: @padding 0;
58 padding: 4px 0 0 0;
85 padding: 4px 0 0 0;
59 line-height: 1em;
86 line-height: 1em;
60
87
61 .rc-user {
88 .rc-user {
62 min-width: 0;
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 .meta {
97 .meta {
67 position: relative;
98 position: relative;
68 width: 100%;
99 width: 100%;
69 margin: 0 0 .5em 0;
70 border-bottom: 1px solid @grey5;
100 border-bottom: 1px solid @grey5;
71 padding: 8px 0px;
101 margin: -5px 0px;
102 line-height: 24px;
72
103
73 &:hover .permalink {
104 &:hover .permalink {
74 visibility: visible;
105 visibility: visible;
75 color: @rcblue;
106 color: @rcblue;
76 }
107 }
77 }
108 }
78
109
79 .author,
110 .author,
80 .date {
111 .date {
81 display: inline;
112 display: inline;
82
113
83 &:after {
114 &:after {
84 content: ' | ';
115 content: ' | ';
85 color: @grey5;
116 color: @grey5;
86 }
117 }
87 }
118 }
88
119
89 .author-general img {
120 .author-general img {
90 top: -3px;
121 top: 3px;
91 }
122 }
92 .author-inline img {
123 .author-inline img {
93 top: -3px;
124 top: 3px;
94 }
125 }
95
126
96 .status-change,
127 .status-change,
97 .permalink,
128 .permalink,
98 .changeset-status-lbl {
129 .changeset-status-lbl {
99 display: inline;
130 display: inline;
100 }
131 }
101
132
102 .permalink {
133 .permalink {
103 visibility: hidden;
134 visibility: hidden;
104 }
135 }
105
136
106 .comment-links-divider {
137 .comment-links-divider {
107 display: inline;
138 display: inline;
108 }
139 }
109
140
110 .comment-links-block {
141 .comment-links-block {
111 float:right;
142 float:right;
112 text-align: right;
143 text-align: right;
113 min-width: 85px;
144 min-width: 85px;
114
145
115 [class^="icon-"]:before,
146 [class^="icon-"]:before,
116 [class*=" icon-"]:before {
147 [class*=" icon-"]:before {
117 margin-left: 0;
148 margin-left: 0;
118 margin-right: 0;
149 margin-right: 0;
119 }
150 }
120 }
151 }
121
152
122 .comment-previous-link {
153 .comment-previous-link {
123 display: inline-block;
154 display: inline-block;
124
155
125 .arrow_comment_link{
156 .arrow_comment_link{
126 cursor: pointer;
157 cursor: pointer;
127 i {
158 i {
128 font-size:10px;
159 font-size:10px;
129 }
160 }
130 }
161 }
131 .arrow_comment_link.disabled {
162 .arrow_comment_link.disabled {
132 cursor: default;
163 cursor: default;
133 color: @grey5;
164 color: @grey5;
134 }
165 }
135 }
166 }
136
167
137 .comment-next-link {
168 .comment-next-link {
138 display: inline-block;
169 display: inline-block;
139
170
140 .arrow_comment_link{
171 .arrow_comment_link{
141 cursor: pointer;
172 cursor: pointer;
142 i {
173 i {
143 font-size:10px;
174 font-size:10px;
144 }
175 }
145 }
176 }
146 .arrow_comment_link.disabled {
177 .arrow_comment_link.disabled {
147 cursor: default;
178 cursor: default;
148 color: @grey5;
179 color: @grey5;
149 }
180 }
150 }
181 }
151
182
152 .flag_status {
183 .flag_status {
153 display: inline-block;
184 display: inline-block;
154 margin: -2px .5em 0 .25em
185 margin: -2px .5em 0 .25em
155 }
186 }
156
187
157 .delete-comment {
188 .delete-comment {
158 display: inline-block;
189 display: inline-block;
159 color: @rcblue;
190 color: @rcblue;
160
191
161 &:hover {
192 &:hover {
162 cursor: pointer;
193 cursor: pointer;
163 }
194 }
164 }
195 }
165
196
166
197
167 .text {
198 .text {
168 clear: both;
199 clear: both;
169 .border-radius(@border-radius);
200 .border-radius(@border-radius);
170 .box-sizing(border-box);
201 .box-sizing(border-box);
171
202
172 .markdown-block p,
203 .markdown-block p,
173 .rst-block p {
204 .rst-block p {
174 margin: .5em 0 !important;
205 margin: .5em 0 !important;
175 // TODO: lisa: This is needed because of other rst !important rules :[
206 // TODO: lisa: This is needed because of other rst !important rules :[
176 }
207 }
177 }
208 }
178
209
179 .pr-version {
210 .pr-version {
180 float: left;
211 float: left;
181 margin: 0px 4px;
212 margin: 0px 4px;
182 }
213 }
183 .pr-version-inline {
214 .pr-version-inline {
184 float: left;
215 float: left;
185 margin: 1px 4px;
216 margin: 0px 4px;
186 }
217 }
187 .pr-version-num {
218 .pr-version-num {
188 font-size: 10px;
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 .show-outdated-comments {
282 .show-outdated-comments {
194 display: inline;
283 display: inline;
195 color: @rcblue;
284 color: @rcblue;
196 }
285 }
197
286
198 // Comment Form
287 // Comment Form
199 div.comment-form {
288 div.comment-form {
200 margin-top: 20px;
289 margin-top: 20px;
201 }
290 }
202
291
203 .comment-form strong {
292 .comment-form strong {
204 display: block;
293 display: block;
205 margin-bottom: 15px;
294 margin-bottom: 15px;
206 }
295 }
207
296
208 .comment-form textarea {
297 .comment-form textarea {
209 width: 100%;
298 width: 100%;
210 height: 100px;
299 height: 100px;
211 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
300 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
212 }
301 }
213
302
214 form.comment-form {
303 form.comment-form {
215 margin-top: 10px;
304 margin-top: 10px;
216 margin-left: 10px;
305 margin-left: 10px;
217 }
306 }
218
307
219 .comment-inline-form .comment-block-ta,
308 .comment-inline-form .comment-block-ta,
220 .comment-form .comment-block-ta,
309 .comment-form .comment-block-ta,
221 .comment-form .preview-box {
310 .comment-form .preview-box {
222 .border-radius(@border-radius);
311 .border-radius(@border-radius);
223 .box-sizing(border-box);
312 .box-sizing(border-box);
224 background-color: white;
313 background-color: white;
225 }
314 }
226
315
227 .comment-form-submit {
316 .comment-form-submit {
228 margin-top: 5px;
317 margin-top: 5px;
229 margin-left: 525px;
318 margin-left: 525px;
230 }
319 }
231
320
232 .file-comments {
321 .file-comments {
233 display: none;
322 display: none;
234 }
323 }
235
324
236 .comment-form .preview-box.unloaded,
325 .comment-form .preview-box.unloaded,
237 .comment-inline-form .preview-box.unloaded {
326 .comment-inline-form .preview-box.unloaded {
238 height: 50px;
327 height: 50px;
239 text-align: center;
328 text-align: center;
240 padding: 20px;
329 padding: 20px;
241 background-color: white;
330 background-color: white;
242 }
331 }
243
332
244 .comment-footer {
333 .comment-footer {
245 position: relative;
334 position: relative;
246 width: 100%;
335 width: 100%;
247 min-height: 42px;
336 min-height: 42px;
248
337
249 .status_box,
338 .status_box,
250 .cancel-button {
339 .cancel-button {
251 float: left;
340 float: left;
252 display: inline-block;
341 display: inline-block;
253 }
342 }
254
343
255 .action-buttons {
344 .action-buttons {
256 float: right;
345 float: right;
257 display: inline-block;
346 display: inline-block;
258 }
347 }
259 }
348 }
260
349
261 .comment-form {
350 .comment-form {
262
351
263 .comment {
352 .comment {
264 margin-left: 10px;
353 margin-left: 10px;
265 }
354 }
266
355
267 .comment-help {
356 .comment-help {
268 color: @grey4;
357 color: @grey4;
269 padding: 5px 0 5px 0;
358 padding: 5px 0 5px 0;
270 }
359 }
271
360
272 .comment-title {
361 .comment-title {
273 padding: 5px 0 5px 0;
362 padding: 5px 0 5px 0;
274 }
363 }
275
364
276 .comment-button {
365 .comment-button {
277 display: inline-block;
366 display: inline-block;
278 }
367 }
279
368
280 .comment-button .comment-button-input {
369 .comment-button .comment-button-input {
281 margin-right: 0;
370 margin-right: 0;
282 }
371 }
283
372
284 .comment-footer {
373 .comment-footer {
285 margin-bottom: 110px;
374 margin-bottom: 110px;
286 margin-top: 10px;
375 margin-top: 10px;
287 }
376 }
288 }
377 }
289
378
290
379
291 .comment-form-login {
380 .comment-form-login {
292 .comment-help {
381 .comment-help {
293 padding: 0.9em; //same as the button
382 padding: 0.9em; //same as the button
294 }
383 }
295
384
296 div.clearfix {
385 div.clearfix {
297 clear: both;
386 clear: both;
298 width: 100%;
387 width: 100%;
299 display: block;
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 .preview-box {
398 .preview-box {
304 min-height: 105px;
399 min-height: 105px;
305 margin-bottom: 15px;
400 margin-bottom: 15px;
306 background-color: white;
401 background-color: white;
307 .border-radius(@border-radius);
402 .border-radius(@border-radius);
308 .box-sizing(border-box);
403 .box-sizing(border-box);
309 }
404 }
310
405
311 .add-another-button {
406 .add-another-button {
312 margin-left: 10px;
407 margin-left: 10px;
313 margin-top: 10px;
408 margin-top: 10px;
314 margin-bottom: 10px;
409 margin-bottom: 10px;
315 }
410 }
316
411
317 .comment .buttons {
412 .comment .buttons {
318 float: right;
413 float: right;
319 margin: -1px 0px 0px 0px;
414 margin: -1px 0px 0px 0px;
320 }
415 }
321
416
322 // Inline Comment Form
417 // Inline Comment Form
323 .injected_diff .comment-inline-form,
418 .injected_diff .comment-inline-form,
324 .comment-inline-form {
419 .comment-inline-form {
325 background-color: white;
420 background-color: white;
326 margin-top: 10px;
421 margin-top: 10px;
327 margin-bottom: 20px;
422 margin-bottom: 20px;
328 }
423 }
329
424
330 .inline-form {
425 .inline-form {
331 padding: 10px 7px;
426 padding: 10px 7px;
332 }
427 }
333
428
334 .inline-form div {
429 .inline-form div {
335 max-width: 100%;
430 max-width: 100%;
336 }
431 }
337
432
338 .overlay {
433 .overlay {
339 display: none;
434 display: none;
340 position: absolute;
435 position: absolute;
341 width: 100%;
436 width: 100%;
342 text-align: center;
437 text-align: center;
343 vertical-align: middle;
438 vertical-align: middle;
344 font-size: 16px;
439 font-size: 16px;
345 background: none repeat scroll 0 0 white;
440 background: none repeat scroll 0 0 white;
346
441
347 &.submitting {
442 &.submitting {
348 display: block;
443 display: block;
349 opacity: 0.5;
444 opacity: 0.5;
350 z-index: 100;
445 z-index: 100;
351 }
446 }
352 }
447 }
353 .comment-inline-form .overlay.submitting .overlay-text {
448 .comment-inline-form .overlay.submitting .overlay-text {
354 margin-top: 5%;
449 margin-top: 5%;
355 }
450 }
356
451
357 .comment-inline-form .clearfix,
452 .comment-inline-form .clearfix,
358 .comment-form .clearfix {
453 .comment-form .clearfix {
359 .border-radius(@border-radius);
454 .border-radius(@border-radius);
360 margin: 0px;
455 margin: 0px;
361 }
456 }
362
457
363 .comment-inline-form .comment-footer {
458 .comment-inline-form .comment-footer {
364 margin: 10px 0px 0px 0px;
459 margin: 10px 0px 0px 0px;
365 }
460 }
366
461
367 .hide-inline-form-button {
462 .hide-inline-form-button {
368 margin-left: 5px;
463 margin-left: 5px;
369 }
464 }
370 .comment-button .hide-inline-form {
465 .comment-button .hide-inline-form {
371 background: white;
466 background: white;
372 }
467 }
373
468
374 .comment-area {
469 .comment-area {
375 padding: 8px 12px;
470 padding: 8px 12px;
376 border: 1px solid @grey5;
471 border: 1px solid @grey5;
377 .border-radius(@border-radius);
472 .border-radius(@border-radius);
378 }
473 }
379
474
380 .comment-area-header .nav-links {
475 .comment-area-header .nav-links {
381 display: flex;
476 display: flex;
382 flex-flow: row wrap;
477 flex-flow: row wrap;
383 -webkit-flex-flow: row wrap;
478 -webkit-flex-flow: row wrap;
384 width: 100%;
479 width: 100%;
385 }
480 }
386
481
387 .comment-area-footer {
482 .comment-area-footer {
388 display: flex;
483 display: flex;
389 }
484 }
390
485
391 .comment-footer .toolbar {
486 .comment-footer .toolbar {
392
487
393 }
488 }
394
489
395 .nav-links {
490 .nav-links {
396 padding: 0;
491 padding: 0;
397 margin: 0;
492 margin: 0;
398 list-style: none;
493 list-style: none;
399 height: auto;
494 height: auto;
400 border-bottom: 1px solid @grey5;
495 border-bottom: 1px solid @grey5;
401 }
496 }
402 .nav-links li {
497 .nav-links li {
403 display: inline-block;
498 display: inline-block;
404 }
499 }
405 .nav-links li:before {
500 .nav-links li:before {
406 content: "";
501 content: "";
407 }
502 }
408 .nav-links li a.disabled {
503 .nav-links li a.disabled {
409 cursor: not-allowed;
504 cursor: not-allowed;
410 }
505 }
411
506
412 .nav-links li.active a {
507 .nav-links li.active a {
413 border-bottom: 2px solid @rcblue;
508 border-bottom: 2px solid @rcblue;
414 color: #000;
509 color: #000;
415 font-weight: 600;
510 font-weight: 600;
416 }
511 }
417 .nav-links li a {
512 .nav-links li a {
418 display: inline-block;
513 display: inline-block;
419 padding: 0px 10px 5px 10px;
514 padding: 0px 10px 5px 10px;
420 margin-bottom: -1px;
515 margin-bottom: -1px;
421 font-size: 14px;
516 font-size: 14px;
422 line-height: 28px;
517 line-height: 28px;
423 color: #8f8f8f;
518 color: #8f8f8f;
424 border-bottom: 2px solid transparent;
519 border-bottom: 2px solid transparent;
425 }
520 }
426
521
427 .toolbar-text {
522 .toolbar-text {
428 float: left;
523 float: left;
429 margin: -5px 0px 0px 0px;
524 margin: -5px 0px 0px 0px;
430 font-size: 12px;
525 font-size: 12px;
431 }
526 }
432
527
@@ -1,2222 +1,2223 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'fonts';
8 @import 'fonts';
9 @import 'variables';
9 @import 'variables';
10 @import 'bootstrap-variables';
10 @import 'bootstrap-variables';
11 @import 'form-bootstrap';
11 @import 'form-bootstrap';
12 @import 'codemirror';
12 @import 'codemirror';
13 @import 'legacy_code_styles';
13 @import 'legacy_code_styles';
14 @import 'progress-bar';
14 @import 'progress-bar';
15
15
16 @import 'type';
16 @import 'type';
17 @import 'alerts';
17 @import 'alerts';
18 @import 'buttons';
18 @import 'buttons';
19 @import 'tags';
19 @import 'tags';
20 @import 'code-block';
20 @import 'code-block';
21 @import 'examples';
21 @import 'examples';
22 @import 'login';
22 @import 'login';
23 @import 'main-content';
23 @import 'main-content';
24 @import 'select2';
24 @import 'select2';
25 @import 'comments';
25 @import 'comments';
26 @import 'panels-bootstrap';
26 @import 'panels-bootstrap';
27 @import 'panels';
27 @import 'panels';
28 @import 'deform';
28 @import 'deform';
29
29
30 //--- BASE ------------------//
30 //--- BASE ------------------//
31 .noscript-error {
31 .noscript-error {
32 top: 0;
32 top: 0;
33 left: 0;
33 left: 0;
34 width: 100%;
34 width: 100%;
35 z-index: 101;
35 z-index: 101;
36 text-align: center;
36 text-align: center;
37 font-family: @text-semibold;
37 font-family: @text-semibold;
38 font-size: 120%;
38 font-size: 120%;
39 color: white;
39 color: white;
40 background-color: @alert2;
40 background-color: @alert2;
41 padding: 5px 0 5px 0;
41 padding: 5px 0 5px 0;
42 }
42 }
43
43
44 html {
44 html {
45 display: table;
45 display: table;
46 height: 100%;
46 height: 100%;
47 width: 100%;
47 width: 100%;
48 }
48 }
49
49
50 body {
50 body {
51 display: table-cell;
51 display: table-cell;
52 width: 100%;
52 width: 100%;
53 }
53 }
54
54
55 //--- LAYOUT ------------------//
55 //--- LAYOUT ------------------//
56
56
57 .hidden{
57 .hidden{
58 display: none !important;
58 display: none !important;
59 }
59 }
60
60
61 .box{
61 .box{
62 float: left;
62 float: left;
63 width: 100%;
63 width: 100%;
64 }
64 }
65
65
66 .browser-header {
66 .browser-header {
67 clear: both;
67 clear: both;
68 }
68 }
69 .main {
69 .main {
70 clear: both;
70 clear: both;
71 padding:0 0 @pagepadding;
71 padding:0 0 @pagepadding;
72 height: auto;
72 height: auto;
73
73
74 &:after { //clearfix
74 &:after { //clearfix
75 content:"";
75 content:"";
76 clear:both;
76 clear:both;
77 width:100%;
77 width:100%;
78 display:block;
78 display:block;
79 }
79 }
80 }
80 }
81
81
82 .action-link{
82 .action-link{
83 margin-left: @padding;
83 margin-left: @padding;
84 padding-left: @padding;
84 padding-left: @padding;
85 border-left: @border-thickness solid @border-default-color;
85 border-left: @border-thickness solid @border-default-color;
86 }
86 }
87
87
88 input + .action-link, .action-link.first{
88 input + .action-link, .action-link.first{
89 border-left: none;
89 border-left: none;
90 }
90 }
91
91
92 .action-link.last{
92 .action-link.last{
93 margin-right: @padding;
93 margin-right: @padding;
94 padding-right: @padding;
94 padding-right: @padding;
95 }
95 }
96
96
97 .action-link.active,
97 .action-link.active,
98 .action-link.active a{
98 .action-link.active a{
99 color: @grey4;
99 color: @grey4;
100 }
100 }
101
101
102 ul.simple-list{
102 ul.simple-list{
103 list-style: none;
103 list-style: none;
104 margin: 0;
104 margin: 0;
105 padding: 0;
105 padding: 0;
106 }
106 }
107
107
108 .main-content {
108 .main-content {
109 padding-bottom: @pagepadding;
109 padding-bottom: @pagepadding;
110 }
110 }
111
111
112 .wide-mode-wrapper {
112 .wide-mode-wrapper {
113 max-width:4000px !important;
113 max-width:4000px !important;
114 }
114 }
115
115
116 .wrapper {
116 .wrapper {
117 position: relative;
117 position: relative;
118 max-width: @wrapper-maxwidth;
118 max-width: @wrapper-maxwidth;
119 margin: 0 auto;
119 margin: 0 auto;
120 }
120 }
121
121
122 #content {
122 #content {
123 clear: both;
123 clear: both;
124 padding: 0 @contentpadding;
124 padding: 0 @contentpadding;
125 }
125 }
126
126
127 .advanced-settings-fields{
127 .advanced-settings-fields{
128 input{
128 input{
129 margin-left: @textmargin;
129 margin-left: @textmargin;
130 margin-right: @padding/2;
130 margin-right: @padding/2;
131 }
131 }
132 }
132 }
133
133
134 .cs_files_title {
134 .cs_files_title {
135 margin: @pagepadding 0 0;
135 margin: @pagepadding 0 0;
136 }
136 }
137
137
138 input.inline[type="file"] {
138 input.inline[type="file"] {
139 display: inline;
139 display: inline;
140 }
140 }
141
141
142 .error_page {
142 .error_page {
143 margin: 10% auto;
143 margin: 10% auto;
144
144
145 h1 {
145 h1 {
146 color: @grey2;
146 color: @grey2;
147 }
147 }
148
148
149 .alert {
149 .alert {
150 margin: @padding 0;
150 margin: @padding 0;
151 }
151 }
152
152
153 .error-branding {
153 .error-branding {
154 font-family: @text-semibold;
154 font-family: @text-semibold;
155 color: @grey4;
155 color: @grey4;
156 }
156 }
157
157
158 .error_message {
158 .error_message {
159 font-family: @text-regular;
159 font-family: @text-regular;
160 }
160 }
161
161
162 .sidebar {
162 .sidebar {
163 min-height: 275px;
163 min-height: 275px;
164 margin: 0;
164 margin: 0;
165 padding: 0 0 @sidebarpadding @sidebarpadding;
165 padding: 0 0 @sidebarpadding @sidebarpadding;
166 border: none;
166 border: none;
167 }
167 }
168
168
169 .main-content {
169 .main-content {
170 position: relative;
170 position: relative;
171 margin: 0 @sidebarpadding @sidebarpadding;
171 margin: 0 @sidebarpadding @sidebarpadding;
172 padding: 0 0 0 @sidebarpadding;
172 padding: 0 0 0 @sidebarpadding;
173 border-left: @border-thickness solid @grey5;
173 border-left: @border-thickness solid @grey5;
174
174
175 @media (max-width:767px) {
175 @media (max-width:767px) {
176 clear: both;
176 clear: both;
177 width: 100%;
177 width: 100%;
178 margin: 0;
178 margin: 0;
179 border: none;
179 border: none;
180 }
180 }
181 }
181 }
182
182
183 .inner-column {
183 .inner-column {
184 float: left;
184 float: left;
185 width: 29.75%;
185 width: 29.75%;
186 min-height: 150px;
186 min-height: 150px;
187 margin: @sidebarpadding 2% 0 0;
187 margin: @sidebarpadding 2% 0 0;
188 padding: 0 2% 0 0;
188 padding: 0 2% 0 0;
189 border-right: @border-thickness solid @grey5;
189 border-right: @border-thickness solid @grey5;
190
190
191 @media (max-width:767px) {
191 @media (max-width:767px) {
192 clear: both;
192 clear: both;
193 width: 100%;
193 width: 100%;
194 border: none;
194 border: none;
195 }
195 }
196
196
197 ul {
197 ul {
198 padding-left: 1.25em;
198 padding-left: 1.25em;
199 }
199 }
200
200
201 &:last-child {
201 &:last-child {
202 margin: @sidebarpadding 0 0;
202 margin: @sidebarpadding 0 0;
203 border: none;
203 border: none;
204 }
204 }
205
205
206 h4 {
206 h4 {
207 margin: 0 0 @padding;
207 margin: 0 0 @padding;
208 font-family: @text-semibold;
208 font-family: @text-semibold;
209 }
209 }
210 }
210 }
211 }
211 }
212 .error-page-logo {
212 .error-page-logo {
213 width: 130px;
213 width: 130px;
214 height: 160px;
214 height: 160px;
215 }
215 }
216
216
217 // HEADER
217 // HEADER
218 .header {
218 .header {
219
219
220 // TODO: johbo: Fix login pages, so that they work without a min-height
220 // TODO: johbo: Fix login pages, so that they work without a min-height
221 // for the header and then remove the min-height. I chose a smaller value
221 // for the header and then remove the min-height. I chose a smaller value
222 // intentionally here to avoid rendering issues in the main navigation.
222 // intentionally here to avoid rendering issues in the main navigation.
223 min-height: 49px;
223 min-height: 49px;
224
224
225 position: relative;
225 position: relative;
226 vertical-align: bottom;
226 vertical-align: bottom;
227 padding: 0 @header-padding;
227 padding: 0 @header-padding;
228 background-color: @grey2;
228 background-color: @grey2;
229 color: @grey5;
229 color: @grey5;
230
230
231 .title {
231 .title {
232 overflow: visible;
232 overflow: visible;
233 }
233 }
234
234
235 &:before,
235 &:before,
236 &:after {
236 &:after {
237 content: "";
237 content: "";
238 clear: both;
238 clear: both;
239 width: 100%;
239 width: 100%;
240 }
240 }
241
241
242 // TODO: johbo: Avoids breaking "Repositories" chooser
242 // TODO: johbo: Avoids breaking "Repositories" chooser
243 .select2-container .select2-choice .select2-arrow {
243 .select2-container .select2-choice .select2-arrow {
244 display: none;
244 display: none;
245 }
245 }
246 }
246 }
247
247
248 #header-inner {
248 #header-inner {
249 &.title {
249 &.title {
250 margin: 0;
250 margin: 0;
251 }
251 }
252 &:before,
252 &:before,
253 &:after {
253 &:after {
254 content: "";
254 content: "";
255 clear: both;
255 clear: both;
256 }
256 }
257 }
257 }
258
258
259 // Gists
259 // Gists
260 #files_data {
260 #files_data {
261 clear: both; //for firefox
261 clear: both; //for firefox
262 }
262 }
263 #gistid {
263 #gistid {
264 margin-right: @padding;
264 margin-right: @padding;
265 }
265 }
266
266
267 // Global Settings Editor
267 // Global Settings Editor
268 .textarea.editor {
268 .textarea.editor {
269 float: left;
269 float: left;
270 position: relative;
270 position: relative;
271 max-width: @texteditor-width;
271 max-width: @texteditor-width;
272
272
273 select {
273 select {
274 position: absolute;
274 position: absolute;
275 top:10px;
275 top:10px;
276 right:0;
276 right:0;
277 }
277 }
278
278
279 .CodeMirror {
279 .CodeMirror {
280 margin: 0;
280 margin: 0;
281 }
281 }
282
282
283 .help-block {
283 .help-block {
284 margin: 0 0 @padding;
284 margin: 0 0 @padding;
285 padding:.5em;
285 padding:.5em;
286 background-color: @grey6;
286 background-color: @grey6;
287 }
287 }
288 }
288 }
289
289
290 ul.auth_plugins {
290 ul.auth_plugins {
291 margin: @padding 0 @padding @legend-width;
291 margin: @padding 0 @padding @legend-width;
292 padding: 0;
292 padding: 0;
293
293
294 li {
294 li {
295 margin-bottom: @padding;
295 margin-bottom: @padding;
296 line-height: 1em;
296 line-height: 1em;
297 list-style-type: none;
297 list-style-type: none;
298
298
299 .auth_buttons .btn {
299 .auth_buttons .btn {
300 margin-right: @padding;
300 margin-right: @padding;
301 }
301 }
302
302
303 &:before { content: none; }
303 &:before { content: none; }
304 }
304 }
305 }
305 }
306
306
307
307
308 // My Account PR list
308 // My Account PR list
309
309
310 #show_closed {
310 #show_closed {
311 margin: 0 1em 0 0;
311 margin: 0 1em 0 0;
312 }
312 }
313
313
314 .pullrequestlist {
314 .pullrequestlist {
315 .closed {
315 .closed {
316 background-color: @grey6;
316 background-color: @grey6;
317 }
317 }
318 .td-status {
318 .td-status {
319 padding-left: .5em;
319 padding-left: .5em;
320 }
320 }
321 .log-container .truncate {
321 .log-container .truncate {
322 height: 2.75em;
322 height: 2.75em;
323 white-space: pre-line;
323 white-space: pre-line;
324 }
324 }
325 table.rctable .user {
325 table.rctable .user {
326 padding-left: 0;
326 padding-left: 0;
327 }
327 }
328 table.rctable {
328 table.rctable {
329 td.td-description,
329 td.td-description,
330 .rc-user {
330 .rc-user {
331 min-width: auto;
331 min-width: auto;
332 }
332 }
333 }
333 }
334 }
334 }
335
335
336 // Pull Requests
336 // Pull Requests
337
337
338 .pullrequests_section_head {
338 .pullrequests_section_head {
339 display: block;
339 display: block;
340 clear: both;
340 clear: both;
341 margin: @padding 0;
341 margin: @padding 0;
342 font-family: @text-bold;
342 font-family: @text-bold;
343 }
343 }
344
344
345 .pr-origininfo, .pr-targetinfo {
345 .pr-origininfo, .pr-targetinfo {
346 position: relative;
346 position: relative;
347
347
348 .tag {
348 .tag {
349 display: inline-block;
349 display: inline-block;
350 margin: 0 1em .5em 0;
350 margin: 0 1em .5em 0;
351 }
351 }
352
352
353 .clone-url {
353 .clone-url {
354 display: inline-block;
354 display: inline-block;
355 margin: 0 0 .5em 0;
355 margin: 0 0 .5em 0;
356 padding: 0;
356 padding: 0;
357 line-height: 1.2em;
357 line-height: 1.2em;
358 }
358 }
359 }
359 }
360
360
361 .pr-pullinfo {
361 .pr-pullinfo {
362 clear: both;
362 clear: both;
363 margin: .5em 0;
363 margin: .5em 0;
364 }
364 }
365
365
366 #pr-title-input {
366 #pr-title-input {
367 width: 72%;
367 width: 72%;
368 font-size: 1em;
368 font-size: 1em;
369 font-family: @text-bold;
369 font-family: @text-bold;
370 margin: 0;
370 margin: 0;
371 padding: 0 0 0 @padding/4;
371 padding: 0 0 0 @padding/4;
372 line-height: 1.7em;
372 line-height: 1.7em;
373 color: @text-color;
373 color: @text-color;
374 letter-spacing: .02em;
374 letter-spacing: .02em;
375 }
375 }
376
376
377 #pullrequest_title {
377 #pullrequest_title {
378 width: 100%;
378 width: 100%;
379 box-sizing: border-box;
379 box-sizing: border-box;
380 }
380 }
381
381
382 #pr_open_message {
382 #pr_open_message {
383 border: @border-thickness solid #fff;
383 border: @border-thickness solid #fff;
384 border-radius: @border-radius;
384 border-radius: @border-radius;
385 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
385 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
386 text-align: right;
386 text-align: right;
387 overflow: hidden;
387 overflow: hidden;
388 }
388 }
389
389
390 .pr-submit-button {
390 .pr-submit-button {
391 float: right;
391 float: right;
392 margin: 0 0 0 5px;
392 margin: 0 0 0 5px;
393 }
393 }
394
394
395 .pr-spacing-container {
395 .pr-spacing-container {
396 padding: 20px;
396 padding: 20px;
397 clear: both
397 clear: both
398 }
398 }
399
399
400 #pr-description-input {
400 #pr-description-input {
401 margin-bottom: 0;
401 margin-bottom: 0;
402 }
402 }
403
403
404 .pr-description-label {
404 .pr-description-label {
405 vertical-align: top;
405 vertical-align: top;
406 }
406 }
407
407
408 .perms_section_head {
408 .perms_section_head {
409 min-width: 625px;
409 min-width: 625px;
410
410
411 h2 {
411 h2 {
412 margin-bottom: 0;
412 margin-bottom: 0;
413 }
413 }
414
414
415 .label-checkbox {
415 .label-checkbox {
416 float: left;
416 float: left;
417 }
417 }
418
418
419 &.field {
419 &.field {
420 margin: @space 0 @padding;
420 margin: @space 0 @padding;
421 }
421 }
422
422
423 &:first-child.field {
423 &:first-child.field {
424 margin-top: 0;
424 margin-top: 0;
425
425
426 .label {
426 .label {
427 margin-top: 0;
427 margin-top: 0;
428 padding-top: 0;
428 padding-top: 0;
429 }
429 }
430
430
431 .radios {
431 .radios {
432 padding-top: 0;
432 padding-top: 0;
433 }
433 }
434 }
434 }
435
435
436 .radios {
436 .radios {
437 float: right;
437 float: right;
438 position: relative;
438 position: relative;
439 width: 405px;
439 width: 405px;
440 }
440 }
441 }
441 }
442
442
443 //--- MODULES ------------------//
443 //--- MODULES ------------------//
444
444
445
445
446 // Server Announcement
446 // Server Announcement
447 #server-announcement {
447 #server-announcement {
448 width: 95%;
448 width: 95%;
449 margin: @padding auto;
449 margin: @padding auto;
450 padding: @padding;
450 padding: @padding;
451 border-width: 2px;
451 border-width: 2px;
452 border-style: solid;
452 border-style: solid;
453 .border-radius(2px);
453 .border-radius(2px);
454 font-family: @text-bold;
454 font-family: @text-bold;
455
455
456 &.info { border-color: @alert4; background-color: @alert4-inner; }
456 &.info { border-color: @alert4; background-color: @alert4-inner; }
457 &.warning { border-color: @alert3; background-color: @alert3-inner; }
457 &.warning { border-color: @alert3; background-color: @alert3-inner; }
458 &.error { border-color: @alert2; background-color: @alert2-inner; }
458 &.error { border-color: @alert2; background-color: @alert2-inner; }
459 &.success { border-color: @alert1; background-color: @alert1-inner; }
459 &.success { border-color: @alert1; background-color: @alert1-inner; }
460 &.neutral { border-color: @grey3; background-color: @grey6; }
460 &.neutral { border-color: @grey3; background-color: @grey6; }
461 }
461 }
462
462
463 // Fixed Sidebar Column
463 // Fixed Sidebar Column
464 .sidebar-col-wrapper {
464 .sidebar-col-wrapper {
465 padding-left: @sidebar-all-width;
465 padding-left: @sidebar-all-width;
466
466
467 .sidebar {
467 .sidebar {
468 width: @sidebar-width;
468 width: @sidebar-width;
469 margin-left: -@sidebar-all-width;
469 margin-left: -@sidebar-all-width;
470 }
470 }
471 }
471 }
472
472
473 .sidebar-col-wrapper.scw-small {
473 .sidebar-col-wrapper.scw-small {
474 padding-left: @sidebar-small-all-width;
474 padding-left: @sidebar-small-all-width;
475
475
476 .sidebar {
476 .sidebar {
477 width: @sidebar-small-width;
477 width: @sidebar-small-width;
478 margin-left: -@sidebar-small-all-width;
478 margin-left: -@sidebar-small-all-width;
479 }
479 }
480 }
480 }
481
481
482
482
483 // FOOTER
483 // FOOTER
484 #footer {
484 #footer {
485 padding: 0;
485 padding: 0;
486 text-align: center;
486 text-align: center;
487 vertical-align: middle;
487 vertical-align: middle;
488 color: @grey2;
488 color: @grey2;
489 background-color: @grey6;
489 background-color: @grey6;
490
490
491 p {
491 p {
492 margin: 0;
492 margin: 0;
493 padding: 1em;
493 padding: 1em;
494 line-height: 1em;
494 line-height: 1em;
495 }
495 }
496
496
497 .server-instance { //server instance
497 .server-instance { //server instance
498 display: none;
498 display: none;
499 }
499 }
500
500
501 .title {
501 .title {
502 float: none;
502 float: none;
503 margin: 0 auto;
503 margin: 0 auto;
504 }
504 }
505 }
505 }
506
506
507 button.close {
507 button.close {
508 padding: 0;
508 padding: 0;
509 cursor: pointer;
509 cursor: pointer;
510 background: transparent;
510 background: transparent;
511 border: 0;
511 border: 0;
512 .box-shadow(none);
512 .box-shadow(none);
513 -webkit-appearance: none;
513 -webkit-appearance: none;
514 }
514 }
515
515
516 .close {
516 .close {
517 float: right;
517 float: right;
518 font-size: 21px;
518 font-size: 21px;
519 font-family: @text-bootstrap;
519 font-family: @text-bootstrap;
520 line-height: 1em;
520 line-height: 1em;
521 font-weight: bold;
521 font-weight: bold;
522 color: @grey2;
522 color: @grey2;
523
523
524 &:hover,
524 &:hover,
525 &:focus {
525 &:focus {
526 color: @grey1;
526 color: @grey1;
527 text-decoration: none;
527 text-decoration: none;
528 cursor: pointer;
528 cursor: pointer;
529 }
529 }
530 }
530 }
531
531
532 // GRID
532 // GRID
533 .sorting,
533 .sorting,
534 .sorting_desc,
534 .sorting_desc,
535 .sorting_asc {
535 .sorting_asc {
536 cursor: pointer;
536 cursor: pointer;
537 }
537 }
538 .sorting_desc:after {
538 .sorting_desc:after {
539 content: "\00A0\25B2";
539 content: "\00A0\25B2";
540 font-size: .75em;
540 font-size: .75em;
541 }
541 }
542 .sorting_asc:after {
542 .sorting_asc:after {
543 content: "\00A0\25BC";
543 content: "\00A0\25BC";
544 font-size: .68em;
544 font-size: .68em;
545 }
545 }
546
546
547
547
548 .user_auth_tokens {
548 .user_auth_tokens {
549
549
550 &.truncate {
550 &.truncate {
551 white-space: nowrap;
551 white-space: nowrap;
552 overflow: hidden;
552 overflow: hidden;
553 text-overflow: ellipsis;
553 text-overflow: ellipsis;
554 }
554 }
555
555
556 .fields .field .input {
556 .fields .field .input {
557 margin: 0;
557 margin: 0;
558 }
558 }
559
559
560 input#description {
560 input#description {
561 width: 100px;
561 width: 100px;
562 margin: 0;
562 margin: 0;
563 }
563 }
564
564
565 .drop-menu {
565 .drop-menu {
566 // TODO: johbo: Remove this, should work out of the box when
566 // TODO: johbo: Remove this, should work out of the box when
567 // having multiple inputs inline
567 // having multiple inputs inline
568 margin: 0 0 0 5px;
568 margin: 0 0 0 5px;
569 }
569 }
570 }
570 }
571 #user_list_table {
571 #user_list_table {
572 .closed {
572 .closed {
573 background-color: @grey6;
573 background-color: @grey6;
574 }
574 }
575 }
575 }
576
576
577
577
578 input {
578 input {
579 &.disabled {
579 &.disabled {
580 opacity: .5;
580 opacity: .5;
581 }
581 }
582 }
582 }
583
583
584 // remove extra padding in firefox
584 // remove extra padding in firefox
585 input::-moz-focus-inner { border:0; padding:0 }
585 input::-moz-focus-inner { border:0; padding:0 }
586
586
587 .adjacent input {
587 .adjacent input {
588 margin-bottom: @padding;
588 margin-bottom: @padding;
589 }
589 }
590
590
591 .permissions_boxes {
591 .permissions_boxes {
592 display: block;
592 display: block;
593 }
593 }
594
594
595 //TODO: lisa: this should be in tables
595 //TODO: lisa: this should be in tables
596 .show_more_col {
596 .show_more_col {
597 width: 20px;
597 width: 20px;
598 }
598 }
599
599
600 //FORMS
600 //FORMS
601
601
602 .medium-inline,
602 .medium-inline,
603 input#description.medium-inline {
603 input#description.medium-inline {
604 display: inline;
604 display: inline;
605 width: @medium-inline-input-width;
605 width: @medium-inline-input-width;
606 min-width: 100px;
606 min-width: 100px;
607 }
607 }
608
608
609 select {
609 select {
610 //reset
610 //reset
611 -webkit-appearance: none;
611 -webkit-appearance: none;
612 -moz-appearance: none;
612 -moz-appearance: none;
613
613
614 display: inline-block;
614 display: inline-block;
615 height: 28px;
615 height: 28px;
616 width: auto;
616 width: auto;
617 margin: 0 @padding @padding 0;
617 margin: 0 @padding @padding 0;
618 padding: 0 18px 0 8px;
618 padding: 0 18px 0 8px;
619 line-height:1em;
619 line-height:1em;
620 font-size: @basefontsize;
620 font-size: @basefontsize;
621 border: @border-thickness solid @rcblue;
621 border: @border-thickness solid @rcblue;
622 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
622 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
623 color: @rcblue;
623 color: @rcblue;
624
624
625 &:after {
625 &:after {
626 content: "\00A0\25BE";
626 content: "\00A0\25BE";
627 }
627 }
628
628
629 &:focus {
629 &:focus {
630 outline: none;
630 outline: none;
631 }
631 }
632 }
632 }
633
633
634 option {
634 option {
635 &:focus {
635 &:focus {
636 outline: none;
636 outline: none;
637 }
637 }
638 }
638 }
639
639
640 input,
640 input,
641 textarea {
641 textarea {
642 padding: @input-padding;
642 padding: @input-padding;
643 border: @input-border-thickness solid @border-highlight-color;
643 border: @input-border-thickness solid @border-highlight-color;
644 .border-radius (@border-radius);
644 .border-radius (@border-radius);
645 font-family: @text-light;
645 font-family: @text-light;
646 font-size: @basefontsize;
646 font-size: @basefontsize;
647
647
648 &.input-sm {
648 &.input-sm {
649 padding: 5px;
649 padding: 5px;
650 }
650 }
651
651
652 &#description {
652 &#description {
653 min-width: @input-description-minwidth;
653 min-width: @input-description-minwidth;
654 min-height: 1em;
654 min-height: 1em;
655 padding: 10px;
655 padding: 10px;
656 }
656 }
657 }
657 }
658
658
659 .field-sm {
659 .field-sm {
660 input,
660 input,
661 textarea {
661 textarea {
662 padding: 5px;
662 padding: 5px;
663 }
663 }
664 }
664 }
665
665
666 textarea {
666 textarea {
667 display: block;
667 display: block;
668 clear: both;
668 clear: both;
669 width: 100%;
669 width: 100%;
670 min-height: 100px;
670 min-height: 100px;
671 margin-bottom: @padding;
671 margin-bottom: @padding;
672 .box-sizing(border-box);
672 .box-sizing(border-box);
673 overflow: auto;
673 overflow: auto;
674 }
674 }
675
675
676 label {
676 label {
677 font-family: @text-light;
677 font-family: @text-light;
678 }
678 }
679
679
680 // GRAVATARS
680 // GRAVATARS
681 // centers gravatar on username to the right
681 // centers gravatar on username to the right
682
682
683 .gravatar {
683 .gravatar {
684 display: inline;
684 display: inline;
685 min-width: 16px;
685 min-width: 16px;
686 min-height: 16px;
686 min-height: 16px;
687 margin: -5px 0;
687 margin: -5px 0;
688 padding: 0;
688 padding: 0;
689 line-height: 1em;
689 line-height: 1em;
690 border: 1px solid @grey4;
690 border: 1px solid @grey4;
691 box-sizing: content-box;
691
692
692 &.gravatar-large {
693 &.gravatar-large {
693 margin: -0.5em .25em -0.5em 0;
694 margin: -0.5em .25em -0.5em 0;
694 }
695 }
695
696
696 & + .user {
697 & + .user {
697 display: inline;
698 display: inline;
698 margin: 0;
699 margin: 0;
699 padding: 0 0 0 .17em;
700 padding: 0 0 0 .17em;
700 line-height: 1em;
701 line-height: 1em;
701 }
702 }
702 }
703 }
703
704
704 .user-inline-data {
705 .user-inline-data {
705 display: inline-block;
706 display: inline-block;
706 float: left;
707 float: left;
707 padding-left: .5em;
708 padding-left: .5em;
708 line-height: 1.3em;
709 line-height: 1.3em;
709 }
710 }
710
711
711 .rc-user { // gravatar + user wrapper
712 .rc-user { // gravatar + user wrapper
712 float: left;
713 float: left;
713 position: relative;
714 position: relative;
714 min-width: 100px;
715 min-width: 100px;
715 max-width: 200px;
716 max-width: 200px;
716 min-height: (@gravatar-size + @border-thickness * 2); // account for border
717 min-height: (@gravatar-size + @border-thickness * 2); // account for border
717 display: block;
718 display: block;
718 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
719 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
719
720
720
721
721 .gravatar {
722 .gravatar {
722 display: block;
723 display: block;
723 position: absolute;
724 position: absolute;
724 top: 0;
725 top: 0;
725 left: 0;
726 left: 0;
726 min-width: @gravatar-size;
727 min-width: @gravatar-size;
727 min-height: @gravatar-size;
728 min-height: @gravatar-size;
728 margin: 0;
729 margin: 0;
729 }
730 }
730
731
731 .user {
732 .user {
732 display: block;
733 display: block;
733 max-width: 175px;
734 max-width: 175px;
734 padding-top: 2px;
735 padding-top: 2px;
735 overflow: hidden;
736 overflow: hidden;
736 text-overflow: ellipsis;
737 text-overflow: ellipsis;
737 }
738 }
738 }
739 }
739
740
740 .gist-gravatar,
741 .gist-gravatar,
741 .journal_container {
742 .journal_container {
742 .gravatar-large {
743 .gravatar-large {
743 margin: 0 .5em -10px 0;
744 margin: 0 .5em -10px 0;
744 }
745 }
745 }
746 }
746
747
747
748
748 // ADMIN SETTINGS
749 // ADMIN SETTINGS
749
750
750 // Tag Patterns
751 // Tag Patterns
751 .tag_patterns {
752 .tag_patterns {
752 .tag_input {
753 .tag_input {
753 margin-bottom: @padding;
754 margin-bottom: @padding;
754 }
755 }
755 }
756 }
756
757
757 .locked_input {
758 .locked_input {
758 position: relative;
759 position: relative;
759
760
760 input {
761 input {
761 display: inline;
762 display: inline;
762 margin-top: 3px;
763 margin-top: 3px;
763 }
764 }
764
765
765 br {
766 br {
766 display: none;
767 display: none;
767 }
768 }
768
769
769 .error-message {
770 .error-message {
770 float: left;
771 float: left;
771 width: 100%;
772 width: 100%;
772 }
773 }
773
774
774 .lock_input_button {
775 .lock_input_button {
775 display: inline;
776 display: inline;
776 }
777 }
777
778
778 .help-block {
779 .help-block {
779 clear: both;
780 clear: both;
780 }
781 }
781 }
782 }
782
783
783 // Notifications
784 // Notifications
784
785
785 .notifications_buttons {
786 .notifications_buttons {
786 margin: 0 0 @space 0;
787 margin: 0 0 @space 0;
787 padding: 0;
788 padding: 0;
788
789
789 .btn {
790 .btn {
790 display: inline-block;
791 display: inline-block;
791 }
792 }
792 }
793 }
793
794
794 .notification-list {
795 .notification-list {
795
796
796 div {
797 div {
797 display: inline-block;
798 display: inline-block;
798 vertical-align: middle;
799 vertical-align: middle;
799 }
800 }
800
801
801 .container {
802 .container {
802 display: block;
803 display: block;
803 margin: 0 0 @padding 0;
804 margin: 0 0 @padding 0;
804 }
805 }
805
806
806 .delete-notifications {
807 .delete-notifications {
807 margin-left: @padding;
808 margin-left: @padding;
808 text-align: right;
809 text-align: right;
809 cursor: pointer;
810 cursor: pointer;
810 }
811 }
811
812
812 .read-notifications {
813 .read-notifications {
813 margin-left: @padding/2;
814 margin-left: @padding/2;
814 text-align: right;
815 text-align: right;
815 width: 35px;
816 width: 35px;
816 cursor: pointer;
817 cursor: pointer;
817 }
818 }
818
819
819 .icon-minus-sign {
820 .icon-minus-sign {
820 color: @alert2;
821 color: @alert2;
821 }
822 }
822
823
823 .icon-ok-sign {
824 .icon-ok-sign {
824 color: @alert1;
825 color: @alert1;
825 }
826 }
826 }
827 }
827
828
828 .user_settings {
829 .user_settings {
829 float: left;
830 float: left;
830 clear: both;
831 clear: both;
831 display: block;
832 display: block;
832 width: 100%;
833 width: 100%;
833
834
834 .gravatar_box {
835 .gravatar_box {
835 margin-bottom: @padding;
836 margin-bottom: @padding;
836
837
837 &:after {
838 &:after {
838 content: " ";
839 content: " ";
839 clear: both;
840 clear: both;
840 width: 100%;
841 width: 100%;
841 }
842 }
842 }
843 }
843
844
844 .fields .field {
845 .fields .field {
845 clear: both;
846 clear: both;
846 }
847 }
847 }
848 }
848
849
849 .advanced_settings {
850 .advanced_settings {
850 margin-bottom: @space;
851 margin-bottom: @space;
851
852
852 .help-block {
853 .help-block {
853 margin-left: 0;
854 margin-left: 0;
854 }
855 }
855
856
856 button + .help-block {
857 button + .help-block {
857 margin-top: @padding;
858 margin-top: @padding;
858 }
859 }
859 }
860 }
860
861
861 // admin settings radio buttons and labels
862 // admin settings radio buttons and labels
862 .label-2 {
863 .label-2 {
863 float: left;
864 float: left;
864 width: @label2-width;
865 width: @label2-width;
865
866
866 label {
867 label {
867 color: @grey1;
868 color: @grey1;
868 }
869 }
869 }
870 }
870 .checkboxes {
871 .checkboxes {
871 float: left;
872 float: left;
872 width: @checkboxes-width;
873 width: @checkboxes-width;
873 margin-bottom: @padding;
874 margin-bottom: @padding;
874
875
875 .checkbox {
876 .checkbox {
876 width: 100%;
877 width: 100%;
877
878
878 label {
879 label {
879 margin: 0;
880 margin: 0;
880 padding: 0;
881 padding: 0;
881 }
882 }
882 }
883 }
883
884
884 .checkbox + .checkbox {
885 .checkbox + .checkbox {
885 display: inline-block;
886 display: inline-block;
886 }
887 }
887
888
888 label {
889 label {
889 margin-right: 1em;
890 margin-right: 1em;
890 }
891 }
891 }
892 }
892
893
893 // CHANGELOG
894 // CHANGELOG
894 .container_header {
895 .container_header {
895 float: left;
896 float: left;
896 display: block;
897 display: block;
897 width: 100%;
898 width: 100%;
898 margin: @padding 0 @padding;
899 margin: @padding 0 @padding;
899
900
900 #filter_changelog {
901 #filter_changelog {
901 float: left;
902 float: left;
902 margin-right: @padding;
903 margin-right: @padding;
903 }
904 }
904
905
905 .breadcrumbs_light {
906 .breadcrumbs_light {
906 display: inline-block;
907 display: inline-block;
907 }
908 }
908 }
909 }
909
910
910 .info_box {
911 .info_box {
911 float: right;
912 float: right;
912 }
913 }
913
914
914
915
915 #graph_nodes {
916 #graph_nodes {
916 padding-top: 43px;
917 padding-top: 43px;
917 }
918 }
918
919
919 #graph_content{
920 #graph_content{
920
921
921 // adjust for table headers so that graph renders properly
922 // adjust for table headers so that graph renders properly
922 // #graph_nodes padding - table cell padding
923 // #graph_nodes padding - table cell padding
923 padding-top: (@space - (@basefontsize * 2.4));
924 padding-top: (@space - (@basefontsize * 2.4));
924
925
925 &.graph_full_width {
926 &.graph_full_width {
926 width: 100%;
927 width: 100%;
927 max-width: 100%;
928 max-width: 100%;
928 }
929 }
929 }
930 }
930
931
931 #graph {
932 #graph {
932 .flag_status {
933 .flag_status {
933 margin: 0;
934 margin: 0;
934 }
935 }
935
936
936 .pagination-left {
937 .pagination-left {
937 float: left;
938 float: left;
938 clear: both;
939 clear: both;
939 }
940 }
940
941
941 .log-container {
942 .log-container {
942 max-width: 345px;
943 max-width: 345px;
943
944
944 .message{
945 .message{
945 max-width: 340px;
946 max-width: 340px;
946 }
947 }
947 }
948 }
948
949
949 .graph-col-wrapper {
950 .graph-col-wrapper {
950 padding-left: 110px;
951 padding-left: 110px;
951
952
952 #graph_nodes {
953 #graph_nodes {
953 width: 100px;
954 width: 100px;
954 margin-left: -110px;
955 margin-left: -110px;
955 float: left;
956 float: left;
956 clear: left;
957 clear: left;
957 }
958 }
958 }
959 }
959 }
960 }
960
961
961 #filter_changelog {
962 #filter_changelog {
962 float: left;
963 float: left;
963 }
964 }
964
965
965
966
966 //--- THEME ------------------//
967 //--- THEME ------------------//
967
968
968 #logo {
969 #logo {
969 float: left;
970 float: left;
970 margin: 9px 0 0 0;
971 margin: 9px 0 0 0;
971
972
972 .header {
973 .header {
973 background-color: transparent;
974 background-color: transparent;
974 }
975 }
975
976
976 a {
977 a {
977 display: inline-block;
978 display: inline-block;
978 }
979 }
979
980
980 img {
981 img {
981 height:30px;
982 height:30px;
982 }
983 }
983 }
984 }
984
985
985 .logo-wrapper {
986 .logo-wrapper {
986 float:left;
987 float:left;
987 }
988 }
988
989
989 .branding{
990 .branding{
990 float: left;
991 float: left;
991 padding: 9px 2px;
992 padding: 9px 2px;
992 line-height: 1em;
993 line-height: 1em;
993 font-size: @navigation-fontsize;
994 font-size: @navigation-fontsize;
994 }
995 }
995
996
996 img {
997 img {
997 border: none;
998 border: none;
998 outline: none;
999 outline: none;
999 }
1000 }
1000 user-profile-header
1001 user-profile-header
1001 label {
1002 label {
1002
1003
1003 input[type="checkbox"] {
1004 input[type="checkbox"] {
1004 margin-right: 1em;
1005 margin-right: 1em;
1005 }
1006 }
1006 input[type="radio"] {
1007 input[type="radio"] {
1007 margin-right: 1em;
1008 margin-right: 1em;
1008 }
1009 }
1009 }
1010 }
1010
1011
1011 .flag_status {
1012 .flag_status {
1012 margin: 2px 8px 6px 2px;
1013 margin: 2px 8px 6px 2px;
1013 &.under_review {
1014 &.under_review {
1014 .circle(5px, @alert3);
1015 .circle(5px, @alert3);
1015 }
1016 }
1016 &.approved {
1017 &.approved {
1017 .circle(5px, @alert1);
1018 .circle(5px, @alert1);
1018 }
1019 }
1019 &.rejected,
1020 &.rejected,
1020 &.forced_closed{
1021 &.forced_closed{
1021 .circle(5px, @alert2);
1022 .circle(5px, @alert2);
1022 }
1023 }
1023 &.not_reviewed {
1024 &.not_reviewed {
1024 .circle(5px, @grey5);
1025 .circle(5px, @grey5);
1025 }
1026 }
1026 }
1027 }
1027
1028
1028 .flag_status_comment_box {
1029 .flag_status_comment_box {
1029 margin: 5px 6px 0px 2px;
1030 margin: 5px 6px 0px 2px;
1030 }
1031 }
1031 .test_pattern_preview {
1032 .test_pattern_preview {
1032 margin: @space 0;
1033 margin: @space 0;
1033
1034
1034 p {
1035 p {
1035 margin-bottom: 0;
1036 margin-bottom: 0;
1036 border-bottom: @border-thickness solid @border-default-color;
1037 border-bottom: @border-thickness solid @border-default-color;
1037 color: @grey3;
1038 color: @grey3;
1038 }
1039 }
1039
1040
1040 .btn {
1041 .btn {
1041 margin-bottom: @padding;
1042 margin-bottom: @padding;
1042 }
1043 }
1043 }
1044 }
1044 #test_pattern_result {
1045 #test_pattern_result {
1045 display: none;
1046 display: none;
1046 &:extend(pre);
1047 &:extend(pre);
1047 padding: .9em;
1048 padding: .9em;
1048 color: @grey3;
1049 color: @grey3;
1049 background-color: @grey7;
1050 background-color: @grey7;
1050 border-right: @border-thickness solid @border-default-color;
1051 border-right: @border-thickness solid @border-default-color;
1051 border-bottom: @border-thickness solid @border-default-color;
1052 border-bottom: @border-thickness solid @border-default-color;
1052 border-left: @border-thickness solid @border-default-color;
1053 border-left: @border-thickness solid @border-default-color;
1053 }
1054 }
1054
1055
1055 #repo_vcs_settings {
1056 #repo_vcs_settings {
1056 #inherit_overlay_vcs_default {
1057 #inherit_overlay_vcs_default {
1057 display: none;
1058 display: none;
1058 }
1059 }
1059 #inherit_overlay_vcs_custom {
1060 #inherit_overlay_vcs_custom {
1060 display: custom;
1061 display: custom;
1061 }
1062 }
1062 &.inherited {
1063 &.inherited {
1063 #inherit_overlay_vcs_default {
1064 #inherit_overlay_vcs_default {
1064 display: block;
1065 display: block;
1065 }
1066 }
1066 #inherit_overlay_vcs_custom {
1067 #inherit_overlay_vcs_custom {
1067 display: none;
1068 display: none;
1068 }
1069 }
1069 }
1070 }
1070 }
1071 }
1071
1072
1072 .issue-tracker-link {
1073 .issue-tracker-link {
1073 color: @rcblue;
1074 color: @rcblue;
1074 }
1075 }
1075
1076
1076 // Issue Tracker Table Show/Hide
1077 // Issue Tracker Table Show/Hide
1077 #repo_issue_tracker {
1078 #repo_issue_tracker {
1078 #inherit_overlay {
1079 #inherit_overlay {
1079 display: none;
1080 display: none;
1080 }
1081 }
1081 #custom_overlay {
1082 #custom_overlay {
1082 display: custom;
1083 display: custom;
1083 }
1084 }
1084 &.inherited {
1085 &.inherited {
1085 #inherit_overlay {
1086 #inherit_overlay {
1086 display: block;
1087 display: block;
1087 }
1088 }
1088 #custom_overlay {
1089 #custom_overlay {
1089 display: none;
1090 display: none;
1090 }
1091 }
1091 }
1092 }
1092 }
1093 }
1093 table.issuetracker {
1094 table.issuetracker {
1094 &.readonly {
1095 &.readonly {
1095 tr, td {
1096 tr, td {
1096 color: @grey3;
1097 color: @grey3;
1097 }
1098 }
1098 }
1099 }
1099 .edit {
1100 .edit {
1100 display: none;
1101 display: none;
1101 }
1102 }
1102 .editopen {
1103 .editopen {
1103 .edit {
1104 .edit {
1104 display: inline;
1105 display: inline;
1105 }
1106 }
1106 .entry {
1107 .entry {
1107 display: none;
1108 display: none;
1108 }
1109 }
1109 }
1110 }
1110 tr td.td-action {
1111 tr td.td-action {
1111 min-width: 117px;
1112 min-width: 117px;
1112 }
1113 }
1113 td input {
1114 td input {
1114 max-width: none;
1115 max-width: none;
1115 min-width: 30px;
1116 min-width: 30px;
1116 width: 80%;
1117 width: 80%;
1117 }
1118 }
1118 .issuetracker_pref input {
1119 .issuetracker_pref input {
1119 width: 40%;
1120 width: 40%;
1120 }
1121 }
1121 input.edit_issuetracker_update {
1122 input.edit_issuetracker_update {
1122 margin-right: 0;
1123 margin-right: 0;
1123 width: auto;
1124 width: auto;
1124 }
1125 }
1125 }
1126 }
1126
1127
1127 table.integrations {
1128 table.integrations {
1128 .td-icon {
1129 .td-icon {
1129 width: 20px;
1130 width: 20px;
1130 .integration-icon {
1131 .integration-icon {
1131 height: 20px;
1132 height: 20px;
1132 width: 20px;
1133 width: 20px;
1133 }
1134 }
1134 }
1135 }
1135 }
1136 }
1136
1137
1137 .integrations {
1138 .integrations {
1138 a.integration-box {
1139 a.integration-box {
1139 color: @text-color;
1140 color: @text-color;
1140 &:hover {
1141 &:hover {
1141 .panel {
1142 .panel {
1142 background: #fbfbfb;
1143 background: #fbfbfb;
1143 }
1144 }
1144 }
1145 }
1145 .integration-icon {
1146 .integration-icon {
1146 width: 30px;
1147 width: 30px;
1147 height: 30px;
1148 height: 30px;
1148 margin-right: 20px;
1149 margin-right: 20px;
1149 float: left;
1150 float: left;
1150 }
1151 }
1151
1152
1152 .panel-body {
1153 .panel-body {
1153 padding: 10px;
1154 padding: 10px;
1154 }
1155 }
1155 .panel {
1156 .panel {
1156 margin-bottom: 10px;
1157 margin-bottom: 10px;
1157 }
1158 }
1158 h2 {
1159 h2 {
1159 display: inline-block;
1160 display: inline-block;
1160 margin: 0;
1161 margin: 0;
1161 min-width: 140px;
1162 min-width: 140px;
1162 }
1163 }
1163 }
1164 }
1164 }
1165 }
1165
1166
1166 //Permissions Settings
1167 //Permissions Settings
1167 #add_perm {
1168 #add_perm {
1168 margin: 0 0 @padding;
1169 margin: 0 0 @padding;
1169 cursor: pointer;
1170 cursor: pointer;
1170 }
1171 }
1171
1172
1172 .perm_ac {
1173 .perm_ac {
1173 input {
1174 input {
1174 width: 95%;
1175 width: 95%;
1175 }
1176 }
1176 }
1177 }
1177
1178
1178 .autocomplete-suggestions {
1179 .autocomplete-suggestions {
1179 width: auto !important; // overrides autocomplete.js
1180 width: auto !important; // overrides autocomplete.js
1180 margin: 0;
1181 margin: 0;
1181 border: @border-thickness solid @rcblue;
1182 border: @border-thickness solid @rcblue;
1182 border-radius: @border-radius;
1183 border-radius: @border-radius;
1183 color: @rcblue;
1184 color: @rcblue;
1184 background-color: white;
1185 background-color: white;
1185 }
1186 }
1186 .autocomplete-selected {
1187 .autocomplete-selected {
1187 background: #F0F0F0;
1188 background: #F0F0F0;
1188 }
1189 }
1189 .ac-container-wrap {
1190 .ac-container-wrap {
1190 margin: 0;
1191 margin: 0;
1191 padding: 8px;
1192 padding: 8px;
1192 border-bottom: @border-thickness solid @rclightblue;
1193 border-bottom: @border-thickness solid @rclightblue;
1193 list-style-type: none;
1194 list-style-type: none;
1194 cursor: pointer;
1195 cursor: pointer;
1195
1196
1196 &:hover {
1197 &:hover {
1197 background-color: @rclightblue;
1198 background-color: @rclightblue;
1198 }
1199 }
1199
1200
1200 img {
1201 img {
1201 height: @gravatar-size;
1202 height: @gravatar-size;
1202 width: @gravatar-size;
1203 width: @gravatar-size;
1203 margin-right: 1em;
1204 margin-right: 1em;
1204 }
1205 }
1205
1206
1206 strong {
1207 strong {
1207 font-weight: normal;
1208 font-weight: normal;
1208 }
1209 }
1209 }
1210 }
1210
1211
1211 // Settings Dropdown
1212 // Settings Dropdown
1212 .user-menu .container {
1213 .user-menu .container {
1213 padding: 0 4px;
1214 padding: 0 4px;
1214 margin: 0;
1215 margin: 0;
1215 }
1216 }
1216
1217
1217 .user-menu .gravatar {
1218 .user-menu .gravatar {
1218 cursor: pointer;
1219 cursor: pointer;
1219 }
1220 }
1220
1221
1221 .codeblock {
1222 .codeblock {
1222 margin-bottom: @padding;
1223 margin-bottom: @padding;
1223 clear: both;
1224 clear: both;
1224
1225
1225 .stats{
1226 .stats{
1226 overflow: hidden;
1227 overflow: hidden;
1227 }
1228 }
1228
1229
1229 .message{
1230 .message{
1230 textarea{
1231 textarea{
1231 margin: 0;
1232 margin: 0;
1232 }
1233 }
1233 }
1234 }
1234
1235
1235 .code-header {
1236 .code-header {
1236 .stats {
1237 .stats {
1237 line-height: 2em;
1238 line-height: 2em;
1238
1239
1239 .revision_id {
1240 .revision_id {
1240 margin-left: 0;
1241 margin-left: 0;
1241 }
1242 }
1242 .buttons {
1243 .buttons {
1243 padding-right: 0;
1244 padding-right: 0;
1244 }
1245 }
1245 }
1246 }
1246
1247
1247 .item{
1248 .item{
1248 margin-right: 0.5em;
1249 margin-right: 0.5em;
1249 }
1250 }
1250 }
1251 }
1251
1252
1252 #editor_container{
1253 #editor_container{
1253 position: relative;
1254 position: relative;
1254 margin: @padding;
1255 margin: @padding;
1255 }
1256 }
1256 }
1257 }
1257
1258
1258 #file_history_container {
1259 #file_history_container {
1259 display: none;
1260 display: none;
1260 }
1261 }
1261
1262
1262 .file-history-inner {
1263 .file-history-inner {
1263 margin-bottom: 10px;
1264 margin-bottom: 10px;
1264 }
1265 }
1265
1266
1266 // Pull Requests
1267 // Pull Requests
1267 .summary-details {
1268 .summary-details {
1268 width: 72%;
1269 width: 72%;
1269 }
1270 }
1270 .pr-summary {
1271 .pr-summary {
1271 border-bottom: @border-thickness solid @grey5;
1272 border-bottom: @border-thickness solid @grey5;
1272 margin-bottom: @space;
1273 margin-bottom: @space;
1273 }
1274 }
1274 .reviewers-title {
1275 .reviewers-title {
1275 width: 25%;
1276 width: 25%;
1276 min-width: 200px;
1277 min-width: 200px;
1277 }
1278 }
1278 .reviewers {
1279 .reviewers {
1279 width: 25%;
1280 width: 25%;
1280 min-width: 200px;
1281 min-width: 200px;
1281 }
1282 }
1282 .reviewers ul li {
1283 .reviewers ul li {
1283 position: relative;
1284 position: relative;
1284 width: 100%;
1285 width: 100%;
1285 margin-bottom: 8px;
1286 margin-bottom: 8px;
1286 }
1287 }
1287 .reviewers_member {
1288 .reviewers_member {
1288 width: 100%;
1289 width: 100%;
1289 overflow: auto;
1290 overflow: auto;
1290 }
1291 }
1291 .reviewer_reason {
1292 .reviewer_reason {
1292 padding-left: 20px;
1293 padding-left: 20px;
1293 }
1294 }
1294 .reviewer_status {
1295 .reviewer_status {
1295 display: inline-block;
1296 display: inline-block;
1296 vertical-align: top;
1297 vertical-align: top;
1297 width: 7%;
1298 width: 7%;
1298 min-width: 20px;
1299 min-width: 20px;
1299 height: 1.2em;
1300 height: 1.2em;
1300 margin-top: 3px;
1301 margin-top: 3px;
1301 line-height: 1em;
1302 line-height: 1em;
1302 }
1303 }
1303
1304
1304 .reviewer_name {
1305 .reviewer_name {
1305 display: inline-block;
1306 display: inline-block;
1306 max-width: 83%;
1307 max-width: 83%;
1307 padding-right: 20px;
1308 padding-right: 20px;
1308 vertical-align: middle;
1309 vertical-align: middle;
1309 line-height: 1;
1310 line-height: 1;
1310
1311
1311 .rc-user {
1312 .rc-user {
1312 min-width: 0;
1313 min-width: 0;
1313 margin: -2px 1em 0 0;
1314 margin: -2px 1em 0 0;
1314 }
1315 }
1315
1316
1316 .reviewer {
1317 .reviewer {
1317 float: left;
1318 float: left;
1318 }
1319 }
1319
1320
1320 &.to-delete {
1321 &.to-delete {
1321 .user,
1322 .user,
1322 .reviewer {
1323 .reviewer {
1323 text-decoration: line-through;
1324 text-decoration: line-through;
1324 }
1325 }
1325 }
1326 }
1326 }
1327 }
1327
1328
1328 .reviewer_member_remove {
1329 .reviewer_member_remove {
1329 position: absolute;
1330 position: absolute;
1330 right: 0;
1331 right: 0;
1331 top: 0;
1332 top: 0;
1332 width: 16px;
1333 width: 16px;
1333 margin-bottom: 10px;
1334 margin-bottom: 10px;
1334 padding: 0;
1335 padding: 0;
1335 color: black;
1336 color: black;
1336 }
1337 }
1337 .reviewer_member_status {
1338 .reviewer_member_status {
1338 margin-top: 5px;
1339 margin-top: 5px;
1339 }
1340 }
1340 .pr-summary #summary{
1341 .pr-summary #summary{
1341 width: 100%;
1342 width: 100%;
1342 }
1343 }
1343 .pr-summary .action_button:hover {
1344 .pr-summary .action_button:hover {
1344 border: 0;
1345 border: 0;
1345 cursor: pointer;
1346 cursor: pointer;
1346 }
1347 }
1347 .pr-details-title {
1348 .pr-details-title {
1348 padding-bottom: 8px;
1349 padding-bottom: 8px;
1349 border-bottom: @border-thickness solid @grey5;
1350 border-bottom: @border-thickness solid @grey5;
1350
1351
1351 .action_button.disabled {
1352 .action_button.disabled {
1352 color: @grey4;
1353 color: @grey4;
1353 cursor: inherit;
1354 cursor: inherit;
1354 }
1355 }
1355 .action_button {
1356 .action_button {
1356 color: @rcblue;
1357 color: @rcblue;
1357 }
1358 }
1358 }
1359 }
1359 .pr-details-content {
1360 .pr-details-content {
1360 margin-top: @textmargin;
1361 margin-top: @textmargin;
1361 margin-bottom: @textmargin;
1362 margin-bottom: @textmargin;
1362 }
1363 }
1363 .pr-description {
1364 .pr-description {
1364 white-space:pre-wrap;
1365 white-space:pre-wrap;
1365 }
1366 }
1366 .group_members {
1367 .group_members {
1367 margin-top: 0;
1368 margin-top: 0;
1368 padding: 0;
1369 padding: 0;
1369 list-style: outside none none;
1370 list-style: outside none none;
1370
1371
1371 img {
1372 img {
1372 height: @gravatar-size;
1373 height: @gravatar-size;
1373 width: @gravatar-size;
1374 width: @gravatar-size;
1374 margin-right: .5em;
1375 margin-right: .5em;
1375 margin-left: 3px;
1376 margin-left: 3px;
1376 }
1377 }
1377
1378
1378 .to-delete {
1379 .to-delete {
1379 .user {
1380 .user {
1380 text-decoration: line-through;
1381 text-decoration: line-through;
1381 }
1382 }
1382 }
1383 }
1383 }
1384 }
1384
1385
1385 .compare_view_commits_title {
1386 .compare_view_commits_title {
1386 .disabled {
1387 .disabled {
1387 cursor: inherit;
1388 cursor: inherit;
1388 &:hover{
1389 &:hover{
1389 background-color: inherit;
1390 background-color: inherit;
1390 color: inherit;
1391 color: inherit;
1391 }
1392 }
1392 }
1393 }
1393 }
1394 }
1394
1395
1395 // new entry in group_members
1396 // new entry in group_members
1396 .td-author-new-entry {
1397 .td-author-new-entry {
1397 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1398 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1398 }
1399 }
1399
1400
1400 .usergroup_member_remove {
1401 .usergroup_member_remove {
1401 width: 16px;
1402 width: 16px;
1402 margin-bottom: 10px;
1403 margin-bottom: 10px;
1403 padding: 0;
1404 padding: 0;
1404 color: black !important;
1405 color: black !important;
1405 cursor: pointer;
1406 cursor: pointer;
1406 }
1407 }
1407
1408
1408 .reviewer_ac .ac-input {
1409 .reviewer_ac .ac-input {
1409 width: 92%;
1410 width: 92%;
1410 margin-bottom: 1em;
1411 margin-bottom: 1em;
1411 }
1412 }
1412
1413
1413 .compare_view_commits tr{
1414 .compare_view_commits tr{
1414 height: 20px;
1415 height: 20px;
1415 }
1416 }
1416 .compare_view_commits td {
1417 .compare_view_commits td {
1417 vertical-align: top;
1418 vertical-align: top;
1418 padding-top: 10px;
1419 padding-top: 10px;
1419 }
1420 }
1420 .compare_view_commits .author {
1421 .compare_view_commits .author {
1421 margin-left: 5px;
1422 margin-left: 5px;
1422 }
1423 }
1423
1424
1424 .compare_view_files {
1425 .compare_view_files {
1425 width: 100%;
1426 width: 100%;
1426
1427
1427 td {
1428 td {
1428 vertical-align: middle;
1429 vertical-align: middle;
1429 }
1430 }
1430 }
1431 }
1431
1432
1432 .compare_view_filepath {
1433 .compare_view_filepath {
1433 color: @grey1;
1434 color: @grey1;
1434 }
1435 }
1435
1436
1436 .show_more {
1437 .show_more {
1437 display: inline-block;
1438 display: inline-block;
1438 position: relative;
1439 position: relative;
1439 vertical-align: middle;
1440 vertical-align: middle;
1440 width: 4px;
1441 width: 4px;
1441 height: @basefontsize;
1442 height: @basefontsize;
1442
1443
1443 &:after {
1444 &:after {
1444 content: "\00A0\25BE";
1445 content: "\00A0\25BE";
1445 display: inline-block;
1446 display: inline-block;
1446 width:10px;
1447 width:10px;
1447 line-height: 5px;
1448 line-height: 5px;
1448 font-size: 12px;
1449 font-size: 12px;
1449 cursor: pointer;
1450 cursor: pointer;
1450 }
1451 }
1451 }
1452 }
1452
1453
1453 .journal_more .show_more {
1454 .journal_more .show_more {
1454 display: inline;
1455 display: inline;
1455
1456
1456 &:after {
1457 &:after {
1457 content: none;
1458 content: none;
1458 }
1459 }
1459 }
1460 }
1460
1461
1461 .open .show_more:after,
1462 .open .show_more:after,
1462 .select2-dropdown-open .show_more:after {
1463 .select2-dropdown-open .show_more:after {
1463 .rotate(180deg);
1464 .rotate(180deg);
1464 margin-left: 4px;
1465 margin-left: 4px;
1465 }
1466 }
1466
1467
1467
1468
1468 .compare_view_commits .collapse_commit:after {
1469 .compare_view_commits .collapse_commit:after {
1469 cursor: pointer;
1470 cursor: pointer;
1470 content: "\00A0\25B4";
1471 content: "\00A0\25B4";
1471 margin-left: -3px;
1472 margin-left: -3px;
1472 font-size: 17px;
1473 font-size: 17px;
1473 color: @grey4;
1474 color: @grey4;
1474 }
1475 }
1475
1476
1476 .diff_links {
1477 .diff_links {
1477 margin-left: 8px;
1478 margin-left: 8px;
1478 }
1479 }
1479
1480
1480 div.ancestor {
1481 div.ancestor {
1481 margin: -30px 0px;
1482 margin: -30px 0px;
1482 }
1483 }
1483
1484
1484 .cs_icon_td input[type="checkbox"] {
1485 .cs_icon_td input[type="checkbox"] {
1485 display: none;
1486 display: none;
1486 }
1487 }
1487
1488
1488 .cs_icon_td .expand_file_icon:after {
1489 .cs_icon_td .expand_file_icon:after {
1489 cursor: pointer;
1490 cursor: pointer;
1490 content: "\00A0\25B6";
1491 content: "\00A0\25B6";
1491 font-size: 12px;
1492 font-size: 12px;
1492 color: @grey4;
1493 color: @grey4;
1493 }
1494 }
1494
1495
1495 .cs_icon_td .collapse_file_icon:after {
1496 .cs_icon_td .collapse_file_icon:after {
1496 cursor: pointer;
1497 cursor: pointer;
1497 content: "\00A0\25BC";
1498 content: "\00A0\25BC";
1498 font-size: 12px;
1499 font-size: 12px;
1499 color: @grey4;
1500 color: @grey4;
1500 }
1501 }
1501
1502
1502 /*new binary
1503 /*new binary
1503 NEW_FILENODE = 1
1504 NEW_FILENODE = 1
1504 DEL_FILENODE = 2
1505 DEL_FILENODE = 2
1505 MOD_FILENODE = 3
1506 MOD_FILENODE = 3
1506 RENAMED_FILENODE = 4
1507 RENAMED_FILENODE = 4
1507 COPIED_FILENODE = 5
1508 COPIED_FILENODE = 5
1508 CHMOD_FILENODE = 6
1509 CHMOD_FILENODE = 6
1509 BIN_FILENODE = 7
1510 BIN_FILENODE = 7
1510 */
1511 */
1511 .cs_files_expand {
1512 .cs_files_expand {
1512 font-size: @basefontsize + 5px;
1513 font-size: @basefontsize + 5px;
1513 line-height: 1.8em;
1514 line-height: 1.8em;
1514 float: right;
1515 float: right;
1515 }
1516 }
1516
1517
1517 .cs_files_expand span{
1518 .cs_files_expand span{
1518 color: @rcblue;
1519 color: @rcblue;
1519 cursor: pointer;
1520 cursor: pointer;
1520 }
1521 }
1521 .cs_files {
1522 .cs_files {
1522 clear: both;
1523 clear: both;
1523 padding-bottom: @padding;
1524 padding-bottom: @padding;
1524
1525
1525 .cur_cs {
1526 .cur_cs {
1526 margin: 10px 2px;
1527 margin: 10px 2px;
1527 font-weight: bold;
1528 font-weight: bold;
1528 }
1529 }
1529
1530
1530 .node {
1531 .node {
1531 float: left;
1532 float: left;
1532 }
1533 }
1533
1534
1534 .changes {
1535 .changes {
1535 float: right;
1536 float: right;
1536 color: white;
1537 color: white;
1537 font-size: @basefontsize - 4px;
1538 font-size: @basefontsize - 4px;
1538 margin-top: 4px;
1539 margin-top: 4px;
1539 opacity: 0.6;
1540 opacity: 0.6;
1540 filter: Alpha(opacity=60); /* IE8 and earlier */
1541 filter: Alpha(opacity=60); /* IE8 and earlier */
1541
1542
1542 .added {
1543 .added {
1543 background-color: @alert1;
1544 background-color: @alert1;
1544 float: left;
1545 float: left;
1545 text-align: center;
1546 text-align: center;
1546 }
1547 }
1547
1548
1548 .deleted {
1549 .deleted {
1549 background-color: @alert2;
1550 background-color: @alert2;
1550 float: left;
1551 float: left;
1551 text-align: center;
1552 text-align: center;
1552 }
1553 }
1553
1554
1554 .bin {
1555 .bin {
1555 background-color: @alert1;
1556 background-color: @alert1;
1556 text-align: center;
1557 text-align: center;
1557 }
1558 }
1558
1559
1559 /*new binary*/
1560 /*new binary*/
1560 .bin.bin1 {
1561 .bin.bin1 {
1561 background-color: @alert1;
1562 background-color: @alert1;
1562 text-align: center;
1563 text-align: center;
1563 }
1564 }
1564
1565
1565 /*deleted binary*/
1566 /*deleted binary*/
1566 .bin.bin2 {
1567 .bin.bin2 {
1567 background-color: @alert2;
1568 background-color: @alert2;
1568 text-align: center;
1569 text-align: center;
1569 }
1570 }
1570
1571
1571 /*mod binary*/
1572 /*mod binary*/
1572 .bin.bin3 {
1573 .bin.bin3 {
1573 background-color: @grey2;
1574 background-color: @grey2;
1574 text-align: center;
1575 text-align: center;
1575 }
1576 }
1576
1577
1577 /*rename file*/
1578 /*rename file*/
1578 .bin.bin4 {
1579 .bin.bin4 {
1579 background-color: @alert4;
1580 background-color: @alert4;
1580 text-align: center;
1581 text-align: center;
1581 }
1582 }
1582
1583
1583 /*copied file*/
1584 /*copied file*/
1584 .bin.bin5 {
1585 .bin.bin5 {
1585 background-color: @alert4;
1586 background-color: @alert4;
1586 text-align: center;
1587 text-align: center;
1587 }
1588 }
1588
1589
1589 /*chmod file*/
1590 /*chmod file*/
1590 .bin.bin6 {
1591 .bin.bin6 {
1591 background-color: @grey2;
1592 background-color: @grey2;
1592 text-align: center;
1593 text-align: center;
1593 }
1594 }
1594 }
1595 }
1595 }
1596 }
1596
1597
1597 .cs_files .cs_added, .cs_files .cs_A,
1598 .cs_files .cs_added, .cs_files .cs_A,
1598 .cs_files .cs_added, .cs_files .cs_M,
1599 .cs_files .cs_added, .cs_files .cs_M,
1599 .cs_files .cs_added, .cs_files .cs_D {
1600 .cs_files .cs_added, .cs_files .cs_D {
1600 height: 16px;
1601 height: 16px;
1601 padding-right: 10px;
1602 padding-right: 10px;
1602 margin-top: 7px;
1603 margin-top: 7px;
1603 text-align: left;
1604 text-align: left;
1604 }
1605 }
1605
1606
1606 .cs_icon_td {
1607 .cs_icon_td {
1607 min-width: 16px;
1608 min-width: 16px;
1608 width: 16px;
1609 width: 16px;
1609 }
1610 }
1610
1611
1611 .pull-request-merge {
1612 .pull-request-merge {
1612 padding: 10px 0;
1613 padding: 10px 0;
1613 margin-top: 10px;
1614 margin-top: 10px;
1614 margin-bottom: 20px;
1615 margin-bottom: 20px;
1615 }
1616 }
1616
1617
1617 .pull-request-merge .pull-request-wrap {
1618 .pull-request-merge .pull-request-wrap {
1618 height: 25px;
1619 height: 25px;
1619 padding: 5px 0;
1620 padding: 5px 0;
1620 }
1621 }
1621
1622
1622 .pull-request-merge span {
1623 .pull-request-merge span {
1623 margin-right: 10px;
1624 margin-right: 10px;
1624 }
1625 }
1625
1626
1626 .pr-versions {
1627 .pr-versions {
1627 position: relative;
1628 position: relative;
1628 top: 6px;
1629 top: 6px;
1629 }
1630 }
1630
1631
1631 #close_pull_request {
1632 #close_pull_request {
1632 margin-right: 0px;
1633 margin-right: 0px;
1633 }
1634 }
1634
1635
1635 .empty_data {
1636 .empty_data {
1636 color: @grey4;
1637 color: @grey4;
1637 }
1638 }
1638
1639
1639 #changeset_compare_view_content {
1640 #changeset_compare_view_content {
1640 margin-bottom: @space;
1641 margin-bottom: @space;
1641 clear: both;
1642 clear: both;
1642 width: 100%;
1643 width: 100%;
1643 box-sizing: border-box;
1644 box-sizing: border-box;
1644 .border-radius(@border-radius);
1645 .border-radius(@border-radius);
1645
1646
1646 .help-block {
1647 .help-block {
1647 margin: @padding 0;
1648 margin: @padding 0;
1648 color: @text-color;
1649 color: @text-color;
1649 }
1650 }
1650
1651
1651 .empty_data {
1652 .empty_data {
1652 margin: @padding 0;
1653 margin: @padding 0;
1653 }
1654 }
1654
1655
1655 .alert {
1656 .alert {
1656 margin-bottom: @space;
1657 margin-bottom: @space;
1657 }
1658 }
1658 }
1659 }
1659
1660
1660 .table_disp {
1661 .table_disp {
1661 .status {
1662 .status {
1662 width: auto;
1663 width: auto;
1663
1664
1664 .flag_status {
1665 .flag_status {
1665 float: left;
1666 float: left;
1666 }
1667 }
1667 }
1668 }
1668 }
1669 }
1669
1670
1670 .status_box_menu {
1671 .status_box_menu {
1671 margin: 0;
1672 margin: 0;
1672 }
1673 }
1673
1674
1674 .notification-table{
1675 .notification-table{
1675 margin-bottom: @space;
1676 margin-bottom: @space;
1676 display: table;
1677 display: table;
1677 width: 100%;
1678 width: 100%;
1678
1679
1679 .container{
1680 .container{
1680 display: table-row;
1681 display: table-row;
1681
1682
1682 .notification-header{
1683 .notification-header{
1683 border-bottom: @border-thickness solid @border-default-color;
1684 border-bottom: @border-thickness solid @border-default-color;
1684 }
1685 }
1685
1686
1686 .notification-subject{
1687 .notification-subject{
1687 display: table-cell;
1688 display: table-cell;
1688 }
1689 }
1689 }
1690 }
1690 }
1691 }
1691
1692
1692 // Notifications
1693 // Notifications
1693 .notification-header{
1694 .notification-header{
1694 display: table;
1695 display: table;
1695 width: 100%;
1696 width: 100%;
1696 padding: floor(@basefontsize/2) 0;
1697 padding: floor(@basefontsize/2) 0;
1697 line-height: 1em;
1698 line-height: 1em;
1698
1699
1699 .desc, .delete-notifications, .read-notifications{
1700 .desc, .delete-notifications, .read-notifications{
1700 display: table-cell;
1701 display: table-cell;
1701 text-align: left;
1702 text-align: left;
1702 }
1703 }
1703
1704
1704 .desc{
1705 .desc{
1705 width: 1163px;
1706 width: 1163px;
1706 }
1707 }
1707
1708
1708 .delete-notifications, .read-notifications{
1709 .delete-notifications, .read-notifications{
1709 width: 35px;
1710 width: 35px;
1710 min-width: 35px; //fixes when only one button is displayed
1711 min-width: 35px; //fixes when only one button is displayed
1711 }
1712 }
1712 }
1713 }
1713
1714
1714 .notification-body {
1715 .notification-body {
1715 .markdown-block,
1716 .markdown-block,
1716 .rst-block {
1717 .rst-block {
1717 padding: @padding 0;
1718 padding: @padding 0;
1718 }
1719 }
1719
1720
1720 .notification-subject {
1721 .notification-subject {
1721 padding: @textmargin 0;
1722 padding: @textmargin 0;
1722 border-bottom: @border-thickness solid @border-default-color;
1723 border-bottom: @border-thickness solid @border-default-color;
1723 }
1724 }
1724 }
1725 }
1725
1726
1726
1727
1727 .notifications_buttons{
1728 .notifications_buttons{
1728 float: right;
1729 float: right;
1729 }
1730 }
1730
1731
1731 #notification-status{
1732 #notification-status{
1732 display: inline;
1733 display: inline;
1733 }
1734 }
1734
1735
1735 // Repositories
1736 // Repositories
1736
1737
1737 #summary.fields{
1738 #summary.fields{
1738 display: table;
1739 display: table;
1739
1740
1740 .field{
1741 .field{
1741 display: table-row;
1742 display: table-row;
1742
1743
1743 .label-summary{
1744 .label-summary{
1744 display: table-cell;
1745 display: table-cell;
1745 min-width: @label-summary-minwidth;
1746 min-width: @label-summary-minwidth;
1746 padding-top: @padding/2;
1747 padding-top: @padding/2;
1747 padding-bottom: @padding/2;
1748 padding-bottom: @padding/2;
1748 padding-right: @padding/2;
1749 padding-right: @padding/2;
1749 }
1750 }
1750
1751
1751 .input{
1752 .input{
1752 display: table-cell;
1753 display: table-cell;
1753 padding: @padding/2;
1754 padding: @padding/2;
1754
1755
1755 input{
1756 input{
1756 min-width: 29em;
1757 min-width: 29em;
1757 padding: @padding/4;
1758 padding: @padding/4;
1758 }
1759 }
1759 }
1760 }
1760 .statistics, .downloads{
1761 .statistics, .downloads{
1761 .disabled{
1762 .disabled{
1762 color: @grey4;
1763 color: @grey4;
1763 }
1764 }
1764 }
1765 }
1765 }
1766 }
1766 }
1767 }
1767
1768
1768 #summary{
1769 #summary{
1769 width: 70%;
1770 width: 70%;
1770 }
1771 }
1771
1772
1772
1773
1773 // Journal
1774 // Journal
1774 .journal.title {
1775 .journal.title {
1775 h5 {
1776 h5 {
1776 float: left;
1777 float: left;
1777 margin: 0;
1778 margin: 0;
1778 width: 70%;
1779 width: 70%;
1779 }
1780 }
1780
1781
1781 ul {
1782 ul {
1782 float: right;
1783 float: right;
1783 display: inline-block;
1784 display: inline-block;
1784 margin: 0;
1785 margin: 0;
1785 width: 30%;
1786 width: 30%;
1786 text-align: right;
1787 text-align: right;
1787
1788
1788 li {
1789 li {
1789 display: inline;
1790 display: inline;
1790 font-size: @journal-fontsize;
1791 font-size: @journal-fontsize;
1791 line-height: 1em;
1792 line-height: 1em;
1792
1793
1793 &:before { content: none; }
1794 &:before { content: none; }
1794 }
1795 }
1795 }
1796 }
1796 }
1797 }
1797
1798
1798 .filterexample {
1799 .filterexample {
1799 position: absolute;
1800 position: absolute;
1800 top: 95px;
1801 top: 95px;
1801 left: @contentpadding;
1802 left: @contentpadding;
1802 color: @rcblue;
1803 color: @rcblue;
1803 font-size: 11px;
1804 font-size: 11px;
1804 font-family: @text-regular;
1805 font-family: @text-regular;
1805 cursor: help;
1806 cursor: help;
1806
1807
1807 &:hover {
1808 &:hover {
1808 color: @rcdarkblue;
1809 color: @rcdarkblue;
1809 }
1810 }
1810
1811
1811 @media (max-width:768px) {
1812 @media (max-width:768px) {
1812 position: relative;
1813 position: relative;
1813 top: auto;
1814 top: auto;
1814 left: auto;
1815 left: auto;
1815 display: block;
1816 display: block;
1816 }
1817 }
1817 }
1818 }
1818
1819
1819
1820
1820 #journal{
1821 #journal{
1821 margin-bottom: @space;
1822 margin-bottom: @space;
1822
1823
1823 .journal_day{
1824 .journal_day{
1824 margin-bottom: @textmargin/2;
1825 margin-bottom: @textmargin/2;
1825 padding-bottom: @textmargin/2;
1826 padding-bottom: @textmargin/2;
1826 font-size: @journal-fontsize;
1827 font-size: @journal-fontsize;
1827 border-bottom: @border-thickness solid @border-default-color;
1828 border-bottom: @border-thickness solid @border-default-color;
1828 }
1829 }
1829
1830
1830 .journal_container{
1831 .journal_container{
1831 margin-bottom: @space;
1832 margin-bottom: @space;
1832
1833
1833 .journal_user{
1834 .journal_user{
1834 display: inline-block;
1835 display: inline-block;
1835 }
1836 }
1836 .journal_action_container{
1837 .journal_action_container{
1837 display: block;
1838 display: block;
1838 margin-top: @textmargin;
1839 margin-top: @textmargin;
1839
1840
1840 div{
1841 div{
1841 display: inline;
1842 display: inline;
1842 }
1843 }
1843
1844
1844 div.journal_action_params{
1845 div.journal_action_params{
1845 display: block;
1846 display: block;
1846 }
1847 }
1847
1848
1848 div.journal_repo:after{
1849 div.journal_repo:after{
1849 content: "\A";
1850 content: "\A";
1850 white-space: pre;
1851 white-space: pre;
1851 }
1852 }
1852
1853
1853 div.date{
1854 div.date{
1854 display: block;
1855 display: block;
1855 margin-bottom: @textmargin;
1856 margin-bottom: @textmargin;
1856 }
1857 }
1857 }
1858 }
1858 }
1859 }
1859 }
1860 }
1860
1861
1861 // Files
1862 // Files
1862 .edit-file-title {
1863 .edit-file-title {
1863 border-bottom: @border-thickness solid @border-default-color;
1864 border-bottom: @border-thickness solid @border-default-color;
1864
1865
1865 .breadcrumbs {
1866 .breadcrumbs {
1866 margin-bottom: 0;
1867 margin-bottom: 0;
1867 }
1868 }
1868 }
1869 }
1869
1870
1870 .edit-file-fieldset {
1871 .edit-file-fieldset {
1871 margin-top: @sidebarpadding;
1872 margin-top: @sidebarpadding;
1872
1873
1873 .fieldset {
1874 .fieldset {
1874 .left-label {
1875 .left-label {
1875 width: 13%;
1876 width: 13%;
1876 }
1877 }
1877 .right-content {
1878 .right-content {
1878 width: 87%;
1879 width: 87%;
1879 max-width: 100%;
1880 max-width: 100%;
1880 }
1881 }
1881 .filename-label {
1882 .filename-label {
1882 margin-top: 13px;
1883 margin-top: 13px;
1883 }
1884 }
1884 .commit-message-label {
1885 .commit-message-label {
1885 margin-top: 4px;
1886 margin-top: 4px;
1886 }
1887 }
1887 .file-upload-input {
1888 .file-upload-input {
1888 input {
1889 input {
1889 display: none;
1890 display: none;
1890 }
1891 }
1891 }
1892 }
1892 p {
1893 p {
1893 margin-top: 5px;
1894 margin-top: 5px;
1894 }
1895 }
1895
1896
1896 }
1897 }
1897 .custom-path-link {
1898 .custom-path-link {
1898 margin-left: 5px;
1899 margin-left: 5px;
1899 }
1900 }
1900 #commit {
1901 #commit {
1901 resize: vertical;
1902 resize: vertical;
1902 }
1903 }
1903 }
1904 }
1904
1905
1905 .delete-file-preview {
1906 .delete-file-preview {
1906 max-height: 250px;
1907 max-height: 250px;
1907 }
1908 }
1908
1909
1909 .new-file,
1910 .new-file,
1910 #filter_activate,
1911 #filter_activate,
1911 #filter_deactivate {
1912 #filter_deactivate {
1912 float: left;
1913 float: left;
1913 margin: 0 0 0 15px;
1914 margin: 0 0 0 15px;
1914 }
1915 }
1915
1916
1916 h3.files_location{
1917 h3.files_location{
1917 line-height: 2.4em;
1918 line-height: 2.4em;
1918 }
1919 }
1919
1920
1920 .browser-nav {
1921 .browser-nav {
1921 display: table;
1922 display: table;
1922 margin-bottom: @space;
1923 margin-bottom: @space;
1923
1924
1924
1925
1925 .info_box {
1926 .info_box {
1926 display: inline-table;
1927 display: inline-table;
1927 height: 2.5em;
1928 height: 2.5em;
1928
1929
1929 .browser-cur-rev, .info_box_elem {
1930 .browser-cur-rev, .info_box_elem {
1930 display: table-cell;
1931 display: table-cell;
1931 vertical-align: middle;
1932 vertical-align: middle;
1932 }
1933 }
1933
1934
1934 .info_box_elem {
1935 .info_box_elem {
1935 border-top: @border-thickness solid @rcblue;
1936 border-top: @border-thickness solid @rcblue;
1936 border-bottom: @border-thickness solid @rcblue;
1937 border-bottom: @border-thickness solid @rcblue;
1937
1938
1938 #at_rev, a {
1939 #at_rev, a {
1939 padding: 0.6em 0.9em;
1940 padding: 0.6em 0.9em;
1940 margin: 0;
1941 margin: 0;
1941 .box-shadow(none);
1942 .box-shadow(none);
1942 border: 0;
1943 border: 0;
1943 height: 12px;
1944 height: 12px;
1944 }
1945 }
1945
1946
1946 input#at_rev {
1947 input#at_rev {
1947 max-width: 50px;
1948 max-width: 50px;
1948 text-align: right;
1949 text-align: right;
1949 }
1950 }
1950
1951
1951 &.previous {
1952 &.previous {
1952 border: @border-thickness solid @rcblue;
1953 border: @border-thickness solid @rcblue;
1953 .disabled {
1954 .disabled {
1954 color: @grey4;
1955 color: @grey4;
1955 cursor: not-allowed;
1956 cursor: not-allowed;
1956 }
1957 }
1957 }
1958 }
1958
1959
1959 &.next {
1960 &.next {
1960 border: @border-thickness solid @rcblue;
1961 border: @border-thickness solid @rcblue;
1961 .disabled {
1962 .disabled {
1962 color: @grey4;
1963 color: @grey4;
1963 cursor: not-allowed;
1964 cursor: not-allowed;
1964 }
1965 }
1965 }
1966 }
1966 }
1967 }
1967
1968
1968 .browser-cur-rev {
1969 .browser-cur-rev {
1969
1970
1970 span{
1971 span{
1971 margin: 0;
1972 margin: 0;
1972 color: @rcblue;
1973 color: @rcblue;
1973 height: 12px;
1974 height: 12px;
1974 display: inline-block;
1975 display: inline-block;
1975 padding: 0.7em 1em ;
1976 padding: 0.7em 1em ;
1976 border: @border-thickness solid @rcblue;
1977 border: @border-thickness solid @rcblue;
1977 margin-right: @padding;
1978 margin-right: @padding;
1978 }
1979 }
1979 }
1980 }
1980 }
1981 }
1981
1982
1982 .search_activate {
1983 .search_activate {
1983 display: table-cell;
1984 display: table-cell;
1984 vertical-align: middle;
1985 vertical-align: middle;
1985
1986
1986 input, label{
1987 input, label{
1987 margin: 0;
1988 margin: 0;
1988 padding: 0;
1989 padding: 0;
1989 }
1990 }
1990
1991
1991 input{
1992 input{
1992 margin-left: @textmargin;
1993 margin-left: @textmargin;
1993 }
1994 }
1994
1995
1995 }
1996 }
1996 }
1997 }
1997
1998
1998 .browser-cur-rev{
1999 .browser-cur-rev{
1999 margin-bottom: @textmargin;
2000 margin-bottom: @textmargin;
2000 }
2001 }
2001
2002
2002 #node_filter_box_loading{
2003 #node_filter_box_loading{
2003 .info_text;
2004 .info_text;
2004 }
2005 }
2005
2006
2006 .browser-search {
2007 .browser-search {
2007 margin: -25px 0px 5px 0px;
2008 margin: -25px 0px 5px 0px;
2008 }
2009 }
2009
2010
2010 .node-filter {
2011 .node-filter {
2011 font-size: @repo-title-fontsize;
2012 font-size: @repo-title-fontsize;
2012 padding: 4px 0px 0px 0px;
2013 padding: 4px 0px 0px 0px;
2013
2014
2014 .node-filter-path {
2015 .node-filter-path {
2015 float: left;
2016 float: left;
2016 color: @grey4;
2017 color: @grey4;
2017 }
2018 }
2018 .node-filter-input {
2019 .node-filter-input {
2019 float: left;
2020 float: left;
2020 margin: -2px 0px 0px 2px;
2021 margin: -2px 0px 0px 2px;
2021 input {
2022 input {
2022 padding: 2px;
2023 padding: 2px;
2023 border: none;
2024 border: none;
2024 font-size: @repo-title-fontsize;
2025 font-size: @repo-title-fontsize;
2025 }
2026 }
2026 }
2027 }
2027 }
2028 }
2028
2029
2029
2030
2030 .browser-result{
2031 .browser-result{
2031 td a{
2032 td a{
2032 margin-left: 0.5em;
2033 margin-left: 0.5em;
2033 display: inline-block;
2034 display: inline-block;
2034
2035
2035 em{
2036 em{
2036 font-family: @text-bold;
2037 font-family: @text-bold;
2037 }
2038 }
2038 }
2039 }
2039 }
2040 }
2040
2041
2041 .browser-highlight{
2042 .browser-highlight{
2042 background-color: @grey5-alpha;
2043 background-color: @grey5-alpha;
2043 }
2044 }
2044
2045
2045
2046
2046 // Search
2047 // Search
2047
2048
2048 .search-form{
2049 .search-form{
2049 #q {
2050 #q {
2050 width: @search-form-width;
2051 width: @search-form-width;
2051 }
2052 }
2052 .fields{
2053 .fields{
2053 margin: 0 0 @space;
2054 margin: 0 0 @space;
2054 }
2055 }
2055
2056
2056 label{
2057 label{
2057 display: inline-block;
2058 display: inline-block;
2058 margin-right: @textmargin;
2059 margin-right: @textmargin;
2059 padding-top: 0.25em;
2060 padding-top: 0.25em;
2060 }
2061 }
2061
2062
2062
2063
2063 .results{
2064 .results{
2064 clear: both;
2065 clear: both;
2065 margin: 0 0 @padding;
2066 margin: 0 0 @padding;
2066 }
2067 }
2067 }
2068 }
2068
2069
2069 div.search-feedback-items {
2070 div.search-feedback-items {
2070 display: inline-block;
2071 display: inline-block;
2071 padding:0px 0px 0px 96px;
2072 padding:0px 0px 0px 96px;
2072 }
2073 }
2073
2074
2074 div.search-code-body {
2075 div.search-code-body {
2075 background-color: #ffffff; padding: 5px 0 5px 10px;
2076 background-color: #ffffff; padding: 5px 0 5px 10px;
2076 pre {
2077 pre {
2077 .match { background-color: #faffa6;}
2078 .match { background-color: #faffa6;}
2078 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2079 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2079 }
2080 }
2080 }
2081 }
2081
2082
2082 .expand_commit.search {
2083 .expand_commit.search {
2083 .show_more.open {
2084 .show_more.open {
2084 height: auto;
2085 height: auto;
2085 max-height: none;
2086 max-height: none;
2086 }
2087 }
2087 }
2088 }
2088
2089
2089 .search-results {
2090 .search-results {
2090
2091
2091 h2 {
2092 h2 {
2092 margin-bottom: 0;
2093 margin-bottom: 0;
2093 }
2094 }
2094 .codeblock {
2095 .codeblock {
2095 border: none;
2096 border: none;
2096 background: transparent;
2097 background: transparent;
2097 }
2098 }
2098
2099
2099 .codeblock-header {
2100 .codeblock-header {
2100 border: none;
2101 border: none;
2101 background: transparent;
2102 background: transparent;
2102 }
2103 }
2103
2104
2104 .code-body {
2105 .code-body {
2105 border: @border-thickness solid @border-default-color;
2106 border: @border-thickness solid @border-default-color;
2106 .border-radius(@border-radius);
2107 .border-radius(@border-radius);
2107 }
2108 }
2108
2109
2109 .td-commit {
2110 .td-commit {
2110 &:extend(pre);
2111 &:extend(pre);
2111 border-bottom: @border-thickness solid @border-default-color;
2112 border-bottom: @border-thickness solid @border-default-color;
2112 }
2113 }
2113
2114
2114 .message {
2115 .message {
2115 height: auto;
2116 height: auto;
2116 max-width: 350px;
2117 max-width: 350px;
2117 white-space: normal;
2118 white-space: normal;
2118 text-overflow: initial;
2119 text-overflow: initial;
2119 overflow: visible;
2120 overflow: visible;
2120
2121
2121 .match { background-color: #faffa6;}
2122 .match { background-color: #faffa6;}
2122 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2123 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2123 }
2124 }
2124
2125
2125 }
2126 }
2126
2127
2127 table.rctable td.td-search-results div {
2128 table.rctable td.td-search-results div {
2128 max-width: 100%;
2129 max-width: 100%;
2129 }
2130 }
2130
2131
2131 #tip-box, .tip-box{
2132 #tip-box, .tip-box{
2132 padding: @menupadding/2;
2133 padding: @menupadding/2;
2133 display: block;
2134 display: block;
2134 border: @border-thickness solid @border-highlight-color;
2135 border: @border-thickness solid @border-highlight-color;
2135 .border-radius(@border-radius);
2136 .border-radius(@border-radius);
2136 background-color: white;
2137 background-color: white;
2137 z-index: 99;
2138 z-index: 99;
2138 white-space: pre-wrap;
2139 white-space: pre-wrap;
2139 }
2140 }
2140
2141
2141 #linktt {
2142 #linktt {
2142 width: 79px;
2143 width: 79px;
2143 }
2144 }
2144
2145
2145 #help_kb .modal-content{
2146 #help_kb .modal-content{
2146 max-width: 750px;
2147 max-width: 750px;
2147 margin: 10% auto;
2148 margin: 10% auto;
2148
2149
2149 table{
2150 table{
2150 td,th{
2151 td,th{
2151 border-bottom: none;
2152 border-bottom: none;
2152 line-height: 2.5em;
2153 line-height: 2.5em;
2153 }
2154 }
2154 th{
2155 th{
2155 padding-bottom: @textmargin/2;
2156 padding-bottom: @textmargin/2;
2156 }
2157 }
2157 td.keys{
2158 td.keys{
2158 text-align: center;
2159 text-align: center;
2159 }
2160 }
2160 }
2161 }
2161
2162
2162 .block-left{
2163 .block-left{
2163 width: 45%;
2164 width: 45%;
2164 margin-right: 5%;
2165 margin-right: 5%;
2165 }
2166 }
2166 .modal-footer{
2167 .modal-footer{
2167 clear: both;
2168 clear: both;
2168 }
2169 }
2169 .key.tag{
2170 .key.tag{
2170 padding: 0.5em;
2171 padding: 0.5em;
2171 background-color: @rcblue;
2172 background-color: @rcblue;
2172 color: white;
2173 color: white;
2173 border-color: @rcblue;
2174 border-color: @rcblue;
2174 .box-shadow(none);
2175 .box-shadow(none);
2175 }
2176 }
2176 }
2177 }
2177
2178
2178
2179
2179
2180
2180 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2181 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2181
2182
2182 @import 'statistics-graph';
2183 @import 'statistics-graph';
2183 @import 'tables';
2184 @import 'tables';
2184 @import 'forms';
2185 @import 'forms';
2185 @import 'diff';
2186 @import 'diff';
2186 @import 'summary';
2187 @import 'summary';
2187 @import 'navigation';
2188 @import 'navigation';
2188
2189
2189 //--- SHOW/HIDE SECTIONS --//
2190 //--- SHOW/HIDE SECTIONS --//
2190
2191
2191 .btn-collapse {
2192 .btn-collapse {
2192 float: right;
2193 float: right;
2193 text-align: right;
2194 text-align: right;
2194 font-family: @text-light;
2195 font-family: @text-light;
2195 font-size: @basefontsize;
2196 font-size: @basefontsize;
2196 cursor: pointer;
2197 cursor: pointer;
2197 border: none;
2198 border: none;
2198 color: @rcblue;
2199 color: @rcblue;
2199 }
2200 }
2200
2201
2201 table.rctable,
2202 table.rctable,
2202 table.dataTable {
2203 table.dataTable {
2203 .btn-collapse {
2204 .btn-collapse {
2204 float: right;
2205 float: right;
2205 text-align: right;
2206 text-align: right;
2206 }
2207 }
2207 }
2208 }
2208
2209
2209
2210
2210 // TODO: johbo: Fix for IE10, this avoids that we see a border
2211 // TODO: johbo: Fix for IE10, this avoids that we see a border
2211 // and padding around checkboxes and radio boxes. Move to the right place,
2212 // and padding around checkboxes and radio boxes. Move to the right place,
2212 // or better: Remove this once we did the form refactoring.
2213 // or better: Remove this once we did the form refactoring.
2213 input[type=checkbox],
2214 input[type=checkbox],
2214 input[type=radio] {
2215 input[type=radio] {
2215 padding: 0;
2216 padding: 0;
2216 border: none;
2217 border: none;
2217 }
2218 }
2218
2219
2219 .toggle-ajax-spinner{
2220 .toggle-ajax-spinner{
2220 height: 16px;
2221 height: 16px;
2221 width: 16px;
2222 width: 16px;
2222 }
2223 }
@@ -1,658 +1,666 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 // returns a node from given html;
28 // returns a node from given html;
29 var fromHTML = function(html){
29 var fromHTML = function(html){
30 var _html = document.createElement('element');
30 var _html = document.createElement('element');
31 _html.innerHTML = html;
31 _html.innerHTML = html;
32 return _html;
32 return _html;
33 };
33 };
34
34
35 var tableTr = function(cls, body){
35 var tableTr = function(cls, body){
36 var _el = document.createElement('div');
36 var _el = document.createElement('div');
37 var _body = $(body).attr('id');
37 var _body = $(body).attr('id');
38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
39 var id = 'comment-tr-{0}'.format(comment_id);
39 var id = 'comment-tr-{0}'.format(comment_id);
40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
41 '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+
41 '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+
42 '<td></td>'+
42 '<td></td>'+
43 '<td></td>'+
43 '<td></td>'+
44 '<td></td>'+
44 '<td></td>'+
45 '<td>{2}</td>'+
45 '<td>{2}</td>'+
46 '</tr></tbody></table>').format(id, cls, body);
46 '</tr></tbody></table>').format(id, cls, body);
47 $(_el).html(_html);
47 $(_el).html(_html);
48 return _el.children[0].children[0].children[0];
48 return _el.children[0].children[0].children[0];
49 };
49 };
50
50
51 function bindDeleteCommentButtons() {
51 function bindDeleteCommentButtons() {
52 $('.delete-comment').one('click', function() {
52 $('.delete-comment').one('click', function() {
53 var comment_id = $(this).data("comment-id");
53 var comment_id = $(this).data("comment-id");
54
54
55 if (comment_id){
55 if (comment_id){
56 deleteComment(comment_id);
56 deleteComment(comment_id);
57 }
57 }
58 });
58 });
59 }
59 }
60
60
61 var deleteComment = function(comment_id) {
61 var deleteComment = function(comment_id) {
62 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
62 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
63 var postData = {
63 var postData = {
64 '_method': 'delete',
64 '_method': 'delete',
65 'csrf_token': CSRF_TOKEN
65 'csrf_token': CSRF_TOKEN
66 };
66 };
67
67
68 var success = function(o) {
68 var success = function(o) {
69 window.location.reload();
69 window.location.reload();
70 };
70 };
71 ajaxPOST(url, postData, success);
71 ajaxPOST(url, postData, success);
72 };
72 };
73
73
74
74
75 var bindToggleButtons = function() {
75 var bindToggleButtons = function() {
76 $('.comment-toggle').on('click', function() {
76 $('.comment-toggle').on('click', function() {
77 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
77 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
78 });
78 });
79 };
79 };
80
80
81 var linkifyComments = function(comments) {
81 var linkifyComments = function(comments) {
82 /* TODO: dan: remove this - it should no longer needed */
82 /* TODO: dan: remove this - it should no longer needed */
83 for (var i = 0; i < comments.length; i++) {
83 for (var i = 0; i < comments.length; i++) {
84 var comment_id = $(comments[i]).data('comment-id');
84 var comment_id = $(comments[i]).data('comment-id');
85 var prev_comment_id = $(comments[i - 1]).data('comment-id');
85 var prev_comment_id = $(comments[i - 1]).data('comment-id');
86 var next_comment_id = $(comments[i + 1]).data('comment-id');
86 var next_comment_id = $(comments[i + 1]).data('comment-id');
87
87
88 // place next/prev links
88 // place next/prev links
89 if (prev_comment_id) {
89 if (prev_comment_id) {
90 $('#prev_c_' + comment_id).show();
90 $('#prev_c_' + comment_id).show();
91 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
91 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
92 'href', '#comment-' + prev_comment_id).removeClass('disabled');
92 'href', '#comment-' + prev_comment_id).removeClass('disabled');
93 }
93 }
94 if (next_comment_id) {
94 if (next_comment_id) {
95 $('#next_c_' + comment_id).show();
95 $('#next_c_' + comment_id).show();
96 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
96 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
97 'href', '#comment-' + next_comment_id).removeClass('disabled');
97 'href', '#comment-' + next_comment_id).removeClass('disabled');
98 }
98 }
99 // place a first link to the total counter
99 // place a first link to the total counter
100 if (i === 0) {
100 if (i === 0) {
101 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
101 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
102 }
102 }
103 }
103 }
104
104
105 };
105 };
106
106
107
107
108 /* Comment form for main and inline comments */
108 /* Comment form for main and inline comments */
109 var CommentForm = (function() {
109 var CommentForm = (function() {
110 "use strict";
110 "use strict";
111
111
112 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
112 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
113
113
114 this.withLineNo = function(selector) {
114 this.withLineNo = function(selector) {
115 var lineNo = this.lineNo;
115 var lineNo = this.lineNo;
116 if (lineNo === undefined) {
116 if (lineNo === undefined) {
117 return selector
117 return selector
118 } else {
118 } else {
119 return selector + '_' + lineNo;
119 return selector + '_' + lineNo;
120 }
120 }
121 };
121 };
122
122
123 this.commitId = commitId;
123 this.commitId = commitId;
124 this.pullRequestId = pullRequestId;
124 this.pullRequestId = pullRequestId;
125 this.lineNo = lineNo;
125 this.lineNo = lineNo;
126 this.initAutocompleteActions = initAutocompleteActions;
126 this.initAutocompleteActions = initAutocompleteActions;
127
127
128 this.previewButton = this.withLineNo('#preview-btn');
128 this.previewButton = this.withLineNo('#preview-btn');
129 this.previewContainer = this.withLineNo('#preview-container');
129 this.previewContainer = this.withLineNo('#preview-container');
130
130
131 this.previewBoxSelector = this.withLineNo('#preview-box');
131 this.previewBoxSelector = this.withLineNo('#preview-box');
132
132
133 this.editButton = this.withLineNo('#edit-btn');
133 this.editButton = this.withLineNo('#edit-btn');
134 this.editContainer = this.withLineNo('#edit-container');
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 this.statusChange = '#change_status';
141 this.statusChange = '#change_status';
139 this.cmBox = this.withLineNo('#text');
140 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
141
142
142 this.submitForm = formElement;
143 this.submitForm = formElement;
143 this.submitButton = $(this.submitForm).find('input[type="submit"]');
144 this.submitButton = $(this.submitForm).find('input[type="submit"]');
144 this.submitButtonText = this.submitButton.val();
145 this.submitButtonText = this.submitButton.val();
145
146
146 this.previewUrl = pyroutes.url('changeset_comment_preview',
147 this.previewUrl = pyroutes.url('changeset_comment_preview',
147 {'repo_name': templateContext.repo_name});
148 {'repo_name': templateContext.repo_name});
148
149
149 // based on commitId, or pullReuqestId decide where do we submit
150 // based on commitId, or pullReuqestId decide where do we submit
150 // out data
151 // out data
151 if (this.commitId){
152 if (this.commitId){
152 this.submitUrl = pyroutes.url('changeset_comment',
153 this.submitUrl = pyroutes.url('changeset_comment',
153 {'repo_name': templateContext.repo_name,
154 {'repo_name': templateContext.repo_name,
154 'revision': this.commitId});
155 'revision': this.commitId});
155
156
156 } else if (this.pullRequestId) {
157 } else if (this.pullRequestId) {
157 this.submitUrl = pyroutes.url('pullrequest_comment',
158 this.submitUrl = pyroutes.url('pullrequest_comment',
158 {'repo_name': templateContext.repo_name,
159 {'repo_name': templateContext.repo_name,
159 'pull_request_id': this.pullRequestId});
160 'pull_request_id': this.pullRequestId});
160
161
161 } else {
162 } else {
162 throw new Error(
163 throw new Error(
163 'CommentForm requires pullRequestId, or commitId to be specified.')
164 'CommentForm requires pullRequestId, or commitId to be specified.')
164 }
165 }
165
166
166 this.getCmInstance = function(){
167 this.getCmInstance = function(){
167 return this.cm
168 return this.cm
168 };
169 };
169
170
170 var self = this;
171 var self = this;
171
172
172 this.getCommentStatus = function() {
173 this.getCommentStatus = function() {
173 return $(this.submitForm).find(this.statusChange).val();
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 this.isAllowedToSubmit = function() {
179 this.isAllowedToSubmit = function() {
177 return !$(this.submitButton).prop('disabled');
180 return !$(this.submitButton).prop('disabled');
178 };
181 };
179
182
180 this.initStatusChangeSelector = function(){
183 this.initStatusChangeSelector = function(){
181 var formatChangeStatus = function(state, escapeMarkup) {
184 var formatChangeStatus = function(state, escapeMarkup) {
182 var originalOption = state.element;
185 var originalOption = state.element;
183 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
186 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
184 '<span>' + escapeMarkup(state.text) + '</span>';
187 '<span>' + escapeMarkup(state.text) + '</span>';
185 };
188 };
186 var formatResult = function(result, container, query, escapeMarkup) {
189 var formatResult = function(result, container, query, escapeMarkup) {
187 return formatChangeStatus(result, escapeMarkup);
190 return formatChangeStatus(result, escapeMarkup);
188 };
191 };
189
192
190 var formatSelection = function(data, container, escapeMarkup) {
193 var formatSelection = function(data, container, escapeMarkup) {
191 return formatChangeStatus(data, escapeMarkup);
194 return formatChangeStatus(data, escapeMarkup);
192 };
195 };
193
196
194 $(this.submitForm).find(this.statusChange).select2({
197 $(this.submitForm).find(this.statusChange).select2({
195 placeholder: _gettext('Status Review'),
198 placeholder: _gettext('Status Review'),
196 formatResult: formatResult,
199 formatResult: formatResult,
197 formatSelection: formatSelection,
200 formatSelection: formatSelection,
198 containerCssClass: "drop-menu status_box_menu",
201 containerCssClass: "drop-menu status_box_menu",
199 dropdownCssClass: "drop-menu-dropdown",
202 dropdownCssClass: "drop-menu-dropdown",
200 dropdownAutoWidth: true,
203 dropdownAutoWidth: true,
201 minimumResultsForSearch: -1
204 minimumResultsForSearch: -1
202 });
205 });
203 $(this.submitForm).find(this.statusChange).on('change', function() {
206 $(this.submitForm).find(this.statusChange).on('change', function() {
204 var status = self.getCommentStatus();
207 var status = self.getCommentStatus();
205 if (status && !self.lineNo) {
208 if (status && !self.lineNo) {
206 $(self.submitButton).prop('disabled', false);
209 $(self.submitButton).prop('disabled', false);
207 }
210 }
208 //todo, fix this name
211 //todo, fix this name
209 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
212 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
210 self.cm.setOption('placeholder', placeholderText);
213 self.cm.setOption('placeholder', placeholderText);
211 })
214 })
212 };
215 };
213
216
214 // reset the comment form into it's original state
217 // reset the comment form into it's original state
215 this.resetCommentFormState = function(content) {
218 this.resetCommentFormState = function(content) {
216 content = content || '';
219 content = content || '';
217
220
218 $(this.editContainer).show();
221 $(this.editContainer).show();
219 $(this.editButton).parent().addClass('active');
222 $(this.editButton).parent().addClass('active');
220
223
221 $(this.previewContainer).hide();
224 $(this.previewContainer).hide();
222 $(this.previewButton).parent().removeClass('active');
225 $(this.previewButton).parent().removeClass('active');
223
226
224 this.setActionButtonsDisabled(true);
227 this.setActionButtonsDisabled(true);
225 self.cm.setValue(content);
228 self.cm.setValue(content);
226 self.cm.setOption("readOnly", false);
229 self.cm.setOption("readOnly", false);
227 };
230 };
228
231
229 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
232 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
230 failHandler = failHandler || function() {};
233 failHandler = failHandler || function() {};
231 var postData = toQueryString(postData);
234 var postData = toQueryString(postData);
232 var request = $.ajax({
235 var request = $.ajax({
233 url: url,
236 url: url,
234 type: 'POST',
237 type: 'POST',
235 data: postData,
238 data: postData,
236 headers: {'X-PARTIAL-XHR': true}
239 headers: {'X-PARTIAL-XHR': true}
237 })
240 })
238 .done(function(data) {
241 .done(function(data) {
239 successHandler(data);
242 successHandler(data);
240 })
243 })
241 .fail(function(data, textStatus, errorThrown){
244 .fail(function(data, textStatus, errorThrown){
242 alert(
245 alert(
243 "Error while submitting comment.\n" +
246 "Error while submitting comment.\n" +
244 "Error code {0} ({1}).".format(data.status, data.statusText));
247 "Error code {0} ({1}).".format(data.status, data.statusText));
245 failHandler()
248 failHandler()
246 });
249 });
247 return request;
250 return request;
248 };
251 };
249
252
250 // overwrite a submitHandler, we need to do it for inline comments
253 // overwrite a submitHandler, we need to do it for inline comments
251 this.setHandleFormSubmit = function(callback) {
254 this.setHandleFormSubmit = function(callback) {
252 this.handleFormSubmit = callback;
255 this.handleFormSubmit = callback;
253 };
256 };
254
257
255 // default handler for for submit for main comments
258 // default handler for for submit for main comments
256 this.handleFormSubmit = function() {
259 this.handleFormSubmit = function() {
257 var text = self.cm.getValue();
260 var text = self.cm.getValue();
258 var status = self.getCommentStatus();
261 var status = self.getCommentStatus();
262 var commentType = self.getCommentType();
259
263
260 if (text === "" && !status) {
264 if (text === "" && !status) {
261 return;
265 return;
262 }
266 }
263
267
264 var excludeCancelBtn = false;
268 var excludeCancelBtn = false;
265 var submitEvent = true;
269 var submitEvent = true;
266 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
270 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
267 self.cm.setOption("readOnly", true);
271 self.cm.setOption("readOnly", true);
268 var postData = {
272 var postData = {
269 'text': text,
273 'text': text,
270 'changeset_status': status,
274 'changeset_status': status,
275 'comment_type': commentType,
271 'csrf_token': CSRF_TOKEN
276 'csrf_token': CSRF_TOKEN
272 };
277 };
273
278
274 var submitSuccessCallback = function(o) {
279 var submitSuccessCallback = function(o) {
275 if (status) {
280 if (status) {
276 location.reload(true);
281 location.reload(true);
277 } else {
282 } else {
278 $('#injected_page_comments').append(o.rendered_text);
283 $('#injected_page_comments').append(o.rendered_text);
279 self.resetCommentFormState();
284 self.resetCommentFormState();
280 bindDeleteCommentButtons();
285 bindDeleteCommentButtons();
281 timeagoActivate();
286 timeagoActivate();
282 }
287 }
283 };
288 };
284 var submitFailCallback = function(){
289 var submitFailCallback = function(){
285 self.resetCommentFormState(text)
290 self.resetCommentFormState(text)
286 };
291 };
287 self.submitAjaxPOST(
292 self.submitAjaxPOST(
288 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
293 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
289 };
294 };
290
295
291 this.previewSuccessCallback = function(o) {
296 this.previewSuccessCallback = function(o) {
292 $(self.previewBoxSelector).html(o);
297 $(self.previewBoxSelector).html(o);
293 $(self.previewBoxSelector).removeClass('unloaded');
298 $(self.previewBoxSelector).removeClass('unloaded');
294
299
295 // swap buttons, making preview active
300 // swap buttons, making preview active
296 $(self.previewButton).parent().addClass('active');
301 $(self.previewButton).parent().addClass('active');
297 $(self.editButton).parent().removeClass('active');
302 $(self.editButton).parent().removeClass('active');
298
303
299 // unlock buttons
304 // unlock buttons
300 self.setActionButtonsDisabled(false);
305 self.setActionButtonsDisabled(false);
301 };
306 };
302
307
303 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
308 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
304 excludeCancelBtn = excludeCancelBtn || false;
309 excludeCancelBtn = excludeCancelBtn || false;
305 submitEvent = submitEvent || false;
310 submitEvent = submitEvent || false;
306
311
307 $(this.editButton).prop('disabled', state);
312 $(this.editButton).prop('disabled', state);
308 $(this.previewButton).prop('disabled', state);
313 $(this.previewButton).prop('disabled', state);
309
314
310 if (!excludeCancelBtn) {
315 if (!excludeCancelBtn) {
311 $(this.cancelButton).prop('disabled', state);
316 $(this.cancelButton).prop('disabled', state);
312 }
317 }
313
318
314 var submitState = state;
319 var submitState = state;
315 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
320 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
316 // if the value of commit review status is set, we allow
321 // if the value of commit review status is set, we allow
317 // submit button, but only on Main form, lineNo means inline
322 // submit button, but only on Main form, lineNo means inline
318 submitState = false
323 submitState = false
319 }
324 }
320 $(this.submitButton).prop('disabled', submitState);
325 $(this.submitButton).prop('disabled', submitState);
321 if (submitEvent) {
326 if (submitEvent) {
322 $(this.submitButton).val(_gettext('Submitting...'));
327 $(this.submitButton).val(_gettext('Submitting...'));
323 } else {
328 } else {
324 $(this.submitButton).val(this.submitButtonText);
329 $(this.submitButton).val(this.submitButtonText);
325 }
330 }
326
331
327 };
332 };
328
333
329 // lock preview/edit/submit buttons on load, but exclude cancel button
334 // lock preview/edit/submit buttons on load, but exclude cancel button
330 var excludeCancelBtn = true;
335 var excludeCancelBtn = true;
331 this.setActionButtonsDisabled(true, excludeCancelBtn);
336 this.setActionButtonsDisabled(true, excludeCancelBtn);
332
337
333 // anonymous users don't have access to initialized CM instance
338 // anonymous users don't have access to initialized CM instance
334 if (this.cm !== undefined){
339 if (this.cm !== undefined){
335 this.cm.on('change', function(cMirror) {
340 this.cm.on('change', function(cMirror) {
336 if (cMirror.getValue() === "") {
341 if (cMirror.getValue() === "") {
337 self.setActionButtonsDisabled(true, excludeCancelBtn)
342 self.setActionButtonsDisabled(true, excludeCancelBtn)
338 } else {
343 } else {
339 self.setActionButtonsDisabled(false, excludeCancelBtn)
344 self.setActionButtonsDisabled(false, excludeCancelBtn)
340 }
345 }
341 });
346 });
342 }
347 }
343
348
344 $(this.editButton).on('click', function(e) {
349 $(this.editButton).on('click', function(e) {
345 e.preventDefault();
350 e.preventDefault();
346
351
347 $(self.previewButton).parent().removeClass('active');
352 $(self.previewButton).parent().removeClass('active');
348 $(self.previewContainer).hide();
353 $(self.previewContainer).hide();
349
354
350 $(self.editButton).parent().addClass('active');
355 $(self.editButton).parent().addClass('active');
351 $(self.editContainer).show();
356 $(self.editContainer).show();
352
357
353 });
358 });
354
359
355 $(this.previewButton).on('click', function(e) {
360 $(this.previewButton).on('click', function(e) {
356 e.preventDefault();
361 e.preventDefault();
357 var text = self.cm.getValue();
362 var text = self.cm.getValue();
358
363
359 if (text === "") {
364 if (text === "") {
360 return;
365 return;
361 }
366 }
362
367
363 var postData = {
368 var postData = {
364 'text': text,
369 'text': text,
365 'renderer': DEFAULT_RENDERER,
370 'renderer': DEFAULT_RENDERER,
366 'csrf_token': CSRF_TOKEN
371 'csrf_token': CSRF_TOKEN
367 };
372 };
368
373
369 // lock ALL buttons on preview
374 // lock ALL buttons on preview
370 self.setActionButtonsDisabled(true);
375 self.setActionButtonsDisabled(true);
371
376
372 $(self.previewBoxSelector).addClass('unloaded');
377 $(self.previewBoxSelector).addClass('unloaded');
373 $(self.previewBoxSelector).html(_gettext('Loading ...'));
378 $(self.previewBoxSelector).html(_gettext('Loading ...'));
374
379
375 $(self.editContainer).hide();
380 $(self.editContainer).hide();
376 $(self.previewContainer).show();
381 $(self.previewContainer).show();
377
382
378 // by default we reset state of comment preserving the text
383 // by default we reset state of comment preserving the text
379 var previewFailCallback = function(){
384 var previewFailCallback = function(){
380 self.resetCommentFormState(text)
385 self.resetCommentFormState(text)
381 };
386 };
382 self.submitAjaxPOST(
387 self.submitAjaxPOST(
383 self.previewUrl, postData, self.previewSuccessCallback,
388 self.previewUrl, postData, self.previewSuccessCallback,
384 previewFailCallback);
389 previewFailCallback);
385
390
386 $(self.previewButton).parent().addClass('active');
391 $(self.previewButton).parent().addClass('active');
387 $(self.editButton).parent().removeClass('active');
392 $(self.editButton).parent().removeClass('active');
388 });
393 });
389
394
390 $(this.submitForm).submit(function(e) {
395 $(this.submitForm).submit(function(e) {
391 e.preventDefault();
396 e.preventDefault();
392 var allowedToSubmit = self.isAllowedToSubmit();
397 var allowedToSubmit = self.isAllowedToSubmit();
393 if (!allowedToSubmit){
398 if (!allowedToSubmit){
394 return false;
399 return false;
395 }
400 }
396 self.handleFormSubmit();
401 self.handleFormSubmit();
397 });
402 });
398
403
399 }
404 }
400
405
401 return CommentForm;
406 return CommentForm;
402 })();
407 })();
403
408
404 var CommentsController = function() { /* comments controller */
409 var CommentsController = function() { /* comments controller */
405 var self = this;
410 var self = this;
406
411
407 this.cancelComment = function(node) {
412 this.cancelComment = function(node) {
408 var $node = $(node);
413 var $node = $(node);
409 var $td = $node.closest('td');
414 var $td = $node.closest('td');
410 $node.closest('.comment-inline-form').removeClass('comment-inline-form-open');
415 $node.closest('.comment-inline-form').removeClass('comment-inline-form-open');
411 return false;
416 return false;
412 };
417 };
413
418
414 this.getLineNumber = function(node) {
419 this.getLineNumber = function(node) {
415 var $node = $(node);
420 var $node = $(node);
416 return $node.closest('td').attr('data-line-number');
421 return $node.closest('td').attr('data-line-number');
417 };
422 };
418
423
419 this.scrollToComment = function(node, offset, outdated) {
424 this.scrollToComment = function(node, offset, outdated) {
420 var outdated = outdated || false;
425 var outdated = outdated || false;
421 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
426 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
422
427
423 if (!node) {
428 if (!node) {
424 node = $('.comment-selected');
429 node = $('.comment-selected');
425 if (!node.length) {
430 if (!node.length) {
426 node = $('comment-current')
431 node = $('comment-current')
427 }
432 }
428 }
433 }
429 $comment = $(node).closest(klass);
434 $comment = $(node).closest(klass);
430 $comments = $(klass);
435 $comments = $(klass);
431
436
432 $('.comment-selected').removeClass('comment-selected');
437 $('.comment-selected').removeClass('comment-selected');
433
438
434 var nextIdx = $(klass).index($comment) + offset;
439 var nextIdx = $(klass).index($comment) + offset;
435 if (nextIdx >= $comments.length) {
440 if (nextIdx >= $comments.length) {
436 nextIdx = 0;
441 nextIdx = 0;
437 }
442 }
438 var $next = $(klass).eq(nextIdx);
443 var $next = $(klass).eq(nextIdx);
439 var $cb = $next.closest('.cb');
444 var $cb = $next.closest('.cb');
440 $cb.removeClass('cb-collapsed');
445 $cb.removeClass('cb-collapsed');
441
446
442 var $filediffCollapseState = $cb.closest('.filediff').prev();
447 var $filediffCollapseState = $cb.closest('.filediff').prev();
443 $filediffCollapseState.prop('checked', false);
448 $filediffCollapseState.prop('checked', false);
444 $next.addClass('comment-selected');
449 $next.addClass('comment-selected');
445 scrollToElement($next);
450 scrollToElement($next);
446 return false;
451 return false;
447 };
452 };
448
453
449 this.nextComment = function(node) {
454 this.nextComment = function(node) {
450 return self.scrollToComment(node, 1);
455 return self.scrollToComment(node, 1);
451 };
456 };
452
457
453 this.prevComment = function(node) {
458 this.prevComment = function(node) {
454 return self.scrollToComment(node, -1);
459 return self.scrollToComment(node, -1);
455 };
460 };
456
461
457 this.nextOutdatedComment = function(node) {
462 this.nextOutdatedComment = function(node) {
458 return self.scrollToComment(node, 1, true);
463 return self.scrollToComment(node, 1, true);
459 };
464 };
460
465
461 this.prevOutdatedComment = function(node) {
466 this.prevOutdatedComment = function(node) {
462 return self.scrollToComment(node, -1, true);
467 return self.scrollToComment(node, -1, true);
463 };
468 };
464
469
465 this.deleteComment = function(node) {
470 this.deleteComment = function(node) {
466 if (!confirm(_gettext('Delete this comment?'))) {
471 if (!confirm(_gettext('Delete this comment?'))) {
467 return false;
472 return false;
468 }
473 }
469 var $node = $(node);
474 var $node = $(node);
470 var $td = $node.closest('td');
475 var $td = $node.closest('td');
471 var $comment = $node.closest('.comment');
476 var $comment = $node.closest('.comment');
472 var comment_id = $comment.attr('data-comment-id');
477 var comment_id = $comment.attr('data-comment-id');
473 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
478 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
474 var postData = {
479 var postData = {
475 '_method': 'delete',
480 '_method': 'delete',
476 'csrf_token': CSRF_TOKEN
481 'csrf_token': CSRF_TOKEN
477 };
482 };
478
483
479 $comment.addClass('comment-deleting');
484 $comment.addClass('comment-deleting');
480 $comment.hide('fast');
485 $comment.hide('fast');
481
486
482 var success = function(response) {
487 var success = function(response) {
483 $comment.remove();
488 $comment.remove();
484 return false;
489 return false;
485 };
490 };
486 var failure = function(data, textStatus, xhr) {
491 var failure = function(data, textStatus, xhr) {
487 alert("error processing request: " + textStatus);
492 alert("error processing request: " + textStatus);
488 $comment.show('fast');
493 $comment.show('fast');
489 $comment.removeClass('comment-deleting');
494 $comment.removeClass('comment-deleting');
490 return false;
495 return false;
491 };
496 };
492 ajaxPOST(url, postData, success, failure);
497 ajaxPOST(url, postData, success, failure);
493 };
498 };
494
499
495 this.toggleWideMode = function (node) {
500 this.toggleWideMode = function (node) {
496 if ($('#content').hasClass('wrapper')) {
501 if ($('#content').hasClass('wrapper')) {
497 $('#content').removeClass("wrapper");
502 $('#content').removeClass("wrapper");
498 $('#content').addClass("wide-mode-wrapper");
503 $('#content').addClass("wide-mode-wrapper");
499 $(node).addClass('btn-success');
504 $(node).addClass('btn-success');
500 } else {
505 } else {
501 $('#content').removeClass("wide-mode-wrapper");
506 $('#content').removeClass("wide-mode-wrapper");
502 $('#content').addClass("wrapper");
507 $('#content').addClass("wrapper");
503 $(node).removeClass('btn-success');
508 $(node).removeClass('btn-success');
504 }
509 }
505 return false;
510 return false;
506 };
511 };
507
512
508 this.toggleComments = function(node, show) {
513 this.toggleComments = function(node, show) {
509 var $filediff = $(node).closest('.filediff');
514 var $filediff = $(node).closest('.filediff');
510 if (show === true) {
515 if (show === true) {
511 $filediff.removeClass('hide-comments');
516 $filediff.removeClass('hide-comments');
512 } else if (show === false) {
517 } else if (show === false) {
513 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
518 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
514 $filediff.addClass('hide-comments');
519 $filediff.addClass('hide-comments');
515 } else {
520 } else {
516 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
521 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
517 $filediff.toggleClass('hide-comments');
522 $filediff.toggleClass('hide-comments');
518 }
523 }
519 return false;
524 return false;
520 };
525 };
521
526
522 this.toggleLineComments = function(node) {
527 this.toggleLineComments = function(node) {
523 self.toggleComments(node, true);
528 self.toggleComments(node, true);
524 var $node = $(node);
529 var $node = $(node);
525 $node.closest('tr').toggleClass('hide-line-comments');
530 $node.closest('tr').toggleClass('hide-line-comments');
526 };
531 };
527
532
528 this.createComment = function(node) {
533 this.createComment = function(node) {
529 var $node = $(node);
534 var $node = $(node);
530 var $td = $node.closest('td');
535 var $td = $node.closest('td');
531 var $form = $td.find('.comment-inline-form');
536 var $form = $td.find('.comment-inline-form');
532
537
533 if (!$form.length) {
538 if (!$form.length) {
534 var tmpl = $('#cb-comment-inline-form-template').html();
539 var tmpl = $('#cb-comment-inline-form-template').html();
535 var $filediff = $node.closest('.filediff');
540 var $filediff = $node.closest('.filediff');
536 $filediff.removeClass('hide-comments');
541 $filediff.removeClass('hide-comments');
537 var f_path = $filediff.attr('data-f-path');
542 var f_path = $filediff.attr('data-f-path');
538 var lineno = self.getLineNumber(node);
543 var lineno = self.getLineNumber(node);
544
539 tmpl = tmpl.format(f_path, lineno);
545 tmpl = tmpl.format(f_path, lineno);
540 $form = $(tmpl);
546 $form = $(tmpl);
541
547
542 var $comments = $td.find('.inline-comments');
548 var $comments = $td.find('.inline-comments');
543 if (!$comments.length) {
549 if (!$comments.length) {
544 $comments = $(
550 $comments = $(
545 $('#cb-comments-inline-container-template').html());
551 $('#cb-comments-inline-container-template').html());
546 $td.append($comments);
552 $td.append($comments);
547 }
553 }
548
554
549 $td.find('.cb-comment-add-button').before($form);
555 $td.find('.cb-comment-add-button').before($form);
550
556
551 var pullRequestId = templateContext.pull_request_data.pull_request_id;
557 var pullRequestId = templateContext.pull_request_data.pull_request_id;
552 var commitId = templateContext.commit_data.commit_id;
558 var commitId = templateContext.commit_data.commit_id;
553 var _form = $form[0];
559 var _form = $form[0];
554 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
560 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
555 var cm = commentForm.getCmInstance();
561 var cm = commentForm.getCmInstance();
556
562
557 // set a CUSTOM submit handler for inline comments.
563 // set a CUSTOM submit handler for inline comments.
558 commentForm.setHandleFormSubmit(function(o) {
564 commentForm.setHandleFormSubmit(function(o) {
559 var text = commentForm.cm.getValue();
565 var text = commentForm.cm.getValue();
566 var commentType = commentForm.getCommentType();
560
567
561 if (text === "") {
568 if (text === "") {
562 return;
569 return;
563 }
570 }
564
571
565 if (lineno === undefined) {
572 if (lineno === undefined) {
566 alert('missing line !');
573 alert('missing line !');
567 return;
574 return;
568 }
575 }
569 if (f_path === undefined) {
576 if (f_path === undefined) {
570 alert('missing file path !');
577 alert('missing file path !');
571 return;
578 return;
572 }
579 }
573
580
574 var excludeCancelBtn = false;
581 var excludeCancelBtn = false;
575 var submitEvent = true;
582 var submitEvent = true;
576 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
583 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
577 commentForm.cm.setOption("readOnly", true);
584 commentForm.cm.setOption("readOnly", true);
578 var postData = {
585 var postData = {
579 'text': text,
586 'text': text,
580 'f_path': f_path,
587 'f_path': f_path,
581 'line': lineno,
588 'line': lineno,
589 'comment_type': commentType,
582 'csrf_token': CSRF_TOKEN
590 'csrf_token': CSRF_TOKEN
583 };
591 };
584 var submitSuccessCallback = function(json_data) {
592 var submitSuccessCallback = function(json_data) {
585 $form.remove();
593 $form.remove();
586 try {
594 try {
587 var html = json_data.rendered_text;
595 var html = json_data.rendered_text;
588 var lineno = json_data.line_no;
596 var lineno = json_data.line_no;
589 var target_id = json_data.target_id;
597 var target_id = json_data.target_id;
590
598
591 $comments.find('.cb-comment-add-button').before(html);
599 $comments.find('.cb-comment-add-button').before(html);
592
600
593 } catch (e) {
601 } catch (e) {
594 console.error(e);
602 console.error(e);
595 }
603 }
596
604
597 // re trigger the linkification of next/prev navigation
605 // re trigger the linkification of next/prev navigation
598 linkifyComments($('.inline-comment-injected'));
606 linkifyComments($('.inline-comment-injected'));
599 timeagoActivate();
607 timeagoActivate();
600 bindDeleteCommentButtons();
608 bindDeleteCommentButtons();
601 commentForm.setActionButtonsDisabled(false);
609 commentForm.setActionButtonsDisabled(false);
602
610
603 };
611 };
604 var submitFailCallback = function(){
612 var submitFailCallback = function(){
605 commentForm.resetCommentFormState(text)
613 commentForm.resetCommentFormState(text)
606 };
614 };
607 commentForm.submitAjaxPOST(
615 commentForm.submitAjaxPOST(
608 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
616 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
609 });
617 });
610
618
611 setTimeout(function() {
619 setTimeout(function() {
612 // callbacks
620 // callbacks
613 if (cm !== undefined) {
621 if (cm !== undefined) {
614 cm.setOption('placeholder', _gettext('Leave a comment on line {0}.').format(lineno));
622 cm.setOption('placeholder', _gettext('Leave a comment on line {0}.').format(lineno));
615 cm.focus();
623 cm.focus();
616 cm.refresh();
624 cm.refresh();
617 }
625 }
618 }, 10);
626 }, 10);
619
627
620 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
628 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
621 form: _form,
629 form: _form,
622 parent: $td[0],
630 parent: $td[0],
623 lineno: lineno,
631 lineno: lineno,
624 f_path: f_path}
632 f_path: f_path}
625 );
633 );
626 }
634 }
627
635
628 $form.addClass('comment-inline-form-open');
636 $form.addClass('comment-inline-form-open');
629 };
637 };
630
638
631 this.renderInlineComments = function(file_comments) {
639 this.renderInlineComments = function(file_comments) {
632 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
640 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
633
641
634 for (var i = 0; i < file_comments.length; i++) {
642 for (var i = 0; i < file_comments.length; i++) {
635 var box = file_comments[i];
643 var box = file_comments[i];
636
644
637 var target_id = $(box).attr('target_id');
645 var target_id = $(box).attr('target_id');
638
646
639 // actually comments with line numbers
647 // actually comments with line numbers
640 var comments = box.children;
648 var comments = box.children;
641
649
642 for (var j = 0; j < comments.length; j++) {
650 for (var j = 0; j < comments.length; j++) {
643 var data = {
651 var data = {
644 'rendered_text': comments[j].outerHTML,
652 'rendered_text': comments[j].outerHTML,
645 'line_no': $(comments[j]).attr('line'),
653 'line_no': $(comments[j]).attr('line'),
646 'target_id': target_id
654 'target_id': target_id
647 };
655 };
648 }
656 }
649 }
657 }
650
658
651 // since order of injection is random, we're now re-iterating
659 // since order of injection is random, we're now re-iterating
652 // from correct order and filling in links
660 // from correct order and filling in links
653 linkifyComments($('.inline-comment-injected'));
661 linkifyComments($('.inline-comment-injected'));
654 bindDeleteCommentButtons();
662 bindDeleteCommentButtons();
655 firefoxAnchorFix();
663 firefoxAnchorFix();
656 };
664 };
657
665
658 };
666 };
@@ -1,299 +1,310 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 <%namespace name="base" file="/base/base.mako"/>
7
7
8 <%def name="comment_block(comment, inline=False)">
8 <%def name="comment_block(comment, inline=False)">
9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
10 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
11
11
12 <div class="comment
12 <div class="comment
13 ${'comment-inline' if inline else 'comment-general'}
13 ${'comment-inline' if inline else 'comment-general'}
14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
15 id="comment-${comment.comment_id}"
15 id="comment-${comment.comment_id}"
16 line="${comment.line_no}"
16 line="${comment.line_no}"
17 data-comment-id="${comment.comment_id}"
17 data-comment-id="${comment.comment_id}"
18 style="${'display: none;' if outdated_at_ver else ''}">
18 style="${'display: none;' if outdated_at_ver else ''}">
19
19
20 <div class="meta">
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 <div class="author ${'author-inline' if inline else 'author-general'}">
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 </div>
29 </div>
24 <div class="date">
30 <div class="date">
25 ${h.age_component(comment.modified_at, time_is_local=True)}
31 ${h.age_component(comment.modified_at, time_is_local=True)}
26 </div>
32 </div>
27 % if inline:
33 % if inline:
28 <span></span>
34 <span></span>
29 % else:
35 % else:
30 <div class="status-change">
36 <div class="status-change">
31 % if comment.pull_request:
37 % if comment.pull_request:
32 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
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 % if comment.status_change:
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 % else:
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 % endif
43 % endif
38 </a>
44 </a>
39 % else:
45 % else:
40 % if comment.status_change:
46 % if comment.status_change:
41 ${_('Status change on commit')}:
47 ${_('Status change on commit')}:
42 % else:
43 ${_('Comment on commit')}
44 % endif
48 % endif
45 % endif
49 % endif
46 </div>
50 </div>
47 % endif
51 % endif
48
52
49 % if comment.status_change:
53 % if comment.status_change:
50 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
54 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
51 <div title="${_('Commit status')}" class="changeset-status-lbl">
55 <div title="${_('Commit status')}" class="changeset-status-lbl">
52 ${comment.status_change[0].status_lbl}
56 ${comment.status_change[0].status_lbl}
53 </div>
57 </div>
54 % endif
58 % endif
55
59
56 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
60 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
57
61
58 <div class="comment-links-block">
62 <div class="comment-links-block">
59
63
60 % if inline:
64 % if inline:
61 % if outdated_at_ver:
65 % if outdated_at_ver:
62 <div class="pr-version-inline">
66 <div class="pr-version-inline">
63 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
67 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
64 <code class="pr-version-num">
68 <code class="pr-version-num">
65 outdated ${'v{}'.format(pr_index_ver)}
69 outdated ${'v{}'.format(pr_index_ver)}
66 </code>
70 </code>
67 </a>
71 </a>
68 </div>
72 </div>
69 |
73 |
70 % endif
74 % endif
71 % else:
75 % else:
72 % if comment.pull_request_version_id and pr_index_ver:
76 % if comment.pull_request_version_id and pr_index_ver:
73 |
77 |
74 <div class="pr-version">
78 <div class="pr-version">
75 % if comment.outdated:
79 % if comment.outdated:
76 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
80 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
77 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
81 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
78 </a>
82 </a>
79 % else:
83 % else:
80 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
84 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
81 <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)}">
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 <code class="pr-version-num">
86 <code class="pr-version-num">
83 ${'v{}'.format(pr_index_ver)}
87 ${'v{}'.format(pr_index_ver)}
84 </code>
88 </code>
85 </a>
89 </a>
86 </div>
90 </div>
87 % endif
91 % endif
88 </div>
92 </div>
89 % endif
93 % endif
90 % endif
94 % endif
91
95
92 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
96 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
93 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
97 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
94 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
98 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
95 ## permissions to delete
99 ## permissions to delete
96 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
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 ## TODO: dan: add edit comment here
101 ## TODO: dan: add edit comment here
98 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
102 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
99 %else:
103 %else:
100 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
104 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
101 %endif
105 %endif
102 %else:
106 %else:
103 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
107 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
104 %endif
108 %endif
105
109
106 %if not outdated_at_ver:
110 %if not outdated_at_ver:
107 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
111 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
108 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
112 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
109 %endif
113 %endif
110
114
111 </div>
115 </div>
112 </div>
116 </div>
113 <div class="text">
117 <div class="text">
114 ${comment.render(mentions=True)|n}
118 ${comment.render(mentions=True)|n}
115 </div>
119 </div>
116
120
117 </div>
121 </div>
118 </%def>
122 </%def>
119 ## generate main comments
123 ## generate main comments
120 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
124 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
121 <div id="comments">
125 <div id="comments">
122 %for comment in c.comments:
126 %for comment in c.comments:
123 <div id="comment-tr-${comment.comment_id}">
127 <div id="comment-tr-${comment.comment_id}">
124 ## only render comments that are not from pull request, or from
128 ## only render comments that are not from pull request, or from
125 ## pull request and a status change
129 ## pull request and a status change
126 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
130 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
127 ${comment_block(comment)}
131 ${comment_block(comment)}
128 %endif
132 %endif
129 </div>
133 </div>
130 %endfor
134 %endfor
131 ## to anchor ajax comments
135 ## to anchor ajax comments
132 <div id="injected_page_comments"></div>
136 <div id="injected_page_comments"></div>
133 </div>
137 </div>
134 </%def>
138 </%def>
135
139
136 ## MAIN COMMENT FORM
140 ## MAIN COMMENT FORM
137 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
141 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
138
142
139 %if is_compare:
143 %if is_compare:
140 <% form_id = "comments_form_compare" %>
144 <% form_id = "comments_form_compare" %>
141 %else:
145 %else:
142 <% form_id = "comments_form" %>
146 <% form_id = "comments_form" %>
143 %endif
147 %endif
144
148
145
149
146 %if is_pull_request:
150 %if is_pull_request:
147 <div class="pull-request-merge">
151 <div class="pull-request-merge">
148 %if c.allowed_to_merge:
152 %if c.allowed_to_merge:
149 <div class="pull-request-wrap">
153 <div class="pull-request-wrap">
150 <div class="pull-right">
154 <div class="pull-right">
151 ${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')}
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 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
156 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
153 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
157 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
154 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
158 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
155 ${h.end_form()}
159 ${h.end_form()}
156 </div>
160 </div>
157 </div>
161 </div>
158 %else:
162 %else:
159 <div class="pull-request-wrap">
163 <div class="pull-request-wrap">
160 <div class="pull-right">
164 <div class="pull-right">
161 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
165 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
162 </div>
166 </div>
163 </div>
167 </div>
164 %endif
168 %endif
165 </div>
169 </div>
166 %endif
170 %endif
167 <div class="comments">
171 <div class="comments">
168 <%
172 <%
169 if is_pull_request:
173 if is_pull_request:
170 placeholder = _('Leave a comment on this Pull Request.')
174 placeholder = _('Leave a comment on this Pull Request.')
171 elif is_compare:
175 elif is_compare:
172 placeholder = _('Leave a comment on all commits in this range.')
176 placeholder = _('Leave a comment on all commits in this range.')
173 else:
177 else:
174 placeholder = _('Leave a comment on this Commit.')
178 placeholder = _('Leave a comment on this Commit.')
175 %>
179 %>
176 % if c.rhodecode_user.username != h.DEFAULT_USER:
180 % if c.rhodecode_user.username != h.DEFAULT_USER:
177 <div class="comment-form ac">
181 <div class="comment-form ac">
178 ${h.secure_form(post_url, id_=form_id)}
182 ${h.secure_form(post_url, id_=form_id)}
179 <div class="comment-area">
183 <div class="comment-area">
180 <div class="comment-area-header">
184 <div class="comment-area-header">
181 <ul class="nav-links clearfix">
185 <ul class="nav-links clearfix">
182 <li class="active">
186 <li class="active">
183 <a href="#edit-btn" tabindex="-1" id="edit-btn">${_('Write')}</a>
187 <a href="#edit-btn" tabindex="-1" id="edit-btn">${_('Write')}</a>
184 </li>
188 </li>
185 <li class="">
189 <li class="">
186 <a href="#preview-btn" tabindex="-1" id="preview-btn">${_('Preview')}</a>
190 <a href="#preview-btn" tabindex="-1" id="preview-btn">${_('Preview')}</a>
187 </li>
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 </ul>
199 </ul>
189 </div>
200 </div>
190
201
191 <div class="comment-area-write" style="display: block;">
202 <div class="comment-area-write" style="display: block;">
192 <div id="edit-container">
203 <div id="edit-container">
193 <textarea id="text" name="text" class="comment-block-ta ac-input"></textarea>
204 <textarea id="text" name="text" class="comment-block-ta ac-input"></textarea>
194 </div>
205 </div>
195 <div id="preview-container" class="clearfix" style="display: none;">
206 <div id="preview-container" class="clearfix" style="display: none;">
196 <div id="preview-box" class="preview-box"></div>
207 <div id="preview-box" class="preview-box"></div>
197 </div>
208 </div>
198 </div>
209 </div>
199
210
200 <div class="comment-area-footer">
211 <div class="comment-area-footer">
201 <div class="toolbar">
212 <div class="toolbar">
202 <div class="toolbar-text">
213 <div class="toolbar-text">
203 ${(_('Comments parsed using %s syntax with %s support.') % (
214 ${(_('Comments parsed using %s syntax with %s support.') % (
204 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
215 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
205 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
216 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
206 )
217 )
207 )|n}
218 )|n}
208 </div>
219 </div>
209 </div>
220 </div>
210 </div>
221 </div>
211 </div>
222 </div>
212
223
213 <div id="comment_form_extras">
224 <div id="comment_form_extras">
214 %if form_extras and isinstance(form_extras, (list, tuple)):
225 %if form_extras and isinstance(form_extras, (list, tuple)):
215 % for form_ex_el in form_extras:
226 % for form_ex_el in form_extras:
216 ${form_ex_el|n}
227 ${form_ex_el|n}
217 % endfor
228 % endfor
218 %endif
229 %endif
219 </div>
230 </div>
220 <div class="comment-footer">
231 <div class="comment-footer">
221 %if change_status:
232 %if change_status:
222 <div class="status_box">
233 <div class="status_box">
223 <select id="change_status" name="changeset_status">
234 <select id="change_status" name="changeset_status">
224 <option></option> # Placeholder
235 <option></option> # Placeholder
225 %for status,lbl in c.commit_statuses:
236 %for status,lbl in c.commit_statuses:
226 <option value="${status}" data-status="${status}">${lbl}</option>
237 <option value="${status}" data-status="${status}">${lbl}</option>
227 %if is_pull_request and change_status and status in ('approved', 'rejected'):
238 %if is_pull_request and change_status and status in ('approved', 'rejected'):
228 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
239 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
229 %endif
240 %endif
230 %endfor
241 %endfor
231 </select>
242 </select>
232 </div>
243 </div>
233 %endif
244 %endif
234 <div class="action-buttons">
245 <div class="action-buttons">
235 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
246 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
236 </div>
247 </div>
237 </div>
248 </div>
238 ${h.end_form()}
249 ${h.end_form()}
239 </div>
250 </div>
240 % else:
251 % else:
241 <div class="comment-form ac">
252 <div class="comment-form ac">
242
253
243 <div class="comment-area">
254 <div class="comment-area">
244 <div class="comment-area-header">
255 <div class="comment-area-header">
245 <ul class="nav-links clearfix">
256 <ul class="nav-links clearfix">
246 <li class="active">
257 <li class="active">
247 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
258 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
248 </li>
259 </li>
249 <li class="">
260 <li class="">
250 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
261 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
251 </li>
262 </li>
252 </ul>
263 </ul>
253 </div>
264 </div>
254
265
255 <div class="comment-area-write" style="display: block;">
266 <div class="comment-area-write" style="display: block;">
256 <div id="edit-container">
267 <div id="edit-container">
257 <div style="padding: 40px 0">
268 <div style="padding: 40px 0">
258 ${_('You need to be logged in to leave comments.')}
269 ${_('You need to be logged in to leave comments.')}
259 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
270 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
260 </div>
271 </div>
261 </div>
272 </div>
262 <div id="preview-container" class="clearfix" style="display: none;">
273 <div id="preview-container" class="clearfix" style="display: none;">
263 <div id="preview-box" class="preview-box"></div>
274 <div id="preview-box" class="preview-box"></div>
264 </div>
275 </div>
265 </div>
276 </div>
266
277
267 <div class="comment-area-footer">
278 <div class="comment-area-footer">
268 <div class="toolbar">
279 <div class="toolbar">
269 <div class="toolbar-text">
280 <div class="toolbar-text">
270 </div>
281 </div>
271 </div>
282 </div>
272 </div>
283 </div>
273 </div>
284 </div>
274
285
275 <div class="comment-footer">
286 <div class="comment-footer">
276 </div>
287 </div>
277
288
278 </div>
289 </div>
279 % endif
290 % endif
280
291
281 </div>
292 </div>
282
293
283 <script>
294 <script>
284 // init active elements of commentForm
295 // init active elements of commentForm
285 var commitId = templateContext.commit_data.commit_id;
296 var commitId = templateContext.commit_data.commit_id;
286 var pullRequestId = templateContext.pull_request_data.pull_request_id;
297 var pullRequestId = templateContext.pull_request_data.pull_request_id;
287 var lineNo;
298 var lineNo;
288
299
289 var mainCommentForm = new CommentForm(
300 var mainCommentForm = new CommentForm(
290 "#${form_id}", commitId, pullRequestId, lineNo, true);
301 "#${form_id}", commitId, pullRequestId, lineNo, true);
291
302
292 if (mainCommentForm.cm){
303 if (mainCommentForm.cm){
293 mainCommentForm.cm.setOption('placeholder', "${placeholder}");
304 mainCommentForm.cm.setOption('placeholder', "${placeholder}");
294 }
305 }
295
306
296 mainCommentForm.initStatusChangeSelector();
307 mainCommentForm.initStatusChangeSelector();
297 bindToggleButtons();
308 bindToggleButtons();
298 </script>
309 </script>
299 </%def>
310 </%def>
@@ -1,711 +1,718 b''
1 <%def name="diff_line_anchor(filename, line, type)"><%
1 <%def name="diff_line_anchor(filename, line, type)"><%
2 return '%s_%s_%i' % (h.safeid(filename), type, line)
2 return '%s_%s_%i' % (h.safeid(filename), type, line)
3 %></%def>
3 %></%def>
4
4
5 <%def name="action_class(action)">
5 <%def name="action_class(action)">
6 <%
6 <%
7 return {
7 return {
8 '-': 'cb-deletion',
8 '-': 'cb-deletion',
9 '+': 'cb-addition',
9 '+': 'cb-addition',
10 ' ': 'cb-context',
10 ' ': 'cb-context',
11 }.get(action, 'cb-empty')
11 }.get(action, 'cb-empty')
12 %>
12 %>
13 </%def>
13 </%def>
14
14
15 <%def name="op_class(op_id)">
15 <%def name="op_class(op_id)">
16 <%
16 <%
17 return {
17 return {
18 DEL_FILENODE: 'deletion', # file deleted
18 DEL_FILENODE: 'deletion', # file deleted
19 BIN_FILENODE: 'warning' # binary diff hidden
19 BIN_FILENODE: 'warning' # binary diff hidden
20 }.get(op_id, 'addition')
20 }.get(op_id, 'addition')
21 %>
21 %>
22 </%def>
22 </%def>
23
23
24 <%def name="link_for(**kw)">
24 <%def name="link_for(**kw)">
25 <%
25 <%
26 new_args = request.GET.mixed()
26 new_args = request.GET.mixed()
27 new_args.update(kw)
27 new_args.update(kw)
28 return h.url('', **new_args)
28 return h.url('', **new_args)
29 %>
29 %>
30 </%def>
30 </%def>
31
31
32 <%def name="render_diffset(diffset, commit=None,
32 <%def name="render_diffset(diffset, commit=None,
33
33
34 # collapse all file diff entries when there are more than this amount of files in the diff
34 # collapse all file diff entries when there are more than this amount of files in the diff
35 collapse_when_files_over=20,
35 collapse_when_files_over=20,
36
36
37 # collapse lines in the diff when more than this amount of lines changed in the file diff
37 # collapse lines in the diff when more than this amount of lines changed in the file diff
38 lines_changed_limit=500,
38 lines_changed_limit=500,
39
39
40 # add a ruler at to the output
40 # add a ruler at to the output
41 ruler_at_chars=0,
41 ruler_at_chars=0,
42
42
43 # show inline comments
43 # show inline comments
44 use_comments=False,
44 use_comments=False,
45
45
46 # disable new comments
46 # disable new comments
47 disable_new_comments=False,
47 disable_new_comments=False,
48
48
49 # special file-comments that were deleted in previous versions
49 # special file-comments that were deleted in previous versions
50 # it's used for showing outdated comments for deleted files in a PR
50 # it's used for showing outdated comments for deleted files in a PR
51 deleted_files_comments=None
51 deleted_files_comments=None
52
52
53 )">
53 )">
54
54
55 %if use_comments:
55 %if use_comments:
56 <div id="cb-comments-inline-container-template" class="js-template">
56 <div id="cb-comments-inline-container-template" class="js-template">
57 ${inline_comments_container([])}
57 ${inline_comments_container([])}
58 </div>
58 </div>
59 <div class="js-template" id="cb-comment-inline-form-template">
59 <div class="js-template" id="cb-comment-inline-form-template">
60 <div class="comment-inline-form ac">
60 <div class="comment-inline-form ac">
61
61
62 %if c.rhodecode_user.username != h.DEFAULT_USER:
62 %if c.rhodecode_user.username != h.DEFAULT_USER:
63 ${h.form('#', method='get')}
63 ${h.form('#', method='get')}
64 <div class="comment-area">
64 <div class="comment-area">
65 <div class="comment-area-header">
65 <div class="comment-area-header">
66 <ul class="nav-links clearfix">
66 <ul class="nav-links clearfix">
67 <li class="active">
67 <li class="active">
68 <a href="#edit-btn" tabindex="-1" id="edit-btn_{1}">${_('Write')}</a>
68 <a href="#edit-btn" tabindex="-1" id="edit-btn_{1}">${_('Write')}</a>
69 </li>
69 </li>
70 <li class="">
70 <li class="">
71 <a href="#preview-btn" tabindex="-1" id="preview-btn_{1}">${_('Preview')}</a>
71 <a href="#preview-btn" tabindex="-1" id="preview-btn_{1}">${_('Preview')}</a>
72 </li>
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 </ul>
80 </ul>
74 </div>
81 </div>
75
82
76 <div class="comment-area-write" style="display: block;">
83 <div class="comment-area-write" style="display: block;">
77 <div id="edit-container_{1}">
84 <div id="edit-container_{1}">
78 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
85 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
79 </div>
86 </div>
80 <div id="preview-container_{1}" class="clearfix" style="display: none;">
87 <div id="preview-container_{1}" class="clearfix" style="display: none;">
81 <div id="preview-box_{1}" class="preview-box"></div>
88 <div id="preview-box_{1}" class="preview-box"></div>
82 </div>
89 </div>
83 </div>
90 </div>
84
91
85 <div class="comment-area-footer">
92 <div class="comment-area-footer">
86 <div class="toolbar">
93 <div class="toolbar">
87 <div class="toolbar-text">
94 <div class="toolbar-text">
88 ${(_('Comments parsed using %s syntax with %s support.') % (
95 ${(_('Comments parsed using %s syntax with %s support.') % (
89 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
96 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
90 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
97 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
91 )
98 )
92 )|n}
99 )|n}
93 </div>
100 </div>
94 </div>
101 </div>
95 </div>
102 </div>
96 </div>
103 </div>
97
104
98 <div class="comment-footer">
105 <div class="comment-footer">
99 <div class="action-buttons">
106 <div class="action-buttons">
100 <input type="hidden" name="f_path" value="{0}">
107 <input type="hidden" name="f_path" value="{0}">
101 <input type="hidden" name="line" value="{1}">
108 <input type="hidden" name="line" value="{1}">
102 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
109 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
103 ${_('Cancel')}
110 ${_('Cancel')}
104 </button>
111 </button>
105 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
112 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
106 </div>
113 </div>
107 ${h.end_form()}
114 ${h.end_form()}
108 </div>
115 </div>
109 %else:
116 %else:
110 ${h.form('', class_='inline-form comment-form-login', method='get')}
117 ${h.form('', class_='inline-form comment-form-login', method='get')}
111 <div class="pull-left">
118 <div class="pull-left">
112 <div class="comment-help pull-right">
119 <div class="comment-help pull-right">
113 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
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 </div>
121 </div>
115 </div>
122 </div>
116 <div class="comment-button pull-right">
123 <div class="comment-button pull-right">
117 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
124 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
118 ${_('Cancel')}
125 ${_('Cancel')}
119 </button>
126 </button>
120 </div>
127 </div>
121 <div class="clearfix"></div>
128 <div class="clearfix"></div>
122 ${h.end_form()}
129 ${h.end_form()}
123 %endif
130 %endif
124 </div>
131 </div>
125 </div>
132 </div>
126
133
127 %endif
134 %endif
128 <%
135 <%
129 collapse_all = len(diffset.files) > collapse_when_files_over
136 collapse_all = len(diffset.files) > collapse_when_files_over
130 %>
137 %>
131
138
132 %if c.diffmode == 'sideside':
139 %if c.diffmode == 'sideside':
133 <style>
140 <style>
134 .wrapper {
141 .wrapper {
135 max-width: 1600px !important;
142 max-width: 1600px !important;
136 }
143 }
137 </style>
144 </style>
138 %endif
145 %endif
139
146
140 %if ruler_at_chars:
147 %if ruler_at_chars:
141 <style>
148 <style>
142 .diff table.cb .cb-content:after {
149 .diff table.cb .cb-content:after {
143 content: "";
150 content: "";
144 border-left: 1px solid blue;
151 border-left: 1px solid blue;
145 position: absolute;
152 position: absolute;
146 top: 0;
153 top: 0;
147 height: 18px;
154 height: 18px;
148 opacity: .2;
155 opacity: .2;
149 z-index: 10;
156 z-index: 10;
150 //## +5 to account for diff action (+/-)
157 //## +5 to account for diff action (+/-)
151 left: ${ruler_at_chars + 5}ch;
158 left: ${ruler_at_chars + 5}ch;
152 </style>
159 </style>
153 %endif
160 %endif
154
161
155 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
162 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
156 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
163 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
157 %if commit:
164 %if commit:
158 <div class="pull-right">
165 <div class="pull-right">
159 <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='')}">
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 ${_('Browse Files')}
167 ${_('Browse Files')}
161 </a>
168 </a>
162 </div>
169 </div>
163 %endif
170 %endif
164 <h2 class="clearinner">
171 <h2 class="clearinner">
165 %if commit:
172 %if commit:
166 <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> -
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 ${h.age_component(commit.date)} -
174 ${h.age_component(commit.date)} -
168 %endif
175 %endif
169 %if diffset.limited_diff:
176 %if diffset.limited_diff:
170 ${_('The requested commit is too big and content was truncated.')}
177 ${_('The requested commit is too big and content was truncated.')}
171
178
172 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
179 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
173 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
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 %else:
181 %else:
175 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
182 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
176 '%(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}}
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 %endif
184 %endif
178
185
179 <% at_ver = getattr(c, 'at_version_pos', None) %>
186 <% at_ver = getattr(c, 'at_version_pos', None) %>
180 % if at_ver:
187 % if at_ver:
181 <div class="pull-right">
188 <div class="pull-right">
182 ${_('Showing changes at version %d') % at_ver}
189 ${_('Showing changes at version %d') % at_ver}
183 </div>
190 </div>
184 % endif
191 % endif
185
192
186 </h2>
193 </h2>
187 </div>
194 </div>
188
195
189 %if not diffset.files:
196 %if not diffset.files:
190 <p class="empty_data">${_('No files')}</p>
197 <p class="empty_data">${_('No files')}</p>
191 %endif
198 %endif
192
199
193 <div class="filediffs">
200 <div class="filediffs">
194 %for i, filediff in enumerate(diffset.files):
201 %for i, filediff in enumerate(diffset.files):
195
202
196 <%
203 <%
197 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
204 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
198 over_lines_changed_limit = lines_changed > lines_changed_limit
205 over_lines_changed_limit = lines_changed > lines_changed_limit
199 %>
206 %>
200 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
207 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
201 <div
208 <div
202 class="filediff"
209 class="filediff"
203 data-f-path="${filediff['patch']['filename']}"
210 data-f-path="${filediff['patch']['filename']}"
204 id="a_${h.FID('', filediff['patch']['filename'])}">
211 id="a_${h.FID('', filediff['patch']['filename'])}">
205 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
212 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
206 <div class="filediff-collapse-indicator"></div>
213 <div class="filediff-collapse-indicator"></div>
207 ${diff_ops(filediff)}
214 ${diff_ops(filediff)}
208 </label>
215 </label>
209 ${diff_menu(filediff, use_comments=use_comments)}
216 ${diff_menu(filediff, use_comments=use_comments)}
210 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
217 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
211 %if not filediff.hunks:
218 %if not filediff.hunks:
212 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
219 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
213 <tr>
220 <tr>
214 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
221 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
215 %if op_id == DEL_FILENODE:
222 %if op_id == DEL_FILENODE:
216 ${_('File was deleted')}
223 ${_('File was deleted')}
217 %elif op_id == BIN_FILENODE:
224 %elif op_id == BIN_FILENODE:
218 ${_('Binary file hidden')}
225 ${_('Binary file hidden')}
219 %else:
226 %else:
220 ${op_text}
227 ${op_text}
221 %endif
228 %endif
222 </td>
229 </td>
223 </tr>
230 </tr>
224 %endfor
231 %endfor
225 %endif
232 %endif
226 %if filediff.patch['is_limited_diff']:
233 %if filediff.patch['is_limited_diff']:
227 <tr class="cb-warning cb-collapser">
234 <tr class="cb-warning cb-collapser">
228 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
235 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
229 ${_('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>
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 </td>
237 </td>
231 </tr>
238 </tr>
232 %else:
239 %else:
233 %if over_lines_changed_limit:
240 %if over_lines_changed_limit:
234 <tr class="cb-warning cb-collapser">
241 <tr class="cb-warning cb-collapser">
235 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
242 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
236 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
243 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
237 <a href="#" class="cb-expand"
244 <a href="#" class="cb-expand"
238 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
245 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
239 </a>
246 </a>
240 <a href="#" class="cb-collapse"
247 <a href="#" class="cb-collapse"
241 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
248 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
242 </a>
249 </a>
243 </td>
250 </td>
244 </tr>
251 </tr>
245 %endif
252 %endif
246 %endif
253 %endif
247
254
248 %for hunk in filediff.hunks:
255 %for hunk in filediff.hunks:
249 <tr class="cb-hunk">
256 <tr class="cb-hunk">
250 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
257 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
251 ## TODO: dan: add ajax loading of more context here
258 ## TODO: dan: add ajax loading of more context here
252 ## <a href="#">
259 ## <a href="#">
253 <i class="icon-more"></i>
260 <i class="icon-more"></i>
254 ## </a>
261 ## </a>
255 </td>
262 </td>
256 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
263 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
257 @@
264 @@
258 -${hunk.source_start},${hunk.source_length}
265 -${hunk.source_start},${hunk.source_length}
259 +${hunk.target_start},${hunk.target_length}
266 +${hunk.target_start},${hunk.target_length}
260 ${hunk.section_header}
267 ${hunk.section_header}
261 </td>
268 </td>
262 </tr>
269 </tr>
263 %if c.diffmode == 'unified':
270 %if c.diffmode == 'unified':
264 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
271 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
265 %elif c.diffmode == 'sideside':
272 %elif c.diffmode == 'sideside':
266 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
273 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
267 %else:
274 %else:
268 <tr class="cb-line">
275 <tr class="cb-line">
269 <td>unknown diff mode</td>
276 <td>unknown diff mode</td>
270 </tr>
277 </tr>
271 %endif
278 %endif
272 %endfor
279 %endfor
273
280
274 ## outdated comments that do not fit into currently displayed lines
281 ## outdated comments that do not fit into currently displayed lines
275 % for lineno, comments in filediff.left_comments.items():
282 % for lineno, comments in filediff.left_comments.items():
276
283
277 %if c.diffmode == 'unified':
284 %if c.diffmode == 'unified':
278 <tr class="cb-line">
285 <tr class="cb-line">
279 <td class="cb-data cb-context"></td>
286 <td class="cb-data cb-context"></td>
280 <td class="cb-lineno cb-context"></td>
287 <td class="cb-lineno cb-context"></td>
281 <td class="cb-lineno cb-context"></td>
288 <td class="cb-lineno cb-context"></td>
282 <td class="cb-content cb-context">
289 <td class="cb-content cb-context">
283 ${inline_comments_container(comments)}
290 ${inline_comments_container(comments)}
284 </td>
291 </td>
285 </tr>
292 </tr>
286 %elif c.diffmode == 'sideside':
293 %elif c.diffmode == 'sideside':
287 <tr class="cb-line">
294 <tr class="cb-line">
288 <td class="cb-data cb-context"></td>
295 <td class="cb-data cb-context"></td>
289 <td class="cb-lineno cb-context"></td>
296 <td class="cb-lineno cb-context"></td>
290 <td class="cb-content cb-context"></td>
297 <td class="cb-content cb-context"></td>
291
298
292 <td class="cb-data cb-context"></td>
299 <td class="cb-data cb-context"></td>
293 <td class="cb-lineno cb-context"></td>
300 <td class="cb-lineno cb-context"></td>
294 <td class="cb-content cb-context">
301 <td class="cb-content cb-context">
295 ${inline_comments_container(comments)}
302 ${inline_comments_container(comments)}
296 </td>
303 </td>
297 </tr>
304 </tr>
298 %endif
305 %endif
299
306
300 % endfor
307 % endfor
301
308
302 </table>
309 </table>
303 </div>
310 </div>
304 %endfor
311 %endfor
305
312
306 ## outdated comments that are made for a file that has been deleted
313 ## outdated comments that are made for a file that has been deleted
307 % for filename, comments_dict in (deleted_files_comments or {}).items():
314 % for filename, comments_dict in (deleted_files_comments or {}).items():
308
315
309 <div class="filediffs filediff-outdated" style="display: none">
316 <div class="filediffs filediff-outdated" style="display: none">
310 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
317 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
311 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
318 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
312 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
319 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
313 <div class="filediff-collapse-indicator"></div>
320 <div class="filediff-collapse-indicator"></div>
314 <span class="pill">
321 <span class="pill">
315 ## file was deleted
322 ## file was deleted
316 <strong>${filename}</strong>
323 <strong>${filename}</strong>
317 </span>
324 </span>
318 <span class="pill-group" style="float: left">
325 <span class="pill-group" style="float: left">
319 ## file op, doesn't need translation
326 ## file op, doesn't need translation
320 <span class="pill" op="removed">removed in this version</span>
327 <span class="pill" op="removed">removed in this version</span>
321 </span>
328 </span>
322 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
329 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
323 <span class="pill-group" style="float: right">
330 <span class="pill-group" style="float: right">
324 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
331 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
325 </span>
332 </span>
326 </label>
333 </label>
327
334
328 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
335 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
329 <tr>
336 <tr>
330 % if c.diffmode == 'unified':
337 % if c.diffmode == 'unified':
331 <td></td>
338 <td></td>
332 %endif
339 %endif
333
340
334 <td></td>
341 <td></td>
335 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
342 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
336 ${_('File was deleted in this version, and outdated comments were made on it')}
343 ${_('File was deleted in this version, and outdated comments were made on it')}
337 </td>
344 </td>
338 </tr>
345 </tr>
339 %if c.diffmode == 'unified':
346 %if c.diffmode == 'unified':
340 <tr class="cb-line">
347 <tr class="cb-line">
341 <td class="cb-data cb-context"></td>
348 <td class="cb-data cb-context"></td>
342 <td class="cb-lineno cb-context"></td>
349 <td class="cb-lineno cb-context"></td>
343 <td class="cb-lineno cb-context"></td>
350 <td class="cb-lineno cb-context"></td>
344 <td class="cb-content cb-context">
351 <td class="cb-content cb-context">
345 ${inline_comments_container(comments_dict['comments'])}
352 ${inline_comments_container(comments_dict['comments'])}
346 </td>
353 </td>
347 </tr>
354 </tr>
348 %elif c.diffmode == 'sideside':
355 %elif c.diffmode == 'sideside':
349 <tr class="cb-line">
356 <tr class="cb-line">
350 <td class="cb-data cb-context"></td>
357 <td class="cb-data cb-context"></td>
351 <td class="cb-lineno cb-context"></td>
358 <td class="cb-lineno cb-context"></td>
352 <td class="cb-content cb-context"></td>
359 <td class="cb-content cb-context"></td>
353
360
354 <td class="cb-data cb-context"></td>
361 <td class="cb-data cb-context"></td>
355 <td class="cb-lineno cb-context"></td>
362 <td class="cb-lineno cb-context"></td>
356 <td class="cb-content cb-context">
363 <td class="cb-content cb-context">
357 ${inline_comments_container(comments_dict['comments'])}
364 ${inline_comments_container(comments_dict['comments'])}
358 </td>
365 </td>
359 </tr>
366 </tr>
360 %endif
367 %endif
361 </table>
368 </table>
362 </div>
369 </div>
363 </div>
370 </div>
364 % endfor
371 % endfor
365
372
366 </div>
373 </div>
367 </div>
374 </div>
368 </%def>
375 </%def>
369
376
370 <%def name="diff_ops(filediff)">
377 <%def name="diff_ops(filediff)">
371 <%
378 <%
372 stats = filediff['patch']['stats']
379 stats = filediff['patch']['stats']
373 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
380 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
374 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
381 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
375 %>
382 %>
376 <span class="pill">
383 <span class="pill">
377 %if filediff.source_file_path and filediff.target_file_path:
384 %if filediff.source_file_path and filediff.target_file_path:
378 %if filediff.source_file_path != filediff.target_file_path:
385 %if filediff.source_file_path != filediff.target_file_path:
379 ## file was renamed
386 ## file was renamed
380 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
387 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
381 %else:
388 %else:
382 ## file was modified
389 ## file was modified
383 <strong>${filediff.source_file_path}</strong>
390 <strong>${filediff.source_file_path}</strong>
384 %endif
391 %endif
385 %else:
392 %else:
386 %if filediff.source_file_path:
393 %if filediff.source_file_path:
387 ## file was deleted
394 ## file was deleted
388 <strong>${filediff.source_file_path}</strong>
395 <strong>${filediff.source_file_path}</strong>
389 %else:
396 %else:
390 ## file was added
397 ## file was added
391 <strong>${filediff.target_file_path}</strong>
398 <strong>${filediff.target_file_path}</strong>
392 %endif
399 %endif
393 %endif
400 %endif
394 </span>
401 </span>
395 <span class="pill-group" style="float: left">
402 <span class="pill-group" style="float: left">
396 %if filediff.patch['is_limited_diff']:
403 %if filediff.patch['is_limited_diff']:
397 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
404 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
398 %endif
405 %endif
399 %if RENAMED_FILENODE in stats['ops']:
406 %if RENAMED_FILENODE in stats['ops']:
400 <span class="pill" op="renamed">renamed</span>
407 <span class="pill" op="renamed">renamed</span>
401 %endif
408 %endif
402
409
403 %if NEW_FILENODE in stats['ops']:
410 %if NEW_FILENODE in stats['ops']:
404 <span class="pill" op="created">created</span>
411 <span class="pill" op="created">created</span>
405 %if filediff['target_mode'].startswith('120'):
412 %if filediff['target_mode'].startswith('120'):
406 <span class="pill" op="symlink">symlink</span>
413 <span class="pill" op="symlink">symlink</span>
407 %else:
414 %else:
408 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
415 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
409 %endif
416 %endif
410 %endif
417 %endif
411
418
412 %if DEL_FILENODE in stats['ops']:
419 %if DEL_FILENODE in stats['ops']:
413 <span class="pill" op="removed">removed</span>
420 <span class="pill" op="removed">removed</span>
414 %endif
421 %endif
415
422
416 %if CHMOD_FILENODE in stats['ops']:
423 %if CHMOD_FILENODE in stats['ops']:
417 <span class="pill" op="mode">
424 <span class="pill" op="mode">
418 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
425 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
419 </span>
426 </span>
420 %endif
427 %endif
421 </span>
428 </span>
422
429
423 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
430 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
424
431
425 <span class="pill-group" style="float: right">
432 <span class="pill-group" style="float: right">
426 %if BIN_FILENODE in stats['ops']:
433 %if BIN_FILENODE in stats['ops']:
427 <span class="pill" op="binary">binary</span>
434 <span class="pill" op="binary">binary</span>
428 %if MOD_FILENODE in stats['ops']:
435 %if MOD_FILENODE in stats['ops']:
429 <span class="pill" op="modified">modified</span>
436 <span class="pill" op="modified">modified</span>
430 %endif
437 %endif
431 %endif
438 %endif
432 %if stats['added']:
439 %if stats['added']:
433 <span class="pill" op="added">+${stats['added']}</span>
440 <span class="pill" op="added">+${stats['added']}</span>
434 %endif
441 %endif
435 %if stats['deleted']:
442 %if stats['deleted']:
436 <span class="pill" op="deleted">-${stats['deleted']}</span>
443 <span class="pill" op="deleted">-${stats['deleted']}</span>
437 %endif
444 %endif
438 </span>
445 </span>
439
446
440 </%def>
447 </%def>
441
448
442 <%def name="nice_mode(filemode)">
449 <%def name="nice_mode(filemode)">
443 ${filemode.startswith('100') and filemode[3:] or filemode}
450 ${filemode.startswith('100') and filemode[3:] or filemode}
444 </%def>
451 </%def>
445
452
446 <%def name="diff_menu(filediff, use_comments=False)">
453 <%def name="diff_menu(filediff, use_comments=False)">
447 <div class="filediff-menu">
454 <div class="filediff-menu">
448 %if filediff.diffset.source_ref:
455 %if filediff.diffset.source_ref:
449 %if filediff.patch['operation'] in ['D', 'M']:
456 %if filediff.patch['operation'] in ['D', 'M']:
450 <a
457 <a
451 class="tooltip"
458 class="tooltip"
452 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
459 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
453 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
460 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
454 >
461 >
455 ${_('Show file before')}
462 ${_('Show file before')}
456 </a> |
463 </a> |
457 %else:
464 %else:
458 <span
465 <span
459 class="tooltip"
466 class="tooltip"
460 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
467 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
461 >
468 >
462 ${_('Show file before')}
469 ${_('Show file before')}
463 </span> |
470 </span> |
464 %endif
471 %endif
465 %if filediff.patch['operation'] in ['A', 'M']:
472 %if filediff.patch['operation'] in ['A', 'M']:
466 <a
473 <a
467 class="tooltip"
474 class="tooltip"
468 href="${h.url('files_home',repo_name=filediff.diffset.source_repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
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 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
476 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
470 >
477 >
471 ${_('Show file after')}
478 ${_('Show file after')}
472 </a> |
479 </a> |
473 %else:
480 %else:
474 <span
481 <span
475 class="tooltip"
482 class="tooltip"
476 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
483 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
477 >
484 >
478 ${_('Show file after')}
485 ${_('Show file after')}
479 </span> |
486 </span> |
480 %endif
487 %endif
481 <a
488 <a
482 class="tooltip"
489 class="tooltip"
483 title="${h.tooltip(_('Raw diff'))}"
490 title="${h.tooltip(_('Raw diff'))}"
484 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')}"
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 ${_('Raw diff')}
493 ${_('Raw diff')}
487 </a> |
494 </a> |
488 <a
495 <a
489 class="tooltip"
496 class="tooltip"
490 title="${h.tooltip(_('Download diff'))}"
497 title="${h.tooltip(_('Download diff'))}"
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='download')}"
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 ${_('Download diff')}
500 ${_('Download diff')}
494 </a>
501 </a>
495 % if use_comments:
502 % if use_comments:
496 |
503 |
497 % endif
504 % endif
498
505
499 ## 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)
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 %if hasattr(c, 'ignorews_url'):
507 %if hasattr(c, 'ignorews_url'):
501 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
508 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
502 %endif
509 %endif
503 %if hasattr(c, 'context_url'):
510 %if hasattr(c, 'context_url'):
504 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
511 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
505 %endif
512 %endif
506
513
507 %if use_comments:
514 %if use_comments:
508 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
515 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
509 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
516 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
510 </a>
517 </a>
511 %endif
518 %endif
512 %endif
519 %endif
513 </div>
520 </div>
514 </%def>
521 </%def>
515
522
516
523
517 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
524 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
518 <%def name="inline_comments_container(comments)">
525 <%def name="inline_comments_container(comments)">
519 <div class="inline-comments">
526 <div class="inline-comments">
520 %for comment in comments:
527 %for comment in comments:
521 ${commentblock.comment_block(comment, inline=True)}
528 ${commentblock.comment_block(comment, inline=True)}
522 %endfor
529 %endfor
523
530
524 % if comments and comments[-1].outdated:
531 % if comments and comments[-1].outdated:
525 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
532 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
526 style="display: none;}">
533 style="display: none;}">
527 ${_('Add another comment')}
534 ${_('Add another comment')}
528 </span>
535 </span>
529 % else:
536 % else:
530 <span onclick="return Rhodecode.comments.createComment(this)"
537 <span onclick="return Rhodecode.comments.createComment(this)"
531 class="btn btn-secondary cb-comment-add-button">
538 class="btn btn-secondary cb-comment-add-button">
532 ${_('Add another comment')}
539 ${_('Add another comment')}
533 </span>
540 </span>
534 % endif
541 % endif
535
542
536 </div>
543 </div>
537 </%def>
544 </%def>
538
545
539
546
540 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
547 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
541 %for i, line in enumerate(hunk.sideside):
548 %for i, line in enumerate(hunk.sideside):
542 <%
549 <%
543 old_line_anchor, new_line_anchor = None, None
550 old_line_anchor, new_line_anchor = None, None
544 if line.original.lineno:
551 if line.original.lineno:
545 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
552 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
546 if line.modified.lineno:
553 if line.modified.lineno:
547 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
554 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
548 %>
555 %>
549
556
550 <tr class="cb-line">
557 <tr class="cb-line">
551 <td class="cb-data ${action_class(line.original.action)}"
558 <td class="cb-data ${action_class(line.original.action)}"
552 data-line-number="${line.original.lineno}"
559 data-line-number="${line.original.lineno}"
553 >
560 >
554 <div>
561 <div>
555 %if line.original.comments:
562 %if line.original.comments:
556 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
563 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
557 %endif
564 %endif
558 </div>
565 </div>
559 </td>
566 </td>
560 <td class="cb-lineno ${action_class(line.original.action)}"
567 <td class="cb-lineno ${action_class(line.original.action)}"
561 data-line-number="${line.original.lineno}"
568 data-line-number="${line.original.lineno}"
562 %if old_line_anchor:
569 %if old_line_anchor:
563 id="${old_line_anchor}"
570 id="${old_line_anchor}"
564 %endif
571 %endif
565 >
572 >
566 %if line.original.lineno:
573 %if line.original.lineno:
567 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
574 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
568 %endif
575 %endif
569 </td>
576 </td>
570 <td class="cb-content ${action_class(line.original.action)}"
577 <td class="cb-content ${action_class(line.original.action)}"
571 data-line-number="o${line.original.lineno}"
578 data-line-number="o${line.original.lineno}"
572 >
579 >
573 %if use_comments and line.original.lineno:
580 %if use_comments and line.original.lineno:
574 ${render_add_comment_button()}
581 ${render_add_comment_button()}
575 %endif
582 %endif
576 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
583 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
577 %if use_comments and line.original.lineno and line.original.comments:
584 %if use_comments and line.original.lineno and line.original.comments:
578 ${inline_comments_container(line.original.comments)}
585 ${inline_comments_container(line.original.comments)}
579 %endif
586 %endif
580 </td>
587 </td>
581 <td class="cb-data ${action_class(line.modified.action)}"
588 <td class="cb-data ${action_class(line.modified.action)}"
582 data-line-number="${line.modified.lineno}"
589 data-line-number="${line.modified.lineno}"
583 >
590 >
584 <div>
591 <div>
585 %if line.modified.comments:
592 %if line.modified.comments:
586 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
593 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
587 %endif
594 %endif
588 </div>
595 </div>
589 </td>
596 </td>
590 <td class="cb-lineno ${action_class(line.modified.action)}"
597 <td class="cb-lineno ${action_class(line.modified.action)}"
591 data-line-number="${line.modified.lineno}"
598 data-line-number="${line.modified.lineno}"
592 %if new_line_anchor:
599 %if new_line_anchor:
593 id="${new_line_anchor}"
600 id="${new_line_anchor}"
594 %endif
601 %endif
595 >
602 >
596 %if line.modified.lineno:
603 %if line.modified.lineno:
597 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
604 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
598 %endif
605 %endif
599 </td>
606 </td>
600 <td class="cb-content ${action_class(line.modified.action)}"
607 <td class="cb-content ${action_class(line.modified.action)}"
601 data-line-number="n${line.modified.lineno}"
608 data-line-number="n${line.modified.lineno}"
602 >
609 >
603 %if use_comments and line.modified.lineno:
610 %if use_comments and line.modified.lineno:
604 ${render_add_comment_button()}
611 ${render_add_comment_button()}
605 %endif
612 %endif
606 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
613 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
607 %if use_comments and line.modified.lineno and line.modified.comments:
614 %if use_comments and line.modified.lineno and line.modified.comments:
608 ${inline_comments_container(line.modified.comments)}
615 ${inline_comments_container(line.modified.comments)}
609 %endif
616 %endif
610 </td>
617 </td>
611 </tr>
618 </tr>
612 %endfor
619 %endfor
613 </%def>
620 </%def>
614
621
615
622
616 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
623 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
617 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
624 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
618 <%
625 <%
619 old_line_anchor, new_line_anchor = None, None
626 old_line_anchor, new_line_anchor = None, None
620 if old_line_no:
627 if old_line_no:
621 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
628 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
622 if new_line_no:
629 if new_line_no:
623 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
630 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
624 %>
631 %>
625 <tr class="cb-line">
632 <tr class="cb-line">
626 <td class="cb-data ${action_class(action)}">
633 <td class="cb-data ${action_class(action)}">
627 <div>
634 <div>
628 %if comments:
635 %if comments:
629 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
636 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
630 %endif
637 %endif
631 </div>
638 </div>
632 </td>
639 </td>
633 <td class="cb-lineno ${action_class(action)}"
640 <td class="cb-lineno ${action_class(action)}"
634 data-line-number="${old_line_no}"
641 data-line-number="${old_line_no}"
635 %if old_line_anchor:
642 %if old_line_anchor:
636 id="${old_line_anchor}"
643 id="${old_line_anchor}"
637 %endif
644 %endif
638 >
645 >
639 %if old_line_anchor:
646 %if old_line_anchor:
640 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
647 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
641 %endif
648 %endif
642 </td>
649 </td>
643 <td class="cb-lineno ${action_class(action)}"
650 <td class="cb-lineno ${action_class(action)}"
644 data-line-number="${new_line_no}"
651 data-line-number="${new_line_no}"
645 %if new_line_anchor:
652 %if new_line_anchor:
646 id="${new_line_anchor}"
653 id="${new_line_anchor}"
647 %endif
654 %endif
648 >
655 >
649 %if new_line_anchor:
656 %if new_line_anchor:
650 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
657 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
651 %endif
658 %endif
652 </td>
659 </td>
653 <td class="cb-content ${action_class(action)}"
660 <td class="cb-content ${action_class(action)}"
654 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
661 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
655 >
662 >
656 %if use_comments:
663 %if use_comments:
657 ${render_add_comment_button()}
664 ${render_add_comment_button()}
658 %endif
665 %endif
659 <span class="cb-code">${action} ${content or '' | n}</span>
666 <span class="cb-code">${action} ${content or '' | n}</span>
660 %if use_comments and comments:
667 %if use_comments and comments:
661 ${inline_comments_container(comments)}
668 ${inline_comments_container(comments)}
662 %endif
669 %endif
663 </td>
670 </td>
664 </tr>
671 </tr>
665 %endfor
672 %endfor
666 </%def>
673 </%def>
667
674
668 <%def name="render_add_comment_button()">
675 <%def name="render_add_comment_button()">
669 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
676 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
670 <span><i class="icon-comment"></i></span>
677 <span><i class="icon-comment"></i></span>
671 </button>
678 </button>
672 </%def>
679 </%def>
673
680
674 <%def name="render_diffset_menu()">
681 <%def name="render_diffset_menu()">
675
682
676 <div class="diffset-menu clearinner">
683 <div class="diffset-menu clearinner">
677 <div class="pull-right">
684 <div class="pull-right">
678 <div class="btn-group">
685 <div class="btn-group">
679
686
680 <a
687 <a
681 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
688 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
682 title="${_('View side by side')}"
689 title="${_('View side by side')}"
683 href="${h.url_replace(diffmode='sideside')}">
690 href="${h.url_replace(diffmode='sideside')}">
684 <span>${_('Side by Side')}</span>
691 <span>${_('Side by Side')}</span>
685 </a>
692 </a>
686 <a
693 <a
687 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
694 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
688 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
695 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
689 <span>${_('Unified')}</span>
696 <span>${_('Unified')}</span>
690 </a>
697 </a>
691 </div>
698 </div>
692 </div>
699 </div>
693
700
694 <div class="pull-left">
701 <div class="pull-left">
695 <div class="btn-group">
702 <div class="btn-group">
696 <a
703 <a
697 class="btn"
704 class="btn"
698 href="#"
705 href="#"
699 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
706 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
700 <a
707 <a
701 class="btn"
708 class="btn"
702 href="#"
709 href="#"
703 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
710 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
704 <a
711 <a
705 class="btn"
712 class="btn"
706 href="#"
713 href="#"
707 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
714 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
708 </div>
715 </div>
709 </div>
716 </div>
710 </div>
717 </div>
711 </%def>
718 </%def>
General Comments 0
You need to be logged in to leave comments. Login now