git-review 2.0.0.alpha → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
|