use crate::error::{GwmError, Result};
use crate::worktree;
use git2::{BranchType, Repository};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncStrategy {
Rebase,
Merge,
}
impl SyncStrategy {
fn verb(self) -> &'static str {
match self {
SyncStrategy::Rebase => "rebase",
SyncStrategy::Merge => "merge",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncAction {
UpToDate,
Integrated,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyncReport {
pub branch: String,
pub upstream: String,
pub strategy: SyncStrategy,
pub ahead_before: usize,
pub behind_before: usize,
pub action: SyncAction,
}
pub fn sync(start: &Path, strategy: SyncStrategy) -> Result<SyncReport> {
let repo = Repository::discover(start).map_err(|_| GwmError::NotInGitRepo)?;
let workdir = repo.workdir().ok_or(GwmError::NotInGitRepo)?.to_path_buf();
if worktree::is_dirty(&repo)? {
return Err(GwmError::Other(
"worktree has uncommitted changes; commit or stash before syncing".into(),
));
}
let head = repo.head().map_err(|_| GwmError::UnbornHead {
reason: "sync: cannot read HEAD (unborn or unreadable)".into(),
})?;
if !head.is_branch() {
return Err(GwmError::UnbornHead {
reason: "sync: HEAD is detached — check out a branch first".into(),
});
}
let branch_short = head
.shorthand()
.ok()
.ok_or_else(|| GwmError::UnbornHead {
reason: "sync: HEAD has no branch name".into(),
})?
.to_string();
let head_refname = head.name().ok().map(|s| s.to_string());
let local = repo
.find_branch(&branch_short, BranchType::Local)
.map_err(|_| GwmError::Other(format!("sync: local branch '{branch_short}' not found")))?;
let upstream = local.upstream().map_err(|_| {
GwmError::Other(format!(
"branch '{branch_short}' has no upstream configured; set one with `git branch --set-upstream-to=<remote>/{branch_short}`"
))
})?;
let upstream_short = upstream
.name()
.ok()
.flatten()
.ok_or_else(|| GwmError::Other("sync: upstream tracking ref has no name".into()))?
.to_string();
let remote = head_refname
.as_deref()
.and_then(|rn| repo.branch_upstream_remote(rn).ok())
.and_then(|buf| buf.as_str().ok().map(|s| s.to_string()));
match &remote {
Some(r) => worktree::run_git_logged(&workdir, &["fetch", r])?,
None => worktree::run_git_logged(&workdir, &["fetch"])?,
};
let repo = Repository::discover(start).map_err(|_| GwmError::NotInGitRepo)?;
let (ahead_before, behind_before) = ahead_behind(&repo, &branch_short)?;
if behind_before == 0 {
return Ok(SyncReport {
branch: branch_short,
upstream: upstream_short,
strategy,
ahead_before,
behind_before,
action: SyncAction::UpToDate,
});
}
let integrate = match strategy {
SyncStrategy::Rebase => worktree::run_git_logged(&workdir, &["rebase", &upstream_short]),
SyncStrategy::Merge => worktree::run_git_logged(&workdir, &["merge", "--no-edit", &upstream_short]),
};
if let Err(e) = integrate {
let conflicted = Repository::discover(start)
.ok()
.and_then(|r| r.index().ok())
.map(|idx| idx.has_conflicts())
.unwrap_or(false);
let _ = worktree::run_git_logged(&workdir, &[strategy.verb(), "--abort"]);
if conflicted {
return Err(GwmError::Other(format!(
"{} onto {} hit conflicts and was aborted; reconcile manually with `git {} {}`",
strategy.verb(),
upstream_short,
strategy.verb(),
upstream_short
)));
}
return Err(GwmError::Other(format!(
"git {} onto {} failed and was aborted: {}",
strategy.verb(),
upstream_short,
e
)));
}
Ok(SyncReport {
branch: branch_short,
upstream: upstream_short,
strategy,
ahead_before,
behind_before,
action: SyncAction::Integrated,
})
}
fn ahead_behind(repo: &Repository, branch: &str) -> Result<(usize, usize)> {
let local = repo
.find_branch(branch, BranchType::Local)
.map_err(|_| GwmError::Other(format!("sync: local branch '{branch}' not found")))?;
let upstream = local
.upstream()
.map_err(|_| GwmError::Other(format!("branch '{branch}' has no upstream configured")))?;
let local_oid = local
.get()
.target()
.ok_or_else(|| GwmError::Other(format!("sync: branch '{branch}' has no commit")))?;
let up_oid = upstream
.get()
.target()
.ok_or_else(|| GwmError::Other("sync: upstream has no commit".into()))?;
let (ahead, behind) = repo.graph_ahead_behind(local_oid, up_oid)?;
Ok((ahead, behind))
}