use std::fs;
use std::path::Path;
use crate::error::{Error, Result};
use crate::objects::ObjectId;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HeadState {
Branch {
refname: String,
short_name: String,
oid: Option<ObjectId>,
},
Detached {
oid: ObjectId,
},
Invalid,
}
impl HeadState {
#[must_use]
pub fn oid(&self) -> Option<&ObjectId> {
match self {
Self::Branch { oid, .. } => oid.as_ref(),
Self::Detached { oid } => Some(oid),
Self::Invalid => None,
}
}
#[must_use]
pub fn branch_name(&self) -> Option<&str> {
match self {
Self::Branch { short_name, .. } => Some(short_name),
_ => None,
}
}
#[must_use]
pub fn is_unborn(&self) -> bool {
matches!(self, Self::Branch { oid: None, .. })
}
#[must_use]
pub fn is_detached(&self) -> bool {
matches!(self, Self::Detached { .. })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InProgressOperation {
Merge,
RebaseInteractive,
Rebase,
CherryPick,
Revert,
Bisect,
Am,
}
impl InProgressOperation {
#[must_use]
pub fn description(&self) -> &'static str {
match self {
Self::Merge => "merge",
Self::RebaseInteractive => "interactive rebase",
Self::Rebase => "rebase",
Self::CherryPick => "cherry-pick",
Self::Revert => "revert",
Self::Bisect => "bisect",
Self::Am => "am",
}
}
#[must_use]
pub fn hint(&self) -> &'static str {
match self {
Self::Merge => "fix conflicts and run \"git commit\"\n (use \"git merge --abort\" to abort the merge)",
Self::RebaseInteractive => "fix conflicts and then run \"git rebase --continue\"\n (use \"git rebase --abort\" to abort the rebase)",
Self::Rebase => "fix conflicts and then run \"git rebase --continue\"\n (use \"git rebase --abort\" to abort the rebase)",
Self::CherryPick => "fix conflicts and run \"git cherry-pick --continue\"\n (use \"git cherry-pick --abort\" to abort the cherry-pick)",
Self::Revert => "fix conflicts and run \"git revert --continue\"\n (use \"git revert --abort\" to abort the revert)",
Self::Bisect => "use \"git bisect reset\" to get back to the original branch",
Self::Am => "fix conflicts and then run \"git am --continue\"\n (use \"git am --abort\" to abort the am)",
}
}
}
#[derive(Debug, Clone)]
pub struct RepoState {
pub head: HeadState,
pub in_progress: Vec<InProgressOperation>,
pub is_bare: bool,
}
pub fn resolve_head(git_dir: &Path) -> Result<HeadState> {
let head_path = git_dir.join("HEAD");
let content = match fs::read_link(&head_path) {
Ok(link_target) => {
let rendered = link_target.to_string_lossy();
if link_target.is_absolute() {
format!("ref: {rendered}")
} else if rendered.starts_with("refs/") {
format!("ref: {rendered}")
} else {
fs::read_to_string(&head_path).map_err(Error::Io)?
}
}
Err(_) => match fs::read_to_string(&head_path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(HeadState::Invalid),
Err(e) => return Err(Error::Io(e)),
},
};
let trimmed = content.trim();
if let Some(refname) = trimmed.strip_prefix("ref: ") {
let refname = refname.to_owned();
let short_name = refname
.strip_prefix("refs/heads/")
.unwrap_or(&refname)
.to_owned();
let oid = match crate::refs::resolve_ref(git_dir, &refname) {
Ok(oid) => Some(oid),
Err(Error::InvalidRef(msg)) if msg.starts_with("ref not found:") => None,
Err(e) => return Err(e),
};
Ok(HeadState::Branch {
refname,
short_name,
oid,
})
} else {
match ObjectId::from_hex(trimmed) {
Ok(oid) => Ok(HeadState::Detached { oid }),
Err(_) => Ok(HeadState::Invalid),
}
}
}
pub fn detect_in_progress(git_dir: &Path) -> Vec<InProgressOperation> {
let mut ops = Vec::new();
if git_dir.join("MERGE_HEAD").exists() {
ops.push(InProgressOperation::Merge);
}
let rebase_merge = git_dir.join("rebase-merge");
if rebase_merge.is_dir() {
if rebase_merge.join("interactive").exists() {
ops.push(InProgressOperation::RebaseInteractive);
} else {
ops.push(InProgressOperation::Rebase);
}
}
let rebase_apply = git_dir.join("rebase-apply");
if rebase_apply.is_dir() {
if rebase_apply.join("applying").exists() {
ops.push(InProgressOperation::Am);
} else {
ops.push(InProgressOperation::Rebase);
}
}
if git_dir.join("CHERRY_PICK_HEAD").exists() {
ops.push(InProgressOperation::CherryPick);
}
if git_dir.join("REVERT_HEAD").exists() {
ops.push(InProgressOperation::Revert);
}
let bisect_log = crate::refs::common_dir(git_dir)
.unwrap_or_else(|| git_dir.to_path_buf())
.join("BISECT_LOG");
if bisect_log.exists() {
ops.push(InProgressOperation::Bisect);
}
ops
}
pub fn repo_state(git_dir: &Path, is_bare: bool) -> Result<RepoState> {
let head = resolve_head(git_dir)?;
let in_progress = detect_in_progress(git_dir);
Ok(RepoState {
head,
in_progress,
is_bare,
})
}
pub fn read_merge_heads(git_dir: &Path) -> Result<Vec<ObjectId>> {
let path = git_dir.join("MERGE_HEAD");
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(Error::Io(e)),
};
let mut oids = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
oids.push(ObjectId::from_hex(trimmed)?);
}
}
Ok(oids)
}
pub fn read_merge_msg(git_dir: &Path) -> Result<Option<String>> {
let path = git_dir.join("MERGE_MSG");
match fs::read_to_string(&path) {
Ok(c) => Ok(Some(c)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(Error::Io(e)),
}
}
pub fn read_cherry_pick_head(git_dir: &Path) -> Result<Option<ObjectId>> {
read_single_oid_file(&git_dir.join("CHERRY_PICK_HEAD"))
}
pub fn read_revert_head(git_dir: &Path) -> Result<Option<ObjectId>> {
read_single_oid_file(&git_dir.join("REVERT_HEAD"))
}
pub fn read_orig_head(git_dir: &Path) -> Result<Option<ObjectId>> {
read_single_oid_file(&git_dir.join("ORIG_HEAD"))
}
fn read_single_oid_file(path: &Path) -> Result<Option<ObjectId>> {
match fs::read_to_string(path) {
Ok(content) => {
let trimmed = content.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(ObjectId::from_hex(trimmed)?))
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(Error::Io(e)),
}
}
pub fn upstream_tracking(_git_dir: &Path, _branch: &str) -> Result<Option<(usize, usize)>> {
Ok(None)
}