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))
}
pub fn warn_old_master_branch(config: &GitConfig<'_>, git_dir: &Path) {
if crate::ci::CiEnv::is_ci() {
return;
}
let mut updated_master = "the upstream master branch".to_string();
match warn_old_master_branch_(config, git_dir, &mut updated_master) {
Ok(branch_is_old) => {
if !branch_is_old {
return;
}
}
Err(err) => {
eprintln!("warning: unable to check if {updated_master} is old due to error: {err}")
}
}
eprintln!(
"warning: {updated_master} is used to determine if files have been modified\n\
warning: if it is not updated, this may cause files to be needlessly reformatted"
);
}
pub fn warn_old_master_branch_(
config: &GitConfig<'_>,
git_dir: &Path,
updated_master: &mut String,
) -> Result<bool, Box<dyn std::error::Error>> {
use std::time::Duration;
*updated_master = updated_master_branch(config, Some(git_dir))?;
let branch_path = git_dir.join(".git/refs/remotes").join(&updated_master);
const WARN_AFTER: Duration = Duration::from_secs(60 * 60 * 24 * 10);
let meta = match std::fs::metadata(&branch_path) {
Ok(meta) => meta,
Err(err) => {
let gcd = git_common_dir(&git_dir)?;
if branch_path.starts_with(&gcd) {
return Err(Box::new(err));
}
std::fs::metadata(Path::new(&gcd).join("refs/remotes").join(&updated_master))?
}
};
if meta.modified()?.elapsed()? > WARN_AFTER {
eprintln!("warning: {updated_master} has not been updated in 10 days");
Ok(true)
} else {
Ok(false)
}
}
fn git_common_dir(dir: &Path) -> Result<String, String> {
output_result(Command::new("git").arg("-C").arg(dir).arg("rev-parse").arg("--git-common-dir"))
.map(|x| x.trim().to_string())
}