git-review 2.0.0.alpha → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 49d9d8f5c8209622227ccaecb90dbb9db6549f06
4
+ data.tar.gz: f8041155345b3f1afd104904a64401295fa4bb6f
5
+ SHA512:
6
+ metadata.gz: 5e6eca5b975528044e599608d58f4d90adb9d466fbe22e9d3da6b21ba53bbe16a49f21e85cd361d3bb3a804c4cacdb1af71b4d04f714fd98c99f4b0943f35b05
7
+ data.tar.gz: 05ac7231034a1b309e434b1257645c5df10f7a9dd8126a9efadcfba6b0808520860c28fc682b161c6fcf119adaf93d77a84eaeddf81ab6407cf69e85b7b5274c
@@ -7,23 +7,23 @@ require 'git-review'
7
7
  require 'gli'
8
8
 
9
9
  include GLI::App
10
-
11
10
  program_desc 'Manage review workflow for Github projects (using pull requests).'
12
11
 
13
12
  # Pre-hook before a command is executed
14
13
  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'
14
+ server = ::GitReview::Server.instance
15
+ if server.configure_access && server.source_repo
16
+ server.update unless cmd == 'clean'
18
17
  end
19
- true # return true to explicitly pass precondition
18
+
19
+ true # return true to explicitly pass precondition
20
20
  end
21
21
 
22
22
  desc 'List all pending requests'
23
23
  command :list do |c|
24
24
  c.switch [:r, :reverse]
25
25
  c.action do |global, opts, args|
26
- ::GitReview::Commands.list(opts[:reverse])
26
+ ::GitReview::Commands.list(opts[:reverse])
27
27
  end
28
28
  end
29
29
 
@@ -46,7 +46,7 @@ end
46
46
 
47
47
  desc 'Checkout a request\'s changes to local repo'
48
48
  command :checkout do |c|
49
- c.switch [:b, :branch]
49
+ c.switch [:b, :branch], default_value: true
50
50
  c.action do |global, opts, args|
51
51
  help_now!('Request number is required.') if args.empty?
52
52
  ::GitReview::Commands.checkout(args.shift, opts[:branch])
@@ -81,7 +81,7 @@ desc 'Create a new local branch for a request'
81
81
  command :prepare do |c|
82
82
  c.switch [:n, :new]
83
83
  c.action do |global, opts, args|
84
- ::GitReview::Commands.prepare(opts[:new], args.shift)
84
+ ::GitReview::Commands.prepare(opts[:new], args.empty? ? nil : args.join(' '))
85
85
  end
86
86
  end
87
87
 
@@ -100,7 +100,16 @@ command :clean do |c|
100
100
  c.action do |global, opts, args|
101
101
  help_now!('Request number is required.') if args.empty? && !opts[:all]
102
102
  number = args.empty? ? nil : args.shift
103
- ::GitReview::Commands.clean(number, opts[:force], opts[:all])
103
+ ::GitReview::Commands.clean(args.shift, opts[:force], opts[:all])
104
+ end
105
+ end
106
+
107
+ if ::GitReview::Settings.instance.review_mode == 'debug' || ENV['DEBUG']
108
+ desc 'Console session for debugging'
109
+ command :console do |c|
110
+ c.action do |global, opts, args|
111
+ ::GitReview::Commands.console(args.shift)
112
+ end
104
113
  end
105
114
  end
106
115
 
@@ -1,28 +1,51 @@
1
+ ### Dependencies
2
+
3
+ ## External Dependencies
4
+
1
5
  # Provide access to GitHub's API.
2
6
  require 'octokit'
3
7
  # Open a browser in 'browse' command.
4
8
  require 'launchy'
5
9
  # Parse time strings from git back into Time objects.
6
10
  require 'time'
7
- # Use temporary files to allow editing a request's title and body.
11
+ # Use temporary files to allow editing a request's.
8
12
  require 'tempfile'
9
13
 
10
- ## Our own dependencies
14
+ ## Internal dependencies
11
15
 
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'
16
+ # Include helper functions to make it work as expected.
17
+ require_relative 'git-review/helpers'
20
18
  # Provide available commands.
21
19
  require_relative 'git-review/commands'
20
+ # Read and write settings from/to the filesystem.
21
+ require_relative 'git-review/settings'
22
+ # Deal with local git repository.
23
+ require_relative 'git-review/local'
22
24
  # Include all kinds of custom-defined errors.
23
25
  require_relative 'git-review/errors'
26
+ # Factory to get git API client..
27
+ require_relative 'git-review/server'
28
+ # Generic base class for shared provider methods.
29
+ require_relative 'git-review/provider/base'
30
+ # Communicate with Github via API.
31
+ require_relative 'git-review/provider/github'
32
+ # Communicate with Bitbucket via API.
33
+ require_relative 'git-review/provider/bitbucket'
24
34
 
35
+ # Allow easy string colorization in the console.
36
+ require_relative 'mixins/colorizable'
37
+ # Allow to access a model's attributes in various ways (feels railsy).
38
+ require_relative 'mixins/accessible'
39
+ # Allow to nest models in other model's attributes.
40
+ require_relative 'mixins/nestable'
25
41
 
26
- module GitReview
42
+ # Include custom string helpers.
43
+ require_relative 'mixins/string'
44
+ # Include custom time helpers.
45
+ require_relative 'mixins/time'
27
46
 
28
- end
47
+ # Add some POROs to get some structure into the entities git-review deals with.
48
+ require_relative 'models/repository'
49
+ require_relative 'models/user'
50
+ require_relative 'models/commit'
51
+ require_relative 'models/request'
@@ -2,66 +2,81 @@ module GitReview
2
2
 
3
3
  module Commands
4
4
 
5
- include ::GitReview::Internals
5
+ include ::GitReview::Helpers
6
6
  extend self
7
7
 
8
8
  # List all pending requests.
9
- def list(reverse=false)
10
- requests = github.current_requests_full.reject { |request|
11
- # find only pending (= unmerged) requests and output summary
12
- # explicitly look for local changes Github does not yet know about
13
- local.merged?(request.head.sha)
14
- }
15
- requests.reverse! if reverse
9
+ def list(reverse = false)
10
+ requests = server.current_requests_full.reject do |request|
11
+ # Find only pending (= unmerged) requests and output summary.
12
+ # Explicitly look for local changes git does not yet know about.
13
+ # TODO: Isn't this a bit confusing? Maybe display pending pushes?
14
+ local.merged? request.head.sha
15
+ end
16
16
  source = local.source
17
17
  if requests.empty?
18
18
  puts "No pending requests for '#{source}'."
19
19
  else
20
20
  puts "Pending requests for '#{source}':"
21
- puts "ID Updated Comments Title"
22
- requests.each { |request| print_request(request) }
21
+ puts "ID Updated Comments Title".pink
22
+ print_requests(requests, reverse)
23
23
  end
24
24
  end
25
25
 
26
26
  # Show details for a single request.
27
- def show(number, full=false)
28
- request = get_request_by_number(number)
29
- # determine whether to show full diff or just stats
27
+ def show(number, full = false)
28
+ request = server.get_request_by_number(number)
29
+ # Determine whether to show full diff or stats only.
30
30
  option = full ? '' : '--stat '
31
31
  diff = "diff --color=always #{option}HEAD...#{request.head.sha}"
32
- print_request_details(request)
32
+ # TODO: Refactor into using Request model.
33
+ print_request_details request
33
34
  puts git_call(diff)
34
- print_request_discussions(request)
35
+ print_request_discussions request
35
36
  end
36
37
 
37
38
  # Open a browser window and review a specified request.
38
39
  def browse(number)
39
- request = get_request_by_number(number)
40
- Launchy.open(request.html_url)
40
+ request = server.get_request_by_number(number)
41
+ # FIXME: Use request.html_url as soon as we are using our Request model.
42
+ Launchy.open request._links.html.href
41
43
  end
42
44
 
43
45
  # Checkout a specified request's changes to your local repository.
44
- def checkout(number, branch=false)
45
- request = get_request_by_number(number)
46
+ def checkout(number, branch = true)
47
+ request = server.get_request_by_number(number)
46
48
  puts 'Checking out changes to your local repository.'
47
49
  puts 'To get back to your original state, just run:'
48
50
  puts
49
- puts ' git checkout master'
51
+ puts ' git checkout master'.pink
50
52
  puts
53
+ # Ensure we are looking at the right remote.
54
+ remote = local.remote_for_request(request)
55
+ git_call "fetch #{remote}"
56
+ # Checkout the right branch.
57
+ branch_name = request.head.ref
51
58
  if branch
52
- git_call("checkout #{request.head.ref}")
59
+ if local.branch_exists?(:local, branch_name)
60
+ if local.source_branch == branch_name
61
+ puts "On branch #{branch_name}."
62
+ else
63
+ git_call "checkout #{branch_name}"
64
+ end
65
+ else
66
+ git_call "checkout --track -b #{branch_name} #{remote}/#{branch_name}"
67
+ end
53
68
  else
54
- git_call("checkout pr/#{request.number}")
69
+ git_call "checkout #{remote}/#{branch_name}"
55
70
  end
56
71
  end
57
72
 
58
73
  # Add an approving comment to the request.
59
74
  def approve(number)
60
- request = get_request_by_number(number)
61
- repo = github.source_repo
75
+ request = server.get_request_by_number(number)
76
+ repo = server.source_repo
62
77
  # TODO: Make this configurable.
63
78
  comment = 'Reviewed and approved.'
64
- response = github.add_comment(repo, request.number, comment)
79
+ response = server.add_comment(repo, request.number, comment)
65
80
  if response[:body] == comment
66
81
  puts 'Successfully approved request.'
67
82
  else
@@ -71,7 +86,7 @@ module GitReview
71
86
 
72
87
  # Accept a specified request by merging it into master.
73
88
  def merge(number)
74
- request = get_request_by_number(number)
89
+ request = server.get_request_by_number(number)
75
90
  if request.head.repo
76
91
  message = "Accept request ##{request.number} " +
77
92
  "and merge changes into \"#{local.target}\""
@@ -85,108 +100,144 @@ module GitReview
85
100
  puts
86
101
  puts git_call(command)
87
102
  else
88
- print_repo_deleted(request)
103
+ print_repo_deleted request
89
104
  end
90
105
  end
91
106
 
92
107
  # Close a specified request.
93
108
  def close(number)
94
- request = get_request_by_number(number)
95
- repo = github.source_repo
96
- github.close_issue(repo, request.number)
97
- unless github.request_exists?('open', request.number)
109
+ request = server.get_request_by_number(number)
110
+ repo = server.source_repo
111
+ server.close_issue(repo, request.number)
112
+ unless server.request_exists?('open', request.number)
98
113
  puts 'Successfully closed request.'
99
114
  end
100
115
  end
101
116
 
102
117
  # Prepare local repository to create a new request.
103
- # People should work on local branches, but especially for single commit
104
- # changes, more often than not, they don't. Therefore we create a branch
105
- # for them, to be able to use code review the way it is intended.
106
- # @return [Array(String, String)] the original branch and the local branch
107
- def prepare(new=false, name=nil)
108
- # remember original branch the user was currently working on
109
- original_branch = local.source_branch
110
- if new || !local.on_feature_branch?
111
- local_branch = move_uncommitted_changes(local.target_branch, name)
118
+ # NOTE:
119
+ # People should work on local branches, but especially for single commit
120
+ # changes, more often than not, they don't. Therefore this is called
121
+ # automatically before creating a pull request, such that we create a
122
+ # proper feature branch for them, to be able to use code review the way it
123
+ # is intended.
124
+ def prepare(force_new_branch = false, feature_name = nil)
125
+ current_branch = local.source_branch
126
+ if force_new_branch || !local.on_feature_branch?
127
+ feature_name ||= get_branch_name
128
+ feature_branch = move_local_changes(
129
+ current_branch, local.sanitize_branch_name(feature_name)
130
+ )
112
131
  else
113
- local_branch = original_branch
132
+ feature_branch = current_branch
114
133
  end
115
- [original_branch, local_branch]
134
+ [current_branch, feature_branch]
116
135
  end
117
136
 
118
137
  # Create a new request.
119
- # TODO: Support creating requests to other repositories and branches (like
120
- # the original repo, this has been forked from).
121
- def create(upstream=false)
122
- # prepare original_branch and local_branch
138
+ def create(upstream = false)
139
+ # Prepare original_branch and local_branch.
140
+ # TODO: Allow to use the same switches and parameters that prepare takes.
123
141
  original_branch, local_branch = prepare
124
- # don't create request with uncommitted changes in current branch
125
- unless git_call('diff HEAD').empty?
142
+ # Don't create request with uncommitted changes in current branch.
143
+ if local.uncommitted_changes?
126
144
  puts 'You have uncommitted changes.'
127
145
  puts 'Please stash or commit before creating the request.'
128
146
  return
129
147
  end
130
- if git_call("cherry #{local.target_branch}").empty?
131
- puts 'Nothing to push to remote yet. Commit something first.'
132
- else
133
- if github.request_exists_for_branch?(upstream)
148
+ if local.new_commits?(upstream)
149
+ # Feature branch differs from local or upstream master.
150
+ if server.request_exists_for_branch?(upstream)
134
151
  puts 'A pull request already exists for this branch.'
135
152
  puts 'Please update the request directly using `git push`.'
136
153
  return
137
154
  end
138
- # push latest commits to the remote branch (create if necessary)
139
- git_call("push --set-upstream origin #{local_branch}", debug_mode, true)
140
- create_pull_request(upstream)
141
- # return to the user's original branch
142
- # FIXME: keep track of original branch etc
143
- git_call("checkout #{original_branch}")
155
+ # Push latest commits to the remote branch (create if necessary).
156
+ remote = local.remote_for_branch(local_branch) || 'origin'
157
+ git_call(
158
+ "push --set-upstream #{remote} #{local_branch}", debug_mode, true
159
+ )
160
+ server.send_pull_request upstream
161
+ # Return to the user's original branch.
162
+ git_call "checkout #{original_branch}"
163
+ else
164
+ puts 'Nothing to push to remote yet. Commit something first.'
144
165
  end
145
166
  end
146
167
 
147
- # delete obsolete branches (left over from already closed requests)
148
- def clean(number=nil, force=false, all=false)
149
- # pruning is needed to remove deleted branches from your local track
150
- git_call('remote prune origin')
151
- # determine strategy to clean.
168
+ # Remove remotes with 'review' prefix (left over from previous reviews).
169
+ # Prune all existing remotes and delete obsolete branches (left over from
170
+ # already closed requests).
171
+ def clean(number = nil, force = false, all = false)
172
+ git_call "checkout #{local.target_branch}"
173
+ local.prune_remotes
174
+ # Determine strategy to clean.
152
175
  if all
153
176
  local.clean_all
154
177
  else
155
178
  local.clean_single(number, force)
156
179
  end
180
+ # Remove al review remotes without existing local branches.
181
+ local.clean_remotes
157
182
  end
158
183
 
159
184
  # Start a console session (used for debugging)
160
- def console
185
+ def console(number = nil)
161
186
  puts 'Entering debug console.'
162
- if RUBY_VERSION == '2.0.0'
163
- require 'byebug'
164
- byebug
187
+ request = server.get_request_by_number(number) if number
188
+
189
+ if RUBY_VERSION.to_f >= 2
190
+ begin
191
+ require 'byebug'
192
+ byebug
193
+ rescue LoadError => e
194
+ puts
195
+ puts 'Missing debugger, please install byebug:'
196
+ puts ' gem install byebug'
197
+ puts
198
+ end
165
199
  else
166
- require 'ruby-debug'
167
- Debugger.start
168
- debugger
200
+ begin
201
+ require 'ruby-debug'
202
+ Debugger.start
203
+ debugger
204
+ rescue LoadError => e
205
+ puts
206
+ puts 'Missing debugger, please install ruby-debug:'
207
+ puts ' gem install ruby-debug'
208
+ puts
209
+ end
169
210
  end
170
211
  puts 'Leaving debug console.'
171
212
  end
172
213
 
173
- private
174
214
 
175
- def print_request(request)
176
- date_string = format_time(request.updated_at)
177
- comments_count = request.comments.to_i + request.review_comments.to_i
178
- line = format_text(request.number, 8)
179
- line << format_text(date_string, 11)
180
- line << format_text(comments_count, 10)
181
- line << format_text(request.title, 91)
182
- puts line
215
+ private
216
+
217
+ def request_summary(request)
218
+ line = request.number.to_s.review_ljust(8)
219
+ line << request.updated_at.review_time.review_ljust(11)
220
+ line << server.comments_count(request).to_s.review_ljust(10)
221
+ line << request.title.review_ljust(91)
222
+ line
223
+ end
224
+
225
+ def print_requests(requests, reverse=false)
226
+ # put all output lines in a hash first, keyed by request number
227
+ # this is to make sure the order is still correct even if we use
228
+ # multi-threading to retrieve the requests
229
+ output = {}
230
+ requests.each { |req| output[req.number] = request_summary(req) }
231
+ numbers = output.keys.sort
232
+ numbers.reverse! if reverse
233
+ numbers.each { |n| puts output[n] }
183
234
  end
184
235
 
185
236
  def print_request_details(request)
186
- comments_count = request.comments.to_i + request.review_comments.to_i
237
+ comments_count = server.comments_count(request)
187
238
  puts 'ID : ' + request.number.to_s
188
239
  puts 'Label : ' + request.head.label
189
- puts 'Updated : ' + format_time(request.updated_at)
240
+ puts 'Updated : ' + request.updated_at.review_time
190
241
  puts 'Comments : ' + comments_count.to_s
191
242
  puts
192
243
  puts request.title
@@ -200,7 +251,7 @@ module GitReview
200
251
  def print_request_discussions(request)
201
252
  puts 'Progress :'
202
253
  puts
203
- puts github.discussion(request.number)
254
+ puts server.discussion(request.number)
204
255
  end
205
256
 
206
257
  # someone deleted the source repo
@@ -221,111 +272,45 @@ module GitReview
221
272
  # @return [String] sanitized branch name
222
273
  def get_branch_name
223
274
  puts 'Please provide a name for the branch:'
224
- branch_name = gets.chomp
225
- branch_name.gsub(/\W+/, '_').downcase
275
+ local.sanitize_branch_name gets.chomp
276
+ end
277
+
278
+ # @return [String] the complete feature branch name
279
+ def create_feature_name(new_branch)
280
+ "review_#{Time.now.strftime("%y%m%d")}_#{new_branch}"
226
281
  end
227
282
 
228
- # move uncommitted changes from target branch to local branch
283
+ # Move uncommitted changes from original_branch to a feature_branch.
229
284
  # @return [String] the new local branch uncommitted changes are moved to
230
- def move_uncommitted_changes(target_branch, new_branch)
231
- new_branch ||= get_branch_name
232
- local_branch = "review_#{Time.now.strftime("%y%m%d")}_#{new_branch}"
233
- git_call("checkout -b #{local_branch}")
234
- # make sure we are on the feature branch
235
- if local.source_branch == local_branch
236
- # stash any uncommitted changes
285
+ def move_local_changes(original_branch, feature_name)
286
+ feature_branch = create_feature_name(feature_name)
287
+ # By checking out the feature branch, the commits on the original branch
288
+ # are copied over. That way we only need to remove pending (local) commits
289
+ # from the original branch.
290
+ git_call "checkout -b #{feature_branch}"
291
+ if local.source_branch == feature_branch
292
+ # Save any uncommitted changes, to be able to reapply them later.
237
293
  save_uncommitted_changes = local.uncommitted_changes?
238
294
  git_call('stash') if save_uncommitted_changes
239
- # go back to target and get rid of pending commits
240
- git_call("checkout #{target_branch}")
241
- git_call("reset --hard origin/#{target_branch}")
242
- git_call("checkout #{local_branch}")
295
+ # Go back to original branch and get rid of pending (local) commits.
296
+ git_call("checkout #{original_branch}")
297
+ remote = local.remote_for_branch(original_branch)
298
+ remote += '/' if remote
299
+ git_call("reset --hard #{remote}#{original_branch}")
300
+ git_call("checkout #{feature_branch}")
243
301
  git_call('stash pop') if save_uncommitted_changes
244
- local_branch
245
- end
246
- end
247
-
248
- def create_pull_request(to_upstream=false)
249
- target_repo = local.target_repo(to_upstream)
250
- head = local.head
251
- base = local.target_branch
252
- title, body = create_title_and_body(base)
253
-
254
- # gather information before creating pull request
255
- lastest_number = github.latest_request_number(target_repo)
256
-
257
- # create the actual pull request
258
- github.create_pull_request(target_repo, base, head, title, body)
259
- # switch back to target_branch and check for success
260
- git_call("checkout #{base}")
261
-
262
- # make sure the new pull request is indeed created
263
- new_number = github.request_number_by_title(title, target_repo)
264
- if new_number && new_number > lastest_number
265
- puts "Successfully created new request ##{new_number}"
266
- puts "https://github.com/#{target_repo}/pull/#{new_number}"
267
- else
268
- puts "Pull request was not created for #{target_repo}."
269
- end
270
- end
271
-
272
-
273
- # @return [Array(String, String)] the title and the body of pull request
274
- def create_title_and_body(target_branch)
275
- source = local.source
276
- login = github.github.login
277
- commits = git_call("log --format='%H' HEAD...#{target_branch}").
278
- lines.count
279
- puts "commits: #{commits}"
280
- if commits == 1
281
- # we can create a really specific title and body
282
- title = git_call("log --format='%s' HEAD...#{target_branch}").chomp
283
- body = git_call("log --format='%b' HEAD...#{target_branch}").chomp
284
- else
285
- title = "[Review] Request from '#{login}' @ '#{source}'"
286
- body = "Please review the following changes:\n"
287
- body += git_call("log --oneline HEAD...#{target_branch}").
288
- lines.map{|l| " * #{l.chomp}"}.join("\n")
302
+ feature_branch
289
303
  end
290
- edit_title_and_body(title, body)
291
304
  end
292
305
 
293
- # TODO: refactor
294
- def edit_title_and_body(title, body)
295
- tmpfile = Tempfile.new('git-review')
296
- tmpfile.write(title + "\n\n" + body)
297
- tmpfile.flush
298
- editor = ENV['TERM_EDITOR'] || ENV['EDITOR']
299
- unless editor
300
- warn 'Please set $EDITOR or $TERM_EDITOR in your .bash_profile.'
301
- end
302
-
303
- system("#{editor || 'open'} #{tmpfile.path}")
304
-
305
- tmpfile.rewind
306
- lines = tmpfile.read.lines.to_a
307
- puts lines.inspect
308
- title = lines.shift.chomp
309
- lines.shift if lines[0].chomp.empty?
310
- body = lines.join
311
- tmpfile.unlink
312
- [title, body]
313
- end
314
-
315
- def github
316
- @github ||= ::GitReview::Github.instance
306
+ def server
307
+ @server ||= ::GitReview::Server.instance
317
308
  end
318
309
 
319
310
  def local
320
311
  @local ||= ::GitReview::Local.instance
321
312
  end
322
313
 
323
- def get_request_by_number(request_number)
324
- request = github.request_exists?(request_number)
325
- request || (raise ::GitReview::InvalidRequestIDError)
326
- end
327
-
328
314
  end
329
315
 
330
316
  end
331
-