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/errors.rb
CHANGED
@@ -6,6 +6,12 @@ module GitReview
|
|
6
6
|
end
|
7
7
|
end
|
8
8
|
|
9
|
+
class InvalidGitProviderError < StandardError
|
10
|
+
def message
|
11
|
+
"It is not a valid git provider."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
9
15
|
class InvalidGitRepositoryError < StandardError
|
10
16
|
def message
|
11
17
|
"It is not a valid git repository or doesn't have a valid remote url."
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module GitReview
|
2
|
+
|
3
|
+
module Helpers
|
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
|
+
|
15
|
+
output = `git #{command}`
|
16
|
+
puts output if verbose and not output.empty?
|
17
|
+
|
18
|
+
if enforce_success and not command_successful?
|
19
|
+
puts output unless output.empty?
|
20
|
+
raise ::GitReview::UnprocessableState
|
21
|
+
end
|
22
|
+
|
23
|
+
output
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Boolean] Whether the last issued system call was successful
|
27
|
+
def command_successful?
|
28
|
+
$?.exitstatus == 0
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Boolean] Whether we are running in debugging moder or not
|
32
|
+
def debug_mode
|
33
|
+
::GitReview::Settings.instance.review_mode == 'debug'
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
data/lib/git-review/local.rb
CHANGED
@@ -5,7 +5,7 @@ module GitReview
|
|
5
5
|
# TODO: remove Github-dependency
|
6
6
|
class Local
|
7
7
|
|
8
|
-
include ::GitReview::
|
8
|
+
include ::GitReview::Helpers
|
9
9
|
|
10
10
|
attr_accessor :config
|
11
11
|
|
@@ -20,32 +20,115 @@ module GitReview
|
|
20
20
|
if git_call('rev-parse --show-toplevel').strip.empty?
|
21
21
|
raise ::GitReview::InvalidGitRepositoryError
|
22
22
|
else
|
23
|
-
add_pull_refspec
|
24
23
|
load_config
|
25
24
|
end
|
26
25
|
end
|
27
26
|
|
27
|
+
# List all available remotes.
|
28
|
+
def remotes
|
29
|
+
git_call('remote').split("\n")
|
30
|
+
end
|
31
|
+
|
32
|
+
# Determine whether a remote with a given name exists?
|
33
|
+
def remote_exists?(name)
|
34
|
+
remotes.include? name
|
35
|
+
end
|
36
|
+
|
37
|
+
# Create a Hash with all remotes as keys and their urls as values.
|
38
|
+
def remotes_with_urls
|
39
|
+
result = {}
|
40
|
+
git_call('remote -vv').split("\n").each do |line|
|
41
|
+
entries = line.split("\t")
|
42
|
+
remote = entries.first
|
43
|
+
target_entry = entries.last.split(' ')
|
44
|
+
direction = target_entry.last[1..-2].to_sym
|
45
|
+
target_url = target_entry.first
|
46
|
+
result[remote] ||= {}
|
47
|
+
result[remote][direction] = target_url
|
48
|
+
end
|
49
|
+
result
|
50
|
+
end
|
51
|
+
|
52
|
+
# Collect all remotes for a given url.
|
53
|
+
def remotes_for_url(remote_url)
|
54
|
+
result = remotes_with_urls.collect do |remote, urls|
|
55
|
+
remote if urls.values.all? { |url| url == remote_url }
|
56
|
+
end
|
57
|
+
result.compact
|
58
|
+
end
|
59
|
+
|
60
|
+
# Find or create the correct remote for a fork with a given owner name.
|
61
|
+
def remote_for_request(request)
|
62
|
+
repo_owner = request.head.repo.owner.login
|
63
|
+
remote_url = server.remote_url_for(repo_owner)
|
64
|
+
remotes = remotes_for_url(remote_url)
|
65
|
+
if remotes.empty?
|
66
|
+
remote = "review_#{repo_owner}"
|
67
|
+
git_call("remote add #{remote} #{remote_url}", debug_mode, true)
|
68
|
+
else
|
69
|
+
remote = remotes.first
|
70
|
+
end
|
71
|
+
remote
|
72
|
+
end
|
73
|
+
|
74
|
+
# Remove obsolete remotes with review prefix.
|
75
|
+
def clean_remotes
|
76
|
+
protected_remotes = remotes_for_branches
|
77
|
+
remotes.each do |remote|
|
78
|
+
# Only remove review remotes that aren't referenced by current branches.
|
79
|
+
if remote.index('review_') == 0 && !protected_remotes.include?(remote)
|
80
|
+
git_call "remote remove #{remote}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Prune all configured remotes.
|
86
|
+
def prune_remotes
|
87
|
+
remotes.each { |remote| git_call "remote prune #{remote}" }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Find all remotes which are currently referenced by local branches.
|
91
|
+
def remotes_for_branches
|
92
|
+
remotes = git_call('branch -lvv').gsub('* ', '').split("\n").map do |line|
|
93
|
+
line.split(' ')[2][1..-2].split('/').first
|
94
|
+
end
|
95
|
+
remotes.uniq
|
96
|
+
end
|
97
|
+
|
98
|
+
# Finds the correct remote for a given branch name.
|
99
|
+
def remote_for_branch(branch_name)
|
100
|
+
git_call('branch -lvv').gsub('* ', '').split("\n").each do |line|
|
101
|
+
entries = line.split(' ')
|
102
|
+
next unless entries.first == branch_name
|
103
|
+
# Return the remote name or nil for local branches.
|
104
|
+
match = entries[2].match(%r(\[(.*)(\]|:)))
|
105
|
+
return match[1].split('/').first if match
|
106
|
+
end
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
28
110
|
# @return [Array<String>] all existing branches
|
29
111
|
def all_branches
|
30
|
-
git_call('branch -a').split("\n").collect { |s| s.strip }
|
112
|
+
git_call('branch -a').gsub('* ', '').split("\n").collect { |s| s.strip }
|
31
113
|
end
|
32
114
|
|
33
115
|
# @return [Array<String>] all open requests' branches shouldn't be deleted
|
34
116
|
def protected_branches
|
35
|
-
|
117
|
+
server.current_requests.collect { |r| r.head.ref }
|
36
118
|
end
|
37
119
|
|
38
120
|
# @return [Array<String>] all review branches with 'review_' prefix
|
39
121
|
def review_branches
|
40
|
-
all_branches.collect { |
|
122
|
+
all_branches.collect { |entry|
|
41
123
|
# only use uniq branch names (no matter if local or remote)
|
42
|
-
|
124
|
+
branch_name = entry.split('/').last
|
125
|
+
branch_name if branch_name.index('review_') == 0
|
43
126
|
}.compact.uniq
|
44
127
|
end
|
45
128
|
|
46
129
|
# clean a single request's obsolete branch
|
47
|
-
def clean_single(number, force=false)
|
48
|
-
request =
|
130
|
+
def clean_single(number, force = false)
|
131
|
+
request = server.pull_request(source_repo, number)
|
49
132
|
if request && request.state == 'closed'
|
50
133
|
# ensure there are no unmerged commits or '--force' flag has been set
|
51
134
|
branch_name = request.head.ref
|
@@ -137,14 +220,30 @@ module GitReview
|
|
137
220
|
end
|
138
221
|
end
|
139
222
|
|
223
|
+
# @return [Boolean] whether there are commits not in target branch yet
|
224
|
+
def new_commits?(upstream = false)
|
225
|
+
# Check if an upstream remote exists and create it if necessary.
|
226
|
+
remote_url = server.remote_url_for(*target_repo(upstream).split('/'))
|
227
|
+
remote = remotes_for_url(remote_url).first
|
228
|
+
unless remote
|
229
|
+
remote = 'upstream'
|
230
|
+
git_call "remote add #{remote} #{remote_url}"
|
231
|
+
end
|
232
|
+
git_call "fetch #{remote}"
|
233
|
+
target = upstream ? "#{remote}/#{target_branch}" : target_branch
|
234
|
+
not git_call("cherry #{target}").empty?
|
235
|
+
end
|
236
|
+
|
140
237
|
# @return [Boolean] whether a specified commit has already been merged.
|
141
238
|
def merged?(sha)
|
142
|
-
|
239
|
+
branches = git_call("branch --contains #{sha} 2>&1").split("\n").
|
240
|
+
collect { |b| b.delete('*').strip }
|
241
|
+
branches.include?(target_branch)
|
143
242
|
end
|
144
243
|
|
145
244
|
# @return [String] the source repo
|
146
245
|
def source_repo
|
147
|
-
|
246
|
+
server.source_repo
|
148
247
|
end
|
149
248
|
|
150
249
|
# @return [String] the current source branch
|
@@ -168,7 +267,7 @@ module GitReview
|
|
168
267
|
def target_repo(upstream=false)
|
169
268
|
# TODO: Manually override this and set arbitrary repositories
|
170
269
|
if upstream
|
171
|
-
|
270
|
+
server.repository(source_repo).parent.full_name
|
172
271
|
else
|
173
272
|
source_repo
|
174
273
|
end
|
@@ -182,7 +281,7 @@ module GitReview
|
|
182
281
|
# @return [String] the head string used for pull requests
|
183
282
|
def head
|
184
283
|
# in the form of 'user:branch'
|
185
|
-
"#{
|
284
|
+
"#{source_repo.split('/').first}:#{source_branch}"
|
186
285
|
end
|
187
286
|
|
188
287
|
# @return [Boolean] whether already on a feature branch
|
@@ -193,12 +292,9 @@ module GitReview
|
|
193
292
|
source_branch != target_branch && source_branch != 'master'
|
194
293
|
end
|
195
294
|
|
196
|
-
#
|
197
|
-
|
198
|
-
|
199
|
-
refspec = '+refs/pull/*/head:refs/remotes/origin/pr/*'
|
200
|
-
fetch_config = "config --local --add remote.origin.fetch #{refspec}"
|
201
|
-
git_call(fetch_config, false) unless config_list.include?(refspec)
|
295
|
+
# Remove all non word characters and turn them into underscores.
|
296
|
+
def sanitize_branch_name(name)
|
297
|
+
name.gsub(/\W+/, '_').downcase
|
202
298
|
end
|
203
299
|
|
204
300
|
def load_config
|
@@ -218,8 +314,49 @@ module GitReview
|
|
218
314
|
git_call('config --list', false)
|
219
315
|
end
|
220
316
|
|
221
|
-
def
|
222
|
-
@
|
317
|
+
def server
|
318
|
+
@server ||= ::GitReview::Server.instance
|
319
|
+
end
|
320
|
+
|
321
|
+
# @return [Array(String, String)] the title and the body of pull request
|
322
|
+
def create_title_and_body(target_branch)
|
323
|
+
login = server.login
|
324
|
+
commits = git_call("log --format='%H' HEAD...#{target_branch}").
|
325
|
+
lines.count
|
326
|
+
puts "Commits: #{commits}"
|
327
|
+
if commits == 1
|
328
|
+
# we can create a really specific title and body
|
329
|
+
title = git_call("log --format='%s' HEAD...#{target_branch}").chomp
|
330
|
+
body = git_call("log --format='%b' HEAD...#{target_branch}").chomp
|
331
|
+
else
|
332
|
+
title = "[Review] Request from '#{login}' @ '#{source}'"
|
333
|
+
body = "Please review the following changes:\n"
|
334
|
+
body += git_call("log --oneline HEAD...#{target_branch}").
|
335
|
+
lines.map{|l| " * #{l.chomp}"}.join("\n")
|
336
|
+
end
|
337
|
+
edit_title_and_body(title, body)
|
338
|
+
end
|
339
|
+
|
340
|
+
# TODO: refactor
|
341
|
+
def edit_title_and_body(title, body)
|
342
|
+
tmpfile = Tempfile.new('git-review')
|
343
|
+
tmpfile.write(title + "\n\n" + body)
|
344
|
+
tmpfile.flush
|
345
|
+
editor = ENV['TERM_EDITOR'] || ENV['EDITOR']
|
346
|
+
unless editor
|
347
|
+
warn 'Please set $EDITOR or $TERM_EDITOR in your .bash_profile.'
|
348
|
+
end
|
349
|
+
|
350
|
+
system("#{editor || 'open'} #{tmpfile.path}")
|
351
|
+
|
352
|
+
tmpfile.rewind
|
353
|
+
lines = tmpfile.read.lines.to_a
|
354
|
+
#puts lines.inspect
|
355
|
+
title = lines.shift.chomp
|
356
|
+
lines.shift if lines[0].chomp.empty?
|
357
|
+
body = lines.join
|
358
|
+
tmpfile.unlink
|
359
|
+
[title, body]
|
223
360
|
end
|
224
361
|
|
225
362
|
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module GitReview
|
2
|
+
|
3
|
+
module Provider
|
4
|
+
|
5
|
+
class Base
|
6
|
+
|
7
|
+
attr_reader :client
|
8
|
+
attr_accessor :source_repo
|
9
|
+
|
10
|
+
def self.instance
|
11
|
+
@instance ||= new
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
configure_access
|
16
|
+
end
|
17
|
+
|
18
|
+
def update
|
19
|
+
git_call('fetch origin')
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [String] Source repo name
|
23
|
+
def source_repo
|
24
|
+
@source_repo ||= begin
|
25
|
+
user, repo = repo_info_from_config
|
26
|
+
"#{user}/#{repo}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [String] Current username
|
31
|
+
def login
|
32
|
+
settings.username
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Array(String, String)] User and repo name from git
|
36
|
+
def repo_info_from_config
|
37
|
+
url = local.config['remote.origin.url']
|
38
|
+
raise ::GitReview::InvalidGitRepositoryError if url.nil?
|
39
|
+
|
40
|
+
user, project = url_matching(url)
|
41
|
+
|
42
|
+
unless user && project
|
43
|
+
insteadof_url, true_url = insteadof_matching(local.config, url)
|
44
|
+
|
45
|
+
if insteadof_url and true_url
|
46
|
+
url = url.sub(insteadof_url, true_url)
|
47
|
+
user, project = url_matching(url)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
[user, project]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Ensure we find the right request
|
55
|
+
def get_request_by_number(request_number)
|
56
|
+
request_exists?(request_number) || (raise ::GitReview::InvalidRequestIDError)
|
57
|
+
end
|
58
|
+
|
59
|
+
def method_missing(method, *args)
|
60
|
+
if client.respond_to?(method)
|
61
|
+
client.send(method, *args)
|
62
|
+
else
|
63
|
+
super
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def respond_to?(method)
|
68
|
+
client.respond_to?(method) || super
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def local
|
74
|
+
@local ||= ::GitReview::Local.instance
|
75
|
+
end
|
76
|
+
|
77
|
+
def settings
|
78
|
+
@settings ||= ::GitReview::Settings.instance
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module GitReview
|
2
|
+
|
3
|
+
module Provider
|
4
|
+
|
5
|
+
class Bitbucket < Base
|
6
|
+
|
7
|
+
include ::GitReview::Helpers
|
8
|
+
|
9
|
+
attr_reader :bitbucket
|
10
|
+
|
11
|
+
def configure_access
|
12
|
+
end
|
13
|
+
|
14
|
+
def source_repo
|
15
|
+
end
|
16
|
+
|
17
|
+
def update
|
18
|
+
git_call('fetch origin')
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'yajl'
|
4
|
+
require 'io/console'
|
5
|
+
require 'stringio'
|
6
|
+
require 'socket'
|
7
|
+
|
8
|
+
module GitReview
|
9
|
+
|
10
|
+
module Provider
|
11
|
+
|
12
|
+
class Github < Base
|
13
|
+
|
14
|
+
include ::GitReview::Helpers
|
15
|
+
|
16
|
+
# @return [String] Authenticated username
|
17
|
+
def configure_access
|
18
|
+
if settings.oauth_token && settings.username
|
19
|
+
@client = Octokit::Client.new(
|
20
|
+
login: settings.username,
|
21
|
+
access_token: settings.oauth_token,
|
22
|
+
auto_traversal: true
|
23
|
+
)
|
24
|
+
|
25
|
+
@client.login
|
26
|
+
else
|
27
|
+
configure_oauth
|
28
|
+
configure_access
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Boolean, Hash] the specified request if exists, otherwise false.
|
33
|
+
# Instead of true, the request itself is returned, so another round-trip
|
34
|
+
# of pull_request can be avoided.
|
35
|
+
def request_exists?(number, state='open')
|
36
|
+
return false if number.nil?
|
37
|
+
request = client.pull_request(source_repo, number)
|
38
|
+
request.state == state ? request : false
|
39
|
+
rescue Octokit::NotFound
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def request_exists_for_branch?(upstream = false, branch = local.source_branch)
|
44
|
+
target_repo = local.target_repo(upstream)
|
45
|
+
client.pull_requests(target_repo).any? { |r| r.head.ref == branch }
|
46
|
+
end
|
47
|
+
|
48
|
+
# an alias to pull_requests
|
49
|
+
def current_requests(repo=source_repo)
|
50
|
+
client.pull_requests(repo)
|
51
|
+
end
|
52
|
+
|
53
|
+
# a more detailed collection of requests
|
54
|
+
def current_requests_full(repo=source_repo)
|
55
|
+
threads = []
|
56
|
+
requests = []
|
57
|
+
client.pull_requests(repo).each do |req|
|
58
|
+
threads << Thread.new {
|
59
|
+
requests << client.pull_request(repo, req.number)
|
60
|
+
}
|
61
|
+
end
|
62
|
+
threads.each { |t| t.join }
|
63
|
+
requests
|
64
|
+
end
|
65
|
+
|
66
|
+
def send_pull_request(to_upstream = false)
|
67
|
+
target_repo = local.target_repo(to_upstream)
|
68
|
+
head = local.head
|
69
|
+
base = local.target_branch
|
70
|
+
title, body = local.create_title_and_body(base)
|
71
|
+
|
72
|
+
# gather information before creating pull request
|
73
|
+
latest_number = latest_request_number(target_repo)
|
74
|
+
|
75
|
+
# create the actual pull request
|
76
|
+
create_pull_request(target_repo, base, head, title, body)
|
77
|
+
# switch back to target_branch and check for success
|
78
|
+
git_call "checkout #{base}"
|
79
|
+
|
80
|
+
# make sure the new pull request is indeed created
|
81
|
+
new_number = request_number_by_title(title, target_repo)
|
82
|
+
if new_number && new_number > latest_number
|
83
|
+
puts "Successfully created new request ##{new_number}"
|
84
|
+
puts request_url_for target_repo, new_number
|
85
|
+
else
|
86
|
+
puts "Pull request was not created for #{target_repo}."
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def commit_discussion(number)
|
91
|
+
pull_commits = client.pull_commits(source_repo, number)
|
92
|
+
repo = client.pull_request(source_repo, number).head.repo.full_name
|
93
|
+
discussion = ["Commits on pull request:\n\n"]
|
94
|
+
discussion += pull_commits.collect { |commit|
|
95
|
+
# commit message
|
96
|
+
name = commit.committer.login
|
97
|
+
output = "\e[35m#{name}\e[m "
|
98
|
+
output << "committed \e[36m#{commit.sha[0..6]}\e[m "
|
99
|
+
output << "on #{commit.commit.committer.date.review_time}"
|
100
|
+
output << ":\n#{''.rjust(output.length + 1, "-")}\n"
|
101
|
+
output << "#{commit.commit.message}"
|
102
|
+
output << "\n\n"
|
103
|
+
result = [output]
|
104
|
+
|
105
|
+
# comments on commit
|
106
|
+
comments = client.commit_comments(repo, commit.sha)
|
107
|
+
result + comments.collect { |comment|
|
108
|
+
name = comment.user.login
|
109
|
+
output = "\e[35m#{name}\e[m "
|
110
|
+
output << "added a comment to \e[36m#{commit.sha[0..6]}\e[m"
|
111
|
+
output << " on #{comment.created_at.review_time}"
|
112
|
+
unless comment.created_at == comment.updated_at
|
113
|
+
output << " (updated on #{comment.updated_at.review_time})"
|
114
|
+
end
|
115
|
+
output << ":\n#{''.rjust(output.length + 1, "-")}\n"
|
116
|
+
output << comment.body
|
117
|
+
output << "\n\n"
|
118
|
+
}
|
119
|
+
}
|
120
|
+
discussion.compact.flatten unless discussion.empty?
|
121
|
+
end
|
122
|
+
|
123
|
+
def issue_discussion(number)
|
124
|
+
comments = client.issue_comments(source_repo, number) +
|
125
|
+
client.review_comments(source_repo, number)
|
126
|
+
discussion = ["\nComments on pull request:\n\n"]
|
127
|
+
discussion += comments.collect { |comment|
|
128
|
+
name = comment.user.login
|
129
|
+
output = "\e[35m#{name}\e[m "
|
130
|
+
output << "added a comment to \e[36m#{comment.id}\e[m"
|
131
|
+
output << " on #{comment.created_at.review_time}"
|
132
|
+
unless comment.created_at == comment.updated_at
|
133
|
+
output << " (updated on #{comment.updated_at.review_time})"
|
134
|
+
end
|
135
|
+
output << ":\n#{''.rjust(output.length + 1, "-")}\n"
|
136
|
+
output << comment.body
|
137
|
+
output << "\n\n"
|
138
|
+
}
|
139
|
+
discussion.compact.flatten unless discussion.empty?
|
140
|
+
end
|
141
|
+
|
142
|
+
# get the number of comments, including comments on commits
|
143
|
+
def comments_count(request)
|
144
|
+
issue_c = request.comments + request.review_comments
|
145
|
+
commits_c = client.pull_commits(source_repo, request.number).
|
146
|
+
inject(0) { |sum, c| sum + c.commit.comment_count }
|
147
|
+
issue_c + commits_c
|
148
|
+
end
|
149
|
+
|
150
|
+
# show discussion for a request
|
151
|
+
def discussion(number)
|
152
|
+
commit_discussion(number) +
|
153
|
+
issue_discussion(number)
|
154
|
+
end
|
155
|
+
|
156
|
+
# show latest pull request number
|
157
|
+
def latest_request_number(repo=source_repo)
|
158
|
+
current_requests(repo).collect(&:number).sort.last.to_i
|
159
|
+
end
|
160
|
+
|
161
|
+
# get the number of the request that matches the title
|
162
|
+
def request_number_by_title(title, repo=source_repo)
|
163
|
+
request = current_requests(repo).find { |r| r.title == title }
|
164
|
+
request.number if request
|
165
|
+
end
|
166
|
+
|
167
|
+
# FIXME: Remove this method after merging create_pull_request from commands.rb, currently no specs
|
168
|
+
def request_url_for(target_repo, request_number)
|
169
|
+
"https://github.com/#{target_repo}/pull/#{request_number}"
|
170
|
+
end
|
171
|
+
|
172
|
+
# FIXME: Needs to be moved into Server class, as its result is dependent of
|
173
|
+
# the actual provider (i.e. GitHub or BitBucket).
|
174
|
+
def remote_url_for(user_name, repo_name = repo_info_from_config.last)
|
175
|
+
"git@github.com:#{user_name}/#{repo_name}.git"
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
def configure_oauth
|
181
|
+
begin
|
182
|
+
prepare_username_and_password
|
183
|
+
prepare_description
|
184
|
+
authorize
|
185
|
+
rescue ::GitReview::AuthenticationError => e
|
186
|
+
warn e.message
|
187
|
+
rescue ::GitReview::UnprocessableState => e
|
188
|
+
warn e.message
|
189
|
+
exit 1
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def prepare_username_and_password
|
194
|
+
puts "Requesting a OAuth token for git-review."
|
195
|
+
puts "This procedure will grant access to your public and private "\
|
196
|
+
"repositories."
|
197
|
+
puts "You can revoke this authorization by visiting the following page: "\
|
198
|
+
"https://github.com/settings/applications"
|
199
|
+
print "Please enter your GitHub's username: "
|
200
|
+
@username = STDIN.gets.chomp
|
201
|
+
print "Please enter your GitHub's password (it won't be stored anywhere): "
|
202
|
+
@password = STDIN.noecho(&:gets).chomp
|
203
|
+
print "\n"
|
204
|
+
end
|
205
|
+
|
206
|
+
def prepare_description(chosen_description=nil)
|
207
|
+
if chosen_description
|
208
|
+
@description = chosen_description
|
209
|
+
else
|
210
|
+
@description = "git-review - #{Socket.gethostname}"
|
211
|
+
puts "Please enter a description to associate to this token, it will "\
|
212
|
+
"make easier to find it inside of GitHub's application page."
|
213
|
+
puts "Press enter to accept the proposed description"
|
214
|
+
print "Description [#{@description}]:"
|
215
|
+
user_description = STDIN.gets.chomp
|
216
|
+
@description = user_description.empty? ? @description : user_description
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def authorize
|
221
|
+
uri = URI('https://api.github.com/authorizations')
|
222
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
223
|
+
http.use_ssl = true
|
224
|
+
req = Net::HTTP::Post.new(uri.request_uri)
|
225
|
+
req.basic_auth(@username, @password)
|
226
|
+
req.body = Yajl::Encoder.encode(
|
227
|
+
{
|
228
|
+
scopes: %w(repo),
|
229
|
+
note: @description
|
230
|
+
}
|
231
|
+
)
|
232
|
+
response = http.request(req)
|
233
|
+
if response.code == '201'
|
234
|
+
parser_response = Yajl::Parser.parse(response.body)
|
235
|
+
save_oauth_token(parser_response['token'])
|
236
|
+
elsif response.code == '401'
|
237
|
+
raise ::GitReview::AuthenticationError
|
238
|
+
else
|
239
|
+
raise ::GitReview::UnprocessableState, response.body
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def save_oauth_token(token)
|
244
|
+
settings = ::GitReview::Settings.instance
|
245
|
+
settings.oauth_token = token
|
246
|
+
settings.username = @username
|
247
|
+
settings.save!
|
248
|
+
puts "OAuth token successfully created.\n"
|
249
|
+
end
|
250
|
+
|
251
|
+
# extract user and project name from GitHub URL.
|
252
|
+
def url_matching(url)
|
253
|
+
matches = /github\.com.(.*?)\/(.*)/.match(url)
|
254
|
+
matches ? [matches[1], matches[2].sub(/\.git\z/, '')] : [nil, nil]
|
255
|
+
end
|
256
|
+
|
257
|
+
# look for 'insteadof' substitutions in URL.
|
258
|
+
def insteadof_matching(config, url)
|
259
|
+
first_match = config.keys.collect { |key|
|
260
|
+
[config[key], /url\.(.*github\.com.*)\.insteadof/.match(key)]
|
261
|
+
}.find { |insteadof_url, true_url|
|
262
|
+
url.index(insteadof_url) and true_url != nil
|
263
|
+
}
|
264
|
+
first_match ? [first_match[0], first_match[1][1]] : [nil, nil]
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|