fuel-cli 0.0.2
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 +15 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/bin/fuel +4 -0
- data/bin/gerrit +4 -0
- data/bin/jenkins +4 -0
- data/bin/jira +4 -0
- data/fuel-cli.gemspec +30 -0
- data/lib/fuel/cli/base.rb +27 -0
- data/lib/fuel/cli/gerrit.rb +328 -0
- data/lib/fuel/cli/gerrit_common.rb +21 -0
- data/lib/fuel/cli/jenkins.rb +89 -0
- data/lib/fuel/cli/jira.rb +86 -0
- data/lib/fuel/cli/main.rb +86 -0
- data/lib/fuel/cli/patches.rb +13 -0
- data/lib/fuel/cli/version.rb +5 -0
- data/lib/fuel/cli.rb +13 -0
- data/lib/fuel/util/config.rb +54 -0
- data/lib/fuel/util/json_parser.rb +15 -0
- metadata +154 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZDBlMjU0MmNiZjIyOTc2NWYzZjQwYjNkNDBiNzBlYjgyMDMxN2MzNQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZWZkMDVmMjZkOTRiZjRiMTk1MmViY2YxZjRhZDNiMjIwNThjMjUxZA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NDFjYWRjYzIyMjlhOGJmOGRmZDlmZGY1M2M1M2E0MzI1MDNkOGIwYzQxYjNl
|
10
|
+
YTY2MmZmMzcyOWFkNTY1YWQ1NzgxZTYwNmQ0NmY0NjY5ZDkwMGIwZDVmNDIz
|
11
|
+
MGZkN2VhM2IyNGQ0ZGRmNTJlZDU3MWViNTE2ZjE2YzAzOWY2ODM=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
Mjg2YWIyYjVmZDMxOTRmMzRmZDUxNmQyMWI2MTA1MzZhNGNlMzNkMzgzMjgz
|
14
|
+
M2QwNThlMzkwMTNkNGJhZTM5OWQ1M2UxNGNjZjE3MDIwN2I0Y2Y3NzQ4OTY0
|
15
|
+
MWRkOTIxNDFiNjY2YzlmOTA5MDE5NWY0OTZmNjI4Y2ZiNGEzZjQ=
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Alex Zherdev
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Fuel::Cli
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'fuel-cli'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install fuel-cli
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it ( https://github.com/[my-github-username]/fuel-cli/fork )
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/fuel
ADDED
data/bin/gerrit
ADDED
data/bin/jenkins
ADDED
data/bin/jira
ADDED
data/fuel-cli.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'fuel/cli/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "fuel-cli"
|
8
|
+
spec.version = Fuel::CLI::VERSION
|
9
|
+
spec.authors = ["Alex Zherdev"]
|
10
|
+
spec.email = ["azherdev@rocketfuel.com"]
|
11
|
+
spec.summary = %q{Simple set of command-line tools to aid in Fuel dev process.}
|
12
|
+
spec.description = %q{}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.post_install_message = "\e[36m\e[1mWelcome, Fuel developer! Type \"fuel setup\" to get started.\e[0m"
|
22
|
+
|
23
|
+
spec.add_dependency "thor"
|
24
|
+
spec.add_dependency "httparty"
|
25
|
+
spec.add_dependency "jenkins_api_client"
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
28
|
+
spec.add_development_dependency "rake"
|
29
|
+
spec.add_development_dependency "debugger"
|
30
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Fuel
|
2
|
+
module CLI
|
3
|
+
class Base < Thor
|
4
|
+
include Fuel::Util
|
5
|
+
|
6
|
+
include HTTParty
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def commit_message
|
11
|
+
`git log -1 --pretty=%B`
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch_change_id_or_fail
|
15
|
+
change_id = commit_message =~ /Change-Id: (.*)/ && $1
|
16
|
+
change_id or raise Thor::Error, set_color("No appropriate commit found at HEAD", :red)
|
17
|
+
end
|
18
|
+
|
19
|
+
def set_credentials(user, user_keys, password_keys)
|
20
|
+
password = ask("Password:", echo: false)
|
21
|
+
Config.set(*user_keys, user)
|
22
|
+
Config.set(*password_keys, password)
|
23
|
+
puts
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,328 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'cgi'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module Fuel
|
7
|
+
module CLI
|
8
|
+
class Gerrit < Base
|
9
|
+
include GerritCommon
|
10
|
+
|
11
|
+
package_name "gerrit"
|
12
|
+
|
13
|
+
headers "Content-Type" => "application/json"
|
14
|
+
digest_auth Config.get('gerrit', 'user'), Config.get('gerrit', 'password')
|
15
|
+
parser GerritJsonParser
|
16
|
+
|
17
|
+
desc "status", "Show a brief overview of the current gerrit status"
|
18
|
+
def status
|
19
|
+
change_id = 'Ia8b172edcedcef7856b4d5fa7aee9e58649abe75'#fetch_change_id_or_fail
|
20
|
+
|
21
|
+
result = self.class.get(changes_endpoint(change_id, "?o=CURRENT_REVISION")).parsed_response
|
22
|
+
current_rev = result['current_revision']
|
23
|
+
|
24
|
+
say result["subject"], :bold
|
25
|
+
table = [["Owner", result["owner"]["name"]]]
|
26
|
+
table << ["Updated", format_time(result["updated"])]
|
27
|
+
table << ["Status", result["status"]]
|
28
|
+
table << ["Ref", gerrit_ref(result)]
|
29
|
+
print_table table
|
30
|
+
|
31
|
+
result = fetch_comments(change_id, current_rev)
|
32
|
+
print_comments_info(result)
|
33
|
+
puts
|
34
|
+
result = self.class.get(reviewers_endpoint(change_id)).parsed_response
|
35
|
+
table = result.map { |row| format_row(row) }
|
36
|
+
table.unshift(["Reviewer", "CR", "QA", "V"])
|
37
|
+
print_table table
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "open", "Open Gerrit in your browser"
|
41
|
+
def open
|
42
|
+
`open #{gerrit_change_url}`
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "review-add USER[, USER, ...]", "Add reviewer(s) to the gerrit"
|
46
|
+
def review_add(*emails_or_aliases)
|
47
|
+
change_id = fetch_change_id_or_fail
|
48
|
+
emails_or_aliases.each do |value|
|
49
|
+
emails = Array(to_email(value))
|
50
|
+
if emails.empty?
|
51
|
+
say "No email found for alias #{value}; reviewer not added", :yellow
|
52
|
+
next
|
53
|
+
end
|
54
|
+
emails.each do |email|
|
55
|
+
result = self.class.post(reviewers_endpoint(change_id), body: { reviewer: email }.to_json).parsed_response
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
desc "alias EMAIL [ALIAS]", "Add an alias or show all existing aliases for the given email"
|
61
|
+
def alias(email, alias_name = nil)
|
62
|
+
if alias_name
|
63
|
+
Config.set('gerrit', 'aliases', alias_name, email)
|
64
|
+
else
|
65
|
+
aliases = Config.get('gerrit-aliases') || {}
|
66
|
+
aliases_found = aliases.map { |k, v| v == email ? k : nil }.compact
|
67
|
+
if aliases_found.empty?
|
68
|
+
say "No aliases created for #{email}", :yellow
|
69
|
+
else
|
70
|
+
say "Aliases for #{email}: #{aliases_found.join("; ")}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
desc "auth USER", "Store authentication information for Gerrit"
|
76
|
+
def auth(user)
|
77
|
+
set_credentials(user, ['gerrit', 'user'], ['gerrit', 'password'])
|
78
|
+
end
|
79
|
+
|
80
|
+
desc "submit", "Submit the current change"
|
81
|
+
def submit
|
82
|
+
change_id = fetch_change_id_or_fail
|
83
|
+
result = self.class.post(changes_endpoint(change_id, 'submit'), body: { wait_for_merge: true }.to_json)
|
84
|
+
puts result.code
|
85
|
+
puts result.parsed_response.inspect
|
86
|
+
end
|
87
|
+
|
88
|
+
option :message, :aliases => '-m'
|
89
|
+
desc "abandon", "Abandon the current change"
|
90
|
+
def abandon
|
91
|
+
change_id = fetch_change_id_or_fail
|
92
|
+
params = options['message'] ? { body: { message: options['message'] }.to_json } : {}
|
93
|
+
result = self.class.post(changes_endpoint(change_id, 'abandon'), params).parsed_response
|
94
|
+
puts result.inspect
|
95
|
+
end
|
96
|
+
|
97
|
+
option :message, :aliases => '-m'
|
98
|
+
desc "restore", "Restore the current change"
|
99
|
+
def restore
|
100
|
+
change_id = fetch_change_id_or_fail
|
101
|
+
params = options['message'] ? { body: { message: options['message'] }.to_json } : {}
|
102
|
+
result = self.class.post(changes_endpoint(change_id, 'restore'), params).parsed_response
|
103
|
+
puts result.inspect
|
104
|
+
end
|
105
|
+
|
106
|
+
desc "rebase", "Rebase the current change"
|
107
|
+
def rebase
|
108
|
+
change_id = fetch_change_id_or_fail
|
109
|
+
result = self.class.post(changes_endpoint(change_id, "rebase"))
|
110
|
+
if result.response.code == "409"
|
111
|
+
say "Rebase failed, conflicts found", :red
|
112
|
+
else
|
113
|
+
say "Rebased successfully", :green
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
desc "checkout USER", "Pulls the latest patchset by the given user for local QA"
|
118
|
+
def checkout(email_or_alias)
|
119
|
+
email = to_email(email_or_alias)
|
120
|
+
say "Group aliases are not supported", :red and return if email.is_a?(Array)
|
121
|
+
result = self.class.get(changes_endpoint(nil, "?q=status:open+owner:#{email}&n=1&o=CURRENT_REVISION&o=DOWNLOAD_COMMANDS")).parsed_response
|
122
|
+
say "No open changes found for #{email_or_alias}", :yellow and return if result.empty?
|
123
|
+
current_rev = result[0]['current_revision']
|
124
|
+
command = result[0]['revisions'][current_rev]['fetch']['ssh']['commands']['Checkout']
|
125
|
+
`#{command}`
|
126
|
+
end
|
127
|
+
|
128
|
+
option :message, :aliases => '-m'
|
129
|
+
desc "qa VOTE", "Put a QA mark on the patch currently checked out"
|
130
|
+
def qa(vote)
|
131
|
+
submit_review('QA-Test-Passed', vote)
|
132
|
+
end
|
133
|
+
|
134
|
+
option :message, :aliases => '-m'
|
135
|
+
desc "cr VOTE", "Put a Code Review mark on the patch currently checked out"
|
136
|
+
def cr(vote)
|
137
|
+
submit_review('Code-Review', vote)
|
138
|
+
end
|
139
|
+
|
140
|
+
desc "comments FILE", "View the file with comments"
|
141
|
+
def comments(file)
|
142
|
+
comments, lines = comments_for_file(file)
|
143
|
+
extra_lines = 0
|
144
|
+
comments.each do |c|
|
145
|
+
line = c['line'] + extra_lines
|
146
|
+
comment_lines = format_comment(c)
|
147
|
+
lines.insert(line, *comment_lines)
|
148
|
+
extra_lines += comment_lines.size
|
149
|
+
end
|
150
|
+
exec("printf #{lines.join('\n').inspect} | less -F -X -R")
|
151
|
+
end
|
152
|
+
|
153
|
+
desc "respond FILE", "Look through the comments and respond to them"
|
154
|
+
def respond(file)
|
155
|
+
comments, lines, change_id, revision_id = comments_for_file(file)
|
156
|
+
commented_on = {}
|
157
|
+
comments_for_lines = comments.inject({}) do |h, c|
|
158
|
+
h[c['line']] ||= []
|
159
|
+
h[c['line']] << c
|
160
|
+
h
|
161
|
+
end
|
162
|
+
comments_to_post = comments_for_lines.map do |line, cc|
|
163
|
+
next if cc.all? { |c| c['author']['username'] != gerrit_user }
|
164
|
+
system 'tput smcup'
|
165
|
+
|
166
|
+
line_in_array = line - 1
|
167
|
+
first_line = [0, line_in_array - 4].max
|
168
|
+
if first_line == line_in_array
|
169
|
+
say "Line #{line}:", :bold
|
170
|
+
else
|
171
|
+
say "Lines #{first_line + 1}-#{line}:", :bold
|
172
|
+
end
|
173
|
+
say lines[first_line..line_in_array].join("\n")
|
174
|
+
cc.each do |c|
|
175
|
+
say format_comment(c).join("\n")
|
176
|
+
end
|
177
|
+
reply = ask "Reply on this line (press enter to skip):"
|
178
|
+
system 'tput rmcup'
|
179
|
+
next if reply.strip.empty?
|
180
|
+
{ line: line, message: reply }
|
181
|
+
end.compact
|
182
|
+
say 'No comments to respond to!', :green and return if comments_to_post.empty?
|
183
|
+
puts comments_to_post.inspect
|
184
|
+
result = self.class.post(revisions_endpoint(change_id, revision_id, 'review'), body: { comments: { file => comments_to_post } }.to_json).parsed_response
|
185
|
+
puts result.inspect
|
186
|
+
end
|
187
|
+
|
188
|
+
no_commands do
|
189
|
+
def gerrit_change_url
|
190
|
+
change_id = fetch_change_id_or_fail
|
191
|
+
result = self.class.get(changes_endpoint(change_id)).parsed_response
|
192
|
+
"#{gerrit_url}/#{result['_number']}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
def format_comment(c)
|
199
|
+
msg = c['message'].gsub("\n\n", "\n")
|
200
|
+
date = format_time(c['updated'])
|
201
|
+
author = set_color(c['author']['name'], :yellow)
|
202
|
+
prefix = "[#{date}] #{author}: "
|
203
|
+
comment_lines = msg.split("\n")
|
204
|
+
comment_lines[0].prepend prefix
|
205
|
+
comment_lines[1..-1].each do |cline|
|
206
|
+
cline.prepend " " * prefix.size
|
207
|
+
end
|
208
|
+
comment_lines
|
209
|
+
end
|
210
|
+
|
211
|
+
def submit_review(label, vote)
|
212
|
+
change_id = fetch_change_id_or_fail
|
213
|
+
result = self.class.get(changes_endpoint(change_id, "?o=CURRENT_REVISION")).parsed_response
|
214
|
+
current_rev = result['current_revision']
|
215
|
+
body = { labels: { label => vote.to_i } }
|
216
|
+
message = options['message'] || run_editor.gsub("\n", "\n\n")
|
217
|
+
|
218
|
+
body[:message] = message
|
219
|
+
result = self.class.post(revisions_endpoint(change_id, current_rev, 'review'), body: body.to_json).parsed_response
|
220
|
+
end
|
221
|
+
|
222
|
+
def to_email(value)
|
223
|
+
is_email?(value) ? value : emails_for_alias(value)
|
224
|
+
end
|
225
|
+
|
226
|
+
def is_email?(value)
|
227
|
+
!!value.index('@')
|
228
|
+
end
|
229
|
+
|
230
|
+
def emails_for_alias(alias_name)
|
231
|
+
aliases = Config.get('gerrit-aliases', alias_name)
|
232
|
+
end
|
233
|
+
|
234
|
+
def format_row(row)
|
235
|
+
result = [row["name"]]
|
236
|
+
approvals = row["approvals"]
|
237
|
+
result << format_review(approvals && approvals["Code-Review"])
|
238
|
+
result << format_verified(approvals && approvals["QA-Test-Passed"])
|
239
|
+
result << format_verified(approvals && approvals["Verified"])
|
240
|
+
result
|
241
|
+
end
|
242
|
+
|
243
|
+
def format_review(cell)
|
244
|
+
characters = { "0" => " ", "-1" => set_color("-1", :red), "+1" => set_color("+1", :green), "+2" => tick }
|
245
|
+
characters[(cell || "").strip]
|
246
|
+
end
|
247
|
+
|
248
|
+
def format_verified(cell)
|
249
|
+
characters = { "0" => " ", "-1" => cross, "+1" => tick }
|
250
|
+
characters[(cell || "").strip]
|
251
|
+
end
|
252
|
+
|
253
|
+
def format_time(time)
|
254
|
+
Time.parse(time + " UTC").getlocal.strftime("%b %-d %l:%M %p")
|
255
|
+
end
|
256
|
+
|
257
|
+
def tick
|
258
|
+
set_color([10003].pack("U*"), :green)
|
259
|
+
end
|
260
|
+
|
261
|
+
def cross
|
262
|
+
set_color([10008].pack("U*"), :red)
|
263
|
+
end
|
264
|
+
|
265
|
+
def revisions_endpoint(change_id, revision_id = nil, path = nil)
|
266
|
+
url = "#{changes_endpoint(change_id)}revisions/"
|
267
|
+
url += "#{revision_id}/" if revision_id
|
268
|
+
url += path if path
|
269
|
+
url
|
270
|
+
end
|
271
|
+
|
272
|
+
def reviewers_endpoint(change_id)
|
273
|
+
changes_endpoint(change_id, "reviewers")
|
274
|
+
end
|
275
|
+
|
276
|
+
def run_editor
|
277
|
+
path = File.expand_path('~/.fuel/MSG')
|
278
|
+
FileUtils.touch(path)
|
279
|
+
system((ENV['EDITOR'] || 'vi') + ' ' + path)
|
280
|
+
content = File.read(path)
|
281
|
+
FileUtils.rm(path)
|
282
|
+
content
|
283
|
+
end
|
284
|
+
|
285
|
+
def fetch_comments(change_id, revision_id)
|
286
|
+
return self.class.get(revisions_endpoint(change_id, revision_id, 'comments')).parsed_response
|
287
|
+
end
|
288
|
+
|
289
|
+
def comments_for_file(file)
|
290
|
+
change_id = fetch_change_id_or_fail
|
291
|
+
result = self.class.get(changes_endpoint(change_id, "detail?o=CURRENT_REVISION&o=CURRENT_FILES")).parsed_response
|
292
|
+
current_rev = result['current_revision']
|
293
|
+
|
294
|
+
content = self.class.get(revisions_endpoint(change_id, current_rev, "files/#{CGI::escape(file)}/content")).parsed_response
|
295
|
+
content = Base64::decode64(content)
|
296
|
+
lines = content.split("\n")
|
297
|
+
comments = fetch_comments(change_id, current_rev)[file]
|
298
|
+
comments.sort_by! { |c| [c['line'], Time.parse(c['updated'])] }
|
299
|
+
[comments, lines, change_id, current_rev]
|
300
|
+
end
|
301
|
+
|
302
|
+
def gerrit_user
|
303
|
+
Config.get('gerrit', 'user')
|
304
|
+
end
|
305
|
+
|
306
|
+
def print_comments_info(data)
|
307
|
+
table = data.inject([]) do |memo, (file, comments)|
|
308
|
+
memo << [file, set_color(plural(comments.count, 'comment'), :yellow)] if comments.count > 0
|
309
|
+
memo
|
310
|
+
end
|
311
|
+
unless table.empty?
|
312
|
+
puts
|
313
|
+
say "Files with comments:"
|
314
|
+
say " (use \"gerrit comments FILE\" to see the comments)"
|
315
|
+
print_table table, :indent => 8
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def plural(n, singular)
|
320
|
+
if n == 1
|
321
|
+
"1 #{singular}"
|
322
|
+
else
|
323
|
+
"#{n} #{singular}s"
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Fuel
|
2
|
+
module CLI
|
3
|
+
module GerritCommon
|
4
|
+
def gerrit_url
|
5
|
+
Fuel::Util::Config.get('gerrit', 'url').chomp('/')
|
6
|
+
end
|
7
|
+
|
8
|
+
def gerrit_ref(result)
|
9
|
+
current_rev = result['current_revision']
|
10
|
+
result['revisions'][current_rev]['fetch']['ssh']['ref']
|
11
|
+
end
|
12
|
+
|
13
|
+
def changes_endpoint(change_id = nil, path = nil)
|
14
|
+
url = "#{gerrit_url}/a/changes/"
|
15
|
+
url += "#{change_id}/" if change_id
|
16
|
+
url += path if path
|
17
|
+
url
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Fuel
|
2
|
+
module CLI
|
3
|
+
class Jenkins < Base
|
4
|
+
include GerritCommon
|
5
|
+
|
6
|
+
headers "Content-Type" => "application/json"
|
7
|
+
digest_auth Config.get('gerrit', 'user'), Config.get('gerrit', 'password')
|
8
|
+
parser GerritJsonParser
|
9
|
+
|
10
|
+
package_name "jenkins"
|
11
|
+
|
12
|
+
desc "open", "Open the most recent Jenkins build for the current change"
|
13
|
+
def open
|
14
|
+
build_link = build_link_or_fail
|
15
|
+
|
16
|
+
`open #{build_link}`
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "deploy", "Deploy the latest patchset to your QA server"
|
20
|
+
def deploy
|
21
|
+
change_id = 'Ia8b172edcedcef7856b4d5fa7aee9e58649abe75'#fetch_change_id_or_fail
|
22
|
+
result = self.class.get(changes_endpoint(change_id, "?o=CURRENT_REVISION")).parsed_response
|
23
|
+
current_rev = result['current_revision']
|
24
|
+
|
25
|
+
client = create_client
|
26
|
+
ref = gerrit_ref(result).sub('refs/', '')
|
27
|
+
client.job.build("ui.fuel-deploy.gerrit", { "Environment" => "qa", "GerritRef" => ref })
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "status", "View the status of the current build"
|
31
|
+
def status
|
32
|
+
details = build_details
|
33
|
+
result = details['result']
|
34
|
+
say 'Build is still running', :yellow and return unless result
|
35
|
+
|
36
|
+
color = case result
|
37
|
+
when "FAILURE", "ABORTED"
|
38
|
+
:red
|
39
|
+
when "SUCCESS"
|
40
|
+
:green
|
41
|
+
else
|
42
|
+
:default
|
43
|
+
end
|
44
|
+
|
45
|
+
say result, color
|
46
|
+
say 'Build artifacts are available via "jenkins artifacts"'
|
47
|
+
end
|
48
|
+
|
49
|
+
desc "artifacts", "Open build artifacts in the browser"
|
50
|
+
def artifacts
|
51
|
+
build_link = build_link_or_fail
|
52
|
+
|
53
|
+
details = build_details
|
54
|
+
say 'No artifacts available, build might be still running', :yellow and return if details['artifacts'].empty?
|
55
|
+
|
56
|
+
`open #{build_link}artifact/`
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "auth USER", "Store authentication information for Jenkins"
|
60
|
+
def auth(user)
|
61
|
+
set_credentials(user, ['jenkins', :username], ['jenkins', :password])
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def build_details
|
67
|
+
build_link = build_link_or_fail
|
68
|
+
build_number = build_link =~ /\/(\d+)\// && $1
|
69
|
+
client = create_client
|
70
|
+
|
71
|
+
client.job.get_build_details("ui.fuel-gerrit", build_number)
|
72
|
+
end
|
73
|
+
|
74
|
+
def create_client
|
75
|
+
@client ||= JenkinsApi::Client.new(Config.get('jenkins'))
|
76
|
+
end
|
77
|
+
|
78
|
+
def build_link_or_fail
|
79
|
+
change_id = 'I89fe9b251904fb929adf61605438fcc6fdbf534d'#fetch_change_id_or_fail
|
80
|
+
result = self.class.get(changes_endpoint(change_id, "detail")).parsed_response
|
81
|
+
build_message = result['messages'].reverse.find { |m| m['author']['username'] == 'hbuilduser' }
|
82
|
+
build_message &&= build_message['message']
|
83
|
+
build_link = build_message && URI.extract(build_message)[0]
|
84
|
+
raise Thor::Error, set_color('No Jenkins build found for this change', :red) unless build_link
|
85
|
+
build_link
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Fuel
|
2
|
+
module CLI
|
3
|
+
class Jira < Base
|
4
|
+
package_name "jira"
|
5
|
+
|
6
|
+
headers "Content-Type" => "application/json"
|
7
|
+
basic_auth Config.get('jira', 'user'), Config.get('jira', 'password')
|
8
|
+
|
9
|
+
desc "status [JIRA]", "View a brief summary of the jira"
|
10
|
+
def status(jira = nil)
|
11
|
+
jira_id = jira || fetch_jira_or_fail
|
12
|
+
result = self.class.get(issue_endpoint(jira_id)).parsed_response
|
13
|
+
fields = result["fields"]
|
14
|
+
say "#{jira_id}: #{fields['summary']}", :bold
|
15
|
+
table = [['Type', fields['issuetype']['name']]]
|
16
|
+
table << ['Status', fields['status']['name']]
|
17
|
+
table << ['Assignee', fields['assignee']['displayName']]
|
18
|
+
print_table table
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "start [JIRA]", "Start progress on the jira"
|
22
|
+
def start(jira = nil)
|
23
|
+
transition('start progress', jira)
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "resolve", "Resolve the current jira"
|
27
|
+
def resolve
|
28
|
+
transition('resolve issue')
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "submit", "Put the current jira in review"
|
32
|
+
def submit
|
33
|
+
transition('submit', nil, fields: { get_field_id(fetch_jira_or_fail, 'review id') => Gerrit.new.gerrit_change_url })
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "auth USER", "Store authentication information for JIRA"
|
37
|
+
def auth(user)
|
38
|
+
set_credentials(user, ['jira', 'user'], ['jira', 'password'])
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def transition(name, jira = nil, extra_body = {})
|
44
|
+
jira_id = jira || fetch_jira_or_fail
|
45
|
+
body = { transition: { id: get_transition_id(jira_id, name) } }.merge(extra_body)
|
46
|
+
result = self.class.post(issue_endpoint(jira_id, "transitions"),
|
47
|
+
body: body.to_json).parsed_response
|
48
|
+
puts result.inspect
|
49
|
+
end
|
50
|
+
|
51
|
+
def jira_url
|
52
|
+
Config.get('jira', 'url').chomp('/')
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_field_id(jira_id, name)
|
56
|
+
result = self.class.get(jira_endpoint("field")).parsed_response
|
57
|
+
field = result.find { |r| r['name'].downcase == name.downcase }
|
58
|
+
field && field['id']
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_transition_id(jira_id, name)
|
62
|
+
get_allowed_transitions(jira_id).each do |transition|
|
63
|
+
return transition["id"] if transition["name"].downcase == name.downcase
|
64
|
+
end
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_allowed_transitions(jira_id)
|
69
|
+
result = self.class.get(issue_endpoint(jira_id, "transitions")).parsed_response
|
70
|
+
result["transitions"]
|
71
|
+
end
|
72
|
+
|
73
|
+
def fetch_jira_or_fail
|
74
|
+
commit_message.split("\n")[2] or raise Thor::Error, set_color("No JIRA found at HEAD", :red)
|
75
|
+
end
|
76
|
+
|
77
|
+
def jira_endpoint(path = "")
|
78
|
+
"#{jira_url}/rest/api/2/#{path}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def issue_endpoint(id, path = "")
|
82
|
+
jira_endpoint("issue/#{id}/#{path}")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Fuel
|
2
|
+
module CLI
|
3
|
+
class Main < Thor
|
4
|
+
include Fuel::Util
|
5
|
+
|
6
|
+
package_name "fuel"
|
7
|
+
|
8
|
+
desc "setup", "Set up the CLI interactively"
|
9
|
+
def setup
|
10
|
+
say flame, :cyan
|
11
|
+
say "\nWelcome to Fuel-CLI!\n\n", :bold
|
12
|
+
|
13
|
+
setup_gerrit
|
14
|
+
setup_jira
|
15
|
+
setup_jenkins
|
16
|
+
|
17
|
+
say "You are good to go!", [:green, :bold]
|
18
|
+
say 'You have "fuel", "gerrit", "jira" and "jenkins" executables in your PATH.', :green
|
19
|
+
say 'Use the "help" command (e.g. "gerrit help") to get started.', :green
|
20
|
+
end
|
21
|
+
|
22
|
+
option :gerrit_url
|
23
|
+
option :jira_url
|
24
|
+
option :jenkins_url
|
25
|
+
desc "config", "Set configuration values"
|
26
|
+
def config
|
27
|
+
raise Thor::Error, set_color("Provide at least one key-value pair!", :red) if options.empty?
|
28
|
+
if options.has_key?('gerrit_url')
|
29
|
+
Config.set('gerrit', 'url', options['gerrit_url'])
|
30
|
+
end
|
31
|
+
if options.has_key?('jira_url')
|
32
|
+
Config.set('jira', 'url', options['jira_url'])
|
33
|
+
end
|
34
|
+
if options.has_key?('jenkins_url')
|
35
|
+
Config.set('jenkins', :server_url, options['jenkins_url'])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def setup_gerrit
|
42
|
+
say "[Setting up gerrit]", :yellow
|
43
|
+
gerrit_url = ask "URL:"
|
44
|
+
self.class.new.invoke :config, [], gerrit_url: gerrit_url
|
45
|
+
gerrit_user = ask "User name:"
|
46
|
+
say "Please go to your Gerrit user settings now, and generate an HTTP password there."
|
47
|
+
Gerrit.new.invoke :auth, [gerrit_user]
|
48
|
+
|
49
|
+
say "gerrit configured successfully!\n\n", :green
|
50
|
+
end
|
51
|
+
|
52
|
+
def setup_jira
|
53
|
+
say "[Setting up jira]", :yellow
|
54
|
+
jira_url = ask "URL:"
|
55
|
+
self.class.new.invoke :config, [], jira_url: jira_url
|
56
|
+
jira_user = ask "User name:"
|
57
|
+
Jira.new.invoke :auth, [jira_user]
|
58
|
+
|
59
|
+
say "jira configured successfully!\n\n", :green
|
60
|
+
end
|
61
|
+
|
62
|
+
def setup_jenkins
|
63
|
+
say "[Setting up jenkins]", :yellow
|
64
|
+
jenkins_url = ask "URL:"
|
65
|
+
self.class.new.invoke :config, [], jenkins_url: jenkins_url
|
66
|
+
jenkins_user = ask "User name:"
|
67
|
+
Jenkins.new.invoke :auth, [jenkins_user]
|
68
|
+
|
69
|
+
say "jenkins configured successfully!\n\n", :green
|
70
|
+
end
|
71
|
+
|
72
|
+
def flame
|
73
|
+
<<-EOT
|
74
|
+
`.--..`
|
75
|
+
`-/+syyso/. .sdddddddhs+:.
|
76
|
+
-+ydddddddddddh/` .dddh:.`.:+sdddho:`
|
77
|
+
:yhsso+oshddddddddds/yddy` `+ddddo`
|
78
|
+
`. ``` .+ddds/-:/+o+: `-+ydds/.
|
79
|
+
.:odddh+ `sdddho:` .:+ydds/.
|
80
|
+
`:ohdddddddddy` /ddddddhhhddhs/.
|
81
|
+
`./oyhddhs:` .ohhdhys+:`
|
82
|
+
EOT
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/fuel/cli.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'jenkins_api_client'
|
3
|
+
|
4
|
+
require 'fuel/cli/version'
|
5
|
+
require 'fuel/util/config'
|
6
|
+
require 'fuel/util/json_parser'
|
7
|
+
require 'fuel/cli/gerrit_common'
|
8
|
+
require 'fuel/cli/patches'
|
9
|
+
require 'fuel/cli/base'
|
10
|
+
require 'fuel/cli/gerrit'
|
11
|
+
require 'fuel/cli/jira'
|
12
|
+
require 'fuel/cli/jenkins'
|
13
|
+
require 'fuel/cli/main'
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Fuel
|
5
|
+
module Util
|
6
|
+
class Config
|
7
|
+
CONFIG_PATH = File.expand_path("~/.fuel")
|
8
|
+
CONFIG_FILENAME = "#{CONFIG_PATH}/cliconfig"
|
9
|
+
|
10
|
+
def self.set(*keys, value)
|
11
|
+
ensure_config_exists
|
12
|
+
|
13
|
+
config = load
|
14
|
+
key = keys.pop
|
15
|
+
h = keys.inject(config) { |memo, k| memo[k] ||= {}; memo[k] }
|
16
|
+
h[key] = value
|
17
|
+
save(config)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.get(*keys)
|
21
|
+
ensure_config_exists
|
22
|
+
|
23
|
+
config = load
|
24
|
+
keys.inject(config, :fetch)
|
25
|
+
rescue KeyError
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.add(key, value)
|
30
|
+
ensure_config_exists
|
31
|
+
|
32
|
+
config = load
|
33
|
+
config[key] ||= []
|
34
|
+
config[key] << value
|
35
|
+
save(config)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def self.load
|
41
|
+
YAML.load_file(CONFIG_FILENAME) || {}
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.save(config)
|
45
|
+
File.open(CONFIG_FILENAME, "w") { |f| f.write(YAML.dump(config)) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.ensure_config_exists
|
49
|
+
FileUtils.mkdir_p(CONFIG_PATH)
|
50
|
+
FileUtils.touch(CONFIG_FILENAME)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
module Fuel
|
4
|
+
module Util
|
5
|
+
class GerritJsonParser < HTTParty::Parser
|
6
|
+
XSSI_PREFIX = ")]}'"
|
7
|
+
SupportedFormats.merge!("application/json" => :json, "text/json" => :json)
|
8
|
+
|
9
|
+
def json
|
10
|
+
body.sub!(XSSI_PREFIX, "") if body.start_with?(XSSI_PREFIX)
|
11
|
+
JSON.load(body, nil)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fuel-cli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex Zherdev
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: httparty
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: jenkins_api_client
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.6'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.6'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: debugger
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: ''
|
98
|
+
email:
|
99
|
+
- azherdev@rocketfuel.com
|
100
|
+
executables:
|
101
|
+
- fuel
|
102
|
+
- gerrit
|
103
|
+
- jenkins
|
104
|
+
- jira
|
105
|
+
extensions: []
|
106
|
+
extra_rdoc_files: []
|
107
|
+
files:
|
108
|
+
- .gitignore
|
109
|
+
- Gemfile
|
110
|
+
- LICENSE.txt
|
111
|
+
- README.md
|
112
|
+
- Rakefile
|
113
|
+
- bin/fuel
|
114
|
+
- bin/gerrit
|
115
|
+
- bin/jenkins
|
116
|
+
- bin/jira
|
117
|
+
- fuel-cli.gemspec
|
118
|
+
- lib/fuel/cli.rb
|
119
|
+
- lib/fuel/cli/base.rb
|
120
|
+
- lib/fuel/cli/gerrit.rb
|
121
|
+
- lib/fuel/cli/gerrit_common.rb
|
122
|
+
- lib/fuel/cli/jenkins.rb
|
123
|
+
- lib/fuel/cli/jira.rb
|
124
|
+
- lib/fuel/cli/main.rb
|
125
|
+
- lib/fuel/cli/patches.rb
|
126
|
+
- lib/fuel/cli/version.rb
|
127
|
+
- lib/fuel/util/config.rb
|
128
|
+
- lib/fuel/util/json_parser.rb
|
129
|
+
homepage: ''
|
130
|
+
licenses:
|
131
|
+
- MIT
|
132
|
+
metadata: {}
|
133
|
+
post_install_message: ! "\e[36m\e[1mWelcome, Fuel developer! Type \"fuel setup\" to
|
134
|
+
get started.\e[0m"
|
135
|
+
rdoc_options: []
|
136
|
+
require_paths:
|
137
|
+
- lib
|
138
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - ! '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
|
+
requirements:
|
145
|
+
- - ! '>='
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
148
|
+
requirements: []
|
149
|
+
rubyforge_project:
|
150
|
+
rubygems_version: 2.3.0
|
151
|
+
signing_key:
|
152
|
+
specification_version: 4
|
153
|
+
summary: Simple set of command-line tools to aid in Fuel dev process.
|
154
|
+
test_files: []
|