# -*- coding: utf-8 -*-
"""
rhodecode.controllers.changeset
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
changeset controller for pylons showoing changes beetween
revisions
:created_on: Apr 25, 2010
:author: marcink
:copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
:license: GPLv3, see COPYING for more details.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import traceback
from collections import defaultdict
from webob.exc import HTTPForbidden
from pylons import tmpl_context as c, url, request, response
from pylons.i18n.translation import _
from pylons.controllers.util import redirect
from pylons.decorators import jsonify
from vcs.exceptions import RepositoryError, ChangesetError, \
ChangesetDoesNotExistError
from vcs.nodes import FileNode
import rhodecode.lib.helpers as h
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
from rhodecode.lib.base import BaseRepoController, render
from rhodecode.lib.utils import EmptyChangeset
from rhodecode.lib.compat import OrderedDict
from rhodecode.lib import diffs
from rhodecode.model.db import ChangesetComment
from rhodecode.model.comment import ChangesetCommentsModel
from rhodecode.model.meta import Session
from rhodecode.lib.diffs import wrapped_diff
log = logging.getLogger(__name__)
def anchor_url(revision, path):
fid = h.FID(revision, path)
return h.url.current(anchor=fid, **request.GET)
def get_ignore_ws(fid, GET):
ig_ws_global = request.GET.get('ignorews')
ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
if ig_ws:
try:
return int(ig_ws[0].split(':')[-1])
except:
pass
return ig_ws_global
def _ignorews_url(fileid=None):
params = defaultdict(list)
lbl = _('show white space')
ig_ws = get_ignore_ws(fileid, request.GET)
ln_ctx = get_line_ctx(fileid, request.GET)
# global option
if fileid is None:
if ig_ws is None:
params['ignorews'] += [1]
lbl = _('ignore white space')
ctx_key = 'context'
ctx_val = ln_ctx
# per file options
else:
params[fileid] += ['WS:1']
ctx_key = fileid
ctx_val = 'C:%s' % ln_ctx
# if we have passed in ln_ctx pass it along to our params
if ln_ctx:
params[ctx_key] += [ctx_val]
params['anchor'] = fileid
return h.link_to(lbl, h.url.current(**params))
def get_line_ctx(fid, GET):
ln_ctx_global = request.GET.get('context')
ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
retval = ln_ctx[0].split(':')[-1]
retval = ln_ctx_global
return int(retval)
return
def _context_url(fileid=None):
Generates url for context lines
:param fileid:
ln_ctx = (get_line_ctx(fileid, request.GET) or 3) * 2
if ln_ctx > 0:
params['context'] += [ln_ctx]
ig_ws_key = 'ignorews'
ig_ws_val = 1
# per file option
params[fileid] += ['C:%s' % ln_ctx]
ig_ws_key = fileid
ig_ws_val = 'WS:%s' % 1
params[ig_ws_key] += [ig_ws_val]
lbl = _('%s line context') % ln_ctx
def wrap_to_table(str_):
return '''<table class="code-difftable">
<tr class="line no-comment">
<td class="lineno new"></td>
<td class="code no-comment"><pre>%s</pre></td>
</tr>
</table>''' % str_
class ChangesetController(BaseRepoController):
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
def __before__(self):
super(ChangesetController, self).__before__()
c.affected_files_cut_off = 60
def index(self, revision):
c.anchor_url = anchor_url
c.ignorews_url = _ignorews_url
c.context_url = _context_url
#get ranges of revisions if preset
rev_range = revision.split('...')[:2]
enable_comments = True
if len(rev_range) == 2:
enable_comments = False
rev_start = rev_range[0]
rev_end = rev_range[1]
rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
end=rev_end)
rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
c.cs_ranges = list(rev_ranges)
if not c.cs_ranges:
raise RepositoryError('Changeset range returned empty result')
except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
log.error(traceback.format_exc())
h.flash(str(e), category='warning')
return redirect(url('home'))
c.changes = OrderedDict()
c.sum_added = 0
c.sum_removed = 0
c.lines_added = 0
c.lines_deleted = 0
c.lines_added = 0 # count of lines added
c.lines_deleted = 0 # count of lines removes
cumulative_diff = 0
c.cut_off = False # defines if cut off limit is reached
c.comments = []
c.inline_comments = []
c.inline_cnt = 0
# Iterate over ranges (default changeset view is always one changeset)
for changeset in c.cs_ranges:
c.comments.extend(ChangesetCommentsModel()\
.get_comments(c.rhodecode_db_repo.repo_id,
changeset.raw_id))
inlines = ChangesetCommentsModel()\
.get_inline_comments(c.rhodecode_db_repo.repo_id,
changeset.raw_id)
c.inline_comments.extend(inlines)
c.changes[changeset.raw_id] = []
changeset_parent = changeset.parents[0]
except IndexError:
changeset_parent = None
#==================================================================
# ADDED FILES
for node in changeset.added:
filenode_old = FileNode(node.path, '', EmptyChangeset())
if filenode_old.is_binary or node.is_binary:
diff = wrap_to_table(_('binary file'))
st = (0, 0)
# in this case node.size is good parameter since those are
# added nodes and their size defines how many changes were
# made
c.sum_added += node.size
fid = h.FID(revision, node.path)
line_context_lcl = get_line_ctx(fid, request.GET)
ignore_whitespace_lcl = get_ignore_ws(fid, request.GET)
if c.sum_added < self.cut_off_limit:
f_gitdiff = diffs.get_gitdiff(filenode_old, node,
ignore_whitespace=ignore_whitespace_lcl,
context=line_context_lcl)
d = diffs.DiffProcessor(f_gitdiff, format='gitdiff')
st = d.stat()
diff = d.as_html(enable_comments=enable_comments)
diff = wrap_to_table(_('Changeset is to big and '
'was cut off, see raw '
'changeset instead'))
c.cut_off = True
break
cs1 = None
cs2 = node.last_changeset.raw_id
ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
lim = self.cut_off_limit
if cumulative_diff > self.cut_off_limit:
lim = -1
size, cs1, cs2, diff, st = wrapped_diff(filenode_old=None,
filenode_new=node,
cut_off_limit=lim,
ignore_whitespace=ign_whitespace_lcl,
line_context=line_context_lcl,
enable_comments=enable_comments)
cumulative_diff += size
c.lines_added += st[0]
c.lines_deleted += st[1]
c.changes[changeset.raw_id].append(('added', node, diff,
cs1, cs2, st))
# CHANGED FILES
if not c.cut_off:
for node in changeset.changed:
filenode_old = changeset_parent.get_node(node.path)
except ChangesetError:
log.warning('Unable to fetch parent node for diff')
filenode_old = FileNode(node.path, '',
EmptyChangeset())
if c.sum_removed < self.cut_off_limit:
ignore_whitespace_lcl = get_ignore_ws(fid, request.GET,)
d = diffs.DiffProcessor(f_gitdiff,
format='gitdiff')
if (st[0] + st[1]) * 256 > self.cut_off_limit:
diff = wrap_to_table(_('Diff is to big '
'and was cut off, see '
'raw diff instead'))
if diff:
c.sum_removed += len(diff)
cs1 = filenode_old.last_changeset.raw_id
c.changes[changeset.raw_id].append(('changed', node, diff,
size, cs1, cs2, diff, st = wrapped_diff(filenode_old=filenode_old,
# REMOVED FILES
for node in changeset.removed:
c.changes[changeset.raw_id].append(('removed', node, None,
None, None, (0, 0)))
# count inline comments
for path, lines in c.inline_comments:
for comments in lines.values():
c.inline_cnt += len(comments)
if len(c.cs_ranges) == 1:
c.changeset = c.cs_ranges[0]
c.changes = c.changes[c.changeset.raw_id]
return render('changeset/changeset.html')
return render('changeset/changeset_range.html')
def raw_changeset(self, revision):
method = request.GET.get('diff', 'show')
ignore_whitespace = request.GET.get('ignorews') == '1'
line_context = request.GET.get('context', 3)
c.scm_type = c.rhodecode_repo.alias
c.changeset = c.rhodecode_repo.get_changeset(revision)
except RepositoryError:
c.changeset_parent = c.changeset.parents[0]
c.changeset_parent = None
c.changes = []
for node in c.changeset.added:
filenode_old = FileNode(node.path, '')
diff = _('binary file') + '\n'
ignore_whitespace=ignore_whitespace,
context=line_context)
diff = diffs.DiffProcessor(f_gitdiff,
format='gitdiff').raw_diff()
c.changes.append(('added', node, diff, cs1, cs2))
for node in c.changeset.changed:
filenode_old = c.changeset_parent.get_node(node.path)
diff = _('binary file')
c.changes.append(('changed', node, diff, cs1, cs2))
response.content_type = 'text/plain'
if method == 'download':
response.content_disposition = 'attachment; filename=%s.patch' \
% revision
c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id for x in
c.changeset.parents])
c.diffs = ''
for x in c.changes:
c.diffs += x[2]
return render('changeset/raw_changeset.html')
def comment(self, repo_name, revision):
ChangesetCommentsModel().create(text=request.POST.get('text'),
repo_id=c.rhodecode_db_repo.repo_id,
user_id=c.rhodecode_user.user_id,
revision=revision,
f_path=request.POST.get('f_path'),
line_no=request.POST.get('line'))
Session.commit()
return redirect(h.url('changeset_home', repo_name=repo_name,
revision=revision))
@jsonify
def delete_comment(self, repo_name, comment_id):
co = ChangesetComment.get(comment_id)
owner = lambda: co.author.user_id == c.rhodecode_user.user_id
if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
ChangesetCommentsModel().delete(comment=co)
return True
rhodecode.controllers.files
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Files controller for RhodeCode
:created_on: Apr 21, 2010
import os
from os.path import join as jn
from pylons import request, response, session, tmpl_context as c, url
from pylons import request, response, tmpl_context as c, url
from vcs.conf import settings
from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
EmptyRepositoryError, ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
from vcs.nodes import FileNode, NodeKind
EmptyRepositoryError, ImproperArchiveTypeError, VCSError, \
NodeAlreadyExistsError
from rhodecode.lib import convert_line_endings, detect_mode, safe_str
from rhodecode.model.repo import RepoModel
from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
_context_url, get_line_ctx, get_ignore_ws
class FilesController(BaseRepoController):
super(FilesController, self).__before__()
c.cut_off_limit = self.cut_off_limit
def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
Safe way to get changeset if error occur it redirects to tip with
proper message
:param rev: revision to fetch
:param repo_name: repo name to redirect after
return c.rhodecode_repo.get_changeset(rev)
except EmptyRepositoryError, e:
if not redirect_after:
return None
url_ = url('files_add_home',
repo_name=c.repo_name,
revision=0, f_path='')
add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
h.flash(h.literal(_('There are no files yet %s' % add_new)),
category='warning')
redirect(h.url('summary_home', repo_name=repo_name))
except RepositoryError, e:
redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
def __get_filenode_or_redirect(self, repo_name, cs, path):
Returns file_node, if error occurs or given path is directory,
it'll redirect to top level path
:param repo_name: repo_name
:param cs: given changeset
:param path: path to lookup
file_node = cs.get_node(path)
if file_node.is_dir():
raise RepositoryError('given path is a directory')
redirect(h.url('files_home', repo_name=repo_name,
revision=cs.raw_id))
return file_node
def __get_paths(self, changeset, starting_path):
"""recursive walk in root dir and return a set of all path in that dir
based on repository walk function
_files = list()
_dirs = list()
tip = changeset
for topnode, dirs, files in tip.walk(starting_path):
for f in files:
_files.append(f.path)
for d in dirs:
_dirs.append(d.path)
log.debug(traceback.format_exc())
return _dirs, _files
def index(self, repo_name, revision, f_path):
#reditect to given revision from form if given
# redirect to given revision from form if given
post_revision = request.POST.get('at_rev', None)
if post_revision:
cs = self.__get_cs_or_redirect(post_revision, repo_name)
redirect(url('files_home', repo_name=c.repo_name,
revision=cs.raw_id, f_path=f_path))
c.changeset = self.__get_cs_or_redirect(revision, repo_name)
c.branch = request.GET.get('branch', None)
c.f_path = f_path
cur_rev = c.changeset.revision
#prev link
# prev link
prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
c.url_prev = url('files_home', repo_name=c.repo_name,
revision=prev_rev.raw_id, f_path=f_path)
if c.branch:
c.url_prev += '?branch=%s' % c.branch
except (ChangesetDoesNotExistError, VCSError):
c.url_prev = '#'
#next link
# next link
next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
c.url_next = url('files_home', repo_name=c.repo_name,
revision=next_rev.raw_id, f_path=f_path)
c.url_next += '?branch=%s' % c.branch
c.url_next = '#'
#files or dirs
# files or dirs
c.file = c.changeset.get_node(f_path)
if c.file.is_file():
c.file_history = self._get_node_history(c.changeset, f_path)
c.file_history = []
return render('files/files.html')
def rawfile(self, repo_name, revision, f_path):
cs = self.__get_cs_or_redirect(revision, repo_name)
file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
response.content_disposition = 'attachment; filename=%s' % \
safe_str(f_path.split(os.sep)[-1])
response.content_type = file_node.mimetype
return file_node.content
def raw(self, repo_name, revision, f_path):
raw_mimetype_mapping = {
# map original mimetype to a mimetype used for "show as raw"
# you can also provide a content-disposition to override the
# default "attachment" disposition.
# orig_type: (new_type, new_dispo)
# show images inline:
'image/x-icon': ('image/x-icon', 'inline'),
'image/png': ('image/png', 'inline'),
'image/gif': ('image/gif', 'inline'),
'image/jpeg': ('image/jpeg', 'inline'),
'image/svg+xml': ('image/svg+xml', 'inline'),
}
mimetype = file_node.mimetype
mimetype, dispo = raw_mimetype_mapping[mimetype]
except KeyError:
# we don't know anything special about this, handle it safely
if file_node.is_binary:
# do same as download raw for binary files
mimetype, dispo = 'application/octet-stream', 'attachment'
# do not just use the original mimetype, but force text/plain,
# otherwise it would serve text/html and that might be unsafe.
# Note: underlying vcs library fakes text/plain mimetype if the
# mimetype can not be determined and it thinks it is not
# binary.This might lead to erroneous text display in some
# cases, but helps in other cases, like with text files
# without extension.
mimetype, dispo = 'text/plain', 'inline'
if dispo == 'attachment':
dispo = 'attachment; filename=%s' % \
response.content_disposition = dispo
response.content_type = mimetype
def annotate(self, repo_name, revision, f_path):
c.cs = self.__get_cs_or_redirect(revision, repo_name)
c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
c.file_history = self._get_node_history(c.cs, f_path)
return render('files/files_annotate.html')
@HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
def edit(self, repo_name, revision, f_path):
r_post = request.POST
if c.file.is_binary:
return redirect(url('files_home', repo_name=c.repo_name,
revision=c.cs.raw_id, f_path=f_path))
if r_post:
@@ -314,210 +315,195 @@ class FilesController(BaseRepoController
file_obj = r_post.get('upload_file', None)
if file_obj is not None and hasattr(file_obj, 'filename'):
filename = file_obj.filename
content = file_obj.file
node_path = os.path.join(location, filename)
author = self.rhodecode_user.full_contact
if not content:
h.flash(_('No content'), category='warning')
return redirect(url('changeset_home', repo_name=c.repo_name,
revision='tip'))
if not filename:
h.flash(_('No filename'), category='warning')
self.scm_model.create_node(repo=c.rhodecode_repo,
repo_name=repo_name, cs=c.cs,
user=self.rhodecode_user,
author=author, message=message,
content=content, f_path=node_path)
h.flash(_('Successfully committed to %s' % node_path),
category='success')
except NodeAlreadyExistsError, e:
h.flash(_(e), category='error')
except Exception:
h.flash(_('Error occurred during commit'), category='error')
return redirect(url('changeset_home',
repo_name=c.repo_name, revision='tip'))
return render('files/files_add.html')
def archivefile(self, repo_name, fname):
fileformat = None
revision = None
ext = None
subrepos = request.GET.get('subrepos') == 'true'
for a_type, ext_data in settings.ARCHIVE_SPECS.items():
archive_spec = fname.split(ext_data[1])
if len(archive_spec) == 2 and archive_spec[1] == '':
fileformat = a_type or ext_data[1]
revision = archive_spec[0]
ext = ext_data[1]
dbrepo = RepoModel().get_by_repo_name(repo_name)
if dbrepo.enable_downloads is False:
return _('downloads disabled')
# patch and reset hooks section of UI config to not run any
# hooks on fetching archives with subrepos
for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
cs = c.rhodecode_repo.get_changeset(revision)
content_type = settings.ARCHIVE_SPECS[fileformat][0]
except ChangesetDoesNotExistError:
return _('Unknown revision %s') % revision
except EmptyRepositoryError:
return _('Empty repository')
except (ImproperArchiveTypeError, KeyError):
return _('Unknown archive type')
response.content_type = content_type
response.content_disposition = 'attachment; filename=%s-%s%s' \
% (repo_name, revision, ext)
import tempfile
archive = tempfile.mkstemp()[1]
t = open(archive, 'wb')
cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
def get_chunked_archive(archive):
stream = open(archive, 'rb')
while True:
data = stream.read(4096)
if not data:
os.remove(archive)
yield data
return get_chunked_archive(archive)
def diff(self, repo_name, f_path):
diff1 = request.GET.get('diff1')
diff2 = request.GET.get('diff2')
diff1 = request.GET.get('diff1', '')
diff2 = request.GET.get('diff2', '')
c.action = request.GET.get('diff')
c.no_changes = diff1 == diff2
c.big_diff = False
c.changes[diff2] = []
if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
node1 = c.changeset_1.get_node(f_path)
c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
node1 = FileNode('.', '', changeset=c.changeset_1)
if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
node2 = c.changeset_2.get_node(f_path)
c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
node2 = FileNode('.', '', changeset=c.changeset_2)
return redirect(url('files_home',
repo_name=c.repo_name, f_path=f_path))
f_path=f_path))
if c.action == 'download':
_diff = diffs.get_gitdiff(node1, node2,
diff = diffs.DiffProcessor(_diff,format='gitdiff')
diff = diffs.DiffProcessor(_diff, format='gitdiff')
diff_name = '%s_vs_%s.diff' % (diff1, diff2)
response.content_disposition = 'attachment; filename=%s' \
% diff_name
return diff.raw_diff()
elif c.action == 'raw':
elif c.action == 'diff':
if node1.is_binary or node2.is_binary:
c.cur_diff = _('Binary file')
elif node1.size > self.cut_off_limit or \
node2.size > self.cut_off_limit:
c.cur_diff = ''
c.big_diff = True
c.cur_diff = diff.as_html()
fid = h.FID(diff2, node2.path)
#default option
lim = request.GET.get('fulldiff') or self.cut_off_limit
_, cs1, cs2, diff, st = wrapped_diff(filenode_old=node1,
filenode_new=node2,
enable_comments=False)
c.changes = [('', node2, diff, cs1, cs2, st,)]
if not c.cur_diff and not c.big_diff:
c.no_changes = True
return render('files/file_diff.html')
def _get_node_history(self, cs, f_path):
changesets = cs.get_file_history(f_path)
hist_l = []
changesets_group = ([], _("Changesets"))
branches_group = ([], _("Branches"))
tags_group = ([], _("Tags"))
for chs in changesets:
n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
changesets_group[0].append((chs.raw_id, n_desc,))
hist_l.append(changesets_group)
for name, chs in c.rhodecode_repo.branches.items():
#chs = chs.split(':')[-1]
branches_group[0].append((chs, name),)
hist_l.append(branches_group)
for name, chs in c.rhodecode_repo.tags.items():
tags_group[0].append((chs, name),)
hist_l.append(tags_group)
return hist_l
def nodelist(self, repo_name, revision, f_path):
if request.environ.get('HTTP_X_PARTIAL_XHR'):
_d, _f = self.__get_paths(cs, f_path)
return _d + _f
rhodecode.lib.diffs
~~~~~~~~~~~~~~~~~~~
Set of diffing helpers, previously part of vcs
:created_on: Dec 4, 2011
:original copyright: 2007-2008 by Armin Ronacher
import re
import difflib
import markupsafe
from itertools import tee, imap
from vcs.exceptions import VCSError
def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
ignore_whitespace=True, line_context=3,
enable_comments=False):
returns a wrapped diff into a table, checks for cut_off_limit and presents
if filenode_old is None:
filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
if filenode_old.is_binary or filenode_new.is_binary:
stats = (0, 0)
size = 0
elif cut_off_limit != -1 and (cut_off_limit is None or
(filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
f_gitdiff = get_gitdiff(filenode_old, filenode_new,
diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
diff = diff_processor.as_html(enable_comments=enable_comments)
stats = diff_processor.stat()
size = len(diff or '')
diff = wrap_to_table(_('Changeset was to big and was cut off, use '
'diff menu to display this diff'))
if not diff:
diff = wrap_to_table(_('No changes detected'))
cs2 = filenode_new.last_changeset.raw_id
return size, cs1, cs2, diff, stats
def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
Returns git style diff between given ``filenode_old`` and ``filenode_new``.
:param ignore_whitespace: ignore whitespaces in diff
for filenode in (filenode_old, filenode_new):
if not isinstance(filenode, FileNode):
raise VCSError("Given object should be FileNode object, not %s"
% filenode.__class__)
old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
repo = filenode_new.changeset.repository
vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
ignore_whitespace, context)
return vcs_gitdiff
class DiffProcessor(object):
Give it a unified diff and it returns a list of the files that were
mentioned in the diff together with a dict of meta information that
can be used to render it in a HTML template.
_chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
def __init__(self, diff, differ='diff', format='udiff'):
:param diff: a text in diff format or generator
:param format: format of diff passed, `udiff` or `gitdiff`
if isinstance(diff, basestring):
diff = [diff]
self.__udiff = diff
self.__format = format
self.adds = 0
self.removes = 0
if isinstance(self.__udiff, basestring):
self.lines = iter(self.__udiff.splitlines(1))
elif self.__format == 'gitdiff':
udiff_copy = self.copy_iterator()
self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
self.lines = imap(self.escaper, udiff_copy)
# Select a differ.
if differ == 'difflib':
self.differ = self._highlight_line_difflib
self.differ = self._highlight_line_udiff
def escaper(self, string):
return markupsafe.escape(string)
def copy_iterator(self):
make a fresh copy of generator, we should not iterate thru
an original as it's needed for repeating operations on
this instance of DiffProcessor
self.__udiff, iterator_copy = tee(self.__udiff)
return iterator_copy
def _extract_rev(self, line1, line2):
Extract the filename and revision hint from a line.
if line1.startswith('--- ') and line2.startswith('+++ '):
l1 = line1[4:].split(None, 1)
old_filename = (l1[0].replace('a/', '', 1)
if len(l1) >= 1 else None)
old_rev = l1[1] if len(l1) == 2 else 'old'
l2 = line2[4:].split(None, 1)
new_filename = (l2[0].replace('b/', '', 1)
new_rev = l2[1] if len(l2) == 2 else 'new'
filename = (old_filename
if old_filename != '/dev/null' else new_filename)
return filename, new_rev, old_rev
except (ValueError, IndexError):
@@ -170,282 +224,292 @@ class DiffProcessor(object):
sequence = difflib.SequenceMatcher(None, oldwords, newwords)
oldfragments, newfragments = [], []
for tag, i1, i2, j1, j2 in sequence.get_opcodes():
oldfrag = ''.join(oldwords[i1:i2])
newfrag = ''.join(newwords[j1:j2])
if tag != 'equal':
if oldfrag:
oldfrag = '<del>%s</del>' % oldfrag
if newfrag:
newfrag = '<ins>%s</ins>' % newfrag
oldfragments.append(oldfrag)
newfragments.append(newfrag)
old['line'] = "".join(oldfragments)
new['line'] = "".join(newfragments)
def _highlight_line_udiff(self, line, next_):
Highlight inline changes in both lines.
start = 0
limit = min(len(line['line']), len(next_['line']))
while start < limit and line['line'][start] == next_['line'][start]:
start += 1
end = -1
limit -= start
while -end <= limit and line['line'][end] == next_['line'][end]:
end -= 1
end += 1
if start or end:
def do(l):
last = end + len(l['line'])
if l['action'] == 'add':
tag = 'ins'
tag = 'del'
l['line'] = '%s<%s>%s</%s>%s' % (
l['line'][:start],
tag,
l['line'][start:last],
l['line'][last:]
)
do(line)
do(next_)
def _parse_udiff(self):
Parse the diff an return data for the template.
lineiter = self.lines
files = []
line = lineiter.next()
# skip first context
skipfirst = True
while 1:
# continue until we found the old file
if not line.startswith('--- '):
continue
chunks = []
filename, old_rev, new_rev = \
self._extract_rev(line, lineiter.next())
files.append({
'filename': filename,
'old_revision': old_rev,
'new_revision': new_rev,
'chunks': chunks
})
while line:
match = self._chunk_re.match(line)
if not match:
lines = []
chunks.append(lines)
old_line, old_end, new_line, new_end = \
[int(x or 1) for x in match.groups()[:-1]]
old_line -= 1
new_line -= 1
context = len(match.groups()) == 5
old_end += old_line
new_end += new_line
if context:
if not skipfirst:
lines.append({
'old_lineno': '...',
'new_lineno': '...',
'action': 'context',
'line': line,
skipfirst = False
while old_line < old_end or new_line < new_end:
if line:
command, line = line[0], line[1:]
command = ' '
affects_old = affects_new = False
# ignore those if we don't expect them
if command in '#@':
elif command == '+':
affects_new = True
action = 'add'
elif command == '-':
affects_old = True
action = 'del'
affects_old = affects_new = True
action = 'unmod'
old_line += affects_old
new_line += affects_new
'old_lineno': affects_old and old_line or '',
'new_lineno': affects_new and new_line or '',
'action': action,
'line': line
except StopIteration:
# highlight inline changes
for _ in files:
for chunk in chunks:
lineiter = iter(chunk)
#first = True
if line['action'] != 'unmod':
nextline = lineiter.next()
if nextline['action'] == 'unmod' or \
nextline['action'] == line['action']:
self.differ(line, nextline)
return files
def prepare(self):
Prepare the passed udiff for HTML rendering. It'l return a list
of dicts
return self._parse_udiff()
def _safe_id(self, idstring):
"""Make a string safe for including in an id attribute.
The HTML spec says that id attributes 'must begin with
a letter ([A-Za-z]) and may be followed by any number
of letters, digits ([0-9]), hyphens ("-"), underscores
("_"), colons (":"), and periods (".")'. These regexps
are slightly over-zealous, in that they remove colons
and periods unnecessarily.
Whitespace is transformed into underscores, and then
anything which is not a hyphen or a character that
matches \w (alphanumerics and underscore) is removed.
# Transform all whitespace to underscore
idstring = re.sub(r'\s', "_", '%s' % idstring)
# Remove everything that is not a hyphen or a member of \w
idstring = re.sub(r'(?!-)\W', "", idstring).lower()
return idstring
def raw_diff(self):
Returns raw string as udiff
if self.__format == 'gitdiff':
udiff_copy = self._parse_gitdiff(udiff_copy)
return u''.join(udiff_copy)
def as_html(self, table_class='code-difftable', line_class='line',
new_lineno_class='lineno old', old_lineno_class='lineno new',
code_class='code', enable_comments=False):
Return udiff as html table with customized css classes
def _link_to_if(condition, label, url):
Generates a link if condition is meet or just the label if not.
if condition:
return '''<a href="%(url)s">%(label)s</a>''' % {'url': url,
'label': label}
return '''<a href="%(url)s">%(label)s</a>''' % {
'url': url,
'label': label
return label
diff_lines = self.prepare()
_html_empty = True
_html = []
_html.append('''<table class="%(table_class)s">\n''' \
% {'table_class': table_class})
_html.append('''<table class="%(table_class)s">\n''' % {
'table_class': table_class
for diff in diff_lines:
for line in diff['chunks']:
_html_empty = False
for change in line:
_html.append('''<tr class="%(line_class)s %(action)s">\n''' \
% {'line_class': line_class,
'action': change['action']})
_html.append('''<tr class="%(lc)s %(action)s">\n''' % {
'lc': line_class,
'action': change['action']
anchor_old_id = ''
anchor_new_id = ''
anchor_old = "%(filename)s_o%(oldline_no)s" % \
{'filename': self._safe_id(diff['filename']),
'oldline_no': change['old_lineno']}
anchor_new = "%(filename)s_n%(oldline_no)s" % \
'oldline_no': change['new_lineno']}
cond_old = change['old_lineno'] != '...' and \
change['old_lineno']
cond_new = change['new_lineno'] != '...' and \
change['new_lineno']
anchor_old = "%(filename)s_o%(oldline_no)s" % {
'filename': self._safe_id(diff['filename']),
'oldline_no': change['old_lineno']
anchor_new = "%(filename)s_n%(oldline_no)s" % {
'oldline_no': change['new_lineno']
cond_old = (change['old_lineno'] != '...' and
change['old_lineno'])
cond_new = (change['new_lineno'] != '...' and
change['new_lineno'])
if cond_old:
anchor_old_id = 'id="%s"' % anchor_old
if cond_new:
anchor_new_id = 'id="%s"' % anchor_new
###########################################################
# OLD LINE NUMBER
_html.append('''\t<td %(a_id)s class="%(old_lineno_cls)s">''' \
% {'a_id': anchor_old_id,
'old_lineno_cls': old_lineno_class})
_html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
'a_id': anchor_old_id,
'olc': old_lineno_class
_html.append('''%(link)s''' \
% {'link':
_link_to_if(cond_old, change['old_lineno'], '#%s' \
% anchor_old)})
_html.append('''%(link)s''' % {
'link': _link_to_if(True, change['old_lineno'],
'#%s' % anchor_old)
_html.append('''</td>\n''')
# NEW LINE NUMBER
_html.append('''\t<td %(a_id)s class="%(new_lineno_cls)s">''' \
% {'a_id': anchor_new_id,
'new_lineno_cls': new_lineno_class})
_html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
'a_id': anchor_new_id,
'nlc': new_lineno_class
_link_to_if(cond_new, change['new_lineno'], '#%s' \
% anchor_new)})
'link': _link_to_if(True, change['new_lineno'],
'#%s' % anchor_new)
# CODE
comments = '' if enable_comments else 'no-comment'
_html.append('''\t<td class="%(code_class)s %(in-comments)s">''' \
% {'code_class': code_class,
'in-comments': comments})
_html.append('''\n\t\t<pre>%(code)s</pre>\n''' \
% {'code': change['line']})
_html.append('''\t<td class="%(cc)s %(inc)s">''' % {
'cc': code_class,
'inc': comments
_html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
'code': change['line']
_html.append('''\t</td>''')
_html.append('''\n</tr>\n''')
_html.append('''</table>''')
if _html_empty:
return ''.join(_html)
def stat(self):
Returns tuple of added, and removed lines for this instance
return self.adds, self.removes
"""Helper functions
Consists of functions to typically be used within templates, but also
available to Controllers. This module is available to both as 'h'.
import random
import hashlib
import StringIO
import urllib
import math
from datetime import datetime
from pygments.formatters.html import HtmlFormatter
from pygments import highlight as code_highlight
from pylons import url, request, config
from pylons.i18n.translation import _, ungettext
from webhelpers.html import literal, HTML, escape
from webhelpers.html.tools import *
from webhelpers.html.builder import make_tag
from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
end_form, file, form, hidden, image, javascript_link, link_to, \
link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
submit, text, password, textarea, title, ul, xml_declaration, radio
from webhelpers.html.tools import auto_link, button_to, highlight, \
js_obfuscate, mail_to, strip_links, strip_tags, tag_re
from webhelpers.number import format_byte_size, format_bit_size
from webhelpers.pylonslib import Flash as _Flash
from webhelpers.pylonslib.secure_form import secure_form
from webhelpers.text import chop_at, collapse, convert_accented_entities, \
convert_misc_entities, lchop, plural, rchop, remove_formatting, \
replace_whitespace, urlify, truncate, wrap_paragraphs
from webhelpers.date import time_ago_in_words
from webhelpers.paginate import Page
from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
convert_boolean_attrs, NotGiven, _make_safe_id_component
from rhodecode.lib.annotate import annotate_highlight
from rhodecode.lib.utils import repo_name_slug
from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
from rhodecode.lib.markup_renderer import MarkupRenderer
def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
Reset button
_set_input_attrs(attrs, type, name, value)
_set_id_attr(attrs, id, name)
convert_boolean_attrs(attrs, ["disabled"])
return HTML.input(**attrs)
reset = _reset
safeid = _make_safe_id_component
def FID(raw_id,path):
def FID(raw_id, path):
Creates a uniqe ID for filenode based on it's path and revision
:param raw_id:
:param path:
return 'C-%s-%s' % (short_id(raw_id), safeid(safe_unicode(path)))
def get_token():
"""Return the current authentication token, creating one if one doesn't
already exist.
token_key = "_authentication_token"
from pylons import session
if not token_key in session:
token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
except AttributeError: # Python < 2.4
token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
session[token_key] = token
if hasattr(session, 'save'):
session.save()
return session[token_key]
class _GetError(object):
"""Get error from form_errors, and represent it as span wrapped error
message
:param field_name: field to fetch errors for
:param form_errors: form errors dict
def __call__(self, field_name, form_errors):
tmpl = """<span class="error_msg">%s</span>"""
if form_errors and form_errors.has_key(field_name):
return literal(tmpl % form_errors.get(field_name))
get_error = _GetError()
class _ToolTip(object):
def __call__(self, tooltip_title, trim_at=50):
"""Special function just to wrap our text into nice formatted
autowrapped text
:param tooltip_title:
return escape(tooltip_title)
tooltip = _ToolTip()
class _FilesBreadCrumbs(object):
def __call__(self, repo_name, rev, paths):
if isinstance(paths, str):
paths = safe_unicode(paths)
url_l = [link_to(repo_name, url('files_home',
repo_name=repo_name,
revision=rev, f_path=''))]
paths_l = paths.split('/')
for cnt, p in enumerate(paths_l):
if p != '':
url_l.append(link_to(p,
url('files_home',
revision=rev,
f_path='/'.join(paths_l[:cnt + 1])
return literal('/'.join(url_l))
files_breadcrumbs = _FilesBreadCrumbs()
class CodeHtmlFormatter(HtmlFormatter):
"""My code Html Formatter for source codes
def wrap(self, source, outfile):
return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
def _wrap_code(self, source):
for cnt, it in enumerate(source):
i, t = it
t = '<div id="L%s">%s</div>' % (cnt + 1, t)
yield i, t
def _wrap_tablelinenos(self, inner):
dummyoutfile = StringIO.StringIO()
lncount = 0
for t, line in inner:
if t:
lncount += 1
dummyoutfile.write(line)
fl = self.linenostart
mw = len(str(lncount + fl - 1))
sp = self.linenospecial
st = self.linenostep
la = self.lineanchors
aln = self.anchorlinenos
nocls = self.noclasses
if sp:
for i in range(fl, fl + lncount):
if i % st == 0:
if i % sp == 0:
if aln:
lines.append('<a href="#%s%d" class="special">%*d</a>' %
(la, i, mw, i))
lines.append('<span class="special">%*d</span>' % (mw, i))
lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
lines.append('%*d' % (mw, i))
lines.append('')
ls = '\n'.join(lines)
# in case you wonder about the seemingly redundant <div> here: since the
# content in the other cell also is wrapped in a div, some browsers in
# some configurations seem to mess up the formatting...
if nocls:
yield 0, ('<table class="%stable">' % self.cssclass +
'<tr><td><div class="linenodiv" '
'style="background-color: #f0f0f0; padding-right: 10px">'
'<pre style="line-height: 125%">' +
ls + '</pre></div></td><td id="hlcode" class="code">')
'<tr><td class="linenos"><div class="linenodiv"><pre>' +
yield 0, dummyoutfile.getvalue()
yield 0, '</td></tr></table>'
def pygmentize(filenode, **kwargs):
"""pygmentize function using pygments
:param filenode:
return literal(code_highlight(filenode.content,
filenode.lexer, CodeHtmlFormatter(**kwargs)))
@@ -645,107 +646,107 @@ def changed_tooltip(nodes):
return ': ' + _('No Files')
def repo_link(groups_and_repos):
Makes a breadcrumbs link to repo within a group
joins » on each group to create a fancy link
ex::
group >> subgroup >> repo
:param groups_and_repos:
groups, repo_name = groups_and_repos
if not groups:
return repo_name
def make_link(group):
return link_to(group.name, url('repos_group_home',
group_name=group.group_name))
return literal(' » '.join(map(make_link, groups)) + \
" » " + repo_name)
def fancy_file_stats(stats):
Displays a fancy two colored bar for number of added/deleted
lines of code on file
:param stats: two element list of added/deleted lines of code
a, d, t = stats[0], stats[1], stats[0] + stats[1]
width = 100
unit = float(width) / (t or 1)
# needs > 9% of width to be visible or 0 to be hidden
a_p = max(9, unit * a) if a > 0 else 0
d_p = max(9, unit * d) if d > 0 else 0
p_sum = a_p + d_p
if p_sum > width:
#adjust the percentage to be == 100% since we adjusted to 9
if a_p > d_p:
a_p = a_p - (p_sum - width)
d_p = d_p - (p_sum - width)
a_v = a if a > 0 else ''
d_v = d if d > 0 else ''
def cgen(l_type):
mapping = {'tr':'top-right-rounded-corner',
'tl':'top-left-rounded-corner',
'br':'bottom-right-rounded-corner',
'bl':'bottom-left-rounded-corner'}
map_getter = lambda x:mapping[x]
if l_type == 'a' and d_v:
#case when added and deleted are present
return ' '.join(map(map_getter, ['tl', 'bl']))
if l_type == 'a' and not d_v:
return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
if l_type == 'd' and a_v:
return ' '.join(map(map_getter, ['tr', 'br']))
if l_type == 'd' and not a_v:
d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
a_p, a_v)
d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
d_p, d_v)
return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
def urlify_text(text):
url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
'''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
def url_func(match_obj):
url_full = match_obj.groups()[0]
return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
return literal(url_pat.sub(url_func, text))
def rst(source):
return literal('<div class="rst-block">%s</div>' %
MarkupRenderer.rst(source))
def rst_w_mentions(source):
Wrapped rst renderer with @mention highlighting
:param source:
MarkupRenderer.rst_with_mentions(source))
rhodecode.model.comment
~~~~~~~~~~~~~~~~~~~~~~~
comments model for RhodeCode
:created_on: Nov 11, 2011
from sqlalchemy.util.compat import defaultdict
from rhodecode.lib import extract_mentioned_users
from rhodecode.lib import helpers as h
from rhodecode.model import BaseModel
from rhodecode.model.db import ChangesetComment, User, Repository, Notification
from rhodecode.model.notification import NotificationModel
class ChangesetCommentsModel(BaseModel):
def __get_changeset_comment(self, changeset_comment):
return self._get_instance(ChangesetComment, changeset_comment)
def _extract_mentions(self, s):
user_objects = []
for username in extract_mentioned_users(s):
user_obj = User.get_by_username(username, case_insensitive=True)
if user_obj:
user_objects.append(user_obj)
return user_objects
def create(self, text, repo_id, user_id, revision, f_path=None,
line_no=None):
Creates new comment for changeset
:param text:
:param repo_id:
:param user_id:
:param revision:
:param f_path:
:param line_no:
if text:
repo = Repository.get(repo_id)
cs = repo.scm_instance.get_changeset(revision)
desc = cs.message
author = cs.author_email
comment = ChangesetComment()
comment.repo = repo
comment.user_id = user_id
comment.revision = revision
comment.text = text
comment.f_path = f_path
comment.line_no = line_no
self.sa.add(comment)
self.sa.flush()
# make notification
line = ''
if line_no:
line = _('on line %s') % line_no
subj = h.link_to('Re commit: %(commit_desc)s %(line)s' % \
{'commit_desc':desc, 'line':line},
{'commit_desc': desc, 'line': line},
h.url('changeset_home', repo_name=repo.repo_name,
anchor='comment-%s' % comment.comment_id,
qualified=True,
body = text
recipients = ChangesetComment.get_users(revision=revision)
# add changeset author
recipients += [User.get_by_email(author)]
NotificationModel().create(created_by=user_id, subject=subj,
body=body, recipients=recipients,
type_=Notification.TYPE_CHANGESET_COMMENT)
mention_recipients = set(self._extract_mentions(body))\
.difference(recipients)
if mention_recipients:
subj = _('[Mention]') + ' ' + subj
body=body,
recipients=mention_recipients,
return comment
def delete(self, comment):
Deletes given comment
:param comment_id:
comment = self.__get_changeset_comment(comment)
self.sa.delete(comment)
def get_comments(self, repo_id, revision):
return ChangesetComment.query()\
.filter(ChangesetComment.repo_id == repo_id)\
.filter(ChangesetComment.revision == revision)\
.filter(ChangesetComment.line_no == None)\
.filter(ChangesetComment.f_path == None).all()
def get_inline_comments(self, repo_id, revision):
comments = self.sa.query(ChangesetComment)\
.filter(ChangesetComment.line_no != None)\
.filter(ChangesetComment.f_path != None).all()
paths = defaultdict(lambda:defaultdict(list))
paths = defaultdict(lambda: defaultdict(list))
for co in comments:
paths[co.f_path][co.line_no].append(co)
return paths.items()
## -*- coding: utf-8 -*-
##usage:
## <%namespace name="diff_block" file="/changeset/diff_block.html"/>
## ${diff_block.diff_block(changes)}
##
<%def name="diff_block(changes)">
%for change,filenode,diff,cs1,cs2,stat in changes:
%if change !='removed':
<div id="${h.FID(filenode.changeset.raw_id,filenode.path)}" style="clear:both;height:90px;margin-top:-60px"></div>
<div class="diffblock margined comm">
<div class="code-header">
<div class="changeset_header">
<div class="changeset_file">
${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name,
revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))}
</div>
<div class="diff-menu-wrapper">
<img class="diff-menu-activate" style="margin-bottom:-6px;cursor: pointer" alt="diff-menu" src="${h.url('/images/icons/script_gear.png')}" />
<div class="diff-menu" style="display:none">
<ul>
<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>
<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>
<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>
<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>
<li>${c.ignorews_url(h.FID(filenode.changeset.raw_id,filenode.path))}</li>
<li>${c.context_url(h.FID(filenode.changeset.raw_id,filenode.path))}</li>
</ul>
<span style="float:right;margin-top:-3px">
<label>
${_('show inline comments')}
${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=h.FID(filenode.changeset.raw_id,filenode.path))}
</label>
</span>
<div class="code-body">
<div class="full_f_path" path="${h.safe_unicode(filenode.path)}"></div>
%if diff:
${diff|n}
%else:
${_('No changes in this file')}
%endif
%endfor
</%def>
\ No newline at end of file
<%inherit file="/base/base.html"/>
<%def name="title()">
${c.repo_name} ${_('File diff')} - ${c.rhodecode_name}
<%def name="breadcrumbs_links()">
${h.link_to(u'Home',h.url('/'))}
»
${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
${_('File diff')} r${c.changeset_1.revision}:${h.short_id(c.changeset_1.raw_id)} → r${c.changeset_2.revision}:${h.short_id(c.changeset_2.raw_id)}
<%def name="page_nav()">
${self.menu('files')}
<%def name="main()">
<div class="box">
<!-- box / title -->
<div class="title">
${self.breadcrumbs()}
<div class="table">
<div id="body" class="diffblock">
<span class="changeset_file">${h.link_to(c.f_path,h.url('files_home',repo_name=c.repo_name,
revision=c.changeset_2.raw_id,f_path=c.f_path))}</span>
» <span>${h.link_to(_('diff'),
h.url.current(diff2=c.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='diff'))}</span>
» <span>${h.link_to(_('raw diff'),
h.url.current(diff2=c.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='raw'))}</span>
» <span>${h.link_to(_('download diff'),
h.url.current(diff2=c.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='download'))}</span>
%if c.no_changes:
${_('No changes')}
%elif c.big_diff:
${_('Diff is to big to display')} ${h.link_to(_('raw diff'),
h.url.current(diff2=c.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='raw'))}
${c.cur_diff|n}
<div>
## diff block
<%namespace name="diff_block" file="/changeset/diff_block.html"/>
${diff_block.diff_block(c.changes)}
<script>
YUE.onDOMReady(function(){
YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
var act = e.currentTarget.nextElementSibling;
if(YUD.hasClass(act,'active')){
YUD.removeClass(act,'active');
YUD.setStyle(act,'display','none');
}else{
YUD.addClass(act,'active');
YUD.setStyle(act,'display','');
});
</script>
${c.repo_name} ${_('Edit file')} - ${c.rhodecode_name}
<%def name="js_extra()">
<script type="text/javascript" src="${h.url('/js/codemirror.js')}"></script>
<%def name="css_extra()">
<link rel="stylesheet" type="text/css" href="${h.url('/css/codemirror.css')}"/>
${_('edit file')} @ R${c.cs.revision}:${h.short_id(c.cs.raw_id)}
<ul class="links">
<li>
<span style="text-transform: uppercase;">
<a href="#">${_('branch')}: ${c.cs.branch}</a></span>
</li>
<div id="files_data">
<h3 class="files_location">${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.cs.revision,c.file.path)}</h3>
${h.form(h.url.current(),method='post',id='eform')}
<div id="body" class="codeblock">
<div class="stats">
<div class="left"><img src="${h.url('/images/icons/file.png')}"/></div>
<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>
<div class="left item">${h.format_byte_size(c.file.size,binary=True)}</div>
<div class="left item last">${c.file.mimetype}</div>
<div class="buttons">
${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")}
${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")}
${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")}
% if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
% if not c.file.is_binary:
${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")}
% endif
<div class="commit">${_('Editing file')}: ${c.file.path}</div>
<pre id="editor_pre"></pre>
<textarea id="editor" name="content" style="display:none">${c.file.content|n}</textarea>
<textarea id="editor" name="content" style="display:none">${h.escape(c.file.content)|n}</textarea>
<div style="padding: 10px;color:#666666">${_('commit message')}</div>
<textarea id="commit" name="message" style="height: 60px;width: 99%;margin-left:4px"></textarea>
<div style="text-align: left;padding-top: 5px">
${h.submit('commit',_('Commit changes'),class_="ui-btn")}
${h.reset('reset',_('Reset'),class_="ui-btn")}
${h.end_form()}
<script type="text/javascript">
var reset_url = "${h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.file.path)}";
initCodeMirror('editor',reset_url);
<dl>
<dt style="padding-top:10px;font-size:16px">${_('History')}</dt>
<dd>
${h.form(h.url('files_diff_home',repo_name=c.repo_name,f_path=c.f_path),method='get')}
${h.hidden('diff2',c.file.last_changeset.raw_id)}
${h.select('diff1',c.file.last_changeset.raw_id,c.file_history)}
${h.submit('diff','diff to revision',class_="ui-btn")}
${h.submit('show_rev','show at revision',class_="ui-btn")}
</dd>
</dl>
<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>
${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")}
${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")}
${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")}
${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")}
<div class="author">
<div class="gravatar">
<img alt="gravatar" src="${h.gravatar_url(h.email(c.changeset.author),16)}"/>
<div title="${c.changeset.author}" class="user">${h.person(c.changeset.author)}</div>
<div class="commit">${c.file.last_changeset.message}</div>
%if c.file.is_binary:
${_('Binary file (%s)') % c.file.mimetype}
% if c.file.size < c.cut_off_limit:
${h.pygmentize(c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
${_('File is too big to display')} ${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))}
function highlight_lines(lines){
for(pos in lines){
YUD.setStyle('L'+lines[pos],'background-color','#FFFFBE');
page_highlights = location.href.substring(location.href.indexOf('#')+1).split('L');
if (page_highlights.length == 2){
highlight_ranges = page_highlights[1].split(",");
var h_lines = [];
for (pos in highlight_ranges){
var _range = highlight_ranges[pos].split('-');
if(_range.length == 2){
var start = parseInt(_range[0]);
var end = parseInt(_range[1]);
if (start < end){
for(var i=start;i<=end;i++){
h_lines.push(i);
else{
h_lines.push(parseInt(highlight_ranges[pos]));
highlight_lines(h_lines);
//remember original location
var old_hash = location.href.substring(location.href.indexOf('#'));
// this makes a jump to anchor moved by 3 posstions for padding
window.location.hash = '#L'+Math.max(parseInt(h_lines[0])-3,1);
//sets old anchor
window.location.hash = old_hash;
YUE.on('show_rev','click',function(e){
YUE.preventDefault(e);
var cs = YUD.get('diff1').value;
var url = "${h.url('files_home',repo_name=c.repo_name,revision='__CS__',f_path=c.f_path)}".replace('__CS__',cs);
window.location = url;
YUE.on('hlcode','mouseup',getSelectionLink("${_('Selection link')}"))
Status change: