terjira 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +13 -0
  3. data/.gitignore +52 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +26 -0
  6. data/.travis.yml +20 -0
  7. data/CODE_OF_CONDUCT.md +49 -0
  8. data/Gemfile +4 -0
  9. data/Gemfile.lock +104 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +47 -0
  12. data/Rakefile +10 -0
  13. data/Vagrantfile +26 -0
  14. data/bin/console +14 -0
  15. data/bin/jira +13 -0
  16. data/bin/setup +8 -0
  17. data/lib/terjira.rb +38 -0
  18. data/lib/terjira/base_cli.rb +36 -0
  19. data/lib/terjira/board_cli.rb +12 -0
  20. data/lib/terjira/client/agile.rb +41 -0
  21. data/lib/terjira/client/auth_option_builder.rb +42 -0
  22. data/lib/terjira/client/base.rb +72 -0
  23. data/lib/terjira/client/board.rb +20 -0
  24. data/lib/terjira/client/field.rb +17 -0
  25. data/lib/terjira/client/issue.rb +110 -0
  26. data/lib/terjira/client/jql_query_builer.rb +25 -0
  27. data/lib/terjira/client/priority.rb +14 -0
  28. data/lib/terjira/client/project.rb +25 -0
  29. data/lib/terjira/client/rapid_view.rb +8 -0
  30. data/lib/terjira/client/resolution.rb +14 -0
  31. data/lib/terjira/client/sprint.rb +28 -0
  32. data/lib/terjira/client/status.rb +15 -0
  33. data/lib/terjira/client/user.rb +44 -0
  34. data/lib/terjira/ext/jira_ruby.rb +70 -0
  35. data/lib/terjira/ext/tty_prompt.rb +34 -0
  36. data/lib/terjira/issue_cli.rb +110 -0
  37. data/lib/terjira/option_support/option_selector.rb +167 -0
  38. data/lib/terjira/option_support/resource_store.rb +45 -0
  39. data/lib/terjira/option_support/shared_options.rb +70 -0
  40. data/lib/terjira/option_supportable.rb +80 -0
  41. data/lib/terjira/presenters/board_presenter.rb +20 -0
  42. data/lib/terjira/presenters/common_presenter.rb +53 -0
  43. data/lib/terjira/presenters/issue_presenter.rb +175 -0
  44. data/lib/terjira/presenters/project_presenter.rb +69 -0
  45. data/lib/terjira/presenters/sprint_presenter.rb +68 -0
  46. data/lib/terjira/project_cli.rb +25 -0
  47. data/lib/terjira/rapidview_cli.rb +6 -0
  48. data/lib/terjira/sprint_cli.rb +58 -0
  49. data/lib/terjira/utils/file_cache.rb +110 -0
  50. data/lib/terjira/version.rb +3 -0
  51. data/terjira.gemspec +38 -0
  52. metadata +282 -0
@@ -0,0 +1,8 @@
1
+ require_relative 'base'
2
+
3
+ module Terjira
4
+ module Client
5
+ class RapidView < Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'base'
2
+
3
+ module Terjira
4
+ module Client
5
+ class Resolution < Base
6
+ class << self
7
+ def all
8
+ resp = api_get("resolution")
9
+ resp.map { |resolution| build(resolution) }
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -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