Point notes sync at the consolidated syncs monorepo

todo-sync, diet-guard-sync, and screen-locker-sync were folded into
one private github.com/kuhyx/syncs monorepo, each keeping its own
subdirectory as the Contents-API path prefix. Update the default
owner/repo, the changeset path prefix, and add a persisted-value
migration so an already-installed device that saved the old
'todo-sync' repo name picks up 'syncs' on next load.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-07-10 14:04:05 +02:00
parent 6398ce9275
commit 9edf603a24
9 changed files with 51 additions and 29 deletions

View File

@ -6,8 +6,9 @@ and sync peer-to-peer through a GitHub repo used as dumb storage.
- Package name: `todo` (Dart SDK `^3.12.2`).
- Git remote: `origin` → `github.com/kuhyx/todo-app`.
- The note **content** also syncs to a separate private repo `kuhyx/todo-sync`
via the in-app GitHub sync (changeset files); that is data, not this codebase.
- The note **content** also syncs to a separate private repo `kuhyx/syncs`
(under `todo-sync/`) via the in-app GitHub sync (changeset files); that is
data, not this codebase.
## Git workflow (repo-specific — overrides global rules)
@ -42,7 +43,8 @@ Three layers under `lib/`, each single-purpose:
AND-combined), `NoteSort`, `watchNotes`/`watchCount` streams, and
`importNotes` (safe newer-wins merge by id).
- `sync/` — GitHub-as-storage sync.
- `sync_service.dart`: each device owns one file `changesets/<nodeId>.json`
- `sync_service.dart`: each device owns one file
`todo-sync/changesets/<nodeId>.json`
holding its full CRDT changeset. No two devices write the same file ⇒ no
git merge conflicts; convergence is the CRDT layer's job (pull every other
device's changeset and `merge()`, which is commutative + idempotent).

View File

@ -22,14 +22,15 @@ class SyncResult {
/// Synchronises a [NoteRepository] with a GitHub repo used as dumb storage.
///
/// Design: each device owns exactly one file, `changesets/<nodeId>.json`,
/// Design: each device owns exactly one file,
/// `todo-sync/changesets/<nodeId>.json`,
/// holding its full CRDT changeset. Because no two devices ever write the
/// same file, there are **no git-level merge conflicts**. Data convergence
/// is handled entirely by the CRDT layer: pulling every other device's
/// changeset and [NoteRepository.merge]-ing it is commutative and
/// idempotent, so repeated syncs in any order converge to the same state.
class SyncService {
const SyncService({this.changesetDir = 'changesets'});
const SyncService({this.changesetDir = 'todo-sync/changesets'});
/// Directory in the repo under which per-device changeset files live.
final String changesetDir;

View File

@ -52,14 +52,24 @@ class SyncSettings {
/// path on Android and use libsecret on Linux.
static const _secure = FlutterSecureStorage();
/// Loads settings, defaulting the repo to `kuhyx/todo-sync` and the client id
/// Loads settings, defaulting the repo to `kuhyx/syncs` and the client id
/// to the baked-in [defaultClientId] so first run (and any reinstall) needs
/// nothing but a single "Connect GitHub" tap.
///
/// A device that already persisted the old standalone `todo-sync` repo
/// name (before it was folded into the shared `syncs` monorepo) is
/// migrated to `syncs` here and the corrected value is written straight
/// back to prefs, so this only ever runs once per device.
static Future<SyncSettings> load() async {
final prefs = await SharedPreferences.getInstance();
var repo = prefs.getString(_kRepo) ?? 'syncs';
if (repo == 'todo-sync') {
repo = 'syncs';
await prefs.setString(_kRepo, repo);
}
return SyncSettings(
owner: prefs.getString(_kOwner) ?? 'kuhyx',
repo: prefs.getString(_kRepo) ?? 'todo-sync',
repo: repo,
token: await _loadToken(prefs),
clientId: prefs.getString(_kClientId) ?? defaultClientId,
);

View File

@ -250,7 +250,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 4),
Text(
'Authorize in your browser — no token to paste. Syncs to '
'kuhyx/todo-sync by default.',
'kuhyx/syncs by default.',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),

View File

@ -78,7 +78,7 @@ void main() {
WidgetTester tester, {
SyncSettings initial = const SyncSettings(
owner: 'kuhyx',
repo: 'todo-sync',
repo: 'syncs',
token: 't',
),
http.Client? httpClient,

View File

@ -43,7 +43,7 @@ void main() {
{
'type': 'file',
'name': otherFile,
'path': 'changesets/$otherFile',
'path': 'todo-sync/changesets/$otherFile',
'sha': 'sha-other',
},
]);
@ -54,7 +54,7 @@ void main() {
putCount++;
return http.Response('{}', 200);
}
if (req.url.path.endsWith('/contents/changesets')) {
if (req.url.path.endsWith('/contents/todo-sync/changesets')) {
return http.Response(listResponse, 200); // directory listing
}
return http.Response(fileResponse, 200); // the other device's file
@ -122,7 +122,7 @@ void main() {
{
'type': 'file',
'name': ownFile,
'path': 'changesets/$ownFile',
'path': 'todo-sync/changesets/$ownFile',
'sha': 'own-sha-123',
},
]);

View File

@ -9,19 +9,26 @@ void main() {
// binding up first (widget tests get this for free via testWidgets).
TestWidgetsFlutterBinding.ensureInitialized();
test(
'load returns the kuhyx/todo-sync defaults on a fresh install',
() async {
SharedPreferences.setMockInitialValues({});
installFakeSecureStorage();
final s = await SyncSettings.load();
expect(s.owner, 'kuhyx');
expect(s.repo, 'todo-sync');
expect(s.token, '');
// Client id defaults to the baked-in OAuth App id (one-tap connect).
expect(s.clientId, SyncSettings.defaultClientId);
},
);
test('load returns the kuhyx/syncs defaults on a fresh install', () async {
SharedPreferences.setMockInitialValues({});
installFakeSecureStorage();
final s = await SyncSettings.load();
expect(s.owner, 'kuhyx');
expect(s.repo, 'syncs');
expect(s.token, '');
// Client id defaults to the baked-in OAuth App id (one-tap connect).
expect(s.clientId, SyncSettings.defaultClientId);
});
test('load migrates a persisted old todo-sync repo value to syncs', () async {
SharedPreferences.setMockInitialValues({'sync.repo': 'todo-sync'});
installFakeSecureStorage();
final s = await SyncSettings.load();
expect(s.repo, 'syncs');
final prefs = await SharedPreferences.getInstance();
expect(prefs.getString('sync.repo'), 'syncs');
});
test('save stores the token in the keystore, not in prefs', () async {
SharedPreferences.setMockInitialValues({});

View File

@ -12,7 +12,7 @@ import 'package:http/http.dart' as http;
const _clientId = 'Ov23li9tF2R46PqzJgch';
const _scope = 'repo';
const _owner = 'kuhyx';
const _repo = 'todo-sync';
const _repo = 'syncs';
Future<void> main() async {
final code = await http.post(

View File

@ -24,9 +24,9 @@ Future<void> main() async {
}
// Throwaway directory so we never pollute the real `changesets/`.
const service = SyncService(changesetDir: 'changesets_smoketest');
const service = SyncService(changesetDir: 'todo-sync/changesets_smoketest');
GitHubClient client() =>
GitHubClient(owner: 'kuhyx', repo: 'todo-sync', token: token);
GitHubClient(owner: 'kuhyx', repo: 'syncs', token: token);
final deviceA = await NoteRepository.openInMemory();
final deviceB = await NoteRepository.openInMemory();
@ -59,7 +59,9 @@ Future<void> main() async {
// Cleanup: remove the throwaway changeset files.
final cleanup = client();
for (final f in await cleanup.listDirectory('changesets_smoketest')) {
for (final f in await cleanup.listDirectory(
'todo-sync/changesets_smoketest',
)) {
await cleanup.deleteFile(f.path, f.sha, message: 'smoke test cleanup');
}
stdout.writeln('Cleaned up throwaway changeset files.');