1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! Utilities for running `git` commands.

use crate::cmd::{get_cmd_stdout, run_cmd};
use anyhow::{anyhow, Context, Result};
use std::env;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;

/// Git repo.
pub struct Repo(PathBuf);

impl Repo {
    /// Get a `Repo` for the current directory.
    ///
    /// This will fail if the current directory does not contain a
    /// `.git` subdirectory.
    pub fn open() -> Result<Self> {
        let path = env::current_dir()?;
        Self::open_path(path)
    }

    /// Get a `Repo` for the given path.
    ///
    /// This will fail if the `path` does not contain a `.git`
    /// subdirectory.
    pub fn open_path<P>(path: P) -> Result<Self>
    where
        P: Into<PathBuf>,
    {
        let path = path.into();

        // Check that this is a git repo. This is just to help fail
        // quickly if the path is wrong; it isn't checking that the git
        // repo is valid or anything.
        //
        // Also, there are various special types of git checkouts so
        // it's quite possible this check is wrong for special
        // circumstances, but for a typical CI release process it should
        // be fine.
        let git_dir = path.join(".git");
        if !git_dir.exists() {
            return Err(anyhow!("{} does not exist", git_dir.display()));
        }

        Ok(Self(path))
    }

    /// Get the repo path.
    pub fn path(&self) -> &Path {
        &self.0
    }

    /// Create a git command with the given args.
    fn get_git_command<I, S>(&self, args: I) -> Command
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        let mut cmd = Command::new("git");
        cmd.arg("-C");
        cmd.arg(self.path());
        cmd.args(args);
        cmd
    }

    /// Get the subject of the commit message for the given commit.
    pub fn get_commit_message_subject(&self, commit_sha: &str) -> Result<String> {
        let cmd = self.get_git_command([
            "log",
            "-1",
            // Only get the subject of the commit message.
            "--format=format:%s",
            commit_sha,
        ]);
        let output = get_cmd_stdout(cmd)?;
        String::from_utf8(output).context("commit message is not utf-8")
    }

    /// Fetch git tags from the remote.
    pub fn fetch_git_tags(&self) -> Result<()> {
        let cmd = self.get_git_command(["fetch", "--tags"]);
        run_cmd(cmd)?;
        Ok(())
    }

    /// Check if a git tag exists locally.
    ///
    /// All git tags were fetched at the start of auto-release, so checking locally
    /// is sufficient.
    pub fn does_git_tag_exist(&self, tag: &str) -> Result<bool> {
        let cmd = self.get_git_command(["tag", "--list", tag]);
        let output = get_cmd_stdout(cmd)?;
        let output = String::from_utf8(output).context("git tag is not utf-8")?;

        Ok(output.lines().any(|line| line == tag))
    }

    /// Create a git tag locally and push it.
    pub fn make_and_push_git_tag(&self, tag: &str, commit_sha: &str) -> Result<()> {
        // Create the tag.
        let cmd = self.get_git_command(["tag", tag, commit_sha]);
        run_cmd(cmd)?;

        // Push it.
        let cmd = self.get_git_command(["push", "--tags"]);
        run_cmd(cmd)?;

        Ok(())
    }
}