git-review 1.1.7 → 2.0.0.alpha
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/LICENSE +1 -1
- data/bin/git-review +100 -1
- data/lib/git-review.rb +18 -624
- metadata +39 -9
- data/lib/oauth_helper.rb +0 -63
- data/lib/settings.rb +0 -47
data/LICENSE
CHANGED
data/bin/git-review
CHANGED
@@ -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
|
-
|
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)
|
data/lib/git-review.rb
CHANGED
@@ -1,634 +1,28 @@
|
|
1
|
-
#
|
1
|
+
# Provide access to GitHub's API.
|
2
2
|
require 'octokit'
|
3
|
-
#
|
3
|
+
# Open a browser in 'browse' command.
|
4
4
|
require 'launchy'
|
5
|
-
#
|
5
|
+
# Parse time strings from git back into Time objects.
|
6
6
|
require 'time'
|
7
|
-
#
|
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
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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:
|
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:
|
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:
|
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:
|
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:
|
143
|
+
version: 1.3.1
|
114
144
|
requirements: []
|
115
145
|
rubyforge_project:
|
116
146
|
rubygems_version: 1.8.23
|
data/lib/oauth_helper.rb
DELETED
@@ -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
|
data/lib/settings.rb
DELETED
@@ -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
|