git-review 0.9.0 → 1.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.
- 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
|