use std::borrow::Cow;
use std::collections::HashSet;
use std::ffi::OsString;
use std::io::Write as _;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Stdio;
use mago_database::Database;
use mago_database::DatabaseReader;
use mago_database::error::DatabaseError;
use mago_database::file::File;
use mago_database::file::FileId;
use crate::error::Error;
pub fn get_staged_file_paths(workspace: &Path) -> Result<Vec<PathBuf>, Error> {
if !is_git_repository(workspace) {
return Err(Error::NotAGitRepository);
}
get_staged_files(workspace)
}
pub fn get_staged_file(workspace: &Path, path: &Path) -> Result<File, Error> {
let mut index_path = OsString::from(":");
index_path.push(path);
let output = Command::new("git")
.args(["cat-file", "-p"])
.arg(index_path)
.current_dir(workspace)
.output()
.map_err(|e| Error::Database(DatabaseError::IOError(e)))?;
Ok(File::ephemeral(Cow::Borrowed("<stdin>"), Cow::Owned(String::from_utf8_lossy(&output.stdout).into_owned())))
}
pub fn update_staged_file(workspace: &Path, path: &Path, new_content: String) -> Result<(), Error> {
let blob_id = create_blob(workspace, path, new_content)?;
let mode = get_mode(workspace, path)?;
let mut cacheinfo = OsString::new();
cacheinfo.push(&mode);
cacheinfo.push(",");
cacheinfo.push(&blob_id);
cacheinfo.push(",");
cacheinfo.push(path.as_os_str());
Command::new("git")
.args(["update-index", "--cacheinfo"])
.arg(cacheinfo)
.current_dir(workspace)
.status()
.map_err(|e| Error::Database(DatabaseError::IOError(e)))?;
Ok(())
}
pub fn ensure_staged_files_are_clean(workspace: &Path, staged_files: &[PathBuf]) -> Result<(), Error> {
let files_with_unstaged = get_files_with_unstaged_changes(workspace)?;
for staged_file in staged_files {
if files_with_unstaged.contains(staged_file) {
return Err(Error::StagedFileHasUnstagedChanges(staged_file.display().to_string()));
}
}
Ok(())
}
pub fn stage_files<I>(workspace: &Path, database: &Database, file_ids: I) -> Result<(), Error>
where
I: IntoIterator<Item = FileId>,
{
let paths: Vec<PathBuf> = file_ids
.into_iter()
.filter_map(|id| database.get_ref(&id).ok())
.map(|file| PathBuf::from(&*file.name))
.collect();
if paths.is_empty() {
return Ok(());
}
let mut cmd = Command::new("git");
cmd.args(["add", "--"]);
for path in &paths {
cmd.arg(path);
}
let status = cmd.current_dir(workspace).status().map_err(|e| Error::Database(DatabaseError::IOError(e)))?;
if !status.success() {
return Err(Error::Database(DatabaseError::IOError(std::io::Error::other("git add failed"))));
}
Ok(())
}
fn is_git_repository(workspace: &Path) -> bool {
Command::new("git")
.args(["rev-parse", "--git-dir"])
.current_dir(workspace)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn get_staged_files(workspace: &Path) -> Result<Vec<PathBuf>, Error> {
let output = Command::new("git")
.args(["diff", "--cached", "--name-only", "--diff-filter=ACMR"])
.current_dir(workspace)
.output()
.map_err(|e| Error::Database(DatabaseError::IOError(e)))?;
if !output.status.success() {
return Err(Error::NotAGitRepository);
}
Ok(String::from_utf8_lossy(&output.stdout).lines().filter(|l| !l.is_empty()).map(PathBuf::from).collect())
}
fn get_files_with_unstaged_changes(workspace: &Path) -> Result<HashSet<PathBuf>, Error> {
let output = Command::new("git")
.args(["diff", "--name-only"])
.current_dir(workspace)
.output()
.map_err(|e| Error::Database(DatabaseError::IOError(e)))?;
Ok(String::from_utf8_lossy(&output.stdout).lines().filter(|l| !l.is_empty()).map(PathBuf::from).collect())
}
fn create_blob(workspace: &Path, path: &Path, content: String) -> Result<String, Error> {
let mut child = Command::new("git")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.args(["hash-object", "-w", "--stdin", "--path"])
.arg(path)
.current_dir(workspace)
.spawn()
.map_err(|e| Error::Database(DatabaseError::IOError(e)))?;
let mut stdin = child.stdin.take().expect("failed to get stdin");
std::thread::spawn(move || {
stdin.write_all(content.as_bytes()).expect("failed to write to stdin");
});
let output = child.wait_with_output().map_err(|e| Error::Database(DatabaseError::IOError(e)))?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
}
fn get_mode(workspace: &Path, path: &Path) -> Result<String, Error> {
let output = Command::new("git")
.args(["ls-files", "--format=%(objectmode)"])
.arg(path)
.current_dir(workspace)
.output()
.map_err(|e| Error::Database(DatabaseError::IOError(e)))?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
}