gerrit 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e622e3bacf3ff603e41ef0aaad895a33c5c0c531
4
- data.tar.gz: 463bc6a58c060e6ad3d8825fc108e9b0fcd3f1f6
3
+ metadata.gz: 87240c9921a5ea4b76b05a57b7e6a0f68c003c88
4
+ data.tar.gz: cd7197693ac6dcc24945697c450e7b8f0712c3ee
5
5
  SHA512:
6
- metadata.gz: 25f0ee6364cd63cdc9d3a2519a6bccff966f03735b5bffba41400be186b23316a7df56b1f0bbf108322be73fc6fa41c4d488214ffe1825477dc95677e8b7ff8a
7
- data.tar.gz: 6e95f4c7472f6ab0c5f422c44316cec1296665ca3f0a0fd6a9cd1b309237d50e20f809266e0710741bba71cede221dca108742e80995599eec8acf0871097c81
6
+ metadata.gz: 5467e171e12670ab484430cb9b0ed93f0a70bc42d42fb2fabe96c31bb0d1f087d3b01fcbc47115fd8d5cd7226472448fe76c1991f4145c5a557b78479a01f7ee
7
+ data.tar.gz: 04f16de2f0662152a187694d0ac06ed32f82f89b01c4465a74653e9633cada93b760d4c5fcade14d42c6dadb73ad556a220cfa54bd1028f869f0defa27914404
data/lib/gerrit/client.rb CHANGED
@@ -43,11 +43,17 @@ module Gerrit
43
43
  #
44
44
  # @param group [String] full name of the group
45
45
  # @param recursive [Boolean] whether to include members of sub-groups.
46
- # @return [Array<String>]
47
- def members(group, recursive: true)
46
+ # @return [Array<Hash>]
47
+ def group_members(group, recursive: true)
48
48
  flags = []
49
49
  flags << '--recursive' if recursive
50
- execute(%w[ls-members] + ["'#{group}'"] + flags)
50
+
51
+ rows = execute(%w[ls-members] + ["'#{group}'"] + flags).split("\n")[1..-1]
52
+
53
+ rows.map do |row|
54
+ id, username, full_name, email = row.split("\t")
55
+ { id: id, username: username, full_name: full_name, email: email }
56
+ end
51
57
  end
52
58
 
53
59
  # Returns basic information about a change.
@@ -63,5 +69,21 @@ module Gerrit
63
69
  JSON.parse(rows.first)
64
70
  end
65
71
  end
72
+
73
+ # Returns a list of all users to include in the default search scope.
74
+ #
75
+ # Gerrit doesn't actually have an endpoint to return all visible users, so
76
+ # we do the next best thing which is to get users for all groups the user is
77
+ # a part of, which for all practical purposes is probably good enough.
78
+ #
79
+ # Set the `user_search_groups` configuration option to speed this up,
80
+ # ideally to just one group so we don't have to make parallel calls.
81
+ def users
82
+ search_groups = Array(@config.fetch(:user_search_groups, groups))
83
+
84
+ Utils.map_in_parallel(search_groups) do |group|
85
+ group_members(group).map{ |user| user[:username] }
86
+ end.flatten.uniq
87
+ end
66
88
  end
67
89
  end
@@ -5,6 +5,8 @@ module Gerrit::Command
5
5
  #
6
6
  # @abstract
7
7
  class Base
8
+ include Gerrit::Utils
9
+
8
10
  # @param config [Gerrit::Configuration]
9
11
  # @param ui [Gerrit::UI]
10
12
  # @param arguments [Array<String>]
@@ -56,7 +58,7 @@ module Gerrit::Command
56
58
  # @param args [Array<String>]
57
59
  # @return [#status, #stdout, #stderr]
58
60
  def spawn(args)
59
- Subprocess.spawn(args)
61
+ Gerrit::Subprocess.spawn(args)
60
62
  end
61
63
  end
62
64
  end
@@ -2,10 +2,17 @@ module Gerrit::Command
2
2
  # Check out a patchset locally.
3
3
  class Checkout < Base
4
4
  def execute
5
- result = spawn(%W[git fetch #{repo.remote_url} #{change_refspec}])
6
- if result.success?
7
- spawn(%w[git checkout FETCH_HEAD])
5
+ refspec = change_refspec
6
+
7
+ ui.spinner('Fetching patchset...') do
8
+ result = spawn(%W[git fetch #{repo.remote_url} #{refspec}])
9
+ if result.success?
10
+ spawn(%w[git checkout FETCH_HEAD])
11
+ end
8
12
  end
13
+
14
+ ui.newline
15
+ ui.success "You have checked out #{refspec}"
9
16
  end
10
17
 
11
18
  private
@@ -31,7 +38,9 @@ module Gerrit::Command
31
38
  ui.ask('Enter change number or Change-ID').argument(:required).read_string
32
39
  end
33
40
 
34
- client.change(change_num_or_id)
41
+ ui.spinner('Finding latest patchset...') do
42
+ client.change(change_num_or_id)
43
+ end
35
44
  end
36
45
  end
37
46
  end
@@ -4,7 +4,13 @@ module Gerrit::Command
4
4
  # This allows you to list the members of a group by regex.
5
5
  class Members < Base
6
6
  def execute
7
- ui.print client.members(find_group), newline: false
7
+ users = client.group_members(find_group)
8
+
9
+ ui.table(header: %w[ID Username Name Email]) do |t|
10
+ users.each do |user|
11
+ t << [user[:id], user[:username], user[:full_name], user[:email]]
12
+ end
13
+ end
8
14
  end
9
15
 
10
16
  private
@@ -0,0 +1,133 @@
1
+ module Gerrit::Command
2
+ # Push one or more commits for review.
3
+ class Push < Base
4
+ def execute
5
+ # Sanity check: does this repository have a valid remote_url?
6
+ # (this will raise an exception if that's not the case)
7
+ remote_url = repo.remote_url
8
+
9
+ # If an explicit ref is given, skip a bunch of the questions
10
+ if commit_hash?(arguments[1]) || arguments[1] == 'HEAD'
11
+ ref = arguments[1]
12
+ reviewer_args = arguments[2..-1] || []
13
+ target_branch = 'master'
14
+ type = 'publish'
15
+ topic = nil
16
+ else
17
+ ref = 'HEAD'
18
+ reviewer_args = arguments[1..-1] || []
19
+ target_branch = ask_target_branch
20
+ type = ask_review_type
21
+ topic = ask_topic
22
+ end
23
+
24
+ reviewers = extract_reviewers(reviewer_args)
25
+
26
+ push_changes(remote_url, ref, reviewers, target_branch, type, topic)
27
+ end
28
+
29
+ private
30
+
31
+ def push_changes(remote_url, ref, reviewers, target_branch, type, topic)
32
+ command = %W[git push #{remote_url}]
33
+
34
+ if reviewers.any?
35
+ reviewer_flags = reviewers.map { |reviewer| "--reviewer=#{reviewer}" }
36
+ command += ['--receive-pack', "git receive-pack #{reviewer_flags.join(' ')}"]
37
+ end
38
+
39
+ destination_ref = "refs/#{type}/#{target_branch}"
40
+ destination_ref += "/#{topic}" if topic
41
+ command += ["#{ref}:#{destination_ref}"]
42
+
43
+ result =
44
+ ui.spinner('Pushing changes...') do
45
+ spawn(command)
46
+ end
47
+
48
+ if result.success?
49
+ ui.success result.stdout
50
+ ui.success result.stderr
51
+ else
52
+ ui.error result.stdout
53
+ ui.error result.stderr
54
+ end
55
+ end
56
+
57
+ def extract_reviewers(reviewer_args)
58
+ if reviewer_args.empty?
59
+ reviewer_args = ui.ask('Enter users/groups you would like to review your changes')
60
+ .argument(:required)
61
+ .read_string
62
+ .split(/\s*,\s*/)
63
+ end
64
+
65
+ return [] if reviewer_args.empty?
66
+
67
+ ui.spinner('Finding matching users/groups...') do
68
+ extract_users(reviewer_args)
69
+ end
70
+ end
71
+
72
+ def extract_users(reviewer_args)
73
+ usernames = []
74
+ groups = client.groups
75
+ users = client.users
76
+
77
+ reviewer_args.each do |arg|
78
+ users_or_groups = arg.split(/\s*,\s*|\s+/)
79
+
80
+ users_or_groups.each do |user_or_group|
81
+ usernames += users_from_pattern(users, groups, user_or_group)
82
+ end
83
+ end
84
+
85
+ usernames.uniq.sort
86
+ end
87
+
88
+ def users_from_pattern(users, groups, pattern)
89
+ group_users = users_from_group(groups, pattern)
90
+
91
+ # Don't scan users since we already matched a group
92
+ return group_users if group_users.any?
93
+
94
+ users.grep(/#{pattern}/i)
95
+ end
96
+
97
+ def users_from_group(groups, group)
98
+ matching_groups = groups.grep(/#{group}/i)
99
+
100
+ map_in_parallel(matching_groups) do |match|
101
+ client.group_members(match).map { |user| user[:username] }
102
+ end.flatten.uniq
103
+ end
104
+
105
+ def ask_target_branch
106
+ target = ui.ask('Target branch (default master)')
107
+ .modify(:trim)
108
+ .read_string
109
+
110
+ target.empty? ? 'master' : target
111
+ end
112
+
113
+ def ask_review_type
114
+ draft = ui.ask('Are you pushing this as a draft? (y/n) [n]')
115
+ .argument(:required)
116
+ .default('n')
117
+ .modify(:downcase)
118
+ .read_string
119
+
120
+ draft == 'y' ? 'draft' : 'publish'
121
+ end
122
+
123
+ def ask_topic
124
+ topic = ui.ask('Topic name (optional; enter * to autofill with your current branch:')
125
+ .argument(:optional)
126
+ .read_string
127
+
128
+ topic = repo.branch('HEAD') if topic == '*'
129
+ topic.strip.empty? ? nil : topic
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,69 @@
1
+ module Gerrit::Command
2
+ # Sets up the remotes for this repository to push/pull to/from Gerrit.
3
+ class Setup < Base
4
+ def execute
5
+ remotes_to_add = config[:remotes]
6
+ existing_remotes = repo.remotes.keys & remotes_to_add.keys
7
+
8
+ if existing_remotes.any?
9
+ return unless can_replace?(existing_remotes)
10
+ end
11
+
12
+ add_remotes(remotes_to_add)
13
+ end
14
+
15
+ private
16
+
17
+ def can_replace?(existing_remotes)
18
+ ui.warning 'The following remotes already exist and will be replaced:'
19
+ existing_remotes.each do |remote|
20
+ ui.info remote
21
+ end
22
+
23
+ ui.newline
24
+ ui.ask('Replace them? (y/n)[n]')
25
+ .argument(:required)
26
+ .default('n')
27
+ .modify(:downcase)
28
+ .read_string == 'y'
29
+ end
30
+
31
+ def add_remotes(remotes)
32
+ remotes.each do |remote_name, remote_config|
33
+ remote_url = render_remote_url(remote_config)
34
+
35
+ `git remote rm #{remote_name} &> /dev/null`
36
+ `git remote add #{remote_name} #{remote_url}`
37
+
38
+ if remote_config['push']
39
+ `git config remote.#{remote_name}.push #{remote_config['push']}`
40
+ end
41
+
42
+ ui.success "Added #{remote_name} #{remote_url}"
43
+ end
44
+
45
+ ui.newline
46
+ ui.info 'You can now push commits for review by running: ', newline: false
47
+ ui.print 'gerrit push'
48
+ end
49
+
50
+ def render_remote_url(remote_config)
51
+ remote_config['url'] % {
52
+ user: config[:user],
53
+ host: config[:host],
54
+ port: config[:port],
55
+ project: project_name,
56
+ }
57
+ end
58
+
59
+ # Allow a project name to be explicitly specified, otherwise just use the
60
+ # repo root directory name.
61
+ def project_name
62
+ if arguments[2]
63
+ arguments[2]
64
+ else
65
+ File.basename(repo.root)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -23,7 +23,9 @@ module Gerrit
23
23
  from_file(config_file)
24
24
  else
25
25
  raise Errors::ConfigurationMissingError,
26
- 'No configuration file was found'
26
+ "No configuration file '#{FILE_NAME}' was found in the " \
27
+ "current directory or any ancestor directory.\n\n" \
28
+ "See #{REPO_URL}#configuration for instructions on setting up."
27
29
  end
28
30
  end
29
31
 
@@ -68,11 +70,19 @@ module Gerrit
68
70
  # Access the configuration as if it were a hash.
69
71
  #
70
72
  # @param key [String, Symbol]
71
- # @return [Array,Hash,Number,String]
73
+ # @return [Array, Hash, Number, String]
72
74
  def [](key)
73
75
  @options[key.to_s]
74
76
  end
75
77
 
78
+ # Access the configuration as if it were a hash.
79
+ #
80
+ # @param key [String, Symbol]
81
+ # @return [Array, Hash, Number, String]
82
+ def fetch(key, *args)
83
+ @options.fetch(key.to_s, *args)
84
+ end
85
+
76
86
  # Compares this configuration with another.
77
87
  #
78
88
  # @param other [HamlLint::Configuration]
@@ -16,6 +16,7 @@ module Gerrit
16
16
  def handle(ex)
17
17
  case ex
18
18
  when Errors::UsageError
19
+ ui.error ex.message
19
20
  CLI::ExitCodes::USAGE
20
21
  when Errors::ConfigurationError
21
22
  ui.error ex.message
data/lib/gerrit/errors.rb CHANGED
@@ -12,6 +12,9 @@ module Gerrit::Errors
12
12
  # Base class for all configuration-related errors.
13
13
  class ConfigurationError < GerritError; end
14
14
 
15
+ # Raised when something is incorrect with the configuration.
16
+ class ConfigurationInvalidError < ConfigurationError; end
17
+
15
18
  # Raised when a configuration file is not present.
16
19
  class ConfigurationMissingError < ConfigurationError; end
17
20
 
data/lib/gerrit/repo.rb CHANGED
@@ -8,6 +8,18 @@ module Gerrit
8
8
  @config = config
9
9
  end
10
10
 
11
+ # Returns the name of the currently checked-out branch or the branch the
12
+ # specified ref is on.
13
+ #
14
+ # Returns nil if it is detached.
15
+ #
16
+ # @return [String, nil]
17
+ def branch(ref = 'HEAD')
18
+ name = `git branch`.split("\n").grep(/^\* /).first[/\w+/]
19
+ # Check if detached head
20
+ name.start_with?('(') ? nil : name
21
+ end
22
+
11
23
  # Returns the absolute path to the root of the current repository the
12
24
  # current working directory resides within.
13
25
  #
@@ -60,12 +72,42 @@ module Gerrit
60
72
  #
61
73
  # @return [String]
62
74
  def project
63
- @config[:project] || File.basename(root)
75
+ if url = remote_url
76
+ File.basename(url[/\/[^\/]+$/], '.git')
77
+ else
78
+ # Otherwise just use the name of this repository
79
+ File.basename(root)
80
+ end
81
+ #
82
+ end
83
+
84
+ # Returns all remotes this repository has configured.
85
+ #
86
+ # @return [Hash] hash of remote names mapping to their URLs
87
+ def remotes
88
+ Hash[
89
+ `git config --get-regexp '^remote\..+\.url$'`.split("\n").map do |line|
90
+ match = line.match(/^remote\.(?<name>\S+)\.url\s+(?<url>.*)/)
91
+ [match[:name], match[:url]]
92
+ end
93
+ ]
64
94
  end
65
95
 
66
96
  # Returns the Gerrit remote URL for this repo.
67
97
  def remote_url
68
- "ssh://#{@config[:user]}@#{@config[:host]}:#{@config[:port]}/#{project}"
98
+ unless push_remote = @config[:push_remote]
99
+ raise Errors::ConfigurationInvalidError,
100
+ 'You must specify the `push_remote` option in your configuration.'
101
+ end
102
+
103
+ unless url = remotes[push_remote]
104
+ raise Errors::ConfigurationInvalidError,
105
+ "The '#{push_remote}' `push_remote` specified in your " \
106
+ 'configuration is not a remote in this repository. ' \
107
+ 'Have you run `gerrit setup`?'
108
+ end
109
+
110
+ url
69
111
  end
70
112
  end
71
113
  end
data/lib/gerrit/ui.rb CHANGED
@@ -90,12 +90,28 @@ module Gerrit
90
90
  print('')
91
91
  end
92
92
 
93
+ # Execute a command with a spinner animation until it completes.
94
+ def spinner(*args, &block)
95
+ spinner = TTY::Spinner.new(*args)
96
+ spinner_thread = Thread.new do
97
+ loop do
98
+ sleep 0.1
99
+ spinner.spin
100
+ end
101
+ end
102
+
103
+ block.call
104
+ ensure
105
+ spinner_thread.kill
106
+ newline # Ensure next line of ouptut on separate line from spinner
107
+ end
108
+
93
109
  # Prints a table.
94
110
  #
95
111
  # Customize the table by passing a block and operating on the table object
96
112
  # passed to that block to add rows and customize its appearance.
97
- def table(&block)
98
- t = TTY::Table.new
113
+ def table(options = {}, &block)
114
+ t = TTY::Table.new(options)
99
115
  block.call(t)
100
116
  print(t.render(:unicode))
101
117
  end
data/lib/gerrit/utils.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'parallel'
2
+
1
3
  module Gerrit
2
4
  # A miscellaneous set of utility functions.
3
5
  module Utils
@@ -13,6 +15,24 @@ module Gerrit
13
15
  .join
14
16
  end
15
17
 
18
+ # Returns whether a string appears to be a commit SHA1 hash.
19
+ #
20
+ # @param string [String]
21
+ # @return [Boolean]
22
+ def commit_hash?(string)
23
+ string =~ /^\h{7,40}$/
24
+ end
25
+
26
+ # Executing a block on each item in parallel.
27
+ #
28
+ # @param items [Enumerable]
29
+ # @return [Array]
30
+ def map_in_parallel(items, &block)
31
+ Parallel.map(items, in_threads: Parallel.processor_count) do |item|
32
+ block.call(item)
33
+ end
34
+ end
35
+
16
36
  # Convert string containing camel case or spaces into snake case.
17
37
  #
18
38
  # @see stackoverflow.com/questions/1509915/converting-camel-case-to-underscore-case-in-ruby
@@ -1,4 +1,4 @@
1
1
  # Defines the gem version.
2
2
  module Gerrit
3
- VERSION = '0.1.0'
3
+ VERSION = '0.2.0'
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gerrit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane da Silva
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.5.6
27
+ - !ruby/object:Gem::Dependency
28
+ name: parallel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.6.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.6.0
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: tty
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -56,6 +70,8 @@ files:
56
70
  - lib/gerrit/command/help.rb
57
71
  - lib/gerrit/command/members.rb
58
72
  - lib/gerrit/command/projects.rb
73
+ - lib/gerrit/command/push.rb
74
+ - lib/gerrit/command/setup.rb
59
75
  - lib/gerrit/command/version.rb
60
76
  - lib/gerrit/configuration.rb
61
77
  - lib/gerrit/constants.rb