gitu 0.41.0

A git client inspired by Magit
Documentation
use super::Screen;
use crate::{
    Res,
    config::Config,
    error::Error,
    git::{self, diff::Diff, status::BranchStatus},
    item_data::{ItemData, SectionHeader},
    items::{self, Item, hash},
};
use git2::Repository;
use ratatui::prelude::Size;
use std::{hash::Hash, path::PathBuf, rc::Rc, sync::Arc};

enum SectionID {
    RebaseStatus,
    MergeStatus,
    RevertStatus,
    Untracked,
    Stashes,
    RecentCommits,
    BranchStatus,
    UnstagedChanges,
    StagedChanges,
}

impl Hash for SectionID {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        let id = match self {
            SectionID::RebaseStatus => "rebase_status",
            SectionID::MergeStatus => "merge_status",
            SectionID::RevertStatus => "revert_status",
            SectionID::Untracked => "untracked",
            SectionID::Stashes => "stashes",
            SectionID::RecentCommits => "recent_commits",
            SectionID::BranchStatus => "branch_status",
            SectionID::UnstagedChanges => "unstaged_changes",
            SectionID::StagedChanges => "staged_changes",
        };

        id.hash(state)
    }
}

pub(crate) fn create(config: Arc<Config>, repo: Rc<Repository>, size: Size) -> Res<Screen> {
    Screen::new(
        Arc::clone(&config),
        size,
        Box::new(move || {
            let status = git::status(repo.workdir().ok_or(Error::NoRepoWorkdir)?)?;
            let untracked_files = status
                .files
                .iter()
                .filter(|status| status.is_untracked())
                .map(|status| &status.path)
                .collect::<Vec<_>>();

            let untracked = untracked_list(&untracked_files);

            let items = if let Some(rebase) = git::rebase_status(&repo)? {
                vec![Item {
                    id: hash(SectionID::RebaseStatus),
                    data: ItemData::Header(SectionHeader::Rebase(rebase.head_name, rebase.onto)),
                    ..Default::default()
                }]
                .into_iter()
            } else if let Some(merge) = git::merge_status(&repo)? {
                vec![Item {
                    id: hash(SectionID::MergeStatus),
                    data: ItemData::Header(SectionHeader::Merge(merge.head)),
                    ..Default::default()
                }]
                .into_iter()
            } else if let Some(revert) = git::revert_status(&repo)? {
                vec![Item {
                    id: hash(SectionID::RevertStatus),
                    data: ItemData::Header(SectionHeader::Revert(revert.head)),
                    ..Default::default()
                }]
                .into_iter()
            } else {
                branch_status_items(&status.branch_status)?.into_iter()
            }
            .chain(if untracked.is_empty() {
                vec![]
            } else {
                vec![
                    items::blank_line(),
                    Item {
                        id: hash(SectionID::Untracked),
                        depth: 0,
                        data: ItemData::AllUntracked(
                            untracked_files.iter().map(PathBuf::from).collect(),
                        ),
                        ..Default::default()
                    },
                ]
            })
            .chain(untracked)
            .chain(create_status_section_items(
                SectionID::UnstagedChanges,
                &Rc::new(git::diff_unstaged(repo.as_ref())?),
            ))
            .chain(create_status_section_items(
                SectionID::StagedChanges,
                &Rc::new(git::diff_staged(repo.as_ref())?),
            ))
            .chain(create_stash_list_section_items(
                repo.as_ref(),
                config.general.stash_list_limit,
            ))
            .chain(create_log_section_items(
                repo.as_ref(),
                config.general.recent_commits_limit,
            ))
            .collect();

            Ok(items)
        }),
    )
}

fn untracked_list(files: &[&String]) -> Vec<Item> {
    files
        .iter()
        .map(|path| Item {
            id: hash(path),
            depth: 1,
            data: ItemData::Untracked(PathBuf::from(path)),
            ..Default::default()
        })
        .collect::<Vec<_>>()
}

fn branch_status_items(status: &BranchStatus) -> Res<Vec<Item>> {
    let Some(ref head) = status.local else {
        return Ok(vec![Item {
            id: hash(SectionID::BranchStatus),
            depth: 0,
            data: ItemData::Header(SectionHeader::NoBranch),
            ..Default::default()
        }]);
    };

    let mut items = vec![Item {
        id: hash(SectionID::BranchStatus),
        depth: 0,
        data: ItemData::Header(SectionHeader::OnBranch(head.clone())),
        ..Default::default()
    }];

    let Some(ref upstream_name) = status.remote else {
        return Ok(items);
    };

    items.push(Item {
        id: hash(SectionID::BranchStatus),
        depth: 1,
        unselectable: true,
        data: ItemData::BranchStatus(upstream_name.clone(), status.ahead, status.behind),
        ..Default::default()
    });

    Ok(items)
}

fn create_status_section_items<'a>(
    section: SectionID,
    diff: &'a Rc<Diff>,
) -> impl Iterator<Item = Item> + 'a {
    if diff.file_diffs.is_empty() {
        vec![]
    } else {
        let count = diff.file_diffs.len();
        let item_data = match section {
            SectionID::UnstagedChanges => ItemData::AllUnstaged(count),
            SectionID::StagedChanges => ItemData::AllStaged(count),
            _ => unreachable!("no other status section should be created"),
        };

        vec![
            items::blank_line(),
            Item {
                id: hash(section),
                depth: 0,
                data: item_data,
                ..Default::default()
            },
        ]
    }
    .into_iter()
    .chain(items::create_diff_items(diff, 1, true))
}

fn create_stash_list_section_items<'a>(
    repo: &Repository,
    limit: usize,
) -> impl Iterator<Item = Item> + 'a {
    let stashes = items::stash_list(repo, limit).unwrap();
    if stashes.is_empty() {
        vec![]
    } else {
        vec![
            items::blank_line(),
            Item {
                id: hash(SectionID::Stashes),
                depth: 0,
                data: ItemData::Header(SectionHeader::Stashes),
                ..Default::default()
            },
        ]
    }
    .into_iter()
    .chain(stashes)
}

fn create_log_section_items<'a>(
    repo: &Repository,
    limit: usize,
) -> impl Iterator<Item = Item> + 'a {
    [
        Item {
            depth: 0,
            unselectable: true,
            ..Default::default()
        },
        Item {
            id: hash(SectionID::RecentCommits),
            depth: 0,
            data: ItemData::Header(SectionHeader::RecentCommits),
            ..Default::default()
        },
    ]
    .into_iter()
    .chain(items::log(repo, limit, None, None).unwrap())
}