thegarage-gitx 2.9.0 → 2.10.0

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