]> source.dussan.org Git - redmine.git/commitdiff
scm: git: mercurial: add a new feature of revision graph (#5501)
authorToshi MARUYAMA <marutosijp2@yahoo.co.jp>
Thu, 3 Nov 2011 11:36:12 +0000 (11:36 +0000)
committerToshi MARUYAMA <marutosijp2@yahoo.co.jp>
Thu, 3 Nov 2011 11:36:12 +0000 (11:36 +0000)
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
app/views/repositories/_revision_graph.html.erb [new file with mode: 0644]
app/views/repositories/_revisions.html.erb
public/javascripts/revision_graph.js [new file with mode: 0644]
public/stylesheets/application.css

index 8954ac5947c0774d9d6f3c5da5201bf78c47df2a..69f2e2558617aee469d3fe9810309e35cc26768a 100644 (file)
@@ -283,4 +283,60 @@ module RepositoriesHelper
                         ) +
                      '<br />'.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 (file)
index 0000000..02e26de
--- /dev/null
@@ -0,0 +1,13 @@
+<%= javascript_include_tag "raphael.js" %>
+<script type="text/javascript" charset="utf-8">
+  var chunk = {commits:<%= commits.values.to_json %>}
+</script>
+<%= javascript_include_tag "revision_graph.js" %>
+
+<script type="text/javascript">
+  window.onload = function(){
+    branchGraph(document.getElementById("holder"));
+  }
+</script>
+
+<div id="holder" class="graph"></div>
index a78e00fda0169f25a32e5f881ce00b9259446f07..2bc72f84d4984388513fa5376998b97b741406db 100644 (file)
@@ -1,6 +1,9 @@
 <% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => to_path_param(path)}, :method => :get) do %>
 <table class="list changesets">
 <thead><tr>
+<% if @repository.supports_revision_graph? %>
+<th></th>
+<% end %>
 <th>#</th>
 <th></th>
 <th></th>
 <% line_num = 1 %>
 <% revisions.each do |changeset| %>
 <tr class="changeset <%= cycle 'odd', 'even' %>">
+<% if @repository.supports_revision_graph? %>
+  <% if line_num == 1 %>
+    <td class="revision_graph" rowspan="<%= revisions.size %>">
+      <% 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
+                                            )
+                    } %>
+    </td>
+  <% end %>
+<% end %>
 <td class="id"><%= link_to_revision(changeset, project) %></td>
 <td class="checkbox"><%= 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) %></td>
 <td class="checkbox"><%= 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) %></td>
 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
 <td class="author"><%= h truncate(changeset.author.to_s, :length => 30) %></td>
-<td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
+<% if @repository.supports_revision_graph? %>
+  <td class="comments_nowrap">
+    <%= textilizable(truncate(truncate_at_line_break(changeset.comments, 0), :length => 90)) %>
+  </td>
+<% else %>
+  <td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
+<% end %>
 </tr>
 <% line_num += 1 %>
 <% end %>
diff --git a/public/javascripts/revision_graph.js b/public/javascripts/revision_graph.js
new file mode 100644 (file)
index 0000000..26b59d5
--- /dev/null
@@ -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);
+};
index 44a67e47718e57d4d702c41f911a853d17545fd4..9bafce440738e1f556954866998e957b0c84ac85 100644 (file)
@@ -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; }