1use super::{open_board, open_board_locked};
7use crate::error::{Error, Result};
8use crate::storage::atomic_write;
9use std::path::{Path, PathBuf};
10use tokio::fs;
11
12const DOD_FILE: &str = "dod.md";
14
15pub async fn common_dod(project_dir: &Path) -> Result<Option<String>> {
20 let (board_dir, _repo, _config) = open_board(project_dir).await?;
21 let path = board_dir.join(DOD_FILE);
22 match fs::read_to_string(&path).await {
23 Ok(text) => {
24 let trimmed = text.trim();
25 Ok((!trimmed.is_empty()).then(|| trimmed.to_string()))
26 }
27 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
28 Err(e) => Err(Error::io(&path, &e)),
29 }
30}
31
32pub async fn set_common_dod(project_dir: &Path, text: &str) -> Result<PathBuf> {
37 let trimmed = text.trim();
38 if trimmed.is_empty() {
39 return Err(Error::EmptyDod);
40 }
41 let (board_dir, repo, _config, _lock) = open_board_locked(project_dir).await?;
42 let path = board_dir.join(DOD_FILE);
43 let body = format!("{trimmed}\n");
45 atomic_write(&path, &body).await?;
46 repo.commit("pinto: update common DoD").await?;
47 Ok(path)
48}
49
50pub async fn clear_common_dod(project_dir: &Path) -> Result<bool> {
54 let (board_dir, repo, _config, _lock) = open_board_locked(project_dir).await?;
55 let path = board_dir.join(DOD_FILE);
56 match fs::remove_file(&path).await {
57 Ok(()) => {
58 repo.commit("pinto: remove common DoD").await?;
59 Ok(true)
60 }
61 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
62 Err(e) => Err(Error::io(&path, &e)),
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::service::test_support::init_temp;
70
71 #[tokio::test]
72 async fn common_dod_is_none_when_unset() {
73 let dir = init_temp().await;
74 assert_eq!(common_dod(dir.path()).await.unwrap(), None);
75 }
76
77 #[tokio::test]
78 async fn set_then_load_roundtrips() {
79 let dir = init_temp().await;
80 set_common_dod(dir.path(), "- [ ] tests pass\n- [ ] reviewed")
81 .await
82 .expect("set succeeds");
83 assert_eq!(
84 common_dod(dir.path()).await.unwrap().as_deref(),
85 Some("- [ ] tests pass\n- [ ] reviewed"),
86 );
87 }
88
89 #[tokio::test]
90 async fn set_replaces_previous_value() {
91 let dir = init_temp().await;
92 set_common_dod(dir.path(), "old").await.unwrap();
93 set_common_dod(dir.path(), "new").await.unwrap();
94 assert_eq!(
95 common_dod(dir.path()).await.unwrap().as_deref(),
96 Some("new")
97 );
98 }
99
100 #[tokio::test]
101 async fn set_persists_as_plain_markdown_file() {
102 let dir = init_temp().await;
103 let path = set_common_dod(dir.path(), "- [ ] done").await.unwrap();
104 assert_eq!(path, dir.path().join(".pinto").join("dod.md"));
105 let raw = std::fs::read_to_string(&path).expect("file exists");
106 assert!(!raw.starts_with("+++"), "no frontmatter: {raw:?}");
108 assert_eq!(raw, "- [ ] done\n");
109 }
110
111 #[tokio::test]
112 async fn set_rejects_empty_text() {
113 let dir = init_temp().await;
114 let err = set_common_dod(dir.path(), " \n\t").await.unwrap_err();
115 assert!(matches!(err, Error::EmptyDod), "got {err:?}");
116 }
117
118 #[tokio::test]
119 async fn clear_removes_and_is_idempotent() {
120 let dir = init_temp().await;
121 set_common_dod(dir.path(), "x").await.unwrap();
122 assert!(clear_common_dod(dir.path()).await.unwrap(), "removed");
123 assert_eq!(common_dod(dir.path()).await.unwrap(), None);
124 assert!(
125 !clear_common_dod(dir.path()).await.unwrap(),
126 "already absent"
127 );
128 }
129
130 #[tokio::test]
131 async fn common_dod_without_init_errors() {
132 let dir = tempfile::TempDir::new().unwrap();
133 let err = common_dod(dir.path()).await.unwrap_err();
134 assert!(matches!(err, Error::NotInitialized { .. }), "got {err:?}");
135 }
136}