use std::path::Path;
use super::{
inspect_source, install_validated, managed_root, package_exists, place, resolve_named_package,
InstallMode, ManagedPackage, SourcePackage,
};
use crate::plugins::{
git_source::{self, Checkout, GitSource},
state::{InstallRecord, PluginScope, PluginStateStore},
ProjectTrust, TRUST_PROJECT_PLUGINS_ENV,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum UpdateSelection<'a> {
All,
Named(&'a str),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum UpdateOutcome {
Updated { previous: String, current: String },
UpToDate { commit: String },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct UpdateTarget {
pub(crate) name: String,
pub(crate) scope: PluginScope,
}
#[derive(Debug, Default)]
pub(crate) struct UpdateReport {
pub(crate) results: Vec<(UpdateTarget, anyhow::Result<UpdateOutcome>)>,
pub(crate) untrusted: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct GitInstall {
pub(crate) package: ManagedPackage,
pub(crate) commit: String,
}
pub(crate) fn install_git(
source: &GitSource,
scope: PluginScope,
force: bool,
cwd: &Path,
home: &Path,
rho_home: Option<&Path>,
) -> anyhow::Result<GitInstall> {
let checkout = git_source::fetch(source)?;
let package = inspect_checkout(&checkout, source)?;
let record = InstallRecord::Git(source.record(checkout.commit.clone()));
let package = install_validated(package, &record, scope, force, cwd, home, rho_home)?;
Ok(GitInstall {
package,
commit: checkout.commit,
})
}
pub(crate) fn update(
selection: UpdateSelection<'_>,
trust: ProjectTrust,
cwd: &Path,
home: &Path,
rho_home: Option<&Path>,
) -> anyhow::Result<UpdateReport> {
let mut state = PluginStateStore::load(cwd, rho_home)?;
let mut report = UpdateReport::default();
for scope in update_scopes(&state) {
for name in state.git_installed(scope) {
if matches!(selection, UpdateSelection::Named(wanted) if wanted != name) {
continue;
}
if scope == PluginScope::Project && !trust.is_trusted() {
report.untrusted.push(name);
continue;
}
let result = update_one(scope, &name, &mut state, cwd, home);
report.results.push((UpdateTarget { name, scope }, result));
}
}
if let UpdateSelection::Named(name) = selection {
if !report.results.is_empty() {
return Ok(report);
}
if !report.untrusted.is_empty() {
anyhow::bail!(
"`{name}` is a project plugin and the workspace is not trusted; set {TRUST_PROJECT_PLUGINS_ENV}=1 to update it"
);
}
let resolved = resolve_named_package(name, cwd, Some(home), rho_home)?;
anyhow::bail!(
"`{name}` has {} origin, not git; reinstall it from a repository with `rho plugins install <url> --force`",
resolved.package.origin.as_str()
);
}
Ok(report)
}
fn update_scopes(state: &PluginStateStore) -> Vec<PluginScope> {
let (project, user) = (&state.project_path, &state.user_path);
let aliased = project == user
|| matches!((project.canonicalize(), user.canonicalize()), (Ok(a), Ok(b)) if a == b);
if aliased {
vec![PluginScope::User]
} else {
vec![PluginScope::Project, PluginScope::User]
}
}
fn update_one(
scope: PluginScope,
name: &str,
state: &mut PluginStateStore,
cwd: &Path,
home: &Path,
) -> anyhow::Result<UpdateOutcome> {
let Some(record) = state.git_record(scope, name).cloned() else {
anyhow::bail!("`{name}` has no recorded git source");
};
let source = GitSource::try_from(&record)?;
let root = managed_root(scope, cwd, home);
let path = root.join(name);
if !package_exists(&path) {
anyhow::bail!(
"`{name}` was installed from {} but {} is missing; reinstall it with `rho plugins install`",
source.describe(),
crate::paths::display(&path)
);
}
let checkout = git_source::fetch(&source)?;
if checkout.commit == record.commit {
return Ok(UpdateOutcome::UpToDate {
commit: record.commit,
});
}
let fetched = inspect_checkout(&checkout, &source)?;
if fetched.manifest.name != name {
anyhow::bail!(
"refusing to update `{name}`: {} now contains package `{}`",
source.describe(),
fetched.manifest.name
);
}
place(&fetched, &root, InstallMode::Copy, true)?;
state.record_git_update(scope, name, source.record(checkout.commit.clone()))?;
Ok(UpdateOutcome::Updated {
previous: record.commit,
current: checkout.commit,
})
}
fn inspect_checkout(checkout: &Checkout, source: &GitSource) -> anyhow::Result<SourcePackage> {
let dir = checkout.package_dir(source)?;
if !dir.join("plugin.json").is_file() {
let commit = git_source::short_commit(&checkout.commit);
let location = source.subdir().unwrap_or("the repository root");
let hint = if source.subdir().is_none() && source.is_github() {
"; link the plugin directory with a /tree/<ref>/<path> URL"
} else {
""
};
anyhow::bail!(
"no plugin.json at {location} in {} at {commit}{hint}",
source.url()
);
}
inspect_source(&dir)
}
#[cfg(test)]
#[path = "manage_git_tests.rs"]
mod tests;