use crate::tasks::git::branch::get_branch_name;
use crate::tasks::git::branch::get_push_branch;
use crate::tasks::git::cherry::unmerged_commits;
use crate::tasks::git::errors::GitError as E;
use crate::utils::files::to_utf8_path;
use color_eyre::eyre::Result;
use color_eyre::eyre::ensure;
use color_eyre::eyre::eyre;
use git2::BranchType;
use git2::Config;
use git2::ErrorCode;
use git2::Repository;
use git2::StatusOptions;
use git2::Statuses;
use git2::SubmoduleIgnore;
use std::fmt::Write as _; use tracing::trace;
use tracing::warn;
pub(super) fn ensure_repo_clean(repo: &Repository) -> Result<()> {
let statuses = repo_statuses(repo)?;
trace!("Repo statuses: '{}'", status_short(repo, &statuses)?);
ensure!(
statuses.is_empty(),
E::UncommittedChanges {
status: status_short(repo, &statuses)?
}
);
Ok(())
}
pub(super) fn warn_for_unpushed_changes(
repo: &mut Repository,
user_git_config: &Config,
) -> Result<()> {
{
let statuses = repo_statuses(repo)?;
if !statuses.is_empty() {
warn!("Uncommitted changes:\n{}", status_short(repo, &statuses)?);
}
}
{
let mut stash_messages = Vec::new();
repo.stash_foreach(|_index, message, _stash_id| {
stash_messages.push(message.to_owned());
true
})?;
if !stash_messages.is_empty() {
warn!("Stashed changes:\n{:#?}", stash_messages);
}
}
for branch in repo.branches(Some(BranchType::Local))? {
let branch = branch?.0;
let branch_name = get_branch_name(&branch)?;
if let Some(push_branch) = get_push_branch(repo, &branch_name, user_git_config)? {
if unmerged_commits(repo, &push_branch, &branch)? {
warn!("Branch '{branch_name}' has changes that aren't in @{{push}}.",);
}
} else {
match branch.upstream() {
Ok(upstream_branch) => {
if unmerged_commits(repo, &upstream_branch, &branch)? {
warn!("Branch '{branch_name}' has changes that aren't in @{{upstream}}.",);
}
}
Err(e) if e.code() == ErrorCode::NotFound => {
warn!("Branch '{branch_name}' has no @{{upstream}} or @{{push}} branch.",);
}
Err(e) => {
return Err(e.into());
}
}
}
}
let mut unmerged_branches = Vec::new();
for branch in repo.branches(Some(BranchType::Remote))? {
let branch = branch?.0;
let branch_name = get_branch_name(&branch)?;
if branch_name.contains("fork")
&& !branch_name.contains("HEAD")
&& !branch_name.contains("forkmain")
{
unmerged_branches.push(
branch_name,
);
}
}
if !unmerged_branches.is_empty() {
warn!("Unmerged fork branches: {} .", unmerged_branches.join(" "),);
}
Ok(())
}
fn repo_statuses(repo: &Repository) -> Result<Statuses<'_>> {
let mut status_options = StatusOptions::new();
status_options
.include_ignored(false)
.include_untracked(true);
Ok(repo.statuses(Some(&mut status_options))?)
}
#[allow(clippy::too_many_lines, clippy::useless_let_if_seq)]
fn status_short(repo: &Repository, statuses: &git2::Statuses) -> Result<String> {
let mut output = String::new();
for entry in statuses
.iter()
.filter(|e| e.status() != git2::Status::CURRENT)
{
let mut index_status = match entry.status() {
s if s.contains(git2::Status::INDEX_NEW) => 'A',
s if s.contains(git2::Status::INDEX_MODIFIED) => 'M',
s if s.contains(git2::Status::INDEX_DELETED) => 'D',
s if s.contains(git2::Status::INDEX_RENAMED) => 'R',
s if s.contains(git2::Status::INDEX_TYPECHANGE) => 'T',
_ => ' ',
};
let mut worktree_status = match entry.status() {
s if s.contains(git2::Status::WT_NEW) => {
if index_status == ' ' {
index_status = '?';
}
'?'
}
s if s.contains(git2::Status::WT_MODIFIED) => 'M',
s if s.contains(git2::Status::WT_DELETED) => 'D',
s if s.contains(git2::Status::WT_RENAMED) => 'R',
s if s.contains(git2::Status::WT_TYPECHANGE) => 'T',
_ => ' ',
};
if entry.status().contains(git2::Status::IGNORED) {
index_status = '!';
worktree_status = '!';
}
if index_status == '?' && worktree_status == '?' {
continue;
}
let mut extra = "";
let status = entry.index_to_workdir().and_then(|diff| {
let ignore = SubmoduleIgnore::Unspecified;
diff.new_file()
.path_bytes()
.and_then(|s| std::str::from_utf8(s).ok())
.and_then(|name| repo.submodule_status(name, ignore).ok())
});
if let Some(status) = status {
if status.contains(git2::SubmoduleStatus::WD_MODIFIED) {
extra = " (new commits)";
} else if status.contains(git2::SubmoduleStatus::WD_INDEX_MODIFIED)
|| status.contains(git2::SubmoduleStatus::WD_WD_MODIFIED)
{
extra = " (modified content)";
} else if status.contains(git2::SubmoduleStatus::WD_UNTRACKED) {
extra = " (untracked content)";
}
}
let (mut a, mut b, mut c) = (None, None, None);
if let Some(diff) = entry.head_to_index() {
a = diff.old_file().path();
b = diff.new_file().path();
}
if let Some(diff) = entry.index_to_workdir() {
a = a.or_else(|| diff.old_file().path());
b = b.or_else(|| diff.old_file().path());
c = diff.new_file().path();
}
let a = to_utf8_path(a.ok_or_else(|| eyre!("Couldn't work out diff status a"))?)?;
let b = to_utf8_path(b.ok_or_else(|| eyre!("Couldn't work out diff status b"))?)?;
let c = to_utf8_path(c.ok_or_else(|| eyre!("Couldn't work out diff status c"))?)?;
write!(
output,
"{}",
&match (index_status, worktree_status) {
('R', 'R') => format!("RR {a} {b} {c}{extra}\n"),
('R', worktree_status) => format!("R{worktree_status} {a} {b}{extra}\n"),
(index_status, 'R') => {
format!("{index_status}R {a} {c}{extra}\n")
}
(index_status, worktree_status) => {
format!("{index_status}{worktree_status} {a}{extra}\n")
}
}
)?;
}
for entry in statuses
.iter()
.filter(|e| e.status() == git2::Status::WT_NEW)
{
_ = writeln!(
output,
"?? {}",
to_utf8_path(
entry
.index_to_workdir()
.ok_or_else(|| eyre!("Couldn't find the workdir for current status entry."))?
.old_file()
.path()
.ok_or_else(|| eyre!("Couldn't work out path to old file."))?
)?
);
}
Ok(output)
}