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,43 @@
1
+ module Txgh
2
+ class EmptyResourceContents
3
+ attr_reader :tx_resource
4
+
5
+ def initialize(tx_resource)
6
+ @tx_resource = tx_resource
7
+ end
8
+
9
+ def phrases
10
+ []
11
+ end
12
+
13
+ def write_to(*args)
14
+ # no-op
15
+ end
16
+
17
+ def to_s(*args)
18
+ ''
19
+ end
20
+
21
+ def to_h
22
+ {}
23
+ end
24
+
25
+ def diff(other_contents)
26
+ other_contents
27
+ end
28
+
29
+ def diff_hash(other_contents)
30
+ DiffCalculator::INCLUDED_STATES.each_with_object({}) do |state, ret|
31
+ ret[state] = []
32
+ end
33
+ end
34
+
35
+ def merge(other_contents, *args)
36
+ other_contents
37
+ end
38
+
39
+ def empty?
40
+ true
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ module Txgh
2
+ class TxghError < StandardError; end
3
+ class TxghInternalError < TxghError; end
4
+
5
+ class TransifexApiError < StandardError; end
6
+ class TransifexNotFoundError < TransifexApiError; end
7
+ class TransifexUnauthorizedError < TransifexApiError; end
8
+ class ConfigNotFoundError < StandardError; end
9
+ end
@@ -0,0 +1,83 @@
1
+ require 'base64'
2
+ require 'octokit'
3
+
4
+ module Txgh
5
+ class GithubApi
6
+ class << self
7
+ def create_from_credentials(login, access_token)
8
+ create_from_client(
9
+ Octokit::Client.new(login: login, access_token: access_token)
10
+ )
11
+ end
12
+
13
+ def create_from_client(client)
14
+ new(client)
15
+ end
16
+ end
17
+
18
+ attr_reader :client
19
+
20
+ def initialize(client)
21
+ @client = client
22
+ end
23
+
24
+ def tree(repo, sha)
25
+ client.tree(repo, sha, recursive: 1)
26
+ end
27
+
28
+ def blob(repo, sha)
29
+ client.blob(repo, sha)
30
+ end
31
+
32
+ def create_ref(repo, branch, sha)
33
+ client.create_ref(repo, branch, sha) rescue false
34
+ end
35
+
36
+ def commit(repo, branch, content_map, allow_empty = false)
37
+ parent = client.ref(repo, branch)
38
+ base_commit = get_commit(repo, parent[:object][:sha])
39
+
40
+ tree_data = content_map.map do |path, content|
41
+ blob = client.create_blob(repo, content)
42
+ { path: path, mode: '100644', type: 'blob', sha: blob }
43
+ end
44
+
45
+ tree_options = { base_tree: base_commit[:commit][:tree][:sha] }
46
+
47
+ tree = client.create_tree(repo, tree_data, tree_options)
48
+ commit = client.create_commit(
49
+ repo, "Updating translations for #{content_map.keys.join(", ")}",
50
+ tree[:sha], parent[:object][:sha]
51
+ )
52
+
53
+ # don't update the ref if the commit introduced no new changes
54
+ unless allow_empty
55
+ diff = client.compare(repo, parent[:object][:sha], commit[:sha])
56
+ return if diff[:files].empty?
57
+ end
58
+
59
+ # false means don't force push
60
+ client.update_ref(repo, branch, commit[:sha], false)
61
+ end
62
+
63
+ def get_commit(repo, sha)
64
+ client.commit(repo, sha)
65
+ end
66
+
67
+ def get_ref(repo, ref)
68
+ client.ref(repo, ref)
69
+ end
70
+
71
+ def download(repo, path, branch)
72
+ master = client.ref(repo, branch)
73
+ commit = client.commit(repo, master[:object][:sha])
74
+ tree = client.tree(repo, commit[:commit][:tree][:sha], recursive: 1)
75
+
76
+ if found = tree[:tree].find { |t| t[:path] == path }
77
+ b = blob(repo, found[:sha])
78
+ b['encoding'] == 'utf-8' ? b['content'] : Base64.decode64(b['content'])
79
+ end
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,88 @@
1
+ module Txgh
2
+ class GithubRepo
3
+ attr_reader :config, :api
4
+
5
+ def initialize(config, api)
6
+ @config = config
7
+ @api = api
8
+ end
9
+
10
+ def name
11
+ config['name']
12
+ end
13
+
14
+ def branch
15
+ config['branch']
16
+ end
17
+
18
+ def tag
19
+ config['tag']
20
+ end
21
+
22
+ def process_all_branches?
23
+ branch == 'all'
24
+ end
25
+
26
+ def upload_diffs?
27
+ !(diff_point || '').empty?
28
+ end
29
+
30
+ def diff_point
31
+ config['diff_point']
32
+ end
33
+
34
+ def process_all_tags?
35
+ tag == 'all'
36
+ end
37
+
38
+ def should_process_ref?(candidate)
39
+ if Utils.is_tag?(candidate)
40
+ should_process_tag?(candidate)
41
+ else
42
+ should_process_branch?(candidate)
43
+ end
44
+ end
45
+
46
+ def github_config_branch
47
+ @github_config_branch ||= begin
48
+ if process_all_branches?
49
+ 'all'
50
+ else
51
+ Utils.absolute_branch(branch || 'master')
52
+ end
53
+ end
54
+ end
55
+
56
+ def github_config_tag
57
+ @github_config_tag ||= begin
58
+ if process_all_tags?
59
+ 'all'
60
+ else
61
+ Utils.absolute_branch(tag) if tag
62
+ end
63
+ end
64
+ end
65
+
66
+ def webhook_secret
67
+ config['webhook_secret']
68
+ end
69
+
70
+ def webhook_protected?
71
+ !(webhook_secret || '').empty?
72
+ end
73
+
74
+ private
75
+
76
+ def should_process_branch?(candidate)
77
+ process_all_branches? ||
78
+ candidate.include?(github_config_branch) ||
79
+ candidate.include?('L10N')
80
+ end
81
+
82
+ def should_process_tag?(candidate)
83
+ process_all_tags? || (
84
+ github_config_tag && candidate.include?(github_config_tag)
85
+ )
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,28 @@
1
+ require 'openssl'
2
+
3
+ module Txgh
4
+ class GithubRequestAuth
5
+ HMAC_DIGEST = OpenSSL::Digest.new('sha1')
6
+ RACK_HEADER = 'HTTP_X_HUB_SIGNATURE'
7
+ GITHUB_HEADER = 'X-Hub-Signature'
8
+
9
+ class << self
10
+ def authentic_request?(request, secret)
11
+ request.body.rewind
12
+ expected_signature = header_value(request.body.read, secret)
13
+ actual_signature = request.env[RACK_HEADER]
14
+ actual_signature == expected_signature
15
+ end
16
+
17
+ def header_value(content, secret)
18
+ "sha1=#{digest(content, secret)}"
19
+ end
20
+
21
+ private
22
+
23
+ def digest(content, secret)
24
+ OpenSSL::HMAC.hexdigest(HMAC_DIGEST, secret, content)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,12 @@
1
+ module Txgh
2
+ module Handlers
3
+ autoload :DownloadHandler, 'txgh/handlers/download_handler'
4
+ autoload :Github, 'txgh/handlers/github'
5
+ autoload :Response, 'txgh/handlers/response'
6
+ autoload :StreamResponse, 'txgh/handlers/stream_response'
7
+ autoload :TgzStreamResponse, 'txgh/handlers/tgz_stream_response'
8
+ autoload :Transifex, 'txgh/handlers/transifex'
9
+ autoload :Triggers, 'txgh/handlers/triggers'
10
+ autoload :ZipStreamResponse, 'txgh/handlers/zip_stream_response'
11
+ end
12
+ end
@@ -0,0 +1,84 @@
1
+ module Txgh
2
+ module Handlers
3
+ class DownloadHandler
4
+ DEFAULT_FORMAT = '.zip'
5
+
6
+ # includes response helpers in both the class and the singleton class
7
+ include ResponseHelpers
8
+
9
+ class << self
10
+ def handle_request(request, logger = nil)
11
+ handle_safely do
12
+ config = config_from(request)
13
+ project, repo = [config.transifex_project, config.github_repo]
14
+ params = params_from(request)
15
+ handler = new(project, repo, params, logger)
16
+ handler.execute
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def config_from(request)
23
+ Txgh::Config::KeyManager.config_from_project(
24
+ request.params.fetch('project_slug')
25
+ )
26
+ end
27
+
28
+ def params_from(request)
29
+ request.params.merge(
30
+ 'format' => format_from(request)
31
+ )
32
+ end
33
+
34
+ def format_from(request)
35
+ # sinatra is dumb and doesn't include any of the URL captures in the
36
+ # request params or env hash
37
+ File.extname(request.env['REQUEST_PATH'])
38
+ end
39
+
40
+ def handle_safely
41
+ yield
42
+ rescue => e
43
+ respond_with_error(500, "Internal server error: #{e.message}", e)
44
+ end
45
+ end
46
+
47
+ attr_reader :project, :repo, :params, :logger
48
+
49
+ def initialize(project, repo, params, logger)
50
+ @project = project
51
+ @repo = repo
52
+ @params = params
53
+ @logger = logger
54
+ end
55
+
56
+ def execute
57
+ downloader = ResourceDownloader.new(project, repo, params['branch'])
58
+ response_class.new(attachment, downloader.each)
59
+ end
60
+
61
+ private
62
+
63
+ def attachment
64
+ project.name
65
+ end
66
+
67
+ def format
68
+ params.fetch('format', DEFAULT_FORMAT)
69
+ end
70
+
71
+ def response_class
72
+ case format
73
+ when '.zip'
74
+ ZipStreamResponse
75
+ when '.tgz'
76
+ TgzStreamResponse
77
+ else
78
+ raise TxghInternalError,
79
+ "'#{format}' is not a valid download format"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,10 @@
1
+ module Txgh
2
+ module Handlers
3
+ module Github
4
+ autoload :DeleteHandler, 'txgh/handlers/github/delete_handler'
5
+ autoload :Handler, 'txgh/handlers/github/handler'
6
+ autoload :PushHandler, 'txgh/handlers/github/push_handler'
7
+ autoload :RequestHandler, 'txgh/handlers/github/request_handler'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,65 @@
1
+ module Txgh
2
+ module Handlers
3
+ module Github
4
+ class DeleteHandler < Handler
5
+
6
+ include CategorySupport
7
+
8
+ def execute
9
+ delete_resources if should_handle_request?
10
+ respond_with(200, true)
11
+ end
12
+
13
+ private
14
+
15
+ def delete_resources
16
+ tx_resources.each do |tx_resource|
17
+ logger.info("Deleting #{tx_resource.resource_slug}")
18
+ project.api.delete(tx_resource)
19
+ end
20
+ end
21
+
22
+ def tx_resources
23
+ project.api.get_resources(project.name).map do |resource_hash|
24
+ categories = deserialize_categories(resource_hash['categories'])
25
+ resource_branch = Utils.absolute_branch(categories['branch'])
26
+
27
+ if resource_branch == branch
28
+ tx_branch_resource_from(resource_hash, branch)
29
+ end
30
+ end.compact
31
+ end
32
+
33
+ def tx_branch_resource_from(resource_hash, branch)
34
+ TxBranchResource.new(
35
+ tx_resource_from(resource_hash, branch), branch
36
+ )
37
+ end
38
+
39
+ # project_slug, resource_slug, type, source_lang, source_file, lang_map, translation_file
40
+ def tx_resource_from(resource_hash, branch)
41
+ TxResource.new(
42
+ project.name,
43
+ TxBranchResource.deslugify(resource_hash['slug'], branch),
44
+ resource_hash['i18n_type'],
45
+ resource_hash['source_language_code'],
46
+ resource_hash['name'],
47
+ '', nil
48
+ )
49
+ end
50
+
51
+ def should_handle_request?
52
+ # ref_type can be either 'branch' or 'tag' - we only care about branches
53
+ payload['ref_type'] == 'branch' &&
54
+ repo.should_process_ref?(branch) &&
55
+ project.auto_delete_resources?
56
+ end
57
+
58
+ def branch
59
+ Utils.absolute_branch(payload['ref'].sub(/^refs\//, ''))
60
+ end
61
+
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,20 @@
1
+ require 'logger'
2
+
3
+ module Txgh
4
+ module Handlers
5
+ module Github
6
+ class Handler
7
+ include ResponseHelpers
8
+
9
+ attr_reader :project, :repo, :payload, :logger
10
+
11
+ def initialize(options = {})
12
+ @project = options.fetch(:project)
13
+ @repo = options.fetch(:repo)
14
+ @payload = options.fetch(:payload)
15
+ @logger = options.fetch(:logger) { Logger.new(STDOUT) }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end