Skip to content
Merged
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
18 changes: 8 additions & 10 deletions src/commands/config/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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()
Expand All @@ -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
)
}
}
Expand All @@ -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: <bold>{}</>",
"{} already exists: <bold>{}</>",
kind.label(),
format_path_for_display(&path)
))
);
Expand All @@ -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 <underline>wt config show</>")
} else if is_project {
} else if kind == ConfigFileKind::Project {
// Project config exists, no user config
cformat!(
"To view, run <underline>wt config show</>. To create a user config, run <underline>wt config create</>"
Expand Down Expand Up @@ -135,7 +133,7 @@ fn create_config_file(
"{}",
success_message(cformat!(
"Created {}: <bold>{}</>",
config_type.to_lowercase(),
kind.label().to_lowercase(),
format_path_for_display(&path)
))
);
Expand Down
3 changes: 2 additions & 1 deletion src/config/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
))
})?;
Expand Down
35 changes: 21 additions & 14 deletions src/config/user/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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<toml::de::Error>,
},
/// Config files parsed cleanly; applying env-var overrides failed.
Expand All @@ -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)
)
}
Expand Down Expand Up @@ -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<toml::Table, LoadError> {
// Validate by deserializing — gives rich line/col errors.
if let Err(err) = toml::from_str::<UserConfig>(migrated) {
return Err(LoadError::File {
path: path.to_path_buf(),
label,
kind,
err: Box::new(err),
});
}
Expand Down Expand Up @@ -450,19 +451,22 @@ impl UserConfig {
&system_path,
&content,
true,
super::ConfigFileKind::System,
ConfigFileKind::System,
None,
true,
) {
Ok(result) => {
super::deprecation::warn_unknown_fields::<UserConfig>(
&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),
}
Expand All @@ -483,19 +487,22 @@ impl UserConfig {
config_path,
&content,
true,
super::ConfigFileKind::User,
ConfigFileKind::User,
None,
true,
) {
Ok(result) => {
super::deprecation::warn_unknown_fields::<UserConfig>(
&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),
}
Expand Down
2 changes: 1 addition & 1 deletion src/config/user/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2474,7 +2474,7 @@ fn test_load_error_display_file() {
let toml_err = toml::from_str::<UserConfig>("[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();
Expand Down
27 changes: 17 additions & 10 deletions src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -496,14 +497,17 @@ fn config_show_output(repo: &Repository) -> Option<String> {

// 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)
));
}

Expand All @@ -515,8 +519,8 @@ fn config_show_output(repo: &Repository) -> Option<String> {
}

/// 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"),
Expand Down Expand Up @@ -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)
");
}
Expand All @@ -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)"));
}

Expand All @@ -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\""));
}

Expand All @@ -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'));
}

Expand All @@ -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);
}
Expand Down
3 changes: 2 additions & 1 deletion src/git/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1631,7 +1631,8 @@ fn parse_config_list_z(stdout: &[u8]) -> indexmap::IndexMap<String, Vec<String>>
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!(
"{}",
Expand Down
Loading