feat(pkg): add --force flag to skip hash verification on upgrade, install, and delete

When upgrading yazi across versions, the content hash computation may
change, causing false "You have modified the contents" errors on
packages the user never touched. This leaves users unable to upgrade,
install, or delete any managed package without manually deleting each
one first.

Add a --force/-f flag to `ya pkg upgrade`, `ya pkg install`, and
`ya pkg delete` that skips the hash_check, allowing the operation to
proceed. A warning is printed when the check is skipped. The error
message now also suggests --force as a remedy.

Closes #2735, #3291
This commit is contained in:
Mario 2026-03-18 10:49:45 +00:00 committed by sxyazi
parent a246e9c7c0
commit c11064c3aa
No known key found for this signature in database
9 changed files with 45 additions and 26 deletions

View file

@ -77,17 +77,27 @@ pub(super) enum CommandPkg {
Delete {
/// Packages to delete.
#[arg(index = 1, num_args = 1..)]
ids: Vec<String>,
ids: Vec<String>,
/// Force delete, skipping local modification checks.
#[arg(short, long)]
force: bool,
},
/// Install all packages.
Install,
Install {
/// Force install, skipping local modification checks.
#[arg(short, long)]
force: bool,
},
/// List all packages.
List,
/// Upgrade all packages.
Upgrade {
/// Packages to upgrade, upgrade all if unspecified.
#[arg(index = 1, num_args = 0..)]
ids: Vec<String>,
ids: Vec<String>,
/// Force upgrade, skipping local modification checks.
#[arg(short, long)]
force: bool,
},
}

View file

@ -78,10 +78,10 @@ async fn run() -> anyhow::Result<()> {
let mut pkg = package::Package::load().await?;
match cmd {
CommandPkg::Add { ids } => pkg.add_many(&ids).await?,
CommandPkg::Delete { ids } => pkg.delete_many(&ids).await?,
CommandPkg::Install => pkg.install().await?,
CommandPkg::Delete { ids, force } => pkg.delete_many(&ids, force).await?,
CommandPkg::Install { force } => pkg.install(force).await?,
CommandPkg::List => pkg.print()?,
CommandPkg::Upgrade { ids } => pkg.upgrade_many(&ids).await?,
CommandPkg::Upgrade { ids, force } => pkg.upgrade_many(&ids, force).await?,
}
}

View file

@ -4,7 +4,7 @@ use super::{Dependency, Git};
use crate::shared::must_exists;
impl Dependency {
pub(super) async fn add(&mut self) -> Result<()> {
pub(super) async fn add(&mut self, force: bool) -> Result<()> {
self.header("Upgrading package `{name}`")?;
let path = self.local();
@ -14,7 +14,7 @@ impl Dependency {
Git::clone(&self.remote(), &path).await?;
};
self.deploy().await?;
self.deploy(force).await?;
self.rev = Git::revision(&path).await?;
Ok(())
}

View file

@ -6,7 +6,7 @@ use super::Dependency;
use crate::shared::{maybe_exists, remove_sealed};
impl Dependency {
pub(super) async fn delete(&self) -> Result<()> {
pub(super) async fn delete(&self, force: bool) -> Result<()> {
self.header("Deleting package `{name}`")?;
let dir = self.target();
@ -14,7 +14,11 @@ impl Dependency {
return Ok(outln!("Not found, skipping")?);
}
self.hash_check().await?;
if force {
outln!("Warning: skipping local modification check for `{}`", self.name)?;
} else {
self.hash_check().await?;
}
self.delete_assets().await?;
self.delete_sources().await?;

View file

@ -8,7 +8,7 @@ use super::Dependency;
use crate::shared::{copy_and_seal, maybe_exists};
impl Dependency {
pub(super) async fn deploy(&mut self) -> Result<()> {
pub(super) async fn deploy(&mut self, force: bool) -> Result<()> {
let from = self.local().join(&self.child);
self.header("Deploying package `{name}`")?;
@ -17,7 +17,11 @@ impl Dependency {
let to = self.target();
let exists = maybe_exists(&to).await;
if exists {
self.hash_check().await?;
if force {
outln!("Warning: skipping local modification check for `{}`", self.name)?;
} else {
self.hash_check().await?;
}
}
Local::regular(&to).create_dir_all().await?;

View file

@ -46,7 +46,8 @@ impl Dependency {
if self.hash != self.hash().await? {
bail!(
"You have modified the contents of the `{}` {}. For safety, the operation has been aborted.
Please manually delete it from `{}` and re-run the command.",
Please manually delete it from `{}` and re-run the command.
If you have not modified this package, re-run the command with --force to skip this check.",
self.name,
if self.is_flavor { "flavor" } else { "plugin" },
self.target().display()

View file

@ -4,7 +4,7 @@ use super::{Dependency, Git};
use crate::shared::must_exists;
impl Dependency {
pub(super) async fn install(&mut self) -> Result<()> {
pub(super) async fn install(&mut self, force: bool) -> Result<()> {
self.header("Fetching package `{name}`")?;
let path = self.local();
@ -18,7 +18,7 @@ impl Dependency {
Git::checkout(&path, self.rev.trim_start_matches('=')).await?;
}
self.deploy().await?;
self.deploy(force).await?;
if self.rev.is_empty() {
self.rev = Git::revision(&path).await?;
}

View file

@ -28,19 +28,19 @@ impl Package {
Ok(())
}
pub(crate) async fn delete_many(&mut self, uses: &[String]) -> Result<()> {
pub(crate) async fn delete_many(&mut self, uses: &[String], force: bool) -> Result<()> {
for u in uses {
let r = self.delete(u).await;
let r = self.delete(u, force).await;
self.save().await?;
r?;
}
Ok(())
}
pub(crate) async fn install(&mut self) -> Result<()> {
pub(crate) async fn install(&mut self, force: bool) -> Result<()> {
macro_rules! go {
($dep:expr) => {
let r = $dep.install().await;
let r = $dep.install(force).await;
self.save().await?;
r?;
};
@ -55,11 +55,11 @@ impl Package {
Ok(())
}
pub(crate) async fn upgrade_many(&mut self, uses: &[String]) -> Result<()> {
pub(crate) async fn upgrade_many(&mut self, uses: &[String], force: bool) -> Result<()> {
macro_rules! go {
($dep:expr) => {
if uses.is_empty() || uses.contains(&$dep.r#use) {
let r = $dep.upgrade().await;
let r = $dep.upgrade(force).await;
self.save().await?;
r?;
}
@ -107,7 +107,7 @@ impl Package {
)
}
dep.add().await?;
dep.add(false).await?;
if dep.is_flavor {
self.flavors.push(dep);
} else {
@ -116,12 +116,12 @@ impl Package {
Ok(())
}
async fn delete(&mut self, r#use: &str) -> Result<()> {
async fn delete(&mut self, r#use: &str, force: bool) -> Result<()> {
let Some(dep) = self.identical(&Dependency::from_str(r#use)?).cloned() else {
bail!("`{}` was not found in package.toml", r#use)
};
dep.delete().await?;
dep.delete(force).await?;
if dep.is_flavor {
self.flavors.retain(|d| !d.identical(&dep));
} else {

View file

@ -3,7 +3,7 @@ use anyhow::Result;
use super::Dependency;
impl Dependency {
pub(super) async fn upgrade(&mut self) -> Result<()> {
if self.rev.starts_with('=') { Ok(()) } else { self.add().await }
pub(super) async fn upgrade(&mut self, force: bool) -> Result<()> {
if self.rev.starts_with('=') { Ok(()) } else { self.add(force).await }
}
}