Commit aa0808386da50db098a189218041fc4f04a8a07e

Authored by Dmitriy Zaporozhets
2 parents b9d989dc 71d67e65

Merge pull request #4212 from karlo57/feature/graphs

Contributors Graph
@@ -107,6 +107,12 @@ gem 'tinder', '~> 1.9.2' @@ -107,6 +107,12 @@ gem 'tinder', '~> 1.9.2'
107 # HipChat integration 107 # HipChat integration
108 gem "hipchat", "~> 0.9.0" 108 gem "hipchat", "~> 0.9.0"
109 109
  110 +# d3
  111 +gem "d3_rails", "~> 3.1.4"
  112 +
  113 +# underscore-rails
  114 +gem "underscore-rails", "~> 1.4.4"
  115 +
110 group :assets do 116 group :assets do
111 gem "sass-rails" 117 gem "sass-rails"
112 gem "coffee-rails" 118 gem "coffee-rails"
@@ -177,6 +183,7 @@ group :development, :test do @@ -177,6 +183,7 @@ group :development, :test do
177 gem 'poltergeist', '~> 1.3.0' 183 gem 'poltergeist', '~> 1.3.0'
178 184
179 gem 'spork', '~> 1.0rc' 185 gem 'spork', '~> 1.0rc'
  186 + gem 'jasmine'
180 end 187 end
181 188
182 group :test do 189 group :test do
@@ -69,6 +69,8 @@ GEM @@ -69,6 +69,8 @@ GEM
69 celluloid (0.14.0) 69 celluloid (0.14.0)
70 timers (>= 1.0.0) 70 timers (>= 1.0.0)
71 charlock_holmes (0.6.9.4) 71 charlock_holmes (0.6.9.4)
  72 + childprocess (0.3.9)
  73 + ffi (~> 1.0, >= 1.0.11)
72 chosen-rails (0.9.8) 74 chosen-rails (0.9.8)
73 railties (~> 3.0) 75 railties (~> 3.0)
74 thor (~> 0.14) 76 thor (~> 0.14)
@@ -92,6 +94,8 @@ GEM @@ -92,6 +94,8 @@ GEM
92 simplecov (>= 0.7) 94 simplecov (>= 0.7)
93 thor 95 thor
94 crack (0.3.2) 96 crack (0.3.2)
  97 + d3_rails (3.1.4)
  98 + railties (>= 3.1.0)
95 daemons (1.1.9) 99 daemons (1.1.9)
96 database_cleaner (1.0.1) 100 database_cleaner (1.0.1)
97 debug_inspector (0.0.2) 101 debug_inspector (0.0.2)
@@ -216,6 +220,12 @@ GEM @@ -216,6 +220,12 @@ GEM
216 multi_xml (>= 0.5.2) 220 multi_xml (>= 0.5.2)
217 httpauth (0.2.0) 221 httpauth (0.2.0)
218 i18n (0.6.1) 222 i18n (0.6.1)
  223 + jasmine (1.3.2)
  224 + jasmine-core (~> 1.3.1)
  225 + rack (~> 1.0)
  226 + rspec (>= 1.3.1)
  227 + selenium-webdriver (>= 0.1.3)
  228 + jasmine-core (1.3.1)
219 journey (1.0.4) 229 journey (1.0.4)
220 jquery-atwho-rails (0.3.0) 230 jquery-atwho-rails (0.3.0)
221 jquery-rails (2.1.3) 231 jquery-rails (2.1.3)
@@ -392,6 +402,7 @@ GEM @@ -392,6 +402,7 @@ GEM
392 rspec-mocks (~> 2.13.0) 402 rspec-mocks (~> 2.13.0)
393 ruby-progressbar (1.0.2) 403 ruby-progressbar (1.0.2)
394 rubyntlm (0.1.1) 404 rubyntlm (0.1.1)
  405 + rubyzip (0.9.9)
395 sanitize (2.0.3) 406 sanitize (2.0.3)
396 nokogiri (>= 1.4.4, < 1.6) 407 nokogiri (>= 1.4.4, < 1.6)
397 sass (3.2.9) 408 sass (3.2.9)
@@ -408,6 +419,11 @@ GEM @@ -408,6 +419,11 @@ GEM
408 select2-rails (3.3.1) 419 select2-rails (3.3.1)
409 sass-rails (>= 3.2) 420 sass-rails (>= 3.2)
410 thor (~> 0.14) 421 thor (~> 0.14)
  422 + selenium-webdriver (2.32.1)
  423 + childprocess (>= 0.2.5)
  424 + multi_json (~> 1.0)
  425 + rubyzip
  426 + websocket (~> 1.0.4)
411 settingslogic (2.0.9) 427 settingslogic (2.0.9)
412 sexp_processor (4.2.1) 428 sexp_processor (4.2.1)
413 shoulda-matchers (2.1.0) 429 shoulda-matchers (2.1.0)
@@ -482,6 +498,7 @@ GEM @@ -482,6 +498,7 @@ GEM
482 uglifier (2.0.1) 498 uglifier (2.0.1)
483 execjs (>= 0.3.0) 499 execjs (>= 0.3.0)
484 multi_json (~> 1.0, >= 1.0.2) 500 multi_json (~> 1.0, >= 1.0.2)
  501 + underscore-rails (1.4.4)
485 virtus (0.5.4) 502 virtus (0.5.4)
486 backports (~> 2.6.1) 503 backports (~> 2.6.1)
487 descendants_tracker (~> 0.0.1) 504 descendants_tracker (~> 0.0.1)
@@ -490,6 +507,7 @@ GEM @@ -490,6 +507,7 @@ GEM
490 webmock (1.11.0) 507 webmock (1.11.0)
491 addressable (>= 2.2.7) 508 addressable (>= 2.2.7)
492 crack (>= 0.3.2) 509 crack (>= 0.3.2)
  510 + websocket (1.0.7)
493 xpath (2.0.0) 511 xpath (2.0.0)
494 nokogiri (~> 1.3) 512 nokogiri (~> 1.3)
495 yajl-ruby (1.1.0) 513 yajl-ruby (1.1.0)
@@ -510,6 +528,7 @@ DEPENDENCIES @@ -510,6 +528,7 @@ DEPENDENCIES
510 coffee-rails 528 coffee-rails
511 colored 529 colored
512 coveralls 530 coveralls
  531 + d3_rails (~> 3.1.4)
513 database_cleaner 532 database_cleaner
514 devise 533 devise
515 email_spec 534 email_spec
@@ -536,6 +555,7 @@ DEPENDENCIES @@ -536,6 +555,7 @@ DEPENDENCIES
536 haml-rails 555 haml-rails
537 hipchat (~> 0.9.0) 556 hipchat (~> 0.9.0)
538 httparty 557 httparty
  558 + jasmine
539 jquery-atwho-rails (= 0.3.0) 559 jquery-atwho-rails (= 0.3.0)
540 jquery-rails (= 2.1.3) 560 jquery-rails (= 2.1.3)
541 jquery-turbolinks 561 jquery-turbolinks
@@ -586,4 +606,5 @@ DEPENDENCIES @@ -586,4 +606,5 @@ DEPENDENCIES
586 tinder (~> 1.9.2) 606 tinder (~> 1.9.2)
587 turbolinks 607 turbolinks
588 uglifier 608 uglifier
  609 + underscore-rails (~> 1.4.4)
589 webmock 610 webmock
app/assets/javascripts/application.js
@@ -27,3 +27,5 @@ @@ -27,3 +27,5 @@
27 //= require branch-graph 27 //= require branch-graph
28 //= require ace-src-noconflict/ace 28 //= require ace-src-noconflict/ace
29 //= require_tree . 29 //= require_tree .
  30 +//= require d3
  31 +//= require underscore
app/assets/javascripts/stat_graph.js.coffee 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +class window.StatGraph
  2 + @log: {}
  3 + @get_log: ->
  4 + @log
  5 + @set_log: (data) ->
  6 + @log = data
app/assets/javascripts/stat_graph_contributors.js.coffee 0 → 100644
@@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
  1 +class window.ContributorsStatGraph
  2 + init: (log) ->
  3 + @parsed_log = ContributorsStatGraphUtil.parse_log(log)
  4 + @set_current_field("commits")
  5 + total_commits = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field)
  6 + author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field)
  7 + @add_master_graph(total_commits)
  8 + @add_authors_graph(author_commits)
  9 + @change_date_header()
  10 + add_master_graph: (total_data) ->
  11 + @master_graph = new ContributorsMasterGraph(total_data)
  12 + @master_graph.draw()
  13 + add_authors_graph: (author_data) ->
  14 + @authors = []
  15 + _.each(author_data, (d) =>
  16 + author_header = @create_author_header(d)
  17 + $(".contributors-list").append(author_header)
  18 + @authors[d.author] = author_graph = new ContributorsAuthorGraph(d.dates)
  19 + author_graph.draw()
  20 + )
  21 + format_author_commit_info: (author) ->
  22 + author.commits + " commits " + author.additions + " ++ / " + author.deletions + " --"
  23 + create_author_header: (author) ->
  24 + list_item = $('<li/>', {
  25 + class: 'person'
  26 + style: 'display: block;'
  27 + })
  28 + author_name = $('<h4>' + author.author + '</h4>')
  29 + author_commit_info_span = $('<span/>', {
  30 + class: 'commits'
  31 + })
  32 + author_commit_info = @format_author_commit_info(author)
  33 + author_commit_info_span.text(author_commit_info)
  34 + list_item.append(author_name)
  35 + list_item.append(author_commit_info_span)
  36 + list_item
  37 + redraw_master: ->
  38 + total_data = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field)
  39 + @master_graph.set_data(total_data)
  40 + @master_graph.redraw()
  41 + redraw_authors: ->
  42 + $("ol").html("")
  43 + x_domain = ContributorsGraph.prototype.x_domain
  44 + author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field, x_domain)
  45 + _.each(author_commits, (d) =>
  46 + @redraw_author_commit_info(d)
  47 + $(@authors[d.author].list_item).appendTo("ol")
  48 + @authors[d.author].set_data(d.dates)
  49 + @authors[d.author].redraw()
  50 + )
  51 + set_current_field: (field) ->
  52 + @field = field
  53 + change_date_header: ->
  54 + x_domain = ContributorsGraph.prototype.x_domain
  55 + print_date_format = d3.time.format("%B %e %Y");
  56 + print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]);
  57 + $("#date_header").text(print);
  58 + redraw_author_commit_info: (author) ->
  59 + author_list_item = $(@authors[author.author].list_item)
  60 + author_commit_info = @format_author_commit_info(author)
  61 + author_list_item.find("span").text(author_commit_info)
0 \ No newline at end of file 62 \ No newline at end of file
app/assets/javascripts/stat_graph_contributors_graph.js.coffee 0 → 100644
@@ -0,0 +1,166 @@ @@ -0,0 +1,166 @@
  1 +class window.ContributorsGraph
  2 + MARGIN:
  3 + top: 20
  4 + right: 20
  5 + bottom: 30
  6 + left: 50
  7 + x_domain: null
  8 + y_domain: null
  9 + dates: []
  10 + @set_x_domain: (data) =>
  11 + @prototype.x_domain = data
  12 + @set_y_domain: (data) =>
  13 + @prototype.y_domain = [0, d3.max(data, (d) ->
  14 + d.commits = d.commits ? d.additions ? d.deletions
  15 + )]
  16 + @init_x_domain: (data) =>
  17 + @prototype.x_domain = d3.extent(data, (d) ->
  18 + d.date
  19 + )
  20 + @init_y_domain: (data) =>
  21 + @prototype.y_domain = [0, d3.max(data, (d) ->
  22 + d.commits = d.commits ? d.additions ? d.deletions
  23 + )]
  24 + @init_domain: (data) =>
  25 + @init_x_domain(data)
  26 + @init_y_domain(data)
  27 + @set_dates: (data) =>
  28 + @prototype.dates = data
  29 + set_x_domain: ->
  30 + @x.domain(@x_domain)
  31 + set_y_domain: ->
  32 + @y.domain(@y_domain)
  33 + set_domain: ->
  34 + @set_x_domain()
  35 + @set_y_domain()
  36 + create_scale: (width, height) ->
  37 + @x = d3.time.scale().range([0, width]).clamp(true)
  38 + @y = d3.scale.linear().range([height, 0]).nice()
  39 + draw_x_axis: ->
  40 + @svg.append("g").attr("class", "x axis").attr("transform", "translate(0, #{@height})")
  41 + .call(@x_axis);
  42 + draw_y_axis: ->
  43 + @svg.append("g").attr("class", "y axis").call(@y_axis)
  44 + set_data: (data) ->
  45 + @data = data
  46 +
  47 +class window.ContributorsMasterGraph extends ContributorsGraph
  48 + constructor: (@data) ->
  49 + @width = 1100
  50 + @height = 125
  51 + @x = null
  52 + @y = null
  53 + @x_axis = null
  54 + @y_axis = null
  55 + @area = null
  56 + @svg = null
  57 + @brush = null
  58 + @x_max_domain = null
  59 + process_dates: (data) ->
  60 + dates = @get_dates(data)
  61 + @parse_dates(data)
  62 + ContributorsGraph.set_dates(dates)
  63 + get_dates: (data) ->
  64 + _.pluck(data, 'date')
  65 + parse_dates: (data) ->
  66 + parseDate = d3.time.format("%Y-%m-%d").parse
  67 + data.forEach((d) ->
  68 + d.date = parseDate(d.date)
  69 + )
  70 + create_scale: ->
  71 + super @width, @height
  72 + create_axes: ->
  73 + @x_axis = d3.svg.axis().scale(@x).orient("bottom")
  74 + @y_axis = d3.svg.axis().scale(@y).orient("left")
  75 + create_svg: ->
  76 + @svg = d3.select("#contributors-master").append("svg")
  77 + .attr("width", @width + @MARGIN.left + @MARGIN.right)
  78 + .attr("height", @height + @MARGIN.top + @MARGIN.bottom)
  79 + .attr("class", "tint-box")
  80 + .append("g")
  81 + .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")")
  82 + create_area: (x, y) ->
  83 + @area = d3.svg.area().x((d) ->
  84 + x(d.date)
  85 + ).y0(@height).y1((d) ->
  86 + y(d.commits = d.commits ? d.additions ? d.deletions)
  87 + ).interpolate("basis")
  88 + create_brush: ->
  89 + @brush = d3.svg.brush().x(@x).on("brushend", @update_content);
  90 + draw_path: (data) ->
  91 + @svg.append("path").datum(data).attr("class", "area").attr("d", @area);
  92 + add_brush: ->
  93 + @svg.append("g").attr("class", "selection").call(@brush).selectAll("rect").attr("height", @height);
  94 + update_content: =>
  95 + ContributorsGraph.set_x_domain(if @brush.empty() then @x_max_domain else @brush.extent())
  96 + $("#brush_change").trigger('change')
  97 + draw: ->
  98 + @process_dates(@data)
  99 + @create_scale()
  100 + @create_axes()
  101 + ContributorsGraph.init_domain(@data)
  102 + @x_max_domain = @x_domain
  103 + @set_domain()
  104 + @create_area(@x, @y)
  105 + @create_svg()
  106 + @create_brush()
  107 + @draw_path(@data)
  108 + @draw_x_axis()
  109 + @draw_y_axis()
  110 + @add_brush()
  111 + redraw: ->
  112 + @process_dates(@data)
  113 + ContributorsGraph.set_y_domain(@data)
  114 + @set_y_domain()
  115 + @svg.select("path").datum(@data)
  116 + @svg.select("path").attr("d", @area)
  117 + @svg.select(".y.axis").call(@y_axis)
  118 +
  119 +class window.ContributorsAuthorGraph extends ContributorsGraph
  120 + constructor: (@data) ->
  121 + @width = 490
  122 + @height = 130
  123 + @x = null
  124 + @y = null
  125 + @x_axis = null
  126 + @y_axis = null
  127 + @area = null
  128 + @svg = null
  129 + @list_item = null
  130 + create_scale: ->
  131 + super @width, @height
  132 + create_axes: ->
  133 + @x_axis = d3.svg.axis().scale(@x).orient("bottom").tickFormat(d3.time.format("%m/%d"));
  134 + @y_axis = d3.svg.axis().scale(@y).orient("left")
  135 + create_area: (x, y) ->
  136 + @area = d3.svg.area().x((d) ->
  137 + parseDate = d3.time.format("%Y-%m-%d").parse
  138 + x(parseDate(d))
  139 + ).y0(@height).y1((d) =>
  140 + if @data[d]? then y(@data[d]) else y(0)
  141 + ).interpolate("basis")
  142 + create_svg: ->
  143 + @list_item = d3.selectAll(".person")[0].pop()
  144 + @svg = d3.select(@list_item).append("svg")
  145 + .attr("width", @width + @MARGIN.left + @MARGIN.right)
  146 + .attr("height", @height + @MARGIN.top + @MARGIN.bottom)
  147 + .attr("class", "spark")
  148 + .append("g")
  149 + .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")")
  150 + draw_path: (data) ->
  151 + @svg.append("path").datum(data).attr("class", "area-contributor").attr("d", @area);
  152 + draw: ->
  153 + @create_scale()
  154 + @create_axes()
  155 + @set_domain()
  156 + @create_area(@x, @y)
  157 + @create_svg()
  158 + @draw_path(@dates)
  159 + @draw_x_axis()
  160 + @draw_y_axis()
  161 + redraw: ->
  162 + @set_domain()
  163 + @svg.select("path").datum(@dates)
  164 + @svg.select("path").attr("d", @area)
  165 + @svg.select(".x.axis").call(@x_axis)
  166 + @svg.select(".y.axis").call(@y_axis)
app/assets/javascripts/stat_graph_contributors_util.js.coffee 0 → 100644
@@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
  1 +window.ContributorsStatGraphUtil =
  2 + parse_log: (log) ->
  3 + total = {}
  4 + by_author = {}
  5 + for entry in log
  6 + @add_date(entry.date, total) unless total[entry.date]?
  7 + @add_author(entry.author, by_author) unless by_author[entry.author]?
  8 + @add_date(entry.date, by_author[entry.author]) unless by_author[entry.author][entry.date]
  9 + @store_data(entry, total[entry.date], by_author[entry.author][entry.date])
  10 + total = _.toArray(total)
  11 + by_author = _.toArray(by_author)
  12 + total: total, by_author: by_author
  13 +
  14 + add_date: (date, collection) ->
  15 + collection[date] = {}
  16 + collection[date].date = date
  17 +
  18 + add_author: (author, by_author) ->
  19 + by_author[author] = {}
  20 + by_author[author].author = author
  21 +
  22 + store_data: (entry, total, by_author) ->
  23 + @store_commits(total, by_author)
  24 + @store_additions(entry, total, by_author)
  25 + @store_deletions(entry, total, by_author)
  26 +
  27 + store_commits: (total, by_author) ->
  28 + @add(total, "commits", 1)
  29 + @add(by_author, "commits", 1)
  30 +
  31 + add: (collection, field, value) ->
  32 + collection[field] ?= 0
  33 + collection[field] += value
  34 +
  35 + store_additions: (entry, total, by_author) ->
  36 + entry.additions ?= 0
  37 + @add(total, "additions", entry.additions)
  38 + @add(by_author, "additions", entry.additions)
  39 +
  40 + store_deletions: (entry, total, by_author) ->
  41 + entry.deletions ?= 0
  42 + @add(total, "deletions", entry.deletions)
  43 + @add(by_author, "deletions", entry.deletions)
  44 +
  45 + get_total_data: (parsed_log, field) ->
  46 + log = parsed_log.total
  47 + total_data = @pick_field(log, field)
  48 + _.sortBy(total_data, (d) ->
  49 + d.date
  50 + )
  51 + pick_field: (log, field) ->
  52 + total_data = []
  53 + _.each(log, (d) ->
  54 + total_data.push(_.pick(d, [field, 'date']))
  55 + )
  56 + total_data
  57 +
  58 + get_author_data: (parsed_log, field, date_range = null) ->
  59 + log = parsed_log.by_author
  60 + author_data = []
  61 +
  62 + _.each(log, (log_entry) =>
  63 + parsed_log_entry = @parse_log_entry(log_entry, field, date_range)
  64 + if not _.isEmpty(parsed_log_entry.dates)
  65 + author_data.push(parsed_log_entry)
  66 + )
  67 +
  68 + _.sortBy(author_data, (d) ->
  69 + d[field]
  70 + ).reverse()
  71 +
  72 + parse_log_entry: (log_entry, field, date_range) ->
  73 + parsed_entry = {}
  74 + parsed_entry.author = log_entry.author
  75 + parsed_entry.dates = {}
  76 + parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0
  77 + _.each(_.omit(log_entry, 'author'), (value, key) =>
  78 + if @in_range(value.date, date_range)
  79 + parsed_entry.dates[value.date] = value[field]
  80 + parsed_entry.commits += value.commits
  81 + parsed_entry.additions += value.additions
  82 + parsed_entry.deletions += value.deletions
  83 + )
  84 + return parsed_entry
  85 +
  86 + in_range: (date, date_range) ->
  87 + if date_range is null || date_range[0] <= new Date(date) <= date_range[1]
  88 + true
  89 + else
  90 + false
  91 +
0 \ No newline at end of file 92 \ No newline at end of file
app/assets/stylesheets/application.scss
@@ -37,6 +37,7 @@ @@ -37,6 +37,7 @@
37 @import "sections/wiki.scss"; 37 @import "sections/wiki.scss";
38 @import "sections/wall.scss"; 38 @import "sections/wall.scss";
39 @import "sections/dashboard.scss"; 39 @import "sections/dashboard.scss";
  40 +@import "sections/stat_graph.scss";
40 41
41 @import "highlight/white.scss"; 42 @import "highlight/white.scss";
42 @import "highlight/dark.scss"; 43 @import "highlight/dark.scss";
app/assets/stylesheets/sections/stat_graph.scss 0 → 100644
@@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
  1 +.tint-box {
  2 + border-radius: 6px;
  3 + background: #f3f3f3;
  4 + position: relative;
  5 + margin-bottom: 10px;
  6 +}
  7 +
  8 +.area {
  9 + fill: #1db34f;
  10 + fill-opacity: 0.5;
  11 +}
  12 +
  13 +.axis {
  14 + fill: #aaa;
  15 + font-size: 10px;
  16 +}
  17 +
  18 +#contributors .person {
  19 + -moz-box-sizing: border-box;
  20 + box-sizing: border-box;
  21 + float: left;
  22 + border-radius: 2px;
  23 + margin: 10px;
  24 + border: 1px solid #ddd;
  25 +}
  26 +
  27 +.contributors-list {
  28 + margin: 0 0 10px 0;
  29 + list-style: none;
  30 + padding: 0;
  31 +}
  32 +
  33 +#contributors .person .spark {
  34 + display: block;
  35 + background: #f7f7f7;
  36 +}
  37 +
  38 +#contributors .person .area-contributor {
  39 + fill: #f17f49;
  40 +}
  41 +
  42 +.selection rect {
  43 + fill: #333;
  44 + fill-opacity: 0.1;
  45 + stroke: #333;
  46 + stroke-width: 1px;
  47 + stroke-opacity: 0.4;
  48 + shape-rendering: crispedges;
  49 + stroke-dasharray: 3 3;
  50 +}
  51 +
  52 +.right{
  53 + float: right;
  54 + display: inline-block;
  55 + margin-top: 5px;
  56 +}
app/controllers/stat_graph_controller.rb 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +class StatGraphController < ProjectResourceController
  2 +
  3 + # Authorize
  4 + before_filter :authorize_read_project!
  5 + before_filter :authorize_code_access!
  6 + before_filter :require_non_empty_project
  7 +
  8 + def show
  9 + @repo = @project.repository
  10 + @stats = Gitlab::GitStats.new(@repo.raw, @repo.root_ref)
  11 + @log = @stats.parsed_log.to_json
  12 + end
  13 +
  14 +end
0 \ No newline at end of file 15 \ No newline at end of file
app/views/layouts/nav/_project.html.haml
@@ -11,6 +11,8 @@ @@ -11,6 +11,8 @@
11 = link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref) 11 = link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref)
12 = nav_link(controller: %w(graph)) do 12 = nav_link(controller: %w(graph)) do
13 = link_to "Network", project_graph_path(@project, @ref || @repository.root_ref) 13 = link_to "Network", project_graph_path(@project, @ref || @repository.root_ref)
  14 + = nav_link(controller: %w(stat_graph)) do
  15 + = link_to "Graphs", project_stat_graph_path(@project, @ref || @repository.root_ref)
14 16
15 - if @project.issues_enabled 17 - if @project.issues_enabled
16 = nav_link(controller: %w(issues milestones labels)) do 18 = nav_link(controller: %w(issues milestones labels)) do
app/views/stat_graph/show.html.haml 0 → 100644
@@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
  1 +.header.clearfix
  2 + .right
  3 + %select
  4 + %option{:value => "commits"} Commits
  5 + %option{:value => "additions"} Additions
  6 + %option{:value => "deletions"} Deletions
  7 + %h3#date_header
  8 + %input#brush_change{:type => "hidden"}
  9 +
  10 +.graphs
  11 + #contributors-master
  12 + #contributors.clearfix
  13 + %ol.contributors-list.clearfix
  14 +
  15 +:javascript
  16 + controller = new ContributorsStatGraph
  17 + controller.init(#{@log})
  18 +
  19 + $("select").change( function () {
  20 + var field = $(this).val()
  21 + controller.set_current_field(field)
  22 + controller.redraw_master()
  23 + controller.redraw_authors()
  24 + })
  25 +
  26 + $("#brush_change").change( function () {
  27 + controller.change_date_header()
  28 + controller.redraw_authors()
  29 + })
0 \ No newline at end of file 30 \ No newline at end of file
config/routes.rb
@@ -190,6 +190,7 @@ Gitlab::Application.routes.draw do @@ -190,6 +190,7 @@ Gitlab::Application.routes.draw do
190 resources :compare, only: [:index, :create] 190 resources :compare, only: [:index, :create]
191 resources :blame, only: [:show], constraints: {id: /.+/} 191 resources :blame, only: [:show], constraints: {id: /.+/}
192 resources :graph, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/} 192 resources :graph, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/}
  193 + resources :stat_graph, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/}
193 match "/compare/:from...:to" => "compare#show", as: "compare", via: [:get, :post], constraints: {from: /.+/, to: /.+/} 194 match "/compare/:from...:to" => "compare#show", as: "compare", via: [:get, :post], constraints: {from: /.+/, to: /.+/}
194 195
195 scope module: :projects do 196 scope module: :projects do
lib/gitlab/git_stats.rb 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 +require 'gitlab/git_stats_log_parser'
  2 +
  3 +module Gitlab
  4 + class GitStats
  5 + attr_accessor :repo, :ref
  6 +
  7 + def initialize repo, ref
  8 + @repo, @ref = repo, ref
  9 + end
  10 +
  11 + def log
  12 + args = ['--format=%aN%x0a%ad', '--date=short', '--shortstat', '--no-merges']
  13 + repo.git.run(nil, 'log', nil, {}, args)
  14 + end
  15 +
  16 + def parsed_log
  17 + LogParser.parse_log(log)
  18 + end
  19 + end
  20 +end
lib/gitlab/git_stats_log_parser.rb 0 → 100644
@@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
  1 +class LogParser
  2 + #Parses the log file into a collection of commits
  3 + #Data model: {author, date, additions, deletions}
  4 + def self.parse_log log_from_git
  5 + log = log_from_git.split("\n")
  6 +
  7 + i = 0
  8 + collection = []
  9 + entry = {}
  10 +
  11 + while i <= log.size do
  12 + pos = i % 4
  13 + case pos
  14 + when 0
  15 + unless i == 0
  16 + collection.push(entry)
  17 + entry = {}
  18 + end
  19 + entry[:author] = log[i].to_s
  20 + when 1
  21 + entry[:date] = log[i].to_s
  22 + when 3
  23 + changes = log[i].split(",")
  24 + entry[:additions] = changes[1].to_i unless changes[1].nil?
  25 + entry[:deletions] = changes[2].to_i unless changes[2].nil?
  26 + end
  27 + i += 1
  28 + end
  29 +
  30 + collection
  31 + end
  32 +end
0 \ No newline at end of file 33 \ No newline at end of file
spec/javascripts/helpers/.gitkeep 0 → 100644
spec/javascripts/stat_graph_contributors_graph_spec.js 0 → 100644
@@ -0,0 +1,125 @@ @@ -0,0 +1,125 @@
  1 +describe("ContributorsGraph", function () {
  2 + describe("#set_x_domain", function () {
  3 + it("set the x_domain", function () {
  4 + ContributorsGraph.set_x_domain(20)
  5 + expect(ContributorsGraph.prototype.x_domain).toEqual(20)
  6 + })
  7 + })
  8 +
  9 + describe("#set_y_domain", function () {
  10 + it("sets the y_domain", function () {
  11 + ContributorsGraph.set_y_domain([{commits: 30}])
  12 + expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30])
  13 + })
  14 + })
  15 +
  16 + describe("#init_x_domain", function () {
  17 + it("sets the initial x_domain", function () {
  18 + ContributorsGraph.init_x_domain([{date: "2013-01-31"}, {date: "2012-01-31"}])
  19 + expect(ContributorsGraph.prototype.x_domain).toEqual(["2012-01-31", "2013-01-31"])
  20 + })
  21 + })
  22 +
  23 + describe("#init_y_domain", function () {
  24 + it("sets the initial y_domain", function () {
  25 + ContributorsGraph.init_y_domain([{commits: 30}])
  26 + expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30])
  27 + })
  28 + })
  29 +
  30 + describe("#init_domain", function () {
  31 + it("calls init_x_domain and init_y_domain", function () {
  32 + spyOn(ContributorsGraph, "init_x_domain")
  33 + spyOn(ContributorsGraph, "init_y_domain")
  34 + ContributorsGraph.init_domain()
  35 + expect(ContributorsGraph.init_x_domain).toHaveBeenCalled()
  36 + expect(ContributorsGraph.init_y_domain).toHaveBeenCalled()
  37 + })
  38 + })
  39 +
  40 + describe("#set_dates", function () {
  41 + it("sets the dates", function () {
  42 + ContributorsGraph.set_dates("2013-12-01")
  43 + expect(ContributorsGraph.prototype.dates).toEqual("2013-12-01")
  44 + })
  45 + })
  46 +
  47 + describe("#set_x_domain", function () {
  48 + it("sets the instance's x domain using the prototype's x_domain", function () {
  49 + ContributorsGraph.prototype.x_domain = 20
  50 + var instance = new ContributorsGraph()
  51 + instance.x = d3.time.scale().range([0, 100]).clamp(true)
  52 + spyOn(instance.x, 'domain')
  53 + instance.set_x_domain()
  54 + expect(instance.x.domain).toHaveBeenCalledWith(20)
  55 + })
  56 + })
  57 +
  58 + describe("#set_y_domain", function () {
  59 + it("sets the instance's y domain using the prototype's y_domain", function () {
  60 + ContributorsGraph.prototype.y_domain = 30
  61 + var instance = new ContributorsGraph()
  62 + instance.y = d3.scale.linear().range([100, 0]).nice()
  63 + spyOn(instance.y, 'domain')
  64 + instance.set_y_domain()
  65 + expect(instance.y.domain).toHaveBeenCalledWith(30)
  66 + })
  67 + })
  68 +
  69 + describe("#set_domain", function () {
  70 + it("calls set_x_domain and set_y_domain", function () {
  71 + var instance = new ContributorsGraph()
  72 + spyOn(instance, 'set_x_domain')
  73 + spyOn(instance, 'set_y_domain')
  74 + instance.set_domain()
  75 + expect(instance.set_x_domain).toHaveBeenCalled()
  76 + expect(instance.set_y_domain).toHaveBeenCalled()
  77 + })
  78 + })
  79 +
  80 + describe("#set_data", function () {
  81 + it("sets the data", function () {
  82 + var instance = new ContributorsGraph()
  83 + instance.set_data("20")
  84 + expect(instance.data).toEqual("20")
  85 + })
  86 + })
  87 +})
  88 +
  89 +describe("ContributorsMasterGraph", function () {
  90 +
  91 + describe("#process_dates", function () {
  92 + it("gets and parses dates", function () {
  93 + var graph = new ContributorsMasterGraph()
  94 + var data = 'random data here'
  95 + spyOn(graph, 'parse_dates')
  96 + spyOn(graph, 'get_dates').andReturn("get")
  97 + spyOn(ContributorsGraph,'set_dates').andCallThrough()
  98 + graph.process_dates(data)
  99 + expect(graph.parse_dates).toHaveBeenCalledWith(data)
  100 + expect(graph.get_dates).toHaveBeenCalledWith(data)
  101 + expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get")
  102 + })
  103 + })
  104 +
  105 + describe("#get_dates", function () {
  106 + it("plucks the date field from data collection", function () {
  107 + var graph = new ContributorsMasterGraph()
  108 + var data = [{date: "2013-01-01"}, {date: "2012-12-15"}]
  109 + expect(graph.get_dates(data)).toEqual(["2013-01-01", "2012-12-15"])
  110 + })
  111 + })
  112 +
  113 + describe("#parse_dates", function () {
  114 + it("parses the dates", function () {
  115 + var graph = new ContributorsMasterGraph()
  116 + var parseDate = d3.time.format("%Y-%m-%d").parse
  117 + var data = [{date: "2013-01-01"}, {date: "2012-12-15"}]
  118 + var correct = [{date: parseDate(data[0].date)}, {date: parseDate(data[1].date)}]
  119 + graph.parse_dates(data)
  120 + expect(data).toEqual(correct)
  121 + })
  122 + })
  123 +
  124 +
  125 +})
spec/javascripts/stat_graph_contributors_util_spec.js 0 → 100644
@@ -0,0 +1,200 @@ @@ -0,0 +1,200 @@
  1 +describe("ContributorsStatGraphUtil", function () {
  2 +
  3 + describe("#parse_log", function () {
  4 + it("returns a correctly parsed log", function () {
  5 + var fake_log = [
  6 + {author: "Karlo Soriano", date: "2013-05-09", additions: 471},
  7 + {author: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1},
  8 + {author: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3},
  9 + {author: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}]
  10 +
  11 + var correct_parsed_log = {
  12 + total: [
  13 + {date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
  14 + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}],
  15 + by_author:
  16 + [
  17 + {
  18 + author: "Karlo Soriano",
  19 + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
  20 + },
  21 + {
  22 + author: "Dmitriy Zaporozhets",
  23 + "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}
  24 + }
  25 + ]
  26 + }
  27 + expect(ContributorsStatGraphUtil.parse_log(fake_log)).toEqual(correct_parsed_log)
  28 + })
  29 + })
  30 +
  31 + describe("#store_data", function () {
  32 +
  33 + var fake_entry = {author: "Karlo Soriano", date: "2013-05-09", additions: 471}
  34 + var fake_total = {}
  35 + var fake_by_author = {}
  36 +
  37 + it("calls #store_commits", function () {
  38 + spyOn(ContributorsStatGraphUtil, 'store_commits')
  39 + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author)
  40 + expect(ContributorsStatGraphUtil.store_commits).toHaveBeenCalled()
  41 + })
  42 +
  43 + it("calls #store_additions", function () {
  44 + spyOn(ContributorsStatGraphUtil, 'store_additions')
  45 + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author)
  46 + expect(ContributorsStatGraphUtil.store_additions).toHaveBeenCalled()
  47 + })
  48 +
  49 + it("calls #store_deletions", function () {
  50 + spyOn(ContributorsStatGraphUtil, 'store_deletions')
  51 + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author)
  52 + expect(ContributorsStatGraphUtil.store_deletions).toHaveBeenCalled()
  53 + })
  54 +
  55 + })
  56 +
  57 + describe("#store_commits", function () {
  58 + var fake_total = "fake_total"
  59 + var fake_by_author = "fake_by_author"
  60 +
  61 + it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
  62 + spyOn(ContributorsStatGraphUtil, 'add')
  63 + ContributorsStatGraphUtil.store_commits(fake_total, fake_by_author)
  64 + expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "commits", 1], ["fake_by_author", "commits", 1]])
  65 + })
  66 + })
  67 +
  68 + describe("#add", function () {
  69 + it("adds 1 to current test_field in collection", function () {
  70 + var fake_collection = {test_field: 10}
  71 + ContributorsStatGraphUtil.add(fake_collection, "test_field", 1)
  72 + expect(fake_collection.test_field).toEqual(11)
  73 + })
  74 +
  75 + it("inits and adds 1 if test_field in collection is not defined", function () {
  76 + var fake_collection = {}
  77 + ContributorsStatGraphUtil.add(fake_collection, "test_field", 1)
  78 + expect(fake_collection.test_field).toEqual(1)
  79 + })
  80 + })
  81 +
  82 + describe("#store_additions", function () {
  83 + var fake_entry = {additions: 10}
  84 + var fake_total= "fake_total"
  85 + var fake_by_author = "fake_by_author"
  86 + it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
  87 + spyOn(ContributorsStatGraphUtil, 'add')
  88 + ContributorsStatGraphUtil.store_additions(fake_entry, fake_total, fake_by_author)
  89 + expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "additions", 10], ["fake_by_author", "additions", 10]])
  90 + })
  91 + })
  92 +
  93 + describe("#store_deletions", function () {
  94 + var fake_entry = {deletions: 10}
  95 + var fake_total= "fake_total"
  96 + var fake_by_author = "fake_by_author"
  97 + it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
  98 + spyOn(ContributorsStatGraphUtil, 'add')
  99 + ContributorsStatGraphUtil.store_deletions(fake_entry, fake_total, fake_by_author)
  100 + expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "deletions", 10], ["fake_by_author", "deletions", 10]])
  101 + })
  102 + })
  103 +
  104 + describe("#add_date", function () {
  105 + it("adds a date field to the collection", function () {
  106 + var fake_date = "2013-10-02"
  107 + var fake_collection = {}
  108 + ContributorsStatGraphUtil.add_date(fake_date, fake_collection)
  109 + expect(fake_collection[fake_date].date).toEqual("2013-10-02")
  110 + })
  111 + })
  112 +
  113 + describe("#add_author", function () {
  114 + it("adds an author field to the collection", function () {
  115 + var fake_author = "Author"
  116 + var fake_collection = {}
  117 + ContributorsStatGraphUtil.add_author(fake_author, fake_collection)
  118 + expect(fake_collection[fake_author].author).toEqual("Author")
  119 + })
  120 + })
  121 +
  122 + describe("#get_total_data", function () {
  123 + it("returns the collection sorted via specified field", function () {
  124 + var fake_parsed_log = {
  125 + total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
  126 + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}],
  127 + by_author:[
  128 + {
  129 + author: "Karlo Soriano",
  130 + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
  131 + },
  132 + {
  133 + author: "Dmitriy Zaporozhets",
  134 + "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}
  135 + }
  136 + ]};
  137 + var correct_total_data = [{date: "2013-05-08", commits: 3},
  138 + {date: "2013-05-09", commits: 1}];
  139 + expect(ContributorsStatGraphUtil.get_total_data(fake_parsed_log, "commits")).toEqual(correct_total_data)
  140 + })
  141 + })
  142 +
  143 + describe("#pick_field", function () {
  144 + it("returns the collection with only the specified field and date", function () {
  145 + var fake_parsed_log_total = [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
  146 + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}];
  147 + ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")
  148 + var correct_pick_field_data = [{date: "2013-05-09", commits: 1},{date: "2013-05-08", commits: 3}];
  149 + expect(ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")).toEqual(correct_pick_field_data)
  150 + })
  151 + })
  152 +
  153 + describe("#get_author_data", function () {
  154 + it("returns the log by author sorted by specified field", function () {
  155 + var fake_parsed_log = {
  156 + total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
  157 + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}],
  158 + by_author:[
  159 + {
  160 + author: "Karlo Soriano",
  161 + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
  162 + },
  163 + {
  164 + author: "Dmitriy Zaporozhets",
  165 + "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}
  166 + }
  167 + ]}
  168 + var correct_author_data = [{author:"Dmitriy Zaporozhets",dates:{"2013-05-08":3},deletions:7,additions:54,"commits":3},
  169 + {author:"Karlo Soriano",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1}]
  170 + expect(ContributorsStatGraphUtil.get_author_data(fake_parsed_log, "commits")).toEqual(correct_author_data)
  171 + })
  172 + })
  173 +
  174 + describe("#parse_log_entry", function () {
  175 + it("adds the corresponding info from the log entry to the author", function () {
  176 + var fake_log_entry = { author: "Karlo Soriano",
  177 + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
  178 + }
  179 + var correct_parsed_log = {author:"Karlo Soriano",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1}
  180 + expect(ContributorsStatGraphUtil.parse_log_entry(fake_log_entry, 'commits', null)).toEqual(correct_parsed_log)
  181 + })
  182 + })
  183 +
  184 + describe("#in_range", function () {
  185 + var date = "2013-05-09"
  186 + it("returns true if date_range is null", function () {
  187 + expect(ContributorsStatGraphUtil.in_range(date, null)).toEqual(true)
  188 + })
  189 + it("returns true if date is in range", function () {
  190 + var date_range = [new Date("2013-01-01"), new Date("2013-12-12")]
  191 + expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(true)
  192 + })
  193 + it("returns false if date is not in range", function () {
  194 + var date_range = [new Date("1999-12-01"), new Date("2000-12-01")]
  195 + expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(false)
  196 + })
  197 + })
  198 +
  199 +
  200 +})
0 \ No newline at end of file 201 \ No newline at end of file
spec/javascripts/stat_graph_spec.js 0 → 100644
@@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
  1 +describe("StatGraph", function () {
  2 +
  3 + describe("#get_log", function () {
  4 + it("returns log", function () {
  5 + StatGraph.log = "test";
  6 + expect(StatGraph.get_log()).toBe("test");
  7 + });
  8 + });
  9 +
  10 + describe("#set_log", function () {
  11 + it("sets the log", function () {
  12 + StatGraph.set_log("test");
  13 + expect(StatGraph.log).toBe("test");
  14 + })
  15 + })
  16 +
  17 +});
0 \ No newline at end of file 18 \ No newline at end of file
spec/javascripts/support/jasmine.yml 0 → 100644
@@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
  1 +# src_files
  2 +#
  3 +# Return an array of filepaths relative to src_dir to include before jasmine specs.
  4 +# Default: []
  5 +#
  6 +# EXAMPLE:
  7 +#
  8 +# src_files:
  9 +# - lib/source1.js
  10 +# - lib/source2.js
  11 +# - dist/**/*.js
  12 +#
  13 +src_files:
  14 + - assets/application.js
  15 +
  16 +# stylesheets
  17 +#
  18 +# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs.
  19 +# Default: []
  20 +#
  21 +# EXAMPLE:
  22 +#
  23 +# stylesheets:
  24 +# - css/style.css
  25 +# - stylesheets/*.css
  26 +#
  27 +stylesheets:
  28 + - stylesheets/**/*.css
  29 +
  30 +# helpers
  31 +#
  32 +# Return an array of filepaths relative to spec_dir to include before jasmine specs.
  33 +# Default: ["helpers/**/*.js"]
  34 +#
  35 +# EXAMPLE:
  36 +#
  37 +# helpers:
  38 +# - helpers/**/*.js
  39 +#
  40 +helpers:
  41 + - helpers/**/*.js
  42 +
  43 +# spec_files
  44 +#
  45 +# Return an array of filepaths relative to spec_dir to include.
  46 +# Default: ["**/*[sS]pec.js"]
  47 +#
  48 +# EXAMPLE:
  49 +#
  50 +# spec_files:
  51 +# - **/*[sS]pec.js
  52 +#
  53 +spec_files:
  54 + - '**/*[sS]pec.js'
  55 +
  56 +# src_dir
  57 +#
  58 +# Source directory path. Your src_files must be returned relative to this path. Will use root if left blank.
  59 +# Default: project root
  60 +#
  61 +# EXAMPLE:
  62 +#
  63 +# src_dir: public
  64 +#
  65 +src_dir:
  66 +
  67 +# spec_dir
  68 +#
  69 +# Spec directory path. Your spec_files must be returned relative to this path.
  70 +# Default: spec/javascripts
  71 +#
  72 +# EXAMPLE:
  73 +#
  74 +# spec_dir: spec/javascripts
  75 +#
  76 +spec_dir: spec/javascripts
spec/javascripts/support/jasmine_helper.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +#Use this file to set/override Jasmine configuration options
  2 +#You can remove it if you don't need it.
  3 +#This file is loaded *after* jasmine.yml is interpreted.
  4 +#
  5 +#Example: using a different boot file.
  6 +#Jasmine.configure do |config|
  7 +# @config.boot_dir = '/absolute/path/to/boot_dir'
  8 +# @config.boot_files = lambda { ['/absolute/path/to/boot_dir/file.js'] }
  9 +#end
  10 +#
  11 +
spec/lib/gitlab/git_stats_log_parser_spec.rb 0 → 100644
@@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
  1 +require 'spec_helper'
  2 +require 'gitlab/git_stats_log_parser'
  3 +
  4 +
  5 +describe LogParser do
  6 +
  7 + describe "#self.parse_log" do
  8 + context "log_from_git is a valid log" do
  9 + it "returns the correct log" do
  10 + fake_log = "Karlo Soriano
  11 +2013-05-09
  12 +
  13 + 14 files changed, 471 insertions(+)
  14 +Dmitriy Zaporozhets
  15 +2013-05-08
  16 +
  17 + 1 file changed, 6 insertions(+), 1 deletion(-)
  18 +Dmitriy Zaporozhets
  19 +2013-05-08
  20 +
  21 + 6 files changed, 19 insertions(+), 3 deletions(-)
  22 +Dmitriy Zaporozhets
  23 +2013-05-08
  24 +
  25 + 3 files changed, 29 insertions(+), 3 deletions(-)";
  26 +
  27 + lp = LogParser.parse_log(fake_log)
  28 + lp.should eq([
  29 + {author: "Karlo Soriano", date: "2013-05-09", additions: 471},
  30 + {author: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1},
  31 + {author: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3},
  32 + {author: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}])
  33 + end
  34 + end
  35 + end
  36 +
  37 +end
0 \ No newline at end of file 38 \ No newline at end of file
spec/lib/gitlab/git_stats_spec.rb 0 → 100644
@@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
  1 +require 'spec_helper'
  2 +
  3 +describe Gitlab::GitStats do
  4 +
  5 + describe "#parsed_log" do
  6 + let(:stats) { Gitlab::GitStats.new(nil, nil) }
  7 + before(:each) do
  8 + stats.stub(:log).and_return("anything")
  9 + end
  10 +
  11 + context "LogParser#parse_log returns 'test'" do
  12 + it "returns 'test'" do
  13 + LogParser.stub(:parse_log).and_return("test")
  14 + stats.parsed_log.should eq("test")
  15 + end
  16 + end
  17 + end
  18 +
  19 + describe "#log" do
  20 + let(:repo) { Repository.new(nil, nil) }
  21 + let(:gs) { Gitlab::GitStats.new(repo.raw, repo.root_ref) }
  22 +
  23 + before(:each) do
  24 + repo.stub(:raw).and_return(nil)
  25 + repo.stub(:root_ref).and_return(nil)
  26 + repo.raw.stub(:git)
  27 + end
  28 +
  29 + context "repo.git.run returns 'test'" do
  30 + it "returns 'test'" do
  31 + repo.raw.git.stub(:run).and_return("test")
  32 + gs.log.should eq("test")
  33 + end
  34 + end
  35 + end
  36 +end
0 \ No newline at end of file 37 \ No newline at end of file