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
@@ -1,69 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'fileutils'
|
4
|
-
|
5
|
-
require 'tty-config'
|
6
|
-
|
7
|
-
module Michael
|
8
|
-
module Models
|
9
|
-
class Configuration
|
10
|
-
CONFIG_ALLOWED_SYMBOLS = %i[set append fetch remove].freeze
|
11
|
-
|
12
|
-
def initialize(
|
13
|
-
config_dir: default_config_dir, config_name: default_filename
|
14
|
-
)
|
15
|
-
@config_dir = config_dir
|
16
|
-
@config_name = config_name
|
17
|
-
@config = create_config
|
18
|
-
end
|
19
|
-
|
20
|
-
def nuke
|
21
|
-
create_config.write(force: true)
|
22
|
-
self
|
23
|
-
end
|
24
|
-
|
25
|
-
def config_file_path
|
26
|
-
config_dir + '/' + config_name + '.yml'
|
27
|
-
end
|
28
|
-
|
29
|
-
def respond_to_missing?(symbol)
|
30
|
-
CONFIG_ALLOWED_SYMBOLS.include?(symbol)
|
31
|
-
end
|
32
|
-
|
33
|
-
def method_missing(symbol, *args)
|
34
|
-
return super unless CONFIG_ALLOWED_SYMBOLS.include?(symbol)
|
35
|
-
|
36
|
-
mkdir_once
|
37
|
-
config.read if config.exist?
|
38
|
-
resp = config.public_send(symbol, *args)
|
39
|
-
config.write(force: true)
|
40
|
-
|
41
|
-
resp
|
42
|
-
end
|
43
|
-
|
44
|
-
private
|
45
|
-
|
46
|
-
attr_reader :config_dir, :config_name, :config
|
47
|
-
|
48
|
-
def create_config
|
49
|
-
config = TTY::Config.new
|
50
|
-
config.filename = config_name
|
51
|
-
config.append_path(config_dir)
|
52
|
-
|
53
|
-
config
|
54
|
-
end
|
55
|
-
|
56
|
-
def default_config_dir
|
57
|
-
"#{ENV['HOME']}/.config/kudrykv/michael"
|
58
|
-
end
|
59
|
-
|
60
|
-
def default_filename
|
61
|
-
'config'
|
62
|
-
end
|
63
|
-
|
64
|
-
def mkdir_once
|
65
|
-
@mkdir_once ||= FileUtils.mkdir_p(config_dir)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
@@ -1,84 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'reviewer'
|
4
|
-
require_relative 'team'
|
5
|
-
|
6
|
-
module Michael
|
7
|
-
module Models
|
8
|
-
module Github
|
9
|
-
class PRWrapper
|
10
|
-
attr_accessor :statuses, :reviews
|
11
|
-
|
12
|
-
def initialize(pull_request)
|
13
|
-
@pr = pull_request
|
14
|
-
@statuses = []
|
15
|
-
@reviews = []
|
16
|
-
end
|
17
|
-
|
18
|
-
def title
|
19
|
-
pr[:title]
|
20
|
-
end
|
21
|
-
|
22
|
-
def number
|
23
|
-
pr[:number]
|
24
|
-
end
|
25
|
-
|
26
|
-
def author
|
27
|
-
pr[:user][:login]
|
28
|
-
end
|
29
|
-
|
30
|
-
def head_sha
|
31
|
-
pr[:head][:sha]
|
32
|
-
end
|
33
|
-
|
34
|
-
def reviewed?
|
35
|
-
reviewers.any? || teams.any?
|
36
|
-
end
|
37
|
-
|
38
|
-
def reviewers
|
39
|
-
pr[:requested_reviewers].map { |reviewer| Reviewer.new(reviewer) }
|
40
|
-
end
|
41
|
-
|
42
|
-
def teams
|
43
|
-
pr[:requested_teams].map { |team| Team.new(team) }
|
44
|
-
end
|
45
|
-
|
46
|
-
def labels
|
47
|
-
pr[:labels].map { |label| label[:name] }
|
48
|
-
end
|
49
|
-
|
50
|
-
def requested_changes
|
51
|
-
reviews.select(&:changes_requested?).map(&:author)
|
52
|
-
end
|
53
|
-
|
54
|
-
def commented_on_pr
|
55
|
-
reviews
|
56
|
-
.reject { |review| author == review.author }
|
57
|
-
.select(&:commented?).map(&:author)
|
58
|
-
end
|
59
|
-
|
60
|
-
def last_update_head?
|
61
|
-
pr_last_update = pr[:updated_at]
|
62
|
-
review_last_update = pr.reviews&.map(&:submitted_at)&.sort&.pop
|
63
|
-
|
64
|
-
return false unless review_last_update
|
65
|
-
|
66
|
-
pr_last_update > review_last_update
|
67
|
-
end
|
68
|
-
|
69
|
-
def last_updated_at
|
70
|
-
updates = [pr[:updated_at]]
|
71
|
-
|
72
|
-
updates.concat pr.statuses.map(&:updated_at) unless pr.statuses.nil? || pr.statuses.any?
|
73
|
-
updates.concat pr.reviews.map(&:submitted_at) unless pr.reviews.nil? || pr.reviews.any?
|
74
|
-
|
75
|
-
updates.sort.pop
|
76
|
-
end
|
77
|
-
|
78
|
-
private
|
79
|
-
|
80
|
-
attr_reader :pr
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
@@ -1,51 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'octokit'
|
4
|
-
|
5
|
-
require_relative 'octokit_initializer'
|
6
|
-
require_relative 'pr_wrapper'
|
7
|
-
require_relative 'review'
|
8
|
-
require_relative 'status'
|
9
|
-
|
10
|
-
module Michael
|
11
|
-
module Models
|
12
|
-
module Github
|
13
|
-
class PullRequest < OctokitInitializer
|
14
|
-
def process_repo(org_repo, hsh = {})
|
15
|
-
list = search(org_repo, hsh)
|
16
|
-
|
17
|
-
return {repo: org_repo, state: :failed} unless list
|
18
|
-
|
19
|
-
{repo: org_repo, state: :success, prs: list}
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
23
|
-
|
24
|
-
def search(org_repo, state: 'open', with_statuses: true, with_reviews: true)
|
25
|
-
octokit.pull_requests(org_repo, state: state).map do |pr|
|
26
|
-
pr = PRWrapper.new(pr)
|
27
|
-
pr.statuses = statuses(org_repo, pr.head_sha) if with_statuses
|
28
|
-
pr.reviews = reviews(org_repo, pr.number) if with_reviews
|
29
|
-
|
30
|
-
pr
|
31
|
-
end
|
32
|
-
rescue Octokit::InvalidRepository
|
33
|
-
false
|
34
|
-
end
|
35
|
-
|
36
|
-
def statuses(org_repo, ref)
|
37
|
-
combined = octokit.combined_status(org_repo, ref)
|
38
|
-
combined[:statuses]&.map { |status| Status.new(status) }
|
39
|
-
end
|
40
|
-
|
41
|
-
def reviews(org_repo, pr_number)
|
42
|
-
octokit
|
43
|
-
.pull_request_reviews(org_repo, pr_number)
|
44
|
-
.map { |review| Review.new(review) }
|
45
|
-
.group_by(&:author).to_a
|
46
|
-
.map { |_author, reviews| reviews.sort_by(&:submitted_at).pop }
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
@@ -1,41 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Michael
|
4
|
-
module Models
|
5
|
-
module Github
|
6
|
-
class Review
|
7
|
-
def initialize(review)
|
8
|
-
@review = review
|
9
|
-
end
|
10
|
-
|
11
|
-
def author
|
12
|
-
review[:user][:login]
|
13
|
-
end
|
14
|
-
|
15
|
-
def state
|
16
|
-
review[:state].to_sym
|
17
|
-
end
|
18
|
-
|
19
|
-
def submitted_at
|
20
|
-
review[:submitted_at]
|
21
|
-
end
|
22
|
-
|
23
|
-
def approved?
|
24
|
-
state == :APPROVED
|
25
|
-
end
|
26
|
-
|
27
|
-
def changes_requested?
|
28
|
-
state == :CHANGES_REQUESTED
|
29
|
-
end
|
30
|
-
|
31
|
-
def commented?
|
32
|
-
state == :COMMENTED
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
attr_reader :review
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
@@ -1,25 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Michael
|
4
|
-
module Models
|
5
|
-
module Github
|
6
|
-
class Status
|
7
|
-
def initialize(status)
|
8
|
-
@status = status
|
9
|
-
end
|
10
|
-
|
11
|
-
def state
|
12
|
-
status[:state].to_sym
|
13
|
-
end
|
14
|
-
|
15
|
-
def updated_at
|
16
|
-
status[:updated_at]
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
attr_reader :status
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
@@ -1,30 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'octokit'
|
4
|
-
|
5
|
-
require_relative '../configuration'
|
6
|
-
|
7
|
-
module Michael
|
8
|
-
module Models
|
9
|
-
module Github
|
10
|
-
class TokenValidator
|
11
|
-
class << self
|
12
|
-
def token_valid?(token)
|
13
|
-
scopes = Octokit::Client.new(access_token: token).scopes
|
14
|
-
|
15
|
-
return true if scopes.include?('repo')
|
16
|
-
|
17
|
-
puts 'Token should have `repo` scope'
|
18
|
-
false
|
19
|
-
rescue StandardError
|
20
|
-
false
|
21
|
-
end
|
22
|
-
|
23
|
-
def save_token(token)
|
24
|
-
Configuration.new.set(:token, value: token)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'octokit_initializer'
|
4
|
-
|
5
|
-
module Michael
|
6
|
-
module Models
|
7
|
-
module Github
|
8
|
-
class User < OctokitInitializer
|
9
|
-
def username
|
10
|
-
me[:login]
|
11
|
-
end
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
def me
|
16
|
-
@me ||= octokit.user
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
data/lib/michael/models/guard.rb
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../command'
|
4
|
-
require_relative 'github/token_validator'
|
5
|
-
|
6
|
-
module Michael
|
7
|
-
module Models
|
8
|
-
class Guard < Michael::Command
|
9
|
-
attr_reader :config, :repos_config
|
10
|
-
|
11
|
-
def initialize
|
12
|
-
@config = Configuration.new
|
13
|
-
@repos_config = Configuration.new(config_name: 'repositories')
|
14
|
-
|
15
|
-
token_valid = Github::TokenValidator.token_valid?(config.fetch(:token))
|
16
|
-
abort 'Your token is invalid' unless token_valid
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
@@ -1,116 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'pastel'
|
4
|
-
require 'ruby-duration'
|
5
|
-
|
6
|
-
module Michael
|
7
|
-
module Models
|
8
|
-
class PullRequestFormatter
|
9
|
-
class << self
|
10
|
-
def pretty(reponame, prs)
|
11
|
-
[
|
12
|
-
pastel.bold(reponame + ':'),
|
13
|
-
prs.map { |pr| PullRequestFormatter.new(pr).pretty }.join("\n")
|
14
|
-
].join("\n")
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def initialize(pr)
|
19
|
-
@pastel = Pastel.new
|
20
|
-
@pr = pr
|
21
|
-
end
|
22
|
-
|
23
|
-
def pretty
|
24
|
-
format(
|
25
|
-
'#%<number>s %<statuses>s %<reviews>s %<title>s %<variable>s',
|
26
|
-
number: pastel.bold(pr.number),
|
27
|
-
statuses: statuses_in_dots,
|
28
|
-
reviews: reviews_in_dots,
|
29
|
-
title: pr.title,
|
30
|
-
variable: variable_line
|
31
|
-
)
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
attr_reader :pastel, :pr
|
37
|
-
|
38
|
-
def statuses_in_dots
|
39
|
-
return '-' if pr.statuses.empty?
|
40
|
-
|
41
|
-
pr.statuses.map { |status| dot_status[status.state] }.join
|
42
|
-
end
|
43
|
-
|
44
|
-
def reviews_in_dots
|
45
|
-
return pastel.yellow('-') unless pr.reviewed? || pr.reviews.any?
|
46
|
-
return pastel.yellow('.') if pr.reviews.empty?
|
47
|
-
|
48
|
-
pr.reviews
|
49
|
-
.reject { |review| review.author == pr.author }
|
50
|
-
.map { |review| dot_status[review.state] }
|
51
|
-
.join
|
52
|
-
end
|
53
|
-
|
54
|
-
def variable_line
|
55
|
-
[
|
56
|
-
labels_stringified(pr.labels),
|
57
|
-
pastel.cyan(pr.author),
|
58
|
-
last_update,
|
59
|
-
who_req_changes_stringified,
|
60
|
-
who_commented_stringified,
|
61
|
-
].reject(&:empty?).join(' ')
|
62
|
-
end
|
63
|
-
|
64
|
-
def labels_stringified(labels)
|
65
|
-
return '' if labels.empty?
|
66
|
-
|
67
|
-
pastel.bold.yellow("[#{labels.join(', ')}]")
|
68
|
-
end
|
69
|
-
|
70
|
-
def who_commented_stringified
|
71
|
-
names = pr.commented_on_pr
|
72
|
-
return '' if names.empty?
|
73
|
-
|
74
|
-
'| Commented: ' + names.join(', ')
|
75
|
-
end
|
76
|
-
|
77
|
-
def who_req_changes_stringified
|
78
|
-
names = pr.requested_changes
|
79
|
-
return '' if names.empty?
|
80
|
-
|
81
|
-
'| ' + pastel.bold('Requested changes: ') << names.map { |name| pastel.underscore(name) }.join(', ')
|
82
|
-
end
|
83
|
-
|
84
|
-
def last_update
|
85
|
-
time_diff = Duration.new(Time.now - pr.last_updated_at)
|
86
|
-
'last update ' + last_update_when(time_diff) + ' ago'
|
87
|
-
end
|
88
|
-
|
89
|
-
def last_update_when(duration)
|
90
|
-
if duration.weeks.positive?
|
91
|
-
pastel.yellow.bold("#{duration.weeks} week(s)")
|
92
|
-
elsif duration.days.positive?
|
93
|
-
pastel.yellow("#{duration.days} day(s)")
|
94
|
-
elsif duration.hours.positive?
|
95
|
-
"#{duration.hours} hour(s)"
|
96
|
-
elsif duration.minutes.positive?
|
97
|
-
"#{duration.minutes} minute(s)"
|
98
|
-
else
|
99
|
-
'seconds'
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def dot_status
|
104
|
-
@dot_status ||= {
|
105
|
-
success: pastel.green('+'),
|
106
|
-
pending: pastel.yellow('.'),
|
107
|
-
error: pastel.red('x'),
|
108
|
-
failure: pastel.red('x'),
|
109
|
-
APPROVED: pastel.green('^'),
|
110
|
-
CHANGES_REQUESTED: pastel.red('X'),
|
111
|
-
COMMENTED: '?'
|
112
|
-
}
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|