mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
230 lines
6.9 KiB
Rust
230 lines
6.9 KiB
Rust
use code_apply_patch::{ApplyPatchAction, ApplyPatchFileChange};
|
|
use crate::config_types::GithubConfig;
|
|
use std::fs;
|
|
use std::io::Write;
|
|
use std::path::{Path, PathBuf};
|
|
use tempfile::TempDir;
|
|
|
|
fn is_workflow_path(path: &Path, cwd: &Path) -> bool {
|
|
let absolute = if path.is_absolute() { path.to_path_buf() } else { cwd.join(path) };
|
|
let mut saw_dot_github = false;
|
|
let mut saw_workflows = false;
|
|
for component in absolute.components() {
|
|
let segment = component.as_os_str().to_string_lossy();
|
|
if !saw_dot_github && segment == ".github" {
|
|
saw_dot_github = true;
|
|
continue;
|
|
}
|
|
if saw_dot_github && !saw_workflows && segment == "workflows" {
|
|
saw_workflows = true;
|
|
continue;
|
|
}
|
|
if saw_dot_github && saw_workflows {
|
|
return matches!(
|
|
absolute.extension().and_then(|ext| ext.to_str()),
|
|
Some("yml" | "yaml")
|
|
);
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn is_in_github_dir(path: &Path, cwd: &Path) -> bool {
|
|
let absolute = if path.is_absolute() { path.to_path_buf() } else { cwd.join(path) };
|
|
if let Ok(relative) = absolute.strip_prefix(cwd) {
|
|
return relative.starts_with(".github");
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Run `actionlint` against a temporary workspace containing the proposed
|
|
/// workflow changes. Returns diagnostic lines when the tool is available and
|
|
/// produced output.
|
|
pub fn maybe_run_actionlint(
|
|
action: &ApplyPatchAction,
|
|
cwd: &Path,
|
|
config: &GithubConfig,
|
|
) -> Option<Vec<String>> {
|
|
if !config.actionlint_on_patch {
|
|
return None;
|
|
}
|
|
|
|
let exe = config
|
|
.actionlint_path
|
|
.clone()
|
|
.unwrap_or_else(|| PathBuf::from("actionlint"));
|
|
let executable = if exe.is_absolute() {
|
|
exe.exists().then_some(exe)
|
|
} else {
|
|
which(&exe)
|
|
};
|
|
let Some(executable) = executable else {
|
|
return None;
|
|
};
|
|
|
|
let touches_workflow = action
|
|
.changes()
|
|
.keys()
|
|
.any(|path| is_workflow_path(path, cwd));
|
|
if !touches_workflow {
|
|
return None;
|
|
}
|
|
|
|
let temp = TempDir::new().ok()?;
|
|
let temp_root = temp.path();
|
|
let temp_github = temp_root.join(".github");
|
|
let temp_workflows = temp_github.join("workflows");
|
|
let _ = fs::create_dir_all(&temp_workflows);
|
|
|
|
let source_github = cwd.join(".github");
|
|
if source_github.exists() {
|
|
let _ = copy_dir_recursive(&source_github, &temp_github);
|
|
}
|
|
|
|
for (path, change) in action.changes() {
|
|
let path_in_github = is_in_github_dir(path, cwd);
|
|
match change {
|
|
ApplyPatchFileChange::Add { content } => {
|
|
if path_in_github {
|
|
let _ = stage_file_with_contents(temp_root, cwd, path, content);
|
|
}
|
|
}
|
|
ApplyPatchFileChange::Update { new_content, move_path, .. } => {
|
|
let dest_path = move_path.as_ref().unwrap_or(path);
|
|
let dest_in_github = is_in_github_dir(dest_path, cwd);
|
|
if !dest_in_github && !path_in_github {
|
|
continue;
|
|
}
|
|
|
|
if dest_in_github {
|
|
let _ = stage_file_with_contents(temp_root, cwd, dest_path, new_content);
|
|
}
|
|
|
|
if move_path.is_none() && path_in_github && !dest_in_github {
|
|
let _ = stage_file_with_contents(temp_root, cwd, path, new_content);
|
|
}
|
|
|
|
if path_in_github && move_path.is_some() {
|
|
remove_from_staged(temp_root, cwd, path);
|
|
}
|
|
}
|
|
ApplyPatchFileChange::Delete { .. } => {
|
|
if path_in_github {
|
|
remove_from_staged(temp_root, cwd, path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let output = std::process::Command::new(executable)
|
|
.arg("-color")
|
|
.arg("never")
|
|
.current_dir(temp_root)
|
|
.output()
|
|
.ok()?;
|
|
|
|
let mut lines: Vec<String> = Vec::new();
|
|
if !output.stdout.is_empty() {
|
|
lines.extend(String::from_utf8_lossy(&output.stdout).lines().map(|s| s.to_string()));
|
|
}
|
|
if !output.stderr.is_empty() {
|
|
lines.extend(String::from_utf8_lossy(&output.stderr).lines().map(|s| s.to_string()));
|
|
}
|
|
if lines.is_empty() {
|
|
None
|
|
} else {
|
|
Some(lines)
|
|
}
|
|
}
|
|
|
|
fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
|
|
if !dst.exists() {
|
|
fs::create_dir_all(dst)?;
|
|
}
|
|
for entry in fs::read_dir(src)? {
|
|
let entry = entry?;
|
|
let file_type = entry.file_type()?;
|
|
let target = dst.join(entry.file_name());
|
|
if file_type.is_dir() {
|
|
copy_dir_recursive(&entry.path(), &target)?;
|
|
} else if file_type.is_file() {
|
|
if let Some(parent) = target.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
let _ = fs::copy(entry.path(), &target);
|
|
} else if file_type.is_symlink() {
|
|
copy_symlink(&entry.path(), &target)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn which(exe: &Path) -> Option<PathBuf> {
|
|
let name = exe.as_os_str();
|
|
let paths: Vec<PathBuf> = std::env::var_os("PATH")
|
|
.map(|paths| std::env::split_paths(&paths).collect())
|
|
.unwrap_or_else(Vec::new);
|
|
for dir in paths {
|
|
let candidate = dir.join(name);
|
|
if candidate.is_file() && is_executable(&candidate) {
|
|
return Some(candidate);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn is_executable(path: &Path) -> bool {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
fs::metadata(path)
|
|
.map(|meta| meta.permissions().mode() & 0o111 != 0)
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn is_executable(path: &Path) -> bool {
|
|
path.is_file()
|
|
}
|
|
|
|
fn stage_file_with_contents(temp_root: &Path, cwd: &Path, target: &Path, contents: &str) -> bool {
|
|
let relative = match target.strip_prefix(cwd) {
|
|
Ok(rel) => rel,
|
|
Err(_) => return false,
|
|
};
|
|
let destination = temp_root.join(relative);
|
|
if let Some(parent) = destination.parent() {
|
|
if fs::create_dir_all(parent).is_err() {
|
|
return false;
|
|
}
|
|
}
|
|
match fs::File::create(&destination) {
|
|
Ok(mut file) => file.write_all(contents.as_bytes()).is_ok(),
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
|
|
fn remove_from_staged(temp_root: &Path, cwd: &Path, target: &Path) {
|
|
if let Ok(relative) = target.strip_prefix(cwd) {
|
|
let destination = temp_root.join(relative);
|
|
let _ = fs::remove_file(&destination);
|
|
}
|
|
}
|
|
|
|
fn copy_symlink(src: &Path, dst: &Path) -> std::io::Result<()> {
|
|
#[cfg(unix)]
|
|
{
|
|
let target = std::fs::read_link(src)?;
|
|
let _ = fs::remove_file(dst);
|
|
std::os::unix::fs::symlink(target, dst)?;
|
|
Ok(())
|
|
}
|
|
#[cfg(not(unix))]
|
|
{
|
|
if let Some(parent) = dst.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
let _ = fs::copy(src, dst)?;
|
|
Ok(())
|
|
}
|
|
}
|