use std::path::Path;
use serde::{Deserialize, Serialize};
use super::model::{Blocked, RepositoryId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GitState {
Settled,
Merge,
Rebase,
CherryPick,
Revert,
Bisect,
Rewriting,
}
impl GitState {
pub fn is_settled(self) -> bool {
matches!(self, Self::Settled)
}
pub fn as_str(self) -> &'static str {
match self {
Self::Settled => "settled",
Self::Merge => "merge",
Self::Rebase => "rebase",
Self::CherryPick => "cherry_pick",
Self::Revert => "revert",
Self::Bisect => "bisect",
Self::Rewriting => "rewriting",
}
}
pub fn read(git_dir: &Path) -> Self {
for (marker, state) in [
("rebase-merge", Self::Rebase),
("rebase-apply", Self::Rebase),
("MERGE_HEAD", Self::Merge),
("CHERRY_PICK_HEAD", Self::CherryPick),
("REVERT_HEAD", Self::Revert),
("BISECT_LOG", Self::Bisect),
] {
if git_dir.join(marker).exists() {
return state;
}
}
Self::Settled
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepositoryFacts {
pub repository: RepositoryId,
pub root_commit: String,
pub branch: Option<String>,
pub head: Option<String>,
pub git_state: GitState,
pub present: bool,
}
impl RepositoryFacts {
pub fn absent(repository: RepositoryId) -> Self {
Self {
repository,
root_commit: String::new(),
branch: None,
head: None,
git_state: GitState::Settled,
present: false,
}
}
pub fn read(
repository: RepositoryId,
workdir: &Path,
git_dir: &Path,
) -> Result<Self, String> {
if !workdir.is_dir() || !git_dir.exists() {
return Ok(Self::absent(repository));
}
let repo = crate::git_backend::open_repo(workdir)
.map_err(|e| format!("{}: {e}", repository.label()))?;
let branch = crate::git_backend::current_branch(&repo).ok();
let head = repo.head_id().ok().map(|id| id.detach().to_string());
let root_commit = root_commit(&repo)?;
Ok(Self {
repository,
root_commit,
branch,
head,
git_state: GitState::read(git_dir),
present: true,
})
}
pub fn epoch(&self) -> Epoch {
Epoch {
repository: self.repository.clone(),
root_commit: self.root_commit.clone(),
branch: self.branch.clone(),
head: self.head.clone(),
git_state: self.git_state,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Epoch {
pub repository: RepositoryId,
pub root_commit: String,
pub branch: Option<String>,
pub head: Option<String>,
pub git_state: GitState,
}
impl Epoch {
pub fn still_current(&self, now: &RepositoryFacts) -> bool {
now.present && *self == now.epoch()
}
}
pub fn eligibility(here: &RepositoryFacts, there: &RepositoryFacts) -> Result<Epoch, Blocked> {
if !here.present || !there.present {
return Err(Blocked::SubmoduleMissing {
here: here.present,
there: there.present,
});
}
if here.root_commit != there.root_commit {
return Err(Blocked::RepositoryMismatch {
here: here.root_commit.clone(),
there: there.root_commit.clone(),
});
}
match (&here.branch, &there.branch) {
(Some(a), Some(b)) if a != b => {
return Err(Blocked::BranchMismatch {
here: a.clone(),
there: b.clone(),
})
}
(None, _) | (_, None) => {
return Err(Blocked::DetachedHead {
here: here.branch.is_none(),
there: there.branch.is_none(),
});
}
_ => {}
}
if here.head != there.head {
return Err(Blocked::HeadMismatch {
here: here.head.clone().unwrap_or_default(),
there: there.head.clone().unwrap_or_default(),
});
}
if !here.git_state.is_settled() || !there.git_state.is_settled() {
return Err(Blocked::GitOperation {
here: !here.git_state.is_settled(),
there: !there.git_state.is_settled(),
});
}
Ok(here.epoch())
}
fn root_commit(repo: &gix::Repository) -> Result<String, String> {
let Ok(head) = repo.head_id() else {
return Ok(String::new());
};
let mut current = head.detach();
loop {
let commit = repo
.find_commit(current)
.map_err(|e| format!("cannot read commit {current}: {e}"))?;
let parent = commit.parent_ids().next().map(|id| id.detach());
match parent {
Some(parent) => current = parent,
None => return Ok(current.to_string()),
}
}
}
#[cfg(test)]
#[path = "compatibility_tests.rs"]
mod tests;