Skip to content

Project config: preserve resources and localized files - #4571

Open
ayshushus wants to merge 21 commits into
mozilla:mainfrom
ayshushus:2844-config
Open

ayshushus wants to merge 21 commits into
mozilla:mainfrom
ayshushus:2844-config

Conversation

@ayshushus

Copy link
Copy Markdown
Contributor

Fix #2844
Fix #4385

  • Current resources whose path is no longer in paths.ref_paths now go through mark_as_obsolete().
  • Localized files are now kept.
  • If the project uses a config and a config file is in the diff, the source file is now added to the diff along with the existing localized files.
  • Add a holistic test.

@ayshushus
ayshushus requested a review from mathjazz October 6, 2026 04:06

@mathjazz mathjazz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the patch! Left a few comments inline.

removed_paths = {
get_db_path(paths, join(checkout.path, co_path)) for co_path in checkout.removed
}
if isinstance(paths, L10nConfigPaths):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking entities obsolete here can't be undone later. When a path comes back, update_resources() un-obsoletes the resource but not its entities, so it creates a second copy of every Entity, and the old translations, comments and reviews stay stuck on the hidden ones.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this effectively #4128? Based on your comment there, I'm applying a content (value & properties) match that is LIFO/newest copy. Also adding a "Restored" log.

Comment thread pontoon/sync/core/entities.py Outdated
get_db_path(paths, join(checkout.path, co_path)) for co_path in checkout.removed
}
if isinstance(paths, L10nConfigPaths):
ref_db_paths = {get_db_path(paths, ref_path) for ref_path in paths.ref_paths}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ref_paths is just whatever the glob matched this run, so a typo in reference (or a source folder moved one commit before the config is updated) makes it empty and wipes out the whole project in one sync.

Shall we refuse to obsolete when the list covers all or nearly all resources?

Comment thread pontoon/sync/core/paths.py Outdated
current = set(project.resources.current().values_list("path", flat=True))
for ref_path in paths.ref_paths:
if (
relpath(ref_path, paths.ref_root) in current

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This skips references we already track, so if only the l10n target moves (e.g. {locale}/a.ftl → l10n/{locale}/a.ftl), the files at the new location never get added to target.changed and their translations aren't picked up.

@eemeli eemeli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A partial review, happy to have Matjaž take the lead here.

Comment thread pontoon/sync/core/paths.py
Comment thread pontoon/sync/core/entities.py Outdated
ref_db_paths = {get_db_path(paths, ref_path) for ref_path in paths.ref_paths}
current_paths = set(project.resources.current().values_list("path", flat=True))
unreferenced_paths = current_paths - ref_db_paths
if len(unreferenced_paths) > 0.9 * len(current_paths):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This magic number should be a constant defined near the top of the file.

Comment thread pontoon/sync/core/entities.py Outdated
}
if isinstance(paths, L10nConfigPaths):
ref_db_paths = {get_db_path(paths, ref_path) for ref_path in paths.ref_paths}
current_paths = set(project.resources.current().values_list("path", flat=True))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the second time you end up doing the same DB query during the same sync; we should avoid that.

Comment thread pontoon/sync/core/entities.py Outdated
ent.obsolete = False
ent.date_obsoleted = None
restored[key] = ent
Entity.objects.bulk_update(restored.values(), ["obsolete", "date_obsoleted"])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entity deobsoletion seems like quite a bit of scope creep for work under "Project config: preserve resources and localized files". I'm pretty sure this ought to get left out at least for now, and addressed separately -- there are almost certainly side effects that need to be accounted for.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mathjazz, thoughts?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, let's leave that out.

@mathjazz mathjazz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, nice work!

Comment thread pontoon/sync/core/checkout.py Outdated
Comment thread pontoon/sync/core/entities.py Outdated
Comment thread pontoon/sync/core/entities.py Outdated
Comment thread pontoon/sync/core/paths.py Outdated
Comment thread pontoon/sync/core/paths.py Outdated
Comment thread pontoon/sync/core/paths.py Outdated
Comment thread pontoon/sync/core/paths.py Outdated
Comment thread pontoon/sync/tests/test_e2e.py Outdated
Comment thread pontoon/sync/tests/test_e2e.py Outdated
Comment thread pontoon/sync/tests/test_entities.py Outdated
@ayshushus
ayshushus requested a review from eemeli October 14, 2026 04:42
Comment thread pontoon/sync/core/paths.py Outdated
Comment on lines +110 to +114
tgt_path = paths.format_target_path(tgt_template, locale)
if tracked and (
ref_path in renamed_refs
or prev_paths is None
or prev_paths.target(ref_path, locale=locale)[0] == tgt_path
):
continue
if has_current_resource:
prev_tgt_path, _ = prev_paths.target(ref_path, locale=locale)
if prev_tgt_path == tgt_path:
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the paths match for any locale, doesn't that mean that they'll match for every locale? Could you not compare the template strings instead, outside this loop?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhhh... yes. I also just realized that the previous approach was a little inefficient...
Changing now.

Comment thread pontoon/sync/core/entities.py Outdated
Comment on lines 53 to 58
config_current_paths: set[str] = set()
if isinstance(paths, L10nConfigPaths):
config_current_paths = {
renamed_db_paths.get(path, path)
for path in project.resources.current().values_list("path", flat=True)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The term "current" is confusing me here, because it's not clear if it's a point in time before the sync started, some point during the sync, or after the sync has completed.

Let's avoid it, and use something more verbose but explicit like config_db_paths_with_renames.

Comment thread pontoon/sync/core/paths.py Outdated
for ref_path in paths.ref_paths:
if not isfile(ref_path) or not is_inside(source.path, ref_path):
continue
has_current_resource = relpath(ref_path, paths.ref_root) in config_current_paths

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely better, but still a bit confusing, again I think driven by the "current" term being unclear.

Would something like this be accurate?

Suggested change
has_current_resource = relpath(ref_path, paths.ref_root) in config_current_paths
is_new = relpath(ref_path, paths.ref_root) not in config_current_paths

Note that the conditional is inverted here.

Comment thread pontoon/sync/core/paths.py Outdated
Comment on lines +102 to +105
if not has_current_resource and ref_path not in src_changed:
source.changed.append(relpath(ref_path, source.path))
if has_current_resource and ref_path in renamed_refs:
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continuing with the previous suggestion:

Suggested change
if not has_current_resource and ref_path not in src_changed:
source.changed.append(relpath(ref_path, source.path))
if has_current_resource and ref_path in renamed_refs:
continue
if is_new:
if ref_path not in src_changed:
source.changed.append(relpath(ref_path, source.path))
else:
if ref_path in renamed_refs:
continue

@ayshushus
ayshushus requested a review from eemeli October 14, 2026 18:18

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Project config: strings in new file appeared only after forced sync Resources removed from project configuration are not removed from Pontoon

3 participants