use std::fs;
use std::path::Path;
use crate::error::{Error, Result};
use crate::objects::ObjectId;
use crate::reflog;
#[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 = if refname == "refs/heads/.invalid" {
match crate::refs::read_ref_file(&git_dir.join("refs").join("heads")) {
Ok(crate::refs::Ref::Symbolic(target)) => target,
_ => refname.to_owned(),
}
} else {
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
}
#[derive(Debug, Clone, Default)]
pub struct WtStatusState {
pub merge_in_progress: bool,
pub rebase_interactive_in_progress: bool,
pub rebase_in_progress: bool,
pub rebase_branch: Option<String>,
pub rebase_onto: Option<String>,
pub am_in_progress: bool,
pub am_empty_patch: bool,
pub cherry_pick_in_progress: bool,
pub cherry_pick_head_oid: Option<ObjectId>,
pub revert_in_progress: bool,
pub revert_head_oid: Option<ObjectId>,
pub bisect_in_progress: bool,
pub bisecting_from: Option<String>,
pub detached_from: Option<String>,
pub detached_at: bool,
}
fn abbrev_oid(oid: &ObjectId) -> String {
oid.to_hex()[..7].to_string()
}
fn read_trimmed_line(path: &Path) -> Option<String> {
let s = fs::read_to_string(path).ok()?;
let mut line = s.lines().next()?.to_string();
while line.ends_with('\n') || line.ends_with('\r') {
line.pop();
}
if line.is_empty() {
None
} else {
Some(line)
}
}
fn get_branch_display(git_dir: &Path, rel: &str) -> Option<String> {
let path = git_dir.join(rel);
let mut sb = read_trimmed_line(&path)?;
if let Some(branch_name) = sb.strip_prefix("refs/heads/") {
sb = branch_name.to_string();
} else if sb.starts_with("refs/") {
} else if ObjectId::from_hex(&sb).is_ok() {
let oid = ObjectId::from_hex(&sb).ok()?;
sb = abbrev_oid(&oid);
} else if sb == "detached HEAD" {
return None;
}
Some(sb)
}
fn strip_ref_for_display(full: &str) -> String {
if let Some(s) = full.strip_prefix("refs/tags/") {
return s.to_string();
}
if let Some(s) = full.strip_prefix("refs/remotes/") {
return s.to_string();
}
if let Some(s) = full.strip_prefix("refs/heads/") {
return s.to_string();
}
full.to_string()
}
fn dwim_detach_label(git_dir: &Path, target: &str, noid: ObjectId) -> String {
if target == "HEAD" {
return abbrev_oid(&noid);
}
if target.starts_with("refs/") {
if let Ok(oid) = crate::refs::resolve_ref(git_dir, target) {
if oid == noid {
return strip_ref_for_display(target);
}
}
}
for candidate in [
format!("refs/heads/{target}"),
format!("refs/tags/{target}"),
format!("refs/remotes/{target}"),
] {
if let Ok(oid) = crate::refs::resolve_ref(git_dir, &candidate) {
if oid == noid {
return strip_ref_for_display(&candidate);
}
}
}
if target.len() == 40 {
if let Ok(oid) = ObjectId::from_hex(target) {
if oid == noid {
return abbrev_oid(&noid);
}
}
}
if !target.is_empty()
&& target.chars().all(|c| c.is_ascii_hexdigit())
&& target.len() <= 40
&& noid.to_hex().starts_with(target)
{
return target.to_owned();
}
abbrev_oid(&noid)
}
fn wt_status_get_detached_from(git_dir: &Path, head_oid: ObjectId) -> Option<(String, bool)> {
let entries = reflog::read_reflog(git_dir, "HEAD").ok()?;
for entry in entries.iter().rev() {
let msg = entry.message.trim();
let Some(rest) = msg.strip_prefix("checkout: moving from ") else {
continue;
};
let Some(idx) = rest.rfind(" to ") else {
continue;
};
let target = rest[idx + 4..].trim();
let noid = entry.new_oid;
let label = dwim_detach_label(git_dir, target, noid);
let detached_at = head_oid == noid;
return Some((label, detached_at));
}
None
}
fn wt_status_check_rebase(git_dir: &Path, state: &mut WtStatusState) -> bool {
let apply = git_dir.join("rebase-apply");
if apply.is_dir() {
if apply.join("applying").exists() {
state.am_in_progress = true;
let patch = apply.join("patch");
if let Ok(meta) = patch.metadata() {
if meta.len() == 0 {
state.am_empty_patch = true;
}
}
} else {
state.rebase_in_progress = true;
state.rebase_branch = get_branch_display(git_dir, "rebase-apply/head-name");
state.rebase_onto = get_branch_display(git_dir, "rebase-apply/onto");
}
return true;
}
let merge = git_dir.join("rebase-merge");
if merge.is_dir() {
if merge.join("interactive").exists() {
state.rebase_interactive_in_progress = true;
} else {
state.rebase_in_progress = true;
}
state.rebase_branch = get_branch_display(git_dir, "rebase-merge/head-name");
state.rebase_onto = get_branch_display(git_dir, "rebase-merge/onto");
return true;
}
false
}
fn sequencer_first_replay(git_dir: &Path) -> Option<bool> {
let path = git_dir.join("sequencer").join("todo");
if !path.is_file() {
return None;
}
let content = fs::read_to_string(&path).ok()?;
for line in content.lines() {
let t = line.trim();
if t.is_empty() || t.starts_with('#') {
continue;
}
let mut parts = t.split_whitespace();
let cmd = parts.next()?;
return Some(matches!(cmd, "pick" | "p" | "revert" | "r"));
}
None
}
pub fn wt_status_get_state(
git_dir: &Path,
head: &HeadState,
get_detached_from: bool,
) -> Result<WtStatusState> {
let mut state = WtStatusState::default();
if git_dir.join("MERGE_HEAD").exists() {
wt_status_check_rebase(git_dir, &mut state);
state.merge_in_progress = true;
} else if wt_status_check_rebase(git_dir, &mut state) {
} else if let Some(oid) = read_cherry_pick_head(git_dir)? {
state.cherry_pick_in_progress = true;
state.cherry_pick_head_oid = Some(oid);
}
let bisect_base = crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
if bisect_base.join("BISECT_LOG").exists() {
state.bisect_in_progress = true;
state.bisecting_from = get_branch_display(&bisect_base, "BISECT_START");
}
if let Some(oid) = read_revert_head(git_dir)? {
state.revert_in_progress = true;
state.revert_head_oid = Some(oid);
}
if let Some(is_pick) = sequencer_first_replay(git_dir) {
if is_pick && !state.cherry_pick_in_progress {
state.cherry_pick_in_progress = true;
state.cherry_pick_head_oid = None;
} else if !is_pick && !state.revert_in_progress {
state.revert_in_progress = true;
state.revert_head_oid = None;
}
}
if get_detached_from {
if let HeadState::Detached { oid } = head {
if let Some((label, at)) = wt_status_get_detached_from(git_dir, *oid) {
state.detached_from = Some(label);
state.detached_at = at;
}
}
}
Ok(state)
}
pub fn split_commit_in_progress(git_dir: &Path, head: &HeadState) -> bool {
let HeadState::Detached { oid: head_oid } = head else {
return false;
};
let Some(amend_line) = read_trimmed_line(&git_dir.join("rebase-merge/amend")) else {
return false;
};
let Some(orig_line) = read_trimmed_line(&git_dir.join("rebase-merge/orig-head")) else {
return false;
};
let Ok(amend_oid) = ObjectId::from_hex(amend_line.trim()) else {
return false;
};
let Ok(orig_head_oid) = ObjectId::from_hex(orig_line.trim()) else {
return false;
};
if amend_line == orig_line {
head_oid != &amend_oid
} else if let Ok(Some(cur_orig)) = read_orig_head(git_dir) {
cur_orig != orig_head_oid
} else {
false
}
}
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_oid_head_file_optional(&git_dir.join("CHERRY_PICK_HEAD"))
}
pub fn read_revert_head(git_dir: &Path) -> Result<Option<ObjectId>> {
read_oid_head_file_optional(&git_dir.join("REVERT_HEAD"))
}
fn read_oid_head_file_optional(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(ObjectId::from_hex(trimmed).ok())
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(Error::Io(e)),
}
}
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)
}