thegarage-gitx 2.9.0 → 2.10.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 967ed00a8d2bd8e358dc56481a1eeab9a96fc552
4
- data.tar.gz: 71ef71cd36da70a4da6b07787d2d2342c8dfb280
3
+ metadata.gz: a3ba4526a6cb6a7cfa24ec14119448eb93802865
4
+ data.tar.gz: 5f98b2f487eb204dab8c060218580871d1873386
5
5
  SHA512:
6
- metadata.gz: ec87914ebce4c62a3891e1d60b76033e601f13a9a57d7ab692ca49da2d165e2a56f9f35f8e5f83300ed47e0e0a05d464dc35174dc630a48a517f4face59311e3
7
- data.tar.gz: 6fc4ea416852c8aec5af068cf3a5b2e4a66a5dc55aee9fdef99baa29e22fd76b6523758561fcbb55fbd72043a3166893079affe1d4f64137cfc91c79d1665f1e
6
+ metadata.gz: 9689ecc4716c330e747e607a2d629d7f0613c52ad025c326d04aefcd33b5055bc16c450725c85af802754f5c6ef40a30b569087f72f85ccf824f057558e2ef7f
7
+ data.tar.gz: 0f5df20623851c98ead0d71f225dcb013c49ee86661fc9267d1b88154b8075ddd226b90c1484e4cb71b991e4592accb1bdd37c8a7d77ab1d88e9d4575c9cea69
data/README.md CHANGED
@@ -35,7 +35,11 @@ options:
35
35
  * `--assign` or `-a` = assign pull request to github user
36
36
  * `--open` or `-o` = open pull request in default web browser.
37
37
  * `--bump` or `-b` = bump an existing pull request by posting a comment to re-review new changes
38
+ * `--approve` = approve/signoff on pull request (with optional feedback)
39
+ * `--reject` = reject pull request (with details)
38
40
 
41
+ NOTE: the `--bump` option will also update the pull request commit status to mark the branch as 'pending peer review'.
42
+ This setting is cleared when a reviewer approves or rejects the pull request.
39
43
 
40
44
  ## git release
41
45
 
@@ -8,7 +8,7 @@ module Thegarage
8
8
  module Gitx
9
9
  module Cli
10
10
  class IntegrateCommand < BaseCommand
11
- include Github
11
+ include Thegarage::Gitx::Github
12
12
  desc 'integrate', 'integrate the current branch into one of the aggregate development branches (default = staging)'
13
13
  method_option :resume, :type => :string, :aliases => '-r', :desc => 'resume merging of feature-branch'
14
14
  def integrate(integration_branch = 'staging')
@@ -10,7 +10,7 @@ module Thegarage
10
10
  module Gitx
11
11
  module Cli
12
12
  class ReleaseCommand < BaseCommand
13
- include Github
13
+ include Thegarage::Gitx::Github
14
14
 
15
15
  desc 'release', 'release the current branch to production'
16
16
  method_option :cleanup, :type => :boolean, :desc => 'cleanup merged branches after release'
@@ -8,20 +8,42 @@ module Thegarage
8
8
  module Gitx
9
9
  module Cli
10
10
  class ReviewCommand < BaseCommand
11
- include Github
11
+ include Thegarage::Gitx::Github
12
+
13
+ BUMP_COMMENT_TEMPLATE = <<-EOS.dedent
14
+ [gitx] review bump :tada:
15
+
16
+ ### Changelog Summary
17
+ EOS
18
+ APPROVAL_COMMENT_TEMPLATE = <<-EOS.dedent
19
+ [gitx] review approved :shipit:
20
+
21
+ ### Feedback
22
+
23
+ ### Follow-up Items
24
+ EOS
25
+ REJECTION_COMMENT_TEMPLATE = <<-EOS.dedent
26
+ [gitx] review rejected
27
+
28
+ ### Feedback
29
+ EOS
12
30
 
13
31
  desc "review", "Create or update a pull request on github"
14
32
  method_option :description, :type => :string, :aliases => '-d', :desc => 'pull request description'
15
33
  method_option :assignee, :type => :string, :aliases => '-a', :desc => 'pull request assignee'
16
34
  method_option :open, :type => :boolean, :aliases => '-o', :desc => 'open the pull request in a web browser'
17
35
  method_option :bump, :type => :boolean, :aliases => '-b', :desc => 'bump an existing pull request by posting a comment to re-review new changes'
36
+ method_option :approve, :type => :boolean, :desc => 'approve the pull request an post comment on pull request'
37
+ method_option :reject, :type => :boolean, :desc => 'reject the pull request an post comment on pull request'
18
38
  # @see http://developer.github.com/v3/pulls/
19
39
  def review
20
40
  fail 'Github authorization token not found' unless authorization_token
21
41
 
22
42
  branch = current_branch.name
23
43
  pull_request = find_or_create_pull_request(branch)
24
- create_bump_comment(pull_request) if options[:bump]
44
+ bump_pull_request(pull_request) if options[:bump]
45
+ approve_pull_request(pull_request) if options[:approve]
46
+ reject_pull_request(pull_request) if options[:reject]
25
47
  assign_pull_request(pull_request) if options[:assignee]
26
48
 
27
49
  run_cmd "open #{pull_request.html_url}" if options[:open]
@@ -42,15 +64,35 @@ module Thegarage
42
64
  github_client.update_issue(github_slug, pull_request.number, title, body, options)
43
65
  end
44
66
 
45
- def create_bump_comment(pull_request)
46
- comment_template = []
47
- comment_template << '[gitx] review bump :tada:'
48
- comment_template << ''
49
- comment_template << '### Summary of Changes'
67
+ def bump_pull_request(pull_request)
68
+ comment = get_editor_input(BUMP_COMMENT_TEMPLATE)
69
+ github_client.add_comment(github_slug, pull_request.number, comment)
70
+
71
+ set_review_status('pending', 'Peer review in progress')
72
+ end
50
73
 
51
- comment = ask_editor(comment_template.join("\n"), repo.config['core.editor'])
52
- comment = comment.chomp.strip
74
+ def reject_pull_request(pull_request)
75
+ comment = get_editor_input(REJECTION_COMMENT_TEMPLATE)
53
76
  github_client.add_comment(github_slug, pull_request.number, comment)
77
+
78
+ set_review_status('failure', 'Peer review rejected')
79
+ end
80
+
81
+ def approve_pull_request(pull_request)
82
+ comment = get_editor_input(APPROVAL_COMMENT_TEMPLATE)
83
+ github_client.add_comment(github_slug, pull_request.number, comment)
84
+
85
+ set_review_status('success', 'Peer review approved')
86
+ end
87
+
88
+ def get_editor_input(template)
89
+ text = ask_editor(template, repo.config['core.editor'])
90
+ text = text.chomp.strip
91
+ end
92
+
93
+ def set_review_status(state, description)
94
+ latest_commit = repo.head.target_id
95
+ update_review_status(latest_commit, state, description)
54
96
  end
55
97
  end
56
98
  end
@@ -1,7 +1,8 @@
1
1
  class String
2
+ # @see http://api.rubyonrails.org/classes/String.html#method-i-strip_heredoc
2
3
  def undent
3
- a = $1 if match(/\A(\s+)(.*\n)(?:\1.*\n)*\z/)
4
- gsub(/^#{a}/,'')
4
+ indent = scan(/^[ \t]*(?=\S)/).min.size || 0
5
+ gsub(/^[ \t]{#{indent}}/, '')
5
6
  end
6
7
  alias :dedent :undent
7
8
 
@@ -2,138 +2,142 @@ require 'octokit'
2
2
 
3
3
  module Thegarage
4
4
  module Gitx
5
- module Cli
6
- module Github
7
- CLIENT_URL = 'https://github.com/thegarage/thegarage-gitx'
8
- PULL_REQUEST_FOOTER = <<-EOS.dedent
9
- # Pull Request Protips(tm):
10
- # * Include description of how this change accomplishes the task at hand.
11
- # * Use GitHub flavored Markdown http://github.github.com/github-flavored-markdown/
12
- # * Review CONTRIBUTING.md for recommendations of artifacts, links, images, screencasts, etc.
13
- #
14
- # This footer will automatically be stripped from the pull request description
15
- EOS
16
-
17
- def find_or_create_pull_request(branch)
18
- pull_request = find_pull_request(branch)
19
- pull_request ||= begin
20
- execute_command(UpdateCommand, :update)
21
- pull_request = create_pull_request(branch)
22
- say 'Created pull request: '
23
- say pull_request.html_url, :green
24
-
25
- pull_request
26
- end
5
+ module Github
6
+ REVIEW_CONTEXT = 'peer_review'
7
+ CLIENT_URL = 'https://github.com/thegarage/thegarage-gitx'
8
+ PULL_REQUEST_FOOTER = <<-EOS.dedent
9
+ # Pull Request Protips(tm):
10
+ # * Include description of how this change accomplishes the task at hand.
11
+ # * Use GitHub flavored Markdown http://github.github.com/github-flavored-markdown/
12
+ # * Review CONTRIBUTING.md for recommendations of artifacts, links, images, screencasts, etc.
13
+ #
14
+ # This footer will automatically be stripped from the pull request description
15
+ EOS
16
+
17
+ def find_or_create_pull_request(branch)
18
+ pull_request = find_pull_request(branch)
19
+ pull_request ||= begin
20
+ execute_command(Thegarage::Gitx::Cli::UpdateCommand, :update)
21
+ pull_request = create_pull_request(branch)
22
+ say 'Created pull request: '
23
+ say pull_request.html_url, :green
24
+
27
25
  pull_request
28
26
  end
27
+ pull_request
28
+ end
29
29
 
30
- # @return [Sawyer::Resource] data structure of pull request info if found
31
- # @return nil if no pull request found
32
- def find_pull_request(branch)
33
- head_reference = "#{github_organization}:#{branch}"
34
- params = {
35
- head: head_reference,
36
- state: 'open'
37
- }
38
- pull_requests = github_client.pull_requests(github_slug, params)
39
- pull_requests.first
40
- end
30
+ # @return [Sawyer::Resource] data structure of pull request info if found
31
+ # @return nil if no pull request found
32
+ def find_pull_request(branch)
33
+ head_reference = "#{github_organization}:#{branch}"
34
+ params = {
35
+ head: head_reference,
36
+ state: 'open'
37
+ }
38
+ pull_requests = github_client.pull_requests(github_slug, params)
39
+ pull_requests.first
40
+ end
41
41
 
42
- # Get the current commit status of a branch
43
- # @see https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
44
- def branch_status(branch)
45
- response = github_client.status(github_slug, branch)
46
- response.state
47
- end
42
+ # Get the current commit status of a branch
43
+ # @see https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
44
+ def branch_status(branch)
45
+ response = github_client.status(github_slug, branch)
46
+ response.state
47
+ end
48
48
 
49
- # @see http://developer.github.com/v3/pulls/
50
- def create_pull_request(branch)
51
- say "Creating pull request for "
52
- say "#{branch} ", :green
53
- say "against "
54
- say "#{Thegarage::Gitx::BASE_BRANCH} ", :green
55
- say "in "
56
- say github_slug, :green
57
-
58
- title = branch
59
- body = pull_request_body(branch)
60
- github_client.create_pull_request(github_slug, Thegarage::Gitx::BASE_BRANCH, branch, title, body)
61
- end
49
+ # Update build status with peer review status
50
+ def update_review_status(commit_sha, state, description)
51
+ github_client.create_status(github_slug, commit_sha, state, context: REVIEW_CONTEXT, description: description)
52
+ end
62
53
 
63
- def pull_request_body(branch)
64
- changelog = run_cmd("git log #{Thegarage::Gitx::BASE_BRANCH}...#{branch} --reverse --no-merges --pretty=format:'* %s%n%b'")
65
- description = options[:description]
54
+ # @see http://developer.github.com/v3/pulls/
55
+ def create_pull_request(branch)
56
+ say "Creating pull request for "
57
+ say "#{branch} ", :green
58
+ say "against "
59
+ say "#{Thegarage::Gitx::BASE_BRANCH} ", :green
60
+ say "in "
61
+ say github_slug, :green
62
+
63
+ title = branch
64
+ body = pull_request_body(branch)
65
+ github_client.create_pull_request(github_slug, Thegarage::Gitx::BASE_BRANCH, branch, title, body)
66
+ end
66
67
 
67
- description_template = []
68
- description_template << "#{description}\n" if description
69
- description_template << '### Changelog'
70
- description_template << changelog
71
- description_template << PULL_REQUEST_FOOTER
68
+ def pull_request_body(branch)
69
+ changelog = run_cmd("git log #{Thegarage::Gitx::BASE_BRANCH}...#{branch} --reverse --no-merges --pretty=format:'* %s%n%b'")
70
+ description = options[:description]
72
71
 
73
- body = ask_editor(description_template.join("\n"), repo.config['core.editor'])
74
- body.gsub(PULL_REQUEST_FOOTER, '').chomp.strip
75
- end
72
+ description_template = []
73
+ description_template << "#{description}\n" if description
74
+ description_template << '### Changelog'
75
+ description_template << changelog
76
+ description_template << PULL_REQUEST_FOOTER
76
77
 
77
- # token is cached in local git config for future use
78
- # @return [String] auth token stored in git (current repo, user config or installed global settings)
79
- # @see http://developer.github.com/v3/oauth/#scopes
80
- # @see http://developer.github.com/v3/#user-agent-required
81
- def authorization_token
82
- auth_token = repo.config['thegarage.gitx.githubauthtoken']
83
- return auth_token unless auth_token.to_s.blank?
84
-
85
- auth_token = create_authorization
86
- repo.config['thegarage.gitx.githubauthtoken'] = auth_token
87
- auth_token
88
- end
78
+ body = ask_editor(description_template.join("\n"), repo.config['core.editor'])
79
+ body.gsub(PULL_REQUEST_FOOTER, '').chomp.strip
80
+ end
89
81
 
90
- def create_authorization
91
- password = ask("Github password for #{username}: ", :echo => false)
92
- say ''
93
- client = Octokit::Client.new(login: username, password: password)
94
- response = client.create_authorization(authorization_request_options)
95
- response.token
96
- end
82
+ # token is cached in local git config for future use
83
+ # @return [String] auth token stored in git (current repo, user config or installed global settings)
84
+ # @see http://developer.github.com/v3/oauth/#scopes
85
+ # @see http://developer.github.com/v3/#user-agent-required
86
+ def authorization_token
87
+ auth_token = repo.config['thegarage.gitx.githubauthtoken']
88
+ return auth_token unless auth_token.to_s.blank?
89
+
90
+ auth_token = create_authorization
91
+ repo.config['thegarage.gitx.githubauthtoken'] = auth_token
92
+ auth_token
93
+ end
97
94
 
98
- def authorization_request_options
99
- timestamp = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S%z')
100
- client_name = "The Garage Git eXtensions - #{github_slug} #{timestamp}"
101
- options = {
102
- :scopes => ['repo'],
103
- :note => client_name,
104
- :note_url => CLIENT_URL
105
- }
106
- two_factor_auth_token = ask("Github two factor authorization token (if enabled): ", :echo => false)
107
- say ''
108
- options[:headers] = {'X-GitHub-OTP' => two_factor_auth_token} if two_factor_auth_token
109
- options
110
- end
95
+ def create_authorization
96
+ password = ask("Github password for #{username}: ", :echo => false)
97
+ say ''
98
+ client = Octokit::Client.new(login: username, password: password)
99
+ response = client.create_authorization(authorization_request_options)
100
+ response.token
101
+ end
111
102
 
112
- def github_client
113
- @client ||= Octokit::Client.new(:access_token => authorization_token)
114
- end
103
+ def authorization_request_options
104
+ timestamp = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S%z')
105
+ client_name = "The Garage Git eXtensions - #{github_slug} #{timestamp}"
106
+ options = {
107
+ :scopes => ['repo'],
108
+ :note => client_name,
109
+ :note_url => CLIENT_URL
110
+ }
111
+ two_factor_auth_token = ask("Github two factor authorization token (if enabled): ", :echo => false)
112
+ say ''
113
+ options[:headers] = {'X-GitHub-OTP' => two_factor_auth_token} if two_factor_auth_token
114
+ options
115
+ end
115
116
 
116
- # @return [String] github username (ex: 'wireframe') of the current github.user
117
- # @raise error if github.user is not configured
118
- def username
119
- username = repo.config['github.user']
120
- fail "Github user not configured. Run: `git config --global github.user 'me@email.com'`" unless username
121
- username
122
- end
117
+ def github_client
118
+ @client ||= Octokit::Client.new(:access_token => authorization_token)
119
+ end
123
120
 
124
- # @return the github slug for the current repository's remote origin url.
125
- # @example
126
- # git@github.com:socialcast/thegarage/gitx.git #=> thegarage/gitx
127
- # @example
128
- # https://github.com/socialcast/thegarage/gitx.git #=> thegarage/gitx
129
- def github_slug
130
- remote = repo.config['remote.origin.url']
131
- remote.to_s.gsub(/\.git$/,'').split(/[:\/]/).last(2).join('/')
132
- end
121
+ # @return [String] github username (ex: 'wireframe') of the current github.user
122
+ # @raise error if github.user is not configured
123
+ def username
124
+ username = repo.config['github.user']
125
+ fail "Github user not configured. Run: `git config --global github.user 'me@email.com'`" unless username
126
+ username
127
+ end
133
128
 
134
- def github_organization
135
- github_slug.split('/').first
136
- end
129
+ # @return the github slug for the current repository's remote origin url.
130
+ # @example
131
+ # git@github.com:socialcast/thegarage/gitx.git #=> thegarage/gitx
132
+ # @example
133
+ # https://github.com/socialcast/thegarage/gitx.git #=> thegarage/gitx
134
+ def github_slug
135
+ remote = repo.config['remote.origin.url']
136
+ remote.to_s.gsub(/\.git$/,'').split(/[:\/]/).last(2).join('/')
137
+ end
138
+
139
+ def github_organization
140
+ github_slug.split('/').first
137
141
  end
138
142
  end
139
143
  end
@@ -1,5 +1,5 @@
1
1
  module Thegarage
2
2
  module Gitx
3
- VERSION = '2.9.0'
3
+ VERSION = '2.10.0'
4
4
  end
5
5
  end
@@ -44,9 +44,10 @@ describe Thegarage::Gitx::Cli::ReviewCommand do
44
44
 
45
45
  allow(cli).to receive(:authorization_token).and_return(authorization_token)
46
46
  expect(cli).to receive(:run_cmd).with("git log master...feature-branch --reverse --no-merges --pretty=format:'* %s%n%b'").and_return("* old commit\n\n* new commit").ordered
47
- expect(cli).to receive(:ask_editor).with("### Changelog\n* old commit\n\n* new commit\n#{Thegarage::Gitx::Cli::Github::PULL_REQUEST_FOOTER}", anything).and_return('description')
47
+ expect(cli).to receive(:ask_editor).with("### Changelog\n* old commit\n\n* new commit\n#{Thegarage::Gitx::Github::PULL_REQUEST_FOOTER}", anything).and_return('description')
48
48
 
49
49
  stub_request(:post, 'https://api.github.com/repos/thegarage/thegarage-gitx/pulls').to_return(:status => 201, :body => new_pull_request.to_json, :headers => {'Content-Type' => 'application/json'})
50
+
50
51
  VCR.use_cassette('pull_request_does_not_exist') do
51
52
  cli.review
52
53
  end
@@ -124,9 +125,11 @@ describe Thegarage::Gitx::Cli::ReviewCommand do
124
125
  }
125
126
  end
126
127
  let(:authorization_token) { '123123' }
128
+ let(:reference) { double('fake reference', target_id: 'e12da4') }
127
129
  before do
128
130
  allow(cli).to receive(:authorization_token).and_return(authorization_token)
129
131
  expect(cli).to receive(:ask_editor).and_return('comment description')
132
+ allow(repo).to receive(:head).and_return(reference)
130
133
  stub_request(:post, /.*api.github.com.*/).to_return(:status => 201)
131
134
 
132
135
  VCR.use_cassette('pull_request_does_exist_with_success_status') do
@@ -137,6 +140,64 @@ describe Thegarage::Gitx::Cli::ReviewCommand do
137
140
  expect(WebMock).to have_requested(:post, "https://api.github.com/repos/thegarage/thegarage-gitx/issues/10/comments").
138
141
  with(body: {body: 'comment description'})
139
142
  end
143
+ it 'creates pending build status for latest commit' do
144
+ expect(WebMock).to have_requested(:post, 'https://api.github.com/repos/thegarage/thegarage-gitx/statuses/e12da4').
145
+ with(body: {state: 'pending', context: 'peer_review', description: 'Peer review in progress'})
146
+ end
147
+ end
148
+ context 'when --reject flag is passed' do
149
+ let(:options) do
150
+ {
151
+ reject: true
152
+ }
153
+ end
154
+ let(:authorization_token) { '123123' }
155
+ let(:reference) { double('fake reference', target_id: 'e12da4') }
156
+ before do
157
+ allow(cli).to receive(:authorization_token).and_return(authorization_token)
158
+ expect(cli).to receive(:ask_editor).and_return('comment body')
159
+ allow(repo).to receive(:head).and_return(reference)
160
+ stub_request(:post, /.*api.github.com.*/).to_return(:status => 201)
161
+
162
+ VCR.use_cassette('pull_request_does_exist_with_success_status') do
163
+ cli.review
164
+ end
165
+ end
166
+ it 'posts comment to github' do
167
+ expect(WebMock).to have_requested(:post, "https://api.github.com/repos/thegarage/thegarage-gitx/issues/10/comments").
168
+ with(body: {body: 'comment body'})
169
+ end
170
+ it 'creates failure build status for latest commit' do
171
+ expect(WebMock).to have_requested(:post, 'https://api.github.com/repos/thegarage/thegarage-gitx/statuses/e12da4').
172
+ with(body: {state: 'failure', context: 'peer_review', description: 'Peer review rejected'})
173
+ end
174
+ end
175
+ context 'when --approve flag is passed' do
176
+ let(:options) do
177
+ {
178
+ approve: true
179
+ }
180
+ end
181
+ let(:authorization_token) { '123123' }
182
+ let(:reference) { double('fake reference', target_id: 'e12da4') }
183
+ before do
184
+ allow(cli).to receive(:authorization_token).and_return(authorization_token)
185
+ expect(cli).to receive(:ask_editor).and_return('comment body')
186
+ allow(repo).to receive(:head).and_return(reference)
187
+ stub_request(:post, /.*api.github.com.*/).to_return(:status => 201)
188
+
189
+ VCR.use_cassette('pull_request_does_exist_with_success_status') do
190
+ cli.review
191
+ end
192
+ end
193
+ it 'posts comment to github' do
194
+ expect(WebMock).to have_requested(:post, "https://api.github.com/repos/thegarage/thegarage-gitx/issues/10/comments").
195
+ with(body: {body: 'comment body'})
196
+ end
197
+ it 'creates success build status for latest commit' do
198
+ expect(WebMock).to have_requested(:post, 'https://api.github.com/repos/thegarage/thegarage-gitx/statuses/e12da4').
199
+ with(body: {state: 'success', context: 'peer_review', description: 'Peer review approved'})
200
+ end
140
201
  end
141
202
  end
142
203
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thegarage-gitx
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.0
4
+ version: 2.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Sonnek
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-12-10 00:00:00.000000000 Z
11
+ date: 2014-12-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rugged