Skip to content

Add "Sync with Door43" for shared/collaborative projects - #7669

Open
deferredreward wants to merge 5 commits into
developfrom
feature-deferredreward-sync-door43
Open

deferredreward wants to merge 5 commits into
developfrom
feature-deferredreward-sync-door43

Conversation

@deferredreward

Copy link
Copy Markdown

Fixes #7667

What

Adds a per-project Sync with Door43 action (in the project card menu, next to Upload) that pulls the latest changes from the project's Door43 repo, merges them with local checking work, and pushes the combined result back — enabling multiple people (or one person on multiple machines) to work on the same shared Door43 project. Today the only push-side action is Upload, a plain git push that aborts on non-fast-forward with no way to reconcile.

How

A raw git merge is not viable for tC data (thousands of timestamp-named JSON check-data files would conflict constantly with no resolution UI), so sync uses a data-level merge built on the existing project-overwrite machinery:

  1. Commit local state (same steps as upload: unlock, save changes, USFM3 export, commit).
  2. Fast path: get the remote HEAD sha (getRemoteRepoHead) and, if it is already an ancestor of local HEAD (new GitApi.isCommitInHistory), just push — no clone or merge. Cheap for the two-machines-one-user case.
  3. Otherwise clone the remote fresh into the imports area, verify/migrate it, and merge the local project into the clone (ProjectSyncHelpers.mergeLocalIntoRemoteClone): local check data is the base, remote-only check data is unioned in (timestamp filenames make file-level union safe), remote alignments are overlaid per verse (reusing copyAlignmentData), manifest checkers/translators are unioned, and external verse edits are recorded with stale selections invalidated (reusing createVerseEditsForAllChangedVerses). For verse text, the Door43 copy wins — disclosed in the confirmation dialog.
  4. Keep the clone's git history so the post-merge commit fast-forwards, swap it into place with a .sync_backup for failure recovery, then push to the origin owner's repo using the logged-in user's token (new GitApi.pushRepo) so collaborators can push to a shared repo.

Edge cases handled: offline / not logged in (same guards as upload); project never uploaded (offer Upload); remote repo deleted (offer re-upload); push race — remote changed mid-sync (prompt to sync again); permission denied (new GIT_ERROR_PUSH_DENIED mapping → suggest collaborator access or an own-copy upload).

Files

  • New src/js/actions/ProjectSyncActions.js, src/js/helpers/ProjectSyncHelpers.js
  • GitApi.js: pushRepo (generalizes pushNewRepo to any owner/name), isCommitInHistory
  • Repo.js: GIT_ERROR_PUSH_DENIED + mapping in convertGitErrorMessage
  • ProjectUploadActions.js: export makeSureProjectUnlocked
  • Sync menu item (ProjectCardMenu/Menu.js + index.js); ~10 new projects.* locale keys + buttons.upload_button (English only; other locales via Crowdin)

Testing

  • New ProjectSyncHelpers.test.js (6 tests: checkData union, alignment overlay, .git preserved, manifest union, no-remote-apps copy), new ProjectSyncActions.test.js (guard cases), extended GitApi.test.js (pushRepo, isCommitInHistory). Also added the missing fs.writeJsonSync alias to the fs-extra test mock (it only had writeJSONSync, so copyAlignmentData's writes were silent no-ops under test). All touched suites pass (ProjectSyncHelpers, ProjectSyncActions, GitApi, Repo, ProjectUploadActions, ProjectCardMenu); eslint clean.
  • Not verified in this PR: a live end-to-end round-trip against Door43 (two clients editing the same repo, merge + push, permission-denied and push-race paths). The merge helpers and git primitives are unit-tested, but the full flow against a real DCS repo should be exercised during review. The interactive validation stepper is intentionally skipped for synced content (MVP); per-verse text conflict UI, org repos / non-master branches, and an auto-sync indicator are noted as follow-ups.

🤖 Generated with Claude Code

deferredreward and others added 5 commits July 9, 2026 11:00
Adds a per-project "Sync with Door43" menu action that pulls the latest
from the project's Door43 repo, merges it with local checking work, and
pushes the combined result back - enabling multiple people (or one
person on multiple machines) to work on the same shared project.

Strategy (data-level merge, not raw git merge):
- fast path: if remote HEAD is already in local history, just push
- otherwise clone the remote fresh, verify/migrate it, merge the local
  project into the clone (local check data kept, remote check data
  unioned in, remote alignments overlaid, manifest checkers/translators
  unioned, external verse edits recorded), keep the clone's git history
  so the merged commit fast-forwards, swap into place with a backup,
  then push to the origin owner's repo with the user's token
- edge cases: offline / not logged in, no remote (offer upload),
  remote deleted (offer re-upload), push race (retry), permission
  denied (suggest collaborator access or own-copy upload)

New: ProjectSyncActions, ProjectSyncHelpers, GitApi.pushRepo/
isCommitInHistory, Repo GIT_ERROR_PUSH_DENIED mapping, Sync menu item.

Fixes #7667

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mock defined writeJSONSync but not the lowercase writeJsonSync
that copyAlignmentData (and other helpers) call, so those writes were
silent no-ops under test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- resolve the sync promise on the invalid-token guard (was hanging)
- treat an empty remote (no HEAD) as up to date -> fast push instead of
  a misleading import error
- validate remote owner/name and the commit sha before passing them to
  git shell commands (prevents injection via a crafted git config)
- reuse ProjectUploadActions.saveChangesInOldProjects instead of
  inlining; collapse the duplicated no-remote/remote-missing upload
  prompt into one helper
- add syncProject flow tests (fast path, merge path, push race, no
  remote) and GitApi pushRepo error-path + isCommitInHistory guard tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The alignment-overlay assertion relied on the fs-extra test mock's
writeJsonSync, which genMockFromModule stubs as a no-op unless
overridden; the override behaved differently under CI's npm ci vs a
local npm i, so the test passed locally but failed on CI. Revert the
shared-mock writeJsonSync alias and instead spy on copyAlignmentData
(whose remote-wins behavior is already covered in
ProjectOverwriteHelpers.test.js), asserting mergeLocalIntoRemoteClone
delegates to it with the correct directories and uses local .apps as
the base.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

Add a "Sync with Door43" action so shared projects can be worked on by multiple people

1 participant