use anyhow::{Context, Result};
use git2::{DiffOptions, Repository, StatusOptions};
use std::process::Command;
pub fn assert_git_repo() -> Result<()> {
Repository::open_from_env().context(
"Not in a git repository. Please run this command from within a git repository.",
)?;
Ok(())
}
pub fn get_staged_files() -> Result<Vec<String>> {
let repo = Repository::open_from_env()?;
let mut status_opts = StatusOptions::new();
status_opts.include_untracked(false);
let statuses = repo.statuses(Some(&mut status_opts))?;
let mut staged_files = Vec::new();
for entry in statuses.iter() {
let status = entry.status();
if status.contains(git2::Status::INDEX_NEW)
|| status.contains(git2::Status::INDEX_MODIFIED)
|| status.contains(git2::Status::INDEX_DELETED)
|| status.contains(git2::Status::INDEX_RENAMED)
|| status.contains(git2::Status::INDEX_TYPECHANGE)
{
if let Some(path) = entry.path() {
staged_files.push(path.to_string());
}
}
}
Ok(staged_files)
}
pub fn get_changed_files() -> Result<Vec<String>> {
let repo = Repository::open_from_env()?;
let mut status_opts = StatusOptions::new();
status_opts.include_untracked(true);
let statuses = repo.statuses(Some(&mut status_opts))?;
let mut changed_files = Vec::new();
for entry in statuses.iter() {
let status = entry.status();
if !status.contains(git2::Status::IGNORED) && !status.is_empty() {
if let Some(path) = entry.path() {
changed_files.push(path.to_string());
}
}
}
Ok(changed_files)
}
pub fn stage_files(files: &[String]) -> Result<()> {
if files.is_empty() {
return Ok(());
}
let output = Command::new("git")
.arg("add")
.args(files)
.output()
.context("Failed to stage files")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Failed to stage files: {}", stderr);
}
Ok(())
}
pub fn get_staged_diff() -> Result<String> {
let repo = Repository::open_from_env()?;
let head = repo.head()?;
let head_tree = head.peel_to_tree()?;
let mut index = repo.index()?;
let oid = index.write_tree()?;
let index_tree = repo.find_tree(oid)?;
let mut diff_opts = DiffOptions::new();
let diff = repo.diff_tree_to_tree(Some(&head_tree), Some(&index_tree), Some(&mut diff_opts))?;
let mut diff_text = String::new();
diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
let content = String::from_utf8_lossy(line.content());
diff_text.push_str(&content);
true
})?;
Ok(diff_text)
}
pub fn get_repo_root() -> Result<String> {
let repo = Repository::open_from_env()?;
let workdir = repo
.workdir()
.context("Could not find repository working directory")?;
Ok(workdir.to_string_lossy().to_string())
}
pub fn get_current_branch() -> Result<String> {
let repo = Repository::open_from_env()?;
let head = repo.head()?;
let branch_name = head
.shorthand()
.context("Could not get current branch name")?
.to_string();
Ok(branch_name)
}
pub fn get_commits_between(base: &str, head: &str) -> Result<Vec<String>> {
let repo = Repository::open_from_env()?;
let base_commit = repo.revparse_single(base)?;
let head_commit = repo.revparse_single(head)?;
let mut revwalk = repo.revwalk()?;
revwalk.push(head_commit.id())?;
revwalk.hide(base_commit.id())?;
let mut commits = Vec::new();
for oid in revwalk {
let oid = oid?;
if let Ok(commit) = repo.find_commit(oid) {
commits.push(format!(
"{} - {}",
commit.id().to_string().chars().take(7).collect::<String>(),
commit.message().unwrap_or("")
));
}
}
Ok(commits)
}
pub fn get_diff_between(base: &str, head: &str) -> Result<String> {
let repo = Repository::open_from_env()?;
let base_commit = repo.revparse_single(base)?;
let head_commit = repo.revparse_single(head)?;
let base_tree = base_commit
.as_tree()
.ok_or(anyhow::anyhow!("Failed to get base commit tree"))?;
let head_tree = head_commit
.as_tree()
.ok_or(anyhow::anyhow!("Failed to get head commit tree"))?;
let mut diff_opts = DiffOptions::new();
let diff = repo.diff_tree_to_tree(Some(base_tree), Some(head_tree), Some(&mut diff_opts))?;
let mut diff_text = String::new();
diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
let content = String::from_utf8_lossy(line.content());
diff_text.push_str(&content);
true
})?;
Ok(diff_text)
}
pub fn get_remote_url(remote_name: Option<&str>) -> Result<String> {
let repo = Repository::open_from_env()?;
let remote = repo.find_remote(remote_name.unwrap_or("origin"))?;
let url = remote
.url()
.context("Could not get remote URL")?
.to_string();
Ok(url)
}
pub fn git_push(remote: &str, branch: &str) -> Result<()> {
let output = Command::new("git")
.args(["push", remote, branch])
.output()
.context("Failed to execute git push")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Git push failed: {}", stderr);
}
Ok(())
}
pub fn git_push_upstream() -> Result<()> {
let output = Command::new("git")
.args(["push", "--set-upstream"])
.output()
.context("Failed to execute git push")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Git push failed: {}", stderr);
}
Ok(())
}
pub fn get_recent_commit_messages(count: usize) -> Result<Vec<String>> {
let repo = Repository::open_from_env()?;
let head = repo.head()?;
let commit = head.peel_to_commit()?;
let mut commits = Vec::new();
let mut queue = vec![commit];
while let Some(c) = queue.pop() {
if commits.len() >= count {
break;
}
if let Some(msg) = c.message() {
commits.push(msg.to_string());
}
let parents: Result<Vec<_>, anyhow::Error> = (0..c.parent_count())
.map(|i| c.parent(i).map_err(anyhow::Error::from))
.collect();
if let Ok(parents) = parents {
for parent in parents.into_iter().rev() {
queue.push(parent);
}
}
}
Ok(commits)
}