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.
Files changed (2) hide show
  1. data/lib/git-review.rb +147 -35
  2. metadata +5 -5
@@ -16,8 +16,8 @@ class GitReview
16
16
 
17
17
  # List all pending requests.
18
18
  def list
19
- @pending_requests.reverse! if @args.shift == '--reverse'
20
- output = @pending_requests.collect do |pending_request|
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 = @pending_request['head']['sha']
45
- puts "ID : #{@pending_request['number']}"
46
- puts "Label : #{@pending_request['head']['label']}"
47
- puts "Updated : #{format_time(@pending_request['updated_at'])}"
48
- puts "Comments : #{@pending_request['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 @pending_request['title']
50
+ puts @current_request['title']
51
51
  puts
52
- puts @pending_request['body']
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(@pending_request['html_url']) if request_exists?
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}#{@pending_request['head']['ref']}"
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 @pending_request['head']['repository']
82
+ unless @current_request['head']['repository']
83
83
  # Someone deleted the source repo.
84
- user = @pending_request['head']['user']['login']
85
- url = @pending_request['patch_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 ##{@pending_request['number']} and merge changes into \"#{target}\""
95
- exec_cmd = "merge #{option} -m '#{message}' #{@pending_request['head']['sha']}"
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 " #{@pending_request['title']}"
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, @pending_request['number'], comment
111
- puts 'Successfully approved request.' if response[:body] == comment
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, @pending_request['number'])
118
- puts 'Successfully closed request.' unless request_exists?(@pending_request['number'])
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 = @pending_requests.collect{|req| req['number'] }.sort.last.to_i
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 = @pending_requests.find{ |req| req['title'] == title }
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 @pending_request.
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
- @pending_request = @pending_requests.find{ |req| req['number'] == request_id }
244
- if @pending_request.nil?
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}' does not exist." unless automated
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
- @pending_requests = Octokit.pull_requests(source_repo)
255
- repos = @pending_requests.collect do |req|
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
- # Show current discussion for @pending_request.
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, @pending_request['number'])
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: 59
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
+ - 1
7
8
  - 0
8
- - 9
9
9
  - 0
10
- version: 0.9.0
10
+ version: 1.0.0
11
11
  platform: ruby
12
12
  authors:
13
- - Dominik Bamberger, Cristian Messel
13
+ - Dominik Bamberger
14
14
  autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-02-09 00:00:00 Z
18
+ date: 2012-02-10 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: launchy