use std::path::Path;
use processkit::ProcessRunner;
use vcs_git::{Git, GitApi, GitPush, StatusEntry, WorktreeAdd};
use crate::dto::{
ChangeKind, CreateOutcome, DiffStat, FileChange, MergeProbe, OperationState, RepoSnapshot,
WorktreeInfo,
};
use crate::error::Result;
pub(crate) async fn current_branch<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
) -> Result<Option<String>> {
let branch = git.current_branch(dir).await?;
Ok((branch != "HEAD").then_some(branch))
}
pub(crate) async fn trunk<R: ProcessRunner>(git: &Git<R>, dir: &Path) -> Result<Option<String>> {
Ok(git.remote_head_branch(dir).await?)
}
pub(crate) async fn local_branches<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
) -> Result<Vec<String>> {
Ok(git
.branches(dir)
.await?
.into_iter()
.map(|b| b.name)
.collect())
}
pub(crate) async fn branch_exists<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
name: &str,
) -> Result<bool> {
Ok(git.branch_exists(dir, name).await?)
}
pub(crate) async fn has_uncommitted_changes<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
) -> Result<bool> {
Ok(!git.status(dir).await?.is_empty())
}
pub(crate) async fn has_tracked_changes<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
) -> Result<bool> {
Ok(!git.status_tracked(dir).await?.is_empty())
}
pub(crate) async fn conflicted_files<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
) -> Result<Vec<String>> {
Ok(git.conflicted_files(dir).await?)
}
pub(crate) async fn delete_branch<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
name: &str,
force: bool,
) -> Result<()> {
git.delete_branch(dir, name, force).await?;
Ok(())
}
pub(crate) async fn rename_branch<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
old: &str,
new: &str,
) -> Result<()> {
git.rename_branch(dir, old, new).await?;
Ok(())
}
pub(crate) async fn changed_files<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
) -> Result<Vec<FileChange>> {
let entries = git.status(dir).await?;
Ok(entries.into_iter().map(file_change_from_status).collect())
}
pub(crate) async fn diff_stat<R: ProcessRunner>(git: &Git<R>, dir: &Path) -> Result<DiffStat> {
let range = if git.is_unborn(dir).await? {
vcs_git::EMPTY_TREE
} else {
"HEAD"
};
git.diff_stat(dir, range).await.map_err(Into::into)
}
pub(crate) async fn snapshot<R: ProcessRunner>(git: &Git<R>, dir: &Path) -> Result<RepoSnapshot> {
let bs = git.branch_status(dir).await?;
let raw = git.git_dir(dir).await?;
let git_dir = if raw.is_absolute() {
raw
} else {
dir.join(raw)
};
let operation = if git_dir.join("MERGE_HEAD").exists() {
OperationState::Merge
} else if git_dir.join("rebase-merge").exists() || git_dir.join("rebase-apply").exists() {
OperationState::Rebase
} else {
OperationState::Clear
};
let dirty = bs.is_dirty();
let change_count = bs.tracked_changes + bs.untracked;
let conflicted = bs.conflicts > 0;
Ok(RepoSnapshot {
head: bs.head,
branch: bs.branch,
upstream: bs.upstream,
ahead: bs.ahead,
behind: bs.behind,
dirty,
change_count,
conflicted,
operation,
})
}
pub(crate) async fn commit_paths<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
paths: &[String],
message: &str,
) -> Result<()> {
git.commit_paths(
dir,
vcs_git::CommitPaths::new(paths.iter().map(String::as_str), message),
)
.await?;
Ok(())
}
pub(crate) async fn fetch<R: ProcessRunner>(git: &Git<R>, dir: &Path) -> Result<()> {
git.fetch(dir).await?;
Ok(())
}
pub(crate) async fn fetch_from<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
remote: &str,
) -> Result<()> {
git.fetch_from(dir, remote).await?;
Ok(())
}
pub(crate) async fn fetch_remote_branch<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
branch: &str,
) -> Result<()> {
git.fetch_remote_branch(dir, branch).await?;
Ok(())
}
pub(crate) async fn push<R: ProcessRunner>(git: &Git<R>, dir: &Path, branch: &str) -> Result<()> {
git.push(dir, GitPush::branch(branch).set_upstream())
.await?;
Ok(())
}
pub(crate) async fn checkout<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
reference: &str,
) -> Result<()> {
git.checkout(dir, reference).await?;
Ok(())
}
pub(crate) async fn rebase<R: ProcessRunner>(git: &Git<R>, dir: &Path, onto: &str) -> Result<()> {
git.rebase(dir, onto).await?;
Ok(())
}
pub(crate) async fn try_merge<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
source: &str,
) -> Result<MergeProbe> {
let merged = git
.merge_no_commit(dir, vcs_git::MergeNoCommit::branch(source).no_ff())
.await;
match merged {
Ok(()) => {
if git.is_merge_in_progress(dir).await? {
git.merge_abort(dir).await?;
}
Ok(MergeProbe::Clean)
}
Err(err) if vcs_git::is_merge_conflict(&err) => {
let files = git.conflicted_files(dir).await?;
git.merge_abort(dir).await?;
Ok(MergeProbe::Conflicts(files))
}
Err(err) => {
if git.is_merge_in_progress(dir).await? {
git.merge_abort(dir).await?;
}
Err(err.into())
}
}
}
pub(crate) async fn abort_in_progress<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
) -> Result<OperationState> {
match in_progress_state(git, dir).await? {
OperationState::Merge => git.merge_abort(dir).await?,
OperationState::Rebase => git.rebase_abort(dir).await?,
_ => {}
}
in_progress_state(git, dir).await
}
pub(crate) async fn continue_in_progress<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
) -> Result<OperationState> {
if !git.conflicted_files(dir).await?.is_empty() {
return Ok(OperationState::Conflict);
}
match in_progress_state(git, dir).await? {
OperationState::Merge => git.merge_continue(dir).await?,
OperationState::Rebase => {
if let Err(err) = git.rebase_continue(dir).await {
if !git.conflicted_files(dir).await?.is_empty() {
return Ok(OperationState::Conflict);
}
return Err(err.into());
}
}
_ => {}
}
if !git.conflicted_files(dir).await?.is_empty() {
return Ok(OperationState::Conflict);
}
in_progress_state(git, dir).await
}
pub(crate) async fn in_progress_state<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
) -> Result<OperationState> {
if git.is_merge_in_progress(dir).await? {
Ok(OperationState::Merge)
} else if git.is_rebase_in_progress(dir).await? {
Ok(OperationState::Rebase)
} else {
Ok(OperationState::Clear)
}
}
pub(crate) async fn list_worktrees<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
) -> Result<Vec<WorktreeInfo>> {
let worktrees = git.worktree_list(dir).await?;
Ok(worktrees
.into_iter()
.map(|w| WorktreeInfo {
path: w.path,
branch: w.branch,
commit: w.head,
is_bare: w.bare,
})
.collect())
}
pub(crate) async fn create_worktree<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
path: &Path,
branch: &str,
base: &str,
) -> Result<CreateOutcome> {
git.worktree_add(dir, WorktreeAdd::create_branch(path, branch, base))
.await?;
Ok(CreateOutcome::Plain)
}
pub(crate) async fn remove_worktree<R: ProcessRunner>(
git: &Git<R>,
dir: &Path,
path: &Path,
force: bool,
) -> Result<()> {
git.worktree_remove(dir, path, force).await?;
Ok(())
}
fn file_change_from_status(entry: StatusEntry) -> FileChange {
FileChange {
kind: change_kind_from_code(&entry.code),
path: entry.path,
old_path: entry.orig_path,
}
}
fn change_kind_from_code(code: &str) -> ChangeKind {
if code.contains('R') {
ChangeKind::Renamed
} else if code.contains('D') {
ChangeKind::Deleted
} else if code.contains('A') || code.contains('?') || code.contains('C') {
ChangeKind::Added
} else {
ChangeKind::Modified
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_code_maps_to_change_kind() {
assert_eq!(change_kind_from_code(" M"), ChangeKind::Modified);
assert_eq!(change_kind_from_code("??"), ChangeKind::Added);
assert_eq!(change_kind_from_code("A "), ChangeKind::Added);
assert_eq!(change_kind_from_code(" D"), ChangeKind::Deleted);
assert_eq!(change_kind_from_code("R "), ChangeKind::Renamed);
assert_eq!(change_kind_from_code("C "), ChangeKind::Added);
}
}