phraseapp_updater 0.1.7 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/iknow/phraseapp_updater.svg?branch=master)](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
|
-
|