mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 19:27:01 +01:00
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:
parent
6398ce9275
commit
9edf603a24
@ -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).
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -78,7 +78,7 @@ void main() {
|
||||
WidgetTester tester, {
|
||||
SyncSettings initial = const SyncSettings(
|
||||
owner: 'kuhyx',
|
||||
repo: 'todo-sync',
|
||||
repo: 'syncs',
|
||||
token: 't',
|
||||
),
|
||||
http.Client? httpClient,
|
||||
|
||||
@ -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',
|
||||
},
|
||||
]);
|
||||
|
||||
@ -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({});
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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.');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user