git-review 0.5.1 → 0.6.1
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/lib/git-review.rb +143 -182
- metadata +7 -21
data/lib/git-review.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
|
-
|
2
|
-
require 'launchy'
|
1
|
+
# Octokit is used to access GitHub's API.
|
3
2
|
require 'octokit'
|
3
|
+
# Launchy is used in 'browse' to open a browser.
|
4
|
+
require 'launchy'
|
4
5
|
|
5
6
|
class GitReview
|
6
7
|
|
7
|
-
REVIEW_CACHE_FILE = '.git/review_cache.json'
|
8
|
-
|
9
8
|
## COMMANDS ##
|
10
9
|
|
11
10
|
# Default command to show a quick reference of available commands.
|
@@ -14,7 +13,7 @@ class GitReview
|
|
14
13
|
puts 'Manage review workflow for projects hosted on GitHub (using pull requests).'
|
15
14
|
puts ''
|
16
15
|
puts 'Available commands:'
|
17
|
-
puts ' list [--reverse] List all
|
16
|
+
puts ' list [--reverse] List all pending requests.'
|
18
17
|
puts ' show <number> [--full] Show details of a single request.'
|
19
18
|
puts ' browse <number> Open a browser window and review a specified request.'
|
20
19
|
puts ' checkout <number> Checkout a specified request\'s changes to your local repository.'
|
@@ -23,98 +22,89 @@ class GitReview
|
|
23
22
|
puts ' create Create a new request.'
|
24
23
|
end
|
25
24
|
|
26
|
-
# List all
|
25
|
+
# List all pending requests.
|
27
26
|
def list
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
27
|
+
@pending_requests.reverse! if @args.shift == '--reverse'
|
28
|
+
output = @pending_requests.collect do |pending_request|
|
29
|
+
# Find only pending (= unmerged) requests and output summary. GitHub might
|
30
|
+
# still think of them as pending, as it doesn't know about local changes.
|
31
|
+
next if merged?(pending_request['head']['sha'])
|
32
|
+
line = format_text(pending_request['number'], 8)
|
33
|
+
date_string = Date.parse(pending_request['updated_at']).strftime('%d-%b-%y')
|
34
|
+
line += format_text(date_string, 11)
|
35
|
+
line += format_text(pending_request['comments'], 10)
|
36
|
+
line += format_text(pending_request['title'], 91)
|
37
|
+
line
|
32
38
|
end
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
open_requests.each do |pull|
|
37
|
-
next unless not_merged?(pull['head']['sha'])
|
38
|
-
line = []
|
39
|
-
line << format_text(pull['number'], 6)
|
40
|
-
line << format_text(Date.parse(pull['created_at']).strftime('%d-%b-%y'), 10)
|
41
|
-
line << format_text(pull['comments'], 10)
|
42
|
-
line << format_text(pull['title'], 94)
|
43
|
-
puts line.join ' '
|
39
|
+
if output.compact.empty?
|
40
|
+
puts "No pending requests for '#{source}'"
|
41
|
+
return
|
44
42
|
end
|
43
|
+
puts "Pending requests for '#{source}'"
|
44
|
+
puts 'ID Date Comments Title'
|
45
|
+
puts output.compact
|
45
46
|
end
|
46
47
|
|
47
48
|
# Show details of a single request.
|
48
49
|
def show
|
49
|
-
return unless
|
50
|
-
option = @args.shift
|
51
|
-
|
52
|
-
puts "
|
53
|
-
puts "
|
54
|
-
puts "
|
55
|
-
puts "
|
56
|
-
puts
|
57
|
-
puts
|
50
|
+
return unless request_exists?
|
51
|
+
option = @args.shift == '--full' ? '' : '--stat '
|
52
|
+
sha = @pending_request['head']['sha']
|
53
|
+
puts "Number : #{@pending_request['number']}"
|
54
|
+
puts "Label : #{@pending_request['head']['label']}"
|
55
|
+
puts "Created : #{@pending_request['created_at']}"
|
56
|
+
puts "Votes : #{@pending_request['votes']}"
|
57
|
+
puts "Comments : #{@pending_request['comments']}"
|
58
|
+
puts ''
|
59
|
+
puts "Title : #{@pending_request['title']}"
|
58
60
|
puts "Body :"
|
59
|
-
puts
|
60
|
-
puts @
|
61
|
-
puts
|
61
|
+
puts ''
|
62
|
+
puts @pending_request['body']
|
63
|
+
puts ''
|
62
64
|
puts '------------'
|
63
|
-
puts
|
64
|
-
|
65
|
-
|
66
|
-
else
|
67
|
-
puts "cmd: git diff HEAD...#{@review['head']['sha']}"
|
68
|
-
puts git("diff --stat --color=always HEAD...#{@review['head']['sha']}")
|
69
|
-
end
|
65
|
+
puts ''
|
66
|
+
puts "cmd: git diff #{option}HEAD...#{sha}"
|
67
|
+
puts git("diff --color=always #{option}HEAD...#{sha}")
|
70
68
|
end
|
71
69
|
|
72
70
|
# Open a browser window and review a specified request.
|
73
71
|
def browse
|
74
|
-
Launchy.open(@
|
72
|
+
Launchy.open(@pending_request['html_url']) if request_exists?
|
75
73
|
end
|
76
74
|
|
77
75
|
# Checkout a specified request's changes to your local repository.
|
78
76
|
def checkout
|
79
|
-
|
80
|
-
git "co origin/#{@review['head']['ref']}"
|
77
|
+
git "co origin/#{@pending_request['head']['ref']}" if request_exists?
|
81
78
|
end
|
82
79
|
|
83
80
|
# Accept a specified request by merging it into master.
|
84
81
|
def accept
|
85
|
-
return unless
|
82
|
+
return unless request_exists?
|
86
83
|
option = @args.shift
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
purl = @review['patch_url']
|
93
|
-
puts "Sorry, #{o} deleted the source repository, git-review doesn't support this."
|
84
|
+
unless @pending_request['head']['repository']
|
85
|
+
# Someone deleted the source repo.
|
86
|
+
user = @pending_request['head']['user']['login']
|
87
|
+
url = @pending_request['patch_url']
|
88
|
+
puts "Sorry, #{user} deleted the source repository, git-review doesn't support this."
|
94
89
|
puts "You can manually patch your repo by running:"
|
95
90
|
puts
|
96
|
-
puts " curl #{
|
91
|
+
puts " curl #{url} | git am"
|
97
92
|
puts
|
98
93
|
puts "Tell the contributor not to do this."
|
99
94
|
return false
|
100
95
|
end
|
101
|
-
|
102
|
-
message
|
103
|
-
message += @
|
104
|
-
|
105
|
-
message += "\n\n---\n\nMerge Log:\n"
|
106
|
-
puts cmd = "git merge --no-ff --log -m '#{message}' #{s}"
|
107
|
-
else
|
108
|
-
puts cmd = "git merge --no-ff -m '#{message}' #{s}"
|
109
|
-
end
|
96
|
+
message = "Accepting request and merging into '#{target}':\n\n"
|
97
|
+
message += "#{@pending_request['title']}\n\n"
|
98
|
+
message += "#{@pending_request['body']}\n\n"
|
99
|
+
puts cmd = "git merge --no-ff #{option} -m '#{message}' #{@pending_request['head']['sha']}"
|
110
100
|
exec(cmd)
|
111
101
|
end
|
112
102
|
|
113
103
|
# Decline and close a specified request.
|
114
104
|
def decline
|
115
|
-
return unless
|
116
|
-
Octokit.post("issues/close/#{source_repo}/#{@
|
117
|
-
puts "Successfully declined request." unless
|
105
|
+
return unless request_exists?
|
106
|
+
Octokit.post("issues/close/#{source_repo}/#{@pending_request['number']}")
|
107
|
+
puts "Successfully declined request." unless request_exists?(@pending_request['number'])
|
118
108
|
end
|
119
109
|
|
120
110
|
# Create a new request.
|
@@ -122,8 +112,8 @@ class GitReview
|
|
122
112
|
def create
|
123
113
|
# TODO: Create and push to a remote branch if necessary.
|
124
114
|
# Gather information.
|
125
|
-
|
126
|
-
title = "[Review] Request from '#{github_login}' @ '#{
|
115
|
+
last_request_id = @pending_requests.collect{|req| req['number'] }.sort.last.to_i
|
116
|
+
title = "[Review] Request from '#{github_login}' @ '#{source}'"
|
127
117
|
# TODO: Insert commit messages (that are not yet in master) into body (since this will be displayed inside the mail that is sent out).
|
128
118
|
body = "You are requested to review the following changes:"
|
129
119
|
# Create the actual pull request.
|
@@ -131,8 +121,8 @@ class GitReview
|
|
131
121
|
# Switch back to target_branch and check for success.
|
132
122
|
git "co #{target_branch}"
|
133
123
|
update
|
134
|
-
|
135
|
-
puts '
|
124
|
+
potential_new_request = @pending_requests.find{ |req| req['title'] == title }
|
125
|
+
puts 'Successfully created new request.' if potential_new_request['number'] > last_request_id
|
136
126
|
end
|
137
127
|
|
138
128
|
# Start a console session (used for debugging).
|
@@ -152,37 +142,46 @@ class GitReview
|
|
152
142
|
command = args.shift
|
153
143
|
@user, @repo = repo_info
|
154
144
|
@args = args
|
155
|
-
|
145
|
+
configure_github_access
|
156
146
|
if command && self.respond_to?(command)
|
157
147
|
update
|
158
148
|
self.send command
|
159
149
|
else
|
160
|
-
unless command.
|
150
|
+
unless command.nil? or command.empty? or %w(-h --help).include?(command)
|
161
151
|
puts "git-review: '#{command}' is not a valid command.\n\n"
|
162
152
|
end
|
163
153
|
help
|
164
154
|
end
|
165
155
|
end
|
166
156
|
|
167
|
-
#
|
168
|
-
def
|
169
|
-
|
170
|
-
fetch_stale_forks
|
171
|
-
end
|
172
|
-
|
173
|
-
# Check existence of specified review and assign @review.
|
174
|
-
def review_exists?(review_id = nil)
|
175
|
-
# NOTE: If review_id is not set explicitly we might need to update to get the
|
157
|
+
# Check existence of specified request and assign @pending_request.
|
158
|
+
def request_exists?(request_id = nil)
|
159
|
+
# NOTE: If request_id is set explicitly we might need to update to get the
|
176
160
|
# latest changes from GitHub, as this is called from within another method.
|
177
|
-
update
|
178
|
-
|
179
|
-
if
|
161
|
+
update unless request_id.nil?
|
162
|
+
request_id ||= @args.shift.to_i
|
163
|
+
if request_id == 0
|
180
164
|
puts "Please specify a valid ID."
|
181
165
|
return false
|
182
166
|
end
|
183
|
-
@
|
184
|
-
puts "
|
185
|
-
not @
|
167
|
+
@pending_request = @pending_requests.find{ |req| req['number'] == request_id }
|
168
|
+
puts "Request '#{request_id}' does not exist." unless @pending_request
|
169
|
+
not @pending_request.nil?
|
170
|
+
end
|
171
|
+
|
172
|
+
# Get latest changes from GitHub.
|
173
|
+
def update
|
174
|
+
@pending_requests = Octokit.pull_requests(source_repo)
|
175
|
+
repos = @pending_requests.collect do |req|
|
176
|
+
repo = req['head']['repository']
|
177
|
+
# Check if fork and commits still exist.
|
178
|
+
next if repo.nil? or not has_sha?(req['head']['sha'])
|
179
|
+
"#{repo['owner']}/#{repo['name']}"
|
180
|
+
end
|
181
|
+
host = URI.parse(github_endpoint).host
|
182
|
+
repos.uniq.compact.each do |repo|
|
183
|
+
git("fetch git@#{host}:#{repo}.git +refs/heads/*:refs/pr/#{repo}/*")
|
184
|
+
end
|
186
185
|
end
|
187
186
|
|
188
187
|
# System call to 'git'.
|
@@ -192,9 +191,9 @@ class GitReview
|
|
192
191
|
s
|
193
192
|
end
|
194
193
|
|
195
|
-
# Display helper to make output more
|
194
|
+
# Display helper to make output more configurable.
|
196
195
|
def format_text(info, size)
|
197
|
-
info.to_s.gsub("\n", ' ')[0, size].ljust(size)
|
196
|
+
info.to_s.gsub("\n", ' ')[0, size-1].ljust(size)
|
198
197
|
end
|
199
198
|
|
200
199
|
# Returns a string that specifies the source repo.
|
@@ -207,6 +206,11 @@ class GitReview
|
|
207
206
|
git('branch', false).match(/\*(.*)/)[0][2..-1]
|
208
207
|
end
|
209
208
|
|
209
|
+
# Returns a string consisting of source repo and branch.
|
210
|
+
def source
|
211
|
+
"#{source_repo}/#{source_branch}"
|
212
|
+
end
|
213
|
+
|
210
214
|
# Returns a string that specifies the target repo.
|
211
215
|
def target_repo
|
212
216
|
# TODO: Enable possibility to manually override this and set arbitrary repositories.
|
@@ -219,131 +223,88 @@ class GitReview
|
|
219
223
|
'master'
|
220
224
|
end
|
221
225
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
pulls.each do |pull|
|
226
|
-
next if pull['head']['repository'].nil? # Fork has been deleted
|
227
|
-
o = pull['head']['repository']['owner']
|
228
|
-
r = pull['head']['repository']['name']
|
229
|
-
s = pull['head']['sha']
|
230
|
-
if !has_sha(s)
|
231
|
-
repo = "#{o}/#{r}"
|
232
|
-
repos[repo] = true
|
233
|
-
end
|
234
|
-
end
|
235
|
-
if github_credentials_provided?
|
236
|
-
endpoint = "git@github.com:"
|
237
|
-
else
|
238
|
-
endpoint = github_endpoint + "/"
|
239
|
-
end
|
240
|
-
repos.each do |repo, bool|
|
241
|
-
puts "fetching #{repo}"
|
242
|
-
git("fetch #{endpoint}#{repo}.git +refs/heads/*:refs/pr/#{repo}/*")
|
243
|
-
end
|
226
|
+
# Returns a string consisting of target repo and branch.
|
227
|
+
def target
|
228
|
+
"#{target_repo}/#{target_branch}"
|
244
229
|
end
|
245
230
|
|
246
|
-
|
231
|
+
# Returns a boolean stating whether a specified commit exists.
|
232
|
+
# TODO: Check if this is still necessary, since we don't cache anymore.
|
233
|
+
def has_sha?(sha)
|
247
234
|
git("show #{sha} 2>&1")
|
248
235
|
$?.exitstatus == 0
|
249
236
|
end
|
250
237
|
|
251
|
-
|
252
|
-
|
253
|
-
|
238
|
+
# Returns a boolean stating whether a specified commit has already been merged.
|
239
|
+
def merged?(sha)
|
240
|
+
not git("rev-list #{sha} ^HEAD 2>&1").split("\n").size > 0
|
254
241
|
end
|
255
242
|
|
256
|
-
#
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
243
|
+
# Checks '~/.gitconfig' for credentials and
|
244
|
+
def configure_github_access
|
245
|
+
if (github_token.empty? or github_login.empty?)
|
246
|
+
puts 'Please update your git config and provide your GitHub user name and token.'
|
247
|
+
puts 'Some commands won\'t work properly without these credentials.'
|
248
|
+
else
|
249
|
+
Octokit.configure do |config|
|
250
|
+
config.login = github_login
|
251
|
+
config.token = github_token
|
252
|
+
config.endpoint = github_endpoint
|
253
|
+
end
|
263
254
|
end
|
264
255
|
end
|
265
256
|
|
257
|
+
# Get GitHub user name.
|
266
258
|
def github_login
|
267
259
|
git("config --get-all github.user")
|
268
260
|
end
|
269
261
|
|
262
|
+
# Get GitHub token.
|
270
263
|
def github_token
|
271
264
|
git("config --get-all github.token")
|
272
265
|
end
|
273
266
|
|
267
|
+
# Determine GitHub endpoint (defaults to 'https://github.com/').
|
274
268
|
def github_endpoint
|
275
269
|
host = git("config --get-all github.host")
|
276
|
-
|
277
|
-
host
|
278
|
-
else
|
279
|
-
'https://github.com'
|
280
|
-
end
|
281
|
-
end
|
282
|
-
|
283
|
-
# API/DATA HELPER FUNCTIONS #
|
284
|
-
|
285
|
-
def github_credentials_provided?
|
286
|
-
if github_token.empty? && github_login.empty?
|
287
|
-
return false
|
288
|
-
end
|
289
|
-
true
|
290
|
-
end
|
291
|
-
|
292
|
-
def get_pull_info
|
293
|
-
get_data(REVIEW_CACHE_FILE)['review']
|
294
|
-
end
|
295
|
-
|
296
|
-
def get_data(file)
|
297
|
-
JSON.parse(File.read(file))
|
298
|
-
end
|
299
|
-
|
300
|
-
def cache_pull_info
|
301
|
-
response = Octokit.pull_requests(source_repo)
|
302
|
-
save_data({'review' => response}, REVIEW_CACHE_FILE)
|
303
|
-
end
|
304
|
-
|
305
|
-
def save_data(data, file)
|
306
|
-
File.open(file, "w+") do |f|
|
307
|
-
f.puts data.to_json
|
308
|
-
end
|
309
|
-
end
|
310
|
-
|
311
|
-
def github_insteadof_matching(c, u)
|
312
|
-
first = c.collect {|k,v| [v, /url\.(.*github\.com.*)\.insteadof/.match(k)]}.
|
313
|
-
find {|v,m| u.index(v) and m != nil}
|
314
|
-
if first
|
315
|
-
return first[0], first[1][1]
|
316
|
-
end
|
317
|
-
return nil, nil
|
318
|
-
end
|
319
|
-
|
320
|
-
def github_user_and_proj(u)
|
321
|
-
# Trouble getting optional ".git" at end to work, so put that logic below
|
322
|
-
m = /github\.com.(.*?)\/(.*)/.match(u)
|
323
|
-
if m
|
324
|
-
return m[1], m[2].sub(/\.git\Z/, "")
|
325
|
-
end
|
326
|
-
return nil, nil
|
270
|
+
host.empty? ? 'https://github.com/' : host
|
327
271
|
end
|
328
272
|
|
329
273
|
def repo_info
|
330
|
-
|
274
|
+
# Read config_hash from local git config.
|
275
|
+
config_hash = {}
|
331
276
|
config = git('config --list')
|
332
277
|
config.split("\n").each do |line|
|
333
|
-
|
334
|
-
|
278
|
+
key, value = line.split('=')
|
279
|
+
config_hash[key] = value
|
335
280
|
end
|
336
|
-
|
337
|
-
|
338
|
-
user,
|
339
|
-
|
340
|
-
|
281
|
+
# Extract user and project name from GitHub URL.
|
282
|
+
url = config_hash['remote.origin.url']
|
283
|
+
user, project = github_user_and_project(url)
|
284
|
+
# If there are no results yet, look for 'insteadof' substitutions in URL and try again.
|
285
|
+
unless (user and project)
|
286
|
+
short, base = github_insteadof_matching(config_hash, url)
|
341
287
|
if short and base
|
342
|
-
|
343
|
-
user,
|
288
|
+
url = url.sub(short, base)
|
289
|
+
user, project = github_user_and_project(url)
|
344
290
|
end
|
345
291
|
end
|
346
|
-
[user,
|
292
|
+
[user, project]
|
293
|
+
end
|
294
|
+
|
295
|
+
def github_insteadof_matching(config_hash, url)
|
296
|
+
first = config_hash.collect { |key,value|
|
297
|
+
[value, /url\.(.*github\.com.*)\.insteadof/.match(key)]
|
298
|
+
}.find { |value, match|
|
299
|
+
url.index(value) and match != nil
|
300
|
+
}
|
301
|
+
first ? [first[0], first[1][1]] : [nil, nil]
|
302
|
+
end
|
303
|
+
|
304
|
+
# Extract user and project name from GitHub URL.
|
305
|
+
def github_user_and_project(github_url)
|
306
|
+
matches = /github\.com.(.*?)\/(.*)/.match(github_url)
|
307
|
+
matches ? [matches[1], matches[2].sub(/\.git\z/, '')] : [nil, nil]
|
347
308
|
end
|
348
309
|
|
349
310
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: git-review
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 5
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 6
|
9
9
|
- 1
|
10
|
-
version: 0.
|
10
|
+
version: 0.6.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Dominik Bamberger, Cristian Messel
|
@@ -15,11 +15,11 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-07-
|
18
|
+
date: 2011-07-18 00:00:00 +02:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
|
-
name:
|
22
|
+
name: launchy
|
23
23
|
prerelease: false
|
24
24
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
25
|
none: false
|
@@ -32,24 +32,10 @@ dependencies:
|
|
32
32
|
version: "0"
|
33
33
|
type: :runtime
|
34
34
|
version_requirements: *id001
|
35
|
-
- !ruby/object:Gem::Dependency
|
36
|
-
name: launchy
|
37
|
-
prerelease: false
|
38
|
-
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
-
none: false
|
40
|
-
requirements:
|
41
|
-
- - ">="
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
hash: 3
|
44
|
-
segments:
|
45
|
-
- 0
|
46
|
-
version: "0"
|
47
|
-
type: :runtime
|
48
|
-
version_requirements: *id002
|
49
35
|
- !ruby/object:Gem::Dependency
|
50
36
|
name: octokit
|
51
37
|
prerelease: false
|
52
|
-
requirement: &
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
53
39
|
none: false
|
54
40
|
requirements:
|
55
41
|
- - "="
|
@@ -61,7 +47,7 @@ dependencies:
|
|
61
47
|
- 1
|
62
48
|
version: 0.5.1
|
63
49
|
type: :runtime
|
64
|
-
version_requirements: *
|
50
|
+
version_requirements: *id002
|
65
51
|
description: Manage review workflow for projects hosted on GitHub (using pull requests).
|
66
52
|
email: bamberger.dominik@gmail.com
|
67
53
|
executables:
|