git-review 1.1.7 → 2.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010 Scott Chacon
1
+ Copyright (c) 2013 Dominik Bamberger
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -4,5 +4,104 @@ require 'rubygems'
4
4
  $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
5
5
 
6
6
  require 'git-review'
7
+ require 'gli'
7
8
 
8
- GitReview.new(ARGV)
9
+ include GLI::App
10
+
11
+ program_desc 'Manage review workflow for Github projects (using pull requests).'
12
+
13
+ # Pre-hook before a command is executed
14
+ pre do |global, cmd, opts, args|
15
+ github = ::GitReview::Github.instance
16
+ if github.configure_github_access && github.source_repo
17
+ github.update unless cmd == 'clean'
18
+ end
19
+ true # return true to explicitly pass precondition
20
+ end
21
+
22
+ desc 'List all pending requests'
23
+ command :list do |c|
24
+ c.switch [:r, :reverse]
25
+ c.action do |global, opts, args|
26
+ ::GitReview::Commands.list(opts[:reverse])
27
+ end
28
+ end
29
+
30
+ desc 'Show details for a single request'
31
+ command :show do |c|
32
+ c.switch [:f, :full]
33
+ c.action do |global, opts, args|
34
+ help_now!('Request number is required.') if args.empty?
35
+ ::GitReview::Commands.show(args.shift, opts[:full])
36
+ end
37
+ end
38
+
39
+ desc 'Open request in a browser window'
40
+ command :browse do |c|
41
+ c.action do |global, opts, args|
42
+ help_now!('Request number is required.') if args.empty?
43
+ ::GitReview::Commands.browse(args.shift)
44
+ end
45
+ end
46
+
47
+ desc 'Checkout a request\'s changes to local repo'
48
+ command :checkout do |c|
49
+ c.switch [:b, :branch]
50
+ c.action do |global, opts, args|
51
+ help_now!('Request number is required.') if args.empty?
52
+ ::GitReview::Commands.checkout(args.shift, opts[:branch])
53
+ end
54
+ end
55
+
56
+ desc 'Add an approvig comment to a request'
57
+ command :approve do |c|
58
+ c.action do |global, opts, args|
59
+ help_now!('Request number is required.') if args.empty?
60
+ ::GitReview::Commands.approve(args.shift)
61
+ end
62
+ end
63
+
64
+ desc 'Accept a request by merging it into master'
65
+ command :merge do |c|
66
+ c.action do |global, opts, args|
67
+ help_now!('Request number is required.') if args.empty?
68
+ ::GitReview::Commands.merge(args.shift)
69
+ end
70
+ end
71
+
72
+ desc 'Close a request'
73
+ command :close do |c|
74
+ c.action do |global, opts, args|
75
+ help_now!('Request number is required.') if args.empty?
76
+ ::GitReview::Commands.close(args.shift)
77
+ end
78
+ end
79
+
80
+ desc 'Create a new local branch for a request'
81
+ command :prepare do |c|
82
+ c.switch [:n, :new]
83
+ c.action do |global, opts, args|
84
+ ::GitReview::Commands.prepare(opts[:new], args.shift)
85
+ end
86
+ end
87
+
88
+ desc 'Create a new pull request'
89
+ command :create do |c|
90
+ c.switch [:u, :upstream]
91
+ c.action do |global, opts, args|
92
+ ::GitReview::Commands.create(opts[:upstream])
93
+ end
94
+ end
95
+
96
+ desc 'Delete a request\'s remote and local branches'
97
+ command :clean do |c|
98
+ c.switch [:f, :force]
99
+ c.switch [:a, :all]
100
+ c.action do |global, opts, args|
101
+ help_now!('Request number is required.') if args.empty? && !opts[:all]
102
+ number = args.empty? ? nil : args.shift
103
+ ::GitReview::Commands.clean(number, opts[:force], opts[:all])
104
+ end
105
+ end
106
+
107
+ exit run(ARGV)
@@ -1,634 +1,28 @@
1
- # Octokit is used to access GitHub's API.
1
+ # Provide access to GitHub's API.
2
2
  require 'octokit'
3
- # Launchy is used in 'browse' to open a browser.
3
+ # Open a browser in 'browse' command.
4
4
  require 'launchy'
5
- # Time is used to parse time strings from git back into Time objects.
5
+ # 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.
7
+ # Use temporary files to allow editing a request's title and body.
9
8
  require 'tempfile'
10
- # This file provides the OAuthHelper module which is used to create a oauth token/
11
- require_relative 'oauth_helper'
12
- # Setting class
13
- require_relative 'settings'
14
9
 
10
+ ## Our own dependencies
15
11
 
16
- # A custom error to raise, if we know we can't go on.
17
- class UnprocessableState < StandardError
18
- end
19
-
20
-
21
- class GitReview
22
- include OAuthHelper
23
-
24
- ## COMMANDS ##
25
-
26
- # List all pending requests.
27
- def list
28
- output = @current_requests.collect do |request|
29
- details = @github.pull_request(source_repo, request.number)
30
- # Find only pending (= unmerged) requests and output summary.
31
- # Explicitly look for local changes (that GitHub does not yet know about).
32
- next if merged?(request.head.sha)
33
- line = format_text(request.number, 8)
34
- date_string = format_time(request.updated_at)
35
- line << format_text(date_string, 11)
36
- line << format_text(details.comments + details.review_comments, 10)
37
- line << format_text(request.title, 91)
38
- line
39
- end
40
- output.compact!
41
- if output.empty?
42
- puts "No pending requests for '#{source}'."
43
- else
44
- puts "Pending requests for '#{source}':"
45
- puts 'ID Updated Comments Title'
46
- output.reverse! if @args.shift == '--reverse'
47
- output.each { |line| puts line }
48
- end
49
- end
50
-
51
-
52
- # Show details for a single request.
53
- def show
54
- return unless request_exists?
55
- option = @args.shift == '--full' ? '' : '--stat '
56
- sha = @current_request['head']['sha']
57
- puts "ID : #{@current_request['number']}"
58
- puts "Label : #{@current_request['head']['label']}"
59
- puts "Updated : #{format_time(@current_request['updated_at'])}"
60
- puts "Comments : #{@current_request['comments']}"
61
- puts
62
- puts @current_request['title']
63
- puts
64
- puts @current_request['body']
65
- puts
66
- puts git_call("diff --color=always #{option}HEAD...#{sha}")
67
- puts
68
- puts "Progress :"
69
- puts
70
- discussion
71
- end
72
-
73
-
74
- # Open a browser window and review a specified request.
75
- def browse
76
- Launchy.open(@current_request['html_url']) if request_exists?
77
- end
78
-
79
-
80
- # Checkout a specified request's changes to your local repository.
81
- def checkout
82
- return unless request_exists?
83
- create_local_branch = @args.shift == '--branch' ? '' : 'origin/'
84
- puts 'Checking out changes to your local repository.'
85
- puts 'To get back to your original state, just run:'
86
- puts
87
- puts ' git checkout master'
88
- puts
89
- git_call "checkout #{create_local_branch}#{@current_request['head']['ref']}"
90
- end
91
-
92
-
93
- # Accept a specified request by merging it into master.
94
- def merge
95
- return unless request_exists?
96
- option = @args.shift
97
- unless @current_request['head']['repo']
98
- # Someone deleted the source repo.
99
- user = @current_request['head']['user']['login']
100
- url = @current_request['patch_url']
101
- puts "Sorry, #{user} deleted the source repository, git-review doesn't support this."
102
- puts 'Tell the contributor not to do this.'
103
- puts
104
- puts 'You can still manually patch your repo by running:'
105
- puts
106
- puts " curl #{url} | git am"
107
- puts
108
- return false
109
- end
110
- message = "Accept request ##{@current_request['number']} and merge changes into \"#{target}\""
111
- exec_cmd = "merge #{option} -m '#{message}' #{@current_request['head']['sha']}"
112
- puts
113
- puts 'Request title:'
114
- puts " #{@current_request['title']}"
115
- puts
116
- puts 'Merge command:'
117
- puts " git #{exec_cmd}"
118
- puts
119
- puts git_call(exec_cmd)
120
- end
121
-
122
-
123
- # Add an approving comment to the request.
124
- def approve
125
- return unless request_exists?
126
- comment = 'Reviewed and approved.'
127
- response = @github.add_comment source_repo, @current_request['number'], comment
128
- if response[:body] == comment
129
- puts 'Successfully approved request.'
130
- else
131
- puts response[:message]
132
- end
133
- end
134
-
135
-
136
- # Close a specified request.
137
- def close
138
- return unless request_exists?
139
- @github.close_issue source_repo, @current_request['number']
140
- puts 'Successfully closed request.' unless request_exists?('open', @current_request['number'])
141
- end
142
-
143
-
144
- # Prepare local repository to create a new request.
145
- # Sets @local_branch.
146
- def prepare
147
- # Remember original branch the user was currently working on.
148
- @original_branch = source_branch
149
- # People should work on local branches, but especially for single commit changes,
150
- # more often than not, they don't. Therefore we create a branch for them,
151
- # to be able to use code review the way it is intended.
152
- if @original_branch == target_branch
153
- # Unless a branch name is already provided, ask for one.
154
- if (branch_name = @args.shift).nil?
155
- puts 'Please provide a name for the branch:'
156
- branch_name = gets.chomp.gsub(/\W+/, '_').downcase
157
- end
158
- # Create the new branch (as a copy of the current one).
159
- @local_branch = "review_#{Time.now.strftime("%y%m%d")}_#{branch_name}"
160
- git_call "checkout -b #{@local_branch}"
161
- if source_branch == @local_branch
162
- # Stash any uncommitted changes.
163
- git_call('stash') if (save_uncommitted_changes = !git_call('diff HEAD').empty?)
164
- # Go back to master and get rid of pending commits (as these are now on the new branch).
165
- git_call "checkout #{target_branch}"
166
- git_call "reset --hard origin/#{target_branch}"
167
- git_call "checkout #{@local_branch}"
168
- git_call('stash pop') if save_uncommitted_changes
169
- end
170
- else
171
- @local_branch = @original_branch
172
- end
173
- end
174
-
175
-
176
- # Create a new request.
177
- # TODO: Support creating requests to other repositories and branches (like the original repo, this has been forked from).
178
- def create
179
- # Prepare @local_branch.
180
- prepare
181
- # Don't create request with uncommitted changes in current branch.
182
- unless git_call('diff HEAD').empty?
183
- puts 'You have uncommitted changes. Please stash or commit before creating the request.'
184
- return
185
- end
186
- unless git_call("cherry #{target_branch}").empty?
187
- # Push latest commits to the remote branch (and by that, create it if necessary).
188
- git_call "push --set-upstream origin #{@local_branch}", debug_mode, true
189
- # Gather information.
190
- last_request_id = @current_requests.collect { |req| req['number'] }.sort.last.to_i
191
- title, body = create_title_and_body(target_branch)
192
- # Create the actual pull request.
193
- @github.create_pull_request target_repo, target_branch, source_branch, title, body
194
- # Switch back to target_branch and check for success.
195
- git_call "checkout #{target_branch}"
196
- update
197
- potential_new_request = @current_requests.find { |req| req['title'] == title }
198
- if potential_new_request and potential_new_request['number'] > last_request_id
199
- puts "Successfully created new request ##{potential_new_request['number']}"
200
- puts File.join("https://github.com", target_repo, "pull", potential_new_request['number'].to_s)
201
- end
202
- # Return to the user's original branch.
203
- git_call "checkout #{@original_branch}"
204
- else
205
- puts 'Nothing to push to remote yet. Commit something first.'
206
- end
207
- end
208
-
209
-
210
- # Deletes obsolete branches (left over from already closed requests).
211
- def clean
212
- # Pruning is needed to remove already deleted branches from your local track.
213
- git_call 'remote prune origin'
214
- # Determine strategy to clean.
215
- case @args.size
216
- when 0
217
- puts 'Argument missing. Please provide either an ID or the option "--all".'
218
- when 1
219
- if @args.first == '--all'
220
- # git review clean --all
221
- clean_all
222
- else
223
- # git review clean ID
224
- clean_single
225
- end
226
- when 2
227
- # git review clean ID --force
228
- clean_single(@args.last == '--force')
229
- else
230
- puts 'Too many arguments.'
231
- end
232
- end
233
-
234
-
235
- # Start a console session (used for debugging).
236
- def console
237
- puts 'Entering debug console.'
238
- request_exists?
239
- require 'ruby-debug'
240
- Debugger.start
241
- debugger
242
- puts 'Leaving debug console.'
243
- end
244
-
245
-
246
- private
247
-
248
- # Setup variables and call actual commands.
249
- def initialize(args = [])
250
- @args = args
251
- command = args.shift
252
- if command and self.respond_to?(command)
253
- @user, @repo = repo_info
254
- return unless @user && @repo && configure_github_access
255
- update unless command == 'clean'
256
- self.send command
257
- else
258
- unless command.nil? or command.empty? or %w(help -h --help).include?(command)
259
- puts "git-review: '#{command}' is not a valid command.\n\n"
260
- end
261
- help
262
- end
263
- rescue UnprocessableState
264
- puts 'Execution of git-review command stopped.'
265
- end
266
-
267
-
268
- # Show a quick reference of available commands.
269
- def help
270
- puts 'Usage: git review <command>'
271
- puts 'Manage review workflow for projects hosted on GitHub (using pull requests).'
272
- puts
273
- puts 'Available commands:'
274
- puts ' list [--reverse] List all pending requests.'
275
- puts ' show <ID> [--full] Show details for a single request.'
276
- puts ' browse <ID> Open a browser window and review a specified request.'
277
- puts ' checkout <ID> [--branch] Checkout a specified request\'s changes to your local repository.'
278
- puts ' approve <ID> Add an approving comment to a specified request.'
279
- puts ' merge <ID> Accept a specified request by merging it into master.'
280
- puts ' close <ID> Close a specified request.'
281
- puts ' prepare Creates a new local branch for a request.'
282
- puts ' create Create a new request.'
283
- puts ' clean <ID> [--force] Delete a request\'s remote and local branches.'
284
- puts ' clean --all Delete all obsolete branches.'
285
- end
286
-
287
-
288
- # Check existence of specified request and assign @current_request.
289
- def request_exists?(state = 'open', request_id = nil)
290
- # NOTE: If request_id is set explicitly we might need to update to get the
291
- # latest changes from GitHub, as this is called from within another method.
292
- automated = !request_id.nil?
293
- update(state) if automated
294
- request_id ||= @args.shift.to_i
295
- if request_id == 0
296
- puts 'Please specify a valid ID.'
297
- return false
298
- end
299
- @current_request = @current_requests.find { |req| req['number'] == request_id }
300
- unless @current_request
301
- # Additional try to get an older request from Github by specifying the number.
302
- request = @github.pull_request source_repo, request_id
303
- @current_request = request if request.state == state
304
- end
305
- if @current_request
306
- true
307
- else
308
- # No output for automated checks.
309
- puts "Request '#{request_id}' could not be found among all '#{state}' requests." unless automated
310
- false
311
- end
312
- end
313
-
314
-
315
- # Get latest changes from GitHub.
316
- def update(state = 'open')
317
- @current_requests = @github.pull_requests(source_repo, state)
318
- repos = @current_requests.collect do |request|
319
- repo = request.head.repository
320
- "#{repo.owner}/#{repo.name}" if repo
321
- end
322
- repos.uniq.compact.each do |repo|
323
- git_call "fetch git@github.com:#{repo}.git +refs/heads/*:refs/pr/#{repo}/*"
324
- end
325
- end
326
-
327
-
328
- # Cleans a single request's obsolete branches.
329
- def clean_single(force_deletion = false)
330
- update('closed')
331
- return unless request_exists?('closed')
332
- # Ensure there are no unmerged commits or '--force' flag has been set.
333
- branch_name = @current_request['head']['ref']
334
- if unmerged_commits?(branch_name) and not force_deletion
335
- return puts "Won't delete branches that contain unmerged commits. Use '--force' to override."
336
- end
337
- delete_branch(branch_name)
338
- end
339
-
340
-
341
- # Cleans all obsolete branches.
342
- def clean_all
343
- update
344
- # Protect all open requests' branches from deletion.
345
- protected_branches = @current_requests.collect { |request| request['head']['ref'] }
346
- # Select all branches with the correct prefix.
347
- review_branches = all_branches.select { |branch| branch.include?('review_') }
348
- # Only use uniq branch names (no matter if local or remote).
349
- review_branches.collect { |branch| branch.split('/').last }.uniq.each do |branch_name|
350
- # Only clean up obsolete branches.
351
- unless protected_branches.include?(branch_name) or unmerged_commits?(branch_name, false)
352
- delete_branch(branch_name)
353
- end
354
- end
355
- end
356
-
357
-
358
- # Delete local and remote branches that match a given name.
359
- def delete_branch(branch_name)
360
- # Delete local branch if it exists.
361
- git_call("branch -D #{branch_name}", true) if branch_exists?(:local, branch_name)
362
- # Delete remote branch if it exists.
363
- git_call("push origin :#{branch_name}", true) if branch_exists?(:remote, branch_name)
364
- end
365
-
366
-
367
- # Returns a boolean stating whether there are unmerged commits on the local or remote branch.
368
- def unmerged_commits?(branch_name, verbose = true)
369
- locations = []
370
- locations << ['', ''] if branch_exists?(:local, branch_name)
371
- locations << ['origin/', 'origin/'] if branch_exists?(:remote, branch_name)
372
- locations = locations + [['', 'origin/'], ['origin/', '']] if locations.size == 2
373
- if locations.empty?
374
- puts 'Nothing to do. All cleaned up already.' if verbose
375
- return false
376
- end
377
- # Compare remote and local branch with remote and local master.
378
- responses = locations.collect do |location|
379
- git_call "cherry #{location.first}#{target_branch} #{location.last}#{branch_name}"
380
- end
381
- # Select commits (= non empty, not just an error message and not only duplicate commits staring with '-').
382
- unmerged_commits = responses.reject do |response|
383
- response.empty? or response.include?('fatal: Unknown commit') or response.split("\n").reject { |x| x.index('-') == 0 }.empty?
384
- end
385
- # If the array ain't empty, we got unmerged commits.
386
- if unmerged_commits.empty?
387
- false
388
- else
389
- puts "Unmerged commits on branch '#{branch_name}'."
390
- true
391
- end
392
- end
393
-
394
-
395
- # Returns a boolean stating whether a branch exists in a specified location.
396
- def branch_exists?(location, branch_name)
397
- return false unless [:remote, :local].include? location
398
- prefix = location == :remote ? 'remotes/origin/' : ''
399
- all_branches.include?(prefix + branch_name)
400
- end
401
-
402
-
403
- # System call to 'git'.
404
- def git_call(command, verbose = debug_mode, enforce_success = false)
405
- if verbose
406
- puts
407
- puts " git #{command}"
408
- puts
409
- end
410
- output = `git #{command}`
411
- puts output if verbose and not output.empty?
412
- # If we need sth. to succeed, but it doesn't stop right there.
413
- if enforce_success and not last_command_successful?
414
- puts output unless output.empty?
415
- raise UnprocessableState
416
- end
417
- output
418
- end
419
-
420
-
421
- # Show current discussion for @current_request.
422
- def discussion
423
- request = @github.pull_request source_repo, @current_request['number']
424
- # FIXME:
425
- puts 'This needs to be updated to work with API v3.'
426
- return
427
- result = request['discussion'].collect do |entry|
428
- user = entry['user'] || entry['author']
429
- name = user['login'].empty? ? user['name'] : user['login']
430
- output = "\e[35m#{name}\e[m "
431
- case entry['type']
432
- # Comments:
433
- when "IssueComment", "CommitComment", "PullRequestReviewComment"
434
- output << "added a comment"
435
- output << " to \e[36m#{entry['commit_id'][0..6]}\e[m" if entry['commit_id']
436
- output << " on #{format_time(entry['created_at'])}"
437
- unless entry['created_at'] == entry['updated_at']
438
- output << " (updated on #{format_time(entry['updated_at'])})"
439
- end
440
- output << ":\n#{''.rjust(output.length + 1, "-")}\n"
441
- output << "> \e[32m#{entry['path']}:#{entry['position']}\e[m\n" if entry['path'] and entry['position']
442
- output << entry['body']
443
- # Commits:
444
- when "Commit"
445
- output << "authored commit \e[36m#{entry['id'][0..6]}\e[m on #{format_time(entry['authored_date'])}"
446
- unless entry['authored_date'] == entry['committed_date']
447
- output << " (committed on #{format_time(entry['committed_date'])})"
448
- end
449
- output << ":\n#{''.rjust(output.length + 1, "-")}\n#{entry["message"]}"
450
- end
451
- output << "\n\n\n"
452
- end
453
- puts result.compact unless result.empty?
454
- end
455
-
456
-
457
- # Display helper to make output more configurable.
458
- def format_text(info, size)
459
- info.to_s.gsub("\n", ' ')[0, size-1].ljust(size)
460
- end
461
-
462
-
463
- # Display helper to unify time output.
464
- def format_time(time_string)
465
- Time.parse(time_string).strftime('%d-%b-%y')
466
- end
467
-
468
-
469
- # Returns a string that specifies the source repo.
470
- def source_repo
471
- "#{@user}/#{@repo}"
472
- end
473
-
474
-
475
- # Returns a string that specifies the source branch.
476
- def source_branch
477
- git_call('branch').chomp!.match(/\*(.*)/)[0][2..-1]
478
- end
479
-
480
-
481
- # Returns a string consisting of source repo and branch.
482
- def source
483
- "#{source_repo}/#{source_branch}"
484
- end
485
-
486
-
487
- # Returns a string that specifies the target repo.
488
- def target_repo
489
- # TODO: Enable possibility to manually override this and set arbitrary repositories.
490
- source_repo
491
- end
492
-
493
-
494
- # Returns a string that specifies the target branch.
495
- def target_branch
496
- # TODO: Enable possibility to manually override this and set arbitrary branches.
497
- ENV['TARGET_BRANCH'] || 'master'
498
- end
499
-
500
-
501
- # Returns a string consisting of target repo and branch.
502
- def target
503
- "#{target_repo}/#{target_branch}"
504
- end
505
-
506
-
507
- # Returns an Array of all existing branches.
508
- def all_branches
509
- @branches ||= git_call('branch -a').split("\n").collect { |s| s.strip }
510
- end
511
-
512
-
513
- # Returns a boolean stating whether a specified commit has already been merged.
514
- def merged?(sha)
515
- not git_call("rev-list #{sha} ^HEAD 2>&1").split("\n").size > 0
516
- end
517
-
518
-
519
- # Uses Octokit to access GitHub.
520
- def configure_github_access
521
- if Settings.instance.oauth_token
522
- @github = Octokit::Client.new(
523
- :login => Settings.instance.username,
524
- :oauth_token => Settings.instance.oauth_token
525
- )
526
- @github.login
527
- else
528
- configure_oauth
529
- configure_github_access
530
- end
531
- end
532
-
533
-
534
- def debug_mode
535
- Settings.instance.review_mode == 'debug'
536
- end
537
-
538
-
539
- # Collect git config information in a Hash for easy access.
540
- # Checks '~/.gitconfig' for credentials.
541
- def git_config
542
- unless @git_config
543
- # Read @git_config from local git config.
544
- @git_config = { }
545
- config_list = git_call('config --list', false)
546
- config_list.split("\n").each do |line|
547
- key, value = line.split('=')
548
- @git_config[key] = value
549
- end
550
- end
551
- @git_config
552
- end
553
-
554
-
555
- # Returns an array consisting of information on the user and the project.
556
- def repo_info
557
- # Extract user and project name from GitHub URL.
558
- url = git_config['remote.origin.url']
559
- if url.nil?
560
- puts "Error: Not a git repository."
561
- return [nil, nil]
562
- end
563
- user, project = github_user_and_project(url)
564
- # If there are no results yet, look for 'insteadof' substitutions in URL and try again.
565
- unless user && project
566
- short, base = github_insteadof_matching(config_hash, url)
567
- if short and base
568
- url = url.sub(short, base)
569
- user, project = github_user_and_project(url)
570
- end
571
- end
572
- [user, project]
573
- end
574
-
575
-
576
- # Looks for 'insteadof' substitutions in URL.
577
- def github_insteadof_matching(config_hash, url)
578
- first = config_hash.collect { |key, value|
579
- [value, /url\.(.*github\.com.*)\.insteadof/.match(key)]
580
- }.find { |value, match|
581
- url.index(value) and match != nil
582
- }
583
- first ? [first[0], first[1][1]] : [nil, nil]
584
- end
585
-
586
-
587
- # Extract user and project name from GitHub URL.
588
- def github_user_and_project(github_url)
589
- matches = /github\.com.(.*?)\/(.*)/.match(github_url)
590
- matches ? [matches[1], matches[2].sub(/\.git\z/, '')] : [nil, nil]
591
- end
592
-
593
-
594
- # Returns a boolean stating whether the last issued system call was successful.
595
- def last_command_successful?
596
- $?.exitstatus == 0
597
- end
598
-
599
- # Returns an array where the 1st item is the title and the 2nd one is the body
600
- def create_title_and_body(target_branch)
601
- commits = git_call("log --format='%H' HEAD...#{target_branch}").lines.count
602
- puts "commits: #{commits}"
603
- if commits == 1
604
- # we can create a really specific title and body
605
- title = git_call("log --format='%s' HEAD...#{target_branch}").chomp
606
- body = git_call("log --format='%b' HEAD...#{target_branch}").chomp
607
- else
608
- title = "[Review] Request from '#{git_config['github.login']}' @ '#{source}'"
609
- body = "Please review the following changes:\n"
610
- body += git_call("log --oneline HEAD...#{target_branch}").lines.map{|l| " * #{l.chomp}"}.join("\n")
611
- end
612
-
613
- tmpfile = Tempfile.new('git-review')
614
- tmpfile.write(title + "\n\n" + body)
615
- tmpfile.flush
616
- editor = ENV['TERM_EDITOR'] || ENV['EDITOR']
617
- warn "Please set $EDITOR or $TERM_EDITOR in your .bash_profile." unless editor
618
-
619
- system("#{editor || 'open'} #{tmpfile.path}")
620
-
621
- tmpfile.rewind
622
- lines = tmpfile.read.lines.to_a
623
- puts lines.inspect
624
- title = lines.shift.chomp
625
- lines.shift if lines[0].chomp.empty?
626
-
627
- body = lines.join
12
+ # Include all helper functions to make GitReview work as expected.
13
+ require_relative 'git-review/internals'
14
+ # Deal with current git repository.
15
+ require_relative 'git-review/local'
16
+ # Communicate with Github via API.
17
+ require_relative 'git-review/github'
18
+ # Read and write settings from/to the filesystem.
19
+ require_relative 'git-review/settings'
20
+ # Provide available commands.
21
+ require_relative 'git-review/commands'
22
+ # Include all kinds of custom-defined errors.
23
+ require_relative 'git-review/errors'
628
24
 
629
- tmpfile.unlink
630
25
 
631
- [title, body]
632
- end
26
+ module GitReview
633
27
 
634
28
  end
metadata CHANGED
@@ -1,8 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git-review
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.7
5
- prerelease:
4
+ version: 2.0.0.alpha
5
+ prerelease: 6
6
6
  platform: ruby
7
7
  authors:
8
8
  - Dominik Bamberger
@@ -34,7 +34,7 @@ dependencies:
34
34
  requirements:
35
35
  - - ~>
36
36
  - !ruby/object:Gem::Version
37
- version: 1.24.0
37
+ version: 2.0.0
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -42,7 +42,7 @@ dependencies:
42
42
  requirements:
43
43
  - - ~>
44
44
  - !ruby/object:Gem::Version
45
- version: 1.24.0
45
+ version: 2.0.0
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: yajl-ruby
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -60,7 +60,7 @@ dependencies:
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  - !ruby/object:Gem::Dependency
63
- name: rspec
63
+ name: gli
64
64
  requirement: !ruby/object:Gem::Requirement
65
65
  none: false
66
66
  requirements:
@@ -75,6 +75,38 @@ dependencies:
75
75
  - - ! '>='
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 2.13.0
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 2.13.0
94
+ - !ruby/object:Gem::Dependency
95
+ name: hashie
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
78
110
  description: Manage review workflow for projects hosted on GitHub (using pull requests).
79
111
  email: bamberger.dominik@gmail.com
80
112
  executables:
@@ -83,7 +115,6 @@ extensions: []
83
115
  extra_rdoc_files: []
84
116
  files:
85
117
  - LICENSE
86
- - lib/settings.rb
87
118
  - lib/git-review.rb
88
119
  - lib/git-review/settings.rb
89
120
  - lib/git-review/commands.rb
@@ -91,7 +122,6 @@ files:
91
122
  - lib/git-review/github.rb
92
123
  - lib/git-review/internals.rb
93
124
  - lib/git-review/local.rb
94
- - lib/oauth_helper.rb
95
125
  - bin/git-review
96
126
  homepage: http://github.com/b4mboo/git-review
97
127
  licenses: []
@@ -108,9 +138,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
108
138
  required_rubygems_version: !ruby/object:Gem::Requirement
109
139
  none: false
110
140
  requirements:
111
- - - ! '>='
141
+ - - ! '>'
112
142
  - !ruby/object:Gem::Version
113
- version: '0'
143
+ version: 1.3.1
114
144
  requirements: []
115
145
  rubyforge_project:
116
146
  rubygems_version: 1.8.23
@@ -1,63 +0,0 @@
1
- require 'net/http'
2
- require 'net/https'
3
- require 'json'
4
- # Required to hide password
5
- require 'io/console'
6
- # Used to retrieve hostname
7
- require 'socket'
8
-
9
- module OAuthHelper
10
- def configure_oauth(chosen_description = nil)
11
- puts "Requesting a OAuth token for git-review."
12
- puts "This procedure will grant access to your public and private repositories."
13
- puts "You can revoke this authorization by visiting the following page: " +
14
- "https://github.com/settings/applications"
15
- print "Plese enter your GitHub's username: "
16
- username = STDIN.gets.chomp
17
- print "Plese enter your GitHub's password (it won't be stored anywhere): "
18
- password = STDIN.noecho(&:gets).chomp
19
- print "\n"
20
-
21
- if chosen_description
22
- description = chosen_description
23
- else
24
- description = "git-review - #{Socket.gethostname}"
25
- puts "Please enter a descriptiont to associate to this token, it will " +
26
- "make easier to find it inside of github's application page."
27
- puts "Press enter to accept the proposed description"
28
- print "Description [#{description}]:"
29
- user_description = STDIN.gets.chomp
30
- description = user_description.empty? ? description : user_description
31
- end
32
-
33
- uri = URI("https://api.github.com/authorizations")
34
-
35
- http = Net::HTTP.new(uri.host, uri.port)
36
- http.use_ssl = true
37
-
38
- req =Net::HTTP::Post.new(uri.request_uri)
39
- req.basic_auth username, password
40
- req.body = {
41
- "scopes" => ["repo"],
42
- "note" => description
43
- }.to_json
44
-
45
- response = http.request req
46
-
47
- if response.code == '401'
48
- warn "You provided the wrong username/password, please try again."
49
- configure_oauth(description)
50
- elsif response.code == '201'
51
- parser_response = JSON.parse(response.body)
52
- settings = Settings.instance
53
- settings.oauth_token = parser_response['token']
54
- settings.username = username
55
- settings.save!
56
- puts "OAuth token successfully created"
57
- else
58
- warn "Something went wrong: #{response.body}"
59
- exit 1
60
- end
61
- end
62
-
63
- end
@@ -1,47 +0,0 @@
1
- require 'fileutils'
2
- require 'singleton'
3
- require 'yaml'
4
-
5
- class Settings
6
- include Singleton
7
-
8
- def initialize
9
- @config_file = File.join(
10
- Dir.home,
11
- '.git_review.yml'
12
- )
13
-
14
- @config = if File.exists?(@config_file)
15
- YAML.load_file(@config_file) || {}
16
- else
17
- {}
18
- end
19
- end
20
-
21
- def save!
22
- File.open(@config_file, 'w') do |file|
23
- file.write(YAML.dump(@config))
24
- end
25
- end
26
-
27
- def review_mode
28
- @config['review_mode']
29
- end
30
-
31
- def oauth_token
32
- @config['oauth_token']
33
- end
34
-
35
- def oauth_token=(token)
36
- @config['oauth_token'] = token
37
- end
38
-
39
- def username
40
- @config['username']
41
- end
42
-
43
- def username=(username)
44
- @config['username'] = username
45
- end
46
-
47
- end