1# coding=utf8
2
3import os
4import logging
5import argparse
6from contextlib import contextmanager
7import importlib
8import sys
9
10import hglib
11import six
12
13from fluent.migrate.context import MigrationContext
14from fluent.migrate.errors import MigrationError
15from fluent.migrate.changesets import convert_blame_to_changesets
16from fluent.migrate.blame import Blame
17
18
19@contextmanager
20def dont_write_bytecode():
21    _dont_write_bytecode = sys.dont_write_bytecode
22    sys.dont_write_bytecode = True
23    yield
24    sys.dont_write_bytecode = _dont_write_bytecode
25
26
27class Migrator(object):
28    def __init__(self, locale, reference_dir, localization_dir, dry_run):
29        self.locale = locale
30        self.reference_dir = reference_dir
31        self.localization_dir = localization_dir
32        self.dry_run = dry_run
33        self._client = None
34
35    @property
36    def client(self):
37        if self._client is None:
38            self._client = hglib.open(self.localization_dir, 'utf-8')
39        return self._client
40
41    def close(self):
42        # close hglib.client, if we cached one.
43        if self._client is not None:
44            self._client.close()
45
46    def run(self, migration):
47        print('\nRunning migration {} for {}'.format(
48            migration.__name__, self.locale))
49
50        # For each migration create a new context.
51        ctx = MigrationContext(
52            self.locale, self.reference_dir, self.localization_dir
53        )
54
55        try:
56            # Add the migration spec.
57            migration.migrate(ctx)
58        except MigrationError as e:
59            print('  Skipping migration {} for {}:\n    {}'.format(
60                migration.__name__, self.locale, e))
61            return
62
63        # Keep track of how many changesets we're committing.
64        index = 0
65        description_template = migration.migrate.__doc__
66
67        # Annotate localization files used as sources by this migration
68        # to preserve attribution of translations.
69        files = ctx.localization_resources.keys()
70        blame = Blame(self.client).attribution(files)
71        changesets = convert_blame_to_changesets(blame)
72        known_legacy_translations = set()
73
74        for changeset in changesets:
75            snapshot = self.snapshot(
76                ctx, changeset['changes'], known_legacy_translations
77            )
78            if not snapshot:
79                continue
80            self.serialize_changeset(snapshot)
81            index += 1
82            self.commit_changeset(
83                description_template, changeset['author'], index
84            )
85
86    def snapshot(self, ctx, changes_in_changeset, known_legacy_translations):
87        '''Run the migration for the changeset, with the set of
88        this and all prior legacy translations.
89        '''
90        known_legacy_translations.update(changes_in_changeset)
91        return ctx.serialize_changeset(
92            changes_in_changeset,
93            known_legacy_translations
94        )
95
96    def serialize_changeset(self, snapshot):
97        '''Write serialized FTL files to disk.'''
98        for path, content in six.iteritems(snapshot):
99            fullpath = os.path.join(self.localization_dir, path)
100            print('  Writing to {}'.format(fullpath))
101            if not self.dry_run:
102                fulldir = os.path.dirname(fullpath)
103                if not os.path.isdir(fulldir):
104                    os.makedirs(fulldir)
105                with open(fullpath, 'wb') as f:
106                    f.write(content.encode('utf8'))
107                    f.close()
108
109    def commit_changeset(
110        self, description_template, author, index
111    ):
112        message = description_template.format(
113            index=index,
114            author=author
115        )
116
117        print('  Committing changeset: {}'.format(message))
118        if self.dry_run:
119            return
120        try:
121            self.client.commit(
122                message, user=author.encode('utf-8'), addremove=True
123            )
124        except hglib.error.CommandError as err:
125            print('    WARNING: hg commit failed ({})'.format(err))
126
127
128def main(locale, reference_dir, localization_dir, migrations, dry_run):
129    """Run migrations and commit files with the result."""
130    migrator = Migrator(locale, reference_dir, localization_dir, dry_run)
131
132    for migration in migrations:
133        migrator.run(migration)
134
135    migrator.close()
136
137
138def cli():
139    parser = argparse.ArgumentParser(
140        description='Migrate translations to FTL.'
141    )
142    parser.add_argument(
143        'migrations', metavar='MIGRATION', type=str, nargs='+',
144        help='migrations to run (Python modules)'
145    )
146    parser.add_argument(
147        '--locale', '--lang', type=str,
148        help='target locale code (--lang is deprecated)'
149    )
150    parser.add_argument(
151        '--reference-dir', type=str,
152        help='directory with reference FTL files'
153    )
154    parser.add_argument(
155        '--localization-dir', type=str,
156        help='directory for localization files'
157    )
158    parser.add_argument(
159        '--dry-run', action='store_true',
160        help='do not write to disk nor commit any changes'
161    )
162    parser.set_defaults(dry_run=False)
163
164    logger = logging.getLogger('migrate')
165    logger.setLevel(logging.INFO)
166
167    args = parser.parse_args()
168
169    # Don't byte-compile migrations.
170    # They're not our code, and infrequently run
171    with dont_write_bytecode():
172        migrations = map(importlib.import_module, args.migrations)
173
174    main(
175        locale=args.locale,
176        reference_dir=args.reference_dir,
177        localization_dir=args.localization_dir,
178        migrations=migrations,
179        dry_run=args.dry_run
180    )
181
182
183if __name__ == '__main__':
184    cli()
185