how_is 18.0.4 → 18.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|