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 +4 -4
- data/lib/gerrit/client.rb +25 -3
- data/lib/gerrit/command/base.rb +3 -1
- data/lib/gerrit/command/checkout.rb +13 -4
- data/lib/gerrit/command/members.rb +7 -1
- data/lib/gerrit/command/push.rb +133 -0
- data/lib/gerrit/command/setup.rb +69 -0
- data/lib/gerrit/configuration.rb +12 -2
- data/lib/gerrit/error_handler.rb +1 -0
- data/lib/gerrit/errors.rb +3 -0
- data/lib/gerrit/repo.rb +44 -2
- data/lib/gerrit/ui.rb +18 -2
- data/lib/gerrit/utils.rb +20 -0
- data/lib/gerrit/version.rb +1 -1
- metadata +17 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 87240c9921a5ea4b76b05a57b7e6a0f68c003c88
|
4
|
+
data.tar.gz: cd7197693ac6dcc24945697c450e7b8f0712c3ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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<
|
47
|
-
def
|
46
|
+
# @return [Array<Hash>]
|
47
|
+
def group_members(group, recursive: true)
|
48
48
|
flags = []
|
49
49
|
flags << '--recursive' if recursive
|
50
|
-
|
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
|
data/lib/gerrit/command/base.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/gerrit/configuration.rb
CHANGED
@@ -23,7 +23,9 @@ module Gerrit
|
|
23
23
|
from_file(config_file)
|
24
24
|
else
|
25
25
|
raise Errors::ConfigurationMissingError,
|
26
|
-
|
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]
|
data/lib/gerrit/error_handler.rb
CHANGED
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
|
-
|
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
|
-
|
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
|
data/lib/gerrit/version.rb
CHANGED
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.
|
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
|