ipt 1.0.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.
@@ -0,0 +1,163 @@
1
+ require 'yaml'
2
+ require 'highline'
3
+ require 'tempfile'
4
+ require 'uri'
5
+ require 'thor'
6
+ require 'paint'
7
+
8
+ # String patch monkey
9
+ class String
10
+ %i[blue yellow green red white black magenta cyan bold].each do |color|
11
+ define_method(color) do
12
+ Paint[self, color]
13
+ end
14
+ end
15
+ end
16
+
17
+ module PT
18
+ class CLI < Thor
19
+ include PT::Action
20
+ include PT::IO
21
+ attr_reader :project
22
+
23
+ TYPE=%w[feature bug chore release]
24
+ ACTION = %w[show open start finish deliver accept reject done assign estimate tasks comment label edit unstart]
25
+
26
+ default_task :mywork
27
+
28
+ def initialize(*args)
29
+ super
30
+ @io = HighLine.new
31
+ @config = PT::Configuration.new
32
+ @client = @config.client || Client.new
33
+ @project = @client.project
34
+ end
35
+
36
+ %w[unscheduled started finished delivered accepted rejected].each do |state|
37
+ desc "#{state} <owner>", "show all #{state} stories"
38
+ define_method(state.to_sym) do |owner = nil|
39
+ filter = "state:#{state}"
40
+ filter << " owner:#{owner}" if owner
41
+ select_story_from_paginated_table(title: "#{state} stories") do |page|
42
+ @client.get_stories(filter: filter, page: page)
43
+ end
44
+ end
45
+ end
46
+
47
+ ACTION.each do |action|
48
+ desc "#{action} [id]", "#{action} story"
49
+ define_method(action.to_sym) do |story_id = nil|
50
+ if story_id
51
+ if story = @client.project.story(story_id.to_i)
52
+ title("#{action} '#{story.name}'")
53
+ send("#{action}_story", story)
54
+ else
55
+ message("No matches found for '#{story_id}', please use a valid pivotal story Id")
56
+ return
57
+ end
58
+ else
59
+ method_name = "get_stories_to_#{action}"
60
+ story = select_story_from_paginated_table(default_action: action, title: "Stories to #{action}") do |page|
61
+ if @client.respond_to?(method_name.to_sym)
62
+ @client.send("get_stories_to_#{action}", page: page)
63
+ else
64
+ @client.get_stories(page: page)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ TYPE.each do |type|
72
+ desc "#{type} <owner>", "show all #{type} stories"
73
+ define_method(type.to_sym) do |owner = nil|
74
+ filter = "story_type:#{type} -state:accepted"
75
+ filter << " owner:#{owner}" if owner
76
+ select_story_from_paginated_table(title: "#{type} stories") do |page|
77
+ @client.get_stories(filter: filter, page: page)
78
+ end
79
+ end
80
+ end
81
+
82
+ desc 'mywork', 'list all your stories'
83
+ def mywork
84
+ select_story_from_paginated_table(title: 'My Work') do |page|
85
+ @client.get_stories(filter: "owner:#{Settings[:user_name]} -state:accepted", page: page)
86
+ end
87
+ end
88
+
89
+ %w[ current backlog ].each do |scope|
90
+ desc "#{scope}", 'list all stories for #{scope} iteration'
91
+ define_method(scope.to_sym) do
92
+ select_story_from_paginated_table(title: "#{scope} iteration") do |page|
93
+ @client.get_stories_from_iteration(scope: scope, page: page)
94
+ end
95
+ end
96
+ end
97
+
98
+ desc "list [owner]", "list all stories from owner"
99
+ def list(owner = nil)
100
+ owner = choose_person.initials unless owner
101
+ select_story_from_paginated_table(title: "stories for #{owner}") do |page|
102
+ @client.get_stories(filter: "owner:#{owner} -state:accepted", page: page)
103
+ end
104
+ end
105
+
106
+ desc "recent", "show stories you've recently shown or commented on with pt"
107
+ def recent
108
+ title("Your recent stories from #{project_to_s}")
109
+ select_story_from_paginated_table(title: "recent stories") do |page|
110
+ @client.get_stories(filter: Settings[:recent_tasks].join(','), page: page)
111
+ end
112
+ end
113
+
114
+ desc 'create [title] --owner <owner> --type <type> -m', "create a new story (and include description ala git commit)"
115
+ long_desc <<-LONGDESC
116
+ create story with title [title]
117
+
118
+ --owner, -o set owner
119
+
120
+ --type , -t set story type
121
+
122
+ -m enable add description using vim
123
+
124
+ omit all parameters will start interactive mode
125
+ LONGDESC
126
+ option :type, aliases: :t
127
+ option :owner, aliases: :o
128
+ option :m, type: :boolean
129
+ def create(title =nil)
130
+ if title
131
+ owner_id = if options[:owner] && (owner = @client.find_member(options[:owner]))
132
+ owner.person.id
133
+ else
134
+ nil
135
+ end
136
+ description = edit_using_editor if options[:m]
137
+ params = {
138
+ name: title,
139
+ requested_by_id: Settings[:user_id],
140
+ owner_ids: [owner_id],
141
+ description: description
142
+ }
143
+ params[:story_type] = options[:type] if options[:type]
144
+ story = @client.create_story(params)
145
+ congrats("Story with title #{story.name} has been created \n #{story.url}")
146
+ else
147
+ create_interactive_story
148
+ end
149
+ end
150
+
151
+ desc "find [query] " ,"looks in your stories by title and presents it"
152
+ long_desc <<-LONGDESC
153
+ search using pivotal tracker search query
154
+ LONGDESC
155
+ def find(query=nil)
156
+ query ||= ask("Please type seach query") { |q| q.readline = true }
157
+ story = select_story_from_paginated_table(title: "Search result for #{query}")do |page|
158
+ @client.get_stories(filter: query, page: page)
159
+ end
160
+ show_story(story)
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,133 @@
1
+ require 'uri'
2
+ require 'tracker_api'
3
+
4
+ module PT
5
+ class Client < SimpleDelegator
6
+
7
+ STORY_FIELDS=':default,requested_by(initials),owners(initials),tasks(complete,description),comments(text,file_attachment_ids,person(initials))'
8
+
9
+ attr_reader :config, :client, :total_record, :limit
10
+
11
+ def initialize
12
+ @client = TrackerApi::Client.new(token: Settings[:pivotal_api_key])
13
+ self.__setobj__(@client)
14
+ @limit = Settings[:limit] || 10
15
+ end
16
+
17
+ def total_page
18
+ @total_record = @client.last_response.env.response_headers["X-Tracker-Pagination-Total"]
19
+ @total_record ? (@total_record.to_f/limit).ceil : 1
20
+ end
21
+
22
+ def current_page
23
+ offset = @client.last_response.env.response_headers["X-Tracker-Pagination-Offset"]
24
+ offset ? ((offset.to_f/limit)+1).to_i.ceil : 1
25
+ end
26
+
27
+ def project
28
+ @client.project(Settings[:project_id])
29
+ end
30
+
31
+ def get_stories_to_estimate(params={})
32
+ params[:filter] = "owner:#{Settings[:user_name]} type:feature estimate:-1"
33
+ get_stories(params)
34
+ end
35
+
36
+ def get_stories_to_unstart(params={})
37
+ params[:filter] = "owner:#{Settings[:user_name]} -state:unstarted"
38
+ get_stories(params)
39
+ end
40
+
41
+ def get_stories_to_start(params={})
42
+ params[:filter] = "owner:#{Settings[:user_name]} type:feature,bug state:unscheduled,rejected,unstarted"
43
+ tasks = get_stories(params)
44
+ tasks.reject{ |t| (t.story_type == 'feature') && (!t.estimate) }
45
+ end
46
+
47
+ def get_stories_to_finish(params={})
48
+ params[:filter] = "owner:#{Settings[:user_name]} -state:unscheduled,rejected"
49
+ get_stories(params)
50
+ end
51
+
52
+ def get_stories_to_deliver(params={})
53
+ params[:filter] = "owner:#{Settings[:user_name]} -state:delivered,accepted,rejected"
54
+ get_stories(params)
55
+ end
56
+
57
+ def get_stories_to_accept(params={})
58
+ params[:filter] = "owner:#{Settings[:user_name]} -state:accepted"
59
+ get_stories(params)
60
+ end
61
+
62
+ def get_stories_to_reject(params={})
63
+ params[:filter] = "owner:#{Settings[:user_name]} -state:rejected"
64
+ get_stories(params)
65
+ end
66
+
67
+ def get_stories_to_assign(params={})
68
+ params[:filter] = "-state:accepted"
69
+ get_stories(params)
70
+ end
71
+
72
+ def get_stories(params={})
73
+ limit = @limit || 10
74
+ page = params[:page] || 0
75
+ offset = page*limit
76
+ filter = params[:filter] || '-state=accepted'
77
+ project.stories limit: limit, fields: STORY_FIELDS, auto_paginate: false, offset: offset, filter: filter
78
+ end
79
+
80
+ def get_stories_from_iteration(params={})
81
+ page = params[:page] || 0
82
+ puts "page #{page}"
83
+ scope = params[:scope] || 'current'
84
+ project.iterations(scope: scope, fields: ":default,stories(#{STORY_FIELDS})")[page]&.stories || []
85
+ end
86
+
87
+
88
+ def get_member(query)
89
+ member = project.memberships.select{ |m| m.person.name.downcase.start_with?(query.downcase) || m.person.initials.downcase == query.downcase }
90
+ member.empty? ? nil : member.first
91
+ end
92
+
93
+ def find_member(query)
94
+ project.memberships.detect do |m|
95
+ m.person.name.downcase.start_with?(query.downcase) || m.person.initials.downcase == query.downcase
96
+ end
97
+ end
98
+
99
+ def get_members
100
+ project.memberships fields: ':default,person'
101
+ end
102
+
103
+
104
+ def mark_task_as(task, state)
105
+ task.current_state = state
106
+ task.save
107
+ end
108
+
109
+ def estimate_story(task, points)
110
+ task.estimate = points
111
+ task.save
112
+ end
113
+
114
+ def assign_story(story, owner)
115
+ story.add_owner(owner)
116
+ story.save
117
+ end
118
+
119
+ def add_label(task, label)
120
+ task.add_label(label)
121
+ task.save
122
+ end
123
+
124
+ def comment_task(story, comment)
125
+ task.create_comment(text: comment)
126
+ end
127
+
128
+ def create_story(args)
129
+ project.create_story(args)
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,65 @@
1
+ require 'config'
2
+ module PT
3
+ class Configuration
4
+ include PT::IO
5
+ GLOBAL_CONFIG_PATH = ENV['HOME'] + "/.pt.yml"
6
+ LOCAL_CONFIG_PATH = Dir.pwd + '/.pt.yml'
7
+
8
+ attr_reader :client
9
+
10
+ def initialize
11
+ Config.load_and_set_settings(GLOBAL_CONFIG_PATH, get_local_config_path)
12
+ unless (Settings[:pivotal_api_key] ||= ENV['PIVOTAL_API_KEY'])
13
+ message "I can't find info about your Pivotal Tracker account in #{GLOBAL_CONFIG_PATH}."
14
+ Settings[:pivotal_api_key] = ask "What is your token?"
15
+ congrats "Thanks!",
16
+ "Your API id is " + Settings[:pivotal_api_key],
17
+ "I'm saving it in #{GLOBAL_CONFIG_PATH} so you don't have to log in again."
18
+ save_config({"pivotal_api_key" => Settings[:pivotal_api_key]}, GLOBAL_CONFIG_PATH)
19
+ end
20
+ @client = Client.new
21
+ Settings[:user_name] ||= @client.me.name || ENV['pivotal_user_name']
22
+ Settings[:user_id] ||= @client.me.id || ENV['pivotal_user_id']
23
+ Settings[:user_initials] ||= @client.me.initials || ENV['pivotal_user_initials']
24
+
25
+
26
+ unless (Settings[:project_id] ||= ENV['PIVOTAL_PROJECT_ID'])
27
+ projects = ProjectTable.new(@client.projects)
28
+ project = select("Please select the project for the current directory", projects)
29
+ Settings[:project_id], Settings[:project_name] = project.id, project.name
30
+ end
31
+
32
+ save_config(Settings.to_hash, get_local_config_path())
33
+ end
34
+
35
+ def get_local_config_path
36
+ # If the local config path does not exist, check to see if we're in a git repo
37
+ # And if so, try the top level of the checkout
38
+ if (!File.exist?(LOCAL_CONFIG_PATH) && system('git rev-parse 2> /dev/null'))
39
+ return `git rev-parse --show-toplevel`.chomp() + '/.pt.yml'
40
+ else
41
+ return LOCAL_CONFIG_PATH
42
+ end
43
+ end
44
+
45
+ def save_config(config, path)
46
+ config = stringify(config)
47
+ File.new(path, 'w') unless File.exists?(path)
48
+ File.open(path, 'w') {|f| f.write(config.to_yaml) }
49
+ end
50
+
51
+ private
52
+
53
+ def check_local_config_path
54
+ if GLOBAL_CONFIG_PATH == get_local_config_path()
55
+ error("Please execute .pt inside your project directory and not in your home.")
56
+ exit
57
+ end
58
+ end
59
+
60
+ def stringify(hash)
61
+ hash.inject({}) { |memo, (k,v)| memo[k.to_s] = v; memo }
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,83 @@
1
+ require 'iconv' unless "older_ruby?".respond_to?(:force_encoding)
2
+
3
+ module PT
4
+ class DataRow
5
+
6
+ attr_accessor :num, :record, :state, :owners
7
+
8
+ def initialize(orig, dataset)
9
+ @record = orig
10
+ @num = dataset.index(orig) + 1
11
+ if defined? orig.current_state
12
+ @state = orig.current_state
13
+ end
14
+ end
15
+
16
+ def method_missing(method)
17
+ str = @record.send(method).to_s
18
+ str.respond_to?(:force_encoding) ? str.force_encoding('utf-8') : Iconv.iconv('UTF-8', 'UTF-8', str)
19
+ end
20
+
21
+ def to_s
22
+ @record.send(self.to_s_attribute)
23
+ end
24
+
25
+ def to_s_attribute
26
+ @n.to_s
27
+ end
28
+
29
+ def name
30
+ _name = @record.name
31
+ if _name.size > 15
32
+ _name[0..15] + '...'
33
+ else
34
+ _name
35
+ end
36
+ end
37
+
38
+ def owners
39
+ if @record.instance_variable_get(:@owners).present?
40
+ @record.owners.map{|o| o.initials == Settings[:user_initials] ? o.initials.red : o.initials }.join(',')
41
+ end
42
+ end
43
+
44
+ def state
45
+ state = @record.current_state
46
+ case state
47
+ when 'delivered'
48
+ state.yellow
49
+ when 'finished'
50
+ state.blue
51
+ when 'accepted'
52
+ state.green
53
+ when 'rejected'
54
+ state.red
55
+ when 'started'
56
+ state.white
57
+ else
58
+ state.black
59
+ end
60
+ end
61
+
62
+ def story_type
63
+ t = @record.story_type
64
+ case t
65
+ when 'bug'
66
+ '🐞'
67
+ when 'feature'
68
+ '⭐'
69
+ when 'release'
70
+ '🏁'
71
+ when 'chore'
72
+ '⚙️'
73
+ else
74
+ t
75
+ end
76
+ end
77
+
78
+ def estimate
79
+ @record.estimate.to_i if @record.estimate
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,107 @@
1
+ # require 'hirb'
2
+ # require 'hirb-unicode'
3
+ require 'terminal-table'
4
+
5
+ module PT
6
+
7
+ class DataTable
8
+
9
+ # extend ::Hirb::Console
10
+
11
+ def initialize(dataset, title=nil)
12
+ @title = title
13
+ @rows = dataset.map{ |row| DataRow.new(row, dataset) }
14
+ end
15
+
16
+ def print(config={})
17
+ if @rows.empty?
18
+ puts "\n#{'-- empty list --'.center(36)}\n"
19
+ else
20
+
21
+ headers = [:num]
22
+ headers += self.class.headers.present? ? self.class.headers : self.class.fields
23
+
24
+ fields = [:num] + self.class.fields
25
+ rows = []
26
+ @rows.each_with_index do |row, index|
27
+ _row = fields.map { |f| row.send(f) }
28
+ rows << _row
29
+ end
30
+ table = Terminal::Table.new(title: @title, headings: headers,
31
+ rows: rows, style: { all_separators: true })
32
+ puts table
33
+ end
34
+ end
35
+
36
+ def [](pos)
37
+ pos = pos.to_i
38
+ (pos < 1 || pos > @rows.length) ? nil : @rows[pos-1].record
39
+ end
40
+
41
+ def length
42
+ @rows.length
43
+ end
44
+
45
+ def self.fields
46
+ []
47
+ end
48
+
49
+ def self.headers
50
+ []
51
+ end
52
+
53
+ end
54
+
55
+
56
+ class ProjectTable < DataTable
57
+
58
+ def self.fields
59
+ [:name]
60
+ end
61
+
62
+ end
63
+
64
+
65
+ class TasksTable < DataTable
66
+
67
+ def self.fields
68
+ [:name, :owners, :story_type, :estimate, :state]
69
+ end
70
+
71
+ def self.headers
72
+ [:title, :owners, :type, :pt, :state]
73
+ end
74
+
75
+ end
76
+
77
+ class MultiUserTasksTable < DataTable
78
+
79
+ def self.fields
80
+ [:owned_by, :name, :state, :id]
81
+ end
82
+
83
+ end
84
+
85
+ class PersonsTable < DataTable
86
+
87
+ def self.fields
88
+ [:initials, :name]
89
+ end
90
+
91
+ end
92
+
93
+ class TodoTaskTable < DataTable
94
+
95
+ def self.fields
96
+ [:description]
97
+ end
98
+ end
99
+
100
+ class ActionTable < DataTable
101
+
102
+ def self.fields
103
+ [:action]
104
+ end
105
+ end
106
+
107
+ end