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,9 @@
1
+ module Txgh
2
+ module Handlers
3
+ module Triggers
4
+ autoload :Handler, 'txgh/handlers/triggers/handler'
5
+ autoload :PullHandler, 'txgh/handlers/triggers/pull_handler'
6
+ autoload :PushHandler, 'txgh/handlers/triggers/push_handler'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,66 @@
1
+ module Txgh
2
+ module Handlers
3
+ module Triggers
4
+ class Handler
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)
11
+ handle_safely do
12
+ config = Txgh::Config::KeyManager.config_from_project(
13
+ request.params.fetch('project_slug')
14
+ )
15
+
16
+ handler_for(config, request, logger).execute
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def handle_safely
23
+ yield
24
+ rescue => e
25
+ respond_with_error(500, "Internal server error: #{e.message}", e)
26
+ end
27
+
28
+ def handler_for(config, request, logger)
29
+ new(
30
+ project: config.transifex_project,
31
+ repo: config.github_repo,
32
+ branch: request.params.fetch('branch'),
33
+ resource_slug: request.params.fetch('resource_slug'),
34
+ logger: logger
35
+ )
36
+ end
37
+ end
38
+
39
+ attr_reader :project, :repo, :branch, :resource_slug, :logger
40
+
41
+ def initialize(options = {})
42
+ @project = options[:project]
43
+ @repo = options[:repo]
44
+ @branch = Utils.absolute_branch(options[:branch])
45
+ @resource_slug = options[:resource_slug]
46
+ @logger = options[:logger]
47
+ end
48
+
49
+ private
50
+
51
+ def branch_resource
52
+ @branch_resource ||= TxBranchResource.new(resource, branch)
53
+ end
54
+
55
+ def resource
56
+ @resource ||= tx_config.resource(resource_slug)
57
+ end
58
+
59
+ def tx_config
60
+ @tx_config ||= Txgh::Config::TxManager.tx_config(project, repo, branch)
61
+ end
62
+
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,29 @@
1
+ module Txgh
2
+ module Handlers
3
+ module Triggers
4
+ class PullHandler < Handler
5
+
6
+ def execute
7
+ languages.each do |language|
8
+ committer.commit_resource(
9
+ branch_resource, branch, language['language_code']
10
+ )
11
+ end
12
+
13
+ respond_with(200, true)
14
+ end
15
+
16
+ private
17
+
18
+ def committer
19
+ @committer ||= Txgh::ResourceCommitter.new(project, repo, logger)
20
+ end
21
+
22
+ def languages
23
+ @languages ||= project.api.get_languages(project.name)
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ module Txgh
2
+ module Handlers
3
+ module Triggers
4
+ class PushHandler < Handler
5
+
6
+ def execute
7
+ ref = repo.api.get_ref(repo.name, branch)
8
+ updater.update_resource(branch_resource, ref[:object][:sha])
9
+ respond_with(200, true)
10
+ end
11
+
12
+ private
13
+
14
+ def updater
15
+ @updater ||= Txgh::ResourceUpdater.new(project, repo, logger)
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ require 'ext/zipline/output_stream'
2
+
3
+ module Txgh
4
+ module Handlers
5
+ class ZipStreamResponse < StreamResponse
6
+
7
+ def write_to(stream)
8
+ Zipline::OutputStream.open(stream) do |zipfile|
9
+ enum.each do |file_name, contents|
10
+ zipfile.put_next_entry(file_name, contents.bytesize)
11
+ zipfile << contents
12
+ end
13
+ end
14
+ end
15
+
16
+ def file_extension
17
+ '.zip'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,74 @@
1
+ module Txgh
2
+ class MergeCalculator
3
+ class << self
4
+ def merge(head_contents, diff_point_contents, diff_hash)
5
+ new(head_contents, diff_point_contents, diff_hash).merge
6
+ end
7
+ end
8
+
9
+ attr_reader :head_contents, :diff_point_contents, :diff_hash
10
+
11
+ # Merges are based on diffs. Whatever was added/removed/modified between
12
+ # two resources is represented by the diff, while the resources themselves
13
+ # are what gets merged. This class uses the given diff to apply one
14
+ # resource's phrases over the top of another.
15
+ #
16
+ # head_contents: translated contents in HEAD
17
+ # diff_point_contents: translated contents in diff point, eg. master
18
+ # diff_hash: what was added/removed/modified in the source
19
+ def initialize(head_contents, diff_point_contents, diff_hash)
20
+ @head_contents = head_contents
21
+ @diff_point_contents = diff_point_contents
22
+ @diff_hash = diff_hash
23
+ end
24
+
25
+ def merge
26
+ phrase_hash = diff_point_hash.dup
27
+ update_added(phrase_hash)
28
+ update_modified(phrase_hash)
29
+ update_removed(phrase_hash)
30
+
31
+ ResourceContents.from_phrase_list(
32
+ head_contents.tx_resource, phrase_hash.values
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ def update_added(phrase_hash)
39
+ diff.fetch(:added, {}).each_pair do |key, phrase|
40
+ if val = head_hash[key]
41
+ phrase_hash[key] = val
42
+ end
43
+ end
44
+ end
45
+
46
+ def update_modified(phrase_hash)
47
+ diff.fetch(:modified, {}).each_pair do |key, phrase|
48
+ if val = head_hash[key]
49
+ phrase_hash[key] = val
50
+ end
51
+ end
52
+ end
53
+
54
+ def update_removed(phrase_hash)
55
+ diff.fetch(:removed, {}).each_pair do |key, _|
56
+ phrase_hash.delete(key)
57
+ end
58
+ end
59
+
60
+ def diff
61
+ @diff ||= diff_hash.each_with_object({}) do |(status, phrases), ret|
62
+ ret[status] = Utils.index_on('key', phrases)
63
+ end
64
+ end
65
+
66
+ def head_hash
67
+ head_contents.to_h
68
+ end
69
+
70
+ def diff_point_hash
71
+ diff_point_contents.to_h
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,24 @@
1
+ require 'parseconfig'
2
+ require 'tempfile'
3
+
4
+ module Txgh
5
+ # This class wraps the ParseConfig class from the parseconfig gem and
6
+ # provides a way to load config from a string instead of just a file.
7
+ class ParseConfig < ::ParseConfig
8
+ class << self
9
+ def load(contents)
10
+ tmp = Tempfile.new('parseconfig')
11
+ tmp.write(contents)
12
+ tmp.close
13
+ load_file(tmp.path)
14
+ ensure
15
+ tmp.unlink if tmp
16
+ end
17
+
18
+ def load_file(path)
19
+ # use the default file loading logic
20
+ new(path)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,39 @@
1
+ module Txgh
2
+ class ResourceCommitter
3
+ attr_reader :project, :repo, :logger
4
+
5
+ def initialize(project, repo, logger = nil)
6
+ @project = project
7
+ @repo = repo
8
+ @logger = logger || Logger.new(STDOUT)
9
+ end
10
+
11
+ def commit_resource(tx_resource, branch, language)
12
+ return if prevent_commit_on?(branch)
13
+
14
+ unless language == tx_resource.source_lang
15
+ file_name, translations = download(tx_resource, branch, language)
16
+
17
+ if translations
18
+ repo.api.commit(repo.name, branch, { file_name => translations })
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def download(tx_resource, branch, language)
26
+ downloader = ResourceDownloader.new(
27
+ project, repo, branch, {
28
+ languages: [language], resources: [tx_resource]
29
+ }
30
+ )
31
+
32
+ downloader.first
33
+ end
34
+
35
+ def prevent_commit_on?(branch)
36
+ project.protected_branches.include?(branch)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,118 @@
1
+ require 'abroad'
2
+ require 'stringio'
3
+
4
+ module Txgh
5
+ class ResourceContents
6
+ EXTRACTOR_MAP = {
7
+ 'YML' => 'yaml/rails',
8
+ 'YAML' => 'yaml/rails',
9
+ 'KEYVALUEJSON' => 'json/key-value',
10
+ 'ANDROID' => 'xml/android'
11
+ }
12
+
13
+ SERIALIZER_MAP = {
14
+ 'YML' => 'yaml/rails',
15
+ 'YAML' => 'yaml/rails',
16
+ 'KEYVALUEJSON' => 'json/key-value',
17
+ 'ANDROID' => 'xml/android'
18
+ }
19
+
20
+ class << self
21
+ def from_phrase_list(tx_resource, phrases)
22
+ new(tx_resource, phrases: phrases)
23
+ end
24
+
25
+ def from_string(tx_resource, string)
26
+ new(tx_resource, raw: string)
27
+ end
28
+ end
29
+
30
+ attr_reader :tx_resource
31
+
32
+ def initialize(tx_resource, options)
33
+ @tx_resource = tx_resource
34
+ @phrases = options[:phrases]
35
+ @raw = options[:raw]
36
+ end
37
+
38
+ def phrases
39
+ @phrases ||= extractor.from_string(raw) do |extractor|
40
+ extractor.extract_each.map do |key, value|
41
+ { 'key' => key, 'string' => value }
42
+ end
43
+ end
44
+ end
45
+
46
+ def add(key, value)
47
+ phrases << { 'key' => key, 'string' => value }
48
+ end
49
+
50
+ # Some formats like Rails YAML require the language to be written somewhere
51
+ # in the file. If you're using this class to parse and serialize the
52
+ # contents of a translated version of a resource, then you'll probably
53
+ # want to override the resource's source language using the second
54
+ # parameter here.
55
+ def write_to(stream, language = tx_resource.source_lang)
56
+ serializer.from_stream(stream, language) do |serializer|
57
+ phrases.each do |phrase|
58
+ serializer.write_key_value(
59
+ phrase['key'], (phrase['string'] || '').to_s
60
+ )
61
+ end
62
+ end
63
+ end
64
+
65
+ # see comment above write_to
66
+ def to_s(language = tx_resource.source_lang)
67
+ stream = StringIO.new
68
+ write_to(stream, language)
69
+ stream.string
70
+ end
71
+
72
+ def to_h
73
+ Utils.index_on('key', phrases)
74
+ end
75
+
76
+ def diff(other_contents)
77
+ diff = diff_hash(other_contents)
78
+ diff_phrases = diff[:added] + diff[:modified]
79
+ self.class.from_phrase_list(tx_resource, diff_phrases)
80
+ end
81
+
82
+ def diff_hash(other_contents)
83
+ DiffCalculator.compare(phrases, other_contents.phrases)
84
+ end
85
+
86
+ def merge(other_contents, diff_hash)
87
+ MergeCalculator.merge(other_contents, self, diff_hash)
88
+ end
89
+
90
+ def empty?
91
+ phrases.empty?
92
+ end
93
+
94
+ private
95
+
96
+ attr_reader :raw
97
+
98
+ def extractor
99
+ id = EXTRACTOR_MAP.fetch(tx_resource.type) do
100
+ raise TxghInternalError,
101
+ "'#{tx_resource.type}' is not a file type that is supported when "\
102
+ "uploading diffs."
103
+ end
104
+
105
+ Abroad.extractor(id)
106
+ end
107
+
108
+ def serializer
109
+ id = SERIALIZER_MAP.fetch(tx_resource.type) do
110
+ raise TxghInternalError,
111
+ "'#{tx_resource.type}' is not a file type that is supported when "\
112
+ "uploading diffs."
113
+ end
114
+
115
+ Abroad.serializer(id)
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,141 @@
1
+ module Txgh
2
+ class ResourceDownloader
3
+ include Enumerable
4
+
5
+ attr_reader :project, :repo, :branch
6
+
7
+ def initialize(project, repo, branch, options = {})
8
+ @project = project
9
+ @repo = repo
10
+ @branch = branch
11
+
12
+ # Provides an override list of languages. If not present, the downloader
13
+ # will make an API call to fetch the list of languages for the project.
14
+ @languages = options[:languages]
15
+ @resources = options[:resources]
16
+ end
17
+
18
+ def each(&block)
19
+ enum.each(&block)
20
+ end
21
+
22
+ private
23
+
24
+ def enum
25
+ if repo.upload_diffs?
26
+ download_merging_diff
27
+ else
28
+ download_without_diff
29
+ end
30
+ end
31
+
32
+ def download_merging_diff
33
+ return to_enum(__method__) unless block_given?
34
+
35
+ download_each do |head_resource, language_code, file_name|
36
+ diff_point_resource = tx_config.resource(
37
+ head_resource.original_resource_slug, repo.diff_point
38
+ )
39
+
40
+ source_diff = source_diff_hash(head_resource, diff_point_resource)
41
+ head_content = wrap(transifex_download(head_resource, language_code), head_resource)
42
+ diff_point_content = wrap(transifex_download(diff_point_resource, language_code), diff_point_resource)
43
+ contents = diff_point_content.merge(head_content, source_diff)
44
+
45
+ yield file_name, contents.to_s(language_code)
46
+ end
47
+ end
48
+
49
+ def source_diff_hash(head_resource, diff_point_resource)
50
+ cache_diff(head_resource, diff_point_resource) do
51
+ br = repo.process_all_branches? ? branch : repo.branch
52
+ head_contents = wrap(git_download(head_resource, br), head_resource)
53
+ diff_point_contents = wrap(git_download(diff_point_resource, repo.diff_point), head_resource)
54
+ head_contents.diff_hash(diff_point_contents)
55
+ end
56
+ end
57
+
58
+ def cache_diff(head_resource, diff_point_resource)
59
+ key = "#{head_resource.resource_slug}|#{diff_point_resource.resource_slug}"
60
+ if diff = diff_cache[key]
61
+ diff
62
+ else
63
+ diff_cache[key] = yield
64
+ end
65
+ end
66
+
67
+ def diff_cache
68
+ @diff_cache ||= {}
69
+ end
70
+
71
+ def download_without_diff
72
+ return to_enum(__method__) unless block_given?
73
+
74
+ download_each do |resource, language_code, file_name|
75
+ contents = transifex_download(resource, language_code)
76
+ yield file_name, contents
77
+ end
78
+ end
79
+
80
+ def download_each
81
+ each_resource do |resource|
82
+ each_language do |language_code|
83
+ file_name = resource.translation_path(resource.lang_map(language_code))
84
+ yield resource, language_code, file_name
85
+ end
86
+ end
87
+ end
88
+
89
+ def wrap(string, resource)
90
+ if string
91
+ ResourceContents.from_string(resource, string)
92
+ else
93
+ EmptyResourceContents.new(resource)
94
+ end
95
+ end
96
+
97
+ def transifex_download(resource, language)
98
+ transifex_api.download(resource, language)
99
+ rescue TransifexNotFoundError
100
+ nil
101
+ end
102
+
103
+ def git_download(resource, branch)
104
+ repo.api.download(repo.name, resource.source_file, branch)
105
+ end
106
+
107
+ def transifex_api
108
+ project.api
109
+ end
110
+
111
+ def each_resource(&block)
112
+ return to_enum(__method__) unless block_given?
113
+ return @resources.each(&block) if @resources
114
+
115
+ ref = repo.process_all_branches? ? branch : nil
116
+
117
+ tx_config.resources.each do |res|
118
+ yield tx_config.resource(res.resource_slug, ref)
119
+ end
120
+ end
121
+
122
+ def each_language(&block)
123
+ return to_enum(__method__) unless block_given?
124
+ return @languages.each(&block) if @languages
125
+
126
+ raw_languages.each(&block)
127
+ end
128
+
129
+ def raw_languages
130
+ @raw_languages ||= transifex_api.get_languages(project.name).map do |lang|
131
+ lang['language_code']
132
+ end
133
+ end
134
+
135
+ def tx_config
136
+ @tx_config ||= Txgh::Config::TxManager.tx_config(
137
+ project, repo, repo.process_all_branches? ? branch : repo.branch
138
+ )
139
+ end
140
+ end
141
+ end