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