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 | 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 | ... | ... |