Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor sync #3312

Merged
merged 68 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
3d4c761
Add new sync implementation (WIP, untested)
eemeli May 20, 2024
9e65c9a
Address code review comments, start adding tests
eemeli Sep 9, 2024
3d6a1ec
Merge branch 'main' into sync-refactor
eemeli Sep 14, 2024
397e5f5
Add integration tests for new code; fix discovered issues
eemeli Sep 14, 2024
5778090
Update requirements
eemeli Sep 17, 2024
7f3bfcb
Satisfy lint
eemeli Sep 17, 2024
83b8fd7
Call repo commit() with pre-formatted author string rather than User …
eemeli Sep 21, 2024
3005870
Add end-to-end test
eemeli Sep 21, 2024
5b227e9
Add task wrapper & force option
eemeli Sep 21, 2024
e657223
Refactor handle_upload_content() into sync_uploaded_file()
eemeli Sep 22, 2024
5da483f
Replace get_download_content() with download_translations_zip()
eemeli Sep 30, 2024
2f0ea10
Remove old sync implementation
eemeli Oct 1, 2024
bb97199
Pretranslate added & changed resources, fix task invocations
eemeli Oct 1, 2024
3da84c0
Move & rename pontoon.sync contents around, adding pontoon.sync.core
eemeli Oct 1, 2024
92fb993
Combine upload & download functions into pontoon.sync.utils
eemeli Oct 1, 2024
410a278
Merge branch 'main' into sync-refactor
eemeli Oct 1, 2024
83049d9
Include --no-strip-extras in `uv pip compile` calls
eemeli Oct 1, 2024
f92e331
Satisfy ruff
eemeli Oct 1, 2024
fcfc5fa
File format detector cleanup, drop unused template
eemeli Oct 1, 2024
15ef5b6
Fix last_synced_revision data, drop remaining multi_locale references
eemeli Oct 1, 2024
f5e16db
More dead code removal
eemeli Oct 1, 2024
455c64a
Revert Repository.permalink_prefix help_text change
eemeli Oct 1, 2024
a5177cf
Support file renames for git repos
eemeli Oct 10, 2024
4efe373
Fix issues discovered by manual testing
eemeli Oct 10, 2024
2714f81
Improve sync logging
eemeli Oct 12, 2024
bb26ab5
Merge branch 'main' into sync-refactor
eemeli Oct 12, 2024
76dfa40
More sync fixes & logging
eemeli Oct 12, 2024
c49e95b
Update moz.l10n dependency
eemeli Oct 13, 2024
63aac31
Oops, fix file upload handling
eemeli Oct 13, 2024
fb538a1
Fix zip download, add test for it
eemeli Oct 15, 2024
8aebdd6
Drop unnecessary extras from test_download
eemeli Oct 15, 2024
646ab4b
Simplify aggregated stats for .po plurals, use SQL UPDATE queries
eemeli Oct 16, 2024
b4854c4
Reduce stats updates further, include total_strings calculation
eemeli Oct 16, 2024
d322f3d
Use simpler query for looking up entity identifiers
eemeli Oct 16, 2024
68f94f7
Use new update_stats() for `manage.py calculate_stats` command
eemeli Oct 17, 2024
240cd73
Add & remove TranslatedResource objects when locales change
eemeli Oct 17, 2024
b70451b
Sum project total_strings from translated resources, not resources
eemeli Oct 18, 2024
33c307d
Update moz.l10n to 0.5.2, log changed resources
eemeli Oct 18, 2024
263a2f1
Always sync all translated resources
eemeli Oct 18, 2024
e6d0425
Merge branch 'main' into sync-refactor
eemeli Oct 18, 2024
5eb6a9f
Merge branch 'main' into sync-refactor
eemeli Nov 26, 2024
f7171bf
Fix manual pretranslation task
eemeli Nov 26, 2024
cf4e4b8
Fix file upload, ensure that it reports at least some error on failure
eemeli Nov 27, 2024
bd649e7
Update to moz.l10n 0.5.5
eemeli Nov 27, 2024
cebc5b2
Satisfy ruff
eemeli Nov 27, 2024
a470d4a
Update to moz.l10n 0.5.6
eemeli Dec 2, 2024
37bdb89
Dedupe updates for multiple changes made to the same resource
eemeli Dec 4, 2024
ff10ffa
Apply suggested changes from code review
eemeli Dec 4, 2024
5b4f8b6
Merge branch 'main' into sync-refactor
eemeli Dec 4, 2024
d575b64
Drop dead code: Entity.reset_active_translation()
eemeli Dec 4, 2024
5d49838
Oops, it's EntityQuerySet.reset_active_translations() that is no long…
eemeli Dec 4, 2024
f166d76
Add test case for translation arriving before its source is added
eemeli Dec 5, 2024
a2ac4fb
Use shallow clones for downloads from projects using git repos
eemeli Dec 12, 2024
c244927
When downloading translations, skip missing files & use full target r…
eemeli Dec 12, 2024
ea4edf0
Merge branch 'main' into sync-refactor
eemeli Dec 12, 2024
619a22a
Fix download tests
eemeli Dec 12, 2024
1f298f4
Update to translate-toolkit 3.14.1
eemeli Dec 12, 2024
c309617
Fix total_strings counts to depend on gettext locale plurals in aggre…
eemeli Dec 13, 2024
c239d7d
Drop unused ResourceQuerySet
eemeli Dec 13, 2024
784dec2
Dismiss local git repo edits when branch is specified
eemeli Dec 16, 2024
8f26503
During update from repo, keep previously fuzzy suggestions unchanged …
eemeli Dec 16, 2024
3762da5
Rather than creating zip, "download" by redirecting to target repository
eemeli Dec 16, 2024
74e4a14
Add shortcut (read: hack) for downloading from projects with separate…
eemeli Dec 16, 2024
357f7fe
Include active fuzzy translations when writing to repo
eemeli Dec 17, 2024
008fa56
When approving matching prior translations, do not also reject them
eemeli Dec 18, 2024
ff95768
Use locale's total_strings for GET <locale>/<slug>/parts/
eemeli Dec 18, 2024
c931f71
Count translation updates before dropping approvals from the dict
eemeli Dec 18, 2024
ff2b00d
Rename add_errors() as add_failed_checks()
eemeli Dec 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions pontoon/base/models/changed_entity_locale.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
from typing import TYPE_CHECKING

from django.db import models
from django.utils import timezone


if TYPE_CHECKING:
from pontoon.base.models import Entity, Locale


class ChangedEntityLocale(models.Model):
"""
ManyToMany model for storing what locales have changed translations for a
specific entity since the last sync.
"""

entity = models.ForeignKey("Entity", models.CASCADE)
locale = models.ForeignKey("Locale", models.CASCADE)
entity: models.ForeignKey["Entity"] = models.ForeignKey("Entity", models.CASCADE)
locale: models.ForeignKey["Locale"] = models.ForeignKey("Locale", models.CASCADE)
eemeli marked this conversation as resolved.
Show resolved Hide resolved
when = models.DateTimeField(default=timezone.now)

class Meta:
Expand Down
8 changes: 8 additions & 0 deletions pontoon/base/models/project.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from collections import defaultdict
from os.path import basename, join, normpath
from typing import TYPE_CHECKING
from urllib.parse import urlparse

from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.db.models import Prefetch
from django.db.models.manager import BaseManager
from django.utils import timezone
from django.utils.functional import cached_property

Expand All @@ -15,6 +17,10 @@
from pontoon.base.models.locale import Locale


if TYPE_CHECKING:
from pontoon.base.models import Resource


class Priority(models.IntegerChoices):
LOWEST = 1, "Lowest"
LOW = 2, "Low"
Expand Down Expand Up @@ -103,6 +109,8 @@ class Project(AggregatedStats):
slug = models.SlugField(unique=True)
locales = models.ManyToManyField(Locale, through="ProjectLocale")

resources: BaseManager["Resource"]
eemeli marked this conversation as resolved.
Show resolved Hide resolved

class DataSource(models.TextChoices):
REPOSITORY = "repository", "Repository"
DATABASE = "database", "Database"
Expand Down
4 changes: 3 additions & 1 deletion pontoon/base/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ def locales(self, create, extracted, **kwargs):

if extracted:
for locale in extracted:
ProjectLocaleFactory.create(project=self, locale=locale)
ProjectLocaleFactory.create(
project=self, locale=locale, total_strings=self.total_strings
)

@factory.post_generation
def repositories(self, create, extracted, **kwargs):
Expand Down
92 changes: 92 additions & 0 deletions pontoon/sync/checkouts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import logging

from os import walk
from os.path import join, normpath, relpath
from typing import NamedTuple, cast

from django.db.models.manager import BaseManager

from pontoon.base.models import Project, Repository
from pontoon.sync.repositories import get_repo


log = logging.getLogger(__name__)


class Checkout:
repo: Repository
is_source: bool
url: str
path: str
prev_commit: str | None
commit: str | None
changed: list[str]
"""Relative paths from the checkout base"""
removed: list[str]
"""Relative paths from the checkout base"""

def __init__(self, slug: str, db_repo: Repository, pull: bool) -> None:
self.repo = db_repo
self.is_source = db_repo.source_repo
self.url = db_repo.url
self.path = normpath(db_repo.checkout_path)

if db_repo.last_synced_revisions is None:
self.prev_commit = None
else:
pc = db_repo.last_synced_revisions.get("single_locale", None)
mathjazz marked this conversation as resolved.
Show resolved Hide resolved
self.prev_commit = pc if isinstance(pc, str) else None

versioncontrol = get_repo(db_repo.type)
if pull:
log.info(f"[{slug}] Pulling updates from {self.url}")
versioncontrol.update(self.url, self.path, db_repo.branch)
else:
log.info(f"[{slug}] Skipping pull")
self.commit = versioncontrol.revision(self.path)
log.info(f"[{slug}] Repo {self.url} now at {self.commit}")

delta = (
versioncontrol.changed_files(self.path, self.prev_commit)
if isinstance(self.prev_commit, str)
else None
)
if delta is not None:
self.changed, self.removed = delta
else:
# Initially and on error, consider all files changed
self.changed = []
for root, dirnames, filenames in walk(self.path):
dirnames[:] = (dn for dn in dirnames if not dn.startswith("."))
rel_root = relpath(root, self.path) if root != self.path else ""
self.changed.extend(
join(rel_root, fn) for fn in filenames if not fn.startswith(".")
)
self.removed = []


class Checkouts(NamedTuple):
source: Checkout
target: Checkout


def get_checkouts(project: Project, pull: bool = True) -> Checkouts:
"""
For each project repository including all multi-locale repositories,
update its local checkout (unless `pull` is false),
and provide a `Checkout` representing their current state.
"""
source: Checkout | None = None
target: Checkout | None = None
for repo in cast(BaseManager[Repository], project.repositories).all():
if repo.source_repo:
if source:
raise Exception("Multiple source repositories")
source = Checkout(project.slug, repo, pull)
elif target:
raise Exception("Multiple target repositories")
else:
target = Checkout(project.slug, repo, pull)
if source is None and target is None:
raise Exception("No repository found")
return Checkouts(source or target, target or source)
3 changes: 2 additions & 1 deletion pontoon/sync/formats/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
xliff,
xml,
)
from pontoon.sync.formats.base import ParsedResource


# To add support for a new resource format, add an entry to this dict
Expand Down Expand Up @@ -56,7 +57,7 @@ def are_compatible_files(file_a, file_b):
return False


def parse(path, source_path=None, locale=None):
def parse(path, source_path=None, locale=None) -> ParsedResource:
"""
Parse the resource file at the given path and return a
ParsedResource with its translations.
Expand Down
7 changes: 6 additions & 1 deletion pontoon/sync/formats/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from pontoon.sync.vcs.translation import VCSTranslation


class ParsedResource:
"""
Parent class for parsed resources as returned by parse.
Expand All @@ -6,8 +9,10 @@ class ParsedResource:
that inherits from this class.
"""

entities: dict[str, VCSTranslation]

@property
def translations(self):
def translations(self) -> list[VCSTranslation]:
"""
Return a list of VCSTranslation instances or subclasses that
represent the translations in the resource.
Expand Down
2 changes: 2 additions & 0 deletions pontoon/sync/formats/po.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ def __repr__(self):


class POResource(ParsedResource):
entities: list[POEntity]

def __init__(self, pofile):
self.pofile = pofile
self.entities = [
Expand Down
39 changes: 39 additions & 0 deletions pontoon/sync/paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import logging

from os.path import join

from moz.l10n.paths import L10nConfigPaths, L10nDiscoverPaths, get_android_locale

from pontoon.base.models import Project
from pontoon.sync.checkouts import Checkouts
from pontoon.sync.vcs.project import MissingLocaleDirectoryError


log = logging.getLogger(__name__)


def get_paths(
project: Project, checkouts: Checkouts
) -> L10nConfigPaths | L10nDiscoverPaths:
force_paths = [
join(checkouts.source.path, path) for path in checkouts.source.removed
]
if project.configuration_file:
paths = L10nConfigPaths(
join(checkouts.source.path, project.configuration_file),
locale_map={"android_locale": get_android_locale},
force_paths=force_paths,
)
if checkouts.target != checkouts.source:
paths.base = checkouts.target.repo.checkout_path
return paths
else:
paths = L10nDiscoverPaths(
project.checkout_path,
ref_root=checkouts.source.path,
force_paths=force_paths,
source_locale=["templates", "en-US", "en"],
)
if paths.base is None:
raise MissingLocaleDirectoryError("Base localization directory not found")
return paths
Loading
Loading