Commit aa0808386da50db098a189218041fc4f04a8a07e
Exists in
master
and in
4 other branches
Merge pull request #4212 from karlo57/feature/graphs
Contributors Graph
Showing
23 changed files
with
1011 additions
and
0 deletions
Show diff stats
Gemfile
@@ -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 |
Gemfile.lock
@@ -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
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"; |
@@ -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 | +} |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 | +}) |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 | + |
@@ -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 |
@@ -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 |