Commit 1c5b2a51 authored by randx's avatar randx

Stats page

parent ca54d43f
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
//= require modernizr //= require modernizr
//= require chosen-jquery //= require chosen-jquery
//= require raphael //= require raphael
//= require g.raphael-min
//= require g.bar-min
//= require branch-graph //= require branch-graph
//= require ace-src-noconflict/ace //= require ace-src-noconflict/ace
//= require_tree . //= require_tree .
...@@ -31,3 +31,11 @@ ul { ...@@ -31,3 +31,11 @@ ul {
} }
} }
} }
ol, ul {
&.styled {
li {
padding:2px;
}
}
}
...@@ -16,9 +16,14 @@ class RepositoriesController < ProjectResourceController ...@@ -16,9 +16,14 @@ class RepositoriesController < ProjectResourceController
@tags = @project.tags @tags = @project.tags
end end
def stats
@stats = Gitlab::GitStats.new(@project.repo, @project.root_ref)
@graph = @stats.graph
end
def archive def archive
unless can?(current_user, :download_code, @project) unless can?(current_user, :download_code, @project)
render_404 and return render_404 and return
end end
......
...@@ -16,6 +16,11 @@ ...@@ -16,6 +16,11 @@
Tags Tags
%span.badge= @project.tags.length %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 - if current_controller?(:commits) && current_user.private_token
%li.right %li.right
%span.rss-icon %span.rss-icon
......
= 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);
})
...@@ -128,6 +128,7 @@ Gitlab::Application.routes.draw do ...@@ -128,6 +128,7 @@ Gitlab::Application.routes.draw do
member do member do
get "branches" get "branches"
get "tags" get "tags"
get "stats"
get "archive" get "archive"
end end
end end
......
...@@ -156,30 +156,30 @@ ActiveRecord::Schema.define(:version => 20121026114600) do ...@@ -156,30 +156,30 @@ ActiveRecord::Schema.define(:version => 20121026114600) do
end end
create_table "users", :force => true do |t| create_table "users", :force => true do |t|
t.string "email", :default => "", :null => false t.string "email", :default => "", :null => false
t.string "encrypted_password", :limit => 128, :default => "", :null => false t.string "encrypted_password", :default => "", :null => false
t.string "reset_password_token" t.string "reset_password_token"
t.datetime "reset_password_sent_at" t.datetime "reset_password_sent_at"
t.datetime "remember_created_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 "current_sign_in_at"
t.datetime "last_sign_in_at" t.datetime "last_sign_in_at"
t.string "current_sign_in_ip" t.string "current_sign_in_ip"
t.string "last_sign_in_ip" t.string "last_sign_in_ip"
t.datetime "created_at", :null => false t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false t.datetime "updated_at", :null => false
t.string "name" t.string "name"
t.boolean "admin", :default => false, :null => false t.boolean "admin", :default => false, :null => false
t.integer "projects_limit", :default => 10 t.integer "projects_limit", :default => 10
t.string "skype", :default => "", :null => false t.string "skype", :default => "", :null => false
t.string "linkedin", :default => "", :null => false t.string "linkedin", :default => "", :null => false
t.string "twitter", :default => "", :null => false t.string "twitter", :default => "", :null => false
t.string "authentication_token" t.string "authentication_token"
t.boolean "dark_scheme", :default => false, :null => false t.boolean "dark_scheme", :default => false, :null => false
t.integer "theme_id", :default => 1, :null => false t.integer "theme_id", :default => 1, :null => false
t.string "bio" t.string "bio"
t.boolean "blocked", :default => false, :null => false t.boolean "blocked", :default => false, :null => false
t.integer "failed_attempts", :default => 0 t.integer "failed_attempts", :default => 0
t.datetime "locked_at" t.datetime "locked_at"
t.string "extern_uid" t.string "extern_uid"
t.string "provider" t.string "provider"
......
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
/*!
* 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<g){g=h;s=["M",o+0.5,m+0.5-~~(p/2),"l",0,0,"a",g,~~(p/2),0,0,1,0,p,"l",0,0,"z"]}else{s=["M",o+0.5,m+0.5-g,"l",h-g,0,"a",g,g,0,1,1,0,p,"l",g-h,0,"z"]}}else{g=~~(h/2);if(p<g){g=p;s=["M",o-~~(h/2),m,"l",0,0,"a",~~(h/2),g,0,0,1,h,0,"l",0,0,"z"]}else{s=["M",o-g,m,"l",0,g-p,"a",g,g,0,1,1,h,0,"l",0,p-g,"z"]}}break;case"sharp":if(!j){var q=~~(p/2);s=["M",o,m+q,"l",0,-p,a(h-q,0),0,f(q,h),q,-f(q,h),q+(q*2<p),"z"]}else{q=~~(h/2);s=["M",o+q,m,"l",-h,0,0,-a(p-q,0),q,-f(q,p),q,f(q,p),q,"z"]}break;case"square":if(!j){s=["M",o,m+~~(p/2),"l",0,-p,h,0,0,p,"z"]}else{s=["M",o+~~(h/2),m,"l",1-h,0,0,-p,h-1,0,"z"]}break;case"soft":if(!j){g=f(h,Math.round(p/5));s=["M",o+0.5,m+0.5-~~(p/2),"l",h-g,0,"a",g,g,0,0,1,g,g,"l",0,p-g*2,"a",g,g,0,0,1,-g,g,"l",g-h,0,"z"]}else{g=f(Math.round(h/5),p);s=["M",o-~~(h/2),m,"l",0,g-p,"a",g,g,0,0,1,g,-g,"l",h-2*g,0,"a",g,g,0,0,1,g,g,"l",0,p-g,"z"]}}if(l){return s.join(",")}else{return i.path(s)}}function d(l,J,H,g,m,V,D){D=D||{};var z=this,W=D.type||"square",u=parseFloat(D.gutter||"20%"),T=l.set(),E=l.set(),n=l.set(),B=l.set(),F=Math.max.apply(Math,V),U=[],I=0,M=D.colors||z.colors,A=V.length;if(Raphael.is(V[0],"array")){F=[];I=A;A=0;for(var R=V.length;R--;){E.push(l.set());F.push(Math.max.apply(Math,V[R]));A=Math.max(A,V[R].length)}if(D.stacked){for(var R=A;R--;){var r=0;for(var Q=V.length;Q--;){r+=+V[Q][R]||0}U.push(r)}}for(var R=V.length;R--;){if(V[R].length<A){for(var Q=A;Q--;){V[R].push(0)}}}F=Math.max.apply(Math,D.stacked?U:F)}F=(D.to)||F;var K=g/(A*(100+u)+u)*100,k=K*u/100,p=D.vgutter==null?20:D.vgutter,C=[],q=J+k,o=(m-2*p)/F;if(!D.stretch){k=Math.round(k);K=Math.floor(K)}!D.stacked&&(K/=I||1);for(var R=0;R<A;R++){C=[];for(var Q=0;Q<(I||1);Q++){var S=Math.round((I?V[Q][R]:V[R])*o),t=H+m-p-S,O=e(Math.round(q+K/2),t+S,K,S,true,W,null,l).attr({stroke:"none",fill:M[I?Q:R]});if(I){E[Q].push(O)}else{E.push(O)}O.y=t;O.x=Math.round(q+K/2);O.w=K;O.h=S;O.value=I?V[Q][R]:V[R];if(!D.stacked){q+=K}else{C.push(O)}}if(D.stacked){var P;B.push(P=l.rect(C[0].x-C[0].w/2,H,K,m).attr(z.shim));P.bars=l.set();var v=0;for(var L=C.length;L--;){C[L].toFront()}for(var L=0,w=C.length;L<w;L++){var O=C[L],G,S=(v+O.value)*o,N=e(O.x,H+m-p-!!v*0.5,K,S,true,W,1,l);P.bars.push(O);v&&O.attr({path:N});O.h=S;O.y=H+m-p-!!v*0.5-S;n.push(G=l.rect(O.x-O.w/2,O.y,K,O.value*o).attr(z.shim));G.bar=O;G.value=O.value;v+=O.value}q+=K}q+=k}B.toFront();q=J+k;if(!D.stacked){for(var R=0;R<A;R++){for(var Q=0;Q<(I||1);Q++){var G;n.push(G=l.rect(Math.round(q),H+p,K,m-p).attr(z.shim));G.bar=I?E[Q][R]:E[R];G.value=G.bar.value;q+=K}q+=k}}T.label=function(y,Z){y=y||[];this.labels=l.set();var aa,h=-Infinity;if(D.stacked){for(var x=0;x<A;x++){var X=0;for(var s=0;s<(I||1);s++){X+=I?V[s][x]:V[x];if(s==I-1){var ab=z.labelise(y[x],X,F);aa=l.text(E[s][x].x,H+m-p/2,ab).attr(z.txtattr).attr({fill:D.legendcolor||"#000","text-anchor":"start"}).insertBefore(n[x*(I||1)+s]);var Y=aa.getBBox();if(Y.x-7<h){aa.remove()}else{this.labels.push(aa);h=Y.x+Y.width}}}}}else{for(var x=0;x<A;x++){for(var s=0;s<(I||1);s++){var ab=z.labelise(I?y[s]&&y[s][x]:y[x],I?V[s][x]:V[x],F);aa=l.text(E[s][x].x-k/2,Z?H+m-p/2:E[s][x].y-10,ab).attr(z.txtattr).attr({fill:D.legendcolor||"#000","text-anchor":"start"}).insertBefore(n[x*(I||1)+s]);var Y=aa.getBBox();if(Y.x-7<h){aa.remove()}else{this.labels.push(aa);h=Y.x+Y.width}}}}return this};T.hover=function(i,h){B.hide();n.show();n.mouseover(i).mouseout(h);return this};T.hoverColumn=function(i,h){n.hide();B.show();h=h||function(){};B.mouseover(i).mouseout(h);return this};T.click=function(h){B.hide();n.show();n.click(h);return this};T.each=function(j){if(!Raphael.is(j,"function")){return this}for(var h=n.length;h--;){j.call(n[h])}return this};T.eachColumn=function(j){if(!Raphael.is(j,"function")){return this}for(var h=B.length;h--;){j.call(B[h])}return this};T.clickColumn=function(h){n.hide();B.show();B.click(h);return this};T.push(E,n,B);T.bars=E;T.covers=n;return T}function b(w,v,u,I,F,l,B){B=B||{};var h=this,n=B.type||"square",o=parseFloat(B.gutter||"20%"),D=w.set(),H=w.set(),q=w.set(),L=w.set(),T=Math.max.apply(Math,l),g=[],J=0,t=B.colors||h.colors,O=l.length;if(Raphael.is(l[0],"array")){T=[];J=O;O=0;for(var N=l.length;N--;){H.push(w.set());T.push(Math.max.apply(Math,l[N]));O=Math.max(O,l[N].length)}if(B.stacked){for(var N=O;N--;){var z=0;for(var M=l.length;M--;){z+=+l[M][N]||0}g.push(z)}}for(var N=l.length;N--;){if(l[N].length<O){for(var M=O;M--;){l[N].push(0)}}}T=Math.max.apply(Math,B.stacked?g:T)}T=(B.to)||T;var Q=Math.floor(F/(O*(100+o)+o)*100),r=Math.floor(Q*o/100),p=[],k=u+r,m=(I-1)/T;!B.stacked&&(Q/=J||1);for(var N=0;N<O;N++){p=[];for(var M=0;M<(J||1);M++){var S=J?l[M][N]:l[N],P=e(v,k+Q/2,Math.round(S*m),Q-1,false,n,null,w).attr({stroke:"none",fill:t[J?M:N]});if(J){H[M].push(P)}else{H.push(P)}P.x=v+Math.round(S*m);P.y=k+Q/2;P.w=Math.round(S*m);P.h=Q;P.value=+S;if(!B.stacked){k+=Q}else{p.push(P)}}if(B.stacked){var A=w.rect(v,p[0].y-p[0].h/2,I,Q).attr(h.shim);L.push(A);A.bars=w.set();var E=0;for(var C=p.length;C--;){p[C].toFront()}for(var C=0,K=p.length;C<K;C++){var P=p[C],R,S=Math.round((E+P.value)*m),G=e(v,P.y,S,Q-1,false,n,1,w);A.bars.push(P);E&&P.attr({path:G});P.w=S;P.x=v+S;q.push(R=w.rect(v+E*m,P.y-P.h/2,P.value*m,Q).attr(h.shim));R.bar=P;E+=P.value}k+=Q}k+=r}L.toFront();k=u+r;if(!B.stacked){for(var N=0;N<O;N++){for(var M=0;M<(J||1);M++){var R=w.rect(v,k,I,Q).attr(h.shim);q.push(R);R.bar=J?H[M][N]:H[N];R.value=R.bar.value;k+=Q}k+=r}}D.label=function(Z,W){Z=Z||[];this.labels=w.set();for(var V=0;V<O;V++){for(var U=0;U<J;U++){var y=h.labelise(J?Z[U]&&Z[U][V]:Z[V],J?l[U][V]:l[V],T),Y=W?H[U][V].x-Q/2+3:v+5,x=W?"end":"start",s;this.labels.push(s=w.text(Y,H[U][V].y,y).attr(h.txtattr).attr({fill:B.legendcolor||"#000","text-anchor":"start"}).insertBefore(q[0]));if(s.getBBox().x<v+5){s.attr({x:v+5,"text-anchor":"start"})}else{H[U][V].label=s}}}return this};D.hover=function(j,i){L.hide();q.show();i=i||function(){};q.mouseover(j).mouseout(i);return this};D.hoverColumn=function(j,i){q.hide();L.show();i=i||function(){};L.mouseover(j).mouseout(i);return this};D.each=function(s){if(!Raphael.is(s,"function")){return this}for(var j=q.length;j--;){s.call(q[j])}return this};D.eachColumn=function(s){if(!Raphael.is(s,"function")){return this}for(var j=L.length;j--;){s.call(L[j])}return this};D.click=function(i){L.hide();q.show();q.click(i);return this};D.clickColumn=function(i){q.hide();L.show();L.click(i);return this};D.push(H,q,L);D.bars=H;D.covers=q;return D}var c=function(){};c.prototype=Raphael.g;b.prototype=d.prototype=new c;Raphael.fn.hbarchart=function(h,l,j,g,i,k){return new b(this,h,l,j,g,i,k)};Raphael.fn.barchart=function(h,l,j,g,i,k){return new d(this,h,l,j,g,i,k)}})();
\ No newline at end of file
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment