use std::sync::Arc;
use crossbeam_channel as chan;
use worktrunk::git::{BranchRef, Repository, WorktreeInfo};
use super::super::model::{
ActiveGitOperation, ItemKind, ListItem, UpstreamStatus, WorkingTreeStatus,
};
use super::CollectOptions;
use super::tasks::{
AheadBehindTask, BranchDiffTask, CiStatusTask, CommitDetailsTask, CommittedTreesMatchTask,
GitOperationTask, HasFileChangesTask, IsAncestorTask, MergeTreeConflictsTask,
SummaryGenerateTask, Task, TaskContext, UpstreamTask, UrlStatusTask, UserMarkerTask,
WorkingTreeConflictsTask, WorkingTreeDiffTask, WouldMergeAddTask,
};
use super::types::{TaskError, TaskKind, TaskResult};
const COMMIT_TASKS: &[TaskKind] = &[
TaskKind::CommitDetails,
TaskKind::AheadBehind,
TaskKind::CommittedTreesMatch,
TaskKind::HasFileChanges,
TaskKind::IsAncestor,
TaskKind::BranchDiff,
TaskKind::MergeTreeConflicts,
TaskKind::WouldMergeAdd,
TaskKind::CiStatus,
TaskKind::Upstream,
];
#[derive(Clone)]
pub struct WorkItem {
pub ctx: TaskContext,
pub kind: TaskKind,
}
impl WorkItem {
pub fn execute(self) -> Result<TaskResult, TaskError> {
let result = dispatch_task(self.kind, self.ctx);
if let Ok(ref task_result) = result {
debug_assert_eq!(TaskKind::from(task_result), self.kind);
}
result
}
}
fn dispatch_task(kind: TaskKind, ctx: TaskContext) -> Result<TaskResult, TaskError> {
match kind {
TaskKind::CommitDetails => CommitDetailsTask::compute(ctx),
TaskKind::AheadBehind => AheadBehindTask::compute(ctx),
TaskKind::CommittedTreesMatch => CommittedTreesMatchTask::compute(ctx),
TaskKind::HasFileChanges => HasFileChangesTask::compute(ctx),
TaskKind::WouldMergeAdd => WouldMergeAddTask::compute(ctx),
TaskKind::IsAncestor => IsAncestorTask::compute(ctx),
TaskKind::BranchDiff => BranchDiffTask::compute(ctx),
TaskKind::WorkingTreeDiff => WorkingTreeDiffTask::compute(ctx),
TaskKind::MergeTreeConflicts => MergeTreeConflictsTask::compute(ctx),
TaskKind::WorkingTreeConflicts => WorkingTreeConflictsTask::compute(ctx),
TaskKind::GitOperation => GitOperationTask::compute(ctx),
TaskKind::UserMarker => UserMarkerTask::compute(ctx),
TaskKind::Upstream => UpstreamTask::compute(ctx),
TaskKind::CiStatus => CiStatusTask::compute(ctx),
TaskKind::UrlStatus => UrlStatusTask::compute(ctx),
TaskKind::SummaryGenerate => SummaryGenerateTask::compute(ctx),
}
}
#[derive(Default)]
pub(crate) struct ExpectedResults {
inner: std::sync::Mutex<Vec<Vec<TaskKind>>>,
}
impl ExpectedResults {
pub fn expect(&self, item_idx: usize, kind: TaskKind) {
let mut inner = self.inner.lock().unwrap();
if inner.len() <= item_idx {
inner.resize_with(item_idx + 1, Vec::new);
}
inner[item_idx].push(kind);
}
pub fn count(&self) -> usize {
self.inner.lock().unwrap().iter().map(|v| v.len()).sum()
}
pub fn results_for(&self, item_idx: usize) -> Vec<TaskKind> {
self.inner
.lock()
.unwrap()
.get(item_idx)
.cloned()
.unwrap_or_default()
}
}
pub(super) fn seed_skipped_task_defaults(item: &mut ListItem, kind: TaskKind) {
match kind {
TaskKind::CommitDetails
| TaskKind::BranchDiff
| TaskKind::CiStatus
| TaskKind::UrlStatus
| TaskKind::SummaryGenerate => {}
TaskKind::AheadBehind => {
item.is_orphan = Some(false);
}
TaskKind::Upstream => {
item.upstream = Some(UpstreamStatus::default());
}
TaskKind::CommittedTreesMatch => {
item.committed_trees_match = Some(false);
}
TaskKind::HasFileChanges => {
item.has_file_changes = Some(true);
}
TaskKind::WouldMergeAdd => {
item.would_merge_add = Some(true);
item.is_patch_id_match = Some(false);
}
TaskKind::IsAncestor => {
item.is_ancestor = Some(false);
}
TaskKind::MergeTreeConflicts => {
item.has_merge_tree_conflicts = Some(false);
}
TaskKind::UserMarker => {
item.user_marker = Some(None);
}
TaskKind::WorkingTreeDiff => {
}
TaskKind::WorkingTreeConflicts => {
if let ItemKind::Worktree(data) = &mut item.kind {
data.has_working_tree_conflicts = Some(None);
}
}
TaskKind::GitOperation => {
if let ItemKind::Worktree(data) = &mut item.kind {
data.git_operation = Some(ActiveGitOperation::None);
}
}
}
}
pub(super) fn seed_unborn_main_state(item: &mut ListItem) {
let is_main = matches!(&item.kind, ItemKind::Worktree(data) if data.is_main);
item.status_symbols.main_state = Some(if is_main {
super::super::model::MainState::IsMain
} else {
super::super::model::MainState::None
});
}
pub(super) fn seed_prunable_item(item: &mut ListItem) {
use super::super::model::{
Divergence, MainState, OperationState, StatusSymbols, WorktreeState,
};
item.status_symbols = StatusSymbols {
working_tree: Some(WorkingTreeStatus::default()),
operation_state: Some(OperationState::None),
worktree_state: Some(WorktreeState::Prunable),
main_state: Some(MainState::None),
upstream_divergence: Some(Divergence::None),
user_marker: Some(None),
};
}
pub fn work_items_for_worktree(
repo: &Repository,
wt: &WorktreeInfo,
item_idx: usize,
options: &CollectOptions,
expected_results: &Arc<ExpectedResults>,
tx: &chan::Sender<Result<TaskResult, TaskError>>,
item: &mut ListItem,
) -> Vec<WorkItem> {
if wt.is_prunable() {
seed_prunable_item(item);
return vec![];
}
let skip = &options.skip_tasks;
let include_url = !skip.contains(&TaskKind::UrlStatus);
let item_url = if include_url {
options.url_template.as_ref().and_then(|template| {
wt.branch.as_ref().and_then(|branch| {
let mut vars = std::collections::HashMap::new();
vars.insert("branch", branch.as_str());
worktrunk::config::expand_template(template, &vars, false, repo, "url-template")
.ok()
})
})
} else {
None
};
if include_url && let Some(ref url) = item_url {
expected_results.expect(item_idx, TaskKind::UrlStatus);
let _ = tx.send(Ok(TaskResult::UrlStatus {
item_idx,
url: Some(url.clone()),
active: None,
}));
}
let ctx = TaskContext {
repo: repo.clone(),
branch_ref: BranchRef::from(wt),
item_idx,
item_url,
llm_command: options.llm_command.clone(),
default_branch: options.default_branch.clone(),
integration_target: options.integration_target.clone(),
};
let has_commits = wt.has_commits();
let mut items = Vec::with_capacity(15);
for kind in [
TaskKind::CommitDetails,
TaskKind::AheadBehind,
TaskKind::CommittedTreesMatch,
TaskKind::HasFileChanges,
TaskKind::IsAncestor,
TaskKind::Upstream,
TaskKind::WorkingTreeDiff,
TaskKind::GitOperation,
TaskKind::UserMarker,
TaskKind::WorkingTreeConflicts,
TaskKind::BranchDiff,
TaskKind::MergeTreeConflicts,
TaskKind::CiStatus,
TaskKind::WouldMergeAdd,
TaskKind::SummaryGenerate,
] {
let will_skip = skip.contains(&kind)
|| (!has_commits && COMMIT_TASKS.contains(&kind))
|| (kind == TaskKind::SummaryGenerate && options.llm_command.is_none());
if will_skip {
seed_skipped_task_defaults(item, kind);
continue;
}
expected_results.expect(item_idx, kind);
items.push(WorkItem {
ctx: ctx.clone(),
kind,
});
}
if !has_commits {
seed_unborn_main_state(item);
}
if include_url && ctx.item_url.is_some() {
expected_results.expect(item_idx, TaskKind::UrlStatus);
items.push(WorkItem {
ctx: ctx.clone(),
kind: TaskKind::UrlStatus,
});
}
items
}
pub struct BranchSpawn<'a> {
pub name: &'a str,
pub commit_sha: &'a str,
pub item_idx: usize,
pub is_remote: bool,
}
pub fn work_items_for_branch(
repo: &Repository,
branch: BranchSpawn<'_>,
options: &CollectOptions,
expected_results: &Arc<ExpectedResults>,
item: &mut ListItem,
) -> Vec<WorkItem> {
let BranchSpawn {
name: branch_name,
commit_sha,
item_idx,
is_remote,
} = branch;
let skip = &options.skip_tasks;
let branch_ref = if is_remote {
BranchRef::remote_branch(branch_name, commit_sha)
} else {
BranchRef::local_branch(branch_name, commit_sha)
};
let ctx = TaskContext {
repo: repo.clone(),
branch_ref,
item_idx,
item_url: None, llm_command: options.llm_command.clone(),
default_branch: options.default_branch.clone(),
integration_target: options.integration_target.clone(),
};
let mut items = Vec::with_capacity(11);
for kind in [
TaskKind::CommitDetails,
TaskKind::AheadBehind,
TaskKind::CommittedTreesMatch,
TaskKind::HasFileChanges,
TaskKind::IsAncestor,
TaskKind::Upstream,
TaskKind::BranchDiff,
TaskKind::MergeTreeConflicts,
TaskKind::CiStatus,
TaskKind::WouldMergeAdd,
TaskKind::SummaryGenerate,
] {
let will_skip = skip.contains(&kind)
|| (kind == TaskKind::SummaryGenerate && options.llm_command.is_none());
if will_skip {
seed_skipped_task_defaults(item, kind);
continue;
}
expected_results.expect(item_idx, kind);
items.push(WorkItem {
ctx: ctx.clone(),
kind,
});
}
seed_skipped_task_defaults(item, TaskKind::UserMarker);
for kind in [
TaskKind::WorkingTreeDiff,
TaskKind::WorkingTreeConflicts,
TaskKind::GitOperation,
] {
seed_skipped_task_defaults(item, kind);
}
items
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::list::collect::build_worktree_item;
use std::collections::HashSet;
#[test]
fn test_skip_url_status_suppresses_placeholder_and_task() {
let test = worktrunk::testing::TestRepo::new();
let repo = Repository::at(test.path()).expect("repo");
let wt = WorktreeInfo {
path: test.path().to_path_buf(),
head: "deadbeef".to_string(),
branch: Some("main".to_string()),
bare: false,
detached: false,
locked: None,
prunable: None,
};
let skip_tasks: HashSet<TaskKind> = [TaskKind::UrlStatus].into_iter().collect();
let options = CollectOptions {
skip_tasks,
url_template: Some("http://localhost/{{ branch }}".to_string()),
llm_command: None,
default_branch: None,
integration_target: None,
};
let expected_results = Arc::new(ExpectedResults::default());
let (tx, rx) = chan::unbounded::<Result<TaskResult, TaskError>>();
let mut item = build_worktree_item(&wt, true, false, false);
let items =
work_items_for_worktree(&repo, &wt, 0, &options, &expected_results, &tx, &mut item);
assert!(rx.try_recv().is_err());
assert!(!items.iter().any(|w| w.kind == TaskKind::UrlStatus));
assert!(
!expected_results
.results_for(0)
.contains(&TaskKind::UrlStatus)
);
assert!(items.iter().all(|w| w.ctx.item_url.is_none()));
}
#[test]
fn test_no_llm_command_skips_summary_generate() {
let test = worktrunk::testing::TestRepo::new();
let repo = Repository::at(test.path()).expect("repo");
let wt = WorktreeInfo {
path: test.path().to_path_buf(),
head: "deadbeef".to_string(),
branch: Some("main".to_string()),
bare: false,
detached: false,
locked: None,
prunable: None,
};
let options = CollectOptions {
skip_tasks: HashSet::new(),
llm_command: None,
url_template: None,
default_branch: None,
integration_target: None,
};
let expected_results = Arc::new(ExpectedResults::default());
let (tx, _rx) = chan::unbounded::<Result<TaskResult, TaskError>>();
let mut item = build_worktree_item(&wt, true, false, false);
let items =
work_items_for_worktree(&repo, &wt, 0, &options, &expected_results, &tx, &mut item);
assert!(
!items.iter().any(|w| w.kind == TaskKind::SummaryGenerate),
"SummaryGenerate should be skipped when llm_command is None"
);
}
}