1use super::{DiffSide, Hunk, PatchLine, RepoPath, patch_derivation::derive_patch};
2use crate::{
3 DiffError, Fingerprint, RepoPathError, SourceDocument, SourceResult, SourceUnavailable,
4};
5use serde::{Deserialize, Serialize};
6use std::{borrow::Cow, ffi::OsStr, path::Path, sync::Arc};
7
8const FILE_CONTENT_DOMAIN: &[u8] = b"diff-file-content-v1";
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum FileStatus {
13 Modified,
14 Added,
15 Deleted,
16 Renamed,
17 Copied,
18 Untracked,
19}
20
21impl FileStatus {
22 #[must_use]
23 pub const fn code(self) -> char {
24 match self {
25 Self::Modified => 'M',
26 Self::Added => 'A',
27 Self::Deleted => 'D',
28 Self::Renamed => 'R',
29 Self::Copied => 'C',
30 Self::Untracked => '?',
31 }
32 }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37pub enum StageState {
38 Unstaged,
39 Staged,
40 PartiallyStaged,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct ModeChange {
46 pub old: Option<String>,
47 pub new: Option<String>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct FileDiff {
57 pub old_path: Option<RepoPath>,
58 pub path: RepoPath,
59 pub status: FileStatus,
60 pub staged: StageState,
61 pub hunks: Vec<Hunk>,
62 pub binary: bool,
63 pub mode: Option<ModeChange>,
64 pub no_newline_at_end: bool,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub omitted_bytes: Option<u64>,
67 #[serde(default = "not_captured", skip_serializing_if = "is_not_captured")]
68 pub old_source: SourceResult,
69 #[serde(default = "not_captured", skip_serializing_if = "is_not_captured")]
70 pub new_source: SourceResult,
71}
72
73fn not_captured() -> SourceResult {
74 Err(SourceUnavailable::NotCaptured)
75}
76
77fn is_not_captured(source: &SourceResult) -> bool {
78 matches!(source, Err(SourceUnavailable::NotCaptured))
79}
80
81impl FileDiff {
82 #[must_use]
84 pub fn path_for_side(&self, side: DiffSide) -> &RepoPath {
85 match side {
86 DiffSide::Old => self.old_path.as_ref().unwrap_or(&self.path),
87 DiffSide::New => &self.path,
88 }
89 }
90
91 pub fn from_texts<T>(path: T, old: &str, new: &str) -> Result<Self, DiffError>
97 where
98 T: TryInto<RepoPath>,
99 T::Error: Into<RepoPathError>,
100 {
101 let path = path
102 .try_into()
103 .map_err(|error| DiffError::InvalidPath(error.into()))?;
104 let status = match (old.is_empty(), new.is_empty()) {
105 (true, false) => FileStatus::Added,
106 (false, true) => FileStatus::Deleted,
107 _ => FileStatus::Modified,
108 };
109 let (hunks, no_newline_at_end) = derive_patch(old, new);
110 let side = |text: &str, absent: bool| {
111 if absent {
112 Err(SourceUnavailable::Absent)
113 } else {
114 SourceDocument::new(text).map(Arc::new)
115 }
116 };
117 Ok(Self {
118 old_path: (status != FileStatus::Added).then(|| path.clone()),
119 path,
120 status,
121 staged: StageState::Unstaged,
122 hunks,
123 binary: false,
124 mode: None,
125 no_newline_at_end,
126 omitted_bytes: None,
127 old_source: side(old, status == FileStatus::Added),
128 new_source: side(new, status == FileStatus::Deleted),
129 })
130 }
131
132 #[must_use]
136 pub fn with_sources(mut self, old: SourceResult, new: SourceResult) -> Self {
137 self.old_source = old;
138 self.new_source = new;
139 if self.binary {
140 return self;
141 }
142 if let (Some(old), Some(new)) =
143 (self.side_text(DiffSide::Old), self.side_text(DiffSide::New))
144 {
145 let (hunks, no_newline_at_end) = derive_patch(old, new);
146 self.hunks = hunks;
147 self.no_newline_at_end = no_newline_at_end;
148 }
149 self
150 }
151
152 pub const fn uncaptured_source(status: FileStatus, side: DiffSide) -> SourceResult {
160 if Self::side_is_absent(status, side) {
161 Err(SourceUnavailable::Absent)
162 } else {
163 Err(SourceUnavailable::NotCaptured)
164 }
165 }
166
167 const fn side_is_absent(status: FileStatus, side: DiffSide) -> bool {
168 matches!(
169 (status, side),
170 (FileStatus::Added | FileStatus::Untracked, DiffSide::Old)
171 | (FileStatus::Deleted, DiffSide::New)
172 )
173 }
174
175 pub const fn source(&self, side: DiffSide) -> &SourceResult {
177 match side {
178 DiffSide::Old => &self.old_source,
179 DiffSide::New => &self.new_source,
180 }
181 }
182
183 #[must_use]
185 pub fn source_document(&self, side: DiffSide) -> Option<&Arc<SourceDocument>> {
186 self.source(side).as_ref().ok()
187 }
188
189 #[must_use]
191 pub fn source_unavailable(&self, side: DiffSide) -> Option<&SourceUnavailable> {
192 self.source(side).as_ref().err()
193 }
194
195 fn side_text(&self, side: DiffSide) -> Option<&str> {
198 match self.source(side) {
199 Ok(source) => Some(source.text()),
200 Err(SourceUnavailable::Absent) if Self::side_is_absent(self.status, side) => Some(""),
201 Err(_) => None,
202 }
203 }
204
205 #[must_use]
212 pub fn content_id(&self) -> Fingerprint {
213 let mut fields: Vec<Cow<'_, [u8]>> = vec![
214 Cow::Borrowed(FILE_CONTENT_DOMAIN),
215 Cow::Owned(vec![
216 u8::try_from(self.status.code()).unwrap_or(b'?'),
217 u8::from(self.binary),
218 ]),
219 ];
220 let mut complete = true;
221 for side in [DiffSide::Old, DiffSide::New] {
222 match self.source(side) {
223 Ok(source) => fields.push(Cow::Owned(source.content_id().as_bytes().to_vec())),
224 Err(reason) => {
225 complete = false;
226 fields.push(Cow::Owned(reason.to_string().into_bytes()));
227 }
228 }
229 }
230 if !complete {
231 for hunk in &self.hunks {
232 fields.push(Cow::Borrowed(hunk.header.as_bytes()));
233 for line in &hunk.lines {
234 fields.push(Cow::Borrowed(line.kind.as_str().as_bytes()));
235 fields.push(Cow::Borrowed(line.text.as_bytes()));
236 fields.push(Cow::Owned(
237 [line.old_line_no, line.new_line_no]
238 .iter()
239 .flat_map(|number| number.unwrap_or(0).to_le_bytes())
240 .chain([u8::from(line.no_newline)])
241 .collect(),
242 ));
243 }
244 }
245 }
246 Fingerprint::of(fields)
247 }
248
249 #[must_use]
251 pub fn additions(&self) -> usize {
252 self.hunks.iter().map(Hunk::additions).sum()
253 }
254
255 #[must_use]
257 pub fn deletions(&self) -> usize {
258 self.hunks.iter().map(Hunk::deletions).sum()
259 }
260
261 #[must_use]
263 pub fn language(&self) -> &str {
264 Path::new(self.path.as_str())
265 .extension()
266 .and_then(OsStr::to_str)
267 .unwrap_or_default()
268 }
269
270 #[must_use]
271 pub fn line(&self, hunk: usize, line: usize) -> Option<&PatchLine> {
272 self.hunks.get(hunk)?.lines.get(line)
273 }
274}