diff --git a/CHANGELOG.md b/CHANGELOG.md index d78571a..fc89cc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## 1.53.0 + +### Added + +- **Git drives show the current checked-out branch.** The node reports the working + tree's branch on clone/sync; it's persisted on the mount (`MountRecord.currentBranch`) + and shown in `omnyshell drive ls` / `drive status` (a `Branch:` line and `src@branch` + in the list) and the dashboard TUI (a `Branch` row in the mount detail and `@branch` + in the list). + +### Fixed + +- Git-drive **pull no longer crashes** when the node is on a branch that isn't on the + origin (via omnydrive 1.10.1): the pull no-ops when there's nothing to pull, and + otherwise fetches the branch by name and fast-forwards `FETCH_HEAD`. + ## 1.52.2 ### Changed diff --git a/bin/omnyshell.dart b/bin/omnyshell.dart index 3216775..6cb3e67 100644 --- a/bin/omnyshell.dart +++ b/bin/omnyshell.dart @@ -5223,9 +5223,10 @@ class DriveCommand extends Command { String _mountLine(MountRecord r) { final st = r.syncState; final src = r.isGit ? (r.gitUrl ?? 'git') : (r.localPath ?? '?'); + final branch = r.isGit ? '@${r.currentBranch ?? r.gitBranch ?? '?'}' : ''; final mode = r.readWrite ? 'rw' : 'ro'; return '${r.id.padRight(22)} ${st.status.wireValue.padRight(10)} ' - '$mode $src -> ${r.nodeId}:${r.remotePath}'; + '$mode $src$branch -> ${r.nodeId}:${r.remotePath}'; } class DriveMountCommand extends Command { @@ -5418,7 +5419,11 @@ class DriveStatusCommand extends Command { ..writeln( 'Kind: ${r.kind} (${r.readWrite ? 'read-write' : 'read-only'})', ) - ..writeln('Source: ${r.isGit ? r.gitUrl : r.localPath}') + ..writeln('Source: ${r.isGit ? r.gitUrl : r.localPath}'); + if (r.isGit) { + stdout.writeln('Branch: ${r.currentBranch ?? r.gitBranch ?? '?'}'); + } + stdout ..writeln('Target: ${r.nodeId}:${r.remotePath}') ..writeln('Status: ${st.status.wireValue}') ..writeln('Baseline: ${st.baselineRef}') diff --git a/lib/src/application/client/dashboard/dashboard_app.dart b/lib/src/application/client/dashboard/dashboard_app.dart index 04b6bd0..cd7ea98 100644 --- a/lib/src/application/client/dashboard/dashboard_app.dart +++ b/lib/src/application/client/dashboard/dashboard_app.dart @@ -2422,9 +2422,10 @@ class DashboardApp { /// A compact one-line description of a mount (mirrors the CLI's `_mountLine`). String _mountLine(MountRecord r) { final src = r.isGit ? (r.gitUrl ?? 'git') : (r.localPath ?? '?'); + final branch = r.isGit ? '@${r.currentBranch ?? r.gitBranch ?? '?'}' : ''; final mode = r.readWrite ? 'rw' : 'ro'; return '${_pad(r.id, 22)} ${_pad(r.syncState.status.wireValue, 10)} ' - '$mode $src -> ${r.nodeId}:${r.remotePath}'; + '$mode $src$branch -> ${r.nodeId}:${r.remotePath}'; } void _renderDriveDetail(ScreenBuffer s, Rect area) { @@ -2436,6 +2437,7 @@ class DashboardApp { ('Mount', m.id), ('Kind', '${m.kind} (${m.readWrite ? 'read-write' : 'read-only'})'), ('Source', m.isGit ? (m.gitUrl ?? '?') : (m.localPath ?? '?')), + if (m.isGit) ('Branch', m.currentBranch ?? m.gitBranch ?? '?'), ('Target', '${m.nodeId}:${m.remotePath}'), ('Status', st.status.wireValue), ('Synced', st.lastSyncedAt?.toIso8601String() ?? 'never'), diff --git a/lib/src/application/client/drive/drive_manager.dart b/lib/src/application/client/drive/drive_manager.dart index a5a07ad..470eadd 100644 --- a/lib/src/application/client/drive/drive_manager.dart +++ b/lib/src/application/client/drive/drive_manager.dart @@ -284,12 +284,13 @@ class DriveManager { record = await _withSession(record, (rpc) async { _emit(onProgress, ProgressPhase.transferring, 'cloning $url'); - final head = await rpc.gitClone(url, branch: branch, depth: depth); + final clone = await rpc.gitClone(url, branch: branch, depth: depth); _emit(onProgress, ProgressPhase.done, 'cloned'); return record.copyWith( + currentBranch: clone.branch, syncState: SyncState( - baselineRef: SyncRef.git(head), - currentRef: SyncRef.git(head), + baselineRef: SyncRef.git(clone.head), + currentRef: SyncRef.git(clone.head), status: SyncStatus.clean, lastSyncedAt: DateTime.now(), ), @@ -699,6 +700,7 @@ class DriveManager { } final head = reply['head'] as String; final updated = record.copyWith( + currentBranch: reply['branch'] as String?, syncState: SyncState( baselineRef: SyncRef.git(head), currentRef: SyncRef.git(head), @@ -1146,15 +1148,16 @@ class DriveManager { ProgressPhase.transferring, 'cloning ${record.gitUrl}', ); - final head = await rpc.gitClone( + final clone = await rpc.gitClone( record.gitUrl!, branch: record.gitBranch, ); _emit(onProgress, ProgressPhase.done, 'cloned'); return record.copyWith( + currentBranch: clone.branch, syncState: SyncState( - baselineRef: SyncRef.git(head), - currentRef: SyncRef.git(head), + baselineRef: SyncRef.git(clone.head), + currentRef: SyncRef.git(clone.head), status: SyncStatus.clean, lastSyncedAt: DateTime.now(), ), diff --git a/lib/src/application/client/drive/drive_rpc_client.dart b/lib/src/application/client/drive/drive_rpc_client.dart index 28270a3..59d2490 100644 --- a/lib/src/application/client/drive/drive_rpc_client.dart +++ b/lib/src/application/client/drive/drive_rpc_client.dart @@ -142,8 +142,9 @@ class DriveRpcClient { return reply.header['copied'] == true; } - /// Clones [url] into the served root, returning the resulting head SHA. - Future gitClone( + /// Clones [url] into the served root, returning the resulting head SHA and the + /// checked-out branch. + Future<({String head, String? branch})> gitClone( String url, { String? branch, int? depth, @@ -158,7 +159,10 @@ class DriveRpcClient { if (bare) 'bare': true, }, ); - return reply.header['head'] as String; + return ( + head: reply.header['head'] as String, + branch: reply.header['branch'] as String?, + ); } /// Synchronizes the served git tree in [direction] from [baseline]. Returns diff --git a/lib/src/application/client/drive/mount_store.dart b/lib/src/application/client/drive/mount_store.dart index 743175d..3a5fc3b 100644 --- a/lib/src/application/client/drive/mount_store.dart +++ b/lib/src/application/client/drive/mount_store.dart @@ -33,9 +33,15 @@ class MountRecord { /// The git repository URL (`git` kind), if any. final String? gitUrl; - /// The git branch to track (`git` kind), if any. + /// The git branch to track (`git` kind), if any — the branch requested at + /// mount time. final String? gitBranch; + /// The branch currently checked out on the node (`git` kind), observed from + /// the last clone/sync. May differ from [gitBranch] (e.g. after the node + /// publishes to a feature branch). Null until first observed. + final String? currentBranch; + /// Whether the node mirror is writable (changes can sync back). final bool readWrite; @@ -79,6 +85,7 @@ class MountRecord { this.localPath, this.gitUrl, this.gitBranch, + this.currentBranch, this.ephemeral = false, this.baselineManifest, PathFilter? filter, @@ -91,10 +98,12 @@ class MountRecord { AccessMode get accessMode => readWrite ? AccessMode.readWrite : AccessMode.readOnly; - /// Returns a copy with a new [syncState] and/or [baselineManifest]. + /// Returns a copy with a new [syncState], [baselineManifest] and/or + /// [currentBranch]. MountRecord copyWith({ SyncState? syncState, FileManifest? baselineManifest, + String? currentBranch, }) => MountRecord( id: id, nodeId: nodeId, @@ -109,6 +118,7 @@ class MountRecord { localPath: localPath, gitUrl: gitUrl, gitBranch: gitBranch, + currentBranch: currentBranch ?? this.currentBranch, ephemeral: ephemeral, filter: filter, ); @@ -122,6 +132,7 @@ class MountRecord { if (localPath != null) 'localPath': localPath, if (gitUrl != null) 'gitUrl': gitUrl, if (gitBranch != null) 'gitBranch': gitBranch, + if (currentBranch != null) 'currentBranch': currentBranch, 'readWrite': readWrite, if (ephemeral) 'ephemeral': true, 'driveId': driveId, @@ -142,6 +153,7 @@ class MountRecord { localPath: json['localPath'] as String?, gitUrl: json['gitUrl'] as String?, gitBranch: json['gitBranch'] as String?, + currentBranch: json['currentBranch'] as String?, readWrite: json['readWrite'] as bool? ?? false, ephemeral: json['ephemeral'] as bool? ?? false, driveId: json['driveId'] as String, diff --git a/lib/src/application/node/node_drive_service.dart b/lib/src/application/node/node_drive_service.dart index 4ba1a90..97bd621 100644 --- a/lib/src/application/node/node_drive_service.dart +++ b/lib/src/application/node/node_drive_service.dart @@ -178,7 +178,8 @@ class NodeDriveService { _reply(await _gitSync(id, msg.header)); case DriveOp.gitHead: final head = await git.revParse(_root); - _reply(DriveMessage.ok(id, fields: {'head': head})); + final branch = await git.currentBranch(_root); + _reply(DriveMessage.ok(id, fields: {'head': head, 'branch': branch})); default: _reply(DriveMessage.err(id, 'unknown op: ${msg.op}')); } @@ -255,7 +256,8 @@ class NodeDriveService { accessMode: _readWrite ? AccessMode.readWrite : AccessMode.readOnly, ); final head = await git.revParse(_root); - return DriveMessage.ok(id, fields: {'head': head}); + final branch = await git.currentBranch(_root); + return DriveMessage.ok(id, fields: {'head': head, 'branch': branch}); } Future _gitSync(int id, Map h) async { @@ -299,6 +301,7 @@ class NodeDriveService { id, fields: { 'head': result.newRef.value, + 'branch': await git.currentBranch(_root), 'applied': result.appliedChanges, if (result.publishedBranch != null) 'publishedBranch': result.publishedBranch, diff --git a/lib/src/version.dart b/lib/src/version.dart index 008c6e2..005295d 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.52.2'; +const String omnyShellVersion = '1.53.0'; diff --git a/pubspec.yaml b/pubspec.yaml index d427f1e..340b87e 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.52.2 +version: 1.53.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.0 + omnydrive: ^1.10.1 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 1ace80e..f5312e8 100644 --- a/test/integration/drive_test.dart +++ b/test/integration/drive_test.dart @@ -869,6 +869,13 @@ void main() { expect(rec.kind, 'git'); expect(rec.syncState.baselineRef.value, isNotEmpty); expect(File('${tmp.path}/clone/main.dart').existsSync(), isTrue); + // The node reports the checked-out branch, captured on the record. + final head = await Process.run('git', [ + 'rev-parse', + '--abbrev-ref', + 'HEAD', + ], workingDirectory: origin.path); + expect(rec.currentBranch, (head.stdout as String).trim()); }); test('unmount forgets the mount', () async {