txgh 1.0.0

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