##// END OF EJS Templates
implements #308 rewrote diffs to enable displaying full diff on each file...
marcink -
r1789:17caf4ef beta
parent child Browse files
Show More
@@ -1,413 +1,365 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.changeset
3 rhodecode.controllers.changeset
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 changeset controller for pylons showoing changes beetween
6 changeset controller for pylons showoing changes beetween
7 revisions
7 revisions
8
8
9 :created_on: Apr 25, 2010
9 :created_on: Apr 25, 2010
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 import logging
26 import logging
27 import traceback
27 import traceback
28 from collections import defaultdict
28 from collections import defaultdict
29 from webob.exc import HTTPForbidden
29 from webob.exc import HTTPForbidden
30
30
31 from pylons import tmpl_context as c, url, request, response
31 from pylons import tmpl_context as c, url, request, response
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.decorators import jsonify
34 from pylons.decorators import jsonify
35
35
36 from vcs.exceptions import RepositoryError, ChangesetError, \
36 from vcs.exceptions import RepositoryError, ChangesetError, \
37 ChangesetDoesNotExistError
37 ChangesetDoesNotExistError
38 from vcs.nodes import FileNode
38 from vcs.nodes import FileNode
39
39
40 import rhodecode.lib.helpers as h
40 import rhodecode.lib.helpers as h
41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
42 from rhodecode.lib.base import BaseRepoController, render
42 from rhodecode.lib.base import BaseRepoController, render
43 from rhodecode.lib.utils import EmptyChangeset
43 from rhodecode.lib.utils import EmptyChangeset
44 from rhodecode.lib.compat import OrderedDict
44 from rhodecode.lib.compat import OrderedDict
45 from rhodecode.lib import diffs
45 from rhodecode.lib import diffs
46 from rhodecode.model.db import ChangesetComment
46 from rhodecode.model.db import ChangesetComment
47 from rhodecode.model.comment import ChangesetCommentsModel
47 from rhodecode.model.comment import ChangesetCommentsModel
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.lib.diffs import wrapped_diff
49
50
50 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
51
52
52
53
53 def anchor_url(revision, path):
54 def anchor_url(revision, path):
54 fid = h.FID(revision, path)
55 fid = h.FID(revision, path)
55 return h.url.current(anchor=fid, **request.GET)
56 return h.url.current(anchor=fid, **request.GET)
56
57
57
58
58 def get_ignore_ws(fid, GET):
59 def get_ignore_ws(fid, GET):
59 ig_ws_global = request.GET.get('ignorews')
60 ig_ws_global = request.GET.get('ignorews')
60 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
61 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
61 if ig_ws:
62 if ig_ws:
62 try:
63 try:
63 return int(ig_ws[0].split(':')[-1])
64 return int(ig_ws[0].split(':')[-1])
64 except:
65 except:
65 pass
66 pass
66 return ig_ws_global
67 return ig_ws_global
67
68
68
69
69 def _ignorews_url(fileid=None):
70 def _ignorews_url(fileid=None):
70
71
71 params = defaultdict(list)
72 params = defaultdict(list)
72 lbl = _('show white space')
73 lbl = _('show white space')
73 ig_ws = get_ignore_ws(fileid, request.GET)
74 ig_ws = get_ignore_ws(fileid, request.GET)
74 ln_ctx = get_line_ctx(fileid, request.GET)
75 ln_ctx = get_line_ctx(fileid, request.GET)
75 # global option
76 # global option
76 if fileid is None:
77 if fileid is None:
77 if ig_ws is None:
78 if ig_ws is None:
78 params['ignorews'] += [1]
79 params['ignorews'] += [1]
79 lbl = _('ignore white space')
80 lbl = _('ignore white space')
80 ctx_key = 'context'
81 ctx_key = 'context'
81 ctx_val = ln_ctx
82 ctx_val = ln_ctx
82 # per file options
83 # per file options
83 else:
84 else:
84 if ig_ws is None:
85 if ig_ws is None:
85 params[fileid] += ['WS:1']
86 params[fileid] += ['WS:1']
86 lbl = _('ignore white space')
87 lbl = _('ignore white space')
87
88
88 ctx_key = fileid
89 ctx_key = fileid
89 ctx_val = 'C:%s' % ln_ctx
90 ctx_val = 'C:%s' % ln_ctx
90 # if we have passed in ln_ctx pass it along to our params
91 # if we have passed in ln_ctx pass it along to our params
91 if ln_ctx:
92 if ln_ctx:
92 params[ctx_key] += [ctx_val]
93 params[ctx_key] += [ctx_val]
93
94
94 params['anchor'] = fileid
95 params['anchor'] = fileid
95 return h.link_to(lbl, h.url.current(**params))
96 return h.link_to(lbl, h.url.current(**params))
96
97
97
98
98 def get_line_ctx(fid, GET):
99 def get_line_ctx(fid, GET):
99 ln_ctx_global = request.GET.get('context')
100 ln_ctx_global = request.GET.get('context')
100 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101
102
102 if ln_ctx:
103 if ln_ctx:
103 retval = ln_ctx[0].split(':')[-1]
104 retval = ln_ctx[0].split(':')[-1]
104 else:
105 else:
105 retval = ln_ctx_global
106 retval = ln_ctx_global
106
107
107 try:
108 try:
108 return int(retval)
109 return int(retval)
109 except:
110 except:
110 return
111 return
111
112
112
113
113 def _context_url(fileid=None):
114 def _context_url(fileid=None):
114 """
115 """
115 Generates url for context lines
116 Generates url for context lines
116
117
117 :param fileid:
118 :param fileid:
118 """
119 """
119 ig_ws = get_ignore_ws(fileid, request.GET)
120 ig_ws = get_ignore_ws(fileid, request.GET)
120 ln_ctx = (get_line_ctx(fileid, request.GET) or 3) * 2
121 ln_ctx = (get_line_ctx(fileid, request.GET) or 3) * 2
121
122
122 params = defaultdict(list)
123 params = defaultdict(list)
123
124
124 # global option
125 # global option
125 if fileid is None:
126 if fileid is None:
126 if ln_ctx > 0:
127 if ln_ctx > 0:
127 params['context'] += [ln_ctx]
128 params['context'] += [ln_ctx]
128
129
129 if ig_ws:
130 if ig_ws:
130 ig_ws_key = 'ignorews'
131 ig_ws_key = 'ignorews'
131 ig_ws_val = 1
132 ig_ws_val = 1
132
133
133 # per file option
134 # per file option
134 else:
135 else:
135 params[fileid] += ['C:%s' % ln_ctx]
136 params[fileid] += ['C:%s' % ln_ctx]
136 ig_ws_key = fileid
137 ig_ws_key = fileid
137 ig_ws_val = 'WS:%s' % 1
138 ig_ws_val = 'WS:%s' % 1
138
139
139 if ig_ws:
140 if ig_ws:
140 params[ig_ws_key] += [ig_ws_val]
141 params[ig_ws_key] += [ig_ws_val]
141
142
142 lbl = _('%s line context') % ln_ctx
143 lbl = _('%s line context') % ln_ctx
143
144
144 params['anchor'] = fileid
145 params['anchor'] = fileid
145 return h.link_to(lbl, h.url.current(**params))
146 return h.link_to(lbl, h.url.current(**params))
146
147
147
148
148 def wrap_to_table(str_):
149 return '''<table class="code-difftable">
150 <tr class="line no-comment">
151 <td class="lineno new"></td>
152 <td class="code no-comment"><pre>%s</pre></td>
153 </tr>
154 </table>''' % str_
155
156
157 class ChangesetController(BaseRepoController):
149 class ChangesetController(BaseRepoController):
158
150
159 @LoginRequired()
151 @LoginRequired()
160 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
152 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
161 'repository.admin')
153 'repository.admin')
162 def __before__(self):
154 def __before__(self):
163 super(ChangesetController, self).__before__()
155 super(ChangesetController, self).__before__()
164 c.affected_files_cut_off = 60
156 c.affected_files_cut_off = 60
165
157
166 def index(self, revision):
158 def index(self, revision):
167
159
168 c.anchor_url = anchor_url
160 c.anchor_url = anchor_url
169 c.ignorews_url = _ignorews_url
161 c.ignorews_url = _ignorews_url
170 c.context_url = _context_url
162 c.context_url = _context_url
171
163
172 #get ranges of revisions if preset
164 #get ranges of revisions if preset
173 rev_range = revision.split('...')[:2]
165 rev_range = revision.split('...')[:2]
174 enable_comments = True
166 enable_comments = True
175 try:
167 try:
176 if len(rev_range) == 2:
168 if len(rev_range) == 2:
177 enable_comments = False
169 enable_comments = False
178 rev_start = rev_range[0]
170 rev_start = rev_range[0]
179 rev_end = rev_range[1]
171 rev_end = rev_range[1]
180 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
172 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
181 end=rev_end)
173 end=rev_end)
182 else:
174 else:
183 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
175 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
184
176
185 c.cs_ranges = list(rev_ranges)
177 c.cs_ranges = list(rev_ranges)
186 if not c.cs_ranges:
178 if not c.cs_ranges:
187 raise RepositoryError('Changeset range returned empty result')
179 raise RepositoryError('Changeset range returned empty result')
188
180
189 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
181 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
190 log.error(traceback.format_exc())
182 log.error(traceback.format_exc())
191 h.flash(str(e), category='warning')
183 h.flash(str(e), category='warning')
192 return redirect(url('home'))
184 return redirect(url('home'))
193
185
194 c.changes = OrderedDict()
186 c.changes = OrderedDict()
195 c.sum_added = 0
187
196 c.sum_removed = 0
188 c.lines_added = 0 # count of lines added
197 c.lines_added = 0
189 c.lines_deleted = 0 # count of lines removes
198 c.lines_deleted = 0
190
191 cumulative_diff = 0
199 c.cut_off = False # defines if cut off limit is reached
192 c.cut_off = False # defines if cut off limit is reached
200
193
201 c.comments = []
194 c.comments = []
202 c.inline_comments = []
195 c.inline_comments = []
203 c.inline_cnt = 0
196 c.inline_cnt = 0
204 # Iterate over ranges (default changeset view is always one changeset)
197 # Iterate over ranges (default changeset view is always one changeset)
205 for changeset in c.cs_ranges:
198 for changeset in c.cs_ranges:
206 c.comments.extend(ChangesetCommentsModel()\
199 c.comments.extend(ChangesetCommentsModel()\
207 .get_comments(c.rhodecode_db_repo.repo_id,
200 .get_comments(c.rhodecode_db_repo.repo_id,
208 changeset.raw_id))
201 changeset.raw_id))
209 inlines = ChangesetCommentsModel()\
202 inlines = ChangesetCommentsModel()\
210 .get_inline_comments(c.rhodecode_db_repo.repo_id,
203 .get_inline_comments(c.rhodecode_db_repo.repo_id,
211 changeset.raw_id)
204 changeset.raw_id)
212 c.inline_comments.extend(inlines)
205 c.inline_comments.extend(inlines)
213 c.changes[changeset.raw_id] = []
206 c.changes[changeset.raw_id] = []
214 try:
207 try:
215 changeset_parent = changeset.parents[0]
208 changeset_parent = changeset.parents[0]
216 except IndexError:
209 except IndexError:
217 changeset_parent = None
210 changeset_parent = None
218
211
219 #==================================================================
212 #==================================================================
220 # ADDED FILES
213 # ADDED FILES
221 #==================================================================
214 #==================================================================
222 for node in changeset.added:
215 for node in changeset.added:
223
216 fid = h.FID(revision, node.path)
224 filenode_old = FileNode(node.path, '', EmptyChangeset())
217 line_context_lcl = get_line_ctx(fid, request.GET)
225 if filenode_old.is_binary or node.is_binary:
218 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
226 diff = wrap_to_table(_('binary file'))
219 lim = self.cut_off_limit
227 st = (0, 0)
220 if cumulative_diff > self.cut_off_limit:
228 else:
221 lim = -1
229 # in this case node.size is good parameter since those are
222 size, cs1, cs2, diff, st = wrapped_diff(filenode_old=None,
230 # added nodes and their size defines how many changes were
223 filenode_new=node,
231 # made
224 cut_off_limit=lim,
232 c.sum_added += node.size
225 ignore_whitespace=ign_whitespace_lcl,
233 fid = h.FID(revision, node.path)
226 line_context=line_context_lcl,
234 line_context_lcl = get_line_ctx(fid, request.GET)
227 enable_comments=enable_comments)
235 ignore_whitespace_lcl = get_ignore_ws(fid, request.GET)
228 cumulative_diff += size
236 if c.sum_added < self.cut_off_limit:
237 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
238 ignore_whitespace=ignore_whitespace_lcl,
239 context=line_context_lcl)
240 d = diffs.DiffProcessor(f_gitdiff, format='gitdiff')
241
242 st = d.stat()
243 diff = d.as_html(enable_comments=enable_comments)
244
245 else:
246 diff = wrap_to_table(_('Changeset is to big and '
247 'was cut off, see raw '
248 'changeset instead'))
249 c.cut_off = True
250 break
251
252 cs1 = None
253 cs2 = node.last_changeset.raw_id
254 c.lines_added += st[0]
229 c.lines_added += st[0]
255 c.lines_deleted += st[1]
230 c.lines_deleted += st[1]
256 c.changes[changeset.raw_id].append(('added', node, diff,
231 c.changes[changeset.raw_id].append(('added', node, diff,
257 cs1, cs2, st))
232 cs1, cs2, st))
258
233
259 #==================================================================
234 #==================================================================
260 # CHANGED FILES
235 # CHANGED FILES
261 #==================================================================
236 #==================================================================
262 if not c.cut_off:
237 for node in changeset.changed:
263 for node in changeset.changed:
238 try:
264 try:
239 filenode_old = changeset_parent.get_node(node.path)
265 filenode_old = changeset_parent.get_node(node.path)
240 except ChangesetError:
266 except ChangesetError:
241 log.warning('Unable to fetch parent node for diff')
267 log.warning('Unable to fetch parent node for diff')
242 filenode_old = FileNode(node.path, '', EmptyChangeset())
268 filenode_old = FileNode(node.path, '',
269 EmptyChangeset())
270
271 if filenode_old.is_binary or node.is_binary:
272 diff = wrap_to_table(_('binary file'))
273 st = (0, 0)
274 else:
275
243
276 if c.sum_removed < self.cut_off_limit:
244 fid = h.FID(revision, node.path)
277 fid = h.FID(revision, node.path)
245 line_context_lcl = get_line_ctx(fid, request.GET)
278 line_context_lcl = get_line_ctx(fid, request.GET)
246 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
279 ignore_whitespace_lcl = get_ignore_ws(fid, request.GET,)
247 lim = self.cut_off_limit
280 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
248 if cumulative_diff > self.cut_off_limit:
281 ignore_whitespace=ignore_whitespace_lcl,
249 lim = -1
282 context=line_context_lcl)
250 size, cs1, cs2, diff, st = wrapped_diff(filenode_old=filenode_old,
283 d = diffs.DiffProcessor(f_gitdiff,
251 filenode_new=node,
284 format='gitdiff')
252 cut_off_limit=lim,
285 st = d.stat()
253 ignore_whitespace=ign_whitespace_lcl,
286 if (st[0] + st[1]) * 256 > self.cut_off_limit:
254 line_context=line_context_lcl,
287 diff = wrap_to_table(_('Diff is to big '
255 enable_comments=enable_comments)
288 'and was cut off, see '
256 cumulative_diff += size
289 'raw diff instead'))
257 c.lines_added += st[0]
290 else:
258 c.lines_deleted += st[1]
291 diff = d.as_html(enable_comments=enable_comments)
259 c.changes[changeset.raw_id].append(('changed', node, diff,
292
260 cs1, cs2, st))
293 if diff:
294 c.sum_removed += len(diff)
295 else:
296 diff = wrap_to_table(_('Changeset is to big and '
297 'was cut off, see raw '
298 'changeset instead'))
299 c.cut_off = True
300 break
301
302 cs1 = filenode_old.last_changeset.raw_id
303 cs2 = node.last_changeset.raw_id
304 c.lines_added += st[0]
305 c.lines_deleted += st[1]
306 c.changes[changeset.raw_id].append(('changed', node, diff,
307 cs1, cs2, st))
308
261
309 #==================================================================
262 #==================================================================
310 # REMOVED FILES
263 # REMOVED FILES
311 #==================================================================
264 #==================================================================
312 if not c.cut_off:
265 for node in changeset.removed:
313 for node in changeset.removed:
266 c.changes[changeset.raw_id].append(('removed', node, None,
314 c.changes[changeset.raw_id].append(('removed', node, None,
267 None, None, (0, 0)))
315 None, None, (0, 0)))
316
268
317 # count inline comments
269 # count inline comments
318 for path, lines in c.inline_comments:
270 for path, lines in c.inline_comments:
319 for comments in lines.values():
271 for comments in lines.values():
320 c.inline_cnt += len(comments)
272 c.inline_cnt += len(comments)
321
273
322 if len(c.cs_ranges) == 1:
274 if len(c.cs_ranges) == 1:
323 c.changeset = c.cs_ranges[0]
275 c.changeset = c.cs_ranges[0]
324 c.changes = c.changes[c.changeset.raw_id]
276 c.changes = c.changes[c.changeset.raw_id]
325
277
326 return render('changeset/changeset.html')
278 return render('changeset/changeset.html')
327 else:
279 else:
328 return render('changeset/changeset_range.html')
280 return render('changeset/changeset_range.html')
329
281
330 def raw_changeset(self, revision):
282 def raw_changeset(self, revision):
331
283
332 method = request.GET.get('diff', 'show')
284 method = request.GET.get('diff', 'show')
333 ignore_whitespace = request.GET.get('ignorews') == '1'
285 ignore_whitespace = request.GET.get('ignorews') == '1'
334 line_context = request.GET.get('context', 3)
286 line_context = request.GET.get('context', 3)
335 try:
287 try:
336 c.scm_type = c.rhodecode_repo.alias
288 c.scm_type = c.rhodecode_repo.alias
337 c.changeset = c.rhodecode_repo.get_changeset(revision)
289 c.changeset = c.rhodecode_repo.get_changeset(revision)
338 except RepositoryError:
290 except RepositoryError:
339 log.error(traceback.format_exc())
291 log.error(traceback.format_exc())
340 return redirect(url('home'))
292 return redirect(url('home'))
341 else:
293 else:
342 try:
294 try:
343 c.changeset_parent = c.changeset.parents[0]
295 c.changeset_parent = c.changeset.parents[0]
344 except IndexError:
296 except IndexError:
345 c.changeset_parent = None
297 c.changeset_parent = None
346 c.changes = []
298 c.changes = []
347
299
348 for node in c.changeset.added:
300 for node in c.changeset.added:
349 filenode_old = FileNode(node.path, '')
301 filenode_old = FileNode(node.path, '')
350 if filenode_old.is_binary or node.is_binary:
302 if filenode_old.is_binary or node.is_binary:
351 diff = _('binary file') + '\n'
303 diff = _('binary file') + '\n'
352 else:
304 else:
353 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
305 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
354 ignore_whitespace=ignore_whitespace,
306 ignore_whitespace=ignore_whitespace,
355 context=line_context)
307 context=line_context)
356 diff = diffs.DiffProcessor(f_gitdiff,
308 diff = diffs.DiffProcessor(f_gitdiff,
357 format='gitdiff').raw_diff()
309 format='gitdiff').raw_diff()
358
310
359 cs1 = None
311 cs1 = None
360 cs2 = node.last_changeset.raw_id
312 cs2 = node.last_changeset.raw_id
361 c.changes.append(('added', node, diff, cs1, cs2))
313 c.changes.append(('added', node, diff, cs1, cs2))
362
314
363 for node in c.changeset.changed:
315 for node in c.changeset.changed:
364 filenode_old = c.changeset_parent.get_node(node.path)
316 filenode_old = c.changeset_parent.get_node(node.path)
365 if filenode_old.is_binary or node.is_binary:
317 if filenode_old.is_binary or node.is_binary:
366 diff = _('binary file')
318 diff = _('binary file')
367 else:
319 else:
368 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
320 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
369 ignore_whitespace=ignore_whitespace,
321 ignore_whitespace=ignore_whitespace,
370 context=line_context)
322 context=line_context)
371 diff = diffs.DiffProcessor(f_gitdiff,
323 diff = diffs.DiffProcessor(f_gitdiff,
372 format='gitdiff').raw_diff()
324 format='gitdiff').raw_diff()
373
325
374 cs1 = filenode_old.last_changeset.raw_id
326 cs1 = filenode_old.last_changeset.raw_id
375 cs2 = node.last_changeset.raw_id
327 cs2 = node.last_changeset.raw_id
376 c.changes.append(('changed', node, diff, cs1, cs2))
328 c.changes.append(('changed', node, diff, cs1, cs2))
377
329
378 response.content_type = 'text/plain'
330 response.content_type = 'text/plain'
379
331
380 if method == 'download':
332 if method == 'download':
381 response.content_disposition = 'attachment; filename=%s.patch' \
333 response.content_disposition = 'attachment; filename=%s.patch' \
382 % revision
334 % revision
383
335
384 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id for x in
336 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id for x in
385 c.changeset.parents])
337 c.changeset.parents])
386
338
387 c.diffs = ''
339 c.diffs = ''
388 for x in c.changes:
340 for x in c.changes:
389 c.diffs += x[2]
341 c.diffs += x[2]
390
342
391 return render('changeset/raw_changeset.html')
343 return render('changeset/raw_changeset.html')
392
344
393 def comment(self, repo_name, revision):
345 def comment(self, repo_name, revision):
394 ChangesetCommentsModel().create(text=request.POST.get('text'),
346 ChangesetCommentsModel().create(text=request.POST.get('text'),
395 repo_id=c.rhodecode_db_repo.repo_id,
347 repo_id=c.rhodecode_db_repo.repo_id,
396 user_id=c.rhodecode_user.user_id,
348 user_id=c.rhodecode_user.user_id,
397 revision=revision,
349 revision=revision,
398 f_path=request.POST.get('f_path'),
350 f_path=request.POST.get('f_path'),
399 line_no=request.POST.get('line'))
351 line_no=request.POST.get('line'))
400 Session.commit()
352 Session.commit()
401 return redirect(h.url('changeset_home', repo_name=repo_name,
353 return redirect(h.url('changeset_home', repo_name=repo_name,
402 revision=revision))
354 revision=revision))
403
355
404 @jsonify
356 @jsonify
405 def delete_comment(self, repo_name, comment_id):
357 def delete_comment(self, repo_name, comment_id):
406 co = ChangesetComment.get(comment_id)
358 co = ChangesetComment.get(comment_id)
407 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
359 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
408 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
360 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
409 ChangesetCommentsModel().delete(comment=co)
361 ChangesetCommentsModel().delete(comment=co)
410 Session.commit()
362 Session.commit()
411 return True
363 return True
412 else:
364 else:
413 raise HTTPForbidden()
365 raise HTTPForbidden()
@@ -1,523 +1,509 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.files
3 rhodecode.controllers.files
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Files controller for RhodeCode
6 Files controller for RhodeCode
7
7
8 :created_on: Apr 21, 2010
8 :created_on: Apr 21, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import os
26 import os
27 import logging
27 import logging
28 import traceback
28 import traceback
29
29
30 from os.path import join as jn
30 from pylons import request, response, tmpl_context as c, url
31
32 from pylons import request, response, session, tmpl_context as c, url
33 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
34 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
35 from pylons.decorators import jsonify
33 from pylons.decorators import jsonify
36
34
37 from vcs.conf import settings
35 from vcs.conf import settings
38 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
36 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
39 EmptyRepositoryError, ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
37 EmptyRepositoryError, ImproperArchiveTypeError, VCSError, \
40 from vcs.nodes import FileNode, NodeKind
38 NodeAlreadyExistsError
39 from vcs.nodes import FileNode
41
40
42
41 from rhodecode.lib.compat import OrderedDict
43 from rhodecode.lib import convert_line_endings, detect_mode, safe_str
42 from rhodecode.lib import convert_line_endings, detect_mode, safe_str
44 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
43 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
45 from rhodecode.lib.base import BaseRepoController, render
44 from rhodecode.lib.base import BaseRepoController, render
46 from rhodecode.lib.utils import EmptyChangeset
45 from rhodecode.lib.utils import EmptyChangeset
47 from rhodecode.lib import diffs
46 from rhodecode.lib import diffs
48 import rhodecode.lib.helpers as h
47 import rhodecode.lib.helpers as h
49 from rhodecode.model.repo import RepoModel
48 from rhodecode.model.repo import RepoModel
49 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
50 _context_url, get_line_ctx, get_ignore_ws
51 from rhodecode.lib.diffs import wrapped_diff
50
52
51 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
52
54
53
55
54 class FilesController(BaseRepoController):
56 class FilesController(BaseRepoController):
55
57
56 @LoginRequired()
58 @LoginRequired()
57 def __before__(self):
59 def __before__(self):
58 super(FilesController, self).__before__()
60 super(FilesController, self).__before__()
59 c.cut_off_limit = self.cut_off_limit
61 c.cut_off_limit = self.cut_off_limit
60
62
61 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
63 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
62 """
64 """
63 Safe way to get changeset if error occur it redirects to tip with
65 Safe way to get changeset if error occur it redirects to tip with
64 proper message
66 proper message
65
67
66 :param rev: revision to fetch
68 :param rev: revision to fetch
67 :param repo_name: repo name to redirect after
69 :param repo_name: repo name to redirect after
68 """
70 """
69
71
70 try:
72 try:
71 return c.rhodecode_repo.get_changeset(rev)
73 return c.rhodecode_repo.get_changeset(rev)
72 except EmptyRepositoryError, e:
74 except EmptyRepositoryError, e:
73 if not redirect_after:
75 if not redirect_after:
74 return None
76 return None
75 url_ = url('files_add_home',
77 url_ = url('files_add_home',
76 repo_name=c.repo_name,
78 repo_name=c.repo_name,
77 revision=0, f_path='')
79 revision=0, f_path='')
78 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
80 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
79 h.flash(h.literal(_('There are no files yet %s' % add_new)),
81 h.flash(h.literal(_('There are no files yet %s' % add_new)),
80 category='warning')
82 category='warning')
81 redirect(h.url('summary_home', repo_name=repo_name))
83 redirect(h.url('summary_home', repo_name=repo_name))
82
84
83 except RepositoryError, e:
85 except RepositoryError, e:
84 h.flash(str(e), category='warning')
86 h.flash(str(e), category='warning')
85 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
87 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
86
88
87 def __get_filenode_or_redirect(self, repo_name, cs, path):
89 def __get_filenode_or_redirect(self, repo_name, cs, path):
88 """
90 """
89 Returns file_node, if error occurs or given path is directory,
91 Returns file_node, if error occurs or given path is directory,
90 it'll redirect to top level path
92 it'll redirect to top level path
91
93
92 :param repo_name: repo_name
94 :param repo_name: repo_name
93 :param cs: given changeset
95 :param cs: given changeset
94 :param path: path to lookup
96 :param path: path to lookup
95 """
97 """
96
98
97 try:
99 try:
98 file_node = cs.get_node(path)
100 file_node = cs.get_node(path)
99 if file_node.is_dir():
101 if file_node.is_dir():
100 raise RepositoryError('given path is a directory')
102 raise RepositoryError('given path is a directory')
101 except RepositoryError, e:
103 except RepositoryError, e:
102 h.flash(str(e), category='warning')
104 h.flash(str(e), category='warning')
103 redirect(h.url('files_home', repo_name=repo_name,
105 redirect(h.url('files_home', repo_name=repo_name,
104 revision=cs.raw_id))
106 revision=cs.raw_id))
105
107
106 return file_node
108 return file_node
107
109
108
109 def __get_paths(self, changeset, starting_path):
110 def __get_paths(self, changeset, starting_path):
110 """recursive walk in root dir and return a set of all path in that dir
111 """recursive walk in root dir and return a set of all path in that dir
111 based on repository walk function
112 based on repository walk function
112 """
113 """
113 _files = list()
114 _files = list()
114 _dirs = list()
115 _dirs = list()
115
116
116 try:
117 try:
117 tip = changeset
118 tip = changeset
118 for topnode, dirs, files in tip.walk(starting_path):
119 for topnode, dirs, files in tip.walk(starting_path):
119 for f in files:
120 for f in files:
120 _files.append(f.path)
121 _files.append(f.path)
121 for d in dirs:
122 for d in dirs:
122 _dirs.append(d.path)
123 _dirs.append(d.path)
123 except RepositoryError, e:
124 except RepositoryError, e:
124 log.debug(traceback.format_exc())
125 log.debug(traceback.format_exc())
125 pass
126 pass
126 return _dirs, _files
127 return _dirs, _files
127
128
128 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
129 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
129 'repository.admin')
130 'repository.admin')
130 def index(self, repo_name, revision, f_path):
131 def index(self, repo_name, revision, f_path):
131 #reditect to given revision from form if given
132 # redirect to given revision from form if given
132 post_revision = request.POST.get('at_rev', None)
133 post_revision = request.POST.get('at_rev', None)
133 if post_revision:
134 if post_revision:
134 cs = self.__get_cs_or_redirect(post_revision, repo_name)
135 cs = self.__get_cs_or_redirect(post_revision, repo_name)
135 redirect(url('files_home', repo_name=c.repo_name,
136 redirect(url('files_home', repo_name=c.repo_name,
136 revision=cs.raw_id, f_path=f_path))
137 revision=cs.raw_id, f_path=f_path))
137
138
138 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
139 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
139 c.branch = request.GET.get('branch', None)
140 c.branch = request.GET.get('branch', None)
140 c.f_path = f_path
141 c.f_path = f_path
141
142
142 cur_rev = c.changeset.revision
143 cur_rev = c.changeset.revision
143
144
144 #prev link
145 # prev link
145 try:
146 try:
146 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
147 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
147 c.url_prev = url('files_home', repo_name=c.repo_name,
148 c.url_prev = url('files_home', repo_name=c.repo_name,
148 revision=prev_rev.raw_id, f_path=f_path)
149 revision=prev_rev.raw_id, f_path=f_path)
149 if c.branch:
150 if c.branch:
150 c.url_prev += '?branch=%s' % c.branch
151 c.url_prev += '?branch=%s' % c.branch
151 except (ChangesetDoesNotExistError, VCSError):
152 except (ChangesetDoesNotExistError, VCSError):
152 c.url_prev = '#'
153 c.url_prev = '#'
153
154
154 #next link
155 # next link
155 try:
156 try:
156 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
157 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
157 c.url_next = url('files_home', repo_name=c.repo_name,
158 c.url_next = url('files_home', repo_name=c.repo_name,
158 revision=next_rev.raw_id, f_path=f_path)
159 revision=next_rev.raw_id, f_path=f_path)
159 if c.branch:
160 if c.branch:
160 c.url_next += '?branch=%s' % c.branch
161 c.url_next += '?branch=%s' % c.branch
161 except (ChangesetDoesNotExistError, VCSError):
162 except (ChangesetDoesNotExistError, VCSError):
162 c.url_next = '#'
163 c.url_next = '#'
163
164
164 #files or dirs
165 # files or dirs
165 try:
166 try:
166 c.file = c.changeset.get_node(f_path)
167 c.file = c.changeset.get_node(f_path)
167
168
168 if c.file.is_file():
169 if c.file.is_file():
169 c.file_history = self._get_node_history(c.changeset, f_path)
170 c.file_history = self._get_node_history(c.changeset, f_path)
170 else:
171 else:
171 c.file_history = []
172 c.file_history = []
172 except RepositoryError, e:
173 except RepositoryError, e:
173 h.flash(str(e), category='warning')
174 h.flash(str(e), category='warning')
174 redirect(h.url('files_home', repo_name=repo_name,
175 redirect(h.url('files_home', repo_name=repo_name,
175 revision=revision))
176 revision=revision))
176
177
177 return render('files/files.html')
178 return render('files/files.html')
178
179
179 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
180 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
180 'repository.admin')
181 'repository.admin')
181 def rawfile(self, repo_name, revision, f_path):
182 def rawfile(self, repo_name, revision, f_path):
182 cs = self.__get_cs_or_redirect(revision, repo_name)
183 cs = self.__get_cs_or_redirect(revision, repo_name)
183 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
184 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
184
185
185 response.content_disposition = 'attachment; filename=%s' % \
186 response.content_disposition = 'attachment; filename=%s' % \
186 safe_str(f_path.split(os.sep)[-1])
187 safe_str(f_path.split(os.sep)[-1])
187
188
188 response.content_type = file_node.mimetype
189 response.content_type = file_node.mimetype
189 return file_node.content
190 return file_node.content
190
191
191 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
192 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
192 'repository.admin')
193 'repository.admin')
193 def raw(self, repo_name, revision, f_path):
194 def raw(self, repo_name, revision, f_path):
194 cs = self.__get_cs_or_redirect(revision, repo_name)
195 cs = self.__get_cs_or_redirect(revision, repo_name)
195 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
196 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
196
197
197 raw_mimetype_mapping = {
198 raw_mimetype_mapping = {
198 # map original mimetype to a mimetype used for "show as raw"
199 # map original mimetype to a mimetype used for "show as raw"
199 # you can also provide a content-disposition to override the
200 # you can also provide a content-disposition to override the
200 # default "attachment" disposition.
201 # default "attachment" disposition.
201 # orig_type: (new_type, new_dispo)
202 # orig_type: (new_type, new_dispo)
202
203
203 # show images inline:
204 # show images inline:
204 'image/x-icon': ('image/x-icon', 'inline'),
205 'image/x-icon': ('image/x-icon', 'inline'),
205 'image/png': ('image/png', 'inline'),
206 'image/png': ('image/png', 'inline'),
206 'image/gif': ('image/gif', 'inline'),
207 'image/gif': ('image/gif', 'inline'),
207 'image/jpeg': ('image/jpeg', 'inline'),
208 'image/jpeg': ('image/jpeg', 'inline'),
208 'image/svg+xml': ('image/svg+xml', 'inline'),
209 'image/svg+xml': ('image/svg+xml', 'inline'),
209 }
210 }
210
211
211 mimetype = file_node.mimetype
212 mimetype = file_node.mimetype
212 try:
213 try:
213 mimetype, dispo = raw_mimetype_mapping[mimetype]
214 mimetype, dispo = raw_mimetype_mapping[mimetype]
214 except KeyError:
215 except KeyError:
215 # we don't know anything special about this, handle it safely
216 # we don't know anything special about this, handle it safely
216 if file_node.is_binary:
217 if file_node.is_binary:
217 # do same as download raw for binary files
218 # do same as download raw for binary files
218 mimetype, dispo = 'application/octet-stream', 'attachment'
219 mimetype, dispo = 'application/octet-stream', 'attachment'
219 else:
220 else:
220 # do not just use the original mimetype, but force text/plain,
221 # do not just use the original mimetype, but force text/plain,
221 # otherwise it would serve text/html and that might be unsafe.
222 # otherwise it would serve text/html and that might be unsafe.
222 # Note: underlying vcs library fakes text/plain mimetype if the
223 # Note: underlying vcs library fakes text/plain mimetype if the
223 # mimetype can not be determined and it thinks it is not
224 # mimetype can not be determined and it thinks it is not
224 # binary.This might lead to erroneous text display in some
225 # binary.This might lead to erroneous text display in some
225 # cases, but helps in other cases, like with text files
226 # cases, but helps in other cases, like with text files
226 # without extension.
227 # without extension.
227 mimetype, dispo = 'text/plain', 'inline'
228 mimetype, dispo = 'text/plain', 'inline'
228
229
229 if dispo == 'attachment':
230 if dispo == 'attachment':
230 dispo = 'attachment; filename=%s' % \
231 dispo = 'attachment; filename=%s' % \
231 safe_str(f_path.split(os.sep)[-1])
232 safe_str(f_path.split(os.sep)[-1])
232
233
233 response.content_disposition = dispo
234 response.content_disposition = dispo
234 response.content_type = mimetype
235 response.content_type = mimetype
235 return file_node.content
236 return file_node.content
236
237
237 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
238 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
238 'repository.admin')
239 'repository.admin')
239 def annotate(self, repo_name, revision, f_path):
240 def annotate(self, repo_name, revision, f_path):
240 c.cs = self.__get_cs_or_redirect(revision, repo_name)
241 c.cs = self.__get_cs_or_redirect(revision, repo_name)
241 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
242 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
242
243
243 c.file_history = self._get_node_history(c.cs, f_path)
244 c.file_history = self._get_node_history(c.cs, f_path)
244 c.f_path = f_path
245 c.f_path = f_path
245 return render('files/files_annotate.html')
246 return render('files/files_annotate.html')
246
247
247 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
248 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
248 def edit(self, repo_name, revision, f_path):
249 def edit(self, repo_name, revision, f_path):
249 r_post = request.POST
250 r_post = request.POST
250
251
251 c.cs = self.__get_cs_or_redirect(revision, repo_name)
252 c.cs = self.__get_cs_or_redirect(revision, repo_name)
252 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
253 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
253
254
254 if c.file.is_binary:
255 if c.file.is_binary:
255 return redirect(url('files_home', repo_name=c.repo_name,
256 return redirect(url('files_home', repo_name=c.repo_name,
256 revision=c.cs.raw_id, f_path=f_path))
257 revision=c.cs.raw_id, f_path=f_path))
257
258
258 c.f_path = f_path
259 c.f_path = f_path
259
260
260 if r_post:
261 if r_post:
261
262
262 old_content = c.file.content
263 old_content = c.file.content
263 sl = old_content.splitlines(1)
264 sl = old_content.splitlines(1)
264 first_line = sl[0] if sl else ''
265 first_line = sl[0] if sl else ''
265 # modes: 0 - Unix, 1 - Mac, 2 - DOS
266 # modes: 0 - Unix, 1 - Mac, 2 - DOS
266 mode = detect_mode(first_line, 0)
267 mode = detect_mode(first_line, 0)
267 content = convert_line_endings(r_post.get('content'), mode)
268 content = convert_line_endings(r_post.get('content'), mode)
268
269
269 message = r_post.get('message') or (_('Edited %s via RhodeCode')
270 message = r_post.get('message') or (_('Edited %s via RhodeCode')
270 % (f_path))
271 % (f_path))
271 author = self.rhodecode_user.full_contact
272 author = self.rhodecode_user.full_contact
272
273
273 if content == old_content:
274 if content == old_content:
274 h.flash(_('No changes'),
275 h.flash(_('No changes'),
275 category='warning')
276 category='warning')
276 return redirect(url('changeset_home', repo_name=c.repo_name,
277 return redirect(url('changeset_home', repo_name=c.repo_name,
277 revision='tip'))
278 revision='tip'))
278
279
279 try:
280 try:
280 self.scm_model.commit_change(repo=c.rhodecode_repo,
281 self.scm_model.commit_change(repo=c.rhodecode_repo,
281 repo_name=repo_name, cs=c.cs,
282 repo_name=repo_name, cs=c.cs,
282 user=self.rhodecode_user,
283 user=self.rhodecode_user,
283 author=author, message=message,
284 author=author, message=message,
284 content=content, f_path=f_path)
285 content=content, f_path=f_path)
285 h.flash(_('Successfully committed to %s' % f_path),
286 h.flash(_('Successfully committed to %s' % f_path),
286 category='success')
287 category='success')
287
288
288 except Exception:
289 except Exception:
289 log.error(traceback.format_exc())
290 log.error(traceback.format_exc())
290 h.flash(_('Error occurred during commit'), category='error')
291 h.flash(_('Error occurred during commit'), category='error')
291 return redirect(url('changeset_home',
292 return redirect(url('changeset_home',
292 repo_name=c.repo_name, revision='tip'))
293 repo_name=c.repo_name, revision='tip'))
293
294
294 return render('files/files_edit.html')
295 return render('files/files_edit.html')
295
296
296 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
297 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
297 def add(self, repo_name, revision, f_path):
298 def add(self, repo_name, revision, f_path):
298 r_post = request.POST
299 r_post = request.POST
299 c.cs = self.__get_cs_or_redirect(revision, repo_name,
300 c.cs = self.__get_cs_or_redirect(revision, repo_name,
300 redirect_after=False)
301 redirect_after=False)
301 if c.cs is None:
302 if c.cs is None:
302 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
303 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
303
304
304 c.f_path = f_path
305 c.f_path = f_path
305
306
306 if r_post:
307 if r_post:
307 unix_mode = 0
308 unix_mode = 0
308 content = convert_line_endings(r_post.get('content'), unix_mode)
309 content = convert_line_endings(r_post.get('content'), unix_mode)
309
310
310 message = r_post.get('message') or (_('Added %s via RhodeCode')
311 message = r_post.get('message') or (_('Added %s via RhodeCode')
311 % (f_path))
312 % (f_path))
312 location = r_post.get('location')
313 location = r_post.get('location')
313 filename = r_post.get('filename')
314 filename = r_post.get('filename')
314 file_obj = r_post.get('upload_file', None)
315 file_obj = r_post.get('upload_file', None)
315
316
316 if file_obj is not None and hasattr(file_obj, 'filename'):
317 if file_obj is not None and hasattr(file_obj, 'filename'):
317 filename = file_obj.filename
318 filename = file_obj.filename
318 content = file_obj.file
319 content = file_obj.file
319
320
320 node_path = os.path.join(location, filename)
321 node_path = os.path.join(location, filename)
321 author = self.rhodecode_user.full_contact
322 author = self.rhodecode_user.full_contact
322
323
323 if not content:
324 if not content:
324 h.flash(_('No content'), category='warning')
325 h.flash(_('No content'), category='warning')
325 return redirect(url('changeset_home', repo_name=c.repo_name,
326 return redirect(url('changeset_home', repo_name=c.repo_name,
326 revision='tip'))
327 revision='tip'))
327 if not filename:
328 if not filename:
328 h.flash(_('No filename'), category='warning')
329 h.flash(_('No filename'), category='warning')
329 return redirect(url('changeset_home', repo_name=c.repo_name,
330 return redirect(url('changeset_home', repo_name=c.repo_name,
330 revision='tip'))
331 revision='tip'))
331
332
332 try:
333 try:
333 self.scm_model.create_node(repo=c.rhodecode_repo,
334 self.scm_model.create_node(repo=c.rhodecode_repo,
334 repo_name=repo_name, cs=c.cs,
335 repo_name=repo_name, cs=c.cs,
335 user=self.rhodecode_user,
336 user=self.rhodecode_user,
336 author=author, message=message,
337 author=author, message=message,
337 content=content, f_path=node_path)
338 content=content, f_path=node_path)
338 h.flash(_('Successfully committed to %s' % node_path),
339 h.flash(_('Successfully committed to %s' % node_path),
339 category='success')
340 category='success')
340 except NodeAlreadyExistsError, e:
341 except NodeAlreadyExistsError, e:
341 h.flash(_(e), category='error')
342 h.flash(_(e), category='error')
342 except Exception:
343 except Exception:
343 log.error(traceback.format_exc())
344 log.error(traceback.format_exc())
344 h.flash(_('Error occurred during commit'), category='error')
345 h.flash(_('Error occurred during commit'), category='error')
345 return redirect(url('changeset_home',
346 return redirect(url('changeset_home',
346 repo_name=c.repo_name, revision='tip'))
347 repo_name=c.repo_name, revision='tip'))
347
348
348 return render('files/files_add.html')
349 return render('files/files_add.html')
349
350
350 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
351 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
351 'repository.admin')
352 'repository.admin')
352 def archivefile(self, repo_name, fname):
353 def archivefile(self, repo_name, fname):
353
354
354 fileformat = None
355 fileformat = None
355 revision = None
356 revision = None
356 ext = None
357 ext = None
357 subrepos = request.GET.get('subrepos') == 'true'
358 subrepos = request.GET.get('subrepos') == 'true'
358
359
359 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
360 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
360 archive_spec = fname.split(ext_data[1])
361 archive_spec = fname.split(ext_data[1])
361 if len(archive_spec) == 2 and archive_spec[1] == '':
362 if len(archive_spec) == 2 and archive_spec[1] == '':
362 fileformat = a_type or ext_data[1]
363 fileformat = a_type or ext_data[1]
363 revision = archive_spec[0]
364 revision = archive_spec[0]
364 ext = ext_data[1]
365 ext = ext_data[1]
365
366
366 try:
367 try:
367 dbrepo = RepoModel().get_by_repo_name(repo_name)
368 dbrepo = RepoModel().get_by_repo_name(repo_name)
368 if dbrepo.enable_downloads is False:
369 if dbrepo.enable_downloads is False:
369 return _('downloads disabled')
370 return _('downloads disabled')
370
371
371 # patch and reset hooks section of UI config to not run any
372 # patch and reset hooks section of UI config to not run any
372 # hooks on fetching archives with subrepos
373 # hooks on fetching archives with subrepos
373 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
374 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
374 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
375 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
375
376
376 cs = c.rhodecode_repo.get_changeset(revision)
377 cs = c.rhodecode_repo.get_changeset(revision)
377 content_type = settings.ARCHIVE_SPECS[fileformat][0]
378 content_type = settings.ARCHIVE_SPECS[fileformat][0]
378 except ChangesetDoesNotExistError:
379 except ChangesetDoesNotExistError:
379 return _('Unknown revision %s') % revision
380 return _('Unknown revision %s') % revision
380 except EmptyRepositoryError:
381 except EmptyRepositoryError:
381 return _('Empty repository')
382 return _('Empty repository')
382 except (ImproperArchiveTypeError, KeyError):
383 except (ImproperArchiveTypeError, KeyError):
383 return _('Unknown archive type')
384 return _('Unknown archive type')
384
385
385 response.content_type = content_type
386 response.content_type = content_type
386 response.content_disposition = 'attachment; filename=%s-%s%s' \
387 response.content_disposition = 'attachment; filename=%s-%s%s' \
387 % (repo_name, revision, ext)
388 % (repo_name, revision, ext)
388
389
389 import tempfile
390 import tempfile
390 archive = tempfile.mkstemp()[1]
391 archive = tempfile.mkstemp()[1]
391 t = open(archive, 'wb')
392 t = open(archive, 'wb')
392 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
393 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
393
394
394 def get_chunked_archive(archive):
395 def get_chunked_archive(archive):
395 stream = open(archive, 'rb')
396 stream = open(archive, 'rb')
396 while True:
397 while True:
397 data = stream.read(4096)
398 data = stream.read(4096)
398 if not data:
399 if not data:
399 os.remove(archive)
400 os.remove(archive)
400 break
401 break
401 yield data
402 yield data
402
403
403 return get_chunked_archive(archive)
404 return get_chunked_archive(archive)
404
405
405 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
406 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
406 'repository.admin')
407 'repository.admin')
407 def diff(self, repo_name, f_path):
408 def diff(self, repo_name, f_path):
408 ignore_whitespace = request.GET.get('ignorews') == '1'
409 ignore_whitespace = request.GET.get('ignorews') == '1'
409 line_context = request.GET.get('context', 3)
410 line_context = request.GET.get('context', 3)
410 diff1 = request.GET.get('diff1')
411 diff1 = request.GET.get('diff1', '')
411 diff2 = request.GET.get('diff2')
412 diff2 = request.GET.get('diff2', '')
412 c.action = request.GET.get('diff')
413 c.action = request.GET.get('diff')
413 c.no_changes = diff1 == diff2
414 c.no_changes = diff1 == diff2
414 c.f_path = f_path
415 c.f_path = f_path
415 c.big_diff = False
416 c.big_diff = False
416
417 c.anchor_url = anchor_url
418 c.ignorews_url = _ignorews_url
419 c.context_url = _context_url
420 c.changes = OrderedDict()
421 c.changes[diff2] = []
417 try:
422 try:
418 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
423 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
419 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
424 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
420 node1 = c.changeset_1.get_node(f_path)
425 node1 = c.changeset_1.get_node(f_path)
421 else:
426 else:
422 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
427 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
423 node1 = FileNode('.', '', changeset=c.changeset_1)
428 node1 = FileNode('.', '', changeset=c.changeset_1)
424
429
425 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
430 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
426 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
431 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
427 node2 = c.changeset_2.get_node(f_path)
432 node2 = c.changeset_2.get_node(f_path)
428 else:
433 else:
429 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
434 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
430 node2 = FileNode('.', '', changeset=c.changeset_2)
435 node2 = FileNode('.', '', changeset=c.changeset_2)
431 except RepositoryError:
436 except RepositoryError:
432 return redirect(url('files_home',
437 return redirect(url('files_home', repo_name=c.repo_name,
433 repo_name=c.repo_name, f_path=f_path))
438 f_path=f_path))
434
439
435 if c.action == 'download':
440 if c.action == 'download':
436 _diff = diffs.get_gitdiff(node1, node2,
441 _diff = diffs.get_gitdiff(node1, node2,
437 ignore_whitespace=ignore_whitespace,
442 ignore_whitespace=ignore_whitespace,
438 context=line_context)
443 context=line_context)
439 diff = diffs.DiffProcessor(_diff,format='gitdiff')
444 diff = diffs.DiffProcessor(_diff, format='gitdiff')
440
445
441 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
446 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
442 response.content_type = 'text/plain'
447 response.content_type = 'text/plain'
443 response.content_disposition = 'attachment; filename=%s' \
448 response.content_disposition = 'attachment; filename=%s' \
444 % diff_name
449 % diff_name
445 return diff.raw_diff()
450 return diff.raw_diff()
446
451
447 elif c.action == 'raw':
452 elif c.action == 'raw':
448 _diff = diffs.get_gitdiff(node1, node2,
453 _diff = diffs.get_gitdiff(node1, node2,
449 ignore_whitespace=ignore_whitespace,
454 ignore_whitespace=ignore_whitespace,
450 context=line_context)
455 context=line_context)
451 diff = diffs.DiffProcessor(_diff,format='gitdiff')
456 diff = diffs.DiffProcessor(_diff, format='gitdiff')
452 response.content_type = 'text/plain'
457 response.content_type = 'text/plain'
453 return diff.raw_diff()
458 return diff.raw_diff()
454
459
455 elif c.action == 'diff':
456 if node1.is_binary or node2.is_binary:
457 c.cur_diff = _('Binary file')
458 elif node1.size > self.cut_off_limit or \
459 node2.size > self.cut_off_limit:
460 c.cur_diff = ''
461 c.big_diff = True
462 else:
463 _diff = diffs.get_gitdiff(node1, node2,
464 ignore_whitespace=ignore_whitespace,
465 context=line_context)
466 diff = diffs.DiffProcessor(_diff,format='gitdiff')
467 c.cur_diff = diff.as_html()
468 else:
460 else:
461 fid = h.FID(diff2, node2.path)
462 line_context_lcl = get_line_ctx(fid, request.GET)
463 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
469
464
470 #default option
465 lim = request.GET.get('fulldiff') or self.cut_off_limit
471 if node1.is_binary or node2.is_binary:
466 _, cs1, cs2, diff, st = wrapped_diff(filenode_old=node1,
472 c.cur_diff = _('Binary file')
467 filenode_new=node2,
473 elif node1.size > self.cut_off_limit or \
468 cut_off_limit=lim,
474 node2.size > self.cut_off_limit:
469 ignore_whitespace=ign_whitespace_lcl,
475 c.cur_diff = ''
470 line_context=line_context_lcl,
476 c.big_diff = True
471 enable_comments=False)
477
472
478 else:
473 c.changes = [('', node2, diff, cs1, cs2, st,)]
479 _diff = diffs.get_gitdiff(node1, node2,
480 ignore_whitespace=ignore_whitespace,
481 context=line_context)
482 diff = diffs.DiffProcessor(_diff,format='gitdiff')
483 c.cur_diff = diff.as_html()
484
474
485 if not c.cur_diff and not c.big_diff:
486 c.no_changes = True
487 return render('files/file_diff.html')
475 return render('files/file_diff.html')
488
476
489 def _get_node_history(self, cs, f_path):
477 def _get_node_history(self, cs, f_path):
490 changesets = cs.get_file_history(f_path)
478 changesets = cs.get_file_history(f_path)
491 hist_l = []
479 hist_l = []
492
480
493 changesets_group = ([], _("Changesets"))
481 changesets_group = ([], _("Changesets"))
494 branches_group = ([], _("Branches"))
482 branches_group = ([], _("Branches"))
495 tags_group = ([], _("Tags"))
483 tags_group = ([], _("Tags"))
496
484
497 for chs in changesets:
485 for chs in changesets:
498 n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
486 n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
499 changesets_group[0].append((chs.raw_id, n_desc,))
487 changesets_group[0].append((chs.raw_id, n_desc,))
500
488
501 hist_l.append(changesets_group)
489 hist_l.append(changesets_group)
502
490
503 for name, chs in c.rhodecode_repo.branches.items():
491 for name, chs in c.rhodecode_repo.branches.items():
504 #chs = chs.split(':')[-1]
505 branches_group[0].append((chs, name),)
492 branches_group[0].append((chs, name),)
506 hist_l.append(branches_group)
493 hist_l.append(branches_group)
507
494
508 for name, chs in c.rhodecode_repo.tags.items():
495 for name, chs in c.rhodecode_repo.tags.items():
509 #chs = chs.split(':')[-1]
510 tags_group[0].append((chs, name),)
496 tags_group[0].append((chs, name),)
511 hist_l.append(tags_group)
497 hist_l.append(tags_group)
512
498
513 return hist_l
499 return hist_l
514
500
515 @jsonify
501 @jsonify
516 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
502 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
517 'repository.admin')
503 'repository.admin')
518 def nodelist(self, repo_name, revision, f_path):
504 def nodelist(self, repo_name, revision, f_path):
519 if request.environ.get('HTTP_X_PARTIAL_XHR'):
505 if request.environ.get('HTTP_X_PARTIAL_XHR'):
520 cs = self.__get_cs_or_redirect(revision, repo_name)
506 cs = self.__get_cs_or_redirect(revision, repo_name)
521 _d, _f = self.__get_paths(cs, f_path)
507 _d, _f = self.__get_paths(cs, f_path)
522 return _d + _f
508 return _d + _f
523
509
@@ -1,451 +1,515 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.diffs
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Set of diffing helpers, previously part of vcs
6 Set of diffing helpers, previously part of vcs
7
7
8
8
9 :created_on: Dec 4, 2011
9 :created_on: Dec 4, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
13 :license: GPLv3, see COPYING for more details.
14 """
14 """
15 # This program is free software: you can redistribute it and/or modify
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
18 # (at your option) any later version.
19 #
19 #
20 # This program is distributed in the hope that it will be useful,
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
23 # GNU General Public License for more details.
24 #
24 #
25 # You should have received a copy of the GNU General Public License
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
28 import re
28 import re
29 import difflib
29 import difflib
30 import markupsafe
31 from itertools import tee, imap
30
32
31 from itertools import tee, imap
33 from pylons.i18n.translation import _
32
34
33 from vcs.exceptions import VCSError
35 from vcs.exceptions import VCSError
34 from vcs.nodes import FileNode
36 from vcs.nodes import FileNode
35 import markupsafe
37
38 from rhodecode.lib.utils import EmptyChangeset
39
40
41 def wrap_to_table(str_):
42 return '''<table class="code-difftable">
43 <tr class="line no-comment">
44 <td class="lineno new"></td>
45 <td class="code no-comment"><pre>%s</pre></td>
46 </tr>
47 </table>''' % str_
48
49
50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
51 ignore_whitespace=True, line_context=3,
52 enable_comments=False):
53 """
54 returns a wrapped diff into a table, checks for cut_off_limit and presents
55 proper message
56 """
57
58 if filenode_old is None:
59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
60
61 if filenode_old.is_binary or filenode_new.is_binary:
62 diff = wrap_to_table(_('binary file'))
63 stats = (0, 0)
64 size = 0
65
66 elif cut_off_limit != -1 and (cut_off_limit is None or
67 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
68
69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
70 ignore_whitespace=ignore_whitespace,
71 context=line_context)
72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
73
74 diff = diff_processor.as_html(enable_comments=enable_comments)
75 stats = diff_processor.stat()
76 size = len(diff or '')
77 else:
78 diff = wrap_to_table(_('Changeset was to big and was cut off, use '
79 'diff menu to display this diff'))
80 stats = (0, 0)
81 size = 0
82
83 if not diff:
84 diff = wrap_to_table(_('No changes detected'))
85
86 cs1 = filenode_old.last_changeset.raw_id
87 cs2 = filenode_new.last_changeset.raw_id
88
89 return size, cs1, cs2, diff, stats
36
90
37
91
38 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
92 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
39 """
93 """
40 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
94 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
41
95
42 :param ignore_whitespace: ignore whitespaces in diff
96 :param ignore_whitespace: ignore whitespaces in diff
43 """
97 """
44
98
45 for filenode in (filenode_old, filenode_new):
99 for filenode in (filenode_old, filenode_new):
46 if not isinstance(filenode, FileNode):
100 if not isinstance(filenode, FileNode):
47 raise VCSError("Given object should be FileNode object, not %s"
101 raise VCSError("Given object should be FileNode object, not %s"
48 % filenode.__class__)
102 % filenode.__class__)
49
103
50 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
104 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
51 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
105 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
52
106
53 repo = filenode_new.changeset.repository
107 repo = filenode_new.changeset.repository
54 vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
108 vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
55 ignore_whitespace, context)
109 ignore_whitespace, context)
56
110
57 return vcs_gitdiff
111 return vcs_gitdiff
58
112
59
113
60 class DiffProcessor(object):
114 class DiffProcessor(object):
61 """
115 """
62 Give it a unified diff and it returns a list of the files that were
116 Give it a unified diff and it returns a list of the files that were
63 mentioned in the diff together with a dict of meta information that
117 mentioned in the diff together with a dict of meta information that
64 can be used to render it in a HTML template.
118 can be used to render it in a HTML template.
65 """
119 """
66 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
120 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
67
121
68 def __init__(self, diff, differ='diff', format='udiff'):
122 def __init__(self, diff, differ='diff', format='udiff'):
69 """
123 """
70 :param diff: a text in diff format or generator
124 :param diff: a text in diff format or generator
71 :param format: format of diff passed, `udiff` or `gitdiff`
125 :param format: format of diff passed, `udiff` or `gitdiff`
72 """
126 """
73 if isinstance(diff, basestring):
127 if isinstance(diff, basestring):
74 diff = [diff]
128 diff = [diff]
75
129
76 self.__udiff = diff
130 self.__udiff = diff
77 self.__format = format
131 self.__format = format
78 self.adds = 0
132 self.adds = 0
79 self.removes = 0
133 self.removes = 0
80
134
81 if isinstance(self.__udiff, basestring):
135 if isinstance(self.__udiff, basestring):
82 self.lines = iter(self.__udiff.splitlines(1))
136 self.lines = iter(self.__udiff.splitlines(1))
83
137
84 elif self.__format == 'gitdiff':
138 elif self.__format == 'gitdiff':
85 udiff_copy = self.copy_iterator()
139 udiff_copy = self.copy_iterator()
86 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
140 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
87 else:
141 else:
88 udiff_copy = self.copy_iterator()
142 udiff_copy = self.copy_iterator()
89 self.lines = imap(self.escaper, udiff_copy)
143 self.lines = imap(self.escaper, udiff_copy)
90
144
91 # Select a differ.
145 # Select a differ.
92 if differ == 'difflib':
146 if differ == 'difflib':
93 self.differ = self._highlight_line_difflib
147 self.differ = self._highlight_line_difflib
94 else:
148 else:
95 self.differ = self._highlight_line_udiff
149 self.differ = self._highlight_line_udiff
96
150
97 def escaper(self, string):
151 def escaper(self, string):
98 return markupsafe.escape(string)
152 return markupsafe.escape(string)
99
153
100 def copy_iterator(self):
154 def copy_iterator(self):
101 """
155 """
102 make a fresh copy of generator, we should not iterate thru
156 make a fresh copy of generator, we should not iterate thru
103 an original as it's needed for repeating operations on
157 an original as it's needed for repeating operations on
104 this instance of DiffProcessor
158 this instance of DiffProcessor
105 """
159 """
106 self.__udiff, iterator_copy = tee(self.__udiff)
160 self.__udiff, iterator_copy = tee(self.__udiff)
107 return iterator_copy
161 return iterator_copy
108
162
109 def _extract_rev(self, line1, line2):
163 def _extract_rev(self, line1, line2):
110 """
164 """
111 Extract the filename and revision hint from a line.
165 Extract the filename and revision hint from a line.
112 """
166 """
113
167
114 try:
168 try:
115 if line1.startswith('--- ') and line2.startswith('+++ '):
169 if line1.startswith('--- ') and line2.startswith('+++ '):
116 l1 = line1[4:].split(None, 1)
170 l1 = line1[4:].split(None, 1)
117 old_filename = (l1[0].replace('a/', '', 1)
171 old_filename = (l1[0].replace('a/', '', 1)
118 if len(l1) >= 1 else None)
172 if len(l1) >= 1 else None)
119 old_rev = l1[1] if len(l1) == 2 else 'old'
173 old_rev = l1[1] if len(l1) == 2 else 'old'
120
174
121 l2 = line2[4:].split(None, 1)
175 l2 = line2[4:].split(None, 1)
122 new_filename = (l2[0].replace('b/', '', 1)
176 new_filename = (l2[0].replace('b/', '', 1)
123 if len(l1) >= 1 else None)
177 if len(l1) >= 1 else None)
124 new_rev = l2[1] if len(l2) == 2 else 'new'
178 new_rev = l2[1] if len(l2) == 2 else 'new'
125
179
126 filename = (old_filename
180 filename = (old_filename
127 if old_filename != '/dev/null' else new_filename)
181 if old_filename != '/dev/null' else new_filename)
128
182
129 return filename, new_rev, old_rev
183 return filename, new_rev, old_rev
130 except (ValueError, IndexError):
184 except (ValueError, IndexError):
131 pass
185 pass
132
186
133 return None, None, None
187 return None, None, None
134
188
135 def _parse_gitdiff(self, diffiterator):
189 def _parse_gitdiff(self, diffiterator):
136 def line_decoder(l):
190 def line_decoder(l):
137 if l.startswith('+') and not l.startswith('+++'):
191 if l.startswith('+') and not l.startswith('+++'):
138 self.adds += 1
192 self.adds += 1
139 elif l.startswith('-') and not l.startswith('---'):
193 elif l.startswith('-') and not l.startswith('---'):
140 self.removes += 1
194 self.removes += 1
141 return l.decode('utf8', 'replace')
195 return l.decode('utf8', 'replace')
142
196
143 output = list(diffiterator)
197 output = list(diffiterator)
144 size = len(output)
198 size = len(output)
145
199
146 if size == 2:
200 if size == 2:
147 l = []
201 l = []
148 l.extend([output[0]])
202 l.extend([output[0]])
149 l.extend(output[1].splitlines(1))
203 l.extend(output[1].splitlines(1))
150 return map(line_decoder, l)
204 return map(line_decoder, l)
151 elif size == 1:
205 elif size == 1:
152 return map(line_decoder, output[0].splitlines(1))
206 return map(line_decoder, output[0].splitlines(1))
153 elif size == 0:
207 elif size == 0:
154 return []
208 return []
155
209
156 raise Exception('wrong size of diff %s' % size)
210 raise Exception('wrong size of diff %s' % size)
157
211
158 def _highlight_line_difflib(self, line, next_):
212 def _highlight_line_difflib(self, line, next_):
159 """
213 """
160 Highlight inline changes in both lines.
214 Highlight inline changes in both lines.
161 """
215 """
162
216
163 if line['action'] == 'del':
217 if line['action'] == 'del':
164 old, new = line, next_
218 old, new = line, next_
165 else:
219 else:
166 old, new = next_, line
220 old, new = next_, line
167
221
168 oldwords = re.split(r'(\W)', old['line'])
222 oldwords = re.split(r'(\W)', old['line'])
169 newwords = re.split(r'(\W)', new['line'])
223 newwords = re.split(r'(\W)', new['line'])
170
224
171 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
225 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
172
226
173 oldfragments, newfragments = [], []
227 oldfragments, newfragments = [], []
174 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
228 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
175 oldfrag = ''.join(oldwords[i1:i2])
229 oldfrag = ''.join(oldwords[i1:i2])
176 newfrag = ''.join(newwords[j1:j2])
230 newfrag = ''.join(newwords[j1:j2])
177 if tag != 'equal':
231 if tag != 'equal':
178 if oldfrag:
232 if oldfrag:
179 oldfrag = '<del>%s</del>' % oldfrag
233 oldfrag = '<del>%s</del>' % oldfrag
180 if newfrag:
234 if newfrag:
181 newfrag = '<ins>%s</ins>' % newfrag
235 newfrag = '<ins>%s</ins>' % newfrag
182 oldfragments.append(oldfrag)
236 oldfragments.append(oldfrag)
183 newfragments.append(newfrag)
237 newfragments.append(newfrag)
184
238
185 old['line'] = "".join(oldfragments)
239 old['line'] = "".join(oldfragments)
186 new['line'] = "".join(newfragments)
240 new['line'] = "".join(newfragments)
187
241
188 def _highlight_line_udiff(self, line, next_):
242 def _highlight_line_udiff(self, line, next_):
189 """
243 """
190 Highlight inline changes in both lines.
244 Highlight inline changes in both lines.
191 """
245 """
192 start = 0
246 start = 0
193 limit = min(len(line['line']), len(next_['line']))
247 limit = min(len(line['line']), len(next_['line']))
194 while start < limit and line['line'][start] == next_['line'][start]:
248 while start < limit and line['line'][start] == next_['line'][start]:
195 start += 1
249 start += 1
196 end = -1
250 end = -1
197 limit -= start
251 limit -= start
198 while -end <= limit and line['line'][end] == next_['line'][end]:
252 while -end <= limit and line['line'][end] == next_['line'][end]:
199 end -= 1
253 end -= 1
200 end += 1
254 end += 1
201 if start or end:
255 if start or end:
202 def do(l):
256 def do(l):
203 last = end + len(l['line'])
257 last = end + len(l['line'])
204 if l['action'] == 'add':
258 if l['action'] == 'add':
205 tag = 'ins'
259 tag = 'ins'
206 else:
260 else:
207 tag = 'del'
261 tag = 'del'
208 l['line'] = '%s<%s>%s</%s>%s' % (
262 l['line'] = '%s<%s>%s</%s>%s' % (
209 l['line'][:start],
263 l['line'][:start],
210 tag,
264 tag,
211 l['line'][start:last],
265 l['line'][start:last],
212 tag,
266 tag,
213 l['line'][last:]
267 l['line'][last:]
214 )
268 )
215 do(line)
269 do(line)
216 do(next_)
270 do(next_)
217
271
218 def _parse_udiff(self):
272 def _parse_udiff(self):
219 """
273 """
220 Parse the diff an return data for the template.
274 Parse the diff an return data for the template.
221 """
275 """
222 lineiter = self.lines
276 lineiter = self.lines
223 files = []
277 files = []
224 try:
278 try:
225 line = lineiter.next()
279 line = lineiter.next()
226 # skip first context
280 # skip first context
227 skipfirst = True
281 skipfirst = True
228 while 1:
282 while 1:
229 # continue until we found the old file
283 # continue until we found the old file
230 if not line.startswith('--- '):
284 if not line.startswith('--- '):
231 line = lineiter.next()
285 line = lineiter.next()
232 continue
286 continue
233
287
234 chunks = []
288 chunks = []
235 filename, old_rev, new_rev = \
289 filename, old_rev, new_rev = \
236 self._extract_rev(line, lineiter.next())
290 self._extract_rev(line, lineiter.next())
237 files.append({
291 files.append({
238 'filename': filename,
292 'filename': filename,
239 'old_revision': old_rev,
293 'old_revision': old_rev,
240 'new_revision': new_rev,
294 'new_revision': new_rev,
241 'chunks': chunks
295 'chunks': chunks
242 })
296 })
243
297
244 line = lineiter.next()
298 line = lineiter.next()
245 while line:
299 while line:
246 match = self._chunk_re.match(line)
300 match = self._chunk_re.match(line)
247 if not match:
301 if not match:
248 break
302 break
249
303
250 lines = []
304 lines = []
251 chunks.append(lines)
305 chunks.append(lines)
252
306
253 old_line, old_end, new_line, new_end = \
307 old_line, old_end, new_line, new_end = \
254 [int(x or 1) for x in match.groups()[:-1]]
308 [int(x or 1) for x in match.groups()[:-1]]
255 old_line -= 1
309 old_line -= 1
256 new_line -= 1
310 new_line -= 1
257 context = len(match.groups()) == 5
311 context = len(match.groups()) == 5
258 old_end += old_line
312 old_end += old_line
259 new_end += new_line
313 new_end += new_line
260
314
261 if context:
315 if context:
262 if not skipfirst:
316 if not skipfirst:
263 lines.append({
317 lines.append({
264 'old_lineno': '...',
318 'old_lineno': '...',
265 'new_lineno': '...',
319 'new_lineno': '...',
266 'action': 'context',
320 'action': 'context',
267 'line': line,
321 'line': line,
268 })
322 })
269 else:
323 else:
270 skipfirst = False
324 skipfirst = False
271
325
272 line = lineiter.next()
326 line = lineiter.next()
273 while old_line < old_end or new_line < new_end:
327 while old_line < old_end or new_line < new_end:
274 if line:
328 if line:
275 command, line = line[0], line[1:]
329 command, line = line[0], line[1:]
276 else:
330 else:
277 command = ' '
331 command = ' '
278 affects_old = affects_new = False
332 affects_old = affects_new = False
279
333
280 # ignore those if we don't expect them
334 # ignore those if we don't expect them
281 if command in '#@':
335 if command in '#@':
282 continue
336 continue
283 elif command == '+':
337 elif command == '+':
284 affects_new = True
338 affects_new = True
285 action = 'add'
339 action = 'add'
286 elif command == '-':
340 elif command == '-':
287 affects_old = True
341 affects_old = True
288 action = 'del'
342 action = 'del'
289 else:
343 else:
290 affects_old = affects_new = True
344 affects_old = affects_new = True
291 action = 'unmod'
345 action = 'unmod'
292
346
293 old_line += affects_old
347 old_line += affects_old
294 new_line += affects_new
348 new_line += affects_new
295 lines.append({
349 lines.append({
296 'old_lineno': affects_old and old_line or '',
350 'old_lineno': affects_old and old_line or '',
297 'new_lineno': affects_new and new_line or '',
351 'new_lineno': affects_new and new_line or '',
298 'action': action,
352 'action': action,
299 'line': line
353 'line': line
300 })
354 })
301 line = lineiter.next()
355 line = lineiter.next()
302
356
303 except StopIteration:
357 except StopIteration:
304 pass
358 pass
305
359
306 # highlight inline changes
360 # highlight inline changes
307 for _ in files:
361 for _ in files:
308 for chunk in chunks:
362 for chunk in chunks:
309 lineiter = iter(chunk)
363 lineiter = iter(chunk)
310 #first = True
364 #first = True
311 try:
365 try:
312 while 1:
366 while 1:
313 line = lineiter.next()
367 line = lineiter.next()
314 if line['action'] != 'unmod':
368 if line['action'] != 'unmod':
315 nextline = lineiter.next()
369 nextline = lineiter.next()
316 if nextline['action'] == 'unmod' or \
370 if nextline['action'] == 'unmod' or \
317 nextline['action'] == line['action']:
371 nextline['action'] == line['action']:
318 continue
372 continue
319 self.differ(line, nextline)
373 self.differ(line, nextline)
320 except StopIteration:
374 except StopIteration:
321 pass
375 pass
322
376
323 return files
377 return files
324
378
325 def prepare(self):
379 def prepare(self):
326 """
380 """
327 Prepare the passed udiff for HTML rendering. It'l return a list
381 Prepare the passed udiff for HTML rendering. It'l return a list
328 of dicts
382 of dicts
329 """
383 """
330 return self._parse_udiff()
384 return self._parse_udiff()
331
385
332 def _safe_id(self, idstring):
386 def _safe_id(self, idstring):
333 """Make a string safe for including in an id attribute.
387 """Make a string safe for including in an id attribute.
334
388
335 The HTML spec says that id attributes 'must begin with
389 The HTML spec says that id attributes 'must begin with
336 a letter ([A-Za-z]) and may be followed by any number
390 a letter ([A-Za-z]) and may be followed by any number
337 of letters, digits ([0-9]), hyphens ("-"), underscores
391 of letters, digits ([0-9]), hyphens ("-"), underscores
338 ("_"), colons (":"), and periods (".")'. These regexps
392 ("_"), colons (":"), and periods (".")'. These regexps
339 are slightly over-zealous, in that they remove colons
393 are slightly over-zealous, in that they remove colons
340 and periods unnecessarily.
394 and periods unnecessarily.
341
395
342 Whitespace is transformed into underscores, and then
396 Whitespace is transformed into underscores, and then
343 anything which is not a hyphen or a character that
397 anything which is not a hyphen or a character that
344 matches \w (alphanumerics and underscore) is removed.
398 matches \w (alphanumerics and underscore) is removed.
345
399
346 """
400 """
347 # Transform all whitespace to underscore
401 # Transform all whitespace to underscore
348 idstring = re.sub(r'\s', "_", '%s' % idstring)
402 idstring = re.sub(r'\s', "_", '%s' % idstring)
349 # Remove everything that is not a hyphen or a member of \w
403 # Remove everything that is not a hyphen or a member of \w
350 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
404 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
351 return idstring
405 return idstring
352
406
353 def raw_diff(self):
407 def raw_diff(self):
354 """
408 """
355 Returns raw string as udiff
409 Returns raw string as udiff
356 """
410 """
357 udiff_copy = self.copy_iterator()
411 udiff_copy = self.copy_iterator()
358 if self.__format == 'gitdiff':
412 if self.__format == 'gitdiff':
359 udiff_copy = self._parse_gitdiff(udiff_copy)
413 udiff_copy = self._parse_gitdiff(udiff_copy)
360 return u''.join(udiff_copy)
414 return u''.join(udiff_copy)
361
415
362 def as_html(self, table_class='code-difftable', line_class='line',
416 def as_html(self, table_class='code-difftable', line_class='line',
363 new_lineno_class='lineno old', old_lineno_class='lineno new',
417 new_lineno_class='lineno old', old_lineno_class='lineno new',
364 code_class='code', enable_comments=False):
418 code_class='code', enable_comments=False):
365 """
419 """
366 Return udiff as html table with customized css classes
420 Return udiff as html table with customized css classes
367 """
421 """
368 def _link_to_if(condition, label, url):
422 def _link_to_if(condition, label, url):
369 """
423 """
370 Generates a link if condition is meet or just the label if not.
424 Generates a link if condition is meet or just the label if not.
371 """
425 """
372
426
373 if condition:
427 if condition:
374 return '''<a href="%(url)s">%(label)s</a>''' % {'url': url,
428 return '''<a href="%(url)s">%(label)s</a>''' % {
375 'label': label}
429 'url': url,
430 'label': label
431 }
376 else:
432 else:
377 return label
433 return label
378 diff_lines = self.prepare()
434 diff_lines = self.prepare()
379 _html_empty = True
435 _html_empty = True
380 _html = []
436 _html = []
381 _html.append('''<table class="%(table_class)s">\n''' \
437 _html.append('''<table class="%(table_class)s">\n''' % {
382 % {'table_class': table_class})
438 'table_class': table_class
439 })
383 for diff in diff_lines:
440 for diff in diff_lines:
384 for line in diff['chunks']:
441 for line in diff['chunks']:
385 _html_empty = False
442 _html_empty = False
386 for change in line:
443 for change in line:
387 _html.append('''<tr class="%(line_class)s %(action)s">\n''' \
444 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
388 % {'line_class': line_class,
445 'lc': line_class,
389 'action': change['action']})
446 'action': change['action']
447 })
390 anchor_old_id = ''
448 anchor_old_id = ''
391 anchor_new_id = ''
449 anchor_new_id = ''
392 anchor_old = "%(filename)s_o%(oldline_no)s" % \
450 anchor_old = "%(filename)s_o%(oldline_no)s" % {
393 {'filename': self._safe_id(diff['filename']),
451 'filename': self._safe_id(diff['filename']),
394 'oldline_no': change['old_lineno']}
452 'oldline_no': change['old_lineno']
395 anchor_new = "%(filename)s_n%(oldline_no)s" % \
453 }
396 {'filename': self._safe_id(diff['filename']),
454 anchor_new = "%(filename)s_n%(oldline_no)s" % {
397 'oldline_no': change['new_lineno']}
455 'filename': self._safe_id(diff['filename']),
398 cond_old = change['old_lineno'] != '...' and \
456 'oldline_no': change['new_lineno']
399 change['old_lineno']
457 }
400 cond_new = change['new_lineno'] != '...' and \
458 cond_old = (change['old_lineno'] != '...' and
401 change['new_lineno']
459 change['old_lineno'])
460 cond_new = (change['new_lineno'] != '...' and
461 change['new_lineno'])
402 if cond_old:
462 if cond_old:
403 anchor_old_id = 'id="%s"' % anchor_old
463 anchor_old_id = 'id="%s"' % anchor_old
404 if cond_new:
464 if cond_new:
405 anchor_new_id = 'id="%s"' % anchor_new
465 anchor_new_id = 'id="%s"' % anchor_new
406 ###########################################################
466 ###########################################################
407 # OLD LINE NUMBER
467 # OLD LINE NUMBER
408 ###########################################################
468 ###########################################################
409 _html.append('''\t<td %(a_id)s class="%(old_lineno_cls)s">''' \
469 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
410 % {'a_id': anchor_old_id,
470 'a_id': anchor_old_id,
411 'old_lineno_cls': old_lineno_class})
471 'olc': old_lineno_class
472 })
412
473
413 _html.append('''%(link)s''' \
474 _html.append('''%(link)s''' % {
414 % {'link':
475 'link': _link_to_if(True, change['old_lineno'],
415 _link_to_if(cond_old, change['old_lineno'], '#%s' \
476 '#%s' % anchor_old)
416 % anchor_old)})
477 })
417 _html.append('''</td>\n''')
478 _html.append('''</td>\n''')
418 ###########################################################
479 ###########################################################
419 # NEW LINE NUMBER
480 # NEW LINE NUMBER
420 ###########################################################
481 ###########################################################
421
482
422 _html.append('''\t<td %(a_id)s class="%(new_lineno_cls)s">''' \
483 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
423 % {'a_id': anchor_new_id,
484 'a_id': anchor_new_id,
424 'new_lineno_cls': new_lineno_class})
485 'nlc': new_lineno_class
486 })
425
487
426 _html.append('''%(link)s''' \
488 _html.append('''%(link)s''' % {
427 % {'link':
489 'link': _link_to_if(True, change['new_lineno'],
428 _link_to_if(cond_new, change['new_lineno'], '#%s' \
490 '#%s' % anchor_new)
429 % anchor_new)})
491 })
430 _html.append('''</td>\n''')
492 _html.append('''</td>\n''')
431 ###########################################################
493 ###########################################################
432 # CODE
494 # CODE
433 ###########################################################
495 ###########################################################
434 comments = '' if enable_comments else 'no-comment'
496 comments = '' if enable_comments else 'no-comment'
435 _html.append('''\t<td class="%(code_class)s %(in-comments)s">''' \
497 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
436 % {'code_class': code_class,
498 'cc': code_class,
437 'in-comments': comments})
499 'inc': comments
438 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' \
500 })
439 % {'code': change['line']})
501 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
502 'code': change['line']
503 })
440 _html.append('''\t</td>''')
504 _html.append('''\t</td>''')
441 _html.append('''\n</tr>\n''')
505 _html.append('''\n</tr>\n''')
442 _html.append('''</table>''')
506 _html.append('''</table>''')
443 if _html_empty:
507 if _html_empty:
444 return None
508 return None
445 return ''.join(_html)
509 return ''.join(_html)
446
510
447 def stat(self):
511 def stat(self):
448 """
512 """
449 Returns tuple of added, and removed lines for this instance
513 Returns tuple of added, and removed lines for this instance
450 """
514 """
451 return self.adds, self.removes
515 return self.adds, self.removes
@@ -1,751 +1,752 b''
1 """Helper functions
1 """Helper functions
2
2
3 Consists of functions to typically be used within templates, but also
3 Consists of functions to typically be used within templates, but also
4 available to Controllers. This module is available to both as 'h'.
4 available to Controllers. This module is available to both as 'h'.
5 """
5 """
6 import random
6 import random
7 import hashlib
7 import hashlib
8 import StringIO
8 import StringIO
9 import urllib
9 import urllib
10 import math
10 import math
11
11
12 from datetime import datetime
12 from datetime import datetime
13 from pygments.formatters.html import HtmlFormatter
13 from pygments.formatters.html import HtmlFormatter
14 from pygments import highlight as code_highlight
14 from pygments import highlight as code_highlight
15 from pylons import url, request, config
15 from pylons import url, request, config
16 from pylons.i18n.translation import _, ungettext
16 from pylons.i18n.translation import _, ungettext
17
17
18 from webhelpers.html import literal, HTML, escape
18 from webhelpers.html import literal, HTML, escape
19 from webhelpers.html.tools import *
19 from webhelpers.html.tools import *
20 from webhelpers.html.builder import make_tag
20 from webhelpers.html.builder import make_tag
21 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
21 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
22 end_form, file, form, hidden, image, javascript_link, link_to, \
22 end_form, file, form, hidden, image, javascript_link, link_to, \
23 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
23 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
24 submit, text, password, textarea, title, ul, xml_declaration, radio
24 submit, text, password, textarea, title, ul, xml_declaration, radio
25 from webhelpers.html.tools import auto_link, button_to, highlight, \
25 from webhelpers.html.tools import auto_link, button_to, highlight, \
26 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
26 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
27 from webhelpers.number import format_byte_size, format_bit_size
27 from webhelpers.number import format_byte_size, format_bit_size
28 from webhelpers.pylonslib import Flash as _Flash
28 from webhelpers.pylonslib import Flash as _Flash
29 from webhelpers.pylonslib.secure_form import secure_form
29 from webhelpers.pylonslib.secure_form import secure_form
30 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
30 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
31 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
31 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
32 replace_whitespace, urlify, truncate, wrap_paragraphs
32 replace_whitespace, urlify, truncate, wrap_paragraphs
33 from webhelpers.date import time_ago_in_words
33 from webhelpers.date import time_ago_in_words
34 from webhelpers.paginate import Page
34 from webhelpers.paginate import Page
35 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
35 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
36 convert_boolean_attrs, NotGiven, _make_safe_id_component
36 convert_boolean_attrs, NotGiven, _make_safe_id_component
37
37
38 from rhodecode.lib.annotate import annotate_highlight
38 from rhodecode.lib.annotate import annotate_highlight
39 from rhodecode.lib.utils import repo_name_slug
39 from rhodecode.lib.utils import repo_name_slug
40 from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
40 from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
41 from rhodecode.lib.markup_renderer import MarkupRenderer
41 from rhodecode.lib.markup_renderer import MarkupRenderer
42
42
43
43 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
44 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
44 """
45 """
45 Reset button
46 Reset button
46 """
47 """
47 _set_input_attrs(attrs, type, name, value)
48 _set_input_attrs(attrs, type, name, value)
48 _set_id_attr(attrs, id, name)
49 _set_id_attr(attrs, id, name)
49 convert_boolean_attrs(attrs, ["disabled"])
50 convert_boolean_attrs(attrs, ["disabled"])
50 return HTML.input(**attrs)
51 return HTML.input(**attrs)
51
52
52 reset = _reset
53 reset = _reset
53 safeid = _make_safe_id_component
54 safeid = _make_safe_id_component
54
55
55
56
56 def FID(raw_id,path):
57 def FID(raw_id, path):
57 """
58 """
58 Creates a uniqe ID for filenode based on it's path and revision
59 Creates a uniqe ID for filenode based on it's path and revision
59
60
60 :param raw_id:
61 :param raw_id:
61 :param path:
62 :param path:
62 """
63 """
63 return 'C-%s-%s' % (short_id(raw_id), safeid(safe_unicode(path)))
64 return 'C-%s-%s' % (short_id(raw_id), safeid(safe_unicode(path)))
64
65
65
66
66 def get_token():
67 def get_token():
67 """Return the current authentication token, creating one if one doesn't
68 """Return the current authentication token, creating one if one doesn't
68 already exist.
69 already exist.
69 """
70 """
70 token_key = "_authentication_token"
71 token_key = "_authentication_token"
71 from pylons import session
72 from pylons import session
72 if not token_key in session:
73 if not token_key in session:
73 try:
74 try:
74 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
75 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
75 except AttributeError: # Python < 2.4
76 except AttributeError: # Python < 2.4
76 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
77 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
77 session[token_key] = token
78 session[token_key] = token
78 if hasattr(session, 'save'):
79 if hasattr(session, 'save'):
79 session.save()
80 session.save()
80 return session[token_key]
81 return session[token_key]
81
82
82 class _GetError(object):
83 class _GetError(object):
83 """Get error from form_errors, and represent it as span wrapped error
84 """Get error from form_errors, and represent it as span wrapped error
84 message
85 message
85
86
86 :param field_name: field to fetch errors for
87 :param field_name: field to fetch errors for
87 :param form_errors: form errors dict
88 :param form_errors: form errors dict
88 """
89 """
89
90
90 def __call__(self, field_name, form_errors):
91 def __call__(self, field_name, form_errors):
91 tmpl = """<span class="error_msg">%s</span>"""
92 tmpl = """<span class="error_msg">%s</span>"""
92 if form_errors and form_errors.has_key(field_name):
93 if form_errors and form_errors.has_key(field_name):
93 return literal(tmpl % form_errors.get(field_name))
94 return literal(tmpl % form_errors.get(field_name))
94
95
95 get_error = _GetError()
96 get_error = _GetError()
96
97
97 class _ToolTip(object):
98 class _ToolTip(object):
98
99
99 def __call__(self, tooltip_title, trim_at=50):
100 def __call__(self, tooltip_title, trim_at=50):
100 """Special function just to wrap our text into nice formatted
101 """Special function just to wrap our text into nice formatted
101 autowrapped text
102 autowrapped text
102
103
103 :param tooltip_title:
104 :param tooltip_title:
104 """
105 """
105 return escape(tooltip_title)
106 return escape(tooltip_title)
106 tooltip = _ToolTip()
107 tooltip = _ToolTip()
107
108
108 class _FilesBreadCrumbs(object):
109 class _FilesBreadCrumbs(object):
109
110
110 def __call__(self, repo_name, rev, paths):
111 def __call__(self, repo_name, rev, paths):
111 if isinstance(paths, str):
112 if isinstance(paths, str):
112 paths = safe_unicode(paths)
113 paths = safe_unicode(paths)
113 url_l = [link_to(repo_name, url('files_home',
114 url_l = [link_to(repo_name, url('files_home',
114 repo_name=repo_name,
115 repo_name=repo_name,
115 revision=rev, f_path=''))]
116 revision=rev, f_path=''))]
116 paths_l = paths.split('/')
117 paths_l = paths.split('/')
117 for cnt, p in enumerate(paths_l):
118 for cnt, p in enumerate(paths_l):
118 if p != '':
119 if p != '':
119 url_l.append(link_to(p,
120 url_l.append(link_to(p,
120 url('files_home',
121 url('files_home',
121 repo_name=repo_name,
122 repo_name=repo_name,
122 revision=rev,
123 revision=rev,
123 f_path='/'.join(paths_l[:cnt + 1])
124 f_path='/'.join(paths_l[:cnt + 1])
124 )
125 )
125 )
126 )
126 )
127 )
127
128
128 return literal('/'.join(url_l))
129 return literal('/'.join(url_l))
129
130
130 files_breadcrumbs = _FilesBreadCrumbs()
131 files_breadcrumbs = _FilesBreadCrumbs()
131
132
132 class CodeHtmlFormatter(HtmlFormatter):
133 class CodeHtmlFormatter(HtmlFormatter):
133 """My code Html Formatter for source codes
134 """My code Html Formatter for source codes
134 """
135 """
135
136
136 def wrap(self, source, outfile):
137 def wrap(self, source, outfile):
137 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
138 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
138
139
139 def _wrap_code(self, source):
140 def _wrap_code(self, source):
140 for cnt, it in enumerate(source):
141 for cnt, it in enumerate(source):
141 i, t = it
142 i, t = it
142 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
143 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
143 yield i, t
144 yield i, t
144
145
145 def _wrap_tablelinenos(self, inner):
146 def _wrap_tablelinenos(self, inner):
146 dummyoutfile = StringIO.StringIO()
147 dummyoutfile = StringIO.StringIO()
147 lncount = 0
148 lncount = 0
148 for t, line in inner:
149 for t, line in inner:
149 if t:
150 if t:
150 lncount += 1
151 lncount += 1
151 dummyoutfile.write(line)
152 dummyoutfile.write(line)
152
153
153 fl = self.linenostart
154 fl = self.linenostart
154 mw = len(str(lncount + fl - 1))
155 mw = len(str(lncount + fl - 1))
155 sp = self.linenospecial
156 sp = self.linenospecial
156 st = self.linenostep
157 st = self.linenostep
157 la = self.lineanchors
158 la = self.lineanchors
158 aln = self.anchorlinenos
159 aln = self.anchorlinenos
159 nocls = self.noclasses
160 nocls = self.noclasses
160 if sp:
161 if sp:
161 lines = []
162 lines = []
162
163
163 for i in range(fl, fl + lncount):
164 for i in range(fl, fl + lncount):
164 if i % st == 0:
165 if i % st == 0:
165 if i % sp == 0:
166 if i % sp == 0:
166 if aln:
167 if aln:
167 lines.append('<a href="#%s%d" class="special">%*d</a>' %
168 lines.append('<a href="#%s%d" class="special">%*d</a>' %
168 (la, i, mw, i))
169 (la, i, mw, i))
169 else:
170 else:
170 lines.append('<span class="special">%*d</span>' % (mw, i))
171 lines.append('<span class="special">%*d</span>' % (mw, i))
171 else:
172 else:
172 if aln:
173 if aln:
173 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
174 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
174 else:
175 else:
175 lines.append('%*d' % (mw, i))
176 lines.append('%*d' % (mw, i))
176 else:
177 else:
177 lines.append('')
178 lines.append('')
178 ls = '\n'.join(lines)
179 ls = '\n'.join(lines)
179 else:
180 else:
180 lines = []
181 lines = []
181 for i in range(fl, fl + lncount):
182 for i in range(fl, fl + lncount):
182 if i % st == 0:
183 if i % st == 0:
183 if aln:
184 if aln:
184 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
185 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
185 else:
186 else:
186 lines.append('%*d' % (mw, i))
187 lines.append('%*d' % (mw, i))
187 else:
188 else:
188 lines.append('')
189 lines.append('')
189 ls = '\n'.join(lines)
190 ls = '\n'.join(lines)
190
191
191 # in case you wonder about the seemingly redundant <div> here: since the
192 # in case you wonder about the seemingly redundant <div> here: since the
192 # content in the other cell also is wrapped in a div, some browsers in
193 # content in the other cell also is wrapped in a div, some browsers in
193 # some configurations seem to mess up the formatting...
194 # some configurations seem to mess up the formatting...
194 if nocls:
195 if nocls:
195 yield 0, ('<table class="%stable">' % self.cssclass +
196 yield 0, ('<table class="%stable">' % self.cssclass +
196 '<tr><td><div class="linenodiv" '
197 '<tr><td><div class="linenodiv" '
197 'style="background-color: #f0f0f0; padding-right: 10px">'
198 'style="background-color: #f0f0f0; padding-right: 10px">'
198 '<pre style="line-height: 125%">' +
199 '<pre style="line-height: 125%">' +
199 ls + '</pre></div></td><td id="hlcode" class="code">')
200 ls + '</pre></div></td><td id="hlcode" class="code">')
200 else:
201 else:
201 yield 0, ('<table class="%stable">' % self.cssclass +
202 yield 0, ('<table class="%stable">' % self.cssclass +
202 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
203 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
203 ls + '</pre></div></td><td id="hlcode" class="code">')
204 ls + '</pre></div></td><td id="hlcode" class="code">')
204 yield 0, dummyoutfile.getvalue()
205 yield 0, dummyoutfile.getvalue()
205 yield 0, '</td></tr></table>'
206 yield 0, '</td></tr></table>'
206
207
207
208
208 def pygmentize(filenode, **kwargs):
209 def pygmentize(filenode, **kwargs):
209 """pygmentize function using pygments
210 """pygmentize function using pygments
210
211
211 :param filenode:
212 :param filenode:
212 """
213 """
213
214
214 return literal(code_highlight(filenode.content,
215 return literal(code_highlight(filenode.content,
215 filenode.lexer, CodeHtmlFormatter(**kwargs)))
216 filenode.lexer, CodeHtmlFormatter(**kwargs)))
216
217
217
218
218 def pygmentize_annotation(repo_name, filenode, **kwargs):
219 def pygmentize_annotation(repo_name, filenode, **kwargs):
219 """
220 """
220 pygmentize function for annotation
221 pygmentize function for annotation
221
222
222 :param filenode:
223 :param filenode:
223 """
224 """
224
225
225 color_dict = {}
226 color_dict = {}
226
227
227 def gen_color(n=10000):
228 def gen_color(n=10000):
228 """generator for getting n of evenly distributed colors using
229 """generator for getting n of evenly distributed colors using
229 hsv color and golden ratio. It always return same order of colors
230 hsv color and golden ratio. It always return same order of colors
230
231
231 :returns: RGB tuple
232 :returns: RGB tuple
232 """
233 """
233
234
234 def hsv_to_rgb(h, s, v):
235 def hsv_to_rgb(h, s, v):
235 if s == 0.0:
236 if s == 0.0:
236 return v, v, v
237 return v, v, v
237 i = int(h * 6.0) # XXX assume int() truncates!
238 i = int(h * 6.0) # XXX assume int() truncates!
238 f = (h * 6.0) - i
239 f = (h * 6.0) - i
239 p = v * (1.0 - s)
240 p = v * (1.0 - s)
240 q = v * (1.0 - s * f)
241 q = v * (1.0 - s * f)
241 t = v * (1.0 - s * (1.0 - f))
242 t = v * (1.0 - s * (1.0 - f))
242 i = i % 6
243 i = i % 6
243 if i == 0:
244 if i == 0:
244 return v, t, p
245 return v, t, p
245 if i == 1:
246 if i == 1:
246 return q, v, p
247 return q, v, p
247 if i == 2:
248 if i == 2:
248 return p, v, t
249 return p, v, t
249 if i == 3:
250 if i == 3:
250 return p, q, v
251 return p, q, v
251 if i == 4:
252 if i == 4:
252 return t, p, v
253 return t, p, v
253 if i == 5:
254 if i == 5:
254 return v, p, q
255 return v, p, q
255
256
256 golden_ratio = 0.618033988749895
257 golden_ratio = 0.618033988749895
257 h = 0.22717784590367374
258 h = 0.22717784590367374
258
259
259 for _ in xrange(n):
260 for _ in xrange(n):
260 h += golden_ratio
261 h += golden_ratio
261 h %= 1
262 h %= 1
262 HSV_tuple = [h, 0.95, 0.95]
263 HSV_tuple = [h, 0.95, 0.95]
263 RGB_tuple = hsv_to_rgb(*HSV_tuple)
264 RGB_tuple = hsv_to_rgb(*HSV_tuple)
264 yield map(lambda x: str(int(x * 256)), RGB_tuple)
265 yield map(lambda x: str(int(x * 256)), RGB_tuple)
265
266
266 cgenerator = gen_color()
267 cgenerator = gen_color()
267
268
268 def get_color_string(cs):
269 def get_color_string(cs):
269 if cs in color_dict:
270 if cs in color_dict:
270 col = color_dict[cs]
271 col = color_dict[cs]
271 else:
272 else:
272 col = color_dict[cs] = cgenerator.next()
273 col = color_dict[cs] = cgenerator.next()
273 return "color: rgb(%s)! important;" % (', '.join(col))
274 return "color: rgb(%s)! important;" % (', '.join(col))
274
275
275 def url_func(repo_name):
276 def url_func(repo_name):
276
277
277 def _url_func(changeset):
278 def _url_func(changeset):
278 author = changeset.author
279 author = changeset.author
279 date = changeset.date
280 date = changeset.date
280 message = tooltip(changeset.message)
281 message = tooltip(changeset.message)
281
282
282 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
283 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
283 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
284 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
284 "</b> %s<br/></div>")
285 "</b> %s<br/></div>")
285
286
286 tooltip_html = tooltip_html % (author, date, message)
287 tooltip_html = tooltip_html % (author, date, message)
287 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
288 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
288 short_id(changeset.raw_id))
289 short_id(changeset.raw_id))
289 uri = link_to(
290 uri = link_to(
290 lnk_format,
291 lnk_format,
291 url('changeset_home', repo_name=repo_name,
292 url('changeset_home', repo_name=repo_name,
292 revision=changeset.raw_id),
293 revision=changeset.raw_id),
293 style=get_color_string(changeset.raw_id),
294 style=get_color_string(changeset.raw_id),
294 class_='tooltip',
295 class_='tooltip',
295 title=tooltip_html
296 title=tooltip_html
296 )
297 )
297
298
298 uri += '\n'
299 uri += '\n'
299 return uri
300 return uri
300 return _url_func
301 return _url_func
301
302
302 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
303 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
303
304
304
305
305 def is_following_repo(repo_name, user_id):
306 def is_following_repo(repo_name, user_id):
306 from rhodecode.model.scm import ScmModel
307 from rhodecode.model.scm import ScmModel
307 return ScmModel().is_following_repo(repo_name, user_id)
308 return ScmModel().is_following_repo(repo_name, user_id)
308
309
309 flash = _Flash()
310 flash = _Flash()
310
311
311 #==============================================================================
312 #==============================================================================
312 # SCM FILTERS available via h.
313 # SCM FILTERS available via h.
313 #==============================================================================
314 #==============================================================================
314 from vcs.utils import author_name, author_email
315 from vcs.utils import author_name, author_email
315 from rhodecode.lib import credentials_filter, age as _age
316 from rhodecode.lib import credentials_filter, age as _age
316 from rhodecode.model.db import User
317 from rhodecode.model.db import User
317
318
318 age = lambda x: _age(x)
319 age = lambda x: _age(x)
319 capitalize = lambda x: x.capitalize()
320 capitalize = lambda x: x.capitalize()
320 email = author_email
321 email = author_email
321 short_id = lambda x: x[:12]
322 short_id = lambda x: x[:12]
322 hide_credentials = lambda x: ''.join(credentials_filter(x))
323 hide_credentials = lambda x: ''.join(credentials_filter(x))
323
324
324
325
325 def email_or_none(author):
326 def email_or_none(author):
326 _email = email(author)
327 _email = email(author)
327 if _email != '':
328 if _email != '':
328 return _email
329 return _email
329
330
330 # See if it contains a username we can get an email from
331 # See if it contains a username we can get an email from
331 user = User.get_by_username(author_name(author), case_insensitive=True,
332 user = User.get_by_username(author_name(author), case_insensitive=True,
332 cache=True)
333 cache=True)
333 if user is not None:
334 if user is not None:
334 return user.email
335 return user.email
335
336
336 # No valid email, not a valid user in the system, none!
337 # No valid email, not a valid user in the system, none!
337 return None
338 return None
338
339
339
340
340 def person(author):
341 def person(author):
341 # attr to return from fetched user
342 # attr to return from fetched user
342 person_getter = lambda usr: usr.username
343 person_getter = lambda usr: usr.username
343
344
344 # Valid email in the attribute passed, see if they're in the system
345 # Valid email in the attribute passed, see if they're in the system
345 _email = email(author)
346 _email = email(author)
346 if _email != '':
347 if _email != '':
347 user = User.get_by_email(_email, case_insensitive=True, cache=True)
348 user = User.get_by_email(_email, case_insensitive=True, cache=True)
348 if user is not None:
349 if user is not None:
349 return person_getter(user)
350 return person_getter(user)
350 return _email
351 return _email
351
352
352 # Maybe it's a username?
353 # Maybe it's a username?
353 _author = author_name(author)
354 _author = author_name(author)
354 user = User.get_by_username(_author, case_insensitive=True,
355 user = User.get_by_username(_author, case_insensitive=True,
355 cache=True)
356 cache=True)
356 if user is not None:
357 if user is not None:
357 return person_getter(user)
358 return person_getter(user)
358
359
359 # Still nothing? Just pass back the author name then
360 # Still nothing? Just pass back the author name then
360 return _author
361 return _author
361
362
362 def bool2icon(value):
363 def bool2icon(value):
363 """Returns True/False values represented as small html image of true/false
364 """Returns True/False values represented as small html image of true/false
364 icons
365 icons
365
366
366 :param value: bool value
367 :param value: bool value
367 """
368 """
368
369
369 if value is True:
370 if value is True:
370 return HTML.tag('img', src=url("/images/icons/accept.png"),
371 return HTML.tag('img', src=url("/images/icons/accept.png"),
371 alt=_('True'))
372 alt=_('True'))
372
373
373 if value is False:
374 if value is False:
374 return HTML.tag('img', src=url("/images/icons/cancel.png"),
375 return HTML.tag('img', src=url("/images/icons/cancel.png"),
375 alt=_('False'))
376 alt=_('False'))
376
377
377 return value
378 return value
378
379
379
380
380 def action_parser(user_log, feed=False):
381 def action_parser(user_log, feed=False):
381 """This helper will action_map the specified string action into translated
382 """This helper will action_map the specified string action into translated
382 fancy names with icons and links
383 fancy names with icons and links
383
384
384 :param user_log: user log instance
385 :param user_log: user log instance
385 :param feed: use output for feeds (no html and fancy icons)
386 :param feed: use output for feeds (no html and fancy icons)
386 """
387 """
387
388
388 action = user_log.action
389 action = user_log.action
389 action_params = ' '
390 action_params = ' '
390
391
391 x = action.split(':')
392 x = action.split(':')
392
393
393 if len(x) > 1:
394 if len(x) > 1:
394 action, action_params = x
395 action, action_params = x
395
396
396 def get_cs_links():
397 def get_cs_links():
397 revs_limit = 3 #display this amount always
398 revs_limit = 3 #display this amount always
398 revs_top_limit = 50 #show upto this amount of changesets hidden
399 revs_top_limit = 50 #show upto this amount of changesets hidden
399 revs = action_params.split(',')
400 revs = action_params.split(',')
400 repo_name = user_log.repository.repo_name
401 repo_name = user_log.repository.repo_name
401
402
402 from rhodecode.model.scm import ScmModel
403 from rhodecode.model.scm import ScmModel
403 repo = user_log.repository.scm_instance
404 repo = user_log.repository.scm_instance
404
405
405 message = lambda rev: get_changeset_safe(repo, rev).message
406 message = lambda rev: get_changeset_safe(repo, rev).message
406 cs_links = []
407 cs_links = []
407 cs_links.append(" " + ', '.join ([link_to(rev,
408 cs_links.append(" " + ', '.join ([link_to(rev,
408 url('changeset_home',
409 url('changeset_home',
409 repo_name=repo_name,
410 repo_name=repo_name,
410 revision=rev), title=tooltip(message(rev)),
411 revision=rev), title=tooltip(message(rev)),
411 class_='tooltip') for rev in revs[:revs_limit] ]))
412 class_='tooltip') for rev in revs[:revs_limit] ]))
412
413
413 compare_view = (' <div class="compare_view tooltip" title="%s">'
414 compare_view = (' <div class="compare_view tooltip" title="%s">'
414 '<a href="%s">%s</a> '
415 '<a href="%s">%s</a> '
415 '</div>' % (_('Show all combined changesets %s->%s' \
416 '</div>' % (_('Show all combined changesets %s->%s' \
416 % (revs[0], revs[-1])),
417 % (revs[0], revs[-1])),
417 url('changeset_home', repo_name=repo_name,
418 url('changeset_home', repo_name=repo_name,
418 revision='%s...%s' % (revs[0], revs[-1])
419 revision='%s...%s' % (revs[0], revs[-1])
419 ),
420 ),
420 _('compare view'))
421 _('compare view'))
421 )
422 )
422
423
423 if len(revs) > revs_limit:
424 if len(revs) > revs_limit:
424 uniq_id = revs[0]
425 uniq_id = revs[0]
425 html_tmpl = ('<span> %s '
426 html_tmpl = ('<span> %s '
426 '<a class="show_more" id="_%s" href="#more">%s</a> '
427 '<a class="show_more" id="_%s" href="#more">%s</a> '
427 '%s</span>')
428 '%s</span>')
428 if not feed:
429 if not feed:
429 cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
430 cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
430 % (len(revs) - revs_limit),
431 % (len(revs) - revs_limit),
431 _('revisions')))
432 _('revisions')))
432
433
433 if not feed:
434 if not feed:
434 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
435 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
435 else:
436 else:
436 html_tmpl = '<span id="%s"> %s </span>'
437 html_tmpl = '<span id="%s"> %s </span>'
437
438
438 cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
439 cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
439 url('changeset_home',
440 url('changeset_home',
440 repo_name=repo_name, revision=rev),
441 repo_name=repo_name, revision=rev),
441 title=message(rev), class_='tooltip')
442 title=message(rev), class_='tooltip')
442 for rev in revs[revs_limit:revs_top_limit]])))
443 for rev in revs[revs_limit:revs_top_limit]])))
443 if len(revs) > 1:
444 if len(revs) > 1:
444 cs_links.append(compare_view)
445 cs_links.append(compare_view)
445 return ''.join(cs_links)
446 return ''.join(cs_links)
446
447
447 def get_fork_name():
448 def get_fork_name():
448 repo_name = action_params
449 repo_name = action_params
449 return _('fork name ') + str(link_to(action_params, url('summary_home',
450 return _('fork name ') + str(link_to(action_params, url('summary_home',
450 repo_name=repo_name,)))
451 repo_name=repo_name,)))
451
452
452 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
453 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
453 'user_created_repo':(_('[created] repository'), None),
454 'user_created_repo':(_('[created] repository'), None),
454 'user_created_fork':(_('[created] repository as fork'), None),
455 'user_created_fork':(_('[created] repository as fork'), None),
455 'user_forked_repo':(_('[forked] repository'), get_fork_name),
456 'user_forked_repo':(_('[forked] repository'), get_fork_name),
456 'user_updated_repo':(_('[updated] repository'), None),
457 'user_updated_repo':(_('[updated] repository'), None),
457 'admin_deleted_repo':(_('[delete] repository'), None),
458 'admin_deleted_repo':(_('[delete] repository'), None),
458 'admin_created_repo':(_('[created] repository'), None),
459 'admin_created_repo':(_('[created] repository'), None),
459 'admin_forked_repo':(_('[forked] repository'), None),
460 'admin_forked_repo':(_('[forked] repository'), None),
460 'admin_updated_repo':(_('[updated] repository'), None),
461 'admin_updated_repo':(_('[updated] repository'), None),
461 'push':(_('[pushed] into'), get_cs_links),
462 'push':(_('[pushed] into'), get_cs_links),
462 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
463 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
463 'push_remote':(_('[pulled from remote] into'), get_cs_links),
464 'push_remote':(_('[pulled from remote] into'), get_cs_links),
464 'pull':(_('[pulled] from'), None),
465 'pull':(_('[pulled] from'), None),
465 'started_following_repo':(_('[started following] repository'), None),
466 'started_following_repo':(_('[started following] repository'), None),
466 'stopped_following_repo':(_('[stopped following] repository'), None),
467 'stopped_following_repo':(_('[stopped following] repository'), None),
467 }
468 }
468
469
469 action_str = action_map.get(action, action)
470 action_str = action_map.get(action, action)
470 if feed:
471 if feed:
471 action = action_str[0].replace('[', '').replace(']', '')
472 action = action_str[0].replace('[', '').replace(']', '')
472 else:
473 else:
473 action = action_str[0].replace('[', '<span class="journal_highlight">')\
474 action = action_str[0].replace('[', '<span class="journal_highlight">')\
474 .replace(']', '</span>')
475 .replace(']', '</span>')
475
476
476 action_params_func = lambda :""
477 action_params_func = lambda :""
477
478
478 if callable(action_str[1]):
479 if callable(action_str[1]):
479 action_params_func = action_str[1]
480 action_params_func = action_str[1]
480
481
481 return [literal(action), action_params_func]
482 return [literal(action), action_params_func]
482
483
483 def action_parser_icon(user_log):
484 def action_parser_icon(user_log):
484 action = user_log.action
485 action = user_log.action
485 action_params = None
486 action_params = None
486 x = action.split(':')
487 x = action.split(':')
487
488
488 if len(x) > 1:
489 if len(x) > 1:
489 action, action_params = x
490 action, action_params = x
490
491
491 tmpl = """<img src="%s%s" alt="%s"/>"""
492 tmpl = """<img src="%s%s" alt="%s"/>"""
492 map = {'user_deleted_repo':'database_delete.png',
493 map = {'user_deleted_repo':'database_delete.png',
493 'user_created_repo':'database_add.png',
494 'user_created_repo':'database_add.png',
494 'user_created_fork':'arrow_divide.png',
495 'user_created_fork':'arrow_divide.png',
495 'user_forked_repo':'arrow_divide.png',
496 'user_forked_repo':'arrow_divide.png',
496 'user_updated_repo':'database_edit.png',
497 'user_updated_repo':'database_edit.png',
497 'admin_deleted_repo':'database_delete.png',
498 'admin_deleted_repo':'database_delete.png',
498 'admin_created_repo':'database_add.png',
499 'admin_created_repo':'database_add.png',
499 'admin_forked_repo':'arrow_divide.png',
500 'admin_forked_repo':'arrow_divide.png',
500 'admin_updated_repo':'database_edit.png',
501 'admin_updated_repo':'database_edit.png',
501 'push':'script_add.png',
502 'push':'script_add.png',
502 'push_local':'script_edit.png',
503 'push_local':'script_edit.png',
503 'push_remote':'connect.png',
504 'push_remote':'connect.png',
504 'pull':'down_16.png',
505 'pull':'down_16.png',
505 'started_following_repo':'heart_add.png',
506 'started_following_repo':'heart_add.png',
506 'stopped_following_repo':'heart_delete.png',
507 'stopped_following_repo':'heart_delete.png',
507 }
508 }
508 return literal(tmpl % ((url('/images/icons/')),
509 return literal(tmpl % ((url('/images/icons/')),
509 map.get(action, action), action))
510 map.get(action, action), action))
510
511
511
512
512 #==============================================================================
513 #==============================================================================
513 # PERMS
514 # PERMS
514 #==============================================================================
515 #==============================================================================
515 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
516 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
516 HasRepoPermissionAny, HasRepoPermissionAll
517 HasRepoPermissionAny, HasRepoPermissionAll
517
518
518 #==============================================================================
519 #==============================================================================
519 # GRAVATAR URL
520 # GRAVATAR URL
520 #==============================================================================
521 #==============================================================================
521
522
522 def gravatar_url(email_address, size=30):
523 def gravatar_url(email_address, size=30):
523 if (not str2bool(config['app_conf'].get('use_gravatar')) or
524 if (not str2bool(config['app_conf'].get('use_gravatar')) or
524 not email_address or email_address == 'anonymous@rhodecode.org'):
525 not email_address or email_address == 'anonymous@rhodecode.org'):
525 return url("/images/user%s.png" % size)
526 return url("/images/user%s.png" % size)
526
527
527 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
528 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
528 default = 'identicon'
529 default = 'identicon'
529 baseurl_nossl = "http://www.gravatar.com/avatar/"
530 baseurl_nossl = "http://www.gravatar.com/avatar/"
530 baseurl_ssl = "https://secure.gravatar.com/avatar/"
531 baseurl_ssl = "https://secure.gravatar.com/avatar/"
531 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
532 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
532
533
533 if isinstance(email_address, unicode):
534 if isinstance(email_address, unicode):
534 #hashlib crashes on unicode items
535 #hashlib crashes on unicode items
535 email_address = safe_str(email_address)
536 email_address = safe_str(email_address)
536 # construct the url
537 # construct the url
537 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
538 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
538 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
539 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
539
540
540 return gravatar_url
541 return gravatar_url
541
542
542
543
543 #==============================================================================
544 #==============================================================================
544 # REPO PAGER, PAGER FOR REPOSITORY
545 # REPO PAGER, PAGER FOR REPOSITORY
545 #==============================================================================
546 #==============================================================================
546 class RepoPage(Page):
547 class RepoPage(Page):
547
548
548 def __init__(self, collection, page=1, items_per_page=20,
549 def __init__(self, collection, page=1, items_per_page=20,
549 item_count=None, url=None, **kwargs):
550 item_count=None, url=None, **kwargs):
550
551
551 """Create a "RepoPage" instance. special pager for paging
552 """Create a "RepoPage" instance. special pager for paging
552 repository
553 repository
553 """
554 """
554 self._url_generator = url
555 self._url_generator = url
555
556
556 # Safe the kwargs class-wide so they can be used in the pager() method
557 # Safe the kwargs class-wide so they can be used in the pager() method
557 self.kwargs = kwargs
558 self.kwargs = kwargs
558
559
559 # Save a reference to the collection
560 # Save a reference to the collection
560 self.original_collection = collection
561 self.original_collection = collection
561
562
562 self.collection = collection
563 self.collection = collection
563
564
564 # The self.page is the number of the current page.
565 # The self.page is the number of the current page.
565 # The first page has the number 1!
566 # The first page has the number 1!
566 try:
567 try:
567 self.page = int(page) # make it int() if we get it as a string
568 self.page = int(page) # make it int() if we get it as a string
568 except (ValueError, TypeError):
569 except (ValueError, TypeError):
569 self.page = 1
570 self.page = 1
570
571
571 self.items_per_page = items_per_page
572 self.items_per_page = items_per_page
572
573
573 # Unless the user tells us how many items the collections has
574 # Unless the user tells us how many items the collections has
574 # we calculate that ourselves.
575 # we calculate that ourselves.
575 if item_count is not None:
576 if item_count is not None:
576 self.item_count = item_count
577 self.item_count = item_count
577 else:
578 else:
578 self.item_count = len(self.collection)
579 self.item_count = len(self.collection)
579
580
580 # Compute the number of the first and last available page
581 # Compute the number of the first and last available page
581 if self.item_count > 0:
582 if self.item_count > 0:
582 self.first_page = 1
583 self.first_page = 1
583 self.page_count = int(math.ceil(float(self.item_count) /
584 self.page_count = int(math.ceil(float(self.item_count) /
584 self.items_per_page))
585 self.items_per_page))
585 self.last_page = self.first_page + self.page_count - 1
586 self.last_page = self.first_page + self.page_count - 1
586
587
587 # Make sure that the requested page number is the range of
588 # Make sure that the requested page number is the range of
588 # valid pages
589 # valid pages
589 if self.page > self.last_page:
590 if self.page > self.last_page:
590 self.page = self.last_page
591 self.page = self.last_page
591 elif self.page < self.first_page:
592 elif self.page < self.first_page:
592 self.page = self.first_page
593 self.page = self.first_page
593
594
594 # Note: the number of items on this page can be less than
595 # Note: the number of items on this page can be less than
595 # items_per_page if the last page is not full
596 # items_per_page if the last page is not full
596 self.first_item = max(0, (self.item_count) - (self.page *
597 self.first_item = max(0, (self.item_count) - (self.page *
597 items_per_page))
598 items_per_page))
598 self.last_item = ((self.item_count - 1) - items_per_page *
599 self.last_item = ((self.item_count - 1) - items_per_page *
599 (self.page - 1))
600 (self.page - 1))
600
601
601 self.items = list(self.collection[self.first_item:self.last_item + 1])
602 self.items = list(self.collection[self.first_item:self.last_item + 1])
602
603
603
604
604 # Links to previous and next page
605 # Links to previous and next page
605 if self.page > self.first_page:
606 if self.page > self.first_page:
606 self.previous_page = self.page - 1
607 self.previous_page = self.page - 1
607 else:
608 else:
608 self.previous_page = None
609 self.previous_page = None
609
610
610 if self.page < self.last_page:
611 if self.page < self.last_page:
611 self.next_page = self.page + 1
612 self.next_page = self.page + 1
612 else:
613 else:
613 self.next_page = None
614 self.next_page = None
614
615
615 # No items available
616 # No items available
616 else:
617 else:
617 self.first_page = None
618 self.first_page = None
618 self.page_count = 0
619 self.page_count = 0
619 self.last_page = None
620 self.last_page = None
620 self.first_item = None
621 self.first_item = None
621 self.last_item = None
622 self.last_item = None
622 self.previous_page = None
623 self.previous_page = None
623 self.next_page = None
624 self.next_page = None
624 self.items = []
625 self.items = []
625
626
626 # This is a subclass of the 'list' type. Initialise the list now.
627 # This is a subclass of the 'list' type. Initialise the list now.
627 list.__init__(self, reversed(self.items))
628 list.__init__(self, reversed(self.items))
628
629
629
630
630 def changed_tooltip(nodes):
631 def changed_tooltip(nodes):
631 """
632 """
632 Generates a html string for changed nodes in changeset page.
633 Generates a html string for changed nodes in changeset page.
633 It limits the output to 30 entries
634 It limits the output to 30 entries
634
635
635 :param nodes: LazyNodesGenerator
636 :param nodes: LazyNodesGenerator
636 """
637 """
637 if nodes:
638 if nodes:
638 pref = ': <br/> '
639 pref = ': <br/> '
639 suf = ''
640 suf = ''
640 if len(nodes) > 30:
641 if len(nodes) > 30:
641 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
642 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
642 return literal(pref + '<br/> '.join([safe_unicode(x.path)
643 return literal(pref + '<br/> '.join([safe_unicode(x.path)
643 for x in nodes[:30]]) + suf)
644 for x in nodes[:30]]) + suf)
644 else:
645 else:
645 return ': ' + _('No Files')
646 return ': ' + _('No Files')
646
647
647
648
648
649
649 def repo_link(groups_and_repos):
650 def repo_link(groups_and_repos):
650 """
651 """
651 Makes a breadcrumbs link to repo within a group
652 Makes a breadcrumbs link to repo within a group
652 joins &raquo; on each group to create a fancy link
653 joins &raquo; on each group to create a fancy link
653
654
654 ex::
655 ex::
655 group >> subgroup >> repo
656 group >> subgroup >> repo
656
657
657 :param groups_and_repos:
658 :param groups_and_repos:
658 """
659 """
659 groups, repo_name = groups_and_repos
660 groups, repo_name = groups_and_repos
660
661
661 if not groups:
662 if not groups:
662 return repo_name
663 return repo_name
663 else:
664 else:
664 def make_link(group):
665 def make_link(group):
665 return link_to(group.name, url('repos_group_home',
666 return link_to(group.name, url('repos_group_home',
666 group_name=group.group_name))
667 group_name=group.group_name))
667 return literal(' &raquo; '.join(map(make_link, groups)) + \
668 return literal(' &raquo; '.join(map(make_link, groups)) + \
668 " &raquo; " + repo_name)
669 " &raquo; " + repo_name)
669
670
670 def fancy_file_stats(stats):
671 def fancy_file_stats(stats):
671 """
672 """
672 Displays a fancy two colored bar for number of added/deleted
673 Displays a fancy two colored bar for number of added/deleted
673 lines of code on file
674 lines of code on file
674
675
675 :param stats: two element list of added/deleted lines of code
676 :param stats: two element list of added/deleted lines of code
676 """
677 """
677
678
678 a, d, t = stats[0], stats[1], stats[0] + stats[1]
679 a, d, t = stats[0], stats[1], stats[0] + stats[1]
679 width = 100
680 width = 100
680 unit = float(width) / (t or 1)
681 unit = float(width) / (t or 1)
681
682
682 # needs > 9% of width to be visible or 0 to be hidden
683 # needs > 9% of width to be visible or 0 to be hidden
683 a_p = max(9, unit * a) if a > 0 else 0
684 a_p = max(9, unit * a) if a > 0 else 0
684 d_p = max(9, unit * d) if d > 0 else 0
685 d_p = max(9, unit * d) if d > 0 else 0
685 p_sum = a_p + d_p
686 p_sum = a_p + d_p
686
687
687 if p_sum > width:
688 if p_sum > width:
688 #adjust the percentage to be == 100% since we adjusted to 9
689 #adjust the percentage to be == 100% since we adjusted to 9
689 if a_p > d_p:
690 if a_p > d_p:
690 a_p = a_p - (p_sum - width)
691 a_p = a_p - (p_sum - width)
691 else:
692 else:
692 d_p = d_p - (p_sum - width)
693 d_p = d_p - (p_sum - width)
693
694
694 a_v = a if a > 0 else ''
695 a_v = a if a > 0 else ''
695 d_v = d if d > 0 else ''
696 d_v = d if d > 0 else ''
696
697
697
698
698 def cgen(l_type):
699 def cgen(l_type):
699 mapping = {'tr':'top-right-rounded-corner',
700 mapping = {'tr':'top-right-rounded-corner',
700 'tl':'top-left-rounded-corner',
701 'tl':'top-left-rounded-corner',
701 'br':'bottom-right-rounded-corner',
702 'br':'bottom-right-rounded-corner',
702 'bl':'bottom-left-rounded-corner'}
703 'bl':'bottom-left-rounded-corner'}
703 map_getter = lambda x:mapping[x]
704 map_getter = lambda x:mapping[x]
704
705
705 if l_type == 'a' and d_v:
706 if l_type == 'a' and d_v:
706 #case when added and deleted are present
707 #case when added and deleted are present
707 return ' '.join(map(map_getter, ['tl', 'bl']))
708 return ' '.join(map(map_getter, ['tl', 'bl']))
708
709
709 if l_type == 'a' and not d_v:
710 if l_type == 'a' and not d_v:
710 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
711 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
711
712
712 if l_type == 'd' and a_v:
713 if l_type == 'd' and a_v:
713 return ' '.join(map(map_getter, ['tr', 'br']))
714 return ' '.join(map(map_getter, ['tr', 'br']))
714
715
715 if l_type == 'd' and not a_v:
716 if l_type == 'd' and not a_v:
716 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
717 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
717
718
718
719
719
720
720 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
721 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
721 a_p, a_v)
722 a_p, a_v)
722 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
723 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
723 d_p, d_v)
724 d_p, d_v)
724 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
725 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
725
726
726
727
727 def urlify_text(text):
728 def urlify_text(text):
728 import re
729 import re
729
730
730 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
731 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
731 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
732 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
732
733
733 def url_func(match_obj):
734 def url_func(match_obj):
734 url_full = match_obj.groups()[0]
735 url_full = match_obj.groups()[0]
735 return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
736 return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
736
737
737 return literal(url_pat.sub(url_func, text))
738 return literal(url_pat.sub(url_func, text))
738
739
739
740
740 def rst(source):
741 def rst(source):
741 return literal('<div class="rst-block">%s</div>' %
742 return literal('<div class="rst-block">%s</div>' %
742 MarkupRenderer.rst(source))
743 MarkupRenderer.rst(source))
743
744
744 def rst_w_mentions(source):
745 def rst_w_mentions(source):
745 """
746 """
746 Wrapped rst renderer with @mention highlighting
747 Wrapped rst renderer with @mention highlighting
747
748
748 :param source:
749 :param source:
749 """
750 """
750 return literal('<div class="rst-block">%s</div>' %
751 return literal('<div class="rst-block">%s</div>' %
751 MarkupRenderer.rst_with_mentions(source))
752 MarkupRenderer.rst_with_mentions(source))
@@ -1,144 +1,143 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.comment
3 rhodecode.model.comment
4 ~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 comments model for RhodeCode
6 comments model for RhodeCode
7
7
8 :created_on: Nov 11, 2011
8 :created_on: Nov 11, 2011
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 from pylons.i18n.translation import _
29 from pylons.i18n.translation import _
30 from sqlalchemy.util.compat import defaultdict
30 from sqlalchemy.util.compat import defaultdict
31
31
32 from rhodecode.lib import extract_mentioned_users
32 from rhodecode.lib import extract_mentioned_users
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.model import BaseModel
34 from rhodecode.model import BaseModel
35 from rhodecode.model.db import ChangesetComment, User, Repository, Notification
35 from rhodecode.model.db import ChangesetComment, User, Repository, Notification
36 from rhodecode.model.notification import NotificationModel
36 from rhodecode.model.notification import NotificationModel
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class ChangesetCommentsModel(BaseModel):
41 class ChangesetCommentsModel(BaseModel):
42
42
43 def __get_changeset_comment(self, changeset_comment):
43 def __get_changeset_comment(self, changeset_comment):
44 return self._get_instance(ChangesetComment, changeset_comment)
44 return self._get_instance(ChangesetComment, changeset_comment)
45
45
46 def _extract_mentions(self, s):
46 def _extract_mentions(self, s):
47 user_objects = []
47 user_objects = []
48 for username in extract_mentioned_users(s):
48 for username in extract_mentioned_users(s):
49 user_obj = User.get_by_username(username, case_insensitive=True)
49 user_obj = User.get_by_username(username, case_insensitive=True)
50 if user_obj:
50 if user_obj:
51 user_objects.append(user_obj)
51 user_objects.append(user_obj)
52 return user_objects
52 return user_objects
53
53
54 def create(self, text, repo_id, user_id, revision, f_path=None,
54 def create(self, text, repo_id, user_id, revision, f_path=None,
55 line_no=None):
55 line_no=None):
56 """
56 """
57 Creates new comment for changeset
57 Creates new comment for changeset
58
58
59 :param text:
59 :param text:
60 :param repo_id:
60 :param repo_id:
61 :param user_id:
61 :param user_id:
62 :param revision:
62 :param revision:
63 :param f_path:
63 :param f_path:
64 :param line_no:
64 :param line_no:
65 """
65 """
66 if text:
66 if text:
67 repo = Repository.get(repo_id)
67 repo = Repository.get(repo_id)
68 cs = repo.scm_instance.get_changeset(revision)
68 cs = repo.scm_instance.get_changeset(revision)
69 desc = cs.message
69 desc = cs.message
70 author = cs.author_email
70 author = cs.author_email
71 comment = ChangesetComment()
71 comment = ChangesetComment()
72 comment.repo = repo
72 comment.repo = repo
73 comment.user_id = user_id
73 comment.user_id = user_id
74 comment.revision = revision
74 comment.revision = revision
75 comment.text = text
75 comment.text = text
76 comment.f_path = f_path
76 comment.f_path = f_path
77 comment.line_no = line_no
77 comment.line_no = line_no
78
78
79 self.sa.add(comment)
79 self.sa.add(comment)
80 self.sa.flush()
80 self.sa.flush()
81
81
82 # make notification
82 # make notification
83 line = ''
83 line = ''
84 if line_no:
84 if line_no:
85 line = _('on line %s') % line_no
85 line = _('on line %s') % line_no
86 subj = h.link_to('Re commit: %(commit_desc)s %(line)s' % \
86 subj = h.link_to('Re commit: %(commit_desc)s %(line)s' % \
87 {'commit_desc':desc, 'line':line},
87 {'commit_desc': desc, 'line': line},
88 h.url('changeset_home', repo_name=repo.repo_name,
88 h.url('changeset_home', repo_name=repo.repo_name,
89 revision=revision,
89 revision=revision,
90 anchor='comment-%s' % comment.comment_id,
90 anchor='comment-%s' % comment.comment_id,
91 qualified=True,
91 qualified=True,
92 )
92 )
93 )
93 )
94 body = text
94 body = text
95 recipients = ChangesetComment.get_users(revision=revision)
95 recipients = ChangesetComment.get_users(revision=revision)
96 # add changeset author
96 # add changeset author
97 recipients += [User.get_by_email(author)]
97 recipients += [User.get_by_email(author)]
98
98
99 NotificationModel().create(created_by=user_id, subject=subj,
99 NotificationModel().create(created_by=user_id, subject=subj,
100 body=body, recipients=recipients,
100 body=body, recipients=recipients,
101 type_=Notification.TYPE_CHANGESET_COMMENT)
101 type_=Notification.TYPE_CHANGESET_COMMENT)
102
102
103 mention_recipients = set(self._extract_mentions(body))\
103 mention_recipients = set(self._extract_mentions(body))\
104 .difference(recipients)
104 .difference(recipients)
105 if mention_recipients:
105 if mention_recipients:
106 subj = _('[Mention]') + ' ' + subj
106 subj = _('[Mention]') + ' ' + subj
107 NotificationModel().create(created_by=user_id, subject=subj,
107 NotificationModel().create(created_by=user_id, subject=subj,
108 body=body,
108 body=body,
109 recipients=mention_recipients,
109 recipients=mention_recipients,
110 type_=Notification.TYPE_CHANGESET_COMMENT)
110 type_=Notification.TYPE_CHANGESET_COMMENT)
111
111
112 return comment
112 return comment
113
113
114 def delete(self, comment):
114 def delete(self, comment):
115 """
115 """
116 Deletes given comment
116 Deletes given comment
117
117
118 :param comment_id:
118 :param comment_id:
119 """
119 """
120 comment = self.__get_changeset_comment(comment)
120 comment = self.__get_changeset_comment(comment)
121 self.sa.delete(comment)
121 self.sa.delete(comment)
122
122
123 return comment
123 return comment
124
124
125
126 def get_comments(self, repo_id, revision):
125 def get_comments(self, repo_id, revision):
127 return ChangesetComment.query()\
126 return ChangesetComment.query()\
128 .filter(ChangesetComment.repo_id == repo_id)\
127 .filter(ChangesetComment.repo_id == repo_id)\
129 .filter(ChangesetComment.revision == revision)\
128 .filter(ChangesetComment.revision == revision)\
130 .filter(ChangesetComment.line_no == None)\
129 .filter(ChangesetComment.line_no == None)\
131 .filter(ChangesetComment.f_path == None).all()
130 .filter(ChangesetComment.f_path == None).all()
132
131
133 def get_inline_comments(self, repo_id, revision):
132 def get_inline_comments(self, repo_id, revision):
134 comments = self.sa.query(ChangesetComment)\
133 comments = self.sa.query(ChangesetComment)\
135 .filter(ChangesetComment.repo_id == repo_id)\
134 .filter(ChangesetComment.repo_id == repo_id)\
136 .filter(ChangesetComment.revision == revision)\
135 .filter(ChangesetComment.revision == revision)\
137 .filter(ChangesetComment.line_no != None)\
136 .filter(ChangesetComment.line_no != None)\
138 .filter(ChangesetComment.f_path != None).all()
137 .filter(ChangesetComment.f_path != None).all()
139
138
140 paths = defaultdict(lambda:defaultdict(list))
139 paths = defaultdict(lambda: defaultdict(list))
141
140
142 for co in comments:
141 for co in comments:
143 paths[co.f_path][co.line_no].append(co)
142 paths[co.f_path][co.line_no].append(co)
144 return paths.items()
143 return paths.items()
@@ -1,50 +1,46 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ##usage:
2 ##usage:
3 ## <%namespace name="diff_block" file="/changeset/diff_block.html"/>
3 ## <%namespace name="diff_block" file="/changeset/diff_block.html"/>
4 ## ${diff_block.diff_block(changes)}
4 ## ${diff_block.diff_block(changes)}
5 ##
5 ##
6 <%def name="diff_block(changes)">
6 <%def name="diff_block(changes)">
7
7
8 %for change,filenode,diff,cs1,cs2,stat in changes:
8 %for change,filenode,diff,cs1,cs2,stat in changes:
9 %if change !='removed':
9 %if change !='removed':
10 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}" style="clear:both;height:90px;margin-top:-60px"></div>
10 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}" style="clear:both;height:90px;margin-top:-60px"></div>
11 <div class="diffblock margined comm">
11 <div class="diffblock margined comm">
12 <div class="code-header">
12 <div class="code-header">
13 <div class="changeset_header">
13 <div class="changeset_header">
14 <div class="changeset_file">
14 <div class="changeset_file">
15 ${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name,
15 ${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name,
16 revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))}
16 revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))}
17 </div>
17 </div>
18 <div class="diff-menu-wrapper">
18 <div class="diff-menu-wrapper">
19 <img class="diff-menu-activate" style="margin-bottom:-6px;cursor: pointer" alt="diff-menu" src="${h.url('/images/icons/script_gear.png')}" />
19 <img class="diff-menu-activate" style="margin-bottom:-6px;cursor: pointer" alt="diff-menu" src="${h.url('/images/icons/script_gear.png')}" />
20 <div class="diff-menu" style="display:none">
20 <div class="diff-menu" style="display:none">
21 <ul>
21 <ul>
22 <li>${h.link_to(_('diff'),h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='diff'))}</li>
22 <li>${h.link_to(_('diff'),h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='diff',fulldiff=1))}</li>
23 <li>${h.link_to(_('raw diff'),h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='raw'))}</li>
23 <li>${h.link_to(_('raw diff'),h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='raw'))}</li>
24 <li>${h.link_to(_('download diff'),h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='download'))}</li>
24 <li>${h.link_to(_('download diff'),h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='download'))}</li>
25 <li>${c.ignorews_url(h.FID(filenode.changeset.raw_id,filenode.path))}</li>
25 <li>${c.ignorews_url(h.FID(filenode.changeset.raw_id,filenode.path))}</li>
26 <li>${c.context_url(h.FID(filenode.changeset.raw_id,filenode.path))}</li>
26 <li>${c.context_url(h.FID(filenode.changeset.raw_id,filenode.path))}</li>
27 </ul>
27 </ul>
28 </div>
28 </div>
29 </div>
29 </div>
30 <span style="float:right;margin-top:-3px">
30 <span style="float:right;margin-top:-3px">
31 <label>
31 <label>
32 ${_('show inline comments')}
32 ${_('show inline comments')}
33 ${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=h.FID(filenode.changeset.raw_id,filenode.path))}
33 ${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=h.FID(filenode.changeset.raw_id,filenode.path))}
34 </label>
34 </label>
35 </span>
35 </span>
36 </div>
36 </div>
37 </div>
37 </div>
38 <div class="code-body">
38 <div class="code-body">
39 <div class="full_f_path" path="${h.safe_unicode(filenode.path)}"></div>
39 <div class="full_f_path" path="${h.safe_unicode(filenode.path)}"></div>
40 %if diff:
40 ${diff|n}
41 ${diff|n}
42 %else:
43 ${_('No changes in this file')}
44 %endif
45 </div>
41 </div>
46 </div>
42 </div>
47 %endif
43 %endif
48 %endfor
44 %endfor
49
45
50 </%def> No newline at end of file
46 </%def>
@@ -1,53 +1,49 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${c.repo_name} ${_('File diff')} - ${c.rhodecode_name}
4 ${c.repo_name} ${_('File diff')} - ${c.rhodecode_name}
5 </%def>
5 </%def>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${h.link_to(u'Home',h.url('/'))}
8 ${h.link_to(u'Home',h.url('/'))}
9 &raquo;
9 &raquo;
10 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
10 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
11 &raquo;
11 &raquo;
12 ${_('File diff')} r${c.changeset_1.revision}:${h.short_id(c.changeset_1.raw_id)} &rarr; r${c.changeset_2.revision}:${h.short_id(c.changeset_2.raw_id)}
12 ${_('File diff')} r${c.changeset_1.revision}:${h.short_id(c.changeset_1.raw_id)} &rarr; r${c.changeset_2.revision}:${h.short_id(c.changeset_2.raw_id)}
13 </%def>
13 </%def>
14
14
15 <%def name="page_nav()">
15 <%def name="page_nav()">
16 ${self.menu('files')}
16 ${self.menu('files')}
17 </%def>
17 </%def>
18 <%def name="main()">
18 <%def name="main()">
19 <div class="box">
19 <div class="box">
20 <!-- box / title -->
20 <!-- box / title -->
21 <div class="title">
21 <div class="title">
22 ${self.breadcrumbs()}
22 ${self.breadcrumbs()}
23 </div>
23 </div>
24 <div class="table">
24 <div>
25 <div id="body" class="diffblock">
25 ## diff block
26 <div class="code-header">
26 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
27 <div class="changeset_header">
27 ${diff_block.diff_block(c.changes)}
28 <span class="changeset_file">${h.link_to(c.f_path,h.url('files_home',repo_name=c.repo_name,
29 revision=c.changeset_2.raw_id,f_path=c.f_path))}</span>
30 &raquo; <span>${h.link_to(_('diff'),
31 h.url.current(diff2=c.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='diff'))}</span>
32 &raquo; <span>${h.link_to(_('raw diff'),
33 h.url.current(diff2=c.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='raw'))}</span>
34 &raquo; <span>${h.link_to(_('download diff'),
35 h.url.current(diff2=c.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='download'))}</span>
36 </div>
37 </div>
38 <div class="code-body">
39 %if c.no_changes:
40 ${_('No changes')}
41 %elif c.big_diff:
42 ${_('Diff is to big to display')} ${h.link_to(_('raw diff'),
43 h.url.current(diff2=c.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='raw'))}
44 %else:
45 ${c.cur_diff|n}
46 %endif
47 </div>
48 </div>
49 </div>
28 </div>
50 </div>
29 </div>
30 <script>
31 YUE.onDOMReady(function(){
32
33 YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
34 var act = e.currentTarget.nextElementSibling;
35
36 if(YUD.hasClass(act,'active')){
37 YUD.removeClass(act,'active');
38 YUD.setStyle(act,'display','none');
39 }else{
40 YUD.addClass(act,'active');
41 YUD.setStyle(act,'display','');
42 }
43 });
44
45 })
46 </script>
51 </%def>
47 </%def>
52
48
53 No newline at end of file
49
@@ -1,78 +1,78 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${c.repo_name} ${_('Edit file')} - ${c.rhodecode_name}
4 ${c.repo_name} ${_('Edit file')} - ${c.rhodecode_name}
5 </%def>
5 </%def>
6
6
7 <%def name="js_extra()">
7 <%def name="js_extra()">
8 <script type="text/javascript" src="${h.url('/js/codemirror.js')}"></script>
8 <script type="text/javascript" src="${h.url('/js/codemirror.js')}"></script>
9 </%def>
9 </%def>
10 <%def name="css_extra()">
10 <%def name="css_extra()">
11 <link rel="stylesheet" type="text/css" href="${h.url('/css/codemirror.css')}"/>
11 <link rel="stylesheet" type="text/css" href="${h.url('/css/codemirror.css')}"/>
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()">
14 <%def name="breadcrumbs_links()">
15 ${h.link_to(u'Home',h.url('/'))}
15 ${h.link_to(u'Home',h.url('/'))}
16 &raquo;
16 &raquo;
17 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
17 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
18 &raquo;
18 &raquo;
19 ${_('edit file')} @ R${c.cs.revision}:${h.short_id(c.cs.raw_id)}
19 ${_('edit file')} @ R${c.cs.revision}:${h.short_id(c.cs.raw_id)}
20 </%def>
20 </%def>
21
21
22 <%def name="page_nav()">
22 <%def name="page_nav()">
23 ${self.menu('files')}
23 ${self.menu('files')}
24 </%def>
24 </%def>
25 <%def name="main()">
25 <%def name="main()">
26 <div class="box">
26 <div class="box">
27 <!-- box / title -->
27 <!-- box / title -->
28 <div class="title">
28 <div class="title">
29 ${self.breadcrumbs()}
29 ${self.breadcrumbs()}
30 <ul class="links">
30 <ul class="links">
31 <li>
31 <li>
32 <span style="text-transform: uppercase;">
32 <span style="text-transform: uppercase;">
33 <a href="#">${_('branch')}: ${c.cs.branch}</a></span>
33 <a href="#">${_('branch')}: ${c.cs.branch}</a></span>
34 </li>
34 </li>
35 </ul>
35 </ul>
36 </div>
36 </div>
37 <div class="table">
37 <div class="table">
38 <div id="files_data">
38 <div id="files_data">
39 <h3 class="files_location">${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.cs.revision,c.file.path)}</h3>
39 <h3 class="files_location">${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.cs.revision,c.file.path)}</h3>
40 ${h.form(h.url.current(),method='post',id='eform')}
40 ${h.form(h.url.current(),method='post',id='eform')}
41 <div id="body" class="codeblock">
41 <div id="body" class="codeblock">
42 <div class="code-header">
42 <div class="code-header">
43 <div class="stats">
43 <div class="stats">
44 <div class="left"><img src="${h.url('/images/icons/file.png')}"/></div>
44 <div class="left"><img src="${h.url('/images/icons/file.png')}"/></div>
45 <div class="left item">${h.link_to("r%s:%s" % (c.file.last_changeset.revision,h.short_id(c.file.last_changeset.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=c.file.last_changeset.raw_id))}</div>
45 <div class="left item">${h.link_to("r%s:%s" % (c.file.last_changeset.revision,h.short_id(c.file.last_changeset.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=c.file.last_changeset.raw_id))}</div>
46 <div class="left item">${h.format_byte_size(c.file.size,binary=True)}</div>
46 <div class="left item">${h.format_byte_size(c.file.size,binary=True)}</div>
47 <div class="left item last">${c.file.mimetype}</div>
47 <div class="left item last">${c.file.mimetype}</div>
48 <div class="buttons">
48 <div class="buttons">
49 ${h.link_to(_('show annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
49 ${h.link_to(_('show annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
50 ${h.link_to(_('show as raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
50 ${h.link_to(_('show as raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
51 ${h.link_to(_('download as raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
51 ${h.link_to(_('download as raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
52 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
52 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
53 % if not c.file.is_binary:
53 % if not c.file.is_binary:
54 ${h.link_to(_('source'),h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
54 ${h.link_to(_('source'),h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
55 % endif
55 % endif
56 % endif
56 % endif
57 </div>
57 </div>
58 </div>
58 </div>
59 <div class="commit">${_('Editing file')}: ${c.file.path}</div>
59 <div class="commit">${_('Editing file')}: ${c.file.path}</div>
60 </div>
60 </div>
61 <pre id="editor_pre"></pre>
61 <pre id="editor_pre"></pre>
62 <textarea id="editor" name="content" style="display:none">${c.file.content|n}</textarea>
62 <textarea id="editor" name="content" style="display:none">${h.escape(c.file.content)|n}</textarea>
63 <div style="padding: 10px;color:#666666">${_('commit message')}</div>
63 <div style="padding: 10px;color:#666666">${_('commit message')}</div>
64 <textarea id="commit" name="message" style="height: 60px;width: 99%;margin-left:4px"></textarea>
64 <textarea id="commit" name="message" style="height: 60px;width: 99%;margin-left:4px"></textarea>
65 </div>
65 </div>
66 <div style="text-align: left;padding-top: 5px">
66 <div style="text-align: left;padding-top: 5px">
67 ${h.submit('commit',_('Commit changes'),class_="ui-btn")}
67 ${h.submit('commit',_('Commit changes'),class_="ui-btn")}
68 ${h.reset('reset',_('Reset'),class_="ui-btn")}
68 ${h.reset('reset',_('Reset'),class_="ui-btn")}
69 </div>
69 </div>
70 ${h.end_form()}
70 ${h.end_form()}
71 <script type="text/javascript">
71 <script type="text/javascript">
72 var reset_url = "${h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.file.path)}";
72 var reset_url = "${h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.file.path)}";
73 initCodeMirror('editor',reset_url);
73 initCodeMirror('editor',reset_url);
74 </script>
74 </script>
75 </div>
75 </div>
76 </div>
76 </div>
77 </div>
77 </div>
78 </%def> No newline at end of file
78 </%def>
@@ -1,105 +1,104 b''
1 <dl>
1 <dl>
2 <dt style="padding-top:10px;font-size:16px">${_('History')}</dt>
2 <dt style="padding-top:10px;font-size:16px">${_('History')}</dt>
3 <dd>
3 <dd>
4 <div>
4 <div>
5 ${h.form(h.url('files_diff_home',repo_name=c.repo_name,f_path=c.f_path),method='get')}
5 ${h.form(h.url('files_diff_home',repo_name=c.repo_name,f_path=c.f_path),method='get')}
6 ${h.hidden('diff2',c.file.last_changeset.raw_id)}
6 ${h.hidden('diff2',c.file.last_changeset.raw_id)}
7 ${h.select('diff1',c.file.last_changeset.raw_id,c.file_history)}
7 ${h.select('diff1',c.file.last_changeset.raw_id,c.file_history)}
8 ${h.submit('diff','diff to revision',class_="ui-btn")}
8 ${h.submit('diff','diff to revision',class_="ui-btn")}
9 ${h.submit('show_rev','show at revision',class_="ui-btn")}
9 ${h.submit('show_rev','show at revision',class_="ui-btn")}
10 ${h.end_form()}
10 ${h.end_form()}
11 </div>
11 </div>
12 </dd>
12 </dd>
13 </dl>
13 </dl>
14
15
14
16 <div id="body" class="codeblock">
15 <div id="body" class="codeblock">
17 <div class="code-header">
16 <div class="code-header">
18 <div class="stats">
17 <div class="stats">
19 <div class="left"><img src="${h.url('/images/icons/file.png')}"/></div>
18 <div class="left"><img src="${h.url('/images/icons/file.png')}"/></div>
20 <div class="left item"><pre>${h.link_to("r%s:%s" % (c.file.last_changeset.revision,h.short_id(c.file.last_changeset.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=c.file.last_changeset.raw_id))}</pre></div>
19 <div class="left item"><pre>${h.link_to("r%s:%s" % (c.file.last_changeset.revision,h.short_id(c.file.last_changeset.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=c.file.last_changeset.raw_id))}</pre></div>
21 <div class="left item">${h.format_byte_size(c.file.size,binary=True)}</div>
20 <div class="left item">${h.format_byte_size(c.file.size,binary=True)}</div>
22 <div class="left item last">${c.file.mimetype}</div>
21 <div class="left item last">${c.file.mimetype}</div>
23 <div class="buttons">
22 <div class="buttons">
24 ${h.link_to(_('show annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
23 ${h.link_to(_('show annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
25 ${h.link_to(_('show as raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
24 ${h.link_to(_('show as raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
26 ${h.link_to(_('download as raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
25 ${h.link_to(_('download as raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
27 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
26 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
28 % if not c.file.is_binary:
27 % if not c.file.is_binary:
29 ${h.link_to(_('edit'),h.url('files_edit_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
28 ${h.link_to(_('edit'),h.url('files_edit_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
30 % endif
29 % endif
31 % endif
30 % endif
32 </div>
31 </div>
33 </div>
32 </div>
34 <div class="author">
33 <div class="author">
35 <div class="gravatar">
34 <div class="gravatar">
36 <img alt="gravatar" src="${h.gravatar_url(h.email(c.changeset.author),16)}"/>
35 <img alt="gravatar" src="${h.gravatar_url(h.email(c.changeset.author),16)}"/>
37 </div>
36 </div>
38 <div title="${c.changeset.author}" class="user">${h.person(c.changeset.author)}</div>
37 <div title="${c.changeset.author}" class="user">${h.person(c.changeset.author)}</div>
39 </div>
38 </div>
40 <div class="commit">${c.file.last_changeset.message}</div>
39 <div class="commit">${c.file.last_changeset.message}</div>
41 </div>
40 </div>
42 <div class="code-body">
41 <div class="code-body">
43 %if c.file.is_binary:
42 %if c.file.is_binary:
44 ${_('Binary file (%s)') % c.file.mimetype}
43 ${_('Binary file (%s)') % c.file.mimetype}
45 %else:
44 %else:
46 % if c.file.size < c.cut_off_limit:
45 % if c.file.size < c.cut_off_limit:
47 ${h.pygmentize(c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
46 ${h.pygmentize(c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
48 %else:
47 %else:
49 ${_('File is too big to display')} ${h.link_to(_('show as raw'),
48 ${_('File is too big to display')} ${h.link_to(_('show as raw'),
50 h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))}
49 h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))}
51 %endif
50 %endif
52 <script type="text/javascript">
51 <script type="text/javascript">
53 function highlight_lines(lines){
52 function highlight_lines(lines){
54 for(pos in lines){
53 for(pos in lines){
55 YUD.setStyle('L'+lines[pos],'background-color','#FFFFBE');
54 YUD.setStyle('L'+lines[pos],'background-color','#FFFFBE');
56 }
55 }
57 }
56 }
58 page_highlights = location.href.substring(location.href.indexOf('#')+1).split('L');
57 page_highlights = location.href.substring(location.href.indexOf('#')+1).split('L');
59 if (page_highlights.length == 2){
58 if (page_highlights.length == 2){
60 highlight_ranges = page_highlights[1].split(",");
59 highlight_ranges = page_highlights[1].split(",");
61
60
62 var h_lines = [];
61 var h_lines = [];
63 for (pos in highlight_ranges){
62 for (pos in highlight_ranges){
64 var _range = highlight_ranges[pos].split('-');
63 var _range = highlight_ranges[pos].split('-');
65 if(_range.length == 2){
64 if(_range.length == 2){
66 var start = parseInt(_range[0]);
65 var start = parseInt(_range[0]);
67 var end = parseInt(_range[1]);
66 var end = parseInt(_range[1]);
68 if (start < end){
67 if (start < end){
69 for(var i=start;i<=end;i++){
68 for(var i=start;i<=end;i++){
70 h_lines.push(i);
69 h_lines.push(i);
71 }
70 }
72 }
71 }
73 }
72 }
74 else{
73 else{
75 h_lines.push(parseInt(highlight_ranges[pos]));
74 h_lines.push(parseInt(highlight_ranges[pos]));
76 }
75 }
77 }
76 }
78 highlight_lines(h_lines);
77 highlight_lines(h_lines);
79
78
80 //remember original location
79 //remember original location
81 var old_hash = location.href.substring(location.href.indexOf('#'));
80 var old_hash = location.href.substring(location.href.indexOf('#'));
82
81
83 // this makes a jump to anchor moved by 3 posstions for padding
82 // this makes a jump to anchor moved by 3 posstions for padding
84 window.location.hash = '#L'+Math.max(parseInt(h_lines[0])-3,1);
83 window.location.hash = '#L'+Math.max(parseInt(h_lines[0])-3,1);
85
84
86 //sets old anchor
85 //sets old anchor
87 window.location.hash = old_hash;
86 window.location.hash = old_hash;
88
87
89 }
88 }
90 </script>
89 </script>
91 %endif
90 %endif
92 </div>
91 </div>
93 </div>
92 </div>
94
93
95 <script type="text/javascript">
94 <script type="text/javascript">
96 YUE.onDOMReady(function(){
95 YUE.onDOMReady(function(){
97 YUE.on('show_rev','click',function(e){
96 YUE.on('show_rev','click',function(e){
98 YUE.preventDefault(e);
97 YUE.preventDefault(e);
99 var cs = YUD.get('diff1').value;
98 var cs = YUD.get('diff1').value;
100 var url = "${h.url('files_home',repo_name=c.repo_name,revision='__CS__',f_path=c.f_path)}".replace('__CS__',cs);
99 var url = "${h.url('files_home',repo_name=c.repo_name,revision='__CS__',f_path=c.f_path)}".replace('__CS__',cs);
101 window.location = url;
100 window.location = url;
102 });
101 });
103 YUE.on('hlcode','mouseup',getSelectionLink("${_('Selection link')}"))
102 YUE.on('hlcode','mouseup',getSelectionLink("${_('Selection link')}"))
104 });
103 });
105 </script>
104 </script>
General Comments 0
You need to be logged in to leave comments. Login now