git-review 0.9.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/git-review.rb +147 -35
- metadata +5 -5
data/lib/git-review.rb
CHANGED
@@ -16,8 +16,8 @@ class GitReview
|
|
16
16
|
|
17
17
|
# List all pending requests.
|
18
18
|
def list
|
19
|
-
@
|
20
|
-
output = @
|
19
|
+
@current_requests.reverse! if @args.shift == '--reverse'
|
20
|
+
output = @current_requests.collect do |pending_request|
|
21
21
|
# Find only pending (= unmerged) requests and output summary. GitHub might
|
22
22
|
# still think of them as pending, as it doesn't know about local changes.
|
23
23
|
next if merged?(pending_request['head']['sha'])
|
@@ -41,15 +41,15 @@ class GitReview
|
|
41
41
|
def show
|
42
42
|
return unless request_exists?
|
43
43
|
option = @args.shift == '--full' ? '' : '--stat '
|
44
|
-
sha = @
|
45
|
-
puts "ID : #{@
|
46
|
-
puts "Label : #{@
|
47
|
-
puts "Updated : #{format_time(@
|
48
|
-
puts "Comments : #{@
|
44
|
+
sha = @current_request['head']['sha']
|
45
|
+
puts "ID : #{@current_request['number']}"
|
46
|
+
puts "Label : #{@current_request['head']['label']}"
|
47
|
+
puts "Updated : #{format_time(@current_request['updated_at'])}"
|
48
|
+
puts "Comments : #{@current_request['comments']}"
|
49
49
|
puts
|
50
|
-
puts @
|
50
|
+
puts @current_request['title']
|
51
51
|
puts
|
52
|
-
puts @
|
52
|
+
puts @current_request['body']
|
53
53
|
puts
|
54
54
|
puts git_call("diff --color=always #{option}HEAD...#{sha}")
|
55
55
|
puts
|
@@ -60,7 +60,7 @@ class GitReview
|
|
60
60
|
|
61
61
|
# Open a browser window and review a specified request.
|
62
62
|
def browse
|
63
|
-
Launchy.open(@
|
63
|
+
Launchy.open(@current_request['html_url']) if request_exists?
|
64
64
|
end
|
65
65
|
|
66
66
|
# Checkout a specified request's changes to your local repository.
|
@@ -72,17 +72,17 @@ class GitReview
|
|
72
72
|
puts
|
73
73
|
puts ' git checkout master'
|
74
74
|
puts
|
75
|
-
git_call "checkout #{create_local_branch}#{@
|
75
|
+
git_call "checkout #{create_local_branch}#{@current_request['head']['ref']}"
|
76
76
|
end
|
77
77
|
|
78
78
|
# Accept a specified request by merging it into master.
|
79
79
|
def merge
|
80
80
|
return unless request_exists?
|
81
81
|
option = @args.shift
|
82
|
-
unless @
|
82
|
+
unless @current_request['head']['repository']
|
83
83
|
# Someone deleted the source repo.
|
84
|
-
user = @
|
85
|
-
url = @
|
84
|
+
user = @current_request['head']['user']['login']
|
85
|
+
url = @current_request['patch_url']
|
86
86
|
puts "Sorry, #{user} deleted the source repository, git-review doesn't support this."
|
87
87
|
puts 'You can manually patch your repo by running:'
|
88
88
|
puts
|
@@ -91,11 +91,11 @@ class GitReview
|
|
91
91
|
puts 'Tell the contributor not to do this.'
|
92
92
|
return false
|
93
93
|
end
|
94
|
-
message = "Accept request ##{@
|
95
|
-
exec_cmd = "merge #{option} -m '#{message}' #{@
|
94
|
+
message = "Accept request ##{@current_request['number']} and merge changes into \"#{target}\""
|
95
|
+
exec_cmd = "merge #{option} -m '#{message}' #{@current_request['head']['sha']}"
|
96
96
|
puts
|
97
97
|
puts 'Request title:'
|
98
|
-
puts " #{@
|
98
|
+
puts " #{@current_request['title']}"
|
99
99
|
puts
|
100
100
|
puts 'Merge command:'
|
101
101
|
puts " git #{exec_cmd}"
|
@@ -107,15 +107,28 @@ class GitReview
|
|
107
107
|
def approve
|
108
108
|
return unless request_exists?
|
109
109
|
comment = 'Reviewed and approved.'
|
110
|
-
response = Octokit.add_comment source_repo, @
|
111
|
-
|
110
|
+
response = Octokit.add_comment source_repo, @current_request['number'], comment
|
111
|
+
if response[:message] == 'Issues are disabled for this repo'
|
112
|
+
# Workaround: Add a pull request comment to the last commit's first file's first line.
|
113
|
+
comment += "\n\nNOTE:\n Issues are disabled on this repository.\n Comments created through API calls may only be inline.\n So I chose to just post here. :P"
|
114
|
+
last_commit = repo_call(:get, "pulls/#{@current_request['number']}/commits").last.sha
|
115
|
+
first_file = repo_call(:get, "pulls/#{@current_request['number']}/files").first.filename
|
116
|
+
response = repo_call(:post, "pulls/#{@current_request['number']}/comments",
|
117
|
+
{:body => comment, :commit_id => last_commit, :path => first_file, :position => 1}
|
118
|
+
)
|
119
|
+
end
|
120
|
+
if response[:body] == comment
|
121
|
+
puts 'Successfully approved request.'
|
122
|
+
else
|
123
|
+
puts response[:message]
|
124
|
+
end
|
112
125
|
end
|
113
126
|
|
114
127
|
# Close a specified request.
|
115
128
|
def close
|
116
129
|
return unless request_exists?
|
117
|
-
Octokit.close_issue(source_repo, @
|
118
|
-
puts 'Successfully closed request.' unless request_exists?(@
|
130
|
+
Octokit.close_issue(source_repo, @current_request['number'])
|
131
|
+
puts 'Successfully closed request.' unless request_exists?('open', @current_request['number'])
|
119
132
|
end
|
120
133
|
|
121
134
|
# Prepare local repository to create a new request.
|
@@ -161,7 +174,7 @@ class GitReview
|
|
161
174
|
# Push latest commits to the remote branch (and by that, create it if necessary).
|
162
175
|
git_call "push --set-upstream origin #{@local_branch}", debug_mode, true
|
163
176
|
# Gather information.
|
164
|
-
last_request_id = @
|
177
|
+
last_request_id = @current_requests.collect{|req| req['number'] }.sort.last.to_i
|
165
178
|
title = "[Review] Request from '#{git_config['github.login']}' @ '#{source}'"
|
166
179
|
# TODO: Insert commit messages (that are not yet in master) into body (since this will be displayed inside the mail that is sent out).
|
167
180
|
body = 'Please review the following changes:'
|
@@ -170,7 +183,7 @@ class GitReview
|
|
170
183
|
# Switch back to target_branch and check for success.
|
171
184
|
git_call "checkout #{target_branch}"
|
172
185
|
update
|
173
|
-
potential_new_request = @
|
186
|
+
potential_new_request = @current_requests.find{ |req| req['title'] == title }
|
174
187
|
if potential_new_request and potential_new_request['number'] > last_request_id
|
175
188
|
puts "Successfully created new request ##{potential_new_request['number']}."
|
176
189
|
end
|
@@ -179,6 +192,28 @@ class GitReview
|
|
179
192
|
end
|
180
193
|
end
|
181
194
|
|
195
|
+
# Deletes obsolete branches (left over from already closed requests).
|
196
|
+
def clean
|
197
|
+
# Determine strategy to clean.
|
198
|
+
case @args.size
|
199
|
+
when 0
|
200
|
+
puts 'Argument missing. Please provide either an ID or the option "--all".'
|
201
|
+
when 1
|
202
|
+
if @args.first == '--all'
|
203
|
+
# git review clean --all
|
204
|
+
clean_all
|
205
|
+
else
|
206
|
+
# git review clean ID
|
207
|
+
clean_single
|
208
|
+
end
|
209
|
+
when 2
|
210
|
+
# git review clean ID --force
|
211
|
+
clean_single(@args.last == '--force')
|
212
|
+
else
|
213
|
+
puts 'Too many arguments.'
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
182
217
|
# Start a console session (used for debugging).
|
183
218
|
def console
|
184
219
|
puts 'Entering debug console.'
|
@@ -200,7 +235,7 @@ class GitReview
|
|
200
235
|
return if @user.nil? or @repo.nil?
|
201
236
|
@args = args
|
202
237
|
return unless configure_github_access
|
203
|
-
update
|
238
|
+
update unless command == 'clean'
|
204
239
|
self.send command
|
205
240
|
else
|
206
241
|
unless command.nil? or command.empty? or %w(help -h --help).include?(command)
|
@@ -227,32 +262,34 @@ class GitReview
|
|
227
262
|
puts ' close <ID> Close a specified request.'
|
228
263
|
puts ' prepare Creates a new local branch for a request.'
|
229
264
|
puts ' create Create a new request.'
|
265
|
+
puts ' clean <ID> [--force] Delete a request\'s remote and local branches.'
|
266
|
+
puts ' clean --all? Delete all obsolete branches.'
|
230
267
|
end
|
231
268
|
|
232
|
-
# Check existence of specified request and assign @
|
233
|
-
def request_exists?(request_id = nil)
|
269
|
+
# Check existence of specified request and assign @current_request.
|
270
|
+
def request_exists?(state = 'open', request_id = nil)
|
234
271
|
# NOTE: If request_id is set explicitly we might need to update to get the
|
235
272
|
# latest changes from GitHub, as this is called from within another method.
|
236
273
|
automated = !request_id.nil?
|
237
|
-
update if automated
|
274
|
+
update(state) if automated
|
238
275
|
request_id ||= @args.shift.to_i
|
239
276
|
if request_id == 0
|
240
277
|
puts 'Please specify a valid ID.'
|
241
278
|
return false
|
242
279
|
end
|
243
|
-
@
|
244
|
-
if @
|
280
|
+
@current_request = @current_requests.find{ |req| req['number'] == request_id }
|
281
|
+
if @current_request.nil?
|
245
282
|
# No output for automated checks.
|
246
|
-
puts "Request '#{request_id}'
|
283
|
+
puts "Request '#{request_id}' could not be found among all '#{state}' requests." unless automated
|
247
284
|
return false
|
248
285
|
end
|
249
286
|
true
|
250
287
|
end
|
251
288
|
|
252
289
|
# Get latest changes from GitHub.
|
253
|
-
def update
|
254
|
-
@
|
255
|
-
repos = @
|
290
|
+
def update(state = 'open')
|
291
|
+
@current_requests = Octokit.pull_requests(source_repo, state)
|
292
|
+
repos = @current_requests.collect do |req|
|
256
293
|
repo = req['head']['repository']
|
257
294
|
"#{repo['owner']}/#{repo['name']}" unless repo.nil?
|
258
295
|
end
|
@@ -261,6 +298,71 @@ class GitReview
|
|
261
298
|
end
|
262
299
|
end
|
263
300
|
|
301
|
+
# Cleans a single request's obsolete branches.
|
302
|
+
def clean_single(force_deletion = false)
|
303
|
+
update('closed')
|
304
|
+
return unless request_exists?('closed')
|
305
|
+
# Ensure there are no unmerged commits or '--force' flag has been set.
|
306
|
+
branch_name = @current_request['head']['ref']
|
307
|
+
if unmerged_commits?(branch_name) and not force_deletion
|
308
|
+
return puts "Won't delete branches that contain unmerged commits. Use '--force' to override."
|
309
|
+
end
|
310
|
+
delete_branch(branch_name)
|
311
|
+
end
|
312
|
+
|
313
|
+
# Cleans all obsolete branches.
|
314
|
+
def clean_all
|
315
|
+
update
|
316
|
+
# Protect all open requests' branches from deletion.
|
317
|
+
protected_branches = @current_requests.collect{|request| request['head']['ref']}
|
318
|
+
# Select all branches with the correct prefix.
|
319
|
+
review_branches = all_branches.select{|branch| branch.include?('review_')}
|
320
|
+
# Only use uniq branch names (no matter if local or remote).
|
321
|
+
review_branches.collect{|branch| branch.split('/').last}.uniq.each do |branch_name|
|
322
|
+
# Only clean up obsolete branches.
|
323
|
+
unless protected_branches.include?(branch_name) or unmerged_commits?(branch_name, false)
|
324
|
+
delete_branch(branch_name)
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# Delete local and remote branches that match a given name.
|
330
|
+
def delete_branch(branch_name)
|
331
|
+
# Delete local branch if it exists.
|
332
|
+
git_call("branch -D #{branch_name}") if branch_exists?(:local, branch_name)
|
333
|
+
# Delete remote branch if it exists.
|
334
|
+
git_call("push origin :#{branch_name}") if branch_exists?(:remote, branch_name)
|
335
|
+
end
|
336
|
+
|
337
|
+
# Returns a boolean stating whether there are unmerged commits on the local or remote branch.
|
338
|
+
def unmerged_commits?(branch_name, verbose = true)
|
339
|
+
locations = []
|
340
|
+
locations << ['', ''] if branch_exists?(:local, branch_name)
|
341
|
+
locations << ['origin/', 'origin/'] if branch_exists?(:remote, branch_name)
|
342
|
+
locations = locations + [['', 'origin/'], ['origin/', '']] if locations.size == 2
|
343
|
+
if locations.empty?
|
344
|
+
puts 'Nothing to do. All cleaned up already.' if verbose
|
345
|
+
return false
|
346
|
+
end
|
347
|
+
# Compare remote and local branch with remote and local master.
|
348
|
+
responses = locations.collect do |location|
|
349
|
+
git_call "cherry #{location.first}#{target_branch} #{location.last}#{branch_name}"
|
350
|
+
end
|
351
|
+
# Select commits (= non empty and not just an error message).
|
352
|
+
unmerged_commits = responses.select do |response|
|
353
|
+
not (response.empty? or response.include?('fatal: Unknown commit'))
|
354
|
+
end
|
355
|
+
# If the array ain't empty, we got unmerged commits.
|
356
|
+
not unmerged_commits.empty?
|
357
|
+
end
|
358
|
+
|
359
|
+
# Returns a boolean stating whether a branch exists in a specified location.
|
360
|
+
def branch_exists?(location, branch_name)
|
361
|
+
return false unless [:remote, :local].include? location
|
362
|
+
prefix = location == :remote ? 'remotes/origin/' : ''
|
363
|
+
all_branches.include?(prefix + branch_name)
|
364
|
+
end
|
365
|
+
|
264
366
|
# System call to 'git'.
|
265
367
|
def git_call(command, verbose = debug_mode, enforce_success = false)
|
266
368
|
if verbose
|
@@ -278,9 +380,14 @@ class GitReview
|
|
278
380
|
output
|
279
381
|
end
|
280
382
|
|
281
|
-
#
|
383
|
+
# Convenience method that uses Octokit to access a repo through Github's API.
|
384
|
+
def repo_call(method, path, options = {})
|
385
|
+
Octokit.send(method, "/repos/#{Octokit::Repository.new(source_repo)}/#{path}", options, 3)
|
386
|
+
end
|
387
|
+
|
388
|
+
# Show current discussion for @current_request.
|
282
389
|
def discussion
|
283
|
-
request = Octokit.pull_request(source_repo, @
|
390
|
+
request = Octokit.pull_request(source_repo, @current_request['number'])
|
284
391
|
result = request['discussion'].collect do |entry|
|
285
392
|
output = "\e[35m#{entry["user"]["login"]}\e[m "
|
286
393
|
case entry['type']
|
@@ -350,6 +457,11 @@ class GitReview
|
|
350
457
|
"#{target_repo}/#{target_branch}"
|
351
458
|
end
|
352
459
|
|
460
|
+
# Returns an Array of all existing branches.
|
461
|
+
def all_branches
|
462
|
+
@branches ||= git_call('branch -a').split("\n").collect{|s|s.strip}
|
463
|
+
end
|
464
|
+
|
353
465
|
# Returns a boolean stating whether a specified commit has already been merged.
|
354
466
|
def merged?(sha)
|
355
467
|
not git_call("rev-list #{sha} ^HEAD 2>&1").split("\n").size > 0
|
metadata
CHANGED
@@ -1,21 +1,21 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: git-review
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
|
+
- 1
|
7
8
|
- 0
|
8
|
-
- 9
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 1.0.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
|
-
- Dominik Bamberger
|
13
|
+
- Dominik Bamberger
|
14
14
|
autorequire:
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-02-
|
18
|
+
date: 2012-02-10 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: launchy
|