Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:
e2e-relevant: ${{ steps.filter.outputs.e2e-relevant }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- uses: dorny/paths-filter@15192bc058cc28a13dbf6cde61f19e18988b7af6 # v3
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4
id: filter
with:
filters: |
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,13 @@ When both sides changed, Git File Sync keeps the conflict explicit. Choose **Kee
| **GitLab** | Token, project ID, base URL | `read_repository`, `write_repository` |
| **Gitea** | Token, owner, repository, base URL | `write:repository` on Gitea 1.19+ |

Other settings include language, branch, repository root path, vault-folder scope, startup refresh, ignore patterns, and symbolic-link handling. See [Symbolic link handling](docs/symlink-handling.md) for details.
Other settings include language, branch, repository root path, vault-folder scope, ignore patterns, and symbolic-link handling. See [Symbolic link handling](docs/symlink-handling.md) for details.

### Automatic sync

Automatic sync is **off by default**. When enabled, Git File Sync refreshes local and remote state on a configurable interval (minimum 1 minute) and applies the same default action the manual **Sync** button would for each pending change — push, pull, or remote delete — through the normal Source Control pipeline. An optional **Sync on startup** runs one automatic pass after Obsidian finishes loading without opening the Source Control view.

Files that need manual conflict resolution are always skipped: they stay visible as conflicts while unrelated safe changes continue to sync. Automatic runs never show confirmation or conflict dialogs, stay silent on success, and skip a tick when another sync is already running. **Refresh status on startup** is a separate setting that only refreshes the Source Control status view.

> **Security:** scope tokens to the smallest possible repository access and permissions, set an expiration where possible, and never place a token inside a note that may be synced. Revoke and rotate a token immediately if it may have been exposed.

Expand Down
39 changes: 36 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ The dependency direction should normally flow downward. Results and state flow b
| Application | `ChangeActionPolicy` | allowed/default action for each change kind | ViewModel, selection reconciliation, intent execution | UI rendering, network calls |
| Application | `SourceControlViewModel` | read-only projection of application state for UI | repository, selection, operation/refresh state | side effects, provider calls, filesystem writes |
| Application | `SourceControlActionService` | stable UI-facing facade for immediate Source Control commands | `SyncWorkspace`, `SyncIntentExecutor` | provider-specific logic, duplicated sync planning |
| Application | `SyncIntentExecutor` | one Sync Queue workflow: resolve intent, plan, confirm, execute, aggregate | repository, action policy, `SyncWorkspace`, notifier | UI DOM, provider API implementation |
| Application | `SyncIntentExecutor` | one Sync Queue workflow: resolve intent, plan, confirm, execute, aggregate; selects interactive vs background execution policy per run | repository, action policy, `SyncWorkspace`, notifier, `SyncExecutionGuard` | UI DOM, provider API implementation |
| Application | `AutomaticSyncService` | one scheduled automatic sync run inside one guard hold: refresh, read repository, build default intents, execute in background, refresh once more only if something executed; reports execution failures to the diagnostic logger | `SyncWorkspace`, `ChangeRepository`, `SourceControlActionService` | timer/scheduling mechanics, UI rendering, classification rules |
| Application | `SyncExecutionGuard` | the single application-level lock serializing provider mutations (manual waits, automatic try-acquires); owned by `SourceControlActionService`, shared with `SyncIntentExecutor` | `SyncIntentExecutor`, `SourceControlActionService` | provider calls, scheduling |
| Plugin runtime | `AutomaticSyncScheduler` (`src/runtime/AutomaticSyncScheduler.ts`) | the automatic-sync interval timer and its lifecycle registration | settings, `AutomaticSyncService`, Obsidian `registerInterval` | sync execution, change classification |
| Boundary | `SyncWorkspace` | application-to-sync execution boundary | `SyncManager`, refresh service, diff service | Source Control rendering |
| Sync domain | `SyncManager` | compatibility/domain facade for sync operations | coordinators, executors, metadata/status services | Source Control UI state |
| Sync domain | `PushCoordinator` | batch push use case including planning/conflict/review/commit coordination | planner, conflict resolver, push executor | Source Control selection state |
Expand Down Expand Up @@ -108,15 +111,42 @@ SyncIntentExecutor
resolve current ChangeId + revalidate explicit action
build one merged Sync Plan
build one merged Sync plan
confirm once
execution policy (interactive: confirm once / background: auto-accept, skip conflicts)
SyncWorkspace
├─ remote mutation bucket (max one provider batch)
└─ local pull bucket
```

### Automatic sync flow

```text
AutomaticSyncScheduler (timer, plugin runtime)
AutomaticSyncService.runOnce()
SourceControlActionService.runBackground() ← try-acquire shared SyncExecutionGuard
├─ busy → skip the whole tick (no refresh, no planning, no mutation, no queue)
└─ held for the whole transaction:
refresh authoritative local + remote state
ChangeRepository → exclude synced/conflict → default intents via ChangeActionPolicy
↓ (no intents → stop; exactly one refresh)
session.sync(intents) → SyncIntentExecutor.executeHeld (background: skip-conflict planning, no confirmation)
SyncWorkspace → Sync domain → provider
report SyncExecutionOutcome failures via onError (logger), never a Notice
refresh status once more
```

Refresh counts: busy tick 0, idle / only synced+conflict 1, executed run 2.

Automatic sync reuses the same application → `SyncWorkspace` → domain → provider path as manual Sync. It does not own classification, rename detection, action routing, conflict algorithms, push/pull planning, or provider mutation logic, and it never calls a concrete provider service directly.

## 4. Architecture rules

### MUST
Expand All @@ -129,6 +159,9 @@ SyncWorkspace
- One Sync Queue action must produce one merged review/confirmation flow.
- Remote mutations from one Sync Queue execution must be grouped into at most one provider mutation batch when supported by the current workflow.
- Existing compatibility identifiers such as `sync-status-view` and `open-sync-status` must be preserved unless a migration explicitly removes them.
- Automatic sync must execute through `SourceControlActionService`/`SyncIntentExecutor`/`SyncWorkspace`, never a second sync engine.
- Manual and automatic provider mutations must be serialized (`SyncExecutionGuard`); automatic work skips a tick rather than queuing when the path is busy.
- Interactive vs background behavior must be an explicit per-execution policy (`SyncExecutionMode` / `PushConflictBehavior`), not a collection of independent booleans.

### MUST NOT

Expand Down
20 changes: 20 additions & 0 deletions docs/source-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ SourceControlItemView
talks directly to a provider or bypasses the workspace to reach sync-domain
coordinators/executors.

## Automatic sync

`AutomaticSyncService` is the application-level use case for one automatic run
(refresh → read `ChangeRepository` → default intents → execute in background →
refresh). It reuses this same call chain and the same `SyncWorkspace` boundary;
it never reaches a provider or coordinator directly.

- Manual `sync()` stays `interactive`: one merged plan, one confirmation, batch
conflict interaction, and a result notice.
- Automatic runs pass `background`: the same plan is built and validated, but
conflicts are skipped (`PushConflictBehavior = 'skip'`) instead of prompting,
the final plan is auto-accepted, and success is silent.
- `PushCoordinator` stays UI-free: it only receives the small conflict-behavior
switch, never Obsidian or an execution mode.
- Conflicting paths left out of the plan are never marked as operation success;
they remain conflicts after the final refresh.
- `SyncExecutionGuard` serializes provider mutations: manual work waits,
automatic work try-acquires and skips when busy. Timer mechanics live in
`AutomaticSyncScheduler` (plugin runtime), not in the service.

## Sync Queue invariant

One Sync click produces one explicit-intent workflow. Requested action
Expand Down
3 changes: 3 additions & 0 deletions e2e-tests/provider/suites/sync-manager.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ function makeSettings(branch: string): GitLabFilesPushSettings {
bannerDismissedVersion: '',
language: 'system',
autoRefreshOnStartup: true,
automaticSyncEnabled: false,
automaticSyncIntervalMinutes: 5,
automaticSyncOnStartup: false,
};
}

Expand Down
3 changes: 3 additions & 0 deletions e2e-tests/provider/support/sync-manager-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ export async function createSyncManagerFixture(options: SyncManagerFixtureOption
bannerDismissedVersion: '',
language: 'system',
autoRefreshOnStartup: true,
automaticSyncEnabled: false,
automaticSyncIntervalMinutes: 5,
automaticSyncOnStartup: false,
};
}

Expand Down
1 change: 1 addition & 0 deletions eslint.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export default tseslint.config(
files: [
"tests/ui/SettingsConnectionStatus.test.ts",
"tests/ui/SettingsObsidian113Compatibility.test.ts",
"tests/ui/SettingsAutomaticSync.test.ts",
],
rules: {
"@typescript-eslint/no-deprecated": "off",
Expand Down
10 changes: 9 additions & 1 deletion feature_list.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
{
"_note": "GitHub Issues (firstsun-dev/git-files-sync, Project #6) is the source of truth for the full backlog and priority/estimate fields. This file mirrors only the active feature and the next few candidates so an agent session has a local, offline checkpoint — sync it against `gh issue list --repo firstsun-dev/git-files-sync --state open` at the start of a session rather than treating it as authoritative.",
"_lastSync": "2026-08-31: Synced against open GitHub issues; issue #143 is active locally, while #139 awaits review on PR #140.",
"_lastSync": "2026-09-21: Issue #141 (Automatic Syncing) merged into claude/mobile-source-control-density (combined PR #156); #143/#139 entries below are carried over from the base branch history.",
"features": [
{
"id": "feat-029",
"name": "feat(sync): automatic scheduled sync (issue #141)",
"description": "Scheduled + optional startup automatic sync that reuses the Source Control execution path in a background policy, skipping conflicts safely.",
"dependencies": [],
"status": "in-review",
"evidence": "Combined PR #156 on claude/mobile-source-control-density; eslint 0 errors, build passed, vitest 82 files / 1035 tests passed; all user-triggered provider mutations share the one SyncExecutionGuard."
},
{
"id": "feat-027",
"name": "test(e2e): run disposable Gitea safely in local and CI environments (issue #139)",
Expand Down
43 changes: 32 additions & 11 deletions progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,48 @@ Completed work is archived in [archive/](./archive/), one file per calendar mont

## Current State

**Last Updated:** 2026-09-01
**Active Feature:** PR2 responsibility cleanup, item 5 done — provider contract cleanup, partial (no tracked issue number; an ad-hoc follow-up plan on top of `origin/1.6.1`, not in `feature_list.json`).
**Branch / PR:** `claude/pr2-source-control-boundary`, branched from `origin/1.6.1` (commit `69e5540`). Pushed; opened as [PR #154](https://github.com/firstsun-dev/git-files-sync/pull/154) against `1.6.1` (covers items 1-4; item 5 below lands as a follow-up commit on the same branch/PR).
**Last Updated:** 2026-09-21
**Active Feature:** Issue #141 — Automatic Syncing (v1.7.0). Implementation complete; PR #156 reviewed, CI green at `12c213d` (run 35562916807).
**Branch / PR:** `claude/mobile-source-control-density` / PR #156. #156 is now a combined PR: Mobile Source Control density (CSS + structural tests) **and** Automatic Sync (#141, merged in via #159). It is no longer CSS-only. semantic-release owns the 1.7.0 bump.

**Scope (item 5, per the PR2 plan):** Moved `ConnectionTestResult` out of `git-service-base.ts` into `git-service-interface.ts` — it's a contract type consumed by `GitServiceInterface.testConnection`, so it belongs with the interface, not the base implementation class. `git-service-base.ts` now imports it back for its own `abstract testConnection` signature; `github-service.ts`/`gitlab-service.ts`/`gitea-service.ts`/`main.ts`/`GitLabSyncSettingTab.ts`/`tests/ui/SettingsConnectionStatus.test.ts` updated to import from the new location. Reviewed `updateConfig(...args: unknown[])` on `GitServiceInterface` per the plan's ask, but did **not** convert it to a typed discriminated union: every actual call site (`main.ts` `initializeGitService()`, 3 branches) already calls `updateConfig` on the concrete class (`GitLabService`/`GiteaService`/`GitHubService`), never through the loose interface type, so the untyped signature isn't causing a real type-safety gap today. A discriminated union would mean reshaping the interface, all three services' `updateConfig` bodies, and all three `main.ts` call sites into config-object form for no functional benefit — exactly the "touches too much, leave for later" case the plan calls out, so left as-is.
**What landed (#141):** persisted `automaticSyncEnabled` / `automaticSyncIntervalMinutes` / `automaticSyncOnStartup` (defaults OFF / 5 / OFF, interval min 1); settings UI rows distinct from the existing `autoRefreshOnStartup`; EN/zh-TW/zh-CN strings; `AutomaticSyncService` (refresh → repository → default intents → background execute → refresh) wired through `createSyncRuntime`; `AutomaticSyncScheduler` in plugin runtime; `SyncExecutionMode` per-execution policy with `PushConflictBehavior = 'skip'` at the `PushCoordinator` planning boundary; `SyncExecutionGuard` serialization; startup sync that never opens Source Control and supersedes the legacy startup refresh; hand-curated 1.7.0 What's New entry.

**Next:** PR2 plan is now fully worked through (items 1-5). Nothing further planned here; watch PR #154 for review feedback.
**Review fixes (2026-09-21):** background failures now reach `onError` via `SyncExecutionOutcome`; a busy tick is skipped before any refresh/planning via `SourceControlActionService.runBackground` (one shared `SyncExecutionGuard`, held across refresh → execute → refresh); the redundant second refresh on an idle vault is gone (idle/synced+conflict-only = 1 refresh, executed run = 2, busy = 0).

Below that: the previous "Outstanding Items"/"Verification Evidence" entries track separate, still-open work on PR #129 / `claude/source-control-foundation`, Issue #143, and `claude/fix-source-control-explicit-sync-intent` — not superseded by this entry, carried over from the base branch history.
**Next:** merge #156 once CI is green on the final e2e-cleanup fix head. Manual Obsidian verification not performed in this environment (no executable Obsidian runtime) — checklist is in the PR body.

- `npx eslint .` — 0 errors.
- `npx vitest run` — 76 files / 953 tests passed (unchanged count; pure type-relocation, no new tests needed).
- `npm run build` (tsc + Obsidian 1.11.0 compat typecheck + esbuild) — passed.
Below that: the previous "Outstanding Items"/"Verification Evidence" entries track separate, still-open work on PR #129 / `claude/source-control-foundation`, Issue #143, and `claude/fix-source-control-explicit-sync-intent` — not superseded by this entry, carried over from the base branch history.

## Outstanding Items

1. Run `npm run test:e2e -- --provider github`, `gitlab`, and `gitea` with provisioned credentials; verify mixed-100 remains under 120s (target <30s) and the provider matrix passes.
2. Commit and push the current working tree, then monitor the CI provider matrix.
1. Confirm CI is green on the head containing the e2e-harness cleanup fix (no persisted run state ⇒ network-free cleanup), then merge #156.
2. Manual Obsidian runtime verification (checklist in the PR body) — no executable Obsidian in this environment.

## Verification Evidence

2026-09-21 e2e cleanup fix (`scripts/e2e-harness.sh cleanup` is network-free when provisioning never wrote `e2e.env`; previously `load_env_file` → `normalize_env` could repeat the GitLab `curl` and mask the original failure):

- Fake-`curl`/`git`/`docker` shell check: gitlab + empty workdir → exit 0, 0 curl/fetch/push calls; state file without branch → exit 0, nothing deleted; github state + branch + clone → reaches `git push origin :refs/heads/<branch>`; gitea cleanup unchanged.
- Provider E2E (GitHub/GitLab/Gitea) + Required Checks green on `12c213d` (run 35562916807; 82 files / 1035 tests, Node 22 + 24).

2026-09-21 review fixes (manual-mutation serialization via `SourceControlActionService.runManual`, e2e cleanup unbound-var fix):

- `npx eslint .` — 0 errors. `npm run build` — passed. `npx vitest run` — 82 files / 1035 tests passed (new: `tests/logic/source-control/ManualSerialization.test.ts`).

2026-09-21 review fixes (Automatic Sync observability / busy skip / single refresh):

- `npx eslint .` — 0 errors. `npm run build` (tsc + Obsidian 1.11.0 compat + esbuild) — passed. `npx vitest run` — 81 files / 1030 tests passed (new: `tests/logic/source-control/AutomaticSyncIntegration.test.ts`, 14 tests over the real action service).
- Provider E2E (`npm run test:e2e`) **not run**: no provider credentials in this environment.

Earlier session (Issue #141 — Automatic Syncing; since merged into `claude/mobile-source-control-density` / combined PR #156 via #159):

- `npx eslint .` — 0 errors, 0 warnings.
- `npm run build` (tsc + Obsidian 1.11.0 compat typecheck + esbuild) — passed.
- `npx vitest run` — 80 files / 1010 tests passed (baseline from the parent branch was 76 files / 958; +41 new Automatic Sync tests across settings, execution policy, service, guard, scheduler, startup, i18n, settings UI, and changelog, plus settings-literal updates).
- New coverage: `tests/logic/source-control/AutomaticSyncService.test.ts`, `tests/logic/source-control/SyncExecutionGuard.test.ts`, `tests/runtime/AutomaticSyncScheduler.test.ts`, `tests/ui/SettingsAutomaticSync.test.ts`; extended `tests/logic/sync/PushCoordinator.test.ts` (prompt vs skip), `tests/logic/source-control/SourceControlActionService.test.ts` (background mode, skipped-conflict OperationState edge case, manual-vs-background serialization), `tests/main.test.ts` (startup decision), `tests/runtime/createSyncRuntime.test.ts`, `tests/settings.test.ts`, `tests/i18n/index.test.ts`, `tests/changelog.test.ts`, `tests/ui/SettingsConnectionStatus.test.ts`.
- **Not verified in this environment:** manual Obsidian runtime verification (no executable Obsidian environment) and the real-provider E2E suite. The manual checklist is included in the #141 PR body.
- semantic-release owns the actual 1.7.0 version bump; `manifest.json`/`package.json`/`versions.json`/generated `CHANGELOG.md` were intentionally not hand-edited.

This session (explicit per-file sync actions, 7 commits on `claude/fix-source-control-explicit-sync-intent`):

- Each commit individually verified before being made: `npx eslint .` (0 errors), `npx vitest run` (68 files, growing from 892 to 914 tests across the branch), `npm run build` (tsc + Obsidian 1.11.0 compat typecheck + esbuild) — all passed at every commit.
Expand Down
Loading
Loading