use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
pub struct GitConfig<'a> {
pub git_repository: &'a str,
pub nightly_branch: &'a str,
pub git_merge_commit_email: &'a str,
}
pub fn output_result(cmd: &mut Command) -> Result<String, String> {
let output = match cmd.stderr(Stdio::inherit()).output() {
Ok(status) => status,
Err(e) => return Err(format!("failed to run command: {:?}: {}", cmd, e)),
};
if !output.status.success() {
return Err(format!(
"command did not execute successfully: {:?}\n\
expected success, got: {}\n{}",
cmd,
output.status,
String::from_utf8(output.stderr).map_err(|err| format!("{err:?}"))?
));
}
String::from_utf8(output.stdout).map_err(|err| format!("{err:?}"))
}
pub fn get_rust_lang_rust_remote(
config: &GitConfig<'_>,
git_dir: Option<&Path>,
) -> Result<String, String> {
let mut git = Command::new("git");
if let Some(git_dir) = git_dir {
git.current_dir(git_dir);
}
git.args(["config", "--local", "--get-regex", "remote\\..*\\.url"]);
let stdout = output_result(&mut git)?;
let rust_lang_remote = stdout
.lines()
.find(|remote| remote.contains(config.git_repository))
.ok_or_else(|| format!("{} remote not found", config.git_repository))?;
let remote_name =
rust_lang_remote.split('.').nth(1).ok_or_else(|| "remote name not found".to_owned())?;
Ok(remote_name.into())
}
pub fn rev_exists(rev: &str, git_dir: Option<&Path>) -> Result<bool, String> {
let mut git = Command::new("git");
if let Some(git_dir) = git_dir {
git.current_dir(git_dir);
}
git.args(["rev-parse", rev]);
let output = git.output().map_err(|err| format!("{err:?}"))?;
match output.status.code() {
Some(0) => Ok(true),
Some(128) => Ok(false),
None => Err(format!(
"git didn't exit properly: {}",
String::from_utf8(output.stderr).map_err(|err| format!("{err:?}"))?
)),
Some(code) => Err(format!(
"git command exited with status code: {code}: {}",
String::from_utf8(output.stderr).map_err(|err| format!("{err:?}"))?
)),
}
}
pub fn updated_master_branch(
config: &GitConfig<'_>,
git_dir: Option<&Path>,
) -> Result<String, String> {
let upstream_remote = get_rust_lang_rust_remote(config, git_dir)?;
let branch = config.nightly_branch;
for upstream_master in [format!("{upstream_remote}/{branch}"), format!("origin/{branch}")] {
if rev_exists(&upstream_master, git_dir)? {
return Ok(upstream_master);
}
}
Err("Cannot find any suitable upstream master branch".to_owned())
}
fn git_upstream_merge_base(
config: &GitConfig<'_>,
git_dir: Option<&Path>,
) -> Result<String, String> {
let updated_master = updated_master_branch(config, git_dir)?;
let mut git = Command::new("git");
if let Some(git_dir) = git_dir {
git.current_dir(git_dir);
}
Ok(output_result(git.arg("merge-base").arg(&updated_master).arg("HEAD"))?.trim().to_owned())
}
pub fn get_closest_merge_commit(
git_dir: Option<&Path>,
config: &GitConfig<'_>,
target_paths: &[PathBuf],
) -> Result<String, String> {
let mut git = Command::new("git");
if let Some(git_dir) = git_dir {
git.current_dir(git_dir);
}
let merge_base = git_upstream_merge_base(config, git_dir).unwrap_or_else(|_| "HEAD".into());
git.args([
"rev-list",
&format!("--author={}", config.git_merge_commit_email),
"-n1",
"--first-parent",
&merge_base,
]);
if !target_paths.is_empty() {
git.arg("--").args(target_paths);
}
Ok(output_result(&mut git)?.trim().to_owned())
}
pub fn get_git_modified_files(
config: &GitConfig<'_>,
git_dir: Option<&Path>,
extensions: &[&str],
) -> Result<Option<Vec<String>>, String> {
let merge_base = get_closest_merge_commit(git_dir, config, &[])?;
let mut git = Command::new("git");
if let Some(git_dir) = git_dir {
git.current_dir(git_dir);
}
let files = output_result(git.args(["diff-index", "--name-status", merge_base.trim()]))?
.lines()
.filter_map(|f| {
let (status, name) = f.trim().split_once(char::is_whitespace).unwrap();
if status == "D" {
None
} else if Path::new(name).extension().map_or(false, |ext| {
extensions.is_empty() || extensions.contains(&ext.to_str().unwrap())
}) {
Some(name.to_owned())
} else {
None
}
})
.collect();
Ok(Some(files))
}
pub fn get_git_untracked_files(
config: &GitConfig<'_>,
git_dir: Option<&Path>,
) -> Result<Option<Vec<String>>, String> {
let Ok(_updated_master) = updated_master_branch(config, git_dir) else {
return Ok(None);
};
let mut git = Command::new("git");
if let Some(git_dir) = git_dir {
git.current_dir(git_dir);
}
let files = output_result(git.arg("ls-files").arg("--others").arg("--exclude-standard"))?
.lines()
.map(|s| s.trim().to_owned())
.collect();
Ok(Some(files))
}