Commit 71d67e6557acb1ce3beeec2c2c6deb35015bd8bb

Authored by Karlo Soriano
Committed by karlo57
1 parent b9d989dc

Contributors graphs feature for GitLab

Created tests and refactored some code along the way

Added stat graph util spec, refactored code

finsihed up tests and refactors

finsihed up tests and refactors
@@ -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