git-review 1.1.6 → 1.1.7
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.
- data/lib/git-review/commands.rb +331 -0
- data/lib/git-review/errors.rb +34 -0
- data/lib/git-review/github.rb +289 -0
- data/lib/git-review/internals.rb +47 -0
- data/lib/git-review/local.rb +227 -0
- data/lib/git-review/settings.rb +48 -0
- metadata +13 -6
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
module GitReview
|
|
2
|
+
|
|
3
|
+
module Commands
|
|
4
|
+
|
|
5
|
+
include ::GitReview::Internals
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
# List all pending requests.
|
|
9
|
+
def list(reverse=false)
|
|
10
|
+
requests = github.current_requests_full.reject { |request|
|
|
11
|
+
# find only pending (= unmerged) requests and output summary
|
|
12
|
+
# explicitly look for local changes Github does not yet know about
|
|
13
|
+
local.merged?(request.head.sha)
|
|
14
|
+
}
|
|
15
|
+
requests.reverse! if reverse
|
|
16
|
+
source = local.source
|
|
17
|
+
if requests.empty?
|
|
18
|
+
puts "No pending requests for '#{source}'."
|
|
19
|
+
else
|
|
20
|
+
puts "Pending requests for '#{source}':"
|
|
21
|
+
puts "ID Updated Comments Title"
|
|
22
|
+
requests.each { |request| print_request(request) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Show details for a single request.
|
|
27
|
+
def show(number, full=false)
|
|
28
|
+
request = get_request_by_number(number)
|
|
29
|
+
# determine whether to show full diff or just stats
|
|
30
|
+
option = full ? '' : '--stat '
|
|
31
|
+
diff = "diff --color=always #{option}HEAD...#{request.head.sha}"
|
|
32
|
+
print_request_details(request)
|
|
33
|
+
puts git_call(diff)
|
|
34
|
+
print_request_discussions(request)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Open a browser window and review a specified request.
|
|
38
|
+
def browse(number)
|
|
39
|
+
request = get_request_by_number(number)
|
|
40
|
+
Launchy.open(request.html_url)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Checkout a specified request's changes to your local repository.
|
|
44
|
+
def checkout(number, branch=false)
|
|
45
|
+
request = get_request_by_number(number)
|
|
46
|
+
puts 'Checking out changes to your local repository.'
|
|
47
|
+
puts 'To get back to your original state, just run:'
|
|
48
|
+
puts
|
|
49
|
+
puts ' git checkout master'
|
|
50
|
+
puts
|
|
51
|
+
if branch
|
|
52
|
+
git_call("checkout #{request.head.ref}")
|
|
53
|
+
else
|
|
54
|
+
git_call("checkout pr/#{request.number}")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Add an approving comment to the request.
|
|
59
|
+
def approve(number)
|
|
60
|
+
request = get_request_by_number(number)
|
|
61
|
+
repo = github.source_repo
|
|
62
|
+
# TODO: Make this configurable.
|
|
63
|
+
comment = 'Reviewed and approved.'
|
|
64
|
+
response = github.add_comment(repo, request.number, comment)
|
|
65
|
+
if response[:body] == comment
|
|
66
|
+
puts 'Successfully approved request.'
|
|
67
|
+
else
|
|
68
|
+
puts response[:message]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Accept a specified request by merging it into master.
|
|
73
|
+
def merge(number)
|
|
74
|
+
request = get_request_by_number(number)
|
|
75
|
+
if request.head.repo
|
|
76
|
+
message = "Accept request ##{request.number} " +
|
|
77
|
+
"and merge changes into \"#{local.target}\""
|
|
78
|
+
command = "merge -m '#{message}' #{request.head.sha}"
|
|
79
|
+
puts
|
|
80
|
+
puts "Request title:"
|
|
81
|
+
puts " #{request.title}"
|
|
82
|
+
puts
|
|
83
|
+
puts "Merge command:"
|
|
84
|
+
puts " git #{command}"
|
|
85
|
+
puts
|
|
86
|
+
puts git_call(command)
|
|
87
|
+
else
|
|
88
|
+
print_repo_deleted(request)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Close a specified request.
|
|
93
|
+
def close(number)
|
|
94
|
+
request = get_request_by_number(number)
|
|
95
|
+
repo = github.source_repo
|
|
96
|
+
github.close_issue(repo, request.number)
|
|
97
|
+
unless github.request_exists?('open', request.number)
|
|
98
|
+
puts 'Successfully closed request.'
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Prepare local repository to create a new request.
|
|
103
|
+
# People should work on local branches, but especially for single commit
|
|
104
|
+
# changes, more often than not, they don't. Therefore we create a branch
|
|
105
|
+
# for them, to be able to use code review the way it is intended.
|
|
106
|
+
# @return [Array(String, String)] the original branch and the local branch
|
|
107
|
+
def prepare(new=false, name=nil)
|
|
108
|
+
# remember original branch the user was currently working on
|
|
109
|
+
original_branch = local.source_branch
|
|
110
|
+
if new || !local.on_feature_branch?
|
|
111
|
+
local_branch = move_uncommitted_changes(local.target_branch, name)
|
|
112
|
+
else
|
|
113
|
+
local_branch = original_branch
|
|
114
|
+
end
|
|
115
|
+
[original_branch, local_branch]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Create a new request.
|
|
119
|
+
# TODO: Support creating requests to other repositories and branches (like
|
|
120
|
+
# the original repo, this has been forked from).
|
|
121
|
+
def create(upstream=false)
|
|
122
|
+
# prepare original_branch and local_branch
|
|
123
|
+
original_branch, local_branch = prepare
|
|
124
|
+
# don't create request with uncommitted changes in current branch
|
|
125
|
+
unless git_call('diff HEAD').empty?
|
|
126
|
+
puts 'You have uncommitted changes.'
|
|
127
|
+
puts 'Please stash or commit before creating the request.'
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
if git_call("cherry #{local.target_branch}").empty?
|
|
131
|
+
puts 'Nothing to push to remote yet. Commit something first.'
|
|
132
|
+
else
|
|
133
|
+
if github.request_exists_for_branch?(upstream)
|
|
134
|
+
puts 'A pull request already exists for this branch.'
|
|
135
|
+
puts 'Please update the request directly using `git push`.'
|
|
136
|
+
return
|
|
137
|
+
end
|
|
138
|
+
# push latest commits to the remote branch (create if necessary)
|
|
139
|
+
git_call("push --set-upstream origin #{local_branch}", debug_mode, true)
|
|
140
|
+
create_pull_request(upstream)
|
|
141
|
+
# return to the user's original branch
|
|
142
|
+
# FIXME: keep track of original branch etc
|
|
143
|
+
git_call("checkout #{original_branch}")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# delete obsolete branches (left over from already closed requests)
|
|
148
|
+
def clean(number=nil, force=false, all=false)
|
|
149
|
+
# pruning is needed to remove deleted branches from your local track
|
|
150
|
+
git_call('remote prune origin')
|
|
151
|
+
# determine strategy to clean.
|
|
152
|
+
if all
|
|
153
|
+
local.clean_all
|
|
154
|
+
else
|
|
155
|
+
local.clean_single(number, force)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Start a console session (used for debugging)
|
|
160
|
+
def console
|
|
161
|
+
puts 'Entering debug console.'
|
|
162
|
+
if RUBY_VERSION == '2.0.0'
|
|
163
|
+
require 'byebug'
|
|
164
|
+
byebug
|
|
165
|
+
else
|
|
166
|
+
require 'ruby-debug'
|
|
167
|
+
Debugger.start
|
|
168
|
+
debugger
|
|
169
|
+
end
|
|
170
|
+
puts 'Leaving debug console.'
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def print_request(request)
|
|
176
|
+
date_string = format_time(request.updated_at)
|
|
177
|
+
comments_count = request.comments.to_i + request.review_comments.to_i
|
|
178
|
+
line = format_text(request.number, 8)
|
|
179
|
+
line << format_text(date_string, 11)
|
|
180
|
+
line << format_text(comments_count, 10)
|
|
181
|
+
line << format_text(request.title, 91)
|
|
182
|
+
puts line
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def print_request_details(request)
|
|
186
|
+
comments_count = request.comments.to_i + request.review_comments.to_i
|
|
187
|
+
puts 'ID : ' + request.number.to_s
|
|
188
|
+
puts 'Label : ' + request.head.label
|
|
189
|
+
puts 'Updated : ' + format_time(request.updated_at)
|
|
190
|
+
puts 'Comments : ' + comments_count.to_s
|
|
191
|
+
puts
|
|
192
|
+
puts request.title
|
|
193
|
+
puts
|
|
194
|
+
unless request.body.empty?
|
|
195
|
+
puts request.body
|
|
196
|
+
puts
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def print_request_discussions(request)
|
|
201
|
+
puts 'Progress :'
|
|
202
|
+
puts
|
|
203
|
+
puts github.discussion(request.number)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# someone deleted the source repo
|
|
207
|
+
def print_repo_deleted(request)
|
|
208
|
+
user = request.head.user.login
|
|
209
|
+
url = request.patch_url
|
|
210
|
+
puts "Sorry, #{user} deleted the source repository."
|
|
211
|
+
puts "git-review doesn't support this."
|
|
212
|
+
puts "Tell the contributor not to do this."
|
|
213
|
+
puts
|
|
214
|
+
puts "You can still manually patch your repo by running:"
|
|
215
|
+
puts
|
|
216
|
+
puts " curl #{url} | git am"
|
|
217
|
+
puts
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# ask for branch name if not provided
|
|
221
|
+
# @return [String] sanitized branch name
|
|
222
|
+
def get_branch_name
|
|
223
|
+
puts 'Please provide a name for the branch:'
|
|
224
|
+
branch_name = gets.chomp
|
|
225
|
+
branch_name.gsub(/\W+/, '_').downcase
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# move uncommitted changes from target branch to local branch
|
|
229
|
+
# @return [String] the new local branch uncommitted changes are moved to
|
|
230
|
+
def move_uncommitted_changes(target_branch, new_branch)
|
|
231
|
+
new_branch ||= get_branch_name
|
|
232
|
+
local_branch = "review_#{Time.now.strftime("%y%m%d")}_#{new_branch}"
|
|
233
|
+
git_call("checkout -b #{local_branch}")
|
|
234
|
+
# make sure we are on the feature branch
|
|
235
|
+
if local.source_branch == local_branch
|
|
236
|
+
# stash any uncommitted changes
|
|
237
|
+
save_uncommitted_changes = local.uncommitted_changes?
|
|
238
|
+
git_call('stash') if save_uncommitted_changes
|
|
239
|
+
# go back to target and get rid of pending commits
|
|
240
|
+
git_call("checkout #{target_branch}")
|
|
241
|
+
git_call("reset --hard origin/#{target_branch}")
|
|
242
|
+
git_call("checkout #{local_branch}")
|
|
243
|
+
git_call('stash pop') if save_uncommitted_changes
|
|
244
|
+
local_branch
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def create_pull_request(to_upstream=false)
|
|
249
|
+
target_repo = local.target_repo(to_upstream)
|
|
250
|
+
head = local.head
|
|
251
|
+
base = local.target_branch
|
|
252
|
+
title, body = create_title_and_body(base)
|
|
253
|
+
|
|
254
|
+
# gather information before creating pull request
|
|
255
|
+
lastest_number = github.latest_request_number(target_repo)
|
|
256
|
+
|
|
257
|
+
# create the actual pull request
|
|
258
|
+
github.create_pull_request(target_repo, base, head, title, body)
|
|
259
|
+
# switch back to target_branch and check for success
|
|
260
|
+
git_call("checkout #{base}")
|
|
261
|
+
|
|
262
|
+
# make sure the new pull request is indeed created
|
|
263
|
+
new_number = github.request_number_by_title(title, target_repo)
|
|
264
|
+
if new_number && new_number > lastest_number
|
|
265
|
+
puts "Successfully created new request ##{new_number}"
|
|
266
|
+
puts "https://github.com/#{target_repo}/pull/#{new_number}"
|
|
267
|
+
else
|
|
268
|
+
puts "Pull request was not created for #{target_repo}."
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# @return [Array(String, String)] the title and the body of pull request
|
|
274
|
+
def create_title_and_body(target_branch)
|
|
275
|
+
source = local.source
|
|
276
|
+
login = github.github.login
|
|
277
|
+
commits = git_call("log --format='%H' HEAD...#{target_branch}").
|
|
278
|
+
lines.count
|
|
279
|
+
puts "commits: #{commits}"
|
|
280
|
+
if commits == 1
|
|
281
|
+
# we can create a really specific title and body
|
|
282
|
+
title = git_call("log --format='%s' HEAD...#{target_branch}").chomp
|
|
283
|
+
body = git_call("log --format='%b' HEAD...#{target_branch}").chomp
|
|
284
|
+
else
|
|
285
|
+
title = "[Review] Request from '#{login}' @ '#{source}'"
|
|
286
|
+
body = "Please review the following changes:\n"
|
|
287
|
+
body += git_call("log --oneline HEAD...#{target_branch}").
|
|
288
|
+
lines.map{|l| " * #{l.chomp}"}.join("\n")
|
|
289
|
+
end
|
|
290
|
+
edit_title_and_body(title, body)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# TODO: refactor
|
|
294
|
+
def edit_title_and_body(title, body)
|
|
295
|
+
tmpfile = Tempfile.new('git-review')
|
|
296
|
+
tmpfile.write(title + "\n\n" + body)
|
|
297
|
+
tmpfile.flush
|
|
298
|
+
editor = ENV['TERM_EDITOR'] || ENV['EDITOR']
|
|
299
|
+
unless editor
|
|
300
|
+
warn 'Please set $EDITOR or $TERM_EDITOR in your .bash_profile.'
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
system("#{editor || 'open'} #{tmpfile.path}")
|
|
304
|
+
|
|
305
|
+
tmpfile.rewind
|
|
306
|
+
lines = tmpfile.read.lines.to_a
|
|
307
|
+
puts lines.inspect
|
|
308
|
+
title = lines.shift.chomp
|
|
309
|
+
lines.shift if lines[0].chomp.empty?
|
|
310
|
+
body = lines.join
|
|
311
|
+
tmpfile.unlink
|
|
312
|
+
[title, body]
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def github
|
|
316
|
+
@github ||= ::GitReview::Github.instance
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def local
|
|
320
|
+
@local ||= ::GitReview::Local.instance
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def get_request_by_number(request_number)
|
|
324
|
+
request = github.request_exists?(request_number)
|
|
325
|
+
request || (raise ::GitReview::InvalidRequestIDError)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
end
|
|
331
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module GitReview
|
|
2
|
+
|
|
3
|
+
class AuthenticationError < StandardError
|
|
4
|
+
def message
|
|
5
|
+
'You provided the wrong username/password, please try again.'
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class InvalidGitRepositoryError < StandardError
|
|
10
|
+
def message
|
|
11
|
+
"It is not a valid git repository or doesn't have a valid remote url."
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# A custom error to raise, if we know we can't go on.
|
|
16
|
+
class UnprocessableState < StandardError
|
|
17
|
+
def message
|
|
18
|
+
'Execution of git-review stopped.'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class InvalidArgumentError < StandardError
|
|
23
|
+
def message
|
|
24
|
+
'Please specify valid arguments. See --help for more information.'
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class InvalidRequestIDError < StandardError
|
|
29
|
+
def message
|
|
30
|
+
'Please specify a valid request ID.'
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
|
@@ -0,0 +1,289 @@
|
|
|
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
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
module GitReview
|
|
2
|
+
|
|
3
|
+
# The local repository is where the git-review command is being called
|
|
4
|
+
# by default. It is (supposedly) able to handle systems other than Github.
|
|
5
|
+
# TODO: remove Github-dependency
|
|
6
|
+
class Local
|
|
7
|
+
|
|
8
|
+
include ::GitReview::Internals
|
|
9
|
+
|
|
10
|
+
attr_accessor :config
|
|
11
|
+
|
|
12
|
+
# acts like a singleton class but it's actually not
|
|
13
|
+
# use ::GitReview::Local.instance everywhere except in tests
|
|
14
|
+
def self.instance
|
|
15
|
+
@instance ||= new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
# find root git directory if currently in subdirectory
|
|
20
|
+
if git_call('rev-parse --show-toplevel').strip.empty?
|
|
21
|
+
raise ::GitReview::InvalidGitRepositoryError
|
|
22
|
+
else
|
|
23
|
+
add_pull_refspec
|
|
24
|
+
load_config
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Array<String>] all existing branches
|
|
29
|
+
def all_branches
|
|
30
|
+
git_call('branch -a').split("\n").collect { |s| s.strip }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Array<String>] all open requests' branches shouldn't be deleted
|
|
34
|
+
def protected_branches
|
|
35
|
+
github.current_requests.collect { |r| r.head.ref }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Array<String>] all review branches with 'review_' prefix
|
|
39
|
+
def review_branches
|
|
40
|
+
all_branches.collect { |branch|
|
|
41
|
+
# only use uniq branch names (no matter if local or remote)
|
|
42
|
+
branch.split('/').last if branch.include?('review_')
|
|
43
|
+
}.compact.uniq
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# clean a single request's obsolete branch
|
|
47
|
+
def clean_single(number, force=false)
|
|
48
|
+
request = github.pull_request(source_repo, number)
|
|
49
|
+
if request && request.state == 'closed'
|
|
50
|
+
# ensure there are no unmerged commits or '--force' flag has been set
|
|
51
|
+
branch_name = request.head.ref
|
|
52
|
+
if unmerged_commits?(branch_name) && !force
|
|
53
|
+
puts "Won't delete branches that contain unmerged commits."
|
|
54
|
+
puts "Use '--force' to override."
|
|
55
|
+
else
|
|
56
|
+
delete_branch(branch_name)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
rescue Octokit::NotFound
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# clean all obsolete branches
|
|
64
|
+
def clean_all
|
|
65
|
+
(review_branches - protected_branches).each do |branch_name|
|
|
66
|
+
# only clean up obsolete branches.
|
|
67
|
+
delete_branch(branch_name) unless unmerged_commits?(branch_name, false)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# delete local and remote branches that match a given name
|
|
72
|
+
# @param branch_name [String] name of the branch to delete
|
|
73
|
+
def delete_branch(branch_name)
|
|
74
|
+
delete_local_branch(branch_name)
|
|
75
|
+
delete_remote_branch(branch_name)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# delete local branch if it exists.
|
|
79
|
+
# @param (see #delete_branch)
|
|
80
|
+
def delete_local_branch(branch_name)
|
|
81
|
+
if branch_exists?(:local, branch_name)
|
|
82
|
+
git_call("branch -D #{branch_name}", true)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# delete remote branch if it exists.
|
|
87
|
+
# @param (see #delete_branch)
|
|
88
|
+
def delete_remote_branch(branch_name)
|
|
89
|
+
if branch_exists?(:remote, branch_name)
|
|
90
|
+
git_call("push origin :#{branch_name}", true)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param location [Symbol] location of the branch, `:remote` or `:local`
|
|
95
|
+
# @param branch_name [String] name of the branch
|
|
96
|
+
# @return [Boolean] whether a branch exists in a specified location
|
|
97
|
+
def branch_exists?(location, branch_name)
|
|
98
|
+
return false unless [:remote, :local].include?(location)
|
|
99
|
+
prefix = location == :remote ? 'remotes/origin/' : ''
|
|
100
|
+
all_branches.include?(prefix + branch_name)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @return [Boolean] whether there are local changes not committed
|
|
104
|
+
def uncommitted_changes?
|
|
105
|
+
!git_call('diff HEAD').empty?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @param branch_name [String] name of the branch
|
|
109
|
+
# @param verbose [Boolean] if verbose output
|
|
110
|
+
# @return [Boolean] whether there are unmerged commits on the local or
|
|
111
|
+
# remote branch.
|
|
112
|
+
def unmerged_commits?(branch_name, verbose=true)
|
|
113
|
+
locations = []
|
|
114
|
+
locations << '' if branch_exists?(:local, branch_name)
|
|
115
|
+
locations << 'origin/' if branch_exists?(:remote, branch_name)
|
|
116
|
+
locations = locations.repeated_permutation(2).to_a
|
|
117
|
+
if locations.empty?
|
|
118
|
+
puts 'Nothing to do. All cleaned up already.' if verbose
|
|
119
|
+
return false
|
|
120
|
+
end
|
|
121
|
+
# compare remote and local branch with remote and local master
|
|
122
|
+
responses = locations.collect { |loc|
|
|
123
|
+
git_call "cherry #{loc.first}#{target_branch} #{loc.last}#{branch_name}"
|
|
124
|
+
}
|
|
125
|
+
# select commits (= non empty, not just an error message and not only
|
|
126
|
+
# duplicate commits staring with '-').
|
|
127
|
+
unmerged_commits = responses.reject { |response|
|
|
128
|
+
response.empty? or response.include?('fatal: Unknown commit') or
|
|
129
|
+
response.split("\n").reject { |x| x.index('-') == 0 }.empty?
|
|
130
|
+
}
|
|
131
|
+
# if the array ain't empty, we got unmerged commits
|
|
132
|
+
if unmerged_commits.empty?
|
|
133
|
+
false
|
|
134
|
+
else
|
|
135
|
+
puts "Unmerged commits on branch '#{branch_name}'."
|
|
136
|
+
true
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @return [Boolean] whether a specified commit has already been merged.
|
|
141
|
+
def merged?(sha)
|
|
142
|
+
not git_call("rev-list #{sha} ^HEAD 2>&1").split("\n").size > 0
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @return [String] the source repo
|
|
146
|
+
def source_repo
|
|
147
|
+
github.source_repo
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# @return [String] the current source branch
|
|
151
|
+
def source_branch
|
|
152
|
+
git_call('branch').chomp.match(/\*(.*)/)[0][2..-1]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @return [String] combine source repo and branch
|
|
156
|
+
def source
|
|
157
|
+
"#{source_repo}/#{source_branch}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @return [String] the name of the target branch
|
|
161
|
+
def target_branch
|
|
162
|
+
# TODO: Manually override this and set arbitrary branches
|
|
163
|
+
ENV['TARGET_BRANCH'] || 'master'
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# if to send a pull request to upstream repo, get the parent as target
|
|
167
|
+
# @return [String] the name of the target repo
|
|
168
|
+
def target_repo(upstream=false)
|
|
169
|
+
# TODO: Manually override this and set arbitrary repositories
|
|
170
|
+
if upstream
|
|
171
|
+
github.repository(source_repo).parent.full_name
|
|
172
|
+
else
|
|
173
|
+
source_repo
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# @return [String] combine target repo and branch
|
|
178
|
+
def target
|
|
179
|
+
"#{target_repo}/#{target_branch}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @return [String] the head string used for pull requests
|
|
183
|
+
def head
|
|
184
|
+
# in the form of 'user:branch'
|
|
185
|
+
"#{github.github.login}:#{source_branch}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# @return [Boolean] whether already on a feature branch
|
|
189
|
+
def on_feature_branch?
|
|
190
|
+
# If current and target are the same, we are not on a feature branch.
|
|
191
|
+
# If they are different, but we are on master, we should still to switch
|
|
192
|
+
# to a separate branch (since master makes for a poor feature branch).
|
|
193
|
+
source_branch != target_branch && source_branch != 'master'
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# add remote.origin.fetch to check out pull request locally
|
|
197
|
+
# see {https://help.github.com/articles/checking-out-pull-requests-locally}
|
|
198
|
+
def add_pull_refspec
|
|
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)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def load_config
|
|
205
|
+
@config = {}
|
|
206
|
+
config_list.split("\n").each do |line|
|
|
207
|
+
key, value = line.split(/=/, 2)
|
|
208
|
+
if @config[key] && @config[key] != value
|
|
209
|
+
@config[key] = [@config[key]].flatten << value
|
|
210
|
+
else
|
|
211
|
+
@config[key] = value
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
@config
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def config_list
|
|
218
|
+
git_call('config --list', false)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def github
|
|
222
|
+
@github ||= ::GitReview::Github.instance
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'yaml'
|
|
3
|
+
|
|
4
|
+
module GitReview
|
|
5
|
+
|
|
6
|
+
class Settings
|
|
7
|
+
|
|
8
|
+
# acts like a singleton class but it's actually not
|
|
9
|
+
# use ::GitReview::Settings.instance everywhere except in tests
|
|
10
|
+
def self.instance
|
|
11
|
+
@instance ||= new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Read settings from ~/.git_review.yml upon initialization.
|
|
15
|
+
def initialize
|
|
16
|
+
@config_file = File.join(Dir.home, '.git_review.yml')
|
|
17
|
+
@config = YAML.load_file(@config_file) if File.exists?(@config_file)
|
|
18
|
+
@config ||= {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Write settings back to file.
|
|
22
|
+
def save!
|
|
23
|
+
File.open(@config_file, 'w') do |file|
|
|
24
|
+
file.write(YAML.dump(@config))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Allow to access config options.
|
|
29
|
+
def method_missing(method, *args)
|
|
30
|
+
# Determine whether to set or get an attribute.
|
|
31
|
+
if method.to_s =~ /(.*)=$/
|
|
32
|
+
@config[$1.to_sym] = args.shift
|
|
33
|
+
else
|
|
34
|
+
@config[method.to_sym]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def respond_to?(method)
|
|
39
|
+
if method.to_s =~ /(.*)=$/ || @config.keys.include?(method.to_sym)
|
|
40
|
+
true
|
|
41
|
+
else
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: git-review
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.7
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2013-
|
|
12
|
+
date: 2013-09-04 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: launchy
|
|
@@ -32,17 +32,17 @@ dependencies:
|
|
|
32
32
|
requirement: !ruby/object:Gem::Requirement
|
|
33
33
|
none: false
|
|
34
34
|
requirements:
|
|
35
|
-
- -
|
|
35
|
+
- - ~>
|
|
36
36
|
- !ruby/object:Gem::Version
|
|
37
|
-
version:
|
|
37
|
+
version: 1.24.0
|
|
38
38
|
type: :runtime
|
|
39
39
|
prerelease: false
|
|
40
40
|
version_requirements: !ruby/object:Gem::Requirement
|
|
41
41
|
none: false
|
|
42
42
|
requirements:
|
|
43
|
-
- -
|
|
43
|
+
- - ~>
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
|
-
version:
|
|
45
|
+
version: 1.24.0
|
|
46
46
|
- !ruby/object:Gem::Dependency
|
|
47
47
|
name: yajl-ruby
|
|
48
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -85,6 +85,12 @@ files:
|
|
|
85
85
|
- LICENSE
|
|
86
86
|
- lib/settings.rb
|
|
87
87
|
- lib/git-review.rb
|
|
88
|
+
- lib/git-review/settings.rb
|
|
89
|
+
- lib/git-review/commands.rb
|
|
90
|
+
- lib/git-review/errors.rb
|
|
91
|
+
- lib/git-review/github.rb
|
|
92
|
+
- lib/git-review/internals.rb
|
|
93
|
+
- lib/git-review/local.rb
|
|
88
94
|
- lib/oauth_helper.rb
|
|
89
95
|
- bin/git-review
|
|
90
96
|
homepage: http://github.com/b4mboo/git-review
|
|
@@ -112,3 +118,4 @@ signing_key:
|
|
|
112
118
|
specification_version: 3
|
|
113
119
|
summary: facilitates GitHub code reviews
|
|
114
120
|
test_files: []
|
|
121
|
+
has_rdoc:
|