1use crate::git;
2use crate::ref_name;
3
4pub struct ExpiredEntry {
5 pub name: String,
6 pub wip_ref: String,
7 pub age_days: i64,
8}
9
10pub struct GcResult {
11 pub entries: Vec<ExpiredEntry>,
12 pub dry_run: bool,
13}
14
15pub fn run(expire: String, dry_run: bool, remote: String) -> Result<GcResult, String> {
16 let user = ref_name::user()?;
17 let pattern = ref_name::list_pattern(Some(&user));
18 let max_age_secs = parse_duration(&expire)?;
19
20 let output = git::git_stdout(&["ls-remote", &remote, &format!("{pattern}*")])?;
22
23 if output.is_empty() {
24 return Ok(GcResult {
25 entries: Vec::new(),
26 dry_run,
27 });
28 }
29
30 let now = chrono::Utc::now().timestamp();
31 let mut entries = Vec::new();
32
33 for line in output.lines() {
34 let parts: Vec<&str> = line.split_whitespace().collect();
35 if parts.len() < 2 {
36 continue;
37 }
38 let refpath = parts[1];
39
40 git::git(&["fetch", &remote, refpath])?;
42 let timestamp_str = git::git_stdout(&["log", "-1", "--format=%ct", "FETCH_HEAD"])?;
43 let timestamp: i64 = timestamp_str
44 .parse()
45 .map_err(|_| format!("bad timestamp for {refpath}"))?;
46
47 let age = now - timestamp;
48 if age > max_age_secs {
49 let display = refpath.strip_prefix("refs/wip/").unwrap_or(refpath);
50 let days = age / 86400;
51
52 if !dry_run {
53 let delete_refspec = format!(":{refpath}");
54 git::git(&["push", &remote, &delete_refspec])?;
55 }
56
57 entries.push(ExpiredEntry {
58 name: display.to_string(),
59 wip_ref: refpath.to_string(),
60 age_days: days,
61 });
62 }
63 }
64
65 Ok(GcResult { entries, dry_run })
66}
67
68pub fn parse_duration(s: &str) -> Result<i64, String> {
69 let s = s.trim();
70 if let Some(days) = s.strip_suffix('d') {
71 let n: i64 = days.parse().map_err(|_| format!("invalid duration: {s}"))?;
72 Ok(n * 86400)
73 } else if let Some(hours) = s.strip_suffix('h') {
74 let n: i64 = hours
75 .parse()
76 .map_err(|_| format!("invalid duration: {s}"))?;
77 Ok(n * 3600)
78 } else {
79 Err(format!("invalid duration: {s} (use e.g. 7d, 24h)"))
80 }
81}