diff --git a/src/commands/config/create.rs b/src/commands/config/create.rs index 3b511d18c3..69ce50398b 100644 --- a/src/commands/config/create.rs +++ b/src/commands/config/create.rs @@ -5,7 +5,7 @@ use anyhow::Context; use color_print::cformat; use std::path::PathBuf; -use worktrunk::config::require_config_path; +use worktrunk::config::{ConfigFileKind, require_config_path}; use worktrunk::git::Repository; use worktrunk::path::format_path_for_display; use worktrunk::styling::{eprintln, hint_message, info_message, success_message}; @@ -57,13 +57,12 @@ pub fn handle_config_create(project: bool) -> anyhow::Result<()> { create_config_file( config_path, PROJECT_CONFIG_EXAMPLE, - "Project config", + ConfigFileKind::Project, &[ "Edit this file to configure hooks for this repository", "See https://worktrunk.dev/hook/ for hook documentation", ], user_config_exists, - true, // is_project ) } else { let project_config_exists = Repository::current() @@ -75,10 +74,9 @@ pub fn handle_config_create(project: bool) -> anyhow::Result<()> { create_config_file( require_config_path()?, USER_CONFIG_EXAMPLE, - "User config", + ConfigFileKind::User, &["Edit this file to customize worktree paths and LLM settings"], project_config_exists, - false, // is_project ) } } @@ -87,17 +85,17 @@ pub fn handle_config_create(project: bool) -> anyhow::Result<()> { fn create_config_file( path: PathBuf, content: &str, - config_type: &str, + kind: ConfigFileKind, success_hints: &[&str], other_config_exists: bool, - is_project: bool, ) -> anyhow::Result<()> { // Check if file already exists if path.exists() { eprintln!( "{}", info_message(cformat!( - "{config_type} already exists: {}", + "{} already exists: {}", + kind.label(), format_path_for_display(&path) )) ); @@ -106,7 +104,7 @@ fn create_config_file( let hint = if other_config_exists { // Both configs exist cformat!("To view both user and project configs, run wt config show") - } else if is_project { + } else if kind == ConfigFileKind::Project { // Project config exists, no user config cformat!( "To view, run wt config show. To create a user config, run wt config create" @@ -135,7 +133,7 @@ fn create_config_file( "{}", success_message(cformat!( "Created {}: {}", - config_type.to_lowercase(), + kind.label().to_lowercase(), format_path_for_display(&path) )) ); diff --git a/src/config/project.rs b/src/config/project.rs index df3d31d61c..3f377a975f 100644 --- a/src/config/project.rs +++ b/src/config/project.rs @@ -298,7 +298,8 @@ impl ProjectConfig { // (e.g. `pre-start`/`post-start`) still load into their canonical fields. let config: ProjectConfig = toml::from_str(&migrated).map_err(|e| { ConfigError(format!( - "Project config at {} failed to parse:\n{e}", + "{} at {} failed to parse:\n{e}", + super::ConfigFileKind::Project.label(), crate::path::format_path_for_display(&config_path), )) })?; diff --git a/src/config/user/mod.rs b/src/config/user/mod.rs index 04e5c6f89f..10fe4cbf91 100644 --- a/src/config/user/mod.rs +++ b/src/config/user/mod.rs @@ -16,7 +16,7 @@ mod tests; use std::path::{Path, PathBuf}; use std::sync::OnceLock; -use super::ConfigError; +use super::{ConfigError, ConfigFileKind}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -68,7 +68,7 @@ pub enum LoadError { /// line/column info and a source-snippet pointer. File { path: PathBuf, - label: &'static str, + kind: ConfigFileKind, err: Box, }, /// Config files parsed cleanly; applying env-var overrides failed. @@ -89,10 +89,11 @@ pub enum LoadError { impl std::fmt::Display for LoadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - LoadError::File { path, label, err } => { + LoadError::File { path, kind, err } => { write!( f, - "{label} at {} failed to parse:\n{err}", + "{} at {} failed to parse:\n{err}", + kind.label(), crate::path::format_path_for_display(path) ) } @@ -285,13 +286,13 @@ fn deep_merge_table(base: &mut toml::Table, overlay: toml::Table) { fn load_config_file( path: &Path, migrated: &str, - label: &'static str, + kind: ConfigFileKind, ) -> Result { // Validate by deserializing — gives rich line/col errors. if let Err(err) = toml::from_str::(migrated) { return Err(LoadError::File { path: path.to_path_buf(), - label, + kind, err: Box::new(err), }); } @@ -450,7 +451,7 @@ impl UserConfig { &system_path, &content, true, - super::ConfigFileKind::System, + ConfigFileKind::System, None, true, ) { @@ -458,11 +459,14 @@ impl UserConfig { super::deprecation::warn_unknown_fields::( &content, &system_path, - super::ConfigFileKind::System, + ConfigFileKind::System, ); - match load_config_file(&system_path, &result.migrated_content, "System config") - { + match load_config_file( + &system_path, + &result.migrated_content, + ConfigFileKind::System, + ) { Ok(table) => deep_merge_table(&mut merged_table, table), Err(e) => warnings.push(e), } @@ -483,7 +487,7 @@ impl UserConfig { config_path, &content, true, - super::ConfigFileKind::User, + ConfigFileKind::User, None, true, ) { @@ -491,11 +495,14 @@ impl UserConfig { super::deprecation::warn_unknown_fields::( &content, config_path, - super::ConfigFileKind::User, + ConfigFileKind::User, ); - match load_config_file(config_path, &result.migrated_content, "User config") - { + match load_config_file( + config_path, + &result.migrated_content, + ConfigFileKind::User, + ) { Ok(table) => deep_merge_table(&mut merged_table, table), Err(e) => warnings.push(e), } diff --git a/src/config/user/tests.rs b/src/config/user/tests.rs index 07ee6e7f6a..8a58c59fa3 100644 --- a/src/config/user/tests.rs +++ b/src/config/user/tests.rs @@ -2474,7 +2474,7 @@ fn test_load_error_display_file() { let toml_err = toml::from_str::("[list]\nbranches = \"bad\"\n").unwrap_err(); let err = LoadError::File { path: std::path::PathBuf::from("/tmp/config.toml"), - label: "User config", + kind: ConfigFileKind::User, err: Box::new(toml_err), }; let msg = err.to_string(); diff --git a/src/diagnostic.rs b/src/diagnostic.rs index ef3e437e8a..23ffc07317 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -62,6 +62,7 @@ use ansi_str::AnsiStr; use anyhow::Context; use color_print::cformat; use minijinja::{Environment, context}; +use worktrunk::config::ConfigFileKind; use worktrunk::git::Repository; use worktrunk::path::format_path_for_display; use worktrunk::shell_exec::Cmd; @@ -496,14 +497,17 @@ fn config_show_output(repo: &Repository) -> Option { // User config if let Some(user_config_path) = worktrunk::config::config_path() { - output.push_str(&format_config_section(&user_config_path, "User config")); + output.push_str(&format_config_section( + &user_config_path, + ConfigFileKind::User, + )); } // Project config if let Ok(Some(project_config_path)) = repo.project_config_path() { output.push_str(&format!( "\n{}", - format_config_section(&project_config_path, "Project config") + format_config_section(&project_config_path, ConfigFileKind::Project) )); } @@ -515,8 +519,8 @@ fn config_show_output(repo: &Repository) -> Option { } /// Format a config file section for diagnostic output. -fn format_config_section(path: &std::path::Path, label: &str) -> String { - let mut output = format!("{}: {}\n", label, path.display()); +fn format_config_section(path: &std::path::Path, kind: ConfigFileKind) -> String { + let mut output = format!("{}: {}\n", kind.label(), path.display()); if path.exists() { match std::fs::read_to_string(path) { Ok(content) if content.trim().is_empty() => output.push_str("(empty file)\n"), @@ -547,9 +551,12 @@ mod tests { #[test] fn test_format_config_section_file_not_found() { - let result = format_config_section(std::path::Path::new("/nonexistent/path.toml"), "Test"); + let result = format_config_section( + std::path::Path::new("/nonexistent/path.toml"), + ConfigFileKind::User, + ); insta::assert_snapshot!(result, @" - Test: /nonexistent/path.toml + User config: /nonexistent/path.toml (file not found) "); } @@ -560,7 +567,7 @@ mod tests { let path = tmp.path().join("empty.toml"); std::fs::write(&path, "").unwrap(); - let result = format_config_section(&path, "Test"); + let result = format_config_section(&path, ConfigFileKind::User); assert!(result.contains("(empty file)")); } @@ -570,7 +577,7 @@ mod tests { let path = tmp.path().join("config.toml"); std::fs::write(&path, "key = \"value\"\n").unwrap(); - let result = format_config_section(&path, "Test"); + let result = format_config_section(&path, ConfigFileKind::User); assert!(result.contains("key = \"value\"")); } @@ -580,7 +587,7 @@ mod tests { let path = tmp.path().join("config.toml"); std::fs::write(&path, "no-newline").unwrap(); - let result = format_config_section(&path, "Test"); + let result = format_config_section(&path, ConfigFileKind::User); assert!(result.ends_with('\n')); } @@ -591,7 +598,7 @@ mod tests { let content = "x".repeat(5000); std::fs::write(&path, &content).unwrap(); - let result = format_config_section(&path, "Test"); + let result = format_config_section(&path, ConfigFileKind::User); assert!(result.contains("(truncated)")); assert!(result.len() < 5000); } diff --git a/src/git/repository/mod.rs b/src/git/repository/mod.rs index 128ae687c9..43d5d713fe 100644 --- a/src/git/repository/mod.rs +++ b/src/git/repository/mod.rs @@ -1631,7 +1631,8 @@ fn parse_config_list_z(stdout: &[u8]) -> indexmap::IndexMap> fn emit_user_config_warnings(warnings: &[LoadError]) { for warning in warnings { match warning { - LoadError::File { path, label, err } => { + LoadError::File { path, kind, err } => { + let label = kind.label(); let path_display = crate::path::format_path_for_display(path); crate::styling::eprintln!( "{}",