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.
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
+ }