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.
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: