use std::fs;
use std::path::Path;
use crate::check_ref_format::{check_refname_format, RefNameOptions};
use crate::error::{Error, Result};
use crate::objects::ObjectId;
use crate::reflog;
use crate::refs::store::RefStoreError;
use crate::repo::Repository;
#[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",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NextStep {
ResolveConflicts,
StageResolved,
Commit,
Abort(InProgressOperation),
Continue(InProgressOperation),
Skip(InProgressOperation),
}
impl NextStep {
#[must_use]
pub fn grit_command(&self) -> Option<String> {
match self {
Self::ResolveConflicts => Some("grit diff".to_owned()),
Self::StageResolved => Some("grit add .".to_owned()),
Self::Commit => Some("grit commit \"message\"".to_owned()),
Self::Continue(InProgressOperation::Merge) => Some("grit merge --continue".to_owned()),
Self::Abort(InProgressOperation::Merge) => Some("grit merge --abort".to_owned()),
Self::Abort(InProgressOperation::CherryPick) => Some("grit pick --abort".to_owned()),
Self::Abort(InProgressOperation::Revert) => Some("grit revert --abort".to_owned()),
Self::Continue(InProgressOperation::CherryPick) => {
Some("grit pick --continue".to_owned())
}
Self::Continue(InProgressOperation::Revert) => {
Some("grit revert --continue".to_owned())
}
Self::Skip(InProgressOperation::CherryPick) => Some("grit pick --skip".to_owned()),
Self::Skip(InProgressOperation::Revert) => Some("grit revert --skip".to_owned()),
Self::Continue(InProgressOperation::Rebase) => {
Some("grit rebase --continue".to_owned())
}
Self::Abort(InProgressOperation::Rebase) => Some("grit rebase --abort".to_owned()),
Self::Skip(InProgressOperation::Rebase) => Some("grit rebase --skip".to_owned()),
Self::Abort(
InProgressOperation::RebaseInteractive
| InProgressOperation::Am
| InProgressOperation::Bisect,
)
| Self::Continue(
InProgressOperation::RebaseInteractive
| InProgressOperation::Am
| InProgressOperation::Bisect,
)
| Self::Skip(InProgressOperation::Merge)
| Self::Skip(
InProgressOperation::RebaseInteractive
| InProgressOperation::Am
| InProgressOperation::Bisect,
) => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Operation {
pub kind: InProgressOperation,
pub next_steps: Vec<NextStep>,
pub alternatives: Vec<NextStep>,
}
#[must_use]
pub fn primary_in_progress_operation(state: &WtStatusState) -> Option<InProgressOperation> {
if state.merge_in_progress {
return Some(InProgressOperation::Merge);
}
if state.rebase_interactive_in_progress {
return Some(InProgressOperation::RebaseInteractive);
}
if state.rebase_in_progress {
return Some(InProgressOperation::Rebase);
}
if state.am_in_progress {
return Some(InProgressOperation::Am);
}
if state.cherry_pick_in_progress {
return Some(InProgressOperation::CherryPick);
}
if state.revert_in_progress {
return Some(InProgressOperation::Revert);
}
if state.bisect_in_progress {
return Some(InProgressOperation::Bisect);
}
None
}
#[must_use]
pub fn operation_next_steps_empty(kind: InProgressOperation) -> Vec<NextStep> {
match kind {
InProgressOperation::CherryPick | InProgressOperation::Revert => {
vec![NextStep::Skip(kind)]
}
other => operation_next_steps(other, false),
}
}
#[must_use]
pub fn operation_next_steps(
kind: InProgressOperation,
has_unmerged_conflicts: bool,
) -> Vec<NextStep> {
match kind {
InProgressOperation::Merge => {
if has_unmerged_conflicts {
vec![NextStep::ResolveConflicts, NextStep::StageResolved]
} else {
vec![NextStep::Continue(InProgressOperation::Merge)]
}
}
InProgressOperation::CherryPick | InProgressOperation::Revert => {
if has_unmerged_conflicts {
vec![
NextStep::ResolveConflicts,
NextStep::StageResolved,
NextStep::Continue(kind),
]
} else {
vec![NextStep::Continue(kind)]
}
}
InProgressOperation::Rebase => {
if has_unmerged_conflicts {
vec![
NextStep::ResolveConflicts,
NextStep::StageResolved,
NextStep::Continue(InProgressOperation::Rebase),
]
} else {
vec![NextStep::Continue(InProgressOperation::Rebase)]
}
}
InProgressOperation::RebaseInteractive
| InProgressOperation::Am
| InProgressOperation::Bisect => vec![],
}
}
#[must_use]
pub fn operation_alternatives(kind: InProgressOperation) -> Vec<NextStep> {
match kind {
InProgressOperation::Merge => vec![NextStep::Abort(InProgressOperation::Merge)],
InProgressOperation::CherryPick => vec![
NextStep::Abort(InProgressOperation::CherryPick),
NextStep::Skip(InProgressOperation::CherryPick),
],
InProgressOperation::Revert => vec![
NextStep::Abort(InProgressOperation::Revert),
NextStep::Skip(InProgressOperation::Revert),
],
InProgressOperation::Rebase => vec![
NextStep::Abort(InProgressOperation::Rebase),
NextStep::Skip(InProgressOperation::Rebase),
],
InProgressOperation::RebaseInteractive
| InProgressOperation::Am
| InProgressOperation::Bisect => vec![],
}
}
#[must_use]
pub fn status_operation(state: &WtStatusState, has_unmerged_conflicts: bool) -> Option<Operation> {
let kind = if state.merge_in_progress {
InProgressOperation::Merge
} else if state.grit_rebase_in_progress {
InProgressOperation::Rebase
} else if let Some(kind) = state.grit_replay_kind {
kind
} else if state.cherry_pick_in_progress {
InProgressOperation::CherryPick
} else if state.revert_in_progress {
InProgressOperation::Revert
} else {
return None;
};
let next_steps = if state.grit_replay_empty_stop && !has_unmerged_conflicts {
operation_next_steps_empty(kind)
} else {
operation_next_steps(kind, has_unmerged_conflicts)
};
Some(Operation {
kind,
next_steps,
alternatives: operation_alternatives(kind),
})
}
#[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> {
resolve_head_impl(git_dir, None)
}
pub fn resolve_head_for_repository(repo: &crate::repo::Repository) -> Result<HeadState> {
resolve_head_impl(repo.git_dir(), Some(repo))
}
fn resolve_head_impl(git_dir: &Path, repo: Option<&crate::repo::Repository>) -> Result<HeadState> {
let resolve_named = |refname: &str| -> Result<Option<crate::objects::ObjectId>> {
if let Some(r) = repo {
return r.references().resolve_if_exists(refname);
}
match crate::refs::resolve_ref(git_dir, refname) {
Ok(oid) => Ok(Some(oid)),
Err(Error::RefStore(RefStoreError::NotFound { .. })) => Ok(None),
Err(e) => Err(e),
}
};
let read_symbolic = |refname: &str| -> Result<Option<String>> {
if let Some(r) = repo {
crate::refs::read_symbolic_ref_for_repository(r, refname)
} else {
crate::refs::read_symbolic_ref(git_dir, refname)
}
};
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() || 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" {
if let Ok(Some(target)) = read_symbolic("HEAD") {
target
} else if let Ok(Some(oid)) = resolve_named("HEAD") {
return Ok(HeadState::Detached { oid });
} else {
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()
};
if check_refname_format(&refname, &RefNameOptions::default()).is_err() {
return Ok(HeadState::Invalid);
}
let short_name = refname
.strip_prefix("refs/heads/")
.unwrap_or(&refname)
.to_owned();
let oid = match resolve_named(&refname)? {
Some(oid) => Some(oid),
None if refname.starts_with("refs/heads/") => None,
None => return Ok(HeadState::Invalid),
};
Ok(HeadState::Branch {
refname,
short_name,
oid,
})
} else {
match ObjectId::from_hex(trimmed) {
Ok(oid) => Ok(HeadState::Detached { oid }),
Err(_) => Ok(HeadState::Invalid),
}
}
}
fn grit_rebase_plan_active(rebase_merge: &Path) -> bool {
rebase_merge.join("grit-plan").is_file()
}
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() && !grit_rebase_plan_active(&rebase_merge) {
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,
pub grit_replay_sequencer: bool,
pub grit_replay_kind: Option<InProgressOperation>,
pub grit_replay_empty_stop: bool,
pub grit_rebase_in_progress: 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 ref_name_exists(git_dir: &Path, refname: &str) -> bool {
git_dir.join(refname).exists()
|| crate::refs::packed_refs_entry_exists(git_dir, refname).unwrap_or(false)
}
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 candidate.starts_with("refs/tags/") && ref_name_exists(git_dir, &candidate) {
return strip_ref_for_display(&candidate);
}
if let Ok(oid) = crate::refs::resolve_ref(git_dir, &candidate) {
if oid == noid {
return strip_ref_for_display(&candidate);
}
}
}
if ObjectId::is_hex_len(target.len()) {
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() <= noid.algo().hex_len()
&& 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() {
let grit_plan = grit_rebase_plan_active(&merge);
if merge.join("interactive").exists() && !grit_plan {
state.rebase_interactive_in_progress = true;
} else {
state.rebase_in_progress = true;
}
state.grit_rebase_in_progress = grit_plan;
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 match cmd {
"pick" | "p" => Some(true),
"revert" | "r" => Some(false),
_ => None,
};
}
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;
}
}
}
state.grit_replay_sequencer = git_dir.join("sequencer").join("grit-opts").is_file();
if state.grit_replay_sequencer {
if let Some((kind, empty_stop)) = read_grit_replay_status_hints(git_dir) {
state.grit_replay_kind = Some(kind);
state.grit_replay_empty_stop = empty_stop;
match kind {
InProgressOperation::CherryPick => {
state.revert_in_progress = false;
state.revert_head_oid = None;
if !state.cherry_pick_in_progress {
state.cherry_pick_in_progress = true;
}
}
InProgressOperation::Revert => {
state.cherry_pick_in_progress = false;
state.cherry_pick_head_oid = None;
if !state.revert_in_progress {
state.revert_in_progress = true;
}
}
_ => {}
}
}
}
Ok(state)
}
fn read_grit_replay_status_hints(git_dir: &Path) -> Option<(InProgressOperation, bool)> {
let path = git_dir.join("sequencer").join("grit-opts");
let content = fs::read_to_string(&path).ok()?;
let mut direction = None;
let mut empty = "stop";
for line in content.lines() {
let line = line.trim();
let Some((key, value)) = line.split_once('=') else {
continue;
};
match key.trim() {
"direction" => {
direction = Some(if value.trim() == "revert" {
InProgressOperation::Revert
} else {
InProgressOperation::CherryPick
});
}
"empty" => empty = value.trim(),
_ => {}
}
}
let kind = direction?;
let empty_stop = empty == "stop"
&& (read_cherry_pick_head(git_dir).ok().flatten().is_some()
|| read_revert_head(git_dir).ok().flatten().is_some());
Some((kind, empty_stop))
}
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 clear_merge_state(git_dir: &Path) -> Result<()> {
for name in merge_completion_sentinels() {
let path = git_dir.join(name);
if path.exists() {
fs::remove_file(&path).map_err(Error::Io)?;
}
}
Ok(())
}
#[must_use]
pub fn merge_completion_sentinels() -> &'static [&'static str] {
&[
"MERGE_HEAD",
"MERGE_MSG",
"MERGE_MODE",
"MERGE_LOG",
"AUTO_MERGE",
"SQUASH_MSG",
"MERGE_AUTOSTASH",
]
}
pub fn finish_merge_state(repo: &Repository) -> Result<bool> {
let autostash_conflicts = crate::porcelain::stash::apply_merge_autostash(repo)?;
let _ = crate::rerere::finish_merge_rerere(repo)?;
clear_merge_state(repo.git_dir())?;
Ok(autostash_conflicts)
}
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)),
}
}
#[cfg(test)]
mod merge_sentinel_tests {
#![allow(clippy::unwrap_used)]
use super::{clear_merge_state, merge_completion_sentinels};
use std::fs;
use tempfile::TempDir;
#[test]
fn clear_merge_state_removes_every_completion_sentinel() {
let tmp = TempDir::new().unwrap();
let git = tmp.path().join(".git");
fs::create_dir_all(&git).unwrap();
for name in merge_completion_sentinels() {
fs::write(git.join(name), b"x").unwrap();
}
clear_merge_state(&git).unwrap();
for name in merge_completion_sentinels() {
assert!(!git.join(name).exists(), "expected {name} to be removed");
}
}
}
#[cfg(test)]
mod head_state_tests {
#![allow(clippy::unwrap_used)]
use super::{resolve_head, resolve_head_for_repository, HeadState};
use crate::repo::init_repository;
use crate::RefStorageFormat;
use tempfile::TempDir;
fn unborn_main() -> HeadState {
HeadState::Branch {
refname: "refs/heads/main".to_owned(),
short_name: "main".to_owned(),
oid: None,
}
}
#[test]
fn unborn_branch_resolves_without_oid_on_every_backend() {
for format in [RefStorageFormat::Files, RefStorageFormat::Reftable] {
let tmp = TempDir::new().unwrap();
let repo = init_repository(tmp.path(), false, "main", None, format).unwrap();
assert_eq!(resolve_head(repo.git_dir()).unwrap(), unborn_main());
assert_eq!(resolve_head_for_repository(&repo).unwrap(), unborn_main());
}
}
#[test]
fn head_symref_to_missing_non_branch_is_invalid() {
let tmp = TempDir::new().unwrap();
let repo =
init_repository(tmp.path(), false, "main", None, RefStorageFormat::Files).unwrap();
std::fs::write(repo.git_dir().join("HEAD"), "ref: refs/tags/missing\n").unwrap();
assert_eq!(resolve_head(repo.git_dir()).unwrap(), HeadState::Invalid);
assert_eq!(
resolve_head_for_repository(&repo).unwrap(),
HeadState::Invalid
);
}
}