diff --git a/CHANGELOG.md b/CHANGELOG.md index fc89cc3..9e6a6ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## 1.54.0 + +### Changed + +- **Git-drive sync pushes the node's unpushed commits to their own branch, and + auto-sync pushes before pulling.** Using omnydrive 1.11.0's pluggable push + policy, the node protects `main`/`master` **and the branch the drive was mounted + to track** (those still publish to a fresh feature branch), while a branch the + node created — or one not yet on the origin — is pushed to **directly** (created + on the origin if absent; a diverged origin surfaces a conflict, never a force). + A read-write **auto** sync now **pushes first, then pulls** so local commits + reach the origin before reconciling. The client threads the mounted branch to + the node so the policy can protect it. + ## 1.53.0 ### Added diff --git a/lib/src/application/client/drive/drive_manager.dart b/lib/src/application/client/drive/drive_manager.dart index 470eadd..336f741 100644 --- a/lib/src/application/client/drive/drive_manager.dart +++ b/lib/src/application/client/drive/drive_manager.dart @@ -664,9 +664,38 @@ class DriveManager { SyncDirection? direction, { DriveProgress? onProgress, }) async { - final dir = - direction ?? - (record.readWrite ? SyncDirection.push : SyncDirection.pull); + // Auto (no explicit direction) on a read-write mount pushes the node's + // unpushed commits first, then pulls to reconcile. An explicit direction — + // or a read-only mount — runs a single pass. + if (direction == null && record.readWrite) { + final pushed = await _gitSyncOnce( + record, + rpc, + SyncDirection.push, + onProgress: onProgress, + ); + if (pushed.conflict != null) return pushed; + return _gitSyncOnce( + pushed.record, + rpc, + SyncDirection.pull, + onProgress: onProgress, + ); + } + return _gitSyncOnce( + record, + rpc, + direction ?? SyncDirection.pull, + onProgress: onProgress, + ); + } + + Future _gitSyncOnce( + MountRecord record, + DriveRpcClient rpc, + SyncDirection dir, { + DriveProgress? onProgress, + }) async { // The node runs git atomically over a single RPC, so per-file events are // not available here; emit a coarse phase so a live bar still animates. _emit( @@ -678,6 +707,7 @@ class DriveManager { url: record.gitUrl!, direction: dir.wireValue, baseline: record.syncState.baselineRef.value, + mountBranch: record.gitBranch, ); _emit(onProgress, ProgressPhase.done, 'synced'); final conflict = reply['conflict']; diff --git a/lib/src/application/client/drive/drive_rpc_client.dart b/lib/src/application/client/drive/drive_rpc_client.dart index 59d2490..6e97567 100644 --- a/lib/src/application/client/drive/drive_rpc_client.dart +++ b/lib/src/application/client/drive/drive_rpc_client.dart @@ -172,10 +172,16 @@ class DriveRpcClient { required String url, required String direction, required String baseline, + String? mountBranch, }) async { final reply = await _call( DriveOp.gitSync, - fields: {'url': url, 'direction': direction, 'baseline': baseline}, + fields: { + 'url': url, + 'direction': direction, + 'baseline': baseline, + 'mountBranch': ?mountBranch, + }, ); return reply.header; } diff --git a/lib/src/application/node/node_drive_service.dart b/lib/src/application/node/node_drive_service.dart index 97bd621..6ddbd0d 100644 --- a/lib/src/application/node/node_drive_service.dart +++ b/lib/src/application/node/node_drive_service.dart @@ -261,14 +261,26 @@ class NodeDriveService { } Future _gitSync(int id, Map h) async { - final drive = _gitDrive ??= - await GitProvider( - endpoint: EndpointId(endpointId), - credentials: gitCredentials, - ).describe( - OriginUri(h['url'] as String), - accessMode: _readWrite ? AccessMode.readWrite : AccessMode.readOnly, - ); + // Protect main/master and the branch the drive was mounted to track; any + // other (node-created) branch is pushed to directly. The rest — creating a + // feature branch for protected branches, pushing others as-is — is handled + // by omnydrive's GitPushPolicy. + final mountBranch = h['mountBranch'] as String?; + final provider = GitProvider( + endpoint: EndpointId(endpointId), + credentials: gitCredentials, + pushPolicy: DefaultGitPushPolicy( + protectedBranches: { + 'main', + 'master', + if (mountBranch != null && mountBranch.isNotEmpty) mountBranch, + }, + ), + ); + final drive = _gitDrive ??= await provider.describe( + OriginUri(h['url'] as String), + accessMode: _readWrite ? AccessMode.readWrite : AccessMode.readOnly, + ); final direction = (h['direction'] as String) == 'push' ? SyncDirection.push : SyncDirection.pull; @@ -282,10 +294,7 @@ class NodeDriveService { mountedAt: clock.now(), syncState: SyncState(baselineRef: baseline), ); - final sync = GitProvider( - endpoint: EndpointId(endpointId), - credentials: gitCredentials, - ).synchronizer(drive); + final sync = provider.synchronizer(drive); try { final plan = await sync.plan( mount: mount, diff --git a/lib/src/version.dart b/lib/src/version.dart index 005295d..9f4ddbf 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -3,4 +3,4 @@ /// This is the single source of truth for "what build is this": it is rendered /// in the CLI banner and is the default a node reports as its /// [NodeConfig.agentVersion] / [PlatformInfo.agentVersion]. -const String omnyShellVersion = '1.53.0'; +const String omnyShellVersion = '1.54.0'; diff --git a/pubspec.yaml b/pubspec.yaml index 340b87e..bd4c649 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ description: >- connect to a Hub by node identity (not host:port); the Hub authenticates, authorizes and brokers encrypted sessions to Nodes over WebSocket-on-TLS. Ships Hub, Node, Client and CLI implementations behind first-class Dart APIs. -version: 1.53.0 +version: 1.54.0 repository: https://github.com/OmnyGrid/omnyshell environment: @@ -22,7 +22,7 @@ dependencies: ffi: ^2.1.0 http: ^1.2.0 meta: ^1.18.3 - omnydrive: ^1.10.1 + omnydrive: ^1.11.0 path: ^1.9.0 pointycastle: ^4.0.0 #portable_pty: ^0.0.5 # waiting crash fix. diff --git a/test/integration/drive_test.dart b/test/integration/drive_test.dart index f5312e8..58a9a7d 100644 --- a/test/integration/drive_test.dart +++ b/test/integration/drive_test.dart @@ -878,6 +878,46 @@ void main() { expect(rec.currentBranch, (head.stdout as String).trim()); }); + test( + 'git drive auto-sync pushes then pulls (protected branch → feature branch)', + () async { + if (!await _gitAvailable()) { + markTestSkipped('git not installed'); + return; + } + final origin = Directory('${tmp.path}/origin')..createSync(); + await _git(['init', '-q', '-b', 'main'], origin.path); + await _git(['config', 'user.email', 't@example.com'], origin.path); + await _git(['config', 'user.name', 'Test'], origin.path); + File('${origin.path}/main.dart').writeAsStringSync('void main() {}\n'); + await _git(['add', '.'], origin.path); + await _git(['commit', '-q', '-m', 'init'], origin.path); + + await cluster.startNode(id: 'web-01'); + final client = await cluster.connectClient(); + final mgr = await DriveManager.open(client, home: home); + final rec = await mgr.mountGit( + url: origin.path, + nodeId: 'web-01', + remotePath: '${tmp.path}/clone', + readWrite: true, + ); + + // Auto sync: read-write pushes (main is protected → a feature branch) then + // pulls. It must complete without a conflict. + final out = await mgr.sync(rec.id); + expect(out.conflict, isNull); + + // The protected push published a feature branch on the origin. + final branches = await Process.run('git', [ + 'branch', + '--list', + 'omnydrive/*', + ], workingDirectory: origin.path); + expect((branches.stdout as String), contains('omnydrive/')); + }, + ); + test('unmount forgets the mount', () async { await cluster.startNode(id: 'web-01'); final client = await cluster.connectClient();