phraseapp_updater 0.1.7 → 2.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.
- checksums.yaml +5 -5
- data/Gemfile +1 -0
- data/README.markdown +79 -72
- data/bin/phraseapp_common.sh +82 -0
- data/bin/phraseapp_updater +220 -93
- data/bin/synchronize_phraseapp.sh +141 -0
- data/lib/phraseapp_updater.rb +105 -116
- data/lib/phraseapp_updater/locale_file.rb +69 -28
- data/lib/phraseapp_updater/locale_file/json_file.rb +20 -13
- data/lib/phraseapp_updater/locale_file/yaml_file.rb +16 -9
- data/lib/phraseapp_updater/phraseapp_api.rb +116 -60
- data/lib/phraseapp_updater/version.rb +3 -1
- data/phraseapp_updater.gemspec +1 -0
- metadata +19 -4
- data/lib/phraseapp_updater/locale_file/loader.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a9a01be297b026f1bcf98b8337306f7dd5340f15bd21dc7264ccf78aff82d242
|
4
|
+
data.tar.gz: 6c8cd2024db62795d678b148621dd3556e5a7771daa33076e004a00d68bde88e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aab7ed4136379375aa89531722ce00af0d7027f0433ad0d77f2793a855966f59f286ad3f154f71c4bfe48a604275f72805e615f3be65e4abf58390f025f7f058
|
7
|
+
data.tar.gz: 83aae79c39f421c21deed218e8c0bfa0dcfbbe663ec49520cda9dbd675268728877ce665aae5150d45438ccb5d241cfb23995d8c3168eb237c28689e5b37639c
|
data/Gemfile
CHANGED
data/README.markdown
CHANGED
@@ -2,34 +2,28 @@
|
|
2
2
|
|
3
3
|
[](https://travis-ci.org/iknow/phraseapp_updater)
|
4
4
|
|
5
|
-
**Version** 0.
|
6
|
-
|
7
|
-
This is a tool for
|
8
|
-
committed in your project.
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
What we want instead is a three way merge where the committed data wins
|
29
|
-
on conflict. Non-conflicting changes on PhraseApp are preserved, while
|
30
|
-
changes on both sides take the committed data. The result of the merge
|
31
|
-
is then sent to PhraseApp, keeping it up-to-date with the newest commit
|
32
|
-
of `master`.
|
5
|
+
**Version** 2.0.0
|
6
|
+
|
7
|
+
This is a tool for managing synchronization between locale data in
|
8
|
+
[PhraseApp](https://phraseapp.com) and committed in your project. It can perform
|
9
|
+
JSON-aware three-way merges with respect to a common ancestor, and maintains a
|
10
|
+
record of the common ancestor on PhraseApp using tags.
|
11
|
+
|
12
|
+
Our workflow considers localization data stored on PhraseApp to be a working
|
13
|
+
copy for a given branch. We expect developers working on the code and
|
14
|
+
translators working on PhraseApp to both be able to make changes and have them
|
15
|
+
integrated.
|
16
|
+
|
17
|
+
PhraseApp provides [APIs](https://phraseapp.com/docs/api/v2/) and a [Ruby
|
18
|
+
gem](https://github.com/phrase/phraseapp-ruby) for accessing them, but the API
|
19
|
+
only allows either a) completely overwriting PhraseApp's data with local data or
|
20
|
+
b) reapplying PhraseApp's data on top of the local data. Neither of these cases
|
21
|
+
is appropriate for integrating changes made on both sides.
|
22
|
+
|
23
|
+
What we want instead is a three way merge where the committed data wins on
|
24
|
+
conflict. Non-conflicting changes on PhraseApp are preserved, while changes to
|
25
|
+
the same key on both sides take the committed data. The result of the merge is
|
26
|
+
then applied to both sides, keeping them up to date with each other.
|
33
27
|
|
34
28
|
This is especially important when removing keys. Imagine we have the
|
35
29
|
following, no-longer useful key:
|
@@ -48,9 +42,8 @@ unused:
|
|
48
42
|
zero: No unused's
|
49
43
|
```
|
50
44
|
|
51
|
-
And in our feature branch, we remove it. The result we want is that the
|
52
|
-
|
53
|
-
the above.
|
45
|
+
And in our feature branch, we remove it. The result we want is that the key
|
46
|
+
completely disappears, instead of getting a result like either of the above.
|
54
47
|
|
55
48
|
## Installation
|
56
49
|
|
@@ -80,68 +73,79 @@ Or install it yourself as:
|
|
80
73
|
CLI
|
81
74
|
---
|
82
75
|
|
83
|
-
**
|
76
|
+
**Setup**
|
84
77
|
|
85
|
-
`phraseapp_updater
|
86
|
-
|
87
|
-
|
88
|
-
committed to your application's respository. These will be used in the
|
89
|
-
merge with the files on PhraseApp.
|
78
|
+
`phraseapp_updater setup` creates and initializes a PhraseApp project
|
79
|
+
corresponding to your branch. It must be provided with the current git revision
|
80
|
+
of the branch and the path to the locale files.
|
90
81
|
|
91
82
|
```
|
92
|
-
phraseapp_updater
|
83
|
+
phraseapp_updater setup --phraseapp_project_name="yourbranch" --parent_commit="yourhash" --phraseapp_api_key=yourkey" path_to_locales
|
93
84
|
```
|
94
85
|
|
95
|
-
|
96
|
-
|
86
|
+
**Synchronize**
|
87
|
+
|
88
|
+
`phraseapp_updater synchronize` synchronizes a git remote branch with its
|
89
|
+
corresponding PhraseApp project, incorporating changes from each side into the
|
90
|
+
other. If both sides were changed, a three-way merge is performed. The result is
|
91
|
+
uploaded to PhraseApp and committed and pushed to the git remote as appropriate.
|
92
|
+
|
93
|
+
The option `--no_commit` may be provided to restrict changes to the PhraseApp
|
94
|
+
side. If specified, then in the case that the branch was modified, the merge
|
95
|
+
result will be uploaded to PhraseApp and the common ancestor updated to the
|
96
|
+
branch head.
|
97
97
|
|
98
98
|
```
|
99
|
-
|
100
|
-
PA_PREVIOUS_LOCALES_PATH
|
101
|
-
PA_API_KEY
|
102
|
-
PA_PROJECT_ID
|
103
|
-
PA_FILE_FORMAT
|
99
|
+
phraseapp_updater synchronize <checkout_path>
|
104
100
|
```
|
105
101
|
|
106
|
-
|
107
|
-
|
102
|
+
**Download**
|
103
|
+
|
104
|
+
`phraseapp_updater download` downloads and normalizes locale files from
|
105
|
+
PhraseApp, saving them to the specified location. The revision of the recorded
|
106
|
+
common ancestor is printed to standard out.
|
108
107
|
|
109
|
-
|
108
|
+
```
|
109
|
+
phraseapp_updater download --phraseapp_project_id="yourid" --phraseapp_api_key="yourkey" target_path
|
110
|
+
```
|
110
111
|
|
111
|
-
|
112
|
-
However, when keys are missing from the PhraseApp data, it restores them
|
113
|
-
(if present) from the files at fallback path provided. This allows you
|
114
|
-
to mark keys as "unverified" on PhraseApp, meaning you don't pull in
|
115
|
-
draft translations, while allowing you to keep the current version of
|
116
|
-
that translation.
|
112
|
+
**Upload**
|
117
113
|
|
118
|
-
|
119
|
-
|
114
|
+
`phraseapp_updater upload` uploads normalized locale files from your branch to
|
115
|
+
PhraseApp and resets the recorded common ancestor to the specified revision.
|
120
116
|
|
121
117
|
```
|
122
|
-
phraseapp_updater
|
118
|
+
phraseapp_updater upload --phraseapp_project_id="yourid" --phraseapp_api_key="yourkey" path_to_locales
|
123
119
|
```
|
124
120
|
|
125
|
-
|
126
|
-
|
121
|
+
**Update Parent Commit**
|
122
|
+
`phraseapp_updater update_parent_commit` records a new common ancestor on
|
123
|
+
PhraseApp without changing the locales.
|
127
124
|
|
128
125
|
```
|
129
|
-
|
130
|
-
PA_PROJECT_ID
|
131
|
-
PA_FILE_FORMAT
|
126
|
+
phraseapp_updater update_parent_commit --phraseapp_project_id="yourid" --phraseapp_api_key="yourkey" --parent_commit="yourhash"
|
132
127
|
```
|
133
128
|
|
134
|
-
|
135
|
-
`.phraseapp.yml` file, specified with `--config-file-path`
|
129
|
+
**Merge**
|
136
130
|
|
137
|
-
|
138
|
-
|
131
|
+
`phraseapp_updater merge` performs a content-aware three-way merge between
|
132
|
+
locale files in three directories: `ancestor_path`, `our_path`, and
|
133
|
+
`their_path`. In the case of conflicts, the changes from `our_path` are
|
134
|
+
accepted. The results are normalized and written to the path specified with
|
135
|
+
`to`.
|
139
136
|
|
140
|
-
|
137
|
+
```
|
138
|
+
phraseapp_updater merge ancestor_path our_path their_path --to target_path
|
139
|
+
```
|
141
140
|
|
142
|
-
|
143
|
-
|
144
|
-
|
141
|
+
|
142
|
+
**Diff**
|
143
|
+
|
144
|
+
Performs a content-aware diff between locale files in two directories. Returns
|
145
|
+
with exit status 1 or 0 to signal differences or no differences respectively
|
146
|
+
|
147
|
+
```
|
148
|
+
phraseapp_updater diff path1 path2
|
145
149
|
```
|
146
150
|
|
147
151
|
|
@@ -157,8 +161,11 @@ https://gist.github.com/kevingriffin/d59821446ce424a56c7da2686d4ae082
|
|
157
161
|
|
158
162
|
If you'd like to contribute, these would be very helpful!
|
159
163
|
|
160
|
-
*
|
161
|
-
|
164
|
+
* We'd like to use "unverified" translations on PhraseApp as the equivalent of
|
165
|
+
an unstaged working copy. For this to work, we need to be able to recover
|
166
|
+
previous translations at the same key. While PhraseApp doesn't itself keep
|
167
|
+
this history, we could do this by restoring the absent keys from the diff
|
168
|
+
between verified and unverified download from the common ancestor.
|
162
169
|
* Expose the changed files on the command line.
|
163
170
|
* Checking if PhraseApp files changed during execution before upload, to reduce the race condition window.
|
164
171
|
* More specs for the API and shell.
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# Set up a working directory
|
4
|
+
working_directory=$(mktemp -d -t phraseapp)
|
5
|
+
|
6
|
+
function cleanup_working_directory(){
|
7
|
+
rm -rf "${working_directory}"
|
8
|
+
}
|
9
|
+
|
10
|
+
trap "cleanup_working_directory" EXIT SIGINT
|
11
|
+
|
12
|
+
function make_temporary_directory() {
|
13
|
+
mktemp -d "${working_directory}/XXXXXXXX"
|
14
|
+
}
|
15
|
+
|
16
|
+
function make_tree_from_directory() {
|
17
|
+
local directory filename object
|
18
|
+
directory="$1"
|
19
|
+
|
20
|
+
if [ ! -d "$directory" ]; then
|
21
|
+
echo "Error: directory not found: '${directory}'" >&2
|
22
|
+
exit 1
|
23
|
+
fi
|
24
|
+
|
25
|
+
for file in "$directory"/*; do
|
26
|
+
if [ -d "$file" ]; then
|
27
|
+
echo "Error: make_tree_from_directory cannot create recursive tree: '${file}'" >&2
|
28
|
+
exit 1
|
29
|
+
fi
|
30
|
+
|
31
|
+
filename=$(basename "${file}")
|
32
|
+
object=$(git hash-object -w "${file}")
|
33
|
+
printf "100644 blob %s\\t%s\\n" "${object}" "${filename}"
|
34
|
+
done | git mktree
|
35
|
+
}
|
36
|
+
|
37
|
+
|
38
|
+
function extract_commit() {
|
39
|
+
extract_files "$1:${PREFIX}"
|
40
|
+
}
|
41
|
+
|
42
|
+
function extract_files() {
|
43
|
+
local path
|
44
|
+
path=$(make_temporary_directory)
|
45
|
+
|
46
|
+
git archive --format=tar "$1" | tar -x -C "${path}"
|
47
|
+
|
48
|
+
echo "${path}"
|
49
|
+
}
|
50
|
+
|
51
|
+
function locales_changed() {
|
52
|
+
! phraseapp_updater diff --quiet "$1" "$2"
|
53
|
+
}
|
54
|
+
|
55
|
+
function tree_changed() {
|
56
|
+
! git diff-tree --quiet "$1" "$2"
|
57
|
+
}
|
58
|
+
|
59
|
+
function replace_nested_tree() {
|
60
|
+
local root path tree
|
61
|
+
root="$1"
|
62
|
+
path="$2"
|
63
|
+
tree="$3"
|
64
|
+
|
65
|
+
while [ "$path" ]; do
|
66
|
+
leaf_name=$(basename "$path")
|
67
|
+
path=$(dirname "$path")
|
68
|
+
[ "$path" = "." ] && path=''
|
69
|
+
|
70
|
+
# replace `leaf_name` in `path` with `tree`, yielding new tree
|
71
|
+
tree=$(git ls-tree "${root}:${path}" | \
|
72
|
+
replace_child_in_tree "${leaf_name}" "${tree}" | \
|
73
|
+
git mktree)
|
74
|
+
done
|
75
|
+
|
76
|
+
echo "$tree"
|
77
|
+
}
|
78
|
+
|
79
|
+
function replace_child_in_tree(){
|
80
|
+
ruby -pe 'BEGIN { file, tree = ARGV.shift(2) };
|
81
|
+
gsub(/ [0-9a-z]{40}\t/, " #{tree}\t") if /\t#{file}$/' "$@"
|
82
|
+
}
|
data/bin/phraseapp_updater
CHANGED
@@ -1,89 +1,202 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
2
4
|
require 'thor'
|
3
5
|
require 'phraseapp_updater'
|
4
6
|
|
5
7
|
class PhraseAppUpdaterCLI < Thor
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
8
|
+
class_option :default_locale, type: :string, default: 'en', desc: 'PhraseApp default locale'
|
9
|
+
class_option :file_format, type: :string, default: 'json', desc: 'Filetype of localization files.'
|
10
|
+
|
11
|
+
desc 'setup <locale_path>',
|
12
|
+
'Create a new PhraseApp project, initializing it with locale files at <locale_path>. the new project ID is printed to STDOUT'
|
13
|
+
method_option :phraseapp_api_key, type: :string, required: true, desc: 'PhraseApp API key.'
|
14
|
+
method_option :phraseapp_project_name, type: :string, required: true, desc: 'Name for new PhraseApp project.'
|
15
|
+
method_option :parent_commit, type: :string, required: true, desc: 'git commit hash of initial locales'
|
16
|
+
|
17
|
+
def setup(locales_path)
|
18
|
+
validate_readable_path!('locales', locales_path)
|
15
19
|
|
16
|
-
|
17
|
-
|
20
|
+
handle_errors do
|
21
|
+
updater, project_id = PhraseAppUpdater.for_new_project(
|
22
|
+
options[:phraseapp_api_key],
|
23
|
+
options[:phraseapp_project_name],
|
24
|
+
options[:file_format],
|
25
|
+
options[:parent_commit])
|
26
|
+
|
27
|
+
updater.upload_directory(locales_path)
|
28
|
+
|
29
|
+
puts project_id
|
30
|
+
end
|
31
|
+
end
|
18
32
|
|
19
|
-
|
20
|
-
|
33
|
+
desc 'synchronize <git_checkout_path>',
|
34
|
+
'Synchronize locales in PhraseApp with '
|
35
|
+
method_option :phraseapp_api_key, type: :string, required: true, desc: 'PhraseApp API key.'
|
36
|
+
method_option :phraseapp_project_id, type: :string, required: true, desc: 'PhraseApp project ID.'
|
37
|
+
method_option :branch, type: :string, required: false, desc: 'Name of (remote) git branch to synchronize'
|
38
|
+
method_option :remote, type: :string, required: false, desc: 'Name of git remote to synchronize with'
|
39
|
+
method_option :prefix, type: :string, default: 'config/locales', desc: 'Path prefix in git branch for locale files'
|
40
|
+
method_option :no_commit, type: :boolean, default: false, desc: 'Do not commit merge results to the branch'
|
41
|
+
|
42
|
+
def synchronize(checkout_path)
|
43
|
+
validate_readable_path!('checkout path', checkout_path)
|
44
|
+
Dir.chdir(checkout_path)
|
45
|
+
|
46
|
+
ENV['PHRASEAPP_API_KEY'] = options[:phraseapp_api_key]
|
47
|
+
ENV['PHRASEAPP_PROJECT_ID'] = options[:phraseapp_project_id]
|
48
|
+
ENV['FILE_FORMAT'] = options[:file_format]
|
49
|
+
ENV['NO_COMMIT'] = options[:no_commit] ? 't' : 'f'
|
50
|
+
ENV['PREFIX'] = options[:prefix]
|
51
|
+
ENV['BRANCH'] = options.fetch(:branch) { sh('git name-rev --name-only HEAD').chomp }
|
52
|
+
ENV['REMOTE'] = options.fetch(:remote) { sh("git config branch.#{ENV['BRANCH']}.remote").chomp }
|
53
|
+
|
54
|
+
shell_script_path = File.join(__dir__, 'synchronize_phraseapp.sh')
|
55
|
+
exec(shell_script_path)
|
56
|
+
end
|
21
57
|
|
22
|
-
|
23
|
-
|
58
|
+
desc 'download <target_path>',
|
59
|
+
'Download and renormalize locale files from PhraseApp to <target_path>'
|
60
|
+
method_option :phraseapp_api_key, type: :string, required: true, desc: 'PhraseApp API key.'
|
61
|
+
method_option :phraseapp_project_id, type: :string, required: true, desc: 'PhraseApp project ID.'
|
24
62
|
|
25
|
-
|
26
|
-
validate_writable_path!('
|
63
|
+
def download(target_path)
|
64
|
+
validate_writable_path!('target path', target_path)
|
27
65
|
|
28
|
-
|
29
|
-
|
66
|
+
handle_errors do
|
67
|
+
updater = PhraseAppUpdater.new(
|
68
|
+
options[:phraseapp_api_key],
|
69
|
+
options[:phraseapp_project_id],
|
70
|
+
options[:file_format])
|
30
71
|
|
31
|
-
|
32
|
-
|
72
|
+
updater.download_to_directory(target_path)
|
73
|
+
parent_commit = updater.read_parent_commit
|
33
74
|
|
34
|
-
|
35
|
-
|
75
|
+
if parent_commit.nil?
|
76
|
+
STDERR.puts 'Error: Locales downloaded from phraseapp, but parent commit details missing'
|
77
|
+
exit(1)
|
36
78
|
end
|
37
79
|
|
38
|
-
|
39
|
-
|
80
|
+
puts parent_commit
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
desc 'upload <locale_path>',
|
85
|
+
'Renormalize and upload locale files at <locale_path> to PhraseApp, replacing current contents.'
|
86
|
+
method_option :phraseapp_api_key, type: :string, required: true, desc: 'PhraseApp API key.'
|
87
|
+
method_option :phraseapp_project_id, type: :string, required: true, desc: 'PhraseApp project ID.'
|
88
|
+
method_option :parent_commit, type: :string, required: true, desc: 'git commit hash of locales being uploaded'
|
89
|
+
|
90
|
+
def upload(source_path)
|
91
|
+
validate_readable_path!('source path', source_path)
|
92
|
+
|
93
|
+
handle_errors do
|
94
|
+
updater = PhraseAppUpdater.new(
|
95
|
+
options[:phraseapp_api_key],
|
96
|
+
options[:phraseapp_project_id],
|
97
|
+
options[:file_format])
|
98
|
+
|
99
|
+
updater.upload_directory(source_path)
|
100
|
+
updater.update_parent_commit(options[:parent_commit])
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
desc 'update_parent_commit', 'Record a new merge-base on PhraseApp without changing contents.'
|
105
|
+
method_option :phraseapp_api_key, type: :string, required: true, desc: 'PhraseApp API key.'
|
106
|
+
method_option :phraseapp_project_id, type: :string, required: true, desc: 'PhraseApp project ID.'
|
107
|
+
method_option :parent_commit, type: :string, required: true, desc: 'git commit hash of locales being uploaded'
|
108
|
+
def update_parent_commit
|
109
|
+
handle_errors do
|
110
|
+
updater = PhraseAppUpdater.new(
|
111
|
+
options[:phraseapp_api_key],
|
112
|
+
options[:phraseapp_project_id],
|
113
|
+
options[:file_format])
|
114
|
+
|
115
|
+
updater.update_parent_commit(options[:parent_commit])
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
desc 'diff <path1> <path2>',
|
120
|
+
'Compare locale file directories <path1> and <path2>'
|
121
|
+
|
122
|
+
long_desc <<-LONGDESC
|
123
|
+
Perform a JSON diff of locale files in path1 and path2.
|
124
|
+
Exits with 1 if there were differences, or 0 if no differences"
|
125
|
+
LONGDESC
|
126
|
+
|
127
|
+
method_option :quiet, type: :boolean, default: false, desc: 'Suppress output'
|
128
|
+
|
129
|
+
def diff(path1, path2)
|
130
|
+
validate_readable_path!('path1', path1)
|
131
|
+
validate_readable_path!('path2', path2)
|
132
|
+
|
133
|
+
handle_errors do
|
134
|
+
updater = PhraseAppUpdater.new(nil, nil, options[:file_format])
|
135
|
+
diffs = updater.diff_directories(path1, path2)
|
136
|
+
if diffs.empty?
|
137
|
+
exit(0)
|
138
|
+
else
|
139
|
+
print_diff(diffs) unless options[:quiet]
|
140
|
+
exit(1)
|
40
141
|
end
|
41
|
-
rescue PhraseAppUpdater::PhraseAppAPI::BadAPIKeyError => e
|
42
|
-
puts "Bad PhraseApp API key."
|
43
|
-
rescue PhraseAppUpdater::PhraseAppAPI::BadProjectIDError => e
|
44
|
-
puts "Bad PhraseApp project ID: #{phraseapp_project_id}"
|
45
|
-
rescue PhraseAppUpdater::LocaleFile::BadFileTypeError => e
|
46
|
-
puts "Bad filetype for localization files: #{e.message}"
|
47
|
-
rescue StandardError => e
|
48
|
-
puts "Unknown error when pushing files."
|
49
|
-
raise e
|
50
142
|
end
|
51
143
|
end
|
52
144
|
|
145
|
+
desc 'merge <ancestor_path> <our_path> <their_path>',
|
146
|
+
'3-way merge locale file directories <ancestor_path>, <our_path>, <their_path> into TO.'
|
53
147
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
def
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
write_locale_files(destination_path, files)
|
74
|
-
rescue PhraseAppUpdater::PhraseAppAPI::BadAPIKeyError => e
|
75
|
-
puts "Bad PhraseApp API key."
|
76
|
-
rescue PhraseAppUpdater::PhraseAppAPI::BadProjectIDError => e
|
77
|
-
puts "Bad PhraseApp project ID: #{phraseapp_project_id}"
|
78
|
-
rescue PhraseAppUpdater::LocaleFile::BadFileTypeError => e
|
79
|
-
puts "Bad filetype for localization files: #{e.message}"
|
80
|
-
rescue StandardError => e
|
81
|
-
puts "Unknown error when pulling files"
|
82
|
-
raise e
|
148
|
+
long_desc <<-LONGDESC
|
149
|
+
Perform a JSON-aware 3-way merge of locale files in directories <ancestor_path>, <our_path>, <their_path> into TO.
|
150
|
+
|
151
|
+
The merge resolution strategy always selects `ours` in the case of a conflict.
|
152
|
+
LONGDESC
|
153
|
+
|
154
|
+
method_option :to, type: :string, required: true, desc: 'Target directory'
|
155
|
+
|
156
|
+
def merge(ancestor_path, our_path, their_path)
|
157
|
+
validate_readable_path!('ancestor_path', ancestor_path)
|
158
|
+
validate_readable_path!('our_path', our_path)
|
159
|
+
validate_readable_path!('their_path', their_path)
|
160
|
+
|
161
|
+
result_path = options[:to]
|
162
|
+
validate_writable_path!('to', result_path)
|
163
|
+
|
164
|
+
handle_errors do
|
165
|
+
updater = PhraseAppUpdater.new(nil, nil, options[:file_format])
|
166
|
+
updater.merge_directories(our_path, their_path, ancestor_path, result_path)
|
83
167
|
end
|
84
168
|
end
|
85
169
|
|
86
|
-
desc
|
170
|
+
desc 'merge_file <ancestor> <ours> <theirs>',
|
171
|
+
'Perform 3-way merge of a single file into TO'
|
172
|
+
|
173
|
+
long_desc <<-LONGDESC
|
174
|
+
Perform 3-way merge of a single file into TO
|
175
|
+
|
176
|
+
Intended for use as a git merge-driver with:
|
177
|
+
[merge "phraseapp-locale"]
|
178
|
+
name = PhraseApp locale file merge driver
|
179
|
+
driver = phraseapp_updater merge_file %O %A %B --to %P
|
180
|
+
LONGDESC
|
181
|
+
|
182
|
+
method_option :to, type: :string, required: true, desc: 'Target file'
|
183
|
+
|
184
|
+
def merge_file(ancestor, ours, theirs)
|
185
|
+
validate_readable_file!('ancestor', ancestor)
|
186
|
+
validate_readable_file!('ours', ours)
|
187
|
+
validate_readable_file!('theirs', theirs)
|
188
|
+
validate_writable_file!('to', to)
|
189
|
+
|
190
|
+
# Git provides an empty file when there is no common ancestor in the
|
191
|
+
# merge-base. Because we want to merge from an empty hash structure instead,
|
192
|
+
# pass `nil` to `merge_files`.
|
193
|
+
ancestor = nil if File.zero?(ancestor)
|
194
|
+
|
195
|
+
updater = PhraseAppUpdater.new(nil, nil, file_format)
|
196
|
+
updater.merge_files(ours, theirs, ancestor, to)
|
197
|
+
end
|
198
|
+
|
199
|
+
desc 'default', 'Prints gem information'
|
87
200
|
option :version, aliases: [:v]
|
88
201
|
|
89
202
|
def default
|
@@ -98,37 +211,54 @@ class PhraseAppUpdaterCLI < Thor
|
|
98
211
|
|
99
212
|
private
|
100
213
|
|
101
|
-
def
|
102
|
-
|
103
|
-
|
104
|
-
if
|
105
|
-
|
214
|
+
def print_diff(diffs)
|
215
|
+
normalized_diffs = diffs.flat_map do |diff|
|
216
|
+
type, path, c1, c2 = diff
|
217
|
+
if type == '~'
|
218
|
+
[['-', path, c1], ['+', path, c2]]
|
219
|
+
else
|
220
|
+
[diff]
|
106
221
|
end
|
107
|
-
|
108
|
-
config = PhraseAppUpdater.load_config(options[:config_file_path])
|
109
|
-
|
110
|
-
phraseapp_api_key = config.api_key
|
111
|
-
phraseapp_project_id = config.project_id
|
112
|
-
file_format = config.file_format
|
113
|
-
else
|
114
|
-
phraseapp_api_key = options.fetch(:phraseapp_api_key, ENV["PA_API_KEY"]).to_s
|
115
|
-
phraseapp_project_id = options.fetch(:phraseapp_project_id, ENV["PA_PROJECT_ID"]).to_s
|
116
|
-
file_format = options.fetch(:file_format, ENV["PA_FILE_FORMAT"]).to_s
|
117
222
|
end
|
118
223
|
|
119
|
-
|
120
|
-
|
224
|
+
normalized_diffs.each do |type, path, change|
|
225
|
+
puts "#{type} #{path}: #{change}"
|
121
226
|
end
|
227
|
+
end
|
122
228
|
|
123
|
-
|
124
|
-
|
125
|
-
|
229
|
+
def handle_errors
|
230
|
+
yield
|
231
|
+
rescue PhraseAppUpdater::PhraseAppAPI::BadAPIKeyError
|
232
|
+
STDERR.puts 'Bad PhraseApp API key.'
|
233
|
+
exit(1)
|
234
|
+
rescue PhraseAppUpdater::PhraseAppAPI::BadProjectIDError => e
|
235
|
+
STDERR.puts "Bad PhraseApp project ID: #{e.project_id}"
|
236
|
+
exit(1)
|
237
|
+
rescue PhraseAppUpdater::PhraseAppAPI::ProjectNameTakenError
|
238
|
+
STDERR.puts "PhraseApp project name already taken: #{options[:phraseapp_project_name]}"
|
239
|
+
exit(1)
|
240
|
+
rescue PhraseAppUpdater::LocaleFile::BadFileTypeError => e
|
241
|
+
STDERR.puts "Bad filetype for localization files: #{e.message}"
|
242
|
+
exit(1)
|
243
|
+
rescue PhraseAppUpdater::PhraseAppAPI::MissingGitParentError
|
244
|
+
STDERR.puts 'Git ancestor commit not recorded on PhraseApp project'
|
245
|
+
exit(1)
|
246
|
+
rescue StandardError => e
|
247
|
+
STDERR.puts "Unknown error occurred: #{e.message}"
|
248
|
+
STDERR.puts e.backtrace
|
249
|
+
exit(1)
|
250
|
+
end
|
126
251
|
|
127
|
-
|
128
|
-
|
252
|
+
def validate_readable_file!(name, file)
|
253
|
+
unless File.readable?(file) && File.file?(file)
|
254
|
+
raise RuntimeError.new("#{name} is not a readable file: #{file}")
|
129
255
|
end
|
256
|
+
end
|
130
257
|
|
131
|
-
|
258
|
+
def validate_writable_file!(name, file)
|
259
|
+
unless File.writable?(file) && File.file?(file)
|
260
|
+
raise RuntimeError.new("#{name} is not a writable file: #{file}")
|
261
|
+
end
|
132
262
|
end
|
133
263
|
|
134
264
|
def validate_path!(name, path)
|
@@ -153,14 +283,11 @@ class PhraseAppUpdaterCLI < Thor
|
|
153
283
|
end
|
154
284
|
end
|
155
285
|
|
156
|
-
def
|
157
|
-
|
158
|
-
|
159
|
-
File.write(full_path, file.content)
|
286
|
+
def sh(x)
|
287
|
+
`#{x}`.tap do
|
288
|
+
raise RuntimeError.new("Shell command failed: '#{x}'") unless $?.success?
|
160
289
|
end
|
161
|
-
puts "Wrote #{files.count} files to #{path}: #{files.map(&:name_with_extension)}"
|
162
290
|
end
|
163
291
|
end
|
164
292
|
|
165
293
|
PhraseAppUpdaterCLI.start(ARGV)
|
166
|
-
|