txgh-server 3.1.0 → 4.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +5 -5
  2. data/lib/txgh-server.rb +1 -0
  3. data/lib/txgh-server/application.rb +7 -1
  4. data/lib/txgh-server/download_handler.rb +1 -1
  5. data/lib/txgh-server/gitlab_request_auth.rb +13 -0
  6. data/lib/txgh-server/triggers/handler.rb +18 -1
  7. data/lib/txgh-server/triggers/pull_handler.rb +1 -10
  8. data/lib/txgh-server/triggers/push_handler.rb +1 -10
  9. data/lib/txgh-server/version.rb +1 -1
  10. data/lib/txgh-server/webhooks.rb +2 -0
  11. data/lib/txgh-server/webhooks/git.rb +15 -0
  12. data/lib/txgh-server/webhooks/git/blank_attributes.rb +20 -0
  13. data/lib/txgh-server/webhooks/git/delete_attributes.rb +54 -0
  14. data/lib/txgh-server/webhooks/git/delete_handler.rb +45 -0
  15. data/lib/txgh-server/webhooks/git/ping_handler.rb +21 -0
  16. data/lib/txgh-server/webhooks/git/push_attributes.rb +79 -0
  17. data/lib/txgh-server/webhooks/git/push_handler.rb +98 -0
  18. data/lib/txgh-server/webhooks/git/request_handler.rb +76 -0
  19. data/lib/txgh-server/webhooks/git/status_updater.rb +15 -0
  20. data/lib/txgh-server/webhooks/github/blank_attributes.rb +1 -13
  21. data/lib/txgh-server/webhooks/github/delete_attributes.rb +1 -38
  22. data/lib/txgh-server/webhooks/github/delete_handler.rb +1 -38
  23. data/lib/txgh-server/webhooks/github/ping_handler.rb +1 -12
  24. data/lib/txgh-server/webhooks/github/push_attributes.rb +1 -51
  25. data/lib/txgh-server/webhooks/github/push_handler.rb +2 -88
  26. data/lib/txgh-server/webhooks/github/request_handler.rb +2 -47
  27. data/lib/txgh-server/webhooks/github/status_updater.rb +1 -9
  28. data/lib/txgh-server/webhooks/gitlab.rb +14 -0
  29. data/lib/txgh-server/webhooks/gitlab/blank_attributes.rb +8 -0
  30. data/lib/txgh-server/webhooks/gitlab/delete_attributes.rb +17 -0
  31. data/lib/txgh-server/webhooks/gitlab/delete_handler.rb +8 -0
  32. data/lib/txgh-server/webhooks/gitlab/push_attributes.rb +29 -0
  33. data/lib/txgh-server/webhooks/gitlab/push_handler.rb +13 -0
  34. data/lib/txgh-server/webhooks/gitlab/request_handler.rb +59 -0
  35. data/lib/txgh-server/webhooks/gitlab/status_updater.rb +37 -0
  36. data/lib/txgh-server/webhooks/transifex/request_handler.rb +1 -1
  37. data/spec/application_spec.rb +11 -11
  38. data/spec/download_handler_spec.rb +6 -8
  39. data/spec/github_request_auth_spec.rb +6 -8
  40. data/spec/gitlab_request_auth_spec.rb +26 -0
  41. data/spec/helpers/gitlab_payload_builder.rb +132 -0
  42. data/spec/integration/hooks_spec.rb +1 -1
  43. data/spec/tgz_stream_response_spec.rb +2 -4
  44. data/spec/transifex_request_auth_spec.rb +6 -8
  45. data/spec/webhooks/github/delete_attributes_spec.rb +2 -5
  46. data/spec/webhooks/github/delete_handler_spec.rb +4 -7
  47. data/spec/webhooks/github/ping_handler_spec.rb +2 -5
  48. data/spec/webhooks/github/push_attributes_spec.rb +3 -6
  49. data/spec/webhooks/github/push_handler_spec.rb +5 -8
  50. data/spec/webhooks/github/request_handler_spec.rb +8 -10
  51. data/spec/webhooks/github/status_updater_spec.rb +2 -4
  52. data/spec/webhooks/gitlab/delete_attributes_spec.rb +27 -0
  53. data/spec/webhooks/gitlab/delete_handler_spec.rb +34 -0
  54. data/spec/webhooks/gitlab/push_attributes_spec.rb +64 -0
  55. data/spec/webhooks/gitlab/push_handler_spec.rb +99 -0
  56. data/spec/webhooks/gitlab/request_handler_spec.rb +73 -0
  57. data/spec/webhooks/gitlab/status_updater_spec.rb +66 -0
  58. data/spec/webhooks/transifex/hook_handler_spec.rb +2 -5
  59. data/spec/webhooks/transifex/request_handler_spec.rb +3 -5
  60. data/spec/zip_stream_response_spec.rb +2 -4
  61. data/txgh-server.gemspec +1 -2
  62. metadata +35 -10
  63. data/spec/integration/payloads/github_postbody_l10n.json +0 -136
@@ -3,15 +3,7 @@ require 'octokit'
3
3
  module TxghServer
4
4
  module Webhooks
5
5
  module Github
6
- class StatusUpdater
7
- attr_reader :project, :repo, :branch
8
-
9
- def initialize(project, repo, branch)
10
- @project = project
11
- @repo = repo
12
- @branch = branch
13
- end
14
-
6
+ class StatusUpdater < TxghServer::Webhooks::Git::StatusUpdater
15
7
  def report_error_and_update_status(error)
16
8
  error_params = Txgh.events.publish_error!(error)
17
9
 
@@ -0,0 +1,14 @@
1
+ module TxghServer
2
+ module Webhooks
3
+ module Gitlab
4
+ autoload :BlankAttributes, 'txgh-server/webhooks/gitlab/blank_attributes'
5
+ autoload :DeleteAttributes, 'txgh-server/webhooks/gitlab/delete_attributes'
6
+ autoload :DeleteHandler, 'txgh-server/webhooks/gitlab/delete_handler'
7
+ autoload :Handler, 'txgh-server/webhooks/gitlab/handler'
8
+ autoload :PushHandler, 'txgh-server/webhooks/gitlab/push_handler'
9
+ autoload :PushAttributes, 'txgh-server/webhooks/gitlab/push_attributes'
10
+ autoload :RequestHandler, 'txgh-server/webhooks/gitlab/request_handler'
11
+ autoload :StatusUpdater, 'txgh-server/webhooks/gitlab/status_updater'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ module TxghServer
2
+ module Webhooks
3
+ module Gitlab
4
+ class BlankAttributes < TxghServer::Webhooks::Git::BlankAttributes
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,17 @@
1
+ module TxghServer
2
+ module Webhooks
3
+ module Gitlab
4
+ class DeleteAttributes < TxghServer::Webhooks::Git::DeleteAttributes
5
+ class << self
6
+ def repo_name(payload)
7
+ payload.fetch('repository').fetch('name')
8
+ end
9
+
10
+ def ref_type(_payload)
11
+ 'branch'
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ module TxghServer
2
+ module Webhooks
3
+ module Gitlab
4
+ class DeleteHandler < TxghServer::Webhooks::Git::DeleteHandler
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,29 @@
1
+ module TxghServer
2
+ module Webhooks
3
+ module Gitlab
4
+ class PushAttributes < TxghServer::Webhooks::Git::PushAttributes
5
+ class << self
6
+ def repo_name(payload)
7
+ payload.fetch('repository').fetch('name')
8
+ end
9
+
10
+ def added_files(payload)
11
+ extract_files(payload, 'added')
12
+ end
13
+
14
+ def modified_files(payload)
15
+ extract_files(payload, 'modified')
16
+ end
17
+
18
+ def author(payload)
19
+ payload.fetch('user_name')
20
+ end
21
+
22
+ def extract_files(payload, state)
23
+ payload.fetch('commits').flat_map { |c| c[state] }.uniq
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ module TxghServer
2
+ module Webhooks
3
+ module Gitlab
4
+ class PushHandler < TxghServer::Webhooks::Git::PushHandler
5
+ private
6
+
7
+ def status_updater
8
+ @status_updater = TxghServer::Webhooks::Gitlab::StatusUpdater.new(project, repo, branch)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,59 @@
1
+ require 'json'
2
+
3
+ module TxghServer
4
+ module Webhooks
5
+ module Gitlab
6
+ class RequestHandler < TxghServer::Webhooks::Git::RequestHandler
7
+ def handle_request
8
+ handle_safely do
9
+ if gitlab_event == 'Push Hook'
10
+ if delete_event?
11
+ DeleteHandler.new(project, repo, logger, attributes).execute
12
+ else
13
+ PushHandler.new(project, repo, logger, attributes).execute
14
+ end
15
+ else
16
+ respond_with_error(400, 'Unexpected event type')
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def attributes
24
+ unless gitlab_event == 'Push Hook'
25
+ return BlankAttributes.from_webhook_payload(payload)
26
+ end
27
+
28
+ if delete_event?
29
+ return DeleteAttributes.from_webhook_payload(payload)
30
+ end
31
+
32
+ PushAttributes.from_webhook_payload(payload)
33
+ end
34
+
35
+ def gitlab_event
36
+ request.env['HTTP_X_GITLAB_EVENT']
37
+ end
38
+
39
+ def delete_event?
40
+ payload.fetch('after', {}) == '0000000000000000000000000000000000000000'
41
+ end
42
+
43
+ def git_repo_name
44
+ payload.fetch('project', {})['path_with_namespace']
45
+ end
46
+
47
+ def authentic_request?
48
+ if repo.webhook_protected?
49
+ GitlabRequestAuth.authentic_request?(
50
+ request, repo.webhook_secret
51
+ )
52
+ else
53
+ true
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,37 @@
1
+ require 'gitlab'
2
+
3
+ module TxghServer
4
+ module Webhooks
5
+ module Gitlab
6
+ class StatusUpdater < TxghServer::Webhooks::Git::StatusUpdater
7
+ def report_error_and_update_status(error)
8
+ error_params = Txgh.events.publish_error!(error)
9
+
10
+ Txgh.events.publish_each('gitlab.status.error', error_params) do |status_params|
11
+ if status_params
12
+ update_status_safely do
13
+ Txgh::GitlabStatus.error(project, repo, branch, status_params)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ def update_status
20
+ update_status_safely do
21
+ Txgh::GitlabStatus.update(project, repo, branch)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def update_status_safely
28
+ yield
29
+ rescue ::Gitlab::Error::Unprocessable, ::Gitlab::Error::BadRequest
30
+ # raised because we've tried to create too many statuses for the commit
31
+ rescue Txgh::TransifexNotFoundError
32
+ # raised if transifex resource can't be found
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -29,7 +29,7 @@ module TxghServer
29
29
  handle_safely do
30
30
  handler = TxghServer::Webhooks::Transifex::HookHandler.new(
31
31
  project: config.transifex_project,
32
- repo: config.github_repo,
32
+ repo: config.git_repo,
33
33
  resource_slug: payload[:resource],
34
34
  language: payload[:language],
35
35
  logger: logger
@@ -94,7 +94,7 @@ describe TxghServer::WebhookEndpoints do
94
94
  end
95
95
 
96
96
  let(:config) do
97
- Txgh::Config::ConfigPair.new(project_config, repo_config)
97
+ Txgh::Config::ConfigPair.new(project_config, github_config)
98
98
  end
99
99
 
100
100
  before(:each) do
@@ -103,7 +103,7 @@ describe TxghServer::WebhookEndpoints do
103
103
  )
104
104
 
105
105
  allow(Txgh::Config::KeyManager).to(
106
- receive(:config_from_repo).with(repo_name).and_return(config)
106
+ receive(:config_from_repo).with(github_repo_name).and_return(config)
107
107
  )
108
108
  end
109
109
 
@@ -145,7 +145,7 @@ describe TxghServer::WebhookEndpoints do
145
145
  allow(TxghServer::Webhooks::Transifex::HookHandler).to(
146
146
  receive(:new) do |options|
147
147
  expect(options[:project].name).to eq(project_name)
148
- expect(options[:repo].name).to eq(repo_name)
148
+ expect(options[:repo].name).to eq(github_repo_name)
149
149
  handler
150
150
  end
151
151
  )
@@ -186,7 +186,7 @@ describe TxghServer::WebhookEndpoints do
186
186
  header(
187
187
  TxghServer::GithubRequestAuth::GITHUB_HEADER,
188
188
  TxghServer::GithubRequestAuth.compute_signature(
189
- body, config.github_repo.webhook_secret
189
+ body, config.git_repo.webhook_secret
190
190
  )
191
191
  )
192
192
  end
@@ -198,7 +198,7 @@ describe TxghServer::WebhookEndpoints do
198
198
  allow(TxghServer::Webhooks::Github::PushHandler).to(
199
199
  receive(:new) do |project, repo, logger, attributes|
200
200
  expect(project.name).to eq(project_name)
201
- expect(repo.name).to eq(repo_name)
201
+ expect(repo.name).to eq(github_repo_name)
202
202
  handler
203
203
  end
204
204
  )
@@ -209,7 +209,7 @@ describe TxghServer::WebhookEndpoints do
209
209
  receive(:execute).and_return(respond_with(200, true))
210
210
  )
211
211
 
212
- payload = GithubPayloadBuilder.push_payload(repo_name, ref)
212
+ payload = GithubPayloadBuilder.push_payload(github_repo_name, ref)
213
213
  payload.add_commit
214
214
 
215
215
  sign_with payload.to_json
@@ -220,7 +220,7 @@ describe TxghServer::WebhookEndpoints do
220
220
  end
221
221
 
222
222
  it 'returns unauthorized if not properly signed' do
223
- payload = GithubPayloadBuilder.push_payload(repo_name, ref)
223
+ payload = GithubPayloadBuilder.push_payload(github_repo_name, ref)
224
224
 
225
225
  header 'X-GitHub-Event', 'push'
226
226
  post '/github', payload.to_json
@@ -229,7 +229,7 @@ describe TxghServer::WebhookEndpoints do
229
229
  end
230
230
 
231
231
  it 'returns invalid request if event unrecognized' do
232
- payload = GithubPayloadBuilder.push_payload(repo_name, ref)
232
+ payload = GithubPayloadBuilder.push_payload(github_repo_name, ref)
233
233
 
234
234
  sign_with payload.to_json
235
235
  header 'X-GitHub-Event', 'foobar'
@@ -239,7 +239,7 @@ describe TxghServer::WebhookEndpoints do
239
239
  end
240
240
 
241
241
  it 'returns internal error on unexpected error' do
242
- payload = GithubPayloadBuilder.push_payload(repo_name, ref)
242
+ payload = GithubPayloadBuilder.push_payload(github_repo_name, ref)
243
243
 
244
244
  expect(Txgh::Config::KeyManager).to(
245
245
  receive(:config_from_repo).and_raise(StandardError)
@@ -266,7 +266,7 @@ describe TxghServer::TriggerEndpoints do
266
266
  end
267
267
 
268
268
  let(:config) do
269
- Txgh::Config::ConfigPair.new(project_config, repo_config)
269
+ Txgh::Config::ConfigPair.new(project_config, github_config)
270
270
  end
271
271
 
272
272
  before(:each) do
@@ -275,7 +275,7 @@ describe TxghServer::TriggerEndpoints do
275
275
  )
276
276
 
277
277
  allow(Txgh::Config::KeyManager).to(
278
- receive(:config_from_repo).with(repo_name).and_return(config)
278
+ receive(:config_from_repo).with(github_repo_name).and_return(config)
279
279
  )
280
280
  end
281
281
 
@@ -2,12 +2,10 @@ require 'spec_helper'
2
2
  require 'helpers/standard_txgh_setup'
3
3
  require 'yaml'
4
4
 
5
- include TxghServer
6
-
7
- describe DownloadHandler do
5
+ describe TxghServer::DownloadHandler do
8
6
  include StandardTxghSetup
9
7
 
10
- let(:format) { DownloadHandler::DEFAULT_FORMAT }
8
+ let(:format) { described_class::DEFAULT_FORMAT }
11
9
 
12
10
  let(:params) do
13
11
  {
@@ -30,7 +28,7 @@ describe DownloadHandler do
30
28
  end
31
29
 
32
30
  it 'responds with a streaming zip and has the project name as the attachment' do
33
- response = DownloadHandler.handle_request(request)
31
+ response = described_class.handle_request(request)
34
32
  expect(response).to be_streaming
35
33
  expect(response).to be_a(ZipStreamResponse)
36
34
  expect(response.attachment).to eq(project_name)
@@ -40,7 +38,7 @@ describe DownloadHandler do
40
38
  let(:format) { '.tgz' }
41
39
 
42
40
  it 'responds with a streaming tgz download' do
43
- response = DownloadHandler.handle_request(request)
41
+ response = described_class.handle_request(request)
44
42
  expect(response).to be_streaming
45
43
  expect(response).to be_a(TgzStreamResponse)
46
44
  end
@@ -49,7 +47,7 @@ describe DownloadHandler do
49
47
  context 'when an error occurs' do
50
48
  before(:each) do
51
49
  expect(request).to receive(:params).and_raise(StandardError)
52
- response = DownloadHandler.handle_request(request)
50
+ response = described_class.handle_request(request)
53
51
  expect(response).to_not be_streaming
54
52
  expect(response.status).to eq(500)
55
53
  end
@@ -58,7 +56,7 @@ describe DownloadHandler do
58
56
 
59
57
  context '#execute' do
60
58
  let(:handler) do
61
- DownloadHandler.new(transifex_project, github_repo, params, logger)
59
+ described_class.new(transifex_project, github_repo, params, logger)
62
60
  end
63
61
 
64
62
  it 'responds with a streaming zip download' do
@@ -1,9 +1,7 @@
1
1
  require 'spec_helper'
2
2
  require 'rack'
3
3
 
4
- include TxghServer
5
-
6
- describe GithubRequestAuth do
4
+ describe TxghServer::GithubRequestAuth do
7
5
  let(:secret) { 'abc123' }
8
6
  let(:params) { '{"param1":"value1","param2":"value2","param3":123}' }
9
7
  let(:valid_signature) { 'ea62c3f65c8e42f155d96a25b7ba6eb5d320630e' }
@@ -11,28 +9,28 @@ describe GithubRequestAuth do
11
9
  describe '.authentic_request?' do
12
10
  it 'returns true if the request is signed correctly' do
13
11
  request = Rack::Request.new(
14
- GithubRequestAuth::RACK_HEADER => "sha1=#{valid_signature}",
12
+ described_class::RACK_HEADER => "sha1=#{valid_signature}",
15
13
  'rack.input' => StringIO.new(params)
16
14
  )
17
15
 
18
- authentic = GithubRequestAuth.authentic_request?(request, secret)
16
+ authentic = described_class.authentic_request?(request, secret)
19
17
  expect(authentic).to eq(true)
20
18
  end
21
19
 
22
20
  it 'returns false if the request is not signed correctly' do
23
21
  request = Rack::Request.new(
24
- GithubRequestAuth::RACK_HEADER => 'incorrect',
22
+ described_class::RACK_HEADER => 'incorrect',
25
23
  'rack.input' => StringIO.new(params)
26
24
  )
27
25
 
28
- authentic = GithubRequestAuth.authentic_request?(request, secret)
26
+ authentic = described_class.authentic_request?(request, secret)
29
27
  expect(authentic).to eq(false)
30
28
  end
31
29
  end
32
30
 
33
31
  describe '.compute_signature' do
34
32
  it 'calculates the signature and formats it as an http header' do
35
- value = GithubRequestAuth.compute_signature(params, secret)
33
+ value = described_class.compute_signature(params, secret)
36
34
  expect(value).to eq("sha1=#{valid_signature}")
37
35
  end
38
36
  end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+ require 'rack'
3
+
4
+ describe TxghServer::GitlabRequestAuth do
5
+ let(:secret) { 'abc123' }
6
+
7
+ describe '.authentic_request?' do
8
+ it 'returns true if the request is signed correctly' do
9
+ request = Rack::Request.new(
10
+ described_class::RACK_HEADER => secret
11
+ )
12
+
13
+ authentic = described_class.authentic_request?(request, secret)
14
+ expect(authentic).to eq(true)
15
+ end
16
+
17
+ it 'returns false if the request is not signed correctly' do
18
+ request = Rack::Request.new(
19
+ described_class::RACK_HEADER => 'incorrect'
20
+ )
21
+
22
+ authentic = described_class.authentic_request?(request, secret)
23
+ expect(authentic).to eq(false)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,132 @@
1
+ require 'json'
2
+
3
+ class GitlabPayloadBuilder
4
+ class << self
5
+ def push_payload(*args)
6
+ GitlabPushPayload.new(*args)
7
+ end
8
+
9
+ def delete_payload(*args)
10
+ GitlabDeletePayload.new(*args)
11
+ end
12
+ end
13
+ end
14
+
15
+ class GitlabPayload
16
+ def to_h
17
+ # convert symbolized keys to strings
18
+ JSON.parse(to_json)
19
+ end
20
+
21
+ def to_json
22
+ @result.to_json
23
+ end
24
+
25
+ def merge!(hash)
26
+ @result.merge!(hash)
27
+ end
28
+
29
+ private
30
+
31
+ def digits
32
+ @@digits ||= ('a'..'f').to_a + ('0'..'9').to_a
33
+ end
34
+
35
+ def generate_timestamp
36
+ Time.now.strftime('%Y-%m-%dT%H:%M:%S%:z')
37
+ end
38
+
39
+ def generate_sha
40
+ blank_commit_id.gsub(/0/) { digits.sample }
41
+ end
42
+
43
+ def blank_commit_id
44
+ '0' * 40
45
+ end
46
+ end
47
+
48
+ class GitlabDeletePayload < GitlabPayload
49
+ attr_reader :repo, :ref
50
+
51
+ def initialize(repo, ref)
52
+ @repo = repo
53
+ @ref = ref
54
+
55
+ @result = {
56
+ ref: "refs/#{ref}",
57
+ before: generate_sha,
58
+ after: blank_commit_id,
59
+ project: {
60
+ path_with_namespace: repo
61
+ },
62
+ repository: {
63
+ name: repo.split('/').last
64
+ }
65
+ }
66
+ end
67
+ end
68
+
69
+ class GitlabPushPayload < GitlabPayload
70
+ attr_reader :repo, :ref, :before, :after
71
+
72
+ DEFAULT_USER = {
73
+ name: 'Test User',
74
+ email: 'test@user.com',
75
+ username: 'testuser'
76
+ }
77
+
78
+ def initialize(repo, ref, before = nil, after = nil)
79
+ @repo = repo
80
+ @ref = ref
81
+ @before = before || blank_commit_id
82
+ @after = after || generate_sha
83
+
84
+ @result = {
85
+ ref: "refs/#{ref}",
86
+ before: @before,
87
+ after: @after,
88
+ commits: [],
89
+ user_name: DEFAULT_USER[:username],
90
+ project: {
91
+ path_with_namespace: repo
92
+ },
93
+ repository: {
94
+ name: repo.split('/').last
95
+ }
96
+ }
97
+ end
98
+
99
+ def add_commit(options = {})
100
+ id = if commits.empty? && !options.include?(:id)
101
+ after
102
+ else
103
+ options.fetch(:id) { generate_sha }
104
+ end
105
+
106
+ commit_data = {
107
+ id: id,
108
+ message: options.fetch(:message, 'Default commit message'),
109
+ timestamp: options.fetch(:timestamp) { generate_timestamp },
110
+ url: "https://gitlab.com/#{repo}/-/commit/#{id}",
111
+ author: options.fetch(:author, DEFAULT_USER),
112
+ committer: options.fetch(:committer, DEFAULT_USER),
113
+ added: options.fetch(:added, []),
114
+ removed: options.fetch(:removed, []),
115
+ modified: options.fetch(:modified, [])
116
+ }
117
+
118
+ if commit_data[:id] == after
119
+ @result[:head_commit] = commit_data
120
+ end
121
+
122
+ commits << commit_data
123
+ end
124
+
125
+ def commits
126
+ @result[:commits]
127
+ end
128
+
129
+ def head_commit
130
+ @result[:head_commit]
131
+ end
132
+ end