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
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Terjira
|
4
|
+
module Client
|
5
|
+
class Sprint < Base
|
6
|
+
class << self
|
7
|
+
delegate :build, to: :resource
|
8
|
+
|
9
|
+
def all(board, options = {})
|
10
|
+
params = options.slice(:state, :maxResults)
|
11
|
+
resp = agile_api_get "board/#{board.key_value}/sprint", params
|
12
|
+
resp['values'].map { |value| build(value) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def find(sprint)
|
16
|
+
resp = agile_api_get "sprint/#{sprint.key_value}"
|
17
|
+
build resp
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_active(board)
|
21
|
+
params = { state: 'active' }
|
22
|
+
resp = agile_api_get "board/#{board.key_value}/sprint", params
|
23
|
+
resp['values'].map { |value| build(value) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Terjira
|
4
|
+
module Client
|
5
|
+
class Status < Base
|
6
|
+
class << self
|
7
|
+
def all(project)
|
8
|
+
resp = api_get "project/#{project.key_value}/statuses"
|
9
|
+
statuses_json = resp.map { |issuetype| issuetype["statuses"] }.flatten.uniq
|
10
|
+
statuses_json.map { |status| build(status) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Terjira
|
4
|
+
module Client
|
5
|
+
class User < Base
|
6
|
+
class << self
|
7
|
+
def assignables_by_project(project)
|
8
|
+
if project.is_a? Array
|
9
|
+
keys = project.map(&:key_value).join(",")
|
10
|
+
fetch_assignables "user/assignable/multiProjectSearch", {projectKeys: keys }
|
11
|
+
else
|
12
|
+
fetch_assignables "user/assignable/search", { project: project.key_value }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def assignables_by_board(board)
|
17
|
+
projects = Client::Project.all_by_board(board)
|
18
|
+
assignables_by_project(projects)
|
19
|
+
end
|
20
|
+
|
21
|
+
def assignables_by_sprint(sprint)
|
22
|
+
board_id = if sprint.respond_to? :originBoardId
|
23
|
+
sprint.originBoardId
|
24
|
+
else
|
25
|
+
Client::Sprint.find(sprint).originBoardId
|
26
|
+
end
|
27
|
+
assignables_by_board(board_id)
|
28
|
+
end
|
29
|
+
|
30
|
+
def assignables_by_issue(issue)
|
31
|
+
fetch_assignables "user/assignable/search", {issueKey: issue.key_value }
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def fetch_assignables(path, params)
|
37
|
+
resp = api_get(path, params)
|
38
|
+
resp.map { |user| build(user) }.
|
39
|
+
reject { |user| user.key_value =~ /^addon/ }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'jira-ruby'
|
2
|
+
require 'tty-screen'
|
3
|
+
require 'tty-spinner'
|
4
|
+
require 'pastel'
|
5
|
+
|
6
|
+
module JIRA
|
7
|
+
# Extend jira-ruby for command line interface.
|
8
|
+
class HttpClient
|
9
|
+
alias origin_make_request make_request
|
10
|
+
|
11
|
+
def make_request(http_method, path, body = '', headers = {})
|
12
|
+
title = http_method.to_s.upcase
|
13
|
+
title = Pastel.new.dim(title)
|
14
|
+
spinner = TTY::Spinner.new ":spinner #{title}", format: :dots, clear: true
|
15
|
+
result = nil
|
16
|
+
|
17
|
+
spinner.run do
|
18
|
+
result = origin_make_request(http_method, path, body, headers)
|
19
|
+
end
|
20
|
+
|
21
|
+
result
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Board model is not defined in jira-ruby gem
|
26
|
+
class Base
|
27
|
+
def key_with_key_value
|
28
|
+
[self.class.key_attribute, key_value]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module Resource
|
33
|
+
class BoardFactory < JIRA::BaseFactory # :nodoc:
|
34
|
+
end
|
35
|
+
|
36
|
+
class Board < JIRA::Base
|
37
|
+
def self.key_attribute; :id; end
|
38
|
+
end
|
39
|
+
|
40
|
+
class User
|
41
|
+
def self.key_attribute; :name; end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Issue
|
45
|
+
def self.key_attribute; :key; end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Issuetype
|
49
|
+
def self.key_attribute; :name; end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Resolution
|
53
|
+
def self.key_attribute; :name; end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Client
|
58
|
+
def Board # :nodoc:
|
59
|
+
JIRA::Resource::BoardFactory.new(self)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class String
|
65
|
+
def key_value; self.strip; end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Integer
|
69
|
+
def key_value; self.to_s; end
|
70
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'tty-prompt'
|
2
|
+
# Fix some unexpected result
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
class Question
|
6
|
+
# Decide how to handle input from user
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
def process_input
|
10
|
+
@input = read_input
|
11
|
+
if Utils.blank?(@input)
|
12
|
+
@input = default? ? default : nil
|
13
|
+
end
|
14
|
+
|
15
|
+
if @input.is_a? String
|
16
|
+
@input = encode_input(@input)
|
17
|
+
elsif @input.is_a? Array
|
18
|
+
@input = @input.map { |input| encode_input(input) }
|
19
|
+
end
|
20
|
+
|
21
|
+
@evaluator.(@input)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Encod input
|
25
|
+
#
|
26
|
+
# @return [Boolean]
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
def encode_input(line)
|
30
|
+
line.codepoints.to_a.pack('C*').force_encoding('utf-8')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'base_cli'
|
4
|
+
|
5
|
+
module Terjira
|
6
|
+
class IssueCLI < BaseCLI
|
7
|
+
no_commands do
|
8
|
+
def client_class
|
9
|
+
Client::Issue
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
default_task :show
|
14
|
+
|
15
|
+
desc '[ISSUE_KEY]', 'Show detail of the issue'
|
16
|
+
def show(issue_key = nil)
|
17
|
+
return invoke(:help) unless issue_key
|
18
|
+
issue = client_class.find(issue_key)
|
19
|
+
render_issue_detail(issue)
|
20
|
+
end
|
21
|
+
|
22
|
+
desc '( ls | list )', 'List of issues'
|
23
|
+
jira_options :assignee, :status, :project, :issuetype, :priority
|
24
|
+
map ls: :list
|
25
|
+
def list
|
26
|
+
opts = suggest_options
|
27
|
+
opts[:statusCategory] ||= %w(To\ Do In\ Progress) unless opts[:status]
|
28
|
+
opts[:assignee] ||= current_username
|
29
|
+
opts.delete(:assignee) if opts[:assignee] =~ /^all/i
|
30
|
+
|
31
|
+
issues = client_class.all(opts)
|
32
|
+
render_issues(issues)
|
33
|
+
end
|
34
|
+
|
35
|
+
desc 'trans [ISSUE_KEY] ([STATUS])', 'Do Transition'
|
36
|
+
jira_options :comment, :assignee, :resolution
|
37
|
+
def trans(*args)
|
38
|
+
issue = args.shift
|
39
|
+
raise 'must pass issue key or id' unless issue
|
40
|
+
status = args.join(' ') if args.present?
|
41
|
+
issue = client_class.find(issue, expand: 'transitions.fields')
|
42
|
+
|
43
|
+
transitions = issue.transitions
|
44
|
+
transition = transitions.find { |t| t.name.casecmp(status.to_s).zero? }
|
45
|
+
|
46
|
+
resources = if transition
|
47
|
+
{ status: transition, issue: issue }
|
48
|
+
else
|
49
|
+
{ statuses: transitions, issue: issue }
|
50
|
+
end
|
51
|
+
|
52
|
+
opts = suggest_options(required: [:status], resources: resources)
|
53
|
+
issue = client_class.trans(issue, opts)
|
54
|
+
render_issue_detail(issue)
|
55
|
+
end
|
56
|
+
|
57
|
+
desc 'new', 'Create issue'
|
58
|
+
jira_options :summary, :description, :project, :issuetype,
|
59
|
+
:priority, :assignee
|
60
|
+
def new
|
61
|
+
opts = suggest_options(required: [:project, :summary, :issuetype])
|
62
|
+
|
63
|
+
if opts[:issuetype].key_value.casecmp('epic').zero?
|
64
|
+
epic_name_field = Client::Field.epic_name
|
65
|
+
opts[epic_name_field.key] = write_epic_name
|
66
|
+
end
|
67
|
+
|
68
|
+
issue = client_class.create(opts)
|
69
|
+
render_issue_detail(issue)
|
70
|
+
end
|
71
|
+
|
72
|
+
desc 'edit', 'Edit issue'
|
73
|
+
jira_options :summary, :description, :project, :issuetype,
|
74
|
+
:priority, :assignee
|
75
|
+
def edit(issue)
|
76
|
+
return if options.blank?
|
77
|
+
issue = client_class.find(issue)
|
78
|
+
opts = suggest_options(resources: { issue: issue })
|
79
|
+
issue = client_class.update(issue, opts)
|
80
|
+
render_issue_detail(issue)
|
81
|
+
end
|
82
|
+
|
83
|
+
desc 'comment', 'Write comment on the issue'
|
84
|
+
jira_options :comment
|
85
|
+
def comment(issue)
|
86
|
+
opts = suggest_options(required: [:comment])
|
87
|
+
issue = client_class.write_comment(issue, opts[:comment])
|
88
|
+
render_issue_detail(issue)
|
89
|
+
end
|
90
|
+
|
91
|
+
desc 'take [ISSUE_KEY]', 'Assign issue to self'
|
92
|
+
def take(issue)
|
93
|
+
assign(issue, current_username)
|
94
|
+
end
|
95
|
+
|
96
|
+
desc 'assign [ISSUE_KEY] ([ASSIGNEE])', 'Assing issue to user'
|
97
|
+
def assign(*keys)
|
98
|
+
issue = keys[0]
|
99
|
+
assignee = keys[1]
|
100
|
+
if assignee.nil?
|
101
|
+
issue = client_class.find(issue)
|
102
|
+
opts = suggest_options(required: [:assignee],
|
103
|
+
resouces: { issue: issue })
|
104
|
+
assignee = opts[:assignee]
|
105
|
+
end
|
106
|
+
client_class.assign(issue, assignee)
|
107
|
+
show(issue.key_value)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tty-prompt'
|
4
|
+
require_relative 'resource_store'
|
5
|
+
|
6
|
+
module Terjira
|
7
|
+
module OptionSelector
|
8
|
+
delegate :get, :set, :fetch, to: :resource_store
|
9
|
+
|
10
|
+
def select_project
|
11
|
+
fetch :project do
|
12
|
+
projects = fetch(:projects) { Client::Project.all }
|
13
|
+
option_prompt.select('Choose project?') do |menu|
|
14
|
+
projects.each { |project| menu.choice project_choice_title(project), project }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def select_board(type = nil)
|
20
|
+
fetch(:board) do
|
21
|
+
boards = fetch(:boards) { Client::Board.all(type: type) }
|
22
|
+
option_prompt.select('Choose board?') do |menu|
|
23
|
+
boards.sort_by(&:id).each do |board|
|
24
|
+
menu.choice "#{board.key_value} - #{board.name}", board
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def select_sprint
|
31
|
+
fetch(:sprint) do
|
32
|
+
board = select_board('scrum')
|
33
|
+
sprints = fetch(:sprints) { Client::Sprint.all(board) }
|
34
|
+
option_prompt.select('Choose sprint?') do |menu|
|
35
|
+
sort_sprint_by_state(sprints).each do |sprint|
|
36
|
+
menu.choice sprint_choice_title(sprint), sprint
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def select_assignee
|
43
|
+
fetch(:assignee) do
|
44
|
+
users = fetch(:users) do
|
45
|
+
if issue = get(:issue)
|
46
|
+
Client::User.assignables_by_issue(issue)
|
47
|
+
elsif board = get(:board)
|
48
|
+
Client::User.assignables_by_board(board)
|
49
|
+
elsif sprint = get(:sprint)
|
50
|
+
Client::User.assignables_by_sprint(sprint)
|
51
|
+
else
|
52
|
+
users = Client::User.assignables_by_project(select_project)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
option_prompt.select('Choose assignee?') do |menu|
|
57
|
+
users.each { |user| menu.choice user_choice_title(user), user }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def select_issuetype
|
63
|
+
fetch(:issuetype) do
|
64
|
+
project = select_project
|
65
|
+
if project.is_a? String
|
66
|
+
project = Client::Project.find(project)
|
67
|
+
set(:project, project)
|
68
|
+
end
|
69
|
+
|
70
|
+
option_prompt.select('Choose isseu type?') do |menu|
|
71
|
+
project.issuetypes.each do |issuetype|
|
72
|
+
menu.choice issuetype.name, issuetype
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def select_issue_status
|
79
|
+
fetch(:status) do
|
80
|
+
statuses = fetch(:statuses) do
|
81
|
+
project = if issue = get(:issue)
|
82
|
+
if issue.respond_to?(:project)
|
83
|
+
issue.project
|
84
|
+
else
|
85
|
+
set(:issue, Client::Issue.find(issue)).project
|
86
|
+
end
|
87
|
+
else
|
88
|
+
select_project
|
89
|
+
end
|
90
|
+
Client::Status.all(project)
|
91
|
+
end
|
92
|
+
|
93
|
+
option_prompt.select('Choose status?') do |menu|
|
94
|
+
statuses.each do |status|
|
95
|
+
menu.choice status.name, status
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def select_priority
|
102
|
+
fetch(:priority) do
|
103
|
+
priorities = fetch(:priorities) { Terjira::Client::Priority.all }
|
104
|
+
option_prompt.select('Choose priority?') do |menu|
|
105
|
+
priorities.each do |priority|
|
106
|
+
menu.choice priority.name, priority
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def select_resolution
|
113
|
+
fetch(:resolution) do
|
114
|
+
resolutions = fetch(:resolutions) { Terjira::Client::Resolution.all }
|
115
|
+
option_prompt.select('Choose resolution?') do |menu|
|
116
|
+
resolutions.each do |resolution|
|
117
|
+
menu.choice resolution.name, resolution
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def write_epic_name
|
124
|
+
option_prompt.ask('Epic name?')
|
125
|
+
end
|
126
|
+
|
127
|
+
def write_comment
|
128
|
+
fetch(:comment) do
|
129
|
+
comment = option_prompt.multiline("Comment? (Return empty line for finish)\n")
|
130
|
+
comment.join("\n") if comment
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def write_description
|
135
|
+
fetch(:description) do
|
136
|
+
desc = option_prompt.multiline("Description? (Return empty line for finish)\n")
|
137
|
+
desc.join("\n") if desc
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def write_summary
|
142
|
+
fetch(:summary) { option_prompt.ask('Summary?') }
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def sprint_choice_title(sprint)
|
148
|
+
"#{sprint.key_value} - #{sprint.name} (#{sprint.state.capitalize})"
|
149
|
+
end
|
150
|
+
|
151
|
+
def user_choice_title(user)
|
152
|
+
"#{user.key_value} - #{user.displayName}"
|
153
|
+
end
|
154
|
+
|
155
|
+
def project_choice_title(project)
|
156
|
+
"#{project.key_value} - #{project.name}"
|
157
|
+
end
|
158
|
+
|
159
|
+
def resource_store
|
160
|
+
ResourceStore.instance
|
161
|
+
end
|
162
|
+
|
163
|
+
def option_prompt
|
164
|
+
@option_prompt ||= TTY::Prompt.new
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|