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
data/lib/terjira.rb ADDED
@@ -0,0 +1,38 @@
1
+ require 'pry'
2
+ require_relative 'terjira/ext/jira_ruby'
3
+ require_relative 'terjira/ext/tty_prompt'
4
+ require 'terjira/version'
5
+ require 'thor'
6
+
7
+ Dir[File.dirname(__FILE__) + '/terjira/*_cli.rb'].each { |f| require f }
8
+
9
+ ENV['PAGER'] ||= 'less'
10
+
11
+ # http://willschenk.com/making-a-command-line-utility-with-gems-and-thor/
12
+ module Terjira
13
+ # Main CLI
14
+ class CLI < Thor
15
+ desc 'login', 'login your Jira'
16
+ def login
17
+ Client::Base.expire_auth_options
18
+ Client::Base.build_auth_options
19
+ end
20
+
21
+ desc 'logout', 'logout your Jira'
22
+ def logout
23
+ Client::Base.expire_auth_options
24
+ end
25
+
26
+ desc 'project SUBCOMMAND ...ARGS', 'Manage proejcts'
27
+ subcommand 'project', ProjectCLI
28
+
29
+ desc 'board SUBCOMMAND ...ARGS', 'Manage boards'
30
+ subcommand 'board', BoardCLI
31
+
32
+ desc 'sprint SUBCOMMAND ...ARGS', 'Manage sprints'
33
+ subcommand 'sprint', SprintCLI
34
+
35
+ desc 'issue SUBCOMMAND ...ARGS', 'Manage issues'
36
+ subcommand 'issue', IssueCLI
37
+ end
38
+ end
@@ -0,0 +1,36 @@
1
+ require 'thor'
2
+
3
+ require_relative 'option_supportable'
4
+ Dir[File.dirname(__FILE__) + "/presenters/*.rb"].each { |f| require f }
5
+
6
+ module Terjira
7
+ module Client
8
+ %w[Base Field Project Board Sprint Issue User Status Resolution Priority RapidView Agile].each do |klass|
9
+ autoload klass, "terjira/client/#{klass.gsub(/(.)([A-Z](?=[a-z]))/,'\1_\2').downcase}"
10
+ end
11
+ end
12
+
13
+ class BaseCLI < Thor
14
+ include OptionSupportable
15
+
16
+ include CommonPresenter
17
+ include IssuePresenter
18
+ include ProjectPresenter
19
+ include BoardPresenter
20
+ include SprintPresenter
21
+
22
+ def self.banner(command, namespace = nil, subcommand = false)
23
+ "#{basename} #{subcommand_prefix} #{command.usage}"
24
+ end
25
+
26
+ def self.subcommand_prefix
27
+ self.name.gsub(%r{.*::}, '').gsub("CLI", '').gsub(%r{^[A-Z]}) { |match| match[0].downcase }.gsub(%r{[A-Z]}) { |match| "-#{match[0].downcase}" }
28
+ end
29
+
30
+ no_commands do
31
+ def current_username
32
+ @current_username ||= Client::Base.username
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'base_cli'
2
+
3
+ module Terjira
4
+ class BoardCLI < BaseCLI
5
+ desc "list(ls)", "list all boards"
6
+ map ls: :list
7
+ def list
8
+ boards = Client::Board.all
9
+ render_boards_summary(boards.sort_by { |b| b.id })
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,41 @@
1
+ require_relative 'base'
2
+
3
+ module Terjira
4
+ module Client
5
+ class Agile < Base
6
+ class << self
7
+ delegate :all, :get_sprints, :backlog_issues, to: :resource
8
+
9
+ def project_by_board(board_id)
10
+ resp = agile_api_get("board/#{board_id}/project")
11
+ end
12
+
13
+ def boards
14
+ all["values"]
15
+ end
16
+
17
+ def sprints(board_id, options = {})
18
+ sprints = get_sprints(board_id)["values"]
19
+ sprints.sort_by do |sprint|
20
+ if sprint["state"] == 'active'
21
+ [0, sprint["id"]]
22
+ elsif sprint["state"] == 'future'
23
+ [1, sprint["id"]]
24
+ elsif sprint["state"] == 'closed'
25
+ [2, sprint["id"] * -1]
26
+ else
27
+ [3, 0]
28
+ end
29
+ end
30
+ end
31
+
32
+ def backlog_issues(board_id)
33
+ get_backlog_issues(board_id)
34
+ end
35
+
36
+ def sprint_issues(board_id)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,42 @@
1
+ require 'tty-prompt'
2
+ require 'terjira/utils/file_cache'
3
+
4
+ module Terjira
5
+ module Client
6
+ module AuthOptionBuilder
7
+ AUTH_CACHE_KEY = 'auth'.freeze
8
+
9
+ def build_auth_options(cache_key = AUTH_CACHE_KEY)
10
+ auth_file_cache.fetch cache_key do
11
+ build_auth_options_by_tty
12
+ end
13
+ end
14
+
15
+ def build_auth_options_by_cached(cache_key = AUTH_CACHE_KEY)
16
+ auth_file_cache.get(cache_key)
17
+ end
18
+
19
+ def expire_auth_options(cache_key = AUTH_CACHE_KEY)
20
+ auth_file_cache.delete(cache_key)
21
+ end
22
+
23
+ def build_auth_options_by_tty
24
+ puts 'Login will be required...'
25
+ prompt = TTY::Prompt.new
26
+
27
+ result = prompt.collect do
28
+ key(:site).ask('Site:', required: true)
29
+ key(:context_path).ask('Context path:', default: '')
30
+ key(:username).ask('Username:', required: true)
31
+ key(:password).mask('Password:', required: true)
32
+ end
33
+ result[:auth_type] = :basic
34
+ result
35
+ end
36
+
37
+ def auth_file_cache
38
+ @auth_file_cache ||= Terjira::FileCache.new('profile')
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,72 @@
1
+ require_relative 'jql_query_builer'
2
+ require_relative 'auth_option_builder'
3
+
4
+ module Terjira
5
+ module Client
6
+ # Abstract class to delegate jira-ruby resource class
7
+ class Base
8
+ extend JQLQueryBuilder
9
+ extend AuthOptionBuilder
10
+
11
+ DEFAULT_CACHE_SEC = 60
12
+ DEFAULT_API_PATH = "/rest/api/2/"
13
+ AGILE_API_PATH = "/rest/agile/1.0/"
14
+
15
+ class << self
16
+
17
+ delegate :build, to: :resource
18
+
19
+ def client
20
+ @@client ||= JIRA::Client.new(build_auth_options)
21
+ end
22
+
23
+ def resource
24
+ client.send(class_name) if client.respond_to?(class_name)
25
+ end
26
+
27
+ def username
28
+ client.options[:username]
29
+ end
30
+
31
+ def class_name
32
+ self.to_s.split("::").last
33
+ end
34
+
35
+ def cache(options = {})
36
+ options[:expiry] ||= DEFAULT_CACHE_SEC
37
+ @cache ||= Terjira::FileCache.new(class_name, expiry)
38
+ end
39
+
40
+ # define `#api_get(post, put, delete)` and `#agile_api_get(post, put, delete)`
41
+ { DEFAULT_API_PATH => "api_",
42
+ AGILE_API_PATH => "agile_api_"
43
+ }.each do |url_prefix, method_prefix|
44
+
45
+ [:get, :delete].each do |http_method|
46
+ method_name = "#{method_prefix}#{http_method}"
47
+ define_method(method_name) do |path, params = {}, headers = {}|
48
+ url = url_prefix + path
49
+ if params.present?
50
+ params.reject! { |k, v| v.blank? }
51
+ url += "?#{URI.encode_www_form(params)}"
52
+ end
53
+ parse_body client.send(http_method, url, headers)
54
+ end
55
+ end
56
+
57
+ [:post, :put].each do |http_method|
58
+ method_name = "#{method_prefix}#{http_method}"
59
+ define_method(method_name) do |path, body = '', headers = {}|
60
+ url = url_prefix + path
61
+ parse_body client.send(http_method, url, body, headers)
62
+ end
63
+ end
64
+ end
65
+
66
+ def parse_body(response)
67
+ JSON.parse(response.body) if response.body.present?
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,20 @@
1
+ module Terjira
2
+ module Client
3
+ class Board < Base
4
+ class << self
5
+ delegate :build, to: :resource
6
+
7
+ def all(options = {})
8
+ params = options.slice(:type)
9
+ resp = agile_api_get("board", params)
10
+ resp["values"].map { |value| build(value) }
11
+ end
12
+
13
+ def find(board_id)
14
+ resp = agile_api_get("board/#{board_id}")
15
+ self.build(resp)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'base'
2
+
3
+ module Terjira
4
+ module Client
5
+ class Field < Base
6
+ class << self
7
+ def all
8
+ @all_fields ||= resource.all
9
+ end
10
+
11
+ def epic_name
12
+ all.find { |field| field.name == "Epic Name" }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,110 @@
1
+ require_relative 'base'
2
+
3
+ module Terjira
4
+ module Client
5
+ class Issue < Base
6
+ class << self
7
+ delegate :build, :find, to: :resource
8
+ ISSUE_JQL_KEYS = [:sprint, :assignee, :reporter, :project, :issuetype, :priority, :status, :statusCategory]
9
+
10
+ def all(options = {})
11
+ opts = options.slice(*ISSUE_JQL_KEYS)
12
+ return resource.all if options.blank?
13
+ max_results = options.delete(:max_results) || 500
14
+ resource.jql(build_jql_query(opts), max_results: max_results)
15
+ end
16
+
17
+ def find(issue, options = {})
18
+ resp = api_get("issue/#{issue.key_value}", options)
19
+ build(resp)
20
+ end
21
+
22
+ def current_my_issues
23
+ jql("assignee = #{self.key_value} AND statusCategory != 'Done'")
24
+ end
25
+
26
+ def assign(issue, assignee)
27
+ body = { name: assignee.key_value }.to_json
28
+ api_put("issue/#{issue.key_value}/assignee", body)
29
+ end
30
+
31
+ def write_comment(issue, message)
32
+ resp = api_post("issue/#{issue.key_value}/comment", { body: message }.to_json)
33
+ find(issue)
34
+ end
35
+
36
+ def create(options = {})
37
+ params = extract_to_fields_params(options)
38
+ if transition_param = extract_to_transition_param(options)
39
+ params.merge!(transition_param)
40
+ end
41
+
42
+ resp = api_post "issue", params.to_json
43
+ result_id = resp["id"]
44
+ find(result_id)
45
+ end
46
+
47
+ def update(issue, options = {})
48
+ params = extract_to_fields_params(options)
49
+ api_put "issue/#{issue.key_value}", params.to_json
50
+ find(issue)
51
+ end
52
+
53
+ def trans(issue, options = {})
54
+ params = extract_to_transition_param(options)
55
+ params.merge!(extract_to_update_params(options))
56
+ params.merge!(extract_to_fields_params(options))
57
+ api_post "issue/#{issue.key_value}/transitions", params.to_json
58
+ find(issue)
59
+ end
60
+
61
+ private
62
+
63
+ def extract_to_update_params(options = {})
64
+ params = {}
65
+ if comment = options.delete(:comment)
66
+ params[:comment] = [{ add: { body: comment } }]
67
+ end
68
+ { update: params }
69
+ end
70
+
71
+ def extract_to_transition_param(options = {})
72
+ transition = options.delete(:status)
73
+ transition ||= options.delete(:transition)
74
+ return unless transition
75
+ { transition: convert_param_key_value_hash(transition) }
76
+ end
77
+
78
+ def extract_to_fields_params(options = {})
79
+ opts = options.dup
80
+ params = {}
81
+
82
+ custom_fields = options.keys.select { |k| k.to_s =~ /^customfield/ }
83
+ (custom_fields + [:summary, :description]).each do |k, v|
84
+ params[k] = opts.delete(k) if opts.key?(k)
85
+ end
86
+
87
+ if opts.key?(:project)
88
+ params[:project] = { key: opts.delete(:project).key_value }
89
+ end
90
+
91
+ opts.each do |k, v|
92
+ params[k] = convert_param_key_value_hash(v)
93
+ end
94
+ { fields: params }
95
+ end
96
+
97
+ def convert_param_key_value_hash(resource)
98
+ if resource.respond_to? :key_with_key_value
99
+ okey, ovalue = resource.key_with_key_value
100
+ { okey => ovalue }
101
+ elsif resource =~ /^\d+$/
102
+ { id: resource.key_value }
103
+ else
104
+ { name: resource.key_value }
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,25 @@
1
+ module Terjira
2
+ module Client
3
+ module JQLQueryBuilder
4
+ JQL_KEYS = %w(board sprint assignee issuetype priority project status statusCategory).freeze
5
+
6
+ def build_jql_query(options = {})
7
+ q_options = options.inject({}) do |memo,(k,v)|
8
+ memo[k.to_s] = v
9
+ memo
10
+ end.slice(*JQL_KEYS)
11
+
12
+ query = q_options.map do |key, value|
13
+ if value.is_a? Array
14
+ values = value.map { |v| "\"#{v.key_value}\""}.join(",")
15
+ "#{key} IN (#{values})"
16
+ else
17
+ "#{key}=#{value.key_value}"
18
+ end
19
+ end.reject(&:blank?).join(" AND ")
20
+
21
+ query
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'base'
2
+
3
+ module Terjira
4
+ module Client
5
+ class Priority < Base
6
+ class << self
7
+ def all
8
+ resp = api_get "priority"
9
+ resp.map { |priority| build(priority) }
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'base'
2
+
3
+ module Terjira
4
+ module Client
5
+ # Project Client Baseed on jira-ruby gem
6
+ class Project < Base
7
+ class << self
8
+ delegate :all, :find, :fetch, to: :resource
9
+
10
+ def all
11
+ expand = %w(description lead issueTypes url projectKeys)
12
+ resp = api_get 'project', expand: expand.join(',')
13
+ resp.map { |project| build(project) }
14
+ end
15
+
16
+ def all_by_board(board)
17
+ resp = agile_api_get "board/#{board.key_value}/project"
18
+ resp['values'].map do |project|
19
+ build(project)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end