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
4 changes: 2 additions & 2 deletions cli/src/services/app_support.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,7 +184,7 @@ fn write_error_diagnostic_with_color_policy<W: Write>(
}
CliError::User {
error: user_error, ..
} => user_error.message().to_string(),
} => user_error.message(),
};
let styled_message = services::style::error_text_with_color_policy(
&services::security::redact_sensitive_text(&rendered),
Expand DownExpand Up@@ -328,7 +328,7 @@ mod tests {

let rendered = String::from_utf8(stderr).expect("stderr is valid utf8");
let redacted_message =
services::security::redact_sensitive_text(UserError::NotAuthenticated.message());
services::security::redact_sensitive_text(&UserError::NotAuthenticated.message());
assert!(rendered.contains(&redacted_message));
}

Expand Down
42 changes: 35 additions & 7 deletions cli/src/services/config/resolver.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,13 +153,7 @@ pub(crate) fn resolve_agent_trace_auto_sync_runtime_config(
pub(crate) fn resolve_agent_trace_storage_runtime_config(
cwd: &Path,
) -> Result<ResolvedAgentTraceStorageRuntimeConfig> {
let runtime = resolve_runtime_config_with(
&ConfigRequest {
report_format: ReportFormat::Text,
config_path: None,
log_level: None,
timeout_ms: None,
},
resolve_agent_trace_storage_runtime_config_with(
cwd,
|key| std::env::var(key).ok(),
|path| {
Expand All@@ -168,8 +162,42 @@ pub(crate) fn resolve_agent_trace_storage_runtime_config(
},
Path::exists,
resolve_default_global_config_path,
)
}

fn resolve_agent_trace_storage_runtime_config_with<FEnv, FRead, FGlobalPath>(
cwd: &Path,
env_lookup: FEnv,
read_file: FRead,
path_exists: fn(&Path) -> bool,
resolve_global_config_path: FGlobalPath,
) -> Result<ResolvedAgentTraceStorageRuntimeConfig>
where
FEnv: Fn(&str) -> Option<String>,
FRead: Fn(&Path) -> Result<String>,
FGlobalPath: Fn() -> Result<PathBuf>,
{
let runtime = resolve_runtime_config_with(
&ConfigRequest {
report_format: ReportFormat::Text,
config_path: None,
log_level: None,
timeout_ms: None,
},
cwd,
env_lookup,
read_file,
path_exists,
resolve_global_config_path,
)?;

if !runtime.validation_errors.is_empty() {
bail!(
"Agent Trace storage config resolution failed because a discovered config file is invalid: {}",
runtime.validation_errors.join(" | ")
);
}

Ok(ResolvedAgentTraceStorageRuntimeConfig {
repository_id: runtime.agent_trace_repository_id.value,
repository_remote: runtime.agent_trace_repository_remote.value,
Expand Down
27 changes: 22 additions & 5 deletions cli/src/services/error.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,31 +50,48 @@ impl FailureClass {

/// Catalog of expected, deliberately-explained failures presented to the user
/// as a friendly diagnostic instead of a technical error chain.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq)]
#[allow(clippy::enum_variant_names)]
pub enum UserError {
#[allow(dead_code)]
NotAuthenticated,
NotGitRepository,
NotGitRemote {
remote_name: String,
},
}

impl UserError {
pub fn class(self) -> FailureClass {
pub fn class(&self) -> FailureClass {
match self {
Self::NotAuthenticated => FailureClass::Runtime,
Self::NotAuthenticated | Self::NotGitRepository | Self::NotGitRemote { .. } => {
FailureClass::Runtime
}
}
}

#[allow(dead_code)]
pub fn key(self) -> &'static str {
pub fn key(&self) -> &'static str {
match self {
Self::NotAuthenticated => "auth.not_authenticated",
Self::NotGitRepository => "setup.not_git_repository",
Self::NotGitRemote { .. } => "setup.not_git_remote",
}
}

pub fn message(self) -> &'static str {
pub fn message(&self) -> String {
match self {
Self::NotAuthenticated => {
"You are not logged in. Please log in using the `sce auth login` command."
.to_string()
}
Self::NotGitRepository => {
"The target directory is not a Git repository. Please run `git init`, then retry."
.to_string()
}
Self::NotGitRemote { remote_name } => format!(
"The Git repository has no configured URL for remote '{remote_name}'. Please run `git remote add <remote> <url>`, then retry."
),
}
}
}
Expand Down
49 changes: 43 additions & 6 deletions cli/src/services/repository_identity/resolve.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@
use std::path::Path;
use std::process::Command;

use anyhow::{bail, Context, Result};

use super::{
repository_identity_from_explicit, repository_identity_from_remote_url, RepositoryIdentity,
RepositoryIdentityError,
Expand DownExpand Up@@ -124,21 +126,56 @@ pub fn resolve_repository_identity_with_lookup(
/// Returns `None` when git is unavailable, the directory is not a
/// repository, or the remote has no URL.
pub fn lookup_remote_url(repository_root: &Path, remote_name: &str) -> Option<String> {
lookup_remote_url_strict(repository_root, remote_name)
.ok()
.flatten()
}

/// Reads a named Git remote URL while distinguishing a missing URL from a
/// failure to execute or interpret the lookup.
pub fn lookup_remote_url_strict(
repository_root: &Path,
remote_name: &str,
) -> Result<Option<String>> {
let output = Command::new("git")
.arg("-C")
.arg(repository_root)
.args(["config", "--get", &format!("remote.{remote_name}.url")])
.env("LC_ALL", "C")
.output()
.ok()?;
.with_context(|| {
format!(
"Failed to look up Git remote '{remote_name}' URL in '{}'",
repository_root.display()
)
})?;

if !output.status.success() {
return None;
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
if output.status.code() == Some(1) && stderr.is_empty() {
return Ok(None);
}

let diagnostic = if stderr.is_empty() {
String::from("git config exited with a non-zero status")
} else {
crate::services::security::redact_sensitive_text(&stderr)
};
bail!(
"Failed to look up Git remote '{remote_name}' URL in '{}': {diagnostic}",
repository_root.display()
);
}
let url = String::from_utf8_lossy(&output.stdout).trim().to_string();

let url = String::from_utf8(output.stdout)
.context("Git remote lookup output contained invalid UTF-8")?
.trim()
.to_string();
if url.is_empty() {
None
} else {
Some(url)
return Ok(None);
}

Ok(Some(url))
}

#[cfg(test)]
Expand Down
37 changes: 33 additions & 4 deletions cli/src/services/setup/command.rs
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
use anyhow::Context;

use crate::app::ContextWithRepoRoot;
use crate::services::error::CliError;
use crate::services::error::{CliError, UserError};
use crate::services::lifecycle::{
lifecycle_providers, RequiredHookInstallStatus, RequiredHooksInstallOutcome,
};
use crate::services::setup;
use crate::services::{config, setup};

pub struct SetupCommand {
pub request: setup::SetupRequest,
Expand All@@ -22,8 +22,8 @@ impl SetupCommand {

// The repository root is resolved before any prompt so the interactive
// optional-workflow prompt can pre-check the persisted selection.
let repository_root =
setup::ensure_git_repository(&setup_start_path).map_err(CliError::runtime)?;
let repository_root = resolve_setup_repository(&setup_start_path)?;
setup::validate_existing_repo_local_config(&repository_root).map_err(CliError::runtime)?;

let setup_dispatch = if self.request.context_only {
None
Expand DownExpand Up@@ -102,6 +102,35 @@ impl SetupCommand {
}
}

fn resolve_setup_repository(start_path: &std::path::Path) -> Result<std::path::PathBuf, CliError> {
let repository_root = setup::ensure_git_repository(start_path).map_err(|source| {
if setup::is_not_git_repository_error(&source) {
CliError::user_with_source(UserError::NotGitRepository, source)
} else {
CliError::runtime(source)
}
})?;
let storage_config = config::resolve_agent_trace_storage_runtime_config(&repository_root)
.map_err(CliError::runtime)?;

setup::ensure_git_remote(&repository_root, &storage_config.repository_remote).map_err(
|source| {
if setup::is_missing_git_remote_error(&source) {
CliError::user_with_source(
UserError::NotGitRemote {
remote_name: storage_config.repository_remote.clone(),
},
source,
)
} else {
CliError::runtime(source)
}
},
)?;

Ok(repository_root)
}

fn setup_required_hooks_outcome_from_lifecycle(
outcome: &RequiredHooksInstallOutcome,
) -> setup::RequiredHooksInstallOutcome {
Expand Down
Loading
Loading