Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
177 linesCodeBlameRaw
1//! `g1t cache`: the repository's actions/cache entries, what they hold
2//! against its quota, and deleting them.
3
4use serde_json::{Value, json};
5
6use crate::chunk::human;
7use crate::cli::{CacheCommand, CacheDeleteArgs, CacheListArgs, CacheSort, SortOrder};
8use crate::context::Ctx;
9use crate::error::Result;
10use crate::output::{Cell, ago, s};
11
12pub fn run(ctx: &Ctx, command: CacheCommand) -> Result<()> {
13 match command {
14 CacheCommand::List(args) => list(ctx, args),
15 CacheCommand::Usage => usage(ctx),
16 CacheCommand::Delete(args) => delete(ctx, args),
17 }
18}
19
20fn sort_name(sort: CacheSort) -> &'static str {
21 match sort {
22 CacheSort::LastAccessedAt => "last_accessed_at",
23 CacheSort::CreatedAt => "created_at",
24 CacheSort::SizeInBytes => "size_in_bytes",
25 }
26}
27
28fn bytes(row: &Value, key: &str) -> u64 {
29 row[key].as_u64().unwrap_or(0)
30}
31
32fn list(ctx: &Ctx, args: CacheListArgs) -> Result<()> {
33 let (repo, client) = ctx.repo_client()?;
34 let limit = args.limit.max(1);
35 let mut caches = Vec::new();
36 let mut total = 0;
37 // Pages of at most 100, until the limit or the end.
38 for page in 1.. {
39 let found = client
40 .op("list_caches")
41 .repo(&repo)
42 .query_opt("key", args.key.clone())
43 .query_opt("ref", args.git_ref.clone())
44 .query("sort", sort_name(args.sort))
45 .query(
46 "direction",
47 if args.order == SortOrder::Asc {
48 "asc"
49 } else {
50 "desc"
51 },
52 )
53 .query("per_page", limit.min(100))
54 .query("page", page)
55 .send()?;
56 total = found["total_count"].as_u64().unwrap_or(0);
57 let rows = super::items(found, "actions_caches");
58 let last = rows.len() < limit.min(100);
59 caches.extend(rows);
60 if last || caches.len() >= limit {
61 break;
62 }
63 }
64 caches.truncate(limit);
65 if ctx.out.structured() {
66 return ctx.out.emit(&Value::Array(caches));
67 }
68 if caches.is_empty() {
69 eprintln!("No cache entries in {repo}.");
70 return Ok(());
71 }
72 let out = &ctx.out;
73 let rows: Vec<Vec<Cell>> = caches
74 .iter()
75 .map(|c| {
76 let id = c["id"].to_string();
77 vec![
78 Cell::styled(id.clone(), out.dim(&id)),
79 Cell::new(s(c, "key")),
80 Cell::styled(s(c, "ref"), out.cyan(s(c, "ref"))),
81 Cell::new(human(bytes(c, "size_in_bytes"))),
82 Cell::styled(ago(s(c, "created_at")), out.dim(&ago(s(c, "created_at")))),
83 Cell::styled(
84 ago(s(c, "last_accessed_at")),
85 out.dim(&ago(s(c, "last_accessed_at"))),
86 ),
87 ]
88 })
89 .collect();
90 out.table(&["id", "key", "ref", "size", "saved", "last used"], &rows)?;
91 if (caches.len() as u64) < total {
92 eprintln!("{} of {total} entries; --limit shows more.", caches.len());
93 }
94 Ok(())
95}
96
97fn usage(ctx: &Ctx) -> Result<()> {
98 let (repo, client) = ctx.repo_client()?;
99 let found = client.op("get_cache_usage").repo(&repo).send()?;
100 if ctx.out.structured() {
101 return ctx.out.emit(&found);
102 }
103 let held = bytes(&found, "active_caches_size_in_bytes");
104 let quota = bytes(&found, "quota_bytes");
105 let count = bytes(&found, "active_caches_count");
106 let share = if quota > 0 {
107 format!(" ({:.0}%)", held as f64 * 100.0 / quota as f64)
108 } else {
109 String::new()
110 };
111 println!(
112 "{repo}: {count} {} holding {} of {}{share}",
113 if count == 1 { "entry" } else { "entries" },
114 human(held),
115 human(quota),
116 );
117 Ok(())
118}
119
120/// An id is all digits; anything else is a key.
121fn is_id(entry: &str) -> bool {
122 !entry.is_empty() && entry.bytes().all(|b| b.is_ascii_digit())
123}
124
125fn delete(ctx: &Ctx, args: CacheDeleteArgs) -> Result<()> {
126 let (repo, client) = ctx.repo_client()?;
127 let deleted = if is_id(&args.entry) {
128 client
129 .op("delete_cache")
130 .repo(&repo)
131 .arg("id", &args.entry)
132 .send()?;
133 vec![json!({ "id": args.entry.parse::<u64>().unwrap_or_default() })]
134 } else {
135 let found = client
136 .op("delete_caches_by_key")
137 .repo(&repo)
138 .query("key", &args.entry)
139 .query_opt("ref", args.git_ref.clone())
140 .send()?;
141 super::items(found, "actions_caches")
142 };
143 if ctx.out.structured() {
144 return ctx.out.emit(&Value::Array(deleted));
145 }
146 let what = if is_id(&args.entry) {
147 format!("cache entry {}", args.entry)
148 } else {
149 let n = deleted.len();
150 format!(
151 "{n} cache {} with the key {}",
152 if n == 1 { "entry" } else { "entries" },
153 args.entry
154 )
155 };
156 eprintln!("{} Deleted {what} from {repo}.", ctx.out.green("✓"));
157 Ok(())
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn a_number_is_an_id_and_anything_else_a_key() {
166 assert!(is_id("9184"));
167 assert!(!is_id("cargo-test-Linux-1.99.0-abc"));
168 assert!(!is_id("12a"));
169 assert!(!is_id(""));
170 }
171
172 #[test]
173 fn sorts_are_github_s_names() {
174 assert_eq!(sort_name(CacheSort::LastAccessedAt), "last_accessed_at");
175 assert_eq!(sort_name(CacheSort::SizeInBytes), "size_in_bytes");
176 }
177}