Skip to main content

chub_core/team/
team_annotations.rs

1use std::fs;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::team::project::project_chub_dir;
7
8/// A team annotation note entry.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TeamAnnotationNote {
11    pub author: String,
12    pub date: String,
13    pub note: String,
14}
15
16/// A team annotation file (`.chub/annotations/<id>.yaml`).
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TeamAnnotation {
19    pub id: String,
20    #[serde(default)]
21    pub notes: Vec<TeamAnnotationNote>,
22}
23
24fn team_annotations_dir() -> Option<PathBuf> {
25    project_chub_dir().map(|d| d.join("annotations"))
26}
27
28fn team_annotation_path(entry_id: &str) -> Option<PathBuf> {
29    let safe = entry_id.replace('/', "--");
30    team_annotations_dir().map(|d| d.join(format!("{}.yaml", safe)))
31}
32
33/// Read team annotations for an entry.
34pub fn read_team_annotation(entry_id: &str) -> Option<TeamAnnotation> {
35    let path = team_annotation_path(entry_id)?;
36    if !path.exists() {
37        return None;
38    }
39    fs::read_to_string(&path)
40        .ok()
41        .and_then(|s| serde_yaml::from_str(&s).ok())
42}
43
44/// Write a team annotation (append a note).
45pub fn write_team_annotation(entry_id: &str, note: &str, author: &str) -> Option<TeamAnnotation> {
46    let dir = team_annotations_dir()?;
47    let _ = fs::create_dir_all(&dir);
48
49    let mut ann = read_team_annotation(entry_id).unwrap_or(TeamAnnotation {
50        id: entry_id.to_string(),
51        notes: vec![],
52    });
53
54    let now = crate::build::builder::days_to_date(
55        std::time::SystemTime::now()
56            .duration_since(std::time::UNIX_EPOCH)
57            .unwrap_or_default()
58            .as_secs()
59            / 86400,
60    );
61    let date = format!("{:04}-{:02}-{:02}", now.0, now.1, now.2);
62
63    ann.notes.push(TeamAnnotationNote {
64        author: author.to_string(),
65        date,
66        note: note.to_string(),
67    });
68
69    let path = team_annotation_path(entry_id)?;
70    let yaml = serde_yaml::to_string(&ann).ok()?;
71    fs::write(&path, yaml).ok()?;
72    Some(ann)
73}
74
75/// List all team annotations.
76pub fn list_team_annotations() -> Vec<TeamAnnotation> {
77    let dir = match team_annotations_dir() {
78        Some(d) if d.exists() => d,
79        _ => return vec![],
80    };
81
82    let files = match fs::read_dir(&dir) {
83        Ok(entries) => entries,
84        Err(_) => return vec![],
85    };
86
87    files
88        .filter_map(|e| e.ok())
89        .filter(|e| {
90            e.path()
91                .extension()
92                .map(|ext| ext == "yaml" || ext == "yml")
93                .unwrap_or(false)
94        })
95        .filter_map(|e| {
96            fs::read_to_string(e.path())
97                .ok()
98                .and_then(|s| serde_yaml::from_str::<TeamAnnotation>(&s).ok())
99        })
100        .collect()
101}
102
103/// Merge annotations: team annotations + personal annotations.
104/// Resolution order: public doc → team annotations → personal annotations.
105/// Returns a combined annotation string for display.
106pub fn get_merged_annotation(entry_id: &str) -> Option<String> {
107    let team = read_team_annotation(entry_id);
108    let personal = crate::annotations::read_annotation(entry_id);
109
110    let mut parts = Vec::new();
111
112    if let Some(ref team_ann) = team {
113        for note in &team_ann.notes {
114            parts.push(format!(
115                "[Team — {} ({})] {}",
116                note.author, note.date, note.note
117            ));
118        }
119    }
120
121    if let Some(ref personal_ann) = personal {
122        parts.push(format!(
123            "[Personal — {}] {}",
124            personal_ann.updated_at, personal_ann.note
125        ));
126    }
127
128    if parts.is_empty() {
129        None
130    } else {
131        Some(parts.join("\n"))
132    }
133}
134
135/// Get the annotation to append when serving a pinned doc.
136pub fn get_pin_notice(
137    _entry_id: &str,
138    pinned_version: Option<&str>,
139    pinned_lang: Option<&str>,
140    reason: Option<&str>,
141) -> String {
142    let mut notice = String::from("\n---\n[Team pin]");
143    if let Some(ver) = pinned_version {
144        notice.push_str(&format!(" Locked to v{}", ver));
145    }
146    if let Some(lang) = pinned_lang {
147        notice.push_str(&format!(" ({})", lang));
148    }
149    notice.push('.');
150    if let Some(reason) = reason {
151        notice.push_str(&format!(" Reason: {}", reason));
152    }
153    notice.push('\n');
154    notice
155}