1use std::path::{Path, PathBuf};
4use std::process::Command;
5
6use anyhow::{Context, Result, bail};
7use chrono::{DateTime, Utc};
8
9use crate::{GitPushTarget, ReleaseConfig};
10
11pub(crate) fn git_head_sha(repo: &Path) -> Result<String> {
12 let output = Command::new("git")
13 .args(["rev-parse", "HEAD"])
14 .current_dir(repo)
15 .output()
16 .context("invoke `git rev-parse HEAD`")?;
17 if !output.status.success() {
18 bail!(
19 "git rev-parse HEAD: {}",
20 String::from_utf8_lossy(&output.stderr)
21 );
22 }
23 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
24}
25
26pub(crate) fn git_commit_release(
27 config: &ReleaseConfig,
28 files: &[PathBuf],
29 ci_commit: Option<&str>,
30 signed_at: DateTime<Utc>,
31) -> Result<bool> {
32 if let Some(name) = &config.git_user_name {
33 run_git(&config.flake_dir, &["config", "user.name", name])?;
34 }
35 if let Some(email) = &config.git_user_email {
36 run_git(&config.flake_dir, &["config", "user.email", email])?;
37 }
38 let mut add_args = vec!["add", "--"];
39 let file_strs: Vec<String> = files
40 .iter()
41 .map(|p| {
42 p.strip_prefix(&config.flake_dir)
43 .unwrap_or(p)
44 .to_string_lossy()
45 .into_owned()
46 })
47 .collect();
48 for f in &file_strs {
49 add_args.push(f);
50 }
51 run_git(&config.flake_dir, &add_args)?;
52
53 let cached_diff = Command::new("git")
54 .args(["diff", "--cached", "--quiet", "--"])
55 .args(&file_strs)
56 .current_dir(&config.flake_dir)
57 .status()
58 .context("invoke `git diff --cached --quiet`")?;
59 if cached_diff.success() {
60 tracing::info!("git: no release change");
61 return Ok(false);
62 }
63
64 let message = render_commit_message(
65 &config.commit_template,
66 ci_commit.unwrap_or("HEAD"),
67 signed_at,
68 );
69 run_git(&config.flake_dir, &["commit", "-m", &message])?;
70 tracing::info!(message = %message, "git commit");
71 Ok(true)
72}
73
74pub fn render_commit_message(template: &str, sha: &str, ts: DateTime<Utc>) -> String {
75 let short = if sha.len() >= 8 { &sha[..8] } else { sha };
76 template
77 .replace("{sha:0:8}", short)
78 .replace("{sha}", sha)
79 .replace("{ts}", &ts.to_rfc3339())
80}
81
82pub(crate) fn git_push_release(repo: &Path, target: &GitPushTarget) -> Result<()> {
83 let refspec = format!("HEAD:{}", target.branch);
84 run_git(repo, &["push", &target.remote, &refspec])?;
85 tracing::info!(
86 remote = %target.remote,
87 branch = %target.branch,
88 "git push",
89 );
90 Ok(())
91}
92
93fn run_git(repo: &Path, args: &[&str]) -> Result<()> {
94 let status = Command::new("git")
95 .args(args)
96 .current_dir(repo)
97 .status()
98 .with_context(|| format!("invoke git {args:?}"))?;
99 if !status.success() {
100 bail!("git {:?} exited {}", args, status.code().unwrap_or(-1));
101 }
102 Ok(())
103}