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/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
|