##// END OF EJS Templates
diffs: fixed other file source when using pull requests. It must use...
marcink -
r1194:f606ad60 default
parent child Browse files
Show More
@@ -1,993 +1,983 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 (
48 from rhodecode.lib.utils2 import (
49 safe_int, safe_str, str2bool, safe_unicode, UnsafeAttributeDict)
49 safe_int, safe_str, str2bool, safe_unicode, StrictAttributeDict)
50 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
50 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
51 from rhodecode.lib.vcs.exceptions import (
51 from rhodecode.lib.vcs.exceptions import (
52 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
52 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
53 NodeDoesNotExistError)
53 NodeDoesNotExistError)
54
54
55 from rhodecode.model.changeset_status import ChangesetStatusModel
55 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.comment import ChangesetCommentsModel
56 from rhodecode.model.comment import ChangesetCommentsModel
57 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
57 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
58 Repository, PullRequestVersion)
58 Repository, PullRequestVersion)
59 from rhodecode.model.forms import PullRequestForm
59 from rhodecode.model.forms import PullRequestForm
60 from rhodecode.model.meta import Session
60 from rhodecode.model.meta import Session
61 from rhodecode.model.pull_request import PullRequestModel
61 from rhodecode.model.pull_request import PullRequestModel
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 class PullrequestsController(BaseRepoController):
66 class PullrequestsController(BaseRepoController):
67 def __before__(self):
67 def __before__(self):
68 super(PullrequestsController, self).__before__()
68 super(PullrequestsController, self).__before__()
69
69
70 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):
71 """
71 """
72 Load context data needed for generating compare diff
72 Load context data needed for generating compare diff
73
73
74 :param pull_request: object related to the request
74 :param pull_request: object related to the request
75 :param enable_comments: flag to determine if comments are included
75 :param enable_comments: flag to determine if comments are included
76 """
76 """
77 source_repo = pull_request.source_repo
77 source_repo = pull_request.source_repo
78 source_ref_id = pull_request.source_ref_parts.commit_id
78 source_ref_id = pull_request.source_ref_parts.commit_id
79
79
80 target_repo = pull_request.target_repo
80 target_repo = pull_request.target_repo
81 target_ref_id = pull_request.target_ref_parts.commit_id
81 target_ref_id = pull_request.target_ref_parts.commit_id
82
82
83 # despite opening commits for bookmarks/branches/tags, we always
83 # despite opening commits for bookmarks/branches/tags, we always
84 # 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
85 c.source_ref_type = 'rev'
85 c.source_ref_type = 'rev'
86 c.source_ref = source_ref_id
86 c.source_ref = source_ref_id
87
87
88 c.target_ref_type = 'rev'
88 c.target_ref_type = 'rev'
89 c.target_ref = target_ref_id
89 c.target_ref = target_ref_id
90
90
91 c.source_repo = source_repo
91 c.source_repo = source_repo
92 c.target_repo = target_repo
92 c.target_repo = target_repo
93
93
94 c.fulldiff = bool(request.GET.get('fulldiff'))
94 c.fulldiff = bool(request.GET.get('fulldiff'))
95
95
96 # 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
97 # if the limit is applied otherwise will just hide the
97 # if the limit is applied otherwise will just hide the
98 # big files from the front-end
98 # big files from the front-end
99 diff_limit = self.cut_off_limit_diff
99 diff_limit = self.cut_off_limit_diff
100 file_limit = self.cut_off_limit_file
100 file_limit = self.cut_off_limit_file
101
101
102 pre_load = ["author", "branch", "date", "message"]
102 pre_load = ["author", "branch", "date", "message"]
103
103
104 c.commit_ranges = []
104 c.commit_ranges = []
105 source_commit = EmptyCommit()
105 source_commit = EmptyCommit()
106 target_commit = EmptyCommit()
106 target_commit = EmptyCommit()
107 c.missing_requirements = False
107 c.missing_requirements = False
108 try:
108 try:
109 c.commit_ranges = [
109 c.commit_ranges = [
110 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
110 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
111 for rev in pull_request.revisions]
111 for rev in pull_request.revisions]
112
112
113 c.statuses = source_repo.statuses(
113 c.statuses = source_repo.statuses(
114 [x.raw_id for x in c.commit_ranges])
114 [x.raw_id for x in c.commit_ranges])
115
115
116 target_commit = source_repo.get_commit(
116 target_commit = source_repo.get_commit(
117 commit_id=safe_str(target_ref_id))
117 commit_id=safe_str(target_ref_id))
118 source_commit = source_repo.get_commit(
118 source_commit = source_repo.get_commit(
119 commit_id=safe_str(source_ref_id))
119 commit_id=safe_str(source_ref_id))
120 except RepositoryRequirementError:
120 except RepositoryRequirementError:
121 c.missing_requirements = True
121 c.missing_requirements = True
122
122
123 c.changes = {}
123 c.changes = {}
124 c.missing_commits = False
124 c.missing_commits = False
125 if (c.missing_requirements or
125 if (c.missing_requirements or
126 isinstance(source_commit, EmptyCommit) or
126 isinstance(source_commit, EmptyCommit) or
127 source_commit == target_commit):
127 source_commit == target_commit):
128 _parsed = []
128 _parsed = []
129 c.missing_commits = True
129 c.missing_commits = True
130 else:
130 else:
131 vcs_diff = PullRequestModel().get_diff(pull_request)
131 vcs_diff = PullRequestModel().get_diff(pull_request)
132 diff_processor = diffs.DiffProcessor(
132 diff_processor = diffs.DiffProcessor(
133 vcs_diff, format='newdiff', diff_limit=diff_limit,
133 vcs_diff, format='newdiff', diff_limit=diff_limit,
134 file_limit=file_limit, show_full_diff=c.fulldiff)
134 file_limit=file_limit, show_full_diff=c.fulldiff)
135 _parsed = diff_processor.prepare()
135 _parsed = diff_processor.prepare()
136
136
137 commit_changes = OrderedDict()
137 commit_changes = OrderedDict()
138 _parsed = diff_processor.prepare()
138 _parsed = diff_processor.prepare()
139 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
139 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
140
140
141 _parsed = diff_processor.prepare()
141 _parsed = diff_processor.prepare()
142
142
143 def _node_getter(commit):
143 def _node_getter(commit):
144 def get_node(fname):
144 def get_node(fname):
145 try:
145 try:
146 return commit.get_node(fname)
146 return commit.get_node(fname)
147 except NodeDoesNotExistError:
147 except NodeDoesNotExistError:
148 return None
148 return None
149 return get_node
149 return get_node
150
150
151 c.diffset = codeblocks.DiffSet(
151 c.diffset = codeblocks.DiffSet(
152 repo_name=c.repo_name,
152 repo_name=c.repo_name,
153 source_repo_name=c.source_repo.repo_name,
153 source_node_getter=_node_getter(target_commit),
154 source_node_getter=_node_getter(target_commit),
154 target_node_getter=_node_getter(source_commit),
155 target_node_getter=_node_getter(source_commit),
155 comments=inline_comments
156 comments=inline_comments
156 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
157 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
157
158
158 c.included_files = []
159 c.included_files = []
159 c.deleted_files = []
160 c.deleted_files = []
160
161
161 for f in _parsed:
162 for f in _parsed:
162 st = f['stats']
163 st = f['stats']
163 fid = h.FID('', f['filename'])
164 fid = h.FID('', f['filename'])
164 c.included_files.append(f['filename'])
165 c.included_files.append(f['filename'])
165
166
166 def _extract_ordering(self, request):
167 def _extract_ordering(self, request):
167 column_index = safe_int(request.GET.get('order[0][column]'))
168 column_index = safe_int(request.GET.get('order[0][column]'))
168 order_dir = request.GET.get('order[0][dir]', 'desc')
169 order_dir = request.GET.get('order[0][dir]', 'desc')
169 order_by = request.GET.get(
170 order_by = request.GET.get(
170 'columns[%s][data][sort]' % column_index, 'name_raw')
171 'columns[%s][data][sort]' % column_index, 'name_raw')
171 return order_by, order_dir
172 return order_by, order_dir
172
173
173 @LoginRequired()
174 @LoginRequired()
174 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
175 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
175 'repository.admin')
176 'repository.admin')
176 @HasAcceptedRepoType('git', 'hg')
177 @HasAcceptedRepoType('git', 'hg')
177 def show_all(self, repo_name):
178 def show_all(self, repo_name):
178 # filter types
179 # filter types
179 c.active = 'open'
180 c.active = 'open'
180 c.source = str2bool(request.GET.get('source'))
181 c.source = str2bool(request.GET.get('source'))
181 c.closed = str2bool(request.GET.get('closed'))
182 c.closed = str2bool(request.GET.get('closed'))
182 c.my = str2bool(request.GET.get('my'))
183 c.my = str2bool(request.GET.get('my'))
183 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
184 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
184 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
185 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
185 c.repo_name = repo_name
186 c.repo_name = repo_name
186
187
187 opened_by = None
188 opened_by = None
188 if c.my:
189 if c.my:
189 c.active = 'my'
190 c.active = 'my'
190 opened_by = [c.rhodecode_user.user_id]
191 opened_by = [c.rhodecode_user.user_id]
191
192
192 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
193 if c.closed:
194 if c.closed:
194 c.active = 'closed'
195 c.active = 'closed'
195 statuses = [PullRequest.STATUS_CLOSED]
196 statuses = [PullRequest.STATUS_CLOSED]
196
197
197 if c.awaiting_review and not c.source:
198 if c.awaiting_review and not c.source:
198 c.active = 'awaiting'
199 c.active = 'awaiting'
199 if c.source and not c.awaiting_review:
200 if c.source and not c.awaiting_review:
200 c.active = 'source'
201 c.active = 'source'
201 if c.awaiting_my_review:
202 if c.awaiting_my_review:
202 c.active = 'awaiting_my'
203 c.active = 'awaiting_my'
203
204
204 data = self._get_pull_requests_list(
205 data = self._get_pull_requests_list(
205 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
206 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
206 if not request.is_xhr:
207 if not request.is_xhr:
207 c.data = json.dumps(data['data'])
208 c.data = json.dumps(data['data'])
208 c.records_total = data['recordsTotal']
209 c.records_total = data['recordsTotal']
209 return render('/pullrequests/pullrequests.html')
210 return render('/pullrequests/pullrequests.html')
210 else:
211 else:
211 return json.dumps(data)
212 return json.dumps(data)
212
213
213 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
214 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
214 # pagination
215 # pagination
215 start = safe_int(request.GET.get('start'), 0)
216 start = safe_int(request.GET.get('start'), 0)
216 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
217 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
217 order_by, order_dir = self._extract_ordering(request)
218 order_by, order_dir = self._extract_ordering(request)
218
219
219 if c.awaiting_review:
220 if c.awaiting_review:
220 pull_requests = PullRequestModel().get_awaiting_review(
221 pull_requests = PullRequestModel().get_awaiting_review(
221 repo_name, source=c.source, opened_by=opened_by,
222 repo_name, source=c.source, opened_by=opened_by,
222 statuses=statuses, offset=start, length=length,
223 statuses=statuses, offset=start, length=length,
223 order_by=order_by, order_dir=order_dir)
224 order_by=order_by, order_dir=order_dir)
224 pull_requests_total_count = PullRequestModel(
225 pull_requests_total_count = PullRequestModel(
225 ).count_awaiting_review(
226 ).count_awaiting_review(
226 repo_name, source=c.source, statuses=statuses,
227 repo_name, source=c.source, statuses=statuses,
227 opened_by=opened_by)
228 opened_by=opened_by)
228 elif c.awaiting_my_review:
229 elif c.awaiting_my_review:
229 pull_requests = PullRequestModel().get_awaiting_my_review(
230 pull_requests = PullRequestModel().get_awaiting_my_review(
230 repo_name, source=c.source, opened_by=opened_by,
231 repo_name, source=c.source, opened_by=opened_by,
231 user_id=c.rhodecode_user.user_id, statuses=statuses,
232 user_id=c.rhodecode_user.user_id, statuses=statuses,
232 offset=start, length=length, order_by=order_by,
233 offset=start, length=length, order_by=order_by,
233 order_dir=order_dir)
234 order_dir=order_dir)
234 pull_requests_total_count = PullRequestModel(
235 pull_requests_total_count = PullRequestModel(
235 ).count_awaiting_my_review(
236 ).count_awaiting_my_review(
236 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
237 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
237 statuses=statuses, opened_by=opened_by)
238 statuses=statuses, opened_by=opened_by)
238 else:
239 else:
239 pull_requests = PullRequestModel().get_all(
240 pull_requests = PullRequestModel().get_all(
240 repo_name, source=c.source, opened_by=opened_by,
241 repo_name, source=c.source, opened_by=opened_by,
241 statuses=statuses, offset=start, length=length,
242 statuses=statuses, offset=start, length=length,
242 order_by=order_by, order_dir=order_dir)
243 order_by=order_by, order_dir=order_dir)
243 pull_requests_total_count = PullRequestModel().count_all(
244 pull_requests_total_count = PullRequestModel().count_all(
244 repo_name, source=c.source, statuses=statuses,
245 repo_name, source=c.source, statuses=statuses,
245 opened_by=opened_by)
246 opened_by=opened_by)
246
247
247 from rhodecode.lib.utils import PartialRenderer
248 from rhodecode.lib.utils import PartialRenderer
248 _render = PartialRenderer('data_table/_dt_elements.html')
249 _render = PartialRenderer('data_table/_dt_elements.html')
249 data = []
250 data = []
250 for pr in pull_requests:
251 for pr in pull_requests:
251 comments = ChangesetCommentsModel().get_all_comments(
252 comments = ChangesetCommentsModel().get_all_comments(
252 c.rhodecode_db_repo.repo_id, pull_request=pr)
253 c.rhodecode_db_repo.repo_id, pull_request=pr)
253
254
254 data.append({
255 data.append({
255 'name': _render('pullrequest_name',
256 'name': _render('pullrequest_name',
256 pr.pull_request_id, pr.target_repo.repo_name),
257 pr.pull_request_id, pr.target_repo.repo_name),
257 'name_raw': pr.pull_request_id,
258 'name_raw': pr.pull_request_id,
258 'status': _render('pullrequest_status',
259 'status': _render('pullrequest_status',
259 pr.calculated_review_status()),
260 pr.calculated_review_status()),
260 'title': _render(
261 'title': _render(
261 'pullrequest_title', pr.title, pr.description),
262 'pullrequest_title', pr.title, pr.description),
262 'description': h.escape(pr.description),
263 'description': h.escape(pr.description),
263 'updated_on': _render('pullrequest_updated_on',
264 'updated_on': _render('pullrequest_updated_on',
264 h.datetime_to_time(pr.updated_on)),
265 h.datetime_to_time(pr.updated_on)),
265 'updated_on_raw': h.datetime_to_time(pr.updated_on),
266 'updated_on_raw': h.datetime_to_time(pr.updated_on),
266 'created_on': _render('pullrequest_updated_on',
267 'created_on': _render('pullrequest_updated_on',
267 h.datetime_to_time(pr.created_on)),
268 h.datetime_to_time(pr.created_on)),
268 'created_on_raw': h.datetime_to_time(pr.created_on),
269 'created_on_raw': h.datetime_to_time(pr.created_on),
269 'author': _render('pullrequest_author',
270 'author': _render('pullrequest_author',
270 pr.author.full_contact, ),
271 pr.author.full_contact, ),
271 'author_raw': pr.author.full_name,
272 'author_raw': pr.author.full_name,
272 'comments': _render('pullrequest_comments', len(comments)),
273 'comments': _render('pullrequest_comments', len(comments)),
273 'comments_raw': len(comments),
274 'comments_raw': len(comments),
274 'closed': pr.is_closed(),
275 'closed': pr.is_closed(),
275 })
276 })
276 # json used to render the grid
277 # json used to render the grid
277 data = ({
278 data = ({
278 'data': data,
279 'data': data,
279 'recordsTotal': pull_requests_total_count,
280 'recordsTotal': pull_requests_total_count,
280 'recordsFiltered': pull_requests_total_count,
281 'recordsFiltered': pull_requests_total_count,
281 })
282 })
282 return data
283 return data
283
284
284 @LoginRequired()
285 @LoginRequired()
285 @NotAnonymous()
286 @NotAnonymous()
286 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
287 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
287 'repository.admin')
288 'repository.admin')
288 @HasAcceptedRepoType('git', 'hg')
289 @HasAcceptedRepoType('git', 'hg')
289 def index(self):
290 def index(self):
290 source_repo = c.rhodecode_db_repo
291 source_repo = c.rhodecode_db_repo
291
292
292 try:
293 try:
293 source_repo.scm_instance().get_commit()
294 source_repo.scm_instance().get_commit()
294 except EmptyRepositoryError:
295 except EmptyRepositoryError:
295 h.flash(h.literal(_('There are no commits yet')),
296 h.flash(h.literal(_('There are no commits yet')),
296 category='warning')
297 category='warning')
297 redirect(url('summary_home', repo_name=source_repo.repo_name))
298 redirect(url('summary_home', repo_name=source_repo.repo_name))
298
299
299 commit_id = request.GET.get('commit')
300 commit_id = request.GET.get('commit')
300 branch_ref = request.GET.get('branch')
301 branch_ref = request.GET.get('branch')
301 bookmark_ref = request.GET.get('bookmark')
302 bookmark_ref = request.GET.get('bookmark')
302
303
303 try:
304 try:
304 source_repo_data = PullRequestModel().generate_repo_data(
305 source_repo_data = PullRequestModel().generate_repo_data(
305 source_repo, commit_id=commit_id,
306 source_repo, commit_id=commit_id,
306 branch=branch_ref, bookmark=bookmark_ref)
307 branch=branch_ref, bookmark=bookmark_ref)
307 except CommitDoesNotExistError as e:
308 except CommitDoesNotExistError as e:
308 log.exception(e)
309 log.exception(e)
309 h.flash(_('Commit does not exist'), 'error')
310 h.flash(_('Commit does not exist'), 'error')
310 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
311 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
311
312
312 default_target_repo = source_repo
313 default_target_repo = source_repo
313
314
314 if source_repo.parent:
315 if source_repo.parent:
315 parent_vcs_obj = source_repo.parent.scm_instance()
316 parent_vcs_obj = source_repo.parent.scm_instance()
316 if parent_vcs_obj and not parent_vcs_obj.is_empty():
317 if parent_vcs_obj and not parent_vcs_obj.is_empty():
317 # change default if we have a parent repo
318 # change default if we have a parent repo
318 default_target_repo = source_repo.parent
319 default_target_repo = source_repo.parent
319
320
320 target_repo_data = PullRequestModel().generate_repo_data(
321 target_repo_data = PullRequestModel().generate_repo_data(
321 default_target_repo)
322 default_target_repo)
322
323
323 selected_source_ref = source_repo_data['refs']['selected_ref']
324 selected_source_ref = source_repo_data['refs']['selected_ref']
324
325
325 title_source_ref = selected_source_ref.split(':', 2)[1]
326 title_source_ref = selected_source_ref.split(':', 2)[1]
326 c.default_title = PullRequestModel().generate_pullrequest_title(
327 c.default_title = PullRequestModel().generate_pullrequest_title(
327 source=source_repo.repo_name,
328 source=source_repo.repo_name,
328 source_ref=title_source_ref,
329 source_ref=title_source_ref,
329 target=default_target_repo.repo_name
330 target=default_target_repo.repo_name
330 )
331 )
331
332
332 c.default_repo_data = {
333 c.default_repo_data = {
333 'source_repo_name': source_repo.repo_name,
334 'source_repo_name': source_repo.repo_name,
334 'source_refs_json': json.dumps(source_repo_data),
335 'source_refs_json': json.dumps(source_repo_data),
335 'target_repo_name': default_target_repo.repo_name,
336 'target_repo_name': default_target_repo.repo_name,
336 'target_refs_json': json.dumps(target_repo_data),
337 'target_refs_json': json.dumps(target_repo_data),
337 }
338 }
338 c.default_source_ref = selected_source_ref
339 c.default_source_ref = selected_source_ref
339
340
340 return render('/pullrequests/pullrequest.html')
341 return render('/pullrequests/pullrequest.html')
341
342
342 @LoginRequired()
343 @LoginRequired()
343 @NotAnonymous()
344 @NotAnonymous()
344 @XHRRequired()
345 @XHRRequired()
345 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
346 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
346 'repository.admin')
347 'repository.admin')
347 @jsonify
348 @jsonify
348 def get_repo_refs(self, repo_name, target_repo_name):
349 def get_repo_refs(self, repo_name, target_repo_name):
349 repo = Repository.get_by_repo_name(target_repo_name)
350 repo = Repository.get_by_repo_name(target_repo_name)
350 if not repo:
351 if not repo:
351 raise HTTPNotFound
352 raise HTTPNotFound
352 return PullRequestModel().generate_repo_data(repo)
353 return PullRequestModel().generate_repo_data(repo)
353
354
354 @LoginRequired()
355 @LoginRequired()
355 @NotAnonymous()
356 @NotAnonymous()
356 @XHRRequired()
357 @XHRRequired()
357 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
358 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
358 'repository.admin')
359 'repository.admin')
359 @jsonify
360 @jsonify
360 def get_repo_destinations(self, repo_name):
361 def get_repo_destinations(self, repo_name):
361 repo = Repository.get_by_repo_name(repo_name)
362 repo = Repository.get_by_repo_name(repo_name)
362 if not repo:
363 if not repo:
363 raise HTTPNotFound
364 raise HTTPNotFound
364 filter_query = request.GET.get('query')
365 filter_query = request.GET.get('query')
365
366
366 query = Repository.query() \
367 query = Repository.query() \
367 .order_by(func.length(Repository.repo_name)) \
368 .order_by(func.length(Repository.repo_name)) \
368 .filter(or_(
369 .filter(or_(
369 Repository.repo_name == repo.repo_name,
370 Repository.repo_name == repo.repo_name,
370 Repository.fork_id == repo.repo_id))
371 Repository.fork_id == repo.repo_id))
371
372
372 if filter_query:
373 if filter_query:
373 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
374 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
374 query = query.filter(
375 query = query.filter(
375 Repository.repo_name.ilike(ilike_expression))
376 Repository.repo_name.ilike(ilike_expression))
376
377
377 add_parent = False
378 add_parent = False
378 if repo.parent:
379 if repo.parent:
379 if filter_query in repo.parent.repo_name:
380 if filter_query in repo.parent.repo_name:
380 parent_vcs_obj = repo.parent.scm_instance()
381 parent_vcs_obj = repo.parent.scm_instance()
381 if parent_vcs_obj and not parent_vcs_obj.is_empty():
382 if parent_vcs_obj and not parent_vcs_obj.is_empty():
382 add_parent = True
383 add_parent = True
383
384
384 limit = 20 - 1 if add_parent else 20
385 limit = 20 - 1 if add_parent else 20
385 all_repos = query.limit(limit).all()
386 all_repos = query.limit(limit).all()
386 if add_parent:
387 if add_parent:
387 all_repos += [repo.parent]
388 all_repos += [repo.parent]
388
389
389 repos = []
390 repos = []
390 for obj in self.scm_model.get_repos(all_repos):
391 for obj in self.scm_model.get_repos(all_repos):
391 repos.append({
392 repos.append({
392 'id': obj['name'],
393 'id': obj['name'],
393 'text': obj['name'],
394 'text': obj['name'],
394 'type': 'repo',
395 'type': 'repo',
395 'obj': obj['dbrepo']
396 'obj': obj['dbrepo']
396 })
397 })
397
398
398 data = {
399 data = {
399 'more': False,
400 'more': False,
400 'results': [{
401 'results': [{
401 'text': _('Repositories'),
402 'text': _('Repositories'),
402 'children': repos
403 'children': repos
403 }] if repos else []
404 }] if repos else []
404 }
405 }
405 return data
406 return data
406
407
407 @LoginRequired()
408 @LoginRequired()
408 @NotAnonymous()
409 @NotAnonymous()
409 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
410 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
410 'repository.admin')
411 'repository.admin')
411 @HasAcceptedRepoType('git', 'hg')
412 @HasAcceptedRepoType('git', 'hg')
412 @auth.CSRFRequired()
413 @auth.CSRFRequired()
413 def create(self, repo_name):
414 def create(self, repo_name):
414 repo = Repository.get_by_repo_name(repo_name)
415 repo = Repository.get_by_repo_name(repo_name)
415 if not repo:
416 if not repo:
416 raise HTTPNotFound
417 raise HTTPNotFound
417
418
418 controls = peppercorn.parse(request.POST.items())
419 controls = peppercorn.parse(request.POST.items())
419
420
420 try:
421 try:
421 _form = PullRequestForm(repo.repo_id)().to_python(controls)
422 _form = PullRequestForm(repo.repo_id)().to_python(controls)
422 except formencode.Invalid as errors:
423 except formencode.Invalid as errors:
423 if errors.error_dict.get('revisions'):
424 if errors.error_dict.get('revisions'):
424 msg = 'Revisions: %s' % errors.error_dict['revisions']
425 msg = 'Revisions: %s' % errors.error_dict['revisions']
425 elif errors.error_dict.get('pullrequest_title'):
426 elif errors.error_dict.get('pullrequest_title'):
426 msg = _('Pull request requires a title with min. 3 chars')
427 msg = _('Pull request requires a title with min. 3 chars')
427 else:
428 else:
428 msg = _('Error creating pull request: {}').format(errors)
429 msg = _('Error creating pull request: {}').format(errors)
429 log.exception(msg)
430 log.exception(msg)
430 h.flash(msg, 'error')
431 h.flash(msg, 'error')
431
432
432 # would rather just go back to form ...
433 # would rather just go back to form ...
433 return redirect(url('pullrequest_home', repo_name=repo_name))
434 return redirect(url('pullrequest_home', repo_name=repo_name))
434
435
435 source_repo = _form['source_repo']
436 source_repo = _form['source_repo']
436 source_ref = _form['source_ref']
437 source_ref = _form['source_ref']
437 target_repo = _form['target_repo']
438 target_repo = _form['target_repo']
438 target_ref = _form['target_ref']
439 target_ref = _form['target_ref']
439 commit_ids = _form['revisions'][::-1]
440 commit_ids = _form['revisions'][::-1]
440 reviewers = [
441 reviewers = [
441 (r['user_id'], r['reasons']) for r in _form['review_members']]
442 (r['user_id'], r['reasons']) for r in _form['review_members']]
442
443
443 # find the ancestor for this pr
444 # find the ancestor for this pr
444 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
445 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
445 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
446 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
446
447
447 source_scm = source_db_repo.scm_instance()
448 source_scm = source_db_repo.scm_instance()
448 target_scm = target_db_repo.scm_instance()
449 target_scm = target_db_repo.scm_instance()
449
450
450 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
451 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
451 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
452 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
452
453
453 ancestor = source_scm.get_common_ancestor(
454 ancestor = source_scm.get_common_ancestor(
454 source_commit.raw_id, target_commit.raw_id, target_scm)
455 source_commit.raw_id, target_commit.raw_id, target_scm)
455
456
456 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
457 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
457 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
458 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
458
459
459 pullrequest_title = _form['pullrequest_title']
460 pullrequest_title = _form['pullrequest_title']
460 title_source_ref = source_ref.split(':', 2)[1]
461 title_source_ref = source_ref.split(':', 2)[1]
461 if not pullrequest_title:
462 if not pullrequest_title:
462 pullrequest_title = PullRequestModel().generate_pullrequest_title(
463 pullrequest_title = PullRequestModel().generate_pullrequest_title(
463 source=source_repo,
464 source=source_repo,
464 source_ref=title_source_ref,
465 source_ref=title_source_ref,
465 target=target_repo
466 target=target_repo
466 )
467 )
467
468
468 description = _form['pullrequest_desc']
469 description = _form['pullrequest_desc']
469 try:
470 try:
470 pull_request = PullRequestModel().create(
471 pull_request = PullRequestModel().create(
471 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
472 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
472 target_ref, commit_ids, reviewers, pullrequest_title,
473 target_ref, commit_ids, reviewers, pullrequest_title,
473 description
474 description
474 )
475 )
475 Session().commit()
476 Session().commit()
476 h.flash(_('Successfully opened new pull request'),
477 h.flash(_('Successfully opened new pull request'),
477 category='success')
478 category='success')
478 except Exception as e:
479 except Exception as e:
479 msg = _('Error occurred during sending pull request')
480 msg = _('Error occurred during sending pull request')
480 log.exception(msg)
481 log.exception(msg)
481 h.flash(msg, category='error')
482 h.flash(msg, category='error')
482 return redirect(url('pullrequest_home', repo_name=repo_name))
483 return redirect(url('pullrequest_home', repo_name=repo_name))
483
484
484 return redirect(url('pullrequest_show', repo_name=target_repo,
485 return redirect(url('pullrequest_show', repo_name=target_repo,
485 pull_request_id=pull_request.pull_request_id))
486 pull_request_id=pull_request.pull_request_id))
486
487
487 @LoginRequired()
488 @LoginRequired()
488 @NotAnonymous()
489 @NotAnonymous()
489 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
490 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
490 'repository.admin')
491 'repository.admin')
491 @auth.CSRFRequired()
492 @auth.CSRFRequired()
492 @jsonify
493 @jsonify
493 def update(self, repo_name, pull_request_id):
494 def update(self, repo_name, pull_request_id):
494 pull_request_id = safe_int(pull_request_id)
495 pull_request_id = safe_int(pull_request_id)
495 pull_request = PullRequest.get_or_404(pull_request_id)
496 pull_request = PullRequest.get_or_404(pull_request_id)
496 # only owner or admin can update it
497 # only owner or admin can update it
497 allowed_to_update = PullRequestModel().check_user_update(
498 allowed_to_update = PullRequestModel().check_user_update(
498 pull_request, c.rhodecode_user)
499 pull_request, c.rhodecode_user)
499 if allowed_to_update:
500 if allowed_to_update:
500 controls = peppercorn.parse(request.POST.items())
501 controls = peppercorn.parse(request.POST.items())
501
502
502 if 'review_members' in controls:
503 if 'review_members' in controls:
503 self._update_reviewers(
504 self._update_reviewers(
504 pull_request_id, controls['review_members'])
505 pull_request_id, controls['review_members'])
505 elif str2bool(request.POST.get('update_commits', 'false')):
506 elif str2bool(request.POST.get('update_commits', 'false')):
506 self._update_commits(pull_request)
507 self._update_commits(pull_request)
507 elif str2bool(request.POST.get('close_pull_request', 'false')):
508 elif str2bool(request.POST.get('close_pull_request', 'false')):
508 self._reject_close(pull_request)
509 self._reject_close(pull_request)
509 elif str2bool(request.POST.get('edit_pull_request', 'false')):
510 elif str2bool(request.POST.get('edit_pull_request', 'false')):
510 self._edit_pull_request(pull_request)
511 self._edit_pull_request(pull_request)
511 else:
512 else:
512 raise HTTPBadRequest()
513 raise HTTPBadRequest()
513 return True
514 return True
514 raise HTTPForbidden()
515 raise HTTPForbidden()
515
516
516 def _edit_pull_request(self, pull_request):
517 def _edit_pull_request(self, pull_request):
517 try:
518 try:
518 PullRequestModel().edit(
519 PullRequestModel().edit(
519 pull_request, request.POST.get('title'),
520 pull_request, request.POST.get('title'),
520 request.POST.get('description'))
521 request.POST.get('description'))
521 except ValueError:
522 except ValueError:
522 msg = _(u'Cannot update closed pull requests.')
523 msg = _(u'Cannot update closed pull requests.')
523 h.flash(msg, category='error')
524 h.flash(msg, category='error')
524 return
525 return
525 else:
526 else:
526 Session().commit()
527 Session().commit()
527
528
528 msg = _(u'Pull request title & description updated.')
529 msg = _(u'Pull request title & description updated.')
529 h.flash(msg, category='success')
530 h.flash(msg, category='success')
530 return
531 return
531
532
532 def _update_commits(self, pull_request):
533 def _update_commits(self, pull_request):
533 resp = PullRequestModel().update_commits(pull_request)
534 resp = PullRequestModel().update_commits(pull_request)
534
535
535 if resp.executed:
536 if resp.executed:
536 msg = _(
537 msg = _(
537 u'Pull request updated to "{source_commit_id}" with '
538 u'Pull request updated to "{source_commit_id}" with '
538 u'{count_added} added, {count_removed} removed commits.')
539 u'{count_added} added, {count_removed} removed commits.')
539 msg = msg.format(
540 msg = msg.format(
540 source_commit_id=pull_request.source_ref_parts.commit_id,
541 source_commit_id=pull_request.source_ref_parts.commit_id,
541 count_added=len(resp.changes.added),
542 count_added=len(resp.changes.added),
542 count_removed=len(resp.changes.removed))
543 count_removed=len(resp.changes.removed))
543 h.flash(msg, category='success')
544 h.flash(msg, category='success')
544
545
545 registry = get_current_registry()
546 registry = get_current_registry()
546 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
547 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
547 channelstream_config = rhodecode_plugins.get('channelstream', {})
548 channelstream_config = rhodecode_plugins.get('channelstream', {})
548 if channelstream_config.get('enabled'):
549 if channelstream_config.get('enabled'):
549 message = msg + (
550 message = msg + (
550 ' - <a onclick="window.location.reload()">'
551 ' - <a onclick="window.location.reload()">'
551 '<strong>{}</strong></a>'.format(_('Reload page')))
552 '<strong>{}</strong></a>'.format(_('Reload page')))
552 channel = '/repo${}$/pr/{}'.format(
553 channel = '/repo${}$/pr/{}'.format(
553 pull_request.target_repo.repo_name,
554 pull_request.target_repo.repo_name,
554 pull_request.pull_request_id
555 pull_request.pull_request_id
555 )
556 )
556 payload = {
557 payload = {
557 'type': 'message',
558 'type': 'message',
558 'user': 'system',
559 'user': 'system',
559 'exclude_users': [request.user.username],
560 'exclude_users': [request.user.username],
560 'channel': channel,
561 'channel': channel,
561 'message': {
562 'message': {
562 'message': message,
563 'message': message,
563 'level': 'success',
564 'level': 'success',
564 'topic': '/notifications'
565 'topic': '/notifications'
565 }
566 }
566 }
567 }
567 channelstream_request(
568 channelstream_request(
568 channelstream_config, [payload], '/message',
569 channelstream_config, [payload], '/message',
569 raise_exc=False)
570 raise_exc=False)
570 else:
571 else:
571 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
572 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
572 warning_reasons = [
573 warning_reasons = [
573 UpdateFailureReason.NO_CHANGE,
574 UpdateFailureReason.NO_CHANGE,
574 UpdateFailureReason.WRONG_REF_TPYE,
575 UpdateFailureReason.WRONG_REF_TPYE,
575 ]
576 ]
576 category = 'warning' if resp.reason in warning_reasons else 'error'
577 category = 'warning' if resp.reason in warning_reasons else 'error'
577 h.flash(msg, category=category)
578 h.flash(msg, category=category)
578
579
579 @auth.CSRFRequired()
580 @auth.CSRFRequired()
580 @LoginRequired()
581 @LoginRequired()
581 @NotAnonymous()
582 @NotAnonymous()
582 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
583 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
583 'repository.admin')
584 'repository.admin')
584 def merge(self, repo_name, pull_request_id):
585 def merge(self, repo_name, pull_request_id):
585 """
586 """
586 POST /{repo_name}/pull-request/{pull_request_id}
587 POST /{repo_name}/pull-request/{pull_request_id}
587
588
588 Merge will perform a server-side merge of the specified
589 Merge will perform a server-side merge of the specified
589 pull request, if the pull request is approved and mergeable.
590 pull request, if the pull request is approved and mergeable.
590 After succesfull merging, the pull request is automatically
591 After succesfull merging, the pull request is automatically
591 closed, with a relevant comment.
592 closed, with a relevant comment.
592 """
593 """
593 pull_request_id = safe_int(pull_request_id)
594 pull_request_id = safe_int(pull_request_id)
594 pull_request = PullRequest.get_or_404(pull_request_id)
595 pull_request = PullRequest.get_or_404(pull_request_id)
595 user = c.rhodecode_user
596 user = c.rhodecode_user
596
597
597 if self._meets_merge_pre_conditions(pull_request, user):
598 if self._meets_merge_pre_conditions(pull_request, user):
598 log.debug("Pre-conditions checked, trying to merge.")
599 log.debug("Pre-conditions checked, trying to merge.")
599 extras = vcs_operation_context(
600 extras = vcs_operation_context(
600 request.environ, repo_name=pull_request.target_repo.repo_name,
601 request.environ, repo_name=pull_request.target_repo.repo_name,
601 username=user.username, action='push',
602 username=user.username, action='push',
602 scm=pull_request.target_repo.repo_type)
603 scm=pull_request.target_repo.repo_type)
603 self._merge_pull_request(pull_request, user, extras)
604 self._merge_pull_request(pull_request, user, extras)
604
605
605 return redirect(url(
606 return redirect(url(
606 'pullrequest_show',
607 'pullrequest_show',
607 repo_name=pull_request.target_repo.repo_name,
608 repo_name=pull_request.target_repo.repo_name,
608 pull_request_id=pull_request.pull_request_id))
609 pull_request_id=pull_request.pull_request_id))
609
610
610 def _meets_merge_pre_conditions(self, pull_request, user):
611 def _meets_merge_pre_conditions(self, pull_request, user):
611 if not PullRequestModel().check_user_merge(pull_request, user):
612 if not PullRequestModel().check_user_merge(pull_request, user):
612 raise HTTPForbidden()
613 raise HTTPForbidden()
613
614
614 merge_status, msg = PullRequestModel().merge_status(pull_request)
615 merge_status, msg = PullRequestModel().merge_status(pull_request)
615 if not merge_status:
616 if not merge_status:
616 log.debug("Cannot merge, not mergeable.")
617 log.debug("Cannot merge, not mergeable.")
617 h.flash(msg, category='error')
618 h.flash(msg, category='error')
618 return False
619 return False
619
620
620 if (pull_request.calculated_review_status()
621 if (pull_request.calculated_review_status()
621 is not ChangesetStatus.STATUS_APPROVED):
622 is not ChangesetStatus.STATUS_APPROVED):
622 log.debug("Cannot merge, approval is pending.")
623 log.debug("Cannot merge, approval is pending.")
623 msg = _('Pull request reviewer approval is pending.')
624 msg = _('Pull request reviewer approval is pending.')
624 h.flash(msg, category='error')
625 h.flash(msg, category='error')
625 return False
626 return False
626 return True
627 return True
627
628
628 def _merge_pull_request(self, pull_request, user, extras):
629 def _merge_pull_request(self, pull_request, user, extras):
629 merge_resp = PullRequestModel().merge(
630 merge_resp = PullRequestModel().merge(
630 pull_request, user, extras=extras)
631 pull_request, user, extras=extras)
631
632
632 if merge_resp.executed:
633 if merge_resp.executed:
633 log.debug("The merge was successful, closing the pull request.")
634 log.debug("The merge was successful, closing the pull request.")
634 PullRequestModel().close_pull_request(
635 PullRequestModel().close_pull_request(
635 pull_request.pull_request_id, user)
636 pull_request.pull_request_id, user)
636 Session().commit()
637 Session().commit()
637 msg = _('Pull request was successfully merged and closed.')
638 msg = _('Pull request was successfully merged and closed.')
638 h.flash(msg, category='success')
639 h.flash(msg, category='success')
639 else:
640 else:
640 log.debug(
641 log.debug(
641 "The merge was not successful. Merge response: %s",
642 "The merge was not successful. Merge response: %s",
642 merge_resp)
643 merge_resp)
643 msg = PullRequestModel().merge_status_message(
644 msg = PullRequestModel().merge_status_message(
644 merge_resp.failure_reason)
645 merge_resp.failure_reason)
645 h.flash(msg, category='error')
646 h.flash(msg, category='error')
646
647
647 def _update_reviewers(self, pull_request_id, review_members):
648 def _update_reviewers(self, pull_request_id, review_members):
648 reviewers = [
649 reviewers = [
649 (int(r['user_id']), r['reasons']) for r in review_members]
650 (int(r['user_id']), r['reasons']) for r in review_members]
650 PullRequestModel().update_reviewers(pull_request_id, reviewers)
651 PullRequestModel().update_reviewers(pull_request_id, reviewers)
651 Session().commit()
652 Session().commit()
652
653
653 def _reject_close(self, pull_request):
654 def _reject_close(self, pull_request):
654 if pull_request.is_closed():
655 if pull_request.is_closed():
655 raise HTTPForbidden()
656 raise HTTPForbidden()
656
657
657 PullRequestModel().close_pull_request_with_comment(
658 PullRequestModel().close_pull_request_with_comment(
658 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
659 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
659 Session().commit()
660 Session().commit()
660
661
661 @LoginRequired()
662 @LoginRequired()
662 @NotAnonymous()
663 @NotAnonymous()
663 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
664 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
664 'repository.admin')
665 'repository.admin')
665 @auth.CSRFRequired()
666 @auth.CSRFRequired()
666 @jsonify
667 @jsonify
667 def delete(self, repo_name, pull_request_id):
668 def delete(self, repo_name, pull_request_id):
668 pull_request_id = safe_int(pull_request_id)
669 pull_request_id = safe_int(pull_request_id)
669 pull_request = PullRequest.get_or_404(pull_request_id)
670 pull_request = PullRequest.get_or_404(pull_request_id)
670 # only owner can delete it !
671 # only owner can delete it !
671 if pull_request.author.user_id == c.rhodecode_user.user_id:
672 if pull_request.author.user_id == c.rhodecode_user.user_id:
672 PullRequestModel().delete(pull_request)
673 PullRequestModel().delete(pull_request)
673 Session().commit()
674 Session().commit()
674 h.flash(_('Successfully deleted pull request'),
675 h.flash(_('Successfully deleted pull request'),
675 category='success')
676 category='success')
676 return redirect(url('my_account_pullrequests'))
677 return redirect(url('my_account_pullrequests'))
677 raise HTTPForbidden()
678 raise HTTPForbidden()
678
679
679 def _get_pr_version(self, pull_request_id, version=None):
680 def _get_pr_version(self, pull_request_id, version=None):
680 pull_request_id = safe_int(pull_request_id)
681 pull_request_id = safe_int(pull_request_id)
681 at_version = None
682 at_version = None
682 if version:
683 if version:
683 pull_request_ver = PullRequestVersion.get_or_404(version)
684 pull_request_ver = PullRequestVersion.get_or_404(version)
684 pull_request_obj = pull_request_ver
685 pull_request_obj = pull_request_ver
685 _org_pull_request_obj = pull_request_ver.pull_request
686 _org_pull_request_obj = pull_request_ver.pull_request
686 at_version = pull_request_ver.pull_request_version_id
687 at_version = pull_request_ver.pull_request_version_id
687 else:
688 else:
688 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
689 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
689
690
690 class PullRequestDisplay(object):
691 class PullRequestDisplay(object):
691 """
692 """
692 Special object wrapper for showing PullRequest data via Versions
693 Special object wrapper for showing PullRequest data via Versions
693 It mimics PR object as close as possible. This is read only object
694 It mimics PR object as close as possible. This is read only object
694 just for display
695 just for display
695 """
696 """
696 def __init__(self, attrs):
697 def __init__(self, attrs):
697 self.attrs = attrs
698 self.attrs = attrs
698 # internal have priority over the given ones via attrs
699 # internal have priority over the given ones via attrs
699 self.internal = ['versions']
700 self.internal = ['versions']
700
701
701 def __getattr__(self, item):
702 def __getattr__(self, item):
702 if item in self.internal:
703 if item in self.internal:
703 return getattr(self, item)
704 return getattr(self, item)
704 try:
705 try:
705 return self.attrs[item]
706 return self.attrs[item]
706 except KeyError:
707 except KeyError:
707 raise AttributeError(
708 raise AttributeError(
708 '%s object has no attribute %s' % (self, item))
709 '%s object has no attribute %s' % (self, item))
709
710
710 def versions(self):
711 def versions(self):
711 return pull_request_obj.versions.order_by(
712 return pull_request_obj.versions.order_by(
712 PullRequestVersion.pull_request_version_id).all()
713 PullRequestVersion.pull_request_version_id).all()
713
714
714 def is_closed(self):
715 def is_closed(self):
715 return pull_request_obj.is_closed()
716 return pull_request_obj.is_closed()
716
717
717 attrs = UnsafeAttributeDict(pull_request_obj.get_api_data())
718 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
718
719
719 attrs.author = UnsafeAttributeDict(
720 attrs.author = StrictAttributeDict(
720 pull_request_obj.author.get_api_data())
721 pull_request_obj.author.get_api_data())
721 if pull_request_obj.target_repo:
722 if pull_request_obj.target_repo:
722 attrs.target_repo = UnsafeAttributeDict(
723 attrs.target_repo = StrictAttributeDict(
723 pull_request_obj.target_repo.get_api_data())
724 pull_request_obj.target_repo.get_api_data())
724 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
725 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
725
726
726 if pull_request_obj.source_repo:
727 if pull_request_obj.source_repo:
727 attrs.source_repo = UnsafeAttributeDict(
728 attrs.source_repo = StrictAttributeDict(
728 pull_request_obj.source_repo.get_api_data())
729 pull_request_obj.source_repo.get_api_data())
729 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
730 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
730
731
731 attrs.source_ref_parts = pull_request_obj.source_ref_parts
732 attrs.source_ref_parts = pull_request_obj.source_ref_parts
732 attrs.target_ref_parts = pull_request_obj.target_ref_parts
733 attrs.target_ref_parts = pull_request_obj.target_ref_parts
733
734
734 attrs.shadow_merge_ref = _org_pull_request_obj.shadow_merge_ref
735 attrs.shadow_merge_ref = _org_pull_request_obj.shadow_merge_ref
735
736
736 pull_request_ver = PullRequestDisplay(attrs)
737 pull_request_display_obj = PullRequestDisplay(attrs)
737
738
738 return _org_pull_request_obj, pull_request_obj, \
739 return _org_pull_request_obj, pull_request_obj, \
739 pull_request_ver, at_version
740 pull_request_display_obj, at_version
740
741
741 @LoginRequired()
742 @LoginRequired()
742 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
743 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
743 'repository.admin')
744 'repository.admin')
744 def show(self, repo_name, pull_request_id):
745 def show(self, repo_name, pull_request_id):
745 pull_request_id = safe_int(pull_request_id)
746 pull_request_id = safe_int(pull_request_id)
747 version = request.GET.get('version')
746
748
747 version = request.GET.get('version')
749 (pull_request_latest,
748 pull_request_latest, \
750 pull_request_at_ver,
749 pull_request, \
751 pull_request_display_obj,
750 pull_request_ver, \
752 at_version) = self._get_pr_version(pull_request_id, version=version)
751 at_version = self._get_pr_version(pull_request_id, version=version)
752
753
753 c.template_context['pull_request_data']['pull_request_id'] = \
754 c.template_context['pull_request_data']['pull_request_id'] = \
754 pull_request_id
755 pull_request_id
755
756
756 # pull_requests repo_name we opened it against
757 # pull_requests repo_name we opened it against
757 # ie. target_repo must match
758 # ie. target_repo must match
758 if repo_name != pull_request.target_repo.repo_name:
759 if repo_name != pull_request_at_ver.target_repo.repo_name:
759 raise HTTPNotFound
760 raise HTTPNotFound
760
761
761 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
762 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
762 pull_request)
763 pull_request_at_ver)
763
764
765 pr_closed = pull_request_latest.is_closed()
764 if at_version:
766 if at_version:
765 c.allowed_to_change_status = False
767 c.allowed_to_change_status = False
768 c.allowed_to_update = False
769 c.allowed_to_merge = False
770 c.allowed_to_delete = False
771 c.allowed_to_comment = False
766 else:
772 else:
767 c.allowed_to_change_status = PullRequestModel(). \
773 c.allowed_to_change_status = PullRequestModel(). \
768 check_user_change_status(pull_request, c.rhodecode_user)
774 check_user_change_status(pull_request_at_ver, 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(
775 c.allowed_to_update = PullRequestModel().check_user_update(
774 pull_request, c.rhodecode_user) and not pull_request.is_closed()
776 pull_request_latest, c.rhodecode_user) and not pr_closed
775
776 if at_version:
777 c.allowed_to_merge = False
778 else:
779 c.allowed_to_merge = PullRequestModel().check_user_merge(
777 c.allowed_to_merge = PullRequestModel().check_user_merge(
780 pull_request, c.rhodecode_user) and not pull_request.is_closed()
778 pull_request_latest, c.rhodecode_user) and not pr_closed
781
782 if at_version:
783 c.allowed_to_delete = False
784 else:
785 c.allowed_to_delete = PullRequestModel().check_user_delete(
779 c.allowed_to_delete = PullRequestModel().check_user_delete(
786 pull_request, c.rhodecode_user) and not pull_request.is_closed()
780 pull_request_latest, c.rhodecode_user) and not pr_closed
787
781 c.allowed_to_comment = not pr_closed
788 if at_version:
789 c.allowed_to_comment = False
790 else:
791 c.allowed_to_comment = not pull_request.is_closed()
792
782
793 cc_model = ChangesetCommentsModel()
783 cc_model = ChangesetCommentsModel()
794
784
795 c.pull_request_reviewers = pull_request.reviewers_statuses()
785 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
796
786 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
797 c.pull_request_review_status = pull_request.calculated_review_status()
798 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
787 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
799 pull_request)
788 pull_request_at_ver)
800 c.approval_msg = None
789 c.approval_msg = None
801 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
790 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
802 c.approval_msg = _('Reviewer approval is pending.')
791 c.approval_msg = _('Reviewer approval is pending.')
803 c.pr_merge_status = False
792 c.pr_merge_status = False
804 # load compare data into template context
805 enable_comments = not pull_request.is_closed()
806
793
807 # inline comments
794 # inline comments
808 c.inline_comments = cc_model.get_inline_comments(
795 c.inline_comments = cc_model.get_inline_comments(
809 c.rhodecode_db_repo.repo_id,
796 c.rhodecode_db_repo.repo_id,
810 pull_request=pull_request_id)
797 pull_request=pull_request_id)
811
798
812 c.inline_cnt = cc_model.get_inline_comments_count(
799 c.inline_cnt = cc_model.get_inline_comments_count(
813 c.inline_comments, version=at_version)
800 c.inline_comments, version=at_version)
814
801
802 # load compare data into template context
803 enable_comments = not pr_closed
815 self._load_compare_data(
804 self._load_compare_data(
816 pull_request, c.inline_comments, enable_comments=enable_comments)
805 pull_request_at_ver,
806 c.inline_comments, enable_comments=enable_comments)
817
807
818 # outdated comments
808 # outdated comments
819 c.outdated_comments = {}
809 c.outdated_comments = {}
820 c.outdated_cnt = 0
810 c.outdated_cnt = 0
821
811
822 if ChangesetCommentsModel.use_outdated_comments(pull_request):
812 if ChangesetCommentsModel.use_outdated_comments(pull_request_latest):
823 c.outdated_comments = cc_model.get_outdated_comments(
813 c.outdated_comments = cc_model.get_outdated_comments(
824 c.rhodecode_db_repo.repo_id,
814 c.rhodecode_db_repo.repo_id,
825 pull_request=pull_request)
815 pull_request=pull_request_at_ver)
826
816
827 # Count outdated comments and check for deleted files
817 # Count outdated comments and check for deleted files
828 for file_name, lines in c.outdated_comments.iteritems():
818 for file_name, lines in c.outdated_comments.iteritems():
829 for comments in lines.values():
819 for comments in lines.values():
830 comments = [comm for comm in comments
820 comments = [comm for comm in comments
831 if comm.outdated_at_version(at_version)]
821 if comm.outdated_at_version(at_version)]
832 c.outdated_cnt += len(comments)
822 c.outdated_cnt += len(comments)
833 if file_name not in c.included_files:
823 if file_name not in c.included_files:
834 c.deleted_files.append(file_name)
824 c.deleted_files.append(file_name)
835
825
836 # this is a hack to properly display links, when creating PR, the
826 # this is a hack to properly display links, when creating PR, the
837 # compare view and others uses different notation, and
827 # compare view and others uses different notation, and
838 # compare_commits.html renders links based on the target_repo.
828 # compare_commits.html renders links based on the target_repo.
839 # We need to swap that here to generate it properly on the html side
829 # We need to swap that here to generate it properly on the html side
840 c.target_repo = c.source_repo
830 c.target_repo = c.source_repo
841
831
842 # comments
832 # comments
843 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
833 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
844 pull_request=pull_request_id)
834 pull_request=pull_request_id)
845
835
846 if c.allowed_to_update:
836 if c.allowed_to_update:
847 force_close = ('forced_closed', _('Close Pull Request'))
837 force_close = ('forced_closed', _('Close Pull Request'))
848 statuses = ChangesetStatus.STATUSES + [force_close]
838 statuses = ChangesetStatus.STATUSES + [force_close]
849 else:
839 else:
850 statuses = ChangesetStatus.STATUSES
840 statuses = ChangesetStatus.STATUSES
851 c.commit_statuses = statuses
841 c.commit_statuses = statuses
852
842
853 c.ancestor = None # TODO: add ancestor here
843 c.ancestor = None # TODO: add ancestor here
854 c.pull_request = pull_request_ver
844 c.pull_request = pull_request_display_obj
855 c.pull_request_latest = pull_request_latest
845 c.pull_request_latest = pull_request_latest
856 c.at_version = at_version
846 c.at_version = at_version
857
847
858 return render('/pullrequests/pullrequest_show.html')
848 return render('/pullrequests/pullrequest_show.html')
859
849
860 @LoginRequired()
850 @LoginRequired()
861 @NotAnonymous()
851 @NotAnonymous()
862 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
852 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
863 'repository.admin')
853 'repository.admin')
864 @auth.CSRFRequired()
854 @auth.CSRFRequired()
865 @jsonify
855 @jsonify
866 def comment(self, repo_name, pull_request_id):
856 def comment(self, repo_name, pull_request_id):
867 pull_request_id = safe_int(pull_request_id)
857 pull_request_id = safe_int(pull_request_id)
868 pull_request = PullRequest.get_or_404(pull_request_id)
858 pull_request = PullRequest.get_or_404(pull_request_id)
869 if pull_request.is_closed():
859 if pull_request.is_closed():
870 raise HTTPForbidden()
860 raise HTTPForbidden()
871
861
872 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
862 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
873 # as a changeset status, still we want to send it in one value.
863 # as a changeset status, still we want to send it in one value.
874 status = request.POST.get('changeset_status', None)
864 status = request.POST.get('changeset_status', None)
875 text = request.POST.get('text')
865 text = request.POST.get('text')
876 if status and '_closed' in status:
866 if status and '_closed' in status:
877 close_pr = True
867 close_pr = True
878 status = status.replace('_closed', '')
868 status = status.replace('_closed', '')
879 else:
869 else:
880 close_pr = False
870 close_pr = False
881
871
882 forced = (status == 'forced')
872 forced = (status == 'forced')
883 if forced:
873 if forced:
884 status = 'rejected'
874 status = 'rejected'
885
875
886 allowed_to_change_status = PullRequestModel().check_user_change_status(
876 allowed_to_change_status = PullRequestModel().check_user_change_status(
887 pull_request, c.rhodecode_user)
877 pull_request, c.rhodecode_user)
888
878
889 if status and allowed_to_change_status:
879 if status and allowed_to_change_status:
890 message = (_('Status change %(transition_icon)s %(status)s')
880 message = (_('Status change %(transition_icon)s %(status)s')
891 % {'transition_icon': '>',
881 % {'transition_icon': '>',
892 'status': ChangesetStatus.get_status_lbl(status)})
882 'status': ChangesetStatus.get_status_lbl(status)})
893 if close_pr:
883 if close_pr:
894 message = _('Closing with') + ' ' + message
884 message = _('Closing with') + ' ' + message
895 text = text or message
885 text = text or message
896 comm = ChangesetCommentsModel().create(
886 comm = ChangesetCommentsModel().create(
897 text=text,
887 text=text,
898 repo=c.rhodecode_db_repo.repo_id,
888 repo=c.rhodecode_db_repo.repo_id,
899 user=c.rhodecode_user.user_id,
889 user=c.rhodecode_user.user_id,
900 pull_request=pull_request_id,
890 pull_request=pull_request_id,
901 f_path=request.POST.get('f_path'),
891 f_path=request.POST.get('f_path'),
902 line_no=request.POST.get('line'),
892 line_no=request.POST.get('line'),
903 status_change=(ChangesetStatus.get_status_lbl(status)
893 status_change=(ChangesetStatus.get_status_lbl(status)
904 if status and allowed_to_change_status else None),
894 if status and allowed_to_change_status else None),
905 status_change_type=(status
895 status_change_type=(status
906 if status and allowed_to_change_status else None),
896 if status and allowed_to_change_status else None),
907 closing_pr=close_pr
897 closing_pr=close_pr
908 )
898 )
909
899
910 if allowed_to_change_status:
900 if allowed_to_change_status:
911 old_calculated_status = pull_request.calculated_review_status()
901 old_calculated_status = pull_request.calculated_review_status()
912 # get status if set !
902 # get status if set !
913 if status:
903 if status:
914 ChangesetStatusModel().set_status(
904 ChangesetStatusModel().set_status(
915 c.rhodecode_db_repo.repo_id,
905 c.rhodecode_db_repo.repo_id,
916 status,
906 status,
917 c.rhodecode_user.user_id,
907 c.rhodecode_user.user_id,
918 comm,
908 comm,
919 pull_request=pull_request_id
909 pull_request=pull_request_id
920 )
910 )
921
911
922 Session().flush()
912 Session().flush()
923 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
913 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
924 # we now calculate the status of pull request, and based on that
914 # we now calculate the status of pull request, and based on that
925 # calculation we set the commits status
915 # calculation we set the commits status
926 calculated_status = pull_request.calculated_review_status()
916 calculated_status = pull_request.calculated_review_status()
927 if old_calculated_status != calculated_status:
917 if old_calculated_status != calculated_status:
928 PullRequestModel()._trigger_pull_request_hook(
918 PullRequestModel()._trigger_pull_request_hook(
929 pull_request, c.rhodecode_user, 'review_status_change')
919 pull_request, c.rhodecode_user, 'review_status_change')
930
920
931 calculated_status_lbl = ChangesetStatus.get_status_lbl(
921 calculated_status_lbl = ChangesetStatus.get_status_lbl(
932 calculated_status)
922 calculated_status)
933
923
934 if close_pr:
924 if close_pr:
935 status_completed = (
925 status_completed = (
936 calculated_status in [ChangesetStatus.STATUS_APPROVED,
926 calculated_status in [ChangesetStatus.STATUS_APPROVED,
937 ChangesetStatus.STATUS_REJECTED])
927 ChangesetStatus.STATUS_REJECTED])
938 if forced or status_completed:
928 if forced or status_completed:
939 PullRequestModel().close_pull_request(
929 PullRequestModel().close_pull_request(
940 pull_request_id, c.rhodecode_user)
930 pull_request_id, c.rhodecode_user)
941 else:
931 else:
942 h.flash(_('Closing pull request on other statuses than '
932 h.flash(_('Closing pull request on other statuses than '
943 'rejected or approved is forbidden. '
933 'rejected or approved is forbidden. '
944 'Calculated status from all reviewers '
934 'Calculated status from all reviewers '
945 'is currently: %s') % calculated_status_lbl,
935 'is currently: %s') % calculated_status_lbl,
946 category='warning')
936 category='warning')
947
937
948 Session().commit()
938 Session().commit()
949
939
950 if not request.is_xhr:
940 if not request.is_xhr:
951 return redirect(h.url('pullrequest_show', repo_name=repo_name,
941 return redirect(h.url('pullrequest_show', repo_name=repo_name,
952 pull_request_id=pull_request_id))
942 pull_request_id=pull_request_id))
953
943
954 data = {
944 data = {
955 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
945 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
956 }
946 }
957 if comm:
947 if comm:
958 c.co = comm
948 c.co = comm
959 data.update(comm.get_dict())
949 data.update(comm.get_dict())
960 data.update({'rendered_text':
950 data.update({'rendered_text':
961 render('changeset/changeset_comment_block.html')})
951 render('changeset/changeset_comment_block.html')})
962
952
963 return data
953 return data
964
954
965 @LoginRequired()
955 @LoginRequired()
966 @NotAnonymous()
956 @NotAnonymous()
967 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
957 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
968 'repository.admin')
958 'repository.admin')
969 @auth.CSRFRequired()
959 @auth.CSRFRequired()
970 @jsonify
960 @jsonify
971 def delete_comment(self, repo_name, comment_id):
961 def delete_comment(self, repo_name, comment_id):
972 return self._delete_comment(comment_id)
962 return self._delete_comment(comment_id)
973
963
974 def _delete_comment(self, comment_id):
964 def _delete_comment(self, comment_id):
975 comment_id = safe_int(comment_id)
965 comment_id = safe_int(comment_id)
976 co = ChangesetComment.get_or_404(comment_id)
966 co = ChangesetComment.get_or_404(comment_id)
977 if co.pull_request.is_closed():
967 if co.pull_request.is_closed():
978 # don't allow deleting comments on closed pull request
968 # don't allow deleting comments on closed pull request
979 raise HTTPForbidden()
969 raise HTTPForbidden()
980
970
981 is_owner = co.author.user_id == c.rhodecode_user.user_id
971 is_owner = co.author.user_id == c.rhodecode_user.user_id
982 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
972 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
983 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
973 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
984 old_calculated_status = co.pull_request.calculated_review_status()
974 old_calculated_status = co.pull_request.calculated_review_status()
985 ChangesetCommentsModel().delete(comment=co)
975 ChangesetCommentsModel().delete(comment=co)
986 Session().commit()
976 Session().commit()
987 calculated_status = co.pull_request.calculated_review_status()
977 calculated_status = co.pull_request.calculated_review_status()
988 if old_calculated_status != calculated_status:
978 if old_calculated_status != calculated_status:
989 PullRequestModel()._trigger_pull_request_hook(
979 PullRequestModel()._trigger_pull_request_hook(
990 co.pull_request, c.rhodecode_user, 'review_status_change')
980 co.pull_request, c.rhodecode_user, 'review_status_change')
991 return True
981 return True
992 else:
982 else:
993 raise HTTPForbidden()
983 raise HTTPForbidden()
@@ -1,665 +1,668 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 import logging
21 import logging
22 import difflib
22 import difflib
23 from itertools import groupby
23 from itertools import groupby
24
24
25 from pygments import lex
25 from pygments import lex
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 from rhodecode.lib.helpers import (
27 from rhodecode.lib.helpers import (
28 get_lexer_for_filenode, get_lexer_safe, html_escape)
28 get_lexer_for_filenode, get_lexer_safe, html_escape)
29 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.lib.utils2 import AttributeDict
30 from rhodecode.lib.vcs.nodes import FileNode
30 from rhodecode.lib.vcs.nodes import FileNode
31 from rhodecode.lib.diff_match_patch import diff_match_patch
31 from rhodecode.lib.diff_match_patch import diff_match_patch
32 from rhodecode.lib.diffs import LimitedDiffContainer
32 from rhodecode.lib.diffs import LimitedDiffContainer
33 from pygments.lexers import get_lexer_by_name
33 from pygments.lexers import get_lexer_by_name
34
34
35 plain_text_lexer = get_lexer_by_name(
35 plain_text_lexer = get_lexer_by_name(
36 'text', stripall=False, stripnl=False, ensurenl=False)
36 'text', stripall=False, stripnl=False, ensurenl=False)
37
37
38
38
39 log = logging.getLogger()
39 log = logging.getLogger()
40
40
41
41
42 def filenode_as_lines_tokens(filenode, lexer=None):
42 def filenode_as_lines_tokens(filenode, lexer=None):
43 lexer = lexer or get_lexer_for_filenode(filenode)
43 lexer = lexer or get_lexer_for_filenode(filenode)
44 log.debug('Generating file node pygment tokens for %s, %s', lexer, filenode)
44 log.debug('Generating file node pygment tokens for %s, %s', lexer, filenode)
45 tokens = tokenize_string(filenode.content, lexer)
45 tokens = tokenize_string(filenode.content, lexer)
46 lines = split_token_stream(tokens, split_string='\n')
46 lines = split_token_stream(tokens, split_string='\n')
47 rv = list(lines)
47 rv = list(lines)
48 return rv
48 return rv
49
49
50
50
51 def tokenize_string(content, lexer):
51 def tokenize_string(content, lexer):
52 """
52 """
53 Use pygments to tokenize some content based on a lexer
53 Use pygments to tokenize some content based on a lexer
54 ensuring all original new lines and whitespace is preserved
54 ensuring all original new lines and whitespace is preserved
55 """
55 """
56
56
57 lexer.stripall = False
57 lexer.stripall = False
58 lexer.stripnl = False
58 lexer.stripnl = False
59 lexer.ensurenl = False
59 lexer.ensurenl = False
60 for token_type, token_text in lex(content, lexer):
60 for token_type, token_text in lex(content, lexer):
61 yield pygment_token_class(token_type), token_text
61 yield pygment_token_class(token_type), token_text
62
62
63
63
64 def split_token_stream(tokens, split_string=u'\n'):
64 def split_token_stream(tokens, split_string=u'\n'):
65 """
65 """
66 Take a list of (TokenType, text) tuples and split them by a string
66 Take a list of (TokenType, text) tuples and split them by a string
67
67
68 >>> split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
68 >>> split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
69 [(TEXT, 'some'), (TEXT, 'text'),
69 [(TEXT, 'some'), (TEXT, 'text'),
70 (TEXT, 'more'), (TEXT, 'text')]
70 (TEXT, 'more'), (TEXT, 'text')]
71 """
71 """
72
72
73 buffer = []
73 buffer = []
74 for token_class, token_text in tokens:
74 for token_class, token_text in tokens:
75 parts = token_text.split(split_string)
75 parts = token_text.split(split_string)
76 for part in parts[:-1]:
76 for part in parts[:-1]:
77 buffer.append((token_class, part))
77 buffer.append((token_class, part))
78 yield buffer
78 yield buffer
79 buffer = []
79 buffer = []
80
80
81 buffer.append((token_class, parts[-1]))
81 buffer.append((token_class, parts[-1]))
82
82
83 if buffer:
83 if buffer:
84 yield buffer
84 yield buffer
85
85
86
86
87 def filenode_as_annotated_lines_tokens(filenode):
87 def filenode_as_annotated_lines_tokens(filenode):
88 """
88 """
89 Take a file node and return a list of annotations => lines, if no annotation
89 Take a file node and return a list of annotations => lines, if no annotation
90 is found, it will be None.
90 is found, it will be None.
91
91
92 eg:
92 eg:
93
93
94 [
94 [
95 (annotation1, [
95 (annotation1, [
96 (1, line1_tokens_list),
96 (1, line1_tokens_list),
97 (2, line2_tokens_list),
97 (2, line2_tokens_list),
98 ]),
98 ]),
99 (annotation2, [
99 (annotation2, [
100 (3, line1_tokens_list),
100 (3, line1_tokens_list),
101 ]),
101 ]),
102 (None, [
102 (None, [
103 (4, line1_tokens_list),
103 (4, line1_tokens_list),
104 ]),
104 ]),
105 (annotation1, [
105 (annotation1, [
106 (5, line1_tokens_list),
106 (5, line1_tokens_list),
107 (6, line2_tokens_list),
107 (6, line2_tokens_list),
108 ])
108 ])
109 ]
109 ]
110 """
110 """
111
111
112 commit_cache = {} # cache commit_getter lookups
112 commit_cache = {} # cache commit_getter lookups
113
113
114 def _get_annotation(commit_id, commit_getter):
114 def _get_annotation(commit_id, commit_getter):
115 if commit_id not in commit_cache:
115 if commit_id not in commit_cache:
116 commit_cache[commit_id] = commit_getter()
116 commit_cache[commit_id] = commit_getter()
117 return commit_cache[commit_id]
117 return commit_cache[commit_id]
118
118
119 annotation_lookup = {
119 annotation_lookup = {
120 line_no: _get_annotation(commit_id, commit_getter)
120 line_no: _get_annotation(commit_id, commit_getter)
121 for line_no, commit_id, commit_getter, line_content
121 for line_no, commit_id, commit_getter, line_content
122 in filenode.annotate
122 in filenode.annotate
123 }
123 }
124
124
125 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
125 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
126 for line_no, tokens
126 for line_no, tokens
127 in enumerate(filenode_as_lines_tokens(filenode), 1))
127 in enumerate(filenode_as_lines_tokens(filenode), 1))
128
128
129 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
129 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
130
130
131 for annotation, group in grouped_annotations_lines:
131 for annotation, group in grouped_annotations_lines:
132 yield (
132 yield (
133 annotation, [(line_no, tokens)
133 annotation, [(line_no, tokens)
134 for (_, line_no, tokens) in group]
134 for (_, line_no, tokens) in group]
135 )
135 )
136
136
137
137
138 def render_tokenstream(tokenstream):
138 def render_tokenstream(tokenstream):
139 result = []
139 result = []
140 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
140 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
141
141
142 if token_class:
142 if token_class:
143 result.append(u'<span class="%s">' % token_class)
143 result.append(u'<span class="%s">' % token_class)
144 else:
144 else:
145 result.append(u'<span>')
145 result.append(u'<span>')
146
146
147 for op_tag, token_text in token_ops_texts:
147 for op_tag, token_text in token_ops_texts:
148
148
149 if op_tag:
149 if op_tag:
150 result.append(u'<%s>' % op_tag)
150 result.append(u'<%s>' % op_tag)
151
151
152 escaped_text = html_escape(token_text)
152 escaped_text = html_escape(token_text)
153
153
154 # TODO: dan: investigate showing hidden characters like space/nl/tab
154 # TODO: dan: investigate showing hidden characters like space/nl/tab
155 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
155 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
156 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
156 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
157 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
157 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
158
158
159 result.append(escaped_text)
159 result.append(escaped_text)
160
160
161 if op_tag:
161 if op_tag:
162 result.append(u'</%s>' % op_tag)
162 result.append(u'</%s>' % op_tag)
163
163
164 result.append(u'</span>')
164 result.append(u'</span>')
165
165
166 html = ''.join(result)
166 html = ''.join(result)
167 return html
167 return html
168
168
169
169
170 def rollup_tokenstream(tokenstream):
170 def rollup_tokenstream(tokenstream):
171 """
171 """
172 Group a token stream of the format:
172 Group a token stream of the format:
173
173
174 ('class', 'op', 'text')
174 ('class', 'op', 'text')
175 or
175 or
176 ('class', 'text')
176 ('class', 'text')
177
177
178 into
178 into
179
179
180 [('class1',
180 [('class1',
181 [('op1', 'text'),
181 [('op1', 'text'),
182 ('op2', 'text')]),
182 ('op2', 'text')]),
183 ('class2',
183 ('class2',
184 [('op3', 'text')])]
184 [('op3', 'text')])]
185
185
186 This is used to get the minimal tags necessary when
186 This is used to get the minimal tags necessary when
187 rendering to html eg for a token stream ie.
187 rendering to html eg for a token stream ie.
188
188
189 <span class="A"><ins>he</ins>llo</span>
189 <span class="A"><ins>he</ins>llo</span>
190 vs
190 vs
191 <span class="A"><ins>he</ins></span><span class="A">llo</span>
191 <span class="A"><ins>he</ins></span><span class="A">llo</span>
192
192
193 If a 2 tuple is passed in, the output op will be an empty string.
193 If a 2 tuple is passed in, the output op will be an empty string.
194
194
195 eg:
195 eg:
196
196
197 >>> rollup_tokenstream([('classA', '', 'h'),
197 >>> rollup_tokenstream([('classA', '', 'h'),
198 ('classA', 'del', 'ell'),
198 ('classA', 'del', 'ell'),
199 ('classA', '', 'o'),
199 ('classA', '', 'o'),
200 ('classB', '', ' '),
200 ('classB', '', ' '),
201 ('classA', '', 'the'),
201 ('classA', '', 'the'),
202 ('classA', '', 're'),
202 ('classA', '', 're'),
203 ])
203 ])
204
204
205 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
205 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
206 ('classB', [('', ' ')],
206 ('classB', [('', ' ')],
207 ('classA', [('', 'there')]]
207 ('classA', [('', 'there')]]
208
208
209 """
209 """
210 if tokenstream and len(tokenstream[0]) == 2:
210 if tokenstream and len(tokenstream[0]) == 2:
211 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
211 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
212
212
213 result = []
213 result = []
214 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
214 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
215 ops = []
215 ops = []
216 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
216 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
217 text_buffer = []
217 text_buffer = []
218 for t_class, t_op, t_text in token_text_list:
218 for t_class, t_op, t_text in token_text_list:
219 text_buffer.append(t_text)
219 text_buffer.append(t_text)
220 ops.append((token_op, ''.join(text_buffer)))
220 ops.append((token_op, ''.join(text_buffer)))
221 result.append((token_class, ops))
221 result.append((token_class, ops))
222 return result
222 return result
223
223
224
224
225 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
225 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
226 """
226 """
227 Converts a list of (token_class, token_text) tuples to a list of
227 Converts a list of (token_class, token_text) tuples to a list of
228 (token_class, token_op, token_text) tuples where token_op is one of
228 (token_class, token_op, token_text) tuples where token_op is one of
229 ('ins', 'del', '')
229 ('ins', 'del', '')
230
230
231 :param old_tokens: list of (token_class, token_text) tuples of old line
231 :param old_tokens: list of (token_class, token_text) tuples of old line
232 :param new_tokens: list of (token_class, token_text) tuples of new line
232 :param new_tokens: list of (token_class, token_text) tuples of new line
233 :param use_diff_match_patch: boolean, will use google's diff match patch
233 :param use_diff_match_patch: boolean, will use google's diff match patch
234 library which has options to 'smooth' out the character by character
234 library which has options to 'smooth' out the character by character
235 differences making nicer ins/del blocks
235 differences making nicer ins/del blocks
236 """
236 """
237
237
238 old_tokens_result = []
238 old_tokens_result = []
239 new_tokens_result = []
239 new_tokens_result = []
240
240
241 similarity = difflib.SequenceMatcher(None,
241 similarity = difflib.SequenceMatcher(None,
242 ''.join(token_text for token_class, token_text in old_tokens),
242 ''.join(token_text for token_class, token_text in old_tokens),
243 ''.join(token_text for token_class, token_text in new_tokens)
243 ''.join(token_text for token_class, token_text in new_tokens)
244 ).ratio()
244 ).ratio()
245
245
246 if similarity < 0.6: # return, the blocks are too different
246 if similarity < 0.6: # return, the blocks are too different
247 for token_class, token_text in old_tokens:
247 for token_class, token_text in old_tokens:
248 old_tokens_result.append((token_class, '', token_text))
248 old_tokens_result.append((token_class, '', token_text))
249 for token_class, token_text in new_tokens:
249 for token_class, token_text in new_tokens:
250 new_tokens_result.append((token_class, '', token_text))
250 new_tokens_result.append((token_class, '', token_text))
251 return old_tokens_result, new_tokens_result, similarity
251 return old_tokens_result, new_tokens_result, similarity
252
252
253 token_sequence_matcher = difflib.SequenceMatcher(None,
253 token_sequence_matcher = difflib.SequenceMatcher(None,
254 [x[1] for x in old_tokens],
254 [x[1] for x in old_tokens],
255 [x[1] for x in new_tokens])
255 [x[1] for x in new_tokens])
256
256
257 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
257 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
258 # check the differences by token block types first to give a more
258 # check the differences by token block types first to give a more
259 # nicer "block" level replacement vs character diffs
259 # nicer "block" level replacement vs character diffs
260
260
261 if tag == 'equal':
261 if tag == 'equal':
262 for token_class, token_text in old_tokens[o1:o2]:
262 for token_class, token_text in old_tokens[o1:o2]:
263 old_tokens_result.append((token_class, '', token_text))
263 old_tokens_result.append((token_class, '', token_text))
264 for token_class, token_text in new_tokens[n1:n2]:
264 for token_class, token_text in new_tokens[n1:n2]:
265 new_tokens_result.append((token_class, '', token_text))
265 new_tokens_result.append((token_class, '', token_text))
266 elif tag == 'delete':
266 elif tag == 'delete':
267 for token_class, token_text in old_tokens[o1:o2]:
267 for token_class, token_text in old_tokens[o1:o2]:
268 old_tokens_result.append((token_class, 'del', token_text))
268 old_tokens_result.append((token_class, 'del', token_text))
269 elif tag == 'insert':
269 elif tag == 'insert':
270 for token_class, token_text in new_tokens[n1:n2]:
270 for token_class, token_text in new_tokens[n1:n2]:
271 new_tokens_result.append((token_class, 'ins', token_text))
271 new_tokens_result.append((token_class, 'ins', token_text))
272 elif tag == 'replace':
272 elif tag == 'replace':
273 # if same type token blocks must be replaced, do a diff on the
273 # if same type token blocks must be replaced, do a diff on the
274 # characters in the token blocks to show individual changes
274 # characters in the token blocks to show individual changes
275
275
276 old_char_tokens = []
276 old_char_tokens = []
277 new_char_tokens = []
277 new_char_tokens = []
278 for token_class, token_text in old_tokens[o1:o2]:
278 for token_class, token_text in old_tokens[o1:o2]:
279 for char in token_text:
279 for char in token_text:
280 old_char_tokens.append((token_class, char))
280 old_char_tokens.append((token_class, char))
281
281
282 for token_class, token_text in new_tokens[n1:n2]:
282 for token_class, token_text in new_tokens[n1:n2]:
283 for char in token_text:
283 for char in token_text:
284 new_char_tokens.append((token_class, char))
284 new_char_tokens.append((token_class, char))
285
285
286 old_string = ''.join([token_text for
286 old_string = ''.join([token_text for
287 token_class, token_text in old_char_tokens])
287 token_class, token_text in old_char_tokens])
288 new_string = ''.join([token_text for
288 new_string = ''.join([token_text for
289 token_class, token_text in new_char_tokens])
289 token_class, token_text in new_char_tokens])
290
290
291 char_sequence = difflib.SequenceMatcher(
291 char_sequence = difflib.SequenceMatcher(
292 None, old_string, new_string)
292 None, old_string, new_string)
293 copcodes = char_sequence.get_opcodes()
293 copcodes = char_sequence.get_opcodes()
294 obuffer, nbuffer = [], []
294 obuffer, nbuffer = [], []
295
295
296 if use_diff_match_patch:
296 if use_diff_match_patch:
297 dmp = diff_match_patch()
297 dmp = diff_match_patch()
298 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
298 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
299 reps = dmp.diff_main(old_string, new_string)
299 reps = dmp.diff_main(old_string, new_string)
300 dmp.diff_cleanupEfficiency(reps)
300 dmp.diff_cleanupEfficiency(reps)
301
301
302 a, b = 0, 0
302 a, b = 0, 0
303 for op, rep in reps:
303 for op, rep in reps:
304 l = len(rep)
304 l = len(rep)
305 if op == 0:
305 if op == 0:
306 for i, c in enumerate(rep):
306 for i, c in enumerate(rep):
307 obuffer.append((old_char_tokens[a+i][0], '', c))
307 obuffer.append((old_char_tokens[a+i][0], '', c))
308 nbuffer.append((new_char_tokens[b+i][0], '', c))
308 nbuffer.append((new_char_tokens[b+i][0], '', c))
309 a += l
309 a += l
310 b += l
310 b += l
311 elif op == -1:
311 elif op == -1:
312 for i, c in enumerate(rep):
312 for i, c in enumerate(rep):
313 obuffer.append((old_char_tokens[a+i][0], 'del', c))
313 obuffer.append((old_char_tokens[a+i][0], 'del', c))
314 a += l
314 a += l
315 elif op == 1:
315 elif op == 1:
316 for i, c in enumerate(rep):
316 for i, c in enumerate(rep):
317 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
317 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
318 b += l
318 b += l
319 else:
319 else:
320 for ctag, co1, co2, cn1, cn2 in copcodes:
320 for ctag, co1, co2, cn1, cn2 in copcodes:
321 if ctag == 'equal':
321 if ctag == 'equal':
322 for token_class, token_text in old_char_tokens[co1:co2]:
322 for token_class, token_text in old_char_tokens[co1:co2]:
323 obuffer.append((token_class, '', token_text))
323 obuffer.append((token_class, '', token_text))
324 for token_class, token_text in new_char_tokens[cn1:cn2]:
324 for token_class, token_text in new_char_tokens[cn1:cn2]:
325 nbuffer.append((token_class, '', token_text))
325 nbuffer.append((token_class, '', token_text))
326 elif ctag == 'delete':
326 elif ctag == 'delete':
327 for token_class, token_text in old_char_tokens[co1:co2]:
327 for token_class, token_text in old_char_tokens[co1:co2]:
328 obuffer.append((token_class, 'del', token_text))
328 obuffer.append((token_class, 'del', token_text))
329 elif ctag == 'insert':
329 elif ctag == 'insert':
330 for token_class, token_text in new_char_tokens[cn1:cn2]:
330 for token_class, token_text in new_char_tokens[cn1:cn2]:
331 nbuffer.append((token_class, 'ins', token_text))
331 nbuffer.append((token_class, 'ins', token_text))
332 elif ctag == 'replace':
332 elif ctag == 'replace':
333 for token_class, token_text in old_char_tokens[co1:co2]:
333 for token_class, token_text in old_char_tokens[co1:co2]:
334 obuffer.append((token_class, 'del', token_text))
334 obuffer.append((token_class, 'del', token_text))
335 for token_class, token_text in new_char_tokens[cn1:cn2]:
335 for token_class, token_text in new_char_tokens[cn1:cn2]:
336 nbuffer.append((token_class, 'ins', token_text))
336 nbuffer.append((token_class, 'ins', token_text))
337
337
338 old_tokens_result.extend(obuffer)
338 old_tokens_result.extend(obuffer)
339 new_tokens_result.extend(nbuffer)
339 new_tokens_result.extend(nbuffer)
340
340
341 return old_tokens_result, new_tokens_result, similarity
341 return old_tokens_result, new_tokens_result, similarity
342
342
343
343
344 class DiffSet(object):
344 class DiffSet(object):
345 """
345 """
346 An object for parsing the diff result from diffs.DiffProcessor and
346 An object for parsing the diff result from diffs.DiffProcessor and
347 adding highlighting, side by side/unified renderings and line diffs
347 adding highlighting, side by side/unified renderings and line diffs
348 """
348 """
349
349
350 HL_REAL = 'REAL' # highlights using original file, slow
350 HL_REAL = 'REAL' # highlights using original file, slow
351 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
351 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
352 # in the case of multiline code
352 # in the case of multiline code
353 HL_NONE = 'NONE' # no highlighting, fastest
353 HL_NONE = 'NONE' # no highlighting, fastest
354
354
355 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
355 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
356 source_repo_name=None,
356 source_node_getter=lambda filename: None,
357 source_node_getter=lambda filename: None,
357 target_node_getter=lambda filename: None,
358 target_node_getter=lambda filename: None,
358 source_nodes=None, target_nodes=None,
359 source_nodes=None, target_nodes=None,
359 max_file_size_limit=150 * 1024, # files over this size will
360 max_file_size_limit=150 * 1024, # files over this size will
360 # use fast highlighting
361 # use fast highlighting
361 comments=None,
362 comments=None,
362 ):
363 ):
363
364
364 self.highlight_mode = highlight_mode
365 self.highlight_mode = highlight_mode
365 self.highlighted_filenodes = {}
366 self.highlighted_filenodes = {}
366 self.source_node_getter = source_node_getter
367 self.source_node_getter = source_node_getter
367 self.target_node_getter = target_node_getter
368 self.target_node_getter = target_node_getter
368 self.source_nodes = source_nodes or {}
369 self.source_nodes = source_nodes or {}
369 self.target_nodes = target_nodes or {}
370 self.target_nodes = target_nodes or {}
370 self.repo_name = repo_name
371 self.repo_name = repo_name
372 self.source_repo_name = source_repo_name or repo_name
371 self.comments = comments or {}
373 self.comments = comments or {}
372 self.max_file_size_limit = max_file_size_limit
374 self.max_file_size_limit = max_file_size_limit
373
375
374 def render_patchset(self, patchset, source_ref=None, target_ref=None):
376 def render_patchset(self, patchset, source_ref=None, target_ref=None):
375 diffset = AttributeDict(dict(
377 diffset = AttributeDict(dict(
376 lines_added=0,
378 lines_added=0,
377 lines_deleted=0,
379 lines_deleted=0,
378 changed_files=0,
380 changed_files=0,
379 files=[],
381 files=[],
380 limited_diff=isinstance(patchset, LimitedDiffContainer),
382 limited_diff=isinstance(patchset, LimitedDiffContainer),
381 repo_name=self.repo_name,
383 repo_name=self.repo_name,
384 source_repo_name=self.source_repo_name,
382 source_ref=source_ref,
385 source_ref=source_ref,
383 target_ref=target_ref,
386 target_ref=target_ref,
384 ))
387 ))
385 for patch in patchset:
388 for patch in patchset:
386 filediff = self.render_patch(patch)
389 filediff = self.render_patch(patch)
387 filediff.diffset = diffset
390 filediff.diffset = diffset
388 diffset.files.append(filediff)
391 diffset.files.append(filediff)
389 diffset.changed_files += 1
392 diffset.changed_files += 1
390 if not patch['stats']['binary']:
393 if not patch['stats']['binary']:
391 diffset.lines_added += patch['stats']['added']
394 diffset.lines_added += patch['stats']['added']
392 diffset.lines_deleted += patch['stats']['deleted']
395 diffset.lines_deleted += patch['stats']['deleted']
393
396
394 return diffset
397 return diffset
395
398
396 _lexer_cache = {}
399 _lexer_cache = {}
397 def _get_lexer_for_filename(self, filename):
400 def _get_lexer_for_filename(self, filename):
398 # cached because we might need to call it twice for source/target
401 # cached because we might need to call it twice for source/target
399 if filename not in self._lexer_cache:
402 if filename not in self._lexer_cache:
400 self._lexer_cache[filename] = get_lexer_safe(filepath=filename)
403 self._lexer_cache[filename] = get_lexer_safe(filepath=filename)
401 return self._lexer_cache[filename]
404 return self._lexer_cache[filename]
402
405
403 def render_patch(self, patch):
406 def render_patch(self, patch):
404 log.debug('rendering diff for %r' % patch['filename'])
407 log.debug('rendering diff for %r' % patch['filename'])
405
408
406 source_filename = patch['original_filename']
409 source_filename = patch['original_filename']
407 target_filename = patch['filename']
410 target_filename = patch['filename']
408
411
409 source_lexer = plain_text_lexer
412 source_lexer = plain_text_lexer
410 target_lexer = plain_text_lexer
413 target_lexer = plain_text_lexer
411
414
412 if not patch['stats']['binary']:
415 if not patch['stats']['binary']:
413 if self.highlight_mode == self.HL_REAL:
416 if self.highlight_mode == self.HL_REAL:
414 if (source_filename and patch['operation'] in ('D', 'M')
417 if (source_filename and patch['operation'] in ('D', 'M')
415 and source_filename not in self.source_nodes):
418 and source_filename not in self.source_nodes):
416 self.source_nodes[source_filename] = (
419 self.source_nodes[source_filename] = (
417 self.source_node_getter(source_filename))
420 self.source_node_getter(source_filename))
418
421
419 if (target_filename and patch['operation'] in ('A', 'M')
422 if (target_filename and patch['operation'] in ('A', 'M')
420 and target_filename not in self.target_nodes):
423 and target_filename not in self.target_nodes):
421 self.target_nodes[target_filename] = (
424 self.target_nodes[target_filename] = (
422 self.target_node_getter(target_filename))
425 self.target_node_getter(target_filename))
423
426
424 elif self.highlight_mode == self.HL_FAST:
427 elif self.highlight_mode == self.HL_FAST:
425 source_lexer = self._get_lexer_for_filename(source_filename)
428 source_lexer = self._get_lexer_for_filename(source_filename)
426 target_lexer = self._get_lexer_for_filename(target_filename)
429 target_lexer = self._get_lexer_for_filename(target_filename)
427
430
428 source_file = self.source_nodes.get(source_filename, source_filename)
431 source_file = self.source_nodes.get(source_filename, source_filename)
429 target_file = self.target_nodes.get(target_filename, target_filename)
432 target_file = self.target_nodes.get(target_filename, target_filename)
430
433
431 source_filenode, target_filenode = None, None
434 source_filenode, target_filenode = None, None
432
435
433 # TODO: dan: FileNode.lexer works on the content of the file - which
436 # TODO: dan: FileNode.lexer works on the content of the file - which
434 # can be slow - issue #4289 explains a lexer clean up - which once
437 # can be slow - issue #4289 explains a lexer clean up - which once
435 # done can allow caching a lexer for a filenode to avoid the file lookup
438 # done can allow caching a lexer for a filenode to avoid the file lookup
436 if isinstance(source_file, FileNode):
439 if isinstance(source_file, FileNode):
437 source_filenode = source_file
440 source_filenode = source_file
438 source_lexer = source_file.lexer
441 source_lexer = source_file.lexer
439 if isinstance(target_file, FileNode):
442 if isinstance(target_file, FileNode):
440 target_filenode = target_file
443 target_filenode = target_file
441 target_lexer = target_file.lexer
444 target_lexer = target_file.lexer
442
445
443 source_file_path, target_file_path = None, None
446 source_file_path, target_file_path = None, None
444
447
445 if source_filename != '/dev/null':
448 if source_filename != '/dev/null':
446 source_file_path = source_filename
449 source_file_path = source_filename
447 if target_filename != '/dev/null':
450 if target_filename != '/dev/null':
448 target_file_path = target_filename
451 target_file_path = target_filename
449
452
450 source_file_type = source_lexer.name
453 source_file_type = source_lexer.name
451 target_file_type = target_lexer.name
454 target_file_type = target_lexer.name
452
455
453 op_hunks = patch['chunks'][0]
456 op_hunks = patch['chunks'][0]
454 hunks = patch['chunks'][1:]
457 hunks = patch['chunks'][1:]
455
458
456 filediff = AttributeDict({
459 filediff = AttributeDict({
457 'source_file_path': source_file_path,
460 'source_file_path': source_file_path,
458 'target_file_path': target_file_path,
461 'target_file_path': target_file_path,
459 'source_filenode': source_filenode,
462 'source_filenode': source_filenode,
460 'target_filenode': target_filenode,
463 'target_filenode': target_filenode,
461 'hunks': [],
464 'hunks': [],
462 'source_file_type': target_file_type,
465 'source_file_type': target_file_type,
463 'target_file_type': source_file_type,
466 'target_file_type': source_file_type,
464 'patch': patch,
467 'patch': patch,
465 'source_mode': patch['stats']['old_mode'],
468 'source_mode': patch['stats']['old_mode'],
466 'target_mode': patch['stats']['new_mode'],
469 'target_mode': patch['stats']['new_mode'],
467 'limited_diff': isinstance(patch, LimitedDiffContainer),
470 'limited_diff': isinstance(patch, LimitedDiffContainer),
468 'diffset': self,
471 'diffset': self,
469 })
472 })
470
473
471 for hunk in hunks:
474 for hunk in hunks:
472 hunkbit = self.parse_hunk(hunk, source_file, target_file)
475 hunkbit = self.parse_hunk(hunk, source_file, target_file)
473 hunkbit.filediff = filediff
476 hunkbit.filediff = filediff
474 filediff.hunks.append(hunkbit)
477 filediff.hunks.append(hunkbit)
475 return filediff
478 return filediff
476
479
477 def parse_hunk(self, hunk, source_file, target_file):
480 def parse_hunk(self, hunk, source_file, target_file):
478 result = AttributeDict(dict(
481 result = AttributeDict(dict(
479 source_start=hunk['source_start'],
482 source_start=hunk['source_start'],
480 source_length=hunk['source_length'],
483 source_length=hunk['source_length'],
481 target_start=hunk['target_start'],
484 target_start=hunk['target_start'],
482 target_length=hunk['target_length'],
485 target_length=hunk['target_length'],
483 section_header=hunk['section_header'],
486 section_header=hunk['section_header'],
484 lines=[],
487 lines=[],
485 ))
488 ))
486 before, after = [], []
489 before, after = [], []
487
490
488 for line in hunk['lines']:
491 for line in hunk['lines']:
489 if line['action'] == 'unmod':
492 if line['action'] == 'unmod':
490 result.lines.extend(
493 result.lines.extend(
491 self.parse_lines(before, after, source_file, target_file))
494 self.parse_lines(before, after, source_file, target_file))
492 after.append(line)
495 after.append(line)
493 before.append(line)
496 before.append(line)
494 elif line['action'] == 'add':
497 elif line['action'] == 'add':
495 after.append(line)
498 after.append(line)
496 elif line['action'] == 'del':
499 elif line['action'] == 'del':
497 before.append(line)
500 before.append(line)
498 elif line['action'] == 'old-no-nl':
501 elif line['action'] == 'old-no-nl':
499 before.append(line)
502 before.append(line)
500 elif line['action'] == 'new-no-nl':
503 elif line['action'] == 'new-no-nl':
501 after.append(line)
504 after.append(line)
502
505
503 result.lines.extend(
506 result.lines.extend(
504 self.parse_lines(before, after, source_file, target_file))
507 self.parse_lines(before, after, source_file, target_file))
505 result.unified = self.as_unified(result.lines)
508 result.unified = self.as_unified(result.lines)
506 result.sideside = result.lines
509 result.sideside = result.lines
507 return result
510 return result
508
511
509 def parse_lines(self, before_lines, after_lines, source_file, target_file):
512 def parse_lines(self, before_lines, after_lines, source_file, target_file):
510 # TODO: dan: investigate doing the diff comparison and fast highlighting
513 # TODO: dan: investigate doing the diff comparison and fast highlighting
511 # on the entire before and after buffered block lines rather than by
514 # on the entire before and after buffered block lines rather than by
512 # line, this means we can get better 'fast' highlighting if the context
515 # line, this means we can get better 'fast' highlighting if the context
513 # allows it - eg.
516 # allows it - eg.
514 # line 4: """
517 # line 4: """
515 # line 5: this gets highlighted as a string
518 # line 5: this gets highlighted as a string
516 # line 6: """
519 # line 6: """
517
520
518 lines = []
521 lines = []
519 while before_lines or after_lines:
522 while before_lines or after_lines:
520 before, after = None, None
523 before, after = None, None
521 before_tokens, after_tokens = None, None
524 before_tokens, after_tokens = None, None
522
525
523 if before_lines:
526 if before_lines:
524 before = before_lines.pop(0)
527 before = before_lines.pop(0)
525 if after_lines:
528 if after_lines:
526 after = after_lines.pop(0)
529 after = after_lines.pop(0)
527
530
528 original = AttributeDict()
531 original = AttributeDict()
529 modified = AttributeDict()
532 modified = AttributeDict()
530
533
531 if before:
534 if before:
532 if before['action'] == 'old-no-nl':
535 if before['action'] == 'old-no-nl':
533 before_tokens = [('nonl', before['line'])]
536 before_tokens = [('nonl', before['line'])]
534 else:
537 else:
535 before_tokens = self.get_line_tokens(
538 before_tokens = self.get_line_tokens(
536 line_text=before['line'], line_number=before['old_lineno'],
539 line_text=before['line'], line_number=before['old_lineno'],
537 file=source_file)
540 file=source_file)
538 original.lineno = before['old_lineno']
541 original.lineno = before['old_lineno']
539 original.content = before['line']
542 original.content = before['line']
540 original.action = self.action_to_op(before['action'])
543 original.action = self.action_to_op(before['action'])
541 original.comments = self.get_comments_for('old',
544 original.comments = self.get_comments_for('old',
542 source_file, before['old_lineno'])
545 source_file, before['old_lineno'])
543
546
544 if after:
547 if after:
545 if after['action'] == 'new-no-nl':
548 if after['action'] == 'new-no-nl':
546 after_tokens = [('nonl', after['line'])]
549 after_tokens = [('nonl', after['line'])]
547 else:
550 else:
548 after_tokens = self.get_line_tokens(
551 after_tokens = self.get_line_tokens(
549 line_text=after['line'], line_number=after['new_lineno'],
552 line_text=after['line'], line_number=after['new_lineno'],
550 file=target_file)
553 file=target_file)
551 modified.lineno = after['new_lineno']
554 modified.lineno = after['new_lineno']
552 modified.content = after['line']
555 modified.content = after['line']
553 modified.action = self.action_to_op(after['action'])
556 modified.action = self.action_to_op(after['action'])
554 modified.comments = self.get_comments_for('new',
557 modified.comments = self.get_comments_for('new',
555 target_file, after['new_lineno'])
558 target_file, after['new_lineno'])
556
559
557 # diff the lines
560 # diff the lines
558 if before_tokens and after_tokens:
561 if before_tokens and after_tokens:
559 o_tokens, m_tokens, similarity = tokens_diff(
562 o_tokens, m_tokens, similarity = tokens_diff(
560 before_tokens, after_tokens)
563 before_tokens, after_tokens)
561 original.content = render_tokenstream(o_tokens)
564 original.content = render_tokenstream(o_tokens)
562 modified.content = render_tokenstream(m_tokens)
565 modified.content = render_tokenstream(m_tokens)
563 elif before_tokens:
566 elif before_tokens:
564 original.content = render_tokenstream(
567 original.content = render_tokenstream(
565 [(x[0], '', x[1]) for x in before_tokens])
568 [(x[0], '', x[1]) for x in before_tokens])
566 elif after_tokens:
569 elif after_tokens:
567 modified.content = render_tokenstream(
570 modified.content = render_tokenstream(
568 [(x[0], '', x[1]) for x in after_tokens])
571 [(x[0], '', x[1]) for x in after_tokens])
569
572
570 lines.append(AttributeDict({
573 lines.append(AttributeDict({
571 'original': original,
574 'original': original,
572 'modified': modified,
575 'modified': modified,
573 }))
576 }))
574
577
575 return lines
578 return lines
576
579
577 def get_comments_for(self, version, file, line_number):
580 def get_comments_for(self, version, file, line_number):
578 if hasattr(file, 'unicode_path'):
581 if hasattr(file, 'unicode_path'):
579 file = file.unicode_path
582 file = file.unicode_path
580
583
581 if not isinstance(file, basestring):
584 if not isinstance(file, basestring):
582 return None
585 return None
583
586
584 line_key = {
587 line_key = {
585 'old': 'o',
588 'old': 'o',
586 'new': 'n',
589 'new': 'n',
587 }[version] + str(line_number)
590 }[version] + str(line_number)
588
591
589 return self.comments.get(file, {}).get(line_key)
592 return self.comments.get(file, {}).get(line_key)
590
593
591 def get_line_tokens(self, line_text, line_number, file=None):
594 def get_line_tokens(self, line_text, line_number, file=None):
592 filenode = None
595 filenode = None
593 filename = None
596 filename = None
594
597
595 if isinstance(file, basestring):
598 if isinstance(file, basestring):
596 filename = file
599 filename = file
597 elif isinstance(file, FileNode):
600 elif isinstance(file, FileNode):
598 filenode = file
601 filenode = file
599 filename = file.unicode_path
602 filename = file.unicode_path
600
603
601 if self.highlight_mode == self.HL_REAL and filenode:
604 if self.highlight_mode == self.HL_REAL and filenode:
602 if line_number and file.size < self.max_file_size_limit:
605 if line_number and file.size < self.max_file_size_limit:
603 return self.get_tokenized_filenode_line(file, line_number)
606 return self.get_tokenized_filenode_line(file, line_number)
604
607
605 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
608 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
606 lexer = self._get_lexer_for_filename(filename)
609 lexer = self._get_lexer_for_filename(filename)
607 return list(tokenize_string(line_text, lexer))
610 return list(tokenize_string(line_text, lexer))
608
611
609 return list(tokenize_string(line_text, plain_text_lexer))
612 return list(tokenize_string(line_text, plain_text_lexer))
610
613
611 def get_tokenized_filenode_line(self, filenode, line_number):
614 def get_tokenized_filenode_line(self, filenode, line_number):
612
615
613 if filenode not in self.highlighted_filenodes:
616 if filenode not in self.highlighted_filenodes:
614 tokenized_lines = filenode_as_lines_tokens(filenode, filenode.lexer)
617 tokenized_lines = filenode_as_lines_tokens(filenode, filenode.lexer)
615 self.highlighted_filenodes[filenode] = tokenized_lines
618 self.highlighted_filenodes[filenode] = tokenized_lines
616 return self.highlighted_filenodes[filenode][line_number - 1]
619 return self.highlighted_filenodes[filenode][line_number - 1]
617
620
618 def action_to_op(self, action):
621 def action_to_op(self, action):
619 return {
622 return {
620 'add': '+',
623 'add': '+',
621 'del': '-',
624 'del': '-',
622 'unmod': ' ',
625 'unmod': ' ',
623 'old-no-nl': ' ',
626 'old-no-nl': ' ',
624 'new-no-nl': ' ',
627 'new-no-nl': ' ',
625 }.get(action, action)
628 }.get(action, action)
626
629
627 def as_unified(self, lines):
630 def as_unified(self, lines):
628 """ Return a generator that yields the lines of a diff in unified order """
631 """ Return a generator that yields the lines of a diff in unified order """
629 def generator():
632 def generator():
630 buf = []
633 buf = []
631 for line in lines:
634 for line in lines:
632
635
633 if buf and not line.original or line.original.action == ' ':
636 if buf and not line.original or line.original.action == ' ':
634 for b in buf:
637 for b in buf:
635 yield b
638 yield b
636 buf = []
639 buf = []
637
640
638 if line.original:
641 if line.original:
639 if line.original.action == ' ':
642 if line.original.action == ' ':
640 yield (line.original.lineno, line.modified.lineno,
643 yield (line.original.lineno, line.modified.lineno,
641 line.original.action, line.original.content,
644 line.original.action, line.original.content,
642 line.original.comments)
645 line.original.comments)
643 continue
646 continue
644
647
645 if line.original.action == '-':
648 if line.original.action == '-':
646 yield (line.original.lineno, None,
649 yield (line.original.lineno, None,
647 line.original.action, line.original.content,
650 line.original.action, line.original.content,
648 line.original.comments)
651 line.original.comments)
649
652
650 if line.modified.action == '+':
653 if line.modified.action == '+':
651 buf.append((
654 buf.append((
652 None, line.modified.lineno,
655 None, line.modified.lineno,
653 line.modified.action, line.modified.content,
656 line.modified.action, line.modified.content,
654 line.modified.comments))
657 line.modified.comments))
655 continue
658 continue
656
659
657 if line.modified:
660 if line.modified:
658 yield (None, line.modified.lineno,
661 yield (None, line.modified.lineno,
659 line.modified.action, line.modified.content,
662 line.modified.action, line.modified.content,
660 line.modified.comments)
663 line.modified.comments)
661
664
662 for b in buf:
665 for b in buf:
663 yield b
666 yield b
664
667
665 return generator()
668 return generator()
@@ -1,946 +1,950 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):
659 class StrictAttributeDict(dict):
660 """
661 Strict Version of Attribute dict which raises an Attribute error when
662 requested attribute is not set
663 """
660 def __getattr__(self, attr):
664 def __getattr__(self, attr):
661 try:
665 try:
662 return self[attr]
666 return self[attr]
663 except KeyError:
667 except KeyError:
664 raise AttributeError('%s object has no attribute %s' % (self, attr))
668 raise AttributeError('%s object has no attribute %s' % (self, attr))
665 __setattr__ = dict.__setitem__
669 __setattr__ = dict.__setitem__
666 __delattr__ = dict.__delitem__
670 __delattr__ = dict.__delitem__
667
671
668
672
669 class AttributeDict(dict):
673 class AttributeDict(dict):
670 def __getattr__(self, attr):
674 def __getattr__(self, attr):
671 return self.get(attr, None)
675 return self.get(attr, None)
672 __setattr__ = dict.__setitem__
676 __setattr__ = dict.__setitem__
673 __delattr__ = dict.__delitem__
677 __delattr__ = dict.__delitem__
674
678
675
679
676 def fix_PATH(os_=None):
680 def fix_PATH(os_=None):
677 """
681 """
678 Get current active python path, and append it to PATH variable to fix
682 Get current active python path, and append it to PATH variable to fix
679 issues of subprocess calls and different python versions
683 issues of subprocess calls and different python versions
680 """
684 """
681 if os_ is None:
685 if os_ is None:
682 import os
686 import os
683 else:
687 else:
684 os = os_
688 os = os_
685
689
686 cur_path = os.path.split(sys.executable)[0]
690 cur_path = os.path.split(sys.executable)[0]
687 if not os.environ['PATH'].startswith(cur_path):
691 if not os.environ['PATH'].startswith(cur_path):
688 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
692 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
689
693
690
694
691 def obfuscate_url_pw(engine):
695 def obfuscate_url_pw(engine):
692 _url = engine or ''
696 _url = engine or ''
693 try:
697 try:
694 _url = sqlalchemy.engine.url.make_url(engine)
698 _url = sqlalchemy.engine.url.make_url(engine)
695 if _url.password:
699 if _url.password:
696 _url.password = 'XXXXX'
700 _url.password = 'XXXXX'
697 except Exception:
701 except Exception:
698 pass
702 pass
699 return unicode(_url)
703 return unicode(_url)
700
704
701
705
702 def get_server_url(environ):
706 def get_server_url(environ):
703 req = webob.Request(environ)
707 req = webob.Request(environ)
704 return req.host_url + req.script_name
708 return req.host_url + req.script_name
705
709
706
710
707 def unique_id(hexlen=32):
711 def unique_id(hexlen=32):
708 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
712 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
709 return suuid(truncate_to=hexlen, alphabet=alphabet)
713 return suuid(truncate_to=hexlen, alphabet=alphabet)
710
714
711
715
712 def suuid(url=None, truncate_to=22, alphabet=None):
716 def suuid(url=None, truncate_to=22, alphabet=None):
713 """
717 """
714 Generate and return a short URL safe UUID.
718 Generate and return a short URL safe UUID.
715
719
716 If the url parameter is provided, set the namespace to the provided
720 If the url parameter is provided, set the namespace to the provided
717 URL and generate a UUID.
721 URL and generate a UUID.
718
722
719 :param url to get the uuid for
723 :param url to get the uuid for
720 :truncate_to: truncate the basic 22 UUID to shorter version
724 :truncate_to: truncate the basic 22 UUID to shorter version
721
725
722 The IDs won't be universally unique any longer, but the probability of
726 The IDs won't be universally unique any longer, but the probability of
723 a collision will still be very low.
727 a collision will still be very low.
724 """
728 """
725 # Define our alphabet.
729 # Define our alphabet.
726 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
730 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
727
731
728 # If no URL is given, generate a random UUID.
732 # If no URL is given, generate a random UUID.
729 if url is None:
733 if url is None:
730 unique_id = uuid.uuid4().int
734 unique_id = uuid.uuid4().int
731 else:
735 else:
732 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
736 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
733
737
734 alphabet_length = len(_ALPHABET)
738 alphabet_length = len(_ALPHABET)
735 output = []
739 output = []
736 while unique_id > 0:
740 while unique_id > 0:
737 digit = unique_id % alphabet_length
741 digit = unique_id % alphabet_length
738 output.append(_ALPHABET[digit])
742 output.append(_ALPHABET[digit])
739 unique_id = int(unique_id / alphabet_length)
743 unique_id = int(unique_id / alphabet_length)
740 return "".join(output)[:truncate_to]
744 return "".join(output)[:truncate_to]
741
745
742
746
743 def get_current_rhodecode_user():
747 def get_current_rhodecode_user():
744 """
748 """
745 Gets rhodecode user from threadlocal tmpl_context variable if it's
749 Gets rhodecode user from threadlocal tmpl_context variable if it's
746 defined, else returns None.
750 defined, else returns None.
747 """
751 """
748 from pylons import tmpl_context as c
752 from pylons import tmpl_context as c
749 if hasattr(c, 'rhodecode_user'):
753 if hasattr(c, 'rhodecode_user'):
750 return c.rhodecode_user
754 return c.rhodecode_user
751
755
752 return None
756 return None
753
757
754
758
755 def action_logger_generic(action, namespace=''):
759 def action_logger_generic(action, namespace=''):
756 """
760 """
757 A generic logger for actions useful to the system overview, tries to find
761 A generic logger for actions useful to the system overview, tries to find
758 an acting user for the context of the call otherwise reports unknown user
762 an acting user for the context of the call otherwise reports unknown user
759
763
760 :param action: logging message eg 'comment 5 deleted'
764 :param action: logging message eg 'comment 5 deleted'
761 :param type: string
765 :param type: string
762
766
763 :param namespace: namespace of the logging message eg. 'repo.comments'
767 :param namespace: namespace of the logging message eg. 'repo.comments'
764 :param type: string
768 :param type: string
765
769
766 """
770 """
767
771
768 logger_name = 'rhodecode.actions'
772 logger_name = 'rhodecode.actions'
769
773
770 if namespace:
774 if namespace:
771 logger_name += '.' + namespace
775 logger_name += '.' + namespace
772
776
773 log = logging.getLogger(logger_name)
777 log = logging.getLogger(logger_name)
774
778
775 # get a user if we can
779 # get a user if we can
776 user = get_current_rhodecode_user()
780 user = get_current_rhodecode_user()
777
781
778 logfunc = log.info
782 logfunc = log.info
779
783
780 if not user:
784 if not user:
781 user = '<unknown user>'
785 user = '<unknown user>'
782 logfunc = log.warning
786 logfunc = log.warning
783
787
784 logfunc('Logging action by {}: {}'.format(user, action))
788 logfunc('Logging action by {}: {}'.format(user, action))
785
789
786
790
787 def escape_split(text, sep=',', maxsplit=-1):
791 def escape_split(text, sep=',', maxsplit=-1):
788 r"""
792 r"""
789 Allows for escaping of the separator: e.g. arg='foo\, bar'
793 Allows for escaping of the separator: e.g. arg='foo\, bar'
790
794
791 It should be noted that the way bash et. al. do command line parsing, those
795 It should be noted that the way bash et. al. do command line parsing, those
792 single quotes are required.
796 single quotes are required.
793 """
797 """
794 escaped_sep = r'\%s' % sep
798 escaped_sep = r'\%s' % sep
795
799
796 if escaped_sep not in text:
800 if escaped_sep not in text:
797 return text.split(sep, maxsplit)
801 return text.split(sep, maxsplit)
798
802
799 before, _mid, after = text.partition(escaped_sep)
803 before, _mid, after = text.partition(escaped_sep)
800 startlist = before.split(sep, maxsplit) # a regular split is fine here
804 startlist = before.split(sep, maxsplit) # a regular split is fine here
801 unfinished = startlist[-1]
805 unfinished = startlist[-1]
802 startlist = startlist[:-1]
806 startlist = startlist[:-1]
803
807
804 # recurse because there may be more escaped separators
808 # recurse because there may be more escaped separators
805 endlist = escape_split(after, sep, maxsplit)
809 endlist = escape_split(after, sep, maxsplit)
806
810
807 # finish building the escaped value. we use endlist[0] becaue the first
811 # finish building the escaped value. we use endlist[0] becaue the first
808 # part of the string sent in recursion is the rest of the escaped value.
812 # part of the string sent in recursion is the rest of the escaped value.
809 unfinished += sep + endlist[0]
813 unfinished += sep + endlist[0]
810
814
811 return startlist + [unfinished] + endlist[1:] # put together all the parts
815 return startlist + [unfinished] + endlist[1:] # put together all the parts
812
816
813
817
814 class OptionalAttr(object):
818 class OptionalAttr(object):
815 """
819 """
816 Special Optional Option that defines other attribute. Example::
820 Special Optional Option that defines other attribute. Example::
817
821
818 def test(apiuser, userid=Optional(OAttr('apiuser')):
822 def test(apiuser, userid=Optional(OAttr('apiuser')):
819 user = Optional.extract(userid)
823 user = Optional.extract(userid)
820 # calls
824 # calls
821
825
822 """
826 """
823
827
824 def __init__(self, attr_name):
828 def __init__(self, attr_name):
825 self.attr_name = attr_name
829 self.attr_name = attr_name
826
830
827 def __repr__(self):
831 def __repr__(self):
828 return '<OptionalAttr:%s>' % self.attr_name
832 return '<OptionalAttr:%s>' % self.attr_name
829
833
830 def __call__(self):
834 def __call__(self):
831 return self
835 return self
832
836
833
837
834 # alias
838 # alias
835 OAttr = OptionalAttr
839 OAttr = OptionalAttr
836
840
837
841
838 class Optional(object):
842 class Optional(object):
839 """
843 """
840 Defines an optional parameter::
844 Defines an optional parameter::
841
845
842 param = param.getval() if isinstance(param, Optional) else param
846 param = param.getval() if isinstance(param, Optional) else param
843 param = param() if isinstance(param, Optional) else param
847 param = param() if isinstance(param, Optional) else param
844
848
845 is equivalent of::
849 is equivalent of::
846
850
847 param = Optional.extract(param)
851 param = Optional.extract(param)
848
852
849 """
853 """
850
854
851 def __init__(self, type_):
855 def __init__(self, type_):
852 self.type_ = type_
856 self.type_ = type_
853
857
854 def __repr__(self):
858 def __repr__(self):
855 return '<Optional:%s>' % self.type_.__repr__()
859 return '<Optional:%s>' % self.type_.__repr__()
856
860
857 def __call__(self):
861 def __call__(self):
858 return self.getval()
862 return self.getval()
859
863
860 def getval(self):
864 def getval(self):
861 """
865 """
862 returns value from this Optional instance
866 returns value from this Optional instance
863 """
867 """
864 if isinstance(self.type_, OAttr):
868 if isinstance(self.type_, OAttr):
865 # use params name
869 # use params name
866 return self.type_.attr_name
870 return self.type_.attr_name
867 return self.type_
871 return self.type_
868
872
869 @classmethod
873 @classmethod
870 def extract(cls, val):
874 def extract(cls, val):
871 """
875 """
872 Extracts value from Optional() instance
876 Extracts value from Optional() instance
873
877
874 :param val:
878 :param val:
875 :return: original value if it's not Optional instance else
879 :return: original value if it's not Optional instance else
876 value of instance
880 value of instance
877 """
881 """
878 if isinstance(val, cls):
882 if isinstance(val, cls):
879 return val.getval()
883 return val.getval()
880 return val
884 return val
881
885
882
886
883 def get_routes_generator_for_server_url(server_url):
887 def get_routes_generator_for_server_url(server_url):
884 parsed_url = urlobject.URLObject(server_url)
888 parsed_url = urlobject.URLObject(server_url)
885 netloc = safe_str(parsed_url.netloc)
889 netloc = safe_str(parsed_url.netloc)
886 script_name = safe_str(parsed_url.path)
890 script_name = safe_str(parsed_url.path)
887
891
888 if ':' in netloc:
892 if ':' in netloc:
889 server_name, server_port = netloc.split(':')
893 server_name, server_port = netloc.split(':')
890 else:
894 else:
891 server_name = netloc
895 server_name = netloc
892 server_port = (parsed_url.scheme == 'https' and '443' or '80')
896 server_port = (parsed_url.scheme == 'https' and '443' or '80')
893
897
894 environ = {
898 environ = {
895 'REQUEST_METHOD': 'GET',
899 'REQUEST_METHOD': 'GET',
896 'PATH_INFO': '/',
900 'PATH_INFO': '/',
897 'SERVER_NAME': server_name,
901 'SERVER_NAME': server_name,
898 'SERVER_PORT': server_port,
902 'SERVER_PORT': server_port,
899 'SCRIPT_NAME': script_name,
903 'SCRIPT_NAME': script_name,
900 }
904 }
901 if parsed_url.scheme == 'https':
905 if parsed_url.scheme == 'https':
902 environ['HTTPS'] = 'on'
906 environ['HTTPS'] = 'on'
903 environ['wsgi.url_scheme'] = 'https'
907 environ['wsgi.url_scheme'] = 'https'
904
908
905 return routes.util.URLGenerator(rhodecode.CONFIG['routes.map'], environ)
909 return routes.util.URLGenerator(rhodecode.CONFIG['routes.map'], environ)
906
910
907
911
908 def glob2re(pat):
912 def glob2re(pat):
909 """
913 """
910 Translate a shell PATTERN to a regular expression.
914 Translate a shell PATTERN to a regular expression.
911
915
912 There is no way to quote meta-characters.
916 There is no way to quote meta-characters.
913 """
917 """
914
918
915 i, n = 0, len(pat)
919 i, n = 0, len(pat)
916 res = ''
920 res = ''
917 while i < n:
921 while i < n:
918 c = pat[i]
922 c = pat[i]
919 i = i+1
923 i = i+1
920 if c == '*':
924 if c == '*':
921 #res = res + '.*'
925 #res = res + '.*'
922 res = res + '[^/]*'
926 res = res + '[^/]*'
923 elif c == '?':
927 elif c == '?':
924 #res = res + '.'
928 #res = res + '.'
925 res = res + '[^/]'
929 res = res + '[^/]'
926 elif c == '[':
930 elif c == '[':
927 j = i
931 j = i
928 if j < n and pat[j] == '!':
932 if j < n and pat[j] == '!':
929 j = j+1
933 j = j+1
930 if j < n and pat[j] == ']':
934 if j < n and pat[j] == ']':
931 j = j+1
935 j = j+1
932 while j < n and pat[j] != ']':
936 while j < n and pat[j] != ']':
933 j = j+1
937 j = j+1
934 if j >= n:
938 if j >= n:
935 res = res + '\\['
939 res = res + '\\['
936 else:
940 else:
937 stuff = pat[i:j].replace('\\','\\\\')
941 stuff = pat[i:j].replace('\\','\\\\')
938 i = j+1
942 i = j+1
939 if stuff[0] == '!':
943 if stuff[0] == '!':
940 stuff = '^' + stuff[1:]
944 stuff = '^' + stuff[1:]
941 elif stuff[0] == '^':
945 elif stuff[0] == '^':
942 stuff = '\\' + stuff
946 stuff = '\\' + stuff
943 res = '%s[%s]' % (res, stuff)
947 res = '%s[%s]' % (res, stuff)
944 else:
948 else:
945 res = res + re.escape(c)
949 res = res + re.escape(c)
946 return res + '\Z(?ms)'
950 return res + '\Z(?ms)'
@@ -1,640 +1,640 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 function bindDeleteCommentButtons() {
51 function bindDeleteCommentButtons() {
52 $('.delete-comment').one('click', function() {
52 $('.delete-comment').one('click', function() {
53 var comment_id = $(this).data("comment-id");
53 var comment_id = $(this).data("comment-id");
54
54
55 if (comment_id){
55 if (comment_id){
56 deleteComment(comment_id);
56 deleteComment(comment_id);
57 }
57 }
58 });
58 });
59 }
59 }
60
60
61 var deleteComment = function(comment_id) {
61 var deleteComment = function(comment_id) {
62 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
62 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
63 var postData = {
63 var postData = {
64 '_method': 'delete',
64 '_method': 'delete',
65 'csrf_token': CSRF_TOKEN
65 'csrf_token': CSRF_TOKEN
66 };
66 };
67
67
68 var success = function(o) {
68 var success = function(o) {
69 window.location.reload();
69 window.location.reload();
70 };
70 };
71 ajaxPOST(url, postData, success);
71 ajaxPOST(url, postData, success);
72 };
72 };
73
73
74
74
75 var bindToggleButtons = function() {
75 var bindToggleButtons = function() {
76 $('.comment-toggle').on('click', function() {
76 $('.comment-toggle').on('click', function() {
77 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
77 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
78 });
78 });
79 };
79 };
80
80
81 var linkifyComments = function(comments) {
81 var linkifyComments = function(comments) {
82 /* TODO: dan: remove this - it should no longer needed */
82 /* TODO: dan: remove this - it should no longer needed */
83 for (var i = 0; i < comments.length; i++) {
83 for (var i = 0; i < comments.length; i++) {
84 var comment_id = $(comments[i]).data('comment-id');
84 var comment_id = $(comments[i]).data('comment-id');
85 var prev_comment_id = $(comments[i - 1]).data('comment-id');
85 var prev_comment_id = $(comments[i - 1]).data('comment-id');
86 var next_comment_id = $(comments[i + 1]).data('comment-id');
86 var next_comment_id = $(comments[i + 1]).data('comment-id');
87
87
88 // place next/prev links
88 // place next/prev links
89 if (prev_comment_id) {
89 if (prev_comment_id) {
90 $('#prev_c_' + comment_id).show();
90 $('#prev_c_' + comment_id).show();
91 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
91 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
92 'href', '#comment-' + prev_comment_id).removeClass('disabled');
92 'href', '#comment-' + prev_comment_id).removeClass('disabled');
93 }
93 }
94 if (next_comment_id) {
94 if (next_comment_id) {
95 $('#next_c_' + comment_id).show();
95 $('#next_c_' + comment_id).show();
96 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
96 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
97 'href', '#comment-' + next_comment_id).removeClass('disabled');
97 'href', '#comment-' + next_comment_id).removeClass('disabled');
98 }
98 }
99 // place a first link to the total counter
99 // place a first link to the total counter
100 if (i === 0) {
100 if (i === 0) {
101 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
101 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
102 }
102 }
103 }
103 }
104
104
105 };
105 };
106
106
107
107
108 /* Comment form for main and inline comments */
108 /* Comment form for main and inline comments */
109 var CommentForm = (function() {
109 var CommentForm = (function() {
110 "use strict";
110 "use strict";
111
111
112 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
112 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
113
113
114 this.withLineNo = function(selector) {
114 this.withLineNo = function(selector) {
115 var lineNo = this.lineNo;
115 var lineNo = this.lineNo;
116 if (lineNo === undefined) {
116 if (lineNo === undefined) {
117 return selector
117 return selector
118 } else {
118 } else {
119 return selector + '_' + lineNo;
119 return selector + '_' + lineNo;
120 }
120 }
121 };
121 };
122
122
123 this.commitId = commitId;
123 this.commitId = commitId;
124 this.pullRequestId = pullRequestId;
124 this.pullRequestId = pullRequestId;
125 this.lineNo = lineNo;
125 this.lineNo = lineNo;
126 this.initAutocompleteActions = initAutocompleteActions;
126 this.initAutocompleteActions = initAutocompleteActions;
127
127
128 this.previewButton = this.withLineNo('#preview-btn');
128 this.previewButton = this.withLineNo('#preview-btn');
129 this.previewContainer = this.withLineNo('#preview-container');
129 this.previewContainer = this.withLineNo('#preview-container');
130
130
131 this.previewBoxSelector = this.withLineNo('#preview-box');
131 this.previewBoxSelector = this.withLineNo('#preview-box');
132
132
133 this.editButton = this.withLineNo('#edit-btn');
133 this.editButton = this.withLineNo('#edit-btn');
134 this.editContainer = this.withLineNo('#edit-container');
134 this.editContainer = this.withLineNo('#edit-container');
135
135
136 this.cancelButton = this.withLineNo('#cancel-btn');
136 this.cancelButton = this.withLineNo('#cancel-btn');
137
137
138 this.statusChange = '#change_status';
138 this.statusChange = '#change_status';
139 this.cmBox = this.withLineNo('#text');
139 this.cmBox = this.withLineNo('#text');
140 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
140 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
141
141
142 this.submitForm = formElement;
142 this.submitForm = formElement;
143 this.submitButton = $(this.submitForm).find('input[type="submit"]');
143 this.submitButton = $(this.submitForm).find('input[type="submit"]');
144 this.submitButtonText = this.submitButton.val();
144 this.submitButtonText = this.submitButton.val();
145
145
146 this.previewUrl = pyroutes.url('changeset_comment_preview',
146 this.previewUrl = pyroutes.url('changeset_comment_preview',
147 {'repo_name': templateContext.repo_name});
147 {'repo_name': templateContext.repo_name});
148
148
149 // based on commitId, or pullReuqestId decide where do we submit
149 // based on commitId, or pullReuqestId decide where do we submit
150 // out data
150 // out data
151 if (this.commitId){
151 if (this.commitId){
152 this.submitUrl = pyroutes.url('changeset_comment',
152 this.submitUrl = pyroutes.url('changeset_comment',
153 {'repo_name': templateContext.repo_name,
153 {'repo_name': templateContext.repo_name,
154 'revision': this.commitId});
154 'revision': this.commitId});
155
155
156 } else if (this.pullRequestId) {
156 } else if (this.pullRequestId) {
157 this.submitUrl = pyroutes.url('pullrequest_comment',
157 this.submitUrl = pyroutes.url('pullrequest_comment',
158 {'repo_name': templateContext.repo_name,
158 {'repo_name': templateContext.repo_name,
159 'pull_request_id': this.pullRequestId});
159 'pull_request_id': this.pullRequestId});
160
160
161 } else {
161 } else {
162 throw new Error(
162 throw new Error(
163 'CommentForm requires pullRequestId, or commitId to be specified.')
163 'CommentForm requires pullRequestId, or commitId to be specified.')
164 }
164 }
165
165
166 this.getCmInstance = function(){
166 this.getCmInstance = function(){
167 return this.cm
167 return this.cm
168 };
168 };
169
169
170 var self = this;
170 var self = this;
171
171
172 this.getCommentStatus = function() {
172 this.getCommentStatus = function() {
173 return $(this.submitForm).find(this.statusChange).val();
173 return $(this.submitForm).find(this.statusChange).val();
174 };
174 };
175
175
176 this.isAllowedToSubmit = function() {
176 this.isAllowedToSubmit = function() {
177 return !$(this.submitButton).prop('disabled');
177 return !$(this.submitButton).prop('disabled');
178 };
178 };
179
179
180 this.initStatusChangeSelector = function(){
180 this.initStatusChangeSelector = function(){
181 var formatChangeStatus = function(state, escapeMarkup) {
181 var formatChangeStatus = function(state, escapeMarkup) {
182 var originalOption = state.element;
182 var originalOption = state.element;
183 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
183 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
184 '<span>' + escapeMarkup(state.text) + '</span>';
184 '<span>' + escapeMarkup(state.text) + '</span>';
185 };
185 };
186 var formatResult = function(result, container, query, escapeMarkup) {
186 var formatResult = function(result, container, query, escapeMarkup) {
187 return formatChangeStatus(result, escapeMarkup);
187 return formatChangeStatus(result, escapeMarkup);
188 };
188 };
189
189
190 var formatSelection = function(data, container, escapeMarkup) {
190 var formatSelection = function(data, container, escapeMarkup) {
191 return formatChangeStatus(data, escapeMarkup);
191 return formatChangeStatus(data, escapeMarkup);
192 };
192 };
193
193
194 $(this.submitForm).find(this.statusChange).select2({
194 $(this.submitForm).find(this.statusChange).select2({
195 placeholder: _gettext('Status Review'),
195 placeholder: _gettext('Status Review'),
196 formatResult: formatResult,
196 formatResult: formatResult,
197 formatSelection: formatSelection,
197 formatSelection: formatSelection,
198 containerCssClass: "drop-menu status_box_menu",
198 containerCssClass: "drop-menu status_box_menu",
199 dropdownCssClass: "drop-menu-dropdown",
199 dropdownCssClass: "drop-menu-dropdown",
200 dropdownAutoWidth: true,
200 dropdownAutoWidth: true,
201 minimumResultsForSearch: -1
201 minimumResultsForSearch: -1
202 });
202 });
203 $(this.submitForm).find(this.statusChange).on('change', function() {
203 $(this.submitForm).find(this.statusChange).on('change', function() {
204 var status = self.getCommentStatus();
204 var status = self.getCommentStatus();
205 if (status && !self.lineNo) {
205 if (status && !self.lineNo) {
206 $(self.submitButton).prop('disabled', false);
206 $(self.submitButton).prop('disabled', false);
207 }
207 }
208 //todo, fix this name
208 //todo, fix this name
209 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);
210 self.cm.setOption('placeholder', placeholderText);
210 self.cm.setOption('placeholder', placeholderText);
211 })
211 })
212 };
212 };
213
213
214 // reset the comment form into it's original state
214 // reset the comment form into it's original state
215 this.resetCommentFormState = function(content) {
215 this.resetCommentFormState = function(content) {
216 content = content || '';
216 content = content || '';
217
217
218 $(this.editContainer).show();
218 $(this.editContainer).show();
219 $(this.editButton).hide();
219 $(this.editButton).hide();
220
220
221 $(this.previewContainer).hide();
221 $(this.previewContainer).hide();
222 $(this.previewButton).show();
222 $(this.previewButton).show();
223
223
224 this.setActionButtonsDisabled(true);
224 this.setActionButtonsDisabled(true);
225 self.cm.setValue(content);
225 self.cm.setValue(content);
226 self.cm.setOption("readOnly", false);
226 self.cm.setOption("readOnly", false);
227 };
227 };
228
228
229 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
229 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
230 failHandler = failHandler || function() {};
230 failHandler = failHandler || function() {};
231 var postData = toQueryString(postData);
231 var postData = toQueryString(postData);
232 var request = $.ajax({
232 var request = $.ajax({
233 url: url,
233 url: url,
234 type: 'POST',
234 type: 'POST',
235 data: postData,
235 data: postData,
236 headers: {'X-PARTIAL-XHR': true}
236 headers: {'X-PARTIAL-XHR': true}
237 })
237 })
238 .done(function(data) {
238 .done(function(data) {
239 successHandler(data);
239 successHandler(data);
240 })
240 })
241 .fail(function(data, textStatus, errorThrown){
241 .fail(function(data, textStatus, errorThrown){
242 alert(
242 alert(
243 "Error while submitting comment.\n" +
243 "Error while submitting comment.\n" +
244 "Error code {0} ({1}).".format(data.status, data.statusText));
244 "Error code {0} ({1}).".format(data.status, data.statusText));
245 failHandler()
245 failHandler()
246 });
246 });
247 return request;
247 return request;
248 };
248 };
249
249
250 // overwrite a submitHandler, we need to do it for inline comments
250 // overwrite a submitHandler, we need to do it for inline comments
251 this.setHandleFormSubmit = function(callback) {
251 this.setHandleFormSubmit = function(callback) {
252 this.handleFormSubmit = callback;
252 this.handleFormSubmit = callback;
253 };
253 };
254
254
255 // default handler for for submit for main comments
255 // default handler for for submit for main comments
256 this.handleFormSubmit = function() {
256 this.handleFormSubmit = function() {
257 var text = self.cm.getValue();
257 var text = self.cm.getValue();
258 var status = self.getCommentStatus();
258 var status = self.getCommentStatus();
259
259
260 if (text === "" && !status) {
260 if (text === "" && !status) {
261 return;
261 return;
262 }
262 }
263
263
264 var excludeCancelBtn = false;
264 var excludeCancelBtn = false;
265 var submitEvent = true;
265 var submitEvent = true;
266 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
266 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
267 self.cm.setOption("readOnly", true);
267 self.cm.setOption("readOnly", true);
268 var postData = {
268 var postData = {
269 'text': text,
269 'text': text,
270 'changeset_status': status,
270 'changeset_status': status,
271 'csrf_token': CSRF_TOKEN
271 'csrf_token': CSRF_TOKEN
272 };
272 };
273
273
274 var submitSuccessCallback = function(o) {
274 var submitSuccessCallback = function(o) {
275 if (status) {
275 if (status) {
276 location.reload(true);
276 location.reload(true);
277 } else {
277 } else {
278 $('#injected_page_comments').append(o.rendered_text);
278 $('#injected_page_comments').append(o.rendered_text);
279 self.resetCommentFormState();
279 self.resetCommentFormState();
280 bindDeleteCommentButtons();
280 bindDeleteCommentButtons();
281 timeagoActivate();
281 timeagoActivate();
282 }
282 }
283 };
283 };
284 var submitFailCallback = function(){
284 var submitFailCallback = function(){
285 self.resetCommentFormState(text)
285 self.resetCommentFormState(text)
286 };
286 };
287 self.submitAjaxPOST(
287 self.submitAjaxPOST(
288 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
288 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
289 };
289 };
290
290
291 this.previewSuccessCallback = function(o) {
291 this.previewSuccessCallback = function(o) {
292 $(self.previewBoxSelector).html(o);
292 $(self.previewBoxSelector).html(o);
293 $(self.previewBoxSelector).removeClass('unloaded');
293 $(self.previewBoxSelector).removeClass('unloaded');
294
294
295 // swap buttons
295 // swap buttons
296 $(self.previewButton).hide();
296 $(self.previewButton).hide();
297 $(self.editButton).show();
297 $(self.editButton).show();
298
298
299 // unlock buttons
299 // unlock buttons
300 self.setActionButtonsDisabled(false);
300 self.setActionButtonsDisabled(false);
301 };
301 };
302
302
303 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
303 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
304 excludeCancelBtn = excludeCancelBtn || false;
304 excludeCancelBtn = excludeCancelBtn || false;
305 submitEvent = submitEvent || false;
305 submitEvent = submitEvent || false;
306
306
307 $(this.editButton).prop('disabled', state);
307 $(this.editButton).prop('disabled', state);
308 $(this.previewButton).prop('disabled', state);
308 $(this.previewButton).prop('disabled', state);
309
309
310 if (!excludeCancelBtn) {
310 if (!excludeCancelBtn) {
311 $(this.cancelButton).prop('disabled', state);
311 $(this.cancelButton).prop('disabled', state);
312 }
312 }
313
313
314 var submitState = state;
314 var submitState = state;
315 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
315 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
316 // if the value of commit review status is set, we allow
316 // if the value of commit review status is set, we allow
317 // submit button, but only on Main form, lineNo means inline
317 // submit button, but only on Main form, lineNo means inline
318 submitState = false
318 submitState = false
319 }
319 }
320 $(this.submitButton).prop('disabled', submitState);
320 $(this.submitButton).prop('disabled', submitState);
321 if (submitEvent) {
321 if (submitEvent) {
322 $(this.submitButton).val(_gettext('Submitting...'));
322 $(this.submitButton).val(_gettext('Submitting...'));
323 } else {
323 } else {
324 $(this.submitButton).val(this.submitButtonText);
324 $(this.submitButton).val(this.submitButtonText);
325 }
325 }
326
326
327 };
327 };
328
328
329 // lock preview/edit/submit buttons on load, but exclude cancel button
329 // lock preview/edit/submit buttons on load, but exclude cancel button
330 var excludeCancelBtn = true;
330 var excludeCancelBtn = true;
331 this.setActionButtonsDisabled(true, excludeCancelBtn);
331 this.setActionButtonsDisabled(true, excludeCancelBtn);
332
332
333 // anonymous users don't have access to initialized CM instance
333 // anonymous users don't have access to initialized CM instance
334 if (this.cm !== undefined){
334 if (this.cm !== undefined){
335 this.cm.on('change', function(cMirror) {
335 this.cm.on('change', function(cMirror) {
336 if (cMirror.getValue() === "") {
336 if (cMirror.getValue() === "") {
337 self.setActionButtonsDisabled(true, excludeCancelBtn)
337 self.setActionButtonsDisabled(true, excludeCancelBtn)
338 } else {
338 } else {
339 self.setActionButtonsDisabled(false, excludeCancelBtn)
339 self.setActionButtonsDisabled(false, excludeCancelBtn)
340 }
340 }
341 });
341 });
342 }
342 }
343
343
344 $(this.editButton).on('click', function(e) {
344 $(this.editButton).on('click', function(e) {
345 e.preventDefault();
345 e.preventDefault();
346
346
347 $(self.previewButton).show();
347 $(self.previewButton).show();
348 $(self.previewContainer).hide();
348 $(self.previewContainer).hide();
349 $(self.editButton).hide();
349 $(self.editButton).hide();
350 $(self.editContainer).show();
350 $(self.editContainer).show();
351
351
352 });
352 });
353
353
354 $(this.previewButton).on('click', function(e) {
354 $(this.previewButton).on('click', function(e) {
355 e.preventDefault();
355 e.preventDefault();
356 var text = self.cm.getValue();
356 var text = self.cm.getValue();
357
357
358 if (text === "") {
358 if (text === "") {
359 return;
359 return;
360 }
360 }
361
361
362 var postData = {
362 var postData = {
363 'text': text,
363 'text': text,
364 'renderer': DEFAULT_RENDERER,
364 'renderer': DEFAULT_RENDERER,
365 'csrf_token': CSRF_TOKEN
365 'csrf_token': CSRF_TOKEN
366 };
366 };
367
367
368 // lock ALL buttons on preview
368 // lock ALL buttons on preview
369 self.setActionButtonsDisabled(true);
369 self.setActionButtonsDisabled(true);
370
370
371 $(self.previewBoxSelector).addClass('unloaded');
371 $(self.previewBoxSelector).addClass('unloaded');
372 $(self.previewBoxSelector).html(_gettext('Loading ...'));
372 $(self.previewBoxSelector).html(_gettext('Loading ...'));
373 $(self.editContainer).hide();
373 $(self.editContainer).hide();
374 $(self.previewContainer).show();
374 $(self.previewContainer).show();
375
375
376 // by default we reset state of comment preserving the text
376 // by default we reset state of comment preserving the text
377 var previewFailCallback = function(){
377 var previewFailCallback = function(){
378 self.resetCommentFormState(text)
378 self.resetCommentFormState(text)
379 };
379 };
380 self.submitAjaxPOST(
380 self.submitAjaxPOST(
381 self.previewUrl, postData, self.previewSuccessCallback, previewFailCallback);
381 self.previewUrl, postData, self.previewSuccessCallback, previewFailCallback);
382
382
383 });
383 });
384
384
385 $(this.submitForm).submit(function(e) {
385 $(this.submitForm).submit(function(e) {
386 e.preventDefault();
386 e.preventDefault();
387 var allowedToSubmit = self.isAllowedToSubmit();
387 var allowedToSubmit = self.isAllowedToSubmit();
388 if (!allowedToSubmit){
388 if (!allowedToSubmit){
389 return false;
389 return false;
390 }
390 }
391 self.handleFormSubmit();
391 self.handleFormSubmit();
392 });
392 });
393
393
394 }
394 }
395
395
396 return CommentForm;
396 return CommentForm;
397 })();
397 })();
398
398
399 var CommentsController = function() { /* comments controller */
399 var CommentsController = function() { /* comments controller */
400 var self = this;
400 var self = this;
401
401
402 this.cancelComment = function(node) {
402 this.cancelComment = function(node) {
403 var $node = $(node);
403 var $node = $(node);
404 var $td = $node.closest('td');
404 var $td = $node.closest('td');
405 $node.closest('.comment-inline-form').removeClass('comment-inline-form-open');
405 $node.closest('.comment-inline-form').removeClass('comment-inline-form-open');
406 return false;
406 return false;
407 };
407 };
408
408
409 this.getLineNumber = function(node) {
409 this.getLineNumber = function(node) {
410 var $node = $(node);
410 var $node = $(node);
411 return $node.closest('td').attr('data-line-number');
411 return $node.closest('td').attr('data-line-number');
412 };
412 };
413
413
414 this.scrollToComment = function(node, offset) {
414 this.scrollToComment = function(node, offset) {
415 if (!node) {
415 if (!node) {
416 node = $('.comment-selected');
416 node = $('.comment-selected');
417 if (!node.length) {
417 if (!node.length) {
418 node = $('comment-current')
418 node = $('comment-current')
419 }
419 }
420 }
420 }
421 $comment = $(node).closest('.comment-current');
421 $comment = $(node).closest('.comment-current');
422 $comments = $('.comment-current');
422 $comments = $('.comment-current');
423
423
424 $('.comment-selected').removeClass('comment-selected');
424 $('.comment-selected').removeClass('comment-selected');
425
425
426 var nextIdx = $('.comment-current').index($comment) + offset;
426 var nextIdx = $('.comment-current').index($comment) + offset;
427 if (nextIdx >= $comments.length) {
427 if (nextIdx >= $comments.length) {
428 nextIdx = 0;
428 nextIdx = 0;
429 }
429 }
430 var $next = $('.comment-current').eq(nextIdx);
430 var $next = $('.comment-current').eq(nextIdx);
431 var $cb = $next.closest('.cb');
431 var $cb = $next.closest('.cb');
432 $cb.removeClass('cb-collapsed');
432 $cb.removeClass('cb-collapsed');
433
433
434 var $filediffCollapseState = $cb.closest('.filediff').prev();
434 var $filediffCollapseState = $cb.closest('.filediff').prev();
435 $filediffCollapseState.prop('checked', false);
435 $filediffCollapseState.prop('checked', false);
436 $next.addClass('comment-selected');
436 $next.addClass('comment-selected');
437 scrollToElement($next);
437 scrollToElement($next);
438 return false;
438 return false;
439 };
439 };
440
440
441 this.nextComment = function(node) {
441 this.nextComment = function(node) {
442 return self.scrollToComment(node, 1);
442 return self.scrollToComment(node, 1);
443 };
443 };
444
444
445 this.prevComment = function(node) {
445 this.prevComment = function(node) {
446 return self.scrollToComment(node, -1);
446 return self.scrollToComment(node, -1);
447 };
447 };
448
448
449 this.deleteComment = function(node) {
449 this.deleteComment = function(node) {
450 if (!confirm(_gettext('Delete this comment?'))) {
450 if (!confirm(_gettext('Delete this comment?'))) {
451 return false;
451 return false;
452 }
452 }
453 var $node = $(node);
453 var $node = $(node);
454 var $td = $node.closest('td');
454 var $td = $node.closest('td');
455 var $comment = $node.closest('.comment');
455 var $comment = $node.closest('.comment');
456 var comment_id = $comment.attr('data-comment-id');
456 var comment_id = $comment.attr('data-comment-id');
457 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
457 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
458 var postData = {
458 var postData = {
459 '_method': 'delete',
459 '_method': 'delete',
460 'csrf_token': CSRF_TOKEN
460 'csrf_token': CSRF_TOKEN
461 };
461 };
462
462
463 $comment.addClass('comment-deleting');
463 $comment.addClass('comment-deleting');
464 $comment.hide('fast');
464 $comment.hide('fast');
465
465
466 var success = function(response) {
466 var success = function(response) {
467 $comment.remove();
467 $comment.remove();
468 return false;
468 return false;
469 };
469 };
470 var failure = function(data, textStatus, xhr) {
470 var failure = function(data, textStatus, xhr) {
471 alert("error processing request: " + textStatus);
471 alert("error processing request: " + textStatus);
472 $comment.show('fast');
472 $comment.show('fast');
473 $comment.removeClass('comment-deleting');
473 $comment.removeClass('comment-deleting');
474 return false;
474 return false;
475 };
475 };
476 ajaxPOST(url, postData, success, failure);
476 ajaxPOST(url, postData, success, failure);
477 };
477 };
478
478
479 this.toggleWideMode = function (node) {
479 this.toggleWideMode = function (node) {
480 if ($('#content').hasClass('wrapper')) {
480 if ($('#content').hasClass('wrapper')) {
481 $('#content').removeClass("wrapper");
481 $('#content').removeClass("wrapper");
482 $('#content').addClass("wide-mode-wrapper");
482 $('#content').addClass("wide-mode-wrapper");
483 $(node).addClass('btn-success');
483 $(node).addClass('btn-success');
484 } else {
484 } else {
485 $('#content').removeClass("wide-mode-wrapper");
485 $('#content').removeClass("wide-mode-wrapper");
486 $('#content').addClass("wrapper");
486 $('#content').addClass("wrapper");
487 $(node).removeClass('btn-success');
487 $(node).removeClass('btn-success');
488 }
488 }
489 return false;
489 return false;
490 };
490 };
491
491
492 this.toggleComments = function(node, show) {
492 this.toggleComments = function(node, show) {
493 var $filediff = $(node).closest('.filediff');
493 var $filediff = $(node).closest('.filediff');
494 if (show === true) {
494 if (show === true) {
495 $filediff.removeClass('hide-comments');
495 $filediff.removeClass('hide-comments');
496 } else if (show === false) {
496 } else if (show === false) {
497 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
497 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
498 $filediff.addClass('hide-comments');
498 $filediff.addClass('hide-comments');
499 } else {
499 } else {
500 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
500 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
501 $filediff.toggleClass('hide-comments');
501 $filediff.toggleClass('hide-comments');
502 }
502 }
503 return false;
503 return false;
504 };
504 };
505
505
506 this.toggleLineComments = function(node) {
506 this.toggleLineComments = function(node) {
507 self.toggleComments(node, true);
507 self.toggleComments(node, true);
508 var $node = $(node);
508 var $node = $(node);
509 $node.closest('tr').toggleClass('hide-line-comments');
509 $node.closest('tr').toggleClass('hide-line-comments');
510 };
510 };
511
511
512 this.createComment = function(node) {
512 this.createComment = function(node) {
513 var $node = $(node);
513 var $node = $(node);
514 var $td = $node.closest('td');
514 var $td = $node.closest('td');
515 var $form = $td.find('.comment-inline-form');
515 var $form = $td.find('.comment-inline-form');
516
516
517 if (!$form.length) {
517 if (!$form.length) {
518 var tmpl = $('#cb-comment-inline-form-template').html();
518 var tmpl = $('#cb-comment-inline-form-template').html();
519 var $filediff = $node.closest('.filediff');
519 var $filediff = $node.closest('.filediff');
520 $filediff.removeClass('hide-comments');
520 $filediff.removeClass('hide-comments');
521 var f_path = $filediff.attr('data-f-path');
521 var f_path = $filediff.attr('data-f-path');
522 var lineno = self.getLineNumber(node);
522 var lineno = self.getLineNumber(node);
523 tmpl = tmpl.format(f_path, lineno);
523 tmpl = tmpl.format(f_path, lineno);
524 $form = $(tmpl);
524 $form = $(tmpl);
525
525
526 var $comments = $td.find('.inline-comments');
526 var $comments = $td.find('.inline-comments');
527 if (!$comments.length) {
527 if (!$comments.length) {
528 $comments = $(
528 $comments = $(
529 $('#cb-comments-inline-container-template').html());
529 $('#cb-comments-inline-container-template').html());
530 $td.append($comments);
530 $td.append($comments);
531 }
531 }
532
532
533 $td.find('.cb-comment-add-button').before($form);
533 $td.find('.cb-comment-add-button').before($form);
534
534
535 var pullRequestId = templateContext.pull_request_data.pull_request_id;
535 var pullRequestId = templateContext.pull_request_data.pull_request_id;
536 var commitId = templateContext.commit_data.commit_id;
536 var commitId = templateContext.commit_data.commit_id;
537 var _form = $form[0];
537 var _form = $form[0];
538 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
538 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
539 var cm = commentForm.getCmInstance();
539 var cm = commentForm.getCmInstance();
540
540
541 // set a CUSTOM submit handler for inline comments.
541 // set a CUSTOM submit handler for inline comments.
542 commentForm.setHandleFormSubmit(function(o) {
542 commentForm.setHandleFormSubmit(function(o) {
543 var text = commentForm.cm.getValue();
543 var text = commentForm.cm.getValue();
544
544
545 if (text === "") {
545 if (text === "") {
546 return;
546 return;
547 }
547 }
548
548
549 if (lineno === undefined) {
549 if (lineno === undefined) {
550 alert('missing line !');
550 alert('missing line !');
551 return;
551 return;
552 }
552 }
553 if (f_path === undefined) {
553 if (f_path === undefined) {
554 alert('missing file path !');
554 alert('missing file path !');
555 return;
555 return;
556 }
556 }
557
557
558 var excludeCancelBtn = false;
558 var excludeCancelBtn = false;
559 var submitEvent = true;
559 var submitEvent = true;
560 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
560 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
561 commentForm.cm.setOption("readOnly", true);
561 commentForm.cm.setOption("readOnly", true);
562 var postData = {
562 var postData = {
563 'text': text,
563 'text': text,
564 'f_path': f_path,
564 'f_path': f_path,
565 'line': lineno,
565 'line': lineno,
566 'csrf_token': CSRF_TOKEN
566 'csrf_token': CSRF_TOKEN
567 };
567 };
568 var submitSuccessCallback = function(json_data) {
568 var submitSuccessCallback = function(json_data) {
569 $form.remove();
569 $form.remove();
570 try {
570 try {
571 var html = json_data.rendered_text;
571 var html = json_data.rendered_text;
572 var lineno = json_data.line_no;
572 var lineno = json_data.line_no;
573 var target_id = json_data.target_id;
573 var target_id = json_data.target_id;
574
574
575 $comments.find('.cb-comment-add-button').before(html);
575 $comments.find('.cb-comment-add-button').before(html);
576
576
577 } catch (e) {
577 } catch (e) {
578 console.error(e);
578 console.error(e);
579 }
579 }
580
580
581 // re trigger the linkification of next/prev navigation
581 // re trigger the linkification of next/prev navigation
582 linkifyComments($('.inline-comment-injected'));
582 linkifyComments($('.inline-comment-injected'));
583 timeagoActivate();
583 timeagoActivate();
584 bindDeleteCommentButtons();
584 bindDeleteCommentButtons();
585 commentForm.setActionButtonsDisabled(false);
585 commentForm.setActionButtonsDisabled(false);
586
586
587 };
587 };
588 var submitFailCallback = function(){
588 var submitFailCallback = function(){
589 commentForm.resetCommentFormState(text)
589 commentForm.resetCommentFormState(text)
590 };
590 };
591 commentForm.submitAjaxPOST(
591 commentForm.submitAjaxPOST(
592 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
592 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
593 });
593 });
594
594
595 setTimeout(function() {
595 setTimeout(function() {
596 // callbacks
596 // callbacks
597 if (cm !== undefined) {
597 if (cm !== undefined) {
598 cm.focus();
598 cm.focus();
599 }
599 }
600 }, 10);
600 }, 10);
601
601
602 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
602 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
603 form: _form,
603 form: _form,
604 parent: $td[0],
604 parent: $td[0],
605 lineno: lineno,
605 lineno: lineno,
606 f_path: f_path}
606 f_path: f_path}
607 );
607 );
608 }
608 }
609
609
610 $form.addClass('comment-inline-form-open');
610 $form.addClass('comment-inline-form-open');
611 };
611 };
612
612
613 this.renderInlineComments = function(file_comments) {
613 this.renderInlineComments = function(file_comments) {
614 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
614 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
615
615
616 for (var i = 0; i < file_comments.length; i++) {
616 for (var i = 0; i < file_comments.length; i++) {
617 var box = file_comments[i];
617 var box = file_comments[i];
618
618
619 var target_id = $(box).attr('target_id');
619 var target_id = $(box).attr('target_id');
620
620
621 // actually comments with line numbers
621 // actually comments with line numbers
622 var comments = box.children;
622 var comments = box.children;
623
623
624 for (var j = 0; j < comments.length; j++) {
624 for (var j = 0; j < comments.length; j++) {
625 var data = {
625 var data = {
626 'rendered_text': comments[j].outerHTML,
626 'rendered_text': comments[j].outerHTML,
627 'line_no': $(comments[j]).attr('line'),
627 'line_no': $(comments[j]).attr('line'),
628 'target_id': target_id
628 'target_id': target_id
629 };
629 };
630 }
630 }
631 }
631 }
632
632
633 // since order of injection is random, we're now re-iterating
633 // since order of injection is random, we're now re-iterating
634 // from correct order and filling in links
634 // from correct order and filling in links
635 linkifyComments($('.inline-comment-injected'));
635 linkifyComments($('.inline-comment-injected'));
636 bindDeleteCommentButtons();
636 bindDeleteCommentButtons();
637 firefoxAnchorFix();
637 firefoxAnchorFix();
638 };
638 };
639
639
640 }; No newline at end of file
640 };
@@ -1,577 +1,577 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.source_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
398 <span onclick="return Rhodecode.comments.createComment(this)"
398 <span onclick="return Rhodecode.comments.createComment(this)"
399 class="btn btn-secondary cb-comment-add-button ${'comment-outdated' if comments and comments[-1].outdated else ''}"
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 ''}">
400 style="${'display: none;' if comments and comments[-1].outdated else ''}">
401 ${_('Add another comment')}
401 ${_('Add another comment')}
402 </span>
402 </span>
403
403
404 </div>
404 </div>
405 </%def>
405 </%def>
406
406
407
407
408 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
408 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
409 %for i, line in enumerate(hunk.sideside):
409 %for i, line in enumerate(hunk.sideside):
410 <%
410 <%
411 old_line_anchor, new_line_anchor = None, None
411 old_line_anchor, new_line_anchor = None, None
412 if line.original.lineno:
412 if line.original.lineno:
413 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')
414 if line.modified.lineno:
414 if line.modified.lineno:
415 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')
416 %>
416 %>
417 <tr class="cb-line">
417 <tr class="cb-line">
418 <td class="cb-data ${action_class(line.original.action)}"
418 <td class="cb-data ${action_class(line.original.action)}"
419 data-line-number="${line.original.lineno}"
419 data-line-number="${line.original.lineno}"
420 >
420 >
421 <div>
421 <div>
422 %if line.original.comments:
422 %if line.original.comments:
423 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
423 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
424 %endif
424 %endif
425 </div>
425 </div>
426 </td>
426 </td>
427 <td class="cb-lineno ${action_class(line.original.action)}"
427 <td class="cb-lineno ${action_class(line.original.action)}"
428 data-line-number="${line.original.lineno}"
428 data-line-number="${line.original.lineno}"
429 %if old_line_anchor:
429 %if old_line_anchor:
430 id="${old_line_anchor}"
430 id="${old_line_anchor}"
431 %endif
431 %endif
432 >
432 >
433 %if line.original.lineno:
433 %if line.original.lineno:
434 <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>
435 %endif
435 %endif
436 </td>
436 </td>
437 <td class="cb-content ${action_class(line.original.action)}"
437 <td class="cb-content ${action_class(line.original.action)}"
438 data-line-number="o${line.original.lineno}"
438 data-line-number="o${line.original.lineno}"
439 >
439 >
440 %if use_comments and line.original.lineno:
440 %if use_comments and line.original.lineno:
441 ${render_add_comment_button()}
441 ${render_add_comment_button()}
442 %endif
442 %endif
443 <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>
444 %if use_comments and line.original.lineno and line.original.comments:
444 %if use_comments and line.original.lineno and line.original.comments:
445 ${inline_comments_container(line.original.comments)}
445 ${inline_comments_container(line.original.comments)}
446 %endif
446 %endif
447 </td>
447 </td>
448 <td class="cb-data ${action_class(line.modified.action)}"
448 <td class="cb-data ${action_class(line.modified.action)}"
449 data-line-number="${line.modified.lineno}"
449 data-line-number="${line.modified.lineno}"
450 >
450 >
451 <div>
451 <div>
452 %if line.modified.comments:
452 %if line.modified.comments:
453 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
453 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
454 %endif
454 %endif
455 </div>
455 </div>
456 </td>
456 </td>
457 <td class="cb-lineno ${action_class(line.modified.action)}"
457 <td class="cb-lineno ${action_class(line.modified.action)}"
458 data-line-number="${line.modified.lineno}"
458 data-line-number="${line.modified.lineno}"
459 %if new_line_anchor:
459 %if new_line_anchor:
460 id="${new_line_anchor}"
460 id="${new_line_anchor}"
461 %endif
461 %endif
462 >
462 >
463 %if line.modified.lineno:
463 %if line.modified.lineno:
464 <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>
465 %endif
465 %endif
466 </td>
466 </td>
467 <td class="cb-content ${action_class(line.modified.action)}"
467 <td class="cb-content ${action_class(line.modified.action)}"
468 data-line-number="n${line.modified.lineno}"
468 data-line-number="n${line.modified.lineno}"
469 >
469 >
470 %if use_comments and line.modified.lineno:
470 %if use_comments and line.modified.lineno:
471 ${render_add_comment_button()}
471 ${render_add_comment_button()}
472 %endif
472 %endif
473 <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>
474 %if use_comments and line.modified.lineno and line.modified.comments:
474 %if use_comments and line.modified.lineno and line.modified.comments:
475 ${inline_comments_container(line.modified.comments)}
475 ${inline_comments_container(line.modified.comments)}
476 %endif
476 %endif
477 </td>
477 </td>
478 </tr>
478 </tr>
479 %endfor
479 %endfor
480 </%def>
480 </%def>
481
481
482
482
483 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
483 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
484 %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:
485 <%
485 <%
486 old_line_anchor, new_line_anchor = None, None
486 old_line_anchor, new_line_anchor = None, None
487 if old_line_no:
487 if old_line_no:
488 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')
489 if new_line_no:
489 if new_line_no:
490 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')
491 %>
491 %>
492 <tr class="cb-line">
492 <tr class="cb-line">
493 <td class="cb-data ${action_class(action)}">
493 <td class="cb-data ${action_class(action)}">
494 <div>
494 <div>
495 %if comments:
495 %if comments:
496 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
496 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
497 %endif
497 %endif
498 </div>
498 </div>
499 </td>
499 </td>
500 <td class="cb-lineno ${action_class(action)}"
500 <td class="cb-lineno ${action_class(action)}"
501 data-line-number="${old_line_no}"
501 data-line-number="${old_line_no}"
502 %if old_line_anchor:
502 %if old_line_anchor:
503 id="${old_line_anchor}"
503 id="${old_line_anchor}"
504 %endif
504 %endif
505 >
505 >
506 %if old_line_anchor:
506 %if old_line_anchor:
507 <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>
508 %endif
508 %endif
509 </td>
509 </td>
510 <td class="cb-lineno ${action_class(action)}"
510 <td class="cb-lineno ${action_class(action)}"
511 data-line-number="${new_line_no}"
511 data-line-number="${new_line_no}"
512 %if new_line_anchor:
512 %if new_line_anchor:
513 id="${new_line_anchor}"
513 id="${new_line_anchor}"
514 %endif
514 %endif
515 >
515 >
516 %if new_line_anchor:
516 %if new_line_anchor:
517 <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>
518 %endif
518 %endif
519 </td>
519 </td>
520 <td class="cb-content ${action_class(action)}"
520 <td class="cb-content ${action_class(action)}"
521 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}"
522 >
522 >
523 %if use_comments:
523 %if use_comments:
524 ${render_add_comment_button()}
524 ${render_add_comment_button()}
525 %endif
525 %endif
526 <span class="cb-code">${action} ${content or '' | n}</span>
526 <span class="cb-code">${action} ${content or '' | n}</span>
527 %if use_comments and comments:
527 %if use_comments and comments:
528 ${inline_comments_container(comments)}
528 ${inline_comments_container(comments)}
529 %endif
529 %endif
530 </td>
530 </td>
531 </tr>
531 </tr>
532 %endfor
532 %endfor
533 </%def>
533 </%def>
534
534
535 <%def name="render_add_comment_button()">
535 <%def name="render_add_comment_button()">
536 <button
536 <button
537 class="btn btn-small btn-primary cb-comment-box-opener"
537 class="btn btn-small btn-primary cb-comment-box-opener"
538 onclick="return Rhodecode.comments.createComment(this)"
538 onclick="return Rhodecode.comments.createComment(this)"
539 ><span>+</span></button>
539 ><span>+</span></button>
540 </%def>
540 </%def>
541
541
542 <%def name="render_diffset_menu()">
542 <%def name="render_diffset_menu()">
543
543
544 <div class="diffset-menu clearinner">
544 <div class="diffset-menu clearinner">
545 <div class="pull-right">
545 <div class="pull-right">
546 <div class="btn-group">
546 <div class="btn-group">
547 <a
547 <a
548 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
548 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
549 title="${_('View side by side')}"
549 title="${_('View side by side')}"
550 href="${h.url_replace(diffmode='sideside')}">
550 href="${h.url_replace(diffmode='sideside')}">
551 <span>${_('Side by Side')}</span>
551 <span>${_('Side by Side')}</span>
552 </a>
552 </a>
553 <a
553 <a
554 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
554 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
555 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
555 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
556 <span>${_('Unified')}</span>
556 <span>${_('Unified')}</span>
557 </a>
557 </a>
558 </div>
558 </div>
559 </div>
559 </div>
560 <div class="pull-left">
560 <div class="pull-left">
561 <div class="btn-group">
561 <div class="btn-group">
562 <a
562 <a
563 class="btn"
563 class="btn"
564 href="#"
564 href="#"
565 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All')}</a>
565 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All')}</a>
566 <a
566 <a
567 class="btn"
567 class="btn"
568 href="#"
568 href="#"
569 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All')}</a>
569 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All')}</a>
570 <a
570 <a
571 class="btn"
571 class="btn"
572 href="#"
572 href="#"
573 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode')}</a>
573 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode')}</a>
574 </div>
574 </div>
575 </div>
575 </div>
576 </div>
576 </div>
577 </%def>
577 </%def>
General Comments 0
You need to be logged in to leave comments. Login now