delta-gitu 0.1.6

An experimental fork of gitu (a git client inspired by Magit), published as a library for delta to embed
Documentation
use std::{cell::RefCell, ops::Range, path::PathBuf, rc::Rc};

use crate::{
    Res, error::Error, git::diff::Diff, highlight::BlameHighlights, rebase_todo::RebaseTodo,
};

#[derive(Clone, Debug)]
pub(crate) enum ItemData {
    Raw(String),
    AllUnstaged(usize),
    AllStaged(usize),
    AllUntracked(Vec<PathBuf>),
    Reference {
        prefix: &'static str,
        kind: Ref,
    },
    Commit {
        oid: String,
        short_id: String,
        associated_references: Vec<Ref>,
        summary: String,
        author: String,
        age: String,
    },
    Untracked(PathBuf),
    Delta {
        diff: Rc<Diff>,
        file_i: usize,
        commit: Option<String>,
    },
    Hunk {
        diff: Rc<Diff>,
        file_i: usize,
        hunk_i: usize,
    },
    HunkLine {
        diff: Rc<Diff>,
        file_i: usize,
        hunk_i: usize,
        line_i: usize,
        line_range: Range<usize>,
        /// Content lines this row stages/reverses as a unit. Usually `[line_i]`;
        /// a fused side-by-side change row also includes its replacement line.
        line_indices: Vec<usize>,
    },
    Stash {
        message: String,
        stash_ref: String,
        id: usize,
    },
    /// One entry of an interactive rebase's instruction list, as its position in
    /// the shared list being edited.
    RebaseTodo {
        todo: Rc<RefCell<RebaseTodo>>,
        index: usize,
    },
    Header(SectionHeader),
    /// The upstream of the current branch, and how far ahead of and behind it
    /// the branch is.
    BranchStatus(String, u32, u32),
    Error(String),
    BlameHeader {
        commit_hash: String,
        short_hash: String,
        _author: String,
        _author_time: i64,
        summary: String,
        file_path: String,
        line_num: u32, // orig line in the introducing commit (for show-screen nav)
        blamed_line_num: u32, // line number in the blamed file (for blame-view nav)
    },
    BlameCodeLine {
        blame_file: Rc<BlameFile>,
        line_i: usize,
        line_num: u32,
        orig_line_num: u32,
        content: String,
        commit_hash: String,
        file_path: String,
    },
}

#[derive(Debug)]
pub(crate) struct BlameFile {
    pub highlights: BlameHighlights,
}

impl ItemData {
    /// What kind of thing the row is, as a heading for the commands that act
    /// on it.
    pub(crate) fn kind(&self) -> &'static str {
        match self {
            ItemData::Raw(_) | ItemData::HunkLine { .. } | ItemData::BlameCodeLine { .. } => "Line",
            ItemData::AllUnstaged(_) => "Unstaged changes",
            ItemData::AllStaged(_) => "Staged changes",
            ItemData::AllUntracked(_) => "Untracked files",
            ItemData::Reference { kind, .. } => match kind {
                Ref::Tag(_) => "Tag",
                Ref::Head(_) | Ref::Remote(_) => "Branch",
            },
            ItemData::Commit { .. }
            | ItemData::RebaseTodo { .. }
            | ItemData::BlameHeader { .. } => "Commit",
            ItemData::Untracked(_) | ItemData::Delta { .. } => "File",
            ItemData::Hunk { .. } => "Hunk",
            ItemData::Stash { .. } => "Stash",
            ItemData::Header(_) => "Section",
            ItemData::BranchStatus(..) => "Branch",
            ItemData::Error(_) => "Error",
        }
    }

    pub(crate) fn is_section(&self) -> bool {
        matches!(
            self,
            ItemData::AllUnstaged(_)
                | ItemData::AllStaged(_)
                | ItemData::AllUntracked(_)
                | ItemData::Untracked(_)
                | ItemData::Delta { .. }
                | ItemData::Hunk { .. }
                | ItemData::Header(_)
                | ItemData::BranchStatus(_, _, _)
                // A commit (or rebase todo entry) is a section so that sibling
                // navigation steps commit-to-commit and folding hides the extra
                // rows a rendered log gives it.
                | ItemData::Commit { .. }
                | ItemData::RebaseTodo { .. }
        )
    }

    pub(crate) fn rev(&self) -> Option<Rev> {
        match &self {
            ItemData::Reference { kind, .. } => Some(Rev::Ref(kind.clone())),
            ItemData::Commit {
                oid,
                associated_references,
                ..
            } => associated_references
                .first()
                .cloned()
                .map(Rev::Ref)
                .or_else(|| Some(Rev::Commit(oid.to_owned()))),
            ItemData::BlameHeader { commit_hash, .. }
            | ItemData::BlameCodeLine { commit_hash, .. } => Some(Rev::Commit(commit_hash.clone())),
            _ => None,
        }
    }
}

impl Default for ItemData {
    fn default() -> Self {
        ItemData::Raw(String::new())
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Rev {
    Ref(Ref),
    Commit(String),
}

impl Rev {
    pub(crate) fn from_reference(reference: &git2::Reference<'_>) -> Res<Self> {
        let shorthand = String::from_utf8_lossy(reference.shorthand_bytes()).to_string();

        if reference.is_branch() {
            Ok(Rev::Ref(Ref::Head(shorthand)))
        } else if reference.is_tag() {
            Ok(Rev::Ref(Ref::Tag(shorthand)))
        } else if reference.is_remote() {
            Ok(Rev::Ref(Ref::Remote(shorthand)))
        } else {
            let commit = reference.peel_to_commit().map_err(Error::ReadOid)?;
            Ok(Rev::Commit(commit.id().to_string()))
        }
    }

    pub(crate) fn shorthand(&self) -> &str {
        match self {
            Rev::Ref(r) => r.shorthand(),
            Rev::Commit(c) => c,
        }
    }
}

/// Represent a reference in git, as found in `.git/refs/heads`, `.git/refs/tags`, etc.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Ref {
    Tag(String),
    Head(String),
    Remote(String),
}

impl Ref {
    /// Convert to fully qualified refname (e.g., "refs/heads/main", "refs/tags/v1.0.0")
    pub(crate) fn to_full_refname(&self) -> String {
        match self {
            Ref::Head(name) => format!("refs/heads/{}", name),
            Ref::Tag(name) => format!("refs/tags/{}", name),
            Ref::Remote(name) => format!("refs/remotes/{}", name),
        }
    }

    /// Get the shorthand name without refs/ prefix
    pub(crate) fn shorthand(&self) -> &str {
        match self {
            Ref::Head(name) | Ref::Tag(name) | Ref::Remote(name) => name,
        }
    }
}

#[derive(Clone, Debug)]
pub(crate) enum SectionHeader {
    Remote(String),
    Tags,
    Branches,
    NoBranch,
    OnBranch(String),
    Rebase(String, String),
    Merge(String),
    Revert(String),
    CherryPick(String),
    Stashes,
    RecentCommits,
    Commit(String),
    StashRef(String),
    StagedChanges(usize),
    UnstagedChanges(usize),
    UntrackedFiles(usize),
    Blame(String, String),
}