michael 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|