pt 0.8.6 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 93ae006e38a2336e7717d7754d1ff07e7e86162e
4
- data.tar.gz: a6f6d11a0f765bb8862b486ac9b96cd3facab79f
3
+ metadata.gz: 1c872c7de07ff476f016e21cd526a26cde532c61
4
+ data.tar.gz: 2b5cd00d28ad94fcf20842d6c50b9eb02bf4ef8d
5
5
  SHA512:
6
- metadata.gz: 739d0a6c02367b3808f71ea867b92cd2937cfcb47a50b429ddace71c7511886fab9aad6e52ef0363dd31b1a500c54e7aa90472d5cc9a5e4bd5b6a97273f67db2
7
- data.tar.gz: f283c929e0a18a1be42cd80c9db675be318654e91b0e8a6024e23446564de17a5483b3a1fccaed2ce674d00b7e98a6ff9d249aebe0042e074d6b37ac6497c913
6
+ metadata.gz: dae2f14490c2b022db21fc2f5408e29365907091b0f41992aa65577f2aeac8b037f41d4487459042ec05aaedba4489c67ae184391b31f571c8abe137de328535
7
+ data.tar.gz: eccedbf39e375f0389423657e0bf9abab46600211ed377735e91a4b364ac2798422a5269dfc1eebdb44efa246c48f031ff6ba6cefec078d26ab6ca66bccee6a6
@@ -1,5 +1,9 @@
1
1
  # pt changelog
2
2
 
3
+ ## v0.9.0
4
+ - add pagination for long stories (limit: default is 20, can be changed by set :limit: on local .pt file)
5
+ -
6
+
3
7
  ## v0.8.6
4
8
  - replace ruby save navigation operator
5
9
  ## v0.8.5
data/Gemfile CHANGED
@@ -2,3 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in pt.gemspec
4
4
  gemspec
5
+ gem 'aruba', '~> 0.14.2'
data/README.md CHANGED
@@ -15,54 +15,11 @@ The first time you run it, `pt` will ask you some data about your Pivotal Tracke
15
15
 
16
16
  ## Usage
17
17
 
18
- Run `pt` from the root folder of your project.
19
-
20
- ```
21
- pt # show all available stories
22
-
23
- pt todo <owner> # show all unscheduled stories
24
-
25
- pt (unscheduled,started,finished,delivered, accepted, rejected) <owner> # show all (unscheduled,started,finished,delivered, accepted, rejected) stories
26
-
27
- pt create [title] <owner> <type> -m # create a new story (and include description ala git commit)
28
-
29
- pt show [id] # shows detailed info about a story
30
-
31
- pt tasks [id] # manage tasks of story
32
-
33
- pt open [id] # open a story in the browser
34
-
35
- pt assign [id] <owner> # assign owner
36
-
37
- pt comment [id] [comment] # add a comment
38
-
39
- pt label [id] [label] # add a label
40
-
41
- pt estimate [id] [0-3] # estimate a story in points scale
42
-
43
- pt (start,finish,deliver,accept) [id] # mark a story as started
44
-
45
- pt reject [id] [reason] # mark a story as rejected, explaining why
46
-
47
- pt done [id] <0-3> <comment> # lazy mans finish story, opens, assigns to you, estimates, finish & delivers
48
-
49
- pt find [query] # looks in your stories by title and presents it
50
-
51
- pt list [owner] # list all stories for another pt user
52
-
53
- pt list all # list all stories for all users
54
-
55
- pt updates # shows number recent activity from your current project
56
-
57
- pt recent # shows stories you've recently shown or commented on with pt
58
-
59
- All commands can be run entirely without arguments for a wizard based UI. Otherwise [required] <optional>.
60
- Anything that takes an id will also take the num (index) from the pt command.
61
- ```
18
+ Run `pt` from the root folder of your project will show all available command.
62
19
 
63
20
  ## Problems?
64
21
 
65
- [Open a new issue](https://github.com/raul/pt/issues/new). It can be helpful to include a trace of the requests and responses you're getting from Pivotal Tracker: you can get it by adding the `--debug` parameter while invoking `ptt` (remember to remove all sensible data though).
22
+ [Open a new issue](https://github.com/raul/pt/issues/new).
66
23
 
67
24
  ## Contribute
68
25
  - `git clone https://github.com/raul/pt.git`
data/bin/pt CHANGED
@@ -7,4 +7,4 @@ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
7
7
 
8
8
  require 'pt'
9
9
 
10
- PT::UI.new ARGV
10
+ PT::CLI.start ARGV
data/lib/pt.rb CHANGED
@@ -3,7 +3,9 @@ require "pt/version"
3
3
  require 'pt/client'
4
4
  require 'pt/data_row'
5
5
  require 'pt/data_table'
6
- require 'pt/ui'
6
+ require 'pt/action'
7
+ require 'pt/helper'
8
+ require 'pt/cli'
7
9
 
8
10
  module PT
9
11
  # Your code goes here...
@@ -0,0 +1,151 @@
1
+ module PT
2
+ module Action
3
+ def show_story(story)
4
+ title('========================================='.red)
5
+ title story.name.red
6
+ title('========================================='.red)
7
+ estimation = [-1, nil].include?(story.estimate) ? "Unestimated" : "#{story.estimate} points"
8
+ requester = story.requested_by ? story.requested_by.initials : @local_config[:user_name]
9
+ message "#{story.current_state.capitalize} #{story.story_type} | #{estimation} | Req: #{requester} | Owners: #{story.owners.map(&:initials).join(',')} | ID: #{story.id}"
10
+
11
+ if story.labels.present?
12
+ message "Labels: " + story.labels.map(&:name).join(', ')
13
+ end
14
+ message story.description.green unless story.description.nil? || story.description.empty?
15
+ message "View on pivotal: #{story.url}"
16
+
17
+ if story.tasks.present?
18
+ title('tasks'.yellow)
19
+ story.tasks.each{ |t| compact_message "- #{t.complete ? "[done]" : ""} #{t.description}" }
20
+ end
21
+
22
+
23
+ story.comments.each do |n|
24
+ title('......................................'.blue)
25
+ text = ">> #{n.person.initials}: #{n.text}"
26
+ text << "[#{n.file_attachment_ids.size}F]" if n.file_attachment_ids
27
+ message text
28
+ end
29
+ save_recent_task( story.id )
30
+ end
31
+
32
+ def tasks_story(story)
33
+ story_task = get_open_story_task_from_params(story)
34
+ if story_task.position == -1
35
+ description = ask('Title for new task')
36
+ story.create_task(:description => description)
37
+ congrats("New todo task added to \"#{story.name}\"")
38
+ else
39
+ edit_story_task story_task
40
+ end
41
+ end
42
+
43
+ def open_story story
44
+ `open #{story.url}`
45
+ end
46
+
47
+ def assign_story story
48
+ if (owner = find_owner @params[1])
49
+ @client.assign_task(story, owner)
50
+ else
51
+ members = @client.get_members
52
+ table = PersonsTable.new(members.map(&:person))
53
+ owner = select("Please select a member to assign him the story", table)
54
+ end
55
+
56
+ congrats("story assigned to #{owner.initials}, thanks!")
57
+ end
58
+
59
+ def comment_story(story)
60
+ comment = @params[1] || ask("Write your comment")
61
+ if @client.comment_task(story, comment)
62
+ congrats("Comment sent, thanks!")
63
+ save_recent_task( story.id )
64
+ else
65
+ error("Ummm, something went wrong.")
66
+ end
67
+ end
68
+
69
+ def label_story(story)
70
+ if @params[1]
71
+ label = @params[1]
72
+ else
73
+ label = ask("Which label?")
74
+ end
75
+
76
+ @client.add_label(story, label );
77
+ show_story(task_by_id_or_pt_id(story.id))
78
+ end
79
+
80
+ def estimate_story(story)
81
+ estimation ||= ask("How many points you estimate for it? (#{project.point_scale})")
82
+ @client.estimate_story(story, estimation)
83
+ congrats("Task estimated, thanks!")
84
+ end
85
+
86
+ def start_story story
87
+ @client.mark_task_as(story, 'started')
88
+ congrats("story started, go for it!")
89
+ end
90
+
91
+ def finish_story story
92
+ if story.story_type == 'chore'
93
+ @client.mark_task_as(story, 'accepted')
94
+ else
95
+ @client.mark_task_as(story, 'finished')
96
+ end
97
+ congrats("Another story bites the dust, yeah!")
98
+ end
99
+
100
+ def deliver_story story
101
+ return if story.story_type == 'chore'
102
+ @client.mark_task_as(story, 'delivered')
103
+ congrats("story delivered, congrats!")
104
+ end
105
+
106
+ def accept_story story
107
+ @client.mark_task_as(story, 'accepted')
108
+ congrats("Accepted")
109
+ end
110
+
111
+ def reject_story(story)
112
+ comment = @params[1] || ask("Please explain why are you rejecting the story.")
113
+ if @client.comment_task(story, comment)
114
+ @client.mark_task_as(story, 'rejected')
115
+ congrats("story rejected, thanks!")
116
+ else
117
+ error("Ummm, something went wrong.")
118
+ end
119
+ end
120
+
121
+ def done_story(story)
122
+ #we need this for finding again later
123
+ story_id = story.id
124
+
125
+ if !@params[1] && story.estimate == -1
126
+ error("You need to give an estimate for this task")
127
+ return
128
+ end
129
+
130
+ if @params[1] && story.estimate == -1
131
+ if [0,1,2,3].include? @params[1].to_i
132
+ estimate_story(story, @params[1].to_i)
133
+ end
134
+ if @params[2]
135
+ story = task_by_id_or_pt_id story_id
136
+ @client.comment_task(story, @params[2])
137
+ end
138
+ else
139
+ @client.comment_task(story, @params[1]) if @params[1]
140
+ end
141
+
142
+ start_story story
143
+
144
+ finish_story story
145
+
146
+ deliver_story story
147
+ end
148
+
149
+ end
150
+ end
151
+
@@ -0,0 +1,183 @@
1
+ require 'yaml'
2
+ require 'colored'
3
+ require 'highline'
4
+ require 'tempfile'
5
+ require 'uri'
6
+ require 'thor'
7
+ module PT
8
+ class CLI < Thor
9
+ include PT::Action
10
+ include PT::Helper
11
+ attr_reader :project
12
+
13
+ def initialize(*args)
14
+ super
15
+ @io = HighLine.new
16
+ @global_config = load_global_config
17
+ @local_config = load_local_config
18
+ @client = Client.new(@global_config[:api_number], @local_config)
19
+ @project = @client.project
20
+ end
21
+
22
+ %w[unscheduled started finished delivered accepted rejected].each do |state|
23
+ desc "#{state} <owner>", "show all #{state} stories"
24
+ define_method(state.to_sym) do |owner = nil|
25
+ filter = "state:#{state}"
26
+ filter << " owner:#{owner}" if owner
27
+ story = select_story_from_paginated_table do |page|
28
+ @client.get_stories(filter: filter, page: page)
29
+ end
30
+ show_story(story)
31
+ end
32
+ end
33
+
34
+ %w[show tasks open assign comments label estimate start finish deliver accept reject done].each do |action|
35
+ desc "#{action} [id]", "#{action} story"
36
+ define_method(action.to_sym) do |story_id = nil|
37
+ if story_id
38
+ story = task_by_id_or_pt_id(story_id.to_i)
39
+ unless story
40
+ message("No matches found for '#{story_id}', please use a valid pivotal story Id")
41
+ return
42
+ end
43
+ else
44
+ method_name = "get_stories_to_#{action}"
45
+ story = select_story_from_paginated_table do |page|
46
+ if @client.respond_to?(method_name.to_sym)
47
+ @client.send("get_stories_to_#{action}", page: page)
48
+ else
49
+ @client.get_stories(page: page)
50
+ end
51
+ end
52
+ end
53
+ title("#{action} '#{story.name}'")
54
+ send("#{action}_story", story)
55
+ end
56
+ end
57
+
58
+ desc 'mywork', 'list all your stories'
59
+ def mywork
60
+ story = select_story_from_paginated_table do |page|
61
+ @client.get_stories(filter: "owner:#{@local_config[:user_name]} -state:accepted", page: page)
62
+ end
63
+ show_story(story)
64
+ end
65
+
66
+ desc "list [owner]", "list all stories from owner"
67
+ def list(owner = nil)
68
+ if owner
69
+ if owner == "all"
70
+ stories = @client.get_work
71
+ TasksTable.new(stories).print @global_config
72
+ else
73
+ stories = @client.get_my_work(owner)
74
+ TasksTable.new(stories).print @global_config
75
+ end
76
+ else
77
+ members = @client.get_members
78
+ table = MembersTable.new(members)
79
+ user = select("Please select a member to see his tasks.", table).name
80
+ title("Work for #{user} in #{project_to_s}")
81
+ stories = @client.get_my_work(user)
82
+ TasksTable.new(stories).print @global_config
83
+ end
84
+ end
85
+
86
+ desc "recent", "show stories you've recently shown or commented on with pt"
87
+ def recent
88
+ title("Your recent stories from #{project_to_s}")
89
+ stories = @project.stories( ids: @local_config[:recent_tasks].join(',') )
90
+ MultiUserTasksTable.new(stories).print @global_config
91
+ end
92
+
93
+ desc 'create [title] --owner <owner> --type <type> -m', "create a new story (and include description ala git commit)"
94
+ long_desc <<-LONGDESC
95
+ create story with title [title]
96
+
97
+ --owner, -o set owner
98
+
99
+ --type , -t set story type
100
+
101
+ -m enable add description using vim
102
+
103
+ omit all parameters will start interactive mode
104
+ LONGDESC
105
+ option :type, aliases: :t
106
+ option :owner, aliases: :o
107
+ option :m, type: :boolean
108
+ def create(title =nil)
109
+ owner = options[:owner]
110
+ type = options[:type]
111
+ requester_id = @local_config[:user_id]
112
+ if title
113
+ name = title
114
+ owner = owner || @local_config[:user_name]
115
+ type = task_type_or_nil(owner) || task_type_or_nil(type) || 'feature'
116
+ else
117
+ title("Let's create a new task:")
118
+ name = ask("Name for the new task:")
119
+ end
120
+
121
+ owner = @client.find_member(owner).person.id if owner.kind_of?(String)
122
+
123
+ unless owner
124
+ if ask('Do you want to assign it now? (y/n)').downcase == 'y'
125
+ members = @client.get_members
126
+ table = PersonsTable.new(members.map(&:person))
127
+ owner = select("Please select a member to assign him the task.", table).id
128
+ else
129
+ owner = nil
130
+ end
131
+ requester = @local_config[:user_name]
132
+ type = ask('Type? (c)hore, (b)ug, anything else for feature)')
133
+ end
134
+
135
+ type = case type
136
+ when 'c', 'chore'
137
+ 'chore'
138
+ when 'b', 'bug'
139
+ 'bug'
140
+ else
141
+ 'feature'
142
+ end
143
+
144
+ owner_ids = [owner]
145
+ # did you do a -m so you can add a description?
146
+ if options[:m]
147
+ editor = ENV.fetch('EDITOR') { 'vi' }
148
+ temp_path = "/tmp/editor-#{ Process.pid }.txt"
149
+ system "#{ editor } #{ temp_path }"
150
+
151
+ description = File.read(temp_path)
152
+ end
153
+
154
+ story = @client.create_story(
155
+ name: name,
156
+ owner_ids: owner_ids,
157
+ requested_by_id: requester_id,
158
+ story_type: type,
159
+ description: description
160
+ )
161
+ congrats("#{type} for #{owner} open #{story.url}")
162
+ show_story(story)
163
+ end
164
+
165
+ desc "find [query] " ,"looks in your stories by title and presents it"
166
+ def find(query)
167
+ story = select_story_from_paginated_table do |page|
168
+ @client.get_stories(filter: query, page: page)
169
+ end
170
+ show_story(story)
171
+ end
172
+
173
+ desc "updates","shows number recent activity from your current project"
174
+ def updates
175
+ activities = @client.get_activities
176
+ tasks = @client.get_my_work
177
+ title("Recent Activity on #{project_to_s}")
178
+ activities.each do |activity|
179
+ show_activity(activity, tasks)
180
+ end
181
+ end
182
+ end
183
+ end
@@ -7,6 +7,7 @@ module PT
7
7
 
8
8
  STORY_FIELDS=':default,requested_by,owners,tasks,comments(:default,person,file_attachments)'
9
9
 
10
+ attr_reader :config, :project
10
11
 
11
12
  def self.get_api_token(email, password)
12
13
  PivotalAPI::Me.retrieve(email, password)
@@ -14,9 +15,10 @@ module PT
14
15
  raise PT::InputError.new("Bad email/password combination.")
15
16
  end
16
17
 
17
- def initialize(token)
18
+ def initialize(token, local_config=nil)
18
19
  @client = TrackerApi::Client.new(token: token)
19
- @project = nil
20
+ @config = local_config
21
+ @project = @client.project(local_config[:project_id]) if local_config
20
22
  end
21
23
 
22
24
  def get_project(project_id)
@@ -28,7 +30,7 @@ module PT
28
30
  @client.projects
29
31
  end
30
32
 
31
- def get_membership(project, email)
33
+ def get_membership(email)
32
34
  PivotalTracker::Membership.all(project).select{ |m| m.email == email }.first
33
35
  end
34
36
 
@@ -40,117 +42,125 @@ module PT
40
42
  PivotalTracker::Iteration.current(project)
41
43
  end
42
44
 
43
- def get_activities(project, limit)
45
+ def get_activities
44
46
  project.activity
45
47
  end
46
48
 
47
- def get_work(project)
49
+ def get_work
48
50
  project.stories(filter: 'state:unscheduled,unstarted,started', fields: STORY_FIELDS )
49
51
  end
50
52
 
51
- def get_my_work(project, user_name)
52
- project.stories(filter: "owner:#{user_name} -state:accepted", limit: 50, fields: STORY_FIELDS)
53
+ def get_my_work
54
+ project.stories(filter: "owner:#{config[:user_name]} -state:accepted", limit: 50, fields: STORY_FIELDS)
53
55
  end
54
56
 
55
- def search_for_story(project, query)
56
- project.stories(filter: query.to_s ,fields: STORY_FIELDS)
57
+ def search_for_story(query, params={})
58
+ params[:filter] = "#{query}"
59
+ get_stories(params)
57
60
  end
58
61
 
59
- def get_task_by_id(project, id)
62
+ def get_task_by_id(id)
60
63
  project.story(id, fields: STORY_FIELDS)
61
64
  end
62
65
  alias :get_story :get_task_by_id
63
66
 
64
- def get_my_open_tasks(project, user_name)
65
- project.stories filter: "owner:#{user_name}", fields: STORY_FIELDS
67
+ def get_my_open_tasks(params={})
68
+ params[:filter] = "owner:#{config[:user_name]}"
69
+ get_stories(params)
66
70
  end
67
71
 
68
- def get_my_tasks_to_estimate(project, user_name)
69
- project.stories( filter: "owner:#{user_name} type:feature estimate:-1", fields: STORY_FIELDS)
72
+ def get_stories_to_estimate(params={})
73
+ params[:filter] = "owner:#{config[:user_name]} type:feature estimate:-1"
74
+ get_stories(params)
70
75
  end
71
76
 
72
- def get_my_tasks_to_start(project, user_name)
73
- tasks = project.stories filter: "owner:#{user_name} state:unscheduled,rejected,unstarted", limit: 50, fields: STORY_FIELDS
74
- tasks.reject{ |t| (t.story_type == 'feature') && (t.estimate == -1) }
77
+ def get_stories_to_start(params={})
78
+ params[:filter] = "owner:#{config[:user_name]} type:feature,bug state:unscheduled,rejected,unstarted"
79
+ tasks = get_stories(params)
80
+ tasks.reject{ |t| (t.story_type == 'feature') && (!t.estimate) }
75
81
  end
76
82
 
77
- def get_my_tasks_to_finish(project, user_name)
78
- project.stories filter: "owner:#{user_name} -state:finished,delivered,accepted,rejected", limit: 50, fields: STORY_FIELDS
83
+ def get_stories_to_finish(params={})
84
+ params[:filter] = "owner:#{config[:user_name]} -state:unscheduled,rejected"
85
+ get_stories(params)
79
86
  end
80
87
 
81
- def get_my_tasks_to_deliver(project, user_name)
82
- project.stories filter: "owner:#{user_name} -state:delivered,accepted,rejected", limit: 50, fields: STORY_FIELDS
88
+ def get_stories_to_deliver(params={})
89
+ params[:filter] = "owner:#{config[:user_name]} -state:delivered,accepted,rejected"
90
+ get_stories(params)
83
91
  end
84
92
 
85
- def get_my_tasks_to_accept(project, user_name)
86
- project.stories filter: "owner:#{user_name} -state:accepted", limit: 50, fields: STORY_FIELDS
93
+ def get_stories_to_accept(params={})
94
+ params[:filter] = "owner:#{config[:user_name]} -state:accepted"
95
+ get_stories(params)
87
96
  end
88
97
 
89
- def get_my_tasks_to_reject(project, user_name)
90
- project.stories filter: "owner:#{user_name} -state:rejected", limit: 50, fields: STORY_FIELDS
98
+ def get_stories_to_reject(params={})
99
+ params[:filter] = "owner:#{config[:user_name]} -state:rejected"
100
+ get_stories(params)
91
101
  end
92
102
 
93
- def get_tasks_to_assign(project)
94
- project.stories filter: "-state:accepted", limit: 50
103
+ def get_stories_to_assign(params={})
104
+ params[:filter] = "-state:accepted"
105
+ get_stories(params)
95
106
  end
96
107
 
97
- def get_all_stories(project, user_name)
98
- project.stories limit: 20, fields: STORY_FIELDS
108
+ def get_stories(params={})
109
+ limit = config[:limit] || 20
110
+ page = params[:page] || 0
111
+ offset = page*limit
112
+ filter = params[:filter] || '-state=accepted'
113
+ project.stories limit: limit, fields: STORY_FIELDS, auto_paginate: false, offset: offset, filter: filter
99
114
  end
100
115
 
101
116
 
102
- def get_member(project, query)
117
+ def get_member(query)
103
118
  member = project.memberships.select{ |m| m.person.name.downcase.start_with?(query.downcase) || m.person.initials.downcase == query.downcase }
104
119
  member.empty? ? nil : member.first
105
120
  end
106
121
 
107
- def find_member(project, query)
108
- memberships = project.memberships.detect do |m|
122
+ def find_member(query)
123
+ project.memberships.detect do |m|
109
124
  m.person.name.downcase.start_with?(query.downcase) || m.person.initials.downcase == query.downcase
110
125
  end
111
126
  end
112
127
 
113
- def get_members(project)
128
+ def get_members
114
129
  project.memberships fields: ':default,person'
115
130
  end
116
131
 
117
132
 
118
- def mark_task_as(project, task, state)
119
- task = get_story(project, task.id)
133
+ def mark_task_as(task, state)
134
+ task = get_story(task.id)
120
135
  task.current_state = state
121
136
  task.save
122
137
  end
123
138
 
124
- def estimate_task(project, task, points)
125
- task = get_story(project, task.id)
139
+ def estimate_story(task, points)
140
+ task = get_story(task.id)
126
141
  task.estimate = points
127
142
  task.save
128
143
  end
129
144
 
130
- def assign_task(project, task, owner)
131
- task = get_story(project, task.id)
145
+ def assign_task(task, owner)
146
+ task = get_story(task.id)
132
147
  task.add_owner(owner)
133
148
  end
134
149
 
135
- def add_label(project, task, label)
136
- task = get_story(project, task.id)
150
+ def add_label(task, label)
151
+ task = get_story(task.id)
137
152
  task.add_label(label)
138
153
  task.save
139
154
  end
140
155
 
141
- def comment_task(project, task, comment)
142
- task = get_story(project, task.id)
156
+ def comment_task(task, comment)
157
+ task = get_story(task.id)
143
158
  task.create_comment(text: comment)
144
159
  end
145
160
 
146
- def create_task(project, name, owner_ids, task_type)
147
- project.create_story(:name => name, :story_type => task_type, owner_ids: owner_ids)
161
+ def create_story(args)
162
+ project.create_story(args)
148
163
  end
149
164
 
150
- def create_task_with_description(project, name, owner, task_type, description)
151
- project.create_story(:name => name, :story_type => task_type, :description => description)
152
- end
153
-
154
-
155
165
  end
156
166
  end