Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
radicle-tui bin git.rs
use std::fmt;
use std::fmt::Debug;
use std::path::Path;
use std::{fs, path::PathBuf};

use ratatui::text::Line;

use radicle_surf::diff::{Copied, DiffFile, EofNewLine, FileStats, Hunk, Modification, Moved};

use radicle::git;
use radicle::git::Oid;

use radicle_cli::git::unified_diff::FileHeader;
use radicle_cli::terminal;
use radicle_cli::terminal::highlight::Highlighter;
use serde::{Deserialize, Serialize};

pub type FilePaths<'a> = (Option<(&'a Path, Oid)>, Option<(&'a Path, Oid)>);

/// Get the diff stats between two commits.
/// Should match the default output of `git diff <old> <new> --stat` exactly.
pub fn diff_stats(
    repo: &git::raw::Repository,
    old: &Oid,
    new: &Oid,
) -> Result<git::raw::DiffStats, git::raw::Error> {
    let old = repo.find_commit(**old)?;
    let new = repo.find_commit(**new)?;
    let old_tree = old.tree()?;
    let new_tree = new.tree()?;
    let mut diff = repo.diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)?;
    let mut find_opts = git::raw::DiffFindOptions::new();

    diff.find_similar(Some(&mut find_opts))?;
    diff.stats()
}

/// A repository of Git blobs.
pub trait Repo {
    /// Lookup a blob from the repo.
    fn blob(&self, oid: git::Oid) -> Result<Blob, git::raw::Error>;
    /// Lookup a file in the workdir.
    fn file(&self, path: &Path) -> Option<Blob>;
}

impl Repo for git::raw::Repository {
    fn blob(&self, oid: git::Oid) -> Result<Blob, git::raw::Error> {
        let blob = self.find_blob(*oid)?;

        if blob.is_binary() {
            Ok(Blob::Binary)
        } else {
            let content = blob.content();

            if content.is_empty() {
                Ok(Blob::Empty)
            } else {
                Ok(Blob::Plain(blob.content().to_vec()))
            }
        }
    }

    fn file(&self, path: &Path) -> Option<Blob> {
        self.workdir()
            .and_then(|dir| fs::read(dir.join(path)).ok())
            .map(|content| {
                // A file is considered binary if there is a zero byte in the first 8 kilobytes
                // of the file. This is the same heuristic Git uses.
                let binary = content.iter().take(8192).any(|b| *b == 0);
                if binary {
                    Blob::Binary
                } else {
                    Blob::Plain(content)
                }
            })
    }
}

/// Blob returned by the [`Repo`] trait.
#[derive(PartialEq, Eq, Debug)]
pub enum Blob {
    Binary,
    Empty,
    Plain(Vec<u8>),
}

/// Blobs passed down to the hunk renderer.
#[derive(Clone, Debug)]
pub struct Blobs<T> {
    pub old: Option<T>,
    pub new: Option<T>,
}

impl<T> Blobs<T> {
    pub fn new(old: Option<T>, new: Option<T>) -> Self {
        Self { old, new }
    }
}

impl<'a> Blobs<(PathBuf, Blob)> {
    pub fn highlight(self, mut hi: Highlighter) -> Blobs<Vec<Line<'a>>> {
        let mut blobs = Blobs::default();
        if let Some((path, Blob::Plain(content))) = &self.old {
            blobs.old = hi
                .highlight(path, content)
                .map(|hi| {
                    hi.into_iter()
                        .map(|line| Line::raw(line.to_string()))
                        .collect::<Vec<_>>()
                })
                .ok();
        }
        if let Some((path, Blob::Plain(content))) = &self.new {
            blobs.new = hi
                .highlight(path, content)
                .map(|hi| {
                    hi.into_iter()
                        .map(|line| Line::raw(line.to_string()))
                        .collect::<Vec<_>>()
                })
                .ok();
        }
        blobs
    }

    pub fn _raw(self) -> Blobs<Vec<Line<'a>>> {
        let mut blobs = Blobs::default();
        if let Some((_, Blob::Plain(content))) = &self.old {
            blobs.old = std::str::from_utf8(content)
                .map(|lines| {
                    lines
                        .lines()
                        .map(terminal::Line::new)
                        .map(|line| Line::raw(line.to_string()))
                        .collect::<Vec<_>>()
                })
                .ok();
        }
        if let Some((_, Blob::Plain(content))) = &self.new {
            blobs.new = std::str::from_utf8(content)
                .map(|lines| {
                    lines
                        .lines()
                        .map(terminal::Line::new)
                        .map(|line| Line::raw(line.to_string()))
                        .collect::<Vec<_>>()
                })
                .ok();
        }
        blobs
    }

    pub fn from_paths<R: Repo>(
        old: Option<(&Path, Oid)>,
        new: Option<(&Path, Oid)>,
        repo: &R,
    ) -> Blobs<(PathBuf, Blob)> {
        Blobs::new(
            old.and_then(|(path, oid)| {
                repo.blob(oid)
                    .ok()
                    .or_else(|| repo.file(path))
                    .map(|blob| (path.to_path_buf(), blob))
            }),
            new.and_then(|(path, oid)| {
                repo.blob(oid)
                    .ok()
                    .or_else(|| repo.file(path))
                    .map(|blob| (path.to_path_buf(), blob))
            }),
        )
    }
}

impl<T> Default for Blobs<T> {
    fn default() -> Self {
        Self {
            old: None,
            new: None,
        }
    }
}

pub enum DiffStats {
    Hunk(HunkStats),
    File(FileStats),
}

#[derive(Default)]
pub struct HunkStats {
    added: usize,
    deleted: usize,
}

impl HunkStats {
    pub fn added(&self) -> usize {
        self.added
    }
    pub fn deleted(&self) -> usize {
        self.deleted
    }
}

impl From<&Hunk<Modification>> for HunkStats {
    fn from(hunk: &Hunk<Modification>) -> Self {
        let mut added = 0_usize;
        let mut deleted = 0_usize;

        for modification in &hunk.lines {
            match modification {
                Modification::Addition(_) => added += 1,
                Modification::Deletion(_) => deleted += 1,
                _ => {}
            }
        }

        Self { added, deleted }
    }
}

#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub enum HunkState {
    #[default]
    Unknown,
    Rejected,
    Accepted,
}

/// A single review item. Can be a hunk or eg. a file move.
/// Files are usually split into multiple review items.
#[derive(Clone, PartialEq)]
pub enum HunkDiff {
    Added {
        path: PathBuf,
        header: FileHeader,
        new: DiffFile,
        hunk: Option<Hunk<Modification>>,
        _stats: Option<FileStats>,
    },
    Deleted {
        path: PathBuf,
        header: FileHeader,
        old: DiffFile,
        hunk: Option<Hunk<Modification>>,
        _stats: Option<FileStats>,
    },
    Modified {
        path: PathBuf,
        header: FileHeader,
        old: DiffFile,
        new: DiffFile,
        hunk: Option<Hunk<Modification>>,
        _stats: Option<FileStats>,
    },
    Moved {
        moved: Moved,
    },
    Copied {
        copied: Copied,
    },
    EofChanged {
        path: PathBuf,
        header: FileHeader,
        old: DiffFile,
        new: DiffFile,
        _eof: EofNewLine,
    },
    ModeChanged {
        path: PathBuf,
        header: FileHeader,
        old: DiffFile,
        new: DiffFile,
    },
}

impl HunkDiff {
    pub fn hunk(&self) -> Option<&Hunk<Modification>> {
        match self {
            Self::Added { hunk, .. } => hunk.as_ref(),
            Self::Deleted { hunk, .. } => hunk.as_ref(),
            Self::Modified { hunk, .. } => hunk.as_ref(),
            _ => None,
        }
    }

    pub fn path(&self) -> &PathBuf {
        match self {
            HunkDiff::Added { path, .. } => path,
            HunkDiff::Modified { path, .. } => path,
            HunkDiff::Deleted { path, .. } => path,
            HunkDiff::Copied { copied } => &copied.new_path,
            HunkDiff::Moved { moved } => &moved.new_path,
            HunkDiff::ModeChanged { path, .. } => path,
            HunkDiff::EofChanged { path, .. } => path,
        }
    }

    pub fn paths(&self) -> FilePaths<'_> {
        match self {
            Self::Added { path, new, .. } => (None, Some((path, new.oid))),
            Self::Deleted { path, old, .. } => (Some((path, old.oid)), None),
            Self::Moved { moved } => (
                Some((&moved.old_path, moved.old.oid)),
                Some((&moved.new_path, moved.new.oid)),
            ),
            Self::Copied { copied } => (
                Some((&copied.old_path, copied.old.oid)),
                Some((&copied.new_path, copied.new.oid)),
            ),
            Self::Modified { path, old, new, .. } => (Some((path, old.oid)), Some((path, new.oid))),
            Self::EofChanged { path, old, new, .. } => {
                (Some((path, old.oid)), Some((path, new.oid)))
            }
            Self::ModeChanged { path, old, new, .. } => {
                (Some((path, old.oid)), Some((path, new.oid)))
            }
        }
    }

    pub fn blobs<R: Repo>(&self, repo: &R) -> Blobs<(PathBuf, Blob)> {
        let (old, new) = self.paths();
        Blobs::from_paths(old, new, repo)
    }
}

impl Debug for HunkDiff {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let (name, path, hunk) = match self {
            Self::Added { path, hunk, .. } => ("Added", path, hunk),
            Self::Deleted { path, hunk, .. } => ("Deleted", path, hunk),
            Self::Moved { moved } => ("Moved", &moved.new_path, &None),
            Self::Copied { copied } => ("Copied", &copied.new_path, &None),
            Self::Modified { path, hunk, .. } => ("Modified", path, hunk),
            Self::EofChanged { path, .. } => ("EofChanged", path, &None),
            Self::ModeChanged { path, .. } => ("ModeChanged", path, &None),
        };

        match hunk {
            Some(hunk) => f
                .debug_struct(name)
                .field("path", path)
                .field("hunk", &(hunk.old.clone(), hunk.new.clone()))
                .finish(),
            _ => f.debug_struct(name).field("path", path).finish(),
        }
    }
}