git-review 0.5.1 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/git-review.rb +143 -182
  2. metadata +7 -21
data/lib/git-review.rb CHANGED
@@ -1,11 +1,10 @@
1
- require 'json'
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 open requests.'
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 open requests.
25
+ # List all pending requests.
27
26
  def list
28
- open_requests = get_pull_info
29
- if open_requests.size == 0
30
- puts "No open requests for '#{source_repo}/#{source_branch}'"
31
- return
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
- puts "Open requests for '#{source_repo}/#{source_branch}'"
34
- puts 'ID Date Comments Title'
35
- open_requests.reverse! if @args.shift == '--reverse'
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 review_exists?
50
- option = @args.shift
51
- puts "Number : #{@review['number']}"
52
- puts "Label : #{@review['head']['label']}"
53
- puts "Created : #{@review['created_at']}"
54
- puts "Votes : #{@review['votes']}"
55
- puts "Comments : #{@review['comments']}"
56
- puts
57
- puts "Title : #{@review['title']}"
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 @review['body']
61
- puts
61
+ puts ''
62
+ puts @pending_request['body']
63
+ puts ''
62
64
  puts '------------'
63
- puts
64
- if option == '--full'
65
- exec "git diff --color=always HEAD...#{@review['head']['sha']}"
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(@review['html_url']) if review_exists?
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
- return unless review_exists?
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 review_exists?
82
+ return unless request_exists?
86
83
  option = @args.shift
87
- if @review['head']['repository']
88
- o = @review['head']['repository']['owner']
89
- r = @review['head']['repository']['name']
90
- else # they deleted the source repo
91
- o = @review['head']['user']['login']
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 #{purl} | git am"
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
- s = @review['head']['sha']
102
- message = "Accepting request ##{@review['number']} from #{o}/#{r}\n\n---\n\n"
103
- message += @review['body'].gsub("'", '')
104
- if option == '--log'
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 review_exists?
116
- Octokit.post("issues/close/#{source_repo}/#{@review['number']}")
117
- puts "Successfully declined request." unless review_exists?(@review['number'])
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
- last_review_id = get_pull_info.collect{|review| review['number']}.sort.last.to_i
126
- title = "[Review] Request from '#{github_login}' @ '#{source_repo}/#{source_branch}'"
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
- potential_new_review = get_pull_info.find{ |review| review['title'] == title}
135
- puts 'Review request successfully created.' if potential_new_review['number'] > last_review_id
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
- configure
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.blank? or %w(-h --help).include?(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
- # Get latest changes from GitHub.
168
- def update
169
- cache_pull_info
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 if review_id.nil?
178
- review_id ||= @args.shift.to_i
179
- if review_id == 0
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
- @review = get_pull_info.find{ |review| review['number'] == review_id}
184
- puts "Review '#{review_id}' does not exist." unless @review
185
- not @review.nil?
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 beautiful.
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
- def fetch_stale_forks
223
- pulls = get_pull_info
224
- repos = {}
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
- def has_sha(sha)
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
- def not_merged?(sha)
252
- commits = git("rev-list #{sha} ^HEAD 2>&1")
253
- commits.split("\n").size > 0
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
- # PRIVATE REPOSITORIES ACCESS
257
-
258
- def configure
259
- Octokit.configure do |config|
260
- config.login = github_login
261
- config.token = github_token
262
- config.endpoint = github_endpoint
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
- if host.size > 0
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
- c = {}
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
- k, v = line.split('=')
334
- c[k] = v
278
+ key, value = line.split('=')
279
+ config_hash[key] = value
335
280
  end
336
- u = c['remote.origin.url']
337
-
338
- user, proj = github_user_and_proj(u)
339
- if !(user and proj)
340
- short, base = github_insteadof_matching(c, u)
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
- u = u.sub(short, base)
343
- user, proj = github_user_and_proj(u)
288
+ url = url.sub(short, base)
289
+ user, project = github_user_and_project(url)
344
290
  end
345
291
  end
346
- [user, proj]
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: 9
4
+ hash: 5
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 5
8
+ - 6
9
9
  - 1
10
- version: 0.5.1
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-14 00:00:00 +02:00
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: json
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: &id003 !ruby/object:Gem::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: *id003
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: