octopolo 0.3.6 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,16 +1,10 @@
1
+ require_relative "issue_creator"
1
2
  require_relative "../renderer"
2
3
  require 'tempfile'
3
4
 
4
5
  module Octopolo
5
6
  module GitHub
6
- class PullRequestCreator
7
- include ConfigWrapper
8
- # for instantiating the pull request creator
9
- attr_accessor :repo_name
10
- attr_accessor :options
11
- # for caputuring the created pull request information
12
- attr_accessor :number
13
- attr_accessor :pull_request_data
7
+ class PullRequestCreator < IssueCreator
14
8
 
15
9
  # Public: Create a pull request for the given repo with the given options
16
10
  #
@@ -20,23 +14,7 @@ module Octopolo
20
14
  # destination_branch: Which branch to merge into
21
15
  # source_branch: Which branch to be merged
22
16
  def initialize repo_name, options
23
- self.repo_name = repo_name
24
- self.options = options
25
- end
26
-
27
- # Public: Create a pull request for the given repo with the given options
28
- #
29
- # repo_name - Full name ("account/repo") of the repo in question
30
- # options - Hash of pull request information
31
- # title: Title of the pull request
32
- # destination_branch: Which branch to merge into
33
- # source_branch: Which branch to be merged
34
- #
35
- # Returns the PullRequestCreator instance
36
- def self.perform repo_name, options
37
- new(repo_name, options).tap do |creator|
38
- creator.perform
39
- end
17
+ super(repo_name, options)
40
18
  end
41
19
 
42
20
  # Public: Create the pull request
@@ -47,21 +25,11 @@ module Octopolo
47
25
  result = GitHub.create_pull_request(repo_name, destination_branch, source_branch, title, body)
48
26
  # capture the information
49
27
  self.number = result.number
50
- self.pull_request_data = result
28
+ self.data = result
51
29
  rescue => e
52
30
  raise CannotCreate, e.message
53
31
  end
54
32
 
55
- # Public: The created pull request's details
56
- def pull_request_data
57
- @pull_request_data || raise(NotYetCreated)
58
- end
59
-
60
- # Public: The created pull request's number
61
- def number
62
- @number || raise(NotYetCreated)
63
- end
64
-
65
33
  # Public: Branch to merge the pull request into
66
34
  #
67
35
  # Returns a String with the branch name
@@ -76,74 +44,21 @@ module Octopolo
76
44
  options[:source_branch] || raise(MissingAttribute)
77
45
  end
78
46
 
79
- # Public: Title of the pull request
80
- #
81
- # Returns a String with the title
82
- def title
83
- options[:title] || raise(MissingAttribute)
84
- end
85
-
86
- # Public: The Pivotal Tracker story IDs associated with the pull request
87
- #
88
- # Returns an Array of Strings
89
- def pivotal_ids
90
- options[:pivotal_ids] || []
91
- end
92
-
93
- # Public: Jira Issue IDs associated with the pull request
47
+ # Public: Rendering template for body property
94
48
  #
95
- # Returns an Array of Strings
96
- def jira_ids
97
- options[:jira_ids] || []
49
+ # Returns Name of template file
50
+ def renderer_template
51
+ Renderer::PULL_REQUEST_BODY
98
52
  end
99
53
 
100
- # Public: Jira Url associated with the pull request
101
- #
102
- # Returns Jira Url
103
- def jira_url
104
- config.jira_url
105
- end
106
54
 
107
- # Public: The body (primary copy) of the pull request
55
+ # Public: Temporary file for body editing
108
56
  #
109
- # Returns a String
110
- def body
111
- output = Renderer.render Renderer::PULL_REQUEST_BODY, body_locals
112
- output = edit_body(output) if options[:editor]
113
- output
114
- end
115
-
116
- def edit_body(body)
117
- return body unless ENV['EDITOR']
118
-
119
- # Open the file, write the contents, and close it
120
- tempfile = Tempfile.new(['octopolo_pull_request', '.md'])
121
- tempfile.write(body)
122
- tempfile.close
123
-
124
- # Allow the user to edit the file
125
- system "#{ENV['EDITOR']} #{tempfile.path}"
126
-
127
- # Reopen the file, read the contents, and delete it
128
- tempfile.open
129
- output = tempfile.read
130
- tempfile.unlink
131
-
132
- output
133
- end
134
-
135
- # Public: The local variables to pass into the template
136
- def body_locals
137
- {
138
- pivotal_ids: pivotal_ids,
139
- jira_ids: jira_ids,
140
- jira_url: jira_url,
141
- }
57
+ # Returns Name of temporary file
58
+ def body_edit_temp_name
59
+ 'octopolo_pull_request'
142
60
  end
143
61
 
144
- MissingAttribute = Class.new(StandardError)
145
- NotYetCreated = Class.new(StandardError)
146
- CannotCreate = Class.new(StandardError)
147
62
  end
148
63
  end
149
64
  end
@@ -5,6 +5,7 @@ module Octopolo
5
5
  class Renderer
6
6
  # Constants for the template file names
7
7
  PULL_REQUEST_BODY = "pull_request_body"
8
+ ISSUE_BODY = "issue_body"
8
9
 
9
10
  # Public: Render a given ERB template
10
11
  #
@@ -0,0 +1,140 @@
1
+ require_relative "../scripts"
2
+ require_relative "../github"
3
+ require_relative "../github/issue"
4
+ require_relative "../github/issue_creator"
5
+ require_relative "../pivotal/story_commenter"
6
+ require_relative "../jira/story_commenter"
7
+
8
+ module Octopolo
9
+ module Scripts
10
+ class Issue
11
+ include CLIWrapper
12
+ include ConfigWrapper
13
+ include GitWrapper
14
+
15
+ attr_accessor :title
16
+ attr_accessor :issue
17
+ attr_accessor :pivotal_ids
18
+ attr_accessor :jira_ids
19
+ attr_accessor :label
20
+ attr_accessor :options
21
+
22
+ def self.execute(options={})
23
+ new(options).execute
24
+ end
25
+
26
+ def initialize(options={})
27
+ @options = options
28
+ end
29
+
30
+ def execute
31
+ GitHub.connect do
32
+ ask_questionaire
33
+ create_issue
34
+ update_pivotal
35
+ update_jira
36
+ update_label
37
+ open_in_browser
38
+ end
39
+ end
40
+
41
+ # Protected: Ask questions to create an issue
42
+ def ask_questionaire
43
+ announce
44
+ ask_title
45
+ ask_label
46
+ ask_pivotal_ids if config.use_pivotal_tracker
47
+ ask_jira_ids if config.use_jira
48
+ end
49
+ protected :ask_questionaire
50
+
51
+ # Protected: Announce to the user the branches the issue will reference
52
+ def announce
53
+ cli.say "Preparing an issue for #{config.github_repo}."
54
+ end
55
+ protected :announce
56
+
57
+ # Protected: Ask for a title for the issue
58
+ def ask_title
59
+ self.title = cli.prompt "Title:"
60
+ end
61
+ protected :ask_title
62
+
63
+ # Protected: Ask for a label for the issue
64
+ def ask_label
65
+ choices = Octopolo::GitHub::Label.get_names(label_choices).concat(["None"])
66
+ response = cli.ask(label_prompt, choices)
67
+ self.label = Hash[label_choices.map{|l| [l.name,l]}][response]
68
+ end
69
+ protected :ask_label
70
+
71
+ # Protected: Ask for a Pivotal Tracker story IDs
72
+ def ask_pivotal_ids
73
+ self.pivotal_ids = cli.prompt("Pivotal Tracker story ID(s):").split(/[\s,]+/)
74
+ end
75
+ protected :ask_pivotal_ids
76
+
77
+ # Protected: Ask for a Pivotal Tracker story IDs
78
+ def ask_jira_ids
79
+ self.jira_ids = cli.prompt("Jira story ID(s):").split(/[\s,]+/)
80
+ end
81
+ protected :ask_pivotal_ids
82
+
83
+ # Protected: Create the issue
84
+ #
85
+ # Returns a GitHub::Issue object
86
+ def create_issue
87
+ self.issue = GitHub::Issue.create config.github_repo, issue_attributes
88
+ end
89
+ protected :create_issue
90
+
91
+ # Protected: The attributes to send to create the issue
92
+ #
93
+ # Returns a Hash
94
+ def issue_attributes
95
+ {
96
+ title: title,
97
+ pivotal_ids: pivotal_ids,
98
+ jira_ids: jira_ids,
99
+ editor: options[:editor]
100
+ }
101
+ end
102
+ protected :issue_attributes
103
+
104
+ # Protected: Handle the newly created issue
105
+ def open_in_browser
106
+ cli.copy_to_clipboard issue.url
107
+ cli.open issue.url
108
+ end
109
+ protected :open_in_browser
110
+
111
+ def label_prompt
112
+ 'Label:'
113
+ end
114
+
115
+ def label_choices
116
+ Octopolo::GitHub::Label.all
117
+ end
118
+
119
+ def update_pivotal
120
+ pivotal_ids.each do |story_id|
121
+ Pivotal::StoryCommenter.new(story_id, issue.url).perform
122
+ end if pivotal_ids
123
+ end
124
+ protected :update_pivotal
125
+
126
+ def update_jira
127
+ jira_ids.each do |story_id|
128
+ Jira::StoryCommenter.new(story_id, issue.url).perform
129
+ end if jira_ids
130
+ end
131
+ protected :update_jira
132
+
133
+ def update_label
134
+ issue.add_labels(label) if label
135
+ end
136
+ protected :update_label
137
+
138
+ end
139
+ end
140
+ end
@@ -1,22 +1,16 @@
1
1
  require_relative "../scripts"
2
+ require_relative "../scripts/issue"
2
3
  require_relative "../github"
3
4
  require_relative "../pivotal/story_commenter"
4
5
  require_relative "../jira/story_commenter"
5
6
 
6
7
  module Octopolo
7
8
  module Scripts
8
- class PullRequest
9
- include CLIWrapper
10
- include ConfigWrapper
11
- include GitWrapper
12
-
13
- attr_accessor :title
9
+ class PullRequest < Issue
14
10
  attr_accessor :pull_request
15
- attr_accessor :pivotal_ids
16
- attr_accessor :jira_ids
17
11
  attr_accessor :destination_branch
18
- attr_accessor :label
19
- attr_accessor :options
12
+
13
+ alias_method :issue, :pull_request
20
14
 
21
15
  def self.execute(destination_branch=nil, options={})
22
16
  new(destination_branch, options).execute
@@ -38,7 +32,7 @@ module Octopolo
38
32
  update_pivotal
39
33
  update_jira
40
34
  update_label
41
- open_pull_request
35
+ open_in_browser
42
36
  end
43
37
  end
44
38
 
@@ -66,32 +60,6 @@ module Octopolo
66
60
  end
67
61
  private :alert_reserved_and_exit
68
62
 
69
- # Private: Ask for a title for the pull request
70
- def ask_title
71
- self.title = cli.prompt "Title:"
72
- end
73
- private :ask_title
74
-
75
- # Private: Ask for a label for the pull request
76
- def ask_label
77
- choices = Octopolo::GitHub::Label.get_names(label_choices).concat(["None"])
78
- response = cli.ask(label_prompt, choices)
79
- self.label = Hash[label_choices.map{|l| [l.name,l]}][response]
80
- end
81
- private :ask_label
82
-
83
- # Private: Ask for a Pivotal Tracker story IDs
84
- def ask_pivotal_ids
85
- self.pivotal_ids = cli.prompt("Pivotal Tracker story ID(s):").split(/[\s,]+/)
86
- end
87
- private :ask_pivotal_ids
88
-
89
- # Private: Ask for a Pivotal Tracker story IDs
90
- def ask_jira_ids
91
- self.jira_ids = cli.prompt("Jira story ID(s):").split(/[\s,]+/)
92
- end
93
- private :ask_pivotal_ids
94
-
95
63
  # Private: Create the pull request
96
64
  #
97
65
  # Returns a GitHub::PullRequest object
@@ -115,40 +83,6 @@ module Octopolo
115
83
  end
116
84
  private :pull_request_attributes
117
85
 
118
- # Private: Handle the newly created pull request
119
- def open_pull_request
120
- cli.copy_to_clipboard pull_request.url
121
- cli.open pull_request.url
122
- end
123
- private :open_pull_request
124
-
125
- def label_prompt
126
- 'Label:'
127
- end
128
-
129
- def label_choices
130
- Octopolo::GitHub::Label.all
131
- end
132
-
133
- def update_pivotal
134
- pivotal_ids.each do |story_id|
135
- Pivotal::StoryCommenter.new(story_id, pull_request.url).perform
136
- end if pivotal_ids
137
- end
138
- private :update_pivotal
139
-
140
- def update_jira
141
- jira_ids.each do |story_id|
142
- Jira::StoryCommenter.new(story_id, pull_request.url).perform
143
- end if jira_ids
144
- end
145
- private :update_jira
146
-
147
- def update_label
148
- pull_request.add_labels(label) if label
149
- end
150
- private :update_label
151
-
152
86
  end
153
87
  end
154
88
  end
@@ -0,0 +1,22 @@
1
+ <%= description %>
2
+
3
+ Deploy Plan
4
+ -----------
5
+ Describe how this change will be deployed.
6
+
7
+ Rollback Plan
8
+ -------------
9
+ Describe how this change can be rolled back.
10
+
11
+ URLs
12
+ ----
13
+ <% pivotal_ids.each do |pivotal_id| -%>
14
+ * [pivotal tracker story <%= pivotal_id %>](https://www.pivotaltracker.com/story/show/<%= pivotal_id %>)
15
+ <% end -%>
16
+ <% jira_ids.each do |jira_id| -%>
17
+ * [Jira issue <%= jira_id %>](<%= jira_url %>/browse/<%= jira_id %>)
18
+ <% end -%>
19
+
20
+ QA Plan
21
+ -------
22
+ Provide a detailed QA plan, or other developers will retain the right to mock you mercilessly.
@@ -1,3 +1,3 @@
1
1
  module Octopolo
2
- VERSION = "0.3.6"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -0,0 +1,217 @@
1
+ require "spec_helper"
2
+ require_relative "../../../lib/octopolo/github/issue_creator"
3
+
4
+ module Octopolo
5
+ module GitHub
6
+ describe IssueCreator do
7
+ let(:creator) { IssueCreator.new repo_name, options }
8
+ let(:repo_name) { "foo/bar" }
9
+ let(:options) { {} }
10
+ let(:title) { "title" }
11
+ let(:body) { "body" }
12
+ let(:pivotal_ids) { %w(123 456) }
13
+ let(:jira_ids) { %w(123 456) }
14
+ let(:jira_url) { "https://example-jira.com" }
15
+
16
+ context ".perform repo_name, options" do
17
+ let(:creator) { stub }
18
+
19
+ it "instantiates a creator and perfoms it" do
20
+ IssueCreator.should_receive(:new).with(repo_name, options) { creator }
21
+ creator.should_receive(:perform)
22
+ IssueCreator.perform(repo_name, options).should == creator
23
+ end
24
+ end
25
+
26
+ context ".new repo_name, options" do
27
+ it "remembers the repo name and options" do
28
+ creator = IssueCreator.new repo_name, options
29
+ creator.repo_name.should == repo_name
30
+ creator.options.should == options
31
+ end
32
+ end
33
+
34
+ context "#perform" do
35
+ let(:data) { stub(:mash, number: 123) }
36
+
37
+ before do
38
+ creator.stub({
39
+ title: title,
40
+ body: body,
41
+ })
42
+ end
43
+
44
+ it "generates the issue with the given details and retains the information" do
45
+ GitHub.should_receive(:create_issue).with(repo_name, title, body, labels: []) { data }
46
+ creator.perform.should == data
47
+ creator.number.should == data.number
48
+ creator.data.should == data
49
+ end
50
+
51
+ it "raises CannotCreate if any exception occurs" do
52
+ GitHub.should_receive(:create_issue).and_raise(Octokit::UnprocessableEntity)
53
+ expect { creator.perform }.to raise_error(IssueCreator::CannotCreate)
54
+ end
55
+ end
56
+
57
+ context "#number" do
58
+ let(:number) { 123 }
59
+
60
+ it "returns the stored issue number" do
61
+ creator.number = number
62
+ creator.number.should == number
63
+ end
64
+
65
+ it "raises an exception if no issue has been created yet" do
66
+ creator.number = nil
67
+ expect { creator.number }.to raise_error(IssueCreator::NotYetCreated)
68
+ end
69
+ end
70
+
71
+ context "#data" do
72
+ let(:details) { stub(:data) }
73
+
74
+ it "returns the stored issue details" do
75
+ creator.data = details
76
+ creator.data.should == details
77
+ end
78
+
79
+ it "raises an exception if no information has been captured yet" do
80
+ creator.data = nil
81
+ expect { creator.data }.to raise_error(IssueCreator::NotYetCreated)
82
+ end
83
+ end
84
+
85
+ context "#title" do
86
+ context "having the option set" do
87
+ before { creator.options[:title] = title }
88
+
89
+ it "fetches from the options" do
90
+ creator.title.should == title
91
+ end
92
+ end
93
+
94
+ it "raises an exception if it's missing" do
95
+ creator.options[:title] = nil
96
+ expect { creator.title }.to raise_error(IssueCreator::MissingAttribute)
97
+ end
98
+ end
99
+
100
+ context "#pivotal_ids" do
101
+ it "fetches from the options" do
102
+ creator.options[:pivotal_ids] = pivotal_ids
103
+ creator.pivotal_ids.should == pivotal_ids
104
+ end
105
+
106
+ it "defaults to an empty array if it's missing" do
107
+ creator.options[:pivotal_ids] = nil
108
+ creator.pivotal_ids.should == []
109
+ end
110
+ end
111
+
112
+ context "#body_locals" do
113
+ let(:urls) { %w(link1 link2) }
114
+
115
+ before do
116
+ creator.stub({
117
+ pivotal_ids: pivotal_ids,
118
+ jira_ids: jira_ids,
119
+ jira_url: jira_url,
120
+ })
121
+ end
122
+ it "includes the necessary keys to render the template" do
123
+ creator.body_locals[:pivotal_ids].should == creator.pivotal_ids
124
+ creator.body_locals[:jira_ids].should == creator.jira_ids
125
+ creator.body_locals[:jira_url].should == creator.jira_url
126
+ end
127
+ end
128
+
129
+ context "#edit_body" do
130
+ let(:path) { stub(:path) }
131
+ let(:body) { stub(:string) }
132
+ let(:tempfile) { stub(:tempfile) }
133
+ let(:edited_body) { stub(:edited_body) }
134
+
135
+ before do
136
+ Tempfile.stub(:new) { tempfile }
137
+ tempfile.stub(path: path, write: nil, read: edited_body, unlink: nil, close: nil, open: nil)
138
+ creator.stub(:system)
139
+ end
140
+
141
+ context "without the $EDITOR env var set" do
142
+ before do
143
+ stub_const('ENV', {'EDITOR' => nil})
144
+ end
145
+
146
+ it "returns the un-edited output" do
147
+ creator.edit_body(body).should == body
148
+ end
149
+ end
150
+
151
+ context "with the $EDITOR env set" do
152
+
153
+ before do
154
+ stub_const('ENV', {'EDITOR' => 'vim'})
155
+ end
156
+
157
+ it "creates a tempfile, write default contents, and close it" do
158
+ Tempfile.should_receive(:new).with(['octopolo_issue', '.md']) { tempfile }
159
+ tempfile.should_receive(:write).with(body)
160
+ tempfile.should_receive(:close)
161
+ creator.edit_body body
162
+ end
163
+
164
+ it "edits the tempfile with the $EDITOR" do
165
+ tempfile.should_receive(:path) { path }
166
+ creator.should_receive(:system).with("vim #{path}")
167
+ creator.edit_body body
168
+ end
169
+
170
+ it "reopens the file, gets the contents, and deletes the temp file" do
171
+ tempfile.should_receive(:open)
172
+ tempfile.should_receive(:read) { edited_body }
173
+ tempfile.should_receive(:unlink)
174
+ creator.edit_body body
175
+ end
176
+
177
+ it "returns the user edited output" do
178
+ creator.edit_body(body).should == edited_body
179
+ end
180
+ end
181
+ end
182
+
183
+ context "#body" do
184
+ let(:locals) { stub(:hash) }
185
+ let(:output) { stub(:string) }
186
+
187
+ before do
188
+ creator.stub({
189
+ body_locals: locals,
190
+ })
191
+ end
192
+
193
+ it "renders the body template with the body locals" do
194
+ Renderer.should_receive(:render).with(Renderer::ISSUE_BODY, locals) { output }
195
+ creator.body.should == output
196
+ end
197
+
198
+ context "when the editor option is set" do
199
+ let(:edited_output) { stub(:output) }
200
+
201
+ before do
202
+ creator.stub({
203
+ body_locals: locals,
204
+ options: { editor: true }
205
+ })
206
+ end
207
+
208
+ it "calls the edit_body method" do
209
+ Renderer.should_receive(:render).with(Renderer::ISSUE_BODY, locals) { output }
210
+ creator.should_receive(:edit_body).with(output) { edited_output }
211
+ creator.body.should == edited_output
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end