Point sync at the consolidated utils/syncs monorepos
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run

gatelock and diet-guard-sync were folded into shared monorepos:
gatelock -> github.com/kuhyx/utils (subdirectory), diet-guard-sync ->
github.com/kuhyx/syncs (subdirectory). Update the gatelock pip URL,
SYNC_REPO_NAME, the devices-dir path prefix (PC and phone), and add a
persisted-value migration so an already-configured phone 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:12:16 +02:00
parent c43e37b09d
commit a121da211d
12 changed files with 47 additions and 23 deletions

View File

@ -92,7 +92,7 @@ repos:
additional_dependencies:
- pytest
- requests
- gatelock @ git+https://github.com/kuhyx/gatelock@v0.1.0
- gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.0#subdirectory=gatelock
# Test suites intentionally use patterns (protected-access, magic-value
# comparisons) that don't belong in the source-code 10/10 gate.
exclude: ^(\.venv/)|(^|/)(tests/|conftest\.py)

View File

@ -34,7 +34,7 @@ rather than in the unit file.
`diet-guard-sync.timer` fires `python -m diet_guard sync` every ~15 minutes
(headless, no `DISPLAY` needed — separate from the gate timer on purpose, see
the unit file's comment for why). It pulls every other device's pushed log
from the private `kuhyx/diet-guard-sync` GitHub repo (used as dumb file
from the private `kuhyx/syncs` GitHub repo (under `diet-guard-sync/`, used as dumb file
storage via the REST Contents API, not a git clone), merges with the local
log (`_sync_merge.merge_logs`: union by `id`, tombstone wins, legacy
`(time, desc)` dedup for pre-`id` entries), **re-signs every persisted entry**
@ -48,7 +48,7 @@ skipping the re-sign would silently lose every phone-logged meal on the very
next read.
Requires a one-time manual setup `install.sh` does **not** automate: create a
fine-grained GitHub PAT scoped to `diet-guard-sync`'s contents (read/write),
fine-grained GitHub PAT scoped to `syncs`'s contents (read/write),
then save it to `~/.config/diet_guard/sync_token`, mode 600. Until that file
exists, every sync tick is a harmless no-op that logs `sync not configured`.
@ -100,7 +100,7 @@ silently does **not** reach the running service.
cross-repo file coupling (unlike wake_alarm, which reads
`~/screen-locker/screen_locker/workout_log.json`). Safe to reason about in
isolation, with one exception: `diet-guard-sync.timer` reads/writes the
private `kuhyx/diet-guard-sync` GitHub repo (see "Cross-device sync" above)
private `kuhyx/syncs` GitHub repo (see "Cross-device sync" above)
and `~/.config/diet_guard/sync_token`.
## Commands

View File

@ -258,7 +258,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 8),
Text(
'Authorize in your browser — no token to paste. Syncs to '
'kuhyx/diet-guard-sync by default.',
'kuhyx/syncs by default.',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
@ -395,7 +395,7 @@ class _ClientIdSetupDialogState extends State<_ClientIdSetupDialog> {
const SizedBox(height: 12),
const Text(
'When you connect below, log in with the GitHub account that '
'has write access to kuhyx/diet-guard-sync.',
'has write access to kuhyx/syncs.',
),
const SizedBox(height: 16),
TextField(

View File

@ -63,7 +63,7 @@ class GitHubClient {
/// The repo owner/org (e.g. `"kuhyx"`).
final String owner;
/// The repo name (e.g. `"diet-guard-sync"`).
/// The repo name (e.g. `"syncs"`).
final String repo;
/// The branch to read/write against.

View File

@ -19,11 +19,12 @@ import 'package:diet_guard_app/services/github_client.dart';
import 'package:diet_guard_app/services/log_storage_service.dart';
import 'package:diet_guard_app/services/sync_merge.dart';
const _devicesDir = 'devices';
const _devicesDir = 'diet-guard-sync/devices';
/// This device's id in the `devices/<id>/food_log.json` layout. The PC
/// pushes under `pc` (`SYNC_DEVICE_ID` in `diet_guard/_constants.py`); the
/// phone is the only other device in this design.
/// This device's id in the `diet-guard-sync/devices/<id>/food_log.json`
/// layout. The PC pushes under `pc` (`SYNC_DEVICE_ID` in
/// `diet_guard/_constants.py`); the phone is the only other device in this
/// design.
const phoneDeviceId = 'phone';
String _deviceLogPath(String deviceId) =>

View File

@ -26,7 +26,7 @@ class SyncSettings {
/// The repo owner/org (e.g. `"kuhyx"`).
final String owner;
/// The repo name (e.g. `"diet-guard-sync"`).
/// The repo name (e.g. `"syncs"`).
final String repo;
/// A GitHub PAT with contents read/write on [owner]/[repo].
@ -66,15 +66,25 @@ class SyncSettings {
/// path on Android and use libsecret on Linux.
static const _secure = FlutterSecureStorage();
/// Loads settings, defaulting the owner/repo to `kuhyx/diet-guard-sync`
/// Loads settings, defaulting the owner/repo to `kuhyx/syncs`
/// (matching the PC's `SYNC_REPO_OWNER`/`SYNC_REPO_NAME` constants) and the
/// client id to the baked-in [defaultClientId], so a fresh install needs
/// only "Connect GitHub" (once an OAuth App is registered) or a pasted PAT.
///
/// A device that already persisted the old standalone `diet-guard-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 == 'diet-guard-sync') {
repo = 'syncs';
await prefs.setString(_kRepo, repo);
}
return SyncSettings(
owner: prefs.getString(_kOwner) ?? 'kuhyx',
repo: prefs.getString(_kRepo) ?? 'diet-guard-sync',
repo: repo,
token: await _loadToken(prefs),
clientId: prefs.getString(_kClientId) ?? defaultClientId,
);

View File

@ -83,7 +83,7 @@ void main() {
}
}
testWidgets('shows the kuhyx/diet-guard-sync defaults on a fresh install', (
testWidgets('shows the kuhyx/syncs defaults on a fresh install', (
tester,
) async {
await tester.runAsync(() async {
@ -91,7 +91,7 @@ void main() {
await settle(tester);
expect(find.widgetWithText(TextField, 'kuhyx'), findsOneWidget);
expect(find.widgetWithText(TextField, 'diet-guard-sync'), findsOneWidget);
expect(find.widgetWithText(TextField, 'syncs'), findsOneWidget);
});
});

View File

@ -10,18 +10,31 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test(
'load returns the kuhyx/diet-guard-sync defaults on a fresh install',
'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, 'diet-guard-sync');
expect(s.repo, 'syncs');
expect(s.token, '');
expect(s.clientId, SyncSettings.defaultClientId);
},
);
test(
'load migrates a persisted old diet-guard-sync repo value to syncs',
() async {
SharedPreferences.setMockInitialValues({'sync.repo': 'diet-guard-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({});
installFakeSecureStorage();

View File

@ -70,7 +70,7 @@ GATE_LOCK_FILE: Path = DATA_DIR / ".gate.lock"
# full current log as one file under devices/<id>/food_log.json; merging
# happens client-side (see _sync_merge.py), never via git.
SYNC_REPO_OWNER: str = "kuhyx"
SYNC_REPO_NAME: str = "diet-guard-sync"
SYNC_REPO_NAME: str = "syncs"
SYNC_DEVICE_ID: str = "pc"
# A fine-grained GitHub PAT, scoped to just SYNC_REPO_NAME's contents. The
# user creates this once via github.com (see CLAUDE.md) and saves it here,

View File

@ -24,7 +24,7 @@ from diet_guard._sync_merge import merge_logs
_logger = logging.getLogger(__name__)
_DEVICES_DIR = "devices"
_DEVICES_DIR = "diet-guard-sync/devices"
class SyncError(Exception):
@ -46,7 +46,7 @@ def _read_token() -> str:
if not SYNC_TOKEN_FILE.exists():
message = (
f"no sync token at {SYNC_TOKEN_FILE} -- create a fine-grained "
"GitHub PAT scoped to the diet-guard-sync repo's contents and "
"GitHub PAT scoped to the syncs repo's contents and "
f"save it there (mode 600), then re-run sync"
)
raise SyncError(message)

View File

@ -4,7 +4,7 @@ version = "1.0.0"
description = "Tkinter/systemd log-to-unlock meal gate with calorie/macro budget tracking"
requires-python = ">=3.10"
dependencies = [
"gatelock @ git+https://github.com/kuhyx/gatelock@v0.1.0",
"gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.0#subdirectory=gatelock",
"requests>=2.31.0",
]

View File

@ -3,7 +3,7 @@
bandit>=1.7.0
codespell>=2.2.0
coverage>=7.4.0
gatelock @ git+https://github.com/kuhyx/gatelock@v0.1.0
gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.0#subdirectory=gatelock
mypy>=1.8.0
pre-commit>=3.6.0
pylint>=3.0.0