use std::path::PathBuf;
use clap::Args;
use serde::Serialize;
use shipshape_core::release::journal::JournalPaths;
use crate::error::CliError;
use crate::output::OutputFormat;
use crate::sys::RealGitRepo;
#[derive(Args, Debug)]
pub struct ConfigArgs {
#[arg(long, value_name = "PATH")]
pub repo_root: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
pub journal_dir: Option<PathBuf>,
}
#[derive(clap::Subcommand, Debug)]
pub enum ConfigAction {
Path(ConfigArgs),
Show(ConfigArgs),
}
pub fn dispatch(action: ConfigAction, format: OutputFormat) -> Result<(), CliError> {
match action {
ConfigAction::Path(args) => path(&args, format),
ConfigAction::Show(args) => show(&args, format),
}
}
fn path(args: &ConfigArgs, format: OutputFormat) -> Result<(), CliError> {
let report = resolve(args)?;
match format {
OutputFormat::Json => crate::output::emit_json(&report, &[])?,
OutputFormat::Text => {
crate::output::stdoutln!("contract_path: {}", report.contract_path.value)?;
match &report.journal_dir.value {
Some(path) => crate::output::stdoutln!("journal_dir: {path}")?,
None => crate::output::stdoutln!(
"journal_dir: unavailable ({})",
report.journal_dir.detail
)?,
}
}
}
Ok(())
}
fn show(args: &ConfigArgs, format: OutputFormat) -> Result<(), CliError> {
let report = resolve(args)?;
match format {
OutputFormat::Json => crate::output::emit_json(&report, &[])?,
OutputFormat::Text => {
crate::output::stdoutln!("contract_path: {}", report.contract_path.value)?;
crate::output::stdoutln!(" source: {}", report.contract_path.source)?;
crate::output::stdoutln!(" detail: {}", report.contract_path.detail)?;
crate::output::stdoutln!(" lossy: {}", report.contract_path.lossy)?;
match &report.journal_dir.value {
Some(path) => crate::output::stdoutln!("journal_dir: {path}")?,
None => crate::output::stdoutln!("journal_dir: unavailable")?,
}
crate::output::stdoutln!(" source: {}", report.journal_dir.source)?;
crate::output::stdoutln!(" detail: {}", report.journal_dir.detail)?;
crate::output::stdoutln!(" lossy: {}", report.journal_dir.lossy)?;
if report.git_environment.is_empty() {
crate::output::stdoutln!("git_environment: none")?;
} else {
for value in &report.git_environment {
crate::output::stdoutln!(
"git_environment.{}: {} ({}, lossy={})",
value.key,
value.value,
value.source,
value.lossy
)?;
}
}
}
}
Ok(())
}
#[derive(Debug, Serialize)]
struct ConfigReport {
contract_path: ResolvedPath<String>,
journal_dir: ResolvedPath<Option<String>>,
git_environment: Vec<EnvironmentValue>,
}
#[derive(Debug, Serialize)]
struct ResolvedPath<T> {
value: T,
source: Source,
detail: String,
lossy: bool,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
enum Source {
Default,
Flag,
Git,
Unresolved,
}
impl std::fmt::Display for Source {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let value = match self {
Self::Default => "default",
Self::Flag => "flag",
Self::Git => "git",
Self::Unresolved => "unresolved",
};
formatter.write_str(value)
}
}
#[derive(Debug, Serialize)]
struct EnvironmentValue {
key: &'static str,
value: String,
source: String,
lossy: bool,
}
fn resolve(args: &ConfigArgs) -> Result<ConfigReport, CliError> {
let (repo_root, repo_source, repo_detail) = match &args.repo_root {
Some(path) => (
path.clone(),
Source::Flag,
"selected by --repo-root".to_string(),
),
None => (
std::env::current_dir()
.map_err(|e| CliError::system("io_error", format!("cannot resolve cwd: {e}")))?,
Source::Default,
"current directory (the default --repo-root)".to_string(),
),
};
let contract_display =
display_path(&repo_root.join(shipshape_core::contract::CONTRACT_FILENAME));
let contract_path = ResolvedPath {
value: contract_display.value,
source: repo_source,
detail: repo_detail,
lossy: contract_display.lossy,
};
let journal_dir = match &args.journal_dir {
Some(path) => {
let display = display_path(path);
ResolvedPath {
value: Some(display.value),
source: Source::Flag,
detail: "selected by --journal-dir".to_string(),
lossy: display.lossy,
}
}
None => match JournalPaths::from_git(&RealGitRepo::new(&repo_root), None) {
Ok(paths) => {
let display = display_path(paths.releases_dir());
ResolvedPath {
value: Some(display.value),
source: Source::Git,
detail: "derived from git rev-parse --git-common-dir".to_string(),
lossy: display.lossy,
}
}
Err(error) => ResolvedPath {
value: None,
source: Source::Unresolved,
detail: format!("could not derive git-common-dir/ossctl/releases: {error}"),
lossy: false,
},
},
};
let git_environment = if args.journal_dir.is_none() {
["GIT_DIR", "GIT_COMMON_DIR"]
.into_iter()
.filter_map(|key| {
std::env::var_os(key).map(|value| {
let rendered = value.to_string_lossy();
EnvironmentValue {
key,
source: format!("env:{key}"),
lossy: matches!(rendered, std::borrow::Cow::Owned(_)),
value: rendered.into_owned(),
}
})
})
.collect()
} else {
Vec::new()
};
Ok(ConfigReport {
contract_path,
journal_dir,
git_environment,
})
}
struct DisplayPath {
value: String,
lossy: bool,
}
fn display_path(path: &std::path::Path) -> DisplayPath {
let rendered = path.as_os_str().to_string_lossy();
DisplayPath {
lossy: matches!(rendered, std::borrow::Cow::Owned(_)),
value: rendered.into_owned(),
}
}