git-review 2.0.0.alpha → 2.0.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 +7 -0
- data/bin/git-review +18 -9
- data/lib/git-review.rb +35 -12
- data/lib/git-review/commands.rb +157 -172
- data/lib/git-review/errors.rb +6 -0
- data/lib/git-review/helpers.rb +38 -0
- data/lib/git-review/local.rb +157 -20
- data/lib/git-review/provider/base.rb +85 -0
- data/lib/git-review/provider/bitbucket.rb +25 -0
- data/lib/git-review/provider/github.rb +271 -0
- data/lib/git-review/server.rb +61 -0
- data/lib/git-review/settings.rb +26 -21
- data/lib/mixins/accessible.rb +35 -0
- data/lib/mixins/colorizable.rb +30 -0
- data/lib/mixins/nestable.rb +16 -0
- data/lib/mixins/string.rb +13 -0
- data/lib/mixins/time.rb +11 -0
- data/lib/models/commit.rb +14 -0
- data/lib/models/repository.rb +8 -0
- data/lib/models/request.rb +18 -0
- data/lib/models/user.rb +7 -0
- metadata +72 -47
- data/lib/git-review/github.rb +0 -289
- data/lib/git-review/internals.rb +0 -47
data/lib/git-review/github.rb
DELETED
@@ -1,289 +0,0 @@
|
|
1
|
-
require 'net/http'
|
2
|
-
require 'net/https'
|
3
|
-
# Used to handle json data
|
4
|
-
require 'yajl'
|
5
|
-
# Required to hide password
|
6
|
-
require 'io/console'
|
7
|
-
# Required by yajl for decoding
|
8
|
-
require 'stringio'
|
9
|
-
# Used to retrieve hostname
|
10
|
-
require 'socket'
|
11
|
-
|
12
|
-
|
13
|
-
module GitReview
|
14
|
-
|
15
|
-
class Github
|
16
|
-
|
17
|
-
include ::GitReview::Internals
|
18
|
-
|
19
|
-
attr_reader :github
|
20
|
-
attr_accessor :source_repo
|
21
|
-
|
22
|
-
# acts like a singleton class but it's actually not
|
23
|
-
# use ::GitReview::Github.instance everywhere except in tests
|
24
|
-
def self.instance
|
25
|
-
@instance ||= new
|
26
|
-
end
|
27
|
-
|
28
|
-
def initialize
|
29
|
-
configure_github_access
|
30
|
-
end
|
31
|
-
|
32
|
-
# setup connection with Github via OAuth
|
33
|
-
# @return [String] the username logged in
|
34
|
-
def configure_github_access
|
35
|
-
settings = ::GitReview::Settings.instance
|
36
|
-
if settings.oauth_token && settings.username
|
37
|
-
@github = Octokit::Client.new(
|
38
|
-
:login => settings.username,
|
39
|
-
:access_token => settings.oauth_token,
|
40
|
-
:auto_traversal => true
|
41
|
-
)
|
42
|
-
@github.login
|
43
|
-
else
|
44
|
-
configure_oauth
|
45
|
-
configure_github_access
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# @return [Boolean, Hash] the specified request if exists, otherwise false.
|
50
|
-
# Instead of true, the request itself is returned, so another round-trip
|
51
|
-
# of pull_request can be avoided.
|
52
|
-
def request_exists?(number, state='open')
|
53
|
-
return false if number.nil?
|
54
|
-
request = @github.pull_request(source_repo, number)
|
55
|
-
request.state == state ? request : false
|
56
|
-
rescue Octokit::NotFound
|
57
|
-
false
|
58
|
-
end
|
59
|
-
|
60
|
-
def request_exists_for_branch?(upstream=false, branch=local.source_branch)
|
61
|
-
target_repo = local.target_repo(upstream)
|
62
|
-
@github.pull_requests(target_repo).any? { |r|
|
63
|
-
r.head.ref == branch
|
64
|
-
}
|
65
|
-
end
|
66
|
-
|
67
|
-
# an alias to pull_requests
|
68
|
-
def current_requests(repo=source_repo)
|
69
|
-
@github.pull_requests(repo)
|
70
|
-
end
|
71
|
-
|
72
|
-
# a more detailed collection of requests
|
73
|
-
def current_requests_full(repo=source_repo)
|
74
|
-
@github.pull_requests(repo).collect { |request|
|
75
|
-
@github.pull_request(repo, request.number)
|
76
|
-
}
|
77
|
-
end
|
78
|
-
|
79
|
-
def update
|
80
|
-
git_call('fetch origin')
|
81
|
-
end
|
82
|
-
|
83
|
-
# @return [Array(String, String)] user and repo name from local git config
|
84
|
-
def repo_info_from_config
|
85
|
-
git_config = local.config
|
86
|
-
url = git_config['remote.origin.url']
|
87
|
-
raise ::GitReview::InvalidGitRepositoryError if url.nil?
|
88
|
-
|
89
|
-
user, project = github_url_matching(url)
|
90
|
-
# if there are no results yet, look for 'insteadof' substitutions
|
91
|
-
# in URL and try again
|
92
|
-
unless user && project
|
93
|
-
insteadof_url, true_url = github_insteadof_matching(git_config, url)
|
94
|
-
if insteadof_url and true_url
|
95
|
-
url = url.sub(insteadof_url, true_url)
|
96
|
-
user, project = github_url_matching(url)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
[user, project]
|
100
|
-
end
|
101
|
-
|
102
|
-
# @return [String] the source repo
|
103
|
-
def source_repo
|
104
|
-
# cache source_repo
|
105
|
-
if @source_repo
|
106
|
-
@source_repo
|
107
|
-
else
|
108
|
-
user, repo = repo_info_from_config
|
109
|
-
@source_repo = "#{user}/#{repo}" if user && repo
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def commit_discussion(number)
|
114
|
-
pull_commits = @github.pull_commits(source_repo, number)
|
115
|
-
repo = @github.pull_request(source_repo, number).head.repo.full_name
|
116
|
-
discussion = ["Commits on pull request:\n\n"]
|
117
|
-
discussion += pull_commits.collect { |commit|
|
118
|
-
# commit message
|
119
|
-
name = commit.committer.login
|
120
|
-
output = "\e[35m#{name}\e[m "
|
121
|
-
output << "committed \e[36m#{commit.sha[0..6]}\e[m "
|
122
|
-
output << "on #{format_time(commit.commit.committer.date)}"
|
123
|
-
output << ":\n#{''.rjust(output.length + 1, "-")}\n"
|
124
|
-
output << "#{commit.commit.message}"
|
125
|
-
output << "\n\n"
|
126
|
-
result = [output]
|
127
|
-
|
128
|
-
# comments on commit
|
129
|
-
comments = @github.commit_comments(repo, commit.sha)
|
130
|
-
result + comments.collect { |comment|
|
131
|
-
name = comment.user.login
|
132
|
-
output = "\e[35m#{name}\e[m "
|
133
|
-
output << "added a comment to \e[36m#{commit.sha[0..6]}\e[m"
|
134
|
-
output << " on #{format_time(comment.created_at)}"
|
135
|
-
unless comment.created_at == comment.updated_at
|
136
|
-
output << " (updated on #{format_time(comment.updated_at)})"
|
137
|
-
end
|
138
|
-
output << ":\n#{''.rjust(output.length + 1, "-")}\n"
|
139
|
-
output << comment.body
|
140
|
-
output << "\n\n"
|
141
|
-
}
|
142
|
-
}
|
143
|
-
discussion.compact.flatten unless discussion.empty?
|
144
|
-
end
|
145
|
-
|
146
|
-
def issue_discussion(number)
|
147
|
-
comments = @github.issue_comments(source_repo, number)
|
148
|
-
discussion = ["\nComments on pull request:\n\n"]
|
149
|
-
discussion += comments.collect { |comment|
|
150
|
-
name = comment.user.login
|
151
|
-
output = "\e[35m#{name}\e[m "
|
152
|
-
output << "added a comment to \e[36m#{comment.id}\e[m"
|
153
|
-
output << " on #{format_time(comment.created_at)}"
|
154
|
-
unless comment.created_at == comment.updated_at
|
155
|
-
output << " (updated on #{format_time(comment.updated_at)})"
|
156
|
-
end
|
157
|
-
output << ":\n#{''.rjust(output.length + 1, "-")}\n"
|
158
|
-
output << comment.body
|
159
|
-
output << "\n\n"
|
160
|
-
}
|
161
|
-
discussion.compact.flatten unless discussion.empty?
|
162
|
-
end
|
163
|
-
|
164
|
-
# show discussion for a request
|
165
|
-
def discussion(number)
|
166
|
-
commit_discussion(number) +
|
167
|
-
issue_discussion(number)
|
168
|
-
end
|
169
|
-
|
170
|
-
# show latest pull request number
|
171
|
-
def latest_request_number(repo=source_repo)
|
172
|
-
current_requests(repo).collect(&:number).sort.last.to_i
|
173
|
-
end
|
174
|
-
|
175
|
-
# get the number of the request that matches the title
|
176
|
-
def request_number_by_title(title, repo=source_repo)
|
177
|
-
request = current_requests(repo).find { |r| r.title == title }
|
178
|
-
request.number if request
|
179
|
-
end
|
180
|
-
|
181
|
-
# delegate methods that interact with Github to Octokit client
|
182
|
-
def method_missing(method, *args)
|
183
|
-
if @github.respond_to?(method)
|
184
|
-
@github.send(method, *args)
|
185
|
-
else
|
186
|
-
super
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
def respond_to?(method)
|
191
|
-
@github.respond_to?(method) || super
|
192
|
-
end
|
193
|
-
|
194
|
-
private
|
195
|
-
|
196
|
-
def configure_oauth
|
197
|
-
begin
|
198
|
-
prepare_username_and_password
|
199
|
-
prepare_description
|
200
|
-
authorize
|
201
|
-
rescue ::GitReview::AuthenticationError => e
|
202
|
-
warn e.message
|
203
|
-
rescue ::GitReview::UnprocessableState => e
|
204
|
-
warn e.message
|
205
|
-
exit 1
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
def prepare_username_and_password
|
210
|
-
puts "Requesting a OAuth token for git-review."
|
211
|
-
puts "This procedure will grant access to your public and private "\
|
212
|
-
"repositories."
|
213
|
-
puts "You can revoke this authorization by visiting the following page: "\
|
214
|
-
"https://github.com/settings/applications"
|
215
|
-
print "Please enter your GitHub's username: "
|
216
|
-
@username = STDIN.gets.chomp
|
217
|
-
print "Please enter your GitHub's password (it won't be stored anywhere): "
|
218
|
-
@password = STDIN.noecho(&:gets).chomp
|
219
|
-
print "\n"
|
220
|
-
end
|
221
|
-
|
222
|
-
def prepare_description(chosen_description=nil)
|
223
|
-
if chosen_description
|
224
|
-
@description = chosen_description
|
225
|
-
else
|
226
|
-
@description = "git-review - #{Socket.gethostname}"
|
227
|
-
puts "Please enter a description to associate to this token, it will "\
|
228
|
-
"make easier to find it inside of GitHub's application page."
|
229
|
-
puts "Press enter to accept the proposed description"
|
230
|
-
print "Description [#{@description}]:"
|
231
|
-
user_description = STDIN.gets.chomp
|
232
|
-
@description = user_description.empty? ? @description : user_description
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
|
-
def authorize
|
237
|
-
uri = URI('https://api.github.com/authorizations')
|
238
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
239
|
-
http.use_ssl = true
|
240
|
-
req = Net::HTTP::Post.new(uri.request_uri)
|
241
|
-
req.basic_auth(@username, @password)
|
242
|
-
req.body = Yajl::Encoder.encode(
|
243
|
-
{
|
244
|
-
:scopes => %w(repo),
|
245
|
-
:note => @description
|
246
|
-
}
|
247
|
-
)
|
248
|
-
response = http.request(req)
|
249
|
-
if response.code == '201'
|
250
|
-
parser_response = Yajl::Parser.parse(response.body)
|
251
|
-
save_oauth_token(parser_response['token'])
|
252
|
-
elsif response.code == '401'
|
253
|
-
raise ::GitReview::AuthenticationError
|
254
|
-
else
|
255
|
-
raise ::GitReview::UnprocessableState, response.body
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
def save_oauth_token(token)
|
260
|
-
settings = ::GitReview::Settings.instance
|
261
|
-
settings.oauth_token = token
|
262
|
-
settings.username = @username
|
263
|
-
settings.save!
|
264
|
-
puts "OAuth token successfully created.\n"
|
265
|
-
end
|
266
|
-
|
267
|
-
# extract user and project name from GitHub URL.
|
268
|
-
def github_url_matching(url)
|
269
|
-
matches = /github\.com.(.*?)\/(.*)/.match(url)
|
270
|
-
matches ? [matches[1], matches[2].sub(/\.git\z/, '')] : [nil, nil]
|
271
|
-
end
|
272
|
-
|
273
|
-
# look for 'insteadof' substitutions in URL.
|
274
|
-
def github_insteadof_matching(config, url)
|
275
|
-
first_match = config.keys.collect { |key|
|
276
|
-
[config[key], /url\.(.*github\.com.*)\.insteadof/.match(key)]
|
277
|
-
}.find { |insteadof_url, true_url|
|
278
|
-
url.index(insteadof_url) and true_url != nil
|
279
|
-
}
|
280
|
-
first_match ? [first_match[0], first_match[1][1]] : [nil, nil]
|
281
|
-
end
|
282
|
-
|
283
|
-
def local
|
284
|
-
@local ||= ::GitReview::Local.instance
|
285
|
-
end
|
286
|
-
|
287
|
-
end
|
288
|
-
|
289
|
-
end
|
data/lib/git-review/internals.rb
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
module GitReview
|
2
|
-
|
3
|
-
module Internals
|
4
|
-
|
5
|
-
private
|
6
|
-
|
7
|
-
# System call to 'git'.
|
8
|
-
def git_call(command, verbose = debug_mode, enforce_success = false)
|
9
|
-
if verbose
|
10
|
-
puts
|
11
|
-
puts " git #{command}"
|
12
|
-
puts
|
13
|
-
end
|
14
|
-
output = `git #{command}`
|
15
|
-
puts output if verbose and not output.empty?
|
16
|
-
# If we need sth. to succeed, but it doesn't, then stop right there.
|
17
|
-
if enforce_success and not last_command_successful?
|
18
|
-
puts output unless output.empty?
|
19
|
-
raise ::GitReview::UnprocessableState
|
20
|
-
end
|
21
|
-
output
|
22
|
-
end
|
23
|
-
|
24
|
-
# @return [Boolean] whether the last issued system call was successful
|
25
|
-
def last_command_successful?
|
26
|
-
$?.exitstatus == 0
|
27
|
-
end
|
28
|
-
|
29
|
-
def debug_mode
|
30
|
-
::GitReview::Settings.instance.review_mode == 'debug'
|
31
|
-
end
|
32
|
-
|
33
|
-
# display helper to make output more configurable
|
34
|
-
def format_text(info, size)
|
35
|
-
info.to_s.gsub("\n", ' ')[0, size-1].ljust(size)
|
36
|
-
end
|
37
|
-
|
38
|
-
# display helper to unify time output
|
39
|
-
def format_time(time)
|
40
|
-
time = Time.parse(time) if time.is_a?(String)
|
41
|
-
time.strftime('%d-%b-%y')
|
42
|
-
end
|
43
|
-
|
44
|
-
end
|
45
|
-
|
46
|
-
end
|
47
|
-
|