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,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