chub_core/team/
team_annotations.rs1use std::fs;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::team::project::project_chub_dir;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TeamAnnotationNote {
11 pub author: String,
12 pub date: String,
13 pub note: String,
14}
15
16#[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
33pub 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
44pub 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
75pub 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
103pub 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
135pub 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}