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 match fs::read_to_string(&path) {
40 Ok(s) => match serde_yaml::from_str(&s) {
41 Ok(pins) => pins,
42 Err(e) => {
43 eprintln!(
44 "Warning: failed to parse {}: {}. Using empty pins.",
45 path.display(),
46 e
47 );
48 PinsFile::default()
49 }
50 },
51 Err(e) => {
52 eprintln!("Warning: failed to read {}: {}", path.display(), e);
53 PinsFile::default()
54 }
55 }
56}
57
58pub fn save_pins(pins: &PinsFile) -> Result<()> {
60 let path = pins_path().ok_or_else(|| {
61 Error::Config("No .chub/ directory found. Run `chub init` first.".to_string())
62 })?;
63 let yaml = serde_yaml::to_string(pins).map_err(|e| Error::Config(e.to_string()))?;
64 crate::util::atomic_write(&path, yaml.as_bytes())?;
65 Ok(())
66}
67
68pub fn add_pin(
70 id: &str,
71 lang: Option<String>,
72 version: Option<String>,
73 reason: Option<String>,
74 source: Option<String>,
75) -> Result<()> {
76 let mut pins = load_pins();
77
78 if let Some(existing) = pins.pins.iter_mut().find(|p| p.id == id) {
80 if lang.is_some() {
81 existing.lang = lang;
82 }
83 if version.is_some() {
84 existing.version = version;
85 }
86 if reason.is_some() {
87 existing.reason = reason;
88 }
89 if source.is_some() {
90 existing.source = source;
91 }
92 } else {
93 pins.pins.push(PinEntry {
94 id: id.to_string(),
95 lang,
96 version,
97 reason,
98 source,
99 });
100 }
101
102 save_pins(&pins)
103}
104
105pub fn remove_pin(id: &str) -> Result<bool> {
107 let mut pins = load_pins();
108 let before = pins.pins.len();
109 pins.pins.retain(|p| p.id != id);
110 let removed = pins.pins.len() < before;
111 if removed {
112 save_pins(&pins)?;
113 }
114 Ok(removed)
115}
116
117pub fn get_pin(id: &str) -> Option<PinEntry> {
119 load_pins().pins.into_iter().find(|p| p.id == id)
120}
121
122pub fn list_pins() -> Vec<PinEntry> {
124 load_pins().pins
125}