diff options
Diffstat (limited to '')
-rw-r--r-- | src/model/config.rs | 110 | ||||
-rw-r--r-- | src/model/mod.rs | 3 | ||||
-rw-r--r-- | src/model/plugin.rs | 283 | ||||
-rw-r--r-- | src/model/repo.rs | 123 |
4 files changed, 519 insertions, 0 deletions
diff --git a/src/model/config.rs b/src/model/config.rs new file mode 100644 index 0000000..844609d --- /dev/null +++ b/src/model/config.rs @@ -0,0 +1,110 @@ +//! Definition of an iris [config][Config]. + +use crate::{get_writer, path, Plugin, Result}; + +use log::trace; +use std::{fs, io::Write, path::Path}; + +/// Iris config +#[derive(Debug)] +pub struct Config { + /// List of iris plugin specifications + pub plugins: Vec<Plugin>, +} + +impl Config { + /// Read an iris config from the file system + pub fn read_from_file<P>(path: P) -> Result<Self> + where + P: AsRef<Path>, + { + trace!("reading: {}", path.as_ref().display()); + let contents = fs::read_to_string(&path)?; + let iris = Self::parse(&contents)?; + Ok(iris) + } + + /// Read an iris config from the plugin file path + pub fn read_from_plugin_file() -> Result<Self> { + trace!("read from plugin file"); + Self::read_from_file(path::plugin_file()) + } + + /// Read an iris config from the lock file path + pub fn read_from_lock_file() -> Result<Self> { + trace!("read from lock file"); + Self::read_from_file(path::lock_file()) + } + + /// Write the iris config to the file system. + pub fn write_to_file<P>(&self, path: P) -> Result<()> + where + P: AsRef<Path>, + { + trace!("writing: {}", path.as_ref().display()); + let contents = self.to_string()?; + let mut write = get_writer(&path, '#')?; + write.write_all(contents.as_bytes())?; + Ok(()) + } + + /// Write the iris config to the plugins file. + pub fn write_to_plugin_file(&self) -> Result<()> { + self.write_to_file(path::plugin_file()) + } + + /// Write the iris config to the lock file. + pub fn write_to_lock_file(&self) -> Result<()> { + self.write_to_file(path::lock_file()) + } + + /// Write the vim autoload file formed from each plugin. + pub fn write_autoload_file(&self) -> Result<()> { + let path = path::autoload_file(); + trace!("writing: {}", path.display()); + + let mut f = get_writer(&path, '"')?; + + // BEGIN iris#load + writeln!(f, "function! iris#load()")?; + // add each plugin to load path + for plugin in &self.plugins { + // runtime path + { + let path = plugin.runtime_path().display(); + writeln!(f, "set runtimepath+={path}")?; + } + // lua package path + if let Some(path) = plugin.lua_package_path() { + let path = path.display(); + writeln!(f, "lua package.path = package.path .. \";{path}\"")?; + } + // vim source file + if let Some(path) = plugin.vim_source_file() { + let path = path.display(); + writeln!(f, "source {path}")?; + } + // lua source file + if let Some(path) = plugin.lua_source_file() { + let path = path.display(); + writeln!(f, "lua dofile('{path}')")?; + } + } + for plugin in &self.plugins { + // vim source after file + if let Some(path) = plugin.vim_source_after_file() { + let path = path.display(); + writeln!(f, "source {path}")?; + } + // lua source after file + if let Some(path) = plugin.lua_source_after_file() { + let path = path.display(); + writeln!(f, "lua dofile('{path}')")?; + } + } + writeln!(f, "endfunction")?; + // END iris#load + + Ok(()) + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..41088af --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod plugin; +pub mod repo; diff --git a/src/model/plugin.rs b/src/model/plugin.rs new file mode 100644 index 0000000..bb097c6 --- /dev/null +++ b/src/model/plugin.rs @@ -0,0 +1,283 @@ +//! Defines of an iris [plugin][Plugin]. + +use crate::{error::error, path, Repository, Result}; + +use log::{debug, trace, warn}; +use std::{borrow::Cow, cmp::Ordering, collections::HashSet, path::PathBuf}; +use url::Url; + +/// Returns the url to the fallback git server. If a repository url is provided +/// and it does not contain a git server (just author and repo), this url will +/// be used in its absence. +fn fallback_git_server() -> &'static Url { + use std::sync::OnceLock; + static FALLBACK: OnceLock<Url> = OnceLock::new(); + FALLBACK.get_or_init(|| { + Url::parse("https://github.com") + .expect("!! IRIS BUG !! invalid fallback git server url") + }) +} + +/// # Plugin +/// +/// A structure representing an iris plugin specification. +/// +/// An iris plugin has two parts. +/// +/// - [id], must be a unique identifier for this plugin and the plugins +/// module name +/// - [args], a set of arguments to configure this plugin +/// +/// [id]: Self::id +/// [args]: Self::args +#[derive(Debug, Eq)] +pub struct Plugin { + /// The plugins unique identifier. Used for the vim module name and + /// repository checkout foler. + pub id: String, + /// The commit oid (if set) that the local repository will be forced to + /// track. If not set then the repository will track the latest commit in + /// the current tracking branch. + pub commit: Option<String>, + /// The branch name (if set) that the local repository will be forced to + /// track. If not set then the repository will track the repositories + /// default branch. + pub branch: Option<String>, + /// Set of plugin ids this plugin should load before + pub run: Option<String>, + /// Handle to the local plugin repository. + pub before: HashSet<String>, + /// Set of plugin ids this plugin should load after + pub after: HashSet<String>, + /// The command to run when vim starts up + repo: Repository, +} + +impl Plugin { + /// Create a new iris plugin. + /// + /// Creates a plugin from the git repo at `url` with the unique id `id`. + /// The plugin will be unlocked meaning it will always track the latest + /// commit until locked. A handle to a local repository will be created but + /// not loaded until called upon. + pub fn new(id: String, url: &str) -> Result<Self> { + // resolve and parse url + let url = { + // try url as is + Url::parse(url) + // try url with fallback + .or_else(|_| fallback_git_server().join(url)) + // invalid git url! + .map_err(|_| error!("invalid git url {url}")) + }?; + + // get the path to the local repository checkout + let repo_path = path::plugin_dir().join(&id); + + // create repository handle + let repo = Repository::new(url, repo_path); + + debug!("found plugin '{id}'"); + + Ok(Self { + id, + commit: None, + branch: None, + run: None, + before: HashSet::new(), + after: HashSet::new(), + repo, + }) + } + + /// The url to the remote git repository. + pub const fn url(&self) -> &Url { + &self.repo.url + } + + /// The path to the local repository + pub const fn repo_path(&self) -> &PathBuf { + &self.repo.path + } + + /// The commit oid that the plugin repository is currently tracking + fn commit(&self) -> Result<Cow<'_, str>> { + // returned locked commit if set + if let Some(commit) = &self.commit { + return Ok(Cow::Borrowed(commit)); + } + + // get commit from repo + let branch = self.branch()?; + let commit = self.repo.latest_commit(&branch)?; + Ok(Cow::Owned(commit)) + } + + /// The branch name that the plugin repository is currently tracking + fn branch(&self) -> Result<Cow<'_, str>> { + // return locked branch if set + if let Some(branch) = &self.branch { + return Ok(Cow::Borrowed(branch)); + } + + // get branch from repo + let branch = self.repo.default_branch()?; + Ok(Cow::Owned(branch)) + } + + /// Fetches latest changes in the local repository but does not update + /// checkout. + pub fn fetch(&self) -> Result<()> { + trace!("fetching {}", self.id); + let branch = self.branch()?; + self.repo.fetch(&branch)?; + Ok(()) + } + + /// Checkout plugin repository using their current refs + pub fn switch(&self) -> Result<()> { + trace!("switching {}", self.id); + let commit = self.commit()?; + self.repo.checkout(&commit)?; + Ok(()) + } + + /// Update plugin ref to point to the latest commit + pub fn lock(&mut self) -> Result<()> { + trace!("locking {}", self.id); + let branch = self.branch()?; + self.repo.fetch(&branch)?; + let commit = self.repo.latest_commit(&branch)?; + self.commit = Some(commit); + Ok(()) + } + + /// Runtime path + /// + /// This this the plugins root directory, and is used by vim/neovim to + /// find the vim/lua runtime scripts. + /// + /// vim: set runtimepath+=<path> + pub fn runtime_path(&self) -> &PathBuf { + self.repo_path() + } + + /// Lua package path + /// + /// This is the path to the lua module if the plugin has one. If so + /// it needs to be added to the lua package path. The path is located + /// in the repos lua directory at /lua/<id>/init.lua or /lua/<id>.lua. If + /// both exist the former takes priority. + /// + /// lua: package.path = package.path .. ";<path>" + pub fn lua_package_path(&self) -> Option<PathBuf> { + let mut lua = self.repo_path().to_owned(); + lua.push("lua"); + lua.push(&self.id); + // /lua/<id>/init.lua + let module = lua.join("init.lua"); + if module.exists() { + return Some(module.with_file_name("?.lua")); + } + // /lua/<id>/.lua + let module = lua.with_extension("lua"); + if module.exists() { + return Some(module.with_file_name("?.lua")); + } + // no lua package path found + None + } + + /// Vim source file + /// + /// Path to a vim file in the /plugin directory that needs to be + /// sourced. This is located at /plugin/<id>.vim + /// + /// vim: source <path> + pub fn vim_source_file(&self) -> Option<PathBuf> { + let mut path = self.repo_path().to_owned(); + path.push("plugin"); + path.push(&self.id); + path.set_extension("vim"); + Some(path).filter(|path| path.exists()) + } + + /// Lua source file + /// + /// Path to a lua file in the /plugin directory that needs to be + /// sourced. This is located at /plugin/<id>.lua + /// + /// lua: dofile('<path>') + pub fn lua_source_file(&self) -> Option<PathBuf> { + let mut path = self.repo_path().to_owned(); + path.push("plugin"); + path.push(&self.id); + path.set_extension("lua"); + Some(path).filter(|path| path.exists()) + } + + /// Vim source after file + /// + /// Path to a vim file in the /after/plugin directory that needs to be + /// sourced last. This is located at /after/plugin/<id>.vim + /// + /// vim: source <path> + pub fn vim_source_after_file(&self) -> Option<PathBuf> { + let mut path = self.repo_path().to_owned(); + path.push("after/plugin"); + path.push(&self.id); + path.set_extension("vim"); + Some(path).filter(|path| path.exists()) + } + + /// Lua source after file + /// + /// Path to a lua file in the /after/plugin directory that needs to be + /// sourced last. This is located at /after/plugin/<id>.lua + /// + /// lua: dofile('<path>') + pub fn lua_source_after_file(&self) -> Option<PathBuf> { + let mut path = self.repo_path().to_owned(); + path.push("after/plugin"); + path.push(&self.id); + path.set_extension("lua"); + Some(path).filter(|path| path.exists()) + } +} + +impl PartialEq for Plugin { + fn eq(&self, other: &Self) -> bool { + self.id.eq(&other.id) + } +} + +impl PartialOrd for Plugin { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + let is_after = + other.before.contains(&self.id) || self.after.contains(&other.id); + let is_before = + other.after.contains(&self.id) || self.before.contains(&other.id); + match (is_before, is_after) { + // equal, no ordering + (false, false) => Some(Ordering::Equal), + // before + (true, false) => Some(Ordering::Less), + // after + (false, true) => Some(Ordering::Greater), + // cycle detected + (true, true) => { + warn!( + "plugin dependency cycle detected between '{}' and '{}'", + &self.id, &other.id + ); + None + } + } + } +} + +impl Ord for Plugin { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other).unwrap_or(Ordering::Equal) + } +} diff --git a/src/model/repo.rs b/src/model/repo.rs new file mode 100644 index 0000000..557efb4 --- /dev/null +++ b/src/model/repo.rs @@ -0,0 +1,123 @@ +//! Definition of an iris [repo][Repository]. + +use crate::{error, Result}; + +use log::{debug, trace}; +use std::{path::PathBuf, sync::OnceLock}; +use url::Url; + +/// A handle to a locally stored git repository. +pub struct Repository { + /// The remote url that the git repository is located at. + pub url: Url, + /// The path on disk that the this repository is/will be located at. + pub path: PathBuf, + /// The git2 lib handle to the repo + inner: OnceLock<git2::Repository>, +} + +impl Repository { + /// Creates a new handle to a local git repository. + pub const fn new(url: Url, path: PathBuf) -> Self { + Self { + url, + path, + inner: OnceLock::new(), + } + } + + /// Gets the handle to the local repository + fn repo(&self) -> Result<&git2::Repository> { + // we only need to initalize once + if let Some(repo) = self.inner.get() { + return Ok(repo); + } + + trace!("loading repo"); + + // repository not loaded, try loading it + let repo = if self.path.exists() { + // repo exists on file system, just open it + git2::Repository::open(&self.path)? + } else { + // repo needs to be cloned + git2::Repository::clone(self.url.as_str(), &self.path)? + }; + + // save and return repo + Ok(self.inner.get_or_init(|| repo)) + } + + /// Gets the current remote in the git repository + fn remote(&self) -> Result<git2::Remote> { + let repo = self.repo()?; + let remote_name = "origin"; + let mut remote = repo.find_remote(remote_name)?; + + // make sure that out remote is connected before + // we do anything with it. otherwise things will + // fail! + if !remote.connected() { + trace!("connecting to remote"); + remote.connect(git2::Direction::Fetch)?; + } + + Ok(remote) + } + + /// Fetches the latest commits in the provided branch. + pub fn fetch(&self, branch: &str) -> Result<()> { + let mut remote = self.remote()?; + trace!("fetching '{branch}"); + remote.fetch(&[branch], None, None)?; + Ok(()) + } + + /// Returns the default branch of the git repositroy. + pub fn default_branch(&self) -> Result<String> { + let remote = self.remote()?; + let buf = remote.default_branch()?; + let name = buf + .as_str() + .and_then(|s| s.strip_prefix("refs/heads/")) + .map(String::from) + .ok_or_else(|| error::error!("cannot get default branch"))?; + debug!("default branch is '{name}'"); + Ok(name) + } + + /// Returns the latest fetched commit in `branch`. + pub fn latest_commit(&self, branch: &str) -> Result<String> { + let repo = self.repo()?; + let refr_name = format!("refs/remotes/origin/{branch}"); + let refr = repo.find_reference(&refr_name)?; + let commit = refr.peel_to_commit()?; + let oid = commit.id().to_string(); + Ok(oid) + } + + /// Checks out the local repository to the given commit and detaches + /// HEAD to that commit. + pub fn checkout(&self, commit: &str) -> Result<()> { + let repo = self.repo()?; + trace!("checkout to '{commit}'"); + let oid = git2::Oid::from_str(commit)?; + let commit = repo.find_commit(oid)?; + repo.reset(commit.as_object(), git2::ResetType::Hard, None)?; + Ok(()) + } +} + +impl std::fmt::Debug for Repository { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "_") + } +} + +impl PartialEq for Repository { + fn eq(&self, other: &Self) -> bool { + self.url.eq(&other.url) && self.path.eq(&other.path) + } +} + +impl Eq for Repository {} |