inq 26.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.cirrus.yml +84 -0
  3. data/.codeclimate.yml +23 -0
  4. data/.github_changelog_generator +2 -0
  5. data/.gitignore +15 -0
  6. data/.rspec +2 -0
  7. data/.rubocop.yml +260 -0
  8. data/.travis.yml +24 -0
  9. data/CHANGELOG.md +499 -0
  10. data/CODE_OF_CONDUCT.md +49 -0
  11. data/CONTRIBUTING.md +34 -0
  12. data/Gemfile +15 -0
  13. data/ISSUES.md +62 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +91 -0
  16. data/Rakefile +76 -0
  17. data/bin/console +14 -0
  18. data/bin/prerelease-generate-changelog +28 -0
  19. data/bin/setup +8 -0
  20. data/bors.toml +17 -0
  21. data/build-debug.rb +20 -0
  22. data/exe/inq +7 -0
  23. data/fixtures/vcr_cassettes/how-is-example-empty-repository.yml +597 -0
  24. data/fixtures/vcr_cassettes/how-is-example-repository.yml +768 -0
  25. data/fixtures/vcr_cassettes/how-is-from-config-frontmatter.yml +23940 -0
  26. data/fixtures/vcr_cassettes/how-is-how-is-travis-api-repos-builds.yml +66 -0
  27. data/fixtures/vcr_cassettes/how-is-with-config-file.yml +23940 -0
  28. data/fixtures/vcr_cassettes/how_is_contributions_additions_count.yml +247 -0
  29. data/fixtures/vcr_cassettes/how_is_contributions_all_contributors.yml +247 -0
  30. data/fixtures/vcr_cassettes/how_is_contributions_changed_files.yml +247 -0
  31. data/fixtures/vcr_cassettes/how_is_contributions_changes.yml +247 -0
  32. data/fixtures/vcr_cassettes/how_is_contributions_commits.yml +247 -0
  33. data/fixtures/vcr_cassettes/how_is_contributions_compare_url.yml +81 -0
  34. data/fixtures/vcr_cassettes/how_is_contributions_default_branch.yml +81 -0
  35. data/fixtures/vcr_cassettes/how_is_contributions_deletions_count.yml +247 -0
  36. data/fixtures/vcr_cassettes/how_is_contributions_new_contributors.yml +402 -0
  37. data/fixtures/vcr_cassettes/how_is_contributions_summary.yml +325 -0
  38. data/fixtures/vcr_cassettes/how_is_contributions_summary_2.yml +325 -0
  39. data/inq.gemspec +45 -0
  40. data/lib/inq.rb +63 -0
  41. data/lib/inq/cacheable.rb +71 -0
  42. data/lib/inq/cli.rb +135 -0
  43. data/lib/inq/config.rb +123 -0
  44. data/lib/inq/constants.rb +9 -0
  45. data/lib/inq/date_time_helpers.rb +48 -0
  46. data/lib/inq/exe.rb +66 -0
  47. data/lib/inq/frontmatter.rb +51 -0
  48. data/lib/inq/report.rb +140 -0
  49. data/lib/inq/report_collection.rb +113 -0
  50. data/lib/inq/sources.rb +11 -0
  51. data/lib/inq/sources/ci/appveyor.rb +87 -0
  52. data/lib/inq/sources/ci/travis.rb +159 -0
  53. data/lib/inq/sources/github.rb +57 -0
  54. data/lib/inq/sources/github/contributions.rb +204 -0
  55. data/lib/inq/sources/github/issue_fetcher.rb +148 -0
  56. data/lib/inq/sources/github/issues.rb +126 -0
  57. data/lib/inq/sources/github/pulls.rb +29 -0
  58. data/lib/inq/sources/github_helpers.rb +106 -0
  59. data/lib/inq/template.rb +9 -0
  60. data/lib/inq/templates/contributions_partial.html +1 -0
  61. data/lib/inq/templates/issues_or_pulls_partial.html +7 -0
  62. data/lib/inq/templates/new_contributors_partial.html +5 -0
  63. data/lib/inq/templates/report.html +19 -0
  64. data/lib/inq/templates/report_partial.html +12 -0
  65. data/lib/inq/text.rb +26 -0
  66. data/lib/inq/version.rb +6 -0
  67. metadata +263 -0
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "inq/version"
4
+ require "inq/date_time_helpers"
5
+ require "inq/sources/github"
6
+ require "inq/text"
7
+
8
+ module Inq
9
+ module Sources
10
+ class Github
11
+ ##
12
+ # Fetches raw data for GitHub issues.
13
+ class IssueFetcher
14
+ include Inq::DateTimeHelpers
15
+
16
+ END_LOOP = :terminate_graphql_loop
17
+
18
+ GRAPHQL_QUERY = <<~QUERY
19
+ repository(owner: %{user}, name: %{repo}) {
20
+ %{type}(first: %{chunk_size}%{after_str}, orderBy:{field: CREATED_AT, direction: ASC}) {
21
+ edges {
22
+ cursor
23
+ node {
24
+ number
25
+ createdAt
26
+ closedAt
27
+ updatedAt
28
+ state
29
+ title
30
+ url
31
+ labels(first: 100) {
32
+ nodes {
33
+ name
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ QUERY
41
+
42
+ CHUNK_SIZE = 100
43
+
44
+ attr_reader :type
45
+
46
+ # @param issues_source [Issues] Inq::Issues or Inq::Pulls instance for which to fetch issues
47
+ def initialize(issues_source)
48
+ @issues_source = issues_source
49
+ @cache = issues_source.cache
50
+ @github = Sources::Github.new(issues_source.config)
51
+ @repository = issues_source.config["repository"]
52
+ @user, @repo = @repository.split("/", 2)
53
+ @start_date = issues_source.start_date
54
+ @end_date = issues_source.end_date
55
+ @type = issues_source.type
56
+ end
57
+
58
+ def data
59
+ return @data if instance_variable_defined?(:@data)
60
+
61
+ @data = []
62
+ return @data if last_cursor.nil?
63
+
64
+ Inq::Text.print "Fetching #{@repository} #{@issues_source.pretty_type} data."
65
+
66
+ @data = @cache.cached("fetch-#{type}") do
67
+ data = []
68
+ after, data = fetch_issues(after, data) until after == END_LOOP
69
+ data.select(&method(:issue_is_relevant?))
70
+ end
71
+
72
+ Inq::Text.puts
73
+
74
+ @data
75
+ end
76
+
77
+ def issue_is_relevant?(issue)
78
+ if !issue["closedAt"].nil? && date_le(issue["closedAt"], @start_date)
79
+ false
80
+ else
81
+ date_ge(issue["createdAt"], @start_date) && date_le(issue["createdAt"], @end_date)
82
+ end
83
+ end
84
+
85
+ def last_cursor
86
+ return @last_cursor if instance_variable_defined?(:@last_cursor)
87
+
88
+ raw_data = @github.graphql <<~QUERY
89
+ repository(owner: #{@user.inspect}, name: #{@repo.inspect}) {
90
+ #{type}(last: 1, orderBy:{field: CREATED_AT, direction: ASC}) {
91
+ edges {
92
+ cursor
93
+ }
94
+ }
95
+ }
96
+ QUERY
97
+
98
+ edges = raw_data.dig("data", "repository", type, "edges")
99
+ @last_cursor =
100
+ if edges.nil? || edges.empty?
101
+ nil
102
+ else
103
+ edges.last["cursor"]
104
+ end
105
+ end
106
+
107
+ def fetch_issues(after, data)
108
+ Inq::Text.print "."
109
+
110
+ after_str = ", after: #{after.inspect}" unless after.nil?
111
+
112
+ query = build_query(@user, @repo, type, after_str)
113
+ raw_data = @github.graphql(query)
114
+ edges = raw_data.dig("data", "repository", type, "edges")
115
+
116
+ data += edge_nodes(edges)
117
+
118
+ next_cursor = edges.last["cursor"]
119
+ next_cursor = END_LOOP if next_cursor == last_cursor
120
+
121
+ [next_cursor, data]
122
+ end
123
+
124
+ def build_query(user, repo, type, after_str)
125
+ format(GRAPHQL_QUERY, {
126
+ user: user.inspect,
127
+ repo: repo.inspect,
128
+ type: type,
129
+ chunk_size: CHUNK_SIZE,
130
+ after_str: after_str,
131
+ })
132
+ end
133
+
134
+ def edge_nodes(edges)
135
+ return [] if edges.nil?
136
+ new_data = edges.map { |issue|
137
+ node = issue["node"]
138
+ node["labels"] = node["labels"]["nodes"]
139
+
140
+ node
141
+ }
142
+
143
+ new_data
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "inq/date_time_helpers"
4
+ require "inq/sources/github"
5
+ require "inq/sources/github_helpers"
6
+ require "inq/sources/github/issue_fetcher"
7
+ require "inq/template"
8
+ require "date"
9
+
10
+ module Inq
11
+ module Sources
12
+ class Github
13
+ ##
14
+ # Fetches various information about GitHub Issues.
15
+ class Issues
16
+ include Inq::DateTimeHelpers
17
+ include Inq::Sources::GithubHelpers
18
+
19
+ attr_reader :config, :start_date, :end_date, :cache
20
+
21
+ # @param repository [String] GitHub repository name, of the format user/repo.
22
+ # @param start_date [String] Start date for the report being generated.
23
+ # @param end_date [String] End date for the report being generated.
24
+ # @param cache [Cacheable] Instance of Inq::Cacheable to cache API calls
25
+ def initialize(config, start_date, end_date, cache)
26
+ @config = config
27
+ @cache = cache
28
+ @repository = config["repository"]
29
+ raise "#{self.class}.new() got nil repository." if @repository.nil?
30
+ @start_date = start_date
31
+ @end_date = end_date
32
+ end
33
+
34
+ def url(values = {})
35
+ defaults = {
36
+ "is" => singular_type,
37
+ "created" => "#{@start_date}..#{@end_date}",
38
+ }
39
+ values = defaults.merge(values)
40
+ raw_query = values.map { |k, v|
41
+ [k, v].join(":")
42
+ }.join(" ")
43
+
44
+ query = CGI.escape(raw_query)
45
+
46
+ "https://github.com/#{@repository}/#{url_suffix}?q=#{query}"
47
+ end
48
+
49
+ def average_age
50
+ average_age_for(data)
51
+ end
52
+
53
+ def oldest
54
+ result = oldest_for(data)
55
+ return {} if result.nil?
56
+
57
+ result["date"] = pretty_date(result["createdAt"])
58
+
59
+ result
60
+ end
61
+
62
+ def newest
63
+ result = newest_for(data)
64
+ return {} if result.nil?
65
+
66
+ result["date"] = pretty_date(result["createdAt"])
67
+
68
+ result
69
+ end
70
+
71
+ def summary
72
+ number_open = to_a.length
73
+ pretty_number = pluralize(pretty_type, number_open, zero_is_no: false)
74
+ was_were = (number_open == 1) ? "was" : "were"
75
+
76
+ "<p>A total of <a href=\"#{url}\">#{pretty_number}</a> #{was_were} opened during this period.</p>"
77
+ end
78
+
79
+ def to_html
80
+ return summary if to_a.empty?
81
+
82
+ Inq::Template.apply("issues_or_pulls_partial.html", {
83
+ summary: summary,
84
+ average_age: average_age,
85
+ pretty_type: pretty_type,
86
+
87
+ oldest_link: oldest["url"],
88
+ oldest_date: oldest["date"],
89
+
90
+ newest_link: newest["url"],
91
+ newest_date: newest["date"],
92
+ })
93
+ end
94
+
95
+ def to_a
96
+ obj_to_array_of_hashes(data)
97
+ end
98
+
99
+ def type
100
+ singular_type + "s"
101
+ end
102
+
103
+ def pretty_type
104
+ "issue"
105
+ end
106
+
107
+ private
108
+
109
+ def url_suffix
110
+ "issues"
111
+ end
112
+
113
+ def singular_type
114
+ "issue"
115
+ end
116
+
117
+ def data
118
+ return @data if instance_variable_defined?(:@data)
119
+
120
+ fetcher = IssueFetcher.new(self)
121
+ @data = fetcher.data
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "inq/sources/github/issues"
4
+
5
+ module Inq
6
+ module Sources
7
+ class Github
8
+ ##
9
+ # Fetches various information about GitHub Pull Requests
10
+ class Pulls < Issues
11
+ def url_suffix
12
+ "pulls"
13
+ end
14
+
15
+ def singular_type
16
+ "pull"
17
+ end
18
+
19
+ def type
20
+ "pullRequests"
21
+ end
22
+
23
+ def pretty_type
24
+ "pull request"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "inq/sources/github"
4
+ require "date"
5
+
6
+ module Inq
7
+ module Sources
8
+ ##
9
+ # Helper functions used by GitHub-related sources.
10
+ module GithubHelpers
11
+ def obj_to_array_of_hashes(object)
12
+ object.to_a.map(&:to_h)
13
+ end
14
+
15
+ # Given an Array of issues or pulls, return the average age of them.
16
+ # Returns nil if no issues or pulls are provided.
17
+ def average_age_for(issues_or_pulls)
18
+ return nil if issues_or_pulls.empty?
19
+
20
+ ages = issues_or_pulls.map { |iop| time_ago_in_seconds(iop["createdAt"]) }
21
+ average_age_in_seconds = ages.reduce(:+) / ages.length
22
+
23
+ values =
24
+ period_pairs_for(average_age_in_seconds) \
25
+ .reject { |(v, _)| v.zero? } \
26
+ .map { |(v, k)| pluralize(k, v) }
27
+
28
+ value = values[0, 2].join(" and ")
29
+
30
+ "approximately #{value}"
31
+ end
32
+
33
+ def sort_iops_by_created_at(issues_or_pulls)
34
+ issues_or_pulls.sort_by { |x| DateTime.parse(x["createdAt"]) }
35
+ end
36
+
37
+ # Given an Array of issues or pulls, return the oldest.
38
+ # Returns nil if no issues or pulls are provided.
39
+ def oldest_for(issues_or_pulls)
40
+ return nil if issues_or_pulls.empty?
41
+
42
+ sort_iops_by_created_at(issues_or_pulls).first
43
+ end
44
+
45
+ # Given an Array of issues or pulls, return the newest.
46
+ # Returns nil if no issues or pulls are provided.
47
+ def newest_for(issues_or_pulls)
48
+ return nil if issues_or_pulls.empty?
49
+
50
+ sort_iops_by_created_at(issues_or_pulls).last
51
+ end
52
+
53
+ private
54
+
55
+ # Returns how many seconds ago a date (as a String) was.
56
+ def time_ago_in_seconds(x)
57
+ DateTime.now.strftime("%s").to_i - DateTime.parse(x).strftime("%s").to_i
58
+ end
59
+
60
+ SECONDS_IN_A_YEAR = 31_556_926
61
+ SECONDS_IN_A_MONTH = 2_629_743
62
+ SECONDS_IN_A_WEEK = 604_800
63
+ SECONDS_IN_A_DAY = 86_400
64
+
65
+ # Calculates a list of pairs of value and period label.
66
+ #
67
+ # @param age_in_seconds [Float]
68
+ #
69
+ # @return [Array<Array>] The input age_in_seconds expressed as different
70
+ # units, as pairs of value and unit name.
71
+ def period_pairs_for(age_in_seconds)
72
+ years_remainder = age_in_seconds % SECONDS_IN_A_YEAR
73
+
74
+ months_remainder = years_remainder % SECONDS_IN_A_MONTH
75
+
76
+ weeks_remainder = months_remainder % SECONDS_IN_A_WEEK
77
+
78
+ [
79
+ [age_in_seconds / SECONDS_IN_A_YEAR, "year"],
80
+ [years_remainder / SECONDS_IN_A_MONTH, "month"],
81
+ [months_remainder / SECONDS_IN_A_WEEK, "week"],
82
+ [weeks_remainder / SECONDS_IN_A_DAY, "day"],
83
+ ]
84
+ end
85
+
86
+ def pluralize(string, number, zero_is_no: false)
87
+ number_str = number
88
+ number_str = "no" if number.zero? && zero_is_no
89
+
90
+ "#{number_str} #{string}#{(number == 1) ? '' : 's'}"
91
+ end
92
+
93
+ def pretty_date(date_or_str)
94
+ if date_or_str.is_a?(DateTime)
95
+ date = datetime_or_str
96
+ elsif date_or_str.is_a?(String)
97
+ date = DateTime.parse(date_or_str)
98
+ else
99
+ raise ArgumentError, "expected DateTime or String, got #{date_or_str.class}"
100
+ end
101
+
102
+ date.strftime("%b %d, %Y")
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "inq/version"
4
+ require "okay/template"
5
+
6
+ module Inq
7
+ # Provides basic templating functionality.
8
+ Template = Okay::Template.new(File.expand_path("./templates/", __dir__))
9
+ end
@@ -0,0 +1 @@
1
+ %{start_text}, %{user}/%{repo} gained <a href="%{compare_url}">%{new_commits}</a>, contributed by %{authors}. There %{additions_count_str} %{additions} and %{deletions} across %{changed_files}.
@@ -0,0 +1,7 @@
1
+ %{summary}
2
+
3
+ <ul>
4
+ <li>Average age: %{average_age}.</li>
5
+ <li><a href="%{oldest_link}">Oldest %{pretty_type}</a> was opened on %{oldest_date}.</li>
6
+ <li><a href="%{newest_link}">Newest %{pretty_type}</a> was opened on %{newest_date}.</li>
7
+ </ul>
@@ -0,0 +1,5 @@
1
+ <p>There %{was_were} %{number_of_new_contributors} new contributor%{contributor_s} during this report period.</p>
2
+
3
+ <ul>
4
+ %{list_items}
5
+ </ul>
@@ -0,0 +1,19 @@
1
+ %{frontmatter}<!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>%{title}</title>
5
+ <style>
6
+ body { font: sans-serif; }
7
+ main {
8
+ max-width: 600px;
9
+ max-width: 72ch;
10
+ margin: auto;
11
+ }
12
+ </style>
13
+ </head>
14
+ <body>
15
+ <main>
16
+ %{report}
17
+ </main>
18
+ </body>
19
+ </html>