git_reflow 0.6.7 → 0.7.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/Gemfile.lock +11 -9
- data/README.rdoc +3 -1
- data/bin/git-reflow +0 -11
- data/bin/gitreflow-common +1 -1
- data/git_reflow.gemspec +3 -2
- data/lib/git_reflow.rb +13 -60
- data/lib/git_reflow/commands/deliver.rb +1 -2
- data/lib/git_reflow/commands/start.rb +0 -6
- data/lib/git_reflow/config.rb +15 -14
- data/lib/git_reflow/git_server.rb +14 -4
- data/lib/git_reflow/git_server/base.rb +0 -39
- data/lib/git_reflow/git_server/bit_bucket.rb +15 -80
- data/lib/git_reflow/git_server/bit_bucket/pull_request.rb +84 -0
- data/lib/git_reflow/git_server/git_hub.rb +18 -75
- data/lib/git_reflow/git_server/git_hub/pull_request.rb +108 -0
- data/lib/git_reflow/git_server/pull_request.rb +97 -0
- data/lib/git_reflow/version.rb +1 -1
- data/spec/fixtures/issues/comment.json.erb +27 -0
- data/spec/fixtures/issues/comments.json.erb +15 -0
- data/spec/fixtures/pull_requests/comment.json.erb +45 -0
- data/spec/fixtures/pull_requests/comments.json.erb +15 -0
- data/spec/fixtures/pull_requests/commits.json +29 -0
- data/spec/fixtures/pull_requests/external_pull_request.json +145 -0
- data/spec/fixtures/pull_requests/pull_request.json +19 -0
- data/spec/fixtures/pull_requests/pull_request.json.erb +142 -0
- data/spec/fixtures/pull_requests/pull_requests.json +19 -0
- data/spec/fixtures/repositories/commit.json.erb +53 -0
- data/spec/fixtures/repositories/commits.json.erb +13 -0
- data/spec/git_reflow_spec.rb +32 -25
- data/spec/lib/git_reflow/config_spec.rb +22 -6
- data/spec/lib/git_server/bit_bucket_spec.rb +5 -34
- data/spec/lib/git_server/git_hub/pull_request_spec.rb +319 -0
- data/spec/lib/git_server/git_hub_spec.rb +17 -25
- data/spec/lib/git_server/pull_request_spec.rb +93 -0
- data/spec/support/command_line_helpers.rb +16 -1
- data/spec/support/fake_github.rb +128 -0
- data/spec/support/fixtures.rb +52 -6
- data/spec/support/github_helpers.rb +22 -12
- metadata +47 -6
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'git_reflow/git_server/pull_request'
|
2
|
+
|
3
|
+
module GitReflow
|
4
|
+
module GitServer
|
5
|
+
class BitBucket
|
6
|
+
class PullRequest < GitReflow::GitServer::PullRequest
|
7
|
+
def initialize(attributes)
|
8
|
+
self.number = attributes.id
|
9
|
+
self.description = attributes.description
|
10
|
+
self.html_url = "#{attributes.source.repository.links.html.href}/pull-request/#{self.number}"
|
11
|
+
self.feature_branch_name = attributes.source.branch.name
|
12
|
+
self.base_branch_name = attributes.destination.branch.name
|
13
|
+
self.build_status = nil
|
14
|
+
self.source_object = attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.create(options = {})
|
18
|
+
self.new GitReflow.git_server.connection.repos.pull_requests.create(
|
19
|
+
GitReflow.git_server.class.remote_user,
|
20
|
+
GitReflow.git_server.class.remote_repo_name,
|
21
|
+
title: options[:title],
|
22
|
+
body: options[:body],
|
23
|
+
source: {
|
24
|
+
branch: { name: GitReflow.git_server.class.current_branch },
|
25
|
+
repository: { full_name: "#{GitReflow.git_server.class.remote_user}/#{GitReflow.git_server.class.remote_repo_name}" }
|
26
|
+
},
|
27
|
+
destination: {
|
28
|
+
branch: { name: options[:base] }
|
29
|
+
},
|
30
|
+
reviewers: [username: GitReflow.git_server.class.user])
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.find_open(to: 'master', from: GitReflow.git_server.class.current_branch)
|
34
|
+
begin
|
35
|
+
matching_pull = GitReflow.git_server.connection.repos.pull_requests.all(GitReflow.git_server.class.remote_user, GitReflow.git_server.class.remote_repo_name, limit: 1).select do |pr|
|
36
|
+
pr.source.branch.name == from and
|
37
|
+
pr.destination.branch.name == to
|
38
|
+
end.first
|
39
|
+
|
40
|
+
if matching_pull
|
41
|
+
self.new matching_pull
|
42
|
+
end
|
43
|
+
rescue ::BitBucket::Error::NotFound => e
|
44
|
+
GitReflow.git_server.say "No BitBucket repo found for #{GitReflow.git_server.class.remote_user}/#{GitReflow.git_server.class.remote_repo_name}", :error
|
45
|
+
rescue ::BitBucket::Error::Forbidden => e
|
46
|
+
GitReflow.git_server.say "You don't have API access to this repo", :error
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def commit_author
|
51
|
+
# use the author of the pull request
|
52
|
+
self.author.username
|
53
|
+
end
|
54
|
+
|
55
|
+
def comments
|
56
|
+
GitReflow.git_server.connection.repos.pull_requests.comments.all(GitReflow.git_server.class.remote_user, GitReflow.git_server.class.remote_repo_name, self.id)
|
57
|
+
end
|
58
|
+
|
59
|
+
def last_comment
|
60
|
+
last_comment = comments.first
|
61
|
+
return "" unless last_comment
|
62
|
+
"#{last_comment.content.raw}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def reviewers
|
66
|
+
return [] unless comments.size > 0
|
67
|
+
comments.map {|c| c.user.username }.uniq - [GitReflow.git_server.class.user]
|
68
|
+
end
|
69
|
+
|
70
|
+
def approvals
|
71
|
+
approved = []
|
72
|
+
|
73
|
+
GitReflow.git_server.connection.repos.pull_requests.activity(GitReflow.git_server.class.remote_user, GitReflow.git_server.class.remote_repo_name, self.id).each do |activity|
|
74
|
+
break unless activity.respond_to?(:approval) and activity.approval.user.username != GitReflow.git_server.class.user
|
75
|
+
approved |= [activity.approval.user.username]
|
76
|
+
end
|
77
|
+
|
78
|
+
approved
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -4,21 +4,11 @@ require 'git_reflow/git_helpers'
|
|
4
4
|
module GitReflow
|
5
5
|
module GitServer
|
6
6
|
class GitHub < Base
|
7
|
+
require_relative 'git_hub/pull_request'
|
8
|
+
|
7
9
|
extend GitHelpers
|
8
10
|
include Sandbox
|
9
11
|
|
10
|
-
class PullRequest < Base::PullRequest
|
11
|
-
def initialize(attributes)
|
12
|
-
self.number = attributes.number
|
13
|
-
self.description = attributes.body
|
14
|
-
self.html_url = attributes.html_url
|
15
|
-
self.feature_branch_name = attributes.head.label
|
16
|
-
self.base_branch_name = attributes.base.label
|
17
|
-
self.build_status = attributes.head.sha
|
18
|
-
self.source_object = attributes
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
12
|
attr_accessor :connection
|
23
13
|
|
24
14
|
def initialize(config_options = {})
|
@@ -36,13 +26,10 @@ module GitReflow
|
|
36
26
|
self.class.site_url = gh_site_url
|
37
27
|
self.class.api_endpoint = gh_api_endpoint
|
38
28
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
GitReflow::Config.unset('reflow.local-projects', value: "#{self.class.remote_user}/#{self.class.remote_repo_name}")
|
44
|
-
GitReflow::Config.set('reflow.git-server', 'GitHub')
|
45
|
-
end
|
29
|
+
# We remove any existing setup first, then setup our required config settings
|
30
|
+
GitReflow::Config.unset('reflow.local-projects', value: "#{self.class.remote_user}/#{self.class.remote_repo_name}")
|
31
|
+
GitReflow::Config.add('reflow.local-projects', "#{self.class.remote_user}/#{self.class.remote_repo_name}") if project_only
|
32
|
+
GitReflow::Config.set('reflow.git-server', 'GitHub', local: project_only)
|
46
33
|
end
|
47
34
|
|
48
35
|
def self.connection
|
@@ -144,69 +131,25 @@ module GitReflow
|
|
144
131
|
@connection
|
145
132
|
end
|
146
133
|
|
147
|
-
def
|
148
|
-
pull_request = connection.pull_requests.create(self.class.remote_user, self.class.remote_repo_name,
|
149
|
-
title: options[:title],
|
150
|
-
body: options[:body],
|
151
|
-
head: "#{self.class.remote_user}:#{self.class.current_branch}",
|
152
|
-
base: options[:base])
|
153
|
-
end
|
154
|
-
|
155
|
-
def find_open_pull_request(options = {})
|
156
|
-
matching_pull = connection.pull_requests.all(self.class.remote_user, self.class.remote_repo_name, base: options[:to], head: "#{self.class.remote_user}:#{options[:from]}", :state => 'open').first
|
157
|
-
if matching_pull
|
158
|
-
PullRequest.new matching_pull
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def reviewers(pull_request)
|
163
|
-
comment_authors_for_pull_request(pull_request)
|
164
|
-
end
|
165
|
-
|
166
|
-
def approvals(pull_request)
|
167
|
-
pull_last_committed_at = get_commited_time(pull_request.head.sha)
|
168
|
-
lgtm_authors = comment_authors_for_pull_request(pull_request, :with => LGTM, :after => pull_last_committed_at)
|
169
|
-
end
|
170
|
-
|
171
|
-
def pull_request_comments(pull_request)
|
172
|
-
comments = connection.issues.comments.all self.class.remote_user, self.class.remote_repo_name, number: pull_request.number
|
173
|
-
review_comments = connection.pull_requests.comments.all self.class.remote_user, self.class.remote_repo_name, number: pull_request.number
|
174
|
-
|
175
|
-
review_comments.to_a + comments.to_a
|
176
|
-
end
|
177
|
-
|
178
|
-
def last_comment_for_pull_request(pull_request)
|
179
|
-
"#{pull_request_comments(pull_request).last.body.inspect}"
|
180
|
-
end
|
181
|
-
|
182
|
-
def get_build_status sha
|
134
|
+
def get_build_status(sha)
|
183
135
|
connection.repos.statuses.all(self.class.remote_user, self.class.remote_repo_name, sha).first
|
184
136
|
end
|
185
137
|
|
186
|
-
def colorized_build_description
|
187
|
-
colorized_statuses = {
|
188
|
-
|
138
|
+
def colorized_build_description(state, description)
|
139
|
+
colorized_statuses = {
|
140
|
+
pending: :yellow,
|
141
|
+
success: :green,
|
142
|
+
error: :red,
|
143
|
+
failure: :red }
|
144
|
+
description.colorize( colorized_statuses[state.to_sym] )
|
189
145
|
end
|
190
146
|
|
191
|
-
def
|
192
|
-
|
193
|
-
comment_authors = []
|
194
|
-
|
195
|
-
all_comments.each do |comment|
|
196
|
-
next if options[:after] and Time.parse(comment.created_at) < options[:after]
|
197
|
-
if (options[:with].nil? or comment[:body] =~ options[:with])
|
198
|
-
comment_authors |= [comment.user.login]
|
199
|
-
end
|
200
|
-
end
|
201
|
-
|
202
|
-
# remove the current user from the list to check
|
203
|
-
comment_authors -= [self.class.remote_user]
|
204
|
-
comment_authors.uniq
|
147
|
+
def create_pull_request(options = {})
|
148
|
+
PullRequest.create(options)
|
205
149
|
end
|
206
150
|
|
207
|
-
def
|
208
|
-
|
209
|
-
Time.parse last_commit.commit.author[:date]
|
151
|
+
def find_open_pull_request(options = {})
|
152
|
+
PullRequest.find_open(options)
|
210
153
|
end
|
211
154
|
|
212
155
|
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'git_reflow/git_server/pull_request'
|
2
|
+
|
3
|
+
module GitReflow
|
4
|
+
module GitServer
|
5
|
+
class GitHub
|
6
|
+
class PullRequest < GitReflow::GitServer::PullRequest
|
7
|
+
def initialize(attributes)
|
8
|
+
self.number = attributes.number
|
9
|
+
self.description = attributes[:body]
|
10
|
+
self.html_url = attributes.html_url
|
11
|
+
self.feature_branch_name = attributes.head.label
|
12
|
+
self.base_branch_name = attributes.base.label
|
13
|
+
self.source_object = attributes
|
14
|
+
self.build_status = build.state
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.create(options = {})
|
18
|
+
self.new(GitReflow.git_server.connection.pull_requests.create(
|
19
|
+
GitReflow.git_server.class.remote_user,
|
20
|
+
GitReflow.git_server.class.remote_repo_name,
|
21
|
+
title: options[:title],
|
22
|
+
body: options[:body],
|
23
|
+
head: "#{GitReflow.git_server.class.remote_user}:#{GitReflow.git_server.class.current_branch}",
|
24
|
+
base: options[:base]))
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.find_open(to: 'master', from: GitReflow.git_server.class.current_branch)
|
28
|
+
matching_pull = GitReflow.git_server.connection.pull_requests.all(GitReflow.git_server.class.remote_user, GitReflow.git_server.class.remote_repo_name, base: to, head: "#{GitReflow.git_server.class.remote_user}:#{from}", state: 'open').first
|
29
|
+
if matching_pull
|
30
|
+
self.new matching_pull
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# override attr_reader for auto-updates
|
35
|
+
def build_status
|
36
|
+
@build_status ||= build.state
|
37
|
+
end
|
38
|
+
|
39
|
+
def commit_author
|
40
|
+
begin
|
41
|
+
username, branch = base.label.split(':')
|
42
|
+
first_commit = GitReflow.git_server.connection.pull_requests.commits(username, GitReflow.git_server.class.remote_repo_name, number.to_s).first
|
43
|
+
"#{first_commit.commit.author.name} <#{first_commit.commit.author.email}>".strip
|
44
|
+
rescue Github::Error::NotFound
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def reviewers
|
50
|
+
comment_authors
|
51
|
+
end
|
52
|
+
|
53
|
+
def approvals
|
54
|
+
pull_last_committed_at = get_committed_time(self.head.sha)
|
55
|
+
comment_authors(with: LGTM, after: pull_last_committed_at)
|
56
|
+
end
|
57
|
+
|
58
|
+
def comments
|
59
|
+
comments = GitReflow.git_server.connection.issues.comments.all GitReflow.git_server.class.remote_user, GitReflow.git_server.class.remote_repo_name, number: self.number
|
60
|
+
review_comments = GitReflow.git_server.connection.pull_requests.comments.all GitReflow.git_server.class.remote_user, GitReflow.git_server.class.remote_repo_name, number: self.number
|
61
|
+
|
62
|
+
review_comments.to_a + comments.to_a
|
63
|
+
end
|
64
|
+
|
65
|
+
def last_comment
|
66
|
+
"#{comments.last.body.inspect}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def build
|
70
|
+
github_build_status = GitReflow.git_server.get_build_status(self.head.sha)
|
71
|
+
build_status_object = Struct.new(:state, :description, :url)
|
72
|
+
if github_build_status
|
73
|
+
build_status_object.new(
|
74
|
+
github_build_status.state,
|
75
|
+
github_build_status.description,
|
76
|
+
github_build_status.target_url
|
77
|
+
)
|
78
|
+
else
|
79
|
+
build_status_object.new
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def comment_authors(with: nil, after: nil)
|
86
|
+
comment_authors = []
|
87
|
+
|
88
|
+
comments.each do |comment|
|
89
|
+
next if after and Time.parse(comment.created_at) < after
|
90
|
+
if (with.nil? or comment[:body] =~ with)
|
91
|
+
comment_authors |= [comment.user.login]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# remove the current user from the list to check
|
96
|
+
comment_authors -= [self.user.login]
|
97
|
+
comment_authors.uniq
|
98
|
+
end
|
99
|
+
|
100
|
+
def get_committed_time(commit_sha)
|
101
|
+
last_commit = GitReflow.git_server.connection.repos.commits.find GitReflow.git_server.class.remote_user, GitReflow.git_server.class.remote_repo_name, commit_sha
|
102
|
+
Time.parse last_commit.commit.author[:date]
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module GitReflow
|
2
|
+
module GitServer
|
3
|
+
class PullRequest
|
4
|
+
attr_accessor :description, :html_url, :feature_branch_name, :base_branch_name, :build_status, :source_object, :number
|
5
|
+
|
6
|
+
def initialize(attributes)
|
7
|
+
raise "PullRequest#initialize must be implemented"
|
8
|
+
end
|
9
|
+
|
10
|
+
def commit_author
|
11
|
+
raise "#{self.class.to_s}#commit_author method must be implemented"
|
12
|
+
end
|
13
|
+
|
14
|
+
def comments
|
15
|
+
raise "#{self.class.to_s}#comments method must be implemented"
|
16
|
+
end
|
17
|
+
|
18
|
+
def has_comments?
|
19
|
+
comments.count > 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def last_comment
|
23
|
+
raise "#{self.class.to_s}#last_comment_for method must be implemented"
|
24
|
+
end
|
25
|
+
|
26
|
+
def reviewers
|
27
|
+
raise "#{self.class.to_s}#reviewers method must be implemented"
|
28
|
+
end
|
29
|
+
|
30
|
+
def approvals
|
31
|
+
raise "#{self.class.to_s}#approvals method must be implemented"
|
32
|
+
end
|
33
|
+
|
34
|
+
def reviewers_pending_response
|
35
|
+
reviewers - approvals
|
36
|
+
end
|
37
|
+
|
38
|
+
def good_to_merge?(force: false)
|
39
|
+
return true if force
|
40
|
+
has_comments_or_approvals = (has_comments? or approvals.any?)
|
41
|
+
|
42
|
+
force == true or (
|
43
|
+
(build_status.nil? or build_status == "success") and
|
44
|
+
(has_comments_or_approvals and reviewers_pending_response.empty?))
|
45
|
+
end
|
46
|
+
|
47
|
+
def display_pull_request_summary
|
48
|
+
summary_data = {
|
49
|
+
"branches" => "#{self.feature_branch_name} -> #{self.base_branch_name}",
|
50
|
+
"number" => self.number,
|
51
|
+
"url" => self.html_url
|
52
|
+
}
|
53
|
+
|
54
|
+
notices = ""
|
55
|
+
reviewed_by = []
|
56
|
+
|
57
|
+
# check for CI build status
|
58
|
+
if self.build_status
|
59
|
+
notices << "[notice] Your build status is not successful: #{self.build.url}.\n" unless self.build.state == "success"
|
60
|
+
summary_data.merge!( "Build status" => GitReflow.git_server.colorized_build_description(self.build.state, self.build.description) )
|
61
|
+
end
|
62
|
+
|
63
|
+
# check for needed lgtm's
|
64
|
+
if self.reviewers.any?
|
65
|
+
reviewed_by = self.reviewers.map {|author| author.colorize(:red) }
|
66
|
+
summary_data.merge!("Last comment" => self.last_comment)
|
67
|
+
|
68
|
+
if self.approvals.any?
|
69
|
+
reviewed_by.map! { |author| approvals.include?(author.uncolorize) ? author.colorize(:green) : author }
|
70
|
+
end
|
71
|
+
|
72
|
+
notices << "[notice] You still need a LGTM from: #{reviewers_pending_response.join(', ')}\n" if reviewers_pending_response.any?
|
73
|
+
else
|
74
|
+
notices << "[notice] No one has reviewed your pull request.\n"
|
75
|
+
end
|
76
|
+
|
77
|
+
summary_data['reviewed by'] = reviewed_by.join(', ')
|
78
|
+
|
79
|
+
padding_size = summary_data.keys.max_by(&:size).size + 2
|
80
|
+
summary_data.keys.sort.each do |name|
|
81
|
+
string_format = " %-#{padding_size}s %s\n"
|
82
|
+
printf string_format, "#{name}:", summary_data[name]
|
83
|
+
end
|
84
|
+
|
85
|
+
puts "\n#{notices}" unless notices.empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
def method_missing(method_sym, *arguments, &block)
|
89
|
+
if source_object and source_object.respond_to? method_sym
|
90
|
+
source_object.send method_sym
|
91
|
+
else
|
92
|
+
super
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/git_reflow/version.rb
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
{
|
2
|
+
"id": <%= id || 1 %>,
|
3
|
+
"url": "https://api.github.com/repos/<%= repo_owner %>/<%= repo_name %>/issues/comments/<%= pull_request_number %>",
|
4
|
+
"html_url": "https://github.com/<%= repo_owner %>/<%= repo_name %>/issues/<%= pull_request_number %>#issuecomment-1",
|
5
|
+
"body": "<%= body || "Hmmm..." %>",
|
6
|
+
"user": {
|
7
|
+
"login": "<%= author %>",
|
8
|
+
"id": 1,
|
9
|
+
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
10
|
+
"gravatar_id": "somehexcode",
|
11
|
+
"url": "https://api.github.com/users/<%= author %>",
|
12
|
+
"html_url": "https://github.com/<%= author %>",
|
13
|
+
"followers_url": "https://api.github.com/users/<%= author %>/followers",
|
14
|
+
"following_url": "https://api.github.com/users/<%= author %>/following{/other_user}",
|
15
|
+
"gists_url": "https://api.github.com/users/<%= author %>/gists{/gist_id}",
|
16
|
+
"starred_url": "https://api.github.com/users/<%= author %>/starred{/owner}{/repo}",
|
17
|
+
"subscriptions_url": "https://api.github.com/users/<%= author %>/subscriptions",
|
18
|
+
"organizations_url": "https://api.github.com/users/<%= author %>/orgs",
|
19
|
+
"repos_url": "https://api.github.com/users/<%= author %>/repos",
|
20
|
+
"events_url": "https://api.github.com/users/<%= author %>/events{/privacy}",
|
21
|
+
"received_events_url": "https://api.github.com/users/<%= author %>/received_events",
|
22
|
+
"type": "User",
|
23
|
+
"site_admin": false
|
24
|
+
},
|
25
|
+
"created_at": "<%= created_at %>",
|
26
|
+
"updated_at": "2011-04-14T16:00:49Z"
|
27
|
+
}
|