Skip to main content

chub_core/team/
pins.rs

1use std::fs;
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::{Error, Result};
6use crate::team::project::project_chub_dir;
7
8/// A single pinned doc entry.
9#[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/// The pins.yaml file structure.
23#[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
33/// Load pins from `.chub/pins.yaml`.
34pub 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
58/// Save pins to `.chub/pins.yaml`.
59pub 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
68/// Add or update a pin.
69pub 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    // Update existing or add new
79    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
105/// Remove a pin by ID. Returns true if it existed.
106pub 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
117/// Get a specific pin by entry ID.
118pub fn get_pin(id: &str) -> Option<PinEntry> {
119    load_pins().pins.into_iter().find(|p| p.id == id)
120}
121
122/// List all pins.
123pub fn list_pins() -> Vec<PinEntry> {
124    load_pins().pins
125}