Changeset - a520d542697e
[Not reviewed]
beta
0 9 0
Marcin Kuzminski - 13 years ago 2012-11-28 01:27:21
marcin@python-works.com
Implemented file history page for showing detailed changelog for a given file
- fixed git detection of filenode history when executed on directory
- shortlog uses urlify_commit function now
9 files changed with 125 insertions and 14 deletions:
0 comments (0 inline, 0 general)
rhodecode/config/routing.py
Show inline comments
 
@@ -505,24 +505,28 @@ def make_map(config):
 

	
 
    rmap.connect('pullrequest_comment_delete',
 
                 '/{repo_name:.*?}/pull-request-comment/{comment_id}/delete',
 
                controller='pullrequests', action='delete_comment',
 
                conditions=dict(function=check_repo, method=["DELETE"]))
 

	
 
    rmap.connect('summary_home', '/{repo_name:.*?}/summary',
 
                controller='summary', conditions=dict(function=check_repo))
 

	
 
    rmap.connect('shortlog_home', '/{repo_name:.*?}/shortlog',
 
                controller='shortlog', conditions=dict(function=check_repo))
 

	
 
    rmap.connect('shortlog_file_home', '/{repo_name:.*?}/shortlog/{revision}/{f_path:.*}',
 
                controller='shortlog', f_path=None,
 
                conditions=dict(function=check_repo))
 

	
 
    rmap.connect('branches_home', '/{repo_name:.*?}/branches',
 
                controller='branches', conditions=dict(function=check_repo))
 

	
 
    rmap.connect('tags_home', '/{repo_name:.*?}/tags',
 
                controller='tags', conditions=dict(function=check_repo))
 

	
 
    rmap.connect('bookmarks_home', '/{repo_name:.*?}/bookmarks',
 
                controller='bookmarks', conditions=dict(function=check_repo))
 

	
 
    rmap.connect('changelog_home', '/{repo_name:.*?}/changelog',
 
                controller='changelog', conditions=dict(function=check_repo))
 

	
rhodecode/controllers/shortlog.py
Show inline comments
 
@@ -17,50 +17,88 @@
 
#
 
# 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
 

	
 
from pylons import tmpl_context as c, request, url
 
from pylons.i18n.translation import _
 

	
 
from rhodecode.lib import helpers as h
 
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
 
from rhodecode.lib.base import BaseRepoController, render
 
from rhodecode.lib.helpers import RepoPage
 
from pylons.controllers.util import redirect
 
from rhodecode.lib.utils2 import safe_int
 
from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError, ChangesetError,\
 
    RepositoryError
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class ShortlogController(BaseRepoController):
 

	
 
    @LoginRequired()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def __before__(self):
 
        super(ShortlogController, self).__before__()
 

	
 
    def index(self, repo_name):
 
    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
 
        """
 

	
 
        try:
 
            return c.rhodecode_repo.get_changeset(rev)
 
        except RepositoryError, e:
 
            h.flash(str(e), category='warning')
 
            redirect(h.url('shortlog_home', repo_name=repo_name))
 

	
 
    def index(self, repo_name, revision=None, f_path=None):
 
        p = safe_int(request.params.get('page', 1), 1)
 
        size = safe_int(request.params.get('size', 20), 20)
 

	
 
        def url_generator(**kw):
 
            return url('shortlog_home', repo_name=repo_name, size=size, **kw)
 

	
 
        c.repo_changesets = RepoPage(c.rhodecode_repo, page=p,
 
        collection = c.rhodecode_repo
 
        c.file_history = f_path
 
        if f_path:
 
            f_path = f_path.lstrip('/')
 
            # get the history for the file !
 
            tip_cs = c.rhodecode_repo.get_changeset()
 
            try:
 
                collection = tip_cs.get_file_history(f_path)
 
            except (NodeDoesNotExistError, ChangesetError):
 
                #this node is not present at tip !
 
                try:
 
                    cs = self.__get_cs_or_redirect(revision, repo_name)
 
                    collection = cs.get_file_history(f_path)
 
                except RepositoryError, e:
 
                    h.flash(str(e), category='warning')
 
                    redirect(h.url('shortlog_home', repo_name=repo_name))
 
            collection = list(reversed(collection))
 

	
 
        c.repo_changesets = RepoPage(collection, page=p,
 
                                    items_per_page=size, url=url_generator)
 
        page_revisions = [x.raw_id for x in list(c.repo_changesets)]
 
        page_revisions = [x.raw_id for x in list(collection)]
 
        c.statuses = c.rhodecode_db_repo.statuses(page_revisions)
 

	
 
        if not c.repo_changesets:
 
            h.flash(_('There are no changesets yet'), category='warning')
 
            return redirect(url('summary_home', repo_name=repo_name))
 

	
 
        c.shortlog_data = render('shortlog/shortlog_data.html')
 
        if request.environ.get('HTTP_X_PARTIAL_XHR'):
 
            return c.shortlog_data
 
        r = render('shortlog/shortlog.html')
 
        return r
rhodecode/lib/vcs/backends/git/changeset.py
Show inline comments
 
@@ -153,24 +153,31 @@ class GitChangeset(BaseChangeset):
 
                raise NodeDoesNotExistError("There is no file nor directory "
 
                    "at the given path %r at revision %r"
 
                    % (path, self.short_id))
 
        return self._paths[path]
 

	
 
    def _get_kind(self, path):
 
        obj = self.repository._repo[self._get_id_for_path(path)]
 
        if isinstance(obj, objects.Blob):
 
            return NodeKind.FILE
 
        elif isinstance(obj, objects.Tree):
 
            return NodeKind.DIR
 

	
 
    def _get_filectx(self, path):
 
        path = self._fix_path(path)
 
        if self._get_kind(path) != NodeKind.FILE:
 
            raise ChangesetError("File does not exist for revision %r at "
 
                " %r" % (self.raw_id, path))
 
        return path
 

	
 
    def _get_file_nodes(self):
 
        return chain(*(t[2] for t in self.walk()))
 

	
 
    @LazyProperty
 
    def parents(self):
 
        """
 
        Returns list of parents changesets.
 
        """
 
        return [self.repository.get_changeset(parent)
 
                for parent in self._commit.parents]
 

	
 
    def next(self, branch=None):
 
@@ -255,24 +262,25 @@ class GitChangeset(BaseChangeset):
 
        node = self.get_node(path)
 
        return node.history[0]
 

	
 
    def get_file_history(self, path):
 
        """
 
        Returns history of file as reversed list of ``Changeset`` objects for
 
        which file at given ``path`` has been modified.
 

	
 
        TODO: This function now uses os underlying 'git' and 'grep' commands
 
        which is generally not good. Should be replaced with algorithm
 
        iterating commits.
 
        """
 
        self._get_filectx(path)
 
        cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
 
                  self.id, path
 
               )
 
        so, se = self.repository.run_git_command(cmd)
 
        ids = re.findall(r'[0-9a-fA-F]{40}', so)
 
        return [self.repository.get_changeset(id) for id in ids]
 

	
 
    def get_file_annotate(self, path):
 
        """
 
        Returns a list of three element tuples with lineno,changeset and line
 

	
 
        TODO: This function now uses os underlying 'git' command which is
rhodecode/lib/vcs/backends/hg/changeset.py
Show inline comments
 
@@ -172,25 +172,25 @@ class MercurialChangeset(BaseChangeset):
 
        if path in self._file_paths:
 
            return NodeKind.FILE
 
        elif path in self._dir_paths:
 
            return NodeKind.DIR
 
        else:
 
            raise ChangesetError("Node does not exist at the given path %r"
 
                % (path))
 

	
 
    def _get_filectx(self, path):
 
        path = self._fix_path(path)
 
        if self._get_kind(path) != NodeKind.FILE:
 
            raise ChangesetError("File does not exist for revision %r at "
 
                " %r" % (self.revision, path))
 
                " %r" % (self.raw_id, path))
 
        return self._ctx.filectx(path)
 

	
 
    def _extract_submodules(self):
 
        """
 
        returns a dictionary with submodule information from substate file
 
        of hg repository
 
        """
 
        return self._ctx.substate
 

	
 
    def get_file_mode(self, path):
 
        """
 
        Returns stat mode of the file at the given ``path``.
rhodecode/public/css/style.css
Show inline comments
 
@@ -2641,33 +2641,32 @@ h3.files_location {
 
	float: left;
 
	margin-left: -4px;
 
	margin-top: 4px;
 
}
 
 
#graph_content .container .mid .message {
 
	white-space: pre-wrap;
 
}
 
 
#graph_content .container .mid .message a:hover{
 
	text-decoration: none;
 
}
 
#content #graph_content .message .revision-link,
 
#changeset_content .container .message .revision-link
 
 
.revision-link
 
 {
 
	color:#3F6F9F;
 
    font-weight: bold !important;
 
}
 
 
#content #graph_content .message .issue-tracker-link,
 
#changeset_content .container .message .issue-tracker-link{
 
.issue-tracker-link{
 
    color:#3F6F9F;
 
    font-weight: bold !important;
 
}
 
 
.changeset-status-container{
 
    padding-right: 5px;
 
    margin-top:1px;
 
    float:right;
 
    height:14px;
 
}
 
.code-header .changeset-status-container{
 
	float:left;
rhodecode/templates/files/files_history_box.html
Show inline comments
 
<dl>
 
    <dt class="file_history">${_('History')}</dt>
 
    <dd>
 
        <div>
 
            <div style="float:left">
 
            ${h.form(h.url('files_diff_home',repo_name=c.repo_name,f_path=c.f_path),method='get')}
 
            ${h.hidden('diff2',c.file_changeset.raw_id)}
 
            ${h.select('diff1',c.file_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")}
 
            ${h.link_to(_('show full history'),h.url('shortlog_file_home',repo_name=c.repo_name, revision=c.file_changeset.raw_id, f_path=c.f_path),class_="ui-btn")}
 
            ${h.hidden('annotate', c.annotate)}
 
            ${h.end_form()}
 
            </div>
 
            <div class="file_author">
 
                <div class="item">${h.literal(ungettext(u'%s author',u'%s authors',len(c.authors)) % ('<b>%s</b>' % len(c.authors))) }</div>
 
                %for email, user in c.authors:
 
                  <div class="contributor tooltip" style="float:left" title="${h.tooltip(user)}">
 
                    <div class="gravatar" style="margin:1px"><img alt="gravatar" src="${h.gravatar_url(email, 20)}"/> </div>
 
                  </div>
 
                %endfor
 
            </div>
 
        </div>
rhodecode/templates/shortlog/shortlog.html
Show inline comments
 
@@ -2,25 +2,31 @@
 
<%inherit file="/base/base.html"/>
 

	
 
<%def name="title()">
 
    ${_('%s Shortlog') % c.repo_name} - ${c.rhodecode_name}
 
</%def>
 

	
 

	
 
<%def name="breadcrumbs_links()">
 
    ${h.link_to(_(u'Home'),h.url('/'))}
 
    &raquo;
 
    ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
 
    &raquo;
 
    %if c.file_history:
 
        ${h.link_to(_('shortlog'),h.url('shortlog_home',repo_name=c.repo_name))}
 
        &raquo;
 
        ${c.file_history}
 
    %else:
 
    ${_('shortlog')}
 
    %endif
 
</%def>
 

	
 
<%def name="page_nav()">
 
    ${self.menu('shortlog')}
 
</%def>
 
<%def name="main()">
 
<div class="box">
 
    <!-- box / title -->
 
    <div class="title">
 
        ${self.breadcrumbs()}
 
    </div>
 
    <!-- end box / title -->
rhodecode/templates/shortlog/shortlog_data.html
Show inline comments
 
@@ -21,27 +21,25 @@
 
                    <img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" />
 
                  </a>
 
                %else:
 
                  <img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" />
 
                %endif
 
                </div>
 
              %endif
 
            </div>
 
            <pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}">r${cs.revision}:${h.short_id(cs.raw_id)}</a></pre>
 
         </div>
 
        </td>
 
        <td>
 
            ${h.link_to(h.truncate(cs.message,50) or _('No commit message'),
 
            h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id),
 
            title=cs.message)}
 
            ${h.urlify_commit(h.truncate(cs.message,50),c.repo_name, h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
 
        </td>
 
        <td><span class="tooltip" title="${h.tooltip(h.fmt_date(cs.date))}">
 
                      ${h.age(cs.date)}</span>
 
        </td>
 
		<td title="${cs.author}">${h.person(cs.author)}</td>
 
		<td>
 
			<span class="logtags">
 
                %if cs.branch:
 
				<span class="branchtag">
 
                    ${cs.branch}
 
                </span>
 
                %endif
rhodecode/tests/functional/test_shortlog.py
Show inline comments
 
from rhodecode.tests import *
 

	
 

	
 
class TestShortlogController(TestController):
 

	
 
    def test_index(self):
 
    def test_index_hg(self):
 
        self.log_user()
 
        response = self.app.get(url(controller='shortlog', action='index',
 
                                    repo_name=HG_REPO))
 
        # Test response...
 

	
 
    def test_index_git(self):
 
        self.log_user()
 
        response = self.app.get(url(controller='shortlog', action='index',
 
                                    repo_name=GIT_REPO))
 
        # Test response...
 

	
 
    def test_index_hg_with_filenode(self):
 
        self.log_user()
 
        response = self.app.get(url(controller='shortlog', action='index',
 
                                    revision='tip', f_path='/vcs/exceptions.py',
 
                                    repo_name=HG_REPO))
 
        #history commits messages
 
        response.mustcontain('Added exceptions module, this time for real')
 
        response.mustcontain('Added not implemented hg backend test case')
 
        response.mustcontain('Added BaseChangeset class')
 
        # Test response...
 

	
 
    def test_index_git_with_filenode(self):
 
        self.log_user()
 
        response = self.app.get(url(controller='shortlog', action='index',repo_name=HG_REPO))
 
        # Test response...
 
        response = self.app.get(url(controller='shortlog', action='index',
 
                                    revision='tip', f_path='/vcs/exceptions.py',
 
                                    repo_name=GIT_REPO))
 
        #history commits messages
 
        response.mustcontain('Added exceptions module, this time for real')
 
        response.mustcontain('Added not implemented hg backend test case')
 
        response.mustcontain('Added BaseChangeset class')
 

	
 
    def test_index_hg_with_filenode_that_is_dirnode(self):
 
        self.log_user()
 
        response = self.app.get(url(controller='shortlog', action='index',
 
                                    revision='tip', f_path='/tests',
 
                                    repo_name=HG_REPO))
 
        self.assertEqual(response.status, '302 Found')
 

	
 
    def test_index_git_with_filenode_that_is_dirnode(self):
 
        self.log_user()
 
        response = self.app.get(url(controller='shortlog', action='index',
 
                                    revision='tip', f_path='/tests',
 
                                    repo_name=GIT_REPO))
 
        self.assertEqual(response.status, '302 Found')
 

	
 
    def test_index_hg_with_filenode_not_existing(self):
 
        self.log_user()
 
        response = self.app.get(url(controller='shortlog', action='index',
 
                                    revision='tip', f_path='/wrong_path',
 
                                    repo_name=HG_REPO))
 
        self.assertEqual(response.status, '302 Found')
 

	
 
    def test_index_git_with_filenode_not_existing(self):
 
        self.log_user()
 
        response = self.app.get(url(controller='shortlog', action='index',
 
                                    revision='tip', f_path='/wrong_path',
 
                                    repo_name=GIT_REPO))
 
        self.assertEqual(response.status, '302 Found')
0 comments (0 inline, 0 general)