pt 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ # pt changelog
2
+
3
+ ## v0.1
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Raul Murciano raul@murciano.net
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,49 @@
1
+ # pt
2
+
3
+ Minimal client to use Pivotal Tracker from the console.
4
+
5
+ ## Setup
6
+
7
+ gem install pt
8
+
9
+ The first time you run it, `pt` will ask you some data about your Pivotal Tracker account and your current project.
10
+
11
+ ## Usage
12
+
13
+ Run `pt` from the root folder of your project.
14
+
15
+ pt # shows your "My work" tasks list
16
+
17
+ Run `pt create` to create a new bug, chore or feature.
18
+
19
+ The rest of the commands will open you a list of your tasks and let you interact with it:
20
+
21
+ pt open # open a task in the browser
22
+
23
+ pt assign # assign owner
24
+
25
+ pt comment # add a comment
26
+
27
+ pt estimate # estimate a task in points scale
28
+
29
+ pt start # mark a task as started
30
+
31
+ pt finish # indicate you've finished a task
32
+
33
+ pt deliver # indicate the task is delivered
34
+
35
+ pt accept # mark a task as accepted
36
+
37
+ pt reject # mark a task as rejected, explaining why
38
+
39
+ ## Thanks to...
40
+
41
+ - the [Pivotal Tracker](https://www.pivotaltracker.com) guys for making a planning tool that doesn't suck and has an API
42
+ - [Justin Smestad](https://github.com/jsmestad) for his nice `pivotal-tracker` gem
43
+ - [Bryan Liles](http://smartic.us/) for letting me take over the gem name
44
+
45
+ ## License
46
+ See the LICENSE file included in the distribution.
47
+
48
+ ## Copyright
49
+ Copyright (C) 2011 Raul Murciano <raul@murciano.net>.
@@ -0,0 +1,11 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ task :default => [:test]
5
+
6
+ desc 'Run tests'
7
+ Rake::TestTask.new('test') do |t|
8
+ t.pattern = 'test/*_test.rb'
9
+ t.verbose = true
10
+ t.warning = true
11
+ end
data/bin/pt ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+
6
+ require File.dirname(__FILE__) + '/../lib/pt'
7
+
8
+ PT::UI.new ARGV
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+
3
+ module PT
4
+ class InputError < StandardError; end
5
+ VERSION = '0.1'
6
+ end
7
+
8
+ require 'pt/client'
9
+ require 'pt/data_row'
10
+ require 'pt/data_table'
11
+ require 'pt/ui'
@@ -0,0 +1,96 @@
1
+ require 'pivotal-tracker'
2
+
3
+ class PT::Client
4
+
5
+ def self.get_api_token(email, password)
6
+ PivotalTracker::Client.token(email, password)
7
+ rescue RestClient::Unauthorized
8
+ raise PT::InputError.new("Bad email/password combination.")
9
+ end
10
+
11
+ def initialize(api_number)
12
+ PivotalTracker::Client.token = api_number
13
+ end
14
+
15
+ def get_projects
16
+ PivotalTracker::Project.all
17
+ end
18
+
19
+ def get_project(project_id)
20
+ PivotalTracker::Project.find(project_id)
21
+ end
22
+
23
+ def get_membership(project, email)
24
+ PivotalTracker::Membership.all(project).select{ |m| m.email == email }.first
25
+ end
26
+
27
+ def get_current_iteration(project)
28
+ PivotalTracker::Iteration.current(project)
29
+ end
30
+
31
+ def get_my_work(project, user_name)
32
+ project.stories.all :mywork => user_name
33
+ end
34
+
35
+ def get_my_open_tasks(project, user_name)
36
+ project.stories.all :owner => user_name
37
+ end
38
+
39
+ def get_my_tasks_to_estimate(project, user_name)
40
+ project.stories.all(:owner => user_name, :story_type => 'feature').select{ |t| t.estimate == -1 }
41
+ end
42
+
43
+ def get_my_tasks_to_start(project, user_name)
44
+ tasks = project.stories.all(:owner => user_name, :current_state => 'unscheduled,rejected')
45
+ tasks.reject{ |t| (t.story_type == 'feature') && (t.estimate == -1) }
46
+ end
47
+
48
+ def get_my_tasks_to_finish(project, user_name)
49
+ project.stories.all(:owner => user_name, :current_state => 'started')
50
+ end
51
+
52
+ def get_my_tasks_to_deliver(project, user_name)
53
+ project.stories.all(:owner => user_name, :current_state => 'finished')
54
+ end
55
+
56
+ def get_my_tasks_to_accept(project, user_name)
57
+ project.stories.all(:owner => user_name, :current_state => 'delivered')
58
+ end
59
+
60
+ def get_my_tasks_to_reject(project, user_name)
61
+ project.stories.all(:owner => user_name, :current_state => 'delivered')
62
+ end
63
+
64
+ def get_tasks_to_assign(project, user_name)
65
+ project.stories.all.select{ |t| t.owned_by == nil }
66
+ end
67
+
68
+ def get_members(project)
69
+ project.memberships.all
70
+ end
71
+
72
+ def mark_task_as(project, task, state)
73
+ task = PivotalTracker::Story.find(task.id, project.id)
74
+ task.update(:current_state => state)
75
+ end
76
+
77
+ def estimate_task(project, task, points)
78
+ task = PivotalTracker::Story.find(task.id, project.id)
79
+ task.update(:estimate => points)
80
+ end
81
+
82
+ def assign_task(project, task, owner)
83
+ task = PivotalTracker::Story.find(task.id, project.id)
84
+ task.update(:owned_by => owner)
85
+ end
86
+
87
+ def comment_task(project, task, comment)
88
+ task = PivotalTracker::Story.find(task.id, project.id)
89
+ task.notes.create(:text => comment)
90
+ end
91
+
92
+ def create_task(project, name, owner, requester, task_type)
93
+ project.stories.create(:name => name, :owned_by => owner, :requested_by => requester, :story_type => task_type)
94
+ end
95
+
96
+ end
@@ -0,0 +1,22 @@
1
+ class PT::DataRow
2
+
3
+ attr_accessor :num, :record
4
+
5
+ def initialize(orig, dataset)
6
+ @record = orig
7
+ @num = dataset.index(orig) + 1
8
+ end
9
+
10
+ def method_missing(method)
11
+ @record.send method
12
+ end
13
+
14
+ def to_s
15
+ @record.send(self.to_s_attribute)
16
+ end
17
+
18
+ def to_s_attribute
19
+ @n.to_s
20
+ end
21
+
22
+ end
@@ -0,0 +1,62 @@
1
+ require 'hirb'
2
+
3
+ module PT
4
+
5
+ class DataTable
6
+
7
+ extend ::Hirb::Console
8
+
9
+ def initialize(dataset)
10
+ @rows = dataset.map{ |row| DataRow.new(row, dataset) }
11
+ end
12
+
13
+ def print
14
+ if @rows.empty?
15
+ puts "\n -- empty list -- \n"
16
+ else
17
+ self.class.table @rows, :fields => [:num] + self.class.fields, :unicode => true, :description => false
18
+ end
19
+ end
20
+
21
+ def [](pos)
22
+ pos = pos.to_i
23
+ (pos < 1 || pos > @rows.length) ? nil : @rows[pos-1].record
24
+ end
25
+
26
+ def length
27
+ @rows.length
28
+ end
29
+
30
+ def self.fields
31
+ []
32
+ end
33
+
34
+ end
35
+
36
+
37
+ class ProjectTable < DataTable
38
+
39
+ def self.fields
40
+ [:name]
41
+ end
42
+
43
+ end
44
+
45
+
46
+ class TasksTable < DataTable
47
+
48
+ def self.fields
49
+ [:name, :current_state]
50
+ end
51
+
52
+ end
53
+
54
+ class MembersTable < DataTable
55
+
56
+ def self.fields
57
+ [:name]
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,292 @@
1
+ require 'yaml'
2
+ require 'colored'
3
+ require 'highline'
4
+
5
+ class PT::UI
6
+
7
+ GLOBAL_CONFIG_PATH = ENV['HOME'] + "/.pt"
8
+ LOCAL_CONFIG_PATH = Dir.pwd + '/.pt'
9
+
10
+ def initialize(args)
11
+ @io = HighLine.new
12
+ @global_config = load_global_config
13
+ @client = PT::Client.new(@global_config[:api_number])
14
+ @local_config = load_local_config
15
+ @project = @client.get_project(@local_config[:project_id])
16
+ command = args[0].to_sym rescue :my_work
17
+ params = args[1..-1]
18
+ commands.include?(command.to_sym) ? send(command.to_sym) : help(command)
19
+ end
20
+
21
+ def my_work
22
+ title("My Work for #{user_s} in #{project_to_s}")
23
+ stories = @client.get_my_work(@project, @local_config[:user_name])
24
+ PT::TasksTable.new(stories).print
25
+ end
26
+
27
+ def create
28
+ title("Let's create a new task:")
29
+ name = ask("Name for the new task:")
30
+ if ask('Do you want to assign it now? (y/n)').downcase == 'y'
31
+ members = @client.get_members(@project)
32
+ table = PT::MembersTable.new(members)
33
+ owner = select("Please select a member to assign him the task", table).name
34
+ else
35
+ owner = nil
36
+ end
37
+ requester = @local_config[:user_name]
38
+ task_type = ask('Type? (c)hore, (b)ug, anything else for feature)')
39
+ task_type = case task_type
40
+ when 'c', 'chore'
41
+ 'chore'
42
+ when 'b', 'bug'
43
+ 'bug'
44
+ else
45
+ 'feature'
46
+ end
47
+ result = @client.create_task(@project, name, owner, requester, task_type)
48
+ if result.errors.any?
49
+ error(result.errors.errors)
50
+ else
51
+ congrats("Task created, yay!")
52
+ end
53
+ end
54
+
55
+ def open
56
+ title("Tasks for #{user_s} in #{project_to_s}")
57
+ tasks = @client.get_my_open_tasks(@project, @local_config[:user_name])
58
+ table = PT::TasksTable.new(tasks)
59
+ task = select("Please select a story to open it in the browser", table)
60
+ `open #{task.url}`
61
+ end
62
+
63
+ def comment
64
+ title("Tasks for #{user_s} in #{project_to_s}")
65
+ tasks = @client.get_my_work(@project, @local_config[:user_name])
66
+ table = PT::TasksTable.new(tasks)
67
+ task = select("Please select a story to comment it", table)
68
+ comment = ask("Write your comment")
69
+ if @client.comment_task(@project, task, comment)
70
+ congrats("Comment sent, thanks!")
71
+ else
72
+ error("Ummm, something went wrong.")
73
+ end
74
+ end
75
+
76
+ def assign
77
+ title("Tasks for #{user_s} in #{project_to_s}")
78
+ tasks = @client.get_tasks_to_assign(@project, @local_config[:user_name])
79
+ table = PT::TasksTable.new(tasks)
80
+ task = select("Please select a task to assign it an owner", table)
81
+ members = @client.get_members(@project)
82
+ table = PT::MembersTable.new(members)
83
+ owner = select("Please select a member to assign him the task", table).name
84
+ result = @client.assign_task(@project, task, owner)
85
+ if result.errors.any?
86
+ error(result.errors.errors)
87
+ else
88
+ congrats("Task assigned, thanks!")
89
+ end
90
+
91
+ end
92
+
93
+ def estimate
94
+ title("Tasks for #{user_s} in #{project_to_s}")
95
+ tasks = @client.get_my_tasks_to_estimate(@project, @local_config[:user_name])
96
+ table = PT::TasksTable.new(tasks)
97
+ task = select("Please select a story to estimate it", table)
98
+ estimation = ask("How many points you estimate for it? (#{@project.point_scale})")
99
+ result = @client.estimate_task(@project, task, estimation)
100
+ if result.errors.any?
101
+ error(result.errors.errors)
102
+ else
103
+ congrats("Task estimated, thanks!")
104
+ end
105
+ end
106
+
107
+ def start
108
+ title("Tasks for #{user_s} in #{project_to_s}")
109
+ tasks = @client.get_my_tasks_to_start(@project, @local_config[:user_name])
110
+ table = PT::TasksTable.new(tasks)
111
+ task = select("Please select a story to mark it as started", table)
112
+ result = @client.mark_task_as(@project, task, 'started')
113
+ if result.errors.any?
114
+ error(result.errors.errors)
115
+ else
116
+ congrats("Task started, go for it!")
117
+ end
118
+ end
119
+
120
+ def finish
121
+ title("Tasks for #{user_s} in #{project_to_s}")
122
+ tasks = @client.get_my_tasks_to_finish(@project, @local_config[:user_name])
123
+ table = PT::TasksTable.new(tasks)
124
+ task = select("Please select a story to mark it as finished", table)
125
+ if task.story_type == 'chore'
126
+ result = @client.mark_task_as(@project, task, 'accepted')
127
+ else
128
+ result = @client.mark_task_as(@project, task, 'finished')
129
+ end
130
+ if result.errors.any?
131
+ error(result.errors.errors)
132
+ else
133
+ congrats("Another task bites the dust, yeah!")
134
+ end
135
+ end
136
+
137
+ def deliver
138
+ title("Tasks for #{user_s} in #{project_to_s}")
139
+ tasks = @client.get_my_tasks_to_deliver(@project, @local_config[:user_name])
140
+ table = PT::TasksTable.new(tasks)
141
+ task = select("Please select a story to mark it as delivered", table)
142
+ result = @client.mark_task_as(@project, task, 'delivered')
143
+ error(result.errors.errors) if result.errors.any?
144
+ if result.errors.any?
145
+ error(result.errors.errors)
146
+ else
147
+ congrats("Task delivered, congrats!")
148
+ end
149
+ end
150
+
151
+ def accept
152
+ title("Tasks for #{user_s} in #{project_to_s}")
153
+ tasks = @client.get_my_tasks_to_accept(@project, @local_config[:user_name])
154
+ table = PT::TasksTable.new(tasks)
155
+ task = select("Please select a story to mark it as accepted", table)
156
+ result = @client.mark_task_as(@project, task, 'accepted')
157
+ if result.errors.any?
158
+ error(result.errors.errors)
159
+ else
160
+ congrats("Task accepted, hooray!")
161
+ end
162
+ end
163
+
164
+ def reject
165
+ title("Tasks for #{user_s} in #{project_to_s}")
166
+ tasks = @client.get_my_tasks_to_reject(@project, @local_config[:user_name])
167
+ table = PT::TasksTable.new(tasks)
168
+ task = select("Please select a story to mark it as rejected", table)
169
+ comment = ask("Please explain why are you rejecting the task")
170
+ if @client.comment_task(@project, task, comment)
171
+ result = @client.mark_task_as(@project, task, 'rejected')
172
+ congrats("Task rejected, thanks!")
173
+ else
174
+ error("Ummm, something went wrong.")
175
+ end
176
+ end
177
+
178
+ protected
179
+
180
+ def commands
181
+ (public_methods - Object.public_methods).sort
182
+ end
183
+
184
+ # Config
185
+
186
+ def load_global_config
187
+ config = YAML.load(File.read(GLOBAL_CONFIG_PATH)) rescue {}
188
+ if config.empty?
189
+ message "I can't find info about your Pivotal Tracker account in #{GLOBAL_CONFIG_PATH}"
190
+ while !config[:api_number] do
191
+ config[:email] = ask "What is your email?"
192
+ password = ask_secret "And your password? (won't be displayed on screen)"
193
+ begin
194
+ config[:api_number] = PT::Client.get_api_token(config[:email], password)
195
+ rescue PT::InputError => e
196
+ error e.message + " Please try again."
197
+ end
198
+ end
199
+ congrats "Thanks!",
200
+ "Your API id is " + config[:api_number],
201
+ "I'm saving it in #{GLOBAL_CONFIG_PATH} to don't ask you again."
202
+ save_config(config, GLOBAL_CONFIG_PATH)
203
+ end
204
+ config
205
+ end
206
+
207
+ def load_local_config
208
+ config = YAML.load(File.read(LOCAL_CONFIG_PATH)) rescue {}
209
+ if config.empty?
210
+ message "I can't find info about this project in #{LOCAL_CONFIG_PATH}"
211
+ projects = PT::ProjectTable.new(@client.get_projects)
212
+ project = select("Please select the project for the current directory", projects)
213
+ config[:project_id], config[:project_name] = project.id, project.name
214
+ membership = @client.get_membership(project, @global_config[:email])
215
+ config[:user_name], config[:user_id], config[:user_initials] = membership.name, membership.id, membership.initials
216
+ congrats "Thanks! I'm saving this project's info",
217
+ "in #{LOCAL_CONFIG_PATH}: remember to .gitignore it!"
218
+ save_config(config, LOCAL_CONFIG_PATH)
219
+ end
220
+ config
221
+ end
222
+
223
+ def save_config(config, path)
224
+ File.new(path, 'w') unless File.exists?(path)
225
+ File.open(path, 'w') {|f| f.write(config.to_yaml) }
226
+ end
227
+
228
+ # I/O
229
+
230
+ def split_lines(text)
231
+ text.respond_to?(:join) ? text.join("\n") : text
232
+ end
233
+
234
+ def title(*msg)
235
+ puts "\n#{split_lines(msg)}".bold
236
+ end
237
+
238
+ def congrats(*msg)
239
+ puts "\n#{split_lines(msg).green.bold}"
240
+ end
241
+
242
+ def message(*msg)
243
+ puts "\n#{split_lines(msg)}"
244
+ end
245
+
246
+ def error(*msg)
247
+ puts "\n#{split_lines(msg).red.bold}"
248
+ end
249
+
250
+ def select(msg, table)
251
+ if table.length > 0
252
+ begin
253
+ table.print
254
+ row = ask "#{msg} (1-#{table.length}, 'q' to exit)"
255
+ quit if row == 'q'
256
+ selected = table[row]
257
+ error "Invalid selection, try again:" unless selected
258
+ end until selected
259
+ selected
260
+ else
261
+ table.print
262
+ message "Sorry, there are no options to select."
263
+ quit
264
+ end
265
+ end
266
+
267
+ def quit
268
+ message "bye!"
269
+ exit
270
+ end
271
+
272
+ def ask(msg)
273
+ @io.ask("#{msg.bold}")
274
+ end
275
+
276
+ def ask_secret(msg)
277
+ @io.ask("#{msg.bold}"){ |q| q.echo = '*' }
278
+ end
279
+
280
+ def help(command)
281
+ error "Command <#{command}> unknown.", "Available commands:" + commands.map{ |c| "\n- #{c}" }.join
282
+ end
283
+
284
+ def user_s
285
+ "#{@local_config[:user_name]} (#{@local_config[:user_initials]})"
286
+ end
287
+
288
+ def project_to_s
289
+ "Project #{@local_config[:project_name].upcase}"
290
+ end
291
+
292
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pt
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ version: "0.1"
9
+ platform: ruby
10
+ authors:
11
+ - Raul Murciano
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2011-06-30 00:00:00 -07:00
17
+ default_executable:
18
+ dependencies:
19
+ - !ruby/object:Gem::Dependency
20
+ name: pivotal-tracker
21
+ prerelease: false
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :runtime
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: hirb
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ none: false
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ segments:
41
+ - 0
42
+ version: "0"
43
+ type: :runtime
44
+ version_requirements: *id002
45
+ - !ruby/object:Gem::Dependency
46
+ name: colored
47
+ prerelease: false
48
+ requirement: &id003 !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ segments:
54
+ - 0
55
+ version: "0"
56
+ type: :runtime
57
+ version_requirements: *id003
58
+ - !ruby/object:Gem::Dependency
59
+ name: highline
60
+ prerelease: false
61
+ requirement: &id004 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ type: :runtime
70
+ version_requirements: *id004
71
+ description: Minimalist, opinionated client to manage your Pivotal Tracker tasks from the command line.
72
+ email:
73
+ - raul@murciano.net
74
+ executables:
75
+ - pt
76
+ extensions: []
77
+
78
+ extra_rdoc_files: []
79
+
80
+ files:
81
+ - lib/pt/client.rb
82
+ - lib/pt/data_row.rb
83
+ - lib/pt/data_table.rb
84
+ - lib/pt/ui.rb
85
+ - lib/pt.rb
86
+ - Changelog.md
87
+ - Gemfile
88
+ - LICENSE
89
+ - RakeFile
90
+ - README.md
91
+ - bin/pt
92
+ has_rdoc: true
93
+ homepage: http://www.github.com/raul/pt
94
+ licenses: []
95
+
96
+ post_install_message:
97
+ rdoc_options: []
98
+
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ segments:
107
+ - 0
108
+ version: "0"
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ segments:
115
+ - 0
116
+ version: "0"
117
+ requirements: []
118
+
119
+ rubyforge_project: pt
120
+ rubygems_version: 1.3.7
121
+ signing_key:
122
+ specification_version: 3
123
+ summary: Client to use Pivotal Tracker from the console.
124
+ test_files: []
125
+