git_reflow 0.4.2 → 0.5.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.
@@ -2,12 +2,16 @@ module GitReflow
2
2
  module Config
3
3
  extend self
4
4
 
5
- def get(key)
6
- if cached_key_value = instance_variable_get(:"@#{key.tr('.-', '_')}")
5
+ def get(key, options = {reload: false})
6
+ if options[:reload] == false and cached_key_value = instance_variable_get(:"@#{key.tr('.-', '_')}")
7
7
  cached_key_value
8
8
  else
9
- new_value = GitReflow::Sandbox.run "git config --get #{key}", loud: false
10
- instance_variable_set(:"@#{key.tr('.-', '_')}", new_value)
9
+ if options[:all]
10
+ new_value = GitReflow::Sandbox.run "git config --get-all #{key}", loud: false
11
+ else
12
+ new_value = GitReflow::Sandbox.run "git config --get #{key}", loud: false
13
+ end
14
+ instance_variable_set(:"@#{key.tr('.-', '_')}", new_value.strip)
11
15
  end
12
16
  end
13
17
 
@@ -19,5 +23,22 @@ module GitReflow
19
23
  GitReflow::Sandbox.run "git config --global --replace-all #{key} \"#{value}\"", loud: false
20
24
  end
21
25
  end
26
+
27
+ def unset(key, options = { local: false })
28
+ value = (options[:value].nil?) ? "" : "\"#{options[:value]}\""
29
+ if options.delete(:local)
30
+ GitReflow::Sandbox.run "git config --unset #{key} #{value}", loud: false
31
+ else
32
+ GitReflow::Sandbox.run "git config --global --unset #{key} #{value}", loud: false
33
+ end
34
+ end
35
+
36
+ def add(key, value, options = { local: false })
37
+ if options.delete(:local)
38
+ GitReflow::Sandbox.run "git config --add #{key} \"#{value}\"", loud: false
39
+ else
40
+ GitReflow::Sandbox.run "git config --global --add #{key} \"#{value}\"", loud: false
41
+ end
42
+ end
22
43
  end
23
44
  end
@@ -2,12 +2,27 @@ require 'git_reflow/config'
2
2
 
3
3
  module GitReflow
4
4
  class GitServer::Base
5
- @@connection = nil
6
- @@project_only = false
5
+ extend GitHelpers
7
6
 
8
- def initialize(options)
9
- @@project_only = !!options.delete(:project_only)
7
+ @@connection = nil
8
+
9
+ class PullRequest
10
+ attr_accessor :description, :html_url, :feature_branch_name, :base_branch_name, :build_status, :source_object
10
11
 
12
+ def initialize(attributes)
13
+ raise "PullRequest#initialize must be implemented"
14
+ end
15
+
16
+ def method_missing(method_sym, *arguments, &block)
17
+ if source_object and source_object.respond_to? method_sym
18
+ source_object.send method_sym
19
+ else
20
+ super
21
+ end
22
+ end
23
+ end
24
+
25
+ def initialize(options)
11
26
  site_url = self.class.site_url
12
27
  api_endpoint = self.class.api_endpoint
13
28
 
@@ -17,14 +32,6 @@ module GitReflow
17
32
  authenticate
18
33
  end
19
34
 
20
- def authenticate
21
- raise "#{self.class.to_s}#authenticate method must be implemented"
22
- end
23
-
24
- def find_pull_request(options)
25
- raise "#{self.class.to_s}#find_pull_request(options) method must be implemented"
26
- end
27
-
28
35
  def self.connection
29
36
  raise "#{self.class.to_s}.connection method must be implemented"
30
37
  end
@@ -33,14 +40,6 @@ module GitReflow
33
40
  raise "#{self.class.to_s}.user method must be implemented"
34
41
  end
35
42
 
36
- def self.oauth_token
37
- raise "#{self.class.to_s}.oauth_token method must be implemented"
38
- end
39
-
40
- def self.oauth_token=(oauth_token)
41
- raise "#{self.class.to_s}.oauth_token= method must be implemented"
42
- end
43
-
44
43
  def self.api_endpoint
45
44
  raise "#{self.class.to_s}.api_endpoint method must be implemented"
46
45
  end
@@ -57,10 +56,22 @@ module GitReflow
57
56
  raise "#{self.class.to_s}.site_url= method must be implemented"
58
57
  end
59
58
 
59
+ def self.project_only?
60
+ GitReflow::Config.get("reflow.local-projects", all: true).include? "#{remote_user}/#{remote_repo_name}"
61
+ end
62
+
60
63
  def connection
61
64
  @connection ||= self.class.connection
62
65
  end
63
66
 
67
+ def authenticate
68
+ raise "#{self.class.to_s}#authenticate method must be implemented"
69
+ end
70
+
71
+ def find_open_pull_request(options)
72
+ raise "#{self.class.to_s}#find_open_pull_request(options) method must be implemented"
73
+ end
74
+
64
75
  def pull_request_comments(pull_request)
65
76
  raise "#{self.class.to_s}#pull_request_comments(pull_request) method must be implemented"
66
77
  end
@@ -69,6 +80,10 @@ module GitReflow
69
80
  pull_request_comments(pull_request).count > 0
70
81
  end
71
82
 
83
+ def last_comment_for_pull_request(pull_request)
84
+ raise "#{self.class.to_s}#last_comment_for_pull_request(pull_request) method must be implemented"
85
+ end
86
+
72
87
  def get_build_status sha
73
88
  raise "#{self.class.to_s}#get_build_status(sha) method must be implemented"
74
89
  end
@@ -77,12 +92,16 @@ module GitReflow
77
92
  raise "#{self.class.to_s}#colorized_build_description(status) method must be implemented"
78
93
  end
79
94
 
80
- def find_authors_of_open_pull_request_comments(pull_request)
81
- raise "#{self.class.to_s}#find_authors_of_open_pull_request_comments(pull_request) method must be implemented"
95
+ def reviewers(pull_request)
96
+ raise "#{self.class.to_s}#reviewers(pull_request) method must be implemented"
97
+ end
98
+
99
+ def approvals(pull_request)
100
+ raise "#{self.class.to_s}#approvals(pull_request) method must be implemented"
82
101
  end
83
102
 
84
- def get_commited_time(commit_sha)
85
- raise "#{self.class.to_s}#get_commited_time(commit_sha) method must be implemented"
103
+ def reviewers_pending_response(pull_request)
104
+ reviewers(pull_request) - approvals(pull_request)
86
105
  end
87
106
  end
88
107
  end
@@ -0,0 +1,166 @@
1
+ require 'bitbucket_rest_api'
2
+ require 'git_reflow/git_helpers'
3
+
4
+ module GitReflow
5
+ module GitServer
6
+ class BitBucket < Base
7
+
8
+ class PullRequest < Base::PullRequest
9
+ def initialize(attributes)
10
+ self.description = attributes.description
11
+ self.source_object = attributes
12
+ self.number = attributes.id
13
+ self.html_url = "#{attributes.source.repository.links.html.href}/pull-request/#{self.number}"
14
+ self.feature_branch_name = attributes.source.branch.name
15
+ self.base_branch_name = attributes.destination.branch.name
16
+ self.build_status = nil
17
+ end
18
+ end
19
+
20
+ attr_accessor :connection
21
+
22
+ def initialize(config_options = {})
23
+ project_only = !!config_options.delete(:project_only)
24
+
25
+ if project_only
26
+ GitReflow::Config.add('reflow.local-projects', "#{self.class.remote_user}/#{self.class.remote_repo_name}")
27
+ GitReflow::Config.set('reflow.git-server', 'BitBucket', local: true)
28
+ else
29
+ GitReflow::Config.unset('reflow.local-projects', value: "#{self.class.remote_user}/#{self.class.remote_repo_name}")
30
+ GitReflow::Config.set('reflow.git-server', 'BitBucket')
31
+ end
32
+ end
33
+
34
+ def self.connection
35
+ if api_key_setup?
36
+ @connection ||= ::BitBucket.new login: remote_user, password: api_key
37
+ end
38
+ end
39
+
40
+ def self.api_endpoint
41
+ endpoint = GitReflow::Config.get("bitbucket.endpoint")
42
+ (endpoint.length > 0) ? endpoint : ::BitBucket::Configuration::DEFAULT_ENDPOINT
43
+ end
44
+
45
+ def self.site_url
46
+ site_url = GitReflow::Config.get("bitbucket.site")
47
+ (site_url.length > 0) ? site_url : 'https://bitbucket.org'
48
+ end
49
+
50
+ def self.api_key
51
+ GitReflow::Config.get("bitbucket.api-key", reload: true)
52
+ end
53
+
54
+ def self.api_key=(key)
55
+ GitReflow::Config.set("bitbucket.api-key", key, local: project_only?)
56
+ end
57
+ def self.api_key_setup?
58
+ (self.api_key.length > 0)
59
+ end
60
+
61
+ def self.user
62
+ GitReflow::Config.get('bitbucket.user')
63
+ end
64
+
65
+ def self.user=(bitbucket_user)
66
+ GitReflow::Config.set('bitbucket.user', bitbucket_user, local: project_only?)
67
+ end
68
+
69
+ def authenticate(options = {silent: false})
70
+ begin
71
+ if connection and self.class.api_key_setup?
72
+ unless options[:silent]
73
+ puts "\nYour BitBucket account was already setup with:"
74
+ puts "\tUser Name: #{self.class.user}"
75
+ end
76
+ else
77
+ self.class.user = options[:user] || ask("Please enter your BitBucket username: ")
78
+ puts "\nIn order to connect your BitBucket account,"
79
+ puts "you'll need to generate an API key for your team"
80
+ puts "Visit #{self.class.site_url}/account/user/#{self.class.remote_user}/api-key/, to generate it\n"
81
+ self.class.api_key = ask("Please enter your team's API key: ")
82
+ connection.repos.all(self.class.remote_user).count
83
+ self.class.say "Connected to BitBucket\!", :success
84
+ end
85
+ rescue ::BitBucket::Error::Unauthorized => e
86
+ GitReflow::Config.unset('bitbucket.api-key', local: self.class.project_only?)
87
+ self.class.say "Invalid API key for team #{self.class.remote_user}.", :error
88
+ end
89
+ end
90
+
91
+ def connection
92
+ @connection ||= self.class.connection
93
+ end
94
+
95
+ def create_pull_request(options = {})
96
+ PullRequest.new connection.repos.pull_requests.create(self.class.remote_user, self.class.remote_repo_name,
97
+ title: options[:title],
98
+ body: options[:body],
99
+ source: {
100
+ branch: { name: self.class.current_branch },
101
+ repository: { full_name: "#{self.class.remote_user}/#{self.class.remote_repo_name}" }
102
+ },
103
+ destination: {
104
+ branch: { name: options[:base] }
105
+ },
106
+ reviewers: [username: self.class.user])
107
+ end
108
+
109
+ def find_open_pull_request(options = {})
110
+ begin
111
+ matching_pull = connection.repos.pull_requests.all(self.class.remote_user, self.class.remote_repo_name, limit: 1).select do |pr|
112
+ pr.source.branch.name == options[:from] and
113
+ pr.destination.branch.name == options[:to]
114
+ end.first
115
+
116
+ if matching_pull
117
+ PullRequest.new matching_pull
118
+ end
119
+ rescue ::BitBucket::Error::NotFound => e
120
+ self.class.say "No BitBucket repo found for #{self.class.remote_user}/#{self.class.remote_repo_name}", :error
121
+ rescue ::BitBucket::Error::Forbidden => e
122
+ self.class.say "You don't have API access to this repo", :error
123
+ end
124
+ end
125
+
126
+ def pull_request_comments(pull_request)
127
+ connection.repos.pull_requests.comments.all(self.class.remote_user, self.class.remote_repo_name, pull_request.id)
128
+ end
129
+
130
+ def last_comment_for_pull_request(pull_request)
131
+ last_comment = pull_request_comments(pull_request).first
132
+ return "" unless last_comment
133
+ "#{pull_request_comments(pull_request).first.content.raw}"
134
+ end
135
+
136
+ def get_build_status sha
137
+ # BitBucket does not currently support build status via API
138
+ # for updates: https://bitbucket.org/site/master/issue/8548/better-ci-integration-add-a-build-status
139
+ return nil
140
+ end
141
+
142
+ def colorized_build_description status
143
+ ""
144
+ end
145
+
146
+ def reviewers(pull_request)
147
+ comments = pull_request_comments(pull_request)
148
+
149
+ return [] unless comments.size > 0
150
+ comments.map {|c| c.user.username } - [self.class.user]
151
+ end
152
+
153
+ def approvals(pull_request)
154
+ approved = []
155
+
156
+ connection.repos.pull_requests.activity(self.class.remote_user, self.class.remote_repo_name, pull_request.id).each do |activity|
157
+ break unless activity.respond_to?(:approval) and activity.approval.user.username != self.class.user
158
+ approved |= [activity.approval.user.username]
159
+ end
160
+
161
+ approved
162
+ end
163
+
164
+ end
165
+ end
166
+ end
@@ -4,21 +4,29 @@ require 'git_reflow/git_helpers'
4
4
  module GitReflow
5
5
  module GitServer
6
6
  class GitHub < Base
7
- include GitHelpers
7
+ extend GitHelpers
8
+
9
+ class PullRequest < Base::PullRequest
10
+ def initialize(attributes)
11
+ self.description = attributes.body
12
+ self.html_url = attributes.html_url
13
+ self.feature_branch_name = attributes.head.label
14
+ self.base_branch_name = attributes.base.label
15
+ self.build_status = attributes.head.sha
16
+ self.source_object = attributes
17
+ end
18
+ end
8
19
 
9
20
  attr_accessor :connection
10
21
 
11
- @@project_only = false
12
- @@using_enterprise = false
13
-
14
22
  def initialize(config_options = {})
15
- @@project_only = !!config_options.delete(:project_only)
16
- @@using_enterprise = !!config_options.delete(:enterprise)
23
+ project_only = !!config_options.delete(:project_only)
24
+ using_enterprise = !!config_options.delete(:enterprise)
17
25
 
18
26
  gh_site_url = self.class.site_url
19
27
  gh_api_endpoint = self.class.api_endpoint
20
-
21
- if @@using_enterprise
28
+
29
+ if using_enterprise
22
30
  gh_site_url = ask("Please enter your Enterprise site URL (e.g. https://github.company.com):")
23
31
  gh_api_endpoint = ask("Please enter your Enterprise API endpoint (e.g. https://github.company.com/api/v3):")
24
32
  end
@@ -26,13 +34,66 @@ module GitReflow
26
34
  self.class.site_url = gh_site_url
27
35
  self.class.api_endpoint = gh_api_endpoint
28
36
 
29
- if @@project_only
37
+ if project_only
38
+ GitReflow::Config.add('reflow.local-projects', "#{self.class.remote_user}/#{self.class.remote_repo_name}")
30
39
  GitReflow::Config.set('reflow.git-server', 'GitHub', local: true)
31
40
  else
41
+ GitReflow::Config.unset('reflow.local-projects', value: "#{self.class.remote_user}/#{self.class.remote_repo_name}")
32
42
  GitReflow::Config.set('reflow.git-server', 'GitHub')
33
43
  end
34
44
  end
35
45
 
46
+ def self.connection
47
+ if self.oauth_token.length > 0
48
+ @connection ||= ::Github.new do |config|
49
+ config.oauth_token = GitServer::GitHub.oauth_token
50
+ config.endpoint = GitServer::GitHub.api_endpoint
51
+ config.site = GitServer::GitHub.site_url
52
+ end
53
+ end
54
+ end
55
+
56
+ def self.user
57
+ GitReflow::Config.get('github.user')
58
+ end
59
+
60
+ def self.user=(github_user)
61
+ GitReflow::Config.set('github.user', github_user, local: project_only?)
62
+ end
63
+
64
+ def self.oauth_token
65
+ GitReflow::Config.get('github.oauth-token')
66
+ end
67
+
68
+ def self.oauth_token=(oauth_token)
69
+ GitReflow::Config.set('github.oauth-token', oauth_token, local: project_only?)
70
+ oauth_token
71
+ end
72
+
73
+ def self.api_endpoint
74
+ endpoint = "#{GitReflow::Config.get('github.endpoint')}".strip
75
+ (endpoint.length > 0) ? endpoint : ::Github.endpoint
76
+ end
77
+
78
+ def self.api_endpoint=(api_endpoint)
79
+ GitReflow::Config.set("github.endpoint", api_endpoint, local: project_only?)
80
+ api_endpoint
81
+ end
82
+
83
+ def self.site_url
84
+ site_url = "#{GitReflow::Config.get('github.site')}".strip
85
+ (site_url.length > 0) ? site_url : ::Github.site
86
+ end
87
+
88
+ def self.site_url=(site_url)
89
+ GitReflow::Config.set("github.site", site_url, local: project_only?)
90
+ site_url
91
+ end
92
+
93
+ def connection
94
+ @connection ||= self.class.connection
95
+ end
96
+
36
97
  def authenticate(options = {silent: false})
37
98
  if connection and self.class.oauth_token.length > 0
38
99
  unless options[:silent]
@@ -82,77 +143,42 @@ module GitReflow
82
143
  end
83
144
 
84
145
  def create_pull_request(options = {})
85
- pull_request = connection.pull_requests.create(remote_user, remote_repo_name,
146
+ pull_request = connection.pull_requests.create(self.class.remote_user, self.class.remote_repo_name,
86
147
  title: options[:title],
87
148
  body: options[:body],
88
- head: "#{remote_user}:#{current_branch}",
149
+ head: "#{self.class.remote_user}:#{self.class.current_branch}",
89
150
  base: options[:base])
90
151
  end
91
152
 
92
- def find_pull_request(options = {})
93
- connection.pull_requests.all(remote_user, remote_repo_name, base: options[:to], head: "#{remote_user}:#{options[:from]}", :state => 'open').first
94
- end
95
-
96
- def self.connection
97
- if self.oauth_token.length > 0
98
- @connection ||= ::Github.new do |config|
99
- config.oauth_token = GitServer::GitHub.oauth_token
100
- config.endpoint = GitServer::GitHub.api_endpoint
101
- config.site = GitServer::GitHub.site_url
102
- end
153
+ def find_open_pull_request(options = {})
154
+ 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
155
+ if matching_pull
156
+ PullRequest.new matching_pull
103
157
  end
104
158
  end
105
159
 
106
- def self.user
107
- GitReflow::Config.get('github.user')
160
+ def reviewers(pull_request)
161
+ comment_authors_for_pull_request(pull_request)
108
162
  end
109
163
 
110
- def self.user=(github_user)
111
- GitReflow::Config.set('github.user', github_user, local: @@project_only)
112
- end
113
-
114
- def self.oauth_token
115
- GitReflow::Config.get('github.oauth-token')
116
- end
117
-
118
- def self.oauth_token=(oauth_token)
119
- GitReflow::Config.set('github.oauth-token', oauth_token, local: @@project_only)
120
- oauth_token
121
- end
122
-
123
- def self.api_endpoint
124
- endpoint = GitReflow::Config.get('github.endpoint')
125
- (endpoint.length > 0) ? endpoint : ::Github.endpoint
126
- end
127
-
128
- def self.api_endpoint=(api_endpoint)
129
- GitReflow::Config.set("github.endpoint", api_endpoint, local: @@project_only)
130
- api_endpoint
131
- end
132
-
133
- def self.site_url
134
- site_url = GitReflow::Config.get('github.site')
135
- (site_url.length > 0) ? site_url : ::Github.site
136
- end
137
-
138
- def self.site_url=(site_url)
139
- GitReflow::Config.set("github.site", site_url, local: @@project_only)
140
- site_url
141
- end
142
-
143
- def connection
144
- @connection ||= self.class.connection
164
+ def approvals(pull_request)
165
+ pull_last_committed_at = get_commited_time(pull_request.head.sha)
166
+ lgtm_authors = comment_authors_for_pull_request(pull_request, :with => LGTM, :after => pull_last_committed_at)
145
167
  end
146
168
 
147
169
  def pull_request_comments(pull_request)
148
- comments = connection.issues.comments.all remote_user, remote_repo_name, number: pull_request.number
149
- review_comments = connection.pull_requests.comments.all remote_user, remote_repo_name, number: pull_request.number
170
+ comments = connection.issues.comments.all self.class.remote_user, self.class.remote_repo_name, number: pull_request.number
171
+ review_comments = connection.pull_requests.comments.all self.class.remote_user, self.class.remote_repo_name, number: pull_request.number
150
172
 
151
173
  review_comments.to_a + comments.to_a
152
174
  end
153
175
 
176
+ def last_comment_for_pull_request(pull_request)
177
+ "#{pull_request_comments(pull_request).last.body.inspect}"
178
+ end
179
+
154
180
  def get_build_status sha
155
- connection.repos.statuses.all(remote_user, remote_repo_name, sha).first
181
+ connection.repos.statuses.all(self.class.remote_user, self.class.remote_repo_name, sha).first
156
182
  end
157
183
 
158
184
  def colorized_build_description status
@@ -160,15 +186,6 @@ module GitReflow
160
186
  status.description.colorize( colorized_statuses[status.state.to_sym] )
161
187
  end
162
188
 
163
- def find_authors_of_open_pull_request_comments(pull_request)
164
- # first we'll gather all the authors that have commented on the pull request
165
- pull_last_committed_at = get_commited_time(pull_request.head.sha)
166
- comment_authors = comment_authors_for_pull_request(pull_request)
167
- lgtm_authors = comment_authors_for_pull_request(pull_request, :with => LGTM, :after => pull_last_committed_at)
168
-
169
- comment_authors - lgtm_authors
170
- end
171
-
172
189
  def comment_authors_for_pull_request(pull_request, options = {})
173
190
  all_comments = pull_request_comments(pull_request)
174
191
  comment_authors = []
@@ -185,7 +202,7 @@ module GitReflow
185
202
  end
186
203
 
187
204
  def get_commited_time(commit_sha)
188
- last_commit = connection.repos.commits.find remote_user, remote_repo_name, commit_sha
205
+ last_commit = connection.repos.commits.find self.class.remote_user, self.class.remote_repo_name, commit_sha
189
206
  Time.parse last_commit.commit.author[:date]
190
207
  end
191
208