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    fs::read_to_string(&path)
40        .ok()
41        .and_then(|s| serde_yaml::from_str(&s).ok())
42        .unwrap_or_default()
43}
44
45/// Save pins to `.chub/pins.yaml`.
46pub 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
55/// Add or update a pin.
56pub 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    // Update existing or add new
66    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
92/// Remove a pin by ID. Returns true if it existed.
93pub 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
104/// Get a specific pin by entry ID.
105pub fn get_pin(id: &str) -> Option<PinEntry> {
106    load_pins().pins.into_iter().find(|p| p.id == id)
107}
108
109/// List all pins.
110pub fn list_pins() -> Vec<PinEntry> {
111    load_pins().pins
112}