mod transport;
use crate::error::{Result, SyncError};
use chrono::Local;
use git2::{BranchType, MergeOptions, Oid, Repository, Status, StatusOptions};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::{debug, error, info, warn};
pub use transport::{CommandGitTransport, CommitOutcome, GitTransport};
pub const FALLBACK_BRANCH_PREFIX: &str = "git-sync/";
#[derive(Debug, Clone)]
pub struct SyncConfig {
pub sync_new_files: bool,
pub skip_hooks: bool,
pub commit_message: Option<String>,
pub remote_name: String,
pub branch_name: String,
pub conflict_branch: bool,
pub target_branch: Option<String>,
}
impl Default for SyncConfig {
fn default() -> Self {
Self {
sync_new_files: true, skip_hooks: false,
commit_message: None,
remote_name: "origin".to_string(),
branch_name: "main".to_string(),
conflict_branch: false,
target_branch: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum RepositoryState {
Clean,
Dirty,
Rebasing,
Merging,
CherryPicking,
Bisecting,
ApplyingPatches,
Reverting,
DetachedHead,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SyncState {
Equal,
Ahead(usize),
Behind(usize),
Diverged { ahead: usize, behind: usize },
NoUpstream,
}
#[derive(Debug, Clone, PartialEq)]
pub enum UnhandledFileState {
Conflicted { path: String },
}
#[derive(Debug, Clone, Default)]
pub struct FallbackState {
pub last_checked_target_oid: Option<Oid>,
}
pub struct RepositorySynchronizer {
repo: Repository,
config: SyncConfig,
_repo_path: PathBuf,
fallback_state: FallbackState,
transport: Arc<dyn GitTransport>,
}
impl RepositorySynchronizer {
pub fn new(repo_path: impl AsRef<Path>, config: SyncConfig) -> Result<Self> {
Self::new_with_transport(repo_path, config, Arc::new(CommandGitTransport))
}
pub fn new_with_transport(
repo_path: impl AsRef<Path>,
config: SyncConfig,
transport: Arc<dyn GitTransport>,
) -> Result<Self> {
let repo_path = repo_path.as_ref().to_path_buf();
let repo = Repository::open(&repo_path).map_err(|_| SyncError::NotARepository {
path: repo_path.display().to_string(),
})?;
Ok(Self {
repo,
config,
_repo_path: repo_path,
fallback_state: FallbackState::default(),
transport,
})
}
pub fn new_with_detected_branch(
repo_path: impl AsRef<Path>,
config: SyncConfig,
) -> Result<Self> {
Self::new_with_detected_branch_and_transport(
repo_path,
config,
Arc::new(CommandGitTransport),
)
}
pub fn new_with_detected_branch_and_transport(
repo_path: impl AsRef<Path>,
mut config: SyncConfig,
transport: Arc<dyn GitTransport>,
) -> Result<Self> {
let repo_path = repo_path.as_ref().to_path_buf();
let repo = Repository::open(&repo_path).map_err(|_| SyncError::NotARepository {
path: repo_path.display().to_string(),
})?;
if let Ok(head) = repo.head() {
if head.is_branch() {
if let Some(branch_name) = head.shorthand() {
config.branch_name = branch_name.to_string();
}
}
}
Ok(Self {
repo,
config,
_repo_path: repo_path,
fallback_state: FallbackState::default(),
transport,
})
}
pub fn get_repository_state(&self) -> Result<RepositoryState> {
match self.repo.head_detached() {
Ok(true) => return Ok(RepositoryState::DetachedHead),
Ok(false) => {}
Err(e) if e.code() == git2::ErrorCode::UnbornBranch => {}
Err(e) => return Err(e.into()),
}
let state = self.repo.state();
match state {
git2::RepositoryState::Clean => {
let mut status_opts = StatusOptions::new();
status_opts.include_untracked(true);
let statuses = self.repo.statuses(Some(&mut status_opts))?;
if statuses.is_empty() {
Ok(RepositoryState::Clean)
} else {
Ok(RepositoryState::Dirty)
}
}
git2::RepositoryState::Merge => Ok(RepositoryState::Merging),
git2::RepositoryState::Rebase
| git2::RepositoryState::RebaseInteractive
| git2::RepositoryState::RebaseMerge => Ok(RepositoryState::Rebasing),
git2::RepositoryState::CherryPick | git2::RepositoryState::CherryPickSequence => {
Ok(RepositoryState::CherryPicking)
}
git2::RepositoryState::Revert | git2::RepositoryState::RevertSequence => {
Ok(RepositoryState::Reverting)
}
git2::RepositoryState::Bisect => Ok(RepositoryState::Bisecting),
git2::RepositoryState::ApplyMailbox | git2::RepositoryState::ApplyMailboxOrRebase => {
Ok(RepositoryState::ApplyingPatches)
}
}
}
pub fn has_local_changes(&self) -> Result<bool> {
let mut status_opts = StatusOptions::new();
status_opts.include_untracked(self.config.sync_new_files);
let statuses = self.repo.statuses(Some(&mut status_opts))?;
for entry in statuses.iter() {
let status = entry.status();
let tracked_or_staged_changes = Status::WT_MODIFIED
| Status::WT_DELETED
| Status::WT_RENAMED
| Status::WT_TYPECHANGE
| Status::INDEX_MODIFIED
| Status::INDEX_DELETED
| Status::INDEX_RENAMED
| Status::INDEX_TYPECHANGE
| Status::INDEX_NEW;
if self.config.sync_new_files {
if status.intersects(tracked_or_staged_changes | Status::WT_NEW) {
return Ok(true);
}
} else {
if status.intersects(tracked_or_staged_changes) {
return Ok(true);
}
}
}
Ok(false)
}
pub fn check_unhandled_files(&self) -> Result<Option<UnhandledFileState>> {
let mut status_opts = StatusOptions::new();
status_opts.include_untracked(true);
let statuses = self.repo.statuses(Some(&mut status_opts))?;
for entry in statuses.iter() {
let status = entry.status();
let path = entry.path().unwrap_or("<unknown>").to_string();
if status.is_conflicted() {
return Ok(Some(UnhandledFileState::Conflicted { path }));
}
}
Ok(None)
}
pub fn get_current_branch(&self) -> Result<String> {
let head = match self.repo.head() {
Ok(head) => head,
Err(e) if e.code() == git2::ErrorCode::UnbornBranch => {
if let Some(branch) = self.unborn_head_branch_name()? {
return Ok(branch);
}
if !self.config.branch_name.is_empty() {
return Ok(self.config.branch_name.clone());
}
return Err(SyncError::Other(
"Repository HEAD is unborn and branch name could not be determined".to_string(),
));
}
Err(e) => return Err(e.into()),
};
if !head.is_branch() {
return Err(SyncError::DetachedHead);
}
let branch_name = head
.shorthand()
.ok_or_else(|| SyncError::Other("Could not get branch name".to_string()))?;
Ok(branch_name.to_string())
}
pub fn get_sync_state(&self) -> Result<SyncState> {
let branch_name = self.get_current_branch()?;
let local_branch = self.repo.find_branch(&branch_name, BranchType::Local)?;
let upstream = match local_branch.upstream() {
Ok(upstream) => upstream,
Err(_) => return Ok(SyncState::NoUpstream),
};
let local_oid = local_branch
.get()
.target()
.ok_or_else(|| SyncError::Other("Could not get local branch OID".to_string()))?;
let upstream_oid = upstream
.get()
.target()
.ok_or_else(|| SyncError::Other("Could not get upstream branch OID".to_string()))?;
if local_oid == upstream_oid {
return Ok(SyncState::Equal);
}
let (ahead, behind) = self.repo.graph_ahead_behind(local_oid, upstream_oid)?;
match (ahead, behind) {
(0, 0) => Ok(SyncState::Equal),
(a, 0) if a > 0 => Ok(SyncState::Ahead(a)),
(0, b) if b > 0 => Ok(SyncState::Behind(b)),
(a, b) if a > 0 && b > 0 => Ok(SyncState::Diverged {
ahead: a,
behind: b,
}),
_ => Ok(SyncState::Equal),
}
}
pub fn auto_commit(&self) -> Result<()> {
info!("Auto-committing local changes");
let mut index = self.repo.index()?;
if self.config.sync_new_files {
let repo_root = self._repo_path.clone();
let mut nested_repo_prefixes: Vec<String> = Vec::new();
let mut cb = |path: &Path, _matched_spec: &[u8]| -> i32 {
let path_s = path.to_string_lossy();
if nested_repo_prefixes.iter().any(|p| path_s.starts_with(p)) {
return 1;
}
if path_s.contains("/.git/") || path_s.ends_with("/.git") {
return 1;
}
if path_s.ends_with('/') {
let no_slash = path_s.trim_end_matches('/');
if repo_root.join(no_slash).join(".git").exists() {
nested_repo_prefixes.push(format!("{}/", no_slash));
}
return 1;
}
0
};
index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, Some(&mut cb))?;
} else {
index.update_all(["*"].iter(), None)?;
}
index.write()?;
let message = if let Some(ref msg) = self.config.commit_message {
msg.replace("{hostname}", &hostname::get()?.to_string_lossy())
.replace(
"{timestamp}",
&Local::now().format("%Y-%m-%d %I:%M:%S %p %Z").to_string(),
)
} else {
format!(
"changes from {} on {}",
hostname::get()?.to_string_lossy(),
Local::now().format("%Y-%m-%d %I:%M:%S %p %Z")
)
};
match self
.transport
.commit(&self._repo_path, &message, self.config.skip_hooks)?
{
CommitOutcome::Created => info!("Created auto-commit: {}", message),
CommitOutcome::NoChanges => {
debug!("No changes to commit");
}
}
Ok(())
}
pub fn fetch_branch(&self, branch: &str) -> Result<()> {
info!(
"Fetching branch {} from remote: {}",
branch, self.config.remote_name
);
if let Err(e) =
self.transport
.fetch_branch(&self._repo_path, &self.config.remote_name, branch)
{
error!("Git fetch failed: {}", e);
return Err(e);
}
info!(
"Fetch completed successfully for branch {} from remote: {}",
branch, self.config.remote_name
);
Ok(())
}
pub fn fetch(&self) -> Result<()> {
let current_branch = self.get_current_branch()?;
self.fetch_branch(¤t_branch)?;
if self.config.conflict_branch {
if let Ok(target) = self.get_target_branch() {
if target != current_branch {
let _ = self.fetch_branch(&target);
}
}
}
Ok(())
}
pub fn push(&self) -> Result<()> {
info!("Pushing to remote: {}", self.config.remote_name);
let current_branch = self.get_current_branch()?;
let refspec = format!("{}:{}", current_branch, current_branch);
self.transport
.push_refspec(&self._repo_path, &self.config.remote_name, &refspec)?;
info!(
"Push completed successfully to remote: {}",
self.config.remote_name
);
Ok(())
}
pub fn fast_forward_merge(&self) -> Result<()> {
info!("Performing fast-forward merge");
let branch_name = self.get_current_branch()?;
let local_branch = self.repo.find_branch(&branch_name, BranchType::Local)?;
let upstream = local_branch.upstream()?;
let upstream_oid = upstream
.get()
.target()
.ok_or_else(|| SyncError::Other("Could not get upstream OID".to_string()))?;
let mut reference = self.repo.head()?;
reference.set_target(upstream_oid, "fast-forward merge")?;
let object = self.repo.find_object(upstream_oid, None)?;
let mut checkout_builder = git2::build::CheckoutBuilder::new();
checkout_builder.force(); self.repo
.checkout_tree(&object, Some(&mut checkout_builder))?;
self.repo.set_head(&format!("refs/heads/{}", branch_name))?;
info!("Fast-forward merge completed - working tree updated");
Ok(())
}
pub fn rebase(&self) -> Result<()> {
info!("Performing rebase");
let branch_name = self.get_current_branch()?;
let local_branch = self.repo.find_branch(&branch_name, BranchType::Local)?;
let upstream = local_branch.upstream()?;
let upstream_commit = upstream.get().peel_to_commit()?;
let local_commit = local_branch.get().peel_to_commit()?;
let merge_base = self
.repo
.merge_base(local_commit.id(), upstream_commit.id())?;
let _merge_base_commit = self.repo.find_commit(merge_base)?;
let sig = self.repo.signature()?;
let local_annotated = self
.repo
.reference_to_annotated_commit(local_branch.get())?;
let upstream_annotated = self.repo.reference_to_annotated_commit(upstream.get())?;
let mut rebase = self.repo.rebase(
Some(&local_annotated),
Some(&upstream_annotated),
None,
None,
)?;
while let Some(operation) = rebase.next() {
let _operation = operation?;
if self.repo.index()?.has_conflicts() {
warn!("Conflicts detected during rebase");
rebase.abort()?;
if self.config.conflict_branch {
return self.handle_conflict_with_fallback();
}
return Err(SyncError::ManualInterventionRequired {
reason: "Rebase conflicts detected. Please resolve manually.".to_string(),
});
}
rebase.commit(None, &sig, None)?;
}
rebase.finish(Some(&sig))?;
let head = self.repo.head()?;
let head_commit = head.peel_to_commit()?;
let mut checkout_builder = git2::build::CheckoutBuilder::new();
checkout_builder.force();
self.repo
.checkout_tree(head_commit.as_object(), Some(&mut checkout_builder))?;
info!("Rebase completed successfully - working tree updated");
Ok(())
}
pub fn detect_default_branch(&self) -> Result<String> {
if let Ok(reference) = self.repo.find_reference("refs/remotes/origin/HEAD") {
if let Ok(resolved) = reference.resolve() {
if let Some(name) = resolved.shorthand() {
if let Some(branch) = name.strip_prefix("origin/") {
debug!("Detected default branch from origin/HEAD: {}", branch);
return Ok(branch.to_string());
}
}
}
}
if self.repo.find_branch("main", BranchType::Local).is_ok()
|| self.repo.find_reference("refs/remotes/origin/main").is_ok()
{
debug!("Falling back to 'main' as default branch");
return Ok("main".to_string());
}
if self.repo.find_branch("master", BranchType::Local).is_ok()
|| self
.repo
.find_reference("refs/remotes/origin/master")
.is_ok()
{
debug!("Falling back to 'master' as default branch");
return Ok("master".to_string());
}
self.get_current_branch()
}
pub fn get_target_branch(&self) -> Result<String> {
if let Some(ref target) = self.config.target_branch {
if !target.is_empty() {
return Ok(target.clone());
}
}
self.detect_default_branch()
}
pub fn is_on_fallback_branch(&self) -> Result<bool> {
let current = self.get_current_branch()?;
Ok(current.starts_with(FALLBACK_BRANCH_PREFIX))
}
fn generate_fallback_branch_name() -> String {
let hostname = hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "unknown".to_string());
let timestamp = Local::now().format("%Y-%m-%d-%H%M%S");
format!("{}{}-{}", FALLBACK_BRANCH_PREFIX, hostname, timestamp)
}
pub fn create_fallback_branch(&self) -> Result<String> {
let branch_name = Self::generate_fallback_branch_name();
info!("Creating fallback branch: {}", branch_name);
let head_commit = self.repo.head()?.peel_to_commit()?;
self.repo
.branch(&branch_name, &head_commit, false)
.map_err(|e| SyncError::Other(format!("Failed to create fallback branch: {}", e)))?;
let refname = format!("refs/heads/{}", branch_name);
self.repo.set_head(&refname)?;
let mut checkout_builder = git2::build::CheckoutBuilder::new();
checkout_builder.force();
self.repo
.checkout_head(Some(&mut checkout_builder))
.map_err(|e| SyncError::Other(format!("Failed to checkout fallback branch: {}", e)))?;
info!("Switched to fallback branch: {}", branch_name);
Ok(branch_name)
}
pub fn push_branch(&self, branch_name: &str) -> Result<()> {
info!("Pushing branch {} to remote", branch_name);
self.transport.push_branch_upstream(
&self._repo_path,
&self.config.remote_name,
branch_name,
)?;
info!("Successfully pushed branch {} to remote", branch_name);
Ok(())
}
pub fn can_merge_cleanly(&self, target_branch: &str) -> Result<bool> {
let target_ref = format!("refs/remotes/{}/{}", self.config.remote_name, target_branch);
let target_reference = self.repo.find_reference(&target_ref).map_err(|e| {
SyncError::Other(format!(
"Failed to find target branch {}: {}",
target_branch, e
))
})?;
let target_commit = target_reference.peel_to_commit()?;
let head_commit = self.repo.head()?.peel_to_commit()?;
if self
.repo
.graph_descendant_of(target_commit.id(), head_commit.id())?
{
debug!(
"Target branch {} is descendant of current HEAD, clean merge possible",
target_branch
);
return Ok(true);
}
let merge_opts = MergeOptions::new();
let index = self
.repo
.merge_commits(&head_commit, &target_commit, Some(&merge_opts))
.map_err(|e| SyncError::Other(format!("Failed to perform merge check: {}", e)))?;
let has_conflicts = index.has_conflicts();
debug!("In-memory merge check: has_conflicts={}", has_conflicts);
Ok(!has_conflicts)
}
fn get_target_branch_oid(&self, target_branch: &str) -> Result<Oid> {
let target_ref = format!("refs/remotes/{}/{}", self.config.remote_name, target_branch);
let reference = self.repo.find_reference(&target_ref)?;
reference
.target()
.ok_or_else(|| SyncError::Other("Target branch has no OID".to_string()))
}
pub fn try_return_to_target(&mut self) -> Result<bool> {
if !self.is_on_fallback_branch()? {
return Ok(false);
}
let target_branch = self.get_target_branch()?;
info!(
"On fallback branch, checking if we can return to {}",
target_branch
);
let target_oid = match self.get_target_branch_oid(&target_branch) {
Ok(oid) => oid,
Err(e) => {
warn!("Could not find target branch {}: {}", target_branch, e);
return Ok(false);
}
};
if let Some(last_checked) = self.fallback_state.last_checked_target_oid {
if last_checked == target_oid {
debug!(
"Target branch {} hasn't changed since last check, skipping merge check",
target_branch
);
return Ok(false);
}
}
if !self.can_merge_cleanly(&target_branch)? {
info!(
"Cannot cleanly merge {} into current branch, staying on fallback",
target_branch
);
self.fallback_state.last_checked_target_oid = Some(target_oid);
return Ok(false);
}
info!(
"Clean merge possible, returning to target branch {}",
target_branch
);
let current_branch = self.get_current_branch()?;
let current_oid = self
.repo
.head()?
.target()
.ok_or_else(|| SyncError::Other("Current HEAD has no OID".to_string()))?;
let merge_base = self.repo.merge_base(current_oid, target_oid)?;
let (ahead, _) = self.repo.graph_ahead_behind(current_oid, merge_base)?;
let has_commits_to_rebase = ahead > 0;
let target_ref = format!("refs/heads/{}", target_branch);
let remote_target_ref =
format!("refs/remotes/{}/{}", self.config.remote_name, target_branch);
let remote_target = self.repo.find_reference(&remote_target_ref)?;
let remote_target_oid = remote_target
.target()
.ok_or_else(|| SyncError::Other("Remote target has no OID".to_string()))?;
if self.repo.find_reference(&target_ref).is_ok() {
self.repo.reference(
&target_ref,
remote_target_oid,
true,
"git-sync: updating target branch before return",
)?;
} else {
let remote_commit = self.repo.find_commit(remote_target_oid)?;
self.repo.branch(&target_branch, &remote_commit, false)?;
}
self.repo.set_head(&target_ref)?;
let mut checkout_builder = git2::build::CheckoutBuilder::new();
checkout_builder.force();
self.repo.checkout_head(Some(&mut checkout_builder))?;
if has_commits_to_rebase {
info!(
"Rebasing {} commits from {} onto {}",
ahead, current_branch, target_branch
);
let fallback_ref = format!("refs/heads/{}", current_branch);
let fallback_reference = self.repo.find_reference(&fallback_ref)?;
let fallback_annotated = self
.repo
.reference_to_annotated_commit(&fallback_reference)?;
let target_reference = self.repo.find_reference(&target_ref)?;
let target_annotated = self.repo.reference_to_annotated_commit(&target_reference)?;
let sig = self.repo.signature()?;
let mut rebase = self.repo.rebase(
Some(&fallback_annotated),
Some(&target_annotated),
None,
None,
)?;
while let Some(operation) = rebase.next() {
let _operation = operation?;
if self.repo.index()?.has_conflicts() {
warn!("Conflicts during rebase back to target, aborting");
rebase.abort()?;
self.repo.set_head(&fallback_ref)?;
self.repo.checkout_head(Some(&mut checkout_builder))?;
self.fallback_state.last_checked_target_oid = Some(target_oid);
return Ok(false);
}
rebase.commit(None, &sig, None)?;
}
rebase.finish(Some(&sig))?;
let head = self.repo.head()?;
let head_commit = head.peel_to_commit()?;
self.repo
.checkout_tree(head_commit.as_object(), Some(&mut checkout_builder))?;
}
self.fallback_state.last_checked_target_oid = None;
info!("Successfully returned to target branch {}", target_branch);
Ok(true)
}
fn handle_conflict_with_fallback(&self) -> Result<()> {
if !self.config.conflict_branch {
return Err(SyncError::ManualInterventionRequired {
reason: "Rebase conflicts detected. Please resolve manually.".to_string(),
});
}
info!("Conflict detected with conflict_branch enabled, creating fallback branch");
let fallback_branch = self.create_fallback_branch()?;
if self.has_local_changes()? {
self.auto_commit()?;
}
self.push_branch(&fallback_branch)?;
info!(
"Switched to fallback branch {} due to conflicts. \
Will automatically return to target branch when conflicts are resolved.",
fallback_branch
);
Ok(())
}
pub fn sync(&mut self, check_only: bool) -> Result<()> {
info!("Starting sync operation (check_only: {})", check_only);
let repo_state = self.get_repository_state()?;
match repo_state {
RepositoryState::Clean | RepositoryState::Dirty => {
}
RepositoryState::DetachedHead => {
return Err(SyncError::DetachedHead);
}
_ => {
return Err(SyncError::UnsafeRepositoryState {
state: format!("{:?}", repo_state),
});
}
}
if let Some(unhandled) = self.check_unhandled_files()? {
let reason = match unhandled {
UnhandledFileState::Conflicted { path } => format!("Conflicted file: {}", path),
};
return Err(SyncError::ManualInterventionRequired { reason });
}
if check_only {
info!("Check passed, sync can proceed");
return Ok(());
}
if self.is_head_unborn()? {
info!("Repository HEAD is unborn; attempting initial publish");
if self.has_local_changes()? {
self.auto_commit()?;
let branch = self.get_current_branch()?;
self.push_branch(&branch)?;
} else {
info!("HEAD is unborn and there are no local changes to publish");
}
return Ok(());
}
self.fetch()?;
if self.config.conflict_branch
&& self.is_on_fallback_branch()?
&& self.try_return_to_target()?
{
info!("Returned to target branch, continuing with normal sync");
}
if self.has_local_changes()? {
self.auto_commit()?;
}
let sync_state = self.get_sync_state()?;
match sync_state {
SyncState::Equal => {
info!("Already in sync");
}
SyncState::Ahead(_) => {
info!("Local is ahead, pushing");
self.push()?;
}
SyncState::Behind(_) => {
info!("Local is behind, fast-forwarding");
self.fast_forward_merge()?;
}
SyncState::Diverged { .. } => {
info!("Branches have diverged, rebasing");
self.rebase()?;
self.push()?;
}
SyncState::NoUpstream => {
if self.is_on_fallback_branch()? {
info!("Fallback branch has no upstream, pushing");
let branch = self.get_current_branch()?;
self.push_branch(&branch)?;
} else {
let branch = self
.get_current_branch()
.unwrap_or_else(|_| "<unknown>".into());
return Err(SyncError::NoRemoteConfigured { branch });
}
}
}
let final_state = self.get_sync_state()?;
if final_state != SyncState::Equal && final_state != SyncState::NoUpstream {
warn!(
"Sync completed but repository is not in sync: {:?}",
final_state
);
return Err(SyncError::Other(
"Sync completed but repository is not in sync".to_string(),
));
}
info!("Sync completed successfully");
Ok(())
}
fn is_head_unborn(&self) -> Result<bool> {
match self.repo.head() {
Ok(head) => match head.peel_to_commit() {
Ok(_) => Ok(false),
Err(e) if e.code() == git2::ErrorCode::UnbornBranch => Ok(true),
Err(e) => Err(e.into()),
},
Err(e) if e.code() == git2::ErrorCode::UnbornBranch => Ok(true),
Err(e) => Err(e.into()),
}
}
fn unborn_head_branch_name(&self) -> Result<Option<String>> {
let head_path = self.repo.path().join("HEAD");
let head_contents = fs::read_to_string(head_path)?;
Ok(head_contents
.trim()
.strip_prefix("ref: refs/heads/")
.map(|s| s.to_string()))
}
}