jj_lib/diff_presentation/
unified.rs1use std::ops::Range;
18
19use bstr::BStr;
20use bstr::BString;
21use thiserror::Error;
22
23use super::DiffTokenType;
24use super::DiffTokenVec;
25use super::FileContent;
26use super::LineCompareMode;
27use super::diff_by_line;
28use super::file_content_for_diff;
29use super::unzip_diff_hunks_to_lines;
30use crate::backend::BackendError;
31use crate::backend::MergedTreeValueExt as _;
32use crate::conflicts::ConflictMaterializeOptions;
33use crate::conflicts::MaterializedTreeValue;
34use crate::conflicts::materialize_merge_result_to_bytes;
35use crate::diff::ContentDiff;
36use crate::diff::DiffHunkKind;
37use crate::merge::Diff;
38use crate::object_id::ObjectId as _;
39use crate::repo_path::RepoPath;
40
41#[derive(Clone, Debug)]
42pub struct GitDiffPart {
43 pub mode: Option<&'static str>,
45 pub hash: String,
46 pub content: FileContent<BString>,
47}
48
49#[derive(Debug, Error)]
50pub enum UnifiedDiffError {
51 #[error(transparent)]
52 Backend(#[from] BackendError),
53 #[error("Access denied to {path}")]
54 AccessDenied {
55 path: String,
56 source: Box<dyn std::error::Error + Send + Sync>,
57 },
58}
59
60pub async fn git_diff_part(
61 path: &RepoPath,
62 value: MaterializedTreeValue,
63 materialize_options: &ConflictMaterializeOptions,
64) -> Result<GitDiffPart, UnifiedDiffError> {
65 const DUMMY_HASH: &str = "0000000000";
66 let mode;
67 let mut hash;
68 let content;
69 match value {
70 MaterializedTreeValue::Absent => {
71 return Ok(GitDiffPart {
72 mode: None,
73 hash: DUMMY_HASH.to_owned(),
74 content: FileContent {
75 is_binary: false,
76 contents: BString::default(),
77 },
78 });
79 }
80 MaterializedTreeValue::AccessDenied(err) => {
81 return Err(UnifiedDiffError::AccessDenied {
82 path: path.as_internal_file_string().to_owned(),
83 source: err,
84 });
85 }
86 MaterializedTreeValue::File(mut file) => {
87 mode = if file.executable { "100755" } else { "100644" };
88 hash = file.id.hex();
89 content = file_content_for_diff(path, &mut file, |content| content).await?;
90 }
91 MaterializedTreeValue::Symlink { id, target } => {
92 mode = "120000";
93 hash = id.hex();
94 content = FileContent {
95 is_binary: false,
97 contents: target.into(),
98 };
99 }
100 MaterializedTreeValue::GitSubmodule(id) => {
101 mode = "040000";
103 hash = id.hex();
104 content = FileContent {
105 is_binary: false,
106 contents: BString::default(),
107 };
108 }
109 MaterializedTreeValue::FileConflict(file) => {
110 mode = match file.executable {
111 Some(true) => "100755",
112 Some(false) | None => "100644",
113 };
114 hash = DUMMY_HASH.to_owned();
115 content = FileContent {
116 is_binary: false, contents: materialize_merge_result_to_bytes(
118 &file.contents,
119 &file.labels,
120 materialize_options,
121 ),
122 };
123 }
124 MaterializedTreeValue::OtherConflict { id, labels } => {
125 mode = "100644";
126 hash = DUMMY_HASH.to_owned();
127 content = FileContent {
128 is_binary: false,
129 contents: id.describe(&labels).into(),
130 };
131 }
132 MaterializedTreeValue::Tree(_) => {
133 panic!("Unexpected tree in diff at path {path:?}");
134 }
135 }
136 hash.truncate(10);
137 Ok(GitDiffPart {
138 mode: Some(mode),
139 hash,
140 content,
141 })
142}
143
144#[derive(Clone, Copy, Debug, Eq, PartialEq)]
145pub enum DiffLineType {
146 Context,
147 Removed,
148 Added,
149}
150
151pub struct UnifiedDiffHunk<'content> {
152 pub left_line_range: Range<usize>,
153 pub right_line_range: Range<usize>,
154 pub lines: Vec<(DiffLineType, DiffTokenVec<'content>)>,
155}
156
157impl<'content> UnifiedDiffHunk<'content> {
158 fn extend_context_lines(&mut self, lines: impl IntoIterator<Item = &'content [u8]>) {
159 let old_len = self.lines.len();
160 self.lines.extend(lines.into_iter().map(|line| {
161 let tokens = vec![(DiffTokenType::Matching, line)];
162 (DiffLineType::Context, tokens)
163 }));
164 self.left_line_range.end += self.lines.len() - old_len;
165 self.right_line_range.end += self.lines.len() - old_len;
166 }
167
168 fn extend_removed_lines(&mut self, lines: impl IntoIterator<Item = DiffTokenVec<'content>>) {
169 let old_len = self.lines.len();
170 self.lines
171 .extend(lines.into_iter().map(|line| (DiffLineType::Removed, line)));
172 self.left_line_range.end += self.lines.len() - old_len;
173 }
174
175 fn extend_added_lines(&mut self, lines: impl IntoIterator<Item = DiffTokenVec<'content>>) {
176 let old_len = self.lines.len();
177 self.lines
178 .extend(lines.into_iter().map(|line| (DiffLineType::Added, line)));
179 self.right_line_range.end += self.lines.len() - old_len;
180 }
181}
182
183pub fn unified_diff_hunks(
184 contents: Diff<&BStr>,
185 context: usize,
186 options: LineCompareMode,
187) -> Vec<UnifiedDiffHunk<'_>> {
188 let mut hunks = vec![];
189 let mut current_hunk = UnifiedDiffHunk {
190 left_line_range: 0..0,
191 right_line_range: 0..0,
192 lines: vec![],
193 };
194 let diff = diff_by_line(contents.into_array(), &options);
195 let mut diff_hunks = diff.hunks().peekable();
196 while let Some(hunk) = diff_hunks.next() {
197 match hunk.kind {
198 DiffHunkKind::Matching => {
199 let [_, right] = hunk.contents[..].try_into().unwrap();
203 let mut lines = right.split_inclusive(|b| *b == b'\n').fuse();
204 if !current_hunk.lines.is_empty() {
205 current_hunk.extend_context_lines(lines.by_ref().take(context));
207 }
208 let before_lines = if diff_hunks.peek().is_some() {
209 lines.by_ref().rev().take(context).collect()
210 } else {
211 vec![] };
213 let num_skip_lines = lines.count();
214 if num_skip_lines > 0 {
215 let left_start = current_hunk.left_line_range.end + num_skip_lines;
216 let right_start = current_hunk.right_line_range.end + num_skip_lines;
217 if !current_hunk.lines.is_empty() {
218 hunks.push(current_hunk);
219 }
220 current_hunk = UnifiedDiffHunk {
221 left_line_range: left_start..left_start,
222 right_line_range: right_start..right_start,
223 lines: vec![],
224 };
225 }
226 current_hunk.extend_context_lines(before_lines.into_iter().rev());
228 }
229 DiffHunkKind::Different => {
230 let lines = unzip_diff_hunks_to_lines(ContentDiff::by_word(hunk.contents).hunks());
231 current_hunk.extend_removed_lines(lines.before);
232 current_hunk.extend_added_lines(lines.after);
233 }
234 }
235 }
236 if !current_hunk.lines.is_empty() {
237 hunks.push(current_hunk);
238 }
239 hunks
240}