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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -1
- data/Gemfile.lock +6 -71
- data/exe/michael +2 -2
- data/lib/michael.rb +1 -0
- data/lib/michael/cli.rb +15 -1
- data/lib/michael/commands/auth.rb +11 -21
- data/lib/michael/commands/repos.rb +39 -2
- data/lib/michael/commands/repos/edit.rb +13 -9
- data/lib/michael/commands/repos/pull_requests.rb +38 -97
- data/lib/michael/constants.rb +7 -0
- data/lib/michael/models/pull_request.rb +139 -0
- data/lib/michael/models/repository.rb +45 -0
- data/lib/michael/models/review.rb +57 -0
- data/lib/michael/models/status.rb +43 -0
- data/lib/michael/models/user.rb +18 -0
- data/lib/michael/services/configuration.rb +44 -0
- data/lib/michael/{models/github/octokit_initializer.rb → services/github/initializer.rb} +9 -7
- data/lib/michael/services/github/pull_requests.rb +46 -0
- data/lib/michael/services/github/token.rb +41 -0
- data/lib/michael/services/github/users.rb +16 -0
- data/lib/michael/services/repositories.rb +34 -0
- data/lib/michael/version.rb +1 -1
- data/michael.gemspec +2 -18
- metadata +19 -245
- data/lib/michael/command.rb +0 -131
- data/lib/michael/models/configuration.rb +0 -69
- data/lib/michael/models/github/pr_wrapper.rb +0 -84
- data/lib/michael/models/github/pull_request.rb +0 -51
- data/lib/michael/models/github/review.rb +0 -41
- data/lib/michael/models/github/reviewer.rb +0 -17
- data/lib/michael/models/github/status.rb +0 -25
- data/lib/michael/models/github/team.rb +0 -17
- data/lib/michael/models/github/token_validator.rb +0 -30
- data/lib/michael/models/github/user.rb +0 -21
- data/lib/michael/models/guard.rb +0 -20
- data/lib/michael/models/pull_request_formatter.rb +0 -116
- data/lib/michael/models/repository_formatter.rb +0 -26
@@ -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,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 '
|
5
|
+
require_relative 'token'
|
6
6
|
|
7
7
|
module Michael
|
8
|
-
module
|
8
|
+
module Services
|
9
9
|
module Github
|
10
|
-
class
|
10
|
+
class Initializer < Token
|
11
11
|
attr_reader :octokit
|
12
12
|
|
13
|
-
def initialize
|
14
|
-
|
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
|