terjira 0.1.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 +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
|