git-review 2.0.0.alpha → 2.0.0

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