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