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
3 changes: 1 addition & 2 deletions dev/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@
#
# columns = ["branch", "status", "ci", "path"] # Columns to show, in order — built-ins or custom headers (omit for the default set)
#
# task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables
# timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables
# timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables
#
# `columns` selects and orders the columns to render; omit it for the default set.
# It is meant to drive a per-invocation alias (https://worktrunk.dev/extending/#aliases)
Expand Down
3 changes: 1 addition & 2 deletions docs/content/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,7 @@ json-schema = 2 # JSON output schema: 2 (envelope) or 1 (bare array, the curr

columns = ["branch", "status", "ci", "path"] # Columns to show, in order — built-ins or custom headers (omit for the default set)

task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables
timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables
timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables
```

`columns` selects and orders the columns to render; omit it for the default set.
Expand Down
3 changes: 1 addition & 2 deletions plugins/worktrunk/skills/worktrunk/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,7 @@ json-schema = 2 # JSON output schema: 2 (envelope) or 1 (bare array, the curr

columns = ["branch", "status", "ci", "path"] # Columns to show, in order — built-ins or custom headers (omit for the default set)

task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables
timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables
timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables
```

`columns` selects and orders the columns to render; omit it for the default set.
Expand Down
3 changes: 1 addition & 2 deletions skills/worktrunk/reference/config.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2055,8 +2055,7 @@ json-schema = 2 # JSON output schema: 2 (envelope) or 1 (bare array, the curr

columns = ["branch", "status", "ci", "path"] # Columns to show, in order — built-ins or custom headers (omit for the default set)

task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables
timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables
timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables
```

`columns` selects and orders the columns to render; omit it for the default set.
Expand Down
27 changes: 5 additions & 22 deletions src/commands/list/collect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,6 @@ pub enum ShowConfig {
Resolved {
show_branches: bool,
show_remotes: bool,
command_timeout: Option<std::time::Duration>,
/// Wall-clock deadline for the collect phase. `None` uses the default
/// [`DRAIN_TIMEOUT`](results::DRAIN_TIMEOUT) and shows a warning on timeout.
collect_deadline: Option<std::time::Instant>,
Expand All @@ -612,7 +611,7 @@ pub enum ShowConfig {
},
/// Raw CLI flags; config resolution deferred to collect's parallel phase
/// so project_identifier runs concurrently with other git operations.
/// Timeouts are resolved from config internally.
/// The collect deadline is resolved from config internally.
DeferredToParallel {
cli_branches: bool,
cli_remotes: bool,
Expand Down Expand Up @@ -908,7 +907,6 @@ pub fn collect(
show_branches,
show_remotes,
show_full,
command_timeout,
collect_deadline,
list_width,
progressive_handler,
Expand All @@ -917,7 +915,6 @@ pub fn collect(
ShowConfig::Resolved {
show_branches,
show_remotes,
command_timeout,
collect_deadline,
list_width,
progressive_handler,
Expand All @@ -930,7 +927,6 @@ pub fn collect(
// opts out of the untracked-inclusive working diff — the last tuple
// field — so the two `show_full`-shaped values aren't the same bucket.
true,
command_timeout,
collect_deadline,
list_width,
progressive_handler,
Expand All @@ -945,19 +941,16 @@ pub fn collect(
let show_branches = cli_branches || config.list.branches();
let show_remotes = cli_remotes || config.list.remotes();
let show_full = cli_full || config.list.full();
// Resolve timeouts from merged config (--full disables both)
let (command_timeout, collect_deadline) = if show_full {
(None, None)
// Resolve the collect budget from merged config (--full disables it)
let collect_deadline = if show_full {
None
} else {
let task_timeout = config.list.task_timeout();
let deadline = config.list.timeout().map(|d| std::time::Instant::now() + d);
(task_timeout, deadline)
config.list.timeout().map(|d| std::time::Instant::now() + d)
};
(
show_branches,
show_remotes,
show_full,
command_timeout,
collect_deadline,
None,
None,
Expand Down Expand Up @@ -1592,15 +1585,6 @@ pub fn collect(
if let Some(snap_arc) = snap.as_ref() {
let snap_for_primer = std::sync::Arc::clone(snap_arc);
s.spawn(move |_| {
// Honor `list.task-timeout-ms` for the primer's git
// commands — these are the same `for-each-ref
// %(ahead-behind)` / `rev-list` invocations that
// used to run inside `UpstreamTask`, where the
// worker loop sets the per-thread timeout. Without
// this, `wt list` could sit at the skeleton on a
// pathologically slow git until the (untimed) batch
// returned.
worktrunk::shell_exec::set_command_timeout(command_timeout);
let all_locals = snap_for_primer.local_branches();
let filtered_locals: Vec<LocalBranch>;
let candidates: &[LocalBranch] = if show_branches {
Expand Down Expand Up @@ -1780,7 +1764,6 @@ pub fn collect(
// when the picker is open. See `COLLECT_POOL`.
COLLECT_POOL.install(|| {
all_work_items.into_par_iter().for_each(|item| {
worktrunk::shell_exec::set_command_timeout(command_timeout);
let result = item.execute();
let _ = tx_worker.send(result);
});
Expand Down
9 changes: 0 additions & 9 deletions src/commands/picker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1377,7 +1377,6 @@ struct PipelineFactory {
header_flash: Arc<items::HeaderFlash>,
preview_dims: (usize, usize),
skim_list_width: usize,
command_timeout: Option<std::time::Duration>,
llm_command: Option<String>,
summary_hint: Option<String>,
show_branches: bool,
Expand Down Expand Up @@ -1507,7 +1506,6 @@ impl PipelineFactory {
let bg_repo = spawn_repo.clone();
let show_branches = self.show_branches;
let show_remotes = self.show_remotes;
let command_timeout = self.command_timeout;
let skim_list_width = self.skim_list_width;
let collect_handle = std::thread::Builder::new()
.name("picker-collect".into())
Expand All @@ -1517,7 +1515,6 @@ impl PipelineFactory {
collect::ShowConfig::Resolved {
show_branches,
show_remotes,
command_timeout,
collect_deadline: None,
list_width: Some(skim_list_width),
progressive_handler: Some(bg_handler),
Expand Down Expand Up @@ -1726,10 +1723,6 @@ pub fn handle_picker(
// the same live status. `--prs` rows carry their own number from the explicit
// `--prs` forge call.

// Per-task command timeout (bounds any single git invocation) from
// shared `[list]` config. Still applies in progressive mode.
let command_timeout = config.list.task_timeout();

// Progressive rendering means the picker never blocks waiting for
// collect — so there's no UI-freeze budget to bound. The drain runs
// until its results channel closes or the fallback DRAIN_TIMEOUT
Expand Down Expand Up @@ -1872,7 +1865,6 @@ summary = true
header_flash: Arc::new(items::HeaderFlash::default()),
preview_dims,
skim_list_width,
command_timeout,
llm_command,
summary_hint,
show_branches,
Expand Down Expand Up @@ -3158,7 +3150,6 @@ pub mod tests {
header_flash: Arc::new(super::items::HeaderFlash::default()),
preview_dims: (80, 24),
skim_list_width: 80,
command_timeout: None,
llm_command: None,
summary_hint: None,
show_branches: false,
Expand Down
140 changes: 140 additions & 0 deletions src/config/deprecation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,9 @@ pub enum DeprecationKind {
NoCd,
/// `timeout-ms` under `[switch.picker]` (removed — picker renders progressively).
SwitchPickerTimeout,
/// `task-timeout-ms` under `[list]` (removed — `[list] timeout-ms` bounds
/// the collect phase).
ListTaskTimeout,
/// `[list] json-schema` unset while the default is scheduled to switch to
/// schema 2 — `wt config update` writes the upcoming `json-schema = 2`.
/// Warns at the JSON-emitting surface (`resolve_json_schema`), not at
Expand Down Expand Up @@ -647,6 +650,17 @@ const DEPRECATION_RULES: &[DeprecationRule] = &[
Vec::new()
}
}),
// list.task-timeout-ms — removed; `[list] timeout-ms` bounds the collect
// phase, and the drain has its own fallback bound.
DeprecationRule::Structural(|doc| {
if for_each_config_table_mut(doc, |_, table| {
remove_section_key_in(table, "list", "task-timeout-ms")
}) {
vec![DeprecationKind::ListTaskTimeout]
} else {
Vec::new()
}
}),
// [list] json-schema unset → write json-schema = 2, adopting the default
// ahead of the release that switches it. User config only: the key isn't
// valid in project config, and the top-level write covers every repo
Expand Down Expand Up @@ -1203,6 +1217,21 @@ fn remove_switch_picker_timeout_in(table: &mut toml_edit::Table) -> bool {
}
}

/// Remove `key` from a top-level `section` in a table (top-level or project).
/// An emptied section is left in place — it round-trips harmlessly.
///
/// A section can be written as a section table (`[list]`) or inline
/// (`list = { … }`); `toml_edit` surfaces these as different node types, so
/// each shape gets its own branch — matching the inline-aware `no-cd`/`no-ff`
/// rules and the two-level [`remove_switch_picker_timeout_in`].
fn remove_section_key_in(table: &mut toml_edit::Table, section: &str, key: &str) -> bool {
match table.get_mut(section) {
Some(toml_edit::Item::Table(t)) => t.remove(key).is_some(),
Some(toml_edit::Item::Value(toml_edit::Value::InlineTable(it))) => it.remove(key).is_some(),
_ => false,
}
}

fn migrate_content_from_doc(content: &str, mut doc: toml_edit::DocumentMut) -> String {
if migrate_content_doc(&mut doc) {
doc.to_string()
Expand Down Expand Up @@ -1677,6 +1706,15 @@ fn format_warning_lines<'a>(
))
);
}
DeprecationKind::ListTaskTimeout => {
let _ = writeln!(
out,
"{}",
warning_message(cformat!(
"{label}: <bold>list.task-timeout-ms</> is no longer used — <bold>list.timeout-ms</> bounds the collect phase"
))
);
}
DeprecationKind::JsonSchemaUnset => {
let _ = writeln!(
out,
Expand Down Expand Up @@ -3265,6 +3303,10 @@ json-schema = 1
// timeout-ms under an inline `switch` is stripped like the section form
"switch = { picker = { timeout-ms = 500 } }\n",
"[select]\ntimeout-ms = 500\n",
// list.task-timeout-ms, section and inline forms (project-scoped so
// the appended `[list]` below isn't a duplicate table)
"[projects.\"github.com/u/r\".list]\ntask-timeout-ms = 500\n",
"[projects.\"github.com/u/r\"]\nlist = { task-timeout-ms = 500 }\n",
"worktree-path = \"../{{ repo_root }}.{{ branch }}\"\n",
"[projects.\"github.com/u/r\"]\napproved-commands = [\"npm test\"]\n",
];
Expand Down Expand Up @@ -4302,6 +4344,104 @@ pager = "delta"
);
}

#[test]
fn test_detect_list_task_timeout_top_level() {
let content = r#"
[list]
branches = true
task-timeout-ms = 500
"#;
let deprecations = detect_deprecations(content, ConfigFileKind::User);
assert!(has_kind(&deprecations, |k| matches!(
k,
DeprecationKind::ListTaskTimeout
)));
}

#[test]
fn test_detect_list_task_timeout_project_level() {
let content = r#"
[projects."github.com/user/repo".list]
task-timeout-ms = 300
"#;
let deprecations = detect_deprecations(content, ConfigFileKind::User);
assert!(has_kind(&deprecations, |k| matches!(
k,
DeprecationKind::ListTaskTimeout
)));
}

#[test]
fn test_detect_list_task_timeout_absent() {
let content = r#"
[list]
timeout-ms = 500
"#;
let deprecations = detect_deprecations(content, ConfigFileKind::User);
assert!(!has_kind(&deprecations, |k| matches!(
k,
DeprecationKind::ListTaskTimeout
)));
}

#[test]
fn test_migrate_list_task_timeout_removes_key() {
let content = r#"
[list]
branches = true
task-timeout-ms = 500
timeout-ms = 2000
"#;
let result = migrate_content(content);
assert!(
!result.contains("task-timeout-ms"),
"Should strip task-timeout-ms: {result}"
);
assert!(
result.contains("timeout-ms = 2000") && result.contains("branches"),
"Should preserve sibling keys: {result}"
);
}

#[test]
fn test_migrate_list_task_timeout_inline_table() {
let content = r#"
list = { branches = true, task-timeout-ms = 500 }
"#;
let result = migrate_content(content);
assert!(!result.contains("task-timeout-ms"));
assert!(result.contains("branches"));
}

#[test]
fn test_migrate_list_task_timeout_noop_when_absent() {
let content = r#"
[list]
timeout-ms = 500
"#;
let result = migrate_content(content);
assert_eq!(result, content);
}

#[test]
fn test_format_deprecation_warnings_list_task_timeout() {
let info = DeprecationInfo {
config_path: std::path::PathBuf::from("/tmp/test-config.toml"),
deprecations: vec![DeprecationKind::ListTaskTimeout],
kind: ConfigFileKind::User,
main_worktree_path: None,
};
let output = format_deprecation_warnings(&info);
assert!(
output.contains("list.task-timeout-ms"),
"Should mention the field: {output}"
);
assert!(
output.contains("list.timeout-ms"),
"Should point at the surviving budget: {output}"
);
}

// ==================== negated bool format + migration tests ====================

#[test]
Expand Down
Loading
Loading