Skip to main content

gor/cmd/
cache.rs

1//! Implementation of the `gor cache` subcommand.
2//!
3//! Provides repository cache listing and deletion.
4
5#![allow(clippy::print_stdout)]
6
7use crate::cli::CacheCommand;
8use crate::client::Client;
9use crate::output::print_json;
10use crate::repository;
11use anyhow::Context;
12
13use std::fmt::Write;
14
15/// Run the `gor cache` subcommand.
16///
17/// # Errors
18///
19/// Returns an error if the command execution fails.
20pub fn run(cmd: CacheCommand) -> anyhow::Result<()> {
21    match cmd {
22        CacheCommand::List {
23            repo,
24            json,
25            hostname,
26        } => list(repo.as_deref(), json, hostname.as_deref()),
27        CacheCommand::Delete {
28            key,
29            repo,
30            all,
31            key_prefix,
32            ref_,
33            hostname,
34        } => delete(
35            key.as_deref(),
36            repo.as_deref(),
37            all,
38            key_prefix.as_deref(),
39            ref_.as_deref(),
40            hostname.as_deref(),
41        ),
42    }
43}
44
45fn list(
46    repo: Option<&str>,
47    json: Option<Vec<String>>,
48    hostname: Option<&str>,
49) -> anyhow::Result<()> {
50    let spec = match repo {
51        Some(s) => repository::parse_repo_spec(s).context("invalid repository spec")?,
52        None => repository::detect_remote().ok_or_else(|| {
53            anyhow::anyhow!("could not detect repository; specify OWNER/REPO with --repo")
54        })?,
55    };
56
57    let host = hostname.unwrap_or("github.com");
58    let client = Client::new(host).context("failed to create HTTP client")?;
59
60    let path = format!(
61        "/repos/{}/{}/actions/caches?per_page=100",
62        spec.owner, spec.repo
63    );
64
65    let response = client.get(&path).context("failed to fetch caches")?;
66    let status = response.status();
67    if !status.is_success() {
68        anyhow::bail!("failed to list caches: HTTP {status}");
69    }
70
71    let result: serde_json::Value = response.json().context("failed to parse response")?;
72    let caches: Vec<serde_json::Value> = result["actions_caches"]
73        .as_array()
74        .map_or_else(Vec::new, Clone::clone);
75
76    if let Some(fields) = json {
77        let fields_ref: Option<&[String]> = if fields.is_empty() {
78            None
79        } else {
80            Some(&fields)
81        };
82        print_json(&caches, fields_ref);
83        return Ok(());
84    }
85
86    if caches.is_empty() {
87        println!("No caches found.");
88        return Ok(());
89    }
90
91    println!("{:<30}  {:<10}  CREATED", "KEY", "SIZE (MB)");
92    for c in &caches {
93        let key = c["key"].as_str().unwrap_or("—");
94        let size = c["size_in_bytes"].as_u64().unwrap_or(0);
95        let created = c["created_at"].as_str().unwrap_or("—");
96        let key_truncated = crate::cmd::util::truncate(key, 30);
97        println!("{key_truncated:<30}  {:<10}  {created}", size / 1024 / 1024);
98    }
99
100    Ok(())
101}
102
103fn delete(
104    key: Option<&str>,
105    repo: Option<&str>,
106    all: bool,
107    key_prefix: Option<&str>,
108    ref_: Option<&str>,
109    hostname: Option<&str>,
110) -> anyhow::Result<()> {
111    let spec = match repo {
112        Some(s) => repository::parse_repo_spec(s).context("invalid repository spec")?,
113        None => repository::detect_remote().ok_or_else(|| {
114            anyhow::anyhow!("could not detect repository; specify OWNER/REPO with --repo")
115        })?,
116    };
117
118    let host = hostname.unwrap_or("github.com");
119    let client = Client::new(host).context("failed to create HTTP client")?;
120
121    let mut path = format!("/repos/{}/{}/actions/caches", spec.owner, spec.repo);
122
123    if let Some(k) = key {
124        let _ = write!(path, "?key={k}");
125    } else if let Some(prefix) = key_prefix {
126        let _ = write!(path, "?key={prefix}");
127    }
128
129    if let Some(r) = ref_ {
130        let sep = if path.contains('?') { "&" } else { "?" };
131        let _ = write!(path, "{sep}ref={r}");
132    }
133
134    let response = client
135        .request("DELETE", &path, &[], None)
136        .context("failed to delete caches")?;
137
138    let status = response.status();
139    if !status.is_success() {
140        anyhow::bail!("failed to delete caches: HTTP {status}");
141    }
142
143    let result: serde_json::Value = response.json().context("failed to parse response")?;
144    let count = result["total_count"].as_u64().unwrap_or(0);
145
146    if all {
147        println!("Deleted all caches ({count} total).");
148    } else if let Some(k) = key {
149        println!("Deleted cache '{k}'.");
150    } else if let Some(prefix) = key_prefix {
151        println!("Deleted {count} cache(s) with prefix '{prefix}'.");
152    }
153
154    Ok(())
155}