txgh 1.0.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.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +202 -0
  3. data/README.md +64 -0
  4. data/lib/ext/zipline/output_stream.rb +62 -0
  5. data/lib/txgh.rb +53 -0
  6. data/lib/txgh/app.rb +135 -0
  7. data/lib/txgh/category_support.rb +31 -0
  8. data/lib/txgh/config.rb +11 -0
  9. data/lib/txgh/config/config_pair.rb +36 -0
  10. data/lib/txgh/config/key_manager.rb +54 -0
  11. data/lib/txgh/config/provider_instance.rb +20 -0
  12. data/lib/txgh/config/provider_support.rb +26 -0
  13. data/lib/txgh/config/providers.rb +9 -0
  14. data/lib/txgh/config/providers/file_provider.rb +19 -0
  15. data/lib/txgh/config/providers/git_provider.rb +58 -0
  16. data/lib/txgh/config/providers/raw_provider.rb +19 -0
  17. data/lib/txgh/config/tx_config.rb +77 -0
  18. data/lib/txgh/config/tx_manager.rb +15 -0
  19. data/lib/txgh/diff_calculator.rb +90 -0
  20. data/lib/txgh/empty_resource_contents.rb +43 -0
  21. data/lib/txgh/errors.rb +9 -0
  22. data/lib/txgh/github_api.rb +83 -0
  23. data/lib/txgh/github_repo.rb +88 -0
  24. data/lib/txgh/github_request_auth.rb +28 -0
  25. data/lib/txgh/handlers.rb +12 -0
  26. data/lib/txgh/handlers/download_handler.rb +84 -0
  27. data/lib/txgh/handlers/github.rb +10 -0
  28. data/lib/txgh/handlers/github/delete_handler.rb +65 -0
  29. data/lib/txgh/handlers/github/handler.rb +20 -0
  30. data/lib/txgh/handlers/github/push_handler.rb +108 -0
  31. data/lib/txgh/handlers/github/request_handler.rb +106 -0
  32. data/lib/txgh/handlers/response.rb +17 -0
  33. data/lib/txgh/handlers/stream_response.rb +39 -0
  34. data/lib/txgh/handlers/tgz_stream_response.rb +41 -0
  35. data/lib/txgh/handlers/transifex.rb +8 -0
  36. data/lib/txgh/handlers/transifex/hook_handler.rb +77 -0
  37. data/lib/txgh/handlers/transifex/request_handler.rb +78 -0
  38. data/lib/txgh/handlers/triggers.rb +9 -0
  39. data/lib/txgh/handlers/triggers/handler.rb +66 -0
  40. data/lib/txgh/handlers/triggers/pull_handler.rb +29 -0
  41. data/lib/txgh/handlers/triggers/push_handler.rb +21 -0
  42. data/lib/txgh/handlers/zip_stream_response.rb +21 -0
  43. data/lib/txgh/merge_calculator.rb +74 -0
  44. data/lib/txgh/parse_config.rb +24 -0
  45. data/lib/txgh/resource_committer.rb +39 -0
  46. data/lib/txgh/resource_contents.rb +118 -0
  47. data/lib/txgh/resource_downloader.rb +141 -0
  48. data/lib/txgh/resource_updater.rb +104 -0
  49. data/lib/txgh/response_helpers.rb +30 -0
  50. data/lib/txgh/transifex_api.rb +165 -0
  51. data/lib/txgh/transifex_project.rb +37 -0
  52. data/lib/txgh/transifex_request_auth.rb +53 -0
  53. data/lib/txgh/tx_branch_resource.rb +59 -0
  54. data/lib/txgh/tx_logger.rb +12 -0
  55. data/lib/txgh/tx_resource.rb +66 -0
  56. data/lib/txgh/utils.rb +44 -0
  57. data/lib/txgh/version.rb +3 -0
  58. data/spec/app_spec.rb +346 -0
  59. data/spec/category_support_spec.rb +43 -0
  60. data/spec/config/config_pair_spec.rb +47 -0
  61. data/spec/config/key_manager_spec.rb +48 -0
  62. data/spec/config/provider_instance_spec.rb +30 -0
  63. data/spec/config/provider_support_spec.rb +55 -0
  64. data/spec/config/tx_config_spec.rb +49 -0
  65. data/spec/config/tx_manager_spec.rb +57 -0
  66. data/spec/diff_calculator_spec.rb +90 -0
  67. data/spec/github_api_spec.rb +148 -0
  68. data/spec/github_repo_spec.rb +178 -0
  69. data/spec/github_request_auth_spec.rb +39 -0
  70. data/spec/handlers/download_handler_spec.rb +81 -0
  71. data/spec/handlers/github/delete_handler_spec.rb +71 -0
  72. data/spec/handlers/github/push_handler_spec.rb +76 -0
  73. data/spec/handlers/tgz_stream_response_spec.rb +59 -0
  74. data/spec/handlers/transifex/hook_handler_spec.rb +115 -0
  75. data/spec/handlers/zip_stream_response_spec.rb +58 -0
  76. data/spec/helpers/github_payload_builder.rb +141 -0
  77. data/spec/helpers/integration_setup.rb +47 -0
  78. data/spec/helpers/nil_logger.rb +10 -0
  79. data/spec/helpers/standard_txgh_setup.rb +92 -0
  80. data/spec/helpers/test_provider.rb +12 -0
  81. data/spec/integration/cassettes/github_l10n_hook_endpoint.yml +536 -0
  82. data/spec/integration/cassettes/pull.yml +47 -0
  83. data/spec/integration/cassettes/push.yml +544 -0
  84. data/spec/integration/cassettes/transifex_hook_endpoint.yml +560 -0
  85. data/spec/integration/config/tx.config +10 -0
  86. data/spec/integration/hooks_spec.rb +158 -0
  87. data/spec/integration/payloads/github_postbody.json +161 -0
  88. data/spec/integration/payloads/github_postbody_l10n.json +136 -0
  89. data/spec/integration/payloads/github_postbody_release.json +136 -0
  90. data/spec/integration/triggers_spec.rb +45 -0
  91. data/spec/merge_calculator_spec.rb +112 -0
  92. data/spec/parse_config_spec.rb +52 -0
  93. data/spec/resource_committer_spec.rb +42 -0
  94. data/spec/resource_contents_spec.rb +212 -0
  95. data/spec/resource_downloader_spec.rb +205 -0
  96. data/spec/resource_updater_spec.rb +147 -0
  97. data/spec/spec_helper.rb +32 -0
  98. data/spec/transifex_api_spec.rb +345 -0
  99. data/spec/transifex_project_spec.rb +45 -0
  100. data/spec/transifex_request_auth_spec.rb +39 -0
  101. data/spec/tx_branch_resource_spec.rb +99 -0
  102. data/spec/tx_resource_spec.rb +47 -0
  103. data/spec/utils_spec.rb +58 -0
  104. data/txgh.gemspec +29 -0
  105. metadata +296 -0
@@ -0,0 +1,108 @@
1
+ require 'base64'
2
+
3
+ module Txgh
4
+ module Handlers
5
+ module Github
6
+ class PushHandler < Handler
7
+
8
+ def execute
9
+ # Check if the branch in the hook data is the configured branch we want
10
+ logger.info("request github branch: #{branch}")
11
+ logger.info("config github branch: #{repo.github_config_branch}")
12
+
13
+ if repo.should_process_ref?(branch)
14
+ logger.info('found branch in github request')
15
+
16
+ tx_resources = tx_resources_for(branch)
17
+ modified_resources = added_and_modified_resources_for(tx_resources)
18
+ modified_resources.merge!(l10n_resources_for(tx_resources))
19
+
20
+ if repo.github_config_branch.include?('tags/')
21
+ modified_resources.merge!(tag_resources_for(tx_resources))
22
+ end
23
+
24
+ # Handle DBZ 'L10N' special case
25
+ if branch.include?("L10N")
26
+ logger.info('processing L10N tag')
27
+
28
+ # Create a new branch off tag commit
29
+ if branch.include?('tags/L10N')
30
+ repo.api.create_ref(repo.name, 'heads/L10N', payload['head_commit']['id'])
31
+ end
32
+ end
33
+
34
+ updater = ResourceUpdater.new(project, repo, logger)
35
+ categories = { 'author' => payload['head_commit']['committer']['name'] }
36
+
37
+ modified_resources.each_pair do |resource, commit_sha|
38
+ updater.update_resource(resource, commit_sha, categories)
39
+ end
40
+ end
41
+
42
+ respond_with(200, true)
43
+ end
44
+
45
+ private
46
+
47
+ def tag_resources_for(tx_resources)
48
+ payload['head_commit']['modified'].each_with_object({}) do |modified, ret|
49
+ logger.info("processing modified file: #{modified}")
50
+
51
+ if tx_resources.include?(modified)
52
+ ret[tx_resources[modified]] = payload['head_commit']['id']
53
+ end
54
+ end
55
+ end
56
+
57
+ def l10n_resources_for(tx_resources)
58
+ payload['head_commit']['modified'].each_with_object({}) do |modified, ret|
59
+ if tx_resources.include?(modified)
60
+ logger.info("setting new resource: #{tx_resources[modified].L10N_resource_slug}")
61
+ ret[tx_resources[modified]] = payload['head_commit']['id']
62
+ end
63
+ end
64
+ end
65
+
66
+ # Finds the updated resources and maps the most recent commit in which
67
+ # each was modified
68
+ def added_and_modified_resources_for(tx_resources)
69
+ payload['commits'].each_with_object({}) do |commit, ret|
70
+ logger.info('processing commit')
71
+
72
+ (commit['modified'] + commit['added']).each do |file|
73
+ logger.info("processing added/modified file: #{file}")
74
+
75
+ if tx_resources.include?(file)
76
+ ret[tx_resources[file]] = commit['id']
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ # Build an index of known Tx resources, by source file
83
+ def tx_resources_for(branch)
84
+ tx_config.resources.each_with_object({}) do |resource, ret|
85
+ logger.info('processing resource')
86
+
87
+ # If we're processing by branch, create a branch resource. Otherwise,
88
+ # use the original resource.
89
+ ret[resource.source_file] = if repo.process_all_branches?
90
+ TxBranchResource.new(resource, branch) # maybe find instead?
91
+ else
92
+ resource
93
+ end
94
+ end
95
+ end
96
+
97
+ def tx_config
98
+ @tx_config ||= Txgh::Config::TxManager.tx_config(project, repo, branch)
99
+ end
100
+
101
+ def branch
102
+ @ref ||= payload['ref'].sub(/^refs\//, '')
103
+ end
104
+
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,106 @@
1
+ require 'json'
2
+
3
+ module Txgh
4
+ module Handlers
5
+ module Github
6
+ class RequestHandler
7
+ class << self
8
+
9
+ include ResponseHelpers
10
+
11
+ def handle_request(request, logger)
12
+ case request.env['HTTP_X_GITHUB_EVENT']
13
+ when 'push'
14
+ handle_push(request, logger)
15
+ when 'delete'
16
+ handle_delete(request, logger)
17
+ else
18
+ handle_unexpected
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def handle_push(request, logger)
25
+ klass = Txgh::Handlers::Github::PushHandler
26
+ new(request, logger).handle(klass)
27
+ end
28
+
29
+ def handle_delete(request, logger)
30
+ klass = Txgh::Handlers::Github::DeleteHandler
31
+ new(request, logger).handle(klass)
32
+ end
33
+
34
+ def handle_unexpected
35
+ respond_with_error(400, 'Unexpected event type')
36
+ end
37
+
38
+ end
39
+
40
+ include ResponseHelpers
41
+
42
+ attr_reader :request, :logger
43
+
44
+ def initialize(request, logger)
45
+ @request = request
46
+ @logger = logger
47
+ end
48
+
49
+ def handle(klass)
50
+ handle_safely do
51
+ handler = klass.new(
52
+ project: config.transifex_project,
53
+ repo: config.github_repo,
54
+ payload: payload,
55
+ logger: logger
56
+ )
57
+
58
+ handler.execute
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def handle_safely
65
+ if authentic_request?
66
+ yield
67
+ else
68
+ respond_with_error(401, 'Unauthorized')
69
+ end
70
+ rescue => e
71
+ respond_with_error(500, "Internal server error: #{e.message}", e)
72
+ end
73
+
74
+ def payload
75
+ @payload ||= if request.params[:payload]
76
+ JSON.parse(request.params[:payload])
77
+ else
78
+ JSON.parse(request.body.read)
79
+ end
80
+ end
81
+
82
+ def github_repo_name
83
+ payload['repository']['full_name']
84
+ end
85
+
86
+ def config
87
+ @config ||= Txgh::Config::KeyManager.config_from_repo(github_repo_name)
88
+ end
89
+
90
+ def repo
91
+ config.github_repo
92
+ end
93
+
94
+ def authentic_request?
95
+ if repo.webhook_protected?
96
+ GithubRequestAuth.authentic_request?(
97
+ request, repo.webhook_secret
98
+ )
99
+ else
100
+ true
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,17 @@
1
+ module Txgh
2
+ module Handlers
3
+ class Response
4
+ attr_reader :status, :body, :error
5
+
6
+ def initialize(status, body, error = nil)
7
+ @status = status
8
+ @body = body
9
+ @error = error
10
+ end
11
+
12
+ def streaming?
13
+ false
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ require 'mime/types'
2
+
3
+ module Txgh
4
+ module Handlers
5
+ class StreamResponse
6
+ attr_reader :attachment, :enum
7
+
8
+ def initialize(attachment, enum)
9
+ @attachment = attachment
10
+ @enum = enum
11
+ end
12
+
13
+ def write_to(stream)
14
+ raise NotImplementedError,
15
+ "please implement #{__method__} in derived classes"
16
+ end
17
+
18
+ def file_extension
19
+ raise NotImplementedError,
20
+ "please implement #{__method__} in derived classes"
21
+ end
22
+
23
+ def headers
24
+ @headers ||= {
25
+ 'Content-Disposition' => "attachment; filename=\"#{attachment}#{file_extension}\"",
26
+ 'Content-Type' => MIME::Types.type_for(file_extension).first.content_type
27
+ }
28
+ end
29
+
30
+ def streaming?
31
+ true
32
+ end
33
+
34
+ def error
35
+ nil
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,41 @@
1
+ require 'rubygems/package'
2
+ require 'stringio'
3
+ require 'zlib'
4
+
5
+ module Txgh
6
+ module Handlers
7
+ class TgzStreamResponse < StreamResponse
8
+ PERMISSIONS = 0644
9
+
10
+ def write_to(stream)
11
+ Zlib::GzipWriter.wrap(stream) do |gz|
12
+ pipe = StringIO.new('', 'wb')
13
+ tar = Gem::Package::TarWriter.new(pipe)
14
+
15
+ enum.each do |file_name, contents|
16
+ tar.add_file(file_name, PERMISSIONS) do |f|
17
+ f.write(contents)
18
+ end
19
+
20
+ flush(tar, pipe, gz)
21
+ stream.flush
22
+ end
23
+
24
+ flush(tar, pipe, gz)
25
+ end
26
+ end
27
+
28
+ def file_extension
29
+ '.tgz'
30
+ end
31
+
32
+ private
33
+
34
+ def flush(tar, pipe, gz)
35
+ tar.flush
36
+ gz.write(pipe.string)
37
+ pipe.reopen('')
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,8 @@
1
+ module Txgh
2
+ module Handlers
3
+ module Transifex
4
+ autoload :HookHandler, 'txgh/handlers/transifex/hook_handler'
5
+ autoload :RequestHandler, 'txgh/handlers/transifex/request_handler'
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,77 @@
1
+ require 'logger'
2
+
3
+ module Txgh
4
+ module Handlers
5
+ module Transifex
6
+ class HookHandler
7
+ include CategorySupport
8
+ include ResponseHelpers
9
+
10
+ attr_reader :project, :repo, :resource_slug, :language, :logger
11
+
12
+ def initialize(options = {})
13
+ @project = options.fetch(:project)
14
+ @repo = options.fetch(:repo)
15
+ @resource_slug = options.fetch(:resource_slug)
16
+ @language = options.fetch(:language)
17
+ @logger = options.fetch(:logger) { Logger.new(STDOUT) }
18
+ end
19
+
20
+ def execute
21
+ logger.info(resource_slug)
22
+
23
+ if tx_config
24
+ if tx_resource
25
+ committer = ResourceCommitter.new(project, repo, logger)
26
+ committer.commit_resource(tx_resource, branch, language)
27
+
28
+ respond_with(200, true)
29
+ else
30
+ respond_with_error(
31
+ 404, "Could not find resource '#{resource_slug}' in config"
32
+ )
33
+ end
34
+ else
35
+ respond_with_error(
36
+ 404, "Could not find configuration for branch '#{branch}'"
37
+ )
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def tx_config
44
+ @tx_config ||= Txgh::Config::TxManager.tx_config(project, repo, branch)
45
+ rescue ConfigNotFoundError, TxghError
46
+ nil
47
+ end
48
+
49
+ def branch
50
+ @branch ||= begin
51
+ branch_candidate = if process_all_branches?
52
+ resource = project.api.get_resource(project.name, resource_slug)
53
+ categories = deserialize_categories(Array(resource['categories']))
54
+ categories['branch']
55
+ else
56
+ repo.branch || 'master'
57
+ end
58
+
59
+ Utils.absolute_branch(branch_candidate)
60
+ end
61
+ end
62
+
63
+ def tx_resource
64
+ @tx_resource ||= if process_all_branches?
65
+ tx_config.resource(resource_slug, branch)
66
+ else
67
+ tx_config.resource(resource_slug)
68
+ end
69
+ end
70
+
71
+ def process_all_branches?
72
+ repo.process_all_branches?
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,78 @@
1
+ require 'uri'
2
+
3
+ module Txgh
4
+ module Handlers
5
+ module Transifex
6
+ class RequestHandler
7
+ class << self
8
+
9
+ def handle_request(request, logger)
10
+ new(request, logger).handle(Txgh::Handlers::Transifex::HookHandler)
11
+ end
12
+
13
+ end
14
+
15
+ include ResponseHelpers
16
+
17
+ attr_reader :request, :logger
18
+
19
+ def initialize(request, logger)
20
+ @request = request
21
+ @logger = logger
22
+ end
23
+
24
+ def handle(klass)
25
+ handle_safely do
26
+ handler = klass.new(
27
+ project: config.transifex_project,
28
+ repo: config.github_repo,
29
+ resource_slug: payload['resource'],
30
+ language: payload['language'],
31
+ logger: logger
32
+ )
33
+
34
+ handler.execute
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def handle_safely
41
+ if authentic_request?
42
+ yield
43
+ else
44
+ respond_with_error(401, 'Unauthorized')
45
+ end
46
+ rescue => e
47
+ respond_with_error(500, "Internal server error: #{e.message}", e)
48
+ end
49
+
50
+ def authentic_request?
51
+ if project.webhook_protected?
52
+ TransifexRequestAuth.authentic_request?(
53
+ request, project.webhook_secret
54
+ )
55
+ else
56
+ true
57
+ end
58
+ end
59
+
60
+ def project
61
+ config.transifex_project
62
+ end
63
+
64
+ def config
65
+ @config ||= Txgh::Config::KeyManager.config_from_project(payload['project'])
66
+ end
67
+
68
+ def payload
69
+ @payload ||= begin
70
+ request.body.rewind
71
+ Hash[URI.decode_www_form(request.body.read)]
72
+ end
73
+ end
74
+
75
+ end
76
+ end
77
+ end
78
+ end