use crate::config::SubmoduleConfig;
use git2::{Repository, StatusOptions, SubmoduleStatus};
use std::path::{Path, PathBuf};
use super::submodule::compute_submodule_head_and_warn;
use super::{FileEntry, FileStatus, SubmoduleInfo, SubmoduleState, SubmoduleWarn};
pub(super) struct ChangeSummary {
pub(super) files: Vec<FileEntry>,
pub(super) is_dirty: bool,
pub(super) has_submodules: bool,
pub(super) submodules: Vec<SubmoduleInfo>,
pub(super) has_dirty_submodules: bool,
pub(super) has_unpushed_submodules: bool,
}
pub(super) fn collect_change_summary(
repo: &Repository,
path: &Path,
recurse_untracked_dirs: bool,
sub_cfg: &SubmoduleConfig,
) -> color_eyre::Result<ChangeSummary> {
let mut opts = StatusOptions::new();
opts.include_untracked(true)
.recurse_untracked_dirs(recurse_untracked_dirs)
.renames_head_to_index(true);
if sub_cfg.ignore_dirty {
opts.exclude_submodules(true);
}
let statuses = repo.statuses(Some(&mut opts))?;
let mut files = Vec::new();
for entry in statuses.iter() {
let s = entry.status();
let file_path = PathBuf::from(entry.path().unwrap_or(""));
let file_status = if s.is_conflicted() {
FileStatus::Conflicted
} else if s.is_index_new() || s.is_wt_new() {
if s.is_wt_new() && !s.is_index_new() {
FileStatus::Untracked
} else {
FileStatus::Added
}
} else if s.is_index_deleted() || s.is_wt_deleted() {
FileStatus::Deleted
} else if s.is_index_renamed() || s.is_wt_renamed() {
FileStatus::Renamed
} else if s.is_index_modified() || s.is_wt_modified() {
FileStatus::Modified
} else {
continue;
};
files.push(FileEntry {
path: file_path,
status: file_status,
staged: s.is_index_new()
|| s.is_index_modified()
|| s.is_index_deleted()
|| s.is_index_renamed()
|| s.is_index_typechange(),
unstaged: s.is_wt_new()
|| s.is_wt_modified()
|| s.is_wt_deleted()
|| s.is_wt_renamed()
|| s.is_wt_typechange(),
is_submodule: false,
submodule_state: None,
submodule_warn: SubmoduleWarn::default(),
submodule_head: None,
});
}
let is_dirty = !files.is_empty();
let has_submodules = path.join(".gitmodules").is_file();
let mut submodules = Vec::new();
let mut has_dirty_submodules = false;
let mut has_unpushed_submodules = false;
if has_submodules
&& (!sub_cfg.ignore_dirty || sub_cfg.warn_unpushed)
&& let Ok(subs) = repo.submodules()
{
for sub in &subs {
let name = sub.name().unwrap_or("").to_string();
let sub_path = PathBuf::from(sub.path());
let state = if sub_cfg.ignore_dirty {
None
} else {
let status = repo
.submodule_status(&name, git2::SubmoduleIgnore::Unspecified)
.unwrap_or(SubmoduleStatus::empty());
if status.is_wd_uninitialized() {
Some(SubmoduleState::Uninitialized)
} else if status.is_wd_wd_modified()
|| status.contains(SubmoduleStatus::WD_UNTRACKED)
{
Some(SubmoduleState::Dirty)
} else if status.is_wd_modified()
|| status.contains(SubmoduleStatus::WD_INDEX_MODIFIED)
{
Some(SubmoduleState::Modified)
} else {
None
}
};
let is_uninit = state == Some(SubmoduleState::Uninitialized);
let (head, warn) = if !is_uninit && (state.is_some() || sub_cfg.warn_unpushed) {
compute_submodule_head_and_warn(sub, sub_cfg.warn_unpushed)
} else {
(None, SubmoduleWarn::default())
};
let has_dirty_signal = state.is_some();
let has_warn_signal = !warn.is_clean();
if !has_dirty_signal && !has_warn_signal {
continue;
}
let head_oid = sub.head_id().map(|id| id.to_string());
let workdir_oid = sub.workdir_id().map(|id| id.to_string());
submodules.push(SubmoduleInfo {
name: name.clone(),
path: sub_path.clone(),
state: state.clone(),
head: head.clone(),
head_oid,
workdir_oid,
warn,
});
if let Some(file_entry) = files.iter_mut().find(|f| f.path == sub_path) {
file_entry.is_submodule = true;
file_entry.submodule_state = state.clone();
file_entry.submodule_warn = warn;
file_entry.submodule_head = head;
} else {
files.push(FileEntry {
path: sub_path,
status: FileStatus::Modified,
staged: false,
unstaged: true,
is_submodule: true,
submodule_state: state,
submodule_warn: warn,
submodule_head: head,
});
}
if has_dirty_signal {
has_dirty_submodules = true;
}
if has_warn_signal {
has_unpushed_submodules = true;
}
}
}
Ok(ChangeSummary {
files,
is_dirty: is_dirty || has_dirty_submodules,
has_submodules,
submodules,
has_dirty_submodules,
has_unpushed_submodules,
})
}
impl FileStatus {
pub fn label(&self) -> &'static str {
match self {
Self::Modified => "M",
Self::Added => "A",
Self::Deleted => "D",
Self::Renamed => "R",
Self::Untracked => "?",
Self::Conflicted => "C",
}
}
}