git_pivotal_tracker_x 0.1.1

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,9 @@
1
+ require 'grit'
2
+ require 'pivotal-tracker'
3
+ require 'optparse'
4
+ require 'fileutils'
5
+
6
+ require 'git_pivotal_tracker/base'
7
+ require 'git_pivotal_tracker/story'
8
+ require 'git_pivotal_tracker/info'
9
+ require 'git_pivotal_tracker/finish'
@@ -0,0 +1,112 @@
1
+ module GitPivotalTracker
2
+ class Base
3
+ GIT_DIR = ENV['GIT_DIR'] || '.git'
4
+
5
+ attr_reader :options, :repository
6
+
7
+ def initialize(*args)
8
+ directories = Dir.pwd.split(::File::SEPARATOR)
9
+ begin
10
+ break if File.directory?(File.join(directories, GIT_DIR))
11
+ end while directories.pop
12
+
13
+ raise "No #{GIT_DIR} directory found" if directories.empty?
14
+ root = File.join(directories, GIT_DIR)
15
+ @repository = Grit::Repo.new(root)
16
+
17
+ new_hook_path = File.join(root, 'hooks', 'commit-msg')
18
+ unless File.executable?(new_hook_path)
19
+ puts "Installing commit-msg hook..."
20
+ old_hook_path = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'commit-msg')
21
+ FileUtils.cp(old_hook_path, new_hook_path, :preserve => true)
22
+ end
23
+
24
+ @options = {}
25
+ parse_gitconfig
26
+ parse_argv(*args)
27
+ end
28
+
29
+ def run!
30
+ unless options[:api_token] && options[:project_id]
31
+ puts "Pivotal Tracker API Token and Project ID are required"
32
+ return 1
33
+ end
34
+
35
+ PivotalTracker::Client.token = options[:api_token]
36
+ PivotalTracker::Client.use_ssl = options[:use_ssl]
37
+
38
+ nil
39
+ end
40
+
41
+ protected
42
+
43
+ def integration_branch
44
+ current_branch_suffix || options[:integration_branch] || 'master'
45
+ end
46
+
47
+ def current_branch_suffix
48
+ if current_branch =~ /.*-\d+?-(.*)/ and @repository.branches.any? { |branch| branch.name == $1 }
49
+ $1
50
+ end
51
+ end
52
+
53
+ def current_branch
54
+ @current_branch ||= repository.head.name
55
+ end
56
+
57
+ def story_id
58
+ if current_branch =~ /-(\d+)-/
59
+ $1
60
+ end
61
+ end
62
+
63
+ def project
64
+ @project ||= PivotalTracker::Project.find(options[:project_id])
65
+ end
66
+
67
+ def story
68
+ @story ||= project.stories.find(story_id)
69
+ end
70
+
71
+ def log(message)
72
+ puts message if options[:verbose]
73
+ end
74
+
75
+ private
76
+
77
+ def parse_gitconfig
78
+ options[:api_token] = repository.config['pivotal.api-token']
79
+ options[:project_id] = repository.config['pivotal.project-id']
80
+ options[:integration_branch] = repository.config['pivotal.integration-branch']
81
+ options[:only_mine] = repository.config['pivotal.only-mine']
82
+ options[:include_rejected] = repository.config['pivotal.include-rejected']
83
+ options[:fast_forward] = repository.config['pivotal.fast-forward']
84
+ options[:rebase] = repository.config['pivotal.rebase']
85
+ options[:full_name] = repository.config['pivotal.full-name'] || repository.config['user.name']
86
+ options[:verbose] = repository.config['pivotal.verbose']
87
+ options[:use_ssl] = repository.config['pivotal.use-ssl']
88
+ options[:delete_branch] = repository.config['pivotal.delete-branch']
89
+ end
90
+
91
+ def parse_argv(*args)
92
+ OptionParser.new do |opts|
93
+ opts.banner = "Usage: git <feature|chore|bug> [options]"
94
+ opts.on("-t", "--api-token=", "Pivotal Tracker API key") { |k| options[:api_token] = k }
95
+ opts.on("-p", "--project-id=", "Pivotal Tracker project id") { |p| options[:project_id] = p }
96
+ opts.on("-b", "--integration-branch=", "The branch to merge finished stories back down onto") { |b| options[:integration_branch] = b }
97
+ opts.on("-n", "--full-name=", "Your Pivotal Tracker full name") { |n| options[:full_name] = n }
98
+
99
+
100
+ opts.on("-D", "--delete-branch", "Delete store branch after merging") { |d| options[:delete_branch] = d }
101
+ opts.on("-I", "--include-rejected", "Include rejected stories as well as unstarted ones") { |i| options[:include_rejected] = i }
102
+ opts.on("-O", "--only-mine", "Only include stories that are assigned to me") { |o| options[:only_mine] = o }
103
+ opts.on("-F", "--fast-forward", "Merge topic branch with fast forward") { |f| options[:fast_forward] = f }
104
+ opts.on("-S", "--use-ssl", "Use SSL for connection to Pivotal Tracker") { |s| options[:use_ssl] = s }
105
+ opts.on("-R", "--rebase", "Fetch and rebase the integration branch before merging") { |r| options[:rebase] = r }
106
+ opts.on("-V", "--verbose", "Verbose command logging") { |v| options[:verbose] = v }
107
+ opts.on_tail("-h", "--help", "This usage guide") { put opts.to_s; exit 0 }
108
+ end.parse!(args)
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,51 @@
1
+ module GitPivotalTracker
2
+ class Finish < Base
3
+
4
+ def run!
5
+ return 1 if super
6
+
7
+ unless story_id
8
+ puts "Branch name must contain a Pivotal Tracker story id"
9
+ return 1
10
+ end
11
+
12
+ if options[:rebase]
13
+ puts "Fetching origin and rebasing #{current_branch}"
14
+ log repository.git.checkout({:raise => true}, integration_branch)
15
+ log repository.git.pull({:raise => true})
16
+ log repository.git.rebase({:raise => true}, integration_branch, current_branch)
17
+ end
18
+
19
+ puts "Merging #{current_branch} into #{integration_branch}"
20
+ log repository.git.checkout({:raise => true}, integration_branch)
21
+
22
+ merge_options = {:raise => true}
23
+ merge_options[:no_ff] = true unless options[:fast_forward]
24
+ log repository.git.merge(merge_options, current_branch)
25
+
26
+ puts "Pushing #{integration_branch}"
27
+ log repository.git.push({:raise => true}, 'origin', integration_branch)
28
+
29
+ puts "Marking Story #{story_id} as finished..."
30
+ if story.update(:current_state => finished_state)
31
+ delete_current_branch if options[:delete_branch]
32
+ puts "Success"
33
+ return 0
34
+ else
35
+ puts "Unable to mark Story #{story_id} as finished"
36
+ return 1
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def finished_state
43
+ story.story_type == "chore" ? "accepted" : "finished"
44
+ end
45
+
46
+ def delete_current_branch
47
+ puts "Deleting #{current_branch}"
48
+ log repository.git.branch({:raise => true, :d => true}, current_branch)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,19 @@
1
+ module GitPivotalTracker
2
+ class Info < Base
3
+
4
+ def run!
5
+ return 1 if super
6
+
7
+ unless story_id
8
+ puts "Branch name must contain a Pivotal Tracker story id"
9
+ return 1
10
+ end
11
+
12
+ puts "URL: #{story.url}"
13
+ puts "Story: #{story.name}"
14
+ puts "Description: #{story.description}"
15
+
16
+ return 0
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,59 @@
1
+ module GitPivotalTracker
2
+ class Story < Base
3
+
4
+ def run!
5
+ return 1 if super
6
+
7
+ puts "Retrieving latest #{type} from Pivotal Tracker"
8
+
9
+ unless story = fetch_story
10
+ puts "No #{type} available!"
11
+ return 1
12
+ end
13
+
14
+ puts "URL: #{story.url}"
15
+ puts "Story: #{story.name}"
16
+
17
+ print "Enter branch name [#{branch_suffix story}]: "
18
+ suffix = gets.chomp
19
+ suffix = branch_suffix(story) if suffix == ""
20
+
21
+ branch = "#{story.story_type}-#{story.id}-#{suffix}"
22
+ puts "Checking out a new branch '#{branch}'"
23
+ log repository.git.checkout({:b => true, :raise => true}, branch)
24
+
25
+ puts "Updating #{type} status in Pivotal Tracker..."
26
+ if story.update(:owned_by => options[:full_name], :current_state => :started)
27
+ puts "Success"
28
+ return 0
29
+ else
30
+ puts "Unable to mark #{type} as started"
31
+ return 1
32
+ end
33
+ end
34
+
35
+ def type
36
+ self.class.name.downcase.split(/::/).last
37
+ end
38
+
39
+ private
40
+
41
+ def fetch_story
42
+ state = options[:include_rejected] ? "unstarted,rejected" : "unstarted"
43
+ conditions = { :current_state => state, :limit => 1 }
44
+ conditions[:owned_by] = "\"#{options[:full_name]}\"" if options[:only_mine]
45
+ conditions[:story_type] = type unless type == 'story'
46
+ project.stories.all(conditions).first
47
+ end
48
+
49
+ def branch_suffix(story)
50
+ story.name.sub(/^\W+/, '').sub(/\W+$/, '').gsub(/\W+/, '_').downcase
51
+ end
52
+ end
53
+
54
+ class Bug < Story; end
55
+
56
+ class Feature < Story; end
57
+
58
+ class Chore < Story; end
59
+ end
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0"?>
2
+ <story>
3
+ <name> Pause the film!</name>
4
+ <description>As a moderator,
5
+ I can pause the film
6
+ In order to allow another activity to take place (discussion, etc).</description>
7
+ <story_type>feature</story_type>
8
+ <estimate>2</estimate>
9
+ <current_state>finished</current_state>
10
+ <requested_by>Ben Lindsey</requested_by>
11
+ <owned_by>Ben Lindsey</owned_by>
12
+ <labels>moderate,2_needs_design</labels>
13
+ <project_id>123</project_id>
14
+ <other_id></other_id>
15
+ <integration_id></integration_id>
16
+ <created_at>2011-07-14T23:06:28+00:00</created_at>
17
+ </story>
@@ -0,0 +1,3 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <stories type="array" count="0" total="2" limit="0">
3
+ </stories>
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <stories type="array" count="1" total="2" limit="1">
3
+ <story>
4
+ <id type="integer">1234567890</id>
5
+ <project_id type="integer">123</project_id>
6
+ <story_type>feature</story_type>
7
+ <url>http://www.pivotaltracker.com/story/show/1234567890</url>
8
+ <estimate type="integer">2</estimate>
9
+ <current_state>unstarted</current_state>
10
+ <description>As a moderator,
11
+ I can pause the film
12
+ In order to allow another activity to take place (discussion, etc).</description>
13
+ <name> Pause the film!</name>
14
+ <requested_by>Ben Lindsey</requested_by>
15
+ <created_at type="datetime">2011/07/14 23:06:28 UTC</created_at>
16
+ <updated_at type="datetime">2011/07/14 23:13:24 UTC</updated_at>
17
+ <labels>moderate,2_needs_design</labels>
18
+ </story>
19
+ </stories>
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project>
3
+ <id>123</id>
4
+ <name>Test Project</name>
5
+ <iteration_length type="integer">1</iteration_length>
6
+ <week_start_day>Monday</week_start_day>
7
+ <point_scale>0,1,2,3,5,8</point_scale>
8
+ <account>Carbon Five</account>
9
+ <start_date type="date">2011/07/18</start_date>
10
+ <first_iteration_start_time type="datetime">2011/07/18 07:00:00 UTC</first_iteration_start_time>
11
+ <current_iteration_number type="integer">1</current_iteration_number>
12
+ <enable_tasks type="boolean">true</enable_tasks>
13
+ <velocity_scheme>Average of 3 iterations</velocity_scheme>
14
+ <current_velocity>10</current_velocity>
15
+ <initial_velocity>10</initial_velocity>
16
+ <number_of_done_iterations_to_show>12</number_of_done_iterations_to_show>
17
+ <labels>1_needs_definition,2_needs_design,discuss,moderate</labels>
18
+ <last_activity_at type="datetime">2011/07/14 23:16:29 UTC</last_activity_at>
19
+ <allow_attachments>true</allow_attachments>
20
+ <public>false</public>
21
+ <use_https>false</use_https>
22
+ <bugs_and_chores_are_estimatable>false</bugs_and_chores_are_estimatable>
23
+ <commit_mode>false</commit_mode>
24
+ <memberships type="array">
25
+ <membership>
26
+ <id>1058581</id>
27
+ <person>
28
+ <email>ben@carbonfive.com</email>
29
+ <name>Ben Lindsey</name>
30
+ <initials>BL</initials>
31
+ </person>
32
+ <role>Owner</role>
33
+ </membership>
34
+ </memberships>
35
+ <integrations type="array">
36
+ </integrations>
37
+ </project>
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0"?>
2
+ <story>
3
+ <name> Pause the film!</name>
4
+ <description>As a moderator,
5
+ I can pause the film
6
+ In order to allow another activity to take place (discussion, etc).</description>
7
+ <story_type>feature</story_type>
8
+ <estimate>2</estimate>
9
+ <current_state>started</current_state>
10
+ <requested_by>Ben Lindsey</requested_by>
11
+ <owned_by>Ben Lindsey</owned_by>
12
+ <labels>moderate,2_needs_design</labels>
13
+ <project_id>123</project_id>
14
+ <other_id></other_id>
15
+ <integration_id></integration_id>
16
+ <created_at>2011-07-14T23:06:28+00:00</created_at>
17
+ </story>
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0"?>
2
+ <story>
3
+ <id>1234567890</id>
4
+ <name> Pause the film!</name>
5
+ <description>As a moderator,
6
+ I can pause the film
7
+ In order to allow another activity to take place (discussion, etc).</description>
8
+ <story_type>feature</story_type>
9
+ <estimate>2</estimate>
10
+ <current_state>started</current_state>
11
+ <requested_by>Ben Lindsey</requested_by>
12
+ <owned_by>Ben Lindsey</owned_by>
13
+ <labels>moderate,2_needs_design</labels>
14
+ <project_id>123</project_id>
15
+ <other_id></other_id>
16
+ <integration_id></integration_id>
17
+ <created_at>2011-07-14T23:06:28+00:00</created_at>
18
+ </story>
@@ -0,0 +1,235 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe GitPivotalTracker::Base do
4
+ describe "#parse_argv" do
5
+
6
+ context "by default" do
7
+ before do
8
+ stub_git_config
9
+ subject = GitPivotalTracker::Base.new
10
+ end
11
+
12
+ it "leaves integration_branch nil" do
13
+ subject.options[:integration_branch].should be_nil
14
+ end
15
+
16
+ it "leaves fast_forward nil" do
17
+ subject.options[:fast_forward].should be_nil
18
+ end
19
+
20
+ it "leaves rebase nil" do
21
+ subject.options[:rebase].should be_nil
22
+ end
23
+
24
+ it "leaves verbose nil" do
25
+ subject.options[:verbose].should be_nil
26
+ end
27
+
28
+ it "leaves use_ssl nil" do
29
+ subject.options[:use_ssl].should be_nil
30
+ end
31
+
32
+ it "leaves full_name nil" do
33
+ subject.options[:full_name].should be_nil
34
+ end
35
+
36
+ it "leaves include_rejected nil" do
37
+ subject.options[:include_rejected].should be_nil
38
+ end
39
+
40
+ it "leaves only_mine nil" do
41
+ subject.options[:only_mine].should be_nil
42
+ end
43
+
44
+ it "leaves delete_branch nil" do
45
+ subject.options[:delete_branch].should be_nil
46
+ end
47
+ end
48
+
49
+ it "sets the api_token" do
50
+ GitPivotalTracker::Base.new("--api-token", "8a8a8a8").options[:api_token].should == '8a8a8a8'
51
+ GitPivotalTracker::Base.new("-t", "8a8a8a8").options[:api_token].should == '8a8a8a8'
52
+ end
53
+
54
+ it "sets the project_id" do
55
+ GitPivotalTracker::Base.new("--project-id", "123").options[:project_id].should == '123'
56
+ GitPivotalTracker::Base.new("-p", "123").options[:project_id].should == '123'
57
+ end
58
+
59
+ it "sets the integration_branch" do
60
+ GitPivotalTracker::Base.new("--integration-branch", "development").options[:integration_branch].should == 'development'
61
+ GitPivotalTracker::Base.new("-b", "development").options[:integration_branch].should == 'development'
62
+ end
63
+
64
+ it "sets full_name" do
65
+ GitPivotalTracker::Base.new("--full-name", "Full Name").options[:full_name].should == 'Full Name'
66
+ GitPivotalTracker::Base.new("-n", "Full Name").options[:full_name].should == 'Full Name'
67
+ end
68
+
69
+ it "sets include_rejected" do
70
+ GitPivotalTracker::Base.new("--include-rejected").options[:include_rejected].should be
71
+ GitPivotalTracker::Base.new("-I").options[:include_rejected].should be
72
+ end
73
+
74
+ it "sets only_mine" do
75
+ GitPivotalTracker::Base.new("--only-mine").options[:only_mine].should be
76
+ GitPivotalTracker::Base.new("-O").options[:only_mine].should be
77
+ end
78
+
79
+ it "sets fast_forward" do
80
+ GitPivotalTracker::Base.new("--fast-forward").options[:fast_forward].should be
81
+ GitPivotalTracker::Base.new("-F").options[:fast_forward].should be
82
+ end
83
+
84
+ it "sets use_ssl" do
85
+ GitPivotalTracker::Base.new("--use-ssl").options[:use_ssl].should be
86
+ GitPivotalTracker::Base.new("-S").options[:use_ssl].should be
87
+ end
88
+
89
+ it "sets rebase" do
90
+ GitPivotalTracker::Base.new("--rebase").options[:rebase].should be
91
+ GitPivotalTracker::Base.new("-R").options[:rebase].should be
92
+ end
93
+
94
+ it "sets verbose" do
95
+ GitPivotalTracker::Base.new("--verbose").options[:verbose].should be
96
+ GitPivotalTracker::Base.new("-V").options[:verbose].should be
97
+ end
98
+
99
+ it "sets delete_branch" do
100
+ GitPivotalTracker::Base.new("--delete-branch").options[:delete_branch].should be
101
+ GitPivotalTracker::Base.new("-D").options[:delete_branch].should be
102
+ end
103
+ end
104
+
105
+ describe "#parse_gitconfig" do
106
+ context "with a full-name" do
107
+ before do
108
+ stub_git_config 'pivotal.full-name' => 'Full Name', 'user.name' => 'User Name'
109
+ subject = GitPivotalTracker::Base.new
110
+ end
111
+
112
+ it "sets the full_name to the full name" do
113
+ subject.options[:full_name].should == 'Full Name'
114
+ end
115
+ end
116
+
117
+ context "with no full-name" do
118
+ before do
119
+ stub_git_config({
120
+ 'user.name' => 'User Name',
121
+ 'pivotal.integration-branch' => 'development',
122
+ 'pivotal.only-mine' => 1,
123
+ 'pivotal.include-rejected' => 1,
124
+ 'pivotal.fast-forward' => 1,
125
+ 'pivotal.rebase' => 1,
126
+ 'pivotal.verbose' => 1,
127
+ 'pivotal.use-ssl' => 1
128
+ })
129
+ subject = GitPivotalTracker::Base.new
130
+ end
131
+
132
+ it "sets use_ssl" do
133
+ subject.options[:use_ssl].should be
134
+ end
135
+
136
+ it "sets the api_token" do
137
+ subject.options[:api_token].should == '8a8a8a8'
138
+ end
139
+
140
+ it "sets the project_id" do
141
+ subject.options[:project_id].should == '123'
142
+ end
143
+
144
+ it "sets only_mine" do
145
+ subject.options[:only_mine].should be
146
+ end
147
+
148
+ it "sets include_rejected" do
149
+ subject.options[:include_rejected].should be
150
+ end
151
+
152
+ it "sets fast_forward" do
153
+ subject.options[:fast_forward].should be
154
+ end
155
+
156
+ it "sets rebase" do
157
+ subject.options[:rebase].should be
158
+ end
159
+
160
+ it "sets verbose" do
161
+ subject.options[:verbose].should be
162
+ end
163
+
164
+ it "sets the full_name to the user name" do
165
+ subject.options[:full_name].should == 'User Name'
166
+ end
167
+ end
168
+ end
169
+
170
+ describe ".new" do
171
+ context "given an invalid git root" do
172
+ before do
173
+ @current_dir = Dir.pwd
174
+ Dir.chdir("/")
175
+ end
176
+
177
+ it "fails to initialize" do
178
+ expect { GitPivotalTracker::Base.new }.to raise_error "No .git directory found"
179
+ end
180
+
181
+ after do
182
+ Dir.chdir @current_dir
183
+ end
184
+ end
185
+
186
+ context "given no commit-msg hook" do
187
+ let(:file_name) { ".git/hooks/commit-msg" }
188
+
189
+ before do
190
+ File.delete file_name if File.exists? file_name
191
+ GitPivotalTracker::Base.new
192
+ end
193
+
194
+ it "installs the hook" do
195
+ File.executable?(file_name).should be
196
+ end
197
+ end
198
+ end
199
+
200
+ describe "#run!" do
201
+ context "given a config with an api token and a project id" do
202
+ before do
203
+ stub_git_config
204
+ subject = GitPivotalTracker::Base.new
205
+ PivotalTracker::Client.should_receive(:token=).with('8a8a8a8')
206
+ end
207
+
208
+ it "succeeds" do
209
+ subject.run!.should be_nil
210
+ end
211
+ end
212
+
213
+ context "given the config has no api token" do
214
+ before do
215
+ stub_git_config 'pivotal.api-token' => nil
216
+ subject = GitPivotalTracker::Base.new
217
+ end
218
+
219
+ it "fails" do
220
+ subject.run!.should == 1
221
+ end
222
+ end
223
+
224
+ context "given the config has no project id" do
225
+ before do
226
+ stub_git_config 'pivotal.project-id' => nil
227
+ subject = GitPivotalTracker::Base.new
228
+ end
229
+
230
+ it "fails" do
231
+ subject.run!.should == 1
232
+ end
233
+ end
234
+ end
235
+ end