michael 0.1.1 → 0.2.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/Gemfile.lock +6 -71
  4. data/exe/michael +2 -2
  5. data/lib/michael.rb +1 -0
  6. data/lib/michael/cli.rb +15 -1
  7. data/lib/michael/commands/auth.rb +11 -21
  8. data/lib/michael/commands/repos.rb +39 -2
  9. data/lib/michael/commands/repos/edit.rb +13 -9
  10. data/lib/michael/commands/repos/pull_requests.rb +38 -97
  11. data/lib/michael/constants.rb +7 -0
  12. data/lib/michael/models/pull_request.rb +139 -0
  13. data/lib/michael/models/repository.rb +45 -0
  14. data/lib/michael/models/review.rb +57 -0
  15. data/lib/michael/models/status.rb +43 -0
  16. data/lib/michael/models/user.rb +18 -0
  17. data/lib/michael/services/configuration.rb +44 -0
  18. data/lib/michael/{models/github/octokit_initializer.rb → services/github/initializer.rb} +9 -7
  19. data/lib/michael/services/github/pull_requests.rb +46 -0
  20. data/lib/michael/services/github/token.rb +41 -0
  21. data/lib/michael/services/github/users.rb +16 -0
  22. data/lib/michael/services/repositories.rb +34 -0
  23. data/lib/michael/version.rb +1 -1
  24. data/michael.gemspec +2 -18
  25. metadata +19 -245
  26. data/lib/michael/command.rb +0 -131
  27. data/lib/michael/models/configuration.rb +0 -69
  28. data/lib/michael/models/github/pr_wrapper.rb +0 -84
  29. data/lib/michael/models/github/pull_request.rb +0 -51
  30. data/lib/michael/models/github/review.rb +0 -41
  31. data/lib/michael/models/github/reviewer.rb +0 -17
  32. data/lib/michael/models/github/status.rb +0 -25
  33. data/lib/michael/models/github/team.rb +0 -17
  34. data/lib/michael/models/github/token_validator.rb +0 -30
  35. data/lib/michael/models/github/user.rb +0 -21
  36. data/lib/michael/models/guard.rb +0 -20
  37. data/lib/michael/models/pull_request_formatter.rb +0 -116
  38. data/lib/michael/models/repository_formatter.rb +0 -26
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Michael
4
+ CONFIG_DIR_ABSOLUTE_PATH = "#{ENV['HOME']}/.config/kudrykv/michael"
5
+ CONFIG_FILENAME = 'config'
6
+ CONFIG_REPOS_FILENAME = 'repositories'
7
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pastel'
4
+ require 'ruby-duration'
5
+
6
+ module Michael
7
+ module Models
8
+ class PullRequest
9
+ attr_reader :pull_request
10
+ attr_accessor :statuses, :reviews, :comments
11
+
12
+ def initialize(pull_request)
13
+ @pull_request = pull_request
14
+ end
15
+
16
+ def number
17
+ pull_request[:number]
18
+ end
19
+
20
+ def title
21
+ pull_request[:title]
22
+ end
23
+
24
+ def author
25
+ pull_request[:user][:login]
26
+ end
27
+
28
+ def head_sha
29
+ pull_request[:head][:sha]
30
+ end
31
+
32
+ def labels
33
+ pull_request[:labels].map(&:name)
34
+ end
35
+
36
+ def approved?
37
+ return false if reviews.nil?
38
+
39
+ reviews.any? && reviews.all?(&:approved?)
40
+ end
41
+
42
+ def needs_review?
43
+ pull_request[:requested_reviewers].any? || pull_request[:requested_teams].any?
44
+ end
45
+
46
+ def author?(name)
47
+ author == name
48
+ end
49
+
50
+ def actionable?(name)
51
+ return false if author?(name)
52
+ return true if reviews.map(&:author).none?(name)
53
+
54
+ last_update_head?
55
+ end
56
+
57
+ def pretty_print
58
+ [
59
+ pastel.bold("\##{number}"),
60
+ statuses_in_dots,
61
+ reviews_in_dots,
62
+ title,
63
+ labels.empty? ? nil : pastel.bold.yellow("[#{labels.join(', ')}]"),
64
+ pastel.cyan(author),
65
+ pretty_last_update(Time.now, last_updated_at),
66
+ requested_changes,
67
+ commented
68
+ ].reject(&:nil?).join(' ')
69
+ end
70
+
71
+ private
72
+
73
+ def last_updated_at
74
+ updates = [pull_request[:updated_at]]
75
+ updates.concat(statuses.map(&:updated_at)) if !statuses.nil? && statuses.any?
76
+ updates.concat(reviews.map(&:submitted_at)) if !reviews.nil? && reviews.any?
77
+
78
+ updates.sort.pop
79
+ end
80
+
81
+ def statuses_in_dots
82
+ return nil if statuses.nil?
83
+ return '-' if statuses.empty?
84
+
85
+ statuses.map(&:dot).join
86
+ end
87
+
88
+ def reviews_in_dots
89
+ return nil if reviews.nil?
90
+ return '-' if !needs_review? && reviews.none?
91
+ return pastel.yellow('.') if reviews.empty?
92
+
93
+ reviews.map(&:dot).join
94
+ end
95
+
96
+ def requested_changes
97
+ rc = reviews.select(&:changes_requested?).map(&:author)
98
+ return nil if rc.empty?
99
+
100
+ '| ' + pastel.bold('Requested changes: ') + rc.map { |n| pastel.underscore(n) }.join(', ')
101
+ end
102
+
103
+ def commented
104
+ rc = reviews.select(&:commented?).map(&:author)
105
+ return nil if rc.empty?
106
+
107
+ '| Commented: ' + rc.join(', ')
108
+ end
109
+
110
+ def pretty_last_update(bigger, smaller)
111
+ duration = Duration.new(bigger-smaller)
112
+
113
+ wh = if duration.weeks.positive?
114
+ pastel.yellow.bold("#{duration.weeks} week(s)")
115
+ elsif duration.days.positive?
116
+ pastel.yellow("#{duration.days} day(s)")
117
+ elsif duration.hours.positive?
118
+ "#{duration.hours} hour(s)"
119
+ elsif duration.minutes.positive?
120
+ "#{duration.minutes} minute(s)"
121
+ else
122
+ 'seconds'
123
+ end
124
+
125
+ "last update #{wh} ago"
126
+ end
127
+
128
+ def last_update_head?
129
+ updated_at = pull_request[:updated_at]
130
+ reviewed_at = reviews.map(&:submitted_at).sort.pop
131
+ updated_at > reviewed_at
132
+ end
133
+
134
+ def pastel
135
+ @pastel ||= Pastel.new
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pastel'
4
+
5
+ module Michael
6
+ module Models
7
+ class Repository
8
+ attr_reader :org_name, :prs
9
+
10
+ def initialize(org_name, prs: nil)
11
+ @org_name = org_name
12
+ @prs = prs
13
+ end
14
+
15
+ def broken?
16
+ prs.nil?
17
+ end
18
+
19
+ def has_prs?
20
+ !broken? && prs.any?
21
+ end
22
+
23
+ def ==(other)
24
+ org_name == other.org_name &&
25
+ prs == other.prs
26
+ end
27
+
28
+ def pretty_print
29
+ return pastel.black.on_red(org_name) if broken?
30
+ return org_name if prs.none?
31
+
32
+ [
33
+ pastel.bold(org_name + ':'),
34
+ prs.map(&:pretty_print).join("\n")
35
+ ].join("\n")
36
+ end
37
+
38
+ private
39
+
40
+ def pastel
41
+ @pastel ||= Pastel.new
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pastel'
4
+
5
+ module Michael
6
+ module Models
7
+ class Review
8
+ def initialize(review)
9
+ @review = review
10
+ end
11
+
12
+ def author
13
+ review[:user][:login]
14
+ end
15
+
16
+ def submitted_at
17
+ review[:submitted_at]
18
+ end
19
+
20
+ def state
21
+ review[:state].to_sym
22
+ end
23
+
24
+ def approved?
25
+ state == :APPROVED
26
+ end
27
+
28
+ def changes_requested?
29
+ state == :CHANGES_REQUESTED
30
+ end
31
+
32
+ def commented?
33
+ state == :COMMENTED
34
+ end
35
+
36
+ def dot
37
+ dot_status[state]
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :review
43
+
44
+ def dot_status
45
+ @dot_status ||= {
46
+ APPROVED: pastel.green('^'),
47
+ CHANGES_REQUESTED: pastel.red('X'),
48
+ COMMENTED: '?'
49
+ }
50
+ end
51
+
52
+ def pastel
53
+ @pastel ||= Pastel.new
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pastel'
4
+
5
+ module Michael
6
+ module Models
7
+ class Status
8
+ def initialize(status, pastel_params: {})
9
+ @status = status
10
+ @pastel_params = pastel_params
11
+ end
12
+
13
+ def state
14
+ status[:state].to_sym
15
+ end
16
+
17
+ def updated_at
18
+ status[:updated_at]
19
+ end
20
+
21
+ def dot
22
+ dot_status[state]
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :status
28
+
29
+ def dot_status
30
+ @dot_status ||= {
31
+ success: pastel.green('+'),
32
+ pending: pastel.yellow('.'),
33
+ error: pastel.red('x'),
34
+ failure: pastel.red('x')
35
+ }
36
+ end
37
+
38
+ def pastel
39
+ @pastel ||= Pastel.new(@pastel_params)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Michael
4
+ module Models
5
+ class User
6
+ def initialize(user)
7
+ @user = user
8
+ end
9
+
10
+ def username
11
+ user[:login]
12
+ end
13
+
14
+ private
15
+ attr_reader :user
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-config'
4
+
5
+ require_relative '../constants'
6
+
7
+ module Michael
8
+ module Services
9
+ class Configuration
10
+ def initialize(config)
11
+ raise Error, 'configuration is not initialized' if config.nil?
12
+
13
+ @config = config
14
+ end
15
+
16
+ def set(*keys, value: nil)
17
+ interact(:set, *keys, value: value)
18
+ end
19
+
20
+ def append(*values, to: nil)
21
+ interact(:append, *values, to: to)
22
+ end
23
+
24
+ def fetch(*keys, default: nil)
25
+ interact(:fetch, *keys, default: default)
26
+ end
27
+
28
+ def remove(*values, from: nil)
29
+ interact(:remove, *values, from: from)
30
+ end
31
+
32
+ private
33
+
34
+ def interact(symbol, *args)
35
+ config.read if config.exist?
36
+ resp = config.public_send(symbol, *args)
37
+ config.write(force: true)
38
+ resp
39
+ end
40
+
41
+ attr_reader :config
42
+ end
43
+ end
44
+ end
@@ -2,20 +2,22 @@
2
2
 
3
3
  require 'octokit'
4
4
 
5
- require_relative '../configuration'
5
+ require_relative 'token'
6
6
 
7
7
  module Michael
8
- module Models
8
+ module Services
9
9
  module Github
10
- class OctokitInitializer
10
+ class Initializer < Token
11
11
  attr_reader :octokit
12
12
 
13
- def initialize
14
- token = Configuration.new.fetch(:token)
13
+ def initialize(config)
14
+ super(config)
15
+
16
+ validate(token)
17
+
15
18
  @octokit = Octokit::Client.new(access_token: token)
16
- @octokit.auto_paginate = true
17
19
  end
18
20
  end
19
21
  end
20
22
  end
21
- end
23
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'initializer'
4
+ require_relative '../../models/pull_request'
5
+ require_relative '../../models/status'
6
+ require_relative '../../models/review'
7
+
8
+ module Michael
9
+ module Services
10
+ module Github
11
+ class PullRequests < Initializer
12
+ def search(org_repo, state: 'open')
13
+ octokit
14
+ .pull_requests(org_repo, state: state)
15
+ .map { |pr| process(org_repo, pr) }
16
+ rescue Octokit::InvalidRepository, Octokit::NotFound
17
+ nil
18
+ end
19
+
20
+ private
21
+
22
+ def process(org_repo, pull_request)
23
+ pr = Michael::Models::PullRequest.new(pull_request)
24
+
25
+ pr.statuses = statuses(org_repo, pr.head_sha)
26
+ pr.reviews = reviews(org_repo, pr.number)
27
+
28
+ pr
29
+ end
30
+
31
+ def statuses(org_repo, sha)
32
+ statuses = octokit.combined_status(org_repo, sha)[:statuses]
33
+ statuses.map { |s| Michael::Models::Status.new(s) }
34
+ end
35
+
36
+ def reviews(org_repo, pr_number)
37
+ octokit
38
+ .pull_request_reviews(org_repo, pr_number)
39
+ .map { |review| Michael::Models::Review.new(review) }
40
+ .group_by(&:author).to_a
41
+ .map { |_author, reviews| reviews.sort_by(&:submitted_at).pop }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'octokit'
4
+
5
+ require_relative '../../../michael'
6
+
7
+ module Michael
8
+ module Services
9
+ module Github
10
+ class Token
11
+ def initialize(config)
12
+ raise Michael::Error, 'config is nil' if config.nil?
13
+
14
+ @config = config
15
+ end
16
+
17
+ def validate(token)
18
+ raise Michael::Error, 'access token must contain `repo` scope' unless scopes(token).include?('repo')
19
+ end
20
+
21
+ def store(token)
22
+ config.set(:token, value: token)
23
+ end
24
+
25
+ def token
26
+ config.fetch(:token)
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :config
32
+
33
+ def scopes(token)
34
+ Octokit::Client.new(access_token: token).scopes
35
+ rescue Octokit::Unauthorized
36
+ raise Michael::Error, 'invalid access token'
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end