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.can_derive_patch() {
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 fn can_derive_patch(&self) -> bool {
153 !self.binary
154 && self.side_text(DiffSide::Old).is_some()
155 && self.side_text(DiffSide::New).is_some()
156 }
157
158 pub const fn uncaptured_source(status: FileStatus, side: DiffSide) -> SourceResult {
166 if Self::side_is_absent(status, side) {
167 Err(SourceUnavailable::Absent)
168 } else {
169 Err(SourceUnavailable::NotCaptured)
170 }
171 }
172
173 const fn side_is_absent(status: FileStatus, side: DiffSide) -> bool {
174 matches!(
175 (status, side),
176 (FileStatus::Added | FileStatus::Untracked, DiffSide::Old)
177 | (FileStatus::Deleted, DiffSide::New)
178 )
179 }
180
181 pub const fn source(&self, side: DiffSide) -> &SourceResult {
183 match side {
184 DiffSide::Old => &self.old_source,
185 DiffSide::New => &self.new_source,
186 }
187 }
188
189 pub(crate) const fn source_side(&self) -> DiffSide {
190 if matches!(self.status, FileStatus::Deleted) {
191 DiffSide::Old
192 } else {
193 DiffSide::New
194 }
195 }
196
197 #[must_use]
199 pub fn source_document(&self, side: DiffSide) -> Option<&Arc<SourceDocument>> {
200 self.source(side).as_ref().ok()
201 }
202
203 #[must_use]
205 pub fn source_unavailable(&self, side: DiffSide) -> Option<&SourceUnavailable> {
206 self.source(side).as_ref().err()
207 }
208
209 fn side_text(&self, side: DiffSide) -> Option<&str> {
212 match self.source(side) {
213 Ok(source) => Some(source.text()),
214 Err(SourceUnavailable::Absent) if Self::side_is_absent(self.status, side) => Some(""),
215 Err(_) => None,
216 }
217 }
218
219 #[must_use]
226 pub fn content_id(&self) -> Fingerprint {
227 let mut fields: Vec<Cow<'_, [u8]>> = vec![
228 Cow::Borrowed(FILE_CONTENT_DOMAIN),
229 Cow::Owned(vec![
230 u8::try_from(self.status.code()).unwrap_or(b'?'),
231 u8::from(self.binary),
232 ]),
233 ];
234 let mut complete = true;
235 for side in [DiffSide::Old, DiffSide::New] {
236 match self.source(side) {
237 Ok(source) => fields.push(Cow::Owned(source.content_id().as_bytes().to_vec())),
238 Err(reason) => {
239 complete = false;
240 fields.push(Cow::Owned(reason.to_string().into_bytes()));
241 }
242 }
243 }
244 if !complete {
245 for hunk in &self.hunks {
246 fields.push(Cow::Borrowed(hunk.header.as_bytes()));
247 for line in &hunk.lines {
248 fields.push(Cow::Borrowed(line.kind.as_str().as_bytes()));
249 fields.push(Cow::Borrowed(line.text.as_bytes()));
250 fields.push(Cow::Owned(
251 [line.old_line_no, line.new_line_no]
252 .iter()
253 .flat_map(|number| number.unwrap_or(0).to_le_bytes())
254 .chain([u8::from(line.no_newline)])
255 .collect(),
256 ));
257 }
258 }
259 }
260 Fingerprint::of(fields)
261 }
262
263 #[must_use]
265 pub fn additions(&self) -> usize {
266 self.hunks.iter().map(Hunk::additions).sum()
267 }
268
269 #[must_use]
271 pub fn deletions(&self) -> usize {
272 self.hunks.iter().map(Hunk::deletions).sum()
273 }
274
275 #[must_use]
277 pub fn language(&self) -> &str {
278 Path::new(self.path.as_str())
279 .extension()
280 .and_then(OsStr::to_str)
281 .unwrap_or_default()
282 }
283
284 #[must_use]
285 pub fn line(&self, hunk: usize, line: usize) -> Option<&PatchLine> {
286 self.hunks.get(hunk)?.lines.get(line)
287 }
288}