Skip to content
Merged
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
36 changes: 33 additions & 3 deletions lib/src/application/client/drive/drive_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<SyncOutcome> _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(
Expand All @@ -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'];
Expand Down
8 changes: 7 additions & 1 deletion lib/src/application/client/drive/drive_rpc_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
33 changes: 21 additions & 12 deletions lib/src/application/node/node_drive_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -261,14 +261,26 @@ class NodeDriveService {
}

Future<DriveMessage> _gitSync(int id, Map<String, dynamic> 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;
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion lib/src/version.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down
40 changes: 40 additions & 0 deletions test/integration/drive_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading