openvcs_core/
lib.rs

1//! OpenVCS Core: shared types for OpenVCS plugins and the OpenVCS client.
2
3pub mod backend_id;
4pub mod models;
5
6pub use crate::backend_id::BackendId;
7
8#[doc(hidden)]
9pub use log as __log;
10
11#[cfg(feature = "backend-registry")]
12pub mod backend_descriptor;
13
14#[cfg(feature = "plugin-protocol")]
15pub mod plugin_protocol;
16
17#[cfg(feature = "plugin-protocol")]
18pub mod plugin_stdio;
19
20#[cfg(feature = "plugin-protocol")]
21pub mod host;
22
23#[cfg(feature = "plugin-protocol")]
24pub mod plugin_runtime;
25
26#[cfg(feature = "plugin-protocol")]
27pub mod events;
28
29#[doc(hidden)]
30pub fn __plugin_log_to_client(level: log::Level, target: &str, args: std::fmt::Arguments<'_>) {
31    #[cfg(feature = "plugin-protocol")]
32    {
33        let msg = format!("[{level}] {target}: {args}");
34        if let Ok(out) = crate::host::stdout() {
35            crate::plugin_stdio::send_message_shared(
36                out,
37                &crate::plugin_protocol::PluginMessage::Event {
38                    event: match level {
39                        log::Level::Error => crate::models::VcsEvent::Error { msg },
40                        log::Level::Warn => crate::models::VcsEvent::Warning { msg },
41                        log::Level::Info | log::Level::Debug | log::Level::Trace => {
42                            crate::models::VcsEvent::Info { msg }
43                        }
44                    },
45                },
46            );
47            return;
48        }
49    }
50
51    let _ = (level, target, args);
52}
53
54#[macro_export]
55macro_rules! trace {
56    (target: $target:expr, $($arg:tt)+) => {{
57        $crate::__plugin_log_to_client($crate::__log::Level::Trace, $target, format_args!($($arg)+));
58        $crate::__log::trace!(target: $target, $($arg)+);
59    }};
60    ($($arg:tt)+) => {{
61        $crate::__plugin_log_to_client($crate::__log::Level::Trace, module_path!(), format_args!($($arg)+));
62        $crate::__log::trace!($($arg)+);
63    }};
64}
65
66#[macro_export]
67macro_rules! debug {
68    (target: $target:expr, $($arg:tt)+) => {{
69        $crate::__plugin_log_to_client($crate::__log::Level::Debug, $target, format_args!($($arg)+));
70        $crate::__log::debug!(target: $target, $($arg)+);
71    }};
72    ($($arg:tt)+) => {{
73        $crate::__plugin_log_to_client($crate::__log::Level::Debug, module_path!(), format_args!($($arg)+));
74        $crate::__log::debug!($($arg)+);
75    }};
76}
77
78#[macro_export]
79macro_rules! info {
80    (target: $target:expr, $($arg:tt)+) => {{
81        $crate::__plugin_log_to_client($crate::__log::Level::Info, $target, format_args!($($arg)+));
82        $crate::__log::info!(target: $target, $($arg)+);
83    }};
84    ($($arg:tt)+) => {{
85        $crate::__plugin_log_to_client($crate::__log::Level::Info, module_path!(), format_args!($($arg)+));
86        $crate::__log::info!($($arg)+);
87    }};
88}
89
90#[macro_export]
91macro_rules! warn {
92    (target: $target:expr, $($arg:tt)+) => {{
93        $crate::__plugin_log_to_client($crate::__log::Level::Warn, $target, format_args!($($arg)+));
94        $crate::__log::warn!(target: $target, $($arg)+);
95    }};
96    ($($arg:tt)+) => {{
97        $crate::__plugin_log_to_client($crate::__log::Level::Warn, module_path!(), format_args!($($arg)+));
98        $crate::__log::warn!($($arg)+);
99    }};
100}
101
102#[macro_export]
103macro_rules! error {
104    (target: $target:expr, $($arg:tt)+) => {{
105        $crate::__plugin_log_to_client($crate::__log::Level::Error, $target, format_args!($($arg)+));
106        $crate::__log::error!(target: $target, $($arg)+);
107    }};
108    ($($arg:tt)+) => {{
109        $crate::__plugin_log_to_client($crate::__log::Level::Error, module_path!(), format_args!($($arg)+));
110        $crate::__log::error!($($arg)+);
111    }};
112}
113
114#[cfg(feature = "plugin-protocol")]
115pub use crate::plugin_protocol::{PluginMessage, RpcRequest, RpcResponse};
116
117#[cfg(feature = "vcs")]
118pub use crate::models::{Capabilities, FetchOptions, OnEvent};
119
120#[cfg(feature = "vcs")]
121use std::path::{Path, PathBuf};
122
123#[cfg(feature = "vcs")]
124#[derive(thiserror::Error, Debug)]
125pub enum VcsError {
126    #[error("not a repository: {0}")]
127    NotARepo(String),
128    #[error("branch not found: {0}")]
129    NoSuchBranch(String),
130    #[error("no upstream configured")]
131    NoUpstream,
132    #[error("nothing to commit")]
133    NothingToCommit,
134    #[error("non-fast-forward; merge or rebase required")]
135    NonFastForward,
136    #[error("unsupported backend: {0}")]
137    Unsupported(BackendId),
138    #[error("io: {0}")]
139    Io(#[from] std::io::Error),
140    #[error("{backend}: {msg}")]
141    Backend { backend: BackendId, msg: String },
142}
143
144#[cfg(feature = "vcs")]
145pub type Result<T> = std::result::Result<T, VcsError>;
146
147/// The single trait every VCS backend implements.
148#[cfg(feature = "vcs")]
149pub trait Vcs: Send + Sync {
150    fn id(&self) -> BackendId;
151    fn caps(&self) -> models::Capabilities;
152
153    // lifecycle
154    fn open(path: &Path) -> Result<Self>
155    where
156        Self: Sized;
157    fn clone(url: &str, dest: &Path, on: Option<models::OnEvent>) -> Result<Self>
158    where
159        Self: Sized;
160
161    // context
162    fn workdir(&self) -> &Path;
163
164    // common ops
165    fn current_branch(&self) -> Result<Option<String>>;
166    fn branches(&self) -> Result<Vec<models::BranchItem>>;
167
168    #[deprecated(
169        since = "0.1.0",
170        note = "This function is being replaced by `branches`."
171    )]
172    fn local_branches(&self) -> Result<Vec<String>>;
173    fn create_branch(&self, name: &str, checkout: bool) -> Result<()>;
174    fn checkout_branch(&self, name: &str) -> Result<()>;
175
176    // network
177    fn ensure_remote(&self, name: &str, url: &str) -> Result<()>;
178    fn list_remotes(&self) -> Result<Vec<(String, String)>>;
179    fn remove_remote(&self, name: &str) -> Result<()>;
180    fn fetch(&self, remote: &str, refspec: &str, on: Option<models::OnEvent>) -> Result<()>;
181    fn fetch_with_options(
182        &self,
183        remote: &str,
184        refspec: &str,
185        _opts: models::FetchOptions,
186        on: Option<models::OnEvent>,
187    ) -> Result<()> {
188        self.fetch(remote, refspec, on)
189    }
190    fn push(&self, remote: &str, refspec: &str, on: Option<models::OnEvent>) -> Result<()>;
191    fn pull_ff_only(&self, remote: &str, branch: &str, on: Option<models::OnEvent>) -> Result<()>;
192
193    // content
194    fn commit(&self, message: &str, name: &str, email: &str, paths: &[PathBuf]) -> Result<String>;
195    fn commit_index(&self, message: &str, name: &str, email: &str) -> Result<String>;
196    fn status_summary(&self) -> Result<models::StatusSummary>;
197    fn status_payload(&self) -> Result<models::StatusPayload>;
198    fn log_commits(&self, query: &models::LogQuery) -> Result<Vec<models::CommitItem>>;
199    fn diff_file(&self, path: &Path) -> Result<Vec<String>>;
200    fn diff_commit(&self, rev: &str) -> Result<Vec<String>>;
201
202    // conflicts
203    fn conflict_details(&self, _path: &Path) -> Result<models::ConflictDetails> {
204        Err(VcsError::Unsupported(self.id()))
205    }
206    fn checkout_conflict_side(&self, _path: &Path, _side: models::ConflictSide) -> Result<()> {
207        Err(VcsError::Unsupported(self.id()))
208    }
209    fn write_merge_result(&self, _path: &Path, _content: &[u8]) -> Result<()> {
210        Err(VcsError::Unsupported(self.id()))
211    }
212
213    fn stage_patch(&self, patch: &str) -> Result<()>;
214    fn discard_paths(&self, paths: &[PathBuf]) -> Result<()>;
215    fn apply_reverse_patch(&self, patch: &str) -> Result<()>;
216
217    // branches
218    fn delete_branch(&self, name: &str, force: bool) -> Result<()>;
219    fn rename_branch(&self, old: &str, new: &str) -> Result<()>;
220    fn merge_into_current(&self, name: &str) -> Result<()>;
221    fn merge_into_current_with_message(&self, name: &str, message: Option<&str>) -> Result<()> {
222        let _ = message;
223        self.merge_into_current(name)
224    }
225    fn merge_abort(&self) -> Result<()> {
226        Err(VcsError::Unsupported(self.id()))
227    }
228    fn merge_continue(&self) -> Result<()> {
229        Err(VcsError::Unsupported(self.id()))
230    }
231    fn merge_in_progress(&self) -> Result<bool> {
232        Ok(false)
233    }
234    fn set_branch_upstream(&self, _branch: &str, _upstream: &str) -> Result<()> {
235        Err(VcsError::Unsupported(self.id()))
236    }
237    fn branch_upstream(&self, _branch: &str) -> Result<Option<String>> {
238        Err(VcsError::Unsupported(self.id()))
239    }
240
241    // recovery
242    fn hard_reset_head(&self) -> Result<()>;
243    fn reset_soft_to(&self, _rev: &str) -> Result<()> {
244        Err(VcsError::Unsupported(self.id()))
245    }
246
247    // config
248    fn get_identity(&self) -> Result<Option<(String, String)>>;
249    fn set_identity_local(&self, name: &str, email: &str) -> Result<()>;
250
251    // stash
252    fn stash_list(&self) -> Result<Vec<models::StashItem>> {
253        Err(VcsError::Unsupported(self.id()))
254    }
255    fn stash_push(
256        &self,
257        _message: &str,
258        _include_untracked: bool,
259        _paths: &[PathBuf],
260    ) -> Result<()> {
261        Err(VcsError::Unsupported(self.id()))
262    }
263    fn stash_apply(&self, _selector: &str) -> Result<()> {
264        Err(VcsError::Unsupported(self.id()))
265    }
266    fn stash_pop(&self, _selector: &str) -> Result<()> {
267        Err(VcsError::Unsupported(self.id()))
268    }
269    fn stash_drop(&self, _selector: &str) -> Result<()> {
270        Err(VcsError::Unsupported(self.id()))
271    }
272    fn stash_show(&self, _selector: &str) -> Result<Vec<String>> {
273        Err(VcsError::Unsupported(self.id()))
274    }
275
276    // lfs
277    fn lfs_fetch(&self) -> Result<()> {
278        Err(VcsError::Unsupported(self.id()))
279    }
280    fn lfs_pull(&self) -> Result<()> {
281        Err(VcsError::Unsupported(self.id()))
282    }
283    fn lfs_prune(&self) -> Result<()> {
284        Err(VcsError::Unsupported(self.id()))
285    }
286    fn lfs_track(&self, _paths: &[PathBuf]) -> Result<()> {
287        Err(VcsError::Unsupported(self.id()))
288    }
289    fn lfs_untrack(&self, _paths: &[PathBuf]) -> Result<()> {
290        Err(VcsError::Unsupported(self.id()))
291    }
292    fn lfs_is_tracked(&self, _path: &Path) -> Result<bool> {
293        Err(VcsError::Unsupported(self.id()))
294    }
295
296    // history operations
297    fn cherry_pick(&self, _rev: &str) -> Result<()> {
298        Err(VcsError::Unsupported(self.id()))
299    }
300    fn revert_commit(&self, _rev: &str, _no_edit: bool) -> Result<()> {
301        Err(VcsError::Unsupported(self.id()))
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    #[test]
308    fn log_macros_are_callable() {
309        crate::trace!("trace");
310        crate::debug!("debug");
311        crate::info!("info");
312        crate::warn!("warn");
313        crate::error!("error");
314    }
315
316    #[cfg(feature = "vcs")]
317    #[test]
318    fn vcs_error_formats_useful_messages() {
319        let e = crate::VcsError::Unsupported(crate::BackendId::from("git"));
320        assert!(e.to_string().contains("unsupported backend"));
321
322        let e = crate::VcsError::Backend {
323            backend: crate::BackendId::from("git"),
324            msg: "boom".into(),
325        };
326        assert_eq!(e.to_string(), "git: boom");
327    }
328}