use crate::config::SubmoduleConfig;
use git2::Repository;
use std::path::{Path, PathBuf};
mod change_summary;
mod submodule;
#[cfg(test)]
mod test_support;
#[cfg(test)]
mod tests_basic;
#[cfg(test)]
mod tests_refs;
#[cfg(test)]
mod tests_submodule;
mod worktree;
use change_summary::{ChangeSummary, collect_change_summary};
use worktree::{collect_worktree_info, compute_ahead_behind, fetch_remote_silent};
#[derive(Clone, Debug)]
pub(crate) struct RepoStatus {
pub branch: String,
pub head_oid: Option<String>,
pub files: Vec<FileEntry>,
pub ahead: usize,
pub behind: usize,
pub is_dirty: bool,
pub worktree_info: Vec<WorktreeEntry>,
pub has_submodules: bool,
pub submodules: Vec<SubmoduleInfo>,
pub has_dirty_submodules: bool,
pub has_unpushed_submodules: bool,
pub fetch_failed: bool,
pub stashes: Vec<StashEntry>,
pub refs: RefsFingerprint,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct RefsFingerprint {
pub local: u64,
pub remote: u64,
pub tags: u64,
}
impl RepoStatus {
pub fn stash_count(&self) -> usize {
self.stashes.len()
}
}
fn graph_refs_fingerprint(repo: &Repository) -> RefsFingerprint {
use std::hash::{Hash, Hasher};
fn mix(acc: &mut u64, name: &str, oid: git2::Oid) {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
name.hash(&mut hasher);
oid.as_bytes().hash(&mut hasher);
*acc ^= hasher.finish();
}
let mut fp = RefsFingerprint::default();
let Ok(references) = repo.references() else {
return fp;
};
for reference in references {
let Ok(reference) = reference else {
continue;
};
let Ok(name) = reference.name() else {
continue;
};
if name.starts_with("refs/heads/") {
if let Some(oid) = reference.target() {
mix(&mut fp.local, name, oid);
}
} else if name.starts_with("refs/remotes/") {
if let Some(oid) = reference.target() {
mix(&mut fp.remote, name, oid);
}
} else if name.starts_with("refs/tags/") {
let oid = reference
.peel_to_commit()
.ok()
.map(|commit| commit.id())
.or_else(|| reference.target());
if let Some(oid) = oid {
mix(&mut fp.tags, name, oid);
}
}
}
fp
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct StashEntry {
pub index: usize,
pub message: String,
pub oid: String,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct FileEntry {
pub path: PathBuf,
pub status: FileStatus,
pub staged: bool,
pub unstaged: bool,
pub is_submodule: bool,
pub submodule_state: Option<SubmoduleState>,
pub submodule_warn: SubmoduleWarn,
pub submodule_head: Option<SubmoduleHead>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum FileStatus {
Modified,
Added,
Deleted,
Renamed,
Untracked,
Conflicted,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum SubmoduleState {
Modified,
Uninitialized,
Dirty,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum SubmoduleHead {
Branch(String),
Detached,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct SubmoduleWarn {
pub unpushed_commits: usize,
pub pointer_unreachable: bool,
pub needs_merge_to_default: bool,
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub(crate) struct WorktreeEntry {
pub name: String,
pub path: PathBuf,
pub branch: String,
pub ahead: usize,
pub behind: usize,
pub is_dirty: bool,
pub file_count: usize,
pub has_dirty_submodules: bool,
pub has_unpushed_submodules: bool,
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub(crate) struct SubmoduleInfo {
pub name: String,
pub path: PathBuf,
pub state: Option<SubmoduleState>,
pub head: Option<SubmoduleHead>,
pub head_oid: Option<String>,
pub workdir_oid: Option<String>,
pub warn: SubmoduleWarn,
}
pub(crate) fn query_status(
path: &Path,
sub_cfg: &SubmoduleConfig,
) -> color_eyre::Result<RepoStatus> {
query_status_inner(path, false, false, sub_cfg)
}
pub(crate) fn query_status_with_fetch(
path: &Path,
sub_cfg: &SubmoduleConfig,
) -> color_eyre::Result<RepoStatus> {
query_status_inner(path, true, true, sub_cfg)
}
fn query_status_inner(
path: &Path,
fetch: bool,
recurse_untracked_dirs: bool,
sub_cfg: &SubmoduleConfig,
) -> color_eyre::Result<RepoStatus> {
let mut repo = Repository::open(path)?;
let mut stashes: Vec<StashEntry> = Vec::new();
let _ = repo.stash_foreach(|index, message, oid| {
stashes.push(StashEntry {
index,
message: message.to_string(),
oid: oid.to_string(),
});
true
});
let (branch, head_oid) = match repo.head() {
Ok(reference) => {
let oid = reference
.target()
.or_else(|| reference.peel_to_commit().ok().map(|commit| commit.id()))
.map(|oid| oid.to_string());
(reference.shorthand().unwrap_or("HEAD").to_string(), oid)
}
Err(_) => ("(no branch)".to_string(), None),
};
let fetch_failed = if fetch {
!fetch_remote_silent(path)
} else {
false
};
let (ahead, behind) = compute_ahead_behind(&repo);
let ChangeSummary {
files,
is_dirty,
has_submodules,
submodules,
has_dirty_submodules,
has_unpushed_submodules,
} = collect_change_summary(&repo, path, recurse_untracked_dirs, sub_cfg)?;
let worktree_info = collect_worktree_info(&repo, sub_cfg);
let refs = graph_refs_fingerprint(&repo);
Ok(RepoStatus {
branch,
head_oid,
files,
ahead,
behind,
is_dirty,
worktree_info,
has_submodules,
submodules,
has_dirty_submodules,
has_unpushed_submodules,
fetch_failed,
stashes,
refs,
})
}
pub(crate) fn default_branch_name(repo: &Repository) -> Option<String> {
if let Ok(head_ref) = repo.find_reference("refs/remotes/origin/HEAD")
&& let Ok(Some(target_name)) = head_ref.symbolic_target()
&& repo.find_reference(target_name).is_ok()
&& let Some(short) = target_name.strip_prefix("refs/remotes/")
{
return Some(short.to_string());
}
for (full, short) in [
("refs/remotes/origin/main", "origin/main"),
("refs/remotes/origin/master", "origin/master"),
] {
if repo.find_reference(full).is_ok() {
return Some(short.to_string());
}
}
None
}