##// END OF EJS Templates
pull-requests: added version browsing for pull requests....
marcink -
r1192:5c951e10 default
parent child Browse files
Show More
@@ -1,901 +1,993 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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
24
25 import peppercorn
25 import peppercorn
26 import formencode
26 import formencode
27 import logging
27 import logging
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 from pylons import request, tmpl_context as c, url
30 from pylons import request, tmpl_context as c, url
31 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
34 from sqlalchemy.sql import func
34 from sqlalchemy.sql import func
35 from sqlalchemy.sql.expression import or_
35 from sqlalchemy.sql.expression import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
38 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
39 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.base import (
40 from rhodecode.lib.base import (
41 BaseRepoController, render, vcs_operation_context)
41 BaseRepoController, render, vcs_operation_context)
42 from rhodecode.lib.auth import (
42 from rhodecode.lib.auth import (
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
44 HasAcceptedRepoType, XHRRequired)
44 HasAcceptedRepoType, XHRRequired)
45 from rhodecode.lib.channelstream import channelstream_request
45 from rhodecode.lib.channelstream import channelstream_request
46 from rhodecode.lib.compat import OrderedDict
46 from rhodecode.lib.compat import OrderedDict
47 from rhodecode.lib.utils import jsonify
47 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
48 from rhodecode.lib.utils2 import (
49 safe_int, safe_str, str2bool, safe_unicode, UnsafeAttributeDict)
49 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
50 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
50 from rhodecode.lib.vcs.exceptions import (
51 from rhodecode.lib.vcs.exceptions import (
51 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
52 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
52 NodeDoesNotExistError)
53 NodeDoesNotExistError)
53 from rhodecode.lib.diffs import LimitedDiffContainer
54
54 from rhodecode.model.changeset_status import ChangesetStatusModel
55 from rhodecode.model.changeset_status import ChangesetStatusModel
55 from rhodecode.model.comment import ChangesetCommentsModel
56 from rhodecode.model.comment import ChangesetCommentsModel
56 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
57 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
57 Repository
58 Repository, PullRequestVersion)
58 from rhodecode.model.forms import PullRequestForm
59 from rhodecode.model.forms import PullRequestForm
59 from rhodecode.model.meta import Session
60 from rhodecode.model.meta import Session
60 from rhodecode.model.pull_request import PullRequestModel
61 from rhodecode.model.pull_request import PullRequestModel
61
62
62 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
63
64
64
65
65 class PullrequestsController(BaseRepoController):
66 class PullrequestsController(BaseRepoController):
66 def __before__(self):
67 def __before__(self):
67 super(PullrequestsController, self).__before__()
68 super(PullrequestsController, self).__before__()
68
69
69 def _load_compare_data(self, pull_request, inline_comments, enable_comments=True):
70 def _load_compare_data(self, pull_request, inline_comments, enable_comments=True):
70 """
71 """
71 Load context data needed for generating compare diff
72 Load context data needed for generating compare diff
72
73
73 :param pull_request: object related to the request
74 :param pull_request: object related to the request
74 :param enable_comments: flag to determine if comments are included
75 :param enable_comments: flag to determine if comments are included
75 """
76 """
76 source_repo = pull_request.source_repo
77 source_repo = pull_request.source_repo
77 source_ref_id = pull_request.source_ref_parts.commit_id
78 source_ref_id = pull_request.source_ref_parts.commit_id
78
79
79 target_repo = pull_request.target_repo
80 target_repo = pull_request.target_repo
80 target_ref_id = pull_request.target_ref_parts.commit_id
81 target_ref_id = pull_request.target_ref_parts.commit_id
81
82
82 # despite opening commits for bookmarks/branches/tags, we always
83 # despite opening commits for bookmarks/branches/tags, we always
83 # convert this to rev to prevent changes after bookmark or branch change
84 # convert this to rev to prevent changes after bookmark or branch change
84 c.source_ref_type = 'rev'
85 c.source_ref_type = 'rev'
85 c.source_ref = source_ref_id
86 c.source_ref = source_ref_id
86
87
87 c.target_ref_type = 'rev'
88 c.target_ref_type = 'rev'
88 c.target_ref = target_ref_id
89 c.target_ref = target_ref_id
89
90
90 c.source_repo = source_repo
91 c.source_repo = source_repo
91 c.target_repo = target_repo
92 c.target_repo = target_repo
92
93
93 c.fulldiff = bool(request.GET.get('fulldiff'))
94 c.fulldiff = bool(request.GET.get('fulldiff'))
94
95
95 # diff_limit is the old behavior, will cut off the whole diff
96 # diff_limit is the old behavior, will cut off the whole diff
96 # if the limit is applied otherwise will just hide the
97 # if the limit is applied otherwise will just hide the
97 # big files from the front-end
98 # big files from the front-end
98 diff_limit = self.cut_off_limit_diff
99 diff_limit = self.cut_off_limit_diff
99 file_limit = self.cut_off_limit_file
100 file_limit = self.cut_off_limit_file
100
101
101 pre_load = ["author", "branch", "date", "message"]
102 pre_load = ["author", "branch", "date", "message"]
102
103
103 c.commit_ranges = []
104 c.commit_ranges = []
104 source_commit = EmptyCommit()
105 source_commit = EmptyCommit()
105 target_commit = EmptyCommit()
106 target_commit = EmptyCommit()
106 c.missing_requirements = False
107 c.missing_requirements = False
107 try:
108 try:
108 c.commit_ranges = [
109 c.commit_ranges = [
109 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
110 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
110 for rev in pull_request.revisions]
111 for rev in pull_request.revisions]
111
112
112 c.statuses = source_repo.statuses(
113 c.statuses = source_repo.statuses(
113 [x.raw_id for x in c.commit_ranges])
114 [x.raw_id for x in c.commit_ranges])
114
115
115 target_commit = source_repo.get_commit(
116 target_commit = source_repo.get_commit(
116 commit_id=safe_str(target_ref_id))
117 commit_id=safe_str(target_ref_id))
117 source_commit = source_repo.get_commit(
118 source_commit = source_repo.get_commit(
118 commit_id=safe_str(source_ref_id))
119 commit_id=safe_str(source_ref_id))
119 except RepositoryRequirementError:
120 except RepositoryRequirementError:
120 c.missing_requirements = True
121 c.missing_requirements = True
121
122
122 c.changes = {}
123 c.changes = {}
123 c.missing_commits = False
124 c.missing_commits = False
124 if (c.missing_requirements or
125 if (c.missing_requirements or
125 isinstance(source_commit, EmptyCommit) or
126 isinstance(source_commit, EmptyCommit) or
126 source_commit == target_commit):
127 source_commit == target_commit):
127 _parsed = []
128 _parsed = []
128 c.missing_commits = True
129 c.missing_commits = True
129 else:
130 else:
130 vcs_diff = PullRequestModel().get_diff(pull_request)
131 vcs_diff = PullRequestModel().get_diff(pull_request)
131 diff_processor = diffs.DiffProcessor(
132 diff_processor = diffs.DiffProcessor(
132 vcs_diff, format='newdiff', diff_limit=diff_limit,
133 vcs_diff, format='newdiff', diff_limit=diff_limit,
133 file_limit=file_limit, show_full_diff=c.fulldiff)
134 file_limit=file_limit, show_full_diff=c.fulldiff)
134 _parsed = diff_processor.prepare()
135 _parsed = diff_processor.prepare()
135
136
136 commit_changes = OrderedDict()
137 commit_changes = OrderedDict()
137 _parsed = diff_processor.prepare()
138 _parsed = diff_processor.prepare()
138 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
139 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
139
140
140 _parsed = diff_processor.prepare()
141 _parsed = diff_processor.prepare()
141
142
142 def _node_getter(commit):
143 def _node_getter(commit):
143 def get_node(fname):
144 def get_node(fname):
144 try:
145 try:
145 return commit.get_node(fname)
146 return commit.get_node(fname)
146 except NodeDoesNotExistError:
147 except NodeDoesNotExistError:
147 return None
148 return None
148 return get_node
149 return get_node
149
150
150 c.diffset = codeblocks.DiffSet(
151 c.diffset = codeblocks.DiffSet(
151 repo_name=c.repo_name,
152 repo_name=c.repo_name,
152 source_node_getter=_node_getter(target_commit),
153 source_node_getter=_node_getter(target_commit),
153 target_node_getter=_node_getter(source_commit),
154 target_node_getter=_node_getter(source_commit),
154 comments=inline_comments
155 comments=inline_comments
155 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
156 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
156
157
157 c.included_files = []
158 c.included_files = []
158 c.deleted_files = []
159 c.deleted_files = []
159
160
160 for f in _parsed:
161 for f in _parsed:
161 st = f['stats']
162 st = f['stats']
162 fid = h.FID('', f['filename'])
163 fid = h.FID('', f['filename'])
163 c.included_files.append(f['filename'])
164 c.included_files.append(f['filename'])
164
165
165 def _extract_ordering(self, request):
166 def _extract_ordering(self, request):
166 column_index = safe_int(request.GET.get('order[0][column]'))
167 column_index = safe_int(request.GET.get('order[0][column]'))
167 order_dir = request.GET.get('order[0][dir]', 'desc')
168 order_dir = request.GET.get('order[0][dir]', 'desc')
168 order_by = request.GET.get(
169 order_by = request.GET.get(
169 'columns[%s][data][sort]' % column_index, 'name_raw')
170 'columns[%s][data][sort]' % column_index, 'name_raw')
170 return order_by, order_dir
171 return order_by, order_dir
171
172
172 @LoginRequired()
173 @LoginRequired()
173 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
174 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
174 'repository.admin')
175 'repository.admin')
175 @HasAcceptedRepoType('git', 'hg')
176 @HasAcceptedRepoType('git', 'hg')
176 def show_all(self, repo_name):
177 def show_all(self, repo_name):
177 # filter types
178 # filter types
178 c.active = 'open'
179 c.active = 'open'
179 c.source = str2bool(request.GET.get('source'))
180 c.source = str2bool(request.GET.get('source'))
180 c.closed = str2bool(request.GET.get('closed'))
181 c.closed = str2bool(request.GET.get('closed'))
181 c.my = str2bool(request.GET.get('my'))
182 c.my = str2bool(request.GET.get('my'))
182 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
183 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
183 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
184 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
184 c.repo_name = repo_name
185 c.repo_name = repo_name
185
186
186 opened_by = None
187 opened_by = None
187 if c.my:
188 if c.my:
188 c.active = 'my'
189 c.active = 'my'
189 opened_by = [c.rhodecode_user.user_id]
190 opened_by = [c.rhodecode_user.user_id]
190
191
191 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
192 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
192 if c.closed:
193 if c.closed:
193 c.active = 'closed'
194 c.active = 'closed'
194 statuses = [PullRequest.STATUS_CLOSED]
195 statuses = [PullRequest.STATUS_CLOSED]
195
196
196 if c.awaiting_review and not c.source:
197 if c.awaiting_review and not c.source:
197 c.active = 'awaiting'
198 c.active = 'awaiting'
198 if c.source and not c.awaiting_review:
199 if c.source and not c.awaiting_review:
199 c.active = 'source'
200 c.active = 'source'
200 if c.awaiting_my_review:
201 if c.awaiting_my_review:
201 c.active = 'awaiting_my'
202 c.active = 'awaiting_my'
202
203
203 data = self._get_pull_requests_list(
204 data = self._get_pull_requests_list(
204 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
205 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
205 if not request.is_xhr:
206 if not request.is_xhr:
206 c.data = json.dumps(data['data'])
207 c.data = json.dumps(data['data'])
207 c.records_total = data['recordsTotal']
208 c.records_total = data['recordsTotal']
208 return render('/pullrequests/pullrequests.html')
209 return render('/pullrequests/pullrequests.html')
209 else:
210 else:
210 return json.dumps(data)
211 return json.dumps(data)
211
212
212 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
213 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
213 # pagination
214 # pagination
214 start = safe_int(request.GET.get('start'), 0)
215 start = safe_int(request.GET.get('start'), 0)
215 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
216 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
216 order_by, order_dir = self._extract_ordering(request)
217 order_by, order_dir = self._extract_ordering(request)
217
218
218 if c.awaiting_review:
219 if c.awaiting_review:
219 pull_requests = PullRequestModel().get_awaiting_review(
220 pull_requests = PullRequestModel().get_awaiting_review(
220 repo_name, source=c.source, opened_by=opened_by,
221 repo_name, source=c.source, opened_by=opened_by,
221 statuses=statuses, offset=start, length=length,
222 statuses=statuses, offset=start, length=length,
222 order_by=order_by, order_dir=order_dir)
223 order_by=order_by, order_dir=order_dir)
223 pull_requests_total_count = PullRequestModel(
224 pull_requests_total_count = PullRequestModel(
224 ).count_awaiting_review(
225 ).count_awaiting_review(
225 repo_name, source=c.source, statuses=statuses,
226 repo_name, source=c.source, statuses=statuses,
226 opened_by=opened_by)
227 opened_by=opened_by)
227 elif c.awaiting_my_review:
228 elif c.awaiting_my_review:
228 pull_requests = PullRequestModel().get_awaiting_my_review(
229 pull_requests = PullRequestModel().get_awaiting_my_review(
229 repo_name, source=c.source, opened_by=opened_by,
230 repo_name, source=c.source, opened_by=opened_by,
230 user_id=c.rhodecode_user.user_id, statuses=statuses,
231 user_id=c.rhodecode_user.user_id, statuses=statuses,
231 offset=start, length=length, order_by=order_by,
232 offset=start, length=length, order_by=order_by,
232 order_dir=order_dir)
233 order_dir=order_dir)
233 pull_requests_total_count = PullRequestModel(
234 pull_requests_total_count = PullRequestModel(
234 ).count_awaiting_my_review(
235 ).count_awaiting_my_review(
235 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
236 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
236 statuses=statuses, opened_by=opened_by)
237 statuses=statuses, opened_by=opened_by)
237 else:
238 else:
238 pull_requests = PullRequestModel().get_all(
239 pull_requests = PullRequestModel().get_all(
239 repo_name, source=c.source, opened_by=opened_by,
240 repo_name, source=c.source, opened_by=opened_by,
240 statuses=statuses, offset=start, length=length,
241 statuses=statuses, offset=start, length=length,
241 order_by=order_by, order_dir=order_dir)
242 order_by=order_by, order_dir=order_dir)
242 pull_requests_total_count = PullRequestModel().count_all(
243 pull_requests_total_count = PullRequestModel().count_all(
243 repo_name, source=c.source, statuses=statuses,
244 repo_name, source=c.source, statuses=statuses,
244 opened_by=opened_by)
245 opened_by=opened_by)
245
246
246 from rhodecode.lib.utils import PartialRenderer
247 from rhodecode.lib.utils import PartialRenderer
247 _render = PartialRenderer('data_table/_dt_elements.html')
248 _render = PartialRenderer('data_table/_dt_elements.html')
248 data = []
249 data = []
249 for pr in pull_requests:
250 for pr in pull_requests:
250 comments = ChangesetCommentsModel().get_all_comments(
251 comments = ChangesetCommentsModel().get_all_comments(
251 c.rhodecode_db_repo.repo_id, pull_request=pr)
252 c.rhodecode_db_repo.repo_id, pull_request=pr)
252
253
253 data.append({
254 data.append({
254 'name': _render('pullrequest_name',
255 'name': _render('pullrequest_name',
255 pr.pull_request_id, pr.target_repo.repo_name),
256 pr.pull_request_id, pr.target_repo.repo_name),
256 'name_raw': pr.pull_request_id,
257 'name_raw': pr.pull_request_id,
257 'status': _render('pullrequest_status',
258 'status': _render('pullrequest_status',
258 pr.calculated_review_status()),
259 pr.calculated_review_status()),
259 'title': _render(
260 'title': _render(
260 'pullrequest_title', pr.title, pr.description),
261 'pullrequest_title', pr.title, pr.description),
261 'description': h.escape(pr.description),
262 'description': h.escape(pr.description),
262 'updated_on': _render('pullrequest_updated_on',
263 'updated_on': _render('pullrequest_updated_on',
263 h.datetime_to_time(pr.updated_on)),
264 h.datetime_to_time(pr.updated_on)),
264 'updated_on_raw': h.datetime_to_time(pr.updated_on),
265 'updated_on_raw': h.datetime_to_time(pr.updated_on),
265 'created_on': _render('pullrequest_updated_on',
266 'created_on': _render('pullrequest_updated_on',
266 h.datetime_to_time(pr.created_on)),
267 h.datetime_to_time(pr.created_on)),
267 'created_on_raw': h.datetime_to_time(pr.created_on),
268 'created_on_raw': h.datetime_to_time(pr.created_on),
268 'author': _render('pullrequest_author',
269 'author': _render('pullrequest_author',
269 pr.author.full_contact, ),
270 pr.author.full_contact, ),
270 'author_raw': pr.author.full_name,
271 'author_raw': pr.author.full_name,
271 'comments': _render('pullrequest_comments', len(comments)),
272 'comments': _render('pullrequest_comments', len(comments)),
272 'comments_raw': len(comments),
273 'comments_raw': len(comments),
273 'closed': pr.is_closed(),
274 'closed': pr.is_closed(),
274 })
275 })
275 # json used to render the grid
276 # json used to render the grid
276 data = ({
277 data = ({
277 'data': data,
278 'data': data,
278 'recordsTotal': pull_requests_total_count,
279 'recordsTotal': pull_requests_total_count,
279 'recordsFiltered': pull_requests_total_count,
280 'recordsFiltered': pull_requests_total_count,
280 })
281 })
281 return data
282 return data
282
283
283 @LoginRequired()
284 @LoginRequired()
284 @NotAnonymous()
285 @NotAnonymous()
285 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
286 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
286 'repository.admin')
287 'repository.admin')
287 @HasAcceptedRepoType('git', 'hg')
288 @HasAcceptedRepoType('git', 'hg')
288 def index(self):
289 def index(self):
289 source_repo = c.rhodecode_db_repo
290 source_repo = c.rhodecode_db_repo
290
291
291 try:
292 try:
292 source_repo.scm_instance().get_commit()
293 source_repo.scm_instance().get_commit()
293 except EmptyRepositoryError:
294 except EmptyRepositoryError:
294 h.flash(h.literal(_('There are no commits yet')),
295 h.flash(h.literal(_('There are no commits yet')),
295 category='warning')
296 category='warning')
296 redirect(url('summary_home', repo_name=source_repo.repo_name))
297 redirect(url('summary_home', repo_name=source_repo.repo_name))
297
298
298 commit_id = request.GET.get('commit')
299 commit_id = request.GET.get('commit')
299 branch_ref = request.GET.get('branch')
300 branch_ref = request.GET.get('branch')
300 bookmark_ref = request.GET.get('bookmark')
301 bookmark_ref = request.GET.get('bookmark')
301
302
302 try:
303 try:
303 source_repo_data = PullRequestModel().generate_repo_data(
304 source_repo_data = PullRequestModel().generate_repo_data(
304 source_repo, commit_id=commit_id,
305 source_repo, commit_id=commit_id,
305 branch=branch_ref, bookmark=bookmark_ref)
306 branch=branch_ref, bookmark=bookmark_ref)
306 except CommitDoesNotExistError as e:
307 except CommitDoesNotExistError as e:
307 log.exception(e)
308 log.exception(e)
308 h.flash(_('Commit does not exist'), 'error')
309 h.flash(_('Commit does not exist'), 'error')
309 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
310 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
310
311
311 default_target_repo = source_repo
312 default_target_repo = source_repo
312
313
313 if source_repo.parent:
314 if source_repo.parent:
314 parent_vcs_obj = source_repo.parent.scm_instance()
315 parent_vcs_obj = source_repo.parent.scm_instance()
315 if parent_vcs_obj and not parent_vcs_obj.is_empty():
316 if parent_vcs_obj and not parent_vcs_obj.is_empty():
316 # change default if we have a parent repo
317 # change default if we have a parent repo
317 default_target_repo = source_repo.parent
318 default_target_repo = source_repo.parent
318
319
319 target_repo_data = PullRequestModel().generate_repo_data(
320 target_repo_data = PullRequestModel().generate_repo_data(
320 default_target_repo)
321 default_target_repo)
321
322
322 selected_source_ref = source_repo_data['refs']['selected_ref']
323 selected_source_ref = source_repo_data['refs']['selected_ref']
323
324
324 title_source_ref = selected_source_ref.split(':', 2)[1]
325 title_source_ref = selected_source_ref.split(':', 2)[1]
325 c.default_title = PullRequestModel().generate_pullrequest_title(
326 c.default_title = PullRequestModel().generate_pullrequest_title(
326 source=source_repo.repo_name,
327 source=source_repo.repo_name,
327 source_ref=title_source_ref,
328 source_ref=title_source_ref,
328 target=default_target_repo.repo_name
329 target=default_target_repo.repo_name
329 )
330 )
330
331
331 c.default_repo_data = {
332 c.default_repo_data = {
332 'source_repo_name': source_repo.repo_name,
333 'source_repo_name': source_repo.repo_name,
333 'source_refs_json': json.dumps(source_repo_data),
334 'source_refs_json': json.dumps(source_repo_data),
334 'target_repo_name': default_target_repo.repo_name,
335 'target_repo_name': default_target_repo.repo_name,
335 'target_refs_json': json.dumps(target_repo_data),
336 'target_refs_json': json.dumps(target_repo_data),
336 }
337 }
337 c.default_source_ref = selected_source_ref
338 c.default_source_ref = selected_source_ref
338
339
339 return render('/pullrequests/pullrequest.html')
340 return render('/pullrequests/pullrequest.html')
340
341
341 @LoginRequired()
342 @LoginRequired()
342 @NotAnonymous()
343 @NotAnonymous()
343 @XHRRequired()
344 @XHRRequired()
344 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
345 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
345 'repository.admin')
346 'repository.admin')
346 @jsonify
347 @jsonify
347 def get_repo_refs(self, repo_name, target_repo_name):
348 def get_repo_refs(self, repo_name, target_repo_name):
348 repo = Repository.get_by_repo_name(target_repo_name)
349 repo = Repository.get_by_repo_name(target_repo_name)
349 if not repo:
350 if not repo:
350 raise HTTPNotFound
351 raise HTTPNotFound
351 return PullRequestModel().generate_repo_data(repo)
352 return PullRequestModel().generate_repo_data(repo)
352
353
353 @LoginRequired()
354 @LoginRequired()
354 @NotAnonymous()
355 @NotAnonymous()
355 @XHRRequired()
356 @XHRRequired()
356 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
357 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
357 'repository.admin')
358 'repository.admin')
358 @jsonify
359 @jsonify
359 def get_repo_destinations(self, repo_name):
360 def get_repo_destinations(self, repo_name):
360 repo = Repository.get_by_repo_name(repo_name)
361 repo = Repository.get_by_repo_name(repo_name)
361 if not repo:
362 if not repo:
362 raise HTTPNotFound
363 raise HTTPNotFound
363 filter_query = request.GET.get('query')
364 filter_query = request.GET.get('query')
364
365
365 query = Repository.query() \
366 query = Repository.query() \
366 .order_by(func.length(Repository.repo_name)) \
367 .order_by(func.length(Repository.repo_name)) \
367 .filter(or_(
368 .filter(or_(
368 Repository.repo_name == repo.repo_name,
369 Repository.repo_name == repo.repo_name,
369 Repository.fork_id == repo.repo_id))
370 Repository.fork_id == repo.repo_id))
370
371
371 if filter_query:
372 if filter_query:
372 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
373 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
373 query = query.filter(
374 query = query.filter(
374 Repository.repo_name.ilike(ilike_expression))
375 Repository.repo_name.ilike(ilike_expression))
375
376
376 add_parent = False
377 add_parent = False
377 if repo.parent:
378 if repo.parent:
378 if filter_query in repo.parent.repo_name:
379 if filter_query in repo.parent.repo_name:
379 parent_vcs_obj = repo.parent.scm_instance()
380 parent_vcs_obj = repo.parent.scm_instance()
380 if parent_vcs_obj and not parent_vcs_obj.is_empty():
381 if parent_vcs_obj and not parent_vcs_obj.is_empty():
381 add_parent = True
382 add_parent = True
382
383
383 limit = 20 - 1 if add_parent else 20
384 limit = 20 - 1 if add_parent else 20
384 all_repos = query.limit(limit).all()
385 all_repos = query.limit(limit).all()
385 if add_parent:
386 if add_parent:
386 all_repos += [repo.parent]
387 all_repos += [repo.parent]
387
388
388 repos = []
389 repos = []
389 for obj in self.scm_model.get_repos(all_repos):
390 for obj in self.scm_model.get_repos(all_repos):
390 repos.append({
391 repos.append({
391 'id': obj['name'],
392 'id': obj['name'],
392 'text': obj['name'],
393 'text': obj['name'],
393 'type': 'repo',
394 'type': 'repo',
394 'obj': obj['dbrepo']
395 'obj': obj['dbrepo']
395 })
396 })
396
397
397 data = {
398 data = {
398 'more': False,
399 'more': False,
399 'results': [{
400 'results': [{
400 'text': _('Repositories'),
401 'text': _('Repositories'),
401 'children': repos
402 'children': repos
402 }] if repos else []
403 }] if repos else []
403 }
404 }
404 return data
405 return data
405
406
406 @LoginRequired()
407 @LoginRequired()
407 @NotAnonymous()
408 @NotAnonymous()
408 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
409 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
409 'repository.admin')
410 'repository.admin')
410 @HasAcceptedRepoType('git', 'hg')
411 @HasAcceptedRepoType('git', 'hg')
411 @auth.CSRFRequired()
412 @auth.CSRFRequired()
412 def create(self, repo_name):
413 def create(self, repo_name):
413 repo = Repository.get_by_repo_name(repo_name)
414 repo = Repository.get_by_repo_name(repo_name)
414 if not repo:
415 if not repo:
415 raise HTTPNotFound
416 raise HTTPNotFound
416
417
417 controls = peppercorn.parse(request.POST.items())
418 controls = peppercorn.parse(request.POST.items())
418
419
419 try:
420 try:
420 _form = PullRequestForm(repo.repo_id)().to_python(controls)
421 _form = PullRequestForm(repo.repo_id)().to_python(controls)
421 except formencode.Invalid as errors:
422 except formencode.Invalid as errors:
422 if errors.error_dict.get('revisions'):
423 if errors.error_dict.get('revisions'):
423 msg = 'Revisions: %s' % errors.error_dict['revisions']
424 msg = 'Revisions: %s' % errors.error_dict['revisions']
424 elif errors.error_dict.get('pullrequest_title'):
425 elif errors.error_dict.get('pullrequest_title'):
425 msg = _('Pull request requires a title with min. 3 chars')
426 msg = _('Pull request requires a title with min. 3 chars')
426 else:
427 else:
427 msg = _('Error creating pull request: {}').format(errors)
428 msg = _('Error creating pull request: {}').format(errors)
428 log.exception(msg)
429 log.exception(msg)
429 h.flash(msg, 'error')
430 h.flash(msg, 'error')
430
431
431 # would rather just go back to form ...
432 # would rather just go back to form ...
432 return redirect(url('pullrequest_home', repo_name=repo_name))
433 return redirect(url('pullrequest_home', repo_name=repo_name))
433
434
434 source_repo = _form['source_repo']
435 source_repo = _form['source_repo']
435 source_ref = _form['source_ref']
436 source_ref = _form['source_ref']
436 target_repo = _form['target_repo']
437 target_repo = _form['target_repo']
437 target_ref = _form['target_ref']
438 target_ref = _form['target_ref']
438 commit_ids = _form['revisions'][::-1]
439 commit_ids = _form['revisions'][::-1]
439 reviewers = [
440 reviewers = [
440 (r['user_id'], r['reasons']) for r in _form['review_members']]
441 (r['user_id'], r['reasons']) for r in _form['review_members']]
441
442
442 # find the ancestor for this pr
443 # find the ancestor for this pr
443 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
444 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
444 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
445 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
445
446
446 source_scm = source_db_repo.scm_instance()
447 source_scm = source_db_repo.scm_instance()
447 target_scm = target_db_repo.scm_instance()
448 target_scm = target_db_repo.scm_instance()
448
449
449 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
450 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
450 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
451 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
451
452
452 ancestor = source_scm.get_common_ancestor(
453 ancestor = source_scm.get_common_ancestor(
453 source_commit.raw_id, target_commit.raw_id, target_scm)
454 source_commit.raw_id, target_commit.raw_id, target_scm)
454
455
455 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
456 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
456 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
457 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
457
458
458 pullrequest_title = _form['pullrequest_title']
459 pullrequest_title = _form['pullrequest_title']
459 title_source_ref = source_ref.split(':', 2)[1]
460 title_source_ref = source_ref.split(':', 2)[1]
460 if not pullrequest_title:
461 if not pullrequest_title:
461 pullrequest_title = PullRequestModel().generate_pullrequest_title(
462 pullrequest_title = PullRequestModel().generate_pullrequest_title(
462 source=source_repo,
463 source=source_repo,
463 source_ref=title_source_ref,
464 source_ref=title_source_ref,
464 target=target_repo
465 target=target_repo
465 )
466 )
466
467
467 description = _form['pullrequest_desc']
468 description = _form['pullrequest_desc']
468 try:
469 try:
469 pull_request = PullRequestModel().create(
470 pull_request = PullRequestModel().create(
470 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
471 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
471 target_ref, commit_ids, reviewers, pullrequest_title,
472 target_ref, commit_ids, reviewers, pullrequest_title,
472 description
473 description
473 )
474 )
474 Session().commit()
475 Session().commit()
475 h.flash(_('Successfully opened new pull request'),
476 h.flash(_('Successfully opened new pull request'),
476 category='success')
477 category='success')
477 except Exception as e:
478 except Exception as e:
478 msg = _('Error occurred during sending pull request')
479 msg = _('Error occurred during sending pull request')
479 log.exception(msg)
480 log.exception(msg)
480 h.flash(msg, category='error')
481 h.flash(msg, category='error')
481 return redirect(url('pullrequest_home', repo_name=repo_name))
482 return redirect(url('pullrequest_home', repo_name=repo_name))
482
483
483 return redirect(url('pullrequest_show', repo_name=target_repo,
484 return redirect(url('pullrequest_show', repo_name=target_repo,
484 pull_request_id=pull_request.pull_request_id))
485 pull_request_id=pull_request.pull_request_id))
485
486
486 @LoginRequired()
487 @LoginRequired()
487 @NotAnonymous()
488 @NotAnonymous()
488 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
489 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
489 'repository.admin')
490 'repository.admin')
490 @auth.CSRFRequired()
491 @auth.CSRFRequired()
491 @jsonify
492 @jsonify
492 def update(self, repo_name, pull_request_id):
493 def update(self, repo_name, pull_request_id):
493 pull_request_id = safe_int(pull_request_id)
494 pull_request_id = safe_int(pull_request_id)
494 pull_request = PullRequest.get_or_404(pull_request_id)
495 pull_request = PullRequest.get_or_404(pull_request_id)
495 # only owner or admin can update it
496 # only owner or admin can update it
496 allowed_to_update = PullRequestModel().check_user_update(
497 allowed_to_update = PullRequestModel().check_user_update(
497 pull_request, c.rhodecode_user)
498 pull_request, c.rhodecode_user)
498 if allowed_to_update:
499 if allowed_to_update:
499 controls = peppercorn.parse(request.POST.items())
500 controls = peppercorn.parse(request.POST.items())
500
501
501 if 'review_members' in controls:
502 if 'review_members' in controls:
502 self._update_reviewers(
503 self._update_reviewers(
503 pull_request_id, controls['review_members'])
504 pull_request_id, controls['review_members'])
504 elif str2bool(request.POST.get('update_commits', 'false')):
505 elif str2bool(request.POST.get('update_commits', 'false')):
505 self._update_commits(pull_request)
506 self._update_commits(pull_request)
506 elif str2bool(request.POST.get('close_pull_request', 'false')):
507 elif str2bool(request.POST.get('close_pull_request', 'false')):
507 self._reject_close(pull_request)
508 self._reject_close(pull_request)
508 elif str2bool(request.POST.get('edit_pull_request', 'false')):
509 elif str2bool(request.POST.get('edit_pull_request', 'false')):
509 self._edit_pull_request(pull_request)
510 self._edit_pull_request(pull_request)
510 else:
511 else:
511 raise HTTPBadRequest()
512 raise HTTPBadRequest()
512 return True
513 return True
513 raise HTTPForbidden()
514 raise HTTPForbidden()
514
515
515 def _edit_pull_request(self, pull_request):
516 def _edit_pull_request(self, pull_request):
516 try:
517 try:
517 PullRequestModel().edit(
518 PullRequestModel().edit(
518 pull_request, request.POST.get('title'),
519 pull_request, request.POST.get('title'),
519 request.POST.get('description'))
520 request.POST.get('description'))
520 except ValueError:
521 except ValueError:
521 msg = _(u'Cannot update closed pull requests.')
522 msg = _(u'Cannot update closed pull requests.')
522 h.flash(msg, category='error')
523 h.flash(msg, category='error')
523 return
524 return
524 else:
525 else:
525 Session().commit()
526 Session().commit()
526
527
527 msg = _(u'Pull request title & description updated.')
528 msg = _(u'Pull request title & description updated.')
528 h.flash(msg, category='success')
529 h.flash(msg, category='success')
529 return
530 return
530
531
531 def _update_commits(self, pull_request):
532 def _update_commits(self, pull_request):
532 resp = PullRequestModel().update_commits(pull_request)
533 resp = PullRequestModel().update_commits(pull_request)
533
534
534 if resp.executed:
535 if resp.executed:
535 msg = _(
536 msg = _(
536 u'Pull request updated to "{source_commit_id}" with '
537 u'Pull request updated to "{source_commit_id}" with '
537 u'{count_added} added, {count_removed} removed commits.')
538 u'{count_added} added, {count_removed} removed commits.')
538 msg = msg.format(
539 msg = msg.format(
539 source_commit_id=pull_request.source_ref_parts.commit_id,
540 source_commit_id=pull_request.source_ref_parts.commit_id,
540 count_added=len(resp.changes.added),
541 count_added=len(resp.changes.added),
541 count_removed=len(resp.changes.removed))
542 count_removed=len(resp.changes.removed))
542 h.flash(msg, category='success')
543 h.flash(msg, category='success')
543
544
544 registry = get_current_registry()
545 registry = get_current_registry()
545 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
546 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
546 channelstream_config = rhodecode_plugins.get('channelstream', {})
547 channelstream_config = rhodecode_plugins.get('channelstream', {})
547 if channelstream_config.get('enabled'):
548 if channelstream_config.get('enabled'):
548 message = msg + (
549 message = msg + (
549 ' - <a onclick="window.location.reload()">'
550 ' - <a onclick="window.location.reload()">'
550 '<strong>{}</strong></a>'.format(_('Reload page')))
551 '<strong>{}</strong></a>'.format(_('Reload page')))
551 channel = '/repo${}$/pr/{}'.format(
552 channel = '/repo${}$/pr/{}'.format(
552 pull_request.target_repo.repo_name,
553 pull_request.target_repo.repo_name,
553 pull_request.pull_request_id
554 pull_request.pull_request_id
554 )
555 )
555 payload = {
556 payload = {
556 'type': 'message',
557 'type': 'message',
557 'user': 'system',
558 'user': 'system',
558 'exclude_users': [request.user.username],
559 'exclude_users': [request.user.username],
559 'channel': channel,
560 'channel': channel,
560 'message': {
561 'message': {
561 'message': message,
562 'message': message,
562 'level': 'success',
563 'level': 'success',
563 'topic': '/notifications'
564 'topic': '/notifications'
564 }
565 }
565 }
566 }
566 channelstream_request(
567 channelstream_request(
567 channelstream_config, [payload], '/message',
568 channelstream_config, [payload], '/message',
568 raise_exc=False)
569 raise_exc=False)
569 else:
570 else:
570 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
571 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
571 warning_reasons = [
572 warning_reasons = [
572 UpdateFailureReason.NO_CHANGE,
573 UpdateFailureReason.NO_CHANGE,
573 UpdateFailureReason.WRONG_REF_TPYE,
574 UpdateFailureReason.WRONG_REF_TPYE,
574 ]
575 ]
575 category = 'warning' if resp.reason in warning_reasons else 'error'
576 category = 'warning' if resp.reason in warning_reasons else 'error'
576 h.flash(msg, category=category)
577 h.flash(msg, category=category)
577
578
578 @auth.CSRFRequired()
579 @auth.CSRFRequired()
579 @LoginRequired()
580 @LoginRequired()
580 @NotAnonymous()
581 @NotAnonymous()
581 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
582 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
582 'repository.admin')
583 'repository.admin')
583 def merge(self, repo_name, pull_request_id):
584 def merge(self, repo_name, pull_request_id):
584 """
585 """
585 POST /{repo_name}/pull-request/{pull_request_id}
586 POST /{repo_name}/pull-request/{pull_request_id}
586
587
587 Merge will perform a server-side merge of the specified
588 Merge will perform a server-side merge of the specified
588 pull request, if the pull request is approved and mergeable.
589 pull request, if the pull request is approved and mergeable.
589 After succesfull merging, the pull request is automatically
590 After succesfull merging, the pull request is automatically
590 closed, with a relevant comment.
591 closed, with a relevant comment.
591 """
592 """
592 pull_request_id = safe_int(pull_request_id)
593 pull_request_id = safe_int(pull_request_id)
593 pull_request = PullRequest.get_or_404(pull_request_id)
594 pull_request = PullRequest.get_or_404(pull_request_id)
594 user = c.rhodecode_user
595 user = c.rhodecode_user
595
596
596 if self._meets_merge_pre_conditions(pull_request, user):
597 if self._meets_merge_pre_conditions(pull_request, user):
597 log.debug("Pre-conditions checked, trying to merge.")
598 log.debug("Pre-conditions checked, trying to merge.")
598 extras = vcs_operation_context(
599 extras = vcs_operation_context(
599 request.environ, repo_name=pull_request.target_repo.repo_name,
600 request.environ, repo_name=pull_request.target_repo.repo_name,
600 username=user.username, action='push',
601 username=user.username, action='push',
601 scm=pull_request.target_repo.repo_type)
602 scm=pull_request.target_repo.repo_type)
602 self._merge_pull_request(pull_request, user, extras)
603 self._merge_pull_request(pull_request, user, extras)
603
604
604 return redirect(url(
605 return redirect(url(
605 'pullrequest_show',
606 'pullrequest_show',
606 repo_name=pull_request.target_repo.repo_name,
607 repo_name=pull_request.target_repo.repo_name,
607 pull_request_id=pull_request.pull_request_id))
608 pull_request_id=pull_request.pull_request_id))
608
609
609 def _meets_merge_pre_conditions(self, pull_request, user):
610 def _meets_merge_pre_conditions(self, pull_request, user):
610 if not PullRequestModel().check_user_merge(pull_request, user):
611 if not PullRequestModel().check_user_merge(pull_request, user):
611 raise HTTPForbidden()
612 raise HTTPForbidden()
612
613
613 merge_status, msg = PullRequestModel().merge_status(pull_request)
614 merge_status, msg = PullRequestModel().merge_status(pull_request)
614 if not merge_status:
615 if not merge_status:
615 log.debug("Cannot merge, not mergeable.")
616 log.debug("Cannot merge, not mergeable.")
616 h.flash(msg, category='error')
617 h.flash(msg, category='error')
617 return False
618 return False
618
619
619 if (pull_request.calculated_review_status()
620 if (pull_request.calculated_review_status()
620 is not ChangesetStatus.STATUS_APPROVED):
621 is not ChangesetStatus.STATUS_APPROVED):
621 log.debug("Cannot merge, approval is pending.")
622 log.debug("Cannot merge, approval is pending.")
622 msg = _('Pull request reviewer approval is pending.')
623 msg = _('Pull request reviewer approval is pending.')
623 h.flash(msg, category='error')
624 h.flash(msg, category='error')
624 return False
625 return False
625 return True
626 return True
626
627
627 def _merge_pull_request(self, pull_request, user, extras):
628 def _merge_pull_request(self, pull_request, user, extras):
628 merge_resp = PullRequestModel().merge(
629 merge_resp = PullRequestModel().merge(
629 pull_request, user, extras=extras)
630 pull_request, user, extras=extras)
630
631
631 if merge_resp.executed:
632 if merge_resp.executed:
632 log.debug("The merge was successful, closing the pull request.")
633 log.debug("The merge was successful, closing the pull request.")
633 PullRequestModel().close_pull_request(
634 PullRequestModel().close_pull_request(
634 pull_request.pull_request_id, user)
635 pull_request.pull_request_id, user)
635 Session().commit()
636 Session().commit()
636 msg = _('Pull request was successfully merged and closed.')
637 msg = _('Pull request was successfully merged and closed.')
637 h.flash(msg, category='success')
638 h.flash(msg, category='success')
638 else:
639 else:
639 log.debug(
640 log.debug(
640 "The merge was not successful. Merge response: %s",
641 "The merge was not successful. Merge response: %s",
641 merge_resp)
642 merge_resp)
642 msg = PullRequestModel().merge_status_message(
643 msg = PullRequestModel().merge_status_message(
643 merge_resp.failure_reason)
644 merge_resp.failure_reason)
644 h.flash(msg, category='error')
645 h.flash(msg, category='error')
645
646
646 def _update_reviewers(self, pull_request_id, review_members):
647 def _update_reviewers(self, pull_request_id, review_members):
647 reviewers = [
648 reviewers = [
648 (int(r['user_id']), r['reasons']) for r in review_members]
649 (int(r['user_id']), r['reasons']) for r in review_members]
649 PullRequestModel().update_reviewers(pull_request_id, reviewers)
650 PullRequestModel().update_reviewers(pull_request_id, reviewers)
650 Session().commit()
651 Session().commit()
651
652
652 def _reject_close(self, pull_request):
653 def _reject_close(self, pull_request):
653 if pull_request.is_closed():
654 if pull_request.is_closed():
654 raise HTTPForbidden()
655 raise HTTPForbidden()
655
656
656 PullRequestModel().close_pull_request_with_comment(
657 PullRequestModel().close_pull_request_with_comment(
657 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
658 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
658 Session().commit()
659 Session().commit()
659
660
660 @LoginRequired()
661 @LoginRequired()
661 @NotAnonymous()
662 @NotAnonymous()
662 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
663 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
663 'repository.admin')
664 'repository.admin')
664 @auth.CSRFRequired()
665 @auth.CSRFRequired()
665 @jsonify
666 @jsonify
666 def delete(self, repo_name, pull_request_id):
667 def delete(self, repo_name, pull_request_id):
667 pull_request_id = safe_int(pull_request_id)
668 pull_request_id = safe_int(pull_request_id)
668 pull_request = PullRequest.get_or_404(pull_request_id)
669 pull_request = PullRequest.get_or_404(pull_request_id)
669 # only owner can delete it !
670 # only owner can delete it !
670 if pull_request.author.user_id == c.rhodecode_user.user_id:
671 if pull_request.author.user_id == c.rhodecode_user.user_id:
671 PullRequestModel().delete(pull_request)
672 PullRequestModel().delete(pull_request)
672 Session().commit()
673 Session().commit()
673 h.flash(_('Successfully deleted pull request'),
674 h.flash(_('Successfully deleted pull request'),
674 category='success')
675 category='success')
675 return redirect(url('my_account_pullrequests'))
676 return redirect(url('my_account_pullrequests'))
676 raise HTTPForbidden()
677 raise HTTPForbidden()
677
678
679 def _get_pr_version(self, pull_request_id, version=None):
680 pull_request_id = safe_int(pull_request_id)
681 at_version = None
682 if version:
683 pull_request_ver = PullRequestVersion.get_or_404(version)
684 pull_request_obj = pull_request_ver
685 _org_pull_request_obj = pull_request_ver.pull_request
686 at_version = pull_request_ver.pull_request_version_id
687 else:
688 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
689
690 class PullRequestDisplay(object):
691 """
692 Special object wrapper for showing PullRequest data via Versions
693 It mimics PR object as close as possible. This is read only object
694 just for display
695 """
696 def __init__(self, attrs):
697 self.attrs = attrs
698 # internal have priority over the given ones via attrs
699 self.internal = ['versions']
700
701 def __getattr__(self, item):
702 if item in self.internal:
703 return getattr(self, item)
704 try:
705 return self.attrs[item]
706 except KeyError:
707 raise AttributeError(
708 '%s object has no attribute %s' % (self, item))
709
710 def versions(self):
711 return pull_request_obj.versions.order_by(
712 PullRequestVersion.pull_request_version_id).all()
713
714 def is_closed(self):
715 return pull_request_obj.is_closed()
716
717 attrs = UnsafeAttributeDict(pull_request_obj.get_api_data())
718
719 attrs.author = UnsafeAttributeDict(
720 pull_request_obj.author.get_api_data())
721 if pull_request_obj.target_repo:
722 attrs.target_repo = UnsafeAttributeDict(
723 pull_request_obj.target_repo.get_api_data())
724 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
725
726 if pull_request_obj.source_repo:
727 attrs.source_repo = UnsafeAttributeDict(
728 pull_request_obj.source_repo.get_api_data())
729 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
730
731 attrs.source_ref_parts = pull_request_obj.source_ref_parts
732 attrs.target_ref_parts = pull_request_obj.target_ref_parts
733
734 attrs.shadow_merge_ref = _org_pull_request_obj.shadow_merge_ref
735
736 pull_request_ver = PullRequestDisplay(attrs)
737
738 return _org_pull_request_obj, pull_request_obj, \
739 pull_request_ver, at_version
740
678 @LoginRequired()
741 @LoginRequired()
679 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
742 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
680 'repository.admin')
743 'repository.admin')
681 def show(self, repo_name, pull_request_id):
744 def show(self, repo_name, pull_request_id):
682 pull_request_id = safe_int(pull_request_id)
745 pull_request_id = safe_int(pull_request_id)
683 c.pull_request = PullRequest.get_or_404(pull_request_id)
746
747 version = request.GET.get('version')
748 pull_request_latest, \
749 pull_request, \
750 pull_request_ver, \
751 at_version = self._get_pr_version(pull_request_id, version=version)
684
752
685 c.template_context['pull_request_data']['pull_request_id'] = \
753 c.template_context['pull_request_data']['pull_request_id'] = \
686 pull_request_id
754 pull_request_id
687
755
688 # pull_requests repo_name we opened it against
756 # pull_requests repo_name we opened it against
689 # ie. target_repo must match
757 # ie. target_repo must match
690 if repo_name != c.pull_request.target_repo.repo_name:
758 if repo_name != pull_request.target_repo.repo_name:
691 raise HTTPNotFound
759 raise HTTPNotFound
692
760
693 c.allowed_to_change_status = PullRequestModel(). \
694 check_user_change_status(c.pull_request, c.rhodecode_user)
695 c.allowed_to_update = PullRequestModel().check_user_update(
696 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
697 c.allowed_to_merge = PullRequestModel().check_user_merge(
698 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
699 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
761 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
700 c.pull_request)
762 pull_request)
701 c.allowed_to_delete = PullRequestModel().check_user_delete(
763
702 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
764 if at_version:
765 c.allowed_to_change_status = False
766 else:
767 c.allowed_to_change_status = PullRequestModel(). \
768 check_user_change_status(pull_request, c.rhodecode_user)
769
770 if at_version:
771 c.allowed_to_update = False
772 else:
773 c.allowed_to_update = PullRequestModel().check_user_update(
774 pull_request, c.rhodecode_user) and not pull_request.is_closed()
775
776 if at_version:
777 c.allowed_to_merge = False
778 else:
779 c.allowed_to_merge = PullRequestModel().check_user_merge(
780 pull_request, c.rhodecode_user) and not pull_request.is_closed()
781
782 if at_version:
783 c.allowed_to_delete = False
784 else:
785 c.allowed_to_delete = PullRequestModel().check_user_delete(
786 pull_request, c.rhodecode_user) and not pull_request.is_closed()
787
788 if at_version:
789 c.allowed_to_comment = False
790 else:
791 c.allowed_to_comment = not pull_request.is_closed()
703
792
704 cc_model = ChangesetCommentsModel()
793 cc_model = ChangesetCommentsModel()
705
794
706 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
795 c.pull_request_reviewers = pull_request.reviewers_statuses()
707
796
708 c.pull_request_review_status = c.pull_request.calculated_review_status()
797 c.pull_request_review_status = pull_request.calculated_review_status()
709 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
798 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
710 c.pull_request)
799 pull_request)
711 c.approval_msg = None
800 c.approval_msg = None
712 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
801 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
713 c.approval_msg = _('Reviewer approval is pending.')
802 c.approval_msg = _('Reviewer approval is pending.')
714 c.pr_merge_status = False
803 c.pr_merge_status = False
715 # load compare data into template context
804 # load compare data into template context
716 enable_comments = not c.pull_request.is_closed()
805 enable_comments = not pull_request.is_closed()
717
718
806
719 # inline comments
807 # inline comments
720 c.inline_comments = cc_model.get_inline_comments(
808 c.inline_comments = cc_model.get_inline_comments(
721 c.rhodecode_db_repo.repo_id,
809 c.rhodecode_db_repo.repo_id,
722 pull_request=pull_request_id)
810 pull_request=pull_request_id)
723
811
724 c.inline_cnt = cc_model.get_inline_comments_count(
812 c.inline_cnt = cc_model.get_inline_comments_count(
725 c.inline_comments, version=at_version)
813 c.inline_comments, version=at_version)
726
814
727 self._load_compare_data(
815 self._load_compare_data(
728 c.pull_request, c.inline_comments, enable_comments=enable_comments)
816 pull_request, c.inline_comments, enable_comments=enable_comments)
729
817
730 # outdated comments
818 # outdated comments
731 c.outdated_comments = {}
819 c.outdated_comments = {}
732 c.outdated_cnt = 0
820 c.outdated_cnt = 0
733 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
821
822 if ChangesetCommentsModel.use_outdated_comments(pull_request):
734 c.outdated_comments = cc_model.get_outdated_comments(
823 c.outdated_comments = cc_model.get_outdated_comments(
735 c.rhodecode_db_repo.repo_id,
824 c.rhodecode_db_repo.repo_id,
736 pull_request=c.pull_request)
825 pull_request=pull_request)
826
737 # Count outdated comments and check for deleted files
827 # Count outdated comments and check for deleted files
738 for file_name, lines in c.outdated_comments.iteritems():
828 for file_name, lines in c.outdated_comments.iteritems():
739 for comments in lines.values():
829 for comments in lines.values():
830 comments = [comm for comm in comments
831 if comm.outdated_at_version(at_version)]
740 c.outdated_cnt += len(comments)
832 c.outdated_cnt += len(comments)
741 if file_name not in c.included_files:
833 if file_name not in c.included_files:
742 c.deleted_files.append(file_name)
834 c.deleted_files.append(file_name)
743
835
744
745 # this is a hack to properly display links, when creating PR, the
836 # this is a hack to properly display links, when creating PR, the
746 # compare view and others uses different notation, and
837 # compare view and others uses different notation, and
747 # compare_commits.html renders links based on the target_repo.
838 # compare_commits.html renders links based on the target_repo.
748 # We need to swap that here to generate it properly on the html side
839 # We need to swap that here to generate it properly on the html side
749 c.target_repo = c.source_repo
840 c.target_repo = c.source_repo
750
841
751 # comments
842 # comments
752 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
843 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
753 pull_request=pull_request_id)
844 pull_request=pull_request_id)
754
845
755 if c.allowed_to_update:
846 if c.allowed_to_update:
756 force_close = ('forced_closed', _('Close Pull Request'))
847 force_close = ('forced_closed', _('Close Pull Request'))
757 statuses = ChangesetStatus.STATUSES + [force_close]
848 statuses = ChangesetStatus.STATUSES + [force_close]
758 else:
849 else:
759 statuses = ChangesetStatus.STATUSES
850 statuses = ChangesetStatus.STATUSES
760 c.commit_statuses = statuses
851 c.commit_statuses = statuses
761
852
762 c.ancestor = None # TODO: add ancestor here
853 c.ancestor = None # TODO: add ancestor here
854 c.pull_request = pull_request_ver
855 c.pull_request_latest = pull_request_latest
856 c.at_version = at_version
763
857
764 return render('/pullrequests/pullrequest_show.html')
858 return render('/pullrequests/pullrequest_show.html')
765
859
766 @LoginRequired()
860 @LoginRequired()
767 @NotAnonymous()
861 @NotAnonymous()
768 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
862 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
769 'repository.admin')
863 'repository.admin')
770 @auth.CSRFRequired()
864 @auth.CSRFRequired()
771 @jsonify
865 @jsonify
772 def comment(self, repo_name, pull_request_id):
866 def comment(self, repo_name, pull_request_id):
773 pull_request_id = safe_int(pull_request_id)
867 pull_request_id = safe_int(pull_request_id)
774 pull_request = PullRequest.get_or_404(pull_request_id)
868 pull_request = PullRequest.get_or_404(pull_request_id)
775 if pull_request.is_closed():
869 if pull_request.is_closed():
776 raise HTTPForbidden()
870 raise HTTPForbidden()
777
871
778 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
872 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
779 # as a changeset status, still we want to send it in one value.
873 # as a changeset status, still we want to send it in one value.
780 status = request.POST.get('changeset_status', None)
874 status = request.POST.get('changeset_status', None)
781 text = request.POST.get('text')
875 text = request.POST.get('text')
782 if status and '_closed' in status:
876 if status and '_closed' in status:
783 close_pr = True
877 close_pr = True
784 status = status.replace('_closed', '')
878 status = status.replace('_closed', '')
785 else:
879 else:
786 close_pr = False
880 close_pr = False
787
881
788 forced = (status == 'forced')
882 forced = (status == 'forced')
789 if forced:
883 if forced:
790 status = 'rejected'
884 status = 'rejected'
791
885
792 allowed_to_change_status = PullRequestModel().check_user_change_status(
886 allowed_to_change_status = PullRequestModel().check_user_change_status(
793 pull_request, c.rhodecode_user)
887 pull_request, c.rhodecode_user)
794
888
795 if status and allowed_to_change_status:
889 if status and allowed_to_change_status:
796 message = (_('Status change %(transition_icon)s %(status)s')
890 message = (_('Status change %(transition_icon)s %(status)s')
797 % {'transition_icon': '>',
891 % {'transition_icon': '>',
798 'status': ChangesetStatus.get_status_lbl(status)})
892 'status': ChangesetStatus.get_status_lbl(status)})
799 if close_pr:
893 if close_pr:
800 message = _('Closing with') + ' ' + message
894 message = _('Closing with') + ' ' + message
801 text = text or message
895 text = text or message
802 comm = ChangesetCommentsModel().create(
896 comm = ChangesetCommentsModel().create(
803 text=text,
897 text=text,
804 repo=c.rhodecode_db_repo.repo_id,
898 repo=c.rhodecode_db_repo.repo_id,
805 user=c.rhodecode_user.user_id,
899 user=c.rhodecode_user.user_id,
806 pull_request=pull_request_id,
900 pull_request=pull_request_id,
807 f_path=request.POST.get('f_path'),
901 f_path=request.POST.get('f_path'),
808 line_no=request.POST.get('line'),
902 line_no=request.POST.get('line'),
809 status_change=(ChangesetStatus.get_status_lbl(status)
903 status_change=(ChangesetStatus.get_status_lbl(status)
810 if status and allowed_to_change_status else None),
904 if status and allowed_to_change_status else None),
811 status_change_type=(status
905 status_change_type=(status
812 if status and allowed_to_change_status else None),
906 if status and allowed_to_change_status else None),
813 closing_pr=close_pr
907 closing_pr=close_pr
814 )
908 )
815
909
816
817
818 if allowed_to_change_status:
910 if allowed_to_change_status:
819 old_calculated_status = pull_request.calculated_review_status()
911 old_calculated_status = pull_request.calculated_review_status()
820 # get status if set !
912 # get status if set !
821 if status:
913 if status:
822 ChangesetStatusModel().set_status(
914 ChangesetStatusModel().set_status(
823 c.rhodecode_db_repo.repo_id,
915 c.rhodecode_db_repo.repo_id,
824 status,
916 status,
825 c.rhodecode_user.user_id,
917 c.rhodecode_user.user_id,
826 comm,
918 comm,
827 pull_request=pull_request_id
919 pull_request=pull_request_id
828 )
920 )
829
921
830 Session().flush()
922 Session().flush()
831 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
923 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
832 # we now calculate the status of pull request, and based on that
924 # we now calculate the status of pull request, and based on that
833 # calculation we set the commits status
925 # calculation we set the commits status
834 calculated_status = pull_request.calculated_review_status()
926 calculated_status = pull_request.calculated_review_status()
835 if old_calculated_status != calculated_status:
927 if old_calculated_status != calculated_status:
836 PullRequestModel()._trigger_pull_request_hook(
928 PullRequestModel()._trigger_pull_request_hook(
837 pull_request, c.rhodecode_user, 'review_status_change')
929 pull_request, c.rhodecode_user, 'review_status_change')
838
930
839 calculated_status_lbl = ChangesetStatus.get_status_lbl(
931 calculated_status_lbl = ChangesetStatus.get_status_lbl(
840 calculated_status)
932 calculated_status)
841
933
842 if close_pr:
934 if close_pr:
843 status_completed = (
935 status_completed = (
844 calculated_status in [ChangesetStatus.STATUS_APPROVED,
936 calculated_status in [ChangesetStatus.STATUS_APPROVED,
845 ChangesetStatus.STATUS_REJECTED])
937 ChangesetStatus.STATUS_REJECTED])
846 if forced or status_completed:
938 if forced or status_completed:
847 PullRequestModel().close_pull_request(
939 PullRequestModel().close_pull_request(
848 pull_request_id, c.rhodecode_user)
940 pull_request_id, c.rhodecode_user)
849 else:
941 else:
850 h.flash(_('Closing pull request on other statuses than '
942 h.flash(_('Closing pull request on other statuses than '
851 'rejected or approved is forbidden. '
943 'rejected or approved is forbidden. '
852 'Calculated status from all reviewers '
944 'Calculated status from all reviewers '
853 'is currently: %s') % calculated_status_lbl,
945 'is currently: %s') % calculated_status_lbl,
854 category='warning')
946 category='warning')
855
947
856 Session().commit()
948 Session().commit()
857
949
858 if not request.is_xhr:
950 if not request.is_xhr:
859 return redirect(h.url('pullrequest_show', repo_name=repo_name,
951 return redirect(h.url('pullrequest_show', repo_name=repo_name,
860 pull_request_id=pull_request_id))
952 pull_request_id=pull_request_id))
861
953
862 data = {
954 data = {
863 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
955 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
864 }
956 }
865 if comm:
957 if comm:
866 c.co = comm
958 c.co = comm
867 data.update(comm.get_dict())
959 data.update(comm.get_dict())
868 data.update({'rendered_text':
960 data.update({'rendered_text':
869 render('changeset/changeset_comment_block.html')})
961 render('changeset/changeset_comment_block.html')})
870
962
871 return data
963 return data
872
964
873 @LoginRequired()
965 @LoginRequired()
874 @NotAnonymous()
966 @NotAnonymous()
875 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
967 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
876 'repository.admin')
968 'repository.admin')
877 @auth.CSRFRequired()
969 @auth.CSRFRequired()
878 @jsonify
970 @jsonify
879 def delete_comment(self, repo_name, comment_id):
971 def delete_comment(self, repo_name, comment_id):
880 return self._delete_comment(comment_id)
972 return self._delete_comment(comment_id)
881
973
882 def _delete_comment(self, comment_id):
974 def _delete_comment(self, comment_id):
883 comment_id = safe_int(comment_id)
975 comment_id = safe_int(comment_id)
884 co = ChangesetComment.get_or_404(comment_id)
976 co = ChangesetComment.get_or_404(comment_id)
885 if co.pull_request.is_closed():
977 if co.pull_request.is_closed():
886 # don't allow deleting comments on closed pull request
978 # don't allow deleting comments on closed pull request
887 raise HTTPForbidden()
979 raise HTTPForbidden()
888
980
889 is_owner = co.author.user_id == c.rhodecode_user.user_id
981 is_owner = co.author.user_id == c.rhodecode_user.user_id
890 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
982 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
891 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
983 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
892 old_calculated_status = co.pull_request.calculated_review_status()
984 old_calculated_status = co.pull_request.calculated_review_status()
893 ChangesetCommentsModel().delete(comment=co)
985 ChangesetCommentsModel().delete(comment=co)
894 Session().commit()
986 Session().commit()
895 calculated_status = co.pull_request.calculated_review_status()
987 calculated_status = co.pull_request.calculated_review_status()
896 if old_calculated_status != calculated_status:
988 if old_calculated_status != calculated_status:
897 PullRequestModel()._trigger_pull_request_hook(
989 PullRequestModel()._trigger_pull_request_hook(
898 co.pull_request, c.rhodecode_user, 'review_status_change')
990 co.pull_request, c.rhodecode_user, 'review_status_change')
899 return True
991 return True
900 else:
992 else:
901 raise HTTPForbidden()
993 raise HTTPForbidden()
@@ -1,936 +1,946 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 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 Some simple helper functions
23 Some simple helper functions
24 """
24 """
25
25
26
26
27 import collections
27 import collections
28 import datetime
28 import datetime
29 import dateutil.relativedelta
29 import dateutil.relativedelta
30 import hashlib
30 import hashlib
31 import logging
31 import logging
32 import re
32 import re
33 import sys
33 import sys
34 import time
34 import time
35 import threading
35 import threading
36 import urllib
36 import urllib
37 import urlobject
37 import urlobject
38 import uuid
38 import uuid
39
39
40 import pygments.lexers
40 import pygments.lexers
41 import sqlalchemy
41 import sqlalchemy
42 import sqlalchemy.engine.url
42 import sqlalchemy.engine.url
43 import webob
43 import webob
44 import routes.util
44 import routes.util
45
45
46 import rhodecode
46 import rhodecode
47
47
48
48
49 def md5(s):
49 def md5(s):
50 return hashlib.md5(s).hexdigest()
50 return hashlib.md5(s).hexdigest()
51
51
52
52
53 def md5_safe(s):
53 def md5_safe(s):
54 return md5(safe_str(s))
54 return md5(safe_str(s))
55
55
56
56
57 def __get_lem(extra_mapping=None):
57 def __get_lem(extra_mapping=None):
58 """
58 """
59 Get language extension map based on what's inside pygments lexers
59 Get language extension map based on what's inside pygments lexers
60 """
60 """
61 d = collections.defaultdict(lambda: [])
61 d = collections.defaultdict(lambda: [])
62
62
63 def __clean(s):
63 def __clean(s):
64 s = s.lstrip('*')
64 s = s.lstrip('*')
65 s = s.lstrip('.')
65 s = s.lstrip('.')
66
66
67 if s.find('[') != -1:
67 if s.find('[') != -1:
68 exts = []
68 exts = []
69 start, stop = s.find('['), s.find(']')
69 start, stop = s.find('['), s.find(']')
70
70
71 for suffix in s[start + 1:stop]:
71 for suffix in s[start + 1:stop]:
72 exts.append(s[:s.find('[')] + suffix)
72 exts.append(s[:s.find('[')] + suffix)
73 return [e.lower() for e in exts]
73 return [e.lower() for e in exts]
74 else:
74 else:
75 return [s.lower()]
75 return [s.lower()]
76
76
77 for lx, t in sorted(pygments.lexers.LEXERS.items()):
77 for lx, t in sorted(pygments.lexers.LEXERS.items()):
78 m = map(__clean, t[-2])
78 m = map(__clean, t[-2])
79 if m:
79 if m:
80 m = reduce(lambda x, y: x + y, m)
80 m = reduce(lambda x, y: x + y, m)
81 for ext in m:
81 for ext in m:
82 desc = lx.replace('Lexer', '')
82 desc = lx.replace('Lexer', '')
83 d[ext].append(desc)
83 d[ext].append(desc)
84
84
85 data = dict(d)
85 data = dict(d)
86
86
87 extra_mapping = extra_mapping or {}
87 extra_mapping = extra_mapping or {}
88 if extra_mapping:
88 if extra_mapping:
89 for k, v in extra_mapping.items():
89 for k, v in extra_mapping.items():
90 if k not in data:
90 if k not in data:
91 # register new mapping2lexer
91 # register new mapping2lexer
92 data[k] = [v]
92 data[k] = [v]
93
93
94 return data
94 return data
95
95
96
96
97 def str2bool(_str):
97 def str2bool(_str):
98 """
98 """
99 returns True/False value from given string, it tries to translate the
99 returns True/False value from given string, it tries to translate the
100 string into boolean
100 string into boolean
101
101
102 :param _str: string value to translate into boolean
102 :param _str: string value to translate into boolean
103 :rtype: boolean
103 :rtype: boolean
104 :returns: boolean from given string
104 :returns: boolean from given string
105 """
105 """
106 if _str is None:
106 if _str is None:
107 return False
107 return False
108 if _str in (True, False):
108 if _str in (True, False):
109 return _str
109 return _str
110 _str = str(_str).strip().lower()
110 _str = str(_str).strip().lower()
111 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
111 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
112
112
113
113
114 def aslist(obj, sep=None, strip=True):
114 def aslist(obj, sep=None, strip=True):
115 """
115 """
116 Returns given string separated by sep as list
116 Returns given string separated by sep as list
117
117
118 :param obj:
118 :param obj:
119 :param sep:
119 :param sep:
120 :param strip:
120 :param strip:
121 """
121 """
122 if isinstance(obj, (basestring,)):
122 if isinstance(obj, (basestring,)):
123 lst = obj.split(sep)
123 lst = obj.split(sep)
124 if strip:
124 if strip:
125 lst = [v.strip() for v in lst]
125 lst = [v.strip() for v in lst]
126 return lst
126 return lst
127 elif isinstance(obj, (list, tuple)):
127 elif isinstance(obj, (list, tuple)):
128 return obj
128 return obj
129 elif obj is None:
129 elif obj is None:
130 return []
130 return []
131 else:
131 else:
132 return [obj]
132 return [obj]
133
133
134
134
135 def convert_line_endings(line, mode):
135 def convert_line_endings(line, mode):
136 """
136 """
137 Converts a given line "line end" accordingly to given mode
137 Converts a given line "line end" accordingly to given mode
138
138
139 Available modes are::
139 Available modes are::
140 0 - Unix
140 0 - Unix
141 1 - Mac
141 1 - Mac
142 2 - DOS
142 2 - DOS
143
143
144 :param line: given line to convert
144 :param line: given line to convert
145 :param mode: mode to convert to
145 :param mode: mode to convert to
146 :rtype: str
146 :rtype: str
147 :return: converted line according to mode
147 :return: converted line according to mode
148 """
148 """
149 if mode == 0:
149 if mode == 0:
150 line = line.replace('\r\n', '\n')
150 line = line.replace('\r\n', '\n')
151 line = line.replace('\r', '\n')
151 line = line.replace('\r', '\n')
152 elif mode == 1:
152 elif mode == 1:
153 line = line.replace('\r\n', '\r')
153 line = line.replace('\r\n', '\r')
154 line = line.replace('\n', '\r')
154 line = line.replace('\n', '\r')
155 elif mode == 2:
155 elif mode == 2:
156 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
156 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
157 return line
157 return line
158
158
159
159
160 def detect_mode(line, default):
160 def detect_mode(line, default):
161 """
161 """
162 Detects line break for given line, if line break couldn't be found
162 Detects line break for given line, if line break couldn't be found
163 given default value is returned
163 given default value is returned
164
164
165 :param line: str line
165 :param line: str line
166 :param default: default
166 :param default: default
167 :rtype: int
167 :rtype: int
168 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
168 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
169 """
169 """
170 if line.endswith('\r\n'):
170 if line.endswith('\r\n'):
171 return 2
171 return 2
172 elif line.endswith('\n'):
172 elif line.endswith('\n'):
173 return 0
173 return 0
174 elif line.endswith('\r'):
174 elif line.endswith('\r'):
175 return 1
175 return 1
176 else:
176 else:
177 return default
177 return default
178
178
179
179
180 def safe_int(val, default=None):
180 def safe_int(val, default=None):
181 """
181 """
182 Returns int() of val if val is not convertable to int use default
182 Returns int() of val if val is not convertable to int use default
183 instead
183 instead
184
184
185 :param val:
185 :param val:
186 :param default:
186 :param default:
187 """
187 """
188
188
189 try:
189 try:
190 val = int(val)
190 val = int(val)
191 except (ValueError, TypeError):
191 except (ValueError, TypeError):
192 val = default
192 val = default
193
193
194 return val
194 return val
195
195
196
196
197 def safe_unicode(str_, from_encoding=None):
197 def safe_unicode(str_, from_encoding=None):
198 """
198 """
199 safe unicode function. Does few trick to turn str_ into unicode
199 safe unicode function. Does few trick to turn str_ into unicode
200
200
201 In case of UnicodeDecode error, we try to return it with encoding detected
201 In case of UnicodeDecode error, we try to return it with encoding detected
202 by chardet library if it fails fallback to unicode with errors replaced
202 by chardet library if it fails fallback to unicode with errors replaced
203
203
204 :param str_: string to decode
204 :param str_: string to decode
205 :rtype: unicode
205 :rtype: unicode
206 :returns: unicode object
206 :returns: unicode object
207 """
207 """
208 if isinstance(str_, unicode):
208 if isinstance(str_, unicode):
209 return str_
209 return str_
210
210
211 if not from_encoding:
211 if not from_encoding:
212 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
212 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
213 'utf8'), sep=',')
213 'utf8'), sep=',')
214 from_encoding = DEFAULT_ENCODINGS
214 from_encoding = DEFAULT_ENCODINGS
215
215
216 if not isinstance(from_encoding, (list, tuple)):
216 if not isinstance(from_encoding, (list, tuple)):
217 from_encoding = [from_encoding]
217 from_encoding = [from_encoding]
218
218
219 try:
219 try:
220 return unicode(str_)
220 return unicode(str_)
221 except UnicodeDecodeError:
221 except UnicodeDecodeError:
222 pass
222 pass
223
223
224 for enc in from_encoding:
224 for enc in from_encoding:
225 try:
225 try:
226 return unicode(str_, enc)
226 return unicode(str_, enc)
227 except UnicodeDecodeError:
227 except UnicodeDecodeError:
228 pass
228 pass
229
229
230 try:
230 try:
231 import chardet
231 import chardet
232 encoding = chardet.detect(str_)['encoding']
232 encoding = chardet.detect(str_)['encoding']
233 if encoding is None:
233 if encoding is None:
234 raise Exception()
234 raise Exception()
235 return str_.decode(encoding)
235 return str_.decode(encoding)
236 except (ImportError, UnicodeDecodeError, Exception):
236 except (ImportError, UnicodeDecodeError, Exception):
237 return unicode(str_, from_encoding[0], 'replace')
237 return unicode(str_, from_encoding[0], 'replace')
238
238
239
239
240 def safe_str(unicode_, to_encoding=None):
240 def safe_str(unicode_, to_encoding=None):
241 """
241 """
242 safe str function. Does few trick to turn unicode_ into string
242 safe str function. Does few trick to turn unicode_ into string
243
243
244 In case of UnicodeEncodeError, we try to return it with encoding detected
244 In case of UnicodeEncodeError, we try to return it with encoding detected
245 by chardet library if it fails fallback to string with errors replaced
245 by chardet library if it fails fallback to string with errors replaced
246
246
247 :param unicode_: unicode to encode
247 :param unicode_: unicode to encode
248 :rtype: str
248 :rtype: str
249 :returns: str object
249 :returns: str object
250 """
250 """
251
251
252 # if it's not basestr cast to str
252 # if it's not basestr cast to str
253 if not isinstance(unicode_, basestring):
253 if not isinstance(unicode_, basestring):
254 return str(unicode_)
254 return str(unicode_)
255
255
256 if isinstance(unicode_, str):
256 if isinstance(unicode_, str):
257 return unicode_
257 return unicode_
258
258
259 if not to_encoding:
259 if not to_encoding:
260 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
260 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
261 'utf8'), sep=',')
261 'utf8'), sep=',')
262 to_encoding = DEFAULT_ENCODINGS
262 to_encoding = DEFAULT_ENCODINGS
263
263
264 if not isinstance(to_encoding, (list, tuple)):
264 if not isinstance(to_encoding, (list, tuple)):
265 to_encoding = [to_encoding]
265 to_encoding = [to_encoding]
266
266
267 for enc in to_encoding:
267 for enc in to_encoding:
268 try:
268 try:
269 return unicode_.encode(enc)
269 return unicode_.encode(enc)
270 except UnicodeEncodeError:
270 except UnicodeEncodeError:
271 pass
271 pass
272
272
273 try:
273 try:
274 import chardet
274 import chardet
275 encoding = chardet.detect(unicode_)['encoding']
275 encoding = chardet.detect(unicode_)['encoding']
276 if encoding is None:
276 if encoding is None:
277 raise UnicodeEncodeError()
277 raise UnicodeEncodeError()
278
278
279 return unicode_.encode(encoding)
279 return unicode_.encode(encoding)
280 except (ImportError, UnicodeEncodeError):
280 except (ImportError, UnicodeEncodeError):
281 return unicode_.encode(to_encoding[0], 'replace')
281 return unicode_.encode(to_encoding[0], 'replace')
282
282
283
283
284 def remove_suffix(s, suffix):
284 def remove_suffix(s, suffix):
285 if s.endswith(suffix):
285 if s.endswith(suffix):
286 s = s[:-1 * len(suffix)]
286 s = s[:-1 * len(suffix)]
287 return s
287 return s
288
288
289
289
290 def remove_prefix(s, prefix):
290 def remove_prefix(s, prefix):
291 if s.startswith(prefix):
291 if s.startswith(prefix):
292 s = s[len(prefix):]
292 s = s[len(prefix):]
293 return s
293 return s
294
294
295
295
296 def find_calling_context(ignore_modules=None):
296 def find_calling_context(ignore_modules=None):
297 """
297 """
298 Look through the calling stack and return the frame which called
298 Look through the calling stack and return the frame which called
299 this function and is part of core module ( ie. rhodecode.* )
299 this function and is part of core module ( ie. rhodecode.* )
300
300
301 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
301 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
302 """
302 """
303
303
304 ignore_modules = ignore_modules or []
304 ignore_modules = ignore_modules or []
305
305
306 f = sys._getframe(2)
306 f = sys._getframe(2)
307 while f.f_back is not None:
307 while f.f_back is not None:
308 name = f.f_globals.get('__name__')
308 name = f.f_globals.get('__name__')
309 if name and name.startswith(__name__.split('.')[0]):
309 if name and name.startswith(__name__.split('.')[0]):
310 if name not in ignore_modules:
310 if name not in ignore_modules:
311 return f
311 return f
312 f = f.f_back
312 f = f.f_back
313 return None
313 return None
314
314
315
315
316 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
316 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
317 """Custom engine_from_config functions."""
317 """Custom engine_from_config functions."""
318 log = logging.getLogger('sqlalchemy.engine')
318 log = logging.getLogger('sqlalchemy.engine')
319 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
319 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
320
320
321 def color_sql(sql):
321 def color_sql(sql):
322 color_seq = '\033[1;33m' # This is yellow: code 33
322 color_seq = '\033[1;33m' # This is yellow: code 33
323 normal = '\x1b[0m'
323 normal = '\x1b[0m'
324 return ''.join([color_seq, sql, normal])
324 return ''.join([color_seq, sql, normal])
325
325
326 if configuration['debug']:
326 if configuration['debug']:
327 # attach events only for debug configuration
327 # attach events only for debug configuration
328
328
329 def before_cursor_execute(conn, cursor, statement,
329 def before_cursor_execute(conn, cursor, statement,
330 parameters, context, executemany):
330 parameters, context, executemany):
331 setattr(conn, 'query_start_time', time.time())
331 setattr(conn, 'query_start_time', time.time())
332 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
332 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
333 calling_context = find_calling_context(ignore_modules=[
333 calling_context = find_calling_context(ignore_modules=[
334 'rhodecode.lib.caching_query',
334 'rhodecode.lib.caching_query',
335 'rhodecode.model.settings',
335 'rhodecode.model.settings',
336 ])
336 ])
337 if calling_context:
337 if calling_context:
338 log.info(color_sql('call context %s:%s' % (
338 log.info(color_sql('call context %s:%s' % (
339 calling_context.f_code.co_filename,
339 calling_context.f_code.co_filename,
340 calling_context.f_lineno,
340 calling_context.f_lineno,
341 )))
341 )))
342
342
343 def after_cursor_execute(conn, cursor, statement,
343 def after_cursor_execute(conn, cursor, statement,
344 parameters, context, executemany):
344 parameters, context, executemany):
345 delattr(conn, 'query_start_time')
345 delattr(conn, 'query_start_time')
346
346
347 sqlalchemy.event.listen(engine, "before_cursor_execute",
347 sqlalchemy.event.listen(engine, "before_cursor_execute",
348 before_cursor_execute)
348 before_cursor_execute)
349 sqlalchemy.event.listen(engine, "after_cursor_execute",
349 sqlalchemy.event.listen(engine, "after_cursor_execute",
350 after_cursor_execute)
350 after_cursor_execute)
351
351
352 return engine
352 return engine
353
353
354
354
355 def get_encryption_key(config):
355 def get_encryption_key(config):
356 secret = config.get('rhodecode.encrypted_values.secret')
356 secret = config.get('rhodecode.encrypted_values.secret')
357 default = config['beaker.session.secret']
357 default = config['beaker.session.secret']
358 return secret or default
358 return secret or default
359
359
360
360
361 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
361 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
362 short_format=False):
362 short_format=False):
363 """
363 """
364 Turns a datetime into an age string.
364 Turns a datetime into an age string.
365 If show_short_version is True, this generates a shorter string with
365 If show_short_version is True, this generates a shorter string with
366 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
366 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
367
367
368 * IMPORTANT*
368 * IMPORTANT*
369 Code of this function is written in special way so it's easier to
369 Code of this function is written in special way so it's easier to
370 backport it to javascript. If you mean to update it, please also update
370 backport it to javascript. If you mean to update it, please also update
371 `jquery.timeago-extension.js` file
371 `jquery.timeago-extension.js` file
372
372
373 :param prevdate: datetime object
373 :param prevdate: datetime object
374 :param now: get current time, if not define we use
374 :param now: get current time, if not define we use
375 `datetime.datetime.now()`
375 `datetime.datetime.now()`
376 :param show_short_version: if it should approximate the date and
376 :param show_short_version: if it should approximate the date and
377 return a shorter string
377 return a shorter string
378 :param show_suffix:
378 :param show_suffix:
379 :param short_format: show short format, eg 2D instead of 2 days
379 :param short_format: show short format, eg 2D instead of 2 days
380 :rtype: unicode
380 :rtype: unicode
381 :returns: unicode words describing age
381 :returns: unicode words describing age
382 """
382 """
383 from pylons.i18n.translation import _, ungettext
383 from pylons.i18n.translation import _, ungettext
384
384
385 def _get_relative_delta(now, prevdate):
385 def _get_relative_delta(now, prevdate):
386 base = dateutil.relativedelta.relativedelta(now, prevdate)
386 base = dateutil.relativedelta.relativedelta(now, prevdate)
387 return {
387 return {
388 'year': base.years,
388 'year': base.years,
389 'month': base.months,
389 'month': base.months,
390 'day': base.days,
390 'day': base.days,
391 'hour': base.hours,
391 'hour': base.hours,
392 'minute': base.minutes,
392 'minute': base.minutes,
393 'second': base.seconds,
393 'second': base.seconds,
394 }
394 }
395
395
396 def _is_leap_year(year):
396 def _is_leap_year(year):
397 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
397 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
398
398
399 def get_month(prevdate):
399 def get_month(prevdate):
400 return prevdate.month
400 return prevdate.month
401
401
402 def get_year(prevdate):
402 def get_year(prevdate):
403 return prevdate.year
403 return prevdate.year
404
404
405 now = now or datetime.datetime.now()
405 now = now or datetime.datetime.now()
406 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
406 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
407 deltas = {}
407 deltas = {}
408 future = False
408 future = False
409
409
410 if prevdate > now:
410 if prevdate > now:
411 now_old = now
411 now_old = now
412 now = prevdate
412 now = prevdate
413 prevdate = now_old
413 prevdate = now_old
414 future = True
414 future = True
415 if future:
415 if future:
416 prevdate = prevdate.replace(microsecond=0)
416 prevdate = prevdate.replace(microsecond=0)
417 # Get date parts deltas
417 # Get date parts deltas
418 for part in order:
418 for part in order:
419 rel_delta = _get_relative_delta(now, prevdate)
419 rel_delta = _get_relative_delta(now, prevdate)
420 deltas[part] = rel_delta[part]
420 deltas[part] = rel_delta[part]
421
421
422 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
422 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
423 # not 1 hour, -59 minutes and -59 seconds)
423 # not 1 hour, -59 minutes and -59 seconds)
424 offsets = [[5, 60], [4, 60], [3, 24]]
424 offsets = [[5, 60], [4, 60], [3, 24]]
425 for element in offsets: # seconds, minutes, hours
425 for element in offsets: # seconds, minutes, hours
426 num = element[0]
426 num = element[0]
427 length = element[1]
427 length = element[1]
428
428
429 part = order[num]
429 part = order[num]
430 carry_part = order[num - 1]
430 carry_part = order[num - 1]
431
431
432 if deltas[part] < 0:
432 if deltas[part] < 0:
433 deltas[part] += length
433 deltas[part] += length
434 deltas[carry_part] -= 1
434 deltas[carry_part] -= 1
435
435
436 # Same thing for days except that the increment depends on the (variable)
436 # Same thing for days except that the increment depends on the (variable)
437 # number of days in the month
437 # number of days in the month
438 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
438 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
439 if deltas['day'] < 0:
439 if deltas['day'] < 0:
440 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
440 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
441 deltas['day'] += 29
441 deltas['day'] += 29
442 else:
442 else:
443 deltas['day'] += month_lengths[get_month(prevdate) - 1]
443 deltas['day'] += month_lengths[get_month(prevdate) - 1]
444
444
445 deltas['month'] -= 1
445 deltas['month'] -= 1
446
446
447 if deltas['month'] < 0:
447 if deltas['month'] < 0:
448 deltas['month'] += 12
448 deltas['month'] += 12
449 deltas['year'] -= 1
449 deltas['year'] -= 1
450
450
451 # Format the result
451 # Format the result
452 if short_format:
452 if short_format:
453 fmt_funcs = {
453 fmt_funcs = {
454 'year': lambda d: u'%dy' % d,
454 'year': lambda d: u'%dy' % d,
455 'month': lambda d: u'%dm' % d,
455 'month': lambda d: u'%dm' % d,
456 'day': lambda d: u'%dd' % d,
456 'day': lambda d: u'%dd' % d,
457 'hour': lambda d: u'%dh' % d,
457 'hour': lambda d: u'%dh' % d,
458 'minute': lambda d: u'%dmin' % d,
458 'minute': lambda d: u'%dmin' % d,
459 'second': lambda d: u'%dsec' % d,
459 'second': lambda d: u'%dsec' % d,
460 }
460 }
461 else:
461 else:
462 fmt_funcs = {
462 fmt_funcs = {
463 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
463 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
464 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
464 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
465 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
465 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
466 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
466 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
467 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
467 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
468 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
468 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
469 }
469 }
470
470
471 i = 0
471 i = 0
472 for part in order:
472 for part in order:
473 value = deltas[part]
473 value = deltas[part]
474 if value != 0:
474 if value != 0:
475
475
476 if i < 5:
476 if i < 5:
477 sub_part = order[i + 1]
477 sub_part = order[i + 1]
478 sub_value = deltas[sub_part]
478 sub_value = deltas[sub_part]
479 else:
479 else:
480 sub_value = 0
480 sub_value = 0
481
481
482 if sub_value == 0 or show_short_version:
482 if sub_value == 0 or show_short_version:
483 _val = fmt_funcs[part](value)
483 _val = fmt_funcs[part](value)
484 if future:
484 if future:
485 if show_suffix:
485 if show_suffix:
486 return _(u'in %s') % _val
486 return _(u'in %s') % _val
487 else:
487 else:
488 return _val
488 return _val
489
489
490 else:
490 else:
491 if show_suffix:
491 if show_suffix:
492 return _(u'%s ago') % _val
492 return _(u'%s ago') % _val
493 else:
493 else:
494 return _val
494 return _val
495
495
496 val = fmt_funcs[part](value)
496 val = fmt_funcs[part](value)
497 val_detail = fmt_funcs[sub_part](sub_value)
497 val_detail = fmt_funcs[sub_part](sub_value)
498
498
499 if short_format:
499 if short_format:
500 datetime_tmpl = u'%s, %s'
500 datetime_tmpl = u'%s, %s'
501 if show_suffix:
501 if show_suffix:
502 datetime_tmpl = _(u'%s, %s ago')
502 datetime_tmpl = _(u'%s, %s ago')
503 if future:
503 if future:
504 datetime_tmpl = _(u'in %s, %s')
504 datetime_tmpl = _(u'in %s, %s')
505 else:
505 else:
506 datetime_tmpl = _(u'%s and %s')
506 datetime_tmpl = _(u'%s and %s')
507 if show_suffix:
507 if show_suffix:
508 datetime_tmpl = _(u'%s and %s ago')
508 datetime_tmpl = _(u'%s and %s ago')
509 if future:
509 if future:
510 datetime_tmpl = _(u'in %s and %s')
510 datetime_tmpl = _(u'in %s and %s')
511
511
512 return datetime_tmpl % (val, val_detail)
512 return datetime_tmpl % (val, val_detail)
513 i += 1
513 i += 1
514 return _(u'just now')
514 return _(u'just now')
515
515
516
516
517 def uri_filter(uri):
517 def uri_filter(uri):
518 """
518 """
519 Removes user:password from given url string
519 Removes user:password from given url string
520
520
521 :param uri:
521 :param uri:
522 :rtype: unicode
522 :rtype: unicode
523 :returns: filtered list of strings
523 :returns: filtered list of strings
524 """
524 """
525 if not uri:
525 if not uri:
526 return ''
526 return ''
527
527
528 proto = ''
528 proto = ''
529
529
530 for pat in ('https://', 'http://'):
530 for pat in ('https://', 'http://'):
531 if uri.startswith(pat):
531 if uri.startswith(pat):
532 uri = uri[len(pat):]
532 uri = uri[len(pat):]
533 proto = pat
533 proto = pat
534 break
534 break
535
535
536 # remove passwords and username
536 # remove passwords and username
537 uri = uri[uri.find('@') + 1:]
537 uri = uri[uri.find('@') + 1:]
538
538
539 # get the port
539 # get the port
540 cred_pos = uri.find(':')
540 cred_pos = uri.find(':')
541 if cred_pos == -1:
541 if cred_pos == -1:
542 host, port = uri, None
542 host, port = uri, None
543 else:
543 else:
544 host, port = uri[:cred_pos], uri[cred_pos + 1:]
544 host, port = uri[:cred_pos], uri[cred_pos + 1:]
545
545
546 return filter(None, [proto, host, port])
546 return filter(None, [proto, host, port])
547
547
548
548
549 def credentials_filter(uri):
549 def credentials_filter(uri):
550 """
550 """
551 Returns a url with removed credentials
551 Returns a url with removed credentials
552
552
553 :param uri:
553 :param uri:
554 """
554 """
555
555
556 uri = uri_filter(uri)
556 uri = uri_filter(uri)
557 # check if we have port
557 # check if we have port
558 if len(uri) > 2 and uri[2]:
558 if len(uri) > 2 and uri[2]:
559 uri[2] = ':' + uri[2]
559 uri[2] = ':' + uri[2]
560
560
561 return ''.join(uri)
561 return ''.join(uri)
562
562
563
563
564 def get_clone_url(uri_tmpl, qualifed_home_url, repo_name, repo_id, **override):
564 def get_clone_url(uri_tmpl, qualifed_home_url, repo_name, repo_id, **override):
565 parsed_url = urlobject.URLObject(qualifed_home_url)
565 parsed_url = urlobject.URLObject(qualifed_home_url)
566 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
566 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
567 args = {
567 args = {
568 'scheme': parsed_url.scheme,
568 'scheme': parsed_url.scheme,
569 'user': '',
569 'user': '',
570 # path if we use proxy-prefix
570 # path if we use proxy-prefix
571 'netloc': parsed_url.netloc+decoded_path,
571 'netloc': parsed_url.netloc+decoded_path,
572 'prefix': decoded_path,
572 'prefix': decoded_path,
573 'repo': repo_name,
573 'repo': repo_name,
574 'repoid': str(repo_id)
574 'repoid': str(repo_id)
575 }
575 }
576 args.update(override)
576 args.update(override)
577 args['user'] = urllib.quote(safe_str(args['user']))
577 args['user'] = urllib.quote(safe_str(args['user']))
578
578
579 for k, v in args.items():
579 for k, v in args.items():
580 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
580 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
581
581
582 # remove leading @ sign if it's present. Case of empty user
582 # remove leading @ sign if it's present. Case of empty user
583 url_obj = urlobject.URLObject(uri_tmpl)
583 url_obj = urlobject.URLObject(uri_tmpl)
584 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
584 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
585
585
586 return safe_unicode(url)
586 return safe_unicode(url)
587
587
588
588
589 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
589 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
590 """
590 """
591 Safe version of get_commit if this commit doesn't exists for a
591 Safe version of get_commit if this commit doesn't exists for a
592 repository it returns a Dummy one instead
592 repository it returns a Dummy one instead
593
593
594 :param repo: repository instance
594 :param repo: repository instance
595 :param commit_id: commit id as str
595 :param commit_id: commit id as str
596 :param pre_load: optional list of commit attributes to load
596 :param pre_load: optional list of commit attributes to load
597 """
597 """
598 # TODO(skreft): remove these circular imports
598 # TODO(skreft): remove these circular imports
599 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
599 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
600 from rhodecode.lib.vcs.exceptions import RepositoryError
600 from rhodecode.lib.vcs.exceptions import RepositoryError
601 if not isinstance(repo, BaseRepository):
601 if not isinstance(repo, BaseRepository):
602 raise Exception('You must pass an Repository '
602 raise Exception('You must pass an Repository '
603 'object as first argument got %s', type(repo))
603 'object as first argument got %s', type(repo))
604
604
605 try:
605 try:
606 commit = repo.get_commit(
606 commit = repo.get_commit(
607 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
607 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
608 except (RepositoryError, LookupError):
608 except (RepositoryError, LookupError):
609 commit = EmptyCommit()
609 commit = EmptyCommit()
610 return commit
610 return commit
611
611
612
612
613 def datetime_to_time(dt):
613 def datetime_to_time(dt):
614 if dt:
614 if dt:
615 return time.mktime(dt.timetuple())
615 return time.mktime(dt.timetuple())
616
616
617
617
618 def time_to_datetime(tm):
618 def time_to_datetime(tm):
619 if tm:
619 if tm:
620 if isinstance(tm, basestring):
620 if isinstance(tm, basestring):
621 try:
621 try:
622 tm = float(tm)
622 tm = float(tm)
623 except ValueError:
623 except ValueError:
624 return
624 return
625 return datetime.datetime.fromtimestamp(tm)
625 return datetime.datetime.fromtimestamp(tm)
626
626
627
627
628 def time_to_utcdatetime(tm):
628 def time_to_utcdatetime(tm):
629 if tm:
629 if tm:
630 if isinstance(tm, basestring):
630 if isinstance(tm, basestring):
631 try:
631 try:
632 tm = float(tm)
632 tm = float(tm)
633 except ValueError:
633 except ValueError:
634 return
634 return
635 return datetime.datetime.utcfromtimestamp(tm)
635 return datetime.datetime.utcfromtimestamp(tm)
636
636
637
637
638 MENTIONS_REGEX = re.compile(
638 MENTIONS_REGEX = re.compile(
639 # ^@ or @ without any special chars in front
639 # ^@ or @ without any special chars in front
640 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
640 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
641 # main body starts with letter, then can be . - _
641 # main body starts with letter, then can be . - _
642 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
642 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
643 re.VERBOSE | re.MULTILINE)
643 re.VERBOSE | re.MULTILINE)
644
644
645
645
646 def extract_mentioned_users(s):
646 def extract_mentioned_users(s):
647 """
647 """
648 Returns unique usernames from given string s that have @mention
648 Returns unique usernames from given string s that have @mention
649
649
650 :param s: string to get mentions
650 :param s: string to get mentions
651 """
651 """
652 usrs = set()
652 usrs = set()
653 for username in MENTIONS_REGEX.findall(s):
653 for username in MENTIONS_REGEX.findall(s):
654 usrs.add(username)
654 usrs.add(username)
655
655
656 return sorted(list(usrs), key=lambda k: k.lower())
656 return sorted(list(usrs), key=lambda k: k.lower())
657
657
658
658
659 class UnsafeAttributeDict(dict):
660 def __getattr__(self, attr):
661 try:
662 return self[attr]
663 except KeyError:
664 raise AttributeError('%s object has no attribute %s' % (self, attr))
665 __setattr__ = dict.__setitem__
666 __delattr__ = dict.__delitem__
667
668
659 class AttributeDict(dict):
669 class AttributeDict(dict):
660 def __getattr__(self, attr):
670 def __getattr__(self, attr):
661 return self.get(attr, None)
671 return self.get(attr, None)
662 __setattr__ = dict.__setitem__
672 __setattr__ = dict.__setitem__
663 __delattr__ = dict.__delitem__
673 __delattr__ = dict.__delitem__
664
674
665
675
666 def fix_PATH(os_=None):
676 def fix_PATH(os_=None):
667 """
677 """
668 Get current active python path, and append it to PATH variable to fix
678 Get current active python path, and append it to PATH variable to fix
669 issues of subprocess calls and different python versions
679 issues of subprocess calls and different python versions
670 """
680 """
671 if os_ is None:
681 if os_ is None:
672 import os
682 import os
673 else:
683 else:
674 os = os_
684 os = os_
675
685
676 cur_path = os.path.split(sys.executable)[0]
686 cur_path = os.path.split(sys.executable)[0]
677 if not os.environ['PATH'].startswith(cur_path):
687 if not os.environ['PATH'].startswith(cur_path):
678 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
688 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
679
689
680
690
681 def obfuscate_url_pw(engine):
691 def obfuscate_url_pw(engine):
682 _url = engine or ''
692 _url = engine or ''
683 try:
693 try:
684 _url = sqlalchemy.engine.url.make_url(engine)
694 _url = sqlalchemy.engine.url.make_url(engine)
685 if _url.password:
695 if _url.password:
686 _url.password = 'XXXXX'
696 _url.password = 'XXXXX'
687 except Exception:
697 except Exception:
688 pass
698 pass
689 return unicode(_url)
699 return unicode(_url)
690
700
691
701
692 def get_server_url(environ):
702 def get_server_url(environ):
693 req = webob.Request(environ)
703 req = webob.Request(environ)
694 return req.host_url + req.script_name
704 return req.host_url + req.script_name
695
705
696
706
697 def unique_id(hexlen=32):
707 def unique_id(hexlen=32):
698 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
708 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
699 return suuid(truncate_to=hexlen, alphabet=alphabet)
709 return suuid(truncate_to=hexlen, alphabet=alphabet)
700
710
701
711
702 def suuid(url=None, truncate_to=22, alphabet=None):
712 def suuid(url=None, truncate_to=22, alphabet=None):
703 """
713 """
704 Generate and return a short URL safe UUID.
714 Generate and return a short URL safe UUID.
705
715
706 If the url parameter is provided, set the namespace to the provided
716 If the url parameter is provided, set the namespace to the provided
707 URL and generate a UUID.
717 URL and generate a UUID.
708
718
709 :param url to get the uuid for
719 :param url to get the uuid for
710 :truncate_to: truncate the basic 22 UUID to shorter version
720 :truncate_to: truncate the basic 22 UUID to shorter version
711
721
712 The IDs won't be universally unique any longer, but the probability of
722 The IDs won't be universally unique any longer, but the probability of
713 a collision will still be very low.
723 a collision will still be very low.
714 """
724 """
715 # Define our alphabet.
725 # Define our alphabet.
716 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
726 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
717
727
718 # If no URL is given, generate a random UUID.
728 # If no URL is given, generate a random UUID.
719 if url is None:
729 if url is None:
720 unique_id = uuid.uuid4().int
730 unique_id = uuid.uuid4().int
721 else:
731 else:
722 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
732 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
723
733
724 alphabet_length = len(_ALPHABET)
734 alphabet_length = len(_ALPHABET)
725 output = []
735 output = []
726 while unique_id > 0:
736 while unique_id > 0:
727 digit = unique_id % alphabet_length
737 digit = unique_id % alphabet_length
728 output.append(_ALPHABET[digit])
738 output.append(_ALPHABET[digit])
729 unique_id = int(unique_id / alphabet_length)
739 unique_id = int(unique_id / alphabet_length)
730 return "".join(output)[:truncate_to]
740 return "".join(output)[:truncate_to]
731
741
732
742
733 def get_current_rhodecode_user():
743 def get_current_rhodecode_user():
734 """
744 """
735 Gets rhodecode user from threadlocal tmpl_context variable if it's
745 Gets rhodecode user from threadlocal tmpl_context variable if it's
736 defined, else returns None.
746 defined, else returns None.
737 """
747 """
738 from pylons import tmpl_context as c
748 from pylons import tmpl_context as c
739 if hasattr(c, 'rhodecode_user'):
749 if hasattr(c, 'rhodecode_user'):
740 return c.rhodecode_user
750 return c.rhodecode_user
741
751
742 return None
752 return None
743
753
744
754
745 def action_logger_generic(action, namespace=''):
755 def action_logger_generic(action, namespace=''):
746 """
756 """
747 A generic logger for actions useful to the system overview, tries to find
757 A generic logger for actions useful to the system overview, tries to find
748 an acting user for the context of the call otherwise reports unknown user
758 an acting user for the context of the call otherwise reports unknown user
749
759
750 :param action: logging message eg 'comment 5 deleted'
760 :param action: logging message eg 'comment 5 deleted'
751 :param type: string
761 :param type: string
752
762
753 :param namespace: namespace of the logging message eg. 'repo.comments'
763 :param namespace: namespace of the logging message eg. 'repo.comments'
754 :param type: string
764 :param type: string
755
765
756 """
766 """
757
767
758 logger_name = 'rhodecode.actions'
768 logger_name = 'rhodecode.actions'
759
769
760 if namespace:
770 if namespace:
761 logger_name += '.' + namespace
771 logger_name += '.' + namespace
762
772
763 log = logging.getLogger(logger_name)
773 log = logging.getLogger(logger_name)
764
774
765 # get a user if we can
775 # get a user if we can
766 user = get_current_rhodecode_user()
776 user = get_current_rhodecode_user()
767
777
768 logfunc = log.info
778 logfunc = log.info
769
779
770 if not user:
780 if not user:
771 user = '<unknown user>'
781 user = '<unknown user>'
772 logfunc = log.warning
782 logfunc = log.warning
773
783
774 logfunc('Logging action by {}: {}'.format(user, action))
784 logfunc('Logging action by {}: {}'.format(user, action))
775
785
776
786
777 def escape_split(text, sep=',', maxsplit=-1):
787 def escape_split(text, sep=',', maxsplit=-1):
778 r"""
788 r"""
779 Allows for escaping of the separator: e.g. arg='foo\, bar'
789 Allows for escaping of the separator: e.g. arg='foo\, bar'
780
790
781 It should be noted that the way bash et. al. do command line parsing, those
791 It should be noted that the way bash et. al. do command line parsing, those
782 single quotes are required.
792 single quotes are required.
783 """
793 """
784 escaped_sep = r'\%s' % sep
794 escaped_sep = r'\%s' % sep
785
795
786 if escaped_sep not in text:
796 if escaped_sep not in text:
787 return text.split(sep, maxsplit)
797 return text.split(sep, maxsplit)
788
798
789 before, _mid, after = text.partition(escaped_sep)
799 before, _mid, after = text.partition(escaped_sep)
790 startlist = before.split(sep, maxsplit) # a regular split is fine here
800 startlist = before.split(sep, maxsplit) # a regular split is fine here
791 unfinished = startlist[-1]
801 unfinished = startlist[-1]
792 startlist = startlist[:-1]
802 startlist = startlist[:-1]
793
803
794 # recurse because there may be more escaped separators
804 # recurse because there may be more escaped separators
795 endlist = escape_split(after, sep, maxsplit)
805 endlist = escape_split(after, sep, maxsplit)
796
806
797 # finish building the escaped value. we use endlist[0] becaue the first
807 # finish building the escaped value. we use endlist[0] becaue the first
798 # part of the string sent in recursion is the rest of the escaped value.
808 # part of the string sent in recursion is the rest of the escaped value.
799 unfinished += sep + endlist[0]
809 unfinished += sep + endlist[0]
800
810
801 return startlist + [unfinished] + endlist[1:] # put together all the parts
811 return startlist + [unfinished] + endlist[1:] # put together all the parts
802
812
803
813
804 class OptionalAttr(object):
814 class OptionalAttr(object):
805 """
815 """
806 Special Optional Option that defines other attribute. Example::
816 Special Optional Option that defines other attribute. Example::
807
817
808 def test(apiuser, userid=Optional(OAttr('apiuser')):
818 def test(apiuser, userid=Optional(OAttr('apiuser')):
809 user = Optional.extract(userid)
819 user = Optional.extract(userid)
810 # calls
820 # calls
811
821
812 """
822 """
813
823
814 def __init__(self, attr_name):
824 def __init__(self, attr_name):
815 self.attr_name = attr_name
825 self.attr_name = attr_name
816
826
817 def __repr__(self):
827 def __repr__(self):
818 return '<OptionalAttr:%s>' % self.attr_name
828 return '<OptionalAttr:%s>' % self.attr_name
819
829
820 def __call__(self):
830 def __call__(self):
821 return self
831 return self
822
832
823
833
824 # alias
834 # alias
825 OAttr = OptionalAttr
835 OAttr = OptionalAttr
826
836
827
837
828 class Optional(object):
838 class Optional(object):
829 """
839 """
830 Defines an optional parameter::
840 Defines an optional parameter::
831
841
832 param = param.getval() if isinstance(param, Optional) else param
842 param = param.getval() if isinstance(param, Optional) else param
833 param = param() if isinstance(param, Optional) else param
843 param = param() if isinstance(param, Optional) else param
834
844
835 is equivalent of::
845 is equivalent of::
836
846
837 param = Optional.extract(param)
847 param = Optional.extract(param)
838
848
839 """
849 """
840
850
841 def __init__(self, type_):
851 def __init__(self, type_):
842 self.type_ = type_
852 self.type_ = type_
843
853
844 def __repr__(self):
854 def __repr__(self):
845 return '<Optional:%s>' % self.type_.__repr__()
855 return '<Optional:%s>' % self.type_.__repr__()
846
856
847 def __call__(self):
857 def __call__(self):
848 return self.getval()
858 return self.getval()
849
859
850 def getval(self):
860 def getval(self):
851 """
861 """
852 returns value from this Optional instance
862 returns value from this Optional instance
853 """
863 """
854 if isinstance(self.type_, OAttr):
864 if isinstance(self.type_, OAttr):
855 # use params name
865 # use params name
856 return self.type_.attr_name
866 return self.type_.attr_name
857 return self.type_
867 return self.type_
858
868
859 @classmethod
869 @classmethod
860 def extract(cls, val):
870 def extract(cls, val):
861 """
871 """
862 Extracts value from Optional() instance
872 Extracts value from Optional() instance
863
873
864 :param val:
874 :param val:
865 :return: original value if it's not Optional instance else
875 :return: original value if it's not Optional instance else
866 value of instance
876 value of instance
867 """
877 """
868 if isinstance(val, cls):
878 if isinstance(val, cls):
869 return val.getval()
879 return val.getval()
870 return val
880 return val
871
881
872
882
873 def get_routes_generator_for_server_url(server_url):
883 def get_routes_generator_for_server_url(server_url):
874 parsed_url = urlobject.URLObject(server_url)
884 parsed_url = urlobject.URLObject(server_url)
875 netloc = safe_str(parsed_url.netloc)
885 netloc = safe_str(parsed_url.netloc)
876 script_name = safe_str(parsed_url.path)
886 script_name = safe_str(parsed_url.path)
877
887
878 if ':' in netloc:
888 if ':' in netloc:
879 server_name, server_port = netloc.split(':')
889 server_name, server_port = netloc.split(':')
880 else:
890 else:
881 server_name = netloc
891 server_name = netloc
882 server_port = (parsed_url.scheme == 'https' and '443' or '80')
892 server_port = (parsed_url.scheme == 'https' and '443' or '80')
883
893
884 environ = {
894 environ = {
885 'REQUEST_METHOD': 'GET',
895 'REQUEST_METHOD': 'GET',
886 'PATH_INFO': '/',
896 'PATH_INFO': '/',
887 'SERVER_NAME': server_name,
897 'SERVER_NAME': server_name,
888 'SERVER_PORT': server_port,
898 'SERVER_PORT': server_port,
889 'SCRIPT_NAME': script_name,
899 'SCRIPT_NAME': script_name,
890 }
900 }
891 if parsed_url.scheme == 'https':
901 if parsed_url.scheme == 'https':
892 environ['HTTPS'] = 'on'
902 environ['HTTPS'] = 'on'
893 environ['wsgi.url_scheme'] = 'https'
903 environ['wsgi.url_scheme'] = 'https'
894
904
895 return routes.util.URLGenerator(rhodecode.CONFIG['routes.map'], environ)
905 return routes.util.URLGenerator(rhodecode.CONFIG['routes.map'], environ)
896
906
897
907
898 def glob2re(pat):
908 def glob2re(pat):
899 """
909 """
900 Translate a shell PATTERN to a regular expression.
910 Translate a shell PATTERN to a regular expression.
901
911
902 There is no way to quote meta-characters.
912 There is no way to quote meta-characters.
903 """
913 """
904
914
905 i, n = 0, len(pat)
915 i, n = 0, len(pat)
906 res = ''
916 res = ''
907 while i < n:
917 while i < n:
908 c = pat[i]
918 c = pat[i]
909 i = i+1
919 i = i+1
910 if c == '*':
920 if c == '*':
911 #res = res + '.*'
921 #res = res + '.*'
912 res = res + '[^/]*'
922 res = res + '[^/]*'
913 elif c == '?':
923 elif c == '?':
914 #res = res + '.'
924 #res = res + '.'
915 res = res + '[^/]'
925 res = res + '[^/]'
916 elif c == '[':
926 elif c == '[':
917 j = i
927 j = i
918 if j < n and pat[j] == '!':
928 if j < n and pat[j] == '!':
919 j = j+1
929 j = j+1
920 if j < n and pat[j] == ']':
930 if j < n and pat[j] == ']':
921 j = j+1
931 j = j+1
922 while j < n and pat[j] != ']':
932 while j < n and pat[j] != ']':
923 j = j+1
933 j = j+1
924 if j >= n:
934 if j >= n:
925 res = res + '\\['
935 res = res + '\\['
926 else:
936 else:
927 stuff = pat[i:j].replace('\\','\\\\')
937 stuff = pat[i:j].replace('\\','\\\\')
928 i = j+1
938 i = j+1
929 if stuff[0] == '!':
939 if stuff[0] == '!':
930 stuff = '^' + stuff[1:]
940 stuff = '^' + stuff[1:]
931 elif stuff[0] == '^':
941 elif stuff[0] == '^':
932 stuff = '\\' + stuff
942 stuff = '\\' + stuff
933 res = '%s[%s]' % (res, stuff)
943 res = '%s[%s]' % (res, stuff)
934 else:
944 else:
935 res = res + re.escape(c)
945 res = res + re.escape(c)
936 return res + '\Z(?ms)'
946 return res + '\Z(?ms)'
@@ -1,153 +1,156 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 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 application's model objects
22 The application's model objects
23
23
24 :example:
24 :example:
25
25
26 .. code-block:: python
26 .. code-block:: python
27
27
28 from paste.deploy import appconfig
28 from paste.deploy import appconfig
29 from pylons import config
29 from pylons import config
30 from sqlalchemy import engine_from_config
30 from sqlalchemy import engine_from_config
31 from rhodecode.config.environment import load_environment
31 from rhodecode.config.environment import load_environment
32
32
33 conf = appconfig('config:development.ini', relative_to = './../../')
33 conf = appconfig('config:development.ini', relative_to = './../../')
34 load_environment(conf.global_conf, conf.local_conf)
34 load_environment(conf.global_conf, conf.local_conf)
35
35
36 engine = engine_from_config(config, 'sqlalchemy.')
36 engine = engine_from_config(config, 'sqlalchemy.')
37 init_model(engine)
37 init_model(engine)
38 # RUN YOUR CODE HERE
38 # RUN YOUR CODE HERE
39
39
40 """
40 """
41
41
42
42
43 import logging
43 import logging
44
44
45 from pylons import config
45 from pylons import config
46 from pyramid.threadlocal import get_current_registry
46 from pyramid.threadlocal import get_current_registry
47
47
48 from rhodecode.model import meta, db
48 from rhodecode.model import meta, db
49 from rhodecode.lib.utils2 import obfuscate_url_pw, get_encryption_key
49 from rhodecode.lib.utils2 import obfuscate_url_pw, get_encryption_key
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 def init_model(engine, encryption_key=None):
54 def init_model(engine, encryption_key=None):
55 """
55 """
56 Initializes db session, bind the engine with the metadata,
56 Initializes db session, bind the engine with the metadata,
57 Call this before using any of the tables or classes in the model,
57 Call this before using any of the tables or classes in the model,
58 preferably once in application start
58 preferably once in application start
59
59
60 :param engine: engine to bind to
60 :param engine: engine to bind to
61 """
61 """
62 engine_str = obfuscate_url_pw(str(engine.url))
62 engine_str = obfuscate_url_pw(str(engine.url))
63 log.info("initializing db for %s", engine_str)
63 log.info("initializing db for %s", engine_str)
64 meta.Base.metadata.bind = engine
64 meta.Base.metadata.bind = engine
65 db.ENCRYPTION_KEY = encryption_key
65 db.ENCRYPTION_KEY = encryption_key
66
66
67
67
68 def init_model_encryption(migration_models):
68 def init_model_encryption(migration_models):
69 migration_models.ENCRYPTION_KEY = get_encryption_key(config)
69 migration_models.ENCRYPTION_KEY = get_encryption_key(config)
70 db.ENCRYPTION_KEY = get_encryption_key(config)
70 db.ENCRYPTION_KEY = get_encryption_key(config)
71
71
72
72
73 class BaseModel(object):
73 class BaseModel(object):
74 """
74 """
75 Base Model for all RhodeCode models, it adds sql alchemy session
75 Base Model for all RhodeCode models, it adds sql alchemy session
76 into instance of model
76 into instance of model
77
77
78 :param sa: If passed it reuses this session instead of creating a new one
78 :param sa: If passed it reuses this session instead of creating a new one
79 """
79 """
80
80
81 cls = None # override in child class
81 cls = None # override in child class
82
82
83 def __init__(self, sa=None):
83 def __init__(self, sa=None):
84 if sa is not None:
84 if sa is not None:
85 self.sa = sa
85 self.sa = sa
86 else:
86 else:
87 self.sa = meta.Session()
87 self.sa = meta.Session()
88
88
89 def _get_instance(self, cls, instance, callback=None):
89 def _get_instance(self, cls, instance, callback=None):
90 """
90 """
91 Gets instance of given cls using some simple lookup mechanism.
91 Gets instance of given cls using some simple lookup mechanism.
92
92
93 :param cls: class to fetch
93 :param cls: classes to fetch
94 :param instance: int or Instance
94 :param instance: int or Instance
95 :param callback: callback to call if all lookups failed
95 :param callback: callback to call if all lookups failed
96 """
96 """
97
97
98 if isinstance(instance, cls):
98 if isinstance(instance, cls):
99 return instance
99 return instance
100 elif isinstance(instance, (int, long)):
100 elif isinstance(instance, (int, long)):
101 if isinstance(cls, tuple):
102 # if we pass multi instances we pick first to .get()
103 cls = cls[0]
101 return cls.get(instance)
104 return cls.get(instance)
102 else:
105 else:
103 if instance:
106 if instance:
104 if callback is None:
107 if callback is None:
105 raise Exception(
108 raise Exception(
106 'given object must be int, long or Instance of %s '
109 'given object must be int, long or Instance of %s '
107 'got %s, no callback provided' % (cls, type(instance))
110 'got %s, no callback provided' % (cls, type(instance))
108 )
111 )
109 else:
112 else:
110 return callback(instance)
113 return callback(instance)
111
114
112 def _get_user(self, user):
115 def _get_user(self, user):
113 """
116 """
114 Helper method to get user by ID, or username fallback
117 Helper method to get user by ID, or username fallback
115
118
116 :param user: UserID, username, or User instance
119 :param user: UserID, username, or User instance
117 """
120 """
118 return self._get_instance(
121 return self._get_instance(
119 db.User, user, callback=db.User.get_by_username)
122 db.User, user, callback=db.User.get_by_username)
120
123
121 def _get_user_group(self, user_group):
124 def _get_user_group(self, user_group):
122 """
125 """
123 Helper method to get user by ID, or username fallback
126 Helper method to get user by ID, or username fallback
124
127
125 :param user_group: UserGroupID, user_group_name, or UserGroup instance
128 :param user_group: UserGroupID, user_group_name, or UserGroup instance
126 """
129 """
127 return self._get_instance(
130 return self._get_instance(
128 db.UserGroup, user_group, callback=db.UserGroup.get_by_group_name)
131 db.UserGroup, user_group, callback=db.UserGroup.get_by_group_name)
129
132
130 def _get_repo(self, repository):
133 def _get_repo(self, repository):
131 """
134 """
132 Helper method to get repository by ID, or repository name
135 Helper method to get repository by ID, or repository name
133
136
134 :param repository: RepoID, repository name or Repository Instance
137 :param repository: RepoID, repository name or Repository Instance
135 """
138 """
136 return self._get_instance(
139 return self._get_instance(
137 db.Repository, repository, callback=db.Repository.get_by_repo_name)
140 db.Repository, repository, callback=db.Repository.get_by_repo_name)
138
141
139 def _get_perm(self, permission):
142 def _get_perm(self, permission):
140 """
143 """
141 Helper method to get permission by ID, or permission name
144 Helper method to get permission by ID, or permission name
142
145
143 :param permission: PermissionID, permission_name or Permission instance
146 :param permission: PermissionID, permission_name or Permission instance
144 """
147 """
145 return self._get_instance(
148 return self._get_instance(
146 db.Permission, permission, callback=db.Permission.get_by_key)
149 db.Permission, permission, callback=db.Permission.get_by_key)
147
150
148 @classmethod
151 @classmethod
149 def get_all(cls):
152 def get_all(cls):
150 """
153 """
151 Returns all instances of what is defined in `cls` class variable
154 Returns all instances of what is defined in `cls` class variable
152 """
155 """
153 return cls.cls.getAll()
156 return cls.cls.getAll()
@@ -1,3705 +1,3730 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 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)
56 glob2re)
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 ip_addresses(self):
605 def ip_addresses(self):
606 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
606 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
607 return [x.ip_addr for x in ret]
607 return [x.ip_addr for x in ret]
608
608
609 @property
609 @property
610 def username_and_name(self):
610 def username_and_name(self):
611 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
611 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
612
612
613 @property
613 @property
614 def username_or_name_or_email(self):
614 def username_or_name_or_email(self):
615 full_name = self.full_name if self.full_name is not ' ' else None
615 full_name = self.full_name if self.full_name is not ' ' else None
616 return self.username or full_name or self.email
616 return self.username or full_name or self.email
617
617
618 @property
618 @property
619 def full_name(self):
619 def full_name(self):
620 return '%s %s' % (self.firstname, self.lastname)
620 return '%s %s' % (self.firstname, self.lastname)
621
621
622 @property
622 @property
623 def full_name_or_username(self):
623 def full_name_or_username(self):
624 return ('%s %s' % (self.firstname, self.lastname)
624 return ('%s %s' % (self.firstname, self.lastname)
625 if (self.firstname and self.lastname) else self.username)
625 if (self.firstname and self.lastname) else self.username)
626
626
627 @property
627 @property
628 def full_contact(self):
628 def full_contact(self):
629 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
629 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
630
630
631 @property
631 @property
632 def short_contact(self):
632 def short_contact(self):
633 return '%s %s' % (self.firstname, self.lastname)
633 return '%s %s' % (self.firstname, self.lastname)
634
634
635 @property
635 @property
636 def is_admin(self):
636 def is_admin(self):
637 return self.admin
637 return self.admin
638
638
639 @property
639 @property
640 def AuthUser(self):
640 def AuthUser(self):
641 """
641 """
642 Returns instance of AuthUser for this user
642 Returns instance of AuthUser for this user
643 """
643 """
644 from rhodecode.lib.auth import AuthUser
644 from rhodecode.lib.auth import AuthUser
645 return AuthUser(user_id=self.user_id, api_key=self.api_key,
645 return AuthUser(user_id=self.user_id, api_key=self.api_key,
646 username=self.username)
646 username=self.username)
647
647
648 @hybrid_property
648 @hybrid_property
649 def user_data(self):
649 def user_data(self):
650 if not self._user_data:
650 if not self._user_data:
651 return {}
651 return {}
652
652
653 try:
653 try:
654 return json.loads(self._user_data)
654 return json.loads(self._user_data)
655 except TypeError:
655 except TypeError:
656 return {}
656 return {}
657
657
658 @user_data.setter
658 @user_data.setter
659 def user_data(self, val):
659 def user_data(self, val):
660 if not isinstance(val, dict):
660 if not isinstance(val, dict):
661 raise Exception('user_data must be dict, got %s' % type(val))
661 raise Exception('user_data must be dict, got %s' % type(val))
662 try:
662 try:
663 self._user_data = json.dumps(val)
663 self._user_data = json.dumps(val)
664 except Exception:
664 except Exception:
665 log.error(traceback.format_exc())
665 log.error(traceback.format_exc())
666
666
667 @classmethod
667 @classmethod
668 def get_by_username(cls, username, case_insensitive=False,
668 def get_by_username(cls, username, case_insensitive=False,
669 cache=False, identity_cache=False):
669 cache=False, identity_cache=False):
670 session = Session()
670 session = Session()
671
671
672 if case_insensitive:
672 if case_insensitive:
673 q = cls.query().filter(
673 q = cls.query().filter(
674 func.lower(cls.username) == func.lower(username))
674 func.lower(cls.username) == func.lower(username))
675 else:
675 else:
676 q = cls.query().filter(cls.username == username)
676 q = cls.query().filter(cls.username == username)
677
677
678 if cache:
678 if cache:
679 if identity_cache:
679 if identity_cache:
680 val = cls.identity_cache(session, 'username', username)
680 val = cls.identity_cache(session, 'username', username)
681 if val:
681 if val:
682 return val
682 return val
683 else:
683 else:
684 q = q.options(
684 q = q.options(
685 FromCache("sql_cache_short",
685 FromCache("sql_cache_short",
686 "get_user_by_name_%s" % _hash_key(username)))
686 "get_user_by_name_%s" % _hash_key(username)))
687
687
688 return q.scalar()
688 return q.scalar()
689
689
690 @classmethod
690 @classmethod
691 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
691 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
692 q = cls.query().filter(cls.api_key == auth_token)
692 q = cls.query().filter(cls.api_key == auth_token)
693
693
694 if cache:
694 if cache:
695 q = q.options(FromCache("sql_cache_short",
695 q = q.options(FromCache("sql_cache_short",
696 "get_auth_token_%s" % auth_token))
696 "get_auth_token_%s" % auth_token))
697 res = q.scalar()
697 res = q.scalar()
698
698
699 if fallback and not res:
699 if fallback and not res:
700 #fallback to additional keys
700 #fallback to additional keys
701 _res = UserApiKeys.query()\
701 _res = UserApiKeys.query()\
702 .filter(UserApiKeys.api_key == auth_token)\
702 .filter(UserApiKeys.api_key == auth_token)\
703 .filter(or_(UserApiKeys.expires == -1,
703 .filter(or_(UserApiKeys.expires == -1,
704 UserApiKeys.expires >= time.time()))\
704 UserApiKeys.expires >= time.time()))\
705 .first()
705 .first()
706 if _res:
706 if _res:
707 res = _res.user
707 res = _res.user
708 return res
708 return res
709
709
710 @classmethod
710 @classmethod
711 def get_by_email(cls, email, case_insensitive=False, cache=False):
711 def get_by_email(cls, email, case_insensitive=False, cache=False):
712
712
713 if case_insensitive:
713 if case_insensitive:
714 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
714 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
715
715
716 else:
716 else:
717 q = cls.query().filter(cls.email == email)
717 q = cls.query().filter(cls.email == email)
718
718
719 if cache:
719 if cache:
720 q = q.options(FromCache("sql_cache_short",
720 q = q.options(FromCache("sql_cache_short",
721 "get_email_key_%s" % _hash_key(email)))
721 "get_email_key_%s" % _hash_key(email)))
722
722
723 ret = q.scalar()
723 ret = q.scalar()
724 if ret is None:
724 if ret is None:
725 q = UserEmailMap.query()
725 q = UserEmailMap.query()
726 # try fetching in alternate email map
726 # try fetching in alternate email map
727 if case_insensitive:
727 if case_insensitive:
728 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
728 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
729 else:
729 else:
730 q = q.filter(UserEmailMap.email == email)
730 q = q.filter(UserEmailMap.email == email)
731 q = q.options(joinedload(UserEmailMap.user))
731 q = q.options(joinedload(UserEmailMap.user))
732 if cache:
732 if cache:
733 q = q.options(FromCache("sql_cache_short",
733 q = q.options(FromCache("sql_cache_short",
734 "get_email_map_key_%s" % email))
734 "get_email_map_key_%s" % email))
735 ret = getattr(q.scalar(), 'user', None)
735 ret = getattr(q.scalar(), 'user', None)
736
736
737 return ret
737 return ret
738
738
739 @classmethod
739 @classmethod
740 def get_from_cs_author(cls, author):
740 def get_from_cs_author(cls, author):
741 """
741 """
742 Tries to get User objects out of commit author string
742 Tries to get User objects out of commit author string
743
743
744 :param author:
744 :param author:
745 """
745 """
746 from rhodecode.lib.helpers import email, author_name
746 from rhodecode.lib.helpers import email, author_name
747 # Valid email in the attribute passed, see if they're in the system
747 # Valid email in the attribute passed, see if they're in the system
748 _email = email(author)
748 _email = email(author)
749 if _email:
749 if _email:
750 user = cls.get_by_email(_email, case_insensitive=True)
750 user = cls.get_by_email(_email, case_insensitive=True)
751 if user:
751 if user:
752 return user
752 return user
753 # Maybe we can match by username?
753 # Maybe we can match by username?
754 _author = author_name(author)
754 _author = author_name(author)
755 user = cls.get_by_username(_author, case_insensitive=True)
755 user = cls.get_by_username(_author, case_insensitive=True)
756 if user:
756 if user:
757 return user
757 return user
758
758
759 def update_userdata(self, **kwargs):
759 def update_userdata(self, **kwargs):
760 usr = self
760 usr = self
761 old = usr.user_data
761 old = usr.user_data
762 old.update(**kwargs)
762 old.update(**kwargs)
763 usr.user_data = old
763 usr.user_data = old
764 Session().add(usr)
764 Session().add(usr)
765 log.debug('updated userdata with ', kwargs)
765 log.debug('updated userdata with ', kwargs)
766
766
767 def update_lastlogin(self):
767 def update_lastlogin(self):
768 """Update user lastlogin"""
768 """Update user lastlogin"""
769 self.last_login = datetime.datetime.now()
769 self.last_login = datetime.datetime.now()
770 Session().add(self)
770 Session().add(self)
771 log.debug('updated user %s lastlogin', self.username)
771 log.debug('updated user %s lastlogin', self.username)
772
772
773 def update_lastactivity(self):
773 def update_lastactivity(self):
774 """Update user lastactivity"""
774 """Update user lastactivity"""
775 usr = self
775 usr = self
776 old = usr.user_data
776 old = usr.user_data
777 old.update({'last_activity': time.time()})
777 old.update({'last_activity': time.time()})
778 usr.user_data = old
778 usr.user_data = old
779 Session().add(usr)
779 Session().add(usr)
780 log.debug('updated user %s lastactivity', usr.username)
780 log.debug('updated user %s lastactivity', usr.username)
781
781
782 def update_password(self, new_password, change_api_key=False):
782 def update_password(self, new_password, change_api_key=False):
783 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
783 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
784
784
785 self.password = get_crypt_password(new_password)
785 self.password = get_crypt_password(new_password)
786 if change_api_key:
786 if change_api_key:
787 self.api_key = generate_auth_token(self.username)
787 self.api_key = generate_auth_token(self.username)
788 Session().add(self)
788 Session().add(self)
789
789
790 @classmethod
790 @classmethod
791 def get_first_super_admin(cls):
791 def get_first_super_admin(cls):
792 user = User.query().filter(User.admin == true()).first()
792 user = User.query().filter(User.admin == true()).first()
793 if user is None:
793 if user is None:
794 raise Exception('FATAL: Missing administrative account!')
794 raise Exception('FATAL: Missing administrative account!')
795 return user
795 return user
796
796
797 @classmethod
797 @classmethod
798 def get_all_super_admins(cls):
798 def get_all_super_admins(cls):
799 """
799 """
800 Returns all admin accounts sorted by username
800 Returns all admin accounts sorted by username
801 """
801 """
802 return User.query().filter(User.admin == true())\
802 return User.query().filter(User.admin == true())\
803 .order_by(User.username.asc()).all()
803 .order_by(User.username.asc()).all()
804
804
805 @classmethod
805 @classmethod
806 def get_default_user(cls, cache=False):
806 def get_default_user(cls, cache=False):
807 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
807 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
808 if user is None:
808 if user is None:
809 raise Exception('FATAL: Missing default account!')
809 raise Exception('FATAL: Missing default account!')
810 return user
810 return user
811
811
812 def _get_default_perms(self, user, suffix=''):
812 def _get_default_perms(self, user, suffix=''):
813 from rhodecode.model.permission import PermissionModel
813 from rhodecode.model.permission import PermissionModel
814 return PermissionModel().get_default_perms(user.user_perms, suffix)
814 return PermissionModel().get_default_perms(user.user_perms, suffix)
815
815
816 def get_default_perms(self, suffix=''):
816 def get_default_perms(self, suffix=''):
817 return self._get_default_perms(self, suffix)
817 return self._get_default_perms(self, suffix)
818
818
819 def get_api_data(self, include_secrets=False, details='full'):
819 def get_api_data(self, include_secrets=False, details='full'):
820 """
820 """
821 Common function for generating user related data for API
821 Common function for generating user related data for API
822
822
823 :param include_secrets: By default secrets in the API data will be replaced
823 :param include_secrets: By default secrets in the API data will be replaced
824 by a placeholder value to prevent exposing this data by accident. In case
824 by a placeholder value to prevent exposing this data by accident. In case
825 this data shall be exposed, set this flag to ``True``.
825 this data shall be exposed, set this flag to ``True``.
826
826
827 :param details: details can be 'basic|full' basic gives only a subset of
827 :param details: details can be 'basic|full' basic gives only a subset of
828 the available user information that includes user_id, name and emails.
828 the available user information that includes user_id, name and emails.
829 """
829 """
830 user = self
830 user = self
831 user_data = self.user_data
831 user_data = self.user_data
832 data = {
832 data = {
833 'user_id': user.user_id,
833 'user_id': user.user_id,
834 'username': user.username,
834 'username': user.username,
835 'firstname': user.name,
835 'firstname': user.name,
836 'lastname': user.lastname,
836 'lastname': user.lastname,
837 'email': user.email,
837 'email': user.email,
838 'emails': user.emails,
838 'emails': user.emails,
839 }
839 }
840 if details == 'basic':
840 if details == 'basic':
841 return data
841 return data
842
842
843 api_key_length = 40
843 api_key_length = 40
844 api_key_replacement = '*' * api_key_length
844 api_key_replacement = '*' * api_key_length
845
845
846 extras = {
846 extras = {
847 'api_key': api_key_replacement,
847 'api_key': api_key_replacement,
848 'api_keys': [api_key_replacement],
848 'api_keys': [api_key_replacement],
849 'active': user.active,
849 'active': user.active,
850 'admin': user.admin,
850 'admin': user.admin,
851 'extern_type': user.extern_type,
851 'extern_type': user.extern_type,
852 'extern_name': user.extern_name,
852 'extern_name': user.extern_name,
853 'last_login': user.last_login,
853 'last_login': user.last_login,
854 'ip_addresses': user.ip_addresses,
854 'ip_addresses': user.ip_addresses,
855 'language': user_data.get('language')
855 'language': user_data.get('language')
856 }
856 }
857 data.update(extras)
857 data.update(extras)
858
858
859 if include_secrets:
859 if include_secrets:
860 data['api_key'] = user.api_key
860 data['api_key'] = user.api_key
861 data['api_keys'] = user.auth_tokens
861 data['api_keys'] = user.auth_tokens
862 return data
862 return data
863
863
864 def __json__(self):
864 def __json__(self):
865 data = {
865 data = {
866 'full_name': self.full_name,
866 'full_name': self.full_name,
867 'full_name_or_username': self.full_name_or_username,
867 'full_name_or_username': self.full_name_or_username,
868 'short_contact': self.short_contact,
868 'short_contact': self.short_contact,
869 'full_contact': self.full_contact,
869 'full_contact': self.full_contact,
870 }
870 }
871 data.update(self.get_api_data())
871 data.update(self.get_api_data())
872 return data
872 return data
873
873
874
874
875 class UserApiKeys(Base, BaseModel):
875 class UserApiKeys(Base, BaseModel):
876 __tablename__ = 'user_api_keys'
876 __tablename__ = 'user_api_keys'
877 __table_args__ = (
877 __table_args__ = (
878 Index('uak_api_key_idx', 'api_key'),
878 Index('uak_api_key_idx', 'api_key'),
879 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
879 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
880 UniqueConstraint('api_key'),
880 UniqueConstraint('api_key'),
881 {'extend_existing': True, 'mysql_engine': 'InnoDB',
881 {'extend_existing': True, 'mysql_engine': 'InnoDB',
882 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
882 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
883 )
883 )
884 __mapper_args__ = {}
884 __mapper_args__ = {}
885
885
886 # ApiKey role
886 # ApiKey role
887 ROLE_ALL = 'token_role_all'
887 ROLE_ALL = 'token_role_all'
888 ROLE_HTTP = 'token_role_http'
888 ROLE_HTTP = 'token_role_http'
889 ROLE_VCS = 'token_role_vcs'
889 ROLE_VCS = 'token_role_vcs'
890 ROLE_API = 'token_role_api'
890 ROLE_API = 'token_role_api'
891 ROLE_FEED = 'token_role_feed'
891 ROLE_FEED = 'token_role_feed'
892 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
892 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
893
893
894 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
894 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
895 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
895 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
896 api_key = Column("api_key", String(255), nullable=False, unique=True)
896 api_key = Column("api_key", String(255), nullable=False, unique=True)
897 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
897 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
898 expires = Column('expires', Float(53), nullable=False)
898 expires = Column('expires', Float(53), nullable=False)
899 role = Column('role', String(255), nullable=True)
899 role = Column('role', String(255), nullable=True)
900 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
900 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
901
901
902 user = relationship('User', lazy='joined')
902 user = relationship('User', lazy='joined')
903
903
904 @classmethod
904 @classmethod
905 def _get_role_name(cls, role):
905 def _get_role_name(cls, role):
906 return {
906 return {
907 cls.ROLE_ALL: _('all'),
907 cls.ROLE_ALL: _('all'),
908 cls.ROLE_HTTP: _('http/web interface'),
908 cls.ROLE_HTTP: _('http/web interface'),
909 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
909 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
910 cls.ROLE_API: _('api calls'),
910 cls.ROLE_API: _('api calls'),
911 cls.ROLE_FEED: _('feed access'),
911 cls.ROLE_FEED: _('feed access'),
912 }.get(role, role)
912 }.get(role, role)
913
913
914 @property
914 @property
915 def expired(self):
915 def expired(self):
916 if self.expires == -1:
916 if self.expires == -1:
917 return False
917 return False
918 return time.time() > self.expires
918 return time.time() > self.expires
919
919
920 @property
920 @property
921 def role_humanized(self):
921 def role_humanized(self):
922 return self._get_role_name(self.role)
922 return self._get_role_name(self.role)
923
923
924
924
925 class UserEmailMap(Base, BaseModel):
925 class UserEmailMap(Base, BaseModel):
926 __tablename__ = 'user_email_map'
926 __tablename__ = 'user_email_map'
927 __table_args__ = (
927 __table_args__ = (
928 Index('uem_email_idx', 'email'),
928 Index('uem_email_idx', 'email'),
929 UniqueConstraint('email'),
929 UniqueConstraint('email'),
930 {'extend_existing': True, 'mysql_engine': 'InnoDB',
930 {'extend_existing': True, 'mysql_engine': 'InnoDB',
931 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
931 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
932 )
932 )
933 __mapper_args__ = {}
933 __mapper_args__ = {}
934
934
935 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
935 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
936 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
936 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
937 _email = Column("email", String(255), nullable=True, unique=False, default=None)
937 _email = Column("email", String(255), nullable=True, unique=False, default=None)
938 user = relationship('User', lazy='joined')
938 user = relationship('User', lazy='joined')
939
939
940 @validates('_email')
940 @validates('_email')
941 def validate_email(self, key, email):
941 def validate_email(self, key, email):
942 # check if this email is not main one
942 # check if this email is not main one
943 main_email = Session().query(User).filter(User.email == email).scalar()
943 main_email = Session().query(User).filter(User.email == email).scalar()
944 if main_email is not None:
944 if main_email is not None:
945 raise AttributeError('email %s is present is user table' % email)
945 raise AttributeError('email %s is present is user table' % email)
946 return email
946 return email
947
947
948 @hybrid_property
948 @hybrid_property
949 def email(self):
949 def email(self):
950 return self._email
950 return self._email
951
951
952 @email.setter
952 @email.setter
953 def email(self, val):
953 def email(self, val):
954 self._email = val.lower() if val else None
954 self._email = val.lower() if val else None
955
955
956
956
957 class UserIpMap(Base, BaseModel):
957 class UserIpMap(Base, BaseModel):
958 __tablename__ = 'user_ip_map'
958 __tablename__ = 'user_ip_map'
959 __table_args__ = (
959 __table_args__ = (
960 UniqueConstraint('user_id', 'ip_addr'),
960 UniqueConstraint('user_id', 'ip_addr'),
961 {'extend_existing': True, 'mysql_engine': 'InnoDB',
961 {'extend_existing': True, 'mysql_engine': 'InnoDB',
962 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
962 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
963 )
963 )
964 __mapper_args__ = {}
964 __mapper_args__ = {}
965
965
966 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
966 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
967 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
967 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
968 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
968 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
969 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
969 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
970 description = Column("description", String(10000), nullable=True, unique=None, default=None)
970 description = Column("description", String(10000), nullable=True, unique=None, default=None)
971 user = relationship('User', lazy='joined')
971 user = relationship('User', lazy='joined')
972
972
973 @classmethod
973 @classmethod
974 def _get_ip_range(cls, ip_addr):
974 def _get_ip_range(cls, ip_addr):
975 net = ipaddress.ip_network(ip_addr, strict=False)
975 net = ipaddress.ip_network(ip_addr, strict=False)
976 return [str(net.network_address), str(net.broadcast_address)]
976 return [str(net.network_address), str(net.broadcast_address)]
977
977
978 def __json__(self):
978 def __json__(self):
979 return {
979 return {
980 'ip_addr': self.ip_addr,
980 'ip_addr': self.ip_addr,
981 'ip_range': self._get_ip_range(self.ip_addr),
981 'ip_range': self._get_ip_range(self.ip_addr),
982 }
982 }
983
983
984 def __unicode__(self):
984 def __unicode__(self):
985 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
985 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
986 self.user_id, self.ip_addr)
986 self.user_id, self.ip_addr)
987
987
988 class UserLog(Base, BaseModel):
988 class UserLog(Base, BaseModel):
989 __tablename__ = 'user_logs'
989 __tablename__ = 'user_logs'
990 __table_args__ = (
990 __table_args__ = (
991 {'extend_existing': True, 'mysql_engine': 'InnoDB',
991 {'extend_existing': True, 'mysql_engine': 'InnoDB',
992 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
992 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
993 )
993 )
994 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
994 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
995 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
995 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
996 username = Column("username", String(255), nullable=True, unique=None, default=None)
996 username = Column("username", String(255), nullable=True, unique=None, default=None)
997 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
997 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
998 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
998 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
999 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
999 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1000 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1000 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1001 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1001 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1002
1002
1003 def __unicode__(self):
1003 def __unicode__(self):
1004 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1004 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1005 self.repository_name,
1005 self.repository_name,
1006 self.action)
1006 self.action)
1007
1007
1008 @property
1008 @property
1009 def action_as_day(self):
1009 def action_as_day(self):
1010 return datetime.date(*self.action_date.timetuple()[:3])
1010 return datetime.date(*self.action_date.timetuple()[:3])
1011
1011
1012 user = relationship('User')
1012 user = relationship('User')
1013 repository = relationship('Repository', cascade='')
1013 repository = relationship('Repository', cascade='')
1014
1014
1015
1015
1016 class UserGroup(Base, BaseModel):
1016 class UserGroup(Base, BaseModel):
1017 __tablename__ = 'users_groups'
1017 __tablename__ = 'users_groups'
1018 __table_args__ = (
1018 __table_args__ = (
1019 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1019 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1020 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1020 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1021 )
1021 )
1022
1022
1023 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1023 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1024 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1024 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1025 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1025 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1026 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1026 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1027 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1027 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1028 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1028 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1029 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1029 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1030 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1030 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1031
1031
1032 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1032 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1033 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1033 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1034 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1034 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1035 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1035 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1036 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1036 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1037 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1037 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1038
1038
1039 user = relationship('User')
1039 user = relationship('User')
1040
1040
1041 @hybrid_property
1041 @hybrid_property
1042 def group_data(self):
1042 def group_data(self):
1043 if not self._group_data:
1043 if not self._group_data:
1044 return {}
1044 return {}
1045
1045
1046 try:
1046 try:
1047 return json.loads(self._group_data)
1047 return json.loads(self._group_data)
1048 except TypeError:
1048 except TypeError:
1049 return {}
1049 return {}
1050
1050
1051 @group_data.setter
1051 @group_data.setter
1052 def group_data(self, val):
1052 def group_data(self, val):
1053 try:
1053 try:
1054 self._group_data = json.dumps(val)
1054 self._group_data = json.dumps(val)
1055 except Exception:
1055 except Exception:
1056 log.error(traceback.format_exc())
1056 log.error(traceback.format_exc())
1057
1057
1058 def __unicode__(self):
1058 def __unicode__(self):
1059 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1059 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1060 self.users_group_id,
1060 self.users_group_id,
1061 self.users_group_name)
1061 self.users_group_name)
1062
1062
1063 @classmethod
1063 @classmethod
1064 def get_by_group_name(cls, group_name, cache=False,
1064 def get_by_group_name(cls, group_name, cache=False,
1065 case_insensitive=False):
1065 case_insensitive=False):
1066 if case_insensitive:
1066 if case_insensitive:
1067 q = cls.query().filter(func.lower(cls.users_group_name) ==
1067 q = cls.query().filter(func.lower(cls.users_group_name) ==
1068 func.lower(group_name))
1068 func.lower(group_name))
1069
1069
1070 else:
1070 else:
1071 q = cls.query().filter(cls.users_group_name == group_name)
1071 q = cls.query().filter(cls.users_group_name == group_name)
1072 if cache:
1072 if cache:
1073 q = q.options(FromCache(
1073 q = q.options(FromCache(
1074 "sql_cache_short",
1074 "sql_cache_short",
1075 "get_group_%s" % _hash_key(group_name)))
1075 "get_group_%s" % _hash_key(group_name)))
1076 return q.scalar()
1076 return q.scalar()
1077
1077
1078 @classmethod
1078 @classmethod
1079 def get(cls, user_group_id, cache=False):
1079 def get(cls, user_group_id, cache=False):
1080 user_group = cls.query()
1080 user_group = cls.query()
1081 if cache:
1081 if cache:
1082 user_group = user_group.options(FromCache("sql_cache_short",
1082 user_group = user_group.options(FromCache("sql_cache_short",
1083 "get_users_group_%s" % user_group_id))
1083 "get_users_group_%s" % user_group_id))
1084 return user_group.get(user_group_id)
1084 return user_group.get(user_group_id)
1085
1085
1086 def permissions(self, with_admins=True, with_owner=True):
1086 def permissions(self, with_admins=True, with_owner=True):
1087 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1087 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1088 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1088 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1089 joinedload(UserUserGroupToPerm.user),
1089 joinedload(UserUserGroupToPerm.user),
1090 joinedload(UserUserGroupToPerm.permission),)
1090 joinedload(UserUserGroupToPerm.permission),)
1091
1091
1092 # get owners and admins and permissions. We do a trick of re-writing
1092 # get owners and admins and permissions. We do a trick of re-writing
1093 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1093 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1094 # has a global reference and changing one object propagates to all
1094 # has a global reference and changing one object propagates to all
1095 # others. This means if admin is also an owner admin_row that change
1095 # others. This means if admin is also an owner admin_row that change
1096 # would propagate to both objects
1096 # would propagate to both objects
1097 perm_rows = []
1097 perm_rows = []
1098 for _usr in q.all():
1098 for _usr in q.all():
1099 usr = AttributeDict(_usr.user.get_dict())
1099 usr = AttributeDict(_usr.user.get_dict())
1100 usr.permission = _usr.permission.permission_name
1100 usr.permission = _usr.permission.permission_name
1101 perm_rows.append(usr)
1101 perm_rows.append(usr)
1102
1102
1103 # filter the perm rows by 'default' first and then sort them by
1103 # filter the perm rows by 'default' first and then sort them by
1104 # admin,write,read,none permissions sorted again alphabetically in
1104 # admin,write,read,none permissions sorted again alphabetically in
1105 # each group
1105 # each group
1106 perm_rows = sorted(perm_rows, key=display_sort)
1106 perm_rows = sorted(perm_rows, key=display_sort)
1107
1107
1108 _admin_perm = 'usergroup.admin'
1108 _admin_perm = 'usergroup.admin'
1109 owner_row = []
1109 owner_row = []
1110 if with_owner:
1110 if with_owner:
1111 usr = AttributeDict(self.user.get_dict())
1111 usr = AttributeDict(self.user.get_dict())
1112 usr.owner_row = True
1112 usr.owner_row = True
1113 usr.permission = _admin_perm
1113 usr.permission = _admin_perm
1114 owner_row.append(usr)
1114 owner_row.append(usr)
1115
1115
1116 super_admin_rows = []
1116 super_admin_rows = []
1117 if with_admins:
1117 if with_admins:
1118 for usr in User.get_all_super_admins():
1118 for usr in User.get_all_super_admins():
1119 # if this admin is also owner, don't double the record
1119 # if this admin is also owner, don't double the record
1120 if usr.user_id == owner_row[0].user_id:
1120 if usr.user_id == owner_row[0].user_id:
1121 owner_row[0].admin_row = True
1121 owner_row[0].admin_row = True
1122 else:
1122 else:
1123 usr = AttributeDict(usr.get_dict())
1123 usr = AttributeDict(usr.get_dict())
1124 usr.admin_row = True
1124 usr.admin_row = True
1125 usr.permission = _admin_perm
1125 usr.permission = _admin_perm
1126 super_admin_rows.append(usr)
1126 super_admin_rows.append(usr)
1127
1127
1128 return super_admin_rows + owner_row + perm_rows
1128 return super_admin_rows + owner_row + perm_rows
1129
1129
1130 def permission_user_groups(self):
1130 def permission_user_groups(self):
1131 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1131 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1132 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1132 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1133 joinedload(UserGroupUserGroupToPerm.target_user_group),
1133 joinedload(UserGroupUserGroupToPerm.target_user_group),
1134 joinedload(UserGroupUserGroupToPerm.permission),)
1134 joinedload(UserGroupUserGroupToPerm.permission),)
1135
1135
1136 perm_rows = []
1136 perm_rows = []
1137 for _user_group in q.all():
1137 for _user_group in q.all():
1138 usr = AttributeDict(_user_group.user_group.get_dict())
1138 usr = AttributeDict(_user_group.user_group.get_dict())
1139 usr.permission = _user_group.permission.permission_name
1139 usr.permission = _user_group.permission.permission_name
1140 perm_rows.append(usr)
1140 perm_rows.append(usr)
1141
1141
1142 return perm_rows
1142 return perm_rows
1143
1143
1144 def _get_default_perms(self, user_group, suffix=''):
1144 def _get_default_perms(self, user_group, suffix=''):
1145 from rhodecode.model.permission import PermissionModel
1145 from rhodecode.model.permission import PermissionModel
1146 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1146 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1147
1147
1148 def get_default_perms(self, suffix=''):
1148 def get_default_perms(self, suffix=''):
1149 return self._get_default_perms(self, suffix)
1149 return self._get_default_perms(self, suffix)
1150
1150
1151 def get_api_data(self, with_group_members=True, include_secrets=False):
1151 def get_api_data(self, with_group_members=True, include_secrets=False):
1152 """
1152 """
1153 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1153 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1154 basically forwarded.
1154 basically forwarded.
1155
1155
1156 """
1156 """
1157 user_group = self
1157 user_group = self
1158
1158
1159 data = {
1159 data = {
1160 'users_group_id': user_group.users_group_id,
1160 'users_group_id': user_group.users_group_id,
1161 'group_name': user_group.users_group_name,
1161 'group_name': user_group.users_group_name,
1162 'group_description': user_group.user_group_description,
1162 'group_description': user_group.user_group_description,
1163 'active': user_group.users_group_active,
1163 'active': user_group.users_group_active,
1164 'owner': user_group.user.username,
1164 'owner': user_group.user.username,
1165 }
1165 }
1166 if with_group_members:
1166 if with_group_members:
1167 users = []
1167 users = []
1168 for user in user_group.members:
1168 for user in user_group.members:
1169 user = user.user
1169 user = user.user
1170 users.append(user.get_api_data(include_secrets=include_secrets))
1170 users.append(user.get_api_data(include_secrets=include_secrets))
1171 data['users'] = users
1171 data['users'] = users
1172
1172
1173 return data
1173 return data
1174
1174
1175
1175
1176 class UserGroupMember(Base, BaseModel):
1176 class UserGroupMember(Base, BaseModel):
1177 __tablename__ = 'users_groups_members'
1177 __tablename__ = 'users_groups_members'
1178 __table_args__ = (
1178 __table_args__ = (
1179 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1179 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1180 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1180 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1181 )
1181 )
1182
1182
1183 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1183 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1184 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1184 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1185 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1185 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1186
1186
1187 user = relationship('User', lazy='joined')
1187 user = relationship('User', lazy='joined')
1188 users_group = relationship('UserGroup')
1188 users_group = relationship('UserGroup')
1189
1189
1190 def __init__(self, gr_id='', u_id=''):
1190 def __init__(self, gr_id='', u_id=''):
1191 self.users_group_id = gr_id
1191 self.users_group_id = gr_id
1192 self.user_id = u_id
1192 self.user_id = u_id
1193
1193
1194
1194
1195 class RepositoryField(Base, BaseModel):
1195 class RepositoryField(Base, BaseModel):
1196 __tablename__ = 'repositories_fields'
1196 __tablename__ = 'repositories_fields'
1197 __table_args__ = (
1197 __table_args__ = (
1198 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1198 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1199 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1199 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1200 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1200 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1201 )
1201 )
1202 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1202 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1203
1203
1204 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1204 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1205 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1205 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1206 field_key = Column("field_key", String(250))
1206 field_key = Column("field_key", String(250))
1207 field_label = Column("field_label", String(1024), nullable=False)
1207 field_label = Column("field_label", String(1024), nullable=False)
1208 field_value = Column("field_value", String(10000), nullable=False)
1208 field_value = Column("field_value", String(10000), nullable=False)
1209 field_desc = Column("field_desc", String(1024), nullable=False)
1209 field_desc = Column("field_desc", String(1024), nullable=False)
1210 field_type = Column("field_type", String(255), nullable=False, unique=None)
1210 field_type = Column("field_type", String(255), nullable=False, unique=None)
1211 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1211 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1212
1212
1213 repository = relationship('Repository')
1213 repository = relationship('Repository')
1214
1214
1215 @property
1215 @property
1216 def field_key_prefixed(self):
1216 def field_key_prefixed(self):
1217 return 'ex_%s' % self.field_key
1217 return 'ex_%s' % self.field_key
1218
1218
1219 @classmethod
1219 @classmethod
1220 def un_prefix_key(cls, key):
1220 def un_prefix_key(cls, key):
1221 if key.startswith(cls.PREFIX):
1221 if key.startswith(cls.PREFIX):
1222 return key[len(cls.PREFIX):]
1222 return key[len(cls.PREFIX):]
1223 return key
1223 return key
1224
1224
1225 @classmethod
1225 @classmethod
1226 def get_by_key_name(cls, key, repo):
1226 def get_by_key_name(cls, key, repo):
1227 row = cls.query()\
1227 row = cls.query()\
1228 .filter(cls.repository == repo)\
1228 .filter(cls.repository == repo)\
1229 .filter(cls.field_key == key).scalar()
1229 .filter(cls.field_key == key).scalar()
1230 return row
1230 return row
1231
1231
1232
1232
1233 class Repository(Base, BaseModel):
1233 class Repository(Base, BaseModel):
1234 __tablename__ = 'repositories'
1234 __tablename__ = 'repositories'
1235 __table_args__ = (
1235 __table_args__ = (
1236 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1236 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1237 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1237 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1238 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1238 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1239 )
1239 )
1240 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1240 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1241 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1241 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1242
1242
1243 STATE_CREATED = 'repo_state_created'
1243 STATE_CREATED = 'repo_state_created'
1244 STATE_PENDING = 'repo_state_pending'
1244 STATE_PENDING = 'repo_state_pending'
1245 STATE_ERROR = 'repo_state_error'
1245 STATE_ERROR = 'repo_state_error'
1246
1246
1247 LOCK_AUTOMATIC = 'lock_auto'
1247 LOCK_AUTOMATIC = 'lock_auto'
1248 LOCK_API = 'lock_api'
1248 LOCK_API = 'lock_api'
1249 LOCK_WEB = 'lock_web'
1249 LOCK_WEB = 'lock_web'
1250 LOCK_PULL = 'lock_pull'
1250 LOCK_PULL = 'lock_pull'
1251
1251
1252 NAME_SEP = URL_SEP
1252 NAME_SEP = URL_SEP
1253
1253
1254 repo_id = Column(
1254 repo_id = Column(
1255 "repo_id", Integer(), nullable=False, unique=True, default=None,
1255 "repo_id", Integer(), nullable=False, unique=True, default=None,
1256 primary_key=True)
1256 primary_key=True)
1257 _repo_name = Column(
1257 _repo_name = Column(
1258 "repo_name", Text(), nullable=False, default=None)
1258 "repo_name", Text(), nullable=False, default=None)
1259 _repo_name_hash = Column(
1259 _repo_name_hash = Column(
1260 "repo_name_hash", String(255), nullable=False, unique=True)
1260 "repo_name_hash", String(255), nullable=False, unique=True)
1261 repo_state = Column("repo_state", String(255), nullable=True)
1261 repo_state = Column("repo_state", String(255), nullable=True)
1262
1262
1263 clone_uri = Column(
1263 clone_uri = Column(
1264 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1264 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1265 default=None)
1265 default=None)
1266 repo_type = Column(
1266 repo_type = Column(
1267 "repo_type", String(255), nullable=False, unique=False, default=None)
1267 "repo_type", String(255), nullable=False, unique=False, default=None)
1268 user_id = Column(
1268 user_id = Column(
1269 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1269 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1270 unique=False, default=None)
1270 unique=False, default=None)
1271 private = Column(
1271 private = Column(
1272 "private", Boolean(), nullable=True, unique=None, default=None)
1272 "private", Boolean(), nullable=True, unique=None, default=None)
1273 enable_statistics = Column(
1273 enable_statistics = Column(
1274 "statistics", Boolean(), nullable=True, unique=None, default=True)
1274 "statistics", Boolean(), nullable=True, unique=None, default=True)
1275 enable_downloads = Column(
1275 enable_downloads = Column(
1276 "downloads", Boolean(), nullable=True, unique=None, default=True)
1276 "downloads", Boolean(), nullable=True, unique=None, default=True)
1277 description = Column(
1277 description = Column(
1278 "description", String(10000), nullable=True, unique=None, default=None)
1278 "description", String(10000), nullable=True, unique=None, default=None)
1279 created_on = Column(
1279 created_on = Column(
1280 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1280 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1281 default=datetime.datetime.now)
1281 default=datetime.datetime.now)
1282 updated_on = Column(
1282 updated_on = Column(
1283 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1283 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1284 default=datetime.datetime.now)
1284 default=datetime.datetime.now)
1285 _landing_revision = Column(
1285 _landing_revision = Column(
1286 "landing_revision", String(255), nullable=False, unique=False,
1286 "landing_revision", String(255), nullable=False, unique=False,
1287 default=None)
1287 default=None)
1288 enable_locking = Column(
1288 enable_locking = Column(
1289 "enable_locking", Boolean(), nullable=False, unique=None,
1289 "enable_locking", Boolean(), nullable=False, unique=None,
1290 default=False)
1290 default=False)
1291 _locked = Column(
1291 _locked = Column(
1292 "locked", String(255), nullable=True, unique=False, default=None)
1292 "locked", String(255), nullable=True, unique=False, default=None)
1293 _changeset_cache = Column(
1293 _changeset_cache = Column(
1294 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1294 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1295
1295
1296 fork_id = Column(
1296 fork_id = Column(
1297 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1297 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1298 nullable=True, unique=False, default=None)
1298 nullable=True, unique=False, default=None)
1299 group_id = Column(
1299 group_id = Column(
1300 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1300 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1301 unique=False, default=None)
1301 unique=False, default=None)
1302
1302
1303 user = relationship('User', lazy='joined')
1303 user = relationship('User', lazy='joined')
1304 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1304 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1305 group = relationship('RepoGroup', lazy='joined')
1305 group = relationship('RepoGroup', lazy='joined')
1306 repo_to_perm = relationship(
1306 repo_to_perm = relationship(
1307 'UserRepoToPerm', cascade='all',
1307 'UserRepoToPerm', cascade='all',
1308 order_by='UserRepoToPerm.repo_to_perm_id')
1308 order_by='UserRepoToPerm.repo_to_perm_id')
1309 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1309 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1310 stats = relationship('Statistics', cascade='all', uselist=False)
1310 stats = relationship('Statistics', cascade='all', uselist=False)
1311
1311
1312 followers = relationship(
1312 followers = relationship(
1313 'UserFollowing',
1313 'UserFollowing',
1314 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1314 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1315 cascade='all')
1315 cascade='all')
1316 extra_fields = relationship(
1316 extra_fields = relationship(
1317 'RepositoryField', cascade="all, delete, delete-orphan")
1317 'RepositoryField', cascade="all, delete, delete-orphan")
1318 logs = relationship('UserLog')
1318 logs = relationship('UserLog')
1319 comments = relationship(
1319 comments = relationship(
1320 'ChangesetComment', cascade="all, delete, delete-orphan")
1320 'ChangesetComment', cascade="all, delete, delete-orphan")
1321 pull_requests_source = relationship(
1321 pull_requests_source = relationship(
1322 'PullRequest',
1322 'PullRequest',
1323 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1323 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1324 cascade="all, delete, delete-orphan")
1324 cascade="all, delete, delete-orphan")
1325 pull_requests_target = relationship(
1325 pull_requests_target = relationship(
1326 'PullRequest',
1326 'PullRequest',
1327 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1327 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1328 cascade="all, delete, delete-orphan")
1328 cascade="all, delete, delete-orphan")
1329 ui = relationship('RepoRhodeCodeUi', cascade="all")
1329 ui = relationship('RepoRhodeCodeUi', cascade="all")
1330 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1330 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1331 integrations = relationship('Integration',
1331 integrations = relationship('Integration',
1332 cascade="all, delete, delete-orphan")
1332 cascade="all, delete, delete-orphan")
1333
1333
1334 def __unicode__(self):
1334 def __unicode__(self):
1335 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1335 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1336 safe_unicode(self.repo_name))
1336 safe_unicode(self.repo_name))
1337
1337
1338 @hybrid_property
1338 @hybrid_property
1339 def landing_rev(self):
1339 def landing_rev(self):
1340 # always should return [rev_type, rev]
1340 # always should return [rev_type, rev]
1341 if self._landing_revision:
1341 if self._landing_revision:
1342 _rev_info = self._landing_revision.split(':')
1342 _rev_info = self._landing_revision.split(':')
1343 if len(_rev_info) < 2:
1343 if len(_rev_info) < 2:
1344 _rev_info.insert(0, 'rev')
1344 _rev_info.insert(0, 'rev')
1345 return [_rev_info[0], _rev_info[1]]
1345 return [_rev_info[0], _rev_info[1]]
1346 return [None, None]
1346 return [None, None]
1347
1347
1348 @landing_rev.setter
1348 @landing_rev.setter
1349 def landing_rev(self, val):
1349 def landing_rev(self, val):
1350 if ':' not in val:
1350 if ':' not in val:
1351 raise ValueError('value must be delimited with `:` and consist '
1351 raise ValueError('value must be delimited with `:` and consist '
1352 'of <rev_type>:<rev>, got %s instead' % val)
1352 'of <rev_type>:<rev>, got %s instead' % val)
1353 self._landing_revision = val
1353 self._landing_revision = val
1354
1354
1355 @hybrid_property
1355 @hybrid_property
1356 def locked(self):
1356 def locked(self):
1357 if self._locked:
1357 if self._locked:
1358 user_id, timelocked, reason = self._locked.split(':')
1358 user_id, timelocked, reason = self._locked.split(':')
1359 lock_values = int(user_id), timelocked, reason
1359 lock_values = int(user_id), timelocked, reason
1360 else:
1360 else:
1361 lock_values = [None, None, None]
1361 lock_values = [None, None, None]
1362 return lock_values
1362 return lock_values
1363
1363
1364 @locked.setter
1364 @locked.setter
1365 def locked(self, val):
1365 def locked(self, val):
1366 if val and isinstance(val, (list, tuple)):
1366 if val and isinstance(val, (list, tuple)):
1367 self._locked = ':'.join(map(str, val))
1367 self._locked = ':'.join(map(str, val))
1368 else:
1368 else:
1369 self._locked = None
1369 self._locked = None
1370
1370
1371 @hybrid_property
1371 @hybrid_property
1372 def changeset_cache(self):
1372 def changeset_cache(self):
1373 from rhodecode.lib.vcs.backends.base import EmptyCommit
1373 from rhodecode.lib.vcs.backends.base import EmptyCommit
1374 dummy = EmptyCommit().__json__()
1374 dummy = EmptyCommit().__json__()
1375 if not self._changeset_cache:
1375 if not self._changeset_cache:
1376 return dummy
1376 return dummy
1377 try:
1377 try:
1378 return json.loads(self._changeset_cache)
1378 return json.loads(self._changeset_cache)
1379 except TypeError:
1379 except TypeError:
1380 return dummy
1380 return dummy
1381 except Exception:
1381 except Exception:
1382 log.error(traceback.format_exc())
1382 log.error(traceback.format_exc())
1383 return dummy
1383 return dummy
1384
1384
1385 @changeset_cache.setter
1385 @changeset_cache.setter
1386 def changeset_cache(self, val):
1386 def changeset_cache(self, val):
1387 try:
1387 try:
1388 self._changeset_cache = json.dumps(val)
1388 self._changeset_cache = json.dumps(val)
1389 except Exception:
1389 except Exception:
1390 log.error(traceback.format_exc())
1390 log.error(traceback.format_exc())
1391
1391
1392 @hybrid_property
1392 @hybrid_property
1393 def repo_name(self):
1393 def repo_name(self):
1394 return self._repo_name
1394 return self._repo_name
1395
1395
1396 @repo_name.setter
1396 @repo_name.setter
1397 def repo_name(self, value):
1397 def repo_name(self, value):
1398 self._repo_name = value
1398 self._repo_name = value
1399 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1399 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1400
1400
1401 @classmethod
1401 @classmethod
1402 def normalize_repo_name(cls, repo_name):
1402 def normalize_repo_name(cls, repo_name):
1403 """
1403 """
1404 Normalizes os specific repo_name to the format internally stored inside
1404 Normalizes os specific repo_name to the format internally stored inside
1405 database using URL_SEP
1405 database using URL_SEP
1406
1406
1407 :param cls:
1407 :param cls:
1408 :param repo_name:
1408 :param repo_name:
1409 """
1409 """
1410 return cls.NAME_SEP.join(repo_name.split(os.sep))
1410 return cls.NAME_SEP.join(repo_name.split(os.sep))
1411
1411
1412 @classmethod
1412 @classmethod
1413 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1413 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1414 session = Session()
1414 session = Session()
1415 q = session.query(cls).filter(cls.repo_name == repo_name)
1415 q = session.query(cls).filter(cls.repo_name == repo_name)
1416
1416
1417 if cache:
1417 if cache:
1418 if identity_cache:
1418 if identity_cache:
1419 val = cls.identity_cache(session, 'repo_name', repo_name)
1419 val = cls.identity_cache(session, 'repo_name', repo_name)
1420 if val:
1420 if val:
1421 return val
1421 return val
1422 else:
1422 else:
1423 q = q.options(
1423 q = q.options(
1424 FromCache("sql_cache_short",
1424 FromCache("sql_cache_short",
1425 "get_repo_by_name_%s" % _hash_key(repo_name)))
1425 "get_repo_by_name_%s" % _hash_key(repo_name)))
1426
1426
1427 return q.scalar()
1427 return q.scalar()
1428
1428
1429 @classmethod
1429 @classmethod
1430 def get_by_full_path(cls, repo_full_path):
1430 def get_by_full_path(cls, repo_full_path):
1431 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1431 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1432 repo_name = cls.normalize_repo_name(repo_name)
1432 repo_name = cls.normalize_repo_name(repo_name)
1433 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1433 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1434
1434
1435 @classmethod
1435 @classmethod
1436 def get_repo_forks(cls, repo_id):
1436 def get_repo_forks(cls, repo_id):
1437 return cls.query().filter(Repository.fork_id == repo_id)
1437 return cls.query().filter(Repository.fork_id == repo_id)
1438
1438
1439 @classmethod
1439 @classmethod
1440 def base_path(cls):
1440 def base_path(cls):
1441 """
1441 """
1442 Returns base path when all repos are stored
1442 Returns base path when all repos are stored
1443
1443
1444 :param cls:
1444 :param cls:
1445 """
1445 """
1446 q = Session().query(RhodeCodeUi)\
1446 q = Session().query(RhodeCodeUi)\
1447 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1447 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1448 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1448 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1449 return q.one().ui_value
1449 return q.one().ui_value
1450
1450
1451 @classmethod
1451 @classmethod
1452 def is_valid(cls, repo_name):
1452 def is_valid(cls, repo_name):
1453 """
1453 """
1454 returns True if given repo name is a valid filesystem repository
1454 returns True if given repo name is a valid filesystem repository
1455
1455
1456 :param cls:
1456 :param cls:
1457 :param repo_name:
1457 :param repo_name:
1458 """
1458 """
1459 from rhodecode.lib.utils import is_valid_repo
1459 from rhodecode.lib.utils import is_valid_repo
1460
1460
1461 return is_valid_repo(repo_name, cls.base_path())
1461 return is_valid_repo(repo_name, cls.base_path())
1462
1462
1463 @classmethod
1463 @classmethod
1464 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1464 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1465 case_insensitive=True):
1465 case_insensitive=True):
1466 q = Repository.query()
1466 q = Repository.query()
1467
1467
1468 if not isinstance(user_id, Optional):
1468 if not isinstance(user_id, Optional):
1469 q = q.filter(Repository.user_id == user_id)
1469 q = q.filter(Repository.user_id == user_id)
1470
1470
1471 if not isinstance(group_id, Optional):
1471 if not isinstance(group_id, Optional):
1472 q = q.filter(Repository.group_id == group_id)
1472 q = q.filter(Repository.group_id == group_id)
1473
1473
1474 if case_insensitive:
1474 if case_insensitive:
1475 q = q.order_by(func.lower(Repository.repo_name))
1475 q = q.order_by(func.lower(Repository.repo_name))
1476 else:
1476 else:
1477 q = q.order_by(Repository.repo_name)
1477 q = q.order_by(Repository.repo_name)
1478 return q.all()
1478 return q.all()
1479
1479
1480 @property
1480 @property
1481 def forks(self):
1481 def forks(self):
1482 """
1482 """
1483 Return forks of this repo
1483 Return forks of this repo
1484 """
1484 """
1485 return Repository.get_repo_forks(self.repo_id)
1485 return Repository.get_repo_forks(self.repo_id)
1486
1486
1487 @property
1487 @property
1488 def parent(self):
1488 def parent(self):
1489 """
1489 """
1490 Returns fork parent
1490 Returns fork parent
1491 """
1491 """
1492 return self.fork
1492 return self.fork
1493
1493
1494 @property
1494 @property
1495 def just_name(self):
1495 def just_name(self):
1496 return self.repo_name.split(self.NAME_SEP)[-1]
1496 return self.repo_name.split(self.NAME_SEP)[-1]
1497
1497
1498 @property
1498 @property
1499 def groups_with_parents(self):
1499 def groups_with_parents(self):
1500 groups = []
1500 groups = []
1501 if self.group is None:
1501 if self.group is None:
1502 return groups
1502 return groups
1503
1503
1504 cur_gr = self.group
1504 cur_gr = self.group
1505 groups.insert(0, cur_gr)
1505 groups.insert(0, cur_gr)
1506 while 1:
1506 while 1:
1507 gr = getattr(cur_gr, 'parent_group', None)
1507 gr = getattr(cur_gr, 'parent_group', None)
1508 cur_gr = cur_gr.parent_group
1508 cur_gr = cur_gr.parent_group
1509 if gr is None:
1509 if gr is None:
1510 break
1510 break
1511 groups.insert(0, gr)
1511 groups.insert(0, gr)
1512
1512
1513 return groups
1513 return groups
1514
1514
1515 @property
1515 @property
1516 def groups_and_repo(self):
1516 def groups_and_repo(self):
1517 return self.groups_with_parents, self
1517 return self.groups_with_parents, self
1518
1518
1519 @LazyProperty
1519 @LazyProperty
1520 def repo_path(self):
1520 def repo_path(self):
1521 """
1521 """
1522 Returns base full path for that repository means where it actually
1522 Returns base full path for that repository means where it actually
1523 exists on a filesystem
1523 exists on a filesystem
1524 """
1524 """
1525 q = Session().query(RhodeCodeUi).filter(
1525 q = Session().query(RhodeCodeUi).filter(
1526 RhodeCodeUi.ui_key == self.NAME_SEP)
1526 RhodeCodeUi.ui_key == self.NAME_SEP)
1527 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1527 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1528 return q.one().ui_value
1528 return q.one().ui_value
1529
1529
1530 @property
1530 @property
1531 def repo_full_path(self):
1531 def repo_full_path(self):
1532 p = [self.repo_path]
1532 p = [self.repo_path]
1533 # we need to split the name by / since this is how we store the
1533 # we need to split the name by / since this is how we store the
1534 # names in the database, but that eventually needs to be converted
1534 # names in the database, but that eventually needs to be converted
1535 # into a valid system path
1535 # into a valid system path
1536 p += self.repo_name.split(self.NAME_SEP)
1536 p += self.repo_name.split(self.NAME_SEP)
1537 return os.path.join(*map(safe_unicode, p))
1537 return os.path.join(*map(safe_unicode, p))
1538
1538
1539 @property
1539 @property
1540 def cache_keys(self):
1540 def cache_keys(self):
1541 """
1541 """
1542 Returns associated cache keys for that repo
1542 Returns associated cache keys for that repo
1543 """
1543 """
1544 return CacheKey.query()\
1544 return CacheKey.query()\
1545 .filter(CacheKey.cache_args == self.repo_name)\
1545 .filter(CacheKey.cache_args == self.repo_name)\
1546 .order_by(CacheKey.cache_key)\
1546 .order_by(CacheKey.cache_key)\
1547 .all()
1547 .all()
1548
1548
1549 def get_new_name(self, repo_name):
1549 def get_new_name(self, repo_name):
1550 """
1550 """
1551 returns new full repository name based on assigned group and new new
1551 returns new full repository name based on assigned group and new new
1552
1552
1553 :param group_name:
1553 :param group_name:
1554 """
1554 """
1555 path_prefix = self.group.full_path_splitted if self.group else []
1555 path_prefix = self.group.full_path_splitted if self.group else []
1556 return self.NAME_SEP.join(path_prefix + [repo_name])
1556 return self.NAME_SEP.join(path_prefix + [repo_name])
1557
1557
1558 @property
1558 @property
1559 def _config(self):
1559 def _config(self):
1560 """
1560 """
1561 Returns db based config object.
1561 Returns db based config object.
1562 """
1562 """
1563 from rhodecode.lib.utils import make_db_config
1563 from rhodecode.lib.utils import make_db_config
1564 return make_db_config(clear_session=False, repo=self)
1564 return make_db_config(clear_session=False, repo=self)
1565
1565
1566 def permissions(self, with_admins=True, with_owner=True):
1566 def permissions(self, with_admins=True, with_owner=True):
1567 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1567 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1568 q = q.options(joinedload(UserRepoToPerm.repository),
1568 q = q.options(joinedload(UserRepoToPerm.repository),
1569 joinedload(UserRepoToPerm.user),
1569 joinedload(UserRepoToPerm.user),
1570 joinedload(UserRepoToPerm.permission),)
1570 joinedload(UserRepoToPerm.permission),)
1571
1571
1572 # get owners and admins and permissions. We do a trick of re-writing
1572 # get owners and admins and permissions. We do a trick of re-writing
1573 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1573 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1574 # has a global reference and changing one object propagates to all
1574 # has a global reference and changing one object propagates to all
1575 # others. This means if admin is also an owner admin_row that change
1575 # others. This means if admin is also an owner admin_row that change
1576 # would propagate to both objects
1576 # would propagate to both objects
1577 perm_rows = []
1577 perm_rows = []
1578 for _usr in q.all():
1578 for _usr in q.all():
1579 usr = AttributeDict(_usr.user.get_dict())
1579 usr = AttributeDict(_usr.user.get_dict())
1580 usr.permission = _usr.permission.permission_name
1580 usr.permission = _usr.permission.permission_name
1581 perm_rows.append(usr)
1581 perm_rows.append(usr)
1582
1582
1583 # filter the perm rows by 'default' first and then sort them by
1583 # filter the perm rows by 'default' first and then sort them by
1584 # admin,write,read,none permissions sorted again alphabetically in
1584 # admin,write,read,none permissions sorted again alphabetically in
1585 # each group
1585 # each group
1586 perm_rows = sorted(perm_rows, key=display_sort)
1586 perm_rows = sorted(perm_rows, key=display_sort)
1587
1587
1588 _admin_perm = 'repository.admin'
1588 _admin_perm = 'repository.admin'
1589 owner_row = []
1589 owner_row = []
1590 if with_owner:
1590 if with_owner:
1591 usr = AttributeDict(self.user.get_dict())
1591 usr = AttributeDict(self.user.get_dict())
1592 usr.owner_row = True
1592 usr.owner_row = True
1593 usr.permission = _admin_perm
1593 usr.permission = _admin_perm
1594 owner_row.append(usr)
1594 owner_row.append(usr)
1595
1595
1596 super_admin_rows = []
1596 super_admin_rows = []
1597 if with_admins:
1597 if with_admins:
1598 for usr in User.get_all_super_admins():
1598 for usr in User.get_all_super_admins():
1599 # if this admin is also owner, don't double the record
1599 # if this admin is also owner, don't double the record
1600 if usr.user_id == owner_row[0].user_id:
1600 if usr.user_id == owner_row[0].user_id:
1601 owner_row[0].admin_row = True
1601 owner_row[0].admin_row = True
1602 else:
1602 else:
1603 usr = AttributeDict(usr.get_dict())
1603 usr = AttributeDict(usr.get_dict())
1604 usr.admin_row = True
1604 usr.admin_row = True
1605 usr.permission = _admin_perm
1605 usr.permission = _admin_perm
1606 super_admin_rows.append(usr)
1606 super_admin_rows.append(usr)
1607
1607
1608 return super_admin_rows + owner_row + perm_rows
1608 return super_admin_rows + owner_row + perm_rows
1609
1609
1610 def permission_user_groups(self):
1610 def permission_user_groups(self):
1611 q = UserGroupRepoToPerm.query().filter(
1611 q = UserGroupRepoToPerm.query().filter(
1612 UserGroupRepoToPerm.repository == self)
1612 UserGroupRepoToPerm.repository == self)
1613 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1613 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1614 joinedload(UserGroupRepoToPerm.users_group),
1614 joinedload(UserGroupRepoToPerm.users_group),
1615 joinedload(UserGroupRepoToPerm.permission),)
1615 joinedload(UserGroupRepoToPerm.permission),)
1616
1616
1617 perm_rows = []
1617 perm_rows = []
1618 for _user_group in q.all():
1618 for _user_group in q.all():
1619 usr = AttributeDict(_user_group.users_group.get_dict())
1619 usr = AttributeDict(_user_group.users_group.get_dict())
1620 usr.permission = _user_group.permission.permission_name
1620 usr.permission = _user_group.permission.permission_name
1621 perm_rows.append(usr)
1621 perm_rows.append(usr)
1622
1622
1623 return perm_rows
1623 return perm_rows
1624
1624
1625 def get_api_data(self, include_secrets=False):
1625 def get_api_data(self, include_secrets=False):
1626 """
1626 """
1627 Common function for generating repo api data
1627 Common function for generating repo api data
1628
1628
1629 :param include_secrets: See :meth:`User.get_api_data`.
1629 :param include_secrets: See :meth:`User.get_api_data`.
1630
1630
1631 """
1631 """
1632 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1632 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1633 # move this methods on models level.
1633 # move this methods on models level.
1634 from rhodecode.model.settings import SettingsModel
1634 from rhodecode.model.settings import SettingsModel
1635
1635
1636 repo = self
1636 repo = self
1637 _user_id, _time, _reason = self.locked
1637 _user_id, _time, _reason = self.locked
1638
1638
1639 data = {
1639 data = {
1640 'repo_id': repo.repo_id,
1640 'repo_id': repo.repo_id,
1641 'repo_name': repo.repo_name,
1641 'repo_name': repo.repo_name,
1642 'repo_type': repo.repo_type,
1642 'repo_type': repo.repo_type,
1643 'clone_uri': repo.clone_uri or '',
1643 'clone_uri': repo.clone_uri or '',
1644 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1644 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1645 'private': repo.private,
1645 'private': repo.private,
1646 'created_on': repo.created_on,
1646 'created_on': repo.created_on,
1647 'description': repo.description,
1647 'description': repo.description,
1648 'landing_rev': repo.landing_rev,
1648 'landing_rev': repo.landing_rev,
1649 'owner': repo.user.username,
1649 'owner': repo.user.username,
1650 'fork_of': repo.fork.repo_name if repo.fork else None,
1650 'fork_of': repo.fork.repo_name if repo.fork else None,
1651 'enable_statistics': repo.enable_statistics,
1651 'enable_statistics': repo.enable_statistics,
1652 'enable_locking': repo.enable_locking,
1652 'enable_locking': repo.enable_locking,
1653 'enable_downloads': repo.enable_downloads,
1653 'enable_downloads': repo.enable_downloads,
1654 'last_changeset': repo.changeset_cache,
1654 'last_changeset': repo.changeset_cache,
1655 'locked_by': User.get(_user_id).get_api_data(
1655 'locked_by': User.get(_user_id).get_api_data(
1656 include_secrets=include_secrets) if _user_id else None,
1656 include_secrets=include_secrets) if _user_id else None,
1657 'locked_date': time_to_datetime(_time) if _time else None,
1657 'locked_date': time_to_datetime(_time) if _time else None,
1658 'lock_reason': _reason if _reason else None,
1658 'lock_reason': _reason if _reason else None,
1659 }
1659 }
1660
1660
1661 # TODO: mikhail: should be per-repo settings here
1661 # TODO: mikhail: should be per-repo settings here
1662 rc_config = SettingsModel().get_all_settings()
1662 rc_config = SettingsModel().get_all_settings()
1663 repository_fields = str2bool(
1663 repository_fields = str2bool(
1664 rc_config.get('rhodecode_repository_fields'))
1664 rc_config.get('rhodecode_repository_fields'))
1665 if repository_fields:
1665 if repository_fields:
1666 for f in self.extra_fields:
1666 for f in self.extra_fields:
1667 data[f.field_key_prefixed] = f.field_value
1667 data[f.field_key_prefixed] = f.field_value
1668
1668
1669 return data
1669 return data
1670
1670
1671 @classmethod
1671 @classmethod
1672 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1672 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1673 if not lock_time:
1673 if not lock_time:
1674 lock_time = time.time()
1674 lock_time = time.time()
1675 if not lock_reason:
1675 if not lock_reason:
1676 lock_reason = cls.LOCK_AUTOMATIC
1676 lock_reason = cls.LOCK_AUTOMATIC
1677 repo.locked = [user_id, lock_time, lock_reason]
1677 repo.locked = [user_id, lock_time, lock_reason]
1678 Session().add(repo)
1678 Session().add(repo)
1679 Session().commit()
1679 Session().commit()
1680
1680
1681 @classmethod
1681 @classmethod
1682 def unlock(cls, repo):
1682 def unlock(cls, repo):
1683 repo.locked = None
1683 repo.locked = None
1684 Session().add(repo)
1684 Session().add(repo)
1685 Session().commit()
1685 Session().commit()
1686
1686
1687 @classmethod
1687 @classmethod
1688 def getlock(cls, repo):
1688 def getlock(cls, repo):
1689 return repo.locked
1689 return repo.locked
1690
1690
1691 def is_user_lock(self, user_id):
1691 def is_user_lock(self, user_id):
1692 if self.lock[0]:
1692 if self.lock[0]:
1693 lock_user_id = safe_int(self.lock[0])
1693 lock_user_id = safe_int(self.lock[0])
1694 user_id = safe_int(user_id)
1694 user_id = safe_int(user_id)
1695 # both are ints, and they are equal
1695 # both are ints, and they are equal
1696 return all([lock_user_id, user_id]) and lock_user_id == user_id
1696 return all([lock_user_id, user_id]) and lock_user_id == user_id
1697
1697
1698 return False
1698 return False
1699
1699
1700 def get_locking_state(self, action, user_id, only_when_enabled=True):
1700 def get_locking_state(self, action, user_id, only_when_enabled=True):
1701 """
1701 """
1702 Checks locking on this repository, if locking is enabled and lock is
1702 Checks locking on this repository, if locking is enabled and lock is
1703 present returns a tuple of make_lock, locked, locked_by.
1703 present returns a tuple of make_lock, locked, locked_by.
1704 make_lock can have 3 states None (do nothing) True, make lock
1704 make_lock can have 3 states None (do nothing) True, make lock
1705 False release lock, This value is later propagated to hooks, which
1705 False release lock, This value is later propagated to hooks, which
1706 do the locking. Think about this as signals passed to hooks what to do.
1706 do the locking. Think about this as signals passed to hooks what to do.
1707
1707
1708 """
1708 """
1709 # TODO: johbo: This is part of the business logic and should be moved
1709 # TODO: johbo: This is part of the business logic and should be moved
1710 # into the RepositoryModel.
1710 # into the RepositoryModel.
1711
1711
1712 if action not in ('push', 'pull'):
1712 if action not in ('push', 'pull'):
1713 raise ValueError("Invalid action value: %s" % repr(action))
1713 raise ValueError("Invalid action value: %s" % repr(action))
1714
1714
1715 # defines if locked error should be thrown to user
1715 # defines if locked error should be thrown to user
1716 currently_locked = False
1716 currently_locked = False
1717 # defines if new lock should be made, tri-state
1717 # defines if new lock should be made, tri-state
1718 make_lock = None
1718 make_lock = None
1719 repo = self
1719 repo = self
1720 user = User.get(user_id)
1720 user = User.get(user_id)
1721
1721
1722 lock_info = repo.locked
1722 lock_info = repo.locked
1723
1723
1724 if repo and (repo.enable_locking or not only_when_enabled):
1724 if repo and (repo.enable_locking or not only_when_enabled):
1725 if action == 'push':
1725 if action == 'push':
1726 # check if it's already locked !, if it is compare users
1726 # check if it's already locked !, if it is compare users
1727 locked_by_user_id = lock_info[0]
1727 locked_by_user_id = lock_info[0]
1728 if user.user_id == locked_by_user_id:
1728 if user.user_id == locked_by_user_id:
1729 log.debug(
1729 log.debug(
1730 'Got `push` action from user %s, now unlocking', user)
1730 'Got `push` action from user %s, now unlocking', user)
1731 # unlock if we have push from user who locked
1731 # unlock if we have push from user who locked
1732 make_lock = False
1732 make_lock = False
1733 else:
1733 else:
1734 # we're not the same user who locked, ban with
1734 # we're not the same user who locked, ban with
1735 # code defined in settings (default is 423 HTTP Locked) !
1735 # code defined in settings (default is 423 HTTP Locked) !
1736 log.debug('Repo %s is currently locked by %s', repo, user)
1736 log.debug('Repo %s is currently locked by %s', repo, user)
1737 currently_locked = True
1737 currently_locked = True
1738 elif action == 'pull':
1738 elif action == 'pull':
1739 # [0] user [1] date
1739 # [0] user [1] date
1740 if lock_info[0] and lock_info[1]:
1740 if lock_info[0] and lock_info[1]:
1741 log.debug('Repo %s is currently locked by %s', repo, user)
1741 log.debug('Repo %s is currently locked by %s', repo, user)
1742 currently_locked = True
1742 currently_locked = True
1743 else:
1743 else:
1744 log.debug('Setting lock on repo %s by %s', repo, user)
1744 log.debug('Setting lock on repo %s by %s', repo, user)
1745 make_lock = True
1745 make_lock = True
1746
1746
1747 else:
1747 else:
1748 log.debug('Repository %s do not have locking enabled', repo)
1748 log.debug('Repository %s do not have locking enabled', repo)
1749
1749
1750 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1750 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1751 make_lock, currently_locked, lock_info)
1751 make_lock, currently_locked, lock_info)
1752
1752
1753 from rhodecode.lib.auth import HasRepoPermissionAny
1753 from rhodecode.lib.auth import HasRepoPermissionAny
1754 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1754 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1755 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1755 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1756 # if we don't have at least write permission we cannot make a lock
1756 # if we don't have at least write permission we cannot make a lock
1757 log.debug('lock state reset back to FALSE due to lack '
1757 log.debug('lock state reset back to FALSE due to lack '
1758 'of at least read permission')
1758 'of at least read permission')
1759 make_lock = False
1759 make_lock = False
1760
1760
1761 return make_lock, currently_locked, lock_info
1761 return make_lock, currently_locked, lock_info
1762
1762
1763 @property
1763 @property
1764 def last_db_change(self):
1764 def last_db_change(self):
1765 return self.updated_on
1765 return self.updated_on
1766
1766
1767 @property
1767 @property
1768 def clone_uri_hidden(self):
1768 def clone_uri_hidden(self):
1769 clone_uri = self.clone_uri
1769 clone_uri = self.clone_uri
1770 if clone_uri:
1770 if clone_uri:
1771 import urlobject
1771 import urlobject
1772 url_obj = urlobject.URLObject(clone_uri)
1772 url_obj = urlobject.URLObject(clone_uri)
1773 if url_obj.password:
1773 if url_obj.password:
1774 clone_uri = url_obj.with_password('*****')
1774 clone_uri = url_obj.with_password('*****')
1775 return clone_uri
1775 return clone_uri
1776
1776
1777 def clone_url(self, **override):
1777 def clone_url(self, **override):
1778 qualified_home_url = url('home', qualified=True)
1778 qualified_home_url = url('home', qualified=True)
1779
1779
1780 uri_tmpl = None
1780 uri_tmpl = None
1781 if 'with_id' in override:
1781 if 'with_id' in override:
1782 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1782 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1783 del override['with_id']
1783 del override['with_id']
1784
1784
1785 if 'uri_tmpl' in override:
1785 if 'uri_tmpl' in override:
1786 uri_tmpl = override['uri_tmpl']
1786 uri_tmpl = override['uri_tmpl']
1787 del override['uri_tmpl']
1787 del override['uri_tmpl']
1788
1788
1789 # we didn't override our tmpl from **overrides
1789 # we didn't override our tmpl from **overrides
1790 if not uri_tmpl:
1790 if not uri_tmpl:
1791 uri_tmpl = self.DEFAULT_CLONE_URI
1791 uri_tmpl = self.DEFAULT_CLONE_URI
1792 try:
1792 try:
1793 from pylons import tmpl_context as c
1793 from pylons import tmpl_context as c
1794 uri_tmpl = c.clone_uri_tmpl
1794 uri_tmpl = c.clone_uri_tmpl
1795 except Exception:
1795 except Exception:
1796 # in any case if we call this outside of request context,
1796 # in any case if we call this outside of request context,
1797 # ie, not having tmpl_context set up
1797 # ie, not having tmpl_context set up
1798 pass
1798 pass
1799
1799
1800 return get_clone_url(uri_tmpl=uri_tmpl,
1800 return get_clone_url(uri_tmpl=uri_tmpl,
1801 qualifed_home_url=qualified_home_url,
1801 qualifed_home_url=qualified_home_url,
1802 repo_name=self.repo_name,
1802 repo_name=self.repo_name,
1803 repo_id=self.repo_id, **override)
1803 repo_id=self.repo_id, **override)
1804
1804
1805 def set_state(self, state):
1805 def set_state(self, state):
1806 self.repo_state = state
1806 self.repo_state = state
1807 Session().add(self)
1807 Session().add(self)
1808 #==========================================================================
1808 #==========================================================================
1809 # SCM PROPERTIES
1809 # SCM PROPERTIES
1810 #==========================================================================
1810 #==========================================================================
1811
1811
1812 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1812 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1813 return get_commit_safe(
1813 return get_commit_safe(
1814 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1814 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1815
1815
1816 def get_changeset(self, rev=None, pre_load=None):
1816 def get_changeset(self, rev=None, pre_load=None):
1817 warnings.warn("Use get_commit", DeprecationWarning)
1817 warnings.warn("Use get_commit", DeprecationWarning)
1818 commit_id = None
1818 commit_id = None
1819 commit_idx = None
1819 commit_idx = None
1820 if isinstance(rev, basestring):
1820 if isinstance(rev, basestring):
1821 commit_id = rev
1821 commit_id = rev
1822 else:
1822 else:
1823 commit_idx = rev
1823 commit_idx = rev
1824 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1824 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1825 pre_load=pre_load)
1825 pre_load=pre_load)
1826
1826
1827 def get_landing_commit(self):
1827 def get_landing_commit(self):
1828 """
1828 """
1829 Returns landing commit, or if that doesn't exist returns the tip
1829 Returns landing commit, or if that doesn't exist returns the tip
1830 """
1830 """
1831 _rev_type, _rev = self.landing_rev
1831 _rev_type, _rev = self.landing_rev
1832 commit = self.get_commit(_rev)
1832 commit = self.get_commit(_rev)
1833 if isinstance(commit, EmptyCommit):
1833 if isinstance(commit, EmptyCommit):
1834 return self.get_commit()
1834 return self.get_commit()
1835 return commit
1835 return commit
1836
1836
1837 def update_commit_cache(self, cs_cache=None, config=None):
1837 def update_commit_cache(self, cs_cache=None, config=None):
1838 """
1838 """
1839 Update cache of last changeset for repository, keys should be::
1839 Update cache of last changeset for repository, keys should be::
1840
1840
1841 short_id
1841 short_id
1842 raw_id
1842 raw_id
1843 revision
1843 revision
1844 parents
1844 parents
1845 message
1845 message
1846 date
1846 date
1847 author
1847 author
1848
1848
1849 :param cs_cache:
1849 :param cs_cache:
1850 """
1850 """
1851 from rhodecode.lib.vcs.backends.base import BaseChangeset
1851 from rhodecode.lib.vcs.backends.base import BaseChangeset
1852 if cs_cache is None:
1852 if cs_cache is None:
1853 # use no-cache version here
1853 # use no-cache version here
1854 scm_repo = self.scm_instance(cache=False, config=config)
1854 scm_repo = self.scm_instance(cache=False, config=config)
1855 if scm_repo:
1855 if scm_repo:
1856 cs_cache = scm_repo.get_commit(
1856 cs_cache = scm_repo.get_commit(
1857 pre_load=["author", "date", "message", "parents"])
1857 pre_load=["author", "date", "message", "parents"])
1858 else:
1858 else:
1859 cs_cache = EmptyCommit()
1859 cs_cache = EmptyCommit()
1860
1860
1861 if isinstance(cs_cache, BaseChangeset):
1861 if isinstance(cs_cache, BaseChangeset):
1862 cs_cache = cs_cache.__json__()
1862 cs_cache = cs_cache.__json__()
1863
1863
1864 def is_outdated(new_cs_cache):
1864 def is_outdated(new_cs_cache):
1865 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1865 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1866 new_cs_cache['revision'] != self.changeset_cache['revision']):
1866 new_cs_cache['revision'] != self.changeset_cache['revision']):
1867 return True
1867 return True
1868 return False
1868 return False
1869
1869
1870 # check if we have maybe already latest cached revision
1870 # check if we have maybe already latest cached revision
1871 if is_outdated(cs_cache) or not self.changeset_cache:
1871 if is_outdated(cs_cache) or not self.changeset_cache:
1872 _default = datetime.datetime.fromtimestamp(0)
1872 _default = datetime.datetime.fromtimestamp(0)
1873 last_change = cs_cache.get('date') or _default
1873 last_change = cs_cache.get('date') or _default
1874 log.debug('updated repo %s with new cs cache %s',
1874 log.debug('updated repo %s with new cs cache %s',
1875 self.repo_name, cs_cache)
1875 self.repo_name, cs_cache)
1876 self.updated_on = last_change
1876 self.updated_on = last_change
1877 self.changeset_cache = cs_cache
1877 self.changeset_cache = cs_cache
1878 Session().add(self)
1878 Session().add(self)
1879 Session().commit()
1879 Session().commit()
1880 else:
1880 else:
1881 log.debug('Skipping update_commit_cache for repo:`%s` '
1881 log.debug('Skipping update_commit_cache for repo:`%s` '
1882 'commit already with latest changes', self.repo_name)
1882 'commit already with latest changes', self.repo_name)
1883
1883
1884 @property
1884 @property
1885 def tip(self):
1885 def tip(self):
1886 return self.get_commit('tip')
1886 return self.get_commit('tip')
1887
1887
1888 @property
1888 @property
1889 def author(self):
1889 def author(self):
1890 return self.tip.author
1890 return self.tip.author
1891
1891
1892 @property
1892 @property
1893 def last_change(self):
1893 def last_change(self):
1894 return self.scm_instance().last_change
1894 return self.scm_instance().last_change
1895
1895
1896 def get_comments(self, revisions=None):
1896 def get_comments(self, revisions=None):
1897 """
1897 """
1898 Returns comments for this repository grouped by revisions
1898 Returns comments for this repository grouped by revisions
1899
1899
1900 :param revisions: filter query by revisions only
1900 :param revisions: filter query by revisions only
1901 """
1901 """
1902 cmts = ChangesetComment.query()\
1902 cmts = ChangesetComment.query()\
1903 .filter(ChangesetComment.repo == self)
1903 .filter(ChangesetComment.repo == self)
1904 if revisions:
1904 if revisions:
1905 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1905 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1906 grouped = collections.defaultdict(list)
1906 grouped = collections.defaultdict(list)
1907 for cmt in cmts.all():
1907 for cmt in cmts.all():
1908 grouped[cmt.revision].append(cmt)
1908 grouped[cmt.revision].append(cmt)
1909 return grouped
1909 return grouped
1910
1910
1911 def statuses(self, revisions=None):
1911 def statuses(self, revisions=None):
1912 """
1912 """
1913 Returns statuses for this repository
1913 Returns statuses for this repository
1914
1914
1915 :param revisions: list of revisions to get statuses for
1915 :param revisions: list of revisions to get statuses for
1916 """
1916 """
1917 statuses = ChangesetStatus.query()\
1917 statuses = ChangesetStatus.query()\
1918 .filter(ChangesetStatus.repo == self)\
1918 .filter(ChangesetStatus.repo == self)\
1919 .filter(ChangesetStatus.version == 0)
1919 .filter(ChangesetStatus.version == 0)
1920
1920
1921 if revisions:
1921 if revisions:
1922 # Try doing the filtering in chunks to avoid hitting limits
1922 # Try doing the filtering in chunks to avoid hitting limits
1923 size = 500
1923 size = 500
1924 status_results = []
1924 status_results = []
1925 for chunk in xrange(0, len(revisions), size):
1925 for chunk in xrange(0, len(revisions), size):
1926 status_results += statuses.filter(
1926 status_results += statuses.filter(
1927 ChangesetStatus.revision.in_(
1927 ChangesetStatus.revision.in_(
1928 revisions[chunk: chunk+size])
1928 revisions[chunk: chunk+size])
1929 ).all()
1929 ).all()
1930 else:
1930 else:
1931 status_results = statuses.all()
1931 status_results = statuses.all()
1932
1932
1933 grouped = {}
1933 grouped = {}
1934
1934
1935 # maybe we have open new pullrequest without a status?
1935 # maybe we have open new pullrequest without a status?
1936 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1936 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1937 status_lbl = ChangesetStatus.get_status_lbl(stat)
1937 status_lbl = ChangesetStatus.get_status_lbl(stat)
1938 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1938 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1939 for rev in pr.revisions:
1939 for rev in pr.revisions:
1940 pr_id = pr.pull_request_id
1940 pr_id = pr.pull_request_id
1941 pr_repo = pr.target_repo.repo_name
1941 pr_repo = pr.target_repo.repo_name
1942 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1942 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1943
1943
1944 for stat in status_results:
1944 for stat in status_results:
1945 pr_id = pr_repo = None
1945 pr_id = pr_repo = None
1946 if stat.pull_request:
1946 if stat.pull_request:
1947 pr_id = stat.pull_request.pull_request_id
1947 pr_id = stat.pull_request.pull_request_id
1948 pr_repo = stat.pull_request.target_repo.repo_name
1948 pr_repo = stat.pull_request.target_repo.repo_name
1949 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1949 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1950 pr_id, pr_repo]
1950 pr_id, pr_repo]
1951 return grouped
1951 return grouped
1952
1952
1953 # ==========================================================================
1953 # ==========================================================================
1954 # SCM CACHE INSTANCE
1954 # SCM CACHE INSTANCE
1955 # ==========================================================================
1955 # ==========================================================================
1956
1956
1957 def scm_instance(self, **kwargs):
1957 def scm_instance(self, **kwargs):
1958 import rhodecode
1958 import rhodecode
1959
1959
1960 # Passing a config will not hit the cache currently only used
1960 # Passing a config will not hit the cache currently only used
1961 # for repo2dbmapper
1961 # for repo2dbmapper
1962 config = kwargs.pop('config', None)
1962 config = kwargs.pop('config', None)
1963 cache = kwargs.pop('cache', None)
1963 cache = kwargs.pop('cache', None)
1964 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1964 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1965 # if cache is NOT defined use default global, else we have a full
1965 # if cache is NOT defined use default global, else we have a full
1966 # control over cache behaviour
1966 # control over cache behaviour
1967 if cache is None and full_cache and not config:
1967 if cache is None and full_cache and not config:
1968 return self._get_instance_cached()
1968 return self._get_instance_cached()
1969 return self._get_instance(cache=bool(cache), config=config)
1969 return self._get_instance(cache=bool(cache), config=config)
1970
1970
1971 def _get_instance_cached(self):
1971 def _get_instance_cached(self):
1972 @cache_region('long_term')
1972 @cache_region('long_term')
1973 def _get_repo(cache_key):
1973 def _get_repo(cache_key):
1974 return self._get_instance()
1974 return self._get_instance()
1975
1975
1976 invalidator_context = CacheKey.repo_context_cache(
1976 invalidator_context = CacheKey.repo_context_cache(
1977 _get_repo, self.repo_name, None, thread_scoped=True)
1977 _get_repo, self.repo_name, None, thread_scoped=True)
1978
1978
1979 with invalidator_context as context:
1979 with invalidator_context as context:
1980 context.invalidate()
1980 context.invalidate()
1981 repo = context.compute()
1981 repo = context.compute()
1982
1982
1983 return repo
1983 return repo
1984
1984
1985 def _get_instance(self, cache=True, config=None):
1985 def _get_instance(self, cache=True, config=None):
1986 config = config or self._config
1986 config = config or self._config
1987 custom_wire = {
1987 custom_wire = {
1988 'cache': cache # controls the vcs.remote cache
1988 'cache': cache # controls the vcs.remote cache
1989 }
1989 }
1990 repo = get_vcs_instance(
1990 repo = get_vcs_instance(
1991 repo_path=safe_str(self.repo_full_path),
1991 repo_path=safe_str(self.repo_full_path),
1992 config=config,
1992 config=config,
1993 with_wire=custom_wire,
1993 with_wire=custom_wire,
1994 create=False,
1994 create=False,
1995 _vcs_alias=self.repo_type)
1995 _vcs_alias=self.repo_type)
1996
1996
1997 return repo
1997 return repo
1998
1998
1999 def __json__(self):
1999 def __json__(self):
2000 return {'landing_rev': self.landing_rev}
2000 return {'landing_rev': self.landing_rev}
2001
2001
2002 def get_dict(self):
2002 def get_dict(self):
2003
2003
2004 # Since we transformed `repo_name` to a hybrid property, we need to
2004 # Since we transformed `repo_name` to a hybrid property, we need to
2005 # keep compatibility with the code which uses `repo_name` field.
2005 # keep compatibility with the code which uses `repo_name` field.
2006
2006
2007 result = super(Repository, self).get_dict()
2007 result = super(Repository, self).get_dict()
2008 result['repo_name'] = result.pop('_repo_name', None)
2008 result['repo_name'] = result.pop('_repo_name', None)
2009 return result
2009 return result
2010
2010
2011
2011
2012 class RepoGroup(Base, BaseModel):
2012 class RepoGroup(Base, BaseModel):
2013 __tablename__ = 'groups'
2013 __tablename__ = 'groups'
2014 __table_args__ = (
2014 __table_args__ = (
2015 UniqueConstraint('group_name', 'group_parent_id'),
2015 UniqueConstraint('group_name', 'group_parent_id'),
2016 CheckConstraint('group_id != group_parent_id'),
2016 CheckConstraint('group_id != group_parent_id'),
2017 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2017 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2018 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2018 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2019 )
2019 )
2020 __mapper_args__ = {'order_by': 'group_name'}
2020 __mapper_args__ = {'order_by': 'group_name'}
2021
2021
2022 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2022 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2023
2023
2024 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2024 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2025 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2025 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2026 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2026 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2027 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2027 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2028 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2028 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2029 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2029 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2030 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2030 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2031 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2031 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2032
2032
2033 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2033 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2034 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2034 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2035 parent_group = relationship('RepoGroup', remote_side=group_id)
2035 parent_group = relationship('RepoGroup', remote_side=group_id)
2036 user = relationship('User')
2036 user = relationship('User')
2037 integrations = relationship('Integration',
2037 integrations = relationship('Integration',
2038 cascade="all, delete, delete-orphan")
2038 cascade="all, delete, delete-orphan")
2039
2039
2040 def __init__(self, group_name='', parent_group=None):
2040 def __init__(self, group_name='', parent_group=None):
2041 self.group_name = group_name
2041 self.group_name = group_name
2042 self.parent_group = parent_group
2042 self.parent_group = parent_group
2043
2043
2044 def __unicode__(self):
2044 def __unicode__(self):
2045 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2045 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2046 self.group_name)
2046 self.group_name)
2047
2047
2048 @classmethod
2048 @classmethod
2049 def _generate_choice(cls, repo_group):
2049 def _generate_choice(cls, repo_group):
2050 from webhelpers.html import literal as _literal
2050 from webhelpers.html import literal as _literal
2051 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2051 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2052 return repo_group.group_id, _name(repo_group.full_path_splitted)
2052 return repo_group.group_id, _name(repo_group.full_path_splitted)
2053
2053
2054 @classmethod
2054 @classmethod
2055 def groups_choices(cls, groups=None, show_empty_group=True):
2055 def groups_choices(cls, groups=None, show_empty_group=True):
2056 if not groups:
2056 if not groups:
2057 groups = cls.query().all()
2057 groups = cls.query().all()
2058
2058
2059 repo_groups = []
2059 repo_groups = []
2060 if show_empty_group:
2060 if show_empty_group:
2061 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2061 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2062
2062
2063 repo_groups.extend([cls._generate_choice(x) for x in groups])
2063 repo_groups.extend([cls._generate_choice(x) for x in groups])
2064
2064
2065 repo_groups = sorted(
2065 repo_groups = sorted(
2066 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2066 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2067 return repo_groups
2067 return repo_groups
2068
2068
2069 @classmethod
2069 @classmethod
2070 def url_sep(cls):
2070 def url_sep(cls):
2071 return URL_SEP
2071 return URL_SEP
2072
2072
2073 @classmethod
2073 @classmethod
2074 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2074 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2075 if case_insensitive:
2075 if case_insensitive:
2076 gr = cls.query().filter(func.lower(cls.group_name)
2076 gr = cls.query().filter(func.lower(cls.group_name)
2077 == func.lower(group_name))
2077 == func.lower(group_name))
2078 else:
2078 else:
2079 gr = cls.query().filter(cls.group_name == group_name)
2079 gr = cls.query().filter(cls.group_name == group_name)
2080 if cache:
2080 if cache:
2081 gr = gr.options(FromCache(
2081 gr = gr.options(FromCache(
2082 "sql_cache_short",
2082 "sql_cache_short",
2083 "get_group_%s" % _hash_key(group_name)))
2083 "get_group_%s" % _hash_key(group_name)))
2084 return gr.scalar()
2084 return gr.scalar()
2085
2085
2086 @classmethod
2086 @classmethod
2087 def get_user_personal_repo_group(cls, user_id):
2087 def get_user_personal_repo_group(cls, user_id):
2088 user = User.get(user_id)
2088 user = User.get(user_id)
2089 return cls.query()\
2089 return cls.query()\
2090 .filter(cls.personal == true())\
2090 .filter(cls.personal == true())\
2091 .filter(cls.user == user).scalar()
2091 .filter(cls.user == user).scalar()
2092
2092
2093 @classmethod
2093 @classmethod
2094 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2094 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2095 case_insensitive=True):
2095 case_insensitive=True):
2096 q = RepoGroup.query()
2096 q = RepoGroup.query()
2097
2097
2098 if not isinstance(user_id, Optional):
2098 if not isinstance(user_id, Optional):
2099 q = q.filter(RepoGroup.user_id == user_id)
2099 q = q.filter(RepoGroup.user_id == user_id)
2100
2100
2101 if not isinstance(group_id, Optional):
2101 if not isinstance(group_id, Optional):
2102 q = q.filter(RepoGroup.group_parent_id == group_id)
2102 q = q.filter(RepoGroup.group_parent_id == group_id)
2103
2103
2104 if case_insensitive:
2104 if case_insensitive:
2105 q = q.order_by(func.lower(RepoGroup.group_name))
2105 q = q.order_by(func.lower(RepoGroup.group_name))
2106 else:
2106 else:
2107 q = q.order_by(RepoGroup.group_name)
2107 q = q.order_by(RepoGroup.group_name)
2108 return q.all()
2108 return q.all()
2109
2109
2110 @property
2110 @property
2111 def parents(self):
2111 def parents(self):
2112 parents_recursion_limit = 10
2112 parents_recursion_limit = 10
2113 groups = []
2113 groups = []
2114 if self.parent_group is None:
2114 if self.parent_group is None:
2115 return groups
2115 return groups
2116 cur_gr = self.parent_group
2116 cur_gr = self.parent_group
2117 groups.insert(0, cur_gr)
2117 groups.insert(0, cur_gr)
2118 cnt = 0
2118 cnt = 0
2119 while 1:
2119 while 1:
2120 cnt += 1
2120 cnt += 1
2121 gr = getattr(cur_gr, 'parent_group', None)
2121 gr = getattr(cur_gr, 'parent_group', None)
2122 cur_gr = cur_gr.parent_group
2122 cur_gr = cur_gr.parent_group
2123 if gr is None:
2123 if gr is None:
2124 break
2124 break
2125 if cnt == parents_recursion_limit:
2125 if cnt == parents_recursion_limit:
2126 # this will prevent accidental infinit loops
2126 # this will prevent accidental infinit loops
2127 log.error(('more than %s parents found for group %s, stopping '
2127 log.error(('more than %s parents found for group %s, stopping '
2128 'recursive parent fetching' % (parents_recursion_limit, self)))
2128 'recursive parent fetching' % (parents_recursion_limit, self)))
2129 break
2129 break
2130
2130
2131 groups.insert(0, gr)
2131 groups.insert(0, gr)
2132 return groups
2132 return groups
2133
2133
2134 @property
2134 @property
2135 def children(self):
2135 def children(self):
2136 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2136 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2137
2137
2138 @property
2138 @property
2139 def name(self):
2139 def name(self):
2140 return self.group_name.split(RepoGroup.url_sep())[-1]
2140 return self.group_name.split(RepoGroup.url_sep())[-1]
2141
2141
2142 @property
2142 @property
2143 def full_path(self):
2143 def full_path(self):
2144 return self.group_name
2144 return self.group_name
2145
2145
2146 @property
2146 @property
2147 def full_path_splitted(self):
2147 def full_path_splitted(self):
2148 return self.group_name.split(RepoGroup.url_sep())
2148 return self.group_name.split(RepoGroup.url_sep())
2149
2149
2150 @property
2150 @property
2151 def repositories(self):
2151 def repositories(self):
2152 return Repository.query()\
2152 return Repository.query()\
2153 .filter(Repository.group == self)\
2153 .filter(Repository.group == self)\
2154 .order_by(Repository.repo_name)
2154 .order_by(Repository.repo_name)
2155
2155
2156 @property
2156 @property
2157 def repositories_recursive_count(self):
2157 def repositories_recursive_count(self):
2158 cnt = self.repositories.count()
2158 cnt = self.repositories.count()
2159
2159
2160 def children_count(group):
2160 def children_count(group):
2161 cnt = 0
2161 cnt = 0
2162 for child in group.children:
2162 for child in group.children:
2163 cnt += child.repositories.count()
2163 cnt += child.repositories.count()
2164 cnt += children_count(child)
2164 cnt += children_count(child)
2165 return cnt
2165 return cnt
2166
2166
2167 return cnt + children_count(self)
2167 return cnt + children_count(self)
2168
2168
2169 def _recursive_objects(self, include_repos=True):
2169 def _recursive_objects(self, include_repos=True):
2170 all_ = []
2170 all_ = []
2171
2171
2172 def _get_members(root_gr):
2172 def _get_members(root_gr):
2173 if include_repos:
2173 if include_repos:
2174 for r in root_gr.repositories:
2174 for r in root_gr.repositories:
2175 all_.append(r)
2175 all_.append(r)
2176 childs = root_gr.children.all()
2176 childs = root_gr.children.all()
2177 if childs:
2177 if childs:
2178 for gr in childs:
2178 for gr in childs:
2179 all_.append(gr)
2179 all_.append(gr)
2180 _get_members(gr)
2180 _get_members(gr)
2181
2181
2182 _get_members(self)
2182 _get_members(self)
2183 return [self] + all_
2183 return [self] + all_
2184
2184
2185 def recursive_groups_and_repos(self):
2185 def recursive_groups_and_repos(self):
2186 """
2186 """
2187 Recursive return all groups, with repositories in those groups
2187 Recursive return all groups, with repositories in those groups
2188 """
2188 """
2189 return self._recursive_objects()
2189 return self._recursive_objects()
2190
2190
2191 def recursive_groups(self):
2191 def recursive_groups(self):
2192 """
2192 """
2193 Returns all children groups for this group including children of children
2193 Returns all children groups for this group including children of children
2194 """
2194 """
2195 return self._recursive_objects(include_repos=False)
2195 return self._recursive_objects(include_repos=False)
2196
2196
2197 def get_new_name(self, group_name):
2197 def get_new_name(self, group_name):
2198 """
2198 """
2199 returns new full group name based on parent and new name
2199 returns new full group name based on parent and new name
2200
2200
2201 :param group_name:
2201 :param group_name:
2202 """
2202 """
2203 path_prefix = (self.parent_group.full_path_splitted if
2203 path_prefix = (self.parent_group.full_path_splitted if
2204 self.parent_group else [])
2204 self.parent_group else [])
2205 return RepoGroup.url_sep().join(path_prefix + [group_name])
2205 return RepoGroup.url_sep().join(path_prefix + [group_name])
2206
2206
2207 def permissions(self, with_admins=True, with_owner=True):
2207 def permissions(self, with_admins=True, with_owner=True):
2208 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2208 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2209 q = q.options(joinedload(UserRepoGroupToPerm.group),
2209 q = q.options(joinedload(UserRepoGroupToPerm.group),
2210 joinedload(UserRepoGroupToPerm.user),
2210 joinedload(UserRepoGroupToPerm.user),
2211 joinedload(UserRepoGroupToPerm.permission),)
2211 joinedload(UserRepoGroupToPerm.permission),)
2212
2212
2213 # get owners and admins and permissions. We do a trick of re-writing
2213 # get owners and admins and permissions. We do a trick of re-writing
2214 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2214 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2215 # has a global reference and changing one object propagates to all
2215 # has a global reference and changing one object propagates to all
2216 # others. This means if admin is also an owner admin_row that change
2216 # others. This means if admin is also an owner admin_row that change
2217 # would propagate to both objects
2217 # would propagate to both objects
2218 perm_rows = []
2218 perm_rows = []
2219 for _usr in q.all():
2219 for _usr in q.all():
2220 usr = AttributeDict(_usr.user.get_dict())
2220 usr = AttributeDict(_usr.user.get_dict())
2221 usr.permission = _usr.permission.permission_name
2221 usr.permission = _usr.permission.permission_name
2222 perm_rows.append(usr)
2222 perm_rows.append(usr)
2223
2223
2224 # filter the perm rows by 'default' first and then sort them by
2224 # filter the perm rows by 'default' first and then sort them by
2225 # admin,write,read,none permissions sorted again alphabetically in
2225 # admin,write,read,none permissions sorted again alphabetically in
2226 # each group
2226 # each group
2227 perm_rows = sorted(perm_rows, key=display_sort)
2227 perm_rows = sorted(perm_rows, key=display_sort)
2228
2228
2229 _admin_perm = 'group.admin'
2229 _admin_perm = 'group.admin'
2230 owner_row = []
2230 owner_row = []
2231 if with_owner:
2231 if with_owner:
2232 usr = AttributeDict(self.user.get_dict())
2232 usr = AttributeDict(self.user.get_dict())
2233 usr.owner_row = True
2233 usr.owner_row = True
2234 usr.permission = _admin_perm
2234 usr.permission = _admin_perm
2235 owner_row.append(usr)
2235 owner_row.append(usr)
2236
2236
2237 super_admin_rows = []
2237 super_admin_rows = []
2238 if with_admins:
2238 if with_admins:
2239 for usr in User.get_all_super_admins():
2239 for usr in User.get_all_super_admins():
2240 # if this admin is also owner, don't double the record
2240 # if this admin is also owner, don't double the record
2241 if usr.user_id == owner_row[0].user_id:
2241 if usr.user_id == owner_row[0].user_id:
2242 owner_row[0].admin_row = True
2242 owner_row[0].admin_row = True
2243 else:
2243 else:
2244 usr = AttributeDict(usr.get_dict())
2244 usr = AttributeDict(usr.get_dict())
2245 usr.admin_row = True
2245 usr.admin_row = True
2246 usr.permission = _admin_perm
2246 usr.permission = _admin_perm
2247 super_admin_rows.append(usr)
2247 super_admin_rows.append(usr)
2248
2248
2249 return super_admin_rows + owner_row + perm_rows
2249 return super_admin_rows + owner_row + perm_rows
2250
2250
2251 def permission_user_groups(self):
2251 def permission_user_groups(self):
2252 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2252 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2253 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2253 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2254 joinedload(UserGroupRepoGroupToPerm.users_group),
2254 joinedload(UserGroupRepoGroupToPerm.users_group),
2255 joinedload(UserGroupRepoGroupToPerm.permission),)
2255 joinedload(UserGroupRepoGroupToPerm.permission),)
2256
2256
2257 perm_rows = []
2257 perm_rows = []
2258 for _user_group in q.all():
2258 for _user_group in q.all():
2259 usr = AttributeDict(_user_group.users_group.get_dict())
2259 usr = AttributeDict(_user_group.users_group.get_dict())
2260 usr.permission = _user_group.permission.permission_name
2260 usr.permission = _user_group.permission.permission_name
2261 perm_rows.append(usr)
2261 perm_rows.append(usr)
2262
2262
2263 return perm_rows
2263 return perm_rows
2264
2264
2265 def get_api_data(self):
2265 def get_api_data(self):
2266 """
2266 """
2267 Common function for generating api data
2267 Common function for generating api data
2268
2268
2269 """
2269 """
2270 group = self
2270 group = self
2271 data = {
2271 data = {
2272 'group_id': group.group_id,
2272 'group_id': group.group_id,
2273 'group_name': group.group_name,
2273 'group_name': group.group_name,
2274 'group_description': group.group_description,
2274 'group_description': group.group_description,
2275 'parent_group': group.parent_group.group_name if group.parent_group else None,
2275 'parent_group': group.parent_group.group_name if group.parent_group else None,
2276 'repositories': [x.repo_name for x in group.repositories],
2276 'repositories': [x.repo_name for x in group.repositories],
2277 'owner': group.user.username,
2277 'owner': group.user.username,
2278 }
2278 }
2279 return data
2279 return data
2280
2280
2281
2281
2282 class Permission(Base, BaseModel):
2282 class Permission(Base, BaseModel):
2283 __tablename__ = 'permissions'
2283 __tablename__ = 'permissions'
2284 __table_args__ = (
2284 __table_args__ = (
2285 Index('p_perm_name_idx', 'permission_name'),
2285 Index('p_perm_name_idx', 'permission_name'),
2286 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2286 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2287 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2287 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2288 )
2288 )
2289 PERMS = [
2289 PERMS = [
2290 ('hg.admin', _('RhodeCode Super Administrator')),
2290 ('hg.admin', _('RhodeCode Super Administrator')),
2291
2291
2292 ('repository.none', _('Repository no access')),
2292 ('repository.none', _('Repository no access')),
2293 ('repository.read', _('Repository read access')),
2293 ('repository.read', _('Repository read access')),
2294 ('repository.write', _('Repository write access')),
2294 ('repository.write', _('Repository write access')),
2295 ('repository.admin', _('Repository admin access')),
2295 ('repository.admin', _('Repository admin access')),
2296
2296
2297 ('group.none', _('Repository group no access')),
2297 ('group.none', _('Repository group no access')),
2298 ('group.read', _('Repository group read access')),
2298 ('group.read', _('Repository group read access')),
2299 ('group.write', _('Repository group write access')),
2299 ('group.write', _('Repository group write access')),
2300 ('group.admin', _('Repository group admin access')),
2300 ('group.admin', _('Repository group admin access')),
2301
2301
2302 ('usergroup.none', _('User group no access')),
2302 ('usergroup.none', _('User group no access')),
2303 ('usergroup.read', _('User group read access')),
2303 ('usergroup.read', _('User group read access')),
2304 ('usergroup.write', _('User group write access')),
2304 ('usergroup.write', _('User group write access')),
2305 ('usergroup.admin', _('User group admin access')),
2305 ('usergroup.admin', _('User group admin access')),
2306
2306
2307 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2307 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2308 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2308 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2309
2309
2310 ('hg.usergroup.create.false', _('User Group creation disabled')),
2310 ('hg.usergroup.create.false', _('User Group creation disabled')),
2311 ('hg.usergroup.create.true', _('User Group creation enabled')),
2311 ('hg.usergroup.create.true', _('User Group creation enabled')),
2312
2312
2313 ('hg.create.none', _('Repository creation disabled')),
2313 ('hg.create.none', _('Repository creation disabled')),
2314 ('hg.create.repository', _('Repository creation enabled')),
2314 ('hg.create.repository', _('Repository creation enabled')),
2315 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2315 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2316 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2316 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2317
2317
2318 ('hg.fork.none', _('Repository forking disabled')),
2318 ('hg.fork.none', _('Repository forking disabled')),
2319 ('hg.fork.repository', _('Repository forking enabled')),
2319 ('hg.fork.repository', _('Repository forking enabled')),
2320
2320
2321 ('hg.register.none', _('Registration disabled')),
2321 ('hg.register.none', _('Registration disabled')),
2322 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2322 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2323 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2323 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2324
2324
2325 ('hg.password_reset.enabled', _('Password reset enabled')),
2325 ('hg.password_reset.enabled', _('Password reset enabled')),
2326 ('hg.password_reset.hidden', _('Password reset hidden')),
2326 ('hg.password_reset.hidden', _('Password reset hidden')),
2327 ('hg.password_reset.disabled', _('Password reset disabled')),
2327 ('hg.password_reset.disabled', _('Password reset disabled')),
2328
2328
2329 ('hg.extern_activate.manual', _('Manual activation of external account')),
2329 ('hg.extern_activate.manual', _('Manual activation of external account')),
2330 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2330 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2331
2331
2332 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2332 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2333 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2333 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2334 ]
2334 ]
2335
2335
2336 # definition of system default permissions for DEFAULT user
2336 # definition of system default permissions for DEFAULT user
2337 DEFAULT_USER_PERMISSIONS = [
2337 DEFAULT_USER_PERMISSIONS = [
2338 'repository.read',
2338 'repository.read',
2339 'group.read',
2339 'group.read',
2340 'usergroup.read',
2340 'usergroup.read',
2341 'hg.create.repository',
2341 'hg.create.repository',
2342 'hg.repogroup.create.false',
2342 'hg.repogroup.create.false',
2343 'hg.usergroup.create.false',
2343 'hg.usergroup.create.false',
2344 'hg.create.write_on_repogroup.true',
2344 'hg.create.write_on_repogroup.true',
2345 'hg.fork.repository',
2345 'hg.fork.repository',
2346 'hg.register.manual_activate',
2346 'hg.register.manual_activate',
2347 'hg.password_reset.enabled',
2347 'hg.password_reset.enabled',
2348 'hg.extern_activate.auto',
2348 'hg.extern_activate.auto',
2349 'hg.inherit_default_perms.true',
2349 'hg.inherit_default_perms.true',
2350 ]
2350 ]
2351
2351
2352 # defines which permissions are more important higher the more important
2352 # defines which permissions are more important higher the more important
2353 # Weight defines which permissions are more important.
2353 # Weight defines which permissions are more important.
2354 # The higher number the more important.
2354 # The higher number the more important.
2355 PERM_WEIGHTS = {
2355 PERM_WEIGHTS = {
2356 'repository.none': 0,
2356 'repository.none': 0,
2357 'repository.read': 1,
2357 'repository.read': 1,
2358 'repository.write': 3,
2358 'repository.write': 3,
2359 'repository.admin': 4,
2359 'repository.admin': 4,
2360
2360
2361 'group.none': 0,
2361 'group.none': 0,
2362 'group.read': 1,
2362 'group.read': 1,
2363 'group.write': 3,
2363 'group.write': 3,
2364 'group.admin': 4,
2364 'group.admin': 4,
2365
2365
2366 'usergroup.none': 0,
2366 'usergroup.none': 0,
2367 'usergroup.read': 1,
2367 'usergroup.read': 1,
2368 'usergroup.write': 3,
2368 'usergroup.write': 3,
2369 'usergroup.admin': 4,
2369 'usergroup.admin': 4,
2370
2370
2371 'hg.repogroup.create.false': 0,
2371 'hg.repogroup.create.false': 0,
2372 'hg.repogroup.create.true': 1,
2372 'hg.repogroup.create.true': 1,
2373
2373
2374 'hg.usergroup.create.false': 0,
2374 'hg.usergroup.create.false': 0,
2375 'hg.usergroup.create.true': 1,
2375 'hg.usergroup.create.true': 1,
2376
2376
2377 'hg.fork.none': 0,
2377 'hg.fork.none': 0,
2378 'hg.fork.repository': 1,
2378 'hg.fork.repository': 1,
2379 'hg.create.none': 0,
2379 'hg.create.none': 0,
2380 'hg.create.repository': 1
2380 'hg.create.repository': 1
2381 }
2381 }
2382
2382
2383 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2383 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2384 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2384 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2385 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2385 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2386
2386
2387 def __unicode__(self):
2387 def __unicode__(self):
2388 return u"<%s('%s:%s')>" % (
2388 return u"<%s('%s:%s')>" % (
2389 self.__class__.__name__, self.permission_id, self.permission_name
2389 self.__class__.__name__, self.permission_id, self.permission_name
2390 )
2390 )
2391
2391
2392 @classmethod
2392 @classmethod
2393 def get_by_key(cls, key):
2393 def get_by_key(cls, key):
2394 return cls.query().filter(cls.permission_name == key).scalar()
2394 return cls.query().filter(cls.permission_name == key).scalar()
2395
2395
2396 @classmethod
2396 @classmethod
2397 def get_default_repo_perms(cls, user_id, repo_id=None):
2397 def get_default_repo_perms(cls, user_id, repo_id=None):
2398 q = Session().query(UserRepoToPerm, Repository, Permission)\
2398 q = Session().query(UserRepoToPerm, Repository, Permission)\
2399 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2399 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2400 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2400 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2401 .filter(UserRepoToPerm.user_id == user_id)
2401 .filter(UserRepoToPerm.user_id == user_id)
2402 if repo_id:
2402 if repo_id:
2403 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2403 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2404 return q.all()
2404 return q.all()
2405
2405
2406 @classmethod
2406 @classmethod
2407 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2407 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2408 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2408 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2409 .join(
2409 .join(
2410 Permission,
2410 Permission,
2411 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2411 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2412 .join(
2412 .join(
2413 Repository,
2413 Repository,
2414 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2414 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2415 .join(
2415 .join(
2416 UserGroup,
2416 UserGroup,
2417 UserGroupRepoToPerm.users_group_id ==
2417 UserGroupRepoToPerm.users_group_id ==
2418 UserGroup.users_group_id)\
2418 UserGroup.users_group_id)\
2419 .join(
2419 .join(
2420 UserGroupMember,
2420 UserGroupMember,
2421 UserGroupRepoToPerm.users_group_id ==
2421 UserGroupRepoToPerm.users_group_id ==
2422 UserGroupMember.users_group_id)\
2422 UserGroupMember.users_group_id)\
2423 .filter(
2423 .filter(
2424 UserGroupMember.user_id == user_id,
2424 UserGroupMember.user_id == user_id,
2425 UserGroup.users_group_active == true())
2425 UserGroup.users_group_active == true())
2426 if repo_id:
2426 if repo_id:
2427 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2427 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2428 return q.all()
2428 return q.all()
2429
2429
2430 @classmethod
2430 @classmethod
2431 def get_default_group_perms(cls, user_id, repo_group_id=None):
2431 def get_default_group_perms(cls, user_id, repo_group_id=None):
2432 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2432 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2433 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2433 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2434 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2434 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2435 .filter(UserRepoGroupToPerm.user_id == user_id)
2435 .filter(UserRepoGroupToPerm.user_id == user_id)
2436 if repo_group_id:
2436 if repo_group_id:
2437 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2437 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2438 return q.all()
2438 return q.all()
2439
2439
2440 @classmethod
2440 @classmethod
2441 def get_default_group_perms_from_user_group(
2441 def get_default_group_perms_from_user_group(
2442 cls, user_id, repo_group_id=None):
2442 cls, user_id, repo_group_id=None):
2443 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2443 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2444 .join(
2444 .join(
2445 Permission,
2445 Permission,
2446 UserGroupRepoGroupToPerm.permission_id ==
2446 UserGroupRepoGroupToPerm.permission_id ==
2447 Permission.permission_id)\
2447 Permission.permission_id)\
2448 .join(
2448 .join(
2449 RepoGroup,
2449 RepoGroup,
2450 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2450 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2451 .join(
2451 .join(
2452 UserGroup,
2452 UserGroup,
2453 UserGroupRepoGroupToPerm.users_group_id ==
2453 UserGroupRepoGroupToPerm.users_group_id ==
2454 UserGroup.users_group_id)\
2454 UserGroup.users_group_id)\
2455 .join(
2455 .join(
2456 UserGroupMember,
2456 UserGroupMember,
2457 UserGroupRepoGroupToPerm.users_group_id ==
2457 UserGroupRepoGroupToPerm.users_group_id ==
2458 UserGroupMember.users_group_id)\
2458 UserGroupMember.users_group_id)\
2459 .filter(
2459 .filter(
2460 UserGroupMember.user_id == user_id,
2460 UserGroupMember.user_id == user_id,
2461 UserGroup.users_group_active == true())
2461 UserGroup.users_group_active == true())
2462 if repo_group_id:
2462 if repo_group_id:
2463 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2463 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2464 return q.all()
2464 return q.all()
2465
2465
2466 @classmethod
2466 @classmethod
2467 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2467 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2468 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2468 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2469 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2469 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2470 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2470 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2471 .filter(UserUserGroupToPerm.user_id == user_id)
2471 .filter(UserUserGroupToPerm.user_id == user_id)
2472 if user_group_id:
2472 if user_group_id:
2473 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2473 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2474 return q.all()
2474 return q.all()
2475
2475
2476 @classmethod
2476 @classmethod
2477 def get_default_user_group_perms_from_user_group(
2477 def get_default_user_group_perms_from_user_group(
2478 cls, user_id, user_group_id=None):
2478 cls, user_id, user_group_id=None):
2479 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2479 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2480 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2480 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2481 .join(
2481 .join(
2482 Permission,
2482 Permission,
2483 UserGroupUserGroupToPerm.permission_id ==
2483 UserGroupUserGroupToPerm.permission_id ==
2484 Permission.permission_id)\
2484 Permission.permission_id)\
2485 .join(
2485 .join(
2486 TargetUserGroup,
2486 TargetUserGroup,
2487 UserGroupUserGroupToPerm.target_user_group_id ==
2487 UserGroupUserGroupToPerm.target_user_group_id ==
2488 TargetUserGroup.users_group_id)\
2488 TargetUserGroup.users_group_id)\
2489 .join(
2489 .join(
2490 UserGroup,
2490 UserGroup,
2491 UserGroupUserGroupToPerm.user_group_id ==
2491 UserGroupUserGroupToPerm.user_group_id ==
2492 UserGroup.users_group_id)\
2492 UserGroup.users_group_id)\
2493 .join(
2493 .join(
2494 UserGroupMember,
2494 UserGroupMember,
2495 UserGroupUserGroupToPerm.user_group_id ==
2495 UserGroupUserGroupToPerm.user_group_id ==
2496 UserGroupMember.users_group_id)\
2496 UserGroupMember.users_group_id)\
2497 .filter(
2497 .filter(
2498 UserGroupMember.user_id == user_id,
2498 UserGroupMember.user_id == user_id,
2499 UserGroup.users_group_active == true())
2499 UserGroup.users_group_active == true())
2500 if user_group_id:
2500 if user_group_id:
2501 q = q.filter(
2501 q = q.filter(
2502 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2502 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2503
2503
2504 return q.all()
2504 return q.all()
2505
2505
2506
2506
2507 class UserRepoToPerm(Base, BaseModel):
2507 class UserRepoToPerm(Base, BaseModel):
2508 __tablename__ = 'repo_to_perm'
2508 __tablename__ = 'repo_to_perm'
2509 __table_args__ = (
2509 __table_args__ = (
2510 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2510 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2511 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2511 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2512 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2512 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2513 )
2513 )
2514 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2514 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2515 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2515 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2516 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2516 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2517 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2517 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2518
2518
2519 user = relationship('User')
2519 user = relationship('User')
2520 repository = relationship('Repository')
2520 repository = relationship('Repository')
2521 permission = relationship('Permission')
2521 permission = relationship('Permission')
2522
2522
2523 @classmethod
2523 @classmethod
2524 def create(cls, user, repository, permission):
2524 def create(cls, user, repository, permission):
2525 n = cls()
2525 n = cls()
2526 n.user = user
2526 n.user = user
2527 n.repository = repository
2527 n.repository = repository
2528 n.permission = permission
2528 n.permission = permission
2529 Session().add(n)
2529 Session().add(n)
2530 return n
2530 return n
2531
2531
2532 def __unicode__(self):
2532 def __unicode__(self):
2533 return u'<%s => %s >' % (self.user, self.repository)
2533 return u'<%s => %s >' % (self.user, self.repository)
2534
2534
2535
2535
2536 class UserUserGroupToPerm(Base, BaseModel):
2536 class UserUserGroupToPerm(Base, BaseModel):
2537 __tablename__ = 'user_user_group_to_perm'
2537 __tablename__ = 'user_user_group_to_perm'
2538 __table_args__ = (
2538 __table_args__ = (
2539 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2539 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2540 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2540 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2541 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2541 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2542 )
2542 )
2543 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2543 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2544 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2544 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2545 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2545 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2546 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2546 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2547
2547
2548 user = relationship('User')
2548 user = relationship('User')
2549 user_group = relationship('UserGroup')
2549 user_group = relationship('UserGroup')
2550 permission = relationship('Permission')
2550 permission = relationship('Permission')
2551
2551
2552 @classmethod
2552 @classmethod
2553 def create(cls, user, user_group, permission):
2553 def create(cls, user, user_group, permission):
2554 n = cls()
2554 n = cls()
2555 n.user = user
2555 n.user = user
2556 n.user_group = user_group
2556 n.user_group = user_group
2557 n.permission = permission
2557 n.permission = permission
2558 Session().add(n)
2558 Session().add(n)
2559 return n
2559 return n
2560
2560
2561 def __unicode__(self):
2561 def __unicode__(self):
2562 return u'<%s => %s >' % (self.user, self.user_group)
2562 return u'<%s => %s >' % (self.user, self.user_group)
2563
2563
2564
2564
2565 class UserToPerm(Base, BaseModel):
2565 class UserToPerm(Base, BaseModel):
2566 __tablename__ = 'user_to_perm'
2566 __tablename__ = 'user_to_perm'
2567 __table_args__ = (
2567 __table_args__ = (
2568 UniqueConstraint('user_id', 'permission_id'),
2568 UniqueConstraint('user_id', 'permission_id'),
2569 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2569 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2570 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2570 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2571 )
2571 )
2572 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2572 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2573 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2573 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2574 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2574 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2575
2575
2576 user = relationship('User')
2576 user = relationship('User')
2577 permission = relationship('Permission', lazy='joined')
2577 permission = relationship('Permission', lazy='joined')
2578
2578
2579 def __unicode__(self):
2579 def __unicode__(self):
2580 return u'<%s => %s >' % (self.user, self.permission)
2580 return u'<%s => %s >' % (self.user, self.permission)
2581
2581
2582
2582
2583 class UserGroupRepoToPerm(Base, BaseModel):
2583 class UserGroupRepoToPerm(Base, BaseModel):
2584 __tablename__ = 'users_group_repo_to_perm'
2584 __tablename__ = 'users_group_repo_to_perm'
2585 __table_args__ = (
2585 __table_args__ = (
2586 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2586 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2587 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2587 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2588 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2588 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2589 )
2589 )
2590 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2590 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2591 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2591 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2592 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2592 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2593 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2593 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2594
2594
2595 users_group = relationship('UserGroup')
2595 users_group = relationship('UserGroup')
2596 permission = relationship('Permission')
2596 permission = relationship('Permission')
2597 repository = relationship('Repository')
2597 repository = relationship('Repository')
2598
2598
2599 @classmethod
2599 @classmethod
2600 def create(cls, users_group, repository, permission):
2600 def create(cls, users_group, repository, permission):
2601 n = cls()
2601 n = cls()
2602 n.users_group = users_group
2602 n.users_group = users_group
2603 n.repository = repository
2603 n.repository = repository
2604 n.permission = permission
2604 n.permission = permission
2605 Session().add(n)
2605 Session().add(n)
2606 return n
2606 return n
2607
2607
2608 def __unicode__(self):
2608 def __unicode__(self):
2609 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2609 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2610
2610
2611
2611
2612 class UserGroupUserGroupToPerm(Base, BaseModel):
2612 class UserGroupUserGroupToPerm(Base, BaseModel):
2613 __tablename__ = 'user_group_user_group_to_perm'
2613 __tablename__ = 'user_group_user_group_to_perm'
2614 __table_args__ = (
2614 __table_args__ = (
2615 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2615 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2616 CheckConstraint('target_user_group_id != user_group_id'),
2616 CheckConstraint('target_user_group_id != user_group_id'),
2617 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2617 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2618 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2618 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2619 )
2619 )
2620 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)
2620 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)
2621 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2621 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2622 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2622 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2623 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2623 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2624
2624
2625 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2625 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2626 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2626 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2627 permission = relationship('Permission')
2627 permission = relationship('Permission')
2628
2628
2629 @classmethod
2629 @classmethod
2630 def create(cls, target_user_group, user_group, permission):
2630 def create(cls, target_user_group, user_group, permission):
2631 n = cls()
2631 n = cls()
2632 n.target_user_group = target_user_group
2632 n.target_user_group = target_user_group
2633 n.user_group = user_group
2633 n.user_group = user_group
2634 n.permission = permission
2634 n.permission = permission
2635 Session().add(n)
2635 Session().add(n)
2636 return n
2636 return n
2637
2637
2638 def __unicode__(self):
2638 def __unicode__(self):
2639 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2639 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2640
2640
2641
2641
2642 class UserGroupToPerm(Base, BaseModel):
2642 class UserGroupToPerm(Base, BaseModel):
2643 __tablename__ = 'users_group_to_perm'
2643 __tablename__ = 'users_group_to_perm'
2644 __table_args__ = (
2644 __table_args__ = (
2645 UniqueConstraint('users_group_id', 'permission_id',),
2645 UniqueConstraint('users_group_id', 'permission_id',),
2646 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2646 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2647 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2647 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2648 )
2648 )
2649 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2649 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2650 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2650 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2651 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2651 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2652
2652
2653 users_group = relationship('UserGroup')
2653 users_group = relationship('UserGroup')
2654 permission = relationship('Permission')
2654 permission = relationship('Permission')
2655
2655
2656
2656
2657 class UserRepoGroupToPerm(Base, BaseModel):
2657 class UserRepoGroupToPerm(Base, BaseModel):
2658 __tablename__ = 'user_repo_group_to_perm'
2658 __tablename__ = 'user_repo_group_to_perm'
2659 __table_args__ = (
2659 __table_args__ = (
2660 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2660 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2661 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2661 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2662 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2662 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2663 )
2663 )
2664
2664
2665 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2665 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2666 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2666 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2667 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2667 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2668 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2668 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2669
2669
2670 user = relationship('User')
2670 user = relationship('User')
2671 group = relationship('RepoGroup')
2671 group = relationship('RepoGroup')
2672 permission = relationship('Permission')
2672 permission = relationship('Permission')
2673
2673
2674 @classmethod
2674 @classmethod
2675 def create(cls, user, repository_group, permission):
2675 def create(cls, user, repository_group, permission):
2676 n = cls()
2676 n = cls()
2677 n.user = user
2677 n.user = user
2678 n.group = repository_group
2678 n.group = repository_group
2679 n.permission = permission
2679 n.permission = permission
2680 Session().add(n)
2680 Session().add(n)
2681 return n
2681 return n
2682
2682
2683
2683
2684 class UserGroupRepoGroupToPerm(Base, BaseModel):
2684 class UserGroupRepoGroupToPerm(Base, BaseModel):
2685 __tablename__ = 'users_group_repo_group_to_perm'
2685 __tablename__ = 'users_group_repo_group_to_perm'
2686 __table_args__ = (
2686 __table_args__ = (
2687 UniqueConstraint('users_group_id', 'group_id'),
2687 UniqueConstraint('users_group_id', 'group_id'),
2688 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2688 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2689 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2689 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2690 )
2690 )
2691
2691
2692 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)
2692 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)
2693 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2693 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2694 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2694 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2695 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2695 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2696
2696
2697 users_group = relationship('UserGroup')
2697 users_group = relationship('UserGroup')
2698 permission = relationship('Permission')
2698 permission = relationship('Permission')
2699 group = relationship('RepoGroup')
2699 group = relationship('RepoGroup')
2700
2700
2701 @classmethod
2701 @classmethod
2702 def create(cls, user_group, repository_group, permission):
2702 def create(cls, user_group, repository_group, permission):
2703 n = cls()
2703 n = cls()
2704 n.users_group = user_group
2704 n.users_group = user_group
2705 n.group = repository_group
2705 n.group = repository_group
2706 n.permission = permission
2706 n.permission = permission
2707 Session().add(n)
2707 Session().add(n)
2708 return n
2708 return n
2709
2709
2710 def __unicode__(self):
2710 def __unicode__(self):
2711 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2711 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2712
2712
2713
2713
2714 class Statistics(Base, BaseModel):
2714 class Statistics(Base, BaseModel):
2715 __tablename__ = 'statistics'
2715 __tablename__ = 'statistics'
2716 __table_args__ = (
2716 __table_args__ = (
2717 UniqueConstraint('repository_id'),
2717 UniqueConstraint('repository_id'),
2718 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2718 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2719 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2719 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2720 )
2720 )
2721 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2721 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2722 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2722 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2723 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2723 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2724 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2724 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2725 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2725 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2726 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2726 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2727
2727
2728 repository = relationship('Repository', single_parent=True)
2728 repository = relationship('Repository', single_parent=True)
2729
2729
2730
2730
2731 class UserFollowing(Base, BaseModel):
2731 class UserFollowing(Base, BaseModel):
2732 __tablename__ = 'user_followings'
2732 __tablename__ = 'user_followings'
2733 __table_args__ = (
2733 __table_args__ = (
2734 UniqueConstraint('user_id', 'follows_repository_id'),
2734 UniqueConstraint('user_id', 'follows_repository_id'),
2735 UniqueConstraint('user_id', 'follows_user_id'),
2735 UniqueConstraint('user_id', 'follows_user_id'),
2736 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2736 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2737 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2737 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2738 )
2738 )
2739
2739
2740 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2740 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2741 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2741 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2742 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2742 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2743 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2743 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2744 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2744 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2745
2745
2746 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2746 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2747
2747
2748 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2748 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2749 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2749 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2750
2750
2751 @classmethod
2751 @classmethod
2752 def get_repo_followers(cls, repo_id):
2752 def get_repo_followers(cls, repo_id):
2753 return cls.query().filter(cls.follows_repo_id == repo_id)
2753 return cls.query().filter(cls.follows_repo_id == repo_id)
2754
2754
2755
2755
2756 class CacheKey(Base, BaseModel):
2756 class CacheKey(Base, BaseModel):
2757 __tablename__ = 'cache_invalidation'
2757 __tablename__ = 'cache_invalidation'
2758 __table_args__ = (
2758 __table_args__ = (
2759 UniqueConstraint('cache_key'),
2759 UniqueConstraint('cache_key'),
2760 Index('key_idx', 'cache_key'),
2760 Index('key_idx', 'cache_key'),
2761 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2761 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2762 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2762 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2763 )
2763 )
2764 CACHE_TYPE_ATOM = 'ATOM'
2764 CACHE_TYPE_ATOM = 'ATOM'
2765 CACHE_TYPE_RSS = 'RSS'
2765 CACHE_TYPE_RSS = 'RSS'
2766 CACHE_TYPE_README = 'README'
2766 CACHE_TYPE_README = 'README'
2767
2767
2768 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2768 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2769 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2769 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2770 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2770 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2771 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2771 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2772
2772
2773 def __init__(self, cache_key, cache_args=''):
2773 def __init__(self, cache_key, cache_args=''):
2774 self.cache_key = cache_key
2774 self.cache_key = cache_key
2775 self.cache_args = cache_args
2775 self.cache_args = cache_args
2776 self.cache_active = False
2776 self.cache_active = False
2777
2777
2778 def __unicode__(self):
2778 def __unicode__(self):
2779 return u"<%s('%s:%s[%s]')>" % (
2779 return u"<%s('%s:%s[%s]')>" % (
2780 self.__class__.__name__,
2780 self.__class__.__name__,
2781 self.cache_id, self.cache_key, self.cache_active)
2781 self.cache_id, self.cache_key, self.cache_active)
2782
2782
2783 def _cache_key_partition(self):
2783 def _cache_key_partition(self):
2784 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2784 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2785 return prefix, repo_name, suffix
2785 return prefix, repo_name, suffix
2786
2786
2787 def get_prefix(self):
2787 def get_prefix(self):
2788 """
2788 """
2789 Try to extract prefix from existing cache key. The key could consist
2789 Try to extract prefix from existing cache key. The key could consist
2790 of prefix, repo_name, suffix
2790 of prefix, repo_name, suffix
2791 """
2791 """
2792 # this returns prefix, repo_name, suffix
2792 # this returns prefix, repo_name, suffix
2793 return self._cache_key_partition()[0]
2793 return self._cache_key_partition()[0]
2794
2794
2795 def get_suffix(self):
2795 def get_suffix(self):
2796 """
2796 """
2797 get suffix that might have been used in _get_cache_key to
2797 get suffix that might have been used in _get_cache_key to
2798 generate self.cache_key. Only used for informational purposes
2798 generate self.cache_key. Only used for informational purposes
2799 in repo_edit.html.
2799 in repo_edit.html.
2800 """
2800 """
2801 # prefix, repo_name, suffix
2801 # prefix, repo_name, suffix
2802 return self._cache_key_partition()[2]
2802 return self._cache_key_partition()[2]
2803
2803
2804 @classmethod
2804 @classmethod
2805 def delete_all_cache(cls):
2805 def delete_all_cache(cls):
2806 """
2806 """
2807 Delete all cache keys from database.
2807 Delete all cache keys from database.
2808 Should only be run when all instances are down and all entries
2808 Should only be run when all instances are down and all entries
2809 thus stale.
2809 thus stale.
2810 """
2810 """
2811 cls.query().delete()
2811 cls.query().delete()
2812 Session().commit()
2812 Session().commit()
2813
2813
2814 @classmethod
2814 @classmethod
2815 def get_cache_key(cls, repo_name, cache_type):
2815 def get_cache_key(cls, repo_name, cache_type):
2816 """
2816 """
2817
2817
2818 Generate a cache key for this process of RhodeCode instance.
2818 Generate a cache key for this process of RhodeCode instance.
2819 Prefix most likely will be process id or maybe explicitly set
2819 Prefix most likely will be process id or maybe explicitly set
2820 instance_id from .ini file.
2820 instance_id from .ini file.
2821 """
2821 """
2822 import rhodecode
2822 import rhodecode
2823 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2823 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2824
2824
2825 repo_as_unicode = safe_unicode(repo_name)
2825 repo_as_unicode = safe_unicode(repo_name)
2826 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2826 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2827 if cache_type else repo_as_unicode
2827 if cache_type else repo_as_unicode
2828
2828
2829 return u'{}{}'.format(prefix, key)
2829 return u'{}{}'.format(prefix, key)
2830
2830
2831 @classmethod
2831 @classmethod
2832 def set_invalidate(cls, repo_name, delete=False):
2832 def set_invalidate(cls, repo_name, delete=False):
2833 """
2833 """
2834 Mark all caches of a repo as invalid in the database.
2834 Mark all caches of a repo as invalid in the database.
2835 """
2835 """
2836
2836
2837 try:
2837 try:
2838 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2838 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2839 if delete:
2839 if delete:
2840 log.debug('cache objects deleted for repo %s',
2840 log.debug('cache objects deleted for repo %s',
2841 safe_str(repo_name))
2841 safe_str(repo_name))
2842 qry.delete()
2842 qry.delete()
2843 else:
2843 else:
2844 log.debug('cache objects marked as invalid for repo %s',
2844 log.debug('cache objects marked as invalid for repo %s',
2845 safe_str(repo_name))
2845 safe_str(repo_name))
2846 qry.update({"cache_active": False})
2846 qry.update({"cache_active": False})
2847
2847
2848 Session().commit()
2848 Session().commit()
2849 except Exception:
2849 except Exception:
2850 log.exception(
2850 log.exception(
2851 'Cache key invalidation failed for repository %s',
2851 'Cache key invalidation failed for repository %s',
2852 safe_str(repo_name))
2852 safe_str(repo_name))
2853 Session().rollback()
2853 Session().rollback()
2854
2854
2855 @classmethod
2855 @classmethod
2856 def get_active_cache(cls, cache_key):
2856 def get_active_cache(cls, cache_key):
2857 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2857 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2858 if inv_obj:
2858 if inv_obj:
2859 return inv_obj
2859 return inv_obj
2860 return None
2860 return None
2861
2861
2862 @classmethod
2862 @classmethod
2863 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2863 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2864 thread_scoped=False):
2864 thread_scoped=False):
2865 """
2865 """
2866 @cache_region('long_term')
2866 @cache_region('long_term')
2867 def _heavy_calculation(cache_key):
2867 def _heavy_calculation(cache_key):
2868 return 'result'
2868 return 'result'
2869
2869
2870 cache_context = CacheKey.repo_context_cache(
2870 cache_context = CacheKey.repo_context_cache(
2871 _heavy_calculation, repo_name, cache_type)
2871 _heavy_calculation, repo_name, cache_type)
2872
2872
2873 with cache_context as context:
2873 with cache_context as context:
2874 context.invalidate()
2874 context.invalidate()
2875 computed = context.compute()
2875 computed = context.compute()
2876
2876
2877 assert computed == 'result'
2877 assert computed == 'result'
2878 """
2878 """
2879 from rhodecode.lib import caches
2879 from rhodecode.lib import caches
2880 return caches.InvalidationContext(
2880 return caches.InvalidationContext(
2881 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2881 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2882
2882
2883
2883
2884 class ChangesetComment(Base, BaseModel):
2884 class ChangesetComment(Base, BaseModel):
2885 __tablename__ = 'changeset_comments'
2885 __tablename__ = 'changeset_comments'
2886 __table_args__ = (
2886 __table_args__ = (
2887 Index('cc_revision_idx', 'revision'),
2887 Index('cc_revision_idx', 'revision'),
2888 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2888 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2889 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2889 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2890 )
2890 )
2891
2891
2892 COMMENT_OUTDATED = u'comment_outdated'
2892 COMMENT_OUTDATED = u'comment_outdated'
2893
2893
2894 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2894 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2895 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2895 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2896 revision = Column('revision', String(40), nullable=True)
2896 revision = Column('revision', String(40), nullable=True)
2897 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2897 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2898 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2898 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2899 line_no = Column('line_no', Unicode(10), nullable=True)
2899 line_no = Column('line_no', Unicode(10), nullable=True)
2900 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2900 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2901 f_path = Column('f_path', Unicode(1000), nullable=True)
2901 f_path = Column('f_path', Unicode(1000), nullable=True)
2902 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2902 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2903 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2903 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2904 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2904 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2905 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2905 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2906 renderer = Column('renderer', Unicode(64), nullable=True)
2906 renderer = Column('renderer', Unicode(64), nullable=True)
2907 display_state = Column('display_state', Unicode(128), nullable=True)
2907 display_state = Column('display_state', Unicode(128), nullable=True)
2908
2908
2909 author = relationship('User', lazy='joined')
2909 author = relationship('User', lazy='joined')
2910 repo = relationship('Repository')
2910 repo = relationship('Repository')
2911 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2911 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2912 pull_request = relationship('PullRequest', lazy='joined')
2912 pull_request = relationship('PullRequest', lazy='joined')
2913 pull_request_version = relationship('PullRequestVersion')
2913 pull_request_version = relationship('PullRequestVersion')
2914
2914
2915 @classmethod
2915 @classmethod
2916 def get_users(cls, revision=None, pull_request_id=None):
2916 def get_users(cls, revision=None, pull_request_id=None):
2917 """
2917 """
2918 Returns user associated with this ChangesetComment. ie those
2918 Returns user associated with this ChangesetComment. ie those
2919 who actually commented
2919 who actually commented
2920
2920
2921 :param cls:
2921 :param cls:
2922 :param revision:
2922 :param revision:
2923 """
2923 """
2924 q = Session().query(User)\
2924 q = Session().query(User)\
2925 .join(ChangesetComment.author)
2925 .join(ChangesetComment.author)
2926 if revision:
2926 if revision:
2927 q = q.filter(cls.revision == revision)
2927 q = q.filter(cls.revision == revision)
2928 elif pull_request_id:
2928 elif pull_request_id:
2929 q = q.filter(cls.pull_request_id == pull_request_id)
2929 q = q.filter(cls.pull_request_id == pull_request_id)
2930 return q.all()
2930 return q.all()
2931
2931
2932 @property
2932 @property
2933 def outdated(self):
2933 def outdated(self):
2934 return self.display_state == self.COMMENT_OUTDATED
2934 return self.display_state == self.COMMENT_OUTDATED
2935
2935
2936 def outdated_at_version(self, version):
2937 """
2938 Checks if comment is outdated for given pull request version
2939 """
2940 return self.outdated and self.pull_request_version_id != version
2941
2936 def render(self, mentions=False):
2942 def render(self, mentions=False):
2937 from rhodecode.lib import helpers as h
2943 from rhodecode.lib import helpers as h
2938 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2944 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2939
2945
2940 def __repr__(self):
2946 def __repr__(self):
2941 if self.comment_id:
2947 if self.comment_id:
2942 return '<DB:ChangesetComment #%s>' % self.comment_id
2948 return '<DB:ChangesetComment #%s>' % self.comment_id
2943 else:
2949 else:
2944 return '<DB:ChangesetComment at %#x>' % id(self)
2950 return '<DB:ChangesetComment at %#x>' % id(self)
2945
2951
2946
2952
2947 class ChangesetStatus(Base, BaseModel):
2953 class ChangesetStatus(Base, BaseModel):
2948 __tablename__ = 'changeset_statuses'
2954 __tablename__ = 'changeset_statuses'
2949 __table_args__ = (
2955 __table_args__ = (
2950 Index('cs_revision_idx', 'revision'),
2956 Index('cs_revision_idx', 'revision'),
2951 Index('cs_version_idx', 'version'),
2957 Index('cs_version_idx', 'version'),
2952 UniqueConstraint('repo_id', 'revision', 'version'),
2958 UniqueConstraint('repo_id', 'revision', 'version'),
2953 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2959 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2954 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2960 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2955 )
2961 )
2956 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2962 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2957 STATUS_APPROVED = 'approved'
2963 STATUS_APPROVED = 'approved'
2958 STATUS_REJECTED = 'rejected'
2964 STATUS_REJECTED = 'rejected'
2959 STATUS_UNDER_REVIEW = 'under_review'
2965 STATUS_UNDER_REVIEW = 'under_review'
2960
2966
2961 STATUSES = [
2967 STATUSES = [
2962 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2968 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2963 (STATUS_APPROVED, _("Approved")),
2969 (STATUS_APPROVED, _("Approved")),
2964 (STATUS_REJECTED, _("Rejected")),
2970 (STATUS_REJECTED, _("Rejected")),
2965 (STATUS_UNDER_REVIEW, _("Under Review")),
2971 (STATUS_UNDER_REVIEW, _("Under Review")),
2966 ]
2972 ]
2967
2973
2968 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2974 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2969 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2975 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2970 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2976 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2971 revision = Column('revision', String(40), nullable=False)
2977 revision = Column('revision', String(40), nullable=False)
2972 status = Column('status', String(128), nullable=False, default=DEFAULT)
2978 status = Column('status', String(128), nullable=False, default=DEFAULT)
2973 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2979 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2974 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2980 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2975 version = Column('version', Integer(), nullable=False, default=0)
2981 version = Column('version', Integer(), nullable=False, default=0)
2976 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2982 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2977
2983
2978 author = relationship('User', lazy='joined')
2984 author = relationship('User', lazy='joined')
2979 repo = relationship('Repository')
2985 repo = relationship('Repository')
2980 comment = relationship('ChangesetComment', lazy='joined')
2986 comment = relationship('ChangesetComment', lazy='joined')
2981 pull_request = relationship('PullRequest', lazy='joined')
2987 pull_request = relationship('PullRequest', lazy='joined')
2982
2988
2983 def __unicode__(self):
2989 def __unicode__(self):
2984 return u"<%s('%s[%s]:%s')>" % (
2990 return u"<%s('%s[%s]:%s')>" % (
2985 self.__class__.__name__,
2991 self.__class__.__name__,
2986 self.status, self.version, self.author
2992 self.status, self.version, self.author
2987 )
2993 )
2988
2994
2989 @classmethod
2995 @classmethod
2990 def get_status_lbl(cls, value):
2996 def get_status_lbl(cls, value):
2991 return dict(cls.STATUSES).get(value)
2997 return dict(cls.STATUSES).get(value)
2992
2998
2993 @property
2999 @property
2994 def status_lbl(self):
3000 def status_lbl(self):
2995 return ChangesetStatus.get_status_lbl(self.status)
3001 return ChangesetStatus.get_status_lbl(self.status)
2996
3002
2997
3003
2998 class _PullRequestBase(BaseModel):
3004 class _PullRequestBase(BaseModel):
2999 """
3005 """
3000 Common attributes of pull request and version entries.
3006 Common attributes of pull request and version entries.
3001 """
3007 """
3002
3008
3003 # .status values
3009 # .status values
3004 STATUS_NEW = u'new'
3010 STATUS_NEW = u'new'
3005 STATUS_OPEN = u'open'
3011 STATUS_OPEN = u'open'
3006 STATUS_CLOSED = u'closed'
3012 STATUS_CLOSED = u'closed'
3007
3013
3008 title = Column('title', Unicode(255), nullable=True)
3014 title = Column('title', Unicode(255), nullable=True)
3009 description = Column(
3015 description = Column(
3010 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3016 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3011 nullable=True)
3017 nullable=True)
3012 # new/open/closed status of pull request (not approve/reject/etc)
3018 # new/open/closed status of pull request (not approve/reject/etc)
3013 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3019 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3014 created_on = Column(
3020 created_on = Column(
3015 'created_on', DateTime(timezone=False), nullable=False,
3021 'created_on', DateTime(timezone=False), nullable=False,
3016 default=datetime.datetime.now)
3022 default=datetime.datetime.now)
3017 updated_on = Column(
3023 updated_on = Column(
3018 'updated_on', DateTime(timezone=False), nullable=False,
3024 'updated_on', DateTime(timezone=False), nullable=False,
3019 default=datetime.datetime.now)
3025 default=datetime.datetime.now)
3020
3026
3021 @declared_attr
3027 @declared_attr
3022 def user_id(cls):
3028 def user_id(cls):
3023 return Column(
3029 return Column(
3024 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3030 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3025 unique=None)
3031 unique=None)
3026
3032
3027 # 500 revisions max
3033 # 500 revisions max
3028 _revisions = Column(
3034 _revisions = Column(
3029 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3035 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3030
3036
3031 @declared_attr
3037 @declared_attr
3032 def source_repo_id(cls):
3038 def source_repo_id(cls):
3033 # TODO: dan: rename column to source_repo_id
3039 # TODO: dan: rename column to source_repo_id
3034 return Column(
3040 return Column(
3035 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3041 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3036 nullable=False)
3042 nullable=False)
3037
3043
3038 source_ref = Column('org_ref', Unicode(255), nullable=False)
3044 source_ref = Column('org_ref', Unicode(255), nullable=False)
3039
3045
3040 @declared_attr
3046 @declared_attr
3041 def target_repo_id(cls):
3047 def target_repo_id(cls):
3042 # TODO: dan: rename column to target_repo_id
3048 # TODO: dan: rename column to target_repo_id
3043 return Column(
3049 return Column(
3044 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3050 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3045 nullable=False)
3051 nullable=False)
3046
3052
3047 target_ref = Column('other_ref', Unicode(255), nullable=False)
3053 target_ref = Column('other_ref', Unicode(255), nullable=False)
3048 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3054 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3049
3055
3050 # TODO: dan: rename column to last_merge_source_rev
3056 # TODO: dan: rename column to last_merge_source_rev
3051 _last_merge_source_rev = Column(
3057 _last_merge_source_rev = Column(
3052 'last_merge_org_rev', String(40), nullable=True)
3058 'last_merge_org_rev', String(40), nullable=True)
3053 # TODO: dan: rename column to last_merge_target_rev
3059 # TODO: dan: rename column to last_merge_target_rev
3054 _last_merge_target_rev = Column(
3060 _last_merge_target_rev = Column(
3055 'last_merge_other_rev', String(40), nullable=True)
3061 'last_merge_other_rev', String(40), nullable=True)
3056 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3062 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3057 merge_rev = Column('merge_rev', String(40), nullable=True)
3063 merge_rev = Column('merge_rev', String(40), nullable=True)
3058
3064
3059 @hybrid_property
3065 @hybrid_property
3060 def revisions(self):
3066 def revisions(self):
3061 return self._revisions.split(':') if self._revisions else []
3067 return self._revisions.split(':') if self._revisions else []
3062
3068
3063 @revisions.setter
3069 @revisions.setter
3064 def revisions(self, val):
3070 def revisions(self, val):
3065 self._revisions = ':'.join(val)
3071 self._revisions = ':'.join(val)
3066
3072
3067 @declared_attr
3073 @declared_attr
3068 def author(cls):
3074 def author(cls):
3069 return relationship('User', lazy='joined')
3075 return relationship('User', lazy='joined')
3070
3076
3071 @declared_attr
3077 @declared_attr
3072 def source_repo(cls):
3078 def source_repo(cls):
3073 return relationship(
3079 return relationship(
3074 'Repository',
3080 'Repository',
3075 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3081 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3076
3082
3077 @property
3083 @property
3078 def source_ref_parts(self):
3084 def source_ref_parts(self):
3079 return self.unicode_to_reference(self.source_ref)
3085 return self.unicode_to_reference(self.source_ref)
3080
3086
3081 @declared_attr
3087 @declared_attr
3082 def target_repo(cls):
3088 def target_repo(cls):
3083 return relationship(
3089 return relationship(
3084 'Repository',
3090 'Repository',
3085 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3091 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3086
3092
3087 @property
3093 @property
3088 def target_ref_parts(self):
3094 def target_ref_parts(self):
3089 return self.unicode_to_reference(self.target_ref)
3095 return self.unicode_to_reference(self.target_ref)
3090
3096
3091 @property
3097 @property
3092 def shadow_merge_ref(self):
3098 def shadow_merge_ref(self):
3093 return self.unicode_to_reference(self._shadow_merge_ref)
3099 return self.unicode_to_reference(self._shadow_merge_ref)
3094
3100
3095 @shadow_merge_ref.setter
3101 @shadow_merge_ref.setter
3096 def shadow_merge_ref(self, ref):
3102 def shadow_merge_ref(self, ref):
3097 self._shadow_merge_ref = self.reference_to_unicode(ref)
3103 self._shadow_merge_ref = self.reference_to_unicode(ref)
3098
3104
3099 def unicode_to_reference(self, raw):
3105 def unicode_to_reference(self, raw):
3100 """
3106 """
3101 Convert a unicode (or string) to a reference object.
3107 Convert a unicode (or string) to a reference object.
3102 If unicode evaluates to False it returns None.
3108 If unicode evaluates to False it returns None.
3103 """
3109 """
3104 if raw:
3110 if raw:
3105 refs = raw.split(':')
3111 refs = raw.split(':')
3106 return Reference(*refs)
3112 return Reference(*refs)
3107 else:
3113 else:
3108 return None
3114 return None
3109
3115
3110 def reference_to_unicode(self, ref):
3116 def reference_to_unicode(self, ref):
3111 """
3117 """
3112 Convert a reference object to unicode.
3118 Convert a reference object to unicode.
3113 If reference is None it returns None.
3119 If reference is None it returns None.
3114 """
3120 """
3115 if ref:
3121 if ref:
3116 return u':'.join(ref)
3122 return u':'.join(ref)
3117 else:
3123 else:
3118 return None
3124 return None
3119
3125
3120
3121 class PullRequest(Base, _PullRequestBase):
3122 __tablename__ = 'pull_requests'
3123 __table_args__ = (
3124 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3125 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3126 )
3127
3128 pull_request_id = Column(
3129 'pull_request_id', Integer(), nullable=False, primary_key=True)
3130
3131 def __repr__(self):
3132 if self.pull_request_id:
3133 return '<DB:PullRequest #%s>' % self.pull_request_id
3134 else:
3135 return '<DB:PullRequest at %#x>' % id(self)
3136
3137 reviewers = relationship('PullRequestReviewers',
3138 cascade="all, delete, delete-orphan")
3139 statuses = relationship('ChangesetStatus')
3140 comments = relationship('ChangesetComment',
3141 cascade="all, delete, delete-orphan")
3142 versions = relationship('PullRequestVersion',
3143 cascade="all, delete, delete-orphan")
3144
3145 def is_closed(self):
3146 return self.status == self.STATUS_CLOSED
3147
3148 def get_api_data(self):
3126 def get_api_data(self):
3149 from rhodecode.model.pull_request import PullRequestModel
3127 from rhodecode.model.pull_request import PullRequestModel
3150 pull_request = self
3128 pull_request = self
3151 merge_status = PullRequestModel().merge_status(pull_request)
3129 merge_status = PullRequestModel().merge_status(pull_request)
3152
3130
3153 pull_request_url = url(
3131 pull_request_url = url(
3154 'pullrequest_show', repo_name=self.target_repo.repo_name,
3132 'pullrequest_show', repo_name=self.target_repo.repo_name,
3155 pull_request_id=self.pull_request_id, qualified=True)
3133 pull_request_id=self.pull_request_id, qualified=True)
3156
3134
3157 merge_data = {
3135 merge_data = {
3158 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3136 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3159 'reference': (
3137 'reference': (
3160 pull_request.shadow_merge_ref._asdict()
3138 pull_request.shadow_merge_ref._asdict()
3161 if pull_request.shadow_merge_ref else None),
3139 if pull_request.shadow_merge_ref else None),
3162 }
3140 }
3163
3141
3164 data = {
3142 data = {
3165 'pull_request_id': pull_request.pull_request_id,
3143 'pull_request_id': pull_request.pull_request_id,
3166 'url': pull_request_url,
3144 'url': pull_request_url,
3167 'title': pull_request.title,
3145 'title': pull_request.title,
3168 'description': pull_request.description,
3146 'description': pull_request.description,
3169 'status': pull_request.status,
3147 'status': pull_request.status,
3170 'created_on': pull_request.created_on,
3148 'created_on': pull_request.created_on,
3171 'updated_on': pull_request.updated_on,
3149 'updated_on': pull_request.updated_on,
3172 'commit_ids': pull_request.revisions,
3150 'commit_ids': pull_request.revisions,
3173 'review_status': pull_request.calculated_review_status(),
3151 'review_status': pull_request.calculated_review_status(),
3174 'mergeable': {
3152 'mergeable': {
3175 'status': merge_status[0],
3153 'status': merge_status[0],
3176 'message': unicode(merge_status[1]),
3154 'message': unicode(merge_status[1]),
3177 },
3155 },
3178 'source': {
3156 'source': {
3179 'clone_url': pull_request.source_repo.clone_url(),
3157 'clone_url': pull_request.source_repo.clone_url(),
3180 'repository': pull_request.source_repo.repo_name,
3158 'repository': pull_request.source_repo.repo_name,
3181 'reference': {
3159 'reference': {
3182 'name': pull_request.source_ref_parts.name,
3160 'name': pull_request.source_ref_parts.name,
3183 'type': pull_request.source_ref_parts.type,
3161 'type': pull_request.source_ref_parts.type,
3184 'commit_id': pull_request.source_ref_parts.commit_id,
3162 'commit_id': pull_request.source_ref_parts.commit_id,
3185 },
3163 },
3186 },
3164 },
3187 'target': {
3165 'target': {
3188 'clone_url': pull_request.target_repo.clone_url(),
3166 'clone_url': pull_request.target_repo.clone_url(),
3189 'repository': pull_request.target_repo.repo_name,
3167 'repository': pull_request.target_repo.repo_name,
3190 'reference': {
3168 'reference': {
3191 'name': pull_request.target_ref_parts.name,
3169 'name': pull_request.target_ref_parts.name,
3192 'type': pull_request.target_ref_parts.type,
3170 'type': pull_request.target_ref_parts.type,
3193 'commit_id': pull_request.target_ref_parts.commit_id,
3171 'commit_id': pull_request.target_ref_parts.commit_id,
3194 },
3172 },
3195 },
3173 },
3196 'merge': merge_data,
3174 'merge': merge_data,
3197 'author': pull_request.author.get_api_data(include_secrets=False,
3175 'author': pull_request.author.get_api_data(include_secrets=False,
3198 details='basic'),
3176 details='basic'),
3199 'reviewers': [
3177 'reviewers': [
3200 {
3178 {
3201 'user': reviewer.get_api_data(include_secrets=False,
3179 'user': reviewer.get_api_data(include_secrets=False,
3202 details='basic'),
3180 details='basic'),
3203 'reasons': reasons,
3181 'reasons': reasons,
3204 'review_status': st[0][1].status if st else 'not_reviewed',
3182 'review_status': st[0][1].status if st else 'not_reviewed',
3205 }
3183 }
3206 for reviewer, reasons, st in pull_request.reviewers_statuses()
3184 for reviewer, reasons, st in pull_request.reviewers_statuses()
3207 ]
3185 ]
3208 }
3186 }
3209
3187
3210 return data
3188 return data
3211
3189
3190
3191 class PullRequest(Base, _PullRequestBase):
3192 __tablename__ = 'pull_requests'
3193 __table_args__ = (
3194 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3195 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3196 )
3197
3198 pull_request_id = Column(
3199 'pull_request_id', Integer(), nullable=False, primary_key=True)
3200
3201 def __repr__(self):
3202 if self.pull_request_id:
3203 return '<DB:PullRequest #%s>' % self.pull_request_id
3204 else:
3205 return '<DB:PullRequest at %#x>' % id(self)
3206
3207 reviewers = relationship('PullRequestReviewers',
3208 cascade="all, delete, delete-orphan")
3209 statuses = relationship('ChangesetStatus')
3210 comments = relationship('ChangesetComment',
3211 cascade="all, delete, delete-orphan")
3212 versions = relationship('PullRequestVersion',
3213 cascade="all, delete, delete-orphan",
3214 lazy='dynamic')
3215
3216 def is_closed(self):
3217 return self.status == self.STATUS_CLOSED
3218
3212 def __json__(self):
3219 def __json__(self):
3213 return {
3220 return {
3214 'revisions': self.revisions,
3221 'revisions': self.revisions,
3215 }
3222 }
3216
3223
3217 def calculated_review_status(self):
3224 def calculated_review_status(self):
3218 from rhodecode.model.changeset_status import ChangesetStatusModel
3225 from rhodecode.model.changeset_status import ChangesetStatusModel
3219 return ChangesetStatusModel().calculated_review_status(self)
3226 return ChangesetStatusModel().calculated_review_status(self)
3220
3227
3221 def reviewers_statuses(self):
3228 def reviewers_statuses(self):
3222 from rhodecode.model.changeset_status import ChangesetStatusModel
3229 from rhodecode.model.changeset_status import ChangesetStatusModel
3223 return ChangesetStatusModel().reviewers_statuses(self)
3230 return ChangesetStatusModel().reviewers_statuses(self)
3224
3231
3225
3232
3226 class PullRequestVersion(Base, _PullRequestBase):
3233 class PullRequestVersion(Base, _PullRequestBase):
3227 __tablename__ = 'pull_request_versions'
3234 __tablename__ = 'pull_request_versions'
3228 __table_args__ = (
3235 __table_args__ = (
3229 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3236 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3230 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3237 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3231 )
3238 )
3232
3239
3233 pull_request_version_id = Column(
3240 pull_request_version_id = Column(
3234 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3241 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3235 pull_request_id = Column(
3242 pull_request_id = Column(
3236 'pull_request_id', Integer(),
3243 'pull_request_id', Integer(),
3237 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3244 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3238 pull_request = relationship('PullRequest')
3245 pull_request = relationship('PullRequest')
3239
3246
3240 def __repr__(self):
3247 def __repr__(self):
3241 if self.pull_request_version_id:
3248 if self.pull_request_version_id:
3242 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3249 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3243 else:
3250 else:
3244 return '<DB:PullRequestVersion at %#x>' % id(self)
3251 return '<DB:PullRequestVersion at %#x>' % id(self)
3245
3252
3253 @property
3254 def reviewers(self):
3255 return self.pull_request.reviewers
3256
3257 @property
3258 def versions(self):
3259 return self.pull_request.versions
3260
3261 def is_closed(self):
3262 # calculate from original
3263 return self.pull_request.status == self.STATUS_CLOSED
3264
3265 def calculated_review_status(self):
3266 return self.pull_request.calculated_review_status()
3267
3268 def reviewers_statuses(self):
3269 return self.pull_request.reviewers_statuses()
3270
3246
3271
3247 class PullRequestReviewers(Base, BaseModel):
3272 class PullRequestReviewers(Base, BaseModel):
3248 __tablename__ = 'pull_request_reviewers'
3273 __tablename__ = 'pull_request_reviewers'
3249 __table_args__ = (
3274 __table_args__ = (
3250 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3275 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3251 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3276 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3252 )
3277 )
3253
3278
3254 def __init__(self, user=None, pull_request=None, reasons=None):
3279 def __init__(self, user=None, pull_request=None, reasons=None):
3255 self.user = user
3280 self.user = user
3256 self.pull_request = pull_request
3281 self.pull_request = pull_request
3257 self.reasons = reasons or []
3282 self.reasons = reasons or []
3258
3283
3259 @hybrid_property
3284 @hybrid_property
3260 def reasons(self):
3285 def reasons(self):
3261 if not self._reasons:
3286 if not self._reasons:
3262 return []
3287 return []
3263 return self._reasons
3288 return self._reasons
3264
3289
3265 @reasons.setter
3290 @reasons.setter
3266 def reasons(self, val):
3291 def reasons(self, val):
3267 val = val or []
3292 val = val or []
3268 if any(not isinstance(x, basestring) for x in val):
3293 if any(not isinstance(x, basestring) for x in val):
3269 raise Exception('invalid reasons type, must be list of strings')
3294 raise Exception('invalid reasons type, must be list of strings')
3270 self._reasons = val
3295 self._reasons = val
3271
3296
3272 pull_requests_reviewers_id = Column(
3297 pull_requests_reviewers_id = Column(
3273 'pull_requests_reviewers_id', Integer(), nullable=False,
3298 'pull_requests_reviewers_id', Integer(), nullable=False,
3274 primary_key=True)
3299 primary_key=True)
3275 pull_request_id = Column(
3300 pull_request_id = Column(
3276 "pull_request_id", Integer(),
3301 "pull_request_id", Integer(),
3277 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3302 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3278 user_id = Column(
3303 user_id = Column(
3279 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3304 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3280 _reasons = Column(
3305 _reasons = Column(
3281 'reason', MutationList.as_mutable(
3306 'reason', MutationList.as_mutable(
3282 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3307 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3283
3308
3284 user = relationship('User')
3309 user = relationship('User')
3285 pull_request = relationship('PullRequest')
3310 pull_request = relationship('PullRequest')
3286
3311
3287
3312
3288 class Notification(Base, BaseModel):
3313 class Notification(Base, BaseModel):
3289 __tablename__ = 'notifications'
3314 __tablename__ = 'notifications'
3290 __table_args__ = (
3315 __table_args__ = (
3291 Index('notification_type_idx', 'type'),
3316 Index('notification_type_idx', 'type'),
3292 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3317 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3293 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3318 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3294 )
3319 )
3295
3320
3296 TYPE_CHANGESET_COMMENT = u'cs_comment'
3321 TYPE_CHANGESET_COMMENT = u'cs_comment'
3297 TYPE_MESSAGE = u'message'
3322 TYPE_MESSAGE = u'message'
3298 TYPE_MENTION = u'mention'
3323 TYPE_MENTION = u'mention'
3299 TYPE_REGISTRATION = u'registration'
3324 TYPE_REGISTRATION = u'registration'
3300 TYPE_PULL_REQUEST = u'pull_request'
3325 TYPE_PULL_REQUEST = u'pull_request'
3301 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3326 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3302
3327
3303 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3328 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3304 subject = Column('subject', Unicode(512), nullable=True)
3329 subject = Column('subject', Unicode(512), nullable=True)
3305 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3330 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3306 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3331 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3307 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3332 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3308 type_ = Column('type', Unicode(255))
3333 type_ = Column('type', Unicode(255))
3309
3334
3310 created_by_user = relationship('User')
3335 created_by_user = relationship('User')
3311 notifications_to_users = relationship('UserNotification', lazy='joined',
3336 notifications_to_users = relationship('UserNotification', lazy='joined',
3312 cascade="all, delete, delete-orphan")
3337 cascade="all, delete, delete-orphan")
3313
3338
3314 @property
3339 @property
3315 def recipients(self):
3340 def recipients(self):
3316 return [x.user for x in UserNotification.query()\
3341 return [x.user for x in UserNotification.query()\
3317 .filter(UserNotification.notification == self)\
3342 .filter(UserNotification.notification == self)\
3318 .order_by(UserNotification.user_id.asc()).all()]
3343 .order_by(UserNotification.user_id.asc()).all()]
3319
3344
3320 @classmethod
3345 @classmethod
3321 def create(cls, created_by, subject, body, recipients, type_=None):
3346 def create(cls, created_by, subject, body, recipients, type_=None):
3322 if type_ is None:
3347 if type_ is None:
3323 type_ = Notification.TYPE_MESSAGE
3348 type_ = Notification.TYPE_MESSAGE
3324
3349
3325 notification = cls()
3350 notification = cls()
3326 notification.created_by_user = created_by
3351 notification.created_by_user = created_by
3327 notification.subject = subject
3352 notification.subject = subject
3328 notification.body = body
3353 notification.body = body
3329 notification.type_ = type_
3354 notification.type_ = type_
3330 notification.created_on = datetime.datetime.now()
3355 notification.created_on = datetime.datetime.now()
3331
3356
3332 for u in recipients:
3357 for u in recipients:
3333 assoc = UserNotification()
3358 assoc = UserNotification()
3334 assoc.notification = notification
3359 assoc.notification = notification
3335
3360
3336 # if created_by is inside recipients mark his notification
3361 # if created_by is inside recipients mark his notification
3337 # as read
3362 # as read
3338 if u.user_id == created_by.user_id:
3363 if u.user_id == created_by.user_id:
3339 assoc.read = True
3364 assoc.read = True
3340
3365
3341 u.notifications.append(assoc)
3366 u.notifications.append(assoc)
3342 Session().add(notification)
3367 Session().add(notification)
3343
3368
3344 return notification
3369 return notification
3345
3370
3346 @property
3371 @property
3347 def description(self):
3372 def description(self):
3348 from rhodecode.model.notification import NotificationModel
3373 from rhodecode.model.notification import NotificationModel
3349 return NotificationModel().make_description(self)
3374 return NotificationModel().make_description(self)
3350
3375
3351
3376
3352 class UserNotification(Base, BaseModel):
3377 class UserNotification(Base, BaseModel):
3353 __tablename__ = 'user_to_notification'
3378 __tablename__ = 'user_to_notification'
3354 __table_args__ = (
3379 __table_args__ = (
3355 UniqueConstraint('user_id', 'notification_id'),
3380 UniqueConstraint('user_id', 'notification_id'),
3356 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3381 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3357 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3382 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3358 )
3383 )
3359 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3384 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3360 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3385 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3361 read = Column('read', Boolean, default=False)
3386 read = Column('read', Boolean, default=False)
3362 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3387 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3363
3388
3364 user = relationship('User', lazy="joined")
3389 user = relationship('User', lazy="joined")
3365 notification = relationship('Notification', lazy="joined",
3390 notification = relationship('Notification', lazy="joined",
3366 order_by=lambda: Notification.created_on.desc(),)
3391 order_by=lambda: Notification.created_on.desc(),)
3367
3392
3368 def mark_as_read(self):
3393 def mark_as_read(self):
3369 self.read = True
3394 self.read = True
3370 Session().add(self)
3395 Session().add(self)
3371
3396
3372
3397
3373 class Gist(Base, BaseModel):
3398 class Gist(Base, BaseModel):
3374 __tablename__ = 'gists'
3399 __tablename__ = 'gists'
3375 __table_args__ = (
3400 __table_args__ = (
3376 Index('g_gist_access_id_idx', 'gist_access_id'),
3401 Index('g_gist_access_id_idx', 'gist_access_id'),
3377 Index('g_created_on_idx', 'created_on'),
3402 Index('g_created_on_idx', 'created_on'),
3378 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3403 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3379 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3404 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3380 )
3405 )
3381 GIST_PUBLIC = u'public'
3406 GIST_PUBLIC = u'public'
3382 GIST_PRIVATE = u'private'
3407 GIST_PRIVATE = u'private'
3383 DEFAULT_FILENAME = u'gistfile1.txt'
3408 DEFAULT_FILENAME = u'gistfile1.txt'
3384
3409
3385 ACL_LEVEL_PUBLIC = u'acl_public'
3410 ACL_LEVEL_PUBLIC = u'acl_public'
3386 ACL_LEVEL_PRIVATE = u'acl_private'
3411 ACL_LEVEL_PRIVATE = u'acl_private'
3387
3412
3388 gist_id = Column('gist_id', Integer(), primary_key=True)
3413 gist_id = Column('gist_id', Integer(), primary_key=True)
3389 gist_access_id = Column('gist_access_id', Unicode(250))
3414 gist_access_id = Column('gist_access_id', Unicode(250))
3390 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3415 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3391 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3416 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3392 gist_expires = Column('gist_expires', Float(53), nullable=False)
3417 gist_expires = Column('gist_expires', Float(53), nullable=False)
3393 gist_type = Column('gist_type', Unicode(128), nullable=False)
3418 gist_type = Column('gist_type', Unicode(128), nullable=False)
3394 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3419 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3395 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3420 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3396 acl_level = Column('acl_level', Unicode(128), nullable=True)
3421 acl_level = Column('acl_level', Unicode(128), nullable=True)
3397
3422
3398 owner = relationship('User')
3423 owner = relationship('User')
3399
3424
3400 def __repr__(self):
3425 def __repr__(self):
3401 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3426 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3402
3427
3403 @classmethod
3428 @classmethod
3404 def get_or_404(cls, id_):
3429 def get_or_404(cls, id_):
3405 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3430 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3406 if not res:
3431 if not res:
3407 raise HTTPNotFound
3432 raise HTTPNotFound
3408 return res
3433 return res
3409
3434
3410 @classmethod
3435 @classmethod
3411 def get_by_access_id(cls, gist_access_id):
3436 def get_by_access_id(cls, gist_access_id):
3412 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3437 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3413
3438
3414 def gist_url(self):
3439 def gist_url(self):
3415 import rhodecode
3440 import rhodecode
3416 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3441 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3417 if alias_url:
3442 if alias_url:
3418 return alias_url.replace('{gistid}', self.gist_access_id)
3443 return alias_url.replace('{gistid}', self.gist_access_id)
3419
3444
3420 return url('gist', gist_id=self.gist_access_id, qualified=True)
3445 return url('gist', gist_id=self.gist_access_id, qualified=True)
3421
3446
3422 @classmethod
3447 @classmethod
3423 def base_path(cls):
3448 def base_path(cls):
3424 """
3449 """
3425 Returns base path when all gists are stored
3450 Returns base path when all gists are stored
3426
3451
3427 :param cls:
3452 :param cls:
3428 """
3453 """
3429 from rhodecode.model.gist import GIST_STORE_LOC
3454 from rhodecode.model.gist import GIST_STORE_LOC
3430 q = Session().query(RhodeCodeUi)\
3455 q = Session().query(RhodeCodeUi)\
3431 .filter(RhodeCodeUi.ui_key == URL_SEP)
3456 .filter(RhodeCodeUi.ui_key == URL_SEP)
3432 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3457 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3433 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3458 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3434
3459
3435 def get_api_data(self):
3460 def get_api_data(self):
3436 """
3461 """
3437 Common function for generating gist related data for API
3462 Common function for generating gist related data for API
3438 """
3463 """
3439 gist = self
3464 gist = self
3440 data = {
3465 data = {
3441 'gist_id': gist.gist_id,
3466 'gist_id': gist.gist_id,
3442 'type': gist.gist_type,
3467 'type': gist.gist_type,
3443 'access_id': gist.gist_access_id,
3468 'access_id': gist.gist_access_id,
3444 'description': gist.gist_description,
3469 'description': gist.gist_description,
3445 'url': gist.gist_url(),
3470 'url': gist.gist_url(),
3446 'expires': gist.gist_expires,
3471 'expires': gist.gist_expires,
3447 'created_on': gist.created_on,
3472 'created_on': gist.created_on,
3448 'modified_at': gist.modified_at,
3473 'modified_at': gist.modified_at,
3449 'content': None,
3474 'content': None,
3450 'acl_level': gist.acl_level,
3475 'acl_level': gist.acl_level,
3451 }
3476 }
3452 return data
3477 return data
3453
3478
3454 def __json__(self):
3479 def __json__(self):
3455 data = dict(
3480 data = dict(
3456 )
3481 )
3457 data.update(self.get_api_data())
3482 data.update(self.get_api_data())
3458 return data
3483 return data
3459 # SCM functions
3484 # SCM functions
3460
3485
3461 def scm_instance(self, **kwargs):
3486 def scm_instance(self, **kwargs):
3462 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3487 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3463 return get_vcs_instance(
3488 return get_vcs_instance(
3464 repo_path=safe_str(full_repo_path), create=False)
3489 repo_path=safe_str(full_repo_path), create=False)
3465
3490
3466
3491
3467 class DbMigrateVersion(Base, BaseModel):
3492 class DbMigrateVersion(Base, BaseModel):
3468 __tablename__ = 'db_migrate_version'
3493 __tablename__ = 'db_migrate_version'
3469 __table_args__ = (
3494 __table_args__ = (
3470 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3495 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3471 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3496 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3472 )
3497 )
3473 repository_id = Column('repository_id', String(250), primary_key=True)
3498 repository_id = Column('repository_id', String(250), primary_key=True)
3474 repository_path = Column('repository_path', Text)
3499 repository_path = Column('repository_path', Text)
3475 version = Column('version', Integer)
3500 version = Column('version', Integer)
3476
3501
3477
3502
3478 class ExternalIdentity(Base, BaseModel):
3503 class ExternalIdentity(Base, BaseModel):
3479 __tablename__ = 'external_identities'
3504 __tablename__ = 'external_identities'
3480 __table_args__ = (
3505 __table_args__ = (
3481 Index('local_user_id_idx', 'local_user_id'),
3506 Index('local_user_id_idx', 'local_user_id'),
3482 Index('external_id_idx', 'external_id'),
3507 Index('external_id_idx', 'external_id'),
3483 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3508 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3484 'mysql_charset': 'utf8'})
3509 'mysql_charset': 'utf8'})
3485
3510
3486 external_id = Column('external_id', Unicode(255), default=u'',
3511 external_id = Column('external_id', Unicode(255), default=u'',
3487 primary_key=True)
3512 primary_key=True)
3488 external_username = Column('external_username', Unicode(1024), default=u'')
3513 external_username = Column('external_username', Unicode(1024), default=u'')
3489 local_user_id = Column('local_user_id', Integer(),
3514 local_user_id = Column('local_user_id', Integer(),
3490 ForeignKey('users.user_id'), primary_key=True)
3515 ForeignKey('users.user_id'), primary_key=True)
3491 provider_name = Column('provider_name', Unicode(255), default=u'',
3516 provider_name = Column('provider_name', Unicode(255), default=u'',
3492 primary_key=True)
3517 primary_key=True)
3493 access_token = Column('access_token', String(1024), default=u'')
3518 access_token = Column('access_token', String(1024), default=u'')
3494 alt_token = Column('alt_token', String(1024), default=u'')
3519 alt_token = Column('alt_token', String(1024), default=u'')
3495 token_secret = Column('token_secret', String(1024), default=u'')
3520 token_secret = Column('token_secret', String(1024), default=u'')
3496
3521
3497 @classmethod
3522 @classmethod
3498 def by_external_id_and_provider(cls, external_id, provider_name,
3523 def by_external_id_and_provider(cls, external_id, provider_name,
3499 local_user_id=None):
3524 local_user_id=None):
3500 """
3525 """
3501 Returns ExternalIdentity instance based on search params
3526 Returns ExternalIdentity instance based on search params
3502
3527
3503 :param external_id:
3528 :param external_id:
3504 :param provider_name:
3529 :param provider_name:
3505 :return: ExternalIdentity
3530 :return: ExternalIdentity
3506 """
3531 """
3507 query = cls.query()
3532 query = cls.query()
3508 query = query.filter(cls.external_id == external_id)
3533 query = query.filter(cls.external_id == external_id)
3509 query = query.filter(cls.provider_name == provider_name)
3534 query = query.filter(cls.provider_name == provider_name)
3510 if local_user_id:
3535 if local_user_id:
3511 query = query.filter(cls.local_user_id == local_user_id)
3536 query = query.filter(cls.local_user_id == local_user_id)
3512 return query.first()
3537 return query.first()
3513
3538
3514 @classmethod
3539 @classmethod
3515 def user_by_external_id_and_provider(cls, external_id, provider_name):
3540 def user_by_external_id_and_provider(cls, external_id, provider_name):
3516 """
3541 """
3517 Returns User instance based on search params
3542 Returns User instance based on search params
3518
3543
3519 :param external_id:
3544 :param external_id:
3520 :param provider_name:
3545 :param provider_name:
3521 :return: User
3546 :return: User
3522 """
3547 """
3523 query = User.query()
3548 query = User.query()
3524 query = query.filter(cls.external_id == external_id)
3549 query = query.filter(cls.external_id == external_id)
3525 query = query.filter(cls.provider_name == provider_name)
3550 query = query.filter(cls.provider_name == provider_name)
3526 query = query.filter(User.user_id == cls.local_user_id)
3551 query = query.filter(User.user_id == cls.local_user_id)
3527 return query.first()
3552 return query.first()
3528
3553
3529 @classmethod
3554 @classmethod
3530 def by_local_user_id(cls, local_user_id):
3555 def by_local_user_id(cls, local_user_id):
3531 """
3556 """
3532 Returns all tokens for user
3557 Returns all tokens for user
3533
3558
3534 :param local_user_id:
3559 :param local_user_id:
3535 :return: ExternalIdentity
3560 :return: ExternalIdentity
3536 """
3561 """
3537 query = cls.query()
3562 query = cls.query()
3538 query = query.filter(cls.local_user_id == local_user_id)
3563 query = query.filter(cls.local_user_id == local_user_id)
3539 return query
3564 return query
3540
3565
3541
3566
3542 class Integration(Base, BaseModel):
3567 class Integration(Base, BaseModel):
3543 __tablename__ = 'integrations'
3568 __tablename__ = 'integrations'
3544 __table_args__ = (
3569 __table_args__ = (
3545 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3570 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3546 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3571 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3547 )
3572 )
3548
3573
3549 integration_id = Column('integration_id', Integer(), primary_key=True)
3574 integration_id = Column('integration_id', Integer(), primary_key=True)
3550 integration_type = Column('integration_type', String(255))
3575 integration_type = Column('integration_type', String(255))
3551 enabled = Column('enabled', Boolean(), nullable=False)
3576 enabled = Column('enabled', Boolean(), nullable=False)
3552 name = Column('name', String(255), nullable=False)
3577 name = Column('name', String(255), nullable=False)
3553 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3578 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3554 default=False)
3579 default=False)
3555
3580
3556 settings = Column(
3581 settings = Column(
3557 'settings_json', MutationObj.as_mutable(
3582 'settings_json', MutationObj.as_mutable(
3558 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3583 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3559 repo_id = Column(
3584 repo_id = Column(
3560 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3585 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3561 nullable=True, unique=None, default=None)
3586 nullable=True, unique=None, default=None)
3562 repo = relationship('Repository', lazy='joined')
3587 repo = relationship('Repository', lazy='joined')
3563
3588
3564 repo_group_id = Column(
3589 repo_group_id = Column(
3565 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3590 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3566 nullable=True, unique=None, default=None)
3591 nullable=True, unique=None, default=None)
3567 repo_group = relationship('RepoGroup', lazy='joined')
3592 repo_group = relationship('RepoGroup', lazy='joined')
3568
3593
3569 @property
3594 @property
3570 def scope(self):
3595 def scope(self):
3571 if self.repo:
3596 if self.repo:
3572 return repr(self.repo)
3597 return repr(self.repo)
3573 if self.repo_group:
3598 if self.repo_group:
3574 if self.child_repos_only:
3599 if self.child_repos_only:
3575 return repr(self.repo_group) + ' (child repos only)'
3600 return repr(self.repo_group) + ' (child repos only)'
3576 else:
3601 else:
3577 return repr(self.repo_group) + ' (recursive)'
3602 return repr(self.repo_group) + ' (recursive)'
3578 if self.child_repos_only:
3603 if self.child_repos_only:
3579 return 'root_repos'
3604 return 'root_repos'
3580 return 'global'
3605 return 'global'
3581
3606
3582 def __repr__(self):
3607 def __repr__(self):
3583 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3608 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3584
3609
3585
3610
3586 class RepoReviewRuleUser(Base, BaseModel):
3611 class RepoReviewRuleUser(Base, BaseModel):
3587 __tablename__ = 'repo_review_rules_users'
3612 __tablename__ = 'repo_review_rules_users'
3588 __table_args__ = (
3613 __table_args__ = (
3589 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3614 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3590 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3615 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3591 )
3616 )
3592 repo_review_rule_user_id = Column(
3617 repo_review_rule_user_id = Column(
3593 'repo_review_rule_user_id', Integer(), primary_key=True)
3618 'repo_review_rule_user_id', Integer(), primary_key=True)
3594 repo_review_rule_id = Column("repo_review_rule_id",
3619 repo_review_rule_id = Column("repo_review_rule_id",
3595 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3620 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3596 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3621 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3597 nullable=False)
3622 nullable=False)
3598 user = relationship('User')
3623 user = relationship('User')
3599
3624
3600
3625
3601 class RepoReviewRuleUserGroup(Base, BaseModel):
3626 class RepoReviewRuleUserGroup(Base, BaseModel):
3602 __tablename__ = 'repo_review_rules_users_groups'
3627 __tablename__ = 'repo_review_rules_users_groups'
3603 __table_args__ = (
3628 __table_args__ = (
3604 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3629 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3605 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3630 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3606 )
3631 )
3607 repo_review_rule_users_group_id = Column(
3632 repo_review_rule_users_group_id = Column(
3608 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3633 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3609 repo_review_rule_id = Column("repo_review_rule_id",
3634 repo_review_rule_id = Column("repo_review_rule_id",
3610 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3635 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3611 users_group_id = Column("users_group_id", Integer(),
3636 users_group_id = Column("users_group_id", Integer(),
3612 ForeignKey('users_groups.users_group_id'), nullable=False)
3637 ForeignKey('users_groups.users_group_id'), nullable=False)
3613 users_group = relationship('UserGroup')
3638 users_group = relationship('UserGroup')
3614
3639
3615
3640
3616 class RepoReviewRule(Base, BaseModel):
3641 class RepoReviewRule(Base, BaseModel):
3617 __tablename__ = 'repo_review_rules'
3642 __tablename__ = 'repo_review_rules'
3618 __table_args__ = (
3643 __table_args__ = (
3619 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3644 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3620 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3645 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3621 )
3646 )
3622
3647
3623 repo_review_rule_id = Column(
3648 repo_review_rule_id = Column(
3624 'repo_review_rule_id', Integer(), primary_key=True)
3649 'repo_review_rule_id', Integer(), primary_key=True)
3625 repo_id = Column(
3650 repo_id = Column(
3626 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3651 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3627 repo = relationship('Repository', backref='review_rules')
3652 repo = relationship('Repository', backref='review_rules')
3628
3653
3629 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3654 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3630 default=u'*') # glob
3655 default=u'*') # glob
3631 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3656 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3632 default=u'*') # glob
3657 default=u'*') # glob
3633
3658
3634 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3659 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3635 nullable=False, default=False)
3660 nullable=False, default=False)
3636 rule_users = relationship('RepoReviewRuleUser')
3661 rule_users = relationship('RepoReviewRuleUser')
3637 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3662 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3638
3663
3639 @hybrid_property
3664 @hybrid_property
3640 def branch_pattern(self):
3665 def branch_pattern(self):
3641 return self._branch_pattern or '*'
3666 return self._branch_pattern or '*'
3642
3667
3643 def _validate_glob(self, value):
3668 def _validate_glob(self, value):
3644 re.compile('^' + glob2re(value) + '$')
3669 re.compile('^' + glob2re(value) + '$')
3645
3670
3646 @branch_pattern.setter
3671 @branch_pattern.setter
3647 def branch_pattern(self, value):
3672 def branch_pattern(self, value):
3648 self._validate_glob(value)
3673 self._validate_glob(value)
3649 self._branch_pattern = value or '*'
3674 self._branch_pattern = value or '*'
3650
3675
3651 @hybrid_property
3676 @hybrid_property
3652 def file_pattern(self):
3677 def file_pattern(self):
3653 return self._file_pattern or '*'
3678 return self._file_pattern or '*'
3654
3679
3655 @file_pattern.setter
3680 @file_pattern.setter
3656 def file_pattern(self, value):
3681 def file_pattern(self, value):
3657 self._validate_glob(value)
3682 self._validate_glob(value)
3658 self._file_pattern = value or '*'
3683 self._file_pattern = value or '*'
3659
3684
3660 def matches(self, branch, files_changed):
3685 def matches(self, branch, files_changed):
3661 """
3686 """
3662 Check if this review rule matches a branch/files in a pull request
3687 Check if this review rule matches a branch/files in a pull request
3663
3688
3664 :param branch: branch name for the commit
3689 :param branch: branch name for the commit
3665 :param files_changed: list of file paths changed in the pull request
3690 :param files_changed: list of file paths changed in the pull request
3666 """
3691 """
3667
3692
3668 branch = branch or ''
3693 branch = branch or ''
3669 files_changed = files_changed or []
3694 files_changed = files_changed or []
3670
3695
3671 branch_matches = True
3696 branch_matches = True
3672 if branch:
3697 if branch:
3673 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3698 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3674 branch_matches = bool(branch_regex.search(branch))
3699 branch_matches = bool(branch_regex.search(branch))
3675
3700
3676 files_matches = True
3701 files_matches = True
3677 if self.file_pattern != '*':
3702 if self.file_pattern != '*':
3678 files_matches = False
3703 files_matches = False
3679 file_regex = re.compile(glob2re(self.file_pattern))
3704 file_regex = re.compile(glob2re(self.file_pattern))
3680 for filename in files_changed:
3705 for filename in files_changed:
3681 if file_regex.search(filename):
3706 if file_regex.search(filename):
3682 files_matches = True
3707 files_matches = True
3683 break
3708 break
3684
3709
3685 return branch_matches and files_matches
3710 return branch_matches and files_matches
3686
3711
3687 @property
3712 @property
3688 def review_users(self):
3713 def review_users(self):
3689 """ Returns the users which this rule applies to """
3714 """ Returns the users which this rule applies to """
3690
3715
3691 users = set()
3716 users = set()
3692 users |= set([
3717 users |= set([
3693 rule_user.user for rule_user in self.rule_users
3718 rule_user.user for rule_user in self.rule_users
3694 if rule_user.user.active])
3719 if rule_user.user.active])
3695 users |= set(
3720 users |= set(
3696 member.user
3721 member.user
3697 for rule_user_group in self.rule_user_groups
3722 for rule_user_group in self.rule_user_groups
3698 for member in rule_user_group.users_group.members
3723 for member in rule_user_group.users_group.members
3699 if member.user.active
3724 if member.user.active
3700 )
3725 )
3701 return users
3726 return users
3702
3727
3703 def __repr__(self):
3728 def __repr__(self):
3704 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3729 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3705 self.repo_review_rule_id, self.repo)
3730 self.repo_review_rule_id, self.repo)
@@ -1,389 +1,395 b''
1
1
2
2
3 //BUTTONS
3 //BUTTONS
4 button,
4 button,
5 .btn,
5 .btn,
6 input[type="button"] {
6 input[type="button"] {
7 -webkit-appearance: none;
7 -webkit-appearance: none;
8 display: inline-block;
8 display: inline-block;
9 margin: 0 @padding/3 0 0;
9 margin: 0 @padding/3 0 0;
10 padding: @button-padding;
10 padding: @button-padding;
11 text-align: center;
11 text-align: center;
12 font-size: @basefontsize;
12 font-size: @basefontsize;
13 line-height: 1em;
13 line-height: 1em;
14 font-family: @text-light;
14 font-family: @text-light;
15 text-decoration: none;
15 text-decoration: none;
16 text-shadow: none;
16 text-shadow: none;
17 color: @grey4;
17 color: @grey4;
18 background-color: white;
18 background-color: white;
19 background-image: none;
19 background-image: none;
20 border: none;
20 border: none;
21 .border ( @border-thickness-buttons, @grey4 );
21 .border ( @border-thickness-buttons, @grey4 );
22 .border-radius (@border-radius);
22 .border-radius (@border-radius);
23 cursor: pointer;
23 cursor: pointer;
24 white-space: nowrap;
24 white-space: nowrap;
25 -webkit-transition: background .3s,color .3s;
25 -webkit-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
29
29
30 a {
30 a {
31 display: block;
31 display: block;
32 margin: 0;
32 margin: 0;
33 padding: 0;
33 padding: 0;
34 color: inherit;
34 color: inherit;
35 text-decoration: none;
35 text-decoration: none;
36
36
37 &:hover {
37 &:hover {
38 text-decoration: none;
38 text-decoration: none;
39 }
39 }
40 }
40 }
41
41
42 &:focus,
42 &:focus,
43 &:active {
43 &:active {
44 outline:none;
44 outline:none;
45 }
45 }
46 &:hover {
46 &:hover {
47 color: white;
47 color: white;
48 background-color: @grey4;
48 background-color: @grey4;
49 }
49 }
50
50
51 .icon-remove-sign {
51 .icon-remove-sign {
52 display: none;
52 display: none;
53 }
53 }
54
54
55 //disabled buttons
55 //disabled buttons
56 //last; overrides any other styles
56 //last; overrides any other styles
57 &:disabled {
57 &:disabled {
58 opacity: .7;
58 opacity: .7;
59 cursor: auto;
59 cursor: auto;
60 background-color: white;
60 background-color: white;
61 color: @grey4;
61 color: @grey4;
62 text-shadow: none;
62 text-shadow: none;
63 }
63 }
64
64
65 }
65 }
66
66
67
67
68 .btn-default {
68 .btn-default {
69 .border ( @border-thickness-buttons, @rcblue );
69 .border ( @border-thickness-buttons, @rcblue );
70 background-image: none;
70 background-image: none;
71 color: @rcblue;
71 color: @rcblue;
72
72
73 a {
73 a {
74 color: @rcblue;
74 color: @rcblue;
75 }
75 }
76
76
77 &:hover,
77 &:hover,
78 &.active {
78 &.active {
79 color: white;
79 color: white;
80 background-color: @rcdarkblue;
80 background-color: @rcdarkblue;
81 .border ( @border-thickness, @rcdarkblue );
81 .border ( @border-thickness, @rcdarkblue );
82
82
83 a {
83 a {
84 color: white;
84 color: white;
85 }
85 }
86 }
86 }
87 &:disabled {
87 &:disabled {
88 .border ( @border-thickness-buttons, @grey4 );
88 .border ( @border-thickness-buttons, @grey4 );
89 background-color: none;
89 background-color: none;
90 }
90 }
91 }
91 }
92
92
93 .btn-primary,
93 .btn-primary,
94 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
94 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
95 .btn-success {
95 .btn-success {
96 .border ( @border-thickness, @rcblue );
96 .border ( @border-thickness, @rcblue );
97 background-color: @rcblue;
97 background-color: @rcblue;
98 color: white;
98 color: white;
99
99
100 a {
100 a {
101 color: white;
101 color: white;
102 }
102 }
103
103
104 &:hover,
104 &:hover,
105 &.active {
105 &.active {
106 .border ( @border-thickness, @rcdarkblue );
106 .border ( @border-thickness, @rcdarkblue );
107 color: white;
107 color: white;
108 background-color: @rcdarkblue;
108 background-color: @rcdarkblue;
109
109
110 a {
110 a {
111 color: white;
111 color: white;
112 }
112 }
113 }
113 }
114 &:disabled {
114 &:disabled {
115 background-color: @rcblue;
115 background-color: @rcblue;
116 }
116 }
117 }
117 }
118
118
119 .btn-secondary {
119 .btn-secondary {
120 &:extend(.btn-default);
120 &:extend(.btn-default);
121
121
122 background-color: white;
122 background-color: white;
123
123
124 &:focus {
124 &:focus {
125 outline: 0;
125 outline: 0;
126 }
126 }
127
127
128 &:hover {
128 &:hover {
129 &:extend(.btn-default:hover);
129 &:extend(.btn-default:hover);
130 }
130 }
131
131
132 &.btn-link {
132 &.btn-link {
133 &:extend(.btn-link);
133 &:extend(.btn-link);
134 color: @rcblue;
134 color: @rcblue;
135 }
135 }
136
136
137 &:disabled {
137 &:disabled {
138 color: @rcblue;
138 color: @rcblue;
139 background-color: white;
139 background-color: white;
140 }
140 }
141 }
141 }
142
142
143 .btn-warning,
143 .btn-warning,
144 .btn-danger,
144 .btn-danger,
145 .revoke_perm,
145 .revoke_perm,
146 .btn-x,
146 .btn-x,
147 .form .action_button.btn-x {
147 .form .action_button.btn-x {
148 .border ( @border-thickness, @alert2 );
148 .border ( @border-thickness, @alert2 );
149 background-color: white;
149 background-color: white;
150 color: @alert2;
150 color: @alert2;
151
151
152 a {
152 a {
153 color: @alert2;
153 color: @alert2;
154 }
154 }
155
155
156 &:hover,
156 &:hover,
157 &.active {
157 &.active {
158 .border ( @border-thickness, @alert2 );
158 .border ( @border-thickness, @alert2 );
159 color: white;
159 color: white;
160 background-color: @alert2;
160 background-color: @alert2;
161
161
162 a {
162 a {
163 color: white;
163 color: white;
164 }
164 }
165 }
165 }
166
166
167 i {
167 i {
168 display:none;
168 display:none;
169 }
169 }
170
170
171 &:disabled {
171 &:disabled {
172 background-color: white;
172 background-color: white;
173 color: @alert2;
173 color: @alert2;
174 }
174 }
175 }
175 }
176
176
177 .btn-sm,
177 .btn-sm,
178 .btn-mini,
178 .btn-mini,
179 .field-sm .btn {
179 .field-sm .btn {
180 padding: @padding/3;
180 padding: @padding/3;
181 }
181 }
182
182
183 .btn-xs {
183 .btn-xs {
184 padding: @padding/4;
184 padding: @padding/4;
185 }
185 }
186
186
187 .btn-lg {
187 .btn-lg {
188 padding: @padding * 1.2;
188 padding: @padding * 1.2;
189 }
189 }
190
190
191 .btn-group {
191 .btn-group {
192 display: inline-block;
192 display: inline-block;
193 .btn {
193 .btn {
194 float: left;
194 float: left;
195 margin: 0 0 0 -1px;
195 margin: 0 0 0 -1px;
196 }
196 }
197 }
197 }
198
198
199 .btn-link {
199 .btn-link {
200 background: transparent;
200 background: transparent;
201 border: none;
201 border: none;
202 padding: 0;
202 padding: 0;
203 color: @rcblue;
203 color: @rcblue;
204
204
205 &:hover {
205 &:hover {
206 background: transparent;
206 background: transparent;
207 border: none;
207 border: none;
208 color: @rcdarkblue;
208 color: @rcdarkblue;
209 }
209 }
210
210
211 //disabled buttons
212 //last; overrides any other styles
211 &:disabled {
213 &:disabled {
214 opacity: .7;
215 cursor: auto;
216 background-color: white;
212 color: @grey4;
217 color: @grey4;
218 text-shadow: none;
213 }
219 }
214
220
215 // TODO: johbo: Check if we can avoid this, indicates that the structure
221 // TODO: johbo: Check if we can avoid this, indicates that the structure
216 // is not yet good.
222 // is not yet good.
217 // lisa: The button CSS reflects the button HTML; both need a cleanup.
223 // lisa: The button CSS reflects the button HTML; both need a cleanup.
218 &.btn-danger {
224 &.btn-danger {
219 color: @alert2;
225 color: @alert2;
220
226
221 &:hover {
227 &:hover {
222 color: darken(@alert2,30%);
228 color: darken(@alert2,30%);
223 }
229 }
224
230
225 &:disabled {
231 &:disabled {
226 color: @alert2;
232 color: @alert2;
227 }
233 }
228 }
234 }
229 }
235 }
230
236
231 .btn-social {
237 .btn-social {
232 &:extend(.btn-default);
238 &:extend(.btn-default);
233 margin: 5px 5px 5px 0px;
239 margin: 5px 5px 5px 0px;
234 min-width: 150px;
240 min-width: 150px;
235 }
241 }
236
242
237 // TODO: johbo: check these exceptions
243 // TODO: johbo: check these exceptions
238
244
239 .links {
245 .links {
240
246
241 .btn + .btn {
247 .btn + .btn {
242 margin-top: @padding;
248 margin-top: @padding;
243 }
249 }
244 }
250 }
245
251
246
252
247 .action_button {
253 .action_button {
248 display:inline;
254 display:inline;
249 margin: 0;
255 margin: 0;
250 padding: 0 1em 0 0;
256 padding: 0 1em 0 0;
251 font-size: inherit;
257 font-size: inherit;
252 color: @rcblue;
258 color: @rcblue;
253 border: none;
259 border: none;
254 .border-radius (0);
260 .border-radius (0);
255 background-color: transparent;
261 background-color: transparent;
256
262
257 &:last-child {
263 &:last-child {
258 border: none;
264 border: none;
259 }
265 }
260
266
261 &:hover {
267 &:hover {
262 color: @rcdarkblue;
268 color: @rcdarkblue;
263 background-color: transparent;
269 background-color: transparent;
264 border: none;
270 border: none;
265 }
271 }
266 }
272 }
267 .grid_delete {
273 .grid_delete {
268 .action_button {
274 .action_button {
269 border: none;
275 border: none;
270 }
276 }
271 }
277 }
272
278
273
279
274 // TODO: johbo: Form button tweaks, check if we can use the classes instead
280 // TODO: johbo: Form button tweaks, check if we can use the classes instead
275 input[type="submit"] {
281 input[type="submit"] {
276 &:extend(.btn-primary);
282 &:extend(.btn-primary);
277
283
278 &:focus {
284 &:focus {
279 outline: 0;
285 outline: 0;
280 }
286 }
281
287
282 &:hover {
288 &:hover {
283 &:extend(.btn-primary:hover);
289 &:extend(.btn-primary:hover);
284 }
290 }
285
291
286 &.btn-link {
292 &.btn-link {
287 &:extend(.btn-link);
293 &:extend(.btn-link);
288 color: @rcblue;
294 color: @rcblue;
289
295
290 &:disabled {
296 &:disabled {
291 color: @rcblue;
297 color: @rcblue;
292 background-color: transparent;
298 background-color: transparent;
293 }
299 }
294 }
300 }
295
301
296 &:disabled {
302 &:disabled {
297 .border ( @border-thickness-buttons, @rcblue );
303 .border ( @border-thickness-buttons, @rcblue );
298 background-color: @rcblue;
304 background-color: @rcblue;
299 color: white;
305 color: white;
300 }
306 }
301 }
307 }
302
308
303 input[type="reset"] {
309 input[type="reset"] {
304 &:extend(.btn-default);
310 &:extend(.btn-default);
305
311
306 // TODO: johbo: Check if this tweak can be avoided.
312 // TODO: johbo: Check if this tweak can be avoided.
307 background: transparent;
313 background: transparent;
308
314
309 &:focus {
315 &:focus {
310 outline: 0;
316 outline: 0;
311 }
317 }
312
318
313 &:hover {
319 &:hover {
314 &:extend(.btn-default:hover);
320 &:extend(.btn-default:hover);
315 }
321 }
316
322
317 &.btn-link {
323 &.btn-link {
318 &:extend(.btn-link);
324 &:extend(.btn-link);
319 color: @rcblue;
325 color: @rcblue;
320
326
321 &:disabled {
327 &:disabled {
322 border: none;
328 border: none;
323 }
329 }
324 }
330 }
325
331
326 &:disabled {
332 &:disabled {
327 .border ( @border-thickness-buttons, @rcblue );
333 .border ( @border-thickness-buttons, @rcblue );
328 background-color: white;
334 background-color: white;
329 color: @rcblue;
335 color: @rcblue;
330 }
336 }
331 }
337 }
332
338
333 input[type="submit"],
339 input[type="submit"],
334 input[type="reset"] {
340 input[type="reset"] {
335 &.btn-danger {
341 &.btn-danger {
336 &:extend(.btn-danger);
342 &:extend(.btn-danger);
337
343
338 &:focus {
344 &:focus {
339 outline: 0;
345 outline: 0;
340 }
346 }
341
347
342 &:hover {
348 &:hover {
343 &:extend(.btn-danger:hover);
349 &:extend(.btn-danger:hover);
344 }
350 }
345
351
346 &.btn-link {
352 &.btn-link {
347 &:extend(.btn-link);
353 &:extend(.btn-link);
348 color: @alert2;
354 color: @alert2;
349
355
350 &:hover {
356 &:hover {
351 color: darken(@alert2,30%);
357 color: darken(@alert2,30%);
352 }
358 }
353 }
359 }
354
360
355 &:disabled {
361 &:disabled {
356 color: @alert2;
362 color: @alert2;
357 background-color: white;
363 background-color: white;
358 }
364 }
359 }
365 }
360 &.btn-danger-action {
366 &.btn-danger-action {
361 .border ( @border-thickness, @alert2 );
367 .border ( @border-thickness, @alert2 );
362 background-color: @alert2;
368 background-color: @alert2;
363 color: white;
369 color: white;
364
370
365 a {
371 a {
366 color: white;
372 color: white;
367 }
373 }
368
374
369 &:hover {
375 &:hover {
370 background-color: darken(@alert2,20%);
376 background-color: darken(@alert2,20%);
371 }
377 }
372
378
373 &.active {
379 &.active {
374 .border ( @border-thickness, @alert2 );
380 .border ( @border-thickness, @alert2 );
375 color: white;
381 color: white;
376 background-color: @alert2;
382 background-color: @alert2;
377
383
378 a {
384 a {
379 color: white;
385 color: white;
380 }
386 }
381 }
387 }
382
388
383 &:disabled {
389 &:disabled {
384 background-color: white;
390 background-color: white;
385 color: @alert2;
391 color: @alert2;
386 }
392 }
387 }
393 }
388 }
394 }
389
395
@@ -1,1176 +1,1187 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;
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 border-width: 9px 0 9px 15.6px;
661 border-width: 9px 0 9px 15.6px;
662 border-color: transparent transparent transparent #ccc;
662 border-color: transparent transparent transparent #ccc;
663 }
663 }
664 .filediff-menu {
664 .filediff-menu {
665 display: none;
665 display: none;
666 }
666 }
667 margin: -1px 0 0 0;
667 margin: -1px 0 0 0;
668 }
668 }
669
669
670 &+ .filediff { /* file diff is expanded */
670 &+ .filediff { /* file diff is expanded */
671 .filediff-collapse-indicator {
671 .filediff-collapse-indicator {
672 border-width: 15.6px 9px 0 9px;
672 border-width: 15.6px 9px 0 9px;
673 border-color: #ccc transparent transparent transparent;
673 border-color: #ccc transparent transparent transparent;
674 }
674 }
675 .filediff-menu {
675 .filediff-menu {
676 display: block;
676 display: block;
677 }
677 }
678 margin: 20px 0;
678 margin: 20px 0;
679 &:nth-child(2) {
679 &:nth-child(2) {
680 margin: 0;
680 margin: 0;
681 }
681 }
682 }
682 }
683 }
683 }
684 .cs_files {
684 .cs_files {
685 clear: both;
685 clear: both;
686 }
686 }
687
687
688 .diffset-menu {
688 .diffset-menu {
689 margin-bottom: 20px;
689 margin-bottom: 20px;
690 }
690 }
691 .diffset {
691 .diffset {
692 margin: 20px auto;
692 margin: 20px auto;
693 .diffset-heading {
693 .diffset-heading {
694 border: 1px solid @grey5;
694 border: 1px solid @grey5;
695 margin-bottom: -1px;
695 margin-bottom: -1px;
696 // margin-top: 20px;
696 // margin-top: 20px;
697 h2 {
697 h2 {
698 margin: 0;
698 margin: 0;
699 line-height: 38px;
699 line-height: 38px;
700 padding-left: 10px;
700 padding-left: 10px;
701 }
701 }
702 .btn {
702 .btn {
703 margin: 0;
703 margin: 0;
704 }
704 }
705 background: @grey6;
705 background: @grey6;
706 display: block;
706 display: block;
707 padding: 5px;
707 padding: 5px;
708 }
708 }
709 .diffset-heading-warning {
709 .diffset-heading-warning {
710 background: @alert3-inner;
710 background: @alert3-inner;
711 border: 1px solid @alert3;
711 border: 1px solid @alert3;
712 }
712 }
713 &.diffset-comments-disabled {
713 &.diffset-comments-disabled {
714 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
714 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
715 display: none !important;
715 display: none !important;
716 }
716 }
717 }
717 }
718 }
718 }
719
719
720 .pill {
720 .pill {
721 display: block;
721 display: block;
722 float: left;
722 float: left;
723 padding: @pill-padding;
723 padding: @pill-padding;
724 }
724 }
725 .pill-group {
725 .pill-group {
726 .pill {
726 .pill {
727 opacity: .8;
727 opacity: .8;
728 &:first-child {
728 &:first-child {
729 border-radius: @border-radius 0 0 @border-radius;
729 border-radius: @border-radius 0 0 @border-radius;
730 }
730 }
731 &:last-child {
731 &:last-child {
732 border-radius: 0 @border-radius @border-radius 0;
732 border-radius: 0 @border-radius @border-radius 0;
733 }
733 }
734 &:only-child {
734 &:only-child {
735 border-radius: @border-radius;
735 border-radius: @border-radius;
736 }
736 }
737 }
737 }
738 }
738 }
739
739
740 /* Main comments*/
741 #comments {
742 .comment-selected {
743 border-left: 6px solid @comment-highlight-color;
744 padding-left: 3px;
745 margin-left: -9px;
746 }
747 }
748
740 .filediff {
749 .filediff {
741 border: 1px solid @grey5;
750 border: 1px solid @grey5;
742
751
743 /* START OVERRIDES */
752 /* START OVERRIDES */
744 .code-highlight {
753 .code-highlight {
745 border: none; // TODO: remove this border from the global
754 border: none; // TODO: remove this border from the global
746 // .code-highlight, it doesn't belong there
755 // .code-highlight, it doesn't belong there
747 }
756 }
748 label {
757 label {
749 margin: 0; // TODO: remove this margin definition from global label
758 margin: 0; // TODO: remove this margin definition from global label
750 // it doesn't belong there - if margin on labels
759 // it doesn't belong there - if margin on labels
751 // are needed for a form they should be defined
760 // are needed for a form they should be defined
752 // in the form's class
761 // in the form's class
753 }
762 }
754 /* END OVERRIDES */
763 /* END OVERRIDES */
755
764
756 * {
765 * {
757 box-sizing: border-box;
766 box-sizing: border-box;
758 }
767 }
759 .filediff-anchor {
768 .filediff-anchor {
760 visibility: hidden;
769 visibility: hidden;
761 }
770 }
762 &:hover {
771 &:hover {
763 .filediff-anchor {
772 .filediff-anchor {
764 visibility: visible;
773 visibility: visible;
765 }
774 }
766 }
775 }
767
776
768 .filediff-collapse-indicator {
777 .filediff-collapse-indicator {
769 width: 0;
778 width: 0;
770 height: 0;
779 height: 0;
771 border-style: solid;
780 border-style: solid;
772 float: left;
781 float: left;
773 margin: 2px 2px 0 0;
782 margin: 2px 2px 0 0;
774 cursor: pointer;
783 cursor: pointer;
775 }
784 }
776
785
777 .filediff-heading {
786 .filediff-heading {
778 background: @grey7;
787 background: @grey7;
779 cursor: pointer;
788 cursor: pointer;
780 display: block;
789 display: block;
781 padding: 5px 10px;
790 padding: 5px 10px;
782 }
791 }
783 .filediff-heading:after {
792 .filediff-heading:after {
784 content: "";
793 content: "";
785 display: table;
794 display: table;
786 clear: both;
795 clear: both;
787 }
796 }
788 .filediff-heading:hover {
797 .filediff-heading:hover {
789 background: #e1e9f4 !important;
798 background: #e1e9f4 !important;
790 }
799 }
791
800
792 .filediff-menu {
801 .filediff-menu {
793 float: right;
802 float: right;
794
803
795 &> a, &> span {
804 &> a, &> span {
796 padding: 5px;
805 padding: 5px;
797 display: block;
806 display: block;
798 float: left
807 float: left
799 }
808 }
800 }
809 }
801
810
802 .pill {
811 .pill {
803 &[op="name"] {
812 &[op="name"] {
804 background: none;
813 background: none;
805 color: @grey2;
814 color: @grey2;
806 opacity: 1;
815 opacity: 1;
807 color: white;
816 color: white;
808 }
817 }
809 &[op="limited"] {
818 &[op="limited"] {
810 background: @grey2;
819 background: @grey2;
811 color: white;
820 color: white;
812 }
821 }
813 &[op="binary"] {
822 &[op="binary"] {
814 background: @color7;
823 background: @color7;
815 color: white;
824 color: white;
816 }
825 }
817 &[op="modified"] {
826 &[op="modified"] {
818 background: @alert1;
827 background: @alert1;
819 color: white;
828 color: white;
820 }
829 }
821 &[op="renamed"] {
830 &[op="renamed"] {
822 background: @color4;
831 background: @color4;
823 color: white;
832 color: white;
824 }
833 }
825 &[op="mode"] {
834 &[op="mode"] {
826 background: @grey3;
835 background: @grey3;
827 color: white;
836 color: white;
828 }
837 }
829 &[op="symlink"] {
838 &[op="symlink"] {
830 background: @color8;
839 background: @color8;
831 color: white;
840 color: white;
832 }
841 }
833
842
834 &[op="added"] { /* added lines */
843 &[op="added"] { /* added lines */
835 background: @alert1;
844 background: @alert1;
836 color: white;
845 color: white;
837 }
846 }
838 &[op="deleted"] { /* deleted lines */
847 &[op="deleted"] { /* deleted lines */
839 background: @alert2;
848 background: @alert2;
840 color: white;
849 color: white;
841 }
850 }
842
851
843 &[op="created"] { /* created file */
852 &[op="created"] { /* created file */
844 background: @alert1;
853 background: @alert1;
845 color: white;
854 color: white;
846 }
855 }
847 &[op="removed"] { /* deleted file */
856 &[op="removed"] { /* deleted file */
848 background: @color5;
857 background: @color5;
849 color: white;
858 color: white;
850 }
859 }
851 }
860 }
852
861
853 .filediff-collapse-button, .filediff-expand-button {
862 .filediff-collapse-button, .filediff-expand-button {
854 cursor: pointer;
863 cursor: pointer;
855 }
864 }
856 .filediff-collapse-button {
865 .filediff-collapse-button {
857 display: inline;
866 display: inline;
858 }
867 }
859 .filediff-expand-button {
868 .filediff-expand-button {
860 display: none;
869 display: none;
861 }
870 }
862 .filediff-collapsed .filediff-collapse-button {
871 .filediff-collapsed .filediff-collapse-button {
863 display: none;
872 display: none;
864 }
873 }
865 .filediff-collapsed .filediff-expand-button {
874 .filediff-collapsed .filediff-expand-button {
866 display: inline;
875 display: inline;
867 }
876 }
868
877
869 @comment-padding: 5px;
878 @comment-padding: 5px;
870
879
871 /**** COMMENTS ****/
880 /**** COMMENTS ****/
872
881
873 .filediff-menu {
882 .filediff-menu {
874 .show-comment-button {
883 .show-comment-button {
875 display: none;
884 display: none;
876 }
885 }
877 }
886 }
878 &.hide-comments {
887 &.hide-comments {
879 .inline-comments {
888 .inline-comments {
880 display: none;
889 display: none;
881 }
890 }
882 .filediff-menu {
891 .filediff-menu {
883 .show-comment-button {
892 .show-comment-button {
884 display: inline;
893 display: inline;
885 }
894 }
886 .hide-comment-button {
895 .hide-comment-button {
887 display: none;
896 display: none;
888 }
897 }
889 }
898 }
890 }
899 }
891
900
892 .hide-line-comments {
901 .hide-line-comments {
893 .inline-comments {
902 .inline-comments {
894 display: none;
903 display: none;
895 }
904 }
896 }
905 }
906
897 .inline-comments {
907 .inline-comments {
898 border-radius: @border-radius;
908 border-radius: @border-radius;
899 background: @grey6;
909 background: @grey6;
900 .comment {
910 .comment {
901 margin: 0;
911 margin: 0;
902 border-radius: @border-radius;
912 border-radius: @border-radius;
903 }
913 }
904 .comment-outdated {
914 .comment-outdated {
905 opacity: 0.5;
915 opacity: 0.5;
906 }
916 }
917
907 .comment-inline {
918 .comment-inline {
908 background: white;
919 background: white;
909 padding: (@comment-padding + 3px) @comment-padding;
920 padding: (@comment-padding + 3px) @comment-padding;
910 border: @comment-padding solid @grey6;
921 border: @comment-padding solid @grey6;
911
922
912 .text {
923 .text {
913 border: none;
924 border: none;
914 }
925 }
915 .meta {
926 .meta {
916 border-bottom: 1px solid @grey6;
927 border-bottom: 1px solid @grey6;
917 padding-bottom: 10px;
928 padding-bottom: 10px;
918 }
929 }
919 }
930 }
920 .comment-selected {
931 .comment-selected {
921 border-left: 6px solid @comment-highlight-color;
932 border-left: 6px solid @comment-highlight-color;
922 }
933 }
923 .comment-inline-form {
934 .comment-inline-form {
924 padding: @comment-padding;
935 padding: @comment-padding;
925 display: none;
936 display: none;
926 }
937 }
927 .cb-comment-add-button {
938 .cb-comment-add-button {
928 margin: @comment-padding;
939 margin: @comment-padding;
929 }
940 }
930 /* hide add comment button when form is open */
941 /* hide add comment button when form is open */
931 .comment-inline-form-open ~ .cb-comment-add-button {
942 .comment-inline-form-open ~ .cb-comment-add-button {
932 display: none;
943 display: none;
933 }
944 }
934 .comment-inline-form-open {
945 .comment-inline-form-open {
935 display: block;
946 display: block;
936 }
947 }
937 /* hide add comment button when form but no comments */
948 /* hide add comment button when form but no comments */
938 .comment-inline-form:first-child + .cb-comment-add-button {
949 .comment-inline-form:first-child + .cb-comment-add-button {
939 display: none;
950 display: none;
940 }
951 }
941 /* hide add comment button when no comments or form */
952 /* hide add comment button when no comments or form */
942 .cb-comment-add-button:first-child {
953 .cb-comment-add-button:first-child {
943 display: none;
954 display: none;
944 }
955 }
945 /* hide add comment button when only comment is being deleted */
956 /* hide add comment button when only comment is being deleted */
946 .comment-deleting:first-child + .cb-comment-add-button {
957 .comment-deleting:first-child + .cb-comment-add-button {
947 display: none;
958 display: none;
948 }
959 }
949 }
960 }
950 /**** END COMMENTS ****/
961 /**** END COMMENTS ****/
951
962
952 }
963 }
953
964
954
965
955 table.cb {
966 table.cb {
956 width: 100%;
967 width: 100%;
957 border-collapse: collapse;
968 border-collapse: collapse;
958
969
959 .cb-text {
970 .cb-text {
960 padding: @cb-text-padding;
971 padding: @cb-text-padding;
961 }
972 }
962 .cb-hunk {
973 .cb-hunk {
963 padding: @cb-text-padding;
974 padding: @cb-text-padding;
964 }
975 }
965 .cb-expand {
976 .cb-expand {
966 display: none;
977 display: none;
967 }
978 }
968 .cb-collapse {
979 .cb-collapse {
969 display: inline;
980 display: inline;
970 }
981 }
971 &.cb-collapsed {
982 &.cb-collapsed {
972 .cb-line {
983 .cb-line {
973 display: none;
984 display: none;
974 }
985 }
975 .cb-expand {
986 .cb-expand {
976 display: inline;
987 display: inline;
977 }
988 }
978 .cb-collapse {
989 .cb-collapse {
979 display: none;
990 display: none;
980 }
991 }
981 }
992 }
982
993
983 /* intentionally general selector since .cb-line-selected must override it
994 /* intentionally general selector since .cb-line-selected must override it
984 and they both use !important since the td itself may have a random color
995 and they both use !important since the td itself may have a random color
985 generated by annotation blocks. TLDR: if you change it, make sure
996 generated by annotation blocks. TLDR: if you change it, make sure
986 annotated block selection and line selection in file view still work */
997 annotated block selection and line selection in file view still work */
987 .cb-line-fresh .cb-content {
998 .cb-line-fresh .cb-content {
988 background: white !important;
999 background: white !important;
989 }
1000 }
990 .cb-warning {
1001 .cb-warning {
991 background: #fff4dd;
1002 background: #fff4dd;
992 }
1003 }
993
1004
994 &.cb-diff-sideside {
1005 &.cb-diff-sideside {
995 td {
1006 td {
996 &.cb-content {
1007 &.cb-content {
997 width: 50%;
1008 width: 50%;
998 }
1009 }
999 }
1010 }
1000 }
1011 }
1001
1012
1002 tr {
1013 tr {
1003 &.cb-annotate {
1014 &.cb-annotate {
1004 border-top: 1px solid #eee;
1015 border-top: 1px solid #eee;
1005
1016
1006 &+ .cb-line {
1017 &+ .cb-line {
1007 border-top: 1px solid #eee;
1018 border-top: 1px solid #eee;
1008 }
1019 }
1009
1020
1010 &:first-child {
1021 &:first-child {
1011 border-top: none;
1022 border-top: none;
1012 &+ .cb-line {
1023 &+ .cb-line {
1013 border-top: none;
1024 border-top: none;
1014 }
1025 }
1015 }
1026 }
1016 }
1027 }
1017
1028
1018 &.cb-hunk {
1029 &.cb-hunk {
1019 font-family: @font-family-monospace;
1030 font-family: @font-family-monospace;
1020 color: rgba(0, 0, 0, 0.3);
1031 color: rgba(0, 0, 0, 0.3);
1021
1032
1022 td {
1033 td {
1023 &:first-child {
1034 &:first-child {
1024 background: #edf2f9;
1035 background: #edf2f9;
1025 }
1036 }
1026 &:last-child {
1037 &:last-child {
1027 background: #f4f7fb;
1038 background: #f4f7fb;
1028 }
1039 }
1029 }
1040 }
1030 }
1041 }
1031 }
1042 }
1032
1043
1033
1044
1034 td {
1045 td {
1035 vertical-align: top;
1046 vertical-align: top;
1036 padding: 0;
1047 padding: 0;
1037
1048
1038 &.cb-content {
1049 &.cb-content {
1039 font-size: 12.35px;
1050 font-size: 12.35px;
1040
1051
1041 &.cb-line-selected .cb-code {
1052 &.cb-line-selected .cb-code {
1042 background: @comment-highlight-color !important;
1053 background: @comment-highlight-color !important;
1043 }
1054 }
1044
1055
1045 span.cb-code {
1056 span.cb-code {
1046 line-height: @cb-line-height;
1057 line-height: @cb-line-height;
1047 padding-left: @cb-line-code-padding;
1058 padding-left: @cb-line-code-padding;
1048 padding-right: @cb-line-code-padding;
1059 padding-right: @cb-line-code-padding;
1049 display: block;
1060 display: block;
1050 white-space: pre-wrap;
1061 white-space: pre-wrap;
1051 font-family: @font-family-monospace;
1062 font-family: @font-family-monospace;
1052 word-break: break-word;
1063 word-break: break-word;
1053 .nonl {
1064 .nonl {
1054 color: @color5;
1065 color: @color5;
1055 }
1066 }
1056 }
1067 }
1057
1068
1058 &> button.cb-comment-box-opener {
1069 &> button.cb-comment-box-opener {
1059
1070
1060 padding: 2px 5px 1px 5px;
1071 padding: 2px 5px 1px 5px;
1061 margin-left: 0px;
1072 margin-left: 0px;
1062 margin-top: -1px;
1073 margin-top: -1px;
1063
1074
1064 border-radius: @border-radius;
1075 border-radius: @border-radius;
1065 position: absolute;
1076 position: absolute;
1066 display: none;
1077 display: none;
1067 }
1078 }
1068 .cb-comment {
1079 .cb-comment {
1069 margin-top: 10px;
1080 margin-top: 10px;
1070 white-space: normal;
1081 white-space: normal;
1071 }
1082 }
1072 }
1083 }
1073 &:hover {
1084 &:hover {
1074 button.cb-comment-box-opener {
1085 button.cb-comment-box-opener {
1075 display: block;
1086 display: block;
1076 }
1087 }
1077 &+ td button.cb-comment-box-opener {
1088 &+ td button.cb-comment-box-opener {
1078 display: block
1089 display: block
1079 }
1090 }
1080 }
1091 }
1081
1092
1082 &.cb-data {
1093 &.cb-data {
1083 text-align: right;
1094 text-align: right;
1084 width: 30px;
1095 width: 30px;
1085 font-family: @font-family-monospace;
1096 font-family: @font-family-monospace;
1086
1097
1087 .icon-comment {
1098 .icon-comment {
1088 cursor: pointer;
1099 cursor: pointer;
1089 }
1100 }
1090 &.cb-line-selected > div {
1101 &.cb-line-selected > div {
1091 display: block;
1102 display: block;
1092 background: @comment-highlight-color !important;
1103 background: @comment-highlight-color !important;
1093 line-height: @cb-line-height;
1104 line-height: @cb-line-height;
1094 color: rgba(0, 0, 0, 0.3);
1105 color: rgba(0, 0, 0, 0.3);
1095 }
1106 }
1096 }
1107 }
1097
1108
1098 &.cb-lineno {
1109 &.cb-lineno {
1099 padding: 0;
1110 padding: 0;
1100 width: 50px;
1111 width: 50px;
1101 color: rgba(0, 0, 0, 0.3);
1112 color: rgba(0, 0, 0, 0.3);
1102 text-align: right;
1113 text-align: right;
1103 border-right: 1px solid #eee;
1114 border-right: 1px solid #eee;
1104 font-family: @font-family-monospace;
1115 font-family: @font-family-monospace;
1105
1116
1106 a::before {
1117 a::before {
1107 content: attr(data-line-no);
1118 content: attr(data-line-no);
1108 }
1119 }
1109 &.cb-line-selected a {
1120 &.cb-line-selected a {
1110 background: @comment-highlight-color !important;
1121 background: @comment-highlight-color !important;
1111 }
1122 }
1112
1123
1113 a {
1124 a {
1114 display: block;
1125 display: block;
1115 padding-right: @cb-line-code-padding;
1126 padding-right: @cb-line-code-padding;
1116 padding-left: @cb-line-code-padding;
1127 padding-left: @cb-line-code-padding;
1117 line-height: @cb-line-height;
1128 line-height: @cb-line-height;
1118 color: rgba(0, 0, 0, 0.3);
1129 color: rgba(0, 0, 0, 0.3);
1119 }
1130 }
1120 }
1131 }
1121
1132
1122 &.cb-empty {
1133 &.cb-empty {
1123 background: @grey7;
1134 background: @grey7;
1124 }
1135 }
1125
1136
1126 ins {
1137 ins {
1127 color: black;
1138 color: black;
1128 background: #a6f3a6;
1139 background: #a6f3a6;
1129 text-decoration: none;
1140 text-decoration: none;
1130 }
1141 }
1131 del {
1142 del {
1132 color: black;
1143 color: black;
1133 background: #f8cbcb;
1144 background: #f8cbcb;
1134 text-decoration: none;
1145 text-decoration: none;
1135 }
1146 }
1136 &.cb-addition {
1147 &.cb-addition {
1137 background: #ecffec;
1148 background: #ecffec;
1138
1149
1139 &.blob-lineno {
1150 &.blob-lineno {
1140 background: #ddffdd;
1151 background: #ddffdd;
1141 }
1152 }
1142 }
1153 }
1143 &.cb-deletion {
1154 &.cb-deletion {
1144 background: #ffecec;
1155 background: #ffecec;
1145
1156
1146 &.blob-lineno {
1157 &.blob-lineno {
1147 background: #ffdddd;
1158 background: #ffdddd;
1148 }
1159 }
1149 }
1160 }
1150
1161
1151 &.cb-annotate-info {
1162 &.cb-annotate-info {
1152 width: 320px;
1163 width: 320px;
1153 min-width: 320px;
1164 min-width: 320px;
1154 max-width: 320px;
1165 max-width: 320px;
1155 padding: 5px 2px;
1166 padding: 5px 2px;
1156 font-size: 13px;
1167 font-size: 13px;
1157
1168
1158 strong.cb-annotate-message {
1169 strong.cb-annotate-message {
1159 padding: 5px 0;
1170 padding: 5px 0;
1160 white-space: pre-line;
1171 white-space: pre-line;
1161 display: inline-block;
1172 display: inline-block;
1162 }
1173 }
1163 .rc-user {
1174 .rc-user {
1164 float: none;
1175 float: none;
1165 padding: 0 6px 0 17px;
1176 padding: 0 6px 0 17px;
1166 min-width: auto;
1177 min-width: auto;
1167 min-height: auto;
1178 min-height: auto;
1168 }
1179 }
1169 }
1180 }
1170
1181
1171 &.cb-annotate-revision {
1182 &.cb-annotate-revision {
1172 cursor: pointer;
1183 cursor: pointer;
1173 text-align: right;
1184 text-align: right;
1174 }
1185 }
1175 }
1186 }
1176 }
1187 }
@@ -1,361 +1,345 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 {
50 .comment {
51 margin: @padding 0;
51 margin: @padding 0;
52 padding: 4px 0 0 0;
52 padding: 4px 0 0 0;
53 line-height: 1em;
53 line-height: 1em;
54
54
55 .rc-user {
55 .rc-user {
56 min-width: 0;
56 min-width: 0;
57 margin: -2px .5em 0 0;
57 margin: -2px .5em 0 0;
58 }
58 }
59
59
60 .meta {
60 .meta {
61 position: relative;
61 position: relative;
62 width: 100%;
62 width: 100%;
63 margin: 0 0 .5em 0;
63 margin: 0 0 .5em 0;
64
64
65 &:hover .permalink {
65 &:hover .permalink {
66 visibility: visible;
66 visibility: visible;
67 color: @rcblue;
67 color: @rcblue;
68 }
68 }
69 }
69 }
70
70
71 .author,
71 .author,
72 .date {
72 .date {
73 display: inline;
73 display: inline;
74 margin: 0 .5 0 0;
74 margin: 0 .5 0 0;
75 padding: 0 .5 0 0;
75 padding: 0 .5 0 0;
76
76
77 &:after {
77 &:after {
78 content: ' | ';
78 content: ' | ';
79 color: @grey5;
79 color: @grey5;
80 }
80 }
81 }
81 }
82
82
83 .status-change,
83 .status-change,
84 .permalink,
84 .permalink,
85 .changeset-status-lbl {
85 .changeset-status-lbl {
86 display: inline;
86 display: inline;
87 }
87 }
88
88
89 .permalink {
89 .permalink {
90 visibility: hidden;
90 visibility: hidden;
91 }
91 }
92
92
93 .comment-links-divider {
93 .comment-links-divider {
94 display: inline;
94 display: inline;
95 }
95 }
96
96
97 .comment-links-block {
97 .comment-links-block {
98 float:right;
98 float:right;
99 text-align: right;
99 text-align: right;
100 min-width: 85px;
100 min-width: 85px;
101
101
102 [class^="icon-"]:before,
102 [class^="icon-"]:before,
103 [class*=" icon-"]:before {
103 [class*=" icon-"]:before {
104 margin-left: 0;
104 margin-left: 0;
105 margin-right: 0;
105 margin-right: 0;
106 }
106 }
107 }
107 }
108
108
109 .comment-previous-link {
109 .comment-previous-link {
110 display: inline-block;
110 display: inline-block;
111
111
112 .arrow_comment_link{
112 .arrow_comment_link{
113 cursor: pointer;
113 cursor: pointer;
114 i {
114 i {
115 font-size:10px;
115 font-size:10px;
116 }
116 }
117 }
117 }
118 .arrow_comment_link.disabled {
118 .arrow_comment_link.disabled {
119 cursor: default;
119 cursor: default;
120 color: @grey5;
120 color: @grey5;
121 }
121 }
122 }
122 }
123
123
124 .comment-next-link {
124 .comment-next-link {
125 display: inline-block;
125 display: inline-block;
126
126
127 .arrow_comment_link{
127 .arrow_comment_link{
128 cursor: pointer;
128 cursor: pointer;
129 i {
129 i {
130 font-size:10px;
130 font-size:10px;
131 }
131 }
132 }
132 }
133 .arrow_comment_link.disabled {
133 .arrow_comment_link.disabled {
134 cursor: default;
134 cursor: default;
135 color: @grey5;
135 color: @grey5;
136 }
136 }
137 }
137 }
138
138
139 .flag_status {
139 .flag_status {
140 display: inline-block;
140 display: inline-block;
141 margin: -2px .5em 0 .25em
141 margin: -2px .5em 0 .25em
142 }
142 }
143
143
144 .delete-comment {
144 .delete-comment {
145 display: inline-block;
145 display: inline-block;
146 color: @rcblue;
146 color: @rcblue;
147
147
148 &:hover {
148 &:hover {
149 cursor: pointer;
149 cursor: pointer;
150 }
150 }
151 }
151 }
152
152
153
153
154 .text {
154 .text {
155 clear: both;
155 clear: both;
156 border: @border-thickness solid @grey5;
156 border: @border-thickness solid @grey5;
157 .border-radius(@border-radius);
157 .border-radius(@border-radius);
158 .box-sizing(border-box);
158 .box-sizing(border-box);
159
159
160 .markdown-block p,
160 .markdown-block p,
161 .rst-block p {
161 .rst-block p {
162 margin: .5em 0 !important;
162 margin: .5em 0 !important;
163 // TODO: lisa: This is needed because of other rst !important rules :[
163 // TODO: lisa: This is needed because of other rst !important rules :[
164 }
164 }
165 }
165 }
166 }
166 }
167
167
168 .show-outdated-comments {
168 .show-outdated-comments {
169 display: inline;
169 display: inline;
170 color: @rcblue;
170 color: @rcblue;
171 }
171 }
172
172
173 .outdated {
174 display: none;
175 opacity: 0.6;
176
177 .comment {
178 margin: 0 0 @padding;
179
180 .date:after {
181 content: none;
182 }
183 }
184 .outdated_comment_block {
185 padding: 0 0 @space 0;
186 }
187 }
188
189 // Comment Form
173 // Comment Form
190 div.comment-form {
174 div.comment-form {
191 margin-top: 20px;
175 margin-top: 20px;
192 }
176 }
193
177
194 .comment-form strong {
178 .comment-form strong {
195 display: block;
179 display: block;
196 margin-bottom: 15px;
180 margin-bottom: 15px;
197 }
181 }
198
182
199 .comment-form textarea {
183 .comment-form textarea {
200 width: 100%;
184 width: 100%;
201 height: 100px;
185 height: 100px;
202 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
186 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
203 }
187 }
204
188
205 form.comment-form {
189 form.comment-form {
206 margin-top: 10px;
190 margin-top: 10px;
207 margin-left: 10px;
191 margin-left: 10px;
208 }
192 }
209
193
210 .comment-inline-form .comment-block-ta,
194 .comment-inline-form .comment-block-ta,
211 .comment-form .comment-block-ta,
195 .comment-form .comment-block-ta,
212 .comment-form .preview-box {
196 .comment-form .preview-box {
213 border: @border-thickness solid @grey5;
197 border: @border-thickness solid @grey5;
214 .border-radius(@border-radius);
198 .border-radius(@border-radius);
215 .box-sizing(border-box);
199 .box-sizing(border-box);
216 background-color: white;
200 background-color: white;
217 }
201 }
218
202
219 .comment-form-submit {
203 .comment-form-submit {
220 margin-top: 5px;
204 margin-top: 5px;
221 margin-left: 525px;
205 margin-left: 525px;
222 }
206 }
223
207
224 .file-comments {
208 .file-comments {
225 display: none;
209 display: none;
226 }
210 }
227
211
228 .comment-form .preview-box.unloaded,
212 .comment-form .preview-box.unloaded,
229 .comment-inline-form .preview-box.unloaded {
213 .comment-inline-form .preview-box.unloaded {
230 height: 50px;
214 height: 50px;
231 text-align: center;
215 text-align: center;
232 padding: 20px;
216 padding: 20px;
233 background-color: white;
217 background-color: white;
234 }
218 }
235
219
236 .comment-footer {
220 .comment-footer {
237 position: relative;
221 position: relative;
238 width: 100%;
222 width: 100%;
239 min-height: 42px;
223 min-height: 42px;
240
224
241 .status_box,
225 .status_box,
242 .cancel-button {
226 .cancel-button {
243 float: left;
227 float: left;
244 display: inline-block;
228 display: inline-block;
245 }
229 }
246
230
247 .action-buttons {
231 .action-buttons {
248 float: right;
232 float: right;
249 display: inline-block;
233 display: inline-block;
250 }
234 }
251 }
235 }
252
236
253 .comment-form {
237 .comment-form {
254
238
255 .comment {
239 .comment {
256 margin-left: 10px;
240 margin-left: 10px;
257 }
241 }
258
242
259 .comment-help {
243 .comment-help {
260 color: @grey4;
244 color: @grey4;
261 padding: 5px 0 5px 0;
245 padding: 5px 0 5px 0;
262 }
246 }
263
247
264 .comment-title {
248 .comment-title {
265 padding: 5px 0 5px 0;
249 padding: 5px 0 5px 0;
266 }
250 }
267
251
268 .comment-button {
252 .comment-button {
269 display: inline-block;
253 display: inline-block;
270 }
254 }
271
255
272 .comment-button .comment-button-input {
256 .comment-button .comment-button-input {
273 margin-right: 0;
257 margin-right: 0;
274 }
258 }
275
259
276 .comment-footer {
260 .comment-footer {
277 margin-bottom: 110px;
261 margin-bottom: 110px;
278 }
262 }
279 }
263 }
280
264
281
265
282 .comment-form-login {
266 .comment-form-login {
283 .comment-help {
267 .comment-help {
284 padding: 0.9em; //same as the button
268 padding: 0.9em; //same as the button
285 }
269 }
286
270
287 div.clearfix {
271 div.clearfix {
288 clear: both;
272 clear: both;
289 width: 100%;
273 width: 100%;
290 display: block;
274 display: block;
291 }
275 }
292 }
276 }
293
277
294 .preview-box {
278 .preview-box {
295 padding: 10px;
279 padding: 10px;
296 min-height: 100px;
280 min-height: 100px;
297 margin-bottom: 15px;
281 margin-bottom: 15px;
298 background-color: white;
282 background-color: white;
299 border: @border-thickness solid #ccc;
283 border: @border-thickness solid #ccc;
300 .border-radius(@border-radius);
284 .border-radius(@border-radius);
301 .box-sizing(border-box);
285 .box-sizing(border-box);
302 }
286 }
303
287
304 .add-another-button {
288 .add-another-button {
305 margin-left: 10px;
289 margin-left: 10px;
306 margin-top: 10px;
290 margin-top: 10px;
307 margin-bottom: 10px;
291 margin-bottom: 10px;
308 }
292 }
309
293
310 .comment .buttons {
294 .comment .buttons {
311 float: right;
295 float: right;
312 margin: -1px 0px 0px 0px;
296 margin: -1px 0px 0px 0px;
313 }
297 }
314
298
315 // Inline Comment Form
299 // Inline Comment Form
316 .injected_diff .comment-inline-form,
300 .injected_diff .comment-inline-form,
317 .comment-inline-form {
301 .comment-inline-form {
318 background-color: @grey6;
302 background-color: @grey6;
319 margin-top: 10px;
303 margin-top: 10px;
320 margin-bottom: 20px;
304 margin-bottom: 20px;
321 }
305 }
322
306
323 .inline-form {
307 .inline-form {
324 padding: 10px 7px;
308 padding: 10px 7px;
325 }
309 }
326
310
327 .inline-form div {
311 .inline-form div {
328 max-width: 100%;
312 max-width: 100%;
329 }
313 }
330
314
331 .overlay {
315 .overlay {
332 display: none;
316 display: none;
333 position: absolute;
317 position: absolute;
334 width: 100%;
318 width: 100%;
335 text-align: center;
319 text-align: center;
336 vertical-align: middle;
320 vertical-align: middle;
337 font-size: 16px;
321 font-size: 16px;
338 background: none repeat scroll 0 0 white;
322 background: none repeat scroll 0 0 white;
339
323
340 &.submitting {
324 &.submitting {
341 display: block;
325 display: block;
342 opacity: 0.5;
326 opacity: 0.5;
343 z-index: 100;
327 z-index: 100;
344 }
328 }
345 }
329 }
346 .comment-inline-form .overlay.submitting .overlay-text {
330 .comment-inline-form .overlay.submitting .overlay-text {
347 margin-top: 5%;
331 margin-top: 5%;
348 }
332 }
349
333
350 .comment-inline-form .clearfix,
334 .comment-inline-form .clearfix,
351 .comment-form .clearfix {
335 .comment-form .clearfix {
352 .border-radius(@border-radius);
336 .border-radius(@border-radius);
353 margin: 0px;
337 margin: 0px;
354 }
338 }
355
339
356 .hide-inline-form-button {
340 .hide-inline-form-button {
357 margin-left: 5px;
341 margin-left: 5px;
358 }
342 }
359 .comment-button .hide-inline-form {
343 .comment-button .hide-inline-form {
360 background: white;
344 background: white;
361 }
345 }
@@ -1,2204 +1,2202 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 .wrapper {
112 .wrapper {
113 position: relative;
113 position: relative;
114 max-width: @wrapper-maxwidth;
114 max-width: @wrapper-maxwidth;
115 margin: 0 auto;
115 margin: 0 auto;
116 }
116 }
117
117
118 #content {
118 #content {
119 clear: both;
119 clear: both;
120 padding: 0 @contentpadding;
120 padding: 0 @contentpadding;
121 }
121 }
122
122
123 .advanced-settings-fields{
123 .advanced-settings-fields{
124 input{
124 input{
125 margin-left: @textmargin;
125 margin-left: @textmargin;
126 margin-right: @padding/2;
126 margin-right: @padding/2;
127 }
127 }
128 }
128 }
129
129
130 .cs_files_title {
130 .cs_files_title {
131 margin: @pagepadding 0 0;
131 margin: @pagepadding 0 0;
132 }
132 }
133
133
134 input.inline[type="file"] {
134 input.inline[type="file"] {
135 display: inline;
135 display: inline;
136 }
136 }
137
137
138 .error_page {
138 .error_page {
139 margin: 10% auto;
139 margin: 10% auto;
140
140
141 h1 {
141 h1 {
142 color: @grey2;
142 color: @grey2;
143 }
143 }
144
144
145 .alert {
145 .alert {
146 margin: @padding 0;
146 margin: @padding 0;
147 }
147 }
148
148
149 .error-branding {
149 .error-branding {
150 font-family: @text-semibold;
150 font-family: @text-semibold;
151 color: @grey4;
151 color: @grey4;
152 }
152 }
153
153
154 .error_message {
154 .error_message {
155 font-family: @text-regular;
155 font-family: @text-regular;
156 }
156 }
157
157
158 .sidebar {
158 .sidebar {
159 min-height: 275px;
159 min-height: 275px;
160 margin: 0;
160 margin: 0;
161 padding: 0 0 @sidebarpadding @sidebarpadding;
161 padding: 0 0 @sidebarpadding @sidebarpadding;
162 border: none;
162 border: none;
163 }
163 }
164
164
165 .main-content {
165 .main-content {
166 position: relative;
166 position: relative;
167 margin: 0 @sidebarpadding @sidebarpadding;
167 margin: 0 @sidebarpadding @sidebarpadding;
168 padding: 0 0 0 @sidebarpadding;
168 padding: 0 0 0 @sidebarpadding;
169 border-left: @border-thickness solid @grey5;
169 border-left: @border-thickness solid @grey5;
170
170
171 @media (max-width:767px) {
171 @media (max-width:767px) {
172 clear: both;
172 clear: both;
173 width: 100%;
173 width: 100%;
174 margin: 0;
174 margin: 0;
175 border: none;
175 border: none;
176 }
176 }
177 }
177 }
178
178
179 .inner-column {
179 .inner-column {
180 float: left;
180 float: left;
181 width: 29.75%;
181 width: 29.75%;
182 min-height: 150px;
182 min-height: 150px;
183 margin: @sidebarpadding 2% 0 0;
183 margin: @sidebarpadding 2% 0 0;
184 padding: 0 2% 0 0;
184 padding: 0 2% 0 0;
185 border-right: @border-thickness solid @grey5;
185 border-right: @border-thickness solid @grey5;
186
186
187 @media (max-width:767px) {
187 @media (max-width:767px) {
188 clear: both;
188 clear: both;
189 width: 100%;
189 width: 100%;
190 border: none;
190 border: none;
191 }
191 }
192
192
193 ul {
193 ul {
194 padding-left: 1.25em;
194 padding-left: 1.25em;
195 }
195 }
196
196
197 &:last-child {
197 &:last-child {
198 margin: @sidebarpadding 0 0;
198 margin: @sidebarpadding 0 0;
199 border: none;
199 border: none;
200 }
200 }
201
201
202 h4 {
202 h4 {
203 margin: 0 0 @padding;
203 margin: 0 0 @padding;
204 font-family: @text-semibold;
204 font-family: @text-semibold;
205 }
205 }
206 }
206 }
207 }
207 }
208 .error-page-logo {
208 .error-page-logo {
209 width: 130px;
209 width: 130px;
210 height: 160px;
210 height: 160px;
211 }
211 }
212
212
213 // HEADER
213 // HEADER
214 .header {
214 .header {
215
215
216 // TODO: johbo: Fix login pages, so that they work without a min-height
216 // TODO: johbo: Fix login pages, so that they work without a min-height
217 // for the header and then remove the min-height. I chose a smaller value
217 // for the header and then remove the min-height. I chose a smaller value
218 // intentionally here to avoid rendering issues in the main navigation.
218 // intentionally here to avoid rendering issues in the main navigation.
219 min-height: 49px;
219 min-height: 49px;
220
220
221 position: relative;
221 position: relative;
222 vertical-align: bottom;
222 vertical-align: bottom;
223 padding: 0 @header-padding;
223 padding: 0 @header-padding;
224 background-color: @grey2;
224 background-color: @grey2;
225 color: @grey5;
225 color: @grey5;
226
226
227 .title {
227 .title {
228 overflow: visible;
228 overflow: visible;
229 }
229 }
230
230
231 &:before,
231 &:before,
232 &:after {
232 &:after {
233 content: "";
233 content: "";
234 clear: both;
234 clear: both;
235 width: 100%;
235 width: 100%;
236 }
236 }
237
237
238 // TODO: johbo: Avoids breaking "Repositories" chooser
238 // TODO: johbo: Avoids breaking "Repositories" chooser
239 .select2-container .select2-choice .select2-arrow {
239 .select2-container .select2-choice .select2-arrow {
240 display: none;
240 display: none;
241 }
241 }
242 }
242 }
243
243
244 #header-inner {
244 #header-inner {
245 &.title {
245 &.title {
246 margin: 0;
246 margin: 0;
247 }
247 }
248 &:before,
248 &:before,
249 &:after {
249 &:after {
250 content: "";
250 content: "";
251 clear: both;
251 clear: both;
252 }
252 }
253 }
253 }
254
254
255 // Gists
255 // Gists
256 #files_data {
256 #files_data {
257 clear: both; //for firefox
257 clear: both; //for firefox
258 }
258 }
259 #gistid {
259 #gistid {
260 margin-right: @padding;
260 margin-right: @padding;
261 }
261 }
262
262
263 // Global Settings Editor
263 // Global Settings Editor
264 .textarea.editor {
264 .textarea.editor {
265 float: left;
265 float: left;
266 position: relative;
266 position: relative;
267 max-width: @texteditor-width;
267 max-width: @texteditor-width;
268
268
269 select {
269 select {
270 position: absolute;
270 position: absolute;
271 top:10px;
271 top:10px;
272 right:0;
272 right:0;
273 }
273 }
274
274
275 .CodeMirror {
275 .CodeMirror {
276 margin: 0;
276 margin: 0;
277 }
277 }
278
278
279 .help-block {
279 .help-block {
280 margin: 0 0 @padding;
280 margin: 0 0 @padding;
281 padding:.5em;
281 padding:.5em;
282 background-color: @grey6;
282 background-color: @grey6;
283 }
283 }
284 }
284 }
285
285
286 ul.auth_plugins {
286 ul.auth_plugins {
287 margin: @padding 0 @padding @legend-width;
287 margin: @padding 0 @padding @legend-width;
288 padding: 0;
288 padding: 0;
289
289
290 li {
290 li {
291 margin-bottom: @padding;
291 margin-bottom: @padding;
292 line-height: 1em;
292 line-height: 1em;
293 list-style-type: none;
293 list-style-type: none;
294
294
295 .auth_buttons .btn {
295 .auth_buttons .btn {
296 margin-right: @padding;
296 margin-right: @padding;
297 }
297 }
298
298
299 &:before { content: none; }
299 &:before { content: none; }
300 }
300 }
301 }
301 }
302
302
303
303
304 // My Account PR list
304 // My Account PR list
305
305
306 #show_closed {
306 #show_closed {
307 margin: 0 1em 0 0;
307 margin: 0 1em 0 0;
308 }
308 }
309
309
310 .pullrequestlist {
310 .pullrequestlist {
311 .closed {
311 .closed {
312 background-color: @grey6;
312 background-color: @grey6;
313 }
313 }
314 .td-status {
314 .td-status {
315 padding-left: .5em;
315 padding-left: .5em;
316 }
316 }
317 .log-container .truncate {
317 .log-container .truncate {
318 height: 2.75em;
318 height: 2.75em;
319 white-space: pre-line;
319 white-space: pre-line;
320 }
320 }
321 table.rctable .user {
321 table.rctable .user {
322 padding-left: 0;
322 padding-left: 0;
323 }
323 }
324 table.rctable {
324 table.rctable {
325 td.td-description,
325 td.td-description,
326 .rc-user {
326 .rc-user {
327 min-width: auto;
327 min-width: auto;
328 }
328 }
329 }
329 }
330 }
330 }
331
331
332 // Pull Requests
332 // Pull Requests
333
333
334 .pullrequests_section_head {
334 .pullrequests_section_head {
335 display: block;
335 display: block;
336 clear: both;
336 clear: both;
337 margin: @padding 0;
337 margin: @padding 0;
338 font-family: @text-bold;
338 font-family: @text-bold;
339 }
339 }
340
340
341 .pr-origininfo, .pr-targetinfo {
341 .pr-origininfo, .pr-targetinfo {
342 position: relative;
342 position: relative;
343
343
344 .tag {
344 .tag {
345 display: inline-block;
345 display: inline-block;
346 margin: 0 1em .5em 0;
346 margin: 0 1em .5em 0;
347 }
347 }
348
348
349 .clone-url {
349 .clone-url {
350 display: inline-block;
350 display: inline-block;
351 margin: 0 0 .5em 0;
351 margin: 0 0 .5em 0;
352 padding: 0;
352 padding: 0;
353 line-height: 1.2em;
353 line-height: 1.2em;
354 }
354 }
355 }
355 }
356
356
357 .pr-pullinfo {
357 .pr-pullinfo {
358 clear: both;
358 clear: both;
359 margin: .5em 0;
359 margin: .5em 0;
360 }
360 }
361
361
362 #pr-title-input {
362 #pr-title-input {
363 width: 72%;
363 width: 72%;
364 font-size: 1em;
364 font-size: 1em;
365 font-family: @text-bold;
365 font-family: @text-bold;
366 margin: 0;
366 margin: 0;
367 padding: 0 0 0 @padding/4;
367 padding: 0 0 0 @padding/4;
368 line-height: 1.7em;
368 line-height: 1.7em;
369 color: @text-color;
369 color: @text-color;
370 letter-spacing: .02em;
370 letter-spacing: .02em;
371 }
371 }
372
372
373 #pullrequest_title {
373 #pullrequest_title {
374 width: 100%;
374 width: 100%;
375 box-sizing: border-box;
375 box-sizing: border-box;
376 }
376 }
377
377
378 #pr_open_message {
378 #pr_open_message {
379 border: @border-thickness solid #fff;
379 border: @border-thickness solid #fff;
380 border-radius: @border-radius;
380 border-radius: @border-radius;
381 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
381 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
382 text-align: right;
382 text-align: right;
383 overflow: hidden;
383 overflow: hidden;
384 }
384 }
385
385
386 .pr-submit-button {
386 .pr-submit-button {
387 float: right;
387 float: right;
388 margin: 0 0 0 5px;
388 margin: 0 0 0 5px;
389 }
389 }
390
390
391 .pr-spacing-container {
391 .pr-spacing-container {
392 padding: 20px;
392 padding: 20px;
393 clear: both
393 clear: both
394 }
394 }
395
395
396 #pr-description-input {
396 #pr-description-input {
397 margin-bottom: 0;
397 margin-bottom: 0;
398 }
398 }
399
399
400 .pr-description-label {
400 .pr-description-label {
401 vertical-align: top;
401 vertical-align: top;
402 }
402 }
403
403
404 .perms_section_head {
404 .perms_section_head {
405 min-width: 625px;
405 min-width: 625px;
406
406
407 h2 {
407 h2 {
408 margin-bottom: 0;
408 margin-bottom: 0;
409 }
409 }
410
410
411 .label-checkbox {
411 .label-checkbox {
412 float: left;
412 float: left;
413 }
413 }
414
414
415 &.field {
415 &.field {
416 margin: @space 0 @padding;
416 margin: @space 0 @padding;
417 }
417 }
418
418
419 &:first-child.field {
419 &:first-child.field {
420 margin-top: 0;
420 margin-top: 0;
421
421
422 .label {
422 .label {
423 margin-top: 0;
423 margin-top: 0;
424 padding-top: 0;
424 padding-top: 0;
425 }
425 }
426
426
427 .radios {
427 .radios {
428 padding-top: 0;
428 padding-top: 0;
429 }
429 }
430 }
430 }
431
431
432 .radios {
432 .radios {
433 float: right;
433 float: right;
434 position: relative;
434 position: relative;
435 width: 405px;
435 width: 405px;
436 }
436 }
437 }
437 }
438
438
439 //--- MODULES ------------------//
439 //--- MODULES ------------------//
440
440
441
441
442 // Server Announcement
442 // Server Announcement
443 #server-announcement {
443 #server-announcement {
444 width: 95%;
444 width: 95%;
445 margin: @padding auto;
445 margin: @padding auto;
446 padding: @padding;
446 padding: @padding;
447 border-width: 2px;
447 border-width: 2px;
448 border-style: solid;
448 border-style: solid;
449 .border-radius(2px);
449 .border-radius(2px);
450 font-family: @text-bold;
450 font-family: @text-bold;
451
451
452 &.info { border-color: @alert4; background-color: @alert4-inner; }
452 &.info { border-color: @alert4; background-color: @alert4-inner; }
453 &.warning { border-color: @alert3; background-color: @alert3-inner; }
453 &.warning { border-color: @alert3; background-color: @alert3-inner; }
454 &.error { border-color: @alert2; background-color: @alert2-inner; }
454 &.error { border-color: @alert2; background-color: @alert2-inner; }
455 &.success { border-color: @alert1; background-color: @alert1-inner; }
455 &.success { border-color: @alert1; background-color: @alert1-inner; }
456 &.neutral { border-color: @grey3; background-color: @grey6; }
456 &.neutral { border-color: @grey3; background-color: @grey6; }
457 }
457 }
458
458
459 // Fixed Sidebar Column
459 // Fixed Sidebar Column
460 .sidebar-col-wrapper {
460 .sidebar-col-wrapper {
461 padding-left: @sidebar-all-width;
461 padding-left: @sidebar-all-width;
462
462
463 .sidebar {
463 .sidebar {
464 width: @sidebar-width;
464 width: @sidebar-width;
465 margin-left: -@sidebar-all-width;
465 margin-left: -@sidebar-all-width;
466 }
466 }
467 }
467 }
468
468
469 .sidebar-col-wrapper.scw-small {
469 .sidebar-col-wrapper.scw-small {
470 padding-left: @sidebar-small-all-width;
470 padding-left: @sidebar-small-all-width;
471
471
472 .sidebar {
472 .sidebar {
473 width: @sidebar-small-width;
473 width: @sidebar-small-width;
474 margin-left: -@sidebar-small-all-width;
474 margin-left: -@sidebar-small-all-width;
475 }
475 }
476 }
476 }
477
477
478
478
479 // FOOTER
479 // FOOTER
480 #footer {
480 #footer {
481 padding: 0;
481 padding: 0;
482 text-align: center;
482 text-align: center;
483 vertical-align: middle;
483 vertical-align: middle;
484 color: @grey2;
484 color: @grey2;
485 background-color: @grey6;
485 background-color: @grey6;
486
486
487 p {
487 p {
488 margin: 0;
488 margin: 0;
489 padding: 1em;
489 padding: 1em;
490 line-height: 1em;
490 line-height: 1em;
491 }
491 }
492
492
493 .server-instance { //server instance
493 .server-instance { //server instance
494 display: none;
494 display: none;
495 }
495 }
496
496
497 .title {
497 .title {
498 float: none;
498 float: none;
499 margin: 0 auto;
499 margin: 0 auto;
500 }
500 }
501 }
501 }
502
502
503 button.close {
503 button.close {
504 padding: 0;
504 padding: 0;
505 cursor: pointer;
505 cursor: pointer;
506 background: transparent;
506 background: transparent;
507 border: 0;
507 border: 0;
508 .box-shadow(none);
508 .box-shadow(none);
509 -webkit-appearance: none;
509 -webkit-appearance: none;
510 }
510 }
511
511
512 .close {
512 .close {
513 float: right;
513 float: right;
514 font-size: 21px;
514 font-size: 21px;
515 font-family: @text-bootstrap;
515 font-family: @text-bootstrap;
516 line-height: 1em;
516 line-height: 1em;
517 font-weight: bold;
517 font-weight: bold;
518 color: @grey2;
518 color: @grey2;
519
519
520 &:hover,
520 &:hover,
521 &:focus {
521 &:focus {
522 color: @grey1;
522 color: @grey1;
523 text-decoration: none;
523 text-decoration: none;
524 cursor: pointer;
524 cursor: pointer;
525 }
525 }
526 }
526 }
527
527
528 // GRID
528 // GRID
529 .sorting,
529 .sorting,
530 .sorting_desc,
530 .sorting_desc,
531 .sorting_asc {
531 .sorting_asc {
532 cursor: pointer;
532 cursor: pointer;
533 }
533 }
534 .sorting_desc:after {
534 .sorting_desc:after {
535 content: "\00A0\25B2";
535 content: "\00A0\25B2";
536 font-size: .75em;
536 font-size: .75em;
537 }
537 }
538 .sorting_asc:after {
538 .sorting_asc:after {
539 content: "\00A0\25BC";
539 content: "\00A0\25BC";
540 font-size: .68em;
540 font-size: .68em;
541 }
541 }
542
542
543
543
544 .user_auth_tokens {
544 .user_auth_tokens {
545
545
546 &.truncate {
546 &.truncate {
547 white-space: nowrap;
547 white-space: nowrap;
548 overflow: hidden;
548 overflow: hidden;
549 text-overflow: ellipsis;
549 text-overflow: ellipsis;
550 }
550 }
551
551
552 .fields .field .input {
552 .fields .field .input {
553 margin: 0;
553 margin: 0;
554 }
554 }
555
555
556 input#description {
556 input#description {
557 width: 100px;
557 width: 100px;
558 margin: 0;
558 margin: 0;
559 }
559 }
560
560
561 .drop-menu {
561 .drop-menu {
562 // TODO: johbo: Remove this, should work out of the box when
562 // TODO: johbo: Remove this, should work out of the box when
563 // having multiple inputs inline
563 // having multiple inputs inline
564 margin: 0 0 0 5px;
564 margin: 0 0 0 5px;
565 }
565 }
566 }
566 }
567 #user_list_table {
567 #user_list_table {
568 .closed {
568 .closed {
569 background-color: @grey6;
569 background-color: @grey6;
570 }
570 }
571 }
571 }
572
572
573
573
574 input {
574 input {
575 &.disabled {
575 &.disabled {
576 opacity: .5;
576 opacity: .5;
577 }
577 }
578 }
578 }
579
579
580 // remove extra padding in firefox
580 // remove extra padding in firefox
581 input::-moz-focus-inner { border:0; padding:0 }
581 input::-moz-focus-inner { border:0; padding:0 }
582
582
583 .adjacent input {
583 .adjacent input {
584 margin-bottom: @padding;
584 margin-bottom: @padding;
585 }
585 }
586
586
587 .permissions_boxes {
587 .permissions_boxes {
588 display: block;
588 display: block;
589 }
589 }
590
590
591 //TODO: lisa: this should be in tables
591 //TODO: lisa: this should be in tables
592 .show_more_col {
592 .show_more_col {
593 width: 20px;
593 width: 20px;
594 }
594 }
595
595
596 //FORMS
596 //FORMS
597
597
598 .medium-inline,
598 .medium-inline,
599 input#description.medium-inline {
599 input#description.medium-inline {
600 display: inline;
600 display: inline;
601 width: @medium-inline-input-width;
601 width: @medium-inline-input-width;
602 min-width: 100px;
602 min-width: 100px;
603 }
603 }
604
604
605 select {
605 select {
606 //reset
606 //reset
607 -webkit-appearance: none;
607 -webkit-appearance: none;
608 -moz-appearance: none;
608 -moz-appearance: none;
609
609
610 display: inline-block;
610 display: inline-block;
611 height: 28px;
611 height: 28px;
612 width: auto;
612 width: auto;
613 margin: 0 @padding @padding 0;
613 margin: 0 @padding @padding 0;
614 padding: 0 18px 0 8px;
614 padding: 0 18px 0 8px;
615 line-height:1em;
615 line-height:1em;
616 font-size: @basefontsize;
616 font-size: @basefontsize;
617 border: @border-thickness solid @rcblue;
617 border: @border-thickness solid @rcblue;
618 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
618 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
619 color: @rcblue;
619 color: @rcblue;
620
620
621 &:after {
621 &:after {
622 content: "\00A0\25BE";
622 content: "\00A0\25BE";
623 }
623 }
624
624
625 &:focus {
625 &:focus {
626 outline: none;
626 outline: none;
627 }
627 }
628 }
628 }
629
629
630 option {
630 option {
631 &:focus {
631 &:focus {
632 outline: none;
632 outline: none;
633 }
633 }
634 }
634 }
635
635
636 input,
636 input,
637 textarea {
637 textarea {
638 padding: @input-padding;
638 padding: @input-padding;
639 border: @input-border-thickness solid @border-highlight-color;
639 border: @input-border-thickness solid @border-highlight-color;
640 .border-radius (@border-radius);
640 .border-radius (@border-radius);
641 font-family: @text-light;
641 font-family: @text-light;
642 font-size: @basefontsize;
642 font-size: @basefontsize;
643
643
644 &.input-sm {
644 &.input-sm {
645 padding: 5px;
645 padding: 5px;
646 }
646 }
647
647
648 &#description {
648 &#description {
649 min-width: @input-description-minwidth;
649 min-width: @input-description-minwidth;
650 min-height: 1em;
650 min-height: 1em;
651 padding: 10px;
651 padding: 10px;
652 }
652 }
653 }
653 }
654
654
655 .field-sm {
655 .field-sm {
656 input,
656 input,
657 textarea {
657 textarea {
658 padding: 5px;
658 padding: 5px;
659 }
659 }
660 }
660 }
661
661
662 textarea {
662 textarea {
663 display: block;
663 display: block;
664 clear: both;
664 clear: both;
665 width: 100%;
665 width: 100%;
666 min-height: 100px;
666 min-height: 100px;
667 margin-bottom: @padding;
667 margin-bottom: @padding;
668 .box-sizing(border-box);
668 .box-sizing(border-box);
669 overflow: auto;
669 overflow: auto;
670 }
670 }
671
671
672 label {
672 label {
673 font-family: @text-light;
673 font-family: @text-light;
674 }
674 }
675
675
676 // GRAVATARS
676 // GRAVATARS
677 // centers gravatar on username to the right
677 // centers gravatar on username to the right
678
678
679 .gravatar {
679 .gravatar {
680 display: inline;
680 display: inline;
681 min-width: 16px;
681 min-width: 16px;
682 min-height: 16px;
682 min-height: 16px;
683 margin: -5px 0;
683 margin: -5px 0;
684 padding: 0;
684 padding: 0;
685 line-height: 1em;
685 line-height: 1em;
686 border: 1px solid @grey4;
686 border: 1px solid @grey4;
687
687
688 &.gravatar-large {
688 &.gravatar-large {
689 margin: -0.5em .25em -0.5em 0;
689 margin: -0.5em .25em -0.5em 0;
690 }
690 }
691
691
692 & + .user {
692 & + .user {
693 display: inline;
693 display: inline;
694 margin: 0;
694 margin: 0;
695 padding: 0 0 0 .17em;
695 padding: 0 0 0 .17em;
696 line-height: 1em;
696 line-height: 1em;
697 }
697 }
698 }
698 }
699
699
700 .user-inline-data {
700 .user-inline-data {
701 display: inline-block;
701 display: inline-block;
702 float: left;
702 float: left;
703 padding-left: .5em;
703 padding-left: .5em;
704 line-height: 1.3em;
704 line-height: 1.3em;
705 }
705 }
706
706
707 .rc-user { // gravatar + user wrapper
707 .rc-user { // gravatar + user wrapper
708 float: left;
708 float: left;
709 position: relative;
709 position: relative;
710 min-width: 100px;
710 min-width: 100px;
711 max-width: 200px;
711 max-width: 200px;
712 min-height: (@gravatar-size + @border-thickness * 2); // account for border
712 min-height: (@gravatar-size + @border-thickness * 2); // account for border
713 display: block;
713 display: block;
714 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
714 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
715
715
716
716
717 .gravatar {
717 .gravatar {
718 display: block;
718 display: block;
719 position: absolute;
719 position: absolute;
720 top: 0;
720 top: 0;
721 left: 0;
721 left: 0;
722 min-width: @gravatar-size;
722 min-width: @gravatar-size;
723 min-height: @gravatar-size;
723 min-height: @gravatar-size;
724 margin: 0;
724 margin: 0;
725 }
725 }
726
726
727 .user {
727 .user {
728 display: block;
728 display: block;
729 max-width: 175px;
729 max-width: 175px;
730 padding-top: 2px;
730 padding-top: 2px;
731 overflow: hidden;
731 overflow: hidden;
732 text-overflow: ellipsis;
732 text-overflow: ellipsis;
733 }
733 }
734 }
734 }
735
735
736 .gist-gravatar,
736 .gist-gravatar,
737 .journal_container {
737 .journal_container {
738 .gravatar-large {
738 .gravatar-large {
739 margin: 0 .5em -10px 0;
739 margin: 0 .5em -10px 0;
740 }
740 }
741 }
741 }
742
742
743
743
744 // ADMIN SETTINGS
744 // ADMIN SETTINGS
745
745
746 // Tag Patterns
746 // Tag Patterns
747 .tag_patterns {
747 .tag_patterns {
748 .tag_input {
748 .tag_input {
749 margin-bottom: @padding;
749 margin-bottom: @padding;
750 }
750 }
751 }
751 }
752
752
753 .locked_input {
753 .locked_input {
754 position: relative;
754 position: relative;
755
755
756 input {
756 input {
757 display: inline;
757 display: inline;
758 margin-top: 3px;
758 margin-top: 3px;
759 }
759 }
760
760
761 br {
761 br {
762 display: none;
762 display: none;
763 }
763 }
764
764
765 .error-message {
765 .error-message {
766 float: left;
766 float: left;
767 width: 100%;
767 width: 100%;
768 }
768 }
769
769
770 .lock_input_button {
770 .lock_input_button {
771 display: inline;
771 display: inline;
772 }
772 }
773
773
774 .help-block {
774 .help-block {
775 clear: both;
775 clear: both;
776 }
776 }
777 }
777 }
778
778
779 // Notifications
779 // Notifications
780
780
781 .notifications_buttons {
781 .notifications_buttons {
782 margin: 0 0 @space 0;
782 margin: 0 0 @space 0;
783 padding: 0;
783 padding: 0;
784
784
785 .btn {
785 .btn {
786 display: inline-block;
786 display: inline-block;
787 }
787 }
788 }
788 }
789
789
790 .notification-list {
790 .notification-list {
791
791
792 div {
792 div {
793 display: inline-block;
793 display: inline-block;
794 vertical-align: middle;
794 vertical-align: middle;
795 }
795 }
796
796
797 .container {
797 .container {
798 display: block;
798 display: block;
799 margin: 0 0 @padding 0;
799 margin: 0 0 @padding 0;
800 }
800 }
801
801
802 .delete-notifications {
802 .delete-notifications {
803 margin-left: @padding;
803 margin-left: @padding;
804 text-align: right;
804 text-align: right;
805 cursor: pointer;
805 cursor: pointer;
806 }
806 }
807
807
808 .read-notifications {
808 .read-notifications {
809 margin-left: @padding/2;
809 margin-left: @padding/2;
810 text-align: right;
810 text-align: right;
811 width: 35px;
811 width: 35px;
812 cursor: pointer;
812 cursor: pointer;
813 }
813 }
814
814
815 .icon-minus-sign {
815 .icon-minus-sign {
816 color: @alert2;
816 color: @alert2;
817 }
817 }
818
818
819 .icon-ok-sign {
819 .icon-ok-sign {
820 color: @alert1;
820 color: @alert1;
821 }
821 }
822 }
822 }
823
823
824 .user_settings {
824 .user_settings {
825 float: left;
825 float: left;
826 clear: both;
826 clear: both;
827 display: block;
827 display: block;
828 width: 100%;
828 width: 100%;
829
829
830 .gravatar_box {
830 .gravatar_box {
831 margin-bottom: @padding;
831 margin-bottom: @padding;
832
832
833 &:after {
833 &:after {
834 content: " ";
834 content: " ";
835 clear: both;
835 clear: both;
836 width: 100%;
836 width: 100%;
837 }
837 }
838 }
838 }
839
839
840 .fields .field {
840 .fields .field {
841 clear: both;
841 clear: both;
842 }
842 }
843 }
843 }
844
844
845 .advanced_settings {
845 .advanced_settings {
846 margin-bottom: @space;
846 margin-bottom: @space;
847
847
848 .help-block {
848 .help-block {
849 margin-left: 0;
849 margin-left: 0;
850 }
850 }
851
851
852 button + .help-block {
852 button + .help-block {
853 margin-top: @padding;
853 margin-top: @padding;
854 }
854 }
855 }
855 }
856
856
857 // admin settings radio buttons and labels
857 // admin settings radio buttons and labels
858 .label-2 {
858 .label-2 {
859 float: left;
859 float: left;
860 width: @label2-width;
860 width: @label2-width;
861
861
862 label {
862 label {
863 color: @grey1;
863 color: @grey1;
864 }
864 }
865 }
865 }
866 .checkboxes {
866 .checkboxes {
867 float: left;
867 float: left;
868 width: @checkboxes-width;
868 width: @checkboxes-width;
869 margin-bottom: @padding;
869 margin-bottom: @padding;
870
870
871 .checkbox {
871 .checkbox {
872 width: 100%;
872 width: 100%;
873
873
874 label {
874 label {
875 margin: 0;
875 margin: 0;
876 padding: 0;
876 padding: 0;
877 }
877 }
878 }
878 }
879
879
880 .checkbox + .checkbox {
880 .checkbox + .checkbox {
881 display: inline-block;
881 display: inline-block;
882 }
882 }
883
883
884 label {
884 label {
885 margin-right: 1em;
885 margin-right: 1em;
886 }
886 }
887 }
887 }
888
888
889 // CHANGELOG
889 // CHANGELOG
890 .container_header {
890 .container_header {
891 float: left;
891 float: left;
892 display: block;
892 display: block;
893 width: 100%;
893 width: 100%;
894 margin: @padding 0 @padding;
894 margin: @padding 0 @padding;
895
895
896 #filter_changelog {
896 #filter_changelog {
897 float: left;
897 float: left;
898 margin-right: @padding;
898 margin-right: @padding;
899 }
899 }
900
900
901 .breadcrumbs_light {
901 .breadcrumbs_light {
902 display: inline-block;
902 display: inline-block;
903 }
903 }
904 }
904 }
905
905
906 .info_box {
906 .info_box {
907 float: right;
907 float: right;
908 }
908 }
909
909
910
910
911 #graph_nodes {
911 #graph_nodes {
912 padding-top: 43px;
912 padding-top: 43px;
913 }
913 }
914
914
915 #graph_content{
915 #graph_content{
916
916
917 // adjust for table headers so that graph renders properly
917 // adjust for table headers so that graph renders properly
918 // #graph_nodes padding - table cell padding
918 // #graph_nodes padding - table cell padding
919 padding-top: (@space - (@basefontsize * 2.4));
919 padding-top: (@space - (@basefontsize * 2.4));
920
920
921 &.graph_full_width {
921 &.graph_full_width {
922 width: 100%;
922 width: 100%;
923 max-width: 100%;
923 max-width: 100%;
924 }
924 }
925 }
925 }
926
926
927 #graph {
927 #graph {
928 .flag_status {
928 .flag_status {
929 margin: 0;
929 margin: 0;
930 }
930 }
931
931
932 .pagination-left {
932 .pagination-left {
933 float: left;
933 float: left;
934 clear: both;
934 clear: both;
935 }
935 }
936
936
937 .log-container {
937 .log-container {
938 max-width: 345px;
938 max-width: 345px;
939
939
940 .message{
940 .message{
941 max-width: 340px;
941 max-width: 340px;
942 }
942 }
943 }
943 }
944
944
945 .graph-col-wrapper {
945 .graph-col-wrapper {
946 padding-left: 110px;
946 padding-left: 110px;
947
947
948 #graph_nodes {
948 #graph_nodes {
949 width: 100px;
949 width: 100px;
950 margin-left: -110px;
950 margin-left: -110px;
951 float: left;
951 float: left;
952 clear: left;
952 clear: left;
953 }
953 }
954 }
954 }
955 }
955 }
956
956
957 #filter_changelog {
957 #filter_changelog {
958 float: left;
958 float: left;
959 }
959 }
960
960
961
961
962 //--- THEME ------------------//
962 //--- THEME ------------------//
963
963
964 #logo {
964 #logo {
965 float: left;
965 float: left;
966 margin: 9px 0 0 0;
966 margin: 9px 0 0 0;
967
967
968 .header {
968 .header {
969 background-color: transparent;
969 background-color: transparent;
970 }
970 }
971
971
972 a {
972 a {
973 display: inline-block;
973 display: inline-block;
974 }
974 }
975
975
976 img {
976 img {
977 height:30px;
977 height:30px;
978 }
978 }
979 }
979 }
980
980
981 .logo-wrapper {
981 .logo-wrapper {
982 float:left;
982 float:left;
983 }
983 }
984
984
985 .branding{
985 .branding{
986 float: left;
986 float: left;
987 padding: 9px 2px;
987 padding: 9px 2px;
988 line-height: 1em;
988 line-height: 1em;
989 font-size: @navigation-fontsize;
989 font-size: @navigation-fontsize;
990 }
990 }
991
991
992 img {
992 img {
993 border: none;
993 border: none;
994 outline: none;
994 outline: none;
995 }
995 }
996 user-profile-header
996 user-profile-header
997 label {
997 label {
998
998
999 input[type="checkbox"] {
999 input[type="checkbox"] {
1000 margin-right: 1em;
1000 margin-right: 1em;
1001 }
1001 }
1002 input[type="radio"] {
1002 input[type="radio"] {
1003 margin-right: 1em;
1003 margin-right: 1em;
1004 }
1004 }
1005 }
1005 }
1006
1006
1007 .flag_status {
1007 .flag_status {
1008 margin: 2px 8px 6px 2px;
1008 margin: 2px 8px 6px 2px;
1009 &.under_review {
1009 &.under_review {
1010 .circle(5px, @alert3);
1010 .circle(5px, @alert3);
1011 }
1011 }
1012 &.approved {
1012 &.approved {
1013 .circle(5px, @alert1);
1013 .circle(5px, @alert1);
1014 }
1014 }
1015 &.rejected,
1015 &.rejected,
1016 &.forced_closed{
1016 &.forced_closed{
1017 .circle(5px, @alert2);
1017 .circle(5px, @alert2);
1018 }
1018 }
1019 &.not_reviewed {
1019 &.not_reviewed {
1020 .circle(5px, @grey5);
1020 .circle(5px, @grey5);
1021 }
1021 }
1022 }
1022 }
1023
1023
1024 .flag_status_comment_box {
1024 .flag_status_comment_box {
1025 margin: 5px 6px 0px 2px;
1025 margin: 5px 6px 0px 2px;
1026 }
1026 }
1027 .test_pattern_preview {
1027 .test_pattern_preview {
1028 margin: @space 0;
1028 margin: @space 0;
1029
1029
1030 p {
1030 p {
1031 margin-bottom: 0;
1031 margin-bottom: 0;
1032 border-bottom: @border-thickness solid @border-default-color;
1032 border-bottom: @border-thickness solid @border-default-color;
1033 color: @grey3;
1033 color: @grey3;
1034 }
1034 }
1035
1035
1036 .btn {
1036 .btn {
1037 margin-bottom: @padding;
1037 margin-bottom: @padding;
1038 }
1038 }
1039 }
1039 }
1040 #test_pattern_result {
1040 #test_pattern_result {
1041 display: none;
1041 display: none;
1042 &:extend(pre);
1042 &:extend(pre);
1043 padding: .9em;
1043 padding: .9em;
1044 color: @grey3;
1044 color: @grey3;
1045 background-color: @grey7;
1045 background-color: @grey7;
1046 border-right: @border-thickness solid @border-default-color;
1046 border-right: @border-thickness solid @border-default-color;
1047 border-bottom: @border-thickness solid @border-default-color;
1047 border-bottom: @border-thickness solid @border-default-color;
1048 border-left: @border-thickness solid @border-default-color;
1048 border-left: @border-thickness solid @border-default-color;
1049 }
1049 }
1050
1050
1051 #repo_vcs_settings {
1051 #repo_vcs_settings {
1052 #inherit_overlay_vcs_default {
1052 #inherit_overlay_vcs_default {
1053 display: none;
1053 display: none;
1054 }
1054 }
1055 #inherit_overlay_vcs_custom {
1055 #inherit_overlay_vcs_custom {
1056 display: custom;
1056 display: custom;
1057 }
1057 }
1058 &.inherited {
1058 &.inherited {
1059 #inherit_overlay_vcs_default {
1059 #inherit_overlay_vcs_default {
1060 display: block;
1060 display: block;
1061 }
1061 }
1062 #inherit_overlay_vcs_custom {
1062 #inherit_overlay_vcs_custom {
1063 display: none;
1063 display: none;
1064 }
1064 }
1065 }
1065 }
1066 }
1066 }
1067
1067
1068 .issue-tracker-link {
1068 .issue-tracker-link {
1069 color: @rcblue;
1069 color: @rcblue;
1070 }
1070 }
1071
1071
1072 // Issue Tracker Table Show/Hide
1072 // Issue Tracker Table Show/Hide
1073 #repo_issue_tracker {
1073 #repo_issue_tracker {
1074 #inherit_overlay {
1074 #inherit_overlay {
1075 display: none;
1075 display: none;
1076 }
1076 }
1077 #custom_overlay {
1077 #custom_overlay {
1078 display: custom;
1078 display: custom;
1079 }
1079 }
1080 &.inherited {
1080 &.inherited {
1081 #inherit_overlay {
1081 #inherit_overlay {
1082 display: block;
1082 display: block;
1083 }
1083 }
1084 #custom_overlay {
1084 #custom_overlay {
1085 display: none;
1085 display: none;
1086 }
1086 }
1087 }
1087 }
1088 }
1088 }
1089 table.issuetracker {
1089 table.issuetracker {
1090 &.readonly {
1090 &.readonly {
1091 tr, td {
1091 tr, td {
1092 color: @grey3;
1092 color: @grey3;
1093 }
1093 }
1094 }
1094 }
1095 .edit {
1095 .edit {
1096 display: none;
1096 display: none;
1097 }
1097 }
1098 .editopen {
1098 .editopen {
1099 .edit {
1099 .edit {
1100 display: inline;
1100 display: inline;
1101 }
1101 }
1102 .entry {
1102 .entry {
1103 display: none;
1103 display: none;
1104 }
1104 }
1105 }
1105 }
1106 tr td.td-action {
1106 tr td.td-action {
1107 min-width: 117px;
1107 min-width: 117px;
1108 }
1108 }
1109 td input {
1109 td input {
1110 max-width: none;
1110 max-width: none;
1111 min-width: 30px;
1111 min-width: 30px;
1112 width: 80%;
1112 width: 80%;
1113 }
1113 }
1114 .issuetracker_pref input {
1114 .issuetracker_pref input {
1115 width: 40%;
1115 width: 40%;
1116 }
1116 }
1117 input.edit_issuetracker_update {
1117 input.edit_issuetracker_update {
1118 margin-right: 0;
1118 margin-right: 0;
1119 width: auto;
1119 width: auto;
1120 }
1120 }
1121 }
1121 }
1122
1122
1123 table.integrations {
1123 table.integrations {
1124 .td-icon {
1124 .td-icon {
1125 width: 20px;
1125 width: 20px;
1126 .integration-icon {
1126 .integration-icon {
1127 height: 20px;
1127 height: 20px;
1128 width: 20px;
1128 width: 20px;
1129 }
1129 }
1130 }
1130 }
1131 }
1131 }
1132
1132
1133 .integrations {
1133 .integrations {
1134 a.integration-box {
1134 a.integration-box {
1135 color: @text-color;
1135 color: @text-color;
1136 &:hover {
1136 &:hover {
1137 .panel {
1137 .panel {
1138 background: #fbfbfb;
1138 background: #fbfbfb;
1139 }
1139 }
1140 }
1140 }
1141 .integration-icon {
1141 .integration-icon {
1142 width: 30px;
1142 width: 30px;
1143 height: 30px;
1143 height: 30px;
1144 margin-right: 20px;
1144 margin-right: 20px;
1145 float: left;
1145 float: left;
1146 }
1146 }
1147
1147
1148 .panel-body {
1148 .panel-body {
1149 padding: 10px;
1149 padding: 10px;
1150 }
1150 }
1151 .panel {
1151 .panel {
1152 margin-bottom: 10px;
1152 margin-bottom: 10px;
1153 }
1153 }
1154 h2 {
1154 h2 {
1155 display: inline-block;
1155 display: inline-block;
1156 margin: 0;
1156 margin: 0;
1157 min-width: 140px;
1157 min-width: 140px;
1158 }
1158 }
1159 }
1159 }
1160 }
1160 }
1161
1161
1162 //Permissions Settings
1162 //Permissions Settings
1163 #add_perm {
1163 #add_perm {
1164 margin: 0 0 @padding;
1164 margin: 0 0 @padding;
1165 cursor: pointer;
1165 cursor: pointer;
1166 }
1166 }
1167
1167
1168 .perm_ac {
1168 .perm_ac {
1169 input {
1169 input {
1170 width: 95%;
1170 width: 95%;
1171 }
1171 }
1172 }
1172 }
1173
1173
1174 .autocomplete-suggestions {
1174 .autocomplete-suggestions {
1175 width: auto !important; // overrides autocomplete.js
1175 width: auto !important; // overrides autocomplete.js
1176 margin: 0;
1176 margin: 0;
1177 border: @border-thickness solid @rcblue;
1177 border: @border-thickness solid @rcblue;
1178 border-radius: @border-radius;
1178 border-radius: @border-radius;
1179 color: @rcblue;
1179 color: @rcblue;
1180 background-color: white;
1180 background-color: white;
1181 }
1181 }
1182 .autocomplete-selected {
1182 .autocomplete-selected {
1183 background: #F0F0F0;
1183 background: #F0F0F0;
1184 }
1184 }
1185 .ac-container-wrap {
1185 .ac-container-wrap {
1186 margin: 0;
1186 margin: 0;
1187 padding: 8px;
1187 padding: 8px;
1188 border-bottom: @border-thickness solid @rclightblue;
1188 border-bottom: @border-thickness solid @rclightblue;
1189 list-style-type: none;
1189 list-style-type: none;
1190 cursor: pointer;
1190 cursor: pointer;
1191
1191
1192 &:hover {
1192 &:hover {
1193 background-color: @rclightblue;
1193 background-color: @rclightblue;
1194 }
1194 }
1195
1195
1196 img {
1196 img {
1197 height: @gravatar-size;
1197 height: @gravatar-size;
1198 width: @gravatar-size;
1198 width: @gravatar-size;
1199 margin-right: 1em;
1199 margin-right: 1em;
1200 }
1200 }
1201
1201
1202 strong {
1202 strong {
1203 font-weight: normal;
1203 font-weight: normal;
1204 }
1204 }
1205 }
1205 }
1206
1206
1207 // Settings Dropdown
1207 // Settings Dropdown
1208 .user-menu .container {
1208 .user-menu .container {
1209 padding: 0 4px;
1209 padding: 0 4px;
1210 margin: 0;
1210 margin: 0;
1211 }
1211 }
1212
1212
1213 .user-menu .gravatar {
1213 .user-menu .gravatar {
1214 cursor: pointer;
1214 cursor: pointer;
1215 }
1215 }
1216
1216
1217 .codeblock {
1217 .codeblock {
1218 margin-bottom: @padding;
1218 margin-bottom: @padding;
1219 clear: both;
1219 clear: both;
1220
1220
1221 .stats{
1221 .stats{
1222 overflow: hidden;
1222 overflow: hidden;
1223 }
1223 }
1224
1224
1225 .message{
1225 .message{
1226 textarea{
1226 textarea{
1227 margin: 0;
1227 margin: 0;
1228 }
1228 }
1229 }
1229 }
1230
1230
1231 .code-header {
1231 .code-header {
1232 .stats {
1232 .stats {
1233 line-height: 2em;
1233 line-height: 2em;
1234
1234
1235 .revision_id {
1235 .revision_id {
1236 margin-left: 0;
1236 margin-left: 0;
1237 }
1237 }
1238 .buttons {
1238 .buttons {
1239 padding-right: 0;
1239 padding-right: 0;
1240 }
1240 }
1241 }
1241 }
1242
1242
1243 .item{
1243 .item{
1244 margin-right: 0.5em;
1244 margin-right: 0.5em;
1245 }
1245 }
1246 }
1246 }
1247
1247
1248 #editor_container{
1248 #editor_container{
1249 position: relative;
1249 position: relative;
1250 margin: @padding;
1250 margin: @padding;
1251 }
1251 }
1252 }
1252 }
1253
1253
1254 #file_history_container {
1254 #file_history_container {
1255 display: none;
1255 display: none;
1256 }
1256 }
1257
1257
1258 .file-history-inner {
1258 .file-history-inner {
1259 margin-bottom: 10px;
1259 margin-bottom: 10px;
1260 }
1260 }
1261
1261
1262 // Pull Requests
1262 // Pull Requests
1263 .summary-details {
1263 .summary-details {
1264 width: 72%;
1264 width: 72%;
1265 }
1265 }
1266 .pr-summary {
1266 .pr-summary {
1267 border-bottom: @border-thickness solid @grey5;
1267 border-bottom: @border-thickness solid @grey5;
1268 margin-bottom: @space;
1268 margin-bottom: @space;
1269 }
1269 }
1270 .reviewers-title {
1270 .reviewers-title {
1271 width: 25%;
1271 width: 25%;
1272 min-width: 200px;
1272 min-width: 200px;
1273 }
1273 }
1274 .reviewers {
1274 .reviewers {
1275 width: 25%;
1275 width: 25%;
1276 min-width: 200px;
1276 min-width: 200px;
1277 }
1277 }
1278 .reviewers ul li {
1278 .reviewers ul li {
1279 position: relative;
1279 position: relative;
1280 width: 100%;
1280 width: 100%;
1281 margin-bottom: 8px;
1281 margin-bottom: 8px;
1282 }
1282 }
1283 .reviewers_member {
1283 .reviewers_member {
1284 width: 100%;
1284 width: 100%;
1285 overflow: auto;
1285 overflow: auto;
1286 }
1286 }
1287 .reviewer_reason {
1287 .reviewer_reason {
1288 padding-left: 20px;
1288 padding-left: 20px;
1289 }
1289 }
1290 .reviewer_status {
1290 .reviewer_status {
1291 display: inline-block;
1291 display: inline-block;
1292 vertical-align: top;
1292 vertical-align: top;
1293 width: 7%;
1293 width: 7%;
1294 min-width: 20px;
1294 min-width: 20px;
1295 height: 1.2em;
1295 height: 1.2em;
1296 margin-top: 3px;
1296 margin-top: 3px;
1297 line-height: 1em;
1297 line-height: 1em;
1298 }
1298 }
1299
1299
1300 .reviewer_name {
1300 .reviewer_name {
1301 display: inline-block;
1301 display: inline-block;
1302 max-width: 83%;
1302 max-width: 83%;
1303 padding-right: 20px;
1303 padding-right: 20px;
1304 vertical-align: middle;
1304 vertical-align: middle;
1305 line-height: 1;
1305 line-height: 1;
1306
1306
1307 .rc-user {
1307 .rc-user {
1308 min-width: 0;
1308 min-width: 0;
1309 margin: -2px 1em 0 0;
1309 margin: -2px 1em 0 0;
1310 }
1310 }
1311
1311
1312 .reviewer {
1312 .reviewer {
1313 float: left;
1313 float: left;
1314 }
1314 }
1315
1315
1316 &.to-delete {
1316 &.to-delete {
1317 .user,
1317 .user,
1318 .reviewer {
1318 .reviewer {
1319 text-decoration: line-through;
1319 text-decoration: line-through;
1320 }
1320 }
1321 }
1321 }
1322 }
1322 }
1323
1323
1324 .reviewer_member_remove {
1324 .reviewer_member_remove {
1325 position: absolute;
1325 position: absolute;
1326 right: 0;
1326 right: 0;
1327 top: 0;
1327 top: 0;
1328 width: 16px;
1328 width: 16px;
1329 margin-bottom: 10px;
1329 margin-bottom: 10px;
1330 padding: 0;
1330 padding: 0;
1331 color: black;
1331 color: black;
1332 }
1332 }
1333 .reviewer_member_status {
1333 .reviewer_member_status {
1334 margin-top: 5px;
1334 margin-top: 5px;
1335 }
1335 }
1336 .pr-summary #summary{
1336 .pr-summary #summary{
1337 width: 100%;
1337 width: 100%;
1338 }
1338 }
1339 .pr-summary .action_button:hover {
1339 .pr-summary .action_button:hover {
1340 border: 0;
1340 border: 0;
1341 cursor: pointer;
1341 cursor: pointer;
1342 }
1342 }
1343 .pr-details-title {
1343 .pr-details-title {
1344 padding-bottom: 8px;
1344 padding-bottom: 8px;
1345 border-bottom: @border-thickness solid @grey5;
1345 border-bottom: @border-thickness solid @grey5;
1346
1346
1347 .action_button.disabled {
1347 .action_button.disabled {
1348 color: @grey4;
1348 color: @grey4;
1349 cursor: inherit;
1349 cursor: inherit;
1350 }
1350 }
1351 .action_button {
1351 .action_button {
1352 color: @rcblue;
1352 color: @rcblue;
1353 }
1353 }
1354 }
1354 }
1355 .pr-details-content {
1355 .pr-details-content {
1356 margin-top: @textmargin;
1356 margin-top: @textmargin;
1357 margin-bottom: @textmargin;
1357 margin-bottom: @textmargin;
1358 }
1358 }
1359 .pr-description {
1359 .pr-description {
1360 white-space:pre-wrap;
1360 white-space:pre-wrap;
1361 }
1361 }
1362 .group_members {
1362 .group_members {
1363 margin-top: 0;
1363 margin-top: 0;
1364 padding: 0;
1364 padding: 0;
1365 list-style: outside none none;
1365 list-style: outside none none;
1366
1366
1367 img {
1367 img {
1368 height: @gravatar-size;
1368 height: @gravatar-size;
1369 width: @gravatar-size;
1369 width: @gravatar-size;
1370 margin-right: .5em;
1370 margin-right: .5em;
1371 margin-left: 3px;
1371 margin-left: 3px;
1372 }
1372 }
1373
1373
1374 .to-delete {
1374 .to-delete {
1375 .user {
1375 .user {
1376 text-decoration: line-through;
1376 text-decoration: line-through;
1377 }
1377 }
1378 }
1378 }
1379 }
1379 }
1380
1380
1381 // new entry in group_members
1381 // new entry in group_members
1382 .td-author-new-entry {
1382 .td-author-new-entry {
1383 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1383 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1384 }
1384 }
1385
1385
1386 .usergroup_member_remove {
1386 .usergroup_member_remove {
1387 width: 16px;
1387 width: 16px;
1388 margin-bottom: 10px;
1388 margin-bottom: 10px;
1389 padding: 0;
1389 padding: 0;
1390 color: black !important;
1390 color: black !important;
1391 cursor: pointer;
1391 cursor: pointer;
1392 }
1392 }
1393
1393
1394 .reviewer_ac .ac-input {
1394 .reviewer_ac .ac-input {
1395 width: 92%;
1395 width: 92%;
1396 margin-bottom: 1em;
1396 margin-bottom: 1em;
1397 }
1397 }
1398 #update_commits {
1398
1399 float: right;
1400 }
1401 .compare_view_commits tr{
1399 .compare_view_commits tr{
1402 height: 20px;
1400 height: 20px;
1403 }
1401 }
1404 .compare_view_commits td {
1402 .compare_view_commits td {
1405 vertical-align: top;
1403 vertical-align: top;
1406 padding-top: 10px;
1404 padding-top: 10px;
1407 }
1405 }
1408 .compare_view_commits .author {
1406 .compare_view_commits .author {
1409 margin-left: 5px;
1407 margin-left: 5px;
1410 }
1408 }
1411
1409
1412 .compare_view_files {
1410 .compare_view_files {
1413 width: 100%;
1411 width: 100%;
1414
1412
1415 td {
1413 td {
1416 vertical-align: middle;
1414 vertical-align: middle;
1417 }
1415 }
1418 }
1416 }
1419
1417
1420 .compare_view_filepath {
1418 .compare_view_filepath {
1421 color: @grey1;
1419 color: @grey1;
1422 }
1420 }
1423
1421
1424 .show_more {
1422 .show_more {
1425 display: inline-block;
1423 display: inline-block;
1426 position: relative;
1424 position: relative;
1427 vertical-align: middle;
1425 vertical-align: middle;
1428 width: 4px;
1426 width: 4px;
1429 height: @basefontsize;
1427 height: @basefontsize;
1430
1428
1431 &:after {
1429 &:after {
1432 content: "\00A0\25BE";
1430 content: "\00A0\25BE";
1433 display: inline-block;
1431 display: inline-block;
1434 width:10px;
1432 width:10px;
1435 line-height: 5px;
1433 line-height: 5px;
1436 font-size: 12px;
1434 font-size: 12px;
1437 cursor: pointer;
1435 cursor: pointer;
1438 }
1436 }
1439 }
1437 }
1440
1438
1441 .journal_more .show_more {
1439 .journal_more .show_more {
1442 display: inline;
1440 display: inline;
1443
1441
1444 &:after {
1442 &:after {
1445 content: none;
1443 content: none;
1446 }
1444 }
1447 }
1445 }
1448
1446
1449 .open .show_more:after,
1447 .open .show_more:after,
1450 .select2-dropdown-open .show_more:after {
1448 .select2-dropdown-open .show_more:after {
1451 .rotate(180deg);
1449 .rotate(180deg);
1452 margin-left: 4px;
1450 margin-left: 4px;
1453 }
1451 }
1454
1452
1455
1453
1456 .compare_view_commits .collapse_commit:after {
1454 .compare_view_commits .collapse_commit:after {
1457 cursor: pointer;
1455 cursor: pointer;
1458 content: "\00A0\25B4";
1456 content: "\00A0\25B4";
1459 margin-left: -3px;
1457 margin-left: -3px;
1460 font-size: 17px;
1458 font-size: 17px;
1461 color: @grey4;
1459 color: @grey4;
1462 }
1460 }
1463
1461
1464 .diff_links {
1462 .diff_links {
1465 margin-left: 8px;
1463 margin-left: 8px;
1466 }
1464 }
1467
1465
1468 p.ancestor {
1466 p.ancestor {
1469 margin: @padding 0;
1467 margin: @padding 0;
1470 }
1468 }
1471
1469
1472 .cs_icon_td input[type="checkbox"] {
1470 .cs_icon_td input[type="checkbox"] {
1473 display: none;
1471 display: none;
1474 }
1472 }
1475
1473
1476 .cs_icon_td .expand_file_icon:after {
1474 .cs_icon_td .expand_file_icon:after {
1477 cursor: pointer;
1475 cursor: pointer;
1478 content: "\00A0\25B6";
1476 content: "\00A0\25B6";
1479 font-size: 12px;
1477 font-size: 12px;
1480 color: @grey4;
1478 color: @grey4;
1481 }
1479 }
1482
1480
1483 .cs_icon_td .collapse_file_icon:after {
1481 .cs_icon_td .collapse_file_icon:after {
1484 cursor: pointer;
1482 cursor: pointer;
1485 content: "\00A0\25BC";
1483 content: "\00A0\25BC";
1486 font-size: 12px;
1484 font-size: 12px;
1487 color: @grey4;
1485 color: @grey4;
1488 }
1486 }
1489
1487
1490 /*new binary
1488 /*new binary
1491 NEW_FILENODE = 1
1489 NEW_FILENODE = 1
1492 DEL_FILENODE = 2
1490 DEL_FILENODE = 2
1493 MOD_FILENODE = 3
1491 MOD_FILENODE = 3
1494 RENAMED_FILENODE = 4
1492 RENAMED_FILENODE = 4
1495 COPIED_FILENODE = 5
1493 COPIED_FILENODE = 5
1496 CHMOD_FILENODE = 6
1494 CHMOD_FILENODE = 6
1497 BIN_FILENODE = 7
1495 BIN_FILENODE = 7
1498 */
1496 */
1499 .cs_files_expand {
1497 .cs_files_expand {
1500 font-size: @basefontsize + 5px;
1498 font-size: @basefontsize + 5px;
1501 line-height: 1.8em;
1499 line-height: 1.8em;
1502 float: right;
1500 float: right;
1503 }
1501 }
1504
1502
1505 .cs_files_expand span{
1503 .cs_files_expand span{
1506 color: @rcblue;
1504 color: @rcblue;
1507 cursor: pointer;
1505 cursor: pointer;
1508 }
1506 }
1509 .cs_files {
1507 .cs_files {
1510 clear: both;
1508 clear: both;
1511 padding-bottom: @padding;
1509 padding-bottom: @padding;
1512
1510
1513 .cur_cs {
1511 .cur_cs {
1514 margin: 10px 2px;
1512 margin: 10px 2px;
1515 font-weight: bold;
1513 font-weight: bold;
1516 }
1514 }
1517
1515
1518 .node {
1516 .node {
1519 float: left;
1517 float: left;
1520 }
1518 }
1521
1519
1522 .changes {
1520 .changes {
1523 float: right;
1521 float: right;
1524 color: white;
1522 color: white;
1525 font-size: @basefontsize - 4px;
1523 font-size: @basefontsize - 4px;
1526 margin-top: 4px;
1524 margin-top: 4px;
1527 opacity: 0.6;
1525 opacity: 0.6;
1528 filter: Alpha(opacity=60); /* IE8 and earlier */
1526 filter: Alpha(opacity=60); /* IE8 and earlier */
1529
1527
1530 .added {
1528 .added {
1531 background-color: @alert1;
1529 background-color: @alert1;
1532 float: left;
1530 float: left;
1533 text-align: center;
1531 text-align: center;
1534 }
1532 }
1535
1533
1536 .deleted {
1534 .deleted {
1537 background-color: @alert2;
1535 background-color: @alert2;
1538 float: left;
1536 float: left;
1539 text-align: center;
1537 text-align: center;
1540 }
1538 }
1541
1539
1542 .bin {
1540 .bin {
1543 background-color: @alert1;
1541 background-color: @alert1;
1544 text-align: center;
1542 text-align: center;
1545 }
1543 }
1546
1544
1547 /*new binary*/
1545 /*new binary*/
1548 .bin.bin1 {
1546 .bin.bin1 {
1549 background-color: @alert1;
1547 background-color: @alert1;
1550 text-align: center;
1548 text-align: center;
1551 }
1549 }
1552
1550
1553 /*deleted binary*/
1551 /*deleted binary*/
1554 .bin.bin2 {
1552 .bin.bin2 {
1555 background-color: @alert2;
1553 background-color: @alert2;
1556 text-align: center;
1554 text-align: center;
1557 }
1555 }
1558
1556
1559 /*mod binary*/
1557 /*mod binary*/
1560 .bin.bin3 {
1558 .bin.bin3 {
1561 background-color: @grey2;
1559 background-color: @grey2;
1562 text-align: center;
1560 text-align: center;
1563 }
1561 }
1564
1562
1565 /*rename file*/
1563 /*rename file*/
1566 .bin.bin4 {
1564 .bin.bin4 {
1567 background-color: @alert4;
1565 background-color: @alert4;
1568 text-align: center;
1566 text-align: center;
1569 }
1567 }
1570
1568
1571 /*copied file*/
1569 /*copied file*/
1572 .bin.bin5 {
1570 .bin.bin5 {
1573 background-color: @alert4;
1571 background-color: @alert4;
1574 text-align: center;
1572 text-align: center;
1575 }
1573 }
1576
1574
1577 /*chmod file*/
1575 /*chmod file*/
1578 .bin.bin6 {
1576 .bin.bin6 {
1579 background-color: @grey2;
1577 background-color: @grey2;
1580 text-align: center;
1578 text-align: center;
1581 }
1579 }
1582 }
1580 }
1583 }
1581 }
1584
1582
1585 .cs_files .cs_added, .cs_files .cs_A,
1583 .cs_files .cs_added, .cs_files .cs_A,
1586 .cs_files .cs_added, .cs_files .cs_M,
1584 .cs_files .cs_added, .cs_files .cs_M,
1587 .cs_files .cs_added, .cs_files .cs_D {
1585 .cs_files .cs_added, .cs_files .cs_D {
1588 height: 16px;
1586 height: 16px;
1589 padding-right: 10px;
1587 padding-right: 10px;
1590 margin-top: 7px;
1588 margin-top: 7px;
1591 text-align: left;
1589 text-align: left;
1592 }
1590 }
1593
1591
1594 .cs_icon_td {
1592 .cs_icon_td {
1595 min-width: 16px;
1593 min-width: 16px;
1596 width: 16px;
1594 width: 16px;
1597 }
1595 }
1598
1596
1599 .pull-request-merge {
1597 .pull-request-merge {
1600 padding: 10px 0;
1598 padding: 10px 0;
1601 margin-top: 10px;
1599 margin-top: 10px;
1602 margin-bottom: 20px;
1600 margin-bottom: 20px;
1603 }
1601 }
1604
1602
1605 .pull-request-merge .pull-request-wrap {
1603 .pull-request-merge .pull-request-wrap {
1606 height: 25px;
1604 height: 25px;
1607 padding: 5px 0;
1605 padding: 5px 0;
1608 }
1606 }
1609
1607
1610 .pull-request-merge span {
1608 .pull-request-merge span {
1611 margin-right: 10px;
1609 margin-right: 10px;
1612 }
1610 }
1613 #close_pull_request {
1611 #close_pull_request {
1614 margin-right: 0px;
1612 margin-right: 0px;
1615 }
1613 }
1616
1614
1617 .empty_data {
1615 .empty_data {
1618 color: @grey4;
1616 color: @grey4;
1619 }
1617 }
1620
1618
1621 #changeset_compare_view_content {
1619 #changeset_compare_view_content {
1622 margin-bottom: @space;
1620 margin-bottom: @space;
1623 clear: both;
1621 clear: both;
1624 width: 100%;
1622 width: 100%;
1625 box-sizing: border-box;
1623 box-sizing: border-box;
1626 .border-radius(@border-radius);
1624 .border-radius(@border-radius);
1627
1625
1628 .help-block {
1626 .help-block {
1629 margin: @padding 0;
1627 margin: @padding 0;
1630 color: @text-color;
1628 color: @text-color;
1631 }
1629 }
1632
1630
1633 .empty_data {
1631 .empty_data {
1634 margin: @padding 0;
1632 margin: @padding 0;
1635 }
1633 }
1636
1634
1637 .alert {
1635 .alert {
1638 margin-bottom: @space;
1636 margin-bottom: @space;
1639 }
1637 }
1640 }
1638 }
1641
1639
1642 .table_disp {
1640 .table_disp {
1643 .status {
1641 .status {
1644 width: auto;
1642 width: auto;
1645
1643
1646 .flag_status {
1644 .flag_status {
1647 float: left;
1645 float: left;
1648 }
1646 }
1649 }
1647 }
1650 }
1648 }
1651
1649
1652 .status_box_menu {
1650 .status_box_menu {
1653 margin: 0;
1651 margin: 0;
1654 }
1652 }
1655
1653
1656 .notification-table{
1654 .notification-table{
1657 margin-bottom: @space;
1655 margin-bottom: @space;
1658 display: table;
1656 display: table;
1659 width: 100%;
1657 width: 100%;
1660
1658
1661 .container{
1659 .container{
1662 display: table-row;
1660 display: table-row;
1663
1661
1664 .notification-header{
1662 .notification-header{
1665 border-bottom: @border-thickness solid @border-default-color;
1663 border-bottom: @border-thickness solid @border-default-color;
1666 }
1664 }
1667
1665
1668 .notification-subject{
1666 .notification-subject{
1669 display: table-cell;
1667 display: table-cell;
1670 }
1668 }
1671 }
1669 }
1672 }
1670 }
1673
1671
1674 // Notifications
1672 // Notifications
1675 .notification-header{
1673 .notification-header{
1676 display: table;
1674 display: table;
1677 width: 100%;
1675 width: 100%;
1678 padding: floor(@basefontsize/2) 0;
1676 padding: floor(@basefontsize/2) 0;
1679 line-height: 1em;
1677 line-height: 1em;
1680
1678
1681 .desc, .delete-notifications, .read-notifications{
1679 .desc, .delete-notifications, .read-notifications{
1682 display: table-cell;
1680 display: table-cell;
1683 text-align: left;
1681 text-align: left;
1684 }
1682 }
1685
1683
1686 .desc{
1684 .desc{
1687 width: 1163px;
1685 width: 1163px;
1688 }
1686 }
1689
1687
1690 .delete-notifications, .read-notifications{
1688 .delete-notifications, .read-notifications{
1691 width: 35px;
1689 width: 35px;
1692 min-width: 35px; //fixes when only one button is displayed
1690 min-width: 35px; //fixes when only one button is displayed
1693 }
1691 }
1694 }
1692 }
1695
1693
1696 .notification-body {
1694 .notification-body {
1697 .markdown-block,
1695 .markdown-block,
1698 .rst-block {
1696 .rst-block {
1699 padding: @padding 0;
1697 padding: @padding 0;
1700 }
1698 }
1701
1699
1702 .notification-subject {
1700 .notification-subject {
1703 padding: @textmargin 0;
1701 padding: @textmargin 0;
1704 border-bottom: @border-thickness solid @border-default-color;
1702 border-bottom: @border-thickness solid @border-default-color;
1705 }
1703 }
1706 }
1704 }
1707
1705
1708
1706
1709 .notifications_buttons{
1707 .notifications_buttons{
1710 float: right;
1708 float: right;
1711 }
1709 }
1712
1710
1713 #notification-status{
1711 #notification-status{
1714 display: inline;
1712 display: inline;
1715 }
1713 }
1716
1714
1717 // Repositories
1715 // Repositories
1718
1716
1719 #summary.fields{
1717 #summary.fields{
1720 display: table;
1718 display: table;
1721
1719
1722 .field{
1720 .field{
1723 display: table-row;
1721 display: table-row;
1724
1722
1725 .label-summary{
1723 .label-summary{
1726 display: table-cell;
1724 display: table-cell;
1727 min-width: @label-summary-minwidth;
1725 min-width: @label-summary-minwidth;
1728 padding-top: @padding/2;
1726 padding-top: @padding/2;
1729 padding-bottom: @padding/2;
1727 padding-bottom: @padding/2;
1730 padding-right: @padding/2;
1728 padding-right: @padding/2;
1731 }
1729 }
1732
1730
1733 .input{
1731 .input{
1734 display: table-cell;
1732 display: table-cell;
1735 padding: @padding/2;
1733 padding: @padding/2;
1736
1734
1737 input{
1735 input{
1738 min-width: 29em;
1736 min-width: 29em;
1739 padding: @padding/4;
1737 padding: @padding/4;
1740 }
1738 }
1741 }
1739 }
1742 .statistics, .downloads{
1740 .statistics, .downloads{
1743 .disabled{
1741 .disabled{
1744 color: @grey4;
1742 color: @grey4;
1745 }
1743 }
1746 }
1744 }
1747 }
1745 }
1748 }
1746 }
1749
1747
1750 #summary{
1748 #summary{
1751 width: 70%;
1749 width: 70%;
1752 }
1750 }
1753
1751
1754
1752
1755 // Journal
1753 // Journal
1756 .journal.title {
1754 .journal.title {
1757 h5 {
1755 h5 {
1758 float: left;
1756 float: left;
1759 margin: 0;
1757 margin: 0;
1760 width: 70%;
1758 width: 70%;
1761 }
1759 }
1762
1760
1763 ul {
1761 ul {
1764 float: right;
1762 float: right;
1765 display: inline-block;
1763 display: inline-block;
1766 margin: 0;
1764 margin: 0;
1767 width: 30%;
1765 width: 30%;
1768 text-align: right;
1766 text-align: right;
1769
1767
1770 li {
1768 li {
1771 display: inline;
1769 display: inline;
1772 font-size: @journal-fontsize;
1770 font-size: @journal-fontsize;
1773 line-height: 1em;
1771 line-height: 1em;
1774
1772
1775 &:before { content: none; }
1773 &:before { content: none; }
1776 }
1774 }
1777 }
1775 }
1778 }
1776 }
1779
1777
1780 .filterexample {
1778 .filterexample {
1781 position: absolute;
1779 position: absolute;
1782 top: 95px;
1780 top: 95px;
1783 left: @contentpadding;
1781 left: @contentpadding;
1784 color: @rcblue;
1782 color: @rcblue;
1785 font-size: 11px;
1783 font-size: 11px;
1786 font-family: @text-regular;
1784 font-family: @text-regular;
1787 cursor: help;
1785 cursor: help;
1788
1786
1789 &:hover {
1787 &:hover {
1790 color: @rcdarkblue;
1788 color: @rcdarkblue;
1791 }
1789 }
1792
1790
1793 @media (max-width:768px) {
1791 @media (max-width:768px) {
1794 position: relative;
1792 position: relative;
1795 top: auto;
1793 top: auto;
1796 left: auto;
1794 left: auto;
1797 display: block;
1795 display: block;
1798 }
1796 }
1799 }
1797 }
1800
1798
1801
1799
1802 #journal{
1800 #journal{
1803 margin-bottom: @space;
1801 margin-bottom: @space;
1804
1802
1805 .journal_day{
1803 .journal_day{
1806 margin-bottom: @textmargin/2;
1804 margin-bottom: @textmargin/2;
1807 padding-bottom: @textmargin/2;
1805 padding-bottom: @textmargin/2;
1808 font-size: @journal-fontsize;
1806 font-size: @journal-fontsize;
1809 border-bottom: @border-thickness solid @border-default-color;
1807 border-bottom: @border-thickness solid @border-default-color;
1810 }
1808 }
1811
1809
1812 .journal_container{
1810 .journal_container{
1813 margin-bottom: @space;
1811 margin-bottom: @space;
1814
1812
1815 .journal_user{
1813 .journal_user{
1816 display: inline-block;
1814 display: inline-block;
1817 }
1815 }
1818 .journal_action_container{
1816 .journal_action_container{
1819 display: block;
1817 display: block;
1820 margin-top: @textmargin;
1818 margin-top: @textmargin;
1821
1819
1822 div{
1820 div{
1823 display: inline;
1821 display: inline;
1824 }
1822 }
1825
1823
1826 div.journal_action_params{
1824 div.journal_action_params{
1827 display: block;
1825 display: block;
1828 }
1826 }
1829
1827
1830 div.journal_repo:after{
1828 div.journal_repo:after{
1831 content: "\A";
1829 content: "\A";
1832 white-space: pre;
1830 white-space: pre;
1833 }
1831 }
1834
1832
1835 div.date{
1833 div.date{
1836 display: block;
1834 display: block;
1837 margin-bottom: @textmargin;
1835 margin-bottom: @textmargin;
1838 }
1836 }
1839 }
1837 }
1840 }
1838 }
1841 }
1839 }
1842
1840
1843 // Files
1841 // Files
1844 .edit-file-title {
1842 .edit-file-title {
1845 border-bottom: @border-thickness solid @border-default-color;
1843 border-bottom: @border-thickness solid @border-default-color;
1846
1844
1847 .breadcrumbs {
1845 .breadcrumbs {
1848 margin-bottom: 0;
1846 margin-bottom: 0;
1849 }
1847 }
1850 }
1848 }
1851
1849
1852 .edit-file-fieldset {
1850 .edit-file-fieldset {
1853 margin-top: @sidebarpadding;
1851 margin-top: @sidebarpadding;
1854
1852
1855 .fieldset {
1853 .fieldset {
1856 .left-label {
1854 .left-label {
1857 width: 13%;
1855 width: 13%;
1858 }
1856 }
1859 .right-content {
1857 .right-content {
1860 width: 87%;
1858 width: 87%;
1861 max-width: 100%;
1859 max-width: 100%;
1862 }
1860 }
1863 .filename-label {
1861 .filename-label {
1864 margin-top: 13px;
1862 margin-top: 13px;
1865 }
1863 }
1866 .commit-message-label {
1864 .commit-message-label {
1867 margin-top: 4px;
1865 margin-top: 4px;
1868 }
1866 }
1869 .file-upload-input {
1867 .file-upload-input {
1870 input {
1868 input {
1871 display: none;
1869 display: none;
1872 }
1870 }
1873 }
1871 }
1874 p {
1872 p {
1875 margin-top: 5px;
1873 margin-top: 5px;
1876 }
1874 }
1877
1875
1878 }
1876 }
1879 .custom-path-link {
1877 .custom-path-link {
1880 margin-left: 5px;
1878 margin-left: 5px;
1881 }
1879 }
1882 #commit {
1880 #commit {
1883 resize: vertical;
1881 resize: vertical;
1884 }
1882 }
1885 }
1883 }
1886
1884
1887 .delete-file-preview {
1885 .delete-file-preview {
1888 max-height: 250px;
1886 max-height: 250px;
1889 }
1887 }
1890
1888
1891 .new-file,
1889 .new-file,
1892 #filter_activate,
1890 #filter_activate,
1893 #filter_deactivate {
1891 #filter_deactivate {
1894 float: left;
1892 float: left;
1895 margin: 0 0 0 15px;
1893 margin: 0 0 0 15px;
1896 }
1894 }
1897
1895
1898 h3.files_location{
1896 h3.files_location{
1899 line-height: 2.4em;
1897 line-height: 2.4em;
1900 }
1898 }
1901
1899
1902 .browser-nav {
1900 .browser-nav {
1903 display: table;
1901 display: table;
1904 margin-bottom: @space;
1902 margin-bottom: @space;
1905
1903
1906
1904
1907 .info_box {
1905 .info_box {
1908 display: inline-table;
1906 display: inline-table;
1909 height: 2.5em;
1907 height: 2.5em;
1910
1908
1911 .browser-cur-rev, .info_box_elem {
1909 .browser-cur-rev, .info_box_elem {
1912 display: table-cell;
1910 display: table-cell;
1913 vertical-align: middle;
1911 vertical-align: middle;
1914 }
1912 }
1915
1913
1916 .info_box_elem {
1914 .info_box_elem {
1917 border-top: @border-thickness solid @rcblue;
1915 border-top: @border-thickness solid @rcblue;
1918 border-bottom: @border-thickness solid @rcblue;
1916 border-bottom: @border-thickness solid @rcblue;
1919
1917
1920 #at_rev, a {
1918 #at_rev, a {
1921 padding: 0.6em 0.9em;
1919 padding: 0.6em 0.9em;
1922 margin: 0;
1920 margin: 0;
1923 .box-shadow(none);
1921 .box-shadow(none);
1924 border: 0;
1922 border: 0;
1925 height: 12px;
1923 height: 12px;
1926 }
1924 }
1927
1925
1928 input#at_rev {
1926 input#at_rev {
1929 max-width: 50px;
1927 max-width: 50px;
1930 text-align: right;
1928 text-align: right;
1931 }
1929 }
1932
1930
1933 &.previous {
1931 &.previous {
1934 border: @border-thickness solid @rcblue;
1932 border: @border-thickness solid @rcblue;
1935 .disabled {
1933 .disabled {
1936 color: @grey4;
1934 color: @grey4;
1937 cursor: not-allowed;
1935 cursor: not-allowed;
1938 }
1936 }
1939 }
1937 }
1940
1938
1941 &.next {
1939 &.next {
1942 border: @border-thickness solid @rcblue;
1940 border: @border-thickness solid @rcblue;
1943 .disabled {
1941 .disabled {
1944 color: @grey4;
1942 color: @grey4;
1945 cursor: not-allowed;
1943 cursor: not-allowed;
1946 }
1944 }
1947 }
1945 }
1948 }
1946 }
1949
1947
1950 .browser-cur-rev {
1948 .browser-cur-rev {
1951
1949
1952 span{
1950 span{
1953 margin: 0;
1951 margin: 0;
1954 color: @rcblue;
1952 color: @rcblue;
1955 height: 12px;
1953 height: 12px;
1956 display: inline-block;
1954 display: inline-block;
1957 padding: 0.7em 1em ;
1955 padding: 0.7em 1em ;
1958 border: @border-thickness solid @rcblue;
1956 border: @border-thickness solid @rcblue;
1959 margin-right: @padding;
1957 margin-right: @padding;
1960 }
1958 }
1961 }
1959 }
1962 }
1960 }
1963
1961
1964 .search_activate {
1962 .search_activate {
1965 display: table-cell;
1963 display: table-cell;
1966 vertical-align: middle;
1964 vertical-align: middle;
1967
1965
1968 input, label{
1966 input, label{
1969 margin: 0;
1967 margin: 0;
1970 padding: 0;
1968 padding: 0;
1971 }
1969 }
1972
1970
1973 input{
1971 input{
1974 margin-left: @textmargin;
1972 margin-left: @textmargin;
1975 }
1973 }
1976
1974
1977 }
1975 }
1978 }
1976 }
1979
1977
1980 .browser-cur-rev{
1978 .browser-cur-rev{
1981 margin-bottom: @textmargin;
1979 margin-bottom: @textmargin;
1982 }
1980 }
1983
1981
1984 #node_filter_box_loading{
1982 #node_filter_box_loading{
1985 .info_text;
1983 .info_text;
1986 }
1984 }
1987
1985
1988 .browser-search {
1986 .browser-search {
1989 margin: -25px 0px 5px 0px;
1987 margin: -25px 0px 5px 0px;
1990 }
1988 }
1991
1989
1992 .node-filter {
1990 .node-filter {
1993 font-size: @repo-title-fontsize;
1991 font-size: @repo-title-fontsize;
1994 padding: 4px 0px 0px 0px;
1992 padding: 4px 0px 0px 0px;
1995
1993
1996 .node-filter-path {
1994 .node-filter-path {
1997 float: left;
1995 float: left;
1998 color: @grey4;
1996 color: @grey4;
1999 }
1997 }
2000 .node-filter-input {
1998 .node-filter-input {
2001 float: left;
1999 float: left;
2002 margin: -2px 0px 0px 2px;
2000 margin: -2px 0px 0px 2px;
2003 input {
2001 input {
2004 padding: 2px;
2002 padding: 2px;
2005 border: none;
2003 border: none;
2006 font-size: @repo-title-fontsize;
2004 font-size: @repo-title-fontsize;
2007 }
2005 }
2008 }
2006 }
2009 }
2007 }
2010
2008
2011
2009
2012 .browser-result{
2010 .browser-result{
2013 td a{
2011 td a{
2014 margin-left: 0.5em;
2012 margin-left: 0.5em;
2015 display: inline-block;
2013 display: inline-block;
2016
2014
2017 em{
2015 em{
2018 font-family: @text-bold;
2016 font-family: @text-bold;
2019 }
2017 }
2020 }
2018 }
2021 }
2019 }
2022
2020
2023 .browser-highlight{
2021 .browser-highlight{
2024 background-color: @grey5-alpha;
2022 background-color: @grey5-alpha;
2025 }
2023 }
2026
2024
2027
2025
2028 // Search
2026 // Search
2029
2027
2030 .search-form{
2028 .search-form{
2031 #q {
2029 #q {
2032 width: @search-form-width;
2030 width: @search-form-width;
2033 }
2031 }
2034 .fields{
2032 .fields{
2035 margin: 0 0 @space;
2033 margin: 0 0 @space;
2036 }
2034 }
2037
2035
2038 label{
2036 label{
2039 display: inline-block;
2037 display: inline-block;
2040 margin-right: @textmargin;
2038 margin-right: @textmargin;
2041 padding-top: 0.25em;
2039 padding-top: 0.25em;
2042 }
2040 }
2043
2041
2044
2042
2045 .results{
2043 .results{
2046 clear: both;
2044 clear: both;
2047 margin: 0 0 @padding;
2045 margin: 0 0 @padding;
2048 }
2046 }
2049 }
2047 }
2050
2048
2051 div.search-feedback-items {
2049 div.search-feedback-items {
2052 display: inline-block;
2050 display: inline-block;
2053 padding:0px 0px 0px 96px;
2051 padding:0px 0px 0px 96px;
2054 }
2052 }
2055
2053
2056 div.search-code-body {
2054 div.search-code-body {
2057 background-color: #ffffff; padding: 5px 0 5px 10px;
2055 background-color: #ffffff; padding: 5px 0 5px 10px;
2058 pre {
2056 pre {
2059 .match { background-color: #faffa6;}
2057 .match { background-color: #faffa6;}
2060 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2058 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2061 }
2059 }
2062 }
2060 }
2063
2061
2064 .expand_commit.search {
2062 .expand_commit.search {
2065 .show_more.open {
2063 .show_more.open {
2066 height: auto;
2064 height: auto;
2067 max-height: none;
2065 max-height: none;
2068 }
2066 }
2069 }
2067 }
2070
2068
2071 .search-results {
2069 .search-results {
2072
2070
2073 h2 {
2071 h2 {
2074 margin-bottom: 0;
2072 margin-bottom: 0;
2075 }
2073 }
2076 .codeblock {
2074 .codeblock {
2077 border: none;
2075 border: none;
2078 background: transparent;
2076 background: transparent;
2079 }
2077 }
2080
2078
2081 .codeblock-header {
2079 .codeblock-header {
2082 border: none;
2080 border: none;
2083 background: transparent;
2081 background: transparent;
2084 }
2082 }
2085
2083
2086 .code-body {
2084 .code-body {
2087 border: @border-thickness solid @border-default-color;
2085 border: @border-thickness solid @border-default-color;
2088 .border-radius(@border-radius);
2086 .border-radius(@border-radius);
2089 }
2087 }
2090
2088
2091 .td-commit {
2089 .td-commit {
2092 &:extend(pre);
2090 &:extend(pre);
2093 border-bottom: @border-thickness solid @border-default-color;
2091 border-bottom: @border-thickness solid @border-default-color;
2094 }
2092 }
2095
2093
2096 .message {
2094 .message {
2097 height: auto;
2095 height: auto;
2098 max-width: 350px;
2096 max-width: 350px;
2099 white-space: normal;
2097 white-space: normal;
2100 text-overflow: initial;
2098 text-overflow: initial;
2101 overflow: visible;
2099 overflow: visible;
2102
2100
2103 .match { background-color: #faffa6;}
2101 .match { background-color: #faffa6;}
2104 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2102 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2105 }
2103 }
2106
2104
2107 }
2105 }
2108
2106
2109 table.rctable td.td-search-results div {
2107 table.rctable td.td-search-results div {
2110 max-width: 100%;
2108 max-width: 100%;
2111 }
2109 }
2112
2110
2113 #tip-box, .tip-box{
2111 #tip-box, .tip-box{
2114 padding: @menupadding/2;
2112 padding: @menupadding/2;
2115 display: block;
2113 display: block;
2116 border: @border-thickness solid @border-highlight-color;
2114 border: @border-thickness solid @border-highlight-color;
2117 .border-radius(@border-radius);
2115 .border-radius(@border-radius);
2118 background-color: white;
2116 background-color: white;
2119 z-index: 99;
2117 z-index: 99;
2120 white-space: pre-wrap;
2118 white-space: pre-wrap;
2121 }
2119 }
2122
2120
2123 #linktt {
2121 #linktt {
2124 width: 79px;
2122 width: 79px;
2125 }
2123 }
2126
2124
2127 #help_kb .modal-content{
2125 #help_kb .modal-content{
2128 max-width: 750px;
2126 max-width: 750px;
2129 margin: 10% auto;
2127 margin: 10% auto;
2130
2128
2131 table{
2129 table{
2132 td,th{
2130 td,th{
2133 border-bottom: none;
2131 border-bottom: none;
2134 line-height: 2.5em;
2132 line-height: 2.5em;
2135 }
2133 }
2136 th{
2134 th{
2137 padding-bottom: @textmargin/2;
2135 padding-bottom: @textmargin/2;
2138 }
2136 }
2139 td.keys{
2137 td.keys{
2140 text-align: center;
2138 text-align: center;
2141 }
2139 }
2142 }
2140 }
2143
2141
2144 .block-left{
2142 .block-left{
2145 width: 45%;
2143 width: 45%;
2146 margin-right: 5%;
2144 margin-right: 5%;
2147 }
2145 }
2148 .modal-footer{
2146 .modal-footer{
2149 clear: both;
2147 clear: both;
2150 }
2148 }
2151 .key.tag{
2149 .key.tag{
2152 padding: 0.5em;
2150 padding: 0.5em;
2153 background-color: @rcblue;
2151 background-color: @rcblue;
2154 color: white;
2152 color: white;
2155 border-color: @rcblue;
2153 border-color: @rcblue;
2156 .box-shadow(none);
2154 .box-shadow(none);
2157 }
2155 }
2158 }
2156 }
2159
2157
2160
2158
2161
2159
2162 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2160 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2163
2161
2164 @import 'statistics-graph';
2162 @import 'statistics-graph';
2165 @import 'tables';
2163 @import 'tables';
2166 @import 'forms';
2164 @import 'forms';
2167 @import 'diff';
2165 @import 'diff';
2168 @import 'summary';
2166 @import 'summary';
2169 @import 'navigation';
2167 @import 'navigation';
2170
2168
2171 //--- SHOW/HIDE SECTIONS --//
2169 //--- SHOW/HIDE SECTIONS --//
2172
2170
2173 .btn-collapse {
2171 .btn-collapse {
2174 float: right;
2172 float: right;
2175 text-align: right;
2173 text-align: right;
2176 font-family: @text-light;
2174 font-family: @text-light;
2177 font-size: @basefontsize;
2175 font-size: @basefontsize;
2178 cursor: pointer;
2176 cursor: pointer;
2179 border: none;
2177 border: none;
2180 color: @rcblue;
2178 color: @rcblue;
2181 }
2179 }
2182
2180
2183 table.rctable,
2181 table.rctable,
2184 table.dataTable {
2182 table.dataTable {
2185 .btn-collapse {
2183 .btn-collapse {
2186 float: right;
2184 float: right;
2187 text-align: right;
2185 text-align: right;
2188 }
2186 }
2189 }
2187 }
2190
2188
2191
2189
2192 // TODO: johbo: Fix for IE10, this avoids that we see a border
2190 // TODO: johbo: Fix for IE10, this avoids that we see a border
2193 // and padding around checkboxes and radio boxes. Move to the right place,
2191 // and padding around checkboxes and radio boxes. Move to the right place,
2194 // or better: Remove this once we did the form refactoring.
2192 // or better: Remove this once we did the form refactoring.
2195 input[type=checkbox],
2193 input[type=checkbox],
2196 input[type=radio] {
2194 input[type=radio] {
2197 padding: 0;
2195 padding: 0;
2198 border: none;
2196 border: none;
2199 }
2197 }
2200
2198
2201 .toggle-ajax-spinner{
2199 .toggle-ajax-spinner{
2202 height: 16px;
2200 height: 16px;
2203 width: 16px;
2201 width: 16px;
2204 }
2202 }
@@ -1,895 +1,627 b''
1 // # Copyright (C) 2010-2016 RhodeCode GmbH
1 // # Copyright (C) 2010-2016 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 var removeInlineForm = function(form) {
52 form.parentNode.removeChild(form);
53 };
54
55 var createInlineForm = function(parent_tr, f_path, line) {
56 var tmpl = $('#comment-inline-form-template').html();
57 tmpl = tmpl.format(f_path, line);
58 var form = tableTr('comment-form-inline', tmpl);
59 var form_hide_button = $(form).find('.hide-inline-form');
60
61 $(form_hide_button).click(function(e) {
62 $('.inline-comments').removeClass('hide-comment-button');
63 var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
64 if ($(newtr.nextElementSibling).hasClass('inline-comments-button')) {
65 $(newtr.nextElementSibling).show();
66 }
67 $(newtr).parents('.comment-form-inline').remove();
68 $(parent_tr).removeClass('form-open');
69 $(parent_tr).removeClass('hl-comment');
70 });
71
72 return form;
73 };
74
75 var getLineNo = function(tr) {
76 var line;
77 // Try to get the id and return "" (empty string) if it doesn't exist
78 var o = ($(tr).find('.lineno.old').attr('id')||"").split('_');
79 var n = ($(tr).find('.lineno.new').attr('id')||"").split('_');
80 if (n.length >= 2) {
81 line = n[n.length-1];
82 } else if (o.length >= 2) {
83 line = o[o.length-1];
84 }
85 return line;
86 };
87
88 /**
89 * make a single inline comment and place it inside
90 */
91 var renderInlineComment = function(json_data, show_add_button) {
92 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
93 try {
94 var html = json_data.rendered_text;
95 var lineno = json_data.line_no;
96 var target_id = json_data.target_id;
97 placeInline(target_id, lineno, html, show_add_button);
98 } catch (e) {
99 console.error(e);
100 }
101 };
102
103 function bindDeleteCommentButtons() {
51 function bindDeleteCommentButtons() {
104 $('.delete-comment').one('click', function() {
52 $('.delete-comment').one('click', function() {
105 var comment_id = $(this).data("comment-id");
53 var comment_id = $(this).data("comment-id");
106
54
107 if (comment_id){
55 if (comment_id){
108 deleteComment(comment_id);
56 deleteComment(comment_id);
109 }
57 }
110 });
58 });
111 }
59 }
112
60
113 /**
114 * Inject inline comment for on given TR this tr should be always an .line
115 * tr containing the line. Code will detect comment, and always put the comment
116 * block at the very bottom
117 */
118 var injectInlineForm = function(tr){
119 if (!$(tr).hasClass('line')) {
120 return;
121 }
122
123 var _td = $(tr).find('.code').get(0);
124 if ($(tr).hasClass('form-open') ||
125 $(tr).hasClass('context') ||
126 $(_td).hasClass('no-comment')) {
127 return;
128 }
129 $(tr).addClass('form-open');
130 $(tr).addClass('hl-comment');
131 var node = $(tr.parentNode.parentNode.parentNode).find('.full_f_path').get(0);
132 var f_path = $(node).attr('path');
133 var lineno = getLineNo(tr);
134 var form = createInlineForm(tr, f_path, lineno);
135
136 var parent = tr;
137 while (1) {
138 var n = parent.nextElementSibling;
139 // next element are comments !
140 if ($(n).hasClass('inline-comments')) {
141 parent = n;
142 }
143 else {
144 break;
145 }
146 }
147 var _parent = $(parent).get(0);
148 $(_parent).after(form);
149 $('.comment-form-inline').prev('.inline-comments').addClass('hide-comment-button');
150 var f = $(form).get(0);
151
152 var _form = $(f).find('.inline-form').get(0);
153
154 var pullRequestId = templateContext.pull_request_data.pull_request_id;
155 var commitId = templateContext.commit_data.commit_id;
156
157 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
158 var cm = commentForm.getCmInstance();
159
160 // set a CUSTOM submit handler for inline comments.
161 commentForm.setHandleFormSubmit(function(o) {
162 var text = commentForm.cm.getValue();
163
164 if (text === "") {
165 return;
166 }
167
168 if (lineno === undefined) {
169 alert('missing line !');
170 return;
171 }
172 if (f_path === undefined) {
173 alert('missing file path !');
174 return;
175 }
176
177 var excludeCancelBtn = false;
178 var submitEvent = true;
179 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
180 commentForm.cm.setOption("readOnly", true);
181 var postData = {
182 'text': text,
183 'f_path': f_path,
184 'line': lineno,
185 'csrf_token': CSRF_TOKEN
186 };
187 var submitSuccessCallback = function(o) {
188 $(tr).removeClass('form-open');
189 removeInlineForm(f);
190 renderInlineComment(o);
191 $('.inline-comments').removeClass('hide-comment-button');
192
193 // re trigger the linkification of next/prev navigation
194 linkifyComments($('.inline-comment-injected'));
195 timeagoActivate();
196 bindDeleteCommentButtons();
197 commentForm.setActionButtonsDisabled(false);
198
199 };
200 var submitFailCallback = function(){
201 commentForm.resetCommentFormState(text)
202 };
203 commentForm.submitAjaxPOST(
204 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
205 });
206
207 setTimeout(function() {
208 // callbacks
209 if (cm !== undefined) {
210 cm.focus();
211 }
212 }, 10);
213
214 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
215 form:_form,
216 parent:_parent,
217 lineno: lineno,
218 f_path: f_path}
219 );
220 };
221
222 var deleteComment = function(comment_id) {
61 var deleteComment = function(comment_id) {
223 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
62 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
224 var postData = {
63 var postData = {
225 '_method': 'delete',
64 '_method': 'delete',
226 'csrf_token': CSRF_TOKEN
65 'csrf_token': CSRF_TOKEN
227 };
66 };
228
67
229 var success = function(o) {
68 var success = function(o) {
230 window.location.reload();
69 window.location.reload();
231 };
70 };
232 ajaxPOST(url, postData, success);
71 ajaxPOST(url, postData, success);
233 };
72 };
234
73
235 var createInlineAddButton = function(tr){
236 var label = _gettext('Add another comment');
237 var html_el = document.createElement('div');
238 $(html_el).addClass('add-comment');
239 html_el.innerHTML = '<span class="btn btn-secondary">{0}</span>'.format(label);
240 var add = new $(html_el);
241 add.on('click', function(e) {
242 injectInlineForm(tr);
243 });
244 return add;
245 };
246
247 var placeAddButton = function(target_tr){
248 if(!target_tr){
249 return;
250 }
251 var last_node = target_tr;
252 // scan
253 while (1){
254 var n = last_node.nextElementSibling;
255 // next element are comments !
256 if($(n).hasClass('inline-comments')){
257 last_node = n;
258 // also remove the comment button from previous
259 var comment_add_buttons = $(last_node).find('.add-comment');
260 for(var i=0; i<comment_add_buttons.length; i++){
261 var b = comment_add_buttons[i];
262 b.parentNode.removeChild(b);
263 }
264 }
265 else{
266 break;
267 }
268 }
269 var add = createInlineAddButton(target_tr);
270 // get the comment div
271 var comment_block = $(last_node).find('.comment')[0];
272 // attach add button
273 $(add).insertAfter(comment_block);
274 };
275
276 /**
277 * Places the inline comment into the changeset block in proper line position
278 */
279 var placeInline = function(target_container, lineno, html, show_add_button) {
280 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
281
282 var lineid = "{0}_{1}".format(target_container, lineno);
283 var target_line = $('#' + lineid).get(0);
284 var comment = new $(tableTr('inline-comments', html));
285 // check if there are comments already !
286 if (target_line) {
287 var parent_node = target_line.parentNode;
288 var root_parent = parent_node;
289
290 while (1) {
291 var n = parent_node.nextElementSibling;
292 // next element are comments !
293 if ($(n).hasClass('inline-comments')) {
294 parent_node = n;
295 }
296 else {
297 break;
298 }
299 }
300 // put in the comment at the bottom
301 $(comment).insertAfter(parent_node);
302 $(comment).find('.comment-inline').addClass('inline-comment-injected');
303 // scan nodes, and attach add button to last one
304 if (show_add_button) {
305 placeAddButton(root_parent);
306 }
307 addCommentToggle(target_line);
308 }
309
310 return target_line;
311 };
312
313 var addCommentToggle = function(target_line) {
314 // exposes comment toggle button
315 $(target_line).siblings('.comment-toggle').addClass('active');
316 return;
317 };
318
74
319 var bindToggleButtons = function() {
75 var bindToggleButtons = function() {
320 $('.comment-toggle').on('click', function() {
76 $('.comment-toggle').on('click', function() {
321 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
77 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
322 });
78 });
323 };
79 };
324
80
325 var linkifyComments = function(comments) {
81 var linkifyComments = function(comments) {
326 /* TODO: dan: remove this - it should no longer needed */
82 /* TODO: dan: remove this - it should no longer needed */
327 for (var i = 0; i < comments.length; i++) {
83 for (var i = 0; i < comments.length; i++) {
328 var comment_id = $(comments[i]).data('comment-id');
84 var comment_id = $(comments[i]).data('comment-id');
329 var prev_comment_id = $(comments[i - 1]).data('comment-id');
85 var prev_comment_id = $(comments[i - 1]).data('comment-id');
330 var next_comment_id = $(comments[i + 1]).data('comment-id');
86 var next_comment_id = $(comments[i + 1]).data('comment-id');
331
87
332 // place next/prev links
88 // place next/prev links
333 if (prev_comment_id) {
89 if (prev_comment_id) {
334 $('#prev_c_' + comment_id).show();
90 $('#prev_c_' + comment_id).show();
335 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
91 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
336 'href', '#comment-' + prev_comment_id).removeClass('disabled');
92 'href', '#comment-' + prev_comment_id).removeClass('disabled');
337 }
93 }
338 if (next_comment_id) {
94 if (next_comment_id) {
339 $('#next_c_' + comment_id).show();
95 $('#next_c_' + comment_id).show();
340 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
96 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
341 'href', '#comment-' + next_comment_id).removeClass('disabled');
97 'href', '#comment-' + next_comment_id).removeClass('disabled');
342 }
98 }
343 // place a first link to the total counter
99 // place a first link to the total counter
344 if (i === 0) {
100 if (i === 0) {
345 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
101 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
346 }
102 }
347 }
103 }
348
104
349 };
105 };
350
106
351 /**
352 * Iterates over all the inlines, and places them inside proper blocks of data
353 */
354 var renderInlineComments = function(file_comments, show_add_button) {
355 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
356
357 for (var i = 0; i < file_comments.length; i++) {
358 var box = file_comments[i];
359
360 var target_id = $(box).attr('target_id');
361
362 // actually comments with line numbers
363 var comments = box.children;
364
365 for (var j = 0; j < comments.length; j++) {
366 var data = {
367 'rendered_text': comments[j].outerHTML,
368 'line_no': $(comments[j]).attr('line'),
369 'target_id': target_id
370 };
371 renderInlineComment(data, show_add_button);
372 }
373 }
374
375 // since order of injection is random, we're now re-iterating
376 // from correct order and filling in links
377 linkifyComments($('.inline-comment-injected'));
378 bindDeleteCommentButtons();
379 firefoxAnchorFix();
380 };
381
382
107
383 /* Comment form for main and inline comments */
108 /* Comment form for main and inline comments */
384 var CommentForm = (function() {
109 var CommentForm = (function() {
385 "use strict";
110 "use strict";
386
111
387 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
112 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
388
113
389 this.withLineNo = function(selector) {
114 this.withLineNo = function(selector) {
390 var lineNo = this.lineNo;
115 var lineNo = this.lineNo;
391 if (lineNo === undefined) {
116 if (lineNo === undefined) {
392 return selector
117 return selector
393 } else {
118 } else {
394 return selector + '_' + lineNo;
119 return selector + '_' + lineNo;
395 }
120 }
396 };
121 };
397
122
398 this.commitId = commitId;
123 this.commitId = commitId;
399 this.pullRequestId = pullRequestId;
124 this.pullRequestId = pullRequestId;
400 this.lineNo = lineNo;
125 this.lineNo = lineNo;
401 this.initAutocompleteActions = initAutocompleteActions;
126 this.initAutocompleteActions = initAutocompleteActions;
402
127
403 this.previewButton = this.withLineNo('#preview-btn');
128 this.previewButton = this.withLineNo('#preview-btn');
404 this.previewContainer = this.withLineNo('#preview-container');
129 this.previewContainer = this.withLineNo('#preview-container');
405
130
406 this.previewBoxSelector = this.withLineNo('#preview-box');
131 this.previewBoxSelector = this.withLineNo('#preview-box');
407
132
408 this.editButton = this.withLineNo('#edit-btn');
133 this.editButton = this.withLineNo('#edit-btn');
409 this.editContainer = this.withLineNo('#edit-container');
134 this.editContainer = this.withLineNo('#edit-container');
410
135
411 this.cancelButton = this.withLineNo('#cancel-btn');
136 this.cancelButton = this.withLineNo('#cancel-btn');
412
137
413 this.statusChange = '#change_status';
138 this.statusChange = '#change_status';
414 this.cmBox = this.withLineNo('#text');
139 this.cmBox = this.withLineNo('#text');
415 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
140 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
416
141
417 this.submitForm = formElement;
142 this.submitForm = formElement;
418 this.submitButton = $(this.submitForm).find('input[type="submit"]');
143 this.submitButton = $(this.submitForm).find('input[type="submit"]');
419 this.submitButtonText = this.submitButton.val();
144 this.submitButtonText = this.submitButton.val();
420
145
421 this.previewUrl = pyroutes.url('changeset_comment_preview',
146 this.previewUrl = pyroutes.url('changeset_comment_preview',
422 {'repo_name': templateContext.repo_name});
147 {'repo_name': templateContext.repo_name});
423
148
424 // based on commitId, or pullReuqestId decide where do we submit
149 // based on commitId, or pullReuqestId decide where do we submit
425 // out data
150 // out data
426 if (this.commitId){
151 if (this.commitId){
427 this.submitUrl = pyroutes.url('changeset_comment',
152 this.submitUrl = pyroutes.url('changeset_comment',
428 {'repo_name': templateContext.repo_name,
153 {'repo_name': templateContext.repo_name,
429 'revision': this.commitId});
154 'revision': this.commitId});
430
155
431 } else if (this.pullRequestId) {
156 } else if (this.pullRequestId) {
432 this.submitUrl = pyroutes.url('pullrequest_comment',
157 this.submitUrl = pyroutes.url('pullrequest_comment',
433 {'repo_name': templateContext.repo_name,
158 {'repo_name': templateContext.repo_name,
434 'pull_request_id': this.pullRequestId});
159 'pull_request_id': this.pullRequestId});
435
160
436 } else {
161 } else {
437 throw new Error(
162 throw new Error(
438 'CommentForm requires pullRequestId, or commitId to be specified.')
163 'CommentForm requires pullRequestId, or commitId to be specified.')
439 }
164 }
440
165
441 this.getCmInstance = function(){
166 this.getCmInstance = function(){
442 return this.cm
167 return this.cm
443 };
168 };
444
169
445 var self = this;
170 var self = this;
446
171
447 this.getCommentStatus = function() {
172 this.getCommentStatus = function() {
448 return $(this.submitForm).find(this.statusChange).val();
173 return $(this.submitForm).find(this.statusChange).val();
449 };
174 };
450
175
451 this.isAllowedToSubmit = function() {
176 this.isAllowedToSubmit = function() {
452 return !$(this.submitButton).prop('disabled');
177 return !$(this.submitButton).prop('disabled');
453 };
178 };
454
179
455 this.initStatusChangeSelector = function(){
180 this.initStatusChangeSelector = function(){
456 var formatChangeStatus = function(state, escapeMarkup) {
181 var formatChangeStatus = function(state, escapeMarkup) {
457 var originalOption = state.element;
182 var originalOption = state.element;
458 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
183 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
459 '<span>' + escapeMarkup(state.text) + '</span>';
184 '<span>' + escapeMarkup(state.text) + '</span>';
460 };
185 };
461 var formatResult = function(result, container, query, escapeMarkup) {
186 var formatResult = function(result, container, query, escapeMarkup) {
462 return formatChangeStatus(result, escapeMarkup);
187 return formatChangeStatus(result, escapeMarkup);
463 };
188 };
464
189
465 var formatSelection = function(data, container, escapeMarkup) {
190 var formatSelection = function(data, container, escapeMarkup) {
466 return formatChangeStatus(data, escapeMarkup);
191 return formatChangeStatus(data, escapeMarkup);
467 };
192 };
468
193
469 $(this.submitForm).find(this.statusChange).select2({
194 $(this.submitForm).find(this.statusChange).select2({
470 placeholder: _gettext('Status Review'),
195 placeholder: _gettext('Status Review'),
471 formatResult: formatResult,
196 formatResult: formatResult,
472 formatSelection: formatSelection,
197 formatSelection: formatSelection,
473 containerCssClass: "drop-menu status_box_menu",
198 containerCssClass: "drop-menu status_box_menu",
474 dropdownCssClass: "drop-menu-dropdown",
199 dropdownCssClass: "drop-menu-dropdown",
475 dropdownAutoWidth: true,
200 dropdownAutoWidth: true,
476 minimumResultsForSearch: -1
201 minimumResultsForSearch: -1
477 });
202 });
478 $(this.submitForm).find(this.statusChange).on('change', function() {
203 $(this.submitForm).find(this.statusChange).on('change', function() {
479 var status = self.getCommentStatus();
204 var status = self.getCommentStatus();
480 if (status && !self.lineNo) {
205 if (status && !self.lineNo) {
481 $(self.submitButton).prop('disabled', false);
206 $(self.submitButton).prop('disabled', false);
482 }
207 }
483 //todo, fix this name
208 //todo, fix this name
484 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
209 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
485 self.cm.setOption('placeholder', placeholderText);
210 self.cm.setOption('placeholder', placeholderText);
486 })
211 })
487 };
212 };
488
213
489 // reset the comment form into it's original state
214 // reset the comment form into it's original state
490 this.resetCommentFormState = function(content) {
215 this.resetCommentFormState = function(content) {
491 content = content || '';
216 content = content || '';
492
217
493 $(this.editContainer).show();
218 $(this.editContainer).show();
494 $(this.editButton).hide();
219 $(this.editButton).hide();
495
220
496 $(this.previewContainer).hide();
221 $(this.previewContainer).hide();
497 $(this.previewButton).show();
222 $(this.previewButton).show();
498
223
499 this.setActionButtonsDisabled(true);
224 this.setActionButtonsDisabled(true);
500 self.cm.setValue(content);
225 self.cm.setValue(content);
501 self.cm.setOption("readOnly", false);
226 self.cm.setOption("readOnly", false);
502 };
227 };
503
228
504 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
229 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
505 failHandler = failHandler || function() {};
230 failHandler = failHandler || function() {};
506 var postData = toQueryString(postData);
231 var postData = toQueryString(postData);
507 var request = $.ajax({
232 var request = $.ajax({
508 url: url,
233 url: url,
509 type: 'POST',
234 type: 'POST',
510 data: postData,
235 data: postData,
511 headers: {'X-PARTIAL-XHR': true}
236 headers: {'X-PARTIAL-XHR': true}
512 })
237 })
513 .done(function(data) {
238 .done(function(data) {
514 successHandler(data);
239 successHandler(data);
515 })
240 })
516 .fail(function(data, textStatus, errorThrown){
241 .fail(function(data, textStatus, errorThrown){
517 alert(
242 alert(
518 "Error while submitting comment.\n" +
243 "Error while submitting comment.\n" +
519 "Error code {0} ({1}).".format(data.status, data.statusText));
244 "Error code {0} ({1}).".format(data.status, data.statusText));
520 failHandler()
245 failHandler()
521 });
246 });
522 return request;
247 return request;
523 };
248 };
524
249
525 // overwrite a submitHandler, we need to do it for inline comments
250 // overwrite a submitHandler, we need to do it for inline comments
526 this.setHandleFormSubmit = function(callback) {
251 this.setHandleFormSubmit = function(callback) {
527 this.handleFormSubmit = callback;
252 this.handleFormSubmit = callback;
528 };
253 };
529
254
530 // default handler for for submit for main comments
255 // default handler for for submit for main comments
531 this.handleFormSubmit = function() {
256 this.handleFormSubmit = function() {
532 var text = self.cm.getValue();
257 var text = self.cm.getValue();
533 var status = self.getCommentStatus();
258 var status = self.getCommentStatus();
534
259
535 if (text === "" && !status) {
260 if (text === "" && !status) {
536 return;
261 return;
537 }
262 }
538
263
539 var excludeCancelBtn = false;
264 var excludeCancelBtn = false;
540 var submitEvent = true;
265 var submitEvent = true;
541 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
266 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
542 self.cm.setOption("readOnly", true);
267 self.cm.setOption("readOnly", true);
543 var postData = {
268 var postData = {
544 'text': text,
269 'text': text,
545 'changeset_status': status,
270 'changeset_status': status,
546 'csrf_token': CSRF_TOKEN
271 'csrf_token': CSRF_TOKEN
547 };
272 };
548
273
549 var submitSuccessCallback = function(o) {
274 var submitSuccessCallback = function(o) {
550 if (status) {
275 if (status) {
551 location.reload(true);
276 location.reload(true);
552 } else {
277 } else {
553 $('#injected_page_comments').append(o.rendered_text);
278 $('#injected_page_comments').append(o.rendered_text);
554 self.resetCommentFormState();
279 self.resetCommentFormState();
555 bindDeleteCommentButtons();
280 bindDeleteCommentButtons();
556 timeagoActivate();
281 timeagoActivate();
557 }
282 }
558 };
283 };
559 var submitFailCallback = function(){
284 var submitFailCallback = function(){
560 self.resetCommentFormState(text)
285 self.resetCommentFormState(text)
561 };
286 };
562 self.submitAjaxPOST(
287 self.submitAjaxPOST(
563 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
288 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
564 };
289 };
565
290
566 this.previewSuccessCallback = function(o) {
291 this.previewSuccessCallback = function(o) {
567 $(self.previewBoxSelector).html(o);
292 $(self.previewBoxSelector).html(o);
568 $(self.previewBoxSelector).removeClass('unloaded');
293 $(self.previewBoxSelector).removeClass('unloaded');
569
294
570 // swap buttons
295 // swap buttons
571 $(self.previewButton).hide();
296 $(self.previewButton).hide();
572 $(self.editButton).show();
297 $(self.editButton).show();
573
298
574 // unlock buttons
299 // unlock buttons
575 self.setActionButtonsDisabled(false);
300 self.setActionButtonsDisabled(false);
576 };
301 };
577
302
578 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
303 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
579 excludeCancelBtn = excludeCancelBtn || false;
304 excludeCancelBtn = excludeCancelBtn || false;
580 submitEvent = submitEvent || false;
305 submitEvent = submitEvent || false;
581
306
582 $(this.editButton).prop('disabled', state);
307 $(this.editButton).prop('disabled', state);
583 $(this.previewButton).prop('disabled', state);
308 $(this.previewButton).prop('disabled', state);
584
309
585 if (!excludeCancelBtn) {
310 if (!excludeCancelBtn) {
586 $(this.cancelButton).prop('disabled', state);
311 $(this.cancelButton).prop('disabled', state);
587 }
312 }
588
313
589 var submitState = state;
314 var submitState = state;
590 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
315 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
591 // if the value of commit review status is set, we allow
316 // if the value of commit review status is set, we allow
592 // submit button, but only on Main form, lineNo means inline
317 // submit button, but only on Main form, lineNo means inline
593 submitState = false
318 submitState = false
594 }
319 }
595 $(this.submitButton).prop('disabled', submitState);
320 $(this.submitButton).prop('disabled', submitState);
596 if (submitEvent) {
321 if (submitEvent) {
597 $(this.submitButton).val(_gettext('Submitting...'));
322 $(this.submitButton).val(_gettext('Submitting...'));
598 } else {
323 } else {
599 $(this.submitButton).val(this.submitButtonText);
324 $(this.submitButton).val(this.submitButtonText);
600 }
325 }
601
326
602 };
327 };
603
328
604 // lock preview/edit/submit buttons on load, but exclude cancel button
329 // lock preview/edit/submit buttons on load, but exclude cancel button
605 var excludeCancelBtn = true;
330 var excludeCancelBtn = true;
606 this.setActionButtonsDisabled(true, excludeCancelBtn);
331 this.setActionButtonsDisabled(true, excludeCancelBtn);
607
332
608 // anonymous users don't have access to initialized CM instance
333 // anonymous users don't have access to initialized CM instance
609 if (this.cm !== undefined){
334 if (this.cm !== undefined){
610 this.cm.on('change', function(cMirror) {
335 this.cm.on('change', function(cMirror) {
611 if (cMirror.getValue() === "") {
336 if (cMirror.getValue() === "") {
612 self.setActionButtonsDisabled(true, excludeCancelBtn)
337 self.setActionButtonsDisabled(true, excludeCancelBtn)
613 } else {
338 } else {
614 self.setActionButtonsDisabled(false, excludeCancelBtn)
339 self.setActionButtonsDisabled(false, excludeCancelBtn)
615 }
340 }
616 });
341 });
617 }
342 }
618
343
619 $(this.editButton).on('click', function(e) {
344 $(this.editButton).on('click', function(e) {
620 e.preventDefault();
345 e.preventDefault();
621
346
622 $(self.previewButton).show();
347 $(self.previewButton).show();
623 $(self.previewContainer).hide();
348 $(self.previewContainer).hide();
624 $(self.editButton).hide();
349 $(self.editButton).hide();
625 $(self.editContainer).show();
350 $(self.editContainer).show();
626
351
627 });
352 });
628
353
629 $(this.previewButton).on('click', function(e) {
354 $(this.previewButton).on('click', function(e) {
630 e.preventDefault();
355 e.preventDefault();
631 var text = self.cm.getValue();
356 var text = self.cm.getValue();
632
357
633 if (text === "") {
358 if (text === "") {
634 return;
359 return;
635 }
360 }
636
361
637 var postData = {
362 var postData = {
638 'text': text,
363 'text': text,
639 'renderer': DEFAULT_RENDERER,
364 'renderer': DEFAULT_RENDERER,
640 'csrf_token': CSRF_TOKEN
365 'csrf_token': CSRF_TOKEN
641 };
366 };
642
367
643 // lock ALL buttons on preview
368 // lock ALL buttons on preview
644 self.setActionButtonsDisabled(true);
369 self.setActionButtonsDisabled(true);
645
370
646 $(self.previewBoxSelector).addClass('unloaded');
371 $(self.previewBoxSelector).addClass('unloaded');
647 $(self.previewBoxSelector).html(_gettext('Loading ...'));
372 $(self.previewBoxSelector).html(_gettext('Loading ...'));
648 $(self.editContainer).hide();
373 $(self.editContainer).hide();
649 $(self.previewContainer).show();
374 $(self.previewContainer).show();
650
375
651 // by default we reset state of comment preserving the text
376 // by default we reset state of comment preserving the text
652 var previewFailCallback = function(){
377 var previewFailCallback = function(){
653 self.resetCommentFormState(text)
378 self.resetCommentFormState(text)
654 };
379 };
655 self.submitAjaxPOST(
380 self.submitAjaxPOST(
656 self.previewUrl, postData, self.previewSuccessCallback, previewFailCallback);
381 self.previewUrl, postData, self.previewSuccessCallback, previewFailCallback);
657
382
658 });
383 });
659
384
660 $(this.submitForm).submit(function(e) {
385 $(this.submitForm).submit(function(e) {
661 e.preventDefault();
386 e.preventDefault();
662 var allowedToSubmit = self.isAllowedToSubmit();
387 var allowedToSubmit = self.isAllowedToSubmit();
663 if (!allowedToSubmit){
388 if (!allowedToSubmit){
664 return false;
389 return false;
665 }
390 }
666 self.handleFormSubmit();
391 self.handleFormSubmit();
667 });
392 });
668
393
669 }
394 }
670
395
671 return CommentForm;
396 return CommentForm;
672 })();
397 })();
673
398
674 var CommentsController = function() { /* comments controller */
399 var CommentsController = function() { /* comments controller */
675 var self = this;
400 var self = this;
676
401
677 this.cancelComment = function(node) {
402 this.cancelComment = function(node) {
678 var $node = $(node);
403 var $node = $(node);
679 var $td = $node.closest('td');
404 var $td = $node.closest('td');
680 $node.closest('.comment-inline-form').removeClass('comment-inline-form-open');
405 $node.closest('.comment-inline-form').removeClass('comment-inline-form-open');
681 return false;
406 return false;
682 }
407 };
408
683 this.getLineNumber = function(node) {
409 this.getLineNumber = function(node) {
684 var $node = $(node);
410 var $node = $(node);
685 return $node.closest('td').attr('data-line-number');
411 return $node.closest('td').attr('data-line-number');
686 }
412 };
413
687 this.scrollToComment = function(node, offset) {
414 this.scrollToComment = function(node, offset) {
688 if (!node) {
415 if (!node) {
689 node = $('.comment-selected');
416 node = $('.comment-selected');
690 if (!node.length) {
417 if (!node.length) {
691 node = $('comment-current')
418 node = $('comment-current')
692 }
419 }
693 }
420 }
694 $comment = $(node).closest('.comment-current');
421 $comment = $(node).closest('.comment-current');
695 $comments = $('.comment-current');
422 $comments = $('.comment-current');
696
423
697 $('.comment-selected').removeClass('comment-selected');
424 $('.comment-selected').removeClass('comment-selected');
698
425
699 var nextIdx = $('.comment-current').index($comment) + offset;
426 var nextIdx = $('.comment-current').index($comment) + offset;
700 if (nextIdx >= $comments.length) {
427 if (nextIdx >= $comments.length) {
701 nextIdx = 0;
428 nextIdx = 0;
702 }
429 }
703 var $next = $('.comment-current').eq(nextIdx);
430 var $next = $('.comment-current').eq(nextIdx);
704 var $cb = $next.closest('.cb');
431 var $cb = $next.closest('.cb');
705 $cb.removeClass('cb-collapsed')
432 $cb.removeClass('cb-collapsed');
706
433
707 var $filediffCollapseState = $cb.closest('.filediff').prev();
434 var $filediffCollapseState = $cb.closest('.filediff').prev();
708 $filediffCollapseState.prop('checked', false);
435 $filediffCollapseState.prop('checked', false);
709 $next.addClass('comment-selected');
436 $next.addClass('comment-selected');
710 scrollToElement($next);
437 scrollToElement($next);
711 return false;
438 return false;
712 }
439 };
440
713 this.nextComment = function(node) {
441 this.nextComment = function(node) {
714 return self.scrollToComment(node, 1);
442 return self.scrollToComment(node, 1);
715 }
443 };
444
716 this.prevComment = function(node) {
445 this.prevComment = function(node) {
717 return self.scrollToComment(node, -1);
446 return self.scrollToComment(node, -1);
718 }
447 };
448
719 this.deleteComment = function(node) {
449 this.deleteComment = function(node) {
720 if (!confirm(_gettext('Delete this comment?'))) {
450 if (!confirm(_gettext('Delete this comment?'))) {
721 return false;
451 return false;
722 }
452 }
723 var $node = $(node);
453 var $node = $(node);
724 var $td = $node.closest('td');
454 var $td = $node.closest('td');
725 var $comment = $node.closest('.comment');
455 var $comment = $node.closest('.comment');
726 var comment_id = $comment.attr('data-comment-id');
456 var comment_id = $comment.attr('data-comment-id');
727 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
457 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
728 var postData = {
458 var postData = {
729 '_method': 'delete',
459 '_method': 'delete',
730 'csrf_token': CSRF_TOKEN
460 'csrf_token': CSRF_TOKEN
731 };
461 };
732
462
733 $comment.addClass('comment-deleting');
463 $comment.addClass('comment-deleting');
734 $comment.hide('fast');
464 $comment.hide('fast');
735
465
736 var success = function(response) {
466 var success = function(response) {
737 $comment.remove();
467 $comment.remove();
738 return false;
468 return false;
739 };
469 };
740 var failure = function(data, textStatus, xhr) {
470 var failure = function(data, textStatus, xhr) {
741 alert("error processing request: " + textStatus);
471 alert("error processing request: " + textStatus);
742 $comment.show('fast');
472 $comment.show('fast');
743 $comment.removeClass('comment-deleting');
473 $comment.removeClass('comment-deleting');
744 return false;
474 return false;
745 };
475 };
746 ajaxPOST(url, postData, success, failure);
476 ajaxPOST(url, postData, success, failure);
747 }
477 };
478
748 this.toggleComments = function(node, show) {
479 this.toggleComments = function(node, show) {
749 var $filediff = $(node).closest('.filediff');
480 var $filediff = $(node).closest('.filediff');
750 if (show === true) {
481 if (show === true) {
751 $filediff.removeClass('hide-comments');
482 $filediff.removeClass('hide-comments');
752 } else if (show === false) {
483 } else if (show === false) {
753 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
484 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
754 $filediff.addClass('hide-comments');
485 $filediff.addClass('hide-comments');
755 } else {
486 } else {
756 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
487 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
757 $filediff.toggleClass('hide-comments');
488 $filediff.toggleClass('hide-comments');
758 }
489 }
759 return false;
490 return false;
760 }
491 };
492
761 this.toggleLineComments = function(node) {
493 this.toggleLineComments = function(node) {
762 self.toggleComments(node, true);
494 self.toggleComments(node, true);
763 var $node = $(node);
495 var $node = $(node);
764 $node.closest('tr').toggleClass('hide-line-comments');
496 $node.closest('tr').toggleClass('hide-line-comments');
765 }
497 };
498
766 this.createComment = function(node) {
499 this.createComment = function(node) {
767 var $node = $(node);
500 var $node = $(node);
768 var $td = $node.closest('td');
501 var $td = $node.closest('td');
769 var $form = $td.find('.comment-inline-form');
502 var $form = $td.find('.comment-inline-form');
770
503
771 if (!$form.length) {
504 if (!$form.length) {
772 var tmpl = $('#cb-comment-inline-form-template').html();
505 var tmpl = $('#cb-comment-inline-form-template').html();
773 var $filediff = $node.closest('.filediff');
506 var $filediff = $node.closest('.filediff');
774 $filediff.removeClass('hide-comments');
507 $filediff.removeClass('hide-comments');
775 var f_path = $filediff.attr('data-f-path');
508 var f_path = $filediff.attr('data-f-path');
776 var lineno = self.getLineNumber(node);
509 var lineno = self.getLineNumber(node);
777 tmpl = tmpl.format(f_path, lineno);
510 tmpl = tmpl.format(f_path, lineno);
778 $form = $(tmpl);
511 $form = $(tmpl);
779
512
780 var $comments = $td.find('.inline-comments');
513 var $comments = $td.find('.inline-comments');
781 if (!$comments.length) {
514 if (!$comments.length) {
782 $comments = $(
515 $comments = $(
783 $('#cb-comments-inline-container-template').html());
516 $('#cb-comments-inline-container-template').html());
784 $td.append($comments);
517 $td.append($comments);
785 }
518 }
786
519
787 $td.find('.cb-comment-add-button').before($form);
520 $td.find('.cb-comment-add-button').before($form);
788
521
789 var pullRequestId = templateContext.pull_request_data.pull_request_id;
522 var pullRequestId = templateContext.pull_request_data.pull_request_id;
790 var commitId = templateContext.commit_data.commit_id;
523 var commitId = templateContext.commit_data.commit_id;
791 var _form = $form[0];
524 var _form = $form[0];
792 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
525 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
793 var cm = commentForm.getCmInstance();
526 var cm = commentForm.getCmInstance();
794
527
795 // set a CUSTOM submit handler for inline comments.
528 // set a CUSTOM submit handler for inline comments.
796 commentForm.setHandleFormSubmit(function(o) {
529 commentForm.setHandleFormSubmit(function(o) {
797 var text = commentForm.cm.getValue();
530 var text = commentForm.cm.getValue();
798
531
799 if (text === "") {
532 if (text === "") {
800 return;
533 return;
801 }
534 }
802
535
803 if (lineno === undefined) {
536 if (lineno === undefined) {
804 alert('missing line !');
537 alert('missing line !');
805 return;
538 return;
806 }
539 }
807 if (f_path === undefined) {
540 if (f_path === undefined) {
808 alert('missing file path !');
541 alert('missing file path !');
809 return;
542 return;
810 }
543 }
811
544
812 var excludeCancelBtn = false;
545 var excludeCancelBtn = false;
813 var submitEvent = true;
546 var submitEvent = true;
814 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
547 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
815 commentForm.cm.setOption("readOnly", true);
548 commentForm.cm.setOption("readOnly", true);
816 var postData = {
549 var postData = {
817 'text': text,
550 'text': text,
818 'f_path': f_path,
551 'f_path': f_path,
819 'line': lineno,
552 'line': lineno,
820 'csrf_token': CSRF_TOKEN
553 'csrf_token': CSRF_TOKEN
821 };
554 };
822 var submitSuccessCallback = function(json_data) {
555 var submitSuccessCallback = function(json_data) {
823 $form.remove();
556 $form.remove();
824 try {
557 try {
825 var html = json_data.rendered_text;
558 var html = json_data.rendered_text;
826 var lineno = json_data.line_no;
559 var lineno = json_data.line_no;
827 var target_id = json_data.target_id;
560 var target_id = json_data.target_id;
828
561
829 $comments.find('.cb-comment-add-button').before(html);
562 $comments.find('.cb-comment-add-button').before(html);
830
563
831 } catch (e) {
564 } catch (e) {
832 console.error(e);
565 console.error(e);
833 }
566 }
834
567
835
836 // re trigger the linkification of next/prev navigation
568 // re trigger the linkification of next/prev navigation
837 linkifyComments($('.inline-comment-injected'));
569 linkifyComments($('.inline-comment-injected'));
838 timeagoActivate();
570 timeagoActivate();
839 bindDeleteCommentButtons();
571 bindDeleteCommentButtons();
840 commentForm.setActionButtonsDisabled(false);
572 commentForm.setActionButtonsDisabled(false);
841
573
842 };
574 };
843 var submitFailCallback = function(){
575 var submitFailCallback = function(){
844 commentForm.resetCommentFormState(text)
576 commentForm.resetCommentFormState(text)
845 };
577 };
846 commentForm.submitAjaxPOST(
578 commentForm.submitAjaxPOST(
847 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
579 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
848 });
580 });
849
581
850 setTimeout(function() {
582 setTimeout(function() {
851 // callbacks
583 // callbacks
852 if (cm !== undefined) {
584 if (cm !== undefined) {
853 cm.focus();
585 cm.focus();
854 }
586 }
855 }, 10);
587 }, 10);
856
588
857 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
589 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
858 form: _form,
590 form: _form,
859 parent: $td[0],
591 parent: $td[0],
860 lineno: lineno,
592 lineno: lineno,
861 f_path: f_path}
593 f_path: f_path}
862 );
594 );
863 }
595 }
864
596
865 $form.addClass('comment-inline-form-open');
597 $form.addClass('comment-inline-form-open');
866 }
598 };
867
599
868 this.renderInlineComments = function(file_comments) {
600 this.renderInlineComments = function(file_comments) {
869 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
601 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
870
602
871 for (var i = 0; i < file_comments.length; i++) {
603 for (var i = 0; i < file_comments.length; i++) {
872 var box = file_comments[i];
604 var box = file_comments[i];
873
605
874 var target_id = $(box).attr('target_id');
606 var target_id = $(box).attr('target_id');
875
607
876 // actually comments with line numbers
608 // actually comments with line numbers
877 var comments = box.children;
609 var comments = box.children;
878
610
879 for (var j = 0; j < comments.length; j++) {
611 for (var j = 0; j < comments.length; j++) {
880 var data = {
612 var data = {
881 'rendered_text': comments[j].outerHTML,
613 'rendered_text': comments[j].outerHTML,
882 'line_no': $(comments[j]).attr('line'),
614 'line_no': $(comments[j]).attr('line'),
883 'target_id': target_id
615 'target_id': target_id
884 };
616 };
885 }
617 }
886 }
618 }
887
619
888 // since order of injection is random, we're now re-iterating
620 // since order of injection is random, we're now re-iterating
889 // from correct order and filling in links
621 // from correct order and filling in links
890 linkifyComments($('.inline-comment-injected'));
622 linkifyComments($('.inline-comment-injected'));
891 bindDeleteCommentButtons();
623 bindDeleteCommentButtons();
892 firefoxAnchorFix();
624 firefoxAnchorFix();
893 };
625 };
894
626
895 } No newline at end of file
627 }; No newline at end of file
@@ -1,315 +1,314 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%inherit file="/base/base.html"/>
3 <%inherit file="/base/base.html"/>
4 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
4 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
5
5
6 <%def name="title()">
6 <%def name="title()">
7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
8 %if c.rhodecode_name:
8 %if c.rhodecode_name:
9 &middot; ${h.branding(c.rhodecode_name)}
9 &middot; ${h.branding(c.rhodecode_name)}
10 %endif
10 %endif
11 </%def>
11 </%def>
12
12
13 <%def name="menu_bar_nav()">
13 <%def name="menu_bar_nav()">
14 ${self.menu_items(active='repositories')}
14 ${self.menu_items(active='repositories')}
15 </%def>
15 </%def>
16
16
17 <%def name="menu_bar_subnav()">
17 <%def name="menu_bar_subnav()">
18 ${self.repo_menu(active='changelog')}
18 ${self.repo_menu(active='changelog')}
19 </%def>
19 </%def>
20
20
21 <%def name="main()">
21 <%def name="main()">
22 <script>
22 <script>
23 // TODO: marcink switch this to pyroutes
23 // TODO: marcink switch this to pyroutes
24 AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
24 AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
26 </script>
26 </script>
27 <div class="box">
27 <div class="box">
28 <div class="title">
28 <div class="title">
29 ${self.repo_page_title(c.rhodecode_db_repo)}
29 ${self.repo_page_title(c.rhodecode_db_repo)}
30 </div>
30 </div>
31
31
32 <div id="changeset_compare_view_content" class="summary changeset">
32 <div id="changeset_compare_view_content" class="summary changeset">
33 <div class="summary-detail">
33 <div class="summary-detail">
34 <div class="summary-detail-header">
34 <div class="summary-detail-header">
35 <span class="breadcrumbs files_location">
35 <span class="breadcrumbs files_location">
36 <h4>${_('Commit')}
36 <h4>${_('Commit')}
37 <code>
37 <code>
38 ${h.show_id(c.commit)}
38 ${h.show_id(c.commit)}
39 </code>
39 </code>
40 </h4>
40 </h4>
41 </span>
41 </span>
42 <span id="parent_link">
42 <span id="parent_link">
43 <a href="#" title="${_('Parent Commit')}">${_('Parent')}</a>
43 <a href="#" title="${_('Parent Commit')}">${_('Parent')}</a>
44 </span>
44 </span>
45 |
45 |
46 <span id="child_link">
46 <span id="child_link">
47 <a href="#" title="${_('Child Commit')}">${_('Child')}</a>
47 <a href="#" title="${_('Child Commit')}">${_('Child')}</a>
48 </span>
48 </span>
49 </div>
49 </div>
50
50
51 <div class="fieldset">
51 <div class="fieldset">
52 <div class="left-label">
52 <div class="left-label">
53 ${_('Description')}:
53 ${_('Description')}:
54 </div>
54 </div>
55 <div class="right-content">
55 <div class="right-content">
56 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
56 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
57 <div id="message_expand" style="display:none;">
57 <div id="message_expand" style="display:none;">
58 ${_('Expand')}
58 ${_('Expand')}
59 </div>
59 </div>
60 </div>
60 </div>
61 </div>
61 </div>
62
62
63 %if c.statuses:
63 %if c.statuses:
64 <div class="fieldset">
64 <div class="fieldset">
65 <div class="left-label">
65 <div class="left-label">
66 ${_('Commit status')}:
66 ${_('Commit status')}:
67 </div>
67 </div>
68 <div class="right-content">
68 <div class="right-content">
69 <div class="changeset-status-ico">
69 <div class="changeset-status-ico">
70 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
70 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
71 </div>
71 </div>
72 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
72 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
73 </div>
73 </div>
74 </div>
74 </div>
75 %endif
75 %endif
76
76
77 <div class="fieldset">
77 <div class="fieldset">
78 <div class="left-label">
78 <div class="left-label">
79 ${_('References')}:
79 ${_('References')}:
80 </div>
80 </div>
81 <div class="right-content">
81 <div class="right-content">
82 <div class="tags">
82 <div class="tags">
83
83
84 %if c.commit.merge:
84 %if c.commit.merge:
85 <span class="mergetag tag">
85 <span class="mergetag tag">
86 <i class="icon-merge"></i>${_('merge')}
86 <i class="icon-merge"></i>${_('merge')}
87 </span>
87 </span>
88 %endif
88 %endif
89
89
90 %if h.is_hg(c.rhodecode_repo):
90 %if h.is_hg(c.rhodecode_repo):
91 %for book in c.commit.bookmarks:
91 %for book in c.commit.bookmarks:
92 <span class="booktag tag" title="${_('Bookmark %s') % book}">
92 <span class="booktag tag" title="${_('Bookmark %s') % book}">
93 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
93 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
94 </span>
94 </span>
95 %endfor
95 %endfor
96 %endif
96 %endif
97
97
98 %for tag in c.commit.tags:
98 %for tag in c.commit.tags:
99 <span class="tagtag tag" title="${_('Tag %s') % tag}">
99 <span class="tagtag tag" title="${_('Tag %s') % tag}">
100 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-tag"></i>${tag}</a>
100 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-tag"></i>${tag}</a>
101 </span>
101 </span>
102 %endfor
102 %endfor
103
103
104 %if c.commit.branch:
104 %if c.commit.branch:
105 <span class="branchtag tag" title="${_('Branch %s') % c.commit.branch}">
105 <span class="branchtag tag" title="${_('Branch %s') % c.commit.branch}">
106 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
106 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
107 </span>
107 </span>
108 %endif
108 %endif
109 </div>
109 </div>
110 </div>
110 </div>
111 </div>
111 </div>
112
112
113 <div class="fieldset">
113 <div class="fieldset">
114 <div class="left-label">
114 <div class="left-label">
115 ${_('Diffs')}:
115 ${_('Diffs')}:
116 </div>
116 </div>
117 <div class="right-content">
117 <div class="right-content">
118 <div class="diff-actions">
118 <div class="diff-actions">
119 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
119 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
120 ${_('Raw Diff')}
120 ${_('Raw Diff')}
121 </a>
121 </a>
122 |
122 |
123 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
123 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
124 ${_('Patch Diff')}
124 ${_('Patch Diff')}
125 </a>
125 </a>
126 |
126 |
127 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.commit.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
127 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.commit.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
128 ${_('Download Diff')}
128 ${_('Download Diff')}
129 </a>
129 </a>
130 |
130 |
131 ${c.ignorews_url(request.GET)}
131 ${c.ignorews_url(request.GET)}
132 |
132 |
133 ${c.context_url(request.GET)}
133 ${c.context_url(request.GET)}
134 </div>
134 </div>
135 </div>
135 </div>
136 </div>
136 </div>
137
137
138 <div class="fieldset">
138 <div class="fieldset">
139 <div class="left-label">
139 <div class="left-label">
140 ${_('Comments')}:
140 ${_('Comments')}:
141 </div>
141 </div>
142 <div class="right-content">
142 <div class="right-content">
143 <div class="comments-number">
143 <div class="comments-number">
144 %if c.comments:
144 %if c.comments:
145 <a href="#comments">${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
145 <a href="#comments">${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
146 %else:
146 %else:
147 ${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
147 ${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
148 %endif
148 %endif
149 %if c.inline_cnt:
149 %if c.inline_cnt:
150 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
150 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
151 %else:
151 %else:
152 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
152 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
153 %endif
153 %endif
154 </div>
154 </div>
155 </div>
155 </div>
156 </div>
156 </div>
157
157
158 </div> <!-- end summary-detail -->
158 </div> <!-- end summary-detail -->
159
159
160 <div id="commit-stats" class="sidebar-right">
160 <div id="commit-stats" class="sidebar-right">
161 <div class="summary-detail-header">
161 <div class="summary-detail-header">
162 <h4 class="item">
162 <h4 class="item">
163 ${_('Author')}
163 ${_('Author')}
164 </h4>
164 </h4>
165 </div>
165 </div>
166 <div class="sidebar-right-content">
166 <div class="sidebar-right-content">
167 ${self.gravatar_with_user(c.commit.author)}
167 ${self.gravatar_with_user(c.commit.author)}
168 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
168 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
169 </div>
169 </div>
170 </div><!-- end sidebar -->
170 </div><!-- end sidebar -->
171 </div> <!-- end summary -->
171 </div> <!-- end summary -->
172 <div class="cs_files">
172 <div class="cs_files">
173 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
173 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
174 ${cbdiffs.render_diffset_menu()}
174 ${cbdiffs.render_diffset_menu()}
175 ${cbdiffs.render_diffset(
175 ${cbdiffs.render_diffset(
176 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True)}
176 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True)}
177 </div>
177 </div>
178
178
179 ## template for inline comment form
179 ## template for inline comment form
180 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
180 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
181 ${comment.comment_inline_form()}
182
181
183 ## ## render comments and inlines
182 ## render comments
184 ${comment.generate_comments()}
183 ${comment.generate_comments()}
185
184
186 ## main comment form and it status
185 ## main comment form and it status
187 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.commit.raw_id),
186 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.commit.raw_id),
188 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
187 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
189 </div>
188 </div>
190
189
191 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
190 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
192 <script type="text/javascript">
191 <script type="text/javascript">
193
192
194 $(document).ready(function() {
193 $(document).ready(function() {
195
194
196 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
195 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
197 if($('#trimmed_message_box').height() === boxmax){
196 if($('#trimmed_message_box').height() === boxmax){
198 $('#message_expand').show();
197 $('#message_expand').show();
199 }
198 }
200
199
201 $('#message_expand').on('click', function(e){
200 $('#message_expand').on('click', function(e){
202 $('#trimmed_message_box').css('max-height', 'none');
201 $('#trimmed_message_box').css('max-height', 'none');
203 $(this).hide();
202 $(this).hide();
204 });
203 });
205
204
206 $('.show-inline-comments').on('click', function(e){
205 $('.show-inline-comments').on('click', function(e){
207 var boxid = $(this).attr('data-comment-id');
206 var boxid = $(this).attr('data-comment-id');
208 var button = $(this);
207 var button = $(this);
209
208
210 if(button.hasClass("comments-visible")) {
209 if(button.hasClass("comments-visible")) {
211 $('#{0} .inline-comments'.format(boxid)).each(function(index){
210 $('#{0} .inline-comments'.format(boxid)).each(function(index){
212 $(this).hide();
211 $(this).hide();
213 })
212 });
214 button.removeClass("comments-visible");
213 button.removeClass("comments-visible");
215 } else {
214 } else {
216 $('#{0} .inline-comments'.format(boxid)).each(function(index){
215 $('#{0} .inline-comments'.format(boxid)).each(function(index){
217 $(this).show();
216 $(this).show();
218 })
217 });
219 button.addClass("comments-visible");
218 button.addClass("comments-visible");
220 }
219 }
221 });
220 });
222
221
223
222
224 // next links
223 // next links
225 $('#child_link').on('click', function(e){
224 $('#child_link').on('click', function(e){
226 // fetch via ajax what is going to be the next link, if we have
225 // fetch via ajax what is going to be the next link, if we have
227 // >1 links show them to user to choose
226 // >1 links show them to user to choose
228 if(!$('#child_link').hasClass('disabled')){
227 if(!$('#child_link').hasClass('disabled')){
229 $.ajax({
228 $.ajax({
230 url: '${h.url('changeset_children',repo_name=c.repo_name, revision=c.commit.raw_id)}',
229 url: '${h.url('changeset_children',repo_name=c.repo_name, revision=c.commit.raw_id)}',
231 success: function(data) {
230 success: function(data) {
232 if(data.results.length === 0){
231 if(data.results.length === 0){
233 $('#child_link').html('${_('No Child Commits')}').addClass('disabled');
232 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
234 }
233 }
235 if(data.results.length === 1){
234 if(data.results.length === 1){
236 var commit = data.results[0];
235 var commit = data.results[0];
237 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
236 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
238 }
237 }
239 else if(data.results.length === 2){
238 else if(data.results.length === 2){
240 $('#child_link').addClass('disabled');
239 $('#child_link').addClass('disabled');
241 $('#child_link').addClass('double');
240 $('#child_link').addClass('double');
242 var _html = '';
241 var _html = '';
243 _html +='<a title="__title__" href="__url__">__rev__</a> '
242 _html +='<a title="__title__" href="__url__">__rev__</a> '
244 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
243 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
245 .replace('__title__', data.results[0].message)
244 .replace('__title__', data.results[0].message)
246 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
245 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
247 _html +=' | '
246 _html +=' | '
248 _html +='<a title="__title__" href="__url__">__rev__</a> '
247 _html +='<a title="__title__" href="__url__">__rev__</a> '
249 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
248 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
250 .replace('__title__', data.results[1].message)
249 .replace('__title__', data.results[1].message)
251 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
250 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
252 $('#child_link').html(_html);
251 $('#child_link').html(_html);
253 }
252 }
254 }
253 }
255 });
254 });
256 e.preventDefault();
255 e.preventDefault();
257 }
256 }
258 });
257 });
259
258
260 // prev links
259 // prev links
261 $('#parent_link').on('click', function(e){
260 $('#parent_link').on('click', function(e){
262 // fetch via ajax what is going to be the next link, if we have
261 // fetch via ajax what is going to be the next link, if we have
263 // >1 links show them to user to choose
262 // >1 links show them to user to choose
264 if(!$('#parent_link').hasClass('disabled')){
263 if(!$('#parent_link').hasClass('disabled')){
265 $.ajax({
264 $.ajax({
266 url: '${h.url('changeset_parents',repo_name=c.repo_name, revision=c.commit.raw_id)}',
265 url: '${h.url("changeset_parents",repo_name=c.repo_name, revision=c.commit.raw_id)}',
267 success: function(data) {
266 success: function(data) {
268 if(data.results.length === 0){
267 if(data.results.length === 0){
269 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
268 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
270 }
269 }
271 if(data.results.length === 1){
270 if(data.results.length === 1){
272 var commit = data.results[0];
271 var commit = data.results[0];
273 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
272 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
274 }
273 }
275 else if(data.results.length === 2){
274 else if(data.results.length === 2){
276 $('#parent_link').addClass('disabled');
275 $('#parent_link').addClass('disabled');
277 $('#parent_link').addClass('double');
276 $('#parent_link').addClass('double');
278 var _html = '';
277 var _html = '';
279 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
278 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
280 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
279 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
281 .replace('__title__', data.results[0].message)
280 .replace('__title__', data.results[0].message)
282 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
281 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
283 _html +=' | '
282 _html +=' | '
284 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
283 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
285 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
284 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
286 .replace('__title__', data.results[1].message)
285 .replace('__title__', data.results[1].message)
287 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
286 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
288 $('#parent_link').html(_html);
287 $('#parent_link').html(_html);
289 }
288 }
290 }
289 }
291 });
290 });
292 e.preventDefault();
291 e.preventDefault();
293 }
292 }
294 });
293 });
295
294
296 if (location.hash) {
295 if (location.hash) {
297 var result = splitDelimitedHash(location.hash);
296 var result = splitDelimitedHash(location.hash);
298 var line = $('html').find(result.loc);
297 var line = $('html').find(result.loc);
299 if (line.length > 0){
298 if (line.length > 0){
300 offsetScroll(line, 70);
299 offsetScroll(line, 70);
301 }
300 }
302 }
301 }
303
302
304 // browse tree @ revision
303 // browse tree @ revision
305 $('#files_link').on('click', function(e){
304 $('#files_link').on('click', function(e){
306 window.location = '${h.url('files_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path='')}';
305 window.location = '${h.url('files_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path='')}';
307 e.preventDefault();
306 e.preventDefault();
308 });
307 });
309
308
310 // inject comments into their proper positions
309 // inject comments into their proper positions
311 var file_comments = $('.inline-comment-placeholder');
310 var file_comments = $('.inline-comment-placeholder');
312 })
311 })
313 </script>
312 </script>
314
313
315 </%def>
314 </%def>
@@ -1,289 +1,208 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.html"/>
6 <%namespace name="base" file="/base/base.html"/>
7
7
8 <%def name="comment_block(comment, inline=False)">
8 <%def name="comment_block(comment, inline=False)">
9 <div
9 <div class="comment
10 class="comment
10 ${'comment-inline' if inline else ''}
11 ${'comment-inline' if inline else ''}
11 ${'comment-outdated' if comment.outdated_at_version(getattr(c, 'at_version', None)) else 'comment-current'}"
12 ${'comment-outdated' if comment.outdated else 'comment-current'}"
12 id="comment-${comment.comment_id}"
13 "
13 line="${comment.line_no}"
14 id="comment-${comment.comment_id}"
14 data-comment-id="${comment.comment_id}"
15 line="${comment.line_no}"
15 style="${'display: none;' if comment.outdated_at_version(getattr(c, 'at_version', None)) else ''}">
16 data-comment-id="${comment.comment_id}">
16
17 <div class="meta">
17 <div class="meta">
18 <div class="author">
18 <div class="author">
19 ${base.gravatar_with_user(comment.author.email, 16)}
19 ${base.gravatar_with_user(comment.author.email, 16)}
20 </div>
20 </div>
21 <div class="date">
21 <div class="date">
22 ${h.age_component(comment.modified_at, time_is_local=True)}
22 ${h.age_component(comment.modified_at, time_is_local=True)}
23 </div>
23 </div>
24 <div class="status-change">
24 <div class="status-change">
25 %if comment.pull_request:
25 % if comment.pull_request:
26 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
26 % if comment.outdated:
27 %if comment.status_change:
27 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
28 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
28 ${_('Outdated comment from pull request version {}').format(comment.pull_request_version_id)}
29 %else:
29 </a>
30 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
30 % else:
31 %endif
31 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
32 </a>
32 %if comment.status_change:
33 %else:
33 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
34 %if comment.status_change:
34 %else:
35 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
36 %endif
37 </a>
38 % endif
39 % else:
40 % if comment.status_change:
35 ${_('Status change on commit')}:
41 ${_('Status change on commit')}:
36 %else:
42 % else:
37 ${_('Comment on commit')}
43 ${_('Comment on commit')}
38 %endif
44 % endif
39 %endif
45 % endif
40 </div>
46 </div>
41 %if comment.status_change:
47 %if comment.status_change:
42 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
48 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
43 <div title="${_('Commit status')}" class="changeset-status-lbl">
49 <div title="${_('Commit status')}" class="changeset-status-lbl">
44 ${comment.status_change[0].status_lbl}
50 ${comment.status_change[0].status_lbl}
45 </div>
51 </div>
46 %endif
52 %endif
47 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
53 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
48
54
49
55
50 <div class="comment-links-block">
56 <div class="comment-links-block">
51
57
52 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
58 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
53 ## only super-admin, repo admin OR comment owner can delete
59 ## only super-admin, repo admin OR comment owner can delete
54 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
60 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
61 ## permissions to delete
55 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
62 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
56 ## TODO: dan: add edit comment here
63 ## TODO: dan: add edit comment here
57 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a> |
64 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
58 %if not comment.outdated:
65 %else:
59 <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a> |
66 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
60 <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
61 %endif
62 %endif
67 %endif
68 %else:
69 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
70 %endif
71 %if not comment.outdated_at_version(getattr(c, 'at_version', None)):
72 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
73 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
63 %endif
74 %endif
64
75
65 </div>
76 </div>
66 </div>
77 </div>
67 <div class="text">
78 <div class="text">
68 ${comment.render(mentions=True)|n}
79 ${comment.render(mentions=True)|n}
69 </div>
80 </div>
70 </div>
71 </%def>
72
81
73 <%def name="comment_block_outdated(comment)">
74 <div class="comments" id="comment-${comment.comment_id}">
75 <div class="comment comment-wrapp">
76 <div class="meta">
77 <div class="author">
78 ${base.gravatar_with_user(comment.author.email, 16)}
79 </div>
80 <div class="date">
81 ${h.age_component(comment.modified_at, time_is_local=True)}
82 </div>
83 %if comment.status_change:
84 <span class="changeset-status-container">
85 <span class="changeset-status-ico">
86 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
87 </span>
88 <span title="${_('Commit status')}" class="changeset-status-lbl"> ${comment.status_change[0].status_lbl}</span>
89 </span>
90 %endif
91 <a class="permalink" href="#comment-${comment.comment_id}">&para;</a>
92 ## 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
94 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
95 <div class="comment-links-block">
96 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
97 <div data-comment-id=${comment.comment_id} class="delete-comment">${_('Delete')}</div>
98 %endif
99 </div>
100 %endif
101 </div>
102 <div class="text">
103 ${comment.render(mentions=True)|n}
104 </div>
105 </div>
106 </div>
82 </div>
107 </%def>
83 </%def>
108
109 <%def name="comment_inline_form()">
110 <div id="comment-inline-form-template" style="display: none;">
111 <div class="comment-inline-form ac">
112 %if c.rhodecode_user.username != h.DEFAULT_USER:
113 ${h.form('#', class_='inline-form', method='get')}
114 <div id="edit-container_{1}" class="clearfix">
115 <div class="comment-title pull-left">
116 ${_('Create a comment on line {1}.')}
117 </div>
118 <div class="comment-help pull-right">
119 ${(_('Comments parsed using %s syntax with %s support.') % (
120 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
121 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
122 )
123 )|n
124 }
125 </div>
126 <div style="clear: both"></div>
127 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
128 </div>
129 <div id="preview-container_{1}" class="clearfix" style="display: none;">
130 <div class="comment-help">
131 ${_('Comment preview')}
132 </div>
133 <div id="preview-box_{1}" class="preview-box"></div>
134 </div>
135 <div class="comment-footer">
136 <div class="comment-button hide-inline-form-button cancel-button">
137 ${h.reset('hide-inline-form', _('Cancel'), class_='btn hide-inline-form', id_="cancel-btn_{1}")}
138 </div>
139 <div class="action-buttons">
140 <input type="hidden" name="f_path" value="{0}">
141 <input type="hidden" name="line" value="{1}">
142 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
143 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
144 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
145 </div>
146 ${h.end_form()}
147 </div>
148 %else:
149 ${h.form('', class_='inline-form comment-form-login', method='get')}
150 <div class="pull-left">
151 <div class="comment-help pull-right">
152 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
153 </div>
154 </div>
155 <div class="comment-button pull-right">
156 ${h.reset('hide-inline-form', _('Hide'), class_='btn hide-inline-form')}
157 </div>
158 <div class="clearfix"></div>
159 ${h.end_form()}
160 %endif
161 </div>
162 </div>
163 </%def>
164
165
166 ## generate main comments
84 ## generate main comments
167 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
85 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
168 <div id="comments">
86 <div id="comments">
169 %for comment in c.comments:
87 %for comment in c.comments:
170 <div id="comment-tr-${comment.comment_id}">
88 <div id="comment-tr-${comment.comment_id}">
171 ## only render comments that are not from pull request, or from
89 ## only render comments that are not from pull request, or from
172 ## pull request and a status change
90 ## pull request and a status change
173 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
91 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
174 ${comment_block(comment)}
92 ${comment_block(comment)}
175 %endif
93 %endif
176 </div>
94 </div>
177 %endfor
95 %endfor
178 ## to anchor ajax comments
96 ## to anchor ajax comments
179 <div id="injected_page_comments"></div>
97 <div id="injected_page_comments"></div>
180 </div>
98 </div>
181 </%def>
99 </%def>
182
100
183 ## MAIN COMMENT FORM
101 ## MAIN COMMENT FORM
184 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
102 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
103
185 %if is_compare:
104 %if is_compare:
186 <% form_id = "comments_form_compare" %>
105 <% form_id = "comments_form_compare" %>
187 %else:
106 %else:
188 <% form_id = "comments_form" %>
107 <% form_id = "comments_form" %>
189 %endif
108 %endif
190
109
191
110
192 %if is_pull_request:
111 %if is_pull_request:
193 <div class="pull-request-merge">
112 <div class="pull-request-merge">
194 %if c.allowed_to_merge:
113 %if c.allowed_to_merge:
195 <div class="pull-request-wrap">
114 <div class="pull-request-wrap">
196 <div class="pull-right">
115 <div class="pull-right">
197 ${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')}
116 ${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')}
198 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
117 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
199 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
118 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
200 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
119 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
201 ${h.end_form()}
120 ${h.end_form()}
202 </div>
121 </div>
203 </div>
122 </div>
204 %else:
123 %else:
205 <div class="pull-request-wrap">
124 <div class="pull-request-wrap">
206 <div class="pull-right">
125 <div class="pull-right">
207 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
126 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
208 </div>
127 </div>
209 </div>
128 </div>
210 %endif
129 %endif
211 </div>
130 </div>
212 %endif
131 %endif
213 <div class="comments">
132 <div class="comments">
214 %if c.rhodecode_user.username != h.DEFAULT_USER:
133 %if c.rhodecode_user.username != h.DEFAULT_USER:
215 <div class="comment-form ac">
134 <div class="comment-form ac">
216 ${h.secure_form(post_url, id_=form_id)}
135 ${h.secure_form(post_url, id_=form_id)}
217 <div id="edit-container" class="clearfix">
136 <div id="edit-container" class="clearfix">
218 <div class="comment-title pull-left">
137 <div class="comment-title pull-left">
219 %if is_pull_request:
138 %if is_pull_request:
220 ${(_('Create a comment on this Pull Request.'))}
139 ${(_('Create a comment on this Pull Request.'))}
221 %elif is_compare:
140 %elif is_compare:
222 ${(_('Create comments on this Commit range.'))}
141 ${(_('Create comments on this Commit range.'))}
223 %else:
142 %else:
224 ${(_('Create a comment on this Commit.'))}
143 ${(_('Create a comment on this Commit.'))}
225 %endif
144 %endif
226 </div>
145 </div>
227 <div class="comment-help pull-right">
146 <div class="comment-help pull-right">
228 ${(_('Comments parsed using %s syntax with %s support.') % (
147 ${(_('Comments parsed using %s syntax with %s support.') % (
229 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
148 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
230 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
149 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
231 )
150 )
232 )|n
151 )|n
233 }
152 }
234 </div>
153 </div>
235 <div style="clear: both"></div>
154 <div style="clear: both"></div>
236 ${h.textarea('text', class_="comment-block-ta")}
155 ${h.textarea('text', class_="comment-block-ta")}
237 </div>
156 </div>
238
157
239 <div id="preview-container" class="clearfix" style="display: none;">
158 <div id="preview-container" class="clearfix" style="display: none;">
240 <div class="comment-title">
159 <div class="comment-title">
241 ${_('Comment preview')}
160 ${_('Comment preview')}
242 </div>
161 </div>
243 <div id="preview-box" class="preview-box"></div>
162 <div id="preview-box" class="preview-box"></div>
244 </div>
163 </div>
245
164
246 <div id="comment_form_extras">
165 <div id="comment_form_extras">
247 %if form_extras and isinstance(form_extras, (list, tuple)):
166 %if form_extras and isinstance(form_extras, (list, tuple)):
248 % for form_ex_el in form_extras:
167 % for form_ex_el in form_extras:
249 ${form_ex_el|n}
168 ${form_ex_el|n}
250 % endfor
169 % endfor
251 %endif
170 %endif
252 </div>
171 </div>
253 <div class="comment-footer">
172 <div class="comment-footer">
254 %if change_status:
173 %if change_status:
255 <div class="status_box">
174 <div class="status_box">
256 <select id="change_status" name="changeset_status">
175 <select id="change_status" name="changeset_status">
257 <option></option> # Placeholder
176 <option></option> # Placeholder
258 %for status,lbl in c.commit_statuses:
177 %for status,lbl in c.commit_statuses:
259 <option value="${status}" data-status="${status}">${lbl}</option>
178 <option value="${status}" data-status="${status}">${lbl}</option>
260 %if is_pull_request and change_status and status in ('approved', 'rejected'):
179 %if is_pull_request and change_status and status in ('approved', 'rejected'):
261 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
180 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
262 %endif
181 %endif
263 %endfor
182 %endfor
264 </select>
183 </select>
265 </div>
184 </div>
266 %endif
185 %endif
267 <div class="action-buttons">
186 <div class="action-buttons">
268 <button id="preview-btn" class="btn btn-secondary">${_('Preview')}</button>
187 <button id="preview-btn" class="btn btn-secondary">${_('Preview')}</button>
269 <button id="edit-btn" class="btn btn-secondary" style="display:none;">${_('Edit')}</button>
188 <button id="edit-btn" class="btn btn-secondary" style="display:none;">${_('Edit')}</button>
270 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
189 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
271 </div>
190 </div>
272 </div>
191 </div>
273 ${h.end_form()}
192 ${h.end_form()}
274 </div>
193 </div>
275 %endif
194 %endif
276 </div>
195 </div>
277 <script>
196 <script>
278 // init active elements of commentForm
197 // init active elements of commentForm
279 var commitId = templateContext.commit_data.commit_id;
198 var commitId = templateContext.commit_data.commit_id;
280 var pullRequestId = templateContext.pull_request_data.pull_request_id;
199 var pullRequestId = templateContext.pull_request_data.pull_request_id;
281 var lineNo;
200 var lineNo;
282
201
283 var mainCommentForm = new CommentForm(
202 var mainCommentForm = new CommentForm(
284 "#${form_id}", commitId, pullRequestId, lineNo, true);
203 "#${form_id}", commitId, pullRequestId, lineNo, true);
285
204
286 mainCommentForm.initStatusChangeSelector();
205 mainCommentForm.initStatusChangeSelector();
287 bindToggleButtons();
206 bindToggleButtons();
288 </script>
207 </script>
289 </%def>
208 </%def>
@@ -1,569 +1,572 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 return {
6 return {
7 '-': 'cb-deletion',
7 '-': 'cb-deletion',
8 '+': 'cb-addition',
8 '+': 'cb-addition',
9 ' ': 'cb-context',
9 ' ': 'cb-context',
10 }.get(action, 'cb-empty')
10 }.get(action, 'cb-empty')
11 %></%def>
11 %></%def>
12
12
13 <%def name="op_class(op_id)"><%
13 <%def name="op_class(op_id)"><%
14 return {
14 return {
15 DEL_FILENODE: 'deletion', # file deleted
15 DEL_FILENODE: 'deletion', # file deleted
16 BIN_FILENODE: 'warning' # binary diff hidden
16 BIN_FILENODE: 'warning' # binary diff hidden
17 }.get(op_id, 'addition')
17 }.get(op_id, 'addition')
18 %></%def>
18 %></%def>
19
19
20 <%def name="link_for(**kw)"><%
20 <%def name="link_for(**kw)"><%
21 new_args = request.GET.mixed()
21 new_args = request.GET.mixed()
22 new_args.update(kw)
22 new_args.update(kw)
23 return h.url('', **new_args)
23 return h.url('', **new_args)
24 %></%def>
24 %></%def>
25
25
26 <%def name="render_diffset(diffset, commit=None,
26 <%def name="render_diffset(diffset, commit=None,
27
27
28 # collapse all file diff entries when there are more than this amount of files in the diff
28 # collapse all file diff entries when there are more than this amount of files in the diff
29 collapse_when_files_over=20,
29 collapse_when_files_over=20,
30
30
31 # collapse lines in the diff when more than this amount of lines changed in the file diff
31 # collapse lines in the diff when more than this amount of lines changed in the file diff
32 lines_changed_limit=500,
32 lines_changed_limit=500,
33
33
34 # add a ruler at to the output
34 # add a ruler at to the output
35 ruler_at_chars=0,
35 ruler_at_chars=0,
36
36
37 # show inline comments
37 # show inline comments
38 use_comments=False,
38 use_comments=False,
39
39
40 # disable new comments
40 # disable new comments
41 disable_new_comments=False,
41 disable_new_comments=False,
42
42
43 )">
43 )">
44
44
45 %if use_comments:
45 %if use_comments:
46 <div id="cb-comments-inline-container-template" class="js-template">
46 <div id="cb-comments-inline-container-template" class="js-template">
47 ${inline_comments_container([])}
47 ${inline_comments_container([])}
48 </div>
48 </div>
49 <div class="js-template" id="cb-comment-inline-form-template">
49 <div class="js-template" id="cb-comment-inline-form-template">
50 <div class="comment-inline-form ac">
50 <div class="comment-inline-form ac">
51 %if c.rhodecode_user.username != h.DEFAULT_USER:
51 %if c.rhodecode_user.username != h.DEFAULT_USER:
52 ${h.form('#', method='get')}
52 ${h.form('#', method='get')}
53 <div id="edit-container_{1}" class="clearfix">
53 <div id="edit-container_{1}" class="clearfix">
54 <div class="comment-title pull-left">
54 <div class="comment-title pull-left">
55 ${_('Create a comment on line {1}.')}
55 ${_('Create a comment on line {1}.')}
56 </div>
56 </div>
57 <div class="comment-help pull-right">
57 <div class="comment-help pull-right">
58 ${(_('Comments parsed using %s syntax with %s support.') % (
58 ${(_('Comments parsed using %s syntax with %s support.') % (
59 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
59 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
60 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
60 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
61 )
61 )
62 )|n
62 )|n
63 }
63 }
64 </div>
64 </div>
65 <div style="clear: both"></div>
65 <div style="clear: both"></div>
66 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
66 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
67 </div>
67 </div>
68 <div id="preview-container_{1}" class="clearfix" style="display: none;">
68 <div id="preview-container_{1}" class="clearfix" style="display: none;">
69 <div class="comment-help">
69 <div class="comment-help">
70 ${_('Comment preview')}
70 ${_('Comment preview')}
71 </div>
71 </div>
72 <div id="preview-box_{1}" class="preview-box"></div>
72 <div id="preview-box_{1}" class="preview-box"></div>
73 </div>
73 </div>
74 <div class="comment-footer">
74 <div class="comment-footer">
75 <div class="action-buttons">
75 <div class="action-buttons">
76 <input type="hidden" name="f_path" value="{0}">
76 <input type="hidden" name="f_path" value="{0}">
77 <input type="hidden" name="line" value="{1}">
77 <input type="hidden" name="line" value="{1}">
78 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
78 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
79 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
79 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
80 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
80 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
81 </div>
81 </div>
82 <div class="comment-button">
82 <div class="comment-button">
83 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
83 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
84 ${_('Cancel')}
84 ${_('Cancel')}
85 </button>
85 </button>
86 </div>
86 </div>
87 ${h.end_form()}
87 ${h.end_form()}
88 </div>
88 </div>
89 %else:
89 %else:
90 ${h.form('', class_='inline-form comment-form-login', method='get')}
90 ${h.form('', class_='inline-form comment-form-login', method='get')}
91 <div class="pull-left">
91 <div class="pull-left">
92 <div class="comment-help pull-right">
92 <div class="comment-help pull-right">
93 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
93 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
94 </div>
94 </div>
95 </div>
95 </div>
96 <div class="comment-button pull-right">
96 <div class="comment-button pull-right">
97 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
97 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
98 ${_('Cancel')}
98 ${_('Cancel')}
99 </button>
99 </button>
100 </div>
100 </div>
101 <div class="clearfix"></div>
101 <div class="clearfix"></div>
102 ${h.end_form()}
102 ${h.end_form()}
103 %endif
103 %endif
104 </div>
104 </div>
105 </div>
105 </div>
106
106
107 %endif
107 %endif
108 <%
108 <%
109 collapse_all = len(diffset.files) > collapse_when_files_over
109 collapse_all = len(diffset.files) > collapse_when_files_over
110 %>
110 %>
111
111
112 %if c.diffmode == 'sideside':
112 %if c.diffmode == 'sideside':
113 <style>
113 <style>
114 .wrapper {
114 .wrapper {
115 max-width: 1600px !important;
115 max-width: 1600px !important;
116 }
116 }
117 </style>
117 </style>
118 %endif
118 %endif
119 %if ruler_at_chars:
119 %if ruler_at_chars:
120 <style>
120 <style>
121 .diff table.cb .cb-content:after {
121 .diff table.cb .cb-content:after {
122 content: "";
122 content: "";
123 border-left: 1px solid blue;
123 border-left: 1px solid blue;
124 position: absolute;
124 position: absolute;
125 top: 0;
125 top: 0;
126 height: 18px;
126 height: 18px;
127 opacity: .2;
127 opacity: .2;
128 z-index: 10;
128 z-index: 10;
129 ## +5 to account for diff action (+/-)
129 ## +5 to account for diff action (+/-)
130 left: ${ruler_at_chars + 5}ch;
130 left: ${ruler_at_chars + 5}ch;
131 </style>
131 </style>
132 %endif
132 %endif
133 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
133 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
134 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
134 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
135 %if commit:
135 %if commit:
136 <div class="pull-right">
136 <div class="pull-right">
137 <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='')}">
137 <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='')}">
138 ${_('Browse Files')}
138 ${_('Browse Files')}
139 </a>
139 </a>
140 </div>
140 </div>
141 %endif
141 %endif
142 <h2 class="clearinner">
142 <h2 class="clearinner">
143 %if commit:
143 %if commit:
144 <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> -
144 <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> -
145 ${h.age_component(commit.date)} -
145 ${h.age_component(commit.date)} -
146 %endif
146 %endif
147 %if diffset.limited_diff:
147 %if diffset.limited_diff:
148 ${_('The requested commit is too big and content was truncated.')}
148 ${_('The requested commit is too big and content was truncated.')}
149
149
150 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
150 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
151 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
151 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
152 %else:
152 %else:
153 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
153 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
154 '%(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}}
154 '%(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}}
155 %endif
155 %endif
156 </h2>
156 </h2>
157 </div>
157 </div>
158
158
159 %if not diffset.files:
159 %if not diffset.files:
160 <p class="empty_data">${_('No files')}</p>
160 <p class="empty_data">${_('No files')}</p>
161 %endif
161 %endif
162
162
163 <div class="filediffs">
163 <div class="filediffs">
164 %for i, filediff in enumerate(diffset.files):
164 %for i, filediff in enumerate(diffset.files):
165 <%
165 <%
166 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
166 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
167 over_lines_changed_limit = lines_changed > lines_changed_limit
167 over_lines_changed_limit = lines_changed > lines_changed_limit
168 %>
168 %>
169 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
169 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
170 <div
170 <div
171 class="filediff"
171 class="filediff"
172 data-f-path="${filediff['patch']['filename']}"
172 data-f-path="${filediff['patch']['filename']}"
173 id="a_${h.FID('', filediff['patch']['filename'])}">
173 id="a_${h.FID('', filediff['patch']['filename'])}">
174 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
174 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
175 <div class="filediff-collapse-indicator"></div>
175 <div class="filediff-collapse-indicator"></div>
176 ${diff_ops(filediff)}
176 ${diff_ops(filediff)}
177 </label>
177 </label>
178 ${diff_menu(filediff, use_comments=use_comments)}
178 ${diff_menu(filediff, use_comments=use_comments)}
179 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
179 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
180 %if not filediff.hunks:
180 %if not filediff.hunks:
181 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
181 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
182 <tr>
182 <tr>
183 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
183 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
184 %if op_id == DEL_FILENODE:
184 %if op_id == DEL_FILENODE:
185 ${_('File was deleted')}
185 ${_('File was deleted')}
186 %elif op_id == BIN_FILENODE:
186 %elif op_id == BIN_FILENODE:
187 ${_('Binary file hidden')}
187 ${_('Binary file hidden')}
188 %else:
188 %else:
189 ${op_text}
189 ${op_text}
190 %endif
190 %endif
191 </td>
191 </td>
192 </tr>
192 </tr>
193 %endfor
193 %endfor
194 %endif
194 %endif
195 %if over_lines_changed_limit:
195 %if over_lines_changed_limit:
196 <tr class="cb-warning cb-collapser">
196 <tr class="cb-warning cb-collapser">
197 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
197 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
198 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
198 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
199 <a href="#" class="cb-expand"
199 <a href="#" class="cb-expand"
200 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
200 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
201 </a>
201 </a>
202 <a href="#" class="cb-collapse"
202 <a href="#" class="cb-collapse"
203 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
203 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
204 </a>
204 </a>
205 </td>
205 </td>
206 </tr>
206 </tr>
207 %endif
207 %endif
208 %if filediff.patch['is_limited_diff']:
208 %if filediff.patch['is_limited_diff']:
209 <tr class="cb-warning cb-collapser">
209 <tr class="cb-warning cb-collapser">
210 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
210 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
211 ${_('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>
211 ${_('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>
212 </td>
212 </td>
213 </tr>
213 </tr>
214 %endif
214 %endif
215 %for hunk in filediff.hunks:
215 %for hunk in filediff.hunks:
216 <tr class="cb-hunk">
216 <tr class="cb-hunk">
217 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
217 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
218 ## TODO: dan: add ajax loading of more context here
218 ## TODO: dan: add ajax loading of more context here
219 ## <a href="#">
219 ## <a href="#">
220 <i class="icon-more"></i>
220 <i class="icon-more"></i>
221 ## </a>
221 ## </a>
222 </td>
222 </td>
223 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
223 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
224 @@
224 @@
225 -${hunk.source_start},${hunk.source_length}
225 -${hunk.source_start},${hunk.source_length}
226 +${hunk.target_start},${hunk.target_length}
226 +${hunk.target_start},${hunk.target_length}
227 ${hunk.section_header}
227 ${hunk.section_header}
228 </td>
228 </td>
229 </tr>
229 </tr>
230 %if c.diffmode == 'unified':
230 %if c.diffmode == 'unified':
231 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
231 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
232 %elif c.diffmode == 'sideside':
232 %elif c.diffmode == 'sideside':
233 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
233 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
234 %else:
234 %else:
235 <tr class="cb-line">
235 <tr class="cb-line">
236 <td>unknown diff mode</td>
236 <td>unknown diff mode</td>
237 </tr>
237 </tr>
238 %endif
238 %endif
239 %endfor
239 %endfor
240 </table>
240 </table>
241 </div>
241 </div>
242 %endfor
242 %endfor
243 </div>
243 </div>
244 </div>
244 </div>
245 </%def>
245 </%def>
246
246
247 <%def name="diff_ops(filediff)">
247 <%def name="diff_ops(filediff)">
248 <%
248 <%
249 stats = filediff['patch']['stats']
249 stats = filediff['patch']['stats']
250 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
250 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
251 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
251 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
252 %>
252 %>
253 <span class="pill">
253 <span class="pill">
254 %if filediff.source_file_path and filediff.target_file_path:
254 %if filediff.source_file_path and filediff.target_file_path:
255 %if filediff.source_file_path != filediff.target_file_path: # file was renamed
255 %if filediff.source_file_path != filediff.target_file_path: # file was renamed
256 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
256 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
257 %else:
257 %else:
258 ## file was modified
258 ## file was modified
259 <strong>${filediff.source_file_path}</strong>
259 <strong>${filediff.source_file_path}</strong>
260 %endif
260 %endif
261 %else:
261 %else:
262 %if filediff.source_file_path:
262 %if filediff.source_file_path:
263 ## file was deleted
263 ## file was deleted
264 <strong>${filediff.source_file_path}</strong>
264 <strong>${filediff.source_file_path}</strong>
265 %else:
265 %else:
266 ## file was added
266 ## file was added
267 <strong>${filediff.target_file_path}</strong>
267 <strong>${filediff.target_file_path}</strong>
268 %endif
268 %endif
269 %endif
269 %endif
270 </span>
270 </span>
271 <span class="pill-group" style="float: left">
271 <span class="pill-group" style="float: left">
272 %if filediff.patch['is_limited_diff']:
272 %if filediff.patch['is_limited_diff']:
273 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
273 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
274 %endif
274 %endif
275 %if RENAMED_FILENODE in stats['ops']:
275 %if RENAMED_FILENODE in stats['ops']:
276 <span class="pill" op="renamed">renamed</span>
276 <span class="pill" op="renamed">renamed</span>
277 %endif
277 %endif
278
278
279 %if NEW_FILENODE in stats['ops']:
279 %if NEW_FILENODE in stats['ops']:
280 <span class="pill" op="created">created</span>
280 <span class="pill" op="created">created</span>
281 %if filediff['target_mode'].startswith('120'):
281 %if filediff['target_mode'].startswith('120'):
282 <span class="pill" op="symlink">symlink</span>
282 <span class="pill" op="symlink">symlink</span>
283 %else:
283 %else:
284 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
284 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
285 %endif
285 %endif
286 %endif
286 %endif
287
287
288 %if DEL_FILENODE in stats['ops']:
288 %if DEL_FILENODE in stats['ops']:
289 <span class="pill" op="removed">removed</span>
289 <span class="pill" op="removed">removed</span>
290 %endif
290 %endif
291
291
292 %if CHMOD_FILENODE in stats['ops']:
292 %if CHMOD_FILENODE in stats['ops']:
293 <span class="pill" op="mode">
293 <span class="pill" op="mode">
294 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
294 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
295 </span>
295 </span>
296 %endif
296 %endif
297 </span>
297 </span>
298
298
299 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
299 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
300
300
301 <span class="pill-group" style="float: right">
301 <span class="pill-group" style="float: right">
302 %if BIN_FILENODE in stats['ops']:
302 %if BIN_FILENODE in stats['ops']:
303 <span class="pill" op="binary">binary</span>
303 <span class="pill" op="binary">binary</span>
304 %if MOD_FILENODE in stats['ops']:
304 %if MOD_FILENODE in stats['ops']:
305 <span class="pill" op="modified">modified</span>
305 <span class="pill" op="modified">modified</span>
306 %endif
306 %endif
307 %endif
307 %endif
308 %if stats['added']:
308 %if stats['added']:
309 <span class="pill" op="added">+${stats['added']}</span>
309 <span class="pill" op="added">+${stats['added']}</span>
310 %endif
310 %endif
311 %if stats['deleted']:
311 %if stats['deleted']:
312 <span class="pill" op="deleted">-${stats['deleted']}</span>
312 <span class="pill" op="deleted">-${stats['deleted']}</span>
313 %endif
313 %endif
314 </span>
314 </span>
315
315
316 </%def>
316 </%def>
317
317
318 <%def name="nice_mode(filemode)">
318 <%def name="nice_mode(filemode)">
319 ${filemode.startswith('100') and filemode[3:] or filemode}
319 ${filemode.startswith('100') and filemode[3:] or filemode}
320 </%def>
320 </%def>
321
321
322 <%def name="diff_menu(filediff, use_comments=False)">
322 <%def name="diff_menu(filediff, use_comments=False)">
323 <div class="filediff-menu">
323 <div class="filediff-menu">
324 %if filediff.diffset.source_ref:
324 %if filediff.diffset.source_ref:
325 %if filediff.patch['operation'] in ['D', 'M']:
325 %if filediff.patch['operation'] in ['D', 'M']:
326 <a
326 <a
327 class="tooltip"
327 class="tooltip"
328 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
328 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
329 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
329 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
330 >
330 >
331 ${_('Show file before')}
331 ${_('Show file before')}
332 </a>
332 </a>
333 %else:
333 %else:
334 <span
334 <span
335 class="tooltip"
335 class="tooltip"
336 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
336 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
337 >
337 >
338 ${_('Show file before')}
338 ${_('Show file before')}
339 </span>
339 </span>
340 %endif
340 %endif
341 %if filediff.patch['operation'] in ['A', 'M']:
341 %if filediff.patch['operation'] in ['A', 'M']:
342 <a
342 <a
343 class="tooltip"
343 class="tooltip"
344 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
344 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
345 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
345 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
346 >
346 >
347 ${_('Show file after')}
347 ${_('Show file after')}
348 </a>
348 </a>
349 %else:
349 %else:
350 <span
350 <span
351 class="tooltip"
351 class="tooltip"
352 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
352 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
353 >
353 >
354 ${_('Show file after')}
354 ${_('Show file after')}
355 </span>
355 </span>
356 %endif
356 %endif
357 <a
357 <a
358 class="tooltip"
358 class="tooltip"
359 title="${h.tooltip(_('Raw diff'))}"
359 title="${h.tooltip(_('Raw diff'))}"
360 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')}"
360 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')}"
361 >
361 >
362 ${_('Raw diff')}
362 ${_('Raw diff')}
363 </a>
363 </a>
364 <a
364 <a
365 class="tooltip"
365 class="tooltip"
366 title="${h.tooltip(_('Download diff'))}"
366 title="${h.tooltip(_('Download diff'))}"
367 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')}"
367 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')}"
368 >
368 >
369 ${_('Download diff')}
369 ${_('Download diff')}
370 </a>
370 </a>
371
371
372 ## 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)
372 ## 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)
373 %if hasattr(c, 'ignorews_url'):
373 %if hasattr(c, 'ignorews_url'):
374 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
374 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
375 %endif
375 %endif
376 %if hasattr(c, 'context_url'):
376 %if hasattr(c, 'context_url'):
377 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
377 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
378 %endif
378 %endif
379
379
380
380
381 %if use_comments:
381 %if use_comments:
382 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
382 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
383 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
383 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
384 </a>
384 </a>
385 %endif
385 %endif
386 %endif
386 %endif
387 </div>
387 </div>
388 </%def>
388 </%def>
389
389
390
390
391 <%namespace name="commentblock" file="/changeset/changeset_file_comment.html"/>
391 <%namespace name="commentblock" file="/changeset/changeset_file_comment.html"/>
392 <%def name="inline_comments_container(comments)">
392 <%def name="inline_comments_container(comments)">
393 <div class="inline-comments">
393 <div class="inline-comments">
394 %for comment in comments:
394 %for comment in comments:
395 ${commentblock.comment_block(comment, inline=True)}
395 ${commentblock.comment_block(comment, inline=True)}
396 %endfor
396 %endfor
397
397 <span onclick="return Rhodecode.comments.createComment(this)"
398 <span onclick="return Rhodecode.comments.createComment(this)"
398 class="btn btn-secondary cb-comment-add-button">
399 class="btn btn-secondary cb-comment-add-button ${'comment-outdated' if comments and comments[-1].outdated else ''}"
400 style="${'display: none;' if comments and comments[-1].outdated else ''}">
399 ${_('Add another comment')}
401 ${_('Add another comment')}
400 </span>
402 </span>
403
401 </div>
404 </div>
402 </%def>
405 </%def>
403
406
404
407
405 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
408 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
406 %for i, line in enumerate(hunk.sideside):
409 %for i, line in enumerate(hunk.sideside):
407 <%
410 <%
408 old_line_anchor, new_line_anchor = None, None
411 old_line_anchor, new_line_anchor = None, None
409 if line.original.lineno:
412 if line.original.lineno:
410 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
413 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
411 if line.modified.lineno:
414 if line.modified.lineno:
412 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
415 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
413 %>
416 %>
414 <tr class="cb-line">
417 <tr class="cb-line">
415 <td class="cb-data ${action_class(line.original.action)}"
418 <td class="cb-data ${action_class(line.original.action)}"
416 data-line-number="${line.original.lineno}"
419 data-line-number="${line.original.lineno}"
417 >
420 >
418 <div>
421 <div>
419 %if line.original.comments:
422 %if line.original.comments:
420 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
423 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
421 %endif
424 %endif
422 </div>
425 </div>
423 </td>
426 </td>
424 <td class="cb-lineno ${action_class(line.original.action)}"
427 <td class="cb-lineno ${action_class(line.original.action)}"
425 data-line-number="${line.original.lineno}"
428 data-line-number="${line.original.lineno}"
426 %if old_line_anchor:
429 %if old_line_anchor:
427 id="${old_line_anchor}"
430 id="${old_line_anchor}"
428 %endif
431 %endif
429 >
432 >
430 %if line.original.lineno:
433 %if line.original.lineno:
431 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
434 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
432 %endif
435 %endif
433 </td>
436 </td>
434 <td class="cb-content ${action_class(line.original.action)}"
437 <td class="cb-content ${action_class(line.original.action)}"
435 data-line-number="o${line.original.lineno}"
438 data-line-number="o${line.original.lineno}"
436 >
439 >
437 %if use_comments and line.original.lineno:
440 %if use_comments and line.original.lineno:
438 ${render_add_comment_button()}
441 ${render_add_comment_button()}
439 %endif
442 %endif
440 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
443 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
441 %if use_comments and line.original.lineno and line.original.comments:
444 %if use_comments and line.original.lineno and line.original.comments:
442 ${inline_comments_container(line.original.comments)}
445 ${inline_comments_container(line.original.comments)}
443 %endif
446 %endif
444 </td>
447 </td>
445 <td class="cb-data ${action_class(line.modified.action)}"
448 <td class="cb-data ${action_class(line.modified.action)}"
446 data-line-number="${line.modified.lineno}"
449 data-line-number="${line.modified.lineno}"
447 >
450 >
448 <div>
451 <div>
449 %if line.modified.comments:
452 %if line.modified.comments:
450 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
453 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
451 %endif
454 %endif
452 </div>
455 </div>
453 </td>
456 </td>
454 <td class="cb-lineno ${action_class(line.modified.action)}"
457 <td class="cb-lineno ${action_class(line.modified.action)}"
455 data-line-number="${line.modified.lineno}"
458 data-line-number="${line.modified.lineno}"
456 %if new_line_anchor:
459 %if new_line_anchor:
457 id="${new_line_anchor}"
460 id="${new_line_anchor}"
458 %endif
461 %endif
459 >
462 >
460 %if line.modified.lineno:
463 %if line.modified.lineno:
461 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
464 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
462 %endif
465 %endif
463 </td>
466 </td>
464 <td class="cb-content ${action_class(line.modified.action)}"
467 <td class="cb-content ${action_class(line.modified.action)}"
465 data-line-number="n${line.modified.lineno}"
468 data-line-number="n${line.modified.lineno}"
466 >
469 >
467 %if use_comments and line.modified.lineno:
470 %if use_comments and line.modified.lineno:
468 ${render_add_comment_button()}
471 ${render_add_comment_button()}
469 %endif
472 %endif
470 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
473 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
471 %if use_comments and line.modified.lineno and line.modified.comments:
474 %if use_comments and line.modified.lineno and line.modified.comments:
472 ${inline_comments_container(line.modified.comments)}
475 ${inline_comments_container(line.modified.comments)}
473 %endif
476 %endif
474 </td>
477 </td>
475 </tr>
478 </tr>
476 %endfor
479 %endfor
477 </%def>
480 </%def>
478
481
479
482
480 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
483 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
481 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
484 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
482 <%
485 <%
483 old_line_anchor, new_line_anchor = None, None
486 old_line_anchor, new_line_anchor = None, None
484 if old_line_no:
487 if old_line_no:
485 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
488 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
486 if new_line_no:
489 if new_line_no:
487 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
490 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
488 %>
491 %>
489 <tr class="cb-line">
492 <tr class="cb-line">
490 <td class="cb-data ${action_class(action)}">
493 <td class="cb-data ${action_class(action)}">
491 <div>
494 <div>
492 %if comments:
495 %if comments:
493 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
496 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
494 %endif
497 %endif
495 </div>
498 </div>
496 </td>
499 </td>
497 <td class="cb-lineno ${action_class(action)}"
500 <td class="cb-lineno ${action_class(action)}"
498 data-line-number="${old_line_no}"
501 data-line-number="${old_line_no}"
499 %if old_line_anchor:
502 %if old_line_anchor:
500 id="${old_line_anchor}"
503 id="${old_line_anchor}"
501 %endif
504 %endif
502 >
505 >
503 %if old_line_anchor:
506 %if old_line_anchor:
504 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
507 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
505 %endif
508 %endif
506 </td>
509 </td>
507 <td class="cb-lineno ${action_class(action)}"
510 <td class="cb-lineno ${action_class(action)}"
508 data-line-number="${new_line_no}"
511 data-line-number="${new_line_no}"
509 %if new_line_anchor:
512 %if new_line_anchor:
510 id="${new_line_anchor}"
513 id="${new_line_anchor}"
511 %endif
514 %endif
512 >
515 >
513 %if new_line_anchor:
516 %if new_line_anchor:
514 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
517 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
515 %endif
518 %endif
516 </td>
519 </td>
517 <td class="cb-content ${action_class(action)}"
520 <td class="cb-content ${action_class(action)}"
518 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
521 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
519 >
522 >
520 %if use_comments:
523 %if use_comments:
521 ${render_add_comment_button()}
524 ${render_add_comment_button()}
522 %endif
525 %endif
523 <span class="cb-code">${action} ${content or '' | n}</span>
526 <span class="cb-code">${action} ${content or '' | n}</span>
524 %if use_comments and comments:
527 %if use_comments and comments:
525 ${inline_comments_container(comments)}
528 ${inline_comments_container(comments)}
526 %endif
529 %endif
527 </td>
530 </td>
528 </tr>
531 </tr>
529 %endfor
532 %endfor
530 </%def>
533 </%def>
531
534
532 <%def name="render_add_comment_button()">
535 <%def name="render_add_comment_button()">
533 <button
536 <button
534 class="btn btn-small btn-primary cb-comment-box-opener"
537 class="btn btn-small btn-primary cb-comment-box-opener"
535 onclick="return Rhodecode.comments.createComment(this)"
538 onclick="return Rhodecode.comments.createComment(this)"
536 ><span>+</span></button>
539 ><span>+</span></button>
537 </%def>
540 </%def>
538
541
539 <%def name="render_diffset_menu()">
542 <%def name="render_diffset_menu()">
540 <div class="diffset-menu clearinner">
543 <div class="diffset-menu clearinner">
541 <div class="pull-right">
544 <div class="pull-right">
542 <div class="btn-group">
545 <div class="btn-group">
543 <a
546 <a
544 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
547 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
545 title="${_('View side by side')}"
548 title="${_('View side by side')}"
546 href="${h.url_replace(diffmode='sideside')}">
549 href="${h.url_replace(diffmode='sideside')}">
547 <span>${_('Side by Side')}</span>
550 <span>${_('Side by Side')}</span>
548 </a>
551 </a>
549 <a
552 <a
550 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
553 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
551 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
554 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
552 <span>${_('Unified')}</span>
555 <span>${_('Unified')}</span>
553 </a>
556 </a>
554 </div>
557 </div>
555 </div>
558 </div>
556 <div class="pull-left">
559 <div class="pull-left">
557 <div class="btn-group">
560 <div class="btn-group">
558 <a
561 <a
559 class="btn"
562 class="btn"
560 href="#"
563 href="#"
561 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All')}</a>
564 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All')}</a>
562 <a
565 <a
563 class="btn"
566 class="btn"
564 href="#"
567 href="#"
565 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All')}</a>
568 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All')}</a>
566 </div>
569 </div>
567 </div>
570 </div>
568 </div>
571 </div>
569 </%def>
572 </%def>
@@ -1,967 +1,964 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/debug_style/index.html"/>
2 <%inherit file="/debug_style/index.html"/>
3
3
4 <%def name="breadcrumbs_links()">
4 <%def name="breadcrumbs_links()">
5 ${h.link_to(_('Style'), h.url('debug_style_home'))}
5 ${h.link_to(_('Style'), h.url('debug_style_home'))}
6 &raquo;
6 &raquo;
7 ${c.active}
7 ${c.active}
8 </%def>
8 </%def>
9
9
10
10
11 <%def name="real_main()">
11 <%def name="real_main()">
12 <div class="box">
12 <div class="box">
13 <div class="title">
13 <div class="title">
14 ${self.breadcrumbs()}
14 ${self.breadcrumbs()}
15 </div>
15 </div>
16
16
17 <div class='sidebar-col-wrapper'>
17 <div class='sidebar-col-wrapper'>
18 ${self.sidebar()}
18 ${self.sidebar()}
19
19
20 <div class="main-content">
20 <div class="main-content">
21
21
22 <h2>Collapsable Content</h2>
22 <h2>Collapsable Content</h2>
23 <p>Where a section may have a very long list of information, it can be desirable to use collapsable content. There is a premade function for showing/hiding elements, though its use may or may not be practical, depending on the situation. Use it, or don't, on a case-by-case basis.</p>
23 <p>Where a section may have a very long list of information, it can be desirable to use collapsable content. There is a premade function for showing/hiding elements, though its use may or may not be practical, depending on the situation. Use it, or don't, on a case-by-case basis.</p>
24
24
25 <p><strong>To use the collapsable-content function:</strong> Create a toggle button using <code>&lt;div class="btn-collapse"&gt;Show More&lt;/div&gt;</code> and a data attribute using <code>data-toggle</code>. Clicking this button will toggle any sibling element(s) containing the class <code>collapsable-content</code> and an identical <code>data-toggle</code> attribute. It will also change the button to read "Show Less"; another click toggles it back to the previous state. Ideally, use pre-existing elements and add the class and attribute; creating a new div around the existing content may lead to unexpected results, as the toggle function will use <code>display:block</code> if no previous display specification was found.
25 <p><strong>To use the collapsable-content function:</strong> Create a toggle button using <code>&lt;div class="btn-collapse"&gt;Show More&lt;/div&gt;</code> and a data attribute using <code>data-toggle</code>. Clicking this button will toggle any sibling element(s) containing the class <code>collapsable-content</code> and an identical <code>data-toggle</code> attribute. It will also change the button to read "Show Less"; another click toggles it back to the previous state. Ideally, use pre-existing elements and add the class and attribute; creating a new div around the existing content may lead to unexpected results, as the toggle function will use <code>display:block</code> if no previous display specification was found.
26 </p>
26 </p>
27 <p>Notes:</p>
27 <p>Notes:</p>
28 <ul>
28 <ul>
29 <li>Changes made to the text of the button will require adjustment to the function, but for the sake of consistency and user experience, this is best avoided. </li>
29 <li>Changes made to the text of the button will require adjustment to the function, but for the sake of consistency and user experience, this is best avoided. </li>
30 <li>Collapsable content inside of a pjax loaded container will require <code>collapsableContent();</code> to be called from within the container. No variables are necessary.</li>
30 <li>Collapsable content inside of a pjax loaded container will require <code>collapsableContent();</code> to be called from within the container. No variables are necessary.</li>
31 </ul>
31 </ul>
32
32
33 </div> <!-- .main-content -->
33 </div> <!-- .main-content -->
34 </div> <!-- .sidebar-col-wrapper -->
34 </div> <!-- .sidebar-col-wrapper -->
35 </div> <!-- .box -->
35 </div> <!-- .box -->
36
36
37 <!-- CONTENT -->
37 <!-- CONTENT -->
38 <div id="content" class="wrapper">
38 <div id="content" class="wrapper">
39
39
40 <div class="main">
40 <div class="main">
41
41
42 <div class="box">
42 <div class="box">
43 <div class="title">
43 <div class="title">
44 <h1>
44 <h1>
45 Diff: enable filename with spaces on diffs
45 Diff: enable filename with spaces on diffs
46 </h1>
46 </h1>
47 <h1>
47 <h1>
48 <i class="icon-hg" ></i>
48 <i class="icon-hg" ></i>
49
49
50 <i class="icon-lock"></i>
50 <i class="icon-lock"></i>
51 <span><a href="/rhodecode-momentum">rhodecode-momentum</a></span>
51 <span><a href="/rhodecode-momentum">rhodecode-momentum</a></span>
52
52
53 </h1>
53 </h1>
54 </div>
54 </div>
55
55
56 <div class="box pr-summary">
56 <div class="box pr-summary">
57 <div class="summary-details block-left">
57 <div class="summary-details block-left">
58
58
59 <div class="pr-details-title">
59 <div class="pr-details-title">
60
60
61 Pull request #720 From Tue, 17 Feb 2015 16:21:38
61 Pull request #720 From Tue, 17 Feb 2015 16:21:38
62 <div class="btn-collapse" data-toggle="description">Show More</div>
62 <div class="btn-collapse" data-toggle="description">Show More</div>
63 </div>
63 </div>
64 <div id="summary" class="fields pr-details-content">
64 <div id="summary" class="fields pr-details-content">
65 <div class="field">
65 <div class="field">
66 <div class="label-summary">
66 <div class="label-summary">
67 <label>Origin:</label>
67 <label>Origin:</label>
68 </div>
68 </div>
69 <div class="input">
69 <div class="input">
70 <div>
70 <div>
71 <span class="tag">
71 <span class="tag">
72 <a href="/andersonsantos/rhodecode-momentum-fork#fix_574">book: fix_574</a>
72 <a href="/andersonsantos/rhodecode-momentum-fork#fix_574">book: fix_574</a>
73 </span>
73 </span>
74 <span class="clone-url">
74 <span class="clone-url">
75 <a href="/andersonsantos/rhodecode-momentum-fork">https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork</a>
75 <a href="/andersonsantos/rhodecode-momentum-fork">https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork</a>
76 </span>
76 </span>
77 </div>
77 </div>
78 <div>
78 <div>
79 <br>
79 <br>
80 <input type="text" value="hg pull -r 46b3d50315f0 https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork" readonly="readonly">
80 <input type="text" value="hg pull -r 46b3d50315f0 https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork" readonly="readonly">
81 </div>
81 </div>
82 </div>
82 </div>
83 </div>
83 </div>
84 <div class="field">
84 <div class="field">
85 <div class="label-summary">
85 <div class="label-summary">
86 <label>Review:</label>
86 <label>Review:</label>
87 </div>
87 </div>
88 <div class="input">
88 <div class="input">
89 <div class="flag_status under_review tooltip pull-left" title="Pull request status calculated from votes"></div>
89 <div class="flag_status under_review tooltip pull-left" title="Pull request status calculated from votes"></div>
90 <span class="changeset-status-lbl tooltip" title="Pull request status calculated from votes">
90 <span class="changeset-status-lbl tooltip" title="Pull request status calculated from votes">
91 Under Review
91 Under Review
92 </span>
92 </span>
93
93
94 </div>
94 </div>
95 </div>
95 </div>
96 <div class="field collapsable-content" data-toggle="description">
96 <div class="field collapsable-content" data-toggle="description">
97 <div class="label-summary">
97 <div class="label-summary">
98 <label>Description:</label>
98 <label>Description:</label>
99 </div>
99 </div>
100 <div class="input">
100 <div class="input">
101 <div class="pr-description">Fixing issue <a class="issue- tracker-link" href="http://bugs.rhodecode.com/issues/574"># 574</a>, changing regex for capturing filenames</div>
101 <div class="pr-description">Fixing issue <a class="issue- tracker-link" href="http://bugs.rhodecode.com/issues/574"># 574</a>, changing regex for capturing filenames</div>
102 </div>
102 </div>
103 </div>
103 </div>
104 <div class="field collapsable-content" data-toggle="description">
104 <div class="field collapsable-content" data-toggle="description">
105 <div class="label-summary">
105 <div class="label-summary">
106 <label>Comments:</label>
106 <label>Comments:</label>
107 </div>
107 </div>
108 <div class="input">
108 <div class="input">
109 <div>
109 <div>
110 <div class="comments-number">
110 <div class="comments-number">
111 <a href="#inline-comments-container">0 Pull request comments</a>,
111 <a href="#inline-comments-container">0 Pull request comments</a>,
112 0 Inline Comments
112 0 Inline Comments
113 </div>
113 </div>
114 </div>
114 </div>
115 </div>
115 </div>
116 </div>
116 </div>
117 </div>
117 </div>
118 </div>
118 </div>
119 <div>
119 <div>
120 <div class="reviewers-title block-right">
120 <div class="reviewers-title block-right">
121 <div class="pr-details-title">
121 <div class="pr-details-title">
122 Author
122 Author
123 </div>
123 </div>
124 </div>
124 </div>
125 <div class="block-right pr-details-content reviewers">
125 <div class="block-right pr-details-content reviewers">
126 <ul class="group_members">
126 <ul class="group_members">
127 <li>
127 <li>
128 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
128 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
129 <span class="user"> <a href="/_profiles/lolek">lolek (Lolek Santos)</a></span>
129 <span class="user"> <a href="/_profiles/lolek">lolek (Lolek Santos)</a></span>
130 </li>
130 </li>
131 </ul>
131 </ul>
132 </div>
132 </div>
133 <div class="reviewers-title block-right">
133 <div class="reviewers-title block-right">
134 <div class="pr-details-title">
134 <div class="pr-details-title">
135 Pull request reviewers
135 Pull request reviewers
136 <span class="btn-collapse" data-toggle="reviewers">Show More</span>
136 <span class="btn-collapse" data-toggle="reviewers">Show More</span>
137 </div>
137 </div>
138
138
139 </div>
139 </div>
140 <div id="reviewers" class="block-right pr-details-content reviewers">
140 <div id="reviewers" class="block-right pr-details-content reviewers">
141
141
142 <ul id="review_members" class="group_members">
142 <ul id="review_members" class="group_members">
143 <li id="reviewer_70">
143 <li id="reviewer_70">
144 <div class="reviewers_member">
144 <div class="reviewers_member">
145 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
145 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
146 <div class="flag_status rejected pull-left reviewer_member_status"></div>
146 <div class="flag_status rejected pull-left reviewer_member_status"></div>
147 </div>
147 </div>
148 <img class="gravatar" src="https://secure.gravatar.com/avatar/153a0fab13160b3e64a2cbc7c0373506?d=identicon&amp;s=32" height="16" width="16">
148 <img class="gravatar" src="https://secure.gravatar.com/avatar/153a0fab13160b3e64a2cbc7c0373506?d=identicon&amp;s=32" height="16" width="16">
149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
150 </div>
150 </div>
151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
152 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(70, true)" style="visibility: hidden;">
152 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(70, true)" style="visibility: hidden;">
153 <i class="icon-remove-sign"></i>
153 <i class="icon-remove-sign"></i>
154 </div>
154 </div>
155 </li>
155 </li>
156 <li id="reviewer_33" class="collapsable-content" data-toggle="reviewers">
156 <li id="reviewer_33" class="collapsable-content" data-toggle="reviewers">
157 <div class="reviewers_member">
157 <div class="reviewers_member">
158 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
158 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
159 <div class="flag_status approved pull-left reviewer_member_status"></div>
159 <div class="flag_status approved pull-left reviewer_member_status"></div>
160 </div>
160 </div>
161 <img class="gravatar" src="https://secure.gravatar.com/avatar/ffd6a317ec2b66be880143cd8459d0d9?d=identicon&amp;s=32" height="16" width="16">
161 <img class="gravatar" src="https://secure.gravatar.com/avatar/ffd6a317ec2b66be880143cd8459d0d9?d=identicon&amp;s=32" height="16" width="16">
162 <span class="user"> <a href="/_profiles/jenkins-tests">garbas (Rok Garbas)</a> (reviewer)</span>
162 <span class="user"> <a href="/_profiles/jenkins-tests">garbas (Rok Garbas)</a> (reviewer)</span>
163 </div>
163 </div>
164 </li>
164 </li>
165 <li id="reviewer_2" class="collapsable-content" data-toggle="reviewers">
165 <li id="reviewer_2" class="collapsable-content" data-toggle="reviewers">
166 <div class="reviewers_member">
166 <div class="reviewers_member">
167 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
167 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
168 <div class="flag_status not_reviewed pull-left reviewer_member_status"></div>
168 <div class="flag_status not_reviewed pull-left reviewer_member_status"></div>
169 </div>
169 </div>
170 <img class="gravatar" src="https://secure.gravatar.com/avatar/aad9d40cac1259ea39b5578554ad9d64?d=identicon&amp;s=32" height="16" width="16">
170 <img class="gravatar" src="https://secure.gravatar.com/avatar/aad9d40cac1259ea39b5578554ad9d64?d=identicon&amp;s=32" height="16" width="16">
171 <span class="user"> <a href="/_profiles/jenkins-tests">marcink (Marcin Kuzminski)</a> (reviewer)</span>
171 <span class="user"> <a href="/_profiles/jenkins-tests">marcink (Marcin Kuzminski)</a> (reviewer)</span>
172 </div>
172 </div>
173 </li>
173 </li>
174 <li id="reviewer_36" class="collapsable-content" data-toggle="reviewers">
174 <li id="reviewer_36" class="collapsable-content" data-toggle="reviewers">
175 <div class="reviewers_member">
175 <div class="reviewers_member">
176 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
176 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
177 <div class="flag_status approved pull-left reviewer_member_status"></div>
177 <div class="flag_status approved pull-left reviewer_member_status"></div>
178 </div>
178 </div>
179 <img class="gravatar" src="https://secure.gravatar.com/avatar/7a4da001a0af0016ed056ab523255db9?d=identicon&amp;s=32" height="16" width="16">
179 <img class="gravatar" src="https://secure.gravatar.com/avatar/7a4da001a0af0016ed056ab523255db9?d=identicon&amp;s=32" height="16" width="16">
180 <span class="user"> <a href="/_profiles/jenkins-tests">johbo (Johannes Bornhold)</a> (reviewer)</span>
180 <span class="user"> <a href="/_profiles/jenkins-tests">johbo (Johannes Bornhold)</a> (reviewer)</span>
181 </div>
181 </div>
182 </li>
182 </li>
183 <li id="reviewer_47" class="collapsable-content" data-toggle="reviewers">
183 <li id="reviewer_47" class="collapsable-content" data-toggle="reviewers">
184 <div class="reviewers_member">
184 <div class="reviewers_member">
185 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
185 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
186 <div class="flag_status under_review pull-left reviewer_member_status"></div>
186 <div class="flag_status under_review pull-left reviewer_member_status"></div>
187 </div>
187 </div>
188 <img class="gravatar" src="https://secure.gravatar.com/avatar/8f6dc00dce79d6bd7d415be5cea6a008?d=identicon&amp;s=32" height="16" width="16">
188 <img class="gravatar" src="https://secure.gravatar.com/avatar/8f6dc00dce79d6bd7d415be5cea6a008?d=identicon&amp;s=32" height="16" width="16">
189 <span class="user"> <a href="/_profiles/jenkins-tests">lisaq (Lisa Quatmann)</a> (reviewer)</span>
189 <span class="user"> <a href="/_profiles/jenkins-tests">lisaq (Lisa Quatmann)</a> (reviewer)</span>
190 </div>
190 </div>
191 </li>
191 </li>
192 <li id="reviewer_49" class="collapsable-content" data-toggle="reviewers">
192 <li id="reviewer_49" class="collapsable-content" data-toggle="reviewers">
193 <div class="reviewers_member">
193 <div class="reviewers_member">
194 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
194 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
195 <div class="flag_status approved pull-left reviewer_member_status"></div>
195 <div class="flag_status approved pull-left reviewer_member_status"></div>
196 </div>
196 </div>
197 <img class="gravatar" src="https://secure.gravatar.com/avatar/89f722927932a8f737a0feafb03a606e?d=identicon&amp;s=32" height="16" width="16">
197 <img class="gravatar" src="https://secure.gravatar.com/avatar/89f722927932a8f737a0feafb03a606e?d=identicon&amp;s=32" height="16" width="16">
198 <span class="user"> <a href="/_profiles/jenkins-tests">paris (Paris Kolios)</a> (reviewer)</span>
198 <span class="user"> <a href="/_profiles/jenkins-tests">paris (Paris Kolios)</a> (reviewer)</span>
199 </div>
199 </div>
200 </li>
200 </li>
201 <li id="reviewer_50" class="collapsable-content" data-toggle="reviewers">
201 <li id="reviewer_50" class="collapsable-content" data-toggle="reviewers">
202 <div class="reviewers_member">
202 <div class="reviewers_member">
203 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
203 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
204 <div class="flag_status approved pull-left reviewer_member_status"></div>
204 <div class="flag_status approved pull-left reviewer_member_status"></div>
205 </div>
205 </div>
206 <img class="gravatar" src="https://secure.gravatar.com/avatar/081322c975e8545ec269372405fbd016?d=identicon&amp;s=32" height="16" width="16">
206 <img class="gravatar" src="https://secure.gravatar.com/avatar/081322c975e8545ec269372405fbd016?d=identicon&amp;s=32" height="16" width="16">
207 <span class="user"> <a href="/_profiles/jenkins-tests">ergo (Marcin Lulek)</a> (reviewer)</span>
207 <span class="user"> <a href="/_profiles/jenkins-tests">ergo (Marcin Lulek)</a> (reviewer)</span>
208 </div>
208 </div>
209 </li>
209 </li>
210 <li id="reviewer_54" class="collapsable-content" data-toggle="reviewers">
210 <li id="reviewer_54" class="collapsable-content" data-toggle="reviewers">
211 <div class="reviewers_member">
211 <div class="reviewers_member">
212 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
212 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
213 <div class="flag_status under_review pull-left reviewer_member_status"></div>
213 <div class="flag_status under_review pull-left reviewer_member_status"></div>
214 </div>
214 </div>
215 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
215 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
216 <span class="user"> <a href="/_profiles/jenkins-tests">anderson (Anderson Santos)</a> (reviewer)</span>
216 <span class="user"> <a href="/_profiles/jenkins-tests">anderson (Anderson Santos)</a> (reviewer)</span>
217 </div>
217 </div>
218 </li>
218 </li>
219 <li id="reviewer_57" class="collapsable-content" data-toggle="reviewers">
219 <li id="reviewer_57" class="collapsable-content" data-toggle="reviewers">
220 <div class="reviewers_member">
220 <div class="reviewers_member">
221 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
221 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
222 <div class="flag_status approved pull-left reviewer_member_status"></div>
222 <div class="flag_status approved pull-left reviewer_member_status"></div>
223 </div>
223 </div>
224 <img class="gravatar" src="https://secure.gravatar.com/avatar/23e2ee8f5fd462cba8129a40cc1e896c?d=identicon&amp;s=32" height="16" width="16">
224 <img class="gravatar" src="https://secure.gravatar.com/avatar/23e2ee8f5fd462cba8129a40cc1e896c?d=identicon&amp;s=32" height="16" width="16">
225 <span class="user"> <a href="/_profiles/jenkins-tests">gmgauthier (Greg Gauthier)</a> (reviewer)</span>
225 <span class="user"> <a href="/_profiles/jenkins-tests">gmgauthier (Greg Gauthier)</a> (reviewer)</span>
226 </div>
226 </div>
227 </li>
227 </li>
228 <li id="reviewer_31" class="collapsable-content" data-toggle="reviewers">
228 <li id="reviewer_31" class="collapsable-content" data-toggle="reviewers">
229 <div class="reviewers_member">
229 <div class="reviewers_member">
230 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
230 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
231 <div class="flag_status under_review pull-left reviewer_member_status"></div>
231 <div class="flag_status under_review pull-left reviewer_member_status"></div>
232 </div>
232 </div>
233 <img class="gravatar" src="https://secure.gravatar.com/avatar/0c9a7e6674b6f0b35d98dbe073e3f0ab?d=identicon&amp;s=32" height="16" width="16">
233 <img class="gravatar" src="https://secure.gravatar.com/avatar/0c9a7e6674b6f0b35d98dbe073e3f0ab?d=identicon&amp;s=32" height="16" width="16">
234 <span class="user"> <a href="/_profiles/jenkins-tests">ostrobel (Oliver Strobel)</a> (reviewer)</span>
234 <span class="user"> <a href="/_profiles/jenkins-tests">ostrobel (Oliver Strobel)</a> (reviewer)</span>
235 </div>
235 </div>
236 </li>
236 </li>
237 </ul>
237 </ul>
238 <div id="add_reviewer_input" class="ac" style="display: none;">
238 <div id="add_reviewer_input" class="ac" style="display: none;">
239 </div>
239 </div>
240 </div>
240 </div>
241 </div>
241 </div>
242 </div>
242 </div>
243 </div>
243 </div>
244 <div class="box">
244 <div class="box">
245 <div class="table" >
245 <div class="table" >
246 <div id="changeset_compare_view_content">
246 <div id="changeset_compare_view_content">
247 <div class="compare_view_commits_title">
247 <div class="compare_view_commits_title">
248 <h2>Compare View: 6 commits<span class="btn-collapse" data-toggle="commits">Show More</span></h2>
248 <h2>Compare View: 6 commits<span class="btn-collapse" data-toggle="commits">Show More</span></h2>
249
249
250 </div>
250 </div>
251 <div class="container">
251 <div class="container">
252
252
253
253
254 <table class="rctable compare_view_commits">
254 <table class="rctable compare_view_commits">
255 <tr>
255 <tr>
256 <th>Time</th>
256 <th>Time</th>
257 <th>Author</th>
257 <th>Author</th>
258 <th>Commit</th>
258 <th>Commit</th>
259 <th></th>
259 <th></th>
260 <th>Title</th>
260 <th>Title</th>
261 </tr>
261 </tr>
262 <tr id="row-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" commit_id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="compare_select">
262 <tr id="row-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" commit_id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="compare_select">
263 <td class="td-time">
263 <td class="td-time">
264 <span class="tooltip" title="3 hours and 23 minutes ago" tt_title="3 hours and 23 minutes ago">2015-02-18 10:13:34</span>
264 <span class="tooltip" title="3 hours and 23 minutes ago" tt_title="3 hours and 23 minutes ago">2015-02-18 10:13:34</span>
265 </td>
265 </td>
266 <td class="td-user">
266 <td class="td-user">
267 <div class="gravatar_with_user">
267 <div class="gravatar_with_user">
268 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
268 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
269 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
269 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
270 </div>
270 </div>
271 </td>
271 </td>
272 <td class="td-hash">
272 <td class="td-hash">
273 <code>
273 <code>
274 <a href="/brian/documentation-rep/changeset/7e83e5cd7812dd9e055ce30e77c65cdc08154b43">r395:7e83e5cd7812</a>
274 <a href="/brian/documentation-rep/changeset/7e83e5cd7812dd9e055ce30e77c65cdc08154b43">r395:7e83e5cd7812</a>
275 </code>
275 </code>
276 </td>
276 </td>
277 <td class="expand_commit" data-commit-id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" title="Expand commit message">
277 <td class="expand_commit" data-commit-id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" title="Expand commit message">
278 <div class="show_more_col">
278 <div class="show_more_col">
279 <i class="show_more"></i>
279 <i class="show_more"></i>
280 </div>
280 </div>
281 </td>
281 </td>
282 <td class="mid td-description">
282 <td class="mid td-description">
283 <div class="log-container truncate-wrap">
283 <div class="log-container truncate-wrap">
284 <div id="c-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="message truncate">rep: added how we doc to guide</div>
284 <div id="c-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="message truncate">rep: added how we doc to guide</div>
285 </div>
285 </div>
286 </td>
286 </td>
287 </tr>
287 </tr>
288 <tr id="row-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" commit_id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="compare_select">
288 <tr id="row-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" commit_id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="compare_select">
289 <td class="td-time">
289 <td class="td-time">
290 <span class="tooltip" title="4 hours and 18 minutes ago">2015-02-18 09:18:31</span>
290 <span class="tooltip" title="4 hours and 18 minutes ago">2015-02-18 09:18:31</span>
291 </td>
291 </td>
292 <td class="td-user">
292 <td class="td-user">
293 <div class="gravatar_with_user">
293 <div class="gravatar_with_user">
294 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
294 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
295 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
295 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
296 </div>
296 </div>
297 </td>
297 </td>
298 <td class="td-hash">
298 <td class="td-hash">
299 <code>
299 <code>
300 <a href="/brian/documentation-rep/changeset/48ce1581bdb3aa7679c246cbdd3fb030623f5c87">r394:48ce1581bdb3</a>
300 <a href="/brian/documentation-rep/changeset/48ce1581bdb3aa7679c246cbdd3fb030623f5c87">r394:48ce1581bdb3</a>
301 </code>
301 </code>
302 </td>
302 </td>
303 <td class="expand_commit" data-commit-id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" title="Expand commit message">
303 <td class="expand_commit" data-commit-id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" title="Expand commit message">
304 <div class="show_more_col">
304 <div class="show_more_col">
305 <i class="show_more"></i>
305 <i class="show_more"></i>
306 </div>
306 </div>
307 </td>
307 </td>
308 <td class="mid td-description">
308 <td class="mid td-description">
309 <div class="log-container truncate-wrap">
309 <div class="log-container truncate-wrap">
310 <div id="c-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="message truncate">repo 0004 - typo</div>
310 <div id="c-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="message truncate">repo 0004 - typo</div>
311 </div>
311 </div>
312 </td>
312 </td>
313 </tr>
313 </tr>
314 <tr id="row-982d857aafb4c71e7686e419c32b71c9a837257d" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d" class="compare_select collapsable-content" data-toggle="commits">
314 <tr id="row-982d857aafb4c71e7686e419c32b71c9a837257d" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d" class="compare_select collapsable-content" data-toggle="commits">
315 <td class="td-time">
315 <td class="td-time">
316 <span class="tooltip" title="4 hours and 22 minutes ago">2015-02-18 09:14:45</span>
316 <span class="tooltip" title="4 hours and 22 minutes ago">2015-02-18 09:14:45</span>
317 </td>
317 </td>
318 <td class="td-user">
318 <td class="td-user">
319 <span class="gravatar" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d">
319 <span class="gravatar" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d">
320 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
320 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
321 </span>
321 </span>
322 <span class="author">brian (Brian Butler)</span>
322 <span class="author">brian (Brian Butler)</span>
323 </td>
323 </td>
324 <td class="td-hash">
324 <td class="td-hash">
325 <code>
325 <code>
326 <a href="/brian/documentation-rep/changeset/982d857aafb4c71e7686e419c32b71c9a837257d">r393:982d857aafb4</a>
326 <a href="/brian/documentation-rep/changeset/982d857aafb4c71e7686e419c32b71c9a837257d">r393:982d857aafb4</a>
327 </code>
327 </code>
328 </td>
328 </td>
329 <td class="expand_commit" data-commit-id="982d857aafb4c71e7686e419c32b71c9a837257d" title="Expand commit message">
329 <td class="expand_commit" data-commit-id="982d857aafb4c71e7686e419c32b71c9a837257d" title="Expand commit message">
330 <div class="show_more_col">
330 <div class="show_more_col">
331 <i class="show_more"></i>
331 <i class="show_more"></i>
332 </div>
332 </div>
333 </td>
333 </td>
334 <td class="mid td-description">
334 <td class="mid td-description">
335 <div class="log-container truncate-wrap">
335 <div class="log-container truncate-wrap">
336 <div id="c-982d857aafb4c71e7686e419c32b71c9a837257d" class="message truncate">internals: how to doc section added</div>
336 <div id="c-982d857aafb4c71e7686e419c32b71c9a837257d" class="message truncate">internals: how to doc section added</div>
337 </div>
337 </div>
338 </td>
338 </td>
339 </tr>
339 </tr>
340 <tr id="row-4c7258ad1af6dae91bbaf87a933e3597e676fab8" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="compare_select collapsable-content" data-toggle="commits">
340 <tr id="row-4c7258ad1af6dae91bbaf87a933e3597e676fab8" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="compare_select collapsable-content" data-toggle="commits">
341 <td class="td-time">
341 <td class="td-time">
342 <span class="tooltip" title="20 hours and 16 minutes ago">2015-02-17 17:20:44</span>
342 <span class="tooltip" title="20 hours and 16 minutes ago">2015-02-17 17:20:44</span>
343 </td>
343 </td>
344 <td class="td-user">
344 <td class="td-user">
345 <span class="gravatar" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8">
345 <span class="gravatar" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8">
346 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
346 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
347 </span>
347 </span>
348 <span class="author">brian (Brian Butler)</span>
348 <span class="author">brian (Brian Butler)</span>
349 </td>
349 </td>
350 <td class="td-hash">
350 <td class="td-hash">
351 <code>
351 <code>
352 <a href="/brian/documentation-rep/changeset/4c7258ad1af6dae91bbaf87a933e3597e676fab8">r392:4c7258ad1af6</a>
352 <a href="/brian/documentation-rep/changeset/4c7258ad1af6dae91bbaf87a933e3597e676fab8">r392:4c7258ad1af6</a>
353 </code>
353 </code>
354 </td>
354 </td>
355 <td class="expand_commit" data-commit-id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" title="Expand commit message">
355 <td class="expand_commit" data-commit-id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" title="Expand commit message">
356 <div class="show_more_col">
356 <div class="show_more_col">
357 <i class="show_more"></i>
357 <i class="show_more"></i>
358 </div>
358 </div>
359 </td>
359 </td>
360 <td class="mid td-description">
360 <td class="mid td-description">
361 <div class="log-container truncate-wrap">
361 <div class="log-container truncate-wrap">
362 <div id="c-4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="message truncate">REP: 0004 Documentation standards</div>
362 <div id="c-4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="message truncate">REP: 0004 Documentation standards</div>
363 </div>
363 </div>
364 </td>
364 </td>
365 </tr>
365 </tr>
366 <tr id="row-46b3d50315f0f2b1f64485ac95af4f384948f9cb" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="compare_select collapsable-content" data-toggle="commits">
366 <tr id="row-46b3d50315f0f2b1f64485ac95af4f384948f9cb" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="compare_select collapsable-content" data-toggle="commits">
367 <td class="td-time">
367 <td class="td-time">
368 <span class="tooltip" title="18 hours and 19 minutes ago">2015-02-17 16:18:49</span>
368 <span class="tooltip" title="18 hours and 19 minutes ago">2015-02-17 16:18:49</span>
369 </td>
369 </td>
370 <td class="td-user">
370 <td class="td-user">
371 <span class="gravatar" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb">
371 <span class="gravatar" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb">
372 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
372 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
373 </span>
373 </span>
374 <span class="author">anderson (Anderson Santos)</span>
374 <span class="author">anderson (Anderson Santos)</span>
375 </td>
375 </td>
376 <td class="td-hash">
376 <td class="td-hash">
377 <code>
377 <code>
378 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/46b3d50315f0f2b1f64485ac95af4f384948f9cb">r8743:46b3d50315f0</a>
378 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/46b3d50315f0f2b1f64485ac95af4f384948f9cb">r8743:46b3d50315f0</a>
379 </code>
379 </code>
380 </td>
380 </td>
381 <td class="expand_commit" data-commit-id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" title="Expand commit message">
381 <td class="expand_commit" data-commit-id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" title="Expand commit message">
382 <div class="show_more_col">
382 <div class="show_more_col">
383 <i class="show_more" ></i>
383 <i class="show_more" ></i>
384 </div>
384 </div>
385 </td>
385 </td>
386 <td class="mid td-description">
386 <td class="mid td-description">
387 <div class="log-container truncate-wrap">
387 <div class="log-container truncate-wrap">
388 <div id="c-46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="message truncate">Diff: created tests for the diff with filenames with spaces</div>
388 <div id="c-46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="message truncate">Diff: created tests for the diff with filenames with spaces</div>
389
389
390 </div>
390 </div>
391 </td>
391 </td>
392 </tr>
392 </tr>
393 <tr id="row-1e57d2549bd6c34798075bf05ac39f708bb33b90" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90" class="compare_select collapsable-content" data-toggle="commits">
393 <tr id="row-1e57d2549bd6c34798075bf05ac39f708bb33b90" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90" class="compare_select collapsable-content" data-toggle="commits">
394 <td class="td-time">
394 <td class="td-time">
395 <span class="tooltip" title="2 days ago">2015-02-16 10:06:08</span>
395 <span class="tooltip" title="2 days ago">2015-02-16 10:06:08</span>
396 </td>
396 </td>
397 <td class="td-user">
397 <td class="td-user">
398 <span class="gravatar" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90">
398 <span class="gravatar" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90">
399 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
399 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
400 </span>
400 </span>
401 <span class="author">anderson (Anderson Santos)</span>
401 <span class="author">anderson (Anderson Santos)</span>
402 </td>
402 </td>
403 <td class="td-hash">
403 <td class="td-hash">
404 <code>
404 <code>
405 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/1e57d2549bd6c34798075bf05ac39f708bb33b90">r8742:1e57d2549bd6</a>
405 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/1e57d2549bd6c34798075bf05ac39f708bb33b90">r8742:1e57d2549bd6</a>
406 </code>
406 </code>
407 </td>
407 </td>
408 <td class="expand_commit" data-commit-id="1e57d2549bd6c34798075bf05ac39f708bb33b90" title="Expand commit message">
408 <td class="expand_commit" data-commit-id="1e57d2549bd6c34798075bf05ac39f708bb33b90" title="Expand commit message">
409 <div class="show_more_col">
409 <div class="show_more_col">
410 <i class="show_more" ></i>
410 <i class="show_more" ></i>
411 </div>
411 </div>
412 </td>
412 </td>
413 <td class="mid td-description">
413 <td class="mid td-description">
414 <div class="log-container truncate-wrap">
414 <div class="log-container truncate-wrap">
415 <div id="c-1e57d2549bd6c34798075bf05ac39f708bb33b90" class="message truncate">Diff: fix renaming files with spaces <a class="issue-tracker-link" href="http://bugs.rhodecode.com/issues/574">#574</a></div>
415 <div id="c-1e57d2549bd6c34798075bf05ac39f708bb33b90" class="message truncate">Diff: fix renaming files with spaces <a class="issue-tracker-link" href="http://bugs.rhodecode.com/issues/574">#574</a></div>
416
416
417 </div>
417 </div>
418 </td>
418 </td>
419 </tr>
419 </tr>
420 </table>
420 </table>
421 </div>
421 </div>
422
422
423 <script>
423 <script>
424 $('.expand_commit').on('click',function(e){
424 $('.expand_commit').on('click',function(e){
425 $(this).children('i').hide();
425 $(this).children('i').hide();
426 var cid = $(this).data('commitId');
426 var cid = $(this).data('commitId');
427 $('#c-'+cid).css({'height': 'auto', 'margin': '.65em 1em .65em 0','white-space': 'pre-line', 'text-overflow': 'initial', 'overflow':'visible'})
427 $('#c-'+cid).css({'height': 'auto', 'margin': '.65em 1em .65em 0','white-space': 'pre-line', 'text-overflow': 'initial', 'overflow':'visible'})
428 $('#t-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible', 'white-space':'normal'})
428 $('#t-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible', 'white-space':'normal'})
429 });
429 });
430 $('.compare_select').on('click',function(e){
430 $('.compare_select').on('click',function(e){
431 var cid = $(this).attr('commit_id');
431 var cid = $(this).attr('commit_id');
432 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
432 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
433 });
433 });
434 </script>
434 </script>
435 <div class="cs_files_title">
435 <div class="cs_files_title">
436 <span class="cs_files_expand">
436 <span class="cs_files_expand">
437 <span id="expand_all_files">Expand All</span> | <span id="collapse_all_files">Collapse All</span>
437 <span id="expand_all_files">Expand All</span> | <span id="collapse_all_files">Collapse All</span>
438 </span>
438 </span>
439 <h2>
439 <h2>
440 7 files changed: 55 inserted, 9 deleted
440 7 files changed: 55 inserted, 9 deleted
441 </h2>
441 </h2>
442 </div>
442 </div>
443 <div class="cs_files">
443 <div class="cs_files">
444 <table class="compare_view_files">
444 <table class="compare_view_files">
445
445
446 <tr class="cs_A expand_file" fid="c--efbe5b7a3f13">
446 <tr class="cs_A expand_file" fid="c--efbe5b7a3f13">
447 <td class="cs_icon_td">
447 <td class="cs_icon_td">
448 <span class="expand_file_icon" fid="c--efbe5b7a3f13"></span>
448 <span class="expand_file_icon" fid="c--efbe5b7a3f13"></span>
449 </td>
449 </td>
450 <td class="cs_icon_td">
450 <td class="cs_icon_td">
451 <div class="flag_status not_reviewed hidden"></div>
451 <div class="flag_status not_reviewed hidden"></div>
452 </td>
452 </td>
453 <td id="a_c--efbe5b7a3f13">
453 <td id="a_c--efbe5b7a3f13">
454 <a class="compare_view_filepath" href="#a_c--efbe5b7a3f13">
454 <a class="compare_view_filepath" href="#a_c--efbe5b7a3f13">
455 rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff
455 rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff
456 </a>
456 </a>
457 <span id="diff_c--efbe5b7a3f13" class="diff_links" style="display: none;">
457 <span id="diff_c--efbe5b7a3f13" class="diff_links" style="display: none;">
458 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
458 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
459 Unified Diff
459 Unified Diff
460 </a>
460 </a>
461 |
461 |
462 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
462 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
463 Side-by-side Diff
463 Side-by-side Diff
464 </a>
464 </a>
465 </span>
465 </span>
466 </td>
466 </td>
467 <td>
467 <td>
468 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">4</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
468 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">4</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
469 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff">
469 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff">
470 <i class="icon-comment"></i>
470 <i class="icon-comment"></i>
471 </div>
471 </div>
472 </td>
472 </td>
473 </tr>
473 </tr>
474 <tr id="tr_c--efbe5b7a3f13">
474 <tr id="tr_c--efbe5b7a3f13">
475 <td></td>
475 <td></td>
476 <td></td>
476 <td></td>
477 <td class="injected_diff" colspan="2">
477 <td class="injected_diff" colspan="2">
478
478
479 <div class="diff-container" id="diff-container-140716195039928">
479 <div class="diff-container" id="diff-container-140716195039928">
480 <div id="c--efbe5b7a3f13_target" ></div>
480 <div id="c--efbe5b7a3f13_target" ></div>
481 <div id="c--efbe5b7a3f13" class="diffblock margined comm" >
481 <div id="c--efbe5b7a3f13" class="diffblock margined comm" >
482 <div class="code-body">
482 <div class="code-body">
483 <div class="full_f_path" path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff" style="display: none;"></div>
483 <div class="full_f_path" path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff" style="display: none;"></div>
484 <table class="code-difftable">
484 <table class="code-difftable">
485 <tr class="line context">
485 <tr class="line context">
486 <td class="add-comment-line"><span class="add-comment-content"></span></td>
486 <td class="add-comment-line"><span class="add-comment-content"></span></td>
487 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
487 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
488 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n"></a></td>
488 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n"></a></td>
489 <td class="code no-comment">
489 <td class="code no-comment">
490 <pre>new file 100644</pre>
490 <pre>new file 100644</pre>
491 </td>
491 </td>
492 </tr>
492 </tr>
493 <tr class="line add">
493 <tr class="line add">
494 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
494 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
495 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
495 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
496 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1">1</a></td>
496 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1">1</a></td>
497 <td class="code">
497 <td class="code">
498 <pre>diff --git a/file_with_ spaces.txt b/file_with_ two spaces.txt
498 <pre>diff --git a/file_with_ spaces.txt b/file_with_ two spaces.txt
499 </pre>
499 </pre>
500 </td>
500 </td>
501 </tr>
501 </tr>
502 <tr class="line add">
502 <tr class="line add">
503 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
503 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
504 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
504 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
505 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2">2</a></td>
505 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2">2</a></td>
506 <td class="code">
506 <td class="code">
507 <pre>similarity index 100%
507 <pre>similarity index 100%
508 </pre>
508 </pre>
509 </td>
509 </td>
510 </tr>
510 </tr>
511 <tr class="line add">
511 <tr class="line add">
512 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
512 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
513 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
513 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
514 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3">3</a></td>
514 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3">3</a></td>
515 <td class="code">
515 <td class="code">
516 <pre>rename from file_with_ spaces.txt
516 <pre>rename from file_with_ spaces.txt
517 </pre>
517 </pre>
518 </td>
518 </td>
519 </tr>
519 </tr>
520 <tr class="line add">
520 <tr class="line add">
521 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
521 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
522 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
522 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
523 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4">4</a></td>
523 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4">4</a></td>
524 <td class="code">
524 <td class="code">
525 <pre>rename to file_with_ two spaces.txt
525 <pre>rename to file_with_ two spaces.txt
526 </pre>
526 </pre>
527 </td>
527 </td>
528 </tr>
528 </tr>
529 <tr class="line context">
529 <tr class="line context">
530 <td class="add-comment-line"><span class="add-comment-content"></span></td>
530 <td class="add-comment-line"><span class="add-comment-content"></span></td>
531 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o...">...</a></td>
531 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o...">...</a></td>
532 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n...">...</a></td>
532 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n...">...</a></td>
533 <td class="code no-comment">
533 <td class="code no-comment">
534 <pre> No newline at end of file</pre>
534 <pre> No newline at end of file</pre>
535 </td>
535 </td>
536 </tr>
536 </tr>
537 </table>
537 </table>
538 </div>
538 </div>
539 </div>
539 </div>
540 </div>
540 </div>
541
541
542 </td>
542 </td>
543 </tr>
543 </tr>
544 <tr class="cs_A expand_file" fid="c--c21377f778f9">
544 <tr class="cs_A expand_file" fid="c--c21377f778f9">
545 <td class="cs_icon_td">
545 <td class="cs_icon_td">
546 <span class="expand_file_icon" fid="c--c21377f778f9"></span>
546 <span class="expand_file_icon" fid="c--c21377f778f9"></span>
547 </td>
547 </td>
548 <td class="cs_icon_td">
548 <td class="cs_icon_td">
549 <div class="flag_status not_reviewed hidden"></div>
549 <div class="flag_status not_reviewed hidden"></div>
550 </td>
550 </td>
551 <td id="a_c--c21377f778f9">
551 <td id="a_c--c21377f778f9">
552 <a class="compare_view_filepath" href="#a_c--c21377f778f9">
552 <a class="compare_view_filepath" href="#a_c--c21377f778f9">
553 rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff
553 rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff
554 </a>
554 </a>
555 <span id="diff_c--c21377f778f9" class="diff_links" style="display: none;">
555 <span id="diff_c--c21377f778f9" class="diff_links" style="display: none;">
556 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
556 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
557 Unified Diff
557 Unified Diff
558 </a>
558 </a>
559 |
559 |
560 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
560 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
561 Side-by-side Diff
561 Side-by-side Diff
562 </a>
562 </a>
563 </span>
563 </span>
564 </td>
564 </td>
565 <td>
565 <td>
566 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
566 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
567 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff">
567 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff">
568 <i class="icon-comment"></i>
568 <i class="icon-comment"></i>
569 </div>
569 </div>
570 </td>
570 </td>
571 </tr>
571 </tr>
572 <tr id="tr_c--c21377f778f9">
572 <tr id="tr_c--c21377f778f9">
573 <td></td>
573 <td></td>
574 <td></td>
574 <td></td>
575 <td class="injected_diff" colspan="2">
575 <td class="injected_diff" colspan="2">
576
576
577 <div class="diff-container" id="diff-container-140716195038344">
577 <div class="diff-container" id="diff-container-140716195038344">
578 <div id="c--c21377f778f9_target" ></div>
578 <div id="c--c21377f778f9_target" ></div>
579 <div id="c--c21377f778f9" class="diffblock margined comm" >
579 <div id="c--c21377f778f9" class="diffblock margined comm" >
580 <div class="code-body">
580 <div class="code-body">
581 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff" style="display: none;"></div>
581 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff" style="display: none;"></div>
582 <table class="code-difftable">
582 <table class="code-difftable">
583 <tr class="line context">
583 <tr class="line context">
584 <td class="add-comment-line"><span class="add-comment-content"></span></td>
584 <td class="add-comment-line"><span class="add-comment-content"></span></td>
585 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
585 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
586 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n"></a></td>
586 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n"></a></td>
587 <td class="code no-comment">
587 <td class="code no-comment">
588 <pre>new file 100644</pre>
588 <pre>new file 100644</pre>
589 </td>
589 </td>
590 </tr>
590 </tr>
591 <tr class="line add">
591 <tr class="line add">
592 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
592 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
593 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
593 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
594 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1">1</a></td>
594 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1">1</a></td>
595 <td class="code">
595 <td class="code">
596 <pre>diff --git a/file_changed_without_spaces.txt b/file_copied_ with spaces.txt
596 <pre>diff --git a/file_changed_without_spaces.txt b/file_copied_ with spaces.txt
597 </pre>
597 </pre>
598 </td>
598 </td>
599 </tr>
599 </tr>
600 <tr class="line add">
600 <tr class="line add">
601 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
601 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
602 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
602 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
603 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2">2</a></td>
603 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2">2</a></td>
604 <td class="code">
604 <td class="code">
605 <pre>copy from file_changed_without_spaces.txt
605 <pre>copy from file_changed_without_spaces.txt
606 </pre>
606 </pre>
607 </td>
607 </td>
608 </tr>
608 </tr>
609 <tr class="line add">
609 <tr class="line add">
610 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
610 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
611 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
611 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
612 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3">3</a></td>
612 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3">3</a></td>
613 <td class="code">
613 <td class="code">
614 <pre>copy to file_copied_ with spaces.txt
614 <pre>copy to file_copied_ with spaces.txt
615 </pre>
615 </pre>
616 </td>
616 </td>
617 </tr>
617 </tr>
618 <tr class="line context">
618 <tr class="line context">
619 <td class="add-comment-line"><span class="add-comment-content"></span></td>
619 <td class="add-comment-line"><span class="add-comment-content"></span></td>
620 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o...">...</a></td>
620 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o...">...</a></td>
621 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n...">...</a></td>
621 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n...">...</a></td>
622 <td class="code no-comment">
622 <td class="code no-comment">
623 <pre> No newline at end of file</pre>
623 <pre> No newline at end of file</pre>
624 </td>
624 </td>
625 </tr>
625 </tr>
626 </table>
626 </table>
627 </div>
627 </div>
628 </div>
628 </div>
629 </div>
629 </div>
630
630
631 </td>
631 </td>
632 </tr>
632 </tr>
633 <tr class="cs_A expand_file" fid="c--ee62085ad7a8">
633 <tr class="cs_A expand_file" fid="c--ee62085ad7a8">
634 <td class="cs_icon_td">
634 <td class="cs_icon_td">
635 <span class="expand_file_icon" fid="c--ee62085ad7a8"></span>
635 <span class="expand_file_icon" fid="c--ee62085ad7a8"></span>
636 </td>
636 </td>
637 <td class="cs_icon_td">
637 <td class="cs_icon_td">
638 <div class="flag_status not_reviewed hidden"></div>
638 <div class="flag_status not_reviewed hidden"></div>
639 </td>
639 </td>
640 <td id="a_c--ee62085ad7a8">
640 <td id="a_c--ee62085ad7a8">
641 <a class="compare_view_filepath" href="#a_c--ee62085ad7a8">
641 <a class="compare_view_filepath" href="#a_c--ee62085ad7a8">
642 rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff
642 rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff
643 </a>
643 </a>
644 <span id="diff_c--ee62085ad7a8" class="diff_links" style="display: none;">
644 <span id="diff_c--ee62085ad7a8" class="diff_links" style="display: none;">
645 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
645 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
646 Unified Diff
646 Unified Diff
647 </a>
647 </a>
648 |
648 |
649 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
649 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
650 Side-by-side Diff
650 Side-by-side Diff
651 </a>
651 </a>
652 </span>
652 </span>
653 </td>
653 </td>
654 <td>
654 <td>
655 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
655 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
656 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff">
656 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff">
657 <i class="icon-comment"></i>
657 <i class="icon-comment"></i>
658 </div>
658 </div>
659 </td>
659 </td>
660 </tr>
660 </tr>
661 <tr id="tr_c--ee62085ad7a8">
661 <tr id="tr_c--ee62085ad7a8">
662 <td></td>
662 <td></td>
663 <td></td>
663 <td></td>
664 <td class="injected_diff" colspan="2">
664 <td class="injected_diff" colspan="2">
665
665
666 <div class="diff-container" id="diff-container-140716195039496">
666 <div class="diff-container" id="diff-container-140716195039496">
667 <div id="c--ee62085ad7a8_target" ></div>
667 <div id="c--ee62085ad7a8_target" ></div>
668 <div id="c--ee62085ad7a8" class="diffblock margined comm" >
668 <div id="c--ee62085ad7a8" class="diffblock margined comm" >
669 <div class="code-body">
669 <div class="code-body">
670 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff" style="display: none;"></div>
670 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff" style="display: none;"></div>
671 <table class="code-difftable">
671 <table class="code-difftable">
672 <tr class="line context">
672 <tr class="line context">
673 <td class="add-comment-line"><span class="add-comment-content"></span></td>
673 <td class="add-comment-line"><span class="add-comment-content"></span></td>
674 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
674 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
675 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n"></a></td>
675 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n"></a></td>
676 <td class="code no-comment">
676 <td class="code no-comment">
677 <pre>new file 100644</pre>
677 <pre>new file 100644</pre>
678 </td>
678 </td>
679 </tr>
679 </tr>
680 <tr class="line add">
680 <tr class="line add">
681 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
681 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
682 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
682 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
683 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1">1</a></td>
683 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1">1</a></td>
684 <td class="code">
684 <td class="code">
685 <pre>diff --git a/file_ with update.txt b/file_changed _.txt
685 <pre>diff --git a/file_ with update.txt b/file_changed _.txt
686 </pre>
686 </pre>
687 </td>
687 </td>
688 </tr>
688 </tr>
689 <tr class="line add">
689 <tr class="line add">
690 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
690 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
691 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
691 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
692 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2">2</a></td>
692 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2">2</a></td>
693 <td class="code">
693 <td class="code">
694 <pre>rename from file_ with update.txt
694 <pre>rename from file_ with update.txt
695 </pre>
695 </pre>
696 </td>
696 </td>
697 </tr>
697 </tr>
698 <tr class="line add">
698 <tr class="line add">
699 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
699 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
700 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
700 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
701 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3">3</a></td>
701 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3">3</a></td>
702 <td class="code">
702 <td class="code">
703 <pre>rename to file_changed _.txt</pre>
703 <pre>rename to file_changed _.txt</pre>
704 </td>
704 </td>
705 </tr>
705 </tr>
706 </table>
706 </table>
707 </div>
707 </div>
708 </div>
708 </div>
709 </div>
709 </div>
710
710
711 </td>
711 </td>
712 </tr>
712 </tr>
713
713
714 </table>
714 </table>
715 </div>
715 </div>
716 </div>
716 </div>
717 </div>
717 </div>
718
718
719 </td>
719 </td>
720 </tr>
720 </tr>
721 </table>
721 </table>
722 </div>
722 </div>
723 </div>
723 </div>
724 </div>
724 </div>
725
725
726
726
727
727
728
728
729 <div id="comment-inline-form-template" style="display: none;">
729 <div id="comment-inline-form-template" style="display: none;">
730 <div class="comment-inline-form ac">
730 <div class="comment-inline-form ac">
731 <div class="overlay"><div class="overlay-text">Submitting...</div></div>
731 <div class="overlay"><div class="overlay-text">Submitting...</div></div>
732 <form action="#" class="inline-form" method="get">
732 <form action="#" class="inline-form" method="get">
733 <div id="edit-container_{1}" class="clearfix">
733 <div id="edit-container_{1}" class="clearfix">
734 <div class="comment-title pull-left">
734 <div class="comment-title pull-left">
735 Commenting on line {1}.
735 Commenting on line {1}.
736 </div>
736 </div>
737 <div class="comment-help pull-right">
737 <div class="comment-help pull-right">
738 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
738 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
739 </div>
739 </div>
740 <div style="clear: both"></div>
740 <div style="clear: both"></div>
741 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
741 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
742 </div>
742 </div>
743 <div id="preview-container_{1}" class="clearfix" style="display: none;">
743 <div id="preview-container_{1}" class="clearfix" style="display: none;">
744 <div class="comment-help">
744 <div class="comment-help">
745 Comment preview
745 Comment preview
746 </div>
746 </div>
747 <div id="preview-box_{1}" class="preview-box"></div>
747 <div id="preview-box_{1}" class="preview-box"></div>
748 </div>
748 </div>
749 <div class="comment-button pull-right">
749 <div class="comment-button pull-right">
750 <input type="hidden" name="f_path" value="{0}">
750 <input type="hidden" name="f_path" value="{0}">
751 <input type="hidden" name="line" value="{1}">
751 <input type="hidden" name="line" value="{1}">
752 <div id="preview-btn_{1}" class="btn btn-default">Preview</div>
752 <div id="preview-btn_{1}" class="btn btn-default">Preview</div>
753 <div id="edit-btn_{1}" class="btn" style="display: none;">Edit</div>
753 <div id="edit-btn_{1}" class="btn" style="display: none;">Edit</div>
754 <input class="btn btn-success save-inline-form" id="save" name="save" type="submit" value="Comment" />
754 <input class="btn btn-success save-inline-form" id="save" name="save" type="submit" value="Comment" />
755 </div>
755 </div>
756 <div class="comment-button hide-inline-form-button">
756 <div class="comment-button hide-inline-form-button">
757 <input class="btn hide-inline-form" id="hide-inline-form" name="hide-inline-form" type="reset" value="Cancel" />
757 <input class="btn hide-inline-form" id="hide-inline-form" name="hide-inline-form" type="reset" value="Cancel" />
758 </div>
758 </div>
759 </form>
759 </form>
760 </div>
760 </div>
761 </div>
761 </div>
762
762
763
763
764
764
765 <div class="comments">
765 <div class="comments">
766 <div id="inline-comments-container">
766 <div id="inline-comments-container">
767
767
768 <h2>0 Pull Request Comments</h2>
768 <h2>0 Pull Request Comments</h2>
769
769
770
770
771 </div>
771 </div>
772
772
773 </div>
773 </div>
774
774
775
775
776
776
777
777
778 <div class="pull-request-merge">
778 <div class="pull-request-merge">
779 </div>
779 </div>
780 <div class="comments">
780 <div class="comments">
781 <div class="comment-form ac">
781 <div class="comment-form ac">
782 <form action="/rhodecode-momentum/pull-request-comment/720" id="comments_form" method="POST">
782 <form action="/rhodecode-momentum/pull-request-comment/720" id="comments_form" method="POST">
783 <div style="display: none;"><input id="csrf_token" name="csrf_token" type="hidden" value="6dbc0b19ac65237df65d57202a3e1f2df4153e38" /></div>
783 <div style="display: none;"><input id="csrf_token" name="csrf_token" type="hidden" value="6dbc0b19ac65237df65d57202a3e1f2df4153e38" /></div>
784 <div id="edit-container" class="clearfix">
784 <div id="edit-container" class="clearfix">
785 <div class="comment-title pull-left">
785 <div class="comment-title pull-left">
786 Create a comment on this Pull Request.
786 Create a comment on this Pull Request.
787 </div>
787 </div>
788 <div class="comment-help pull-right">
788 <div class="comment-help pull-right">
789 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
789 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
790 </div>
790 </div>
791 <div style="clear: both"></div>
791 <div style="clear: both"></div>
792 <textarea class="comment-block-ta" id="text" name="text"></textarea>
792 <textarea class="comment-block-ta" id="text" name="text"></textarea>
793 </div>
793 </div>
794
794
795 <div id="preview-container" class="clearfix" style="display: none;">
795 <div id="preview-container" class="clearfix" style="display: none;">
796 <div class="comment-title">
796 <div class="comment-title">
797 Comment preview
797 Comment preview
798 </div>
798 </div>
799 <div id="preview-box" class="preview-box"></div>
799 <div id="preview-box" class="preview-box"></div>
800 </div>
800 </div>
801
801
802 <div id="comment_form_extras">
802 <div id="comment_form_extras">
803 </div>
803 </div>
804 <div class="action-button pull-right">
804 <div class="action-button pull-right">
805 <div id="preview-btn" class="btn">
805 <div id="preview-btn" class="btn">
806 Preview
806 Preview
807 </div>
807 </div>
808 <div id="edit-btn" class="btn" style="display: none;">
808 <div id="edit-btn" class="btn" style="display: none;">
809 Edit
809 Edit
810 </div>
810 </div>
811 <div class="comment-button">
811 <div class="comment-button">
812 <input class="btn btn-small btn-success comment-button-input" id="save" name="save" type="submit" value="Comment" />
812 <input class="btn btn-small btn-success comment-button-input" id="save" name="save" type="submit" value="Comment" />
813 </div>
813 </div>
814 </div>
814 </div>
815 </form>
815 </form>
816 </div>
816 </div>
817 </div>
817 </div>
818 <script>
818 <script>
819
819
820 $(document).ready(function() {
820 $(document).ready(function() {
821
821
822 var cm = initCommentBoxCodeMirror('#text');
822 var cm = initCommentBoxCodeMirror('#text');
823
823
824 // main form preview
824 // main form preview
825 $('#preview-btn').on('click', function(e) {
825 $('#preview-btn').on('click', function(e) {
826 $('#preview-btn').hide();
826 $('#preview-btn').hide();
827 $('#edit-btn').show();
827 $('#edit-btn').show();
828 var _text = cm.getValue();
828 var _text = cm.getValue();
829 if (!_text) {
829 if (!_text) {
830 return;
830 return;
831 }
831 }
832 var post_data = {
832 var post_data = {
833 'text': _text,
833 'text': _text,
834 'renderer': DEFAULT_RENDERER,
834 'renderer': DEFAULT_RENDERER,
835 'csrf_token': CSRF_TOKEN
835 'csrf_token': CSRF_TOKEN
836 };
836 };
837 var previewbox = $('#preview-box');
837 var previewbox = $('#preview-box');
838 previewbox.addClass('unloaded');
838 previewbox.addClass('unloaded');
839 previewbox.html(_gettext('Loading ...'));
839 previewbox.html(_gettext('Loading ...'));
840 $('#edit-container').hide();
840 $('#edit-container').hide();
841 $('#preview-container').show();
841 $('#preview-container').show();
842
842
843 var url = pyroutes.url('changeset_comment_preview', {'repo_name': 'rhodecode-momentum'});
843 var url = pyroutes.url('changeset_comment_preview', {'repo_name': 'rhodecode-momentum'});
844
844
845 ajaxPOST(url, post_data, function(o) {
845 ajaxPOST(url, post_data, function(o) {
846 previewbox.html(o);
846 previewbox.html(o);
847 previewbox.removeClass('unloaded');
847 previewbox.removeClass('unloaded');
848 });
848 });
849 });
849 });
850 $('#edit-btn').on('click', function(e) {
850 $('#edit-btn').on('click', function(e) {
851 $('#preview-btn').show();
851 $('#preview-btn').show();
852 $('#edit-btn').hide();
852 $('#edit-btn').hide();
853 $('#edit-container').show();
853 $('#edit-container').show();
854 $('#preview-container').hide();
854 $('#preview-container').hide();
855 });
855 });
856
856
857 var formatChangeStatus = function(state, escapeMarkup) {
857 var formatChangeStatus = function(state, escapeMarkup) {
858 var originalOption = state.element;
858 var originalOption = state.element;
859 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
859 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
860 '<span>' + escapeMarkup(state.text) + '</span>';
860 '<span>' + escapeMarkup(state.text) + '</span>';
861 };
861 };
862
862
863 var formatResult = function(result, container, query, escapeMarkup) {
863 var formatResult = function(result, container, query, escapeMarkup) {
864 return formatChangeStatus(result, escapeMarkup);
864 return formatChangeStatus(result, escapeMarkup);
865 };
865 };
866
866
867 var formatSelection = function(data, container, escapeMarkup) {
867 var formatSelection = function(data, container, escapeMarkup) {
868 return formatChangeStatus(data, escapeMarkup);
868 return formatChangeStatus(data, escapeMarkup);
869 };
869 };
870
870
871 $('#change_status').select2({
871 $('#change_status').select2({
872 placeholder: "Status Review",
872 placeholder: "Status Review",
873 formatResult: formatResult,
873 formatResult: formatResult,
874 formatSelection: formatSelection,
874 formatSelection: formatSelection,
875 containerCssClass: "drop-menu status_box_menu",
875 containerCssClass: "drop-menu status_box_menu",
876 dropdownCssClass: "drop-menu-dropdown",
876 dropdownCssClass: "drop-menu-dropdown",
877 dropdownAutoWidth: true,
877 dropdownAutoWidth: true,
878 minimumResultsForSearch: -1
878 minimumResultsForSearch: -1
879 });
879 });
880 });
880 });
881 </script>
881 </script>
882
882
883
883
884 <script type="text/javascript">
884 <script type="text/javascript">
885 // TODO: switch this to pyroutes
885 // TODO: switch this to pyroutes
886 AJAX_COMMENT_DELETE_URL = "/rhodecode-momentum/pull-request-comment/__COMMENT_ID__/delete";
886 AJAX_COMMENT_DELETE_URL = "/rhodecode-momentum/pull-request-comment/__COMMENT_ID__/delete";
887
887
888 $(function(){
888 $(function(){
889 ReviewerAutoComplete('user');
889 ReviewerAutoComplete('user');
890
890
891 $('#open_edit_reviewers').on('click', function(e){
891 $('#open_edit_reviewers').on('click', function(e){
892 $('#open_edit_reviewers').hide();
892 $('#open_edit_reviewers').hide();
893 $('#close_edit_reviewers').show();
893 $('#close_edit_reviewers').show();
894 $('#add_reviewer_input').show();
894 $('#add_reviewer_input').show();
895 $('.reviewer_member_remove').css('visibility', 'visible');
895 $('.reviewer_member_remove').css('visibility', 'visible');
896 });
896 });
897
897
898 $('#close_edit_reviewers').on('click', function(e){
898 $('#close_edit_reviewers').on('click', function(e){
899 $('#open_edit_reviewers').show();
899 $('#open_edit_reviewers').show();
900 $('#close_edit_reviewers').hide();
900 $('#close_edit_reviewers').hide();
901 $('#add_reviewer_input').hide();
901 $('#add_reviewer_input').hide();
902 $('.reviewer_member_remove').css('visibility', 'hidden');
902 $('.reviewer_member_remove').css('visibility', 'hidden');
903 });
903 });
904
904
905 $('.show-inline-comments').on('change', function(e){
905 $('.show-inline-comments').on('change', function(e){
906 var show = 'none';
906 var show = 'none';
907 var target = e.currentTarget;
907 var target = e.currentTarget;
908 if(target.checked){
908 if(target.checked){
909 show = ''
909 show = ''
910 }
910 }
911 var boxid = $(target).attr('id_for');
911 var boxid = $(target).attr('id_for');
912 var comments = $('#{0} .inline-comments'.format(boxid));
912 var comments = $('#{0} .inline-comments'.format(boxid));
913 var fn_display = function(idx){
913 var fn_display = function(idx){
914 $(this).css('display', show);
914 $(this).css('display', show);
915 };
915 };
916 $(comments).each(fn_display);
916 $(comments).each(fn_display);
917 var btns = $('#{0} .inline-comments-button'.format(boxid));
917 var btns = $('#{0} .inline-comments-button'.format(boxid));
918 $(btns).each(fn_display);
918 $(btns).each(fn_display);
919 });
919 });
920
920
921 // inject comments into they proper positions
922 var file_comments = $('.inline-comment-placeholder');
923 renderInlineComments(file_comments);
924 var commentTotals = {};
921 var commentTotals = {};
925 $.each(file_comments, function(i, comment) {
922 $.each(file_comments, function(i, comment) {
926 var path = $(comment).attr('path');
923 var path = $(comment).attr('path');
927 var comms = $(comment).children().length;
924 var comms = $(comment).children().length;
928 if (path in commentTotals) {
925 if (path in commentTotals) {
929 commentTotals[path] += comms;
926 commentTotals[path] += comms;
930 } else {
927 } else {
931 commentTotals[path] = comms;
928 commentTotals[path] = comms;
932 }
929 }
933 });
930 });
934 $.each(commentTotals, function(path, total) {
931 $.each(commentTotals, function(path, total) {
935 var elem = $('.comment-bubble[data-path="'+ path +'"]')
932 var elem = $('.comment-bubble[data-path="'+ path +'"]')
936 elem.css('visibility', 'visible');
933 elem.css('visibility', 'visible');
937 elem.html(elem.html() + ' ' + total );
934 elem.html(elem.html() + ' ' + total );
938 });
935 });
939
936
940 $('#merge_pull_request_form').submit(function() {
937 $('#merge_pull_request_form').submit(function() {
941 if (!$('#merge_pull_request').attr('disabled')) {
938 if (!$('#merge_pull_request').attr('disabled')) {
942 $('#merge_pull_request').attr('disabled', 'disabled');
939 $('#merge_pull_request').attr('disabled', 'disabled');
943 }
940 }
944 return true;
941 return true;
945 });
942 });
946
943
947 $('#update_pull_request').on('click', function(e){
944 $('#update_pull_request').on('click', function(e){
948 updateReviewers(undefined, "rhodecode-momentum", "720");
945 updateReviewers(undefined, "rhodecode-momentum", "720");
949 });
946 });
950
947
951 $('#update_commits').on('click', function(e){
948 $('#update_commits').on('click', function(e){
952 updateCommits("rhodecode-momentum", "720");
949 updateCommits("rhodecode-momentum", "720");
953 });
950 });
954
951
955 $('#close_pull_request').on('click', function(e){
952 $('#close_pull_request').on('click', function(e){
956 closePullRequest("rhodecode-momentum", "720");
953 closePullRequest("rhodecode-momentum", "720");
957 });
954 });
958 })
955 })
959 </script>
956 </script>
960
957
961 </div>
958 </div>
962 </div></div>
959 </div></div>
963
960
964 </div>
961 </div>
965
962
966
963
967 </%def>
964 </%def>
@@ -1,511 +1,527 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()">
10 <%def name="breadcrumbs_links()">
11 <span id="pr-title">
11 <span id="pr-title">
12 ${c.pull_request.title}
12 ${c.pull_request.title}
13 %if c.pull_request.is_closed():
13 %if c.pull_request.is_closed():
14 (${_('Closed')})
14 (${_('Closed')})
15 %endif
15 %endif
16 </span>
16 </span>
17 <div id="pr-title-edit" class="input" style="display: none;">
17 <div id="pr-title-edit" class="input" style="display: none;">
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 </div>
19 </div>
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_nav()">
22 <%def name="menu_bar_nav()">
23 ${self.menu_items(active='repositories')}
23 ${self.menu_items(active='repositories')}
24 </%def>
24 </%def>
25
25
26 <%def name="menu_bar_subnav()">
26 <%def name="menu_bar_subnav()">
27 ${self.repo_menu(active='showpullrequest')}
27 ${self.repo_menu(active='showpullrequest')}
28 </%def>
28 </%def>
29
29
30 <%def name="main()">
30 <%def name="main()">
31 <script type="text/javascript">
31 <script type="text/javascript">
32 // TODO: marcink switch this to pyroutes
32 // TODO: marcink switch this to pyroutes
33 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
33 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
34 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
34 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 </script>
35 </script>
36 <div class="box">
36 <div class="box">
37 <div class="title">
37 <div class="title">
38 ${self.repo_page_title(c.rhodecode_db_repo)}
38 ${self.repo_page_title(c.rhodecode_db_repo)}
39 </div>
39 </div>
40
40
41 ${self.breadcrumbs()}
41 ${self.breadcrumbs()}
42
42
43
43
44 <div class="box pr-summary">
44 <div class="box pr-summary">
45 <div class="summary-details block-left">
45 <div class="summary-details block-left">
46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
47 <div class="pr-details-title">
47 <div class="pr-details-title">
48 ${_('Pull request #%s') % c.pull_request.pull_request_id} ${_('From')} ${h.format_date(c.pull_request.created_on)}
48 ${_('Pull request #%s') % c.pull_request.pull_request_id} ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 %if c.allowed_to_update:
49 %if c.allowed_to_update:
50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 % if c.allowed_to_delete:
51 % if c.allowed_to_delete:
52 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
52 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
54 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
54 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
55 ${h.end_form()}
55 ${h.end_form()}
56 % else:
56 % else:
57 ${_('Delete')}
57 ${_('Delete')}
58 % endif
58 % endif
59 </div>
59 </div>
60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel edit')}</div>
61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel edit')}</div>
62 %endif
62 %endif
63 </div>
63 </div>
64
64
65 <div id="summary" class="fields pr-details-content">
65 <div id="summary" class="fields pr-details-content">
66 <div class="field">
66 <div class="field">
67 <div class="label-summary">
67 <div class="label-summary">
68 <label>${_('Origin')}:</label>
68 <label>${_('Origin')}:</label>
69 </div>
69 </div>
70 <div class="input">
70 <div class="input">
71 <div class="pr-origininfo">
71 <div class="pr-origininfo">
72 ## branch link is only valid if it is a branch
72 ## branch link is only valid if it is a branch
73 <span class="tag">
73 <span class="tag">
74 %if c.pull_request.source_ref_parts.type == 'branch':
74 %if c.pull_request.source_ref_parts.type == 'branch':
75 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
75 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
76 %else:
76 %else:
77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
78 %endif
78 %endif
79 </span>
79 </span>
80 <span class="clone-url">
80 <span class="clone-url">
81 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
81 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
82 </span>
82 </span>
83 </div>
83 </div>
84 <div class="pr-pullinfo">
84 <div class="pr-pullinfo">
85 %if h.is_hg(c.pull_request.source_repo):
85 %if h.is_hg(c.pull_request.source_repo):
86 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
86 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
87 %elif h.is_git(c.pull_request.source_repo):
87 %elif h.is_git(c.pull_request.source_repo):
88 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
88 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
89 %endif
89 %endif
90 </div>
90 </div>
91 </div>
91 </div>
92 </div>
92 </div>
93 <div class="field">
93 <div class="field">
94 <div class="label-summary">
94 <div class="label-summary">
95 <label>${_('Target')}:</label>
95 <label>${_('Target')}:</label>
96 </div>
96 </div>
97 <div class="input">
97 <div class="input">
98 <div class="pr-targetinfo">
98 <div class="pr-targetinfo">
99 ## branch link is only valid if it is a branch
99 ## branch link is only valid if it is a branch
100 <span class="tag">
100 <span class="tag">
101 %if c.pull_request.target_ref_parts.type == 'branch':
101 %if c.pull_request.target_ref_parts.type == 'branch':
102 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
102 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
103 %else:
103 %else:
104 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
104 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
105 %endif
105 %endif
106 </span>
106 </span>
107 <span class="clone-url">
107 <span class="clone-url">
108 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
108 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
109 </span>
109 </span>
110 </div>
110 </div>
111 </div>
111 </div>
112 </div>
112 </div>
113
113
114 ## Link to the shadow repository.
114 ## Link to the shadow repository.
115 %if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
115 %if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
116 <div class="field">
116 <div class="field">
117 <div class="label-summary">
117 <div class="label-summary">
118 <label>Merge:</label>
118 <label>Merge:</label>
119 </div>
119 </div>
120 <div class="input">
120 <div class="input">
121 <div class="pr-mergeinfo">
121 <div class="pr-mergeinfo">
122 %if h.is_hg(c.pull_request.target_repo):
122 %if h.is_hg(c.pull_request.target_repo):
123 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
123 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
124 %elif h.is_git(c.pull_request.target_repo):
124 %elif h.is_git(c.pull_request.target_repo):
125 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
125 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
126 %endif
126 %endif
127 </div>
127 </div>
128 </div>
128 </div>
129 </div>
129 </div>
130 %endif
130 %endif
131
131
132 <div class="field">
132 <div class="field">
133 <div class="label-summary">
133 <div class="label-summary">
134 <label>${_('Review')}:</label>
134 <label>${_('Review')}:</label>
135 </div>
135 </div>
136 <div class="input">
136 <div class="input">
137 %if c.pull_request_review_status:
137 %if c.pull_request_review_status:
138 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
138 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
139 <span class="changeset-status-lbl tooltip">
139 <span class="changeset-status-lbl tooltip">
140 %if c.pull_request.is_closed():
140 %if c.pull_request.is_closed():
141 ${_('Closed')},
141 ${_('Closed')},
142 %endif
142 %endif
143 ${h.commit_status_lbl(c.pull_request_review_status)}
143 ${h.commit_status_lbl(c.pull_request_review_status)}
144 </span>
144 </span>
145 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
145 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
146 %endif
146 %endif
147 </div>
147 </div>
148 </div>
148 </div>
149 <div class="field">
149 <div class="field">
150 <div class="pr-description-label label-summary">
150 <div class="pr-description-label label-summary">
151 <label>${_('Description')}:</label>
151 <label>${_('Description')}:</label>
152 </div>
152 </div>
153 <div id="pr-desc" class="input">
153 <div id="pr-desc" class="input">
154 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
154 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
155 </div>
155 </div>
156 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
156 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
157 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
157 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
158 </div>
158 </div>
159 </div>
159 </div>
160 <div class="field">
160 <div class="field">
161 <div class="label-summary">
161 <div class="label-summary">
162 <label>${_('Comments')}:</label>
162 <label>${_('Comments')}:</label>
163 </div>
163 </div>
164 <div class="input">
164 <div class="input">
165 <div>
165 <div>
166 <div class="comments-number">
166 <div class="comments-number">
167 %if c.comments:
167 %if c.comments:
168 <a href="#comments">${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}</a>,
168 <a href="#comments">${ungettext("%d General Comment", "%d General Comments", len(c.comments)) % len(c.comments)}</a>,
169 %else:
169 %else:
170 ${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}
170 ${ungettext("%d General Comment", "%d General Comments", len(c.comments)) % len(c.comments)}
171 %endif
171 %endif
172
172 %if c.inline_cnt:
173 %if c.inline_cnt:
173 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
174 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
174 %else:
175 %else:
175 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
176 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
176 %endif
177 %endif
177
178
178
179 %if c.outdated_cnt:
179 % if c.outdated_cnt:
180 , ${ungettext("%d Outdated Comment", "%d Outdated Comments", c.outdated_cnt) % c.outdated_cnt} <span id="show-outdated-comments" class="btn btn-link">${_('(Show)')}</span>
180 ,${ungettext("%d Outdated Comment", "%d Outdated Comments", c.outdated_cnt) % c.outdated_cnt} <span id="show-outdated-comments" class="btn btn-link">${_('(Show)')}</span>
181 %endif
181 % endif
182 </div>
182 </div>
183 </div>
183 </div>
184 </div>
184 </div>
185
186 </div>
187
188 <div class="field">
189 <div class="label-summary">
190 <label>${_('Versions')}:</label>
185 </div>
191 </div>
192 <div>
193 <table>
194 <tr>
195 <td>
196 % if c.at_version == None:
197 <i class="icon-ok link"></i>
198 % endif
199 </td>
200 <td><code><a href="${h.url.current()}">latest</a></code></td>
201 <td>
202 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
203 </td>
204 <td>${_('created')} ${h.age_component(c.pull_request.created_on)}</td>
205 </tr>
206 % for ver in reversed(c.pull_request.versions()):
207 <tr>
208 <td>
209 % if c.at_version == ver.pull_request_version_id:
210 <i class="icon-ok link"></i>
211 % endif
212 </td>
213 <td><code><a href="${h.url.current(version=ver.pull_request_version_id)}">version ${ver.pull_request_version_id}</a></code></td>
214 <td>
215 <code>${ver.source_ref_parts.commit_id[:6]}</code>
216 </td>
217 <td>${_('created')} ${h.age_component(ver.created_on)}</td>
218 </tr>
219 % endfor
220 </table>
221 </div>
222 </div>
223
186 <div id="pr-save" class="field" style="display: none;">
224 <div id="pr-save" class="field" style="display: none;">
187 <div class="label-summary"></div>
225 <div class="label-summary"></div>
188 <div class="input">
226 <div class="input">
189 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
227 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
190 </div>
228 </div>
191 </div>
229 </div>
192 </div>
230 </div>
193 </div>
231 </div>
194 <div>
232 <div>
195 ## AUTHOR
233 ## AUTHOR
196 <div class="reviewers-title block-right">
234 <div class="reviewers-title block-right">
197 <div class="pr-details-title">
235 <div class="pr-details-title">
198 ${_('Author')}
236 ${_('Author')}
199 </div>
237 </div>
200 </div>
238 </div>
201 <div class="block-right pr-details-content reviewers">
239 <div class="block-right pr-details-content reviewers">
202 <ul class="group_members">
240 <ul class="group_members">
203 <li>
241 <li>
204 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
242 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
205 </li>
243 </li>
206 </ul>
244 </ul>
207 </div>
245 </div>
208 ## REVIEWERS
246 ## REVIEWERS
209 <div class="reviewers-title block-right">
247 <div class="reviewers-title block-right">
210 <div class="pr-details-title">
248 <div class="pr-details-title">
211 ${_('Pull request reviewers')}
249 ${_('Pull request reviewers')}
212 %if c.allowed_to_update:
250 %if c.allowed_to_update:
213 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
251 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
214 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
252 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
215 %endif
253 %endif
216 </div>
254 </div>
217 </div>
255 </div>
218 <div id="reviewers" class="block-right pr-details-content reviewers">
256 <div id="reviewers" class="block-right pr-details-content reviewers">
219 ## members goes here !
257 ## members goes here !
220 <input type="hidden" name="__start__" value="review_members:sequence">
258 <input type="hidden" name="__start__" value="review_members:sequence">
221 <ul id="review_members" class="group_members">
259 <ul id="review_members" class="group_members">
222 %for member,reasons,status in c.pull_request_reviewers:
260 %for member,reasons,status in c.pull_request_reviewers:
223 <li id="reviewer_${member.user_id}">
261 <li id="reviewer_${member.user_id}">
224 <div class="reviewers_member">
262 <div class="reviewers_member">
225 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
263 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
226 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
264 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
227 </div>
265 </div>
228 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
266 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
229 ${self.gravatar_with_user(member.email, 16)}
267 ${self.gravatar_with_user(member.email, 16)}
230 </div>
268 </div>
231 <input type="hidden" name="__start__" value="reviewer:mapping">
269 <input type="hidden" name="__start__" value="reviewer:mapping">
232 <input type="hidden" name="__start__" value="reasons:sequence">
270 <input type="hidden" name="__start__" value="reasons:sequence">
233 %for reason in reasons:
271 %for reason in reasons:
234 <div class="reviewer_reason">- ${reason}</div>
272 <div class="reviewer_reason">- ${reason}</div>
235 <input type="hidden" name="reason" value="${reason}">
273 <input type="hidden" name="reason" value="${reason}">
236
274
237 %endfor
275 %endfor
238 <input type="hidden" name="__end__" value="reasons:sequence">
276 <input type="hidden" name="__end__" value="reasons:sequence">
239 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
277 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
240 <input type="hidden" name="__end__" value="reviewer:mapping">
278 <input type="hidden" name="__end__" value="reviewer:mapping">
241 %if c.allowed_to_update:
279 %if c.allowed_to_update:
242 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
280 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
243 <i class="icon-remove-sign" ></i>
281 <i class="icon-remove-sign" ></i>
244 </div>
282 </div>
245 %endif
283 %endif
246 </div>
284 </div>
247 </li>
285 </li>
248 %endfor
286 %endfor
249 </ul>
287 </ul>
250 <input type="hidden" name="__end__" value="review_members:sequence">
288 <input type="hidden" name="__end__" value="review_members:sequence">
251 %if not c.pull_request.is_closed():
289 %if not c.pull_request.is_closed():
252 <div id="add_reviewer_input" class='ac' style="display: none;">
290 <div id="add_reviewer_input" class='ac' style="display: none;">
253 %if c.allowed_to_update:
291 %if c.allowed_to_update:
254 <div class="reviewer_ac">
292 <div class="reviewer_ac">
255 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
293 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
256 <div id="reviewers_container"></div>
294 <div id="reviewers_container"></div>
257 </div>
295 </div>
258 <div>
296 <div>
259 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
297 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
260 </div>
298 </div>
261 %endif
299 %endif
262 </div>
300 </div>
263 %endif
301 %endif
264 </div>
302 </div>
265 </div>
303 </div>
266 </div>
304 </div>
267 <div class="box">
305 <div class="box">
268 ##DIFF
306 ##DIFF
269 <div class="table" >
307 <div class="table" >
270 <div id="changeset_compare_view_content">
308 <div id="changeset_compare_view_content">
271 ##CS
309 ##CS
272 % if c.missing_requirements:
310 % if c.missing_requirements:
273 <div class="box">
311 <div class="box">
274 <div class="alert alert-warning">
312 <div class="alert alert-warning">
275 <div>
313 <div>
276 <strong>${_('Missing requirements:')}</strong>
314 <strong>${_('Missing requirements:')}</strong>
277 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
315 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
278 </div>
316 </div>
279 </div>
317 </div>
280 </div>
318 </div>
281 % elif c.missing_commits:
319 % elif c.missing_commits:
282 <div class="box">
320 <div class="box">
283 <div class="alert alert-warning">
321 <div class="alert alert-warning">
284 <div>
322 <div>
285 <strong>${_('Missing commits')}:</strong>
323 <strong>${_('Missing commits')}:</strong>
286 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
324 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
287 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
325 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
288 </div>
326 </div>
289 </div>
327 </div>
290 </div>
328 </div>
291 % endif
329 % endif
292 <div class="compare_view_commits_title">
330 <div class="compare_view_commits_title">
293 % if c.allowed_to_update and not c.pull_request.is_closed():
331 % if c.allowed_to_update and not c.pull_request.is_closed():
294 <button id="update_commits" class="btn btn-small">${_('Update commits')}</button>
332 <button id="update_commits" class="btn pull-right">${_('Update commits')}</button>
333 % else:
334 <button class="btn disabled pull-right" disabled="disabled">${_('Update commits')}</button>
295 % endif
335 % endif
296 % if len(c.commit_ranges):
336 % if len(c.commit_ranges):
297 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
337 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
298 % endif
338 % endif
299 </div>
339 </div>
300 % if not c.missing_commits:
340 % if not c.missing_commits:
301 <%include file="/compare/compare_commits.html" />
341 <%include file="/compare/compare_commits.html" />
302 <div class="cs_files">
342 <div class="cs_files">
303 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
343 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
304 ${cbdiffs.render_diffset_menu()}
344 ${cbdiffs.render_diffset_menu()}
305 ${cbdiffs.render_diffset(
345 ${cbdiffs.render_diffset(
306 c.diffset, use_comments=True,
346 c.diffset, use_comments=True,
307 collapse_when_files_over=30,
347 collapse_when_files_over=30,
308 disable_new_comments=c.pull_request.is_closed())}
348 disable_new_comments=not c.allowed_to_comment)}
309
349
310 </div>
350 </div>
311 % endif
351 % endif
312 </div>
352 </div>
313
353
314 ## template for inline comment form
354 ## template for inline comment form
315 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
355 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
316 ${comment.comment_inline_form()}
317
356
318 ## render comments and inlines
357 ## render general comments
319 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
358 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
320
359
321 % if not c.pull_request.is_closed():
360 % if not c.pull_request.is_closed():
322 ## main comment form and it status
361 ## main comment form and it status
323 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
362 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
324 pull_request_id=c.pull_request.pull_request_id),
363 pull_request_id=c.pull_request.pull_request_id),
325 c.pull_request_review_status,
364 c.pull_request_review_status,
326 is_pull_request=True, change_status=c.allowed_to_change_status)}
365 is_pull_request=True, change_status=c.allowed_to_change_status)}
327 %endif
366 %endif
328
367
329 <script type="text/javascript">
368 <script type="text/javascript">
330 if (location.hash) {
369 if (location.hash) {
331 var result = splitDelimitedHash(location.hash);
370 var result = splitDelimitedHash(location.hash);
332 var line = $('html').find(result.loc);
371 var line = $('html').find(result.loc);
333 if (line.length > 0){
372 if (line.length > 0){
334 offsetScroll(line, 70);
373 offsetScroll(line, 70);
335 }
374 }
336 }
375 }
337 $(function(){
376 $(function(){
338 ReviewerAutoComplete('user');
377 ReviewerAutoComplete('user');
339 // custom code mirror
378 // custom code mirror
340 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
379 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
341
380
342 var PRDetails = {
381 var PRDetails = {
343 editButton: $('#open_edit_pullrequest'),
382 editButton: $('#open_edit_pullrequest'),
344 closeButton: $('#close_edit_pullrequest'),
383 closeButton: $('#close_edit_pullrequest'),
345 deleteButton: $('#delete_pullrequest'),
384 deleteButton: $('#delete_pullrequest'),
346 viewFields: $('#pr-desc, #pr-title'),
385 viewFields: $('#pr-desc, #pr-title'),
347 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
386 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
348
387
349 init: function() {
388 init: function() {
350 var that = this;
389 var that = this;
351 this.editButton.on('click', function(e) { that.edit(); });
390 this.editButton.on('click', function(e) { that.edit(); });
352 this.closeButton.on('click', function(e) { that.view(); });
391 this.closeButton.on('click', function(e) { that.view(); });
353 },
392 },
354
393
355 edit: function(event) {
394 edit: function(event) {
356 this.viewFields.hide();
395 this.viewFields.hide();
357 this.editButton.hide();
396 this.editButton.hide();
358 this.deleteButton.hide();
397 this.deleteButton.hide();
359 this.closeButton.show();
398 this.closeButton.show();
360 this.editFields.show();
399 this.editFields.show();
361 codeMirrorInstance.refresh();
400 codeMirrorInstance.refresh();
362 },
401 },
363
402
364 view: function(event) {
403 view: function(event) {
365 this.editButton.show();
404 this.editButton.show();
366 this.deleteButton.show();
405 this.deleteButton.show();
367 this.editFields.hide();
406 this.editFields.hide();
368 this.closeButton.hide();
407 this.closeButton.hide();
369 this.viewFields.show();
408 this.viewFields.show();
370 }
409 }
371 };
410 };
372
411
373 var ReviewersPanel = {
412 var ReviewersPanel = {
374 editButton: $('#open_edit_reviewers'),
413 editButton: $('#open_edit_reviewers'),
375 closeButton: $('#close_edit_reviewers'),
414 closeButton: $('#close_edit_reviewers'),
376 addButton: $('#add_reviewer_input'),
415 addButton: $('#add_reviewer_input'),
377 removeButtons: $('.reviewer_member_remove'),
416 removeButtons: $('.reviewer_member_remove'),
378
417
379 init: function() {
418 init: function() {
380 var that = this;
419 var that = this;
381 this.editButton.on('click', function(e) { that.edit(); });
420 this.editButton.on('click', function(e) { that.edit(); });
382 this.closeButton.on('click', function(e) { that.close(); });
421 this.closeButton.on('click', function(e) { that.close(); });
383 },
422 },
384
423
385 edit: function(event) {
424 edit: function(event) {
386 this.editButton.hide();
425 this.editButton.hide();
387 this.closeButton.show();
426 this.closeButton.show();
388 this.addButton.show();
427 this.addButton.show();
389 this.removeButtons.css('visibility', 'visible');
428 this.removeButtons.css('visibility', 'visible');
390 },
429 },
391
430
392 close: function(event) {
431 close: function(event) {
393 this.editButton.show();
432 this.editButton.show();
394 this.closeButton.hide();
433 this.closeButton.hide();
395 this.addButton.hide();
434 this.addButton.hide();
396 this.removeButtons.css('visibility', 'hidden');
435 this.removeButtons.css('visibility', 'hidden');
397 },
436 }
398 };
437 };
399
438
400 PRDetails.init();
439 PRDetails.init();
401 ReviewersPanel.init();
440 ReviewersPanel.init();
402
441
403 $('#show-outdated-comments').on('click', function(e){
442 $('#show-outdated-comments').on('click', function(e){
404 var button = $(this);
443 var button = $(this);
405 var outdated = $('.outdated');
444 var outdated = $('.comment-outdated');
406 if (button.html() === "(Show)") {
445 if (button.html() === "(Show)") {
407 button.html("(Hide)");
446 button.html("(Hide)");
408 outdated.show();
447 outdated.show();
409 } else {
448 } else {
410 button.html("(Show)");
449 button.html("(Show)");
411 outdated.hide();
450 outdated.hide();
412 }
451 }
413 });
452 });
414
453
415 $('.show-inline-comments').on('change', function(e){
454 $('.show-inline-comments').on('change', function(e){
416 var show = 'none';
455 var show = 'none';
417 var target = e.currentTarget;
456 var target = e.currentTarget;
418 if(target.checked){
457 if(target.checked){
419 show = ''
458 show = ''
420 }
459 }
421 var boxid = $(target).attr('id_for');
460 var boxid = $(target).attr('id_for');
422 var comments = $('#{0} .inline-comments'.format(boxid));
461 var comments = $('#{0} .inline-comments'.format(boxid));
423 var fn_display = function(idx){
462 var fn_display = function(idx){
424 $(this).css('display', show);
463 $(this).css('display', show);
425 };
464 };
426 $(comments).each(fn_display);
465 $(comments).each(fn_display);
427 var btns = $('#{0} .inline-comments-button'.format(boxid));
466 var btns = $('#{0} .inline-comments-button'.format(boxid));
428 $(btns).each(fn_display);
467 $(btns).each(fn_display);
429 });
468 });
430
469
431 // inject comments into their proper positions
432 var file_comments = $('.inline-comment-placeholder');
433 %if c.pull_request.is_closed():
434 renderInlineComments(file_comments, false);
435 %else:
436 renderInlineComments(file_comments, true);
437 %endif
438 var commentTotals = {};
439 $.each(file_comments, function(i, comment) {
440 var path = $(comment).attr('path');
441 var comms = $(comment).children().length;
442 if (path in commentTotals) {
443 commentTotals[path] += comms;
444 } else {
445 commentTotals[path] = comms;
446 }
447 });
448 $.each(commentTotals, function(path, total) {
449 var elem = $('.comment-bubble[data-path="'+ path +'"]');
450 elem.css('visibility', 'visible');
451 elem.html(elem.html() + ' ' + total );
452 });
453
454 $('#merge_pull_request_form').submit(function() {
470 $('#merge_pull_request_form').submit(function() {
455 if (!$('#merge_pull_request').attr('disabled')) {
471 if (!$('#merge_pull_request').attr('disabled')) {
456 $('#merge_pull_request').attr('disabled', 'disabled');
472 $('#merge_pull_request').attr('disabled', 'disabled');
457 }
473 }
458 return true;
474 return true;
459 });
475 });
460
476
461 $('#edit_pull_request').on('click', function(e){
477 $('#edit_pull_request').on('click', function(e){
462 var title = $('#pr-title-input').val();
478 var title = $('#pr-title-input').val();
463 var description = codeMirrorInstance.getValue();
479 var description = codeMirrorInstance.getValue();
464 editPullRequest(
480 editPullRequest(
465 "${c.repo_name}", "${c.pull_request.pull_request_id}",
481 "${c.repo_name}", "${c.pull_request.pull_request_id}",
466 title, description);
482 title, description);
467 });
483 });
468
484
469 $('#update_pull_request').on('click', function(e){
485 $('#update_pull_request').on('click', function(e){
470 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
486 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
471 });
487 });
472
488
473 $('#update_commits').on('click', function(e){
489 $('#update_commits').on('click', function(e){
474 var isDisabled = !$(e.currentTarget).attr('disabled');
490 var isDisabled = !$(e.currentTarget).attr('disabled');
475 $(e.currentTarget).text(_gettext('Updating...'));
491 $(e.currentTarget).text(_gettext('Updating...'));
476 $(e.currentTarget).attr('disabled', 'disabled');
492 $(e.currentTarget).attr('disabled', 'disabled');
477 if(isDisabled){
493 if(isDisabled){
478 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
494 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
479 }
495 }
480
496
481 });
497 });
482 // fixing issue with caches on firefox
498 // fixing issue with caches on firefox
483 $('#update_commits').removeAttr("disabled");
499 $('#update_commits').removeAttr("disabled");
484
500
485 $('#close_pull_request').on('click', function(e){
501 $('#close_pull_request').on('click', function(e){
486 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
502 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
487 });
503 });
488
504
489 $('.show-inline-comments').on('click', function(e){
505 $('.show-inline-comments').on('click', function(e){
490 var boxid = $(this).attr('data-comment-id');
506 var boxid = $(this).attr('data-comment-id');
491 var button = $(this);
507 var button = $(this);
492
508
493 if(button.hasClass("comments-visible")) {
509 if(button.hasClass("comments-visible")) {
494 $('#{0} .inline-comments'.format(boxid)).each(function(index){
510 $('#{0} .inline-comments'.format(boxid)).each(function(index){
495 $(this).hide();
511 $(this).hide();
496 });
512 });
497 button.removeClass("comments-visible");
513 button.removeClass("comments-visible");
498 } else {
514 } else {
499 $('#{0} .inline-comments'.format(boxid)).each(function(index){
515 $('#{0} .inline-comments'.format(boxid)).each(function(index){
500 $(this).show();
516 $(this).show();
501 });
517 });
502 button.addClass("comments-visible");
518 button.addClass("comments-visible");
503 }
519 }
504 });
520 });
505 })
521 })
506 </script>
522 </script>
507
523
508 </div>
524 </div>
509 </div>
525 </div>
510
526
511 </%def>
527 </%def>
General Comments 0
You need to be logged in to leave comments. Login now