octopolo 0.3.6 → 0.4.0

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.
@@ -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