From 1c5b2a5153abee3a140ce1f2926d746675addcf5 Mon Sep 17 00:00:00 2001 From: randx Date: Sat, 10 Nov 2012 23:08:47 +0200 Subject: [PATCH] Stats page --- app/assets/javascripts/application.js | 2 + .../stylesheets/gitlab_bootstrap/lists.scss | 8 ++ app/controllers/repositories_controller.rb | 7 +- app/views/commits/_head.html.haml | 5 ++ app/views/repositories/stats.html.haml | 39 ++++++++++ config/routes.rb | 1 + db/schema.rb | 28 +++---- lib/gitlab/git_stats.rb | 76 +++++++++++++++++++ vendor/assets/javascripts/g.bar-min.js | 7 ++ vendor/assets/javascripts/g.raphael-min.js | 28 +++++++ 10 files changed, 186 insertions(+), 15 deletions(-) create mode 100644 app/views/repositories/stats.html.haml create mode 100644 lib/gitlab/git_stats.rb create mode 100644 vendor/assets/javascripts/g.bar-min.js create mode 100644 vendor/assets/javascripts/g.raphael-min.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index d88f2a66..c4e9a1b0 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -17,6 +17,8 @@ //= require modernizr //= require chosen-jquery //= require raphael +//= require g.raphael-min +//= require g.bar-min //= require branch-graph //= require ace-src-noconflict/ace //= require_tree . diff --git a/app/assets/stylesheets/gitlab_bootstrap/lists.scss b/app/assets/stylesheets/gitlab_bootstrap/lists.scss index a5d6bd0a..96bdfc91 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/lists.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/lists.scss @@ -31,3 +31,11 @@ ul { } } } + +ol, ul { + &.styled { + li { + padding:2px; + } + } +} diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 18b240e4..7678fbff 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -16,9 +16,14 @@ class RepositoriesController < ProjectResourceController @tags = @project.tags end + def stats + @stats = Gitlab::GitStats.new(@project.repo, @project.root_ref) + @graph = @stats.graph + end + def archive unless can?(current_user, :download_code, @project) - render_404 and return + render_404 and return end diff --git a/app/views/commits/_head.html.haml b/app/views/commits/_head.html.haml index c001c2f7..2ec1d24b 100644 --- a/app/views/commits/_head.html.haml +++ b/app/views/commits/_head.html.haml @@ -16,6 +16,11 @@ Tags %span.badge= @project.tags.length + = nav_link(controller: :repositories, action: :stats) do + = link_to stats_project_repository_path(@project) do + Stats + + - if current_controller?(:commits) && current_user.private_token %li.right %span.rss-icon diff --git a/app/views/repositories/stats.html.haml b/app/views/repositories/stats.html.haml new file mode 100644 index 00000000..bf68dc28 --- /dev/null +++ b/app/views/repositories/stats.html.haml @@ -0,0 +1,39 @@ += render "commits/head" +.row + .span5 + %h4 + Stats for #{@project.root_ref}: + %p + %b Total commits: + %span= @stats.commits_count + %p + %b Total files: + %span= @stats.files_count + %p + %b Authors: + %span= @stats.authors_count + + %br + %div#activity-chart + .span7 + %h4 Top 100 Committers: + %ol.styled + - @stats.authors[0...100].each do |author| + %li + = image_tag gravatar_icon(author.email, 16), class: 'avatar s16' + = author.name + %small.light= author.email + .right + = author.commits + + +:javascript + $(function(){ + var labels = [#{@graph.labels.to_json}]; + var r = Raphael('activity-chart'); + r.text(160, 10, "Commit activity for last #{@graph.weeks} weeks").attr({ font: "13px sans-serif" }); + r.barchart( + 10, 10, 400, 160, + [[#{@graph.commits.join(', ')}]] + ).label(labels, true); + }) diff --git a/config/routes.rb b/config/routes.rb index e597c61e..bf762865 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -128,6 +128,7 @@ Gitlab::Application.routes.draw do member do get "branches" get "tags" + get "stats" get "archive" end end diff --git a/db/schema.rb b/db/schema.rb index 51ab2072..e7eb5696 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -156,30 +156,30 @@ ActiveRecord::Schema.define(:version => 20121026114600) do end create_table "users", :force => true do |t| - t.string "email", :default => "", :null => false - t.string "encrypted_password", :limit => 128, :default => "", :null => false + t.string "email", :default => "", :null => false + t.string "encrypted_password", :default => "", :null => false t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", :default => 0 + t.integer "sign_in_count", :default => 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" t.string "current_sign_in_ip" t.string "last_sign_in_ip" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.string "name" - t.boolean "admin", :default => false, :null => false - t.integer "projects_limit", :default => 10 - t.string "skype", :default => "", :null => false - t.string "linkedin", :default => "", :null => false - t.string "twitter", :default => "", :null => false + t.boolean "admin", :default => false, :null => false + t.integer "projects_limit", :default => 10 + t.string "skype", :default => "", :null => false + t.string "linkedin", :default => "", :null => false + t.string "twitter", :default => "", :null => false t.string "authentication_token" - t.boolean "dark_scheme", :default => false, :null => false - t.integer "theme_id", :default => 1, :null => false + t.boolean "dark_scheme", :default => false, :null => false + t.integer "theme_id", :default => 1, :null => false t.string "bio" - t.boolean "blocked", :default => false, :null => false - t.integer "failed_attempts", :default => 0 + t.boolean "blocked", :default => false, :null => false + t.integer "failed_attempts", :default => 0 t.datetime "locked_at" t.string "extern_uid" t.string "provider" diff --git a/lib/gitlab/git_stats.rb b/lib/gitlab/git_stats.rb new file mode 100644 index 00000000..a83c10ff --- /dev/null +++ b/lib/gitlab/git_stats.rb @@ -0,0 +1,76 @@ +module Gitlab + class GitStats + attr_accessor :repo, :ref + + def initialize repo, ref + @repo, @ref = repo, ref + end + + def authors + @authors ||= collect_authors + end + + def commits_count + @commits_count ||= repo.commit_count(ref) + end + + def files_count + repo.git.sh("git ls-tree -r --name-only #{ref} | wc -l").first.to_i + end + + def authors_count + authors.size + end + + def graph + @graph ||= build_graph + end + + protected + + def collect_authors + shortlog = repo.git.shortlog({:e => true, :s => true }, ref) + + authors = [] + + lines = shortlog.split("\n") + + lines.each do |line| + data = line.split("\t") + commits = data.first + author = Grit::Actor.from_string(data.last) + + authors << OpenStruct.new( + name: author.name, + email: author.email, + commits: commits.to_i + ) + end + + authors.sort_by(&:commits).reverse + end + + def build_graph n = 4 + from, to = (Date.today - n.weeks), Date.today + + format = "--pretty=format:'%h|%at|%ai|%aE'" + commits_strings = repo.git.sh("git rev-list --since #{from.to_s(:date)} #{format} #{ref} | grep -v commit")[0].split("\n") + + commits_dates = commits_strings.map do |string| + data = string.split("|") + date = data[2] + Time.parse(date).to_date.to_s(:date) + end + + commits_per_day = from.upto(to).map do |day| + commits_dates.count(day.to_date.to_s(:date)) + end + + OpenStruct.new( + labels: from.upto(to).map { |day| day.stamp('Aug 23') }, + commits: commits_per_day, + weeks: n + ) + end + end +end diff --git a/vendor/assets/javascripts/g.bar-min.js b/vendor/assets/javascripts/g.bar-min.js new file mode 100644 index 00000000..42f452af --- /dev/null +++ b/vendor/assets/javascripts/g.bar-min.js @@ -0,0 +1,7 @@ +/*! + * g.Raphael 0.5 - Charting library, based on Raphaël + * + * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com) + * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. + */ +(function(){var f=Math.min,a=Math.max;function e(o,m,h,p,j,k,l,i){var s,n={round:"round",sharp:"sharp",soft:"soft",square:"square"};if((j&&!p)||(!j&&!h)){return l?"":i.path()}k=n[k]||"square";p=Math.round(p);h=Math.round(h);o=Math.round(o);m=Math.round(m);switch(k){case"round":if(!j){var g=~~(p/2);if(h=2*a?e.attr({path:["M",b,f+a,"a",a,a,0,1,1,0,2*-a,a,a,0,1,1,0,2*a,"m",0,2*-a-3,"a",a+3,a+3,0,1,0,0,2*(a+3),"L",b+a+3,f+c.height/2+3,"l",c.width+6,0, +0,-c.height-6,-c.width-6,0,"L",b,f-a-3].join()}):(g=Math.sqrt(Math.pow(a+3,2)-Math.pow(c.height/2+3,2)),e.attr({path:["M",b,f+a,"c",-i,0,-a,i-a,-a,-a,0,-i,a-i,-a,a,-a,i,0,a,a-i,a,a,0,i,i-a,a,-a,a,"M",b+g,f-c.height/2-3,"a",a+3,a+3,0,1,0,0,c.height+6,"l",a+3-g+c.width+6,0,0,-c.height-6,"L",b+g,f-c.height/2-3].join()}));d=360-d;e.rotate(d,b,f);this.attrs?(this.attr(this.attrs.x?"x":"cx",b+a+3+(!h?"text"==this.type?c.width:0:c.width/2)).attr("y",h?f:f-c.height/2),this.rotate(d,b,f),90d&&this.attr(this.attrs.x? +"x":"cx",b-a-3-(!h?c.width:c.width/2)).rotate(180,b,f)):90d?(this.translate(b-c.x-c.width-a-3,f-c.y-c.height/2),this.rotate(d-180,c.x+c.width+a+3,c.y+c.height/2)):(this.translate(b-c.x+a+3,f-c.y-c.height/2),this.rotate(d,c.x-a-3,c.y+c.height/2));return e.insertBefore(this.node?this:this[0])}}; +Raphael.el.drop=function(d,a,b){var f=this.getBBox(),e=this.paper||this[0].paper,c,g;if(e){switch(this.type){case "text":case "circle":case "ellipse":c=!0;break;default:c=!1}d=d||0;a="number"==typeof a?a:c?f.x+f.width/2:f.x;b="number"==typeof b?b:c?f.y+f.height/2:f.y;g=Math.max(f.width,f.height)+Math.min(f.width,f.height);e=e.path(["M",a,b,"l",g,0,"A",0.4*g,0.4*g,0,1,0,a+0.7*g,b-0.7*g,"z"]).attr({fill:"#000",stroke:"none"}).rotate(22.5-d,a,b);d=(d+90)*Math.PI/180;a=a+g*Math.sin(d)-(c?0:f.width/2); +d=b+g*Math.cos(d)-(c?0:f.height/2);this.attrs?this.attr(this.attrs.x?"x":"cx",a).attr(this.attrs.y?"y":"cy",d):this.translate(a-f.x,d-f.y);return e.insertBefore(this.node?this:this[0])}}; +Raphael.el.flag=function(d,a,b){var f=this.paper||this[0].paper;if(f){var f=f.path().attr({fill:"#000",stroke:"#000"}),e=this.getBBox(),c=e.height/2,g;switch(this.type){case "text":case "circle":case "ellipse":g=!0;break;default:g=!1}d=d||0;a="number"==typeof a?a:g?e.x+e.width/2:e.x;b="number"==typeof b?b:g?e.y+e.height/2:e.y;f.attr({path:["M",a,b,"l",c+3,-c-3,e.width+6,0,0,e.height+6,-e.width-6,0,"z"].join()});d=360-d;f.rotate(d,a,b);this.attrs?(this.attr(this.attrs.x?"x":"cx",a+c+3+(!g?"text"== +this.type?e.width:0:e.width/2)).attr("y",g?b:b-e.height/2),this.rotate(d,a,b),90d&&this.attr(this.attrs.x?"x":"cx",a-c-3-(!g?e.width:e.width/2)).rotate(180,a,b)):90d?(this.translate(a-e.x-e.width-c-3,b-e.y-e.height/2),this.rotate(d-180,e.x+e.width+c+3,e.y+e.height/2)):(this.translate(a-e.x+c+3,b-e.y-e.height/2),this.rotate(d,e.x-c-3,e.y+e.height/2));return f.insertBefore(this.node?this:this[0])}}; +Raphael.el.label=function(){var d=this.getBBox(),a=this.paper||this[0].paper,b=Math.min(20,d.width+10,d.height+10)/2;if(a)return a.rect(d.x-b/2,d.y-b/2,d.width+b,d.height+b,b).attr({stroke:"none",fill:"#000"}).insertBefore(this.node?this:this[0])}; +Raphael.el.blob=function(d,a,b){var f=this.getBBox(),e=Math.PI/180,c=this.paper||this[0].paper,g,i;if(c){switch(this.type){case "text":case "circle":case "ellipse":g=!0;break;default:g=!1}c=c.path().attr({fill:"#000",stroke:"none"});d=(+d+1?d:45)+90;i=Math.min(f.height,f.width);var a="number"==typeof a?a:g?f.x+f.width/2:f.x,b="number"==typeof b?b:g?f.y+f.height/2:f.y,h=Math.max(f.width+i,25*i/12),j=Math.max(f.height+i,25*i/12);g=a+i*Math.sin((d-22.5)*e);var o=b+i*Math.cos((d-22.5)*e),l=a+i*Math.sin((d+ +22.5)*e),d=b+i*Math.cos((d+22.5)*e),e=(l-g)/2;i=(d-o)/2;var h=h/2,j=j/2,n=-Math.sqrt(Math.abs(h*h*j*j-h*h*i*i-j*j*e*e)/(h*h*i*i+j*j*e*e));i=n*h*i/j+(l+g)/2;e=n*-j*e/h+(d+o)/2;c.attr({x:i,y:e,path:["M",a,b,"L",l,d,"A",h,j,0,1,1,g,o,"z"].join()});this.translate(i-f.x-f.width/2,e-f.y-f.height/2);return c.insertBefore(this.node?this:this[0])}};Raphael.fn.label=function(d,a,b){var f=this.set(),b=this.text(d,a,b).attr(Raphael.g.txtattr);return f.push(b.label(),b)}; +Raphael.fn.popup=function(d,a,b,f,e){var c=this.set(),b=this.text(d,a,b).attr(Raphael.g.txtattr);return c.push(b.popup(f,e),b)};Raphael.fn.tag=function(d,a,b,f,e){var c=this.set(),b=this.text(d,a,b).attr(Raphael.g.txtattr);return c.push(b.tag(f,e),b)};Raphael.fn.flag=function(d,a,b,f){var e=this.set(),b=this.text(d,a,b).attr(Raphael.g.txtattr);return e.push(b.flag(f),b)};Raphael.fn.drop=function(d,a,b,f){var e=this.set(),b=this.text(d,a,b).attr(Raphael.g.txtattr);return e.push(b.drop(f),b)}; +Raphael.fn.blob=function(d,a,b,f){var e=this.set(),b=this.text(d,a,b).attr(Raphael.g.txtattr);return e.push(b.blob(f),b)};Raphael.el.lighter=function(d){var d=d||2,a=[this.attrs.fill,this.attrs.stroke];this.fs=this.fs||[a[0],a[1]];a[0]=Raphael.rgb2hsb(Raphael.getRGB(a[0]).hex);a[1]=Raphael.rgb2hsb(Raphael.getRGB(a[1]).hex);a[0].b=Math.min(a[0].b*d,1);a[0].s/=d;a[1].b=Math.min(a[1].b*d,1);a[1].s/=d;this.attr({fill:"hsb("+[a[0].h,a[0].s,a[0].b]+")",stroke:"hsb("+[a[1].h,a[1].s,a[1].b]+")"});return this}; +Raphael.el.darker=function(d){var d=d||2,a=[this.attrs.fill,this.attrs.stroke];this.fs=this.fs||[a[0],a[1]];a[0]=Raphael.rgb2hsb(Raphael.getRGB(a[0]).hex);a[1]=Raphael.rgb2hsb(Raphael.getRGB(a[1]).hex);a[0].s=Math.min(a[0].s*d,1);a[0].b/=d;a[1].s=Math.min(a[1].s*d,1);a[1].b/=d;this.attr({fill:"hsb("+[a[0].h,a[0].s,a[0].b]+")",stroke:"hsb("+[a[1].h,a[1].s,a[1].b]+")"});return this};Raphael.el.resetBrightness=function(){this.fs&&(this.attr({fill:this.fs[0],stroke:this.fs[1]}),delete this.fs);return this}; +(function(){var d=["lighter","darker","resetBrightness"],a="popup tag flag label drop blob".split(" "),b;for(b in a)(function(a){Raphael.st[a]=function(){return Raphael.el[a].apply(this,arguments)}})(a[b]);for(b in d)(function(a){Raphael.st[a]=function(){for(var b=0;bb;b++)bMath.abs(a-0.5)?~~a+0.5:Math.round(a)}var e=d,c=a;if(e==c)return{from:e,to:c,power:0};var e=(c-e)/b,g=c=~~e,b=0;if(c){for(;g;)b--,g=~~(e*Math.pow(10,b))/Math.pow(10,b); +b++}else{if(0==e||!isFinite(e))b=1;else for(;!c;)b=b||1,c=~~(e*Math.pow(10,b))/Math.pow(10,b),b++;b&&b--}c=f(a*Math.pow(10,b))/Math.pow(10,b);c=a-b;)"-"!=h&&" "!=h&&(l=l.concat(["M",d-("+"==h||"|"==h?j:2*!(g-1)*j),m+0.5,"l",2*j+1,0])),n.push(o.text(d+p,m,i&&i[u++]||(Math.round(k)==k?k:+k.toFixed(r))).attr(v).attr({"text-anchor":g-1?"start":"end"})),k+=t,m-=s;Math.round(m+s-(a-b))&&("-"!=h&&" "!=h&&(l=l.concat(["M",d-("+"==h||"|"==h?j:2*!(g-1)*j),a-b+0.5,"l",2*j+1,0])),n.push(o.text(d+ +p,a-b,i&&i[u]||(Math.round(k)==k?k:+k.toFixed(r))).attr(v).attr({"text-anchor":g-1?"start":"end"})))}else{for(var k=p,r=(0=q.x-5?n.pop(n.length-1).remove():w=q.x+q.width,k+=t,m+=s;Math.round(m-s-d-b)&&("-"!=h&&" "!=h&&(l=l.concat(["M",d+b+0.5,a-("+"==h?j:2*!!g*j),"l",0,2*j+1])),n.push(o.text(d+ +b,a+p,i&&i[u]||(Math.round(k)==k?k:+k.toFixed(r))).attr(v)))}l=o.path(l);l.text=n;l.all=o.set([l,n]);l.remove=function(){this.text.remove();this.constructor.prototype.remove.call(this)};return l},labelise:function(d,a,b){return d?(d+"").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g,function(d,e,c){if(e)return(+a).toFixed(e.replace(/^#+\.?/g,"").length);if(c)return(100*a/b).toFixed(c.replace(/^%+\.?/g,"").length)+"%"}):(+a).toFixed(0)}}; \ No newline at end of file