1use std::path::Path;
9
10use crate::error::Result;
11use crate::git::cli::GitCli;
12
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
15pub struct WtMeta {
16 pub base_ref: Option<String>,
18 pub pr_number: Option<u64>,
20 pub pr_state: Option<String>,
22 pub pr_title: Option<String>,
24 pub pr_url: Option<String>,
26 pub created_by_wt: bool,
28}
29
30fn key(branch: &str, name: &str) -> String {
32 format!("wt.{branch}.{name}")
33}
34
35pub fn read_meta(repo: &gix::Repository, branch: &str) -> WtMeta {
37 let config = repo.config_snapshot();
38 let base_ref = config
39 .string(key(branch, "baseRef").as_str())
40 .map(|v| v.to_string());
41 let pr_number = config
42 .string(key(branch, "prNumber").as_str())
43 .and_then(|v| v.to_string().parse::<u64>().ok());
44 let pr_state = config
45 .string(key(branch, "prState").as_str())
46 .map(|v| v.to_string());
47 let pr_title = config
48 .string(key(branch, "prTitle").as_str())
49 .map(|v| v.to_string());
50 let pr_url = config
51 .string(key(branch, "prUrl").as_str())
52 .map(|v| v.to_string());
53 let created_by_wt = config
54 .boolean(key(branch, "createdByWt").as_str())
55 .unwrap_or(false);
56 WtMeta {
57 base_ref,
58 pr_number,
59 pr_state,
60 pr_title,
61 pr_url,
62 created_by_wt,
63 }
64}
65
66pub fn write_pr(
68 git: &dyn GitCli,
69 repo_root: &Path,
70 branch: &str,
71 number: u64,
72 state: &str,
73 title: &str,
74) -> Result<()> {
75 write_pr_number(git, repo_root, branch, number)?;
76 git.run(repo_root, &["config", &key(branch, "prState"), state])?;
77 git.run(repo_root, &["config", &key(branch, "prTitle"), title])?;
78 Ok(())
79}
80
81pub fn write_pr_url(git: &dyn GitCli, repo_root: &Path, branch: &str, url: &str) -> Result<()> {
83 git.run(repo_root, &["config", &key(branch, "prUrl"), url])?;
84 Ok(())
85}
86
87pub fn write_base_ref(
89 git: &dyn GitCli,
90 repo_root: &Path,
91 branch: &str,
92 base_ref: &str,
93) -> Result<()> {
94 git.run(repo_root, &["config", &key(branch, "baseRef"), base_ref])?;
95 Ok(())
96}
97
98pub fn write_pr_number(
100 git: &dyn GitCli,
101 repo_root: &Path,
102 branch: &str,
103 number: u64,
104) -> Result<()> {
105 git.run(
106 repo_root,
107 &["config", &key(branch, "prNumber"), &number.to_string()],
108 )?;
109 Ok(())
110}
111
112pub fn mark_created_by_wt(git: &dyn GitCli, repo_root: &Path, branch: &str) -> Result<()> {
114 git.run(repo_root, &["config", &key(branch, "createdByWt"), "true"])?;
115 Ok(())
116}
117
118pub fn clear_meta(git: &dyn GitCli, repo_root: &Path, branch: &str) -> Result<()> {
121 let section = format!("wt.{branch}");
122 git.run_raw(repo_root, &["config", "--remove-section", §ion])?;
124 Ok(())
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::git::cli::RealGit;
131 use crate::git::discover::Repo;
132 use crate::testutil::TestRepo;
133
134 fn meta(repo: &TestRepo, branch: &str) -> WtMeta {
135 let r = Repo::discover(repo.root()).unwrap();
136 read_meta(r.gix(), branch)
137 }
138
139 #[test]
140 fn unset_metadata_is_empty() {
141 let repo = TestRepo::init();
142 assert_eq!(meta(&repo, "main"), WtMeta::default());
143 }
144
145 #[test]
146 fn base_ref_round_trips() {
147 let repo = TestRepo::init();
148 write_base_ref(&RealGit, repo.root(), "main", "develop").unwrap();
149 assert_eq!(meta(&repo, "main").base_ref.as_deref(), Some("develop"));
150 }
151
152 #[test]
153 fn pr_number_round_trips() {
154 let repo = TestRepo::init();
155 write_pr_number(&RealGit, repo.root(), "main", 42).unwrap();
156 assert_eq!(meta(&repo, "main").pr_number, Some(42));
157 }
158
159 #[test]
160 fn created_by_wt_round_trips() {
161 let repo = TestRepo::init();
162 assert!(!meta(&repo, "main").created_by_wt);
163 mark_created_by_wt(&RealGit, repo.root(), "main").unwrap();
164 assert!(meta(&repo, "main").created_by_wt);
165 }
166
167 #[test]
168 fn metadata_works_for_slashed_branch_names() {
169 let repo = TestRepo::init();
170 write_base_ref(&RealGit, repo.root(), "feature/login", "main").unwrap();
171 write_pr_number(&RealGit, repo.root(), "feature/login", 7).unwrap();
172 mark_created_by_wt(&RealGit, repo.root(), "feature/login").unwrap();
173 let m = meta(&repo, "feature/login");
174 assert_eq!(m.base_ref.as_deref(), Some("main"));
175 assert_eq!(m.pr_number, Some(7));
176 assert!(m.created_by_wt);
177 }
178
179 #[test]
180 fn write_pr_caches_number_state_and_title() {
181 let repo = TestRepo::init();
182 write_pr(&RealGit, repo.root(), "main", 99, "open", "Add feature").unwrap();
183 let m = meta(&repo, "main");
184 assert_eq!(m.pr_number, Some(99));
185 assert_eq!(m.pr_state.as_deref(), Some("open"));
186 assert_eq!(m.pr_title.as_deref(), Some("Add feature"));
187 }
188
189 #[test]
190 fn clear_removes_all_metadata() {
191 let repo = TestRepo::init();
192 write_base_ref(&RealGit, repo.root(), "topic", "main").unwrap();
193 mark_created_by_wt(&RealGit, repo.root(), "topic").unwrap();
194 clear_meta(&RealGit, repo.root(), "topic").unwrap();
195 assert_eq!(meta(&repo, "topic"), WtMeta::default());
196 clear_meta(&RealGit, repo.root(), "topic").unwrap();
198 }
199}