git_pivotal_tracker_x 0.1.1

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