terjira 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +13 -0
- data/.gitignore +52 -0
- data/.rspec +2 -0
- data/.rubocop.yml +26 -0
- data/.travis.yml +20 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +104 -0
- data/LICENSE.txt +21 -0
- data/README.md +47 -0
- data/Rakefile +10 -0
- data/Vagrantfile +26 -0
- data/bin/console +14 -0
- data/bin/jira +13 -0
- data/bin/setup +8 -0
- data/lib/terjira.rb +38 -0
- data/lib/terjira/base_cli.rb +36 -0
- data/lib/terjira/board_cli.rb +12 -0
- data/lib/terjira/client/agile.rb +41 -0
- data/lib/terjira/client/auth_option_builder.rb +42 -0
- data/lib/terjira/client/base.rb +72 -0
- data/lib/terjira/client/board.rb +20 -0
- data/lib/terjira/client/field.rb +17 -0
- data/lib/terjira/client/issue.rb +110 -0
- data/lib/terjira/client/jql_query_builer.rb +25 -0
- data/lib/terjira/client/priority.rb +14 -0
- data/lib/terjira/client/project.rb +25 -0
- data/lib/terjira/client/rapid_view.rb +8 -0
- data/lib/terjira/client/resolution.rb +14 -0
- data/lib/terjira/client/sprint.rb +28 -0
- data/lib/terjira/client/status.rb +15 -0
- data/lib/terjira/client/user.rb +44 -0
- data/lib/terjira/ext/jira_ruby.rb +70 -0
- data/lib/terjira/ext/tty_prompt.rb +34 -0
- data/lib/terjira/issue_cli.rb +110 -0
- data/lib/terjira/option_support/option_selector.rb +167 -0
- data/lib/terjira/option_support/resource_store.rb +45 -0
- data/lib/terjira/option_support/shared_options.rb +70 -0
- data/lib/terjira/option_supportable.rb +80 -0
- data/lib/terjira/presenters/board_presenter.rb +20 -0
- data/lib/terjira/presenters/common_presenter.rb +53 -0
- data/lib/terjira/presenters/issue_presenter.rb +175 -0
- data/lib/terjira/presenters/project_presenter.rb +69 -0
- data/lib/terjira/presenters/sprint_presenter.rb +68 -0
- data/lib/terjira/project_cli.rb +25 -0
- data/lib/terjira/rapidview_cli.rb +6 -0
- data/lib/terjira/sprint_cli.rb +58 -0
- data/lib/terjira/utils/file_cache.rb +110 -0
- data/lib/terjira/version.rb +3 -0
- data/terjira.gemspec +38 -0
- metadata +282 -0
data/lib/terjira.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'pry'
|
2
|
+
require_relative 'terjira/ext/jira_ruby'
|
3
|
+
require_relative 'terjira/ext/tty_prompt'
|
4
|
+
require 'terjira/version'
|
5
|
+
require 'thor'
|
6
|
+
|
7
|
+
Dir[File.dirname(__FILE__) + '/terjira/*_cli.rb'].each { |f| require f }
|
8
|
+
|
9
|
+
ENV['PAGER'] ||= 'less'
|
10
|
+
|
11
|
+
# http://willschenk.com/making-a-command-line-utility-with-gems-and-thor/
|
12
|
+
module Terjira
|
13
|
+
# Main CLI
|
14
|
+
class CLI < Thor
|
15
|
+
desc 'login', 'login your Jira'
|
16
|
+
def login
|
17
|
+
Client::Base.expire_auth_options
|
18
|
+
Client::Base.build_auth_options
|
19
|
+
end
|
20
|
+
|
21
|
+
desc 'logout', 'logout your Jira'
|
22
|
+
def logout
|
23
|
+
Client::Base.expire_auth_options
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'project SUBCOMMAND ...ARGS', 'Manage proejcts'
|
27
|
+
subcommand 'project', ProjectCLI
|
28
|
+
|
29
|
+
desc 'board SUBCOMMAND ...ARGS', 'Manage boards'
|
30
|
+
subcommand 'board', BoardCLI
|
31
|
+
|
32
|
+
desc 'sprint SUBCOMMAND ...ARGS', 'Manage sprints'
|
33
|
+
subcommand 'sprint', SprintCLI
|
34
|
+
|
35
|
+
desc 'issue SUBCOMMAND ...ARGS', 'Manage issues'
|
36
|
+
subcommand 'issue', IssueCLI
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
require_relative 'option_supportable'
|
4
|
+
Dir[File.dirname(__FILE__) + "/presenters/*.rb"].each { |f| require f }
|
5
|
+
|
6
|
+
module Terjira
|
7
|
+
module Client
|
8
|
+
%w[Base Field Project Board Sprint Issue User Status Resolution Priority RapidView Agile].each do |klass|
|
9
|
+
autoload klass, "terjira/client/#{klass.gsub(/(.)([A-Z](?=[a-z]))/,'\1_\2').downcase}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class BaseCLI < Thor
|
14
|
+
include OptionSupportable
|
15
|
+
|
16
|
+
include CommonPresenter
|
17
|
+
include IssuePresenter
|
18
|
+
include ProjectPresenter
|
19
|
+
include BoardPresenter
|
20
|
+
include SprintPresenter
|
21
|
+
|
22
|
+
def self.banner(command, namespace = nil, subcommand = false)
|
23
|
+
"#{basename} #{subcommand_prefix} #{command.usage}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.subcommand_prefix
|
27
|
+
self.name.gsub(%r{.*::}, '').gsub("CLI", '').gsub(%r{^[A-Z]}) { |match| match[0].downcase }.gsub(%r{[A-Z]}) { |match| "-#{match[0].downcase}" }
|
28
|
+
end
|
29
|
+
|
30
|
+
no_commands do
|
31
|
+
def current_username
|
32
|
+
@current_username ||= Client::Base.username
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Terjira
|
4
|
+
module Client
|
5
|
+
class Agile < Base
|
6
|
+
class << self
|
7
|
+
delegate :all, :get_sprints, :backlog_issues, to: :resource
|
8
|
+
|
9
|
+
def project_by_board(board_id)
|
10
|
+
resp = agile_api_get("board/#{board_id}/project")
|
11
|
+
end
|
12
|
+
|
13
|
+
def boards
|
14
|
+
all["values"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def sprints(board_id, options = {})
|
18
|
+
sprints = get_sprints(board_id)["values"]
|
19
|
+
sprints.sort_by do |sprint|
|
20
|
+
if sprint["state"] == 'active'
|
21
|
+
[0, sprint["id"]]
|
22
|
+
elsif sprint["state"] == 'future'
|
23
|
+
[1, sprint["id"]]
|
24
|
+
elsif sprint["state"] == 'closed'
|
25
|
+
[2, sprint["id"] * -1]
|
26
|
+
else
|
27
|
+
[3, 0]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def backlog_issues(board_id)
|
33
|
+
get_backlog_issues(board_id)
|
34
|
+
end
|
35
|
+
|
36
|
+
def sprint_issues(board_id)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'tty-prompt'
|
2
|
+
require 'terjira/utils/file_cache'
|
3
|
+
|
4
|
+
module Terjira
|
5
|
+
module Client
|
6
|
+
module AuthOptionBuilder
|
7
|
+
AUTH_CACHE_KEY = 'auth'.freeze
|
8
|
+
|
9
|
+
def build_auth_options(cache_key = AUTH_CACHE_KEY)
|
10
|
+
auth_file_cache.fetch cache_key do
|
11
|
+
build_auth_options_by_tty
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def build_auth_options_by_cached(cache_key = AUTH_CACHE_KEY)
|
16
|
+
auth_file_cache.get(cache_key)
|
17
|
+
end
|
18
|
+
|
19
|
+
def expire_auth_options(cache_key = AUTH_CACHE_KEY)
|
20
|
+
auth_file_cache.delete(cache_key)
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_auth_options_by_tty
|
24
|
+
puts 'Login will be required...'
|
25
|
+
prompt = TTY::Prompt.new
|
26
|
+
|
27
|
+
result = prompt.collect do
|
28
|
+
key(:site).ask('Site:', required: true)
|
29
|
+
key(:context_path).ask('Context path:', default: '')
|
30
|
+
key(:username).ask('Username:', required: true)
|
31
|
+
key(:password).mask('Password:', required: true)
|
32
|
+
end
|
33
|
+
result[:auth_type] = :basic
|
34
|
+
result
|
35
|
+
end
|
36
|
+
|
37
|
+
def auth_file_cache
|
38
|
+
@auth_file_cache ||= Terjira::FileCache.new('profile')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require_relative 'jql_query_builer'
|
2
|
+
require_relative 'auth_option_builder'
|
3
|
+
|
4
|
+
module Terjira
|
5
|
+
module Client
|
6
|
+
# Abstract class to delegate jira-ruby resource class
|
7
|
+
class Base
|
8
|
+
extend JQLQueryBuilder
|
9
|
+
extend AuthOptionBuilder
|
10
|
+
|
11
|
+
DEFAULT_CACHE_SEC = 60
|
12
|
+
DEFAULT_API_PATH = "/rest/api/2/"
|
13
|
+
AGILE_API_PATH = "/rest/agile/1.0/"
|
14
|
+
|
15
|
+
class << self
|
16
|
+
|
17
|
+
delegate :build, to: :resource
|
18
|
+
|
19
|
+
def client
|
20
|
+
@@client ||= JIRA::Client.new(build_auth_options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def resource
|
24
|
+
client.send(class_name) if client.respond_to?(class_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def username
|
28
|
+
client.options[:username]
|
29
|
+
end
|
30
|
+
|
31
|
+
def class_name
|
32
|
+
self.to_s.split("::").last
|
33
|
+
end
|
34
|
+
|
35
|
+
def cache(options = {})
|
36
|
+
options[:expiry] ||= DEFAULT_CACHE_SEC
|
37
|
+
@cache ||= Terjira::FileCache.new(class_name, expiry)
|
38
|
+
end
|
39
|
+
|
40
|
+
# define `#api_get(post, put, delete)` and `#agile_api_get(post, put, delete)`
|
41
|
+
{ DEFAULT_API_PATH => "api_",
|
42
|
+
AGILE_API_PATH => "agile_api_"
|
43
|
+
}.each do |url_prefix, method_prefix|
|
44
|
+
|
45
|
+
[:get, :delete].each do |http_method|
|
46
|
+
method_name = "#{method_prefix}#{http_method}"
|
47
|
+
define_method(method_name) do |path, params = {}, headers = {}|
|
48
|
+
url = url_prefix + path
|
49
|
+
if params.present?
|
50
|
+
params.reject! { |k, v| v.blank? }
|
51
|
+
url += "?#{URI.encode_www_form(params)}"
|
52
|
+
end
|
53
|
+
parse_body client.send(http_method, url, headers)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
[:post, :put].each do |http_method|
|
58
|
+
method_name = "#{method_prefix}#{http_method}"
|
59
|
+
define_method(method_name) do |path, body = '', headers = {}|
|
60
|
+
url = url_prefix + path
|
61
|
+
parse_body client.send(http_method, url, body, headers)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def parse_body(response)
|
67
|
+
JSON.parse(response.body) if response.body.present?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Terjira
|
2
|
+
module Client
|
3
|
+
class Board < Base
|
4
|
+
class << self
|
5
|
+
delegate :build, to: :resource
|
6
|
+
|
7
|
+
def all(options = {})
|
8
|
+
params = options.slice(:type)
|
9
|
+
resp = agile_api_get("board", params)
|
10
|
+
resp["values"].map { |value| build(value) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def find(board_id)
|
14
|
+
resp = agile_api_get("board/#{board_id}")
|
15
|
+
self.build(resp)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Terjira
|
4
|
+
module Client
|
5
|
+
class Field < Base
|
6
|
+
class << self
|
7
|
+
def all
|
8
|
+
@all_fields ||= resource.all
|
9
|
+
end
|
10
|
+
|
11
|
+
def epic_name
|
12
|
+
all.find { |field| field.name == "Epic Name" }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Terjira
|
4
|
+
module Client
|
5
|
+
class Issue < Base
|
6
|
+
class << self
|
7
|
+
delegate :build, :find, to: :resource
|
8
|
+
ISSUE_JQL_KEYS = [:sprint, :assignee, :reporter, :project, :issuetype, :priority, :status, :statusCategory]
|
9
|
+
|
10
|
+
def all(options = {})
|
11
|
+
opts = options.slice(*ISSUE_JQL_KEYS)
|
12
|
+
return resource.all if options.blank?
|
13
|
+
max_results = options.delete(:max_results) || 500
|
14
|
+
resource.jql(build_jql_query(opts), max_results: max_results)
|
15
|
+
end
|
16
|
+
|
17
|
+
def find(issue, options = {})
|
18
|
+
resp = api_get("issue/#{issue.key_value}", options)
|
19
|
+
build(resp)
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_my_issues
|
23
|
+
jql("assignee = #{self.key_value} AND statusCategory != 'Done'")
|
24
|
+
end
|
25
|
+
|
26
|
+
def assign(issue, assignee)
|
27
|
+
body = { name: assignee.key_value }.to_json
|
28
|
+
api_put("issue/#{issue.key_value}/assignee", body)
|
29
|
+
end
|
30
|
+
|
31
|
+
def write_comment(issue, message)
|
32
|
+
resp = api_post("issue/#{issue.key_value}/comment", { body: message }.to_json)
|
33
|
+
find(issue)
|
34
|
+
end
|
35
|
+
|
36
|
+
def create(options = {})
|
37
|
+
params = extract_to_fields_params(options)
|
38
|
+
if transition_param = extract_to_transition_param(options)
|
39
|
+
params.merge!(transition_param)
|
40
|
+
end
|
41
|
+
|
42
|
+
resp = api_post "issue", params.to_json
|
43
|
+
result_id = resp["id"]
|
44
|
+
find(result_id)
|
45
|
+
end
|
46
|
+
|
47
|
+
def update(issue, options = {})
|
48
|
+
params = extract_to_fields_params(options)
|
49
|
+
api_put "issue/#{issue.key_value}", params.to_json
|
50
|
+
find(issue)
|
51
|
+
end
|
52
|
+
|
53
|
+
def trans(issue, options = {})
|
54
|
+
params = extract_to_transition_param(options)
|
55
|
+
params.merge!(extract_to_update_params(options))
|
56
|
+
params.merge!(extract_to_fields_params(options))
|
57
|
+
api_post "issue/#{issue.key_value}/transitions", params.to_json
|
58
|
+
find(issue)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def extract_to_update_params(options = {})
|
64
|
+
params = {}
|
65
|
+
if comment = options.delete(:comment)
|
66
|
+
params[:comment] = [{ add: { body: comment } }]
|
67
|
+
end
|
68
|
+
{ update: params }
|
69
|
+
end
|
70
|
+
|
71
|
+
def extract_to_transition_param(options = {})
|
72
|
+
transition = options.delete(:status)
|
73
|
+
transition ||= options.delete(:transition)
|
74
|
+
return unless transition
|
75
|
+
{ transition: convert_param_key_value_hash(transition) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def extract_to_fields_params(options = {})
|
79
|
+
opts = options.dup
|
80
|
+
params = {}
|
81
|
+
|
82
|
+
custom_fields = options.keys.select { |k| k.to_s =~ /^customfield/ }
|
83
|
+
(custom_fields + [:summary, :description]).each do |k, v|
|
84
|
+
params[k] = opts.delete(k) if opts.key?(k)
|
85
|
+
end
|
86
|
+
|
87
|
+
if opts.key?(:project)
|
88
|
+
params[:project] = { key: opts.delete(:project).key_value }
|
89
|
+
end
|
90
|
+
|
91
|
+
opts.each do |k, v|
|
92
|
+
params[k] = convert_param_key_value_hash(v)
|
93
|
+
end
|
94
|
+
{ fields: params }
|
95
|
+
end
|
96
|
+
|
97
|
+
def convert_param_key_value_hash(resource)
|
98
|
+
if resource.respond_to? :key_with_key_value
|
99
|
+
okey, ovalue = resource.key_with_key_value
|
100
|
+
{ okey => ovalue }
|
101
|
+
elsif resource =~ /^\d+$/
|
102
|
+
{ id: resource.key_value }
|
103
|
+
else
|
104
|
+
{ name: resource.key_value }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Terjira
|
2
|
+
module Client
|
3
|
+
module JQLQueryBuilder
|
4
|
+
JQL_KEYS = %w(board sprint assignee issuetype priority project status statusCategory).freeze
|
5
|
+
|
6
|
+
def build_jql_query(options = {})
|
7
|
+
q_options = options.inject({}) do |memo,(k,v)|
|
8
|
+
memo[k.to_s] = v
|
9
|
+
memo
|
10
|
+
end.slice(*JQL_KEYS)
|
11
|
+
|
12
|
+
query = q_options.map do |key, value|
|
13
|
+
if value.is_a? Array
|
14
|
+
values = value.map { |v| "\"#{v.key_value}\""}.join(",")
|
15
|
+
"#{key} IN (#{values})"
|
16
|
+
else
|
17
|
+
"#{key}=#{value.key_value}"
|
18
|
+
end
|
19
|
+
end.reject(&:blank?).join(" AND ")
|
20
|
+
|
21
|
+
query
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Terjira
|
4
|
+
module Client
|
5
|
+
# Project Client Baseed on jira-ruby gem
|
6
|
+
class Project < Base
|
7
|
+
class << self
|
8
|
+
delegate :all, :find, :fetch, to: :resource
|
9
|
+
|
10
|
+
def all
|
11
|
+
expand = %w(description lead issueTypes url projectKeys)
|
12
|
+
resp = api_get 'project', expand: expand.join(',')
|
13
|
+
resp.map { |project| build(project) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def all_by_board(board)
|
17
|
+
resp = agile_api_get "board/#{board.key_value}/project"
|
18
|
+
resp['values'].map do |project|
|
19
|
+
build(project)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|