From 5d98eb6ece8562d80147616cbee55caaa8263329 Mon Sep 17 00:00:00 2001 From: Toshi MARUYAMA Date: Thu, 3 Nov 2011 11:36:12 +0000 Subject: [PATCH] scm: git: mercurial: add a new feature of revision graph (#5501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contributed by Jan TopiƄski. git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@7725 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/repositories_helper.rb | 56 ++++++ .../repositories/_revision_graph.html.erb | 13 ++ app/views/repositories/_revisions.html.erb | 29 ++- public/javascripts/revision_graph.js | 172 ++++++++++++++++++ public/stylesheets/application.css | 3 + 5 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 app/views/repositories/_revision_graph.html.erb create mode 100644 public/javascripts/revision_graph.js diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb index 8954ac594..69f2e2558 100644 --- a/app/helpers/repositories_helper.rb +++ b/app/helpers/repositories_helper.rb @@ -283,4 +283,60 @@ module RepositoriesHelper ) + '
'.html_safe + l(:text_scm_path_encoding_note)) end + + def index_commits(commits, heads, href_proc = nil) + return nil if commits.nil? or commits.first.parents.nil? + map = {} + commit_hashes = [] + refs_map = {} + href_proc ||= Proc.new {|x|x} + heads.each{|r| refs_map[r.scmid] ||= []; refs_map[r.scmid] << r} + commits.reverse.each_with_index do |c, i| + h = {} + h[:parents] = c.parents.collect do |p| + [p.scmid, 0, 0] + end + h[:rdmid] = i + h[:space] = 0 + h[:refs] = refs_map[c.scmid].join(" ") if refs_map.include? c.scmid + h[:scmid] = c.scmid + h[:href] = href_proc.call(c.scmid) + commit_hashes << h + map[c.scmid] = h + end + heads.sort! do |a,b| + a.to_s <=> b.to_s + end + j = 0 + heads.each do |h| + if map.include? h.scmid then + j = mark_chain(j += 1, map[h.scmid], map) + end + end + # when no head matched anything use first commit + if j == 0 then + mark_chain(j += 1, map.values.first, map) + end + map + end + + def mark_chain(mark, commit, map) + stack = [[mark, commit]] + markmax = mark + until stack.empty? + current = stack.pop + m, commit = current + commit[:space] = m if commit[:space] == 0 + m1 = m - 1 + commit[:parents].each_with_index do |p, i| + psha = p[0] + if map.include? psha and map[psha][:space] == 0 then + stack << [m1 += 1, map[psha]] if i == 0 + stack = [[m1 += 1, map[psha]]] + stack if i > 0 + end + end + markmax = m1 if markmax < m1 + end + markmax + end end diff --git a/app/views/repositories/_revision_graph.html.erb b/app/views/repositories/_revision_graph.html.erb new file mode 100644 index 000000000..02e26de14 --- /dev/null +++ b/app/views/repositories/_revision_graph.html.erb @@ -0,0 +1,13 @@ +<%= javascript_include_tag "raphael.js" %> + +<%= javascript_include_tag "revision_graph.js" %> + + + +
diff --git a/app/views/repositories/_revisions.html.erb b/app/views/repositories/_revisions.html.erb index a78e00fda..2bc72f84d 100644 --- a/app/views/repositories/_revisions.html.erb +++ b/app/views/repositories/_revisions.html.erb @@ -1,6 +1,9 @@ <% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => to_path_param(path)}, :method => :get) do %> +<% if @repository.supports_revision_graph? %> + +<% end %> @@ -13,12 +16,36 @@ <% line_num = 1 %> <% revisions.each do |changeset| %> +<% if @repository.supports_revision_graph? %> + <% if line_num == 1 %> + + <% end %> +<% end %> - +<% if @repository.supports_revision_graph? %> + +<% else %> + +<% end %> <% line_num += 1 %> <% end %> diff --git a/public/javascripts/revision_graph.js b/public/javascripts/revision_graph.js new file mode 100644 index 000000000..26b59d530 --- /dev/null +++ b/public/javascripts/revision_graph.js @@ -0,0 +1,172 @@ +var commits = chunk.commits, + comms = {}, + pixelsX = [], + pixelsY = [], + mmax = Math.max, + max_rdmid = 0, + max_space = 0, + parents = {}; +for (var i = 0, ii = commits.length; i < ii; i++) { + for (var j = 0, jj = commits[i].parents.length; j < jj; j++) { + parents[commits[i].parents[j][0]] = true; + } + max_rdmid = Math.max(max_rdmid, commits[i].rdmid); + max_space = Math.max(max_space, commits[i].space); +} + +for (i = 0; i < ii; i++) { + if (commits[i].scmid in parents) { + commits[i].isParent = true; + } + comms[commits[i].scmid] = commits[i]; +} +var colors = ["#000"]; +for (var k = 0; k < max_space; k++) { + colors.push(Raphael.getColor()); +} + +function branchGraph(holder) { + var xstep = 20, ystep = 20; + var ch, cw; + cw = max_space * xstep + xstep; + ch = max_rdmid * ystep + ystep; + var r = Raphael("holder", cw, ch), + top = r.set(); + var cuday = 0, cumonth = ""; + + for (i = 0; i < ii; i++) { + var x, y; + y = 10 + ystep *(max_rdmid - commits[i].rdmid); + x = 3 + xstep * commits[i].space; + var stroke = "none"; + r.circle(x, y, 3).attr({fill: colors[commits[i].space], stroke: stroke}); + if (commits[i].refs != null && commits[i].refs != "") { + var longrefs = commits[i].refs + var shortrefs = commits[i].refs; + if (shortrefs.length > 15) { + shortrefs = shortrefs.substr(0,13) + "..."; + } + var t = r.text(x+5,y+5,shortrefs).attr({font: "12px Fontin-Sans, Arial", fill: "#666", + title: longrefs, cursor: "pointer", rotation: "0"}); + + var textbox = t.getBBox(); + t.translate(textbox.width / 2, textbox.height / -3); + } + for (var j = 0, jj = commits[i].parents.length; j < jj; j++) { + var c = comms[commits[i].parents[j][0]]; + var p,arrow; + if (c) { + var cy, cx; + cy = 10 + ystep * (max_rdmid - c.rdmid), + cx = 3 + xstep * c.space; + + if (c.space == commits[i].space) { + p = r.path("M" + x + "," + y + "L" + cx + "," + cy); + } else { + p = r.path(["M", x, y, "C",x,y,x, y+(cy-y)/2,x+(cx-x)/2, y+(cy-y)/2, + "C", x+(cx-x)/2,y+(cy-y)/2, cx, cy-(cy-y)/2, cx, cy]); + } + } else { + p = r.path("M" + x + "," + y + "L" + x + "," + ch); + } + p.attr({stroke: colors[commits[i].space], "stroke-width": 1.5}); + } + (function (c, x, y) { + top.push(r.circle(x, y, 10).attr({fill: "#000", opacity: 0, + cursor: "pointer", href: commits[i].href}) + .hover(function () {}, function () {}) + ); + }(commits[i], x, y)); + } + top.toFront(); + var hw = holder.offsetWidth, + hh = holder.offsetHeight, + drag, + dragger = function (e) { + if (drag) { + e = e || window.event; + holder.scrollLeft = drag.sl - (e.clientX - drag.x); + holder.scrollTop = drag.st - (e.clientY - drag.y); + } + }; + holder.onmousedown = function (e) { + e = e || window.event; + drag = {x: e.clientX, y: e.clientY, st: holder.scrollTop, sl: holder.scrollLeft}; + document.onmousemove = dragger; + }; + document.onmouseup = function () { + drag = false; + document.onmousemove = null; + }; + holder.scrollLeft = cw; +}; + +Raphael.fn.popupit = function (x, y, set, dir, size) { + dir = dir == null ? 2 : dir; + size = size || 5; + x = Math.round(x); + y = Math.round(y); + var bb = set.getBBox(), + w = Math.round(bb.width / 2), + h = Math.round(bb.height / 2), + dx = [0, w + size * 2, 0, -w - size * 2], + dy = [-h * 2 - size * 3, -h - size, 0, -h - size], + p = ["M", x - dx[dir], y - dy[dir], "l", -size, (dir == 2) * -size, -mmax(w - size, 0), + 0, "a", size, size, 0, 0, 1, -size, -size, + "l", 0, -mmax(h - size, 0), (dir == 3) * -size, -size, (dir == 3) * size, -size, 0, + -mmax(h - size, 0), "a", size, size, 0, 0, 1, size, -size, + "l", mmax(w - size, 0), 0, size, !dir * -size, size, !dir * size, mmax(w - size, 0), + 0, "a", size, size, 0, 0, 1, size, size, + "l", 0, mmax(h - size, 0), (dir == 1) * size, size, (dir == 1) * -size, size, 0, + mmax(h - size, 0), "a", size, size, 0, 0, 1, -size, size, + "l", -mmax(w - size, 0), 0, "z"].join(","), + xy = [{x: x, y: y + size * 2 + h}, + {x: x - size * 2 - w, y: y}, + {x: x, y: y - size * 2 - h}, + {x: x + size * 2 + w, y: y}] + [dir]; + set.translate(xy.x - w - bb.x, xy.y - h - bb.y); + return this.set(this.path(p).attr({fill: "#234", stroke: "none"}) + .insertBefore(set.node ? set : set[0]), set); +}; + +Raphael.fn.popup = function (x, y, text, dir, size) { + dir = dir == null ? 2 : dir > 3 ? 3 : dir; + size = size || 5; + text = text || "$9.99"; + var res = this.set(), + d = 3; + res.push(this.path().attr({fill: "#000", stroke: "#000"})); + res.push(this.text(x, y, text).attr(this.g.txtattr).attr({fill: "#fff", "font-family": "Helvetica, Arial"})); + res.update = function (X, Y, withAnimation) { + X = X || x; + Y = Y || y; + var bb = this[1].getBBox(), + w = bb.width / 2, + h = bb.height / 2, + dx = [0, w + size * 2, 0, -w - size * 2], + dy = [-h * 2 - size * 3, -h - size, 0, -h - size], + p = ["M", X - dx[dir], Y - dy[dir], "l", -size, (dir == 2) * -size, + -mmax(w - size, 0), 0, "a", size, size, 0, 0, 1, -size, -size, + "l", 0, -mmax(h - size, 0), (dir == 3) * -size, -size, (dir == 3) * size, -size, + 0, -mmax(h - size, 0), "a", size, size, 0, 0, 1, size, -size, + "l", mmax(w - size, 0), 0, size, !dir * -size, size, !dir * size, mmax(w - size, 0), + 0, "a", size, size, 0, 0, 1, size, size, + "l", 0, mmax(h - size, 0), (dir == 1) * size, size, (dir == 1) * -size, size, 0, + mmax(h - size, 0), "a", size, size, 0, 0, 1, -size, size, + "l", -mmax(w - size, 0), 0, "z"].join(","), + xy = [{x: X, y: Y + size * 2 + h}, + {x: X - size * 2 - w, y: Y}, + {x: X, y: Y - size * 2 - h}, + {x: X + size * 2 + w, y: Y}] + [dir]; + xy.path = p; + if (withAnimation) { + this.animate(xy, 500, ">"); + } else { + this.attr(xy); + } + return this; + }; + return res.update(x, y); +}; diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 44a67e477..9bafce440 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -155,9 +155,12 @@ tr.entry.file td.filename_no_report a { margin-left: 16px; } tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;} tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);} +tr.changeset { height: 20px } tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; } +tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; } tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;} tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;} +tr.changeset td.comments_nowrap { width: 45%; white-space:nowrap;} table.files tr.file td { text-align: center; } table.files tr.file td.filename { text-align: left; padding-left: 24px; }
#
+ <% href_base = Proc.new {|x| url_for(:controller => 'repositories', + :action => 'revision', + :id => project, + :rev => x) } %> + <%= render :partial => 'revision_graph', + :locals => { + :commits => index_commits( + revisions, + @repository.branches, + href_base + ) + } %> + <%= link_to_revision(changeset, project) %> <%= radio_button_tag('rev', changeset.identifier, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %> <%= radio_button_tag('rev_to', changeset.identifier, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %> <%= format_time(changeset.committed_on) %> <%= h truncate(changeset.author.to_s, :length => 30) %><%= textilizable(truncate_at_line_break(changeset.comments)) %> + <%= textilizable(truncate(truncate_at_line_break(changeset.comments, 0), :length => 90)) %> + <%= textilizable(truncate_at_line_break(changeset.comments)) %>