Menu
AkurAI-Build
publicLatest change 4759a9b548fe43c6f97d6218584713b88ae5aea2 - AKURAI-BUILD-13 share branch listing by Ólafur Búi Ólafsson
use std::{path::Path, process::Command};
use anyhow::{Context, Result, ensure};
use serde::Serialize;
pub(crate) fn command(cwd: &Path) -> Command {
let mut command = Command::new("git");
command
.current_dir(cwd)
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_CONFIG_NOSYSTEM", "1");
command
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub(crate) struct RemoteBranch {
pub name: String,
pub is_default: bool,
}
pub(crate) fn remote_branches(url: &str, default_branch: &str) -> Result<Vec<RemoteBranch>> {
let output = Command::new("git")
.args(["ls-remote", "--heads", url])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.context("failed to list branches")?;
ensure!(
output.status.success(),
"git ls-remote failed: {}",
String::from_utf8_lossy(&output.stderr)
.lines()
.next()
.unwrap_or("unknown error")
);
let mut names: Vec<String> = String::from_utf8(output.stdout)
.context("invalid git output")?
.lines()
.filter_map(|line| line.split('\t').nth(1))
.filter_map(|reference| reference.strip_prefix("refs/heads/"))
.map(str::to_owned)
.collect();
names.sort();
Ok(names
.into_iter()
.map(|name| RemoteBranch {
is_default: name == default_branch,
name,
})
.collect())
}
pub(crate) fn output<const N: usize>(cwd: &Path, arguments: [&str; N]) -> Result<String> {
let output = command(cwd).args(arguments).output()?;
ensure!(
output.status.success(),
"git failed: {}",
String::from_utf8_lossy(&output.stderr)
.chars()
.take(16 * 1024)
.collect::<String>()
.trim()
);
Ok(String::from_utf8_lossy(&output.stdout)
.trim_end()
.to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn reads_git_version_successfully() {
let dir = tempdir().expect("create temp dir");
let version = output(dir.path(), ["version"]).expect("git version should succeed");
assert!(
version.starts_with("git version"),
"should start with 'git version'"
);
}
#[test]
fn errors_on_bad_arguments() {
let dir = tempdir().expect("create temp dir");
let result = output(dir.path(), ["--invalid-flag-that-does-not-exist"]);
assert!(result.is_err(), "bad arguments should error, not panic");
}
#[test]
fn errors_on_non_repository() {
let dir = tempdir().expect("create temp dir");
let result = output(dir.path(), ["rev-parse", "HEAD"]);
assert!(result.is_err(), "non-repo should produce Err");
}
#[test]
fn reads_commit_sha_in_valid_repo() -> Result<()> {
let dir = tempdir()?;
let repo = dir.path().join("repo");
fs::create_dir(&repo)?;
let init = std::process::Command::new("git")
.arg("init")
.args(["-b", "main"])
.arg(&repo)
.output()?;
ensure!(init.status.success(), "git init failed");
let commit = std::process::Command::new("git")
.arg("-C")
.arg(&repo)
.args([
"-c",
"user.name=test",
"-c",
"user.email=test@test",
"commit",
"--allow-empty",
"-m",
"init",
])
.output()?;
ensure!(commit.status.success(), "git commit failed");
let sha = output(repo.as_path(), ["rev-parse", "HEAD"])?;
assert_eq!(sha.len(), 40, "SHA should be 40 hex chars");
assert!(
sha.chars().all(|c| c.is_ascii_hexdigit()),
"SHA should be hex"
);
Ok(())
}
#[test]
fn lists_sorted_remote_branches_and_marks_default() -> Result<()> {
let dir = tempdir()?;
let repo = dir.path().join("repo");
fs::create_dir(&repo)?;
ensure!(
Command::new("git")
.arg("init")
.args(["-b", "main"])
.arg(&repo)
.status()?
.success(),
"git init failed"
);
ensure!(
Command::new("git")
.arg("-C")
.arg(&repo)
.args([
"-c",
"user.name=test",
"-c",
"user.email=test@test",
"commit",
"--allow-empty",
"-m",
"init",
])
.status()?
.success(),
"git commit failed"
);
ensure!(
Command::new("git")
.arg("-C")
.arg(&repo)
.args(["branch", "feature"])
.status()?
.success(),
"git branch failed"
);
let branches = remote_branches(repo.to_str().context("utf-8 path")?, "main")?;
assert_eq!(
branches,
vec![
RemoteBranch {
name: "feature".into(),
is_default: false,
},
RemoteBranch {
name: "main".into(),
is_default: true,
},
]
);
Ok(())
}
}