how_is 18.0.4 → 18.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github_changelog_generator +3 -0
- data/.gitignore +1 -1
- data/.rspec +1 -1
- data/.rubocop.yml +22 -9
- data/.rubocop_todo.yml +21 -0
- data/CHANGELOG.md +266 -140
- data/Gemfile +12 -1
- data/Rakefile +33 -14
- data/exe/how_is +6 -11
- data/fixtures/vcr_cassettes/how_is_contributions_all_contributors.yml +78 -0
- data/fixtures/vcr_cassettes/how_is_contributions_new_contributors.yml +219 -0
- data/how_is.gemspec +2 -1
- data/lib/how_is/analyzer.rb +53 -47
- data/lib/how_is/contributions.rb +46 -0
- data/lib/how_is/fetcher.rb +10 -10
- data/lib/how_is/pulse.rb +20 -19
- data/lib/how_is/report/base_report.rb +15 -12
- data/lib/how_is/report/html.rb +59 -52
- data/lib/how_is/report/json.rb +2 -2
- data/lib/how_is/report.rb +9 -6
- data/lib/how_is/version.rb +1 -1
- data/lib/how_is.rb +19 -16
- metadata +24 -6
- data/Gemfile.lock +0 -110
data/exe/how_is
CHANGED
@@ -8,11 +8,9 @@ require "how_is/cli"
|
|
8
8
|
begin
|
9
9
|
result = HowIs::CLI.parse(ARGV)
|
10
10
|
rescue HowIs::CLI::OptionsError => e
|
11
|
-
if ENV[
|
12
|
-
|
13
|
-
|
14
|
-
abort "Error: #{e.message}"
|
15
|
-
end
|
11
|
+
raise if ENV["SHOW_TRACE"]
|
12
|
+
|
13
|
+
abort "Error: #{e.message}"
|
16
14
|
end
|
17
15
|
|
18
16
|
options = result[:options]
|
@@ -45,10 +43,7 @@ begin
|
|
45
43
|
)
|
46
44
|
end
|
47
45
|
rescue => e
|
48
|
-
if ENV[
|
49
|
-
raise
|
50
|
-
else
|
51
|
-
abort "Error: #{e.message}"
|
52
|
-
end
|
53
|
-
end
|
46
|
+
raise if ENV["SHOW_TRACE"]
|
54
47
|
|
48
|
+
abort "Error: #{e.message}"
|
49
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: get
|
5
|
+
uri: https://api.github.com/repos/how-is/example-repository/commits?since=2017-01-01
|
6
|
+
body:
|
7
|
+
encoding: US-ASCII
|
8
|
+
string: ''
|
9
|
+
headers:
|
10
|
+
Accept:
|
11
|
+
- application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1
|
12
|
+
Accept-Charset:
|
13
|
+
- utf-8
|
14
|
+
User-Agent:
|
15
|
+
- Github API Ruby Gem 0.17.0
|
16
|
+
Accept-Encoding:
|
17
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
18
|
+
response:
|
19
|
+
status:
|
20
|
+
code: 200
|
21
|
+
message: OK
|
22
|
+
headers:
|
23
|
+
Server:
|
24
|
+
- GitHub.com
|
25
|
+
Date:
|
26
|
+
- Sat, 05 Aug 2017 21:05:34 GMT
|
27
|
+
Content-Type:
|
28
|
+
- application/json; charset=utf-8
|
29
|
+
Transfer-Encoding:
|
30
|
+
- chunked
|
31
|
+
Status:
|
32
|
+
- 200 OK
|
33
|
+
X-Ratelimit-Limit:
|
34
|
+
- '60'
|
35
|
+
X-Ratelimit-Remaining:
|
36
|
+
- '6'
|
37
|
+
X-Ratelimit-Reset:
|
38
|
+
- '1501967489'
|
39
|
+
Cache-Control:
|
40
|
+
- public, max-age=60, s-maxage=60
|
41
|
+
Vary:
|
42
|
+
- Accept
|
43
|
+
Etag:
|
44
|
+
- W/"c4e64de253131e8983284594017e505c"
|
45
|
+
Last-Modified:
|
46
|
+
- Sat, 05 Aug 2017 20:39:01 GMT
|
47
|
+
X-Github-Media-Type:
|
48
|
+
- github.v3; format=json
|
49
|
+
Access-Control-Expose-Headers:
|
50
|
+
- ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset,
|
51
|
+
X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
|
52
|
+
Access-Control-Allow-Origin:
|
53
|
+
- "*"
|
54
|
+
Content-Security-Policy:
|
55
|
+
- default-src 'none'
|
56
|
+
Strict-Transport-Security:
|
57
|
+
- max-age=31536000; includeSubdomains; preload
|
58
|
+
X-Content-Type-Options:
|
59
|
+
- nosniff
|
60
|
+
X-Frame-Options:
|
61
|
+
- deny
|
62
|
+
X-Xss-Protection:
|
63
|
+
- 1; mode=block
|
64
|
+
X-Runtime-Rack:
|
65
|
+
- '0.046194'
|
66
|
+
X-Github-Request-Id:
|
67
|
+
- DA1A:795E:9C82C1:19614C6:5986331E
|
68
|
+
body:
|
69
|
+
encoding: ASCII-8BIT
|
70
|
+
string: '[{"sha":"40c01ab6ebec6cbd8ad9e521a732f941c169e557","commit":{"author":{"name":"Ellen
|
71
|
+
Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"committer":{"name":"Ellen
|
72
|
+
Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"message":"meep","tree":{"sha":"6911e0637822f44b83f04f47821adab56fdbc0b9","url":"https://api.github.com/repos/how-is/example-repository/git/trees/6911e0637822f44b83f04f47821adab56fdbc0b9"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comment_count":0},"url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","html_url":"https://github.com/how-is/example-repository/commit/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557/comments","author":{"login":"duckinator","id":39698,"avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"committer":{"login":"duckinator","id":39698,"avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"parents":[{"sha":"3794aa1c4b76623748faf280abe5760b76823162","url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162"}]},{"sha":"3794aa1c4b76623748faf280abe5760b76823162","commit":{"author":{"name":"fake
|
73
|
+
author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"committer":{"name":"fake
|
74
|
+
author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"message":"test
|
75
|
+
commit","tree":{"sha":"8286e548e330cfe01efcf7189f4df1fa53e777a7","url":"https://api.github.com/repos/how-is/example-repository/git/trees/8286e548e330cfe01efcf7189f4df1fa53e777a7"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/3794aa1c4b76623748faf280abe5760b76823162","comment_count":0},"url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162/comments","author":null,"committer":null,"parents":[{"sha":"9e29405efa433529b86722542b8fb4b34dfd9edd","url":"https://api.github.com/repos/how-is/example-repository/commits/9e29405efa433529b86722542b8fb4b34dfd9edd","html_url":"https://github.com/how-is/example-repository/commit/9e29405efa433529b86722542b8fb4b34dfd9edd"}]}]'
|
76
|
+
http_version:
|
77
|
+
recorded_at: Sat, 05 Aug 2017 21:05:34 GMT
|
78
|
+
recorded_with: VCR 3.0.3
|
@@ -0,0 +1,219 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: get
|
5
|
+
uri: https://api.github.com/repos/how-is/example-repository/commits?since=2017-01-01
|
6
|
+
body:
|
7
|
+
encoding: US-ASCII
|
8
|
+
string: ''
|
9
|
+
headers:
|
10
|
+
Accept:
|
11
|
+
- application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1
|
12
|
+
Accept-Charset:
|
13
|
+
- utf-8
|
14
|
+
User-Agent:
|
15
|
+
- Github API Ruby Gem 0.17.0
|
16
|
+
Accept-Encoding:
|
17
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
18
|
+
response:
|
19
|
+
status:
|
20
|
+
code: 200
|
21
|
+
message: OK
|
22
|
+
headers:
|
23
|
+
Server:
|
24
|
+
- GitHub.com
|
25
|
+
Date:
|
26
|
+
- Sat, 05 Aug 2017 21:08:49 GMT
|
27
|
+
Content-Type:
|
28
|
+
- application/json; charset=utf-8
|
29
|
+
Transfer-Encoding:
|
30
|
+
- chunked
|
31
|
+
Status:
|
32
|
+
- 200 OK
|
33
|
+
X-Ratelimit-Limit:
|
34
|
+
- '60'
|
35
|
+
X-Ratelimit-Remaining:
|
36
|
+
- '5'
|
37
|
+
X-Ratelimit-Reset:
|
38
|
+
- '1501967489'
|
39
|
+
Cache-Control:
|
40
|
+
- public, max-age=60, s-maxage=60
|
41
|
+
Vary:
|
42
|
+
- Accept
|
43
|
+
Etag:
|
44
|
+
- W/"c4e64de253131e8983284594017e505c"
|
45
|
+
Last-Modified:
|
46
|
+
- Sat, 05 Aug 2017 20:39:01 GMT
|
47
|
+
X-Github-Media-Type:
|
48
|
+
- github.v3; format=json
|
49
|
+
Access-Control-Expose-Headers:
|
50
|
+
- ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset,
|
51
|
+
X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
|
52
|
+
Access-Control-Allow-Origin:
|
53
|
+
- "*"
|
54
|
+
Content-Security-Policy:
|
55
|
+
- default-src 'none'
|
56
|
+
Strict-Transport-Security:
|
57
|
+
- max-age=31536000; includeSubdomains; preload
|
58
|
+
X-Content-Type-Options:
|
59
|
+
- nosniff
|
60
|
+
X-Frame-Options:
|
61
|
+
- deny
|
62
|
+
X-Xss-Protection:
|
63
|
+
- 1; mode=block
|
64
|
+
X-Runtime-Rack:
|
65
|
+
- '0.044265'
|
66
|
+
X-Github-Request-Id:
|
67
|
+
- DA58:795D:BB06DF:178487C:598633E1
|
68
|
+
body:
|
69
|
+
encoding: ASCII-8BIT
|
70
|
+
string: '[{"sha":"40c01ab6ebec6cbd8ad9e521a732f941c169e557","commit":{"author":{"name":"Ellen
|
71
|
+
Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"committer":{"name":"Ellen
|
72
|
+
Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"message":"meep","tree":{"sha":"6911e0637822f44b83f04f47821adab56fdbc0b9","url":"https://api.github.com/repos/how-is/example-repository/git/trees/6911e0637822f44b83f04f47821adab56fdbc0b9"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comment_count":0},"url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","html_url":"https://github.com/how-is/example-repository/commit/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557/comments","author":{"login":"duckinator","id":39698,"avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"committer":{"login":"duckinator","id":39698,"avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"parents":[{"sha":"3794aa1c4b76623748faf280abe5760b76823162","url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162"}]},{"sha":"3794aa1c4b76623748faf280abe5760b76823162","commit":{"author":{"name":"fake
|
73
|
+
author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"committer":{"name":"fake
|
74
|
+
author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"message":"test
|
75
|
+
commit","tree":{"sha":"8286e548e330cfe01efcf7189f4df1fa53e777a7","url":"https://api.github.com/repos/how-is/example-repository/git/trees/8286e548e330cfe01efcf7189f4df1fa53e777a7"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/3794aa1c4b76623748faf280abe5760b76823162","comment_count":0},"url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162/comments","author":null,"committer":null,"parents":[{"sha":"9e29405efa433529b86722542b8fb4b34dfd9edd","url":"https://api.github.com/repos/how-is/example-repository/commits/9e29405efa433529b86722542b8fb4b34dfd9edd","html_url":"https://github.com/how-is/example-repository/commit/9e29405efa433529b86722542b8fb4b34dfd9edd"}]}]'
|
76
|
+
http_version:
|
77
|
+
recorded_at: Sat, 05 Aug 2017 21:08:49 GMT
|
78
|
+
- request:
|
79
|
+
method: get
|
80
|
+
uri: https://api.github.com/repos/how-is/example-repository/commits?author=me@duckie.co&until=2017-01-01
|
81
|
+
body:
|
82
|
+
encoding: US-ASCII
|
83
|
+
string: ''
|
84
|
+
headers:
|
85
|
+
Accept:
|
86
|
+
- application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1
|
87
|
+
Accept-Charset:
|
88
|
+
- utf-8
|
89
|
+
User-Agent:
|
90
|
+
- Github API Ruby Gem 0.17.0
|
91
|
+
Accept-Encoding:
|
92
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
93
|
+
response:
|
94
|
+
status:
|
95
|
+
code: 200
|
96
|
+
message: OK
|
97
|
+
headers:
|
98
|
+
Server:
|
99
|
+
- GitHub.com
|
100
|
+
Date:
|
101
|
+
- Sat, 05 Aug 2017 21:08:50 GMT
|
102
|
+
Content-Type:
|
103
|
+
- application/json; charset=utf-8
|
104
|
+
Transfer-Encoding:
|
105
|
+
- chunked
|
106
|
+
Status:
|
107
|
+
- 200 OK
|
108
|
+
X-Ratelimit-Limit:
|
109
|
+
- '60'
|
110
|
+
X-Ratelimit-Remaining:
|
111
|
+
- '4'
|
112
|
+
X-Ratelimit-Reset:
|
113
|
+
- '1501967489'
|
114
|
+
Cache-Control:
|
115
|
+
- public, max-age=60, s-maxage=60
|
116
|
+
Vary:
|
117
|
+
- Accept
|
118
|
+
Etag:
|
119
|
+
- W/"63ec50619f12827a37594ae87db65a6d"
|
120
|
+
Last-Modified:
|
121
|
+
- Sun, 07 Aug 2016 03:52:54 GMT
|
122
|
+
X-Github-Media-Type:
|
123
|
+
- github.v3; format=json
|
124
|
+
Access-Control-Expose-Headers:
|
125
|
+
- ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset,
|
126
|
+
X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
|
127
|
+
Access-Control-Allow-Origin:
|
128
|
+
- "*"
|
129
|
+
Content-Security-Policy:
|
130
|
+
- default-src 'none'
|
131
|
+
Strict-Transport-Security:
|
132
|
+
- max-age=31536000; includeSubdomains; preload
|
133
|
+
X-Content-Type-Options:
|
134
|
+
- nosniff
|
135
|
+
X-Frame-Options:
|
136
|
+
- deny
|
137
|
+
X-Xss-Protection:
|
138
|
+
- 1; mode=block
|
139
|
+
X-Runtime-Rack:
|
140
|
+
- '0.044928'
|
141
|
+
X-Github-Request-Id:
|
142
|
+
- DA59:795E:9CC15F:196C854:598633E2
|
143
|
+
body:
|
144
|
+
encoding: ASCII-8BIT
|
145
|
+
string: '[{"sha":"9e29405efa433529b86722542b8fb4b34dfd9edd","commit":{"author":{"name":"Ellen
|
146
|
+
Marie Dash","email":"me@duckie.co","date":"2016-08-07T03:52:54Z"},"committer":{"name":"Ellen
|
147
|
+
Marie Dash","email":"me@duckie.co","date":"2016-08-07T03:52:54Z"},"message":"Initial
|
148
|
+
commit","tree":{"sha":"7bee2a10cc8c7c036d5006fa0aacadd869d09c4e","url":"https://api.github.com/repos/how-is/example-repository/git/trees/7bee2a10cc8c7c036d5006fa0aacadd869d09c4e"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/9e29405efa433529b86722542b8fb4b34dfd9edd","comment_count":0},"url":"https://api.github.com/repos/how-is/example-repository/commits/9e29405efa433529b86722542b8fb4b34dfd9edd","html_url":"https://github.com/how-is/example-repository/commit/9e29405efa433529b86722542b8fb4b34dfd9edd","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/9e29405efa433529b86722542b8fb4b34dfd9edd/comments","author":{"login":"duckinator","id":39698,"avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"committer":{"login":"duckinator","id":39698,"avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"parents":[]}]'
|
149
|
+
http_version:
|
150
|
+
recorded_at: Sat, 05 Aug 2017 21:08:50 GMT
|
151
|
+
- request:
|
152
|
+
method: get
|
153
|
+
uri: https://api.github.com/repos/how-is/example-repository/commits?author=fake@duckinator.net&until=2017-01-01
|
154
|
+
body:
|
155
|
+
encoding: US-ASCII
|
156
|
+
string: ''
|
157
|
+
headers:
|
158
|
+
Accept:
|
159
|
+
- application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1
|
160
|
+
Accept-Charset:
|
161
|
+
- utf-8
|
162
|
+
User-Agent:
|
163
|
+
- Github API Ruby Gem 0.17.0
|
164
|
+
Accept-Encoding:
|
165
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
166
|
+
response:
|
167
|
+
status:
|
168
|
+
code: 200
|
169
|
+
message: OK
|
170
|
+
headers:
|
171
|
+
Server:
|
172
|
+
- GitHub.com
|
173
|
+
Date:
|
174
|
+
- Sat, 05 Aug 2017 21:08:51 GMT
|
175
|
+
Content-Type:
|
176
|
+
- application/json; charset=utf-8
|
177
|
+
Content-Length:
|
178
|
+
- '2'
|
179
|
+
Status:
|
180
|
+
- 200 OK
|
181
|
+
X-Ratelimit-Limit:
|
182
|
+
- '60'
|
183
|
+
X-Ratelimit-Remaining:
|
184
|
+
- '3'
|
185
|
+
X-Ratelimit-Reset:
|
186
|
+
- '1501967489'
|
187
|
+
Cache-Control:
|
188
|
+
- public, max-age=60, s-maxage=60
|
189
|
+
Vary:
|
190
|
+
- Accept
|
191
|
+
Etag:
|
192
|
+
- '"fafd06a937c504cece6726ba7201b0a1"'
|
193
|
+
X-Github-Media-Type:
|
194
|
+
- github.v3; format=json
|
195
|
+
Access-Control-Expose-Headers:
|
196
|
+
- ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset,
|
197
|
+
X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
|
198
|
+
Access-Control-Allow-Origin:
|
199
|
+
- "*"
|
200
|
+
Content-Security-Policy:
|
201
|
+
- default-src 'none'
|
202
|
+
Strict-Transport-Security:
|
203
|
+
- max-age=31536000; includeSubdomains; preload
|
204
|
+
X-Content-Type-Options:
|
205
|
+
- nosniff
|
206
|
+
X-Frame-Options:
|
207
|
+
- deny
|
208
|
+
X-Xss-Protection:
|
209
|
+
- 1; mode=block
|
210
|
+
X-Runtime-Rack:
|
211
|
+
- '0.033466'
|
212
|
+
X-Github-Request-Id:
|
213
|
+
- DA5A:795A:69CBD4:E6BF0D:598633E2
|
214
|
+
body:
|
215
|
+
encoding: UTF-8
|
216
|
+
string: "[]"
|
217
|
+
http_version:
|
218
|
+
recorded_at: Sat, 05 Aug 2017 21:08:51 GMT
|
219
|
+
recorded_with: VCR 3.0.3
|
data/how_is.gemspec
CHANGED
@@ -31,5 +31,6 @@ Gem::Specification.new do |spec|
|
|
31
31
|
spec.add_development_dependency "timecop", "~> 0.8.1"
|
32
32
|
spec.add_development_dependency "vcr", "~> 3.0"
|
33
33
|
spec.add_development_dependency "webmock"
|
34
|
-
spec.add_development_dependency "rubocop", "~> 0.
|
34
|
+
spec.add_development_dependency "rubocop", "~> 0.49.1"
|
35
|
+
spec.add_development_dependency "github_changelog_generator"
|
35
36
|
end
|
data/lib/how_is/analyzer.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
3
|
+
require "contracts"
|
4
|
+
require "ostruct"
|
5
|
+
require "date"
|
6
|
+
require "json"
|
7
7
|
|
8
8
|
class HowIs
|
9
9
|
##
|
@@ -11,11 +11,12 @@ class HowIs
|
|
11
11
|
class Analysis < OpenStruct
|
12
12
|
end
|
13
13
|
|
14
|
+
# Creates Analysis objects with input data formatted in useful ways.
|
14
15
|
class Analyzer
|
15
16
|
include Contracts::Core
|
16
17
|
|
17
18
|
##
|
18
|
-
# Raised when attempting to
|
19
|
+
# Raised when attempting to import to an unsupported format.
|
19
20
|
class UnsupportedImportFormat < StandardError
|
20
21
|
def initialize(format)
|
21
22
|
super("Unsupported import format: #{format}")
|
@@ -23,7 +24,7 @@ class HowIs
|
|
23
24
|
end
|
24
25
|
|
25
26
|
##
|
26
|
-
# Generates and returns an analysis.
|
27
|
+
# Generates and returns an analysis.
|
27
28
|
#
|
28
29
|
# @param data [Fetcher::Results] The results gathered by Fetcher.
|
29
30
|
# @param analysis_class (You don't need this.) A class to use instead of
|
@@ -43,7 +44,7 @@ class HowIs
|
|
43
44
|
number_of_pulls: pulls.length,
|
44
45
|
|
45
46
|
issues_with_label: with_label_links(num_with_label(issues), data.repository),
|
46
|
-
issues_with_no_label: {
|
47
|
+
issues_with_no_label: {"link" => nil, "total" => num_with_no_label(issues)},
|
47
48
|
|
48
49
|
average_issue_age: average_age_for(issues),
|
49
50
|
average_pull_age: average_age_for(pulls),
|
@@ -64,15 +65,15 @@ class HowIs
|
|
64
65
|
# @param data [Hash] The hash to generate an Analysis from.
|
65
66
|
def self.from_hash(data)
|
66
67
|
hash = data.map { |k, v|
|
67
|
-
v = DateTime.parse(v) if k.end_with?(
|
68
|
+
v = DateTime.parse(v) if k.end_with?("_date")
|
68
69
|
|
69
70
|
[k, v]
|
70
71
|
}.to_h
|
71
72
|
|
72
73
|
hash.keys.each do |key|
|
73
|
-
next unless hash[key].is_a?(Hash) && hash[key][
|
74
|
+
next unless hash[key].is_a?(Hash) && hash[key]["date"]
|
74
75
|
|
75
|
-
hash[key][
|
76
|
+
hash[key]["date"] = DateTime.parse(hash[key]["date"])
|
76
77
|
end
|
77
78
|
|
78
79
|
Analysis.new(hash)
|
@@ -90,10 +91,10 @@ class HowIs
|
|
90
91
|
|
91
92
|
hash = Hash.new(0)
|
92
93
|
issues_or_pulls.each do |iop|
|
93
|
-
next unless iop[
|
94
|
+
next unless iop["labels"]
|
94
95
|
|
95
|
-
iop[
|
96
|
-
hash[label[
|
96
|
+
iop["labels"].each do |label|
|
97
|
+
hash[label["name"]] += 1
|
97
98
|
end
|
98
99
|
end
|
99
100
|
hash
|
@@ -101,16 +102,16 @@ class HowIs
|
|
101
102
|
|
102
103
|
# Returns the number of issues with no label.
|
103
104
|
def num_with_no_label(issues)
|
104
|
-
issues.select { |x| x[
|
105
|
+
issues.select { |x| x["labels"].empty? }.length
|
105
106
|
end
|
106
107
|
|
107
108
|
# Given an Array of dates, average the timestamps and return the date that
|
108
109
|
# represents.
|
109
110
|
def average_date_for(issues_or_pulls)
|
110
|
-
timestamps = issues_or_pulls.map { |iop| Date.parse(iop[
|
111
|
+
timestamps = issues_or_pulls.map { |iop| Date.parse(iop["created_at"]).strftime("%s").to_i }
|
111
112
|
average_timestamp = timestamps.reduce(:+) / issues_or_pulls.length
|
112
113
|
|
113
|
-
DateTime.strptime(average_timestamp.to_s,
|
114
|
+
DateTime.strptime(average_timestamp.to_s, "%s")
|
114
115
|
end
|
115
116
|
|
116
117
|
# Given an Array of issues or pulls, return the average age of them.
|
@@ -118,32 +119,11 @@ class HowIs
|
|
118
119
|
def average_age_for(issues_or_pulls)
|
119
120
|
return nil if issues_or_pulls.empty?
|
120
121
|
|
121
|
-
ages = issues_or_pulls.map { |iop| time_ago_in_seconds(iop[
|
122
|
-
|
122
|
+
ages = issues_or_pulls.map { |iop| time_ago_in_seconds(iop["created_at"]) }
|
123
|
+
average_age_in_seconds = ages.reduce(:+) / ages.length
|
123
124
|
|
124
|
-
|
125
|
-
|
126
|
-
seconds_in_a_week = 604_800
|
127
|
-
seconds_in_a_day = 86_400
|
128
|
-
|
129
|
-
years = raw_average / seconds_in_a_year
|
130
|
-
years_remainder = raw_average % seconds_in_a_year
|
131
|
-
|
132
|
-
months = years_remainder / seconds_in_a_month
|
133
|
-
months_remainder = years_remainder % seconds_in_a_month
|
134
|
-
|
135
|
-
weeks = months_remainder / seconds_in_a_week
|
136
|
-
weeks_remainder = months_remainder % seconds_in_a_week
|
137
|
-
|
138
|
-
days = weeks_remainder / seconds_in_a_day
|
139
|
-
|
140
|
-
values = [
|
141
|
-
[years, "year"],
|
142
|
-
[months, "month"],
|
143
|
-
[weeks, "week"],
|
144
|
-
[days, "day"],
|
145
|
-
].reject { |(v, _)| v == 0 }.map { |(v, k)|
|
146
|
-
k += 's' if v != 1
|
125
|
+
values = period_pairs_for(average_age_in_seconds).reject { |(v, _)| v.zero? }.map { |(v, k)|
|
126
|
+
k += "s" if v != 1
|
147
127
|
[v, k]
|
148
128
|
}
|
149
129
|
|
@@ -160,7 +140,7 @@ class HowIs
|
|
160
140
|
end
|
161
141
|
|
162
142
|
def sort_iops_by_created_at(issues_or_pulls)
|
163
|
-
issues_or_pulls.sort_by { |x| DateTime.parse(x[
|
143
|
+
issues_or_pulls.sort_by { |x| DateTime.parse(x["created_at"]) }
|
164
144
|
end
|
165
145
|
|
166
146
|
# Given an Array of issues or pulls, return the oldest.
|
@@ -181,7 +161,7 @@ class HowIs
|
|
181
161
|
|
182
162
|
# Given an issue or PR, returns the date it was created.
|
183
163
|
def date_for(issue_or_pull)
|
184
|
-
DateTime.parse(issue_or_pull[
|
164
|
+
DateTime.parse(issue_or_pull["created_at"])
|
185
165
|
end
|
186
166
|
|
187
167
|
private
|
@@ -192,7 +172,7 @@ class HowIs
|
|
192
172
|
labels.map { |label, num_issues|
|
193
173
|
label_link = "https://github.com/#{repository}/issues?q=" + CGI.escape("is:open is:issue label:\"#{label}\"")
|
194
174
|
|
195
|
-
[label, {
|
175
|
+
[label, {"link" => label_link, "total" => num_issues}]
|
196
176
|
}.to_h
|
197
177
|
end
|
198
178
|
|
@@ -206,11 +186,37 @@ class HowIs
|
|
206
186
|
|
207
187
|
ret = {}
|
208
188
|
|
209
|
-
ret[
|
210
|
-
ret[
|
211
|
-
ret[
|
189
|
+
ret["html_url"] = iop["html_url"]
|
190
|
+
ret["number"] = iop["number"]
|
191
|
+
ret["date"] = date_for(iop)
|
212
192
|
|
213
193
|
ret
|
214
194
|
end
|
195
|
+
|
196
|
+
SECONDS_IN_A_YEAR = 31_556_926
|
197
|
+
SECONDS_IN_A_MONTH = 2_629_743
|
198
|
+
SECONDS_IN_A_WEEK = 604_800
|
199
|
+
SECONDS_IN_A_DAY = 86_400
|
200
|
+
|
201
|
+
# Calculates a list of pairs of value and period label.
|
202
|
+
#
|
203
|
+
# @param age_in_seconds [Float]
|
204
|
+
#
|
205
|
+
# @return [Array<Array>] The input age_in_seconds expressed as different
|
206
|
+
# units, as pairs of value and unit name.
|
207
|
+
def period_pairs_for(age_in_seconds)
|
208
|
+
years_remainder = age_in_seconds % SECONDS_IN_A_YEAR
|
209
|
+
|
210
|
+
months_remainder = years_remainder % SECONDS_IN_A_MONTH
|
211
|
+
|
212
|
+
weeks_remainder = months_remainder % SECONDS_IN_A_WEEK
|
213
|
+
|
214
|
+
[
|
215
|
+
[age_in_seconds / SECONDS_IN_A_YEAR, "year"],
|
216
|
+
[years_remainder / SECONDS_IN_A_MONTH, "month"],
|
217
|
+
[months_remainder / SECONDS_IN_A_WEEK, "week"],
|
218
|
+
[weeks_remainder / SECONDS_IN_A_DAY, "day"],
|
219
|
+
]
|
220
|
+
end
|
215
221
|
end
|
216
222
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class HowIs
|
4
|
+
# Investigates who is a new committer since given date
|
5
|
+
#
|
6
|
+
# # /repos/:owner/:repo/commits?since=<start date for the report>
|
7
|
+
class Contributions
|
8
|
+
# @param github [Github] configured github client
|
9
|
+
# @param since_date [String] A value which fits Repos.Commits "since" and
|
10
|
+
# "until" fields. This supports many formats, for
|
11
|
+
# example a timestamp in ISO 8601 format:
|
12
|
+
# YYYY-MM-DDTHH:MM:SSZ.
|
13
|
+
# @param user [String] GitHub user of repository
|
14
|
+
# @param repo [String] GitHub repository name
|
15
|
+
def initialize(github:, since_date:, user:, repo:)
|
16
|
+
@github = github
|
17
|
+
@since_date = since_date
|
18
|
+
@user = user
|
19
|
+
@repo = repo
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns a list of contributors that have zero commits before the @since_date.
|
23
|
+
#
|
24
|
+
# @return [Hash{String => Hash] Committers keyed by GitHub login name
|
25
|
+
def new_contributors
|
26
|
+
# author: GitHub login, name or email by which to filter by commit author.
|
27
|
+
all_contributors.select do |email, _committer|
|
28
|
+
@github.repos.commits.list(user: @user,
|
29
|
+
repo: @repo,
|
30
|
+
until: @since_date,
|
31
|
+
author: email).count.zero?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Hash{String => Hash}] Author information keyed by author's email
|
36
|
+
def all_contributors
|
37
|
+
commits.map { |api_response|
|
38
|
+
[api_response.commit.author.email, api_response.commit.author.to_h]
|
39
|
+
}.to_h
|
40
|
+
end
|
41
|
+
|
42
|
+
def commits
|
43
|
+
@github.repos.commits.list(user: @user, repo: @repo, since: @since_date)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/how_is/fetcher.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
3
|
+
require "contracts"
|
4
|
+
require "github_api"
|
5
|
+
require "how_is/pulse"
|
6
6
|
|
7
|
-
##
|
8
|
-
# Fetches data from GitHub.
|
9
7
|
class HowIs
|
8
|
+
##
|
9
|
+
# Fetches data from GitHub.
|
10
10
|
class Fetcher
|
11
11
|
include Contracts::Core
|
12
12
|
|
@@ -15,7 +15,7 @@ class HowIs
|
|
15
15
|
#
|
16
16
|
# Implemented as a class instead of passing around a Hash so that it can
|
17
17
|
# be more easily referenced by Contracts.
|
18
|
-
|
18
|
+
Results = Struct.new(:repository, :issues, :pulls, :pulse) do
|
19
19
|
include Contracts::Core
|
20
20
|
|
21
21
|
Contract String, C::ArrayOf[Hash], C::ArrayOf[Hash], String => nil
|
@@ -37,12 +37,12 @@ class HowIs
|
|
37
37
|
pulse = nil)
|
38
38
|
github ||= Github.new(auto_pagination: true)
|
39
39
|
pulse ||= HowIs::Pulse.new(repository)
|
40
|
-
user, repo = repository.split(
|
40
|
+
user, repo = repository.split("/", 2)
|
41
41
|
|
42
42
|
unless user && repo
|
43
|
-
raise HowIs::CLI::OptionsError,
|
44
|
-
|
45
|
-
|
43
|
+
raise HowIs::CLI::OptionsError, "To generate a report from GitHub, " \
|
44
|
+
"provide the repository " \
|
45
|
+
"username/project. Quitting!"
|
46
46
|
end
|
47
47
|
|
48
48
|
issues = github.issues.list user: user, repo: repo
|