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.
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,45 @@
1
+ require 'singleton'
2
+
3
+ module Terjira
4
+ # Store resource or key value of selected options
5
+ class ResourceStore
6
+ include Singleton
7
+
8
+ attr_accessor :store
9
+
10
+ def initialize
11
+ initialize_store
12
+ end
13
+
14
+ def fetch(resource_name)
15
+ resouce = get(resource_name)
16
+ if resouce
17
+ resouce
18
+ elsif block_given?
19
+ resouce = yield
20
+ set(resource_name, resouce)
21
+ end
22
+ end
23
+
24
+ def get(resource_name)
25
+ store[resource_name]
26
+ end
27
+
28
+ def set(resource_name, resource)
29
+ store[resource_name] = resource
30
+ resource
31
+ end
32
+
33
+ def exists?(resource_name)
34
+ store[resource_name].present?
35
+ end
36
+
37
+ def clear
38
+ initialize_store
39
+ end
40
+
41
+ def initialize_store
42
+ @store = Thor::CoreExt::HashWithIndifferentAccess.new
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,70 @@
1
+ module Terjira
2
+ module SharedOptions
3
+ OPTIONS = {
4
+ 'project' => {
5
+ type: :string,
6
+ aliases: '-p',
7
+ desc: 'project key'
8
+ },
9
+ 'board' => {
10
+ type: :numeric,
11
+ aliases: '-b',
12
+ banner: 'BOARD_ID',
13
+ lazy_default: 'board'
14
+ },
15
+ 'sprint' => {
16
+ type: :numeric,
17
+ banner: 'SPRINT_ID',
18
+ lazy_default: 'sprint'
19
+ },
20
+ 'assignee' => {
21
+ type: :string,
22
+ aliases: '-a'
23
+ },
24
+ 'state' => {
25
+ type: :array,
26
+ aliases: '-s',
27
+ default: %w(Active Future),
28
+ lazy_default: %w(Active Future),
29
+ enum: %w(Active Future Closed)
30
+ },
31
+ 'status' => {
32
+ type: :string,
33
+ aliases: '-s',
34
+ desc: 'status'
35
+ },
36
+ 'resolution' => {
37
+ type: :string,
38
+ aliases: '-r'
39
+ },
40
+ 'issuetype' => {
41
+ type: :string,
42
+ aliases: '-t'
43
+ },
44
+ 'priority' => {
45
+ type: :string,
46
+ aliases: '-P'
47
+ },
48
+ 'summary' => {
49
+ type: :string,
50
+ aliases: '-S'
51
+ },
52
+ 'description' => {
53
+ type: :string,
54
+ aliases: '-d'
55
+ },
56
+ 'comment' => {
57
+ type: :string,
58
+ aliases: '-m'
59
+ }
60
+ }.freeze
61
+
62
+ def jira_options(*keys)
63
+ keys.each { |key| jira_option(key) }
64
+ end
65
+
66
+ def jira_option(key, opts = {})
67
+ method_option(key, (OPTIONS[key.to_s] || {}).merge(opts))
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,80 @@
1
+ require_relative 'option_support/option_selector'
2
+ require_relative 'option_support/resource_store'
3
+ require_relative 'option_support/shared_options'
4
+
5
+ module Terjira
6
+ # For support CLI options.
7
+ module OptionSupportable
8
+ def self.included(klass)
9
+ klass.class_eval do
10
+ extend SharedOptions
11
+ include OptionSelector
12
+ end
13
+ end
14
+
15
+ OPTION_TO_SELECTOR = {
16
+ project: :select_project,
17
+ board: :select_board,
18
+ summary: :write_summary,
19
+ description: :write_description,
20
+ sprint: :select_sprint,
21
+ issuetype: :select_issuetype,
22
+ assignee: :select_assignee,
23
+ status: :select_issue_status,
24
+ priority: :select_priority,
25
+ resolution: :select_resolution,
26
+ comment: :write_comment
27
+ }.freeze
28
+
29
+ # Transforming and clening options
30
+ # and suggest list of option values
31
+ def suggest_options(opts = {})
32
+ origin = options.dup
33
+
34
+ if opts[:required].is_a? Array
35
+ opts[:required].inject(origin) do |memo, opt|
36
+ memo[opt] ||= opt.to_s
37
+ memo
38
+ end
39
+ end
40
+
41
+ # Store assigned options
42
+ origin.reject { |k, v| k.to_s.casecmp(v.to_s).zero? }.each do |k, v|
43
+ resource_store.set(k.to_sym, v)
44
+ end
45
+
46
+ # Store given options from arguments
47
+ (opts[:resources] || {}).each do |k, v|
48
+ resource_store.set(k.to_sym, v)
49
+ end
50
+
51
+ # Select options that are not assigned value from user
52
+ default_value_options = origin.select do |k, v|
53
+ k.to_s.casecmp(v.to_s).zero?
54
+ end
55
+
56
+ # Sort order for suggest option values
57
+ default_value_options = default_value_options.sort do |hash|
58
+ OPTION_TO_SELECTOR.keys.index(hash[0].to_sym) || 999
59
+ end
60
+ default_value_options = Hash[default_value_options]
61
+
62
+ # Suggest option values and save to resource store
63
+ default_value_options.each do |k, _v|
64
+ selector_method = OPTION_TO_SELECTOR[k.to_sym]
65
+ send(selector_method) if selector_method
66
+ end
67
+
68
+ # Fetch selected values from resource store
69
+ default_value_options.each do |k, _v|
70
+ default_value_options[k] = resource_store.get(k)
71
+ end
72
+
73
+ origin.merge! default_value_options
74
+ end
75
+
76
+ def resource_store
77
+ ResourceStore.instance
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module Terjira
4
+ module BoardPresenter
5
+ def render_boards_summary(boards)
6
+ pastel = Pastel.new
7
+
8
+ header = %w(ID Name Type).map { |title| pastel.bold(title) }
9
+ rows = []
10
+ boards.each do |board|
11
+ rows << [pastel.bold(board.id), board.name, board.type]
12
+ end
13
+
14
+ table = TTY::Table.new header, rows
15
+ result = table.render(:unicode, padding: [0, 1, 0, 1])
16
+
17
+ render(result)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-table'
4
+ require 'pastel'
5
+
6
+ module Terjira
7
+ module CommonPresenter
8
+ def render(text)
9
+ if text.is_a? Array
10
+ puts text.join("\n")
11
+ else
12
+ puts text
13
+ end
14
+ end
15
+
16
+ def pastel
17
+ @pastel ||= Pastel.new
18
+ end
19
+
20
+ def formatted_date(date_str)
21
+ return nil if date_str.nil? || date_str.empty?
22
+ Time.parse(date_str).strftime('%c')
23
+ end
24
+
25
+ def username_with_email(user)
26
+ if user.nil?
27
+ 'None'
28
+ else
29
+ title = "#{user.name}, #{user.displayName}"
30
+ title += " <#{user.emailAddress}>" if user.respond_to?(:emailAddress)
31
+ title
32
+ end
33
+ end
34
+
35
+ def screen_width
36
+ TTY::Screen.width
37
+ end
38
+
39
+ # Insert new line(`\n`)
40
+ # when string display length is longger than length argument
41
+ def insert_new_line(str, length)
42
+ str.split(/\r\n|\n/).map do |line|
43
+ if line.display_width < 1
44
+ line
45
+ else
46
+ display_length = pastel.strip(line).display_width
47
+ split_length = (line.length * length / display_length).to_i
48
+ line.scan(/.{1,#{split_length}}/).join("\n")
49
+ end
50
+ end.join("\n")
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,175 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-prompt'
4
+ require 'tty-table'
5
+ require 'pastel'
6
+
7
+ module Terjira
8
+ module IssuePresenter
9
+ def render_issues(issues, opts = {})
10
+ return render('Empty') if issues.blank?
11
+
12
+ header = [pastel.bold('Key'), pastel.bold('Summary')] if opts[:header]
13
+
14
+ rows = issues.map do |issue|
15
+ [pastel.bold(issue.key), summarise_issue(issue)]
16
+ end
17
+
18
+ table = TTY::Table.new header, rows
19
+ table_opts = { padding: [0, 1, 0, 1], multiline: true }
20
+ result = table.render(:unicode, table_opts) do |renderer|
21
+ renderer.border.separator = :each_row
22
+ end
23
+
24
+ render(result)
25
+ end
26
+
27
+ def render_divided_issues_by_status(issues)
28
+ extract_status_names(issues).each do |name|
29
+ selected_issues = issues.select { |issue| issue.status.name == name }
30
+ title = colorize_issue_stastus(selected_issues.first.status)
31
+ title += "(#{selected_issues.size})"
32
+ render(title)
33
+ render_issues(selected_issues, header: false)
34
+ end
35
+ end
36
+
37
+ def render_issue_detail(issue)
38
+ header_title = "#{pastel.bold(issue.key)} in #{issue.project.name}"
39
+ header = [insert_new_line(header_title, screen_width - 10)]
40
+
41
+ rows = []
42
+ rows << pastel.underline.bold(issue.summary)
43
+ rows << ''
44
+ rows << issue_sutats_bar(issue)
45
+ rows << ''
46
+
47
+ rows << [pastel.bold('Assignee'), username_with_email(issue.assignee)].join(' ')
48
+ rows << [pastel.bold('Reporter'), username_with_email(issue.reporter)].join(' ')
49
+ rows << ''
50
+ rows << pastel.bold('Description')
51
+ rows << (issue.description.blank? ? 'None' : issue.description)
52
+
53
+ if issue.respond_to?(:environment) && issue.environment.present?
54
+ rows << pastel.bold('Environment')
55
+ rows << issue.environment
56
+ end
57
+
58
+ if issue.comments.present?
59
+ rows << ''
60
+ rows << pastel.bold('Comments')
61
+ remain_comments = issue.comments
62
+ comments = remain_comments.pop(4)
63
+
64
+ if comments.size.zero?
65
+ rows << 'None'
66
+ elsif remain_comments.empty?
67
+ rows << pastel.dim("- #{remain_comments.size} previous comments exist -")
68
+ end
69
+
70
+ comments.each do |comment|
71
+ comment_title = pastel.bold(comment.author['displayName'])
72
+ comment_title += " #{formatted_date(comment.created)}"
73
+ rows << comment_title
74
+ rows << comment.body
75
+ rows << ''
76
+ end
77
+ end
78
+
79
+ rows = rows.map { |row| insert_new_line(row, screen_width - 10) }
80
+
81
+ table = TTY::Table.new header, rows.map { |r| [r] }
82
+ result = table.render(:unicode, padding: [0, 1, 0, 1], multiline: true)
83
+
84
+ render(result)
85
+ end
86
+
87
+ def summarise_issue(issue)
88
+ first_line = [colorize_issue_stastus(issue.status),
89
+ issue.summary.tr("\t", ' ')].join
90
+
91
+ second_line = [colorize_priority(issue.priority),
92
+ colorize_issue_type(issue.issuetype),
93
+ assign_info(issue)].join(' ')
94
+
95
+ lines = [first_line, second_line].map do |line|
96
+ insert_new_line(line, screen_width - 30)
97
+ end
98
+ lines.join("\n")
99
+ end
100
+
101
+ private
102
+
103
+ def assign_info(issue)
104
+ reporter = issue.reporter ? issue.reporter.name : 'None'
105
+ assignee = issue.assignee ? issue.assignee.name : 'None'
106
+ "#{reporter} ⇨ #{assignee}"
107
+ end
108
+
109
+ def issue_sutats_bar(issue)
110
+ bar = ["#{pastel.bold('Type')}: #{colorize_issue_type(issue.issuetype)}",
111
+ "#{pastel.bold('Status')}: #{colorize_issue_stastus(issue.status)}",
112
+ "#{pastel.bold('priority')}: #{colorize_priority(issue.priority, title: true)}"]
113
+ bar.join("\s\s\s")
114
+ end
115
+
116
+ def colorize_issue_type(issue_type)
117
+ title = " #{issue_type.name} "
118
+ if title =~ /bug/i
119
+ pastel.on_red.bold(title)
120
+ elsif title =~ /task/i
121
+ pastel.on_blue.bold(title)
122
+ elsif title =~ /story/i
123
+ pastel.on_green.bold(title)
124
+ elsif title =~ /epic/i
125
+ pastel.on_magenta.bold(title)
126
+ else
127
+ pastel.on_cyan.bold(title)
128
+ end
129
+ end
130
+
131
+ def colorize_issue_stastus(status)
132
+ title = "#{status.name} "
133
+ category = title
134
+ if status.respond_to? :statusCategory
135
+ category = (status.statusCategory || {})['name'] || ''
136
+ end
137
+ if category =~ /to\sdo|open/i
138
+ pastel.blue.bold(title)
139
+ elsif category =~ /in\sprogress/i
140
+ pastel.yellow.bold(title)
141
+ elsif category =~ /done|close/i
142
+ pastel.green.bold(title)
143
+ else
144
+ pastel.magenta.bold(title)
145
+ end
146
+ end
147
+
148
+ def colorize_priority(priority, opts = {})
149
+ return '' unless priority.respond_to? :name
150
+ name = priority.name
151
+ info = if name =~ /high|major|critic/i
152
+ { color: :red, icon: '⬆' }
153
+ elsif name =~ /medium|default/i
154
+ { color: :yellow, icon: '⬆' }
155
+ elsif name =~ /minor|low|trivial/i
156
+ { color: :green, icon: '⬇' }
157
+ else
158
+ { color: :green, icon: '•' }
159
+ end
160
+ title = opts[:title] ? "#{info[:icon]} #{name}" : info[:icon]
161
+ pastel.send(info[:color], title)
162
+ end
163
+
164
+ def extract_status_names(issues)
165
+ issue_names = issues.sort_by do |issue|
166
+ status_key = %w(new indeterminate done)
167
+ idx = if issue.status.respond_to? :statusCategory
168
+ status_key.index(issue.status.statusCategory['key'])
169
+ end
170
+ idx || status_key.size
171
+ end
172
+ issue_names.map { |issue| issue.status.name }.uniq
173
+ end
174
+ end
175
+ end