git-review 1.1.3 → 1.1.5

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 +124 -49
  2. metadata +23 -7
@@ -4,6 +4,9 @@ require 'octokit'
4
4
  require 'launchy'
5
5
  # Time is used to parse time strings from git back into Time objects.
6
6
  require 'time'
7
+ # tempfile is used to create a temporary file containing PR's title and body.
8
+ # This file is going to be edited by the system editor.
9
+ require 'tempfile'
7
10
 
8
11
  # A custom error to raise, if we know we can't go on.
9
12
  class UnprocessableState < StandardError
@@ -16,27 +19,30 @@ class GitReview
16
19
 
17
20
  # List all pending requests.
18
21
  def list
19
- @current_requests.reverse! if @args.shift == '--reverse'
20
- output = @current_requests.collect do |pending_request|
21
- # Find only pending (= unmerged) requests and output summary. GitHub might
22
- # still think of them as pending, as it doesn't know about local changes.
23
- next if merged?(pending_request['head']['sha'])
24
- line = format_text(pending_request['number'], 8)
25
- date_string = format_time(pending_request['updated_at'])
22
+ output = @current_requests.collect do |request|
23
+ details = @github.pull_request(source_repo, request.number)
24
+ # Find only pending (= unmerged) requests and output summary.
25
+ # Explicitly look for local changes (that GitHub does not yet know about).
26
+ next if merged?(request.head.sha)
27
+ line = format_text(request.number, 8)
28
+ date_string = format_time(request.updated_at)
26
29
  line << format_text(date_string, 11)
27
- line << format_text(pending_request['comments'], 10)
28
- line << format_text(pending_request['title'], 91)
30
+ line << format_text(details.comments + details.review_comments, 10)
31
+ line << format_text(request.title, 91)
29
32
  line
30
33
  end
31
- if output.compact.empty?
32
- puts "No pending requests for '#{source}'"
33
- return
34
+ output.compact!
35
+ if output.empty?
36
+ puts "No pending requests for '#{source}'."
37
+ else
38
+ puts "Pending requests for '#{source}':"
39
+ puts 'ID Updated Comments Title'
40
+ output.reverse! if @args.shift == '--reverse'
41
+ output.each { |line| puts line }
34
42
  end
35
- puts "Pending requests for '#{source}'"
36
- puts 'ID Updated Comments Title'
37
- puts output.compact
38
43
  end
39
44
 
45
+
40
46
  # Show details for a single request.
41
47
  def show
42
48
  return unless request_exists?
@@ -58,11 +64,13 @@ class GitReview
58
64
  discussion
59
65
  end
60
66
 
67
+
61
68
  # Open a browser window and review a specified request.
62
69
  def browse
63
70
  Launchy.open(@current_request['html_url']) if request_exists?
64
71
  end
65
72
 
73
+
66
74
  # Checkout a specified request's changes to your local repository.
67
75
  def checkout
68
76
  return unless request_exists?
@@ -75,6 +83,7 @@ class GitReview
75
83
  git_call "checkout #{create_local_branch}#{@current_request['head']['ref']}"
76
84
  end
77
85
 
86
+
78
87
  # Accept a specified request by merging it into master.
79
88
  def merge
80
89
  return unless request_exists?
@@ -104,6 +113,7 @@ class GitReview
104
113
  puts git_call(exec_cmd)
105
114
  end
106
115
 
116
+
107
117
  # Add an approving comment to the request.
108
118
  def approve
109
119
  return unless request_exists?
@@ -116,6 +126,7 @@ class GitReview
116
126
  end
117
127
  end
118
128
 
129
+
119
130
  # Close a specified request.
120
131
  def close
121
132
  return unless request_exists?
@@ -123,6 +134,7 @@ class GitReview
123
134
  puts 'Successfully closed request.' unless request_exists?('open', @current_request['number'])
124
135
  end
125
136
 
137
+
126
138
  # Prepare local repository to create a new request.
127
139
  # Sets @local_branch.
128
140
  def prepare
@@ -154,6 +166,7 @@ class GitReview
154
166
  end
155
167
  end
156
168
 
169
+
157
170
  # Create a new request.
158
171
  # TODO: Support creating requests to other repositories and branches (like the original repo, this has been forked from).
159
172
  def create
@@ -168,18 +181,17 @@ class GitReview
168
181
  # Push latest commits to the remote branch (and by that, create it if necessary).
169
182
  git_call "push --set-upstream origin #{@local_branch}", debug_mode, true
170
183
  # Gather information.
171
- last_request_id = @current_requests.collect{|req| req['number'] }.sort.last.to_i
172
- title = "[Review] Request from '#{git_config['github.login']}' @ '#{source}'"
173
- # TODO: Insert commit messages (that are not yet in master) into body (since this will be displayed inside the mail that is sent out).
174
- body = 'Please review the following changes:'
184
+ last_request_id = @current_requests.collect { |req| req['number'] }.sort.last.to_i
185
+ title, body = create_title_and_body(target_branch)
175
186
  # Create the actual pull request.
176
187
  @github.create_pull_request target_repo, target_branch, source_branch, title, body
177
188
  # Switch back to target_branch and check for success.
178
189
  git_call "checkout #{target_branch}"
179
190
  update
180
- potential_new_request = @current_requests.find{ |req| req['title'] == title }
191
+ potential_new_request = @current_requests.find { |req| req['title'] == title }
181
192
  if potential_new_request and potential_new_request['number'] > last_request_id
182
- puts "Successfully created new request ##{potential_new_request['number']}."
193
+ puts "Successfully created new request ##{potential_new_request['number']}" \
194
+ "(#{File.join("https://github.com", target_repo, "pull", potential_new_request['number'].to_s)})."
183
195
  end
184
196
  # Return to the user's original branch.
185
197
  git_call "checkout #{@original_branch}"
@@ -188,10 +200,11 @@ class GitReview
188
200
  end
189
201
  end
190
202
 
203
+
191
204
  # Deletes obsolete branches (left over from already closed requests).
192
205
  def clean
193
206
  # Pruning is needed to remove already deleted branches from your local track.
194
- git_call "remote prune origin"
207
+ git_call 'remote prune origin'
195
208
  # Determine strategy to clean.
196
209
  case @args.size
197
210
  when 0
@@ -212,6 +225,7 @@ class GitReview
212
225
  end
213
226
  end
214
227
 
228
+
215
229
  # Start a console session (used for debugging).
216
230
  def console
217
231
  puts 'Entering debug console.'
@@ -226,13 +240,12 @@ class GitReview
226
240
  private
227
241
 
228
242
  # Setup variables and call actual commands.
229
- def initialize(args)
243
+ def initialize(args = [])
244
+ @args = args
230
245
  command = args.shift
231
246
  if command and self.respond_to?(command)
232
247
  @user, @repo = repo_info
233
- return if @user.nil? or @repo.nil?
234
- @args = args
235
- return unless configure_github_access
248
+ return unless @user && @repo && configure_github_access
236
249
  update unless command == 'clean'
237
250
  self.send command
238
251
  else
@@ -245,6 +258,7 @@ class GitReview
245
258
  puts 'Execution of git-review command stopped.'
246
259
  end
247
260
 
261
+
248
262
  # Show a quick reference of available commands.
249
263
  def help
250
264
  puts 'Usage: git review <command>'
@@ -261,9 +275,10 @@ class GitReview
261
275
  puts ' prepare Creates a new local branch for a request.'
262
276
  puts ' create Create a new request.'
263
277
  puts ' clean <ID> [--force] Delete a request\'s remote and local branches.'
264
- puts ' clean --all? Delete all obsolete branches.'
278
+ puts ' clean --all Delete all obsolete branches.'
265
279
  end
266
280
 
281
+
267
282
  # Check existence of specified request and assign @current_request.
268
283
  def request_exists?(state = 'open', request_id = nil)
269
284
  # NOTE: If request_id is set explicitly we might need to update to get the
@@ -275,7 +290,7 @@ class GitReview
275
290
  puts 'Please specify a valid ID.'
276
291
  return false
277
292
  end
278
- @current_request = @current_requests.find{ |req| req['number'] == request_id }
293
+ @current_request = @current_requests.find { |req| req['number'] == request_id }
279
294
  unless @current_request
280
295
  # Additional try to get an older request from Github by specifying the number.
281
296
  request = @github.pull_request source_repo, request_id
@@ -290,18 +305,20 @@ class GitReview
290
305
  end
291
306
  end
292
307
 
308
+
293
309
  # Get latest changes from GitHub.
294
310
  def update(state = 'open')
295
- @current_requests = @github.pull_requests source_repo, state
296
- repos = @current_requests.collect do |req|
297
- repo = req['head']['repository']
298
- "#{repo['owner']}/#{repo['name']}" unless repo.nil?
311
+ @current_requests = @github.pull_requests(source_repo, state)
312
+ repos = @current_requests.collect do |request|
313
+ repo = request.head.repository
314
+ "#{repo.owner}/#{repo.name}" if repo
299
315
  end
300
316
  repos.uniq.compact.each do |repo|
301
- git_call("fetch git@github.com:#{repo}.git +refs/heads/*:refs/pr/#{repo}/*")
317
+ git_call "fetch git@github.com:#{repo}.git +refs/heads/*:refs/pr/#{repo}/*"
302
318
  end
303
319
  end
304
320
 
321
+
305
322
  # Cleans a single request's obsolete branches.
306
323
  def clean_single(force_deletion = false)
307
324
  update('closed')
@@ -314,15 +331,16 @@ class GitReview
314
331
  delete_branch(branch_name)
315
332
  end
316
333
 
334
+
317
335
  # Cleans all obsolete branches.
318
336
  def clean_all
319
337
  update
320
338
  # Protect all open requests' branches from deletion.
321
- protected_branches = @current_requests.collect{|request| request['head']['ref']}
339
+ protected_branches = @current_requests.collect { |request| request['head']['ref'] }
322
340
  # Select all branches with the correct prefix.
323
- review_branches = all_branches.select{|branch| branch.include?('review_')}
341
+ review_branches = all_branches.select { |branch| branch.include?('review_') }
324
342
  # Only use uniq branch names (no matter if local or remote).
325
- review_branches.collect{|branch| branch.split('/').last}.uniq.each do |branch_name|
343
+ review_branches.collect { |branch| branch.split('/').last }.uniq.each do |branch_name|
326
344
  # Only clean up obsolete branches.
327
345
  unless protected_branches.include?(branch_name) or unmerged_commits?(branch_name, false)
328
346
  delete_branch(branch_name)
@@ -330,6 +348,7 @@ class GitReview
330
348
  end
331
349
  end
332
350
 
351
+
333
352
  # Delete local and remote branches that match a given name.
334
353
  def delete_branch(branch_name)
335
354
  # Delete local branch if it exists.
@@ -338,6 +357,7 @@ class GitReview
338
357
  git_call("push origin :#{branch_name}", true) if branch_exists?(:remote, branch_name)
339
358
  end
340
359
 
360
+
341
361
  # Returns a boolean stating whether there are unmerged commits on the local or remote branch.
342
362
  def unmerged_commits?(branch_name, verbose = true)
343
363
  locations = []
@@ -354,7 +374,7 @@ class GitReview
354
374
  end
355
375
  # Select commits (= non empty, not just an error message and not only duplicate commits staring with '-').
356
376
  unmerged_commits = responses.reject do |response|
357
- response.empty? or response.include?('fatal: Unknown commit') or response.split("\n").reject{|x| x.index('-') == 0}.empty?
377
+ response.empty? or response.include?('fatal: Unknown commit') or response.split("\n").reject { |x| x.index('-') == 0 }.empty?
358
378
  end
359
379
  # If the array ain't empty, we got unmerged commits.
360
380
  if unmerged_commits.empty?
@@ -365,6 +385,7 @@ class GitReview
365
385
  end
366
386
  end
367
387
 
388
+
368
389
  # Returns a boolean stating whether a branch exists in a specified location.
369
390
  def branch_exists?(location, branch_name)
370
391
  return false unless [:remote, :local].include? location
@@ -372,6 +393,7 @@ class GitReview
372
393
  all_branches.include?(prefix + branch_name)
373
394
  end
374
395
 
396
+
375
397
  # System call to 'git'.
376
398
  def git_call(command, verbose = debug_mode, enforce_success = false)
377
399
  if verbose
@@ -389,6 +411,7 @@ class GitReview
389
411
  output
390
412
  end
391
413
 
414
+
392
415
  # Show current discussion for @current_request.
393
416
  def discussion
394
417
  request = @github.pull_request source_repo, @current_request['number']
@@ -404,7 +427,7 @@ class GitReview
404
427
  when "IssueComment", "CommitComment", "PullRequestReviewComment"
405
428
  output << "added a comment"
406
429
  output << " to \e[36m#{entry['commit_id'][0..6]}\e[m" if entry['commit_id']
407
- output << " on #{format_time(entry['created_at'])}"
430
+ output << " on #{format_time(entry['created_at'])}"
408
431
  unless entry['created_at'] == entry['updated_at']
409
432
  output << " (updated on #{format_time(entry['updated_at'])})"
410
433
  end
@@ -413,75 +436,86 @@ class GitReview
413
436
  output << entry['body']
414
437
  # Commits:
415
438
  when "Commit"
416
- output << "authored commit \e[36m#{entry['id'][0..6]}\e[m on #{format_time(entry['authored_date'])}"
417
- unless entry['authored_date'] == entry['committed_date']
418
- output << " (committed on #{format_time(entry['committed_date'])})"
419
- end
420
- output << ":\n#{''.rjust(output.length + 1, "-")}\n#{entry["message"]}"
439
+ output << "authored commit \e[36m#{entry['id'][0..6]}\e[m on #{format_time(entry['authored_date'])}"
440
+ unless entry['authored_date'] == entry['committed_date']
441
+ output << " (committed on #{format_time(entry['committed_date'])})"
442
+ end
443
+ output << ":\n#{''.rjust(output.length + 1, "-")}\n#{entry["message"]}"
421
444
  end
422
445
  output << "\n\n\n"
423
446
  end
424
447
  puts result.compact unless result.empty?
425
448
  end
426
449
 
450
+
427
451
  # Display helper to make output more configurable.
428
452
  def format_text(info, size)
429
453
  info.to_s.gsub("\n", ' ')[0, size-1].ljust(size)
430
454
  end
431
455
 
456
+
432
457
  # Display helper to unify time output.
433
458
  def format_time(time_string)
434
459
  Time.parse(time_string).strftime('%d-%b-%y')
435
460
  end
436
461
 
462
+
437
463
  # Returns a string that specifies the source repo.
438
464
  def source_repo
439
465
  "#{@user}/#{@repo}"
440
466
  end
441
467
 
468
+
442
469
  # Returns a string that specifies the source branch.
443
470
  def source_branch
444
471
  git_call('branch').chomp!.match(/\*(.*)/)[0][2..-1]
445
472
  end
446
473
 
474
+
447
475
  # Returns a string consisting of source repo and branch.
448
476
  def source
449
477
  "#{source_repo}/#{source_branch}"
450
478
  end
451
479
 
480
+
452
481
  # Returns a string that specifies the target repo.
453
482
  def target_repo
454
483
  # TODO: Enable possibility to manually override this and set arbitrary repositories.
455
484
  source_repo
456
485
  end
457
486
 
487
+
458
488
  # Returns a string that specifies the target branch.
459
489
  def target_branch
460
490
  # TODO: Enable possibility to manually override this and set arbitrary branches.
461
491
  ENV['TARGET_BRANCH'] || 'master'
462
492
  end
463
493
 
494
+
464
495
  # Returns a string consisting of target repo and branch.
465
496
  def target
466
497
  "#{target_repo}/#{target_branch}"
467
498
  end
468
499
 
500
+
469
501
  # Returns an Array of all existing branches.
470
502
  def all_branches
471
- @branches ||= git_call('branch -a').split("\n").collect{|s|s.strip}
503
+ @branches ||= git_call('branch -a').split("\n").collect { |s| s.strip }
472
504
  end
473
505
 
506
+
474
507
  # Returns a boolean stating whether a specified commit has already been merged.
475
508
  def merged?(sha)
476
509
  not git_call("rev-list #{sha} ^HEAD 2>&1").split("\n").size > 0
477
510
  end
478
511
 
512
+
479
513
  # Uses Octokit to access GitHub.
480
514
  def configure_github_access
481
515
  if git_config['github.login'] and git_config['github.password']
482
516
  @github = Octokit::Client.new(
483
- :login => git_config['github.login'],
484
- :password => git_config['github.password']
517
+ :login => git_config['github.login'],
518
+ :password => git_config['github.password']
485
519
  )
486
520
  true
487
521
  @github.login
@@ -495,16 +529,18 @@ class GitReview
495
529
  end
496
530
  end
497
531
 
532
+
498
533
  def debug_mode
499
534
  git_config['review.mode'] == 'debug'
500
535
  end
501
536
 
537
+
502
538
  # Collect git config information in a Hash for easy access.
503
539
  # Checks '~/.gitconfig' for credentials.
504
540
  def git_config
505
541
  unless @git_config
506
542
  # Read @git_config from local git config.
507
- @git_config = {}
543
+ @git_config = { }
508
544
  config_list = git_call('config --list', false)
509
545
  config_list.split("\n").each do |line|
510
546
  key, value = line.split('=')
@@ -514,6 +550,7 @@ class GitReview
514
550
  @git_config
515
551
  end
516
552
 
553
+
517
554
  # Returns an array consisting of information on the user and the project.
518
555
  def repo_info
519
556
  # Extract user and project name from GitHub URL.
@@ -524,7 +561,7 @@ class GitReview
524
561
  end
525
562
  user, project = github_user_and_project(url)
526
563
  # If there are no results yet, look for 'insteadof' substitutions in URL and try again.
527
- unless (user and project)
564
+ unless user && project
528
565
  short, base = github_insteadof_matching(config_hash, url)
529
566
  if short and base
530
567
  url = url.sub(short, base)
@@ -534,9 +571,10 @@ class GitReview
534
571
  [user, project]
535
572
  end
536
573
 
574
+
537
575
  # Looks for 'insteadof' substitutions in URL.
538
576
  def github_insteadof_matching(config_hash, url)
539
- first = config_hash.collect { |key,value|
577
+ first = config_hash.collect { |key, value|
540
578
  [value, /url\.(.*github\.com.*)\.insteadof/.match(key)]
541
579
  }.find { |value, match|
542
580
  url.index(value) and match != nil
@@ -544,15 +582,52 @@ class GitReview
544
582
  first ? [first[0], first[1][1]] : [nil, nil]
545
583
  end
546
584
 
585
+
547
586
  # Extract user and project name from GitHub URL.
548
587
  def github_user_and_project(github_url)
549
588
  matches = /github\.com.(.*?)\/(.*)/.match(github_url)
550
589
  matches ? [matches[1], matches[2].sub(/\.git\z/, '')] : [nil, nil]
551
590
  end
552
591
 
592
+
553
593
  # Returns a boolean stating whether the last issued system call was successful.
554
594
  def last_command_successful?
555
595
  $?.exitstatus == 0
556
596
  end
557
597
 
598
+ # Returns an array where the 1st item is the title and the 2nd one is the body
599
+ def create_title_and_body(target_branch)
600
+ commits = git_call("log --format='%H' HEAD...#{target_branch}").lines.count
601
+ puts "commits: #{commits}"
602
+ if commits == 1
603
+ # we can create a really specific title and body
604
+ title = git_call("log --format='%s' HEAD...#{target_branch}").chomp
605
+ body = git_call("log --format='%b' HEAD...#{target_branch}").chomp
606
+ else
607
+ title = "[Review] Request from '#{git_config['github.login']}' @ '#{source}'"
608
+ body = "Please review the following changes:\n"
609
+ body += git_call("log --oneline HEAD...#{target_branch}").lines.map{|l| " * #{l.chomp}"}.join("\n")
610
+ end
611
+
612
+ tmpfile = Tempfile.new('git-review')
613
+ tmpfile.write(title + "\n\n" + body)
614
+ tmpfile.flush
615
+ editor = ENV['TERM_EDITOR'] || ENV['EDITOR']
616
+ warn "Please set $EDITOR or $TERM_EDITOR in your .bash_profile." unless editor
617
+
618
+ system("#{editor || 'open'} #{tmpfile.path}")
619
+
620
+ tmpfile.rewind
621
+ lines = tmpfile.read.lines.to_a
622
+ puts lines.inspect
623
+ title = lines.shift.chomp
624
+ lines.shift if lines[0].chomp.empty?
625
+
626
+ body = lines.join
627
+
628
+ tmpfile.unlink
629
+
630
+ [title, body]
631
+ end
632
+
558
633
  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.3
4
+ version: 1.1.5
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: 2012-06-30 00:00:00.000000000 Z
12
+ date: 2013-02-08 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: 1.7.0
37
+ version: '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: 1.7.0
45
+ version: '0'
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: yajl-ruby
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -59,6 +59,22 @@ dependencies:
59
59
  - - ! '>='
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
62
78
  description: Manage review workflow for projects hosted on GitHub (using pull requests).
63
79
  email: bamberger.dominik@gmail.com
64
80
  executables:
@@ -89,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
105
  version: '0'
90
106
  requirements: []
91
107
  rubyforge_project:
92
- rubygems_version: 1.8.24
108
+ rubygems_version: 1.8.23
93
109
  signing_key:
94
110
  specification_version: 3
95
111
  summary: facilitates GitHub code reviews