From 3401966ee624d5dff07dfb45c3db265ab1ea09ec Mon Sep 17 00:00:00 2001 From: Jordon <16258926+Jordonbc@users.noreply.github.com> Date: Sat, 27 Dec 2025 22:37:42 +0000 Subject: [PATCH 01/52] Update release workflow to always mark as prerelease Removed prerelease input option and set it to true. --- .github/workflows/release.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 385ca18..b83f3ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,12 +10,6 @@ on: description: "Release tag (default: core-v)" required: false type: string - prerelease: - description: "Mark as pre-release" - required: false - type: boolean - default: false - permissions: contents: read @@ -78,7 +72,7 @@ jobs: with: tag_name: ${{ steps.tag.outputs.tag }} name: ${{ steps.tag.outputs.tag }} - prerelease: ${{ github.event_name == 'workflow_dispatch' && inputs.prerelease || false }} + prerelease: true generate_release_notes: true files: | target/package/*.crate From 64207e857bc7e969ff7bfde9257515532485cc50 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 03:07:14 +0000 Subject: [PATCH 02/52] Improve --- Cargo.toml | 5 --- src/backend_descriptor.rs | 94 --------------------------------------- src/lib.rs | 3 -- src/plugin_protocol.rs | 5 +++ 4 files changed, 5 insertions(+), 102 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c3457a0..3ab0b83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,15 +13,10 @@ plugin-protocol = ["dep:serde_json"] # The VCS trait and VCS-related error type. vcs = ["dep:thiserror"] -# Static + dynamic backend registry (linkme + logging). -backend-registry = ["vcs", "dep:linkme", "dep:log"] - [dependencies] serde = { version = "1", features = ["derive"] } serde_json = { version = "1", optional = true } thiserror = { version = "2", optional = true } -linkme = { version = "0.3", optional = true } -log = { version = "0.4", optional = true } [dev-dependencies] serde_json = "1" diff --git a/src/backend_descriptor.rs b/src/backend_descriptor.rs index ac4d952..e69de29 100644 --- a/src/backend_descriptor.rs +++ b/src/backend_descriptor.rs @@ -1,94 +0,0 @@ -/* ========================= Runtime backend registry ========================= - Backends contribute a `BackendDescriptor` into the distributed slice below. - The app can enumerate and pick any registered backend at runtime. -=============================================================================*/ -use crate::Vcs; -use crate::backend_id::BackendId; -use crate::models::{Capabilities, OnEvent}; -use std::path::Path; -use std::sync::Arc; -use std::sync::{Mutex, OnceLock}; - -pub type OpenRepoFn = fn(&Path) -> crate::Result>; -pub type CloneRepoFn = fn(&str, &Path, Option) -> crate::Result>; - -/// Factory & metadata for a backend implementation. -pub struct BackendDescriptor { - pub id: BackendId, - pub name: &'static str, - pub caps: fn() -> Capabilities, - pub open: OpenRepoFn, - pub clone_repo: CloneRepoFn, -} - -/// The global registry. Each backend crate declares exactly one `BackendDescriptor` here. -#[linkme::distributed_slice] -pub static BACKENDS: [BackendDescriptor] = [..]; - -static DYNAMIC_BACKENDS: OnceLock>> = OnceLock::new(); - -fn dynamic_backends() -> &'static Mutex> { - DYNAMIC_BACKENDS.get_or_init(|| Mutex::new(Vec::new())) -} - -/// Register a backend at runtime (e.g., loaded from an OpenVCS plugin). -/// -/// The descriptor is leaked to produce a `'static` reference, matching the static registry API. -pub fn register_backend(descriptor: BackendDescriptor) -> &'static BackendDescriptor { - let leaked: &'static BackendDescriptor = Box::leak(Box::new(descriptor)); - let mut lock = dynamic_backends() - .lock() - .expect("openvcs-core dynamic backend lock poisoned"); - lock.push(leaked); - leaked -} - -/// Enumerate all registered backends (static + runtime-registered). -/// -/// Order is link-order for static backends, then registration order for runtime backends. -pub fn list_backends() -> Vec<&'static BackendDescriptor> { - use log::{debug, trace}; - - let mut out: Vec<&'static BackendDescriptor> = Vec::new(); - out.extend(BACKENDS.iter()); - - let lock = dynamic_backends() - .lock() - .expect("openvcs-core dynamic backend lock poisoned"); - out.extend(lock.iter().copied()); - - debug!("openvcs-core: {} backends registered", out.len()); - for b in &out { - trace!("openvcs-core: backend loaded: {} ({})", b.id, b.name); - } - - out -} - -/// Lookup a backend descriptor by id. -pub fn get_backend(id: impl AsRef) -> Option<&'static BackendDescriptor> { - use log::{debug, warn}; - - let id = id.as_ref(); - let found = BACKENDS.iter().find(|b| b.id.as_ref() == id).or_else(|| { - dynamic_backends() - .lock() - .expect("openvcs-core dynamic backend lock poisoned") - .iter() - .copied() - .find(|b| b.id.as_ref() == id) - }); - match found { - Some(b) => { - debug!( - "openvcs-core: backend lookup succeeded → {} ({})", - b.id, b.name - ); - Some(b) - } - None => { - warn!("openvcs-core: backend lookup failed for id='{}'", id); - None - } - } -} diff --git a/src/lib.rs b/src/lib.rs index e24d74b..58e3440 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -198,6 +198,3 @@ pub trait Vcs: Send + Sync { Err(VcsError::Unsupported(self.id())) } } - -#[cfg(all(feature = "backend-registry", feature = "vcs"))] -pub mod backend_descriptor; diff --git a/src/plugin_protocol.rs b/src/plugin_protocol.rs index b0f1a3e..6ef1974 100644 --- a/src/plugin_protocol.rs +++ b/src/plugin_protocol.rs @@ -20,11 +20,16 @@ pub struct RpcResponse { pub result: Value, #[serde(default)] pub error: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error_code: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error_data: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum PluginMessage { + Request(RpcRequest), Response(RpcResponse), Event { event: VcsEvent }, } From f5efecde5550d2d5cabd151a19c9443c82213f02 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 11:12:47 +0000 Subject: [PATCH 03/52] Fixed compile issue --- Cargo.lock | 28 ---------------------------- Cargo.toml | 3 +++ 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d708ea8..6121967 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,32 +8,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" -[[package]] -name = "linkme" -version = "0.3.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e3283ed2d0e50c06dd8602e0ab319bb048b6325d0bba739db64ed8205179898" -dependencies = [ - "linkme-impl", -] - -[[package]] -name = "linkme-impl" -version = "0.3.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5cec0ec4228b4853bb129c84dbf093a27e6c7a20526da046defc334a1b017f7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - [[package]] name = "memchr" version = "2.7.6" @@ -44,8 +18,6 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" name = "openvcs-core" version = "0.1.0" dependencies = [ - "linkme", - "log", "serde", "serde_json", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 3ab0b83..b67768f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,9 @@ name = "openvcs-core" version = "0.1.0" edition = "2024" +description = "Core types and traits for OpenVCS." +license-file = "LICENSE" +readme = "README.md" [features] # Keep the default lightweight for non-VCS plugins; enable what you need. From ed8021728b7d7a0345372a0a8891820316c4cd97 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 11:15:31 +0000 Subject: [PATCH 04/52] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3b1816..e709080 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,4 +59,4 @@ jobs: run: cargo test - name: Test (full features) - run: cargo test --no-default-features --features plugin-protocol,vcs,backend-registry + run: cargo test --no-default-features --features plugin-protocol,vcs From 7872f441348f06e6c88e1e986f7c5f1e75c07e23 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 11:25:19 +0000 Subject: [PATCH 05/52] Fix compile issues --- Cargo.toml | 3 ++- src/backend_descriptor.rs | 23 +++++++++++++++++++++++ src/lib.rs | 3 +++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b67768f..1ba934b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,12 +14,13 @@ default = ["plugin-protocol"] plugin-protocol = ["dep:serde_json"] # The VCS trait and VCS-related error type. -vcs = ["dep:thiserror"] +vcs = ["dep:thiserror", "dep:linkme"] [dependencies] serde = { version = "1", features = ["derive"] } serde_json = { version = "1", optional = true } thiserror = { version = "2", optional = true } +linkme = { version = "0.3", optional = true } [dev-dependencies] serde_json = "1" diff --git a/src/backend_descriptor.rs b/src/backend_descriptor.rs index e69de29..199399d 100644 --- a/src/backend_descriptor.rs +++ b/src/backend_descriptor.rs @@ -0,0 +1,23 @@ +#![cfg(feature = "vcs")] + +use crate::backend_id::BackendId; +use crate::models::{Capabilities, OnEvent}; +use crate::{Result, Vcs}; +use std::path::Path; +use std::sync::Arc; + +pub type CapsFn = fn() -> Capabilities; +pub type OpenFn = fn(&Path) -> Result>; +pub type CloneRepoFn = fn(&str, &Path, Option) -> Result>; + +#[derive(Clone)] +pub struct BackendDescriptor { + pub id: BackendId, + pub name: &'static str, + pub caps: CapsFn, + pub open: OpenFn, + pub clone_repo: CloneRepoFn, +} + +#[linkme::distributed_slice] +pub static BACKENDS: [BackendDescriptor] = [..]; diff --git a/src/lib.rs b/src/lib.rs index 58e3440..8bed32a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,9 @@ pub mod models; pub use crate::backend_id::BackendId; +#[cfg(feature = "vcs")] +pub mod backend_descriptor; + #[cfg(feature = "plugin-protocol")] pub mod plugin_protocol; From 373d5856e2303b5dc29bb092daa31ddcefec2ff6 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 11:27:47 +0000 Subject: [PATCH 06/52] Fix compile issues --- Cargo.lock | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 6121967..b944518 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,26 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "linkme" +version = "0.3.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e3283ed2d0e50c06dd8602e0ab319bb048b6325d0bba739db64ed8205179898" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5cec0ec4228b4853bb129c84dbf093a27e6c7a20526da046defc334a1b017f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "memchr" version = "2.7.6" @@ -18,6 +38,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" name = "openvcs-core" version = "0.1.0" dependencies = [ + "linkme", "serde", "serde_json", "thiserror", From 6c603bf7419d353557cad11e13db947657319040 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 11:28:50 +0000 Subject: [PATCH 07/52] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e709080..c3b1816 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,4 +59,4 @@ jobs: run: cargo test - name: Test (full features) - run: cargo test --no-default-features --features plugin-protocol,vcs + run: cargo test --no-default-features --features plugin-protocol,vcs,backend-registry From 4ae51c34d1760ad5162b32a240f528e849e531c6 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 11:30:08 +0000 Subject: [PATCH 08/52] Update Cargo.toml --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 1ba934b..4f70a47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,10 @@ plugin-protocol = ["dep:serde_json"] # The VCS trait and VCS-related error type. vcs = ["dep:thiserror", "dep:linkme"] +# Backend discovery via the `BACKENDS` registry (link-time registration). +# Kept as a separate feature for CI/backward-compat; currently also implied by `vcs`. +backend-registry = ["dep:linkme"] + [dependencies] serde = { version = "1", features = ["derive"] } serde_json = { version = "1", optional = true } From 3f036c4bcb09467c75060a3571dc3cb6ee64c054 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 11:30:56 +0000 Subject: [PATCH 09/52] Fixed issue --- Cargo.toml | 3 +-- src/backend_descriptor.rs | 2 +- src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4f70a47..1903aa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,9 @@ default = ["plugin-protocol"] plugin-protocol = ["dep:serde_json"] # The VCS trait and VCS-related error type. -vcs = ["dep:thiserror", "dep:linkme"] +vcs = ["dep:thiserror", "backend-registry"] # Backend discovery via the `BACKENDS` registry (link-time registration). -# Kept as a separate feature for CI/backward-compat; currently also implied by `vcs`. backend-registry = ["dep:linkme"] [dependencies] diff --git a/src/backend_descriptor.rs b/src/backend_descriptor.rs index 199399d..2af7e49 100644 --- a/src/backend_descriptor.rs +++ b/src/backend_descriptor.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "vcs")] +#![cfg(feature = "backend-registry")] use crate::backend_id::BackendId; use crate::models::{Capabilities, OnEvent}; diff --git a/src/lib.rs b/src/lib.rs index 8bed32a..e3ad0a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ pub mod models; pub use crate::backend_id::BackendId; -#[cfg(feature = "vcs")] +#[cfg(feature = "backend-registry")] pub mod backend_descriptor; #[cfg(feature = "plugin-protocol")] From 82965c0f3eaa795b02e3f5e4730c877e2f1cd5b5 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 13:59:59 +0000 Subject: [PATCH 10/52] Update ci.yml --- .github/workflows/ci.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3b1816..8c35410 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,8 +40,6 @@ jobs: - name: Install Rust (stable) uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable - with: - components: rustfmt - name: Setup sccache uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 @@ -49,9 +47,6 @@ jobs: - name: Rust cache uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - - name: Format - run: cargo fmt --all --check - - name: Cargo check (all features) run: cargo check --all-targets --all-features From 0bdd5486bd12ada8e2f04ba098f378d94aa436a1 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 16:14:52 +0000 Subject: [PATCH 11/52] Update backend_descriptor.rs --- src/backend_descriptor.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/backend_descriptor.rs b/src/backend_descriptor.rs index 2af7e49..c2d60d0 100644 --- a/src/backend_descriptor.rs +++ b/src/backend_descriptor.rs @@ -19,5 +19,11 @@ pub struct BackendDescriptor { pub clone_repo: CloneRepoFn, } +#[cfg(not(target_arch = "wasm32"))] #[linkme::distributed_slice] pub static BACKENDS: [BackendDescriptor] = [..]; + +// `linkme` distributed slices are not supported on WASI/WASM targets. +// Plugins compiled to WASI do not need link-time backend discovery. +#[cfg(target_arch = "wasm32")] +pub static BACKENDS: [BackendDescriptor; 0] = []; From e80376940a68d9dc3a38bcfa7e2ecd882814cb72 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 17:16:29 +0000 Subject: [PATCH 12/52] Added helpers --- README.md | 3 ++ src/lib.rs | 3 ++ src/plugin_stdio.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 src/plugin_stdio.rs diff --git a/README.md b/README.md index 6e98d87..21841aa 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,6 @@ Shared Rust crate for OpenVCS plugins and the OpenVCS client. - `plugin-protocol` (default): JSON RPC message types for plugin stdio communication. - `vcs`: the `Vcs` trait + `VcsError`. - `backend-registry`: backend registry helpers (requires `vcs`). + +## Plugin helpers +- `openvcs_core::plugin_stdio`: small helpers for stdio JSON-RPC plugins (read/write messages and send a single `respond_shared(...)` call). diff --git a/src/lib.rs b/src/lib.rs index e3ad0a7..72fb412 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,9 @@ pub mod backend_descriptor; #[cfg(feature = "plugin-protocol")] pub mod plugin_protocol; +#[cfg(feature = "plugin-protocol")] +pub mod plugin_stdio; + #[cfg(feature = "plugin-protocol")] pub use crate::plugin_protocol::{PluginMessage, RpcRequest, RpcResponse}; diff --git a/src/plugin_stdio.rs b/src/plugin_stdio.rs new file mode 100644 index 0000000..16a9626 --- /dev/null +++ b/src/plugin_stdio.rs @@ -0,0 +1,93 @@ +use crate::plugin_protocol::{PluginMessage, RpcResponse}; +use serde::de::DeserializeOwned; +use serde_json::Value; +use std::io::{self, BufRead, Write}; +use std::sync::{Arc, Mutex}; + +#[derive(Debug, Clone)] +pub struct PluginError { + pub code: Option, + pub message: String, + pub data: Option, +} + +impl PluginError { + pub fn message(message: impl Into) -> Self { + Self { + code: None, + message: message.into(), + data: None, + } + } + + pub fn code(code: impl Into, message: impl Into) -> Self { + Self { + code: Some(code.into()), + message: message.into(), + data: None, + } + } + + pub fn with_data(mut self, data: Value) -> Self { + self.data = Some(data); + self + } +} + +pub fn read_message(stdin: &mut R) -> Option { + let mut line = String::new(); + loop { + line.clear(); + let n = stdin.read_line(&mut line).ok()?; + if n == 0 { + return None; + } + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + if let Ok(msg) = serde_json::from_str::(trimmed) { + return Some(msg); + } + } +} + +pub fn write_message(out: &mut W, msg: &PluginMessage) -> io::Result<()> { + let line = serde_json::to_string(msg).unwrap_or_else(|_| "{}".into()); + writeln!(out, "{line}")?; + out.flush()?; + Ok(()) +} + +pub fn write_message_shared(out: &Arc>, msg: &PluginMessage) { + if let Ok(mut w) = out.lock() { + let _ = write_message(&mut *w, msg); + } +} + +pub fn respond_shared(out: &Arc>, id: u64, res: Result) { + let response = match res { + Ok(result) => RpcResponse { + id, + ok: true, + result, + error: None, + error_code: None, + error_data: None, + }, + Err(err) => RpcResponse { + id, + ok: false, + result: Value::Null, + error: Some(err.message), + error_code: err.code, + error_data: err.data, + }, + }; + + write_message_shared(out, &PluginMessage::Response(response)); +} + +pub fn parse_json_params(value: Value) -> Result { + serde_json::from_value(value).map_err(|e| format!("invalid params: {e}")) +} From 011dfc49fb5dca178bf1942783e9c331e24fa5d2 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 17:25:13 +0000 Subject: [PATCH 13/52] Update plugin_stdio.rs --- src/plugin_stdio.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/plugin_stdio.rs b/src/plugin_stdio.rs index 16a9626..1638694 100644 --- a/src/plugin_stdio.rs +++ b/src/plugin_stdio.rs @@ -1,8 +1,10 @@ use crate::plugin_protocol::{PluginMessage, RpcResponse}; use serde::de::DeserializeOwned; use serde_json::Value; +use std::collections::{HashMap, VecDeque}; use std::io::{self, BufRead, Write}; use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; #[derive(Debug, Clone)] pub struct PluginError { @@ -91,3 +93,87 @@ pub fn respond_shared(out: &Arc>, id: u64, res: Result(value: Value) -> Result { serde_json::from_value(value).map_err(|e| format!("invalid params: {e}")) } + +#[derive(Debug)] +pub struct AsyncHostCallState { + pub next_id: u64, +} + +pub fn host_call_blocking( + out: &Arc>, + stdin: &Arc>, + queue: &Arc>>, + pending: &Arc>, + method: &str, + params: Value, + timeout: Duration, +) -> Result { + let id = { + let mut lock = pending + .lock() + .map_err(|_| PluginError::message("pending lock poisoned"))?; + let id = lock.next_id; + lock.next_id = lock.next_id.saturating_add(1); + id + }; + + write_message_shared( + out, + &PluginMessage::Request(crate::plugin_protocol::RpcRequest { + id, + method: method.to_string(), + params, + }), + ); + + let deadline = Instant::now() + timeout; + let mut stash: HashMap = HashMap::new(); + + loop { + if Instant::now() > deadline { + return Err(PluginError::code("host.timeout", "host call timed out")); + } + + if let Some(resp) = stash.remove(&id) { + return if resp.ok { + Ok(resp.result) + } else { + Err(PluginError { + code: resp.error_code.or(Some("host.error".into())), + message: resp.error.unwrap_or_else(|| "error".into()), + data: resp.error_data, + }) + }; + } + + let msg = { + let mut lock = stdin + .lock() + .map_err(|_| PluginError::message("stdin lock poisoned"))?; + read_message(&mut *lock).ok_or_else(|| PluginError::message("host closed stdin"))? + }; + + match msg { + PluginMessage::Response(resp) => { + if resp.id == id { + return if resp.ok { + Ok(resp.result) + } else { + Err(PluginError { + code: resp.error_code.or(Some("host.error".into())), + message: resp.error.unwrap_or_else(|| "error".into()), + data: resp.error_data, + }) + }; + } + stash.insert(resp.id, resp); + } + PluginMessage::Request(req) => { + if let Ok(mut q) = queue.lock() { + q.push_back(req); + } + } + PluginMessage::Event { .. } => {} + } + } +} From 3601da1b54db4276e68ac87aec1f35eeb68f06f3 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 17:56:06 +0000 Subject: [PATCH 14/52] Update plugin_stdio.rs --- src/plugin_stdio.rs | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/plugin_stdio.rs b/src/plugin_stdio.rs index 1638694..537038c 100644 --- a/src/plugin_stdio.rs +++ b/src/plugin_stdio.rs @@ -1,4 +1,4 @@ -use crate::plugin_protocol::{PluginMessage, RpcResponse}; +use crate::plugin_protocol::{PluginMessage, RpcRequest, RpcResponse}; use serde::de::DeserializeOwned; use serde_json::Value; use std::collections::{HashMap, VecDeque}; @@ -36,7 +36,7 @@ impl PluginError { } } -pub fn read_message(stdin: &mut R) -> Option { +pub fn receive_message(stdin: &mut R) -> Option { let mut line = String::new(); loop { line.clear(); @@ -61,12 +61,29 @@ pub fn write_message(out: &mut W, msg: &PluginMessage) -> io::Result<( Ok(()) } -pub fn write_message_shared(out: &Arc>, msg: &PluginMessage) { +pub fn send_message_shared(out: &Arc>, msg: &PluginMessage) { if let Ok(mut w) = out.lock() { let _ = write_message(&mut *w, msg); } } +pub fn send_request_shared(out: &Arc>, req: RpcRequest) { + send_message_shared(out, &PluginMessage::Request(req)); +} + +pub fn send_request(out: &mut W, req: RpcRequest) -> io::Result<()> { + write_message(out, &PluginMessage::Request(req)) +} + +pub fn receive_request(stdin: &mut R) -> Option { + loop { + match receive_message(stdin)? { + PluginMessage::Request(req) => return Some(req), + PluginMessage::Response(_) | PluginMessage::Event { .. } => {} + } + } +} + pub fn respond_shared(out: &Arc>, id: u64, res: Result) { let response = match res { Ok(result) => RpcResponse { @@ -87,7 +104,7 @@ pub fn respond_shared(out: &Arc>, id: u64, res: Result(value: Value) -> Result { @@ -95,21 +112,21 @@ pub fn parse_json_params(value: Value) -> Result } #[derive(Debug)] -pub struct AsyncHostCallState { +pub struct RequestIdState { pub next_id: u64, } -pub fn host_call_blocking( +pub fn call_host( out: &Arc>, stdin: &Arc>, queue: &Arc>>, - pending: &Arc>, + ids: &Arc>, method: &str, params: Value, timeout: Duration, ) -> Result { let id = { - let mut lock = pending + let mut lock = ids .lock() .map_err(|_| PluginError::message("pending lock poisoned"))?; let id = lock.next_id; @@ -117,13 +134,13 @@ pub fn host_call_blocking( id }; - write_message_shared( + send_request_shared( out, - &PluginMessage::Request(crate::plugin_protocol::RpcRequest { + crate::plugin_protocol::RpcRequest { id, method: method.to_string(), params, - }), + }, ); let deadline = Instant::now() + timeout; @@ -150,7 +167,7 @@ pub fn host_call_blocking( let mut lock = stdin .lock() .map_err(|_| PluginError::message("stdin lock poisoned"))?; - read_message(&mut *lock).ok_or_else(|| PluginError::message("host closed stdin"))? + receive_message(&mut *lock).ok_or_else(|| PluginError::message("host closed stdin"))? }; match msg { From bd61a7738a7acb1a7896af142936a8c77778c74a Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 18:01:24 +0000 Subject: [PATCH 15/52] Added logger --- Cargo.lock | 7 ++++++ Cargo.toml | 1 + src/lib.rs | 3 +++ src/plugin_logging.rs | 56 +++++++++++++++++++++++++++++++++++++++++++ src/plugin_stdio.rs | 4 ++++ 5 files changed, 71 insertions(+) create mode 100644 src/plugin_logging.rs diff --git a/Cargo.lock b/Cargo.lock index b944518..d708ea8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,12 @@ dependencies = [ "syn", ] +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "memchr" version = "2.7.6" @@ -39,6 +45,7 @@ name = "openvcs-core" version = "0.1.0" dependencies = [ "linkme", + "log", "serde", "serde_json", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 1903aa9..8d663da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ serde = { version = "1", features = ["derive"] } serde_json = { version = "1", optional = true } thiserror = { version = "2", optional = true } linkme = { version = "0.3", optional = true } +log = "0.4" [dev-dependencies] serde_json = "1" diff --git a/src/lib.rs b/src/lib.rs index 72fb412..02579e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,9 @@ pub mod plugin_protocol; #[cfg(feature = "plugin-protocol")] pub mod plugin_stdio; +#[cfg(feature = "plugin-protocol")] +mod plugin_logging; + #[cfg(feature = "plugin-protocol")] pub use crate::plugin_protocol::{PluginMessage, RpcRequest, RpcResponse}; diff --git a/src/plugin_logging.rs b/src/plugin_logging.rs new file mode 100644 index 0000000..430c15a --- /dev/null +++ b/src/plugin_logging.rs @@ -0,0 +1,56 @@ +use std::sync::OnceLock; +use std::io::Write; + +fn parse_level_filter_from_env() -> log::LevelFilter { + let raw = std::env::var("OPENVCS_LOG") + .ok() + .or_else(|| std::env::var("RUST_LOG").ok()); + + match raw.as_deref().map(str::trim) { + None | Some("") => log::LevelFilter::Info, + Some("off") | Some("OFF") => log::LevelFilter::Off, + Some("error") | Some("ERROR") => log::LevelFilter::Error, + Some("warn") | Some("WARN") | Some("warning") | Some("WARNING") => log::LevelFilter::Warn, + Some("info") | Some("INFO") => log::LevelFilter::Info, + Some("debug") | Some("DEBUG") => log::LevelFilter::Debug, + Some("trace") | Some("TRACE") => log::LevelFilter::Trace, + _ => log::LevelFilter::Info, + } +} + +struct StderrLogger; + +impl log::Log for StderrLogger { + fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { + metadata.level() <= log::max_level() + } + + fn log(&self, record: &log::Record<'_>) { + if !self.enabled(record.metadata()) { + return; + } + let mut stderr = std::io::stderr().lock(); + let _ = writeln!( + stderr, + "[{}] {}: {}", + record.level(), + record.target(), + record.args() + ); + } + + fn flush(&self) {} +} + +static INIT: OnceLock<()> = OnceLock::new(); +static LOGGER: StderrLogger = StderrLogger; + +pub(crate) fn ensure_initialized() { + INIT.get_or_init(|| { + let level = parse_level_filter_from_env(); + // Only install the logger if nothing else already did. + if log::set_logger(&LOGGER).is_ok() { + log::set_max_level(level); + } + }); +} diff --git a/src/plugin_stdio.rs b/src/plugin_stdio.rs index 537038c..8ced4f8 100644 --- a/src/plugin_stdio.rs +++ b/src/plugin_stdio.rs @@ -37,6 +37,7 @@ impl PluginError { } pub fn receive_message(stdin: &mut R) -> Option { + crate::plugin_logging::ensure_initialized(); let mut line = String::new(); loop { line.clear(); @@ -55,6 +56,7 @@ pub fn receive_message(stdin: &mut R) -> Option { } pub fn write_message(out: &mut W, msg: &PluginMessage) -> io::Result<()> { + crate::plugin_logging::ensure_initialized(); let line = serde_json::to_string(msg).unwrap_or_else(|_| "{}".into()); writeln!(out, "{line}")?; out.flush()?; @@ -62,6 +64,7 @@ pub fn write_message(out: &mut W, msg: &PluginMessage) -> io::Result<( } pub fn send_message_shared(out: &Arc>, msg: &PluginMessage) { + crate::plugin_logging::ensure_initialized(); if let Ok(mut w) = out.lock() { let _ = write_message(&mut *w, msg); } @@ -125,6 +128,7 @@ pub fn call_host( params: Value, timeout: Duration, ) -> Result { + crate::plugin_logging::ensure_initialized(); let id = { let mut lock = ids .lock() From 7d580527dc4c8e647b34e5131c430a1c26621007 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 18:28:35 +0000 Subject: [PATCH 16/52] Update plugin_stdio.rs --- src/plugin_stdio.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/plugin_stdio.rs b/src/plugin_stdio.rs index 8ced4f8..638d820 100644 --- a/src/plugin_stdio.rs +++ b/src/plugin_stdio.rs @@ -1,4 +1,5 @@ use crate::plugin_protocol::{PluginMessage, RpcRequest, RpcResponse}; +use serde::Serialize; use serde::de::DeserializeOwned; use serde_json::Value; use std::collections::{HashMap, VecDeque}; @@ -110,6 +111,14 @@ pub fn respond_shared(out: &Arc>, id: u64, res: Result(value: T) -> Result { + serde_json::to_value(value).map_err(|e| PluginError::code("plugin.serialize", e.to_string())) +} + +pub fn ok_null() -> Result { + Ok(Value::Null) +} + pub fn parse_json_params(value: Value) -> Result { serde_json::from_value(value).map_err(|e| format!("invalid params: {e}")) } From 9c952d1388735ad2947d8026300649d6b53bffe7 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 18:39:34 +0000 Subject: [PATCH 17/52] Improve core --- src/host.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 ++ 2 files changed, 96 insertions(+) create mode 100644 src/host.rs diff --git a/src/host.rs b/src/host.rs new file mode 100644 index 0000000..1f19118 --- /dev/null +++ b/src/host.rs @@ -0,0 +1,93 @@ +#![cfg(feature = "plugin-protocol")] + +use crate::plugin_protocol::RpcRequest; +use crate::plugin_stdio::{PluginError, RequestIdState, call_host}; +use serde_json::Value; +use std::collections::VecDeque; +use std::io::{BufReader, LineWriter}; +use std::sync::{Arc, Mutex, OnceLock}; +use std::time::Duration; + +pub type HostStdout = LineWriter; +pub type HostStdin = BufReader; + +#[derive(Clone)] +struct HostContext { + out: Arc>, + stdin: Arc>, + queue: Arc>>, + ids: Arc>, + timeout: Duration, +} + +static HOST: OnceLock = OnceLock::new(); + +pub fn init_stdio_default(next_id: u64, timeout: Duration) { + let out = Arc::new(Mutex::new(LineWriter::new(std::io::stdout()))); + let stdin = Arc::new(Mutex::new(BufReader::new(std::io::stdin()))); + let queue = Arc::new(Mutex::new(VecDeque::new())); + let ids = Arc::new(Mutex::new(RequestIdState { next_id })); + init_default_stdio_host(out, stdin, queue, ids, timeout); +} + +pub fn init_default_stdio_host( + out: Arc>, + stdin: Arc>, + queue: Arc>>, + ids: Arc>, + timeout: Duration, +) { + let _ = HOST.set(HostContext { + out, + stdin, + queue, + ids, + timeout, + }); +} + +pub fn stdout() -> Result<&'static Arc>, PluginError> { + Ok(&HOST + .get() + .ok_or_else(|| PluginError::code("host.uninitialized", "host not initialized"))? + .out) +} + +pub fn stdin() -> Result<&'static Arc>, PluginError> { + Ok(&HOST + .get() + .ok_or_else(|| PluginError::code("host.uninitialized", "host not initialized"))? + .stdin) +} + +pub fn queue() -> Result<&'static Arc>>, PluginError> { + Ok(&HOST + .get() + .ok_or_else(|| PluginError::code("host.uninitialized", "host not initialized"))? + .queue) +} + +pub fn ids() -> Result<&'static Arc>, PluginError> { + Ok(&HOST + .get() + .ok_or_else(|| PluginError::code("host.uninitialized", "host not initialized"))? + .ids) +} + +pub fn call(method: &str, params: Value) -> Result { + let ctx = HOST.get().ok_or_else(|| { + PluginError::code( + "host.uninitialized", + "host bridge not initialized (call init_default_stdio_host)", + ) + })?; + call_host( + &ctx.out, + &ctx.stdin, + &ctx.queue, + &ctx.ids, + method, + params, + ctx.timeout, + ) +} diff --git a/src/lib.rs b/src/lib.rs index 02579e6..3da4d63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,9 @@ pub mod plugin_protocol; #[cfg(feature = "plugin-protocol")] pub mod plugin_stdio; +#[cfg(feature = "plugin-protocol")] +pub mod host; + #[cfg(feature = "plugin-protocol")] mod plugin_logging; From 0c74263fdef9274bbd0a13b5913b4729d3150e49 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 18:49:59 +0000 Subject: [PATCH 18/52] Improvements --- src/lib.rs | 3 ++ src/plugin_runtime.rs | 93 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/plugin_runtime.rs diff --git a/src/lib.rs b/src/lib.rs index 3da4d63..7d4cbec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,9 @@ pub mod plugin_stdio; #[cfg(feature = "plugin-protocol")] pub mod host; +#[cfg(feature = "plugin-protocol")] +pub mod plugin_runtime; + #[cfg(feature = "plugin-protocol")] mod plugin_logging; diff --git a/src/plugin_runtime.rs b/src/plugin_runtime.rs new file mode 100644 index 0000000..347a8c9 --- /dev/null +++ b/src/plugin_runtime.rs @@ -0,0 +1,93 @@ +#![cfg(feature = "plugin-protocol")] + +use crate::models::VcsEvent; +use crate::plugin_protocol::{PluginMessage, RpcRequest}; +use crate::plugin_stdio::{PluginError, receive_message, respond_shared, send_message_shared}; +use std::collections::VecDeque; +use std::io::{self, BufReader, LineWriter}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +pub type HandlerResult = Result; + +const DEFAULT_HOST_TIMEOUT_MS: u64 = 60_000; +const HOST_TIMEOUT_ENV: &str = "OPENVCS_PLUGIN_HOST_TIMEOUT_MS"; + +pub struct PluginCtx { + stdout: Arc>>, +} + +impl PluginCtx { + pub fn stdout(&self) -> Arc>> { + Arc::clone(&self.stdout) + } + + pub fn emit(&self, event: VcsEvent) { + send_message_shared(&self.stdout, &PluginMessage::Event { event }); + } +} + +fn next_request( + queue: &Arc>>, + stdin: &Arc>>, +) -> Option { + if let Ok(mut q) = queue.lock() { + if let Some(req) = q.pop_front() { + return Some(req); + } + } + + loop { + let msg = { + let mut lock = stdin.lock().ok()?; + receive_message(&mut *lock)? + }; + match msg { + PluginMessage::Request(req) => return Some(req), + PluginMessage::Response(_) | PluginMessage::Event { .. } => continue, + } + } +} + +pub fn run_stdio_plugin( + mut handle: impl FnMut(&mut PluginCtx, RpcRequest) -> HandlerResult, +) -> io::Result<()> { + #[cfg(target_arch = "wasm32")] + let next_id = 1u64 << 63; + + #[cfg(not(target_arch = "wasm32"))] + let next_id = 1u64; + + let timeout = std::env::var(HOST_TIMEOUT_ENV) + .ok() + .and_then(|s| s.parse::().ok()) + .filter(|&ms| ms > 0) + .map(Duration::from_millis) + .unwrap_or(Duration::from_millis(DEFAULT_HOST_TIMEOUT_MS)); + + let stdout = Arc::new(Mutex::new(LineWriter::new(io::stdout()))); + let stdin = Arc::new(Mutex::new(BufReader::new(io::stdin()))); + let queue: Arc>> = Arc::new(Mutex::new(VecDeque::new())); + let ids = Arc::new(Mutex::new(crate::plugin_stdio::RequestIdState { next_id })); + + crate::host::init_default_stdio_host( + Arc::clone(&stdout), + Arc::clone(&stdin), + Arc::clone(&queue), + Arc::clone(&ids), + timeout, + ); + + let mut ctx = PluginCtx { stdout }; + + loop { + let Some(req) = next_request(&queue, &stdin) else { + break; + }; + let id = req.id; + let res = handle(&mut ctx, req); + respond_shared(&ctx.stdout, id, res); + } + + Ok(()) +} From dca4e787320ebfdcff40686c31dbc665a3a766bc Mon Sep 17 00:00:00 2001 From: Jordon Date: Sun, 28 Dec 2025 20:42:53 +0000 Subject: [PATCH 19/52] Improved core --- src/events.rs | 27 ++++++ src/lib.rs | 3 + src/plugin_runtime.rs | 217 +++++++++++++++++++++++++++++++++++------- src/plugin_stdio.rs | 4 + 4 files changed, 214 insertions(+), 37 deletions(-) create mode 100644 src/events.rs diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..e974749 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,27 @@ +#![cfg(feature = "plugin-protocol")] + +use crate::host; +use crate::plugin_runtime::{PluginCtx, on_event}; +use crate::plugin_stdio::PluginError; +use serde_json::Value; + +pub fn subscribe_host(name: &str) -> Result<(), PluginError> { + let _ = host::call("events.subscribe", serde_json::json!({ "name": name }))?; + Ok(()) +} + +pub fn subscribe( + name: &'static str, + mut handler: impl FnMut(Value) -> Result<(), PluginError> + Send + 'static, +) -> Result<(), PluginError> { + on_event(name, move |_ctx: &mut PluginCtx, payload: Value| handler(payload)); + subscribe_host(name) +} + +pub fn emit(name: &str, payload: Value) -> Result<(), PluginError> { + let _ = host::call( + "events.emit", + serde_json::json!({ "name": name, "payload": payload }), + )?; + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 7d4cbec..b0839f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,9 @@ pub mod host; #[cfg(feature = "plugin-protocol")] pub mod plugin_runtime; +#[cfg(feature = "plugin-protocol")] +pub mod events; + #[cfg(feature = "plugin-protocol")] mod plugin_logging; diff --git a/src/plugin_runtime.rs b/src/plugin_runtime.rs index 347a8c9..abd74f3 100644 --- a/src/plugin_runtime.rs +++ b/src/plugin_runtime.rs @@ -3,28 +3,40 @@ use crate::models::VcsEvent; use crate::plugin_protocol::{PluginMessage, RpcRequest}; use crate::plugin_stdio::{PluginError, receive_message, respond_shared, send_message_shared}; +use crate::plugin_stdio::ok_null; use std::collections::VecDeque; +use std::collections::HashMap; use std::io::{self, BufReader, LineWriter}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; use std::time::Duration; pub type HandlerResult = Result; +pub type EventHandlerResult = Result<(), PluginError>; const DEFAULT_HOST_TIMEOUT_MS: u64 = 60_000; const HOST_TIMEOUT_ENV: &str = "OPENVCS_PLUGIN_HOST_TIMEOUT_MS"; +fn host_timeout() -> Duration { + std::env::var(HOST_TIMEOUT_ENV) + .ok() + .and_then(|s| s.parse::().ok()) + .filter(|&ms| ms > 0) + .map(Duration::from_millis) + .unwrap_or(Duration::from_millis(DEFAULT_HOST_TIMEOUT_MS)) +} + pub struct PluginCtx { stdout: Arc>>, } impl PluginCtx { - pub fn stdout(&self) -> Arc>> { - Arc::clone(&self.stdout) - } - pub fn emit(&self, event: VcsEvent) { send_message_shared(&self.stdout, &PluginMessage::Event { event }); } + + pub fn stdout(&self) -> Arc>> { + Arc::clone(&self.stdout) + } } fn next_request( @@ -49,45 +61,176 @@ fn next_request( } } -pub fn run_stdio_plugin( - mut handle: impl FnMut(&mut PluginCtx, RpcRequest) -> HandlerResult, -) -> io::Result<()> { - #[cfg(target_arch = "wasm32")] - let next_id = 1u64 << 63; +pub struct PluginRuntime { + ctx: PluginCtx, + stdin: Arc>>, + queue: Arc>>, +} - #[cfg(not(target_arch = "wasm32"))] - let next_id = 1u64; +impl PluginRuntime { + pub fn init() -> Self { + #[cfg(target_arch = "wasm32")] + let next_id = 1u64 << 63; + + #[cfg(not(target_arch = "wasm32"))] + let next_id = 1u64; + + let timeout = host_timeout(); + + let stdout = Arc::new(Mutex::new(LineWriter::new(io::stdout()))); + let stdin = Arc::new(Mutex::new(BufReader::new(io::stdin()))); + let queue: Arc>> = Arc::new(Mutex::new(VecDeque::new())); + let ids = Arc::new(Mutex::new(crate::plugin_stdio::RequestIdState { next_id })); + + crate::host::init_default_stdio_host( + Arc::clone(&stdout), + Arc::clone(&stdin), + Arc::clone(&queue), + Arc::clone(&ids), + timeout, + ); + + Self { + ctx: PluginCtx { stdout }, + stdin, + queue, + } + } - let timeout = std::env::var(HOST_TIMEOUT_ENV) - .ok() - .and_then(|s| s.parse::().ok()) - .filter(|&ms| ms > 0) - .map(Duration::from_millis) - .unwrap_or(Duration::from_millis(DEFAULT_HOST_TIMEOUT_MS)); + pub fn ctx(&mut self) -> &mut PluginCtx { + &mut self.ctx + } - let stdout = Arc::new(Mutex::new(LineWriter::new(io::stdout()))); - let stdin = Arc::new(Mutex::new(BufReader::new(io::stdin()))); - let queue: Arc>> = Arc::new(Mutex::new(VecDeque::new())); - let ids = Arc::new(Mutex::new(crate::plugin_stdio::RequestIdState { next_id })); + pub(crate) fn tick( + &mut self, + mut handle: impl FnMut(&mut PluginCtx, RpcRequest) -> HandlerResult, + ) -> io::Result { + let Some(req) = next_request(&self.queue, &self.stdin) else { + return Ok(false); + }; + let id = req.id; + let res = handle(&mut self.ctx, req); + respond_shared(&self.ctx.stdout, id, res); + Ok(true) + } +} - crate::host::init_default_stdio_host( - Arc::clone(&stdout), - Arc::clone(&stdin), - Arc::clone(&queue), - Arc::clone(&ids), - timeout, - ); +type RpcHandler = Box HandlerResult + Send + 'static>; +type EventHandler = + Box EventHandlerResult + Send + 'static>; - let mut ctx = PluginCtx { stdout }; +struct Registry { + rpc: HashMap, + event: HashMap>, + fallback: Option, +} - loop { - let Some(req) = next_request(&queue, &stdin) else { - break; - }; - let id = req.id; - let res = handle(&mut ctx, req); - respond_shared(&ctx.stdout, id, res); +static REGISTRY: OnceLock> = OnceLock::new(); + +fn registry() -> &'static Mutex { + REGISTRY.get_or_init(|| { + Mutex::new(Registry { + rpc: HashMap::new(), + event: HashMap::new(), + fallback: None, + }) + }) +} + +fn register_rpc_impl( + method: &str, + handler: impl FnMut(&mut PluginCtx, RpcRequest) -> HandlerResult + Send + 'static, +) { + if let Ok(mut lock) = registry().lock() { + lock.rpc.insert(method.to_string(), Box::new(handler)); + } +} + +pub fn register_delegate( + method: &str, + handler: impl FnMut(&mut PluginCtx, RpcRequest) -> HandlerResult + Send + 'static, +) { + register_rpc_impl(method, handler) +} + +/// Backward-compatible alias (will be removed once callers migrate). +#[deprecated(note = "renamed to register_delegate")] +pub fn on_rpc( + method: &str, + handler: impl FnMut(&mut PluginCtx, RpcRequest) -> HandlerResult + Send + 'static, +) { + register_rpc_impl(method, handler) +} + +#[deprecated(note = "renamed to register_delegate")] +pub fn register_rpc( + method: &str, + handler: impl FnMut(&mut PluginCtx, RpcRequest) -> HandlerResult + Send + 'static, +) { + register_rpc_impl(method, handler) +} + +pub fn on_event( + name: &str, + handler: impl FnMut(&mut PluginCtx, serde_json::Value) -> EventHandlerResult + Send + 'static, +) { + if let Ok(mut lock) = registry().lock() { + lock.event.entry(name.to_string()).or_default().push(Box::new(handler)); + } +} + +pub fn set_fallback_rpc( + handler: impl FnMut(&mut PluginCtx, RpcRequest) -> HandlerResult + Send + 'static, +) { + if let Ok(mut lock) = registry().lock() { + lock.fallback = Some(Box::new(handler)); } +} + +fn dispatch_registered(ctx: &mut PluginCtx, req: RpcRequest) -> HandlerResult { + if req.method == "event.dispatch" { + #[derive(serde::Deserialize)] + struct P { + name: String, + #[serde(default)] + payload: serde_json::Value, + } + let p: P = crate::plugin_stdio::parse_json_params(req.params) + .map_err(PluginError::message)?; + + if let Ok(mut lock) = registry().lock() { + if let Some(handlers) = lock.event.get_mut(&p.name) { + for h in handlers.iter_mut() { + h(ctx, p.payload.clone())?; + } + } + } + return ok_null(); + } + + if let Ok(mut lock) = registry().lock() { + if let Some(h) = lock.rpc.get_mut(&req.method) { + return h(ctx, req); + } + if let Some(fallback) = lock.fallback.as_mut() { + return fallback(ctx, req); + } + } + + Err(PluginError::code( + "plugin.unknown_method", + format!("unknown method '{}'", req.method), + )) +} + +pub fn run_registered() -> io::Result<()> { + let mut rt = PluginRuntime::init(); + while rt.tick(|ctx, req| dispatch_registered(ctx, req))? {} + Ok(()) +} +pub fn run(mut handle: impl FnMut(&mut PluginCtx, RpcRequest) -> HandlerResult) -> io::Result<()> { + let mut rt = PluginRuntime::init(); + while rt.tick(|ctx, req| handle(ctx, req))? {} Ok(()) } diff --git a/src/plugin_stdio.rs b/src/plugin_stdio.rs index 638d820..f26c35f 100644 --- a/src/plugin_stdio.rs +++ b/src/plugin_stdio.rs @@ -37,6 +37,10 @@ impl PluginError { } } +pub fn err_display(err: impl std::fmt::Display) -> PluginError { + PluginError::message(err.to_string()) +} + pub fn receive_message(stdin: &mut R) -> Option { crate::plugin_logging::ensure_initialized(); let mut line = String::new(); From d2f51f7e2829a8d2a70febccce8389ccb2c6fabb Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 13:14:42 +0000 Subject: [PATCH 20/52] Update lib.rs --- src/lib.rs | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index b0839f1..778d99f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,9 @@ pub mod models; pub use crate::backend_id::BackendId; +#[doc(hidden)] +pub use log as __log; + #[cfg(feature = "backend-registry")] pub mod backend_descriptor; @@ -26,6 +29,72 @@ pub mod events; #[cfg(feature = "plugin-protocol")] mod plugin_logging; +#[doc(hidden)] +pub fn __ensure_plugin_logging_initialized() { + #[cfg(feature = "plugin-protocol")] + crate::plugin_logging::ensure_initialized(); +} + +#[macro_export] +macro_rules! trace { + (target: $target:expr, $($arg:tt)+) => {{ + $crate::__ensure_plugin_logging_initialized(); + $crate::__log::trace!(target: $target, $($arg)+); + }}; + ($($arg:tt)+) => {{ + $crate::__ensure_plugin_logging_initialized(); + $crate::__log::trace!($($arg)+); + }}; +} + +#[macro_export] +macro_rules! debug { + (target: $target:expr, $($arg:tt)+) => {{ + $crate::__ensure_plugin_logging_initialized(); + $crate::__log::debug!(target: $target, $($arg)+); + }}; + ($($arg:tt)+) => {{ + $crate::__ensure_plugin_logging_initialized(); + $crate::__log::debug!($($arg)+); + }}; +} + +#[macro_export] +macro_rules! info { + (target: $target:expr, $($arg:tt)+) => {{ + $crate::__ensure_plugin_logging_initialized(); + $crate::__log::info!(target: $target, $($arg)+); + }}; + ($($arg:tt)+) => {{ + $crate::__ensure_plugin_logging_initialized(); + $crate::__log::info!($($arg)+); + }}; +} + +#[macro_export] +macro_rules! warn { + (target: $target:expr, $($arg:tt)+) => {{ + $crate::__ensure_plugin_logging_initialized(); + $crate::__log::warn!(target: $target, $($arg)+); + }}; + ($($arg:tt)+) => {{ + $crate::__ensure_plugin_logging_initialized(); + $crate::__log::warn!($($arg)+); + }}; +} + +#[macro_export] +macro_rules! error { + (target: $target:expr, $($arg:tt)+) => {{ + $crate::__ensure_plugin_logging_initialized(); + $crate::__log::error!(target: $target, $($arg)+); + }}; + ($($arg:tt)+) => {{ + $crate::__ensure_plugin_logging_initialized(); + $crate::__log::error!($($arg)+); + }}; +} + #[cfg(feature = "plugin-protocol")] pub use crate::plugin_protocol::{PluginMessage, RpcRequest, RpcResponse}; From 0d3150064e5047ed67e193f549879d441a0f011a Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 13:23:28 +0000 Subject: [PATCH 21/52] Create AGENTS.md --- AGENTS.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..19debdf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,34 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +- `src/`: Single Rust crate (`openvcs-core`) providing shared types/traits for the OpenVCS client and plugins. + - Feature-gated modules: `plugin_protocol`, `plugin_stdio`, `plugin_runtime`, `events`, `host`. + - VCS surface area lives behind the `vcs` feature (see `src/lib.rs`). +- `.github/workflows/`: CI, CodeQL, nightly packaging/release automation. +- `target/`: Cargo build artifacts (do not commit). + +## Build, Test, and Development Commands + +- `cargo check --all-targets --all-features`: Fast compile check matching CI’s broadest configuration. +- `cargo test`: Run default-feature unit tests. +- `cargo test --no-default-features --features plugin-protocol,vcs,backend-registry`: Validate feature combinations used in CI. +- `cargo package`: Ensure the crate packages cleanly (used by nightly automation). + +## Coding Style & Naming Conventions + +- Format with Rustfmt: `cargo fmt`. +- Prefer Clippy-clean code: `cargo clippy --all-targets --all-features -D warnings`. +- Naming: `snake_case` (functions/modules), `CamelCase` (types), `SCREAMING_SNAKE_CASE` (constants). +- Preserve feature gates (`#[cfg(feature = "...")]`) when adding new APIs; keep the default feature set lightweight. + +## Testing Guidelines + +- Uses Rust’s built-in test harness (`#[test]`) with unit tests colocated in modules (e.g., `src/models.rs`). +- Add tests next to the code they cover; keep tests deterministic and free of network/file-system assumptions unless required. + +## Commit & Pull Request Guidelines + +- Commit messages in this repo are short and imperative (e.g., “Update lib.rs”, “Fix compile issues”). Follow that pattern and mention the touched area. +- Open PRs against the `Dev` branch; include a clear description, rationale, and any relevant issue links. +- Ensure CI passes locally where possible (at minimum `cargo test`, ideally the full-feature test command above). From 46282058498c77c5ee5cc40d32abbbff4a2854d4 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 16:12:46 +0000 Subject: [PATCH 22/52] Update Cargo.toml --- Cargo.toml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8d663da..047dfee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,26 @@ name = "openvcs-core" version = "0.1.0" edition = "2024" description = "Core types and traits for OpenVCS." -license-file = "LICENSE" +license = "GPL-3.0-or-later" +homepage = "https://bbgames.dev/" +repository = "https://github.com/Open-VCS/OpenVCS-Core" readme = "README.md" +keywords = [ + "openvcs", + "vcs", + "version-control", + "plugin", + "backend", + "rpc" +] + +categories = [ + "development-tools", + "development-tools::build-utils", + "api-bindings" +] + [features] # Keep the default lightweight for non-VCS plugins; enable what you need. default = ["plugin-protocol"] From cf1a654adf06cb511fb8ecad4d9e26b399a37044 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 16:14:02 +0000 Subject: [PATCH 23/52] Update Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 047dfee..cb3e509 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,10 @@ keywords = [ "vcs", "version-control", "plugin", - "backend", "rpc" ] + categories = [ "development-tools", "development-tools::build-utils", From 455b248eb1e1cd37912ed4c2aecc864c2629a157 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 16:54:55 +0000 Subject: [PATCH 24/52] Added fmt requirement --- .github/workflows/ci.yml | 5 +++++ .github/workflows/nightly.yml | 5 +++++ .github/workflows/release.yml | 5 +++++ AGENTS.md | 1 + src/events.rs | 4 +++- src/plugin_logging.rs | 2 +- src/plugin_runtime.rs | 13 ++++++++----- 7 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c35410..bb0e469 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,8 @@ jobs: - name: Install Rust (stable) uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable + with: + components: rustfmt - name: Setup sccache uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 @@ -47,6 +49,9 @@ jobs: - name: Rust cache uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - name: Rustfmt (check) + run: cargo fmt --all -- --check + - name: Cargo check (all features) run: cargo check --all-targets --all-features diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5170303..784cb4a 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -133,6 +133,8 @@ jobs: - name: Install Rust (stable) uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable + with: + components: rustfmt - name: Setup sccache uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 @@ -140,6 +142,9 @@ jobs: - name: Rust cache uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - name: Rustfmt (check) + run: cargo fmt --all -- --check + - name: Cargo package run: cargo package diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b83f3ad..e4b783a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,6 +36,8 @@ jobs: - name: Install Rust (stable) uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable + with: + components: rustfmt - name: Setup sccache uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 @@ -43,6 +45,9 @@ jobs: - name: Rust cache uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + - name: Rustfmt (check) + run: cargo fmt --all -- --check + - name: Cargo package run: cargo package diff --git a/AGENTS.md b/AGENTS.md index 19debdf..5230aee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,7 @@ ## Commit & Pull Request Guidelines +- Before committing, run `cargo fmt` (CI enforces `cargo fmt --all -- --check`). - Commit messages in this repo are short and imperative (e.g., “Update lib.rs”, “Fix compile issues”). Follow that pattern and mention the touched area. - Open PRs against the `Dev` branch; include a clear description, rationale, and any relevant issue links. - Ensure CI passes locally where possible (at minimum `cargo test`, ideally the full-feature test command above). diff --git a/src/events.rs b/src/events.rs index e974749..a53651c 100644 --- a/src/events.rs +++ b/src/events.rs @@ -14,7 +14,9 @@ pub fn subscribe( name: &'static str, mut handler: impl FnMut(Value) -> Result<(), PluginError> + Send + 'static, ) -> Result<(), PluginError> { - on_event(name, move |_ctx: &mut PluginCtx, payload: Value| handler(payload)); + on_event(name, move |_ctx: &mut PluginCtx, payload: Value| { + handler(payload) + }); subscribe_host(name) } diff --git a/src/plugin_logging.rs b/src/plugin_logging.rs index 430c15a..856702f 100644 --- a/src/plugin_logging.rs +++ b/src/plugin_logging.rs @@ -1,5 +1,5 @@ -use std::sync::OnceLock; use std::io::Write; +use std::sync::OnceLock; fn parse_level_filter_from_env() -> log::LevelFilter { let raw = std::env::var("OPENVCS_LOG") diff --git a/src/plugin_runtime.rs b/src/plugin_runtime.rs index abd74f3..85937ae 100644 --- a/src/plugin_runtime.rs +++ b/src/plugin_runtime.rs @@ -2,10 +2,10 @@ use crate::models::VcsEvent; use crate::plugin_protocol::{PluginMessage, RpcRequest}; -use crate::plugin_stdio::{PluginError, receive_message, respond_shared, send_message_shared}; use crate::plugin_stdio::ok_null; -use std::collections::VecDeque; +use crate::plugin_stdio::{PluginError, receive_message, respond_shared, send_message_shared}; use std::collections::HashMap; +use std::collections::VecDeque; use std::io::{self, BufReader, LineWriter}; use std::sync::{Arc, Mutex, OnceLock}; use std::time::Duration; @@ -175,7 +175,10 @@ pub fn on_event( handler: impl FnMut(&mut PluginCtx, serde_json::Value) -> EventHandlerResult + Send + 'static, ) { if let Ok(mut lock) = registry().lock() { - lock.event.entry(name.to_string()).or_default().push(Box::new(handler)); + lock.event + .entry(name.to_string()) + .or_default() + .push(Box::new(handler)); } } @@ -195,8 +198,8 @@ fn dispatch_registered(ctx: &mut PluginCtx, req: RpcRequest) -> HandlerResult { #[serde(default)] payload: serde_json::Value, } - let p: P = crate::plugin_stdio::parse_json_params(req.params) - .map_err(PluginError::message)?; + let p: P = + crate::plugin_stdio::parse_json_params(req.params).map_err(PluginError::message)?; if let Ok(mut lock) = registry().lock() { if let Some(handlers) = lock.event.get_mut(&p.name) { From 55734617ba460ae0e656c34bbadfc5aaa83d4b21 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 17:14:34 +0000 Subject: [PATCH 25/52] Improved core --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/lib.rs | 71 +++++++++++++++++----- src/models.rs | 85 +++++++++++++++++++++++++- src/plugin_logging.rs | 56 ----------------- src/plugin_protocol.rs | 59 ++++++++++++++++++ src/plugin_runtime.rs | 135 +++++++++++++++++++++++++++++++++++++++++ src/plugin_stdio.rs | 121 ++++++++++++++++++++++++++++++++++-- 8 files changed, 451 insertions(+), 80 deletions(-) delete mode 100644 src/plugin_logging.rs diff --git a/Cargo.lock b/Cargo.lock index d708ea8..626d2b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,7 +42,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "openvcs-core" -version = "0.1.0" +version = "0.1.1" dependencies = [ "linkme", "log", diff --git a/Cargo.toml b/Cargo.toml index cb3e509..999c155 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openvcs-core" -version = "0.1.0" +version = "0.1.1" edition = "2024" description = "Core types and traits for OpenVCS." license = "GPL-3.0-or-later" diff --git a/src/lib.rs b/src/lib.rs index 778d99f..2a52338 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,23 +26,39 @@ pub mod plugin_runtime; #[cfg(feature = "plugin-protocol")] pub mod events; -#[cfg(feature = "plugin-protocol")] -mod plugin_logging; - #[doc(hidden)] -pub fn __ensure_plugin_logging_initialized() { +pub fn __plugin_log_to_client(level: log::Level, target: &str, args: std::fmt::Arguments<'_>) { #[cfg(feature = "plugin-protocol")] - crate::plugin_logging::ensure_initialized(); + { + let msg = format!("[{level}] {target}: {args}"); + if let Ok(out) = crate::host::stdout() { + crate::plugin_stdio::send_message_shared( + out, + &crate::plugin_protocol::PluginMessage::Event { + event: match level { + log::Level::Error => crate::models::VcsEvent::Error { msg }, + log::Level::Warn => crate::models::VcsEvent::Warning { msg }, + log::Level::Info | log::Level::Debug | log::Level::Trace => { + crate::models::VcsEvent::Info { msg } + } + }, + }, + ); + return; + } + } + + let _ = (level, target, args); } #[macro_export] macro_rules! trace { (target: $target:expr, $($arg:tt)+) => {{ - $crate::__ensure_plugin_logging_initialized(); + $crate::__plugin_log_to_client($crate::__log::Level::Trace, $target, format_args!($($arg)+)); $crate::__log::trace!(target: $target, $($arg)+); }}; ($($arg:tt)+) => {{ - $crate::__ensure_plugin_logging_initialized(); + $crate::__plugin_log_to_client($crate::__log::Level::Trace, module_path!(), format_args!($($arg)+)); $crate::__log::trace!($($arg)+); }}; } @@ -50,11 +66,11 @@ macro_rules! trace { #[macro_export] macro_rules! debug { (target: $target:expr, $($arg:tt)+) => {{ - $crate::__ensure_plugin_logging_initialized(); + $crate::__plugin_log_to_client($crate::__log::Level::Debug, $target, format_args!($($arg)+)); $crate::__log::debug!(target: $target, $($arg)+); }}; ($($arg:tt)+) => {{ - $crate::__ensure_plugin_logging_initialized(); + $crate::__plugin_log_to_client($crate::__log::Level::Debug, module_path!(), format_args!($($arg)+)); $crate::__log::debug!($($arg)+); }}; } @@ -62,11 +78,11 @@ macro_rules! debug { #[macro_export] macro_rules! info { (target: $target:expr, $($arg:tt)+) => {{ - $crate::__ensure_plugin_logging_initialized(); + $crate::__plugin_log_to_client($crate::__log::Level::Info, $target, format_args!($($arg)+)); $crate::__log::info!(target: $target, $($arg)+); }}; ($($arg:tt)+) => {{ - $crate::__ensure_plugin_logging_initialized(); + $crate::__plugin_log_to_client($crate::__log::Level::Info, module_path!(), format_args!($($arg)+)); $crate::__log::info!($($arg)+); }}; } @@ -74,11 +90,11 @@ macro_rules! info { #[macro_export] macro_rules! warn { (target: $target:expr, $($arg:tt)+) => {{ - $crate::__ensure_plugin_logging_initialized(); + $crate::__plugin_log_to_client($crate::__log::Level::Warn, $target, format_args!($($arg)+)); $crate::__log::warn!(target: $target, $($arg)+); }}; ($($arg:tt)+) => {{ - $crate::__ensure_plugin_logging_initialized(); + $crate::__plugin_log_to_client($crate::__log::Level::Warn, module_path!(), format_args!($($arg)+)); $crate::__log::warn!($($arg)+); }}; } @@ -86,11 +102,11 @@ macro_rules! warn { #[macro_export] macro_rules! error { (target: $target:expr, $($arg:tt)+) => {{ - $crate::__ensure_plugin_logging_initialized(); + $crate::__plugin_log_to_client($crate::__log::Level::Error, $target, format_args!($($arg)+)); $crate::__log::error!(target: $target, $($arg)+); }}; ($($arg:tt)+) => {{ - $crate::__ensure_plugin_logging_initialized(); + $crate::__plugin_log_to_client($crate::__log::Level::Error, module_path!(), format_args!($($arg)+)); $crate::__log::error!($($arg)+); }}; } @@ -285,3 +301,28 @@ pub trait Vcs: Send + Sync { Err(VcsError::Unsupported(self.id())) } } + +#[cfg(test)] +mod tests { + #[test] + fn log_macros_are_callable() { + crate::trace!("trace"); + crate::debug!("debug"); + crate::info!("info"); + crate::warn!("warn"); + crate::error!("error"); + } + + #[cfg(feature = "vcs")] + #[test] + fn vcs_error_formats_useful_messages() { + let e = crate::VcsError::Unsupported(crate::BackendId::from("git")); + assert!(e.to_string().contains("unsupported backend")); + + let e = crate::VcsError::Backend { + backend: crate::BackendId::from("git"), + msg: "boom".into(), + }; + assert_eq!(e.to_string(), "git: boom"); + } +} diff --git a/src/models.rs b/src/models.rs index b270e31..fe22e75 100644 --- a/src/models.rs +++ b/src/models.rs @@ -129,7 +129,9 @@ pub enum VcsEvent { Info { msg: String, }, - RemoteMessage(String), + RemoteMessage { + msg: String, + }, Progress { phase: String, detail: String, @@ -142,8 +144,12 @@ pub enum VcsEvent { refname: String, status: Option, }, - Warning(String), - Error(String), + Warning { + msg: String, + }, + Error { + msg: String, + }, } pub type OnEvent = Arc; @@ -170,4 +176,77 @@ mod tests { assert_eq!(summary.staged, 0); assert_eq!(summary.conflicted, 0); } + + #[test] + fn branch_kind_roundtrips_via_json() { + let local = BranchKind::Local; + let local_json = serde_json::to_value(&local).expect("serialize"); + let local_back: BranchKind = serde_json::from_value(local_json).expect("deserialize"); + assert_eq!(local_back, BranchKind::Local); + + let remote = BranchKind::Remote { + remote: "origin".into(), + }; + let remote_json = serde_json::to_value(&remote).expect("serialize"); + let remote_back: BranchKind = serde_json::from_value(remote_json).expect("deserialize"); + assert_eq!(remote_back, remote); + } + + #[test] + fn conflict_side_serializes_as_kebab_case_strings() { + let ours = serde_json::to_value(ConflictSide::Ours).expect("serialize"); + let theirs = serde_json::to_value(ConflictSide::Theirs).expect("serialize"); + assert_eq!(ours, serde_json::Value::String("ours".into())); + assert_eq!(theirs, serde_json::Value::String("theirs".into())); + + let ours_back: ConflictSide = serde_json::from_value(serde_json::json!("ours")).unwrap(); + let theirs_back: ConflictSide = serde_json::from_value(serde_json::json!("theirs")).unwrap(); + assert_eq!(ours_back, ConflictSide::Ours); + assert_eq!(theirs_back, ConflictSide::Theirs); + } + + #[test] + fn file_entry_deserializes_optional_fields_with_defaults() { + let v = serde_json::json!({ + "path": "a.txt", + "status": "M", + "hunks": [] + }); + + let entry: FileEntry = serde_json::from_value(v).expect("deserialize"); + assert_eq!(entry.path, "a.txt"); + assert_eq!(entry.status, "M"); + assert!(entry.old_path.is_none()); + assert!(!entry.staged); + assert!(!entry.resolved_conflict); + assert!(entry.hunks.is_empty()); + } + + #[test] + fn vcs_event_roundtrips_via_json() { + let events = vec![ + VcsEvent::Info { + msg: "hello".into(), + }, + VcsEvent::Progress { + phase: "fetch".into(), + detail: "10/20".into(), + }, + VcsEvent::Auth { + method: "ssh".into(), + detail: "key".into(), + }, + VcsEvent::RemoteMessage { + msg: "remote".into(), + }, + VcsEvent::Warning { msg: "warn".into() }, + VcsEvent::Error { msg: "err".into() }, + ]; + + for e in events { + let v = serde_json::to_value(&e).expect("serialize"); + let back: VcsEvent = serde_json::from_value(v).expect("deserialize"); + assert_eq!(format!("{e:?}"), format!("{back:?}")); + } + } } diff --git a/src/plugin_logging.rs b/src/plugin_logging.rs deleted file mode 100644 index 856702f..0000000 --- a/src/plugin_logging.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::io::Write; -use std::sync::OnceLock; - -fn parse_level_filter_from_env() -> log::LevelFilter { - let raw = std::env::var("OPENVCS_LOG") - .ok() - .or_else(|| std::env::var("RUST_LOG").ok()); - - match raw.as_deref().map(str::trim) { - None | Some("") => log::LevelFilter::Info, - Some("off") | Some("OFF") => log::LevelFilter::Off, - Some("error") | Some("ERROR") => log::LevelFilter::Error, - Some("warn") | Some("WARN") | Some("warning") | Some("WARNING") => log::LevelFilter::Warn, - Some("info") | Some("INFO") => log::LevelFilter::Info, - Some("debug") | Some("DEBUG") => log::LevelFilter::Debug, - Some("trace") | Some("TRACE") => log::LevelFilter::Trace, - _ => log::LevelFilter::Info, - } -} - -struct StderrLogger; - -impl log::Log for StderrLogger { - fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { - metadata.level() <= log::max_level() - } - - fn log(&self, record: &log::Record<'_>) { - if !self.enabled(record.metadata()) { - return; - } - let mut stderr = std::io::stderr().lock(); - let _ = writeln!( - stderr, - "[{}] {}: {}", - record.level(), - record.target(), - record.args() - ); - } - - fn flush(&self) {} -} - -static INIT: OnceLock<()> = OnceLock::new(); -static LOGGER: StderrLogger = StderrLogger; - -pub(crate) fn ensure_initialized() { - INIT.get_or_init(|| { - let level = parse_level_filter_from_env(); - // Only install the logger if nothing else already did. - if log::set_logger(&LOGGER).is_ok() { - log::set_max_level(level); - } - }); -} diff --git a/src/plugin_protocol.rs b/src/plugin_protocol.rs index 6ef1974..e7bd41e 100644 --- a/src/plugin_protocol.rs +++ b/src/plugin_protocol.rs @@ -33,3 +33,62 @@ pub enum PluginMessage { Response(RpcResponse), Event { event: VcsEvent }, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rpc_request_params_defaults_to_null() { + let req: RpcRequest = + serde_json::from_str(r#"{"id":1,"method":"ping"}"#).expect("valid request"); + assert_eq!(req.id, 1); + assert_eq!(req.method, "ping"); + assert_eq!(req.params, serde_json::Value::Null); + } + + #[test] + fn rpc_response_fields_default_when_missing() { + let resp: RpcResponse = + serde_json::from_str(r#"{"id":9,"ok":true}"#).expect("valid response"); + assert_eq!(resp.id, 9); + assert!(resp.ok); + assert_eq!(resp.result, serde_json::Value::Null); + assert!(resp.error.is_none()); + assert!(resp.error_code.is_none()); + assert!(resp.error_data.is_none()); + } + + #[test] + fn rpc_response_skips_optional_error_fields_when_none() { + let resp = RpcResponse { + id: 1, + ok: false, + result: serde_json::Value::Null, + error: Some("boom".into()), + error_code: None, + error_data: None, + }; + + let value = serde_json::to_value(&resp).expect("serializes"); + assert!(value.get("error").is_some()); + assert!(value.get("error_code").is_none()); + assert!(value.get("error_data").is_none()); + } + + #[test] + fn plugin_message_deserializes_event_variant() { + let msg: PluginMessage = serde_json::from_str( + r#"{"event":{"type":"info","msg":"hello"}}"#, + ) + .expect("valid event message"); + + match msg { + PluginMessage::Event { event } => match event { + VcsEvent::Info { msg } => assert_eq!(msg, "hello"), + other => panic!("unexpected event: {other:?}"), + }, + other => panic!("unexpected message: {other:?}"), + } + } +} diff --git a/src/plugin_runtime.rs b/src/plugin_runtime.rs index 85937ae..1817b36 100644 --- a/src/plugin_runtime.rs +++ b/src/plugin_runtime.rs @@ -237,3 +237,138 @@ pub fn run(mut handle: impl FnMut(&mut PluginCtx, RpcRequest) -> HandlerResult) while rt.tick(|ctx, req| handle(ctx, req))? {} Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::OnceLock; + + static TEST_LOCK: OnceLock> = OnceLock::new(); + + fn with_test_lock(f: impl FnOnce() -> T) -> T { + let lock = TEST_LOCK.get_or_init(|| Mutex::new(())); + let _guard = lock.lock().expect("test lock"); + f() + } + + fn reset_registry() { + if let Ok(mut lock) = registry().lock() { + lock.rpc.clear(); + lock.event.clear(); + lock.fallback = None; + } + } + + fn test_ctx() -> PluginCtx { + PluginCtx { + stdout: Arc::new(Mutex::new(LineWriter::new(io::stdout()))), + } + } + + #[test] + fn dispatch_registered_routes_to_rpc_handler() { + with_test_lock(|| { + reset_registry(); + register_delegate("ping", |_ctx, req| { + assert_eq!(req.method, "ping"); + crate::plugin_stdio::ok(serde_json::json!({"pong": true})) + }); + + let mut ctx = test_ctx(); + let req = RpcRequest { + id: 1, + method: "ping".into(), + params: serde_json::Value::Null, + }; + let res = dispatch_registered(&mut ctx, req).expect("ok"); + assert_eq!(res, serde_json::json!({"pong": true})); + }); + } + + #[test] + fn dispatch_registered_uses_fallback_when_no_handler() { + with_test_lock(|| { + reset_registry(); + set_fallback_rpc(|_ctx, req| { + crate::plugin_stdio::ok(serde_json::json!({ "method": req.method })) + }); + + let mut ctx = test_ctx(); + let req = RpcRequest { + id: 1, + method: "unknown".into(), + params: serde_json::Value::Null, + }; + + let res = dispatch_registered(&mut ctx, req).expect("ok"); + assert_eq!(res, serde_json::json!({ "method": "unknown" })); + }); + } + + #[test] + fn dispatch_registered_returns_unknown_method_error_when_unhandled() { + with_test_lock(|| { + reset_registry(); + + let mut ctx = test_ctx(); + let req = RpcRequest { + id: 1, + method: "unknown".into(), + params: serde_json::Value::Null, + }; + + let err = dispatch_registered(&mut ctx, req).expect_err("should error"); + assert_eq!(err.code.as_deref(), Some("plugin.unknown_method")); + assert!(err.message.contains("unknown method")); + }); + } + + #[test] + fn dispatch_registered_event_dispatch_invokes_handlers() { + with_test_lock(|| { + reset_registry(); + + let seen: Arc>> = Arc::new(Mutex::new(Vec::new())); + let seen_1 = Arc::clone(&seen); + on_event("evt", move |_ctx, payload| { + if let Ok(mut lock) = seen_1.lock() { + lock.push(payload); + } + Ok(()) + }); + + let mut ctx = test_ctx(); + let req = RpcRequest { + id: 1, + method: "event.dispatch".into(), + params: serde_json::json!({ "name": "evt", "payload": { "x": 1 } }), + }; + + let res = dispatch_registered(&mut ctx, req).expect("ok"); + assert_eq!(res, serde_json::Value::Null); + + let lock = seen.lock().expect("lock"); + assert_eq!(lock.as_slice(), &[serde_json::json!({ "x": 1 })]); + }); + } + + #[test] + fn dispatch_registered_event_dispatch_propagates_handler_error() { + with_test_lock(|| { + reset_registry(); + + on_event("evt", |_ctx, _payload| Err(PluginError::code("evt.fail", "nope"))); + + let mut ctx = test_ctx(); + let req = RpcRequest { + id: 1, + method: "event.dispatch".into(), + params: serde_json::json!({ "name": "evt", "payload": null }), + }; + + let err = dispatch_registered(&mut ctx, req).expect_err("should error"); + assert_eq!(err.code.as_deref(), Some("evt.fail")); + assert_eq!(err.message, "nope"); + }); + } +} diff --git a/src/plugin_stdio.rs b/src/plugin_stdio.rs index f26c35f..ffcca73 100644 --- a/src/plugin_stdio.rs +++ b/src/plugin_stdio.rs @@ -42,7 +42,6 @@ pub fn err_display(err: impl std::fmt::Display) -> PluginError { } pub fn receive_message(stdin: &mut R) -> Option { - crate::plugin_logging::ensure_initialized(); let mut line = String::new(); loop { line.clear(); @@ -61,7 +60,6 @@ pub fn receive_message(stdin: &mut R) -> Option { } pub fn write_message(out: &mut W, msg: &PluginMessage) -> io::Result<()> { - crate::plugin_logging::ensure_initialized(); let line = serde_json::to_string(msg).unwrap_or_else(|_| "{}".into()); writeln!(out, "{line}")?; out.flush()?; @@ -69,7 +67,6 @@ pub fn write_message(out: &mut W, msg: &PluginMessage) -> io::Result<( } pub fn send_message_shared(out: &Arc>, msg: &PluginMessage) { - crate::plugin_logging::ensure_initialized(); if let Ok(mut w) = out.lock() { let _ = write_message(&mut *w, msg); } @@ -141,7 +138,6 @@ pub fn call_host( params: Value, timeout: Duration, ) -> Result { - crate::plugin_logging::ensure_initialized(); let id = { let mut lock = ids .lock() @@ -211,3 +207,120 @@ pub fn call_host( } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::VcsEvent; + use std::io::Cursor; + + #[test] + fn plugin_error_builders_set_fields() { + let err = PluginError::message("nope"); + assert!(err.code.is_none()); + assert_eq!(err.message, "nope"); + assert!(err.data.is_none()); + + let err = PluginError::code("x.y", "bad").with_data(serde_json::json!({"k": 1})); + assert_eq!(err.code.as_deref(), Some("x.y")); + assert_eq!(err.message, "bad"); + assert_eq!(err.data, Some(serde_json::json!({"k": 1}))); + } + + #[test] + fn receive_message_skips_blank_and_invalid_lines() { + let input = b"\n \nnot json\n{\"id\":1,\"method\":\"ping\"}\n"; + let mut cursor = Cursor::new(&input[..]); + + let msg = receive_message(&mut cursor).expect("message"); + match msg { + PluginMessage::Request(req) => { + assert_eq!(req.id, 1); + assert_eq!(req.method, "ping"); + assert_eq!(req.params, Value::Null); + } + other => panic!("unexpected message: {other:?}"), + } + } + + #[test] + fn receive_request_ignores_non_request_messages() { + let input = b"{\"id\":7,\"ok\":true,\"result\":null}\n{\"event\":{\"type\":\"info\",\"msg\":\"hi\"}}\n{\"id\":1,\"method\":\"ping\"}\n"; + let mut cursor = Cursor::new(&input[..]); + + let req = receive_request(&mut cursor).expect("request"); + assert_eq!(req.id, 1); + assert_eq!(req.method, "ping"); + } + + #[test] + fn write_message_writes_one_json_line() { + let msg = PluginMessage::Event { + event: VcsEvent::Info { + msg: "hello".into(), + }, + }; + + let mut out = Vec::::new(); + write_message(&mut out, &msg).expect("write ok"); + assert!(out.ends_with(b"\n")); + + let line = std::str::from_utf8(&out).expect("utf-8"); + let parsed: PluginMessage = serde_json::from_str(line.trim()).expect("valid message"); + match parsed { + PluginMessage::Event { event } => match event { + VcsEvent::Info { msg } => assert_eq!(msg, "hello"), + other => panic!("unexpected event: {other:?}"), + }, + other => panic!("unexpected message: {other:?}"), + } + } + + #[test] + fn parse_json_params_errors_are_prefixed() { + let err = parse_json_params::>(Value::String("x".into())) + .expect_err("should fail"); + assert!(err.starts_with("invalid params:")); + } + + #[test] + fn call_host_returns_ok_and_queues_incoming_requests() { + let out = Arc::new(Mutex::new(Vec::::new())); + let stdin = Arc::new(Mutex::new(Cursor::new( + b"{\"id\":999,\"ok\":true,\"result\":{\"ignored\":true}}\n\ + {\"id\":77,\"method\":\"noop\",\"params\":null}\n\ + {\"id\":5,\"ok\":true,\"result\":{\"answer\":42}}\n" as &[u8], + ))); + let queue = Arc::new(Mutex::new(VecDeque::::new())); + let ids = Arc::new(Mutex::new(RequestIdState { next_id: 5 })); + + let result = call_host( + &out, + &stdin, + &queue, + &ids, + "math.answer", + serde_json::json!({}), + Duration::from_secs(1), + ) + .expect("host call ok"); + assert_eq!(result, serde_json::json!({"answer": 42})); + + let queue = queue.lock().expect("queue lock"); + assert_eq!(queue.len(), 1); + assert_eq!(queue[0].id, 77); + assert_eq!(queue[0].method, "noop"); + + let out = out.lock().expect("out lock"); + let line = std::str::from_utf8(&out).expect("utf-8"); + let first = line.lines().next().expect("at least one line"); + let sent: PluginMessage = serde_json::from_str(first).expect("valid sent message"); + match sent { + PluginMessage::Request(req) => { + assert_eq!(req.id, 5); + assert_eq!(req.method, "math.answer"); + } + other => panic!("unexpected sent message: {other:?}"), + } + } +} From ab8249fa6f4ea6283e22736959e7e26dc89b9272 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 17:17:33 +0000 Subject: [PATCH 26/52] Update README.md --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 21841aa..294f544 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,54 @@ -# OpenVCS-Core +# OpenVCS Core (`openvcs-core`) -Shared Rust crate for OpenVCS plugins and the OpenVCS client. +[![Dev CI (fast)](https://github.com/Open-VCS/OpenVCS-Core/actions/workflows/ci.yml/badge.svg)](https://github.com/Open-VCS/OpenVCS-Core/actions/workflows/ci.yml) + +Shared Rust crate for: +- OpenVCS plugins (JSON-RPC over stdio) +- The OpenVCS client/host (shared models + backend trait surface) ## Cargo features -- `plugin-protocol` (default): JSON RPC message types for plugin stdio communication. -- `vcs`: the `Vcs` trait + `VcsError`. -- `backend-registry`: backend registry helpers (requires `vcs`). -## Plugin helpers -- `openvcs_core::plugin_stdio`: small helpers for stdio JSON-RPC plugins (read/write messages and send a single `respond_shared(...)` call). +- `plugin-protocol` (default): JSON-RPC wire types + plugin helper modules: + - `openvcs_core::plugin_protocol` (`PluginMessage`, `RpcRequest`, `RpcResponse`) + - `openvcs_core::plugin_stdio` (read/write helpers, `respond_shared`, host calls) + - `openvcs_core::plugin_runtime` (simple request dispatch loop + handler registry) + - `openvcs_core::events` (host event subscribe/emit helpers) + - `openvcs_core::host` (bridge for calling the host over stdio) +- `vcs`: the backend trait surface: + - `openvcs_core::Vcs`, `openvcs_core::VcsError`, `openvcs_core::Result` + - `openvcs_core::models` (shared request/response/event types) + - enables `backend-registry` +- `backend-registry`: link-time backend discovery via `openvcs_core::backend_descriptor::BACKENDS` + - intended to be enabled together with `vcs` + - on `wasm32`, the registry is always empty (no `linkme` support) + +## Plugin quickstart (stdio JSON-RPC) + +Register one or more RPC handlers and run the dispatch loop: + +```rust +use openvcs_core::plugin_runtime::{register_delegate, run_registered}; +use openvcs_core::plugin_stdio::ok; + +fn main() -> std::io::Result<()> { + register_delegate("ping", |_ctx, _req| ok(serde_json::json!({ "pong": true }))); + run_registered() +} +``` + +Notes: +- `openvcs_core::{trace, debug, info, warn, error}` forward logs to the OpenVCS host when available (and also emit normal `log` records). +- Host calls from plugins go through `openvcs_core::host::call(...)` (the runtime initializes the host bridge for stdio). +- `OPENVCS_PLUGIN_HOST_TIMEOUT_MS` controls host call timeouts (default: 60000ms). + +## Development + +Common checks (matches CI): +- `cargo check --all-targets --all-features` +- `cargo test` +- `cargo test --no-default-features --features plugin-protocol,vcs,backend-registry` +- `cargo package` + +## License + +GPL-3.0-or-later (see `LICENSE`). From dc8a7f12ec421cf5ac2eade44b7465a2a075c467 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 17:49:20 +0000 Subject: [PATCH 27/52] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 294f544..2a4f332 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Notes: ## Development Common checks (matches CI): +- `cargo fmt --all -- --check` - `cargo check --all-targets --all-features` - `cargo test` - `cargo test --no-default-features --features plugin-protocol,vcs,backend-registry` From 5e6cab0e2e9cfc9721dcb248f75c425b7c5426ce Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 18:58:41 +0000 Subject: [PATCH 28/52] Update --- src/models.rs | 3 ++- src/plugin_protocol.rs | 6 ++---- src/plugin_runtime.rs | 4 +++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/models.rs b/src/models.rs index fe22e75..586ed75 100644 --- a/src/models.rs +++ b/src/models.rs @@ -200,7 +200,8 @@ mod tests { assert_eq!(theirs, serde_json::Value::String("theirs".into())); let ours_back: ConflictSide = serde_json::from_value(serde_json::json!("ours")).unwrap(); - let theirs_back: ConflictSide = serde_json::from_value(serde_json::json!("theirs")).unwrap(); + let theirs_back: ConflictSide = + serde_json::from_value(serde_json::json!("theirs")).unwrap(); assert_eq!(ours_back, ConflictSide::Ours); assert_eq!(theirs_back, ConflictSide::Theirs); } diff --git a/src/plugin_protocol.rs b/src/plugin_protocol.rs index e7bd41e..878c1d8 100644 --- a/src/plugin_protocol.rs +++ b/src/plugin_protocol.rs @@ -78,10 +78,8 @@ mod tests { #[test] fn plugin_message_deserializes_event_variant() { - let msg: PluginMessage = serde_json::from_str( - r#"{"event":{"type":"info","msg":"hello"}}"#, - ) - .expect("valid event message"); + let msg: PluginMessage = serde_json::from_str(r#"{"event":{"type":"info","msg":"hello"}}"#) + .expect("valid event message"); match msg { PluginMessage::Event { event } => match event { diff --git a/src/plugin_runtime.rs b/src/plugin_runtime.rs index 1817b36..ac6854a 100644 --- a/src/plugin_runtime.rs +++ b/src/plugin_runtime.rs @@ -357,7 +357,9 @@ mod tests { with_test_lock(|| { reset_registry(); - on_event("evt", |_ctx, _payload| Err(PluginError::code("evt.fail", "nope"))); + on_event("evt", |_ctx, _payload| { + Err(PluginError::code("evt.fail", "nope")) + }); let mut ctx = test_ctx(); let req = RpcRequest { From fa560c2d43d38d0cd89de86a382156b58848bb3f Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 18:59:46 +0000 Subject: [PATCH 29/52] Version bump --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 626d2b1..11ed28e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,7 +42,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "openvcs-core" -version = "0.1.1" +version = "0.1.2" dependencies = [ "linkme", "log", diff --git a/Cargo.toml b/Cargo.toml index 999c155..8f5fa99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openvcs-core" -version = "0.1.1" +version = "0.1.2" edition = "2024" description = "Core types and traits for OpenVCS." license = "GPL-3.0-or-later" From e5470926a671032a5634632eefd4bd18b2e269a8 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 19:22:23 +0000 Subject: [PATCH 30/52] Improved workflows --- .github/workflows/ci.yml | 5 ++++- .github/workflows/nightly.yml | 5 ++++- .github/workflows/release.yml | 5 ++++- Justfile | 9 +++++++++ README.md | 2 ++ 5 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 Justfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb0e469..2a5b246 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: - name: Install Rust (stable) uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable with: - components: rustfmt + components: rustfmt, clippy - name: Setup sccache uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 @@ -52,6 +52,9 @@ jobs: - name: Rustfmt (check) run: cargo fmt --all -- --check + - name: Cargo clippy (all features) + run: cargo clippy --all-targets --all-features -- -D warnings + - name: Cargo check (all features) run: cargo check --all-targets --all-features diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 784cb4a..540f900 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -134,7 +134,7 @@ jobs: - name: Install Rust (stable) uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable with: - components: rustfmt + components: rustfmt, clippy - name: Setup sccache uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 @@ -145,6 +145,9 @@ jobs: - name: Rustfmt (check) run: cargo fmt --all -- --check + - name: Cargo clippy (all features) + run: cargo clippy --all-targets --all-features -- -D warnings + - name: Cargo package run: cargo package diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4b783a..399b574 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: - name: Install Rust (stable) uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable with: - components: rustfmt + components: rustfmt, clippy - name: Setup sccache uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 @@ -48,6 +48,9 @@ jobs: - name: Rustfmt (check) run: cargo fmt --all -- --check + - name: Cargo clippy (all features) + run: cargo clippy --all-targets --all-features -- -D warnings + - name: Cargo package run: cargo package diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..3563cac --- /dev/null +++ b/Justfile @@ -0,0 +1,9 @@ +set shell := ["bash", "-eu", "-o", "pipefail", "-c"] + +default: + @just --list + +fix: + cargo fmt --all + cargo clippy --all-targets --all-features -- -D warnings + diff --git a/README.md b/README.md index 2a4f332..c2f77ee 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,9 @@ Notes: ## Development Common checks (matches CI): +- `just fix` (runs rustfmt + clippy) - `cargo fmt --all -- --check` +- `cargo clippy --all-targets --all-features -- -D warnings` - `cargo check --all-targets --all-features` - `cargo test` - `cargo test --no-default-features --features plugin-protocol,vcs,backend-registry` From 9fac44286323e7299913089e8e1a273d571fbe16 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 19:31:04 +0000 Subject: [PATCH 31/52] Update plugin_runtime.rs --- src/plugin_runtime.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/plugin_runtime.rs b/src/plugin_runtime.rs index ac6854a..f134153 100644 --- a/src/plugin_runtime.rs +++ b/src/plugin_runtime.rs @@ -43,10 +43,10 @@ fn next_request( queue: &Arc>>, stdin: &Arc>>, ) -> Option { - if let Ok(mut q) = queue.lock() { - if let Some(req) = q.pop_front() { - return Some(req); - } + if let Ok(mut q) = queue.lock() + && let Some(req) = q.pop_front() + { + return Some(req); } loop { @@ -201,11 +201,11 @@ fn dispatch_registered(ctx: &mut PluginCtx, req: RpcRequest) -> HandlerResult { let p: P = crate::plugin_stdio::parse_json_params(req.params).map_err(PluginError::message)?; - if let Ok(mut lock) = registry().lock() { - if let Some(handlers) = lock.event.get_mut(&p.name) { - for h in handlers.iter_mut() { - h(ctx, p.payload.clone())?; - } + if let Ok(mut lock) = registry().lock() + && let Some(handlers) = lock.event.get_mut(&p.name) + { + for h in handlers.iter_mut() { + h(ctx, p.payload.clone())?; } } return ok_null(); @@ -228,7 +228,7 @@ fn dispatch_registered(ctx: &mut PluginCtx, req: RpcRequest) -> HandlerResult { pub fn run_registered() -> io::Result<()> { let mut rt = PluginRuntime::init(); - while rt.tick(|ctx, req| dispatch_registered(ctx, req))? {} + while rt.tick(dispatch_registered)? {} Ok(()) } From 9d1977a9b9ea47f898e20ac146a5e5c65d7b0dcb Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 19:31:08 +0000 Subject: [PATCH 32/52] Update Justfile --- Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Justfile b/Justfile index 3563cac..1121b5e 100644 --- a/Justfile +++ b/Justfile @@ -5,5 +5,5 @@ default: fix: cargo fmt --all - cargo clippy --all-targets --all-features -- -D warnings + cargo clippy --fix --allow-dirty --allow-staged From 5a8a6fb0ebe540b6a57f593df4eb04a1243a1855 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 19:39:47 +0000 Subject: [PATCH 33/52] Version bump --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/events.rs | 2 -- src/host.rs | 2 -- src/plugin_runtime.rs | 2 -- 5 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11ed28e..5288271 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,7 +42,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "openvcs-core" -version = "0.1.2" +version = "0.1.3" dependencies = [ "linkme", "log", diff --git a/Cargo.toml b/Cargo.toml index 8f5fa99..151cc84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openvcs-core" -version = "0.1.2" +version = "0.1.3" edition = "2024" description = "Core types and traits for OpenVCS." license = "GPL-3.0-or-later" diff --git a/src/events.rs b/src/events.rs index a53651c..f91a9d5 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "plugin-protocol")] - use crate::host; use crate::plugin_runtime::{PluginCtx, on_event}; use crate::plugin_stdio::PluginError; diff --git a/src/host.rs b/src/host.rs index 1f19118..077f19f 100644 --- a/src/host.rs +++ b/src/host.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "plugin-protocol")] - use crate::plugin_protocol::RpcRequest; use crate::plugin_stdio::{PluginError, RequestIdState, call_host}; use serde_json::Value; diff --git a/src/plugin_runtime.rs b/src/plugin_runtime.rs index f134153..4943526 100644 --- a/src/plugin_runtime.rs +++ b/src/plugin_runtime.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "plugin-protocol")] - use crate::models::VcsEvent; use crate::plugin_protocol::{PluginMessage, RpcRequest}; use crate::plugin_stdio::ok_null; From 03c1c8f26e20d481da0f7ef87b51845c320ea757 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 19:46:36 +0000 Subject: [PATCH 34/52] Update release.yml --- .github/workflows/release.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 399b574..3fc4dc8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,6 +54,14 @@ jobs: - name: Cargo package run: cargo package + - name: Publish to crates.io + if: secrets.CARGO_REGISTRY_TOKEN != '' + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + set -euo pipefail + cargo publish --token "$CARGO_REGISTRY_TOKEN" + - name: Determine tag id: tag shell: bash From 08ffa2ed8016af5079ba7ae67ec69b3fdd30de39 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 19:49:20 +0000 Subject: [PATCH 35/52] Update release.yml --- .github/workflows/release.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3fc4dc8..55bbba1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,8 +54,17 @@ jobs: - name: Cargo package run: cargo package + - name: Require crates.io token + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + set -euo pipefail + if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then + echo 'Error: secrets.CARGO_REGISTRY_TOKEN is not set. Aborting release.' + exit 1 + fi + - name: Publish to crates.io - if: secrets.CARGO_REGISTRY_TOKEN != '' env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: | From 371276d2d53eefe59357c655b8b2ca0a40c7ed06 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 19:50:21 +0000 Subject: [PATCH 36/52] Version Bump --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5288271..54cf82d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,7 +42,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "openvcs-core" -version = "0.1.3" +version = "0.1.4" dependencies = [ "linkme", "log", diff --git a/Cargo.toml b/Cargo.toml index 151cc84..4928314 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openvcs-core" -version = "0.1.3" +version = "0.1.4" edition = "2024" description = "Core types and traits for OpenVCS." license = "GPL-3.0-or-later" From b833211203e24448639e55e2313b838977a422b5 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 19:54:03 +0000 Subject: [PATCH 37/52] Remove duplicated cfg attribute from backend_descriptor.rs --- Justfile | 2 +- src/backend_descriptor.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Justfile b/Justfile index 1121b5e..66d931d 100644 --- a/Justfile +++ b/Justfile @@ -5,5 +5,5 @@ default: fix: cargo fmt --all - cargo clippy --fix --allow-dirty --allow-staged + cargo clippy --fix --all-targets --all-features --allow-dirty --allow-staged diff --git a/src/backend_descriptor.rs b/src/backend_descriptor.rs index c2d60d0..9b81280 100644 --- a/src/backend_descriptor.rs +++ b/src/backend_descriptor.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "backend-registry")] - use crate::backend_id::BackendId; use crate::models::{Capabilities, OnEvent}; use crate::{Result, Vcs}; From e034c0d0113ecca8aaabe71be98ac51d2ed8e99a Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 20:05:54 +0000 Subject: [PATCH 38/52] Update AGENTS.md --- AGENTS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 5230aee..785dfbe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,3 +33,6 @@ - Commit messages in this repo are short and imperative (e.g., “Update lib.rs”, “Fix compile issues”). Follow that pattern and mention the touched area. - Open PRs against the `Dev` branch; include a clear description, rationale, and any relevant issue links. - Ensure CI passes locally where possible (at minimum `cargo test`, ideally the full-feature test command above). + +- After making changes, run `just fix` to automatically apply formatting and simple fixes before committing. +- Commit changes locally with a clear, conventional message, but do NOT push to remotes or create PRs—leave pushing and PR creation to a human maintainer. From 7d281f02c4703b746a563f69fb2c76d75ed2838f Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 20:19:34 +0000 Subject: [PATCH 39/52] agents: require 72-char commit title in AGENTS.md Add guideline requiring a short (<=72 chars) title, a blank line, and optional body to AGENTS.md files for agents. --- AGENTS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 785dfbe..37ce10b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,9 @@ - Before committing, run `cargo fmt` (CI enforces `cargo fmt --all -- --check`). - Commit messages in this repo are short and imperative (e.g., “Update lib.rs”, “Fix compile issues”). Follow that pattern and mention the touched area. +- Commit message format: agents must format commit messages with a short + title of at most 72 characters, followed by a blank line and any + additional explanatory text in the body. - Open PRs against the `Dev` branch; include a clear description, rationale, and any relevant issue links. - Ensure CI passes locally where possible (at minimum `cargo test`, ideally the full-feature test command above). From dbaa32237531c3e84a1b4f73f17f86944e352157 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 31 Dec 2025 20:36:01 +0000 Subject: [PATCH 40/52] docs: note sandbox requirements for and cargo commands in AGENTS.md --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 37ce10b..6785d35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,3 +39,5 @@ - After making changes, run `just fix` to automatically apply formatting and simple fixes before committing. - Commit changes locally with a clear, conventional message, but do NOT push to remotes or create PRs—leave pushing and PR creation to a human maintainer. + +**Sandbox note**: Running `just fix` and some `cargo` commands (for example `cargo build` or commands that fetch dependencies or build native binaries) may require network access or host-level tooling and therefore should be run outside a restricted sandbox or container. If operating with sandboxing or restricted network access, request approval before executing these commands or run them on the host machine. From fd5e913605a74e8de8274b72003d19759aca194b Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 7 Jan 2026 17:55:39 +0000 Subject: [PATCH 41/52] Update Cargo.lock --- Cargo.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 54cf82d..e5f66a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,18 +53,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -101,9 +101,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", @@ -114,9 +114,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -151,6 +151,6 @@ checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "zmij" -version = "1.0.0" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" From e9e0d6410054b89fbae39c664f325829b26433e2 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 7 Jan 2026 23:47:24 +0000 Subject: [PATCH 42/52] Update version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e5f66a9..cd0b865 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,7 +42,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "openvcs-core" -version = "0.1.4" +version = "0.1.5" dependencies = [ "linkme", "log", diff --git a/Cargo.toml b/Cargo.toml index 4928314..6416ee2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openvcs-core" -version = "0.1.4" +version = "0.1.5" edition = "2024" description = "Core types and traits for OpenVCS." license = "GPL-3.0-or-later" From 27011b3a3196831a7ec9efbdfd2953a7dad22738 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 02:50:46 +0000 Subject: [PATCH 43/52] Bump github/codeql-action in the actions-minor-patch group (#5) Bumps the actions-minor-patch group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 4.31.9 to 4.31.10 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/5d4e8d1aca955e8d8589aabd499c5cae939e33c7...cdefb33c0f6224e58673d9004f47f7cb3e328b89) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.31.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8e1dbb1..53006cf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: fetch-depth: 0 - name: Initialize CodeQL - uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3 + uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -55,10 +55,10 @@ jobs: - name: Autobuild if: matrix.build-mode == 'autobuild' - uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3 + uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3 + uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3 with: category: "/language:${{ matrix.language }}" From 8215e5905e5a9d6a8baa14d9d454462e5a9735a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 02:51:12 +0000 Subject: [PATCH 44/52] Bump thiserror from 2.0.17 to 2.0.18 in the cargo-minor-patch group (#6) Bumps the cargo-minor-patch group with 1 update: [thiserror](https://github.com/dtolnay/thiserror). Updates `thiserror` from 2.0.17 to 2.0.18 - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/2.0.17...2.0.18) --- updated-dependencies: - dependency-name: thiserror dependency-version: 2.0.18 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd0b865..ebb712a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,18 +125,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", From aaed6f3d07e840fb1cfb0f16b6ec78036c1f1769 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:15:18 +0000 Subject: [PATCH 45/52] Bump the actions-minor-patch group with 2 updates (#7) Bumps the actions-minor-patch group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/checkout` from 6.0.1 to 6.0.2 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/8e8c483db84b4bee98b60c0593521ed34d9990e8...de0fac2e4500dabe0009e67214ff5f5447ce83dd) Updates `github/codeql-action` from 4.31.10 to 4.31.11 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/cdefb33c0f6224e58673d9004f47f7cb3e328b89...19b2f06db2b6f5108140aeb04014ef02b648f789) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-patch - dependency-name: github/codeql-action dependency-version: 4.31.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/nightly.yml | 4 ++-- .github/workflows/release.yml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a5b246..18e5cd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: SCCACHE_CACHE_SIZE: '2G' steps: - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 53006cf..81a77ae 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,12 +41,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Initialize CodeQL - uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3 + uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -55,10 +55,10 @@ jobs: - name: Autobuild if: matrix.build-mode == 'autobuild' - uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3 + uses: github/codeql-action/autobuild@19b2f06db2b6f5108140aeb04014ef02b648f789 # v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3 + uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v3 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 540f900..71a625f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -31,7 +31,7 @@ jobs: ahead_count: ${{ steps.diff.outputs.ahead_count }} steps: - name: Checkout target ref - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.TARGET_REF }} fetch-depth: 0 @@ -80,7 +80,7 @@ jobs: SCCACHE_CACHE_SIZE: '2G' steps: - name: Checkout target ref - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.TARGET_REF }} fetch-depth: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55bbba1..aa71f7c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: SCCACHE_CACHE_SIZE: '2G' steps: - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 From 926243cd2a4dfac89369c249c0fb13b7445f2ad0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 11:27:16 +0000 Subject: [PATCH 46/52] Bump github/codeql-action in the actions-minor-patch group (#8) Bumps the actions-minor-patch group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 4.31.11 to 4.32.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/19b2f06db2b6f5108140aeb04014ef02b648f789...b20883b0cd1f46c72ae0ba6d1090936928f9fa30) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.32.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 81a77ae..51ca1f0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: fetch-depth: 0 - name: Initialize CodeQL - uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v3 + uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -55,10 +55,10 @@ jobs: - name: Autobuild if: matrix.build-mode == 'autobuild' - uses: github/codeql-action/autobuild@19b2f06db2b6f5108140aeb04014ef02b648f789 # v3 + uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v3 + uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v3 with: category: "/language:${{ matrix.language }}" From a436b4e4c4c5f56ac0a4f4f5a9701057b9765d5a Mon Sep 17 00:00:00 2001 From: Jordon <16258926+Jordonbc@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:50:12 +0000 Subject: [PATCH 47/52] Update README with CI badges Added badges for nightly, dev, and stable workflows. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c2f77ee..736a79f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # OpenVCS Core (`openvcs-core`) -[![Dev CI (fast)](https://github.com/Open-VCS/OpenVCS-Core/actions/workflows/ci.yml/badge.svg)](https://github.com/Open-VCS/OpenVCS-Core/actions/workflows/ci.yml) +[![Nightly](https://github.com/Open-VCS/OpenVCS-Core/actions/workflows/nightly.yml/badge.svg?branch=Dev)](https://github.com/Open-VCS/OpenVCS-Core/actions/workflows/nightly.yml) +[![Dev](https://github.com/Open-VCS/OpenVCS-Core/actions/workflows/ci.yml/badge.svg?branch=Dev)](https://github.com/Open-VCS/OpenVCS-Core/actions/workflows/ci.yml) +[![Stable](https://github.com/Open-VCS/OpenVCS-Core/actions/workflows/release.yml/badge.svg?branch=Dev)](https://github.com/Open-VCS/OpenVCS-Core/actions/workflows/release.yml) Shared Rust crate for: - OpenVCS plugins (JSON-RPC over stdio) From 8d4f30612402bdad5a671f5769374ed1f686e8c8 Mon Sep 17 00:00:00 2001 From: Jordon <16258926+Jordonbc@users.noreply.github.com> Date: Sat, 7 Feb 2026 23:00:30 +0000 Subject: [PATCH 48/52] Update lib.rs (#9) --- src/lib.rs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2a52338..f2bf61f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -273,26 +273,6 @@ pub trait Vcs: Send + Sync { Err(VcsError::Unsupported(self.id())) } - // lfs - fn lfs_fetch(&self) -> Result<()> { - Err(VcsError::Unsupported(self.id())) - } - fn lfs_pull(&self) -> Result<()> { - Err(VcsError::Unsupported(self.id())) - } - fn lfs_prune(&self) -> Result<()> { - Err(VcsError::Unsupported(self.id())) - } - fn lfs_track(&self, _paths: &[PathBuf]) -> Result<()> { - Err(VcsError::Unsupported(self.id())) - } - fn lfs_untrack(&self, _paths: &[PathBuf]) -> Result<()> { - Err(VcsError::Unsupported(self.id())) - } - fn lfs_is_tracked(&self, _path: &Path) -> Result { - Err(VcsError::Unsupported(self.id())) - } - // history operations fn cherry_pick(&self, _rev: &str) -> Result<()> { Err(VcsError::Unsupported(self.id())) From c12bf1783d8db28eb8053f8128943f69ea9a47c0 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sat, 7 Feb 2026 23:32:48 +0000 Subject: [PATCH 49/52] Update version --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ebb712a..3d803a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,13 +36,13 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "openvcs-core" -version = "0.1.5" +version = "0.1.6" dependencies = [ "linkme", "log", @@ -53,18 +53,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -151,6 +151,6 @@ checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "zmij" -version = "1.0.12" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/Cargo.toml b/Cargo.toml index 6416ee2..edfa1a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openvcs-core" -version = "0.1.5" +version = "0.1.6" edition = "2024" description = "Core types and traits for OpenVCS." license = "GPL-3.0-or-later" From 8bb8788b976a431306b956af4b645cad94a5c473 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sat, 7 Feb 2026 23:46:42 +0000 Subject: [PATCH 50/52] Fix stdio timeout issues --- src/host.rs | 25 ++++-- src/plugin_runtime.rs | 41 ++++------ src/plugin_stdio.rs | 174 +++++++++++++++++++++++++++++++++--------- 3 files changed, 170 insertions(+), 70 deletions(-) diff --git a/src/host.rs b/src/host.rs index 077f19f..17d2a15 100644 --- a/src/host.rs +++ b/src/host.rs @@ -1,7 +1,7 @@ use crate::plugin_protocol::RpcRequest; -use crate::plugin_stdio::{PluginError, RequestIdState, call_host}; +use crate::plugin_protocol::RpcResponse; +use crate::plugin_stdio::{PluginError, RequestIdState, SharedQueue, call_host}; use serde_json::Value; -use std::collections::VecDeque; use std::io::{BufReader, LineWriter}; use std::sync::{Arc, Mutex, OnceLock}; use std::time::Duration; @@ -13,7 +13,8 @@ pub type HostStdin = BufReader; struct HostContext { out: Arc>, stdin: Arc>, - queue: Arc>>, + queue: Arc>, + responses: Arc>, ids: Arc>, timeout: Duration, } @@ -23,15 +24,22 @@ static HOST: OnceLock = OnceLock::new(); pub fn init_stdio_default(next_id: u64, timeout: Duration) { let out = Arc::new(Mutex::new(LineWriter::new(std::io::stdout()))); let stdin = Arc::new(Mutex::new(BufReader::new(std::io::stdin()))); - let queue = Arc::new(Mutex::new(VecDeque::new())); + let queue = Arc::new(SharedQueue::new()); + let responses = Arc::new(SharedQueue::new()); let ids = Arc::new(Mutex::new(RequestIdState { next_id })); - init_default_stdio_host(out, stdin, queue, ids, timeout); + crate::plugin_stdio::start_reader_thread( + Arc::clone(&stdin), + Arc::clone(&queue), + Arc::clone(&responses), + ); + init_default_stdio_host(out, stdin, queue, responses, ids, timeout); } pub fn init_default_stdio_host( out: Arc>, stdin: Arc>, - queue: Arc>>, + queue: Arc>, + responses: Arc>, ids: Arc>, timeout: Duration, ) { @@ -39,6 +47,7 @@ pub fn init_default_stdio_host( out, stdin, queue, + responses, ids, timeout, }); @@ -58,7 +67,7 @@ pub fn stdin() -> Result<&'static Arc>, PluginError> { .stdin) } -pub fn queue() -> Result<&'static Arc>>, PluginError> { +pub fn queue() -> Result<&'static Arc>, PluginError> { Ok(&HOST .get() .ok_or_else(|| PluginError::code("host.uninitialized", "host not initialized"))? @@ -82,7 +91,7 @@ pub fn call(method: &str, params: Value) -> Result { call_host( &ctx.out, &ctx.stdin, - &ctx.queue, + &ctx.responses, &ctx.ids, method, params, diff --git a/src/plugin_runtime.rs b/src/plugin_runtime.rs index 4943526..1ecc833 100644 --- a/src/plugin_runtime.rs +++ b/src/plugin_runtime.rs @@ -1,9 +1,8 @@ use crate::models::VcsEvent; use crate::plugin_protocol::{PluginMessage, RpcRequest}; use crate::plugin_stdio::ok_null; -use crate::plugin_stdio::{PluginError, receive_message, respond_shared, send_message_shared}; +use crate::plugin_stdio::{PluginError, SharedQueue, respond_shared, send_message_shared}; use std::collections::HashMap; -use std::collections::VecDeque; use std::io::{self, BufReader, LineWriter}; use std::sync::{Arc, Mutex, OnceLock}; use std::time::Duration; @@ -38,31 +37,18 @@ impl PluginCtx { } fn next_request( - queue: &Arc>>, - stdin: &Arc>>, + queue: &Arc>, ) -> Option { - if let Ok(mut q) = queue.lock() - && let Some(req) = q.pop_front() - { + if let Some(req) = queue.pop_now() { return Some(req); } - loop { - let msg = { - let mut lock = stdin.lock().ok()?; - receive_message(&mut *lock)? - }; - match msg { - PluginMessage::Request(req) => return Some(req), - PluginMessage::Response(_) | PluginMessage::Event { .. } => continue, - } - } + queue.pop_wait() } pub struct PluginRuntime { ctx: PluginCtx, - stdin: Arc>>, - queue: Arc>>, + queue: Arc>, } impl PluginRuntime { @@ -77,22 +63,25 @@ impl PluginRuntime { let stdout = Arc::new(Mutex::new(LineWriter::new(io::stdout()))); let stdin = Arc::new(Mutex::new(BufReader::new(io::stdin()))); - let queue: Arc>> = Arc::new(Mutex::new(VecDeque::new())); + let queue: Arc> = Arc::new(SharedQueue::new()); + let responses = Arc::new(SharedQueue::new()); let ids = Arc::new(Mutex::new(crate::plugin_stdio::RequestIdState { next_id })); + crate::plugin_stdio::start_reader_thread( + Arc::clone(&stdin), + Arc::clone(&queue), + Arc::clone(&responses), + ); crate::host::init_default_stdio_host( Arc::clone(&stdout), Arc::clone(&stdin), Arc::clone(&queue), + Arc::clone(&responses), Arc::clone(&ids), timeout, ); - Self { - ctx: PluginCtx { stdout }, - stdin, - queue, - } + Self { ctx: PluginCtx { stdout }, queue } } pub fn ctx(&mut self) -> &mut PluginCtx { @@ -103,7 +92,7 @@ impl PluginRuntime { &mut self, mut handle: impl FnMut(&mut PluginCtx, RpcRequest) -> HandlerResult, ) -> io::Result { - let Some(req) = next_request(&self.queue, &self.stdin) else { + let Some(req) = next_request(&self.queue) else { return Ok(false); }; let id = req.id; diff --git a/src/plugin_stdio.rs b/src/plugin_stdio.rs index ffcca73..e721c46 100644 --- a/src/plugin_stdio.rs +++ b/src/plugin_stdio.rs @@ -4,7 +4,8 @@ use serde::de::DeserializeOwned; use serde_json::Value; use std::collections::{HashMap, VecDeque}; use std::io::{self, BufRead, Write}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Condvar, Mutex}; +use std::thread; use std::time::{Duration, Instant}; #[derive(Debug, Clone)] @@ -129,10 +130,122 @@ pub struct RequestIdState { pub next_id: u64, } +#[derive(Debug)] +struct SharedQueueState { + items: VecDeque, + closed: bool, +} + +#[derive(Debug)] +pub struct SharedQueue { + state: Mutex>, + cv: Condvar, +} + +impl SharedQueue { + pub fn new() -> Self { + Self { + state: Mutex::new(SharedQueueState { + items: VecDeque::new(), + closed: false, + }), + cv: Condvar::new(), + } + } + + pub fn push(&self, item: T) { + if let Ok(mut state) = self.state.lock() { + state.items.push_back(item); + self.cv.notify_all(); + } + } + + pub fn close(&self) { + if let Ok(mut state) = self.state.lock() { + state.closed = true; + self.cv.notify_all(); + } + } + + pub fn pop_now(&self) -> Option { + let mut state = self.state.lock().ok()?; + state.items.pop_front() + } + + pub fn pop_wait(&self) -> Option { + let mut state = self.state.lock().ok()?; + loop { + if let Some(item) = state.items.pop_front() { + return Some(item); + } + if state.closed { + return None; + } + state = self.cv.wait(state).ok()?; + } + } + + pub fn pop_wait_until(&self, deadline: Instant) -> Result, ()> { + let mut state = self.state.lock().map_err(|_| ())?; + loop { + if let Some(item) = state.items.pop_front() { + return Ok(Some(item)); + } + if state.closed { + return Ok(None); + } + + let now = Instant::now(); + if now >= deadline { + return Err(()); + } + let timeout = deadline.saturating_duration_since(now); + let (next_state, wait_result) = self.cv.wait_timeout(state, timeout).map_err(|_| ())?; + state = next_state; + if wait_result.timed_out() { + return Err(()); + } + } + } +} + +pub fn start_reader_thread( + stdin: Arc>, + requests: Arc>, + responses: Arc>, +) { + thread::spawn(move || { + loop { + let msg = { + let mut lock = match stdin.lock() { + Ok(lock) => lock, + Err(_) => { + requests.close(); + responses.close(); + return; + } + }; + receive_message(&mut *lock) + }; + + match msg { + Some(PluginMessage::Request(req)) => requests.push(req), + Some(PluginMessage::Response(resp)) => responses.push(resp), + Some(PluginMessage::Event { .. }) => {} + None => { + requests.close(); + responses.close(); + return; + } + } + } + }); +} + pub fn call_host( out: &Arc>, - stdin: &Arc>, - queue: &Arc>>, + _stdin: &Arc>, + responses: &Arc>, ids: &Arc>, method: &str, params: Value, @@ -176,35 +289,23 @@ pub fn call_host( }; } - let msg = { - let mut lock = stdin - .lock() - .map_err(|_| PluginError::message("stdin lock poisoned"))?; - receive_message(&mut *lock).ok_or_else(|| PluginError::message("host closed stdin"))? - }; + let resp = responses + .pop_wait_until(deadline) + .map_err(|_| PluginError::code("host.timeout", "host call timed out"))? + .ok_or_else(|| PluginError::message("host closed stdin"))?; - match msg { - PluginMessage::Response(resp) => { - if resp.id == id { - return if resp.ok { - Ok(resp.result) - } else { - Err(PluginError { - code: resp.error_code.or(Some("host.error".into())), - message: resp.error.unwrap_or_else(|| "error".into()), - data: resp.error_data, - }) - }; - } - stash.insert(resp.id, resp); - } - PluginMessage::Request(req) => { - if let Ok(mut q) = queue.lock() { - q.push_back(req); - } - } - PluginMessage::Event { .. } => {} + if resp.id == id { + return if resp.ok { + Ok(resp.result) + } else { + Err(PluginError { + code: resp.error_code.or(Some("host.error".into())), + message: resp.error.unwrap_or_else(|| "error".into()), + data: resp.error_data, + }) + }; } + stash.insert(resp.id, resp); } } @@ -291,13 +392,15 @@ mod tests { {\"id\":77,\"method\":\"noop\",\"params\":null}\n\ {\"id\":5,\"ok\":true,\"result\":{\"answer\":42}}\n" as &[u8], ))); - let queue = Arc::new(Mutex::new(VecDeque::::new())); + let queue = Arc::new(SharedQueue::::new()); + let responses = Arc::new(SharedQueue::::new()); let ids = Arc::new(Mutex::new(RequestIdState { next_id: 5 })); + start_reader_thread(Arc::clone(&stdin), Arc::clone(&queue), Arc::clone(&responses)); let result = call_host( &out, &stdin, - &queue, + &responses, &ids, "math.answer", serde_json::json!({}), @@ -306,10 +409,9 @@ mod tests { .expect("host call ok"); assert_eq!(result, serde_json::json!({"answer": 42})); - let queue = queue.lock().expect("queue lock"); - assert_eq!(queue.len(), 1); - assert_eq!(queue[0].id, 77); - assert_eq!(queue[0].method, "noop"); + let queued = queue.pop_now().expect("queued request"); + assert_eq!(queued.id, 77); + assert_eq!(queued.method, "noop"); let out = out.lock().expect("out lock"); let line = std::str::from_utf8(&out).expect("utf-8"); From f49cca7ec245815b96734bd7289163973cdcc0d1 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sat, 7 Feb 2026 23:49:10 +0000 Subject: [PATCH 51/52] Fix Backend registry issue --- Cargo.toml | 4 ++-- src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index edfa1a9..23548f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,10 +31,10 @@ default = ["plugin-protocol"] plugin-protocol = ["dep:serde_json"] # The VCS trait and VCS-related error type. -vcs = ["dep:thiserror", "backend-registry"] +vcs = ["dep:thiserror"] # Backend discovery via the `BACKENDS` registry (link-time registration). -backend-registry = ["dep:linkme"] +backend-registry = ["dep:linkme", "vcs"] [dependencies] serde = { version = "1", features = ["derive"] } diff --git a/src/lib.rs b/src/lib.rs index f2bf61f..281e0ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ pub use crate::backend_id::BackendId; #[doc(hidden)] pub use log as __log; -#[cfg(feature = "backend-registry")] +#[cfg(all(feature = "backend-registry", feature = "vcs"))] pub mod backend_descriptor; #[cfg(feature = "plugin-protocol")] From e9a87b28494e0244316a16148d96ef31b7866009 Mon Sep 17 00:00:00 2001 From: Jordon Date: Sat, 7 Feb 2026 23:51:07 +0000 Subject: [PATCH 52/52] Fix issues --- src/plugin_runtime.rs | 9 +++++---- src/plugin_stdio.rs | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/plugin_runtime.rs b/src/plugin_runtime.rs index 1ecc833..c9a9255 100644 --- a/src/plugin_runtime.rs +++ b/src/plugin_runtime.rs @@ -36,9 +36,7 @@ impl PluginCtx { } } -fn next_request( - queue: &Arc>, -) -> Option { +fn next_request(queue: &Arc>) -> Option { if let Some(req) = queue.pop_now() { return Some(req); } @@ -81,7 +79,10 @@ impl PluginRuntime { timeout, ); - Self { ctx: PluginCtx { stdout }, queue } + Self { + ctx: PluginCtx { stdout }, + queue, + } } pub fn ctx(&mut self) -> &mut PluginCtx { diff --git a/src/plugin_stdio.rs b/src/plugin_stdio.rs index e721c46..1d2b67a 100644 --- a/src/plugin_stdio.rs +++ b/src/plugin_stdio.rs @@ -142,6 +142,18 @@ pub struct SharedQueue { cv: Condvar, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SharedQueueError { + Timeout, + Poisoned, +} + +impl Default for SharedQueue { + fn default() -> Self { + Self::new() + } +} + impl SharedQueue { pub fn new() -> Self { Self { @@ -185,8 +197,8 @@ impl SharedQueue { } } - pub fn pop_wait_until(&self, deadline: Instant) -> Result, ()> { - let mut state = self.state.lock().map_err(|_| ())?; + pub fn pop_wait_until(&self, deadline: Instant) -> Result, SharedQueueError> { + let mut state = self.state.lock().map_err(|_| SharedQueueError::Poisoned)?; loop { if let Some(item) = state.items.pop_front() { return Ok(Some(item)); @@ -197,13 +209,16 @@ impl SharedQueue { let now = Instant::now(); if now >= deadline { - return Err(()); + return Err(SharedQueueError::Timeout); } let timeout = deadline.saturating_duration_since(now); - let (next_state, wait_result) = self.cv.wait_timeout(state, timeout).map_err(|_| ())?; + let (next_state, wait_result) = self + .cv + .wait_timeout(state, timeout) + .map_err(|_| SharedQueueError::Poisoned)?; state = next_state; if wait_result.timed_out() { - return Err(()); + return Err(SharedQueueError::Timeout); } } } @@ -291,7 +306,12 @@ pub fn call_host( let resp = responses .pop_wait_until(deadline) - .map_err(|_| PluginError::code("host.timeout", "host call timed out"))? + .map_err(|e| match e { + SharedQueueError::Timeout => { + PluginError::code("host.timeout", "host call timed out") + } + SharedQueueError::Poisoned => PluginError::message("response queue lock poisoned"), + })? .ok_or_else(|| PluginError::message("host closed stdin"))?; if resp.id == id { @@ -395,7 +415,11 @@ mod tests { let queue = Arc::new(SharedQueue::::new()); let responses = Arc::new(SharedQueue::::new()); let ids = Arc::new(Mutex::new(RequestIdState { next_id: 5 })); - start_reader_thread(Arc::clone(&stdin), Arc::clone(&queue), Arc::clone(&responses)); + start_reader_thread( + Arc::clone(&stdin), + Arc::clone(&queue), + Arc::clone(&responses), + ); let result = call_host( &out,