use std::{
fs,
path::{Path, PathBuf},
};
use anyhow::{Context, bail};
use git2::{
Cred, FetchOptions, IndexAddOption, PushOptions, RemoteCallbacks, Repository, Status, StatusOptions,
build::{CheckoutBuilder, RepoBuilder},
};
use secrecy::{ExposeSecret, SecretBox};
pub async fn clone_repo(
owner: &str,
repo: &str,
path: &Path,
head_ref: &str,
token: &SecretBox<str>,
) -> Result<Repository, anyhow::Error> {
let owner_path = path.join(owner);
fs::create_dir_all(&owner_path)?;
let repo_path = path.join(owner).join(repo);
let repo_url = format!("https://github.com/{owner}/{repo}.git");
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(move |_url, _username_from_url, _allowed_types| {
Cred::userpass_plaintext("x-access-token", token.expose_secret())
});
let mut fetch_opts = FetchOptions::new();
fetch_opts.remote_callbacks(callbacks);
fetch_opts.depth(1);
let _checkout_opts = CheckoutBuilder::new();
let repository = match Repository::open(&repo_path) {
Ok(repository) => repository,
Err(_) => RepoBuilder::new()
.fetch_options(fetch_opts)
.branch(head_ref)
.clone(&repo_url, &repo_path)
.context("RepoBuilder::new()")?,
};
repository.remote_set_url("origin", &repo_url)?;
Ok(repository)
}
pub async fn checkout_new_branch(repo_path: &Path, branch_name: &str) -> Result<Repository, anyhow::Error> {
let Ok(repository) = Repository::open(repo_path) else {
bail!("No repository at {}", &repo_path.to_str().unwrap_or_default())
};
{
let branch = repository.branch(branch_name, &repository.head()?.peel_to_commit()?, false)?;
repository.checkout_tree(&branch.get().peel(git2::ObjectType::Tree)?, None)?;
repository.set_head(&format!("refs/heads/{branch_name}"))?;
}
Ok(repository)
}
pub async fn checkout_branch(repo_path: &Path, branch_name: &str) -> anyhow::Result<()> {
let Ok(repository) = Repository::open(repo_path) else {
bail!("No repository at {}", &repo_path.to_str().unwrap_or_default())
};
let (object, reference) = repository.revparse_ext(branch_name)?;
repository.checkout_tree(&object, None)?;
match reference {
Some(gref) => {
repository.set_head(gref.name().unwrap())?;
}
None => {
repository.set_head_detached(object.id())?;
}
}
Ok(())
}
pub fn git_add(repo_path: &Path, path: &Path) -> anyhow::Result<()> {
let Ok(repository) = Repository::open(repo_path) else {
bail!("No repository at {}", &repo_path.to_str().unwrap_or_default())
};
let mut index = repository.index()?;
index.add_all([path], IndexAddOption::default(), None)?;
index.write()?;
Ok(())
}
pub fn git_commit(repo_path: &Path, username: &str, email: &str, message: &str) -> anyhow::Result<()> {
let Ok(repository) = Repository::open(repo_path) else {
bail!("No repository at {}", &repo_path.to_str().unwrap_or_default())
};
let mut index = repository.index()?;
let oid = index.write_tree()?;
let parent_commit = repository.head()?.peel_to_commit()?;
let tree = repository.find_tree(oid)?;
let sig = git2::Signature::now(username, email)?;
repository.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent_commit])?;
Ok(())
}
pub fn git_commit_and_push(repo_path: &Path, head_ref: &str, token: &SecretBox<str>, message: &str) -> anyhow::Result<()> {
let Ok(repository) = Repository::open(repo_path) else {
bail!("No repository at {}", &repo_path.to_str().unwrap_or_default())
};
let mut index = repository.index()?;
let oid = index.write_tree()?;
let parent_commit = repository.head()?.peel_to_commit()?;
let tree = repository.find_tree(oid)?;
let sig = git2::Signature::now("autoschematic", "apply@autoschematic.sh")?;
repository.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent_commit])?;
let mut remote = repository.find_remote("origin")?;
let refspec = format!("refs/heads/{head_ref}:refs/heads/{head_ref}");
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(move |_url, _username_from_url, _allowed_types| {
Cred::userpass_plaintext("x-access-token", token.expose_secret())
});
let mut push_options = PushOptions::new();
push_options.remote_callbacks(callbacks);
remote.push::<&str>(&[&refspec], Some(&mut push_options))?;
Ok(())
}
pub fn pull_with_rebase(repo_path: &Path, branch_name: &str, token: &SecretBox<str>) -> Result<(), anyhow::Error> {
let Ok(repository) = Repository::open(repo_path) else {
bail!("No repository at {}", repo_path.to_str().unwrap_or_default())
};
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(move |_url, _username_from_url, _allowed_types| {
Cred::userpass_plaintext("x-access-token", token.expose_secret())
});
let mut fetch_opts = FetchOptions::new();
fetch_opts.remote_callbacks(callbacks);
let mut remote = repository.find_remote("origin")?;
remote.fetch(&[branch_name], Some(&mut fetch_opts), None)?;
let fetch_head = repository.find_reference("FETCH_HEAD")?;
let fetch_ref = repository.reference_to_annotated_commit(&fetch_head)?;
let branch_ref_name = format!("refs/heads/{branch_name}");
let mut branch_ref = repository.find_reference(&branch_ref_name)?;
let msg = format!("Fast-Forward: Setting {} to id: {}", branch_ref_name, fetch_ref.id());
branch_ref.set_target(fetch_ref.id(), &msg)?;
repository.set_head(&branch_ref_name)?;
repository.checkout_head(Some(
git2::build::CheckoutBuilder::default()
.force(),
))?;
Ok(())
}
pub fn get_head_sha(repo_path: &Path) -> anyhow::Result<String> {
if let Ok(repository) = Repository::open(repo_path) {
let head = repository.head()?;
Ok(head.peel_to_commit()?.id().to_string())
} else {
bail!("No repository at {}", repo_path.to_str().unwrap_or_default())
}
}
pub fn get_staged_files() -> Result<Vec<PathBuf>, git2::Error> {
let repo = Repository::discover(".")?;
let mut status_opts = StatusOptions::new();
status_opts.show(git2::StatusShow::Index);
status_opts.include_untracked(false).renames_head_to_index(true);
let statuses = repo.statuses(Some(&mut status_opts))?;
let mut staged = Vec::new();
for entry in statuses.iter() {
let s = entry.status();
let is_staged = s.intersects(
Status::INDEX_NEW
| Status::INDEX_MODIFIED
| Status::INDEX_DELETED
| Status::INDEX_RENAMED
| Status::INDEX_TYPECHANGE,
);
if is_staged && let Some(path) = entry.path() {
staged.push(PathBuf::from(path));
}
if s.intersects(Status::INDEX_RENAMED)
&& let Some(new_path) = entry.head_to_index().and_then(|d| d.new_file().path())
{
staged.push(PathBuf::from(new_path));
}
}
Ok(staged)
}