diff --git a/grunt_config.json b/grunt_config.json --- a/grunt_config.json +++ b/grunt_config.json @@ -68,6 +68,7 @@ "<%= dirs.js.src %>/rhodecode/utils/os.js", "<%= dirs.js.src %>/rhodecode/utils/topics.js", "<%= dirs.js.src %>/rhodecode/init.js", + "<%= dirs.js.src %>/rhodecode/changelog.js", "<%= dirs.js.src %>/rhodecode/codemirror.js", "<%= dirs.js.src %>/rhodecode/comments.js", "<%= dirs.js.src %>/rhodecode/constants.js", diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -997,10 +997,10 @@ def make_map(config): conditions={'function': check_repo}, requirements=URL_NAME_REQUIREMENTS, jsroute=True) - rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}', - controller='changelog', action='changelog_details', + rmap.connect('changelog_elements', '/{repo_name}/changelog_details', + controller='changelog', action='changelog_elements', conditions={'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) + requirements=URL_NAME_REQUIREMENTS, jsroute=True) rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}', controller='files', revision='tip', f_path='', diff --git a/rhodecode/controllers/changelog.py b/rhodecode/controllers/changelog.py --- a/rhodecode/controllers/changelog.py +++ b/rhodecode/controllers/changelog.py @@ -30,7 +30,8 @@ from pylons.i18n.translation import _ from webob.exc import HTTPNotFound, HTTPBadRequest import rhodecode.lib.helpers as h -from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator +from rhodecode.lib.auth import ( + LoginRequired, HasRepoPermissionAnyDecorator, XHRRequired) from rhodecode.lib.base import BaseRepoController, render from rhodecode.lib.ext_json import json from rhodecode.lib.graphmod import _colored, _dagwalker @@ -98,7 +99,7 @@ class ChangelogController(BaseRepoContro redirect(h.url('changelog_home', repo_name=repo.repo_name)) raise HTTPBadRequest() - def _graph(self, repo, commits): + def _graph(self, repo, commits, prev_data=None, next_data=None): """ Generates a DAG graph for repo @@ -106,12 +107,30 @@ class ChangelogController(BaseRepoContro :param commits: list of commits """ if not commits: - c.jsdata = json.dumps([]) - return + return json.dumps([]) + + def serialize(commit, parents=True): + data = dict( + raw_id=commit.raw_id, + idx=commit.idx, + branch=commit.branch, + ) + if parents: + data['parents'] = [ + serialize(x, parents=False) for x in commit.parents] + return data + + prev_data = prev_data or [] + next_data = next_data or [] + + current = [serialize(x) for x in commits] + commits = prev_data + current + next_data dag = _dagwalker(repo, commits) - data = [['', vtx, edges] for vtx, edges in _colored(dag)] - c.jsdata = json.dumps(data) + + data = [[commit_id, vtx, edges, branch] + for commit_id, vtx, edges, branch in _colored(dag)] + return json.dumps(data), json.dumps(current) def _check_if_valid_branch(self, branch_name, repo_name, f_path): if branch_name not in c.rhodecode_repo.branches_all: @@ -120,26 +139,37 @@ class ChangelogController(BaseRepoContro redirect(url('changelog_file_home', repo_name=repo_name, revision=branch_name, f_path=f_path or '')) + def _load_changelog_data(self, collection, page, chunk_size, branch_name=None, dynamic=False): + c.total_cs = len(collection) + c.showing_commits = min(chunk_size, c.total_cs) + c.pagination = RepoPage(collection, page=page, item_count=c.total_cs, + items_per_page=chunk_size, branch=branch_name) + + c.next_page = c.pagination.next_page + c.prev_page = c.pagination.previous_page + + if dynamic: + if request.GET.get('chunk') != 'next': + c.next_page = None + if request.GET.get('chunk') != 'prev': + c.prev_page = None + + page_commit_ids = [x.raw_id for x in c.pagination] + c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids) + c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids) + @LoginRequired() @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', 'repository.admin') def index(self, repo_name, revision=None, f_path=None): commit_id = revision - limit = 100 + chunk_size = 20 + + c.branch_name = branch_name = request.GET.get('branch', None) + c.book_name = book_name = request.GET.get('bookmark', None) hist_limit = safe_int(request.GET.get('limit')) or None - if request.GET.get('size'): - c.size = safe_int(request.GET.get('size'), 1) - session['changelog_size'] = c.size - session.save() - else: - c.size = int(session.get('changelog_size', DEFAULT_CHANGELOG_SIZE)) - - # min size must be 1 and less than limit - c.size = max(c.size, 1) if c.size <= limit else limit p = safe_int(request.GET.get('page', 1), 1) - c.branch_name = branch_name = request.GET.get('branch', None) - c.book_name = book_name = request.GET.get('bookmark', None) c.selected_name = branch_name or book_name if not commit_id and branch_name: @@ -147,6 +177,8 @@ class ChangelogController(BaseRepoContro c.changelog_for_path = f_path pre_load = ['author', 'branch', 'date', 'message', 'parents'] + commit_ids = [] + try: if f_path: log.debug('generating changelog for path %s', f_path) @@ -174,13 +206,9 @@ class ChangelogController(BaseRepoContro collection = c.rhodecode_repo.get_commits( branch_name=branch_name, pre_load=pre_load) - c.total_cs = len(collection) - c.showing_commits = min(c.size, c.total_cs) - c.pagination = RepoPage(collection, page=p, item_count=c.total_cs, - items_per_page=c.size, branch=branch_name) - page_commit_ids = [x.raw_id for x in c.pagination] - c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids) - c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids) + self._load_changelog_data( + collection, p, chunk_size, c.branch_name, dynamic=f_path) + except EmptyRepositoryError as e: h.flash(safe_str(e), category='warning') return redirect(url('summary_home', repo_name=repo_name)) @@ -195,22 +223,62 @@ class ChangelogController(BaseRepoContro # loading from ajax, we don't want the first result, it's popped return render('changelog/changelog_file_history.mako') - if f_path: - revs = [] - else: - revs = c.pagination - self._graph(c.rhodecode_repo, revs) + if not f_path: + commit_ids = c.pagination + + c.graph_data, c.graph_commits = self._graph( + c.rhodecode_repo, commit_ids) return render('changelog/changelog.mako') @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') - def changelog_details(self, commit_id): - if request.environ.get('HTTP_X_PARTIAL_XHR'): - c.commit = c.rhodecode_repo.get_commit(commit_id=commit_id) - return render('changelog/changelog_details.mako') - raise HTTPNotFound() + @XHRRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + def changelog_elements(self, repo_name): + commit_id = None + chunk_size = 20 + + def wrap_for_error(err): + return 'ERROR: {}'.format(err) + + c.branch_name = branch_name = request.GET.get('branch', None) + c.book_name = book_name = request.GET.get('bookmark', None) + + p = safe_int(request.GET.get('page', 1), 1) + + c.selected_name = branch_name or book_name + if not commit_id and branch_name: + if branch_name not in c.rhodecode_repo.branches_all: + return wrap_for_error( + safe_str('Missing branch: {}'.format(branch_name))) + + pre_load = ['author', 'branch', 'date', 'message', 'parents'] + collection = c.rhodecode_repo.get_commits( + branch_name=branch_name, pre_load=pre_load) + + try: + self._load_changelog_data(collection, p, chunk_size, dynamic=True) + except EmptyRepositoryError as e: + return wrap_for_error(safe_str(e)) + except (RepositoryError, CommitDoesNotExistError, Exception) as e: + log.exception('Failed to fetch commits') + return wrap_for_error(safe_str(e)) + + prev_data = None + next_data = None + + prev_graph = json.loads(request.POST.get('graph', '')) + + if request.GET.get('chunk') == 'prev': + next_data = prev_graph + elif request.GET.get('chunk') == 'next': + prev_data = prev_graph + + c.graph_data, c.graph_commits = self._graph( + c.rhodecode_repo, c.pagination, + prev_data=prev_data, next_data=next_data) + return render('changelog/changelog_elements.mako') @LoginRequired() @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', diff --git a/rhodecode/lib/graphmod.py b/rhodecode/lib/graphmod.py --- a/rhodecode/lib/graphmod.py +++ b/rhodecode/lib/graphmod.py @@ -21,7 +21,7 @@ """ Modified mercurial DAG graph functions that re-uses VCS structure -It allows to have a shared codebase for DAG generation for hg and git repos +It allows to have a shared codebase for DAG generation for hg, git, svn repos """ nullrev = -1 @@ -51,36 +51,30 @@ def _dagwalker(repo, commits): if not commits: return - # TODO: johbo: Use some sort of vcs api here - if repo.alias == 'hg': - def get_parent_indexes(idx): - return repo._remote.ctx_parents(idx) + def get_parent_indexes(idx): + return [commit.idx for commit in repo[idx].parents] - elif repo.alias in ['git', 'svn']: - def get_parent_indexes(idx): - return [commit.idx for commit in repo[idx].parents] - - indexes = [commit.idx for commit in commits] + indexes = [commit['idx'] for commit in commits] lowest_idx = min(indexes) known_indexes = set(indexes) - gpcache = {} + grandparnet_cache = {} for commit in commits: - parents = sorted(set([p.idx for p in commit.parents - if p.idx in known_indexes])) - mpars = [p.idx for p in commit.parents if - p.idx != nullrev and p.idx not in parents] + parents = sorted(set([p['idx'] for p in commit['parents'] + if p['idx'] in known_indexes])) + mpars = [p['idx'] for p in commit['parents'] if + p['idx'] != nullrev and p['idx'] not in parents] for mpar in mpars: - gp = gpcache.get(mpar) + gp = grandparnet_cache.get(mpar) if gp is None: - gp = gpcache[mpar] = grandparent( + gp = grandparnet_cache[mpar] = grandparent( get_parent_indexes, lowest_idx, indexes, mpar) if not gp: parents.append(mpar) else: parents.extend(g for g in gp if g not in parents) - yield (commit.idx, parents) + yield (commit['raw_id'], commit['idx'], parents, commit['branch']) def _colored(dag): @@ -100,7 +94,7 @@ def _colored(dag): colors = {} newcolor = 1 - for commit_idx, parents in dag: + for commit_id, commit_idx, parents, branch in dag: # Compute seen and next_ if commit_idx not in seen: @@ -137,7 +131,7 @@ def _colored(dag): for p in parents]) # Yield and move on - yield ((col, color), edges) + yield (commit_id, (col, color), edges, branch) seen = next_ diff --git a/rhodecode/public/css/main.less b/rhodecode/public/css/main.less --- a/rhodecode/public/css/main.less +++ b/rhodecode/public/css/main.less @@ -957,6 +957,18 @@ label { clear: left; } } + + .load-more-commits { + text-align: center; + } + .load-more-commits:hover { + background-color: @grey7; + } + .load-more-commits { + a { + display: block; + } + } } #filter_changelog { diff --git a/rhodecode/public/css/type.less b/rhodecode/public/css/type.less --- a/rhodecode/public/css/type.less +++ b/rhodecode/public/css/type.less @@ -50,7 +50,8 @@ h6, .h6 { font-size: 1em; font-family .breadcrumbs_light { float:left; - margin: @padding 0; + font-size: 1.3em; + line-height: 38px; } // Body text diff --git a/rhodecode/public/js/jquery.commits-graph.js b/rhodecode/public/js/jquery.commits-graph.js --- a/rhodecode/public/js/jquery.commits-graph.js +++ b/rhodecode/public/js/jquery.commits-graph.js @@ -21,7 +21,7 @@ function Route( commit, data, options ) self.branch = data[2]; } -Route.prototype.drawRoute = function ( ctx ) { +Route.prototype.drawRoute = function ( commit, ctx ) { var self = this; if (self.options.orientation === "horizontal") { @@ -52,12 +52,13 @@ Route.prototype.drawRoute = function ( c } else { var from_x = self.options.width * self.options.scaleFactor - (self.from + 1) * self.options.x_step * self.options.scaleFactor; - var row = $("#chg_"+(self.commit.idx+1)) + var row = $("#sha_" + commit.sha); if (row.length) { var from_y = (row.offset().top + row.height() / 2 - self.options.relaOffset) * self.options.scaleFactor; } var to_x = self.options.width * self.options.scaleFactor - (self.to + 1) * self.options.x_step * self.options.scaleFactor; - var next_row = $("#chg_"+(self.commit.idx+2)) + var next_row = $("#sha_" + commit.sha).next('tr'); + if (next_row.length) { var to_y = ((next_row.offset().top + next_row.height() / 2 - self.options.relaOffset) + 0.2) * self.options.scaleFactor; } @@ -108,7 +109,8 @@ Commit.prototype.drawDot = function ( ct } else { var x = self.options.width * self.options.scaleFactor - (self.dot_offset + 1) * self.options.x_step * self.options.scaleFactor; - var row = $("#chg_"+(self.idx+1)) + var row = $("#sha_" + self.sha); + var y = (row.offset().top + row.height() / 2 - self.options.relaOffset) * self.options.scaleFactor; ctx.fillStyle = self.graph.get_color(self.dot_branch); ctx.beginPath(); @@ -218,7 +220,7 @@ GraphCanvas.prototype.draw = function () ctx.lineWidth = self.options.lineWidth; - self.options.relaOffset = $("#chg_1").offset().top; + self.options.relaOffset = $(".changelogRow").first().offset().top; var n_commits = self.data.length; for (var i=0; i. +// # +// # This program is dual-licensed. If you wish to learn more about the +// # RhodeCode Enterprise Edition, including its added features, Support services, +// # and proprietary license terms, please see https://rhodecode.com/licenses/ + + +var CommitsController = function () { + var self = this; + this.$graphCanvas = $('#graph_canvas'); + this.$commitCounter = $('#commit-counter'); + + this.getCurrentGraphData = function () { + // raw form + return self.$graphCanvas.data('commits'); + }; + + this.setLabelText = function (graphData) { + var shown = $('.commit_hash').length; + var total = self.$commitCounter.data('total'); + + if (shown == 1) { + var text = _gettext('showing {0} out of {1} commit').format(shown, total); + } else { + var text = _gettext('showing {0} out of {1} commits').format(shown, total); + } + self.$commitCounter.html(text) + }; + + this.reloadGraph = function (chunk) { + chunk = chunk || 'next'; + + // reset state on re-render ! + self.$graphCanvas.html(''); + + var edgeData = $("[data-graph]").data('graph') || this.$graphCanvas.data('graph') || []; + + // Determine max number of edges per row in graph + var edgeCount = 1; + $.each(edgeData, function (i, item) { + $.each(item[2], function (key, value) { + if (value[1] > edgeCount) { + edgeCount = value[1]; + } + }); + }); + + var x_step = Math.min(10, Math.floor(86 / edgeCount)); + var graph_options = { + width: 100, + height: $('#changesets').find('.commits-range').height(), + x_step: x_step, + y_step: 42, + dotRadius: 3.5, + lineWidth: 2.5 + }; + + var prevCommitsData = this.$graphCanvas.data('commits') || []; + var nextCommitsData = $("[data-graph]").data('commits') || []; + + if (chunk == 'next') { + var commitData = $.merge(prevCommitsData, nextCommitsData); + } else { + var commitData = $.merge(nextCommitsData, prevCommitsData); + } + + this.$graphCanvas.data('graph', edgeData); + this.$graphCanvas.data('commits', commitData); + + // destroy dynamic loaded graph + $("[data-graph]").remove(); + + this.$graphCanvas.commits(graph_options); + + this.setLabelText(edgeData); + if ($('.load-more-commits').find('.prev-commits').get(0)) { + var padding = 75; + + } else { + var padding = 43; + } + $('#graph_nodes').css({'padding-top': padding}); + }; + + this.getChunkUrl = function (page, chunk, branch) { + var urlData = { + 'repo_name': templateContext.repo_name, + 'page': page, + 'chunk': chunk + }; + + if (branch !== undefined && branch !== '') { + urlData['branch'] = branch; + } + + return pyroutes.url('changelog_elements', urlData); + }; + + this.loadNext = function (node, page, branch) { + var loadUrl = this.getChunkUrl(page, 'next', branch); + var postData = {'graph': JSON.stringify(this.getCurrentGraphData())}; + + $.post(loadUrl, postData, function (data) { + $(node).closest('tbody').append(data); + $(node).closest('td').remove(); + self.reloadGraph('next'); + }) + }; + + this.loadPrev = function (node, page, branch) { + var loadUrl = this.getChunkUrl(page, 'prev', branch); + var postData = {'graph': JSON.stringify(this.getCurrentGraphData())}; + + $.post(loadUrl, postData, function (data) { + $(node).closest('tbody').prepend(data); + $(node).closest('td').remove(); + self.reloadGraph('prev'); + }) + }; + + this.expandCommit = function (node) { + + var target_expand = $(node); + var cid = target_expand.data('commitId'); + + if (target_expand.hasClass('open')) { + $('#c-' + cid).css({ + 'height': '1.5em', + 'white-space': 'nowrap', + 'text-overflow': 'ellipsis', + 'overflow': 'hidden' + }); + $('#t-' + cid).css({ + 'height': 'auto', + 'line-height': '.9em', + 'text-overflow': 'ellipsis', + 'overflow': 'hidden', + 'white-space': 'nowrap' + }); + target_expand.removeClass('open'); + } + else { + $('#c-' + cid).css({ + 'height': 'auto', + 'white-space': 'pre-line', + 'text-overflow': 'initial', + 'overflow': 'visible' + }); + $('#t-' + cid).css({ + 'height': 'auto', + 'max-height': 'none', + 'text-overflow': 'initial', + 'overflow': 'visible', + 'white-space': 'normal' + }); + target_expand.addClass('open'); + } + // redraw the graph + self.reloadGraph(); + } +}; diff --git a/rhodecode/templates/changelog/changelog.mako b/rhodecode/templates/changelog/changelog.mako --- a/rhodecode/templates/changelog/changelog.mako +++ b/rhodecode/templates/changelog/changelog.mako @@ -16,7 +16,6 @@ %if c.changelog_for_path: /${c.changelog_for_path} %endif - ${ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)} <%def name="menu_bar_nav()"> @@ -74,6 +73,7 @@ % if c.pagination: +
@@ -85,134 +85,53 @@ %endif
${self.breadcrumbs('breadcrumbs_light')} +
+ ${ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)} +
-
-
-
-
-
-
- -
- - - ## checkbox - - +
+
+
+
+
+
-
- ## commit message expand arrow - - - - - +
+
${_('Commit')}${_('Commit Message')}${_('Age')}${_('Author')}
+ + ## checkbox + + - - - - %for cnt,commit in enumerate(c.pagination): - - - - + ## commit message expand arrow + + - %if c.statuses.get(commit.raw_id): -
- %if c.statuses.get(commit.raw_id)[2]: - -
-
- %else: - -
-
- %endif -
- %else: -
- %endif - - - - - + + + + + - - - - + <%include file='changelog_elements.mako'/> + +
${_('Refs')}
- ${h.checkbox(commit.raw_id,class_="commit-range")} - + ${_('Commit')}${_('Commit Message')} - %if c.comments.get(commit.raw_id): - - ${len(c.comments[commit.raw_id])} - - %endif - - - - ${h.show_id(commit)} - - - -
-   -
-
-
-
${h.urlify_commit_message(commit.message, c.repo_name)}
-
-
${_('Age')}${_('Author')}${_('Refs')}
- ${h.age_component(commit.date)} - - ${self.gravatar_with_user(commit.author)} - -
- ## branch - %if commit.branch: - - ${h.shorter(commit.branch)} - - %endif +
+
+
+
+ ${c.pagination.pager('$link_previous ~2~ $link_next')} +
+
- ## bookmarks - %if h.is_hg(c.rhodecode_repo): - %for book in commit.bookmarks: - - ${h.shorter(book)} - - %endfor - %endif - - ## tags - %for tag in commit.tags: - - ${h.shorter(tag)} - - %endfor - -
- - - %endfor - - - - - -
- ${c.pagination.pager('$link_previous ~2~ $link_next')} -
- - - - %else: + + % else: ${_('There are no changes yet')} - %endif - + % endif diff --git a/rhodecode/templates/changelog/changelog_details.mako b/rhodecode/templates/changelog/changelog_elements.mako rename from rhodecode/templates/changelog/changelog_details.mako rename to rhodecode/templates/changelog/changelog_elements.mako --- a/rhodecode/templates/changelog/changelog_details.mako +++ b/rhodecode/templates/changelog/changelog_elements.mako @@ -1,11 +1,122 @@ ## small box that displays changed/added/removed details fetched by AJAX +<%namespace name="base" file="/base/base.mako"/> + + +% if c.prev_page: + + + + ${_('load previous')} + + + +% endif + +% for cnt,commit in enumerate(c.pagination): + + + + ${h.checkbox(commit.raw_id,class_="commit-range")} + + -% if len(c.commit.affected_files) <= c.affected_files_cut_off: -${len(c.commit.removed)} -${len(c.commit.changed)} -${len(c.commit.added)} -% else: - ! - ! - ! + %if c.statuses.get(commit.raw_id): +
+ %if c.statuses.get(commit.raw_id)[2]: + +
+
+ %else: + +
+
+ %endif +
+ %else: +
+ %endif + + + %if c.comments.get(commit.raw_id): + + ${len(c.comments[commit.raw_id])} + + %endif + + + + + ${h.show_id(commit)} + + + + +
+   +
+ + +
+
${h.urlify_commit_message(commit.message, c.repo_name)}
+
+ + + + ${h.age_component(commit.date)} + + + ${base.gravatar_with_user(commit.author)} + + + +
+ + ## merge + %if commit.merge: + + ${_('merge')} + + %endif + + ## branch + %if commit.branch: + + ${h.shorter(commit.branch)} + + %endif + + ## bookmarks + %if h.is_hg(c.rhodecode_repo): + %for book in commit.bookmarks: + + ${h.shorter(book)} + + %endfor + %endif + + ## tags + %for tag in commit.tags: + + ${h.shorter(tag)} + + %endfor + +
+ + +% endfor + +% if c.next_page: + + + + ${_('load next')} + + + % endif + + \ No newline at end of file diff --git a/rhodecode/templates/changelog/changelog_file_history.mako b/rhodecode/templates/changelog/changelog_file_history.mako --- a/rhodecode/templates/changelog/changelog_file_history.mako +++ b/rhodecode/templates/changelog/changelog_file_history.mako @@ -14,12 +14,6 @@
- - %if cs.merge: - - ${_('merge')} - - %endif