gerrit 0.1.0 → 0.2.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.
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