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