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