Commit 71d67e6557acb1ce3beeec2c2c6deb35015bd8bb
Committed by
karlo57
1 parent
b9d989dc
Exists in
master
and in
4 other branches
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
Showing
23 changed files
with
1011 additions
and
0 deletions
Show diff stats
Gemfile
... | ... | @@ -107,6 +107,12 @@ gem 'tinder', '~> 1.9.2' |
107 | 107 | # HipChat integration |
108 | 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 | 116 | group :assets do |
111 | 117 | gem "sass-rails" |
112 | 118 | gem "coffee-rails" |
... | ... | @@ -177,6 +183,7 @@ group :development, :test do |
177 | 183 | gem 'poltergeist', '~> 1.3.0' |
178 | 184 | |
179 | 185 | gem 'spork', '~> 1.0rc' |
186 | + gem 'jasmine' | |
180 | 187 | end |
181 | 188 | |
182 | 189 | group :test do | ... | ... |
Gemfile.lock
... | ... | @@ -69,6 +69,8 @@ GEM |
69 | 69 | celluloid (0.14.0) |
70 | 70 | timers (>= 1.0.0) |
71 | 71 | charlock_holmes (0.6.9.4) |
72 | + childprocess (0.3.9) | |
73 | + ffi (~> 1.0, >= 1.0.11) | |
72 | 74 | chosen-rails (0.9.8) |
73 | 75 | railties (~> 3.0) |
74 | 76 | thor (~> 0.14) |
... | ... | @@ -92,6 +94,8 @@ GEM |
92 | 94 | simplecov (>= 0.7) |
93 | 95 | thor |
94 | 96 | crack (0.3.2) |
97 | + d3_rails (3.1.4) | |
98 | + railties (>= 3.1.0) | |
95 | 99 | daemons (1.1.9) |
96 | 100 | database_cleaner (1.0.1) |
97 | 101 | debug_inspector (0.0.2) |
... | ... | @@ -216,6 +220,12 @@ GEM |
216 | 220 | multi_xml (>= 0.5.2) |
217 | 221 | httpauth (0.2.0) |
218 | 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 | 229 | journey (1.0.4) |
220 | 230 | jquery-atwho-rails (0.3.0) |
221 | 231 | jquery-rails (2.1.3) |
... | ... | @@ -392,6 +402,7 @@ GEM |
392 | 402 | rspec-mocks (~> 2.13.0) |
393 | 403 | ruby-progressbar (1.0.2) |
394 | 404 | rubyntlm (0.1.1) |
405 | + rubyzip (0.9.9) | |
395 | 406 | sanitize (2.0.3) |
396 | 407 | nokogiri (>= 1.4.4, < 1.6) |
397 | 408 | sass (3.2.9) |
... | ... | @@ -408,6 +419,11 @@ GEM |
408 | 419 | select2-rails (3.3.1) |
409 | 420 | sass-rails (>= 3.2) |
410 | 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 | 427 | settingslogic (2.0.9) |
412 | 428 | sexp_processor (4.2.1) |
413 | 429 | shoulda-matchers (2.1.0) |
... | ... | @@ -482,6 +498,7 @@ GEM |
482 | 498 | uglifier (2.0.1) |
483 | 499 | execjs (>= 0.3.0) |
484 | 500 | multi_json (~> 1.0, >= 1.0.2) |
501 | + underscore-rails (1.4.4) | |
485 | 502 | virtus (0.5.4) |
486 | 503 | backports (~> 2.6.1) |
487 | 504 | descendants_tracker (~> 0.0.1) |
... | ... | @@ -490,6 +507,7 @@ GEM |
490 | 507 | webmock (1.11.0) |
491 | 508 | addressable (>= 2.2.7) |
492 | 509 | crack (>= 0.3.2) |
510 | + websocket (1.0.7) | |
493 | 511 | xpath (2.0.0) |
494 | 512 | nokogiri (~> 1.3) |
495 | 513 | yajl-ruby (1.1.0) |
... | ... | @@ -510,6 +528,7 @@ DEPENDENCIES |
510 | 528 | coffee-rails |
511 | 529 | colored |
512 | 530 | coveralls |
531 | + d3_rails (~> 3.1.4) | |
513 | 532 | database_cleaner |
514 | 533 | devise |
515 | 534 | email_spec |
... | ... | @@ -536,6 +555,7 @@ DEPENDENCIES |
536 | 555 | haml-rails |
537 | 556 | hipchat (~> 0.9.0) |
538 | 557 | httparty |
558 | + jasmine | |
539 | 559 | jquery-atwho-rails (= 0.3.0) |
540 | 560 | jquery-rails (= 2.1.3) |
541 | 561 | jquery-turbolinks |
... | ... | @@ -586,4 +606,5 @@ DEPENDENCIES |
586 | 606 | tinder (~> 1.9.2) |
587 | 607 | turbolinks |
588 | 608 | uglifier |
609 | + underscore-rails (~> 1.4.4) | |
589 | 610 | webmock | ... | ... |
app/assets/javascripts/application.js
app/assets/javascripts/stat_graph_contributors.js.coffee
0 → 100644
... | ... | @@ -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 | 62 | \ No newline at end of file | ... | ... |
app/assets/javascripts/stat_graph_contributors_graph.js.coffee
0 → 100644
... | ... | @@ -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 @@ |
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 | 92 | \ No newline at end of file | ... | ... |
app/assets/stylesheets/application.scss
... | ... | @@ -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 | +} | ... | ... |
... | ... | @@ -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 | 15 | \ No newline at end of file | ... | ... |
app/views/layouts/nav/_project.html.haml
... | ... | @@ -11,6 +11,8 @@ |
11 | 11 | = link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref) |
12 | 12 | = nav_link(controller: %w(graph)) do |
13 | 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 | 17 | - if @project.issues_enabled |
16 | 18 | = nav_link(controller: %w(issues milestones labels)) do | ... | ... |
... | ... | @@ -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 | 30 | \ No newline at end of file | ... | ... |
config/routes.rb
... | ... | @@ -190,6 +190,7 @@ Gitlab::Application.routes.draw do |
190 | 190 | resources :compare, only: [:index, :create] |
191 | 191 | resources :blame, only: [:show], constraints: {id: /.+/} |
192 | 192 | resources :graph, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/} |
193 | + resources :stat_graph, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/} | |
193 | 194 | match "/compare/:from...:to" => "compare#show", as: "compare", via: [:get, :post], constraints: {from: /.+/, to: /.+/} |
194 | 195 | |
195 | 196 | scope module: :projects do | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | 33 | \ No newline at end of file | ... | ... |
... | ... | @@ -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 | +}) | ... | ... |
... | ... | @@ -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 | 201 | \ No newline at end of file | ... | ... |
... | ... | @@ -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 | 18 | \ No newline at end of file | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | + | ... | ... |
... | ... | @@ -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 | 38 | \ No newline at end of file | ... | ... |
... | ... | @@ -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 | 37 | \ No newline at end of file | ... | ... |