git_reflow 0.6.7 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +11 -9
  3. data/README.rdoc +3 -1
  4. data/bin/git-reflow +0 -11
  5. data/bin/gitreflow-common +1 -1
  6. data/git_reflow.gemspec +3 -2
  7. data/lib/git_reflow.rb +13 -60
  8. data/lib/git_reflow/commands/deliver.rb +1 -2
  9. data/lib/git_reflow/commands/start.rb +0 -6
  10. data/lib/git_reflow/config.rb +15 -14
  11. data/lib/git_reflow/git_server.rb +14 -4
  12. data/lib/git_reflow/git_server/base.rb +0 -39
  13. data/lib/git_reflow/git_server/bit_bucket.rb +15 -80
  14. data/lib/git_reflow/git_server/bit_bucket/pull_request.rb +84 -0
  15. data/lib/git_reflow/git_server/git_hub.rb +18 -75
  16. data/lib/git_reflow/git_server/git_hub/pull_request.rb +108 -0
  17. data/lib/git_reflow/git_server/pull_request.rb +97 -0
  18. data/lib/git_reflow/version.rb +1 -1
  19. data/spec/fixtures/issues/comment.json.erb +27 -0
  20. data/spec/fixtures/issues/comments.json.erb +15 -0
  21. data/spec/fixtures/pull_requests/comment.json.erb +45 -0
  22. data/spec/fixtures/pull_requests/comments.json.erb +15 -0
  23. data/spec/fixtures/pull_requests/commits.json +29 -0
  24. data/spec/fixtures/pull_requests/external_pull_request.json +145 -0
  25. data/spec/fixtures/pull_requests/pull_request.json +19 -0
  26. data/spec/fixtures/pull_requests/pull_request.json.erb +142 -0
  27. data/spec/fixtures/pull_requests/pull_requests.json +19 -0
  28. data/spec/fixtures/repositories/commit.json.erb +53 -0
  29. data/spec/fixtures/repositories/commits.json.erb +13 -0
  30. data/spec/git_reflow_spec.rb +32 -25
  31. data/spec/lib/git_reflow/config_spec.rb +22 -6
  32. data/spec/lib/git_server/bit_bucket_spec.rb +5 -34
  33. data/spec/lib/git_server/git_hub/pull_request_spec.rb +319 -0
  34. data/spec/lib/git_server/git_hub_spec.rb +17 -25
  35. data/spec/lib/git_server/pull_request_spec.rb +93 -0
  36. data/spec/support/command_line_helpers.rb +16 -1
  37. data/spec/support/fake_github.rb +128 -0
  38. data/spec/support/fixtures.rb +52 -6
  39. data/spec/support/github_helpers.rb +22 -12
  40. 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
- if project_only
40
- GitReflow::Config.add('reflow.local-projects', "#{self.class.remote_user}/#{self.class.remote_repo_name}")
41
- GitReflow::Config.set('reflow.git-server', 'GitHub', local: true)
42
- else
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 create_pull_request(options = {})
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 status
187
- colorized_statuses = { pending: :yellow, success: :green, error: :red, failure: :red }
188
- status.description.colorize( colorized_statuses[status.state.to_sym] )
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 comment_authors_for_pull_request(pull_request, options = {})
192
- all_comments = pull_request_comments(pull_request)
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 get_commited_time(commit_sha)
208
- last_commit = connection.repos.commits.find self.class.remote_user, self.class.remote_repo_name, commit_sha
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
@@ -1,3 +1,3 @@
1
1
  module GitReflow
2
- VERSION = "0.6.7"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -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
+ }