1use std::fs;
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::{Error, Result};
6use crate::team::project::project_chub_dir;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PinEntry {
11 pub id: String,
12 #[serde(skip_serializing_if = "Option::is_none")]
13 pub lang: Option<String>,
14 #[serde(skip_serializing_if = "Option::is_none")]
15 pub version: Option<String>,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub reason: Option<String>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub source: Option<String>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, Default)]
24pub struct PinsFile {
25 #[serde(default)]
26 pub pins: Vec<PinEntry>,
27}
28
29fn pins_path() -> Option<std::path::PathBuf> {
30 project_chub_dir().map(|d| d.join("pins.yaml"))
31}
32
33pub fn load_pins() -> PinsFile {
35 let path = match pins_path() {
36 Some(p) if p.exists() => p,
37 _ => return PinsFile::default(),
38 };
39 fs::read_to_string(&path)
40 .ok()
41 .and_then(|s| serde_yaml::from_str(&s).ok())
42 .unwrap_or_default()
43}
44
45pub fn save_pins(pins: &PinsFile) -> Result<()> {
47 let path = pins_path().ok_or_else(|| {
48 Error::Config("No .chub/ directory found. Run `chub init` first.".to_string())
49 })?;
50 let yaml = serde_yaml::to_string(pins).map_err(|e| Error::Config(e.to_string()))?;
51 fs::write(&path, yaml)?;
52 Ok(())
53}
54
55pub fn add_pin(
57 id: &str,
58 lang: Option<String>,
59 version: Option<String>,
60 reason: Option<String>,
61 source: Option<String>,
62) -> Result<()> {
63 let mut pins = load_pins();
64
65 if let Some(existing) = pins.pins.iter_mut().find(|p| p.id == id) {
67 if lang.is_some() {
68 existing.lang = lang;
69 }
70 if version.is_some() {
71 existing.version = version;
72 }
73 if reason.is_some() {
74 existing.reason = reason;
75 }
76 if source.is_some() {
77 existing.source = source;
78 }
79 } else {
80 pins.pins.push(PinEntry {
81 id: id.to_string(),
82 lang,
83 version,
84 reason,
85 source,
86 });
87 }
88
89 save_pins(&pins)
90}
91
92pub fn remove_pin(id: &str) -> Result<bool> {
94 let mut pins = load_pins();
95 let before = pins.pins.len();
96 pins.pins.retain(|p| p.id != id);
97 let removed = pins.pins.len() < before;
98 if removed {
99 save_pins(&pins)?;
100 }
101 Ok(removed)
102}
103
104pub fn get_pin(id: &str) -> Option<PinEntry> {
106 load_pins().pins.into_iter().find(|p| p.id == id)
107}
108
109pub fn list_pins() -> Vec<PinEntry> {
111 load_pins().pins
112}