use super::{DiffSide, Hunk, PatchLine, RepoPath, patch_derivation::derive_patch};
use crate::{
DiffError, Fingerprint, RepoPathError, SourceDocument, SourceResult, SourceUnavailable,
};
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, ffi::OsStr, path::Path, sync::Arc};
const FILE_CONTENT_DOMAIN: &[u8] = b"diff-file-content-v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileStatus {
Modified,
Added,
Deleted,
Renamed,
Copied,
Untracked,
}
impl FileStatus {
#[must_use]
pub const fn code(self) -> char {
match self {
Self::Modified => 'M',
Self::Added => 'A',
Self::Deleted => 'D',
Self::Renamed => 'R',
Self::Copied => 'C',
Self::Untracked => '?',
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StageState {
Unstaged,
Staged,
PartiallyStaged,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModeChange {
pub old: Option<String>,
pub new: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FileDiff {
pub old_path: Option<RepoPath>,
pub path: RepoPath,
pub status: FileStatus,
pub staged: StageState,
pub hunks: Vec<Hunk>,
pub binary: bool,
pub mode: Option<ModeChange>,
pub no_newline_at_end: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub omitted_bytes: Option<u64>,
#[serde(default = "not_captured", skip_serializing_if = "is_not_captured")]
pub old_source: SourceResult,
#[serde(default = "not_captured", skip_serializing_if = "is_not_captured")]
pub new_source: SourceResult,
}
fn not_captured() -> SourceResult {
Err(SourceUnavailable::NotCaptured)
}
fn is_not_captured(source: &SourceResult) -> bool {
matches!(source, Err(SourceUnavailable::NotCaptured))
}
impl FileDiff {
#[must_use]
pub fn path_for_side(&self, side: DiffSide) -> &RepoPath {
match side {
DiffSide::Old => self.old_path.as_ref().unwrap_or(&self.path),
DiffSide::New => &self.path,
}
}
pub fn from_texts<T>(path: T, old: &str, new: &str) -> Result<Self, DiffError>
where
T: TryInto<RepoPath>,
T::Error: Into<RepoPathError>,
{
let path = path
.try_into()
.map_err(|error| DiffError::InvalidPath(error.into()))?;
let status = match (old.is_empty(), new.is_empty()) {
(true, false) => FileStatus::Added,
(false, true) => FileStatus::Deleted,
_ => FileStatus::Modified,
};
let (hunks, no_newline_at_end) = derive_patch(old, new);
let side = |text: &str, absent: bool| {
if absent {
Err(SourceUnavailable::Absent)
} else {
SourceDocument::new(text).map(Arc::new)
}
};
Ok(Self {
old_path: (status != FileStatus::Added).then(|| path.clone()),
path,
status,
staged: StageState::Unstaged,
hunks,
binary: false,
mode: None,
no_newline_at_end,
omitted_bytes: None,
old_source: side(old, status == FileStatus::Added),
new_source: side(new, status == FileStatus::Deleted),
})
}
#[must_use]
pub fn with_sources(mut self, old: SourceResult, new: SourceResult) -> Self {
self.old_source = old;
self.new_source = new;
if self.binary {
return self;
}
if let (Some(old), Some(new)) =
(self.side_text(DiffSide::Old), self.side_text(DiffSide::New))
{
let (hunks, no_newline_at_end) = derive_patch(old, new);
self.hunks = hunks;
self.no_newline_at_end = no_newline_at_end;
}
self
}
pub const fn uncaptured_source(status: FileStatus, side: DiffSide) -> SourceResult {
if Self::side_is_absent(status, side) {
Err(SourceUnavailable::Absent)
} else {
Err(SourceUnavailable::NotCaptured)
}
}
const fn side_is_absent(status: FileStatus, side: DiffSide) -> bool {
matches!(
(status, side),
(FileStatus::Added | FileStatus::Untracked, DiffSide::Old)
| (FileStatus::Deleted, DiffSide::New)
)
}
pub const fn source(&self, side: DiffSide) -> &SourceResult {
match side {
DiffSide::Old => &self.old_source,
DiffSide::New => &self.new_source,
}
}
#[must_use]
pub fn source_document(&self, side: DiffSide) -> Option<&Arc<SourceDocument>> {
self.source(side).as_ref().ok()
}
#[must_use]
pub fn source_unavailable(&self, side: DiffSide) -> Option<&SourceUnavailable> {
self.source(side).as_ref().err()
}
fn side_text(&self, side: DiffSide) -> Option<&str> {
match self.source(side) {
Ok(source) => Some(source.text()),
Err(SourceUnavailable::Absent) if Self::side_is_absent(self.status, side) => Some(""),
Err(_) => None,
}
}
#[must_use]
pub fn content_id(&self) -> Fingerprint {
let mut fields: Vec<Cow<'_, [u8]>> = vec![
Cow::Borrowed(FILE_CONTENT_DOMAIN),
Cow::Owned(vec![
u8::try_from(self.status.code()).unwrap_or(b'?'),
u8::from(self.binary),
]),
];
let mut complete = true;
for side in [DiffSide::Old, DiffSide::New] {
match self.source(side) {
Ok(source) => fields.push(Cow::Owned(source.content_id().as_bytes().to_vec())),
Err(reason) => {
complete = false;
fields.push(Cow::Owned(reason.to_string().into_bytes()));
}
}
}
if !complete {
for hunk in &self.hunks {
fields.push(Cow::Borrowed(hunk.header.as_bytes()));
for line in &hunk.lines {
fields.push(Cow::Borrowed(line.kind.as_str().as_bytes()));
fields.push(Cow::Borrowed(line.text.as_bytes()));
fields.push(Cow::Owned(
[line.old_line_no, line.new_line_no]
.iter()
.flat_map(|number| number.unwrap_or(0).to_le_bytes())
.chain([u8::from(line.no_newline)])
.collect(),
));
}
}
}
Fingerprint::of(fields)
}
#[must_use]
pub fn additions(&self) -> usize {
self.hunks.iter().map(Hunk::additions).sum()
}
#[must_use]
pub fn deletions(&self) -> usize {
self.hunks.iter().map(Hunk::deletions).sum()
}
#[must_use]
pub fn language(&self) -> &str {
Path::new(self.path.as_str())
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
}
#[must_use]
pub fn line(&self, hunk: usize, line: usize) -> Option<&PatchLine> {
self.hunks.get(hunk)?.lines.get(line)
}
}