Changeset - 5610fd9b6803
[Not reviewed]
beta
0 3 0
Marcin Kuzminski - 14 years ago 2011-12-08 01:25:23
marcin@python-works.com
added line context control to diffs
3 files changed with 21 insertions and 10 deletions:
0 comments (0 inline, 0 general)
rhodecode/controllers/changeset.py
Show inline comments
 
@@ -15,288 +15,294 @@
 
# 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 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
 

	
 
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 vcs.exceptions import RepositoryError, ChangesetError, \
 
    ChangesetDoesNotExistError
 
from vcs.nodes import FileNode
 
from webob.exc import HTTPForbidden
 
from rhodecode.model.meta import Session
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
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):
 
        ignore_whitespace = request.GET.get('ignorews') == '1'
 
        line_context = request.GET.get('context', 3)
 
        def wrap_to_table(str):
 

	
 
            return '''<table class="code-difftable">
 
                        <tr class="line">
 
                        <td class="lineno new"></td>
 
                        <td class="code"><pre>%s</pre></td>
 
                        </tr>
 
                      </table>''' % str
 

	
 
        #get ranges of revisions if preset
 
        rev_range = revision.split('...')[:2]
 

	
 
        try:
 
            if len(rev_range) == 2:
 
                rev_start = rev_range[0]
 
                rev_end = rev_range[1]
 
                rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
 
                                                            end=rev_end)
 
            else:
 
                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.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] = []
 
            try:
 
                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)
 
                else:
 
                    # 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
 
                    if c.sum_added < self.cut_off_limit:
 
                        f_gitdiff = diffs.get_gitdiff(filenode_old, node,
 
                                           ignore_whitespace=ignore_whitespace)
 
                                           ignore_whitespace=ignore_whitespace,
 
                                           context=line_context)
 
                        d = diffs.DiffProcessor(f_gitdiff, format='gitdiff')
 

	
 
                        st = d.stat()
 
                        diff = d.as_html()
 

	
 
                    else:
 
                        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
 
                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:
 
                    try:
 
                        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 filenode_old.is_binary or node.is_binary:
 
                        diff = wrap_to_table(_('binary file'))
 
                        st = (0, 0)
 
                    else:
 

	
 
                        if c.sum_removed < self.cut_off_limit:
 
                            f_gitdiff = diffs.get_gitdiff(filenode_old, node,
 
                                           ignore_whitespace=ignore_whitespace)
 
                                           ignore_whitespace=ignore_whitespace,
 
                                           context=line_context)
 
                            d = diffs.DiffProcessor(f_gitdiff,
 
                                                     format='gitdiff')
 
                            st = d.stat()
 
                            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'))
 
                            else:
 
                                diff = d.as_html()
 

	
 
                            if diff:
 
                                c.sum_removed += len(diff)
 
                        else:
 
                            diff = wrap_to_table(_('Changeset is to big and '
 
                                                   'was cut off, see raw '
 
                                                   'changeset instead'))
 
                            c.cut_off = True
 
                            break
 

	
 
                    cs1 = filenode_old.last_changeset.raw_id
 
                    cs2 = node.last_changeset.raw_id
 
                    c.lines_added += st[0]
 
                    c.lines_deleted += st[1]
 
                    c.changes[changeset.raw_id].append(('changed', node, diff,
 
                                                        cs1, cs2, st))
 

	
 
            #==================================================================
 
            # REMOVED FILES
 
            #==================================================================
 
            if not c.cut_off:
 
                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')
 
        else:
 
            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)
 
        try:
 
            c.scm_type = c.rhodecode_repo.alias
 
            c.changeset = c.rhodecode_repo.get_changeset(revision)
 
        except RepositoryError:
 
            log.error(traceback.format_exc())
 
            return redirect(url('home'))
 
        else:
 
            try:
 
                c.changeset_parent = c.changeset.parents[0]
 
            except IndexError:
 
                c.changeset_parent = None
 
            c.changes = []
 

	
 
            for node in c.changeset.added:
 
                filenode_old = FileNode(node.path, '')
 
                if filenode_old.is_binary or node.is_binary:
 
                    diff = _('binary file') + '\n'
 
                else:
 
                    f_gitdiff = diffs.get_gitdiff(filenode_old, node,
 
                                           ignore_whitespace=ignore_whitespace)
 
                                           ignore_whitespace=ignore_whitespace,
 
                                           context=line_context)
 
                    diff = diffs.DiffProcessor(f_gitdiff,
 
                                                format='gitdiff').raw_diff()
 

	
 
                cs1 = None
 
                cs2 = node.last_changeset.raw_id
 
                c.changes.append(('added', node, diff, cs1, cs2))
 

	
 
            for node in c.changeset.changed:
 
                filenode_old = c.changeset_parent.get_node(node.path)
 
                if filenode_old.is_binary or node.is_binary:
 
                    diff = _('binary file')
 
                else:
 
                    f_gitdiff = diffs.get_gitdiff(filenode_old, node,
 
                                           ignore_whitespace=ignore_whitespace)
 
                                           ignore_whitespace=ignore_whitespace,
 
                                           context=line_context)
 
                    diff = diffs.DiffProcessor(f_gitdiff,
 
                                                format='gitdiff').raw_diff()
 

	
 
                cs1 = filenode_old.last_changeset.raw_id
 
                cs2 = node.last_changeset.raw_id
 
                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)
 
            Session.commit()
 
            return True
 
        else:
 
            raise HTTPForbidden()
 

	
rhodecode/controllers/files.py
Show inline comments
 
@@ -361,158 +361,163 @@ class FilesController(BaseRepoController
 
            if len(archive_spec) == 2 and archive_spec[1] == '':
 
                fileformat = a_type or ext_data[1]
 
                revision = archive_spec[0]
 
                ext = ext_data[1]
 

	
 
        try:
 
            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)
 
                    break
 
                yield data
 

	
 
        return get_chunked_archive(archive)
 

	
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def diff(self, repo_name, f_path):
 
        ignore_whitespace = request.GET.get('ignorews') == '1'
 
        line_context = request.GET.get('context', 3)
 
        diff1 = request.GET.get('diff1')
 
        diff2 = request.GET.get('diff2')
 
        c.action = request.GET.get('diff')
 
        c.no_changes = diff1 == diff2
 
        c.f_path = f_path
 
        c.big_diff = False
 

	
 
        try:
 
            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)
 
            else:
 
                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)
 
            else:
 
                c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
 
                node2 = FileNode('.', '', changeset=c.changeset_2)
 
        except RepositoryError:
 
            return redirect(url('files_home',
 
                                repo_name=c.repo_name, f_path=f_path))
 

	
 
        if c.action == 'download':
 
            _diff = diffs.get_gitdiff(node1, node2,
 
                                       ignore_whitespace=ignore_whitespace)
 
                                      ignore_whitespace=ignore_whitespace,
 
                                      context=line_context)
 
            diff = diffs.DiffProcessor(_diff,format='gitdiff')
 

	
 
            diff_name = '%s_vs_%s.diff' % (diff1, diff2)
 
            response.content_type = 'text/plain'
 
            response.content_disposition = 'attachment; filename=%s' \
 
                                                    % diff_name
 
            return diff.raw_diff()
 

	
 
        elif c.action == 'raw':
 
            _diff = diffs.get_gitdiff(node1, node2,
 
                                       ignore_whitespace=ignore_whitespace)
 
                                      ignore_whitespace=ignore_whitespace,
 
                                      context=line_context)
 
            diff = diffs.DiffProcessor(_diff,format='gitdiff')
 
            response.content_type = 'text/plain'
 
            return diff.raw_diff()
 

	
 
        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
 
            else:
 
                _diff = diffs.get_gitdiff(node1, node2,
 
                                           ignore_whitespace=ignore_whitespace)
 
                                           ignore_whitespace=ignore_whitespace,
 
                                           context=line_context)
 
                diff = diffs.DiffProcessor(_diff,format='gitdiff')
 
                c.cur_diff = diff.as_html()
 
        else:
 

	
 
            #default option
 
            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
 

	
 
            else:
 
                _diff = diffs.get_gitdiff(node1, node2,
 
                                           ignore_whitespace=ignore_whitespace)
 
                                          ignore_whitespace=ignore_whitespace,
 
                                          context=line_context)
 
                diff = diffs.DiffProcessor(_diff,format='gitdiff')
 
                c.cur_diff = diff.as_html()
 

	
 
        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():
 
            #chs = chs.split(':')[-1]
 
            tags_group[0].append((chs, name),)
 
        hist_l.append(tags_group)
 

	
 
        return hist_l
 

	
 
    @jsonify
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def nodelist(self, repo_name, revision, f_path):
 
        if request.environ.get('HTTP_X_PARTIAL_XHR'):
 
            cs = self.__get_cs_or_redirect(revision, repo_name)
 
            _d, _f = self.__get_paths(cs, f_path)
 
            return _d + _f
 

	
rhodecode/lib/diffs.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    rhodecode.lib.diffs
 
    ~~~~~~~~~~~~~~~~~~~
 

	
 
    Set of diffing helpers, previously part of vcs
 
    
 
    
 
    :created_on: Dec 4, 2011
 
    :author: marcink
 
    :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
 
    :original copyright: 2007-2008 by Armin Ronacher    
 
    :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 re
 
import difflib
 

	
 
from itertools import tee, imap
 

	
 
from mercurial.match import match
 

	
 
from vcs.exceptions import VCSError
 
from vcs.nodes import FileNode
 

	
 
def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True):
 
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)
 
                                 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))
 
        else:
 
            udiff_copy = self.copy_iterator()
 
            self.lines = imap(self.escaper, udiff_copy)
 

	
 
        # Select a differ.
 
        if differ == 'difflib':
 
            self.differ = self._highlight_line_difflib
 
        else:
 
            self.differ = self._highlight_line_udiff
 

	
 
    def escaper(self, string):
 
        return string.replace('<', '&lt;').replace('>', '&gt;')
 

	
 
    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
0 comments (0 inline, 0 general)