Skip to main content

ready_set_rust/
gitignore.rs

1//! Manage the `.gitignore` ready-set block.
2
3use crate::templates::{GITIGNORE, GITIGNORE_BEGIN, GITIGNORE_END};
4
5/// Plan the gitignore content for a project root.
6///
7/// Given the current file content (or `None` if the file does not yet exist),
8/// returns `Some(new_content)` when a write is required, or `None` when the
9/// file already matches the desired state.
10#[must_use]
11pub fn plan(current: Option<&str>) -> Option<String> {
12    let desired = render();
13    match current {
14        None => Some(desired),
15        Some(text) if text == desired => None,
16        Some(text) => Some(merge_managed(text, &desired)),
17    }
18}
19
20fn render() -> String {
21    let mut out = String::new();
22    out.push_str(GITIGNORE_BEGIN);
23    out.push('\n');
24    out.push_str(GITIGNORE.trim_end());
25    out.push('\n');
26    out.push_str(GITIGNORE_END);
27    out.push('\n');
28    out
29}
30
31fn merge_managed(existing: &str, managed_block: &str) -> String {
32    let begin = format!("{GITIGNORE_BEGIN}\n");
33    let end = format!("{GITIGNORE_END}\n");
34
35    if let (Some(b), Some(e)) = (existing.find(&begin), existing.find(&end))
36        && b < e
37    {
38        let mut out = String::with_capacity(existing.len() + managed_block.len());
39        out.push_str(&existing[..b]);
40        out.push_str(managed_block);
41        let after = e + end.len();
42        if after < existing.len() {
43            out.push_str(&existing[after..]);
44        }
45        return out;
46    }
47
48    let mut out = String::with_capacity(existing.len() + managed_block.len() + 1);
49    out.push_str(existing);
50    if !existing.ends_with('\n') {
51        out.push('\n');
52    }
53    out.push_str(managed_block);
54    out
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn matching_content_is_a_noop() {
63        let target = render();
64        assert!(plan(Some(&target)).is_none());
65    }
66}