ipt 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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