inq 26.0.0

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