1use std::path::Path;
2use crate::config::Config;
3
4const DEP_COMMIT_CAP: usize = 20;
5
6pub fn build_dependency_bundle(root: &Path, depends_on: &[String], config: &Config) -> String {
18 if depends_on.is_empty() {
19 return String::new();
20 }
21
22 let all_tickets = crate::ticket::load_all_from_git(root, &config.tickets.dir)
23 .unwrap_or_default();
24
25 let terminal_ids = config.terminal_state_ids();
26 let default_branch = config.project.default_branch.clone();
27
28 let mut out = String::new();
29 out.push_str("# Dependency Context Bundle\n\n");
30
31 let mut any = false;
32
33 for dep_id in depends_on {
34 let Some(dep) = all_tickets.iter().find(|t| &t.frontmatter.id == dep_id) else {
35 out.push_str(&format!("### Dependency: {dep_id}\n*Ticket not found.*\n\n"));
36 any = true;
37 continue;
38 };
39
40 any = true;
41 let is_terminal = terminal_ids.contains(&dep.frontmatter.state);
42
43 out.push_str(&format!(
44 "### Dependency: {} — {}\n",
45 dep_id, dep.frontmatter.title
46 ));
47 out.push_str(&format!("**State:** {}", dep.frontmatter.state));
48 if !is_terminal {
49 out.push_str(
50 " ⚠️ *This dependency is not yet closed — its API may still change. Tread carefully.*",
51 );
52 }
53 out.push('\n');
54
55 if let Ok(doc) = dep.document() {
57 if let Some(approach) = crate::spec::get_section(&doc, "Approach")
58 .filter(|s| !s.is_empty())
59 {
60 out.push_str("\n**Approach:**\n");
61 out.push_str(&approach);
62 out.push('\n');
63 }
64 }
65
66 let dep_branch = dep
68 .frontmatter
69 .branch
70 .clone()
71 .or_else(|| crate::ticket_fmt::branch_name_from_path(&dep.path))
72 .unwrap_or_else(|| format!("ticket/{dep_id}"));
73 let target = dep
74 .frontmatter
75 .target_branch
76 .as_deref()
77 .unwrap_or(default_branch.as_str());
78 let subjects = commit_subjects(root, target, &dep_branch, DEP_COMMIT_CAP);
79 if !subjects.is_empty() {
80 out.push_str("\n**Commits landed:**\n");
81 for s in &subjects {
82 out.push_str(&format!("- {s}\n"));
83 }
84 }
85
86 if let Some(ref trans_ids) = dep.frontmatter.depends_on {
88 if !trans_ids.is_empty() {
89 out.push_str("\n**Transitive dependencies:**\n");
90 for trans_id in trans_ids {
91 if let Some(trans) = all_tickets.iter().find(|t| &t.frontmatter.id == trans_id) {
92 out.push_str(&format!("- **{}:** {}", trans_id, trans.frontmatter.title));
93 if let Ok(doc) = trans.document() {
94 if let Some(problem) = crate::spec::get_section(&doc, "Problem") {
95 if let Some(line) =
96 problem.lines().find(|l| !l.trim().is_empty())
97 {
98 out.push_str(&format!(" — {}", line.trim()));
99 }
100 }
101 }
102 out.push('\n');
103 } else {
104 out.push_str(&format!("- **{trans_id}:** *(not found)*\n"));
105 }
106 }
107 }
108 }
109
110 out.push('\n');
111 }
112
113 if !any {
114 return String::new();
115 }
116
117 out.push_str("***\n");
118 out
119}
120
121fn commit_subjects(root: &Path, target: &str, dep_branch: &str, max_count: usize) -> Vec<String> {
124 let dep_ref = if crate::git_util::remote_branch_tip(root, dep_branch).is_some() {
125 format!("origin/{dep_branch}")
126 } else {
127 dep_branch.to_string()
128 };
129 let target_ref = if crate::git_util::remote_branch_tip(root, target).is_some() {
130 format!("origin/{target}")
131 } else {
132 target.to_string()
133 };
134 let range = format!("{target_ref}..{dep_ref}");
135 let max_str = max_count.to_string();
136 let output = crate::git_util::run(
137 root,
138 &["log", "--pretty=%s", &range, &format!("--max-count={max_str}")],
139 )
140 .unwrap_or_default();
141 output
142 .lines()
143 .filter(|l| !l.trim().is_empty())
144 .map(|l| l.to_string())
145 .collect()
146}
147