Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 74 additions & 11 deletions crates/spfs-cli/main/src/cmd_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use std::collections::VecDeque;
use clap::Args;
use colored::*;
use futures::TryFutureExt;
use miette::Result;
use miette::{Result, miette};
use spfs::config::ToAddress;
use spfs::env::SPFS_DIR;
use spfs::find_path::ObjectPathEntry;
use spfs::graph::Annotation;
Expand All @@ -16,6 +17,10 @@ use spfs::prelude::*;
use spfs::{self};
use spfs_cli_common as cli;

#[cfg(test)]
#[path = "./cmd_info_test.rs"]
mod cmd_info_test;

/// Display information about the current environment, or specific items
#[derive(Debug, Args)]
pub struct CmdInfo {
Expand All @@ -32,6 +37,13 @@ pub struct CmdInfo {
#[clap(flatten)]
pub(crate) repos: cli::Repositories,

/// When set, use local/origin as fallback pair for object reads.
///
/// With no --remote, local is primary and origin is fallback.
/// With --remote origin, origin is primary and local is fallback.
#[clap(long)]
origin_local_fallback: bool,

/// Tag, id, or /spfs/file/path to show information about
#[clap(value_name = "REF")]
refs: Vec<String>,
Expand All @@ -56,8 +68,30 @@ pub struct CmdInfo {

impl CmdInfo {
pub async fn run(&mut self, config: &spfs::Config) -> Result<i32> {
let repo =
let primary_repo =
spfs::config::open_repository_from_string(config, self.repos.remote.as_ref()).await?;
let repo = if self.origin_local_fallback {
let secondary_repo = match self.repos.remote.as_deref() {
None => spfs::config::open_repository_from_string(config, Some("origin")).await?,
Some("origin") => {
spfs::config::open_repository_from_string(config, Option::<&str>::None).await?
}
Some(other) => {
return Err(miette!(
"--origin-local-fallback only supports local or --remote origin, got --remote {other}"
));
}
};

let proxy_config = spfs::storage::proxy::Config {
primary: primary_repo.address().to_string(),
secondary: vec![secondary_repo.address().to_string()],
include_secondary_tags: false,
};
spfs::open_repository(proxy_config.to_address()?).await?
} else {
primary_repo
};

self.to_process.extend(self.refs.iter().cloned());

Expand Down Expand Up @@ -269,15 +303,23 @@ impl CmdInfo {
verbosity: usize,
) -> Result<()> {
let mut in_a_runtime = true;
let found = match spfs::find_path::find_path_providers_in_spfs_runtime(filepath, repo).await
{
Ok(f) => f,
Err(spfs::Error::NoActiveRuntime) => {
in_a_runtime = false;
Vec::new()
}
Err(err) => return Err(err.into()),
};
let search_result =
match spfs::find_path::find_path_providers_in_spfs_runtime_with_diagnostics(
filepath, repo,
)
.await
{
Ok(result) => result,
Err(spfs::Error::NoActiveRuntime) => {
in_a_runtime = false;
spfs::find_path::FindPathProvidersResult {
providers: Vec::new(),
skipped_unknown_objects: false,
}
}
Err(err) => return Err(err.into()),
};
let found = search_result.providers;

if found.is_empty() {
println!("{filepath}: {}", "not found".yellow());
Expand All @@ -289,6 +331,12 @@ impl CmdInfo {
"No active runtime".red()
}
);
if let Some(hint) = missing_file_provider_hint(MissingProviderContext {
in_a_runtime,
skipped_unknown_objects: search_result.skipped_unknown_objects,
}) {
println!(" - {}", hint.yellow());
}
} else {
if let Some(first_path) = found.first()
&& let Some(ObjectPathEntry::FilePath(file_entry)) = first_path.last()
Expand Down Expand Up @@ -321,3 +369,18 @@ impl CmdInfo {
Ok(())
}
}

struct MissingProviderContext {
in_a_runtime: bool,
skipped_unknown_objects: bool,
}

fn missing_file_provider_hint(ctx: MissingProviderContext) -> Option<&'static str> {
if ctx.in_a_runtime && ctx.skipped_unknown_objects {
Some(
"some runtime objects were unavailable in this repo; enable extra repos (try --origin-local-fallback)",
)
} else {
None
}
}
33 changes: 33 additions & 0 deletions crates/spfs-cli/main/src/cmd_info_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) Contributors to the SPK project.
// SPDX-License-Identifier: Apache-2.0
// https://github.com/spkenv/spk

use super::{MissingProviderContext, missing_file_provider_hint};

#[test]
fn suggests_origin_local_fallback_for_split_runtime_objects() {
let hint = missing_file_provider_hint(MissingProviderContext {
in_a_runtime: true,
skipped_unknown_objects: true,
})
.expect("expected hint text");
assert!(hint.contains("--origin-local-fallback"));
}

#[test]
fn omits_fallback_hint_when_lookup_cannot_be_influenced_by_repo_selection() {
assert!(
missing_file_provider_hint(MissingProviderContext {
in_a_runtime: false,
skipped_unknown_objects: true
})
.is_none()
);
assert!(
missing_file_provider_hint(MissingProviderContext {
in_a_runtime: true,
skipped_unknown_objects: false
})
.is_none()
);
}
100 changes: 88 additions & 12 deletions crates/spfs/src/find_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ use spfs_encoding::prelude::*;
use crate::graph::{self, DatabaseView, Object};
use crate::{Error, Result, env, status, storage, tracking};

#[cfg(test)]
#[path = "./find_path_test.rs"]
mod find_path_test;

/// Used for items in a list of spfs objects that contain a filepath.
/// The parent containers down to the filepath will be graph objects.
/// The filepath itself will be a manifest node entry.
Expand All @@ -36,30 +40,74 @@ impl ObjectPathEntry {

pub type ObjectPath = Vec<ObjectPathEntry>;

/// Result data from searching for providers of a path in the active runtime.
#[derive(Debug)]
pub struct FindPathProvidersResult {
/// Paths to providers found in the active runtime.
pub providers: Vec<ObjectPath>,
/// Whether objects were skipped because they were not present in the
/// selected repository while searching.
pub skipped_unknown_objects: bool,
}

struct FindPathInItemResult {
paths: Vec<ObjectPath>,
skipped_unknown_objects: bool,
}

/// Finds all the spfs object paths to the objects that provide the
/// entry for the given filepaths in the current spfs runtime.
/// Returns tuple of a boolean for whether we are in an active spfs
/// runtime or not, and a list of all the spfs object paths (as lists)
/// that end in the entry for the given filepath.
pub async fn find_path_providers_in_spfs_runtime(
pub async fn find_path_providers_in_spfs_runtime_with_diagnostics(
filepath: &str,
repo: &storage::RepositoryHandle,
) -> Result<Vec<ObjectPath>> {
) -> Result<FindPathProvidersResult> {
let mut found: Vec<ObjectPath> = Vec::new();
let mut skipped_unknown_objects = false;

if let Ok(runtime) = status::active_runtime().await {
for digest in runtime.status.stack.iter_bottom_up() {
let item = repo.read_object(digest).await?;
let item = match repo.read_object(digest).await {
Ok(item) => item,
// The selected repo may not have every object in the active
// runtime stack (for example local-only or origin-only
// objects); skip missing ones and keep searching.
Err(Error::UnknownObject(_)) => {
skipped_unknown_objects = true;
continue;
}
Err(err) => return Err(err),
};
let file_data = find_path_in_spfs_item(filepath, &item, repo).await?;
if !file_data.is_empty() {
found.extend(file_data);
if file_data.skipped_unknown_objects {
skipped_unknown_objects = true;
}
if !file_data.paths.is_empty() {
found.extend(file_data.paths);
}
}
} else {
return Err(Error::NoActiveRuntime);
}

Ok(found)
Ok(FindPathProvidersResult {
providers: found,
skipped_unknown_objects,
})
}

/// Finds all spfs object paths that provide the filepath in the current runtime.
pub async fn find_path_providers_in_spfs_runtime(
filepath: &str,
repo: &storage::RepositoryHandle,
) -> Result<Vec<ObjectPath>> {
Ok(
find_path_providers_in_spfs_runtime_with_diagnostics(filepath, repo)
.await?
.providers,
)
}

/// Returns a list of spfs object paths (as lists) from the given spfs
Expand All @@ -71,15 +119,27 @@ async fn find_path_in_spfs_item(
filepath: &str,
obj: &Object,
repo: &storage::RepositoryHandle,
) -> Result<Vec<ObjectPath>> {
) -> Result<FindPathInItemResult> {
let mut paths: Vec<ObjectPath> = Vec::new();
let mut skipped_unknown_objects = false;

match obj.to_enum() {
graph::object::Enum::Platform(obj) => {
for reference in obj.iter_bottom_up() {
let item = repo.read_object(*reference).await?;
let item = match repo.read_object(*reference).await {
Ok(item) => item,
// Some child objects may exist only in a different repo.
Err(Error::UnknownObject(_)) => {
skipped_unknown_objects = true;
continue;
}
Err(err) => return Err(err),
};
let paths_to_file = find_path_in_spfs_item(filepath, &item, repo).await?;
for path in paths_to_file {
if paths_to_file.skipped_unknown_objects {
skipped_unknown_objects = true;
}
for path in paths_to_file.paths {
let mut new_path: ObjectPath = Vec::new();
new_path.push(ObjectPathEntry::Parent(obj.to_object()));
new_path.extend(path);
Expand All @@ -90,9 +150,22 @@ async fn find_path_in_spfs_item(

graph::object::Enum::Layer(obj) => {
if let Some(manifest_digest) = obj.manifest() {
let item = repo.read_object(*manifest_digest).await?;
let item = match repo.read_object(*manifest_digest).await {
Ok(item) => item,
// The manifest might not exist in this repo.
Err(Error::UnknownObject(_)) => {
return Ok(FindPathInItemResult {
paths,
skipped_unknown_objects: true,
});
}
Err(err) => return Err(err),
};
let paths_to_file = find_path_in_spfs_item(filepath, &item, repo).await?;
for path in paths_to_file {
if paths_to_file.skipped_unknown_objects {
skipped_unknown_objects = true;
}
for path in paths_to_file.paths {
let mut new_path: ObjectPath = Vec::new();
new_path.push(ObjectPathEntry::Parent(obj.to_object()));
new_path.extend(path);
Expand Down Expand Up @@ -122,5 +195,8 @@ async fn find_path_in_spfs_item(
}
};

Ok(paths)
Ok(FindPathInItemResult {
paths,
skipped_unknown_objects,
})
}
Loading
Loading