1use crate::config::ConfigSet;
4
5use super::filter::DiffFilter;
6use super::{indent_heuristic_from_config, resolve_indent_heuristic, WhitespaceMode};
7
8#[non_exhaustive]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum DiffAlgorithm {
12 #[default]
14 Myers,
15 Minimal,
17 Patience,
19 Histogram,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
25#[error("unknown diff algorithm `{0}`")]
26pub struct UnknownDiffAlgorithm(pub String);
27
28impl std::str::FromStr for DiffAlgorithm {
29 type Err = UnknownDiffAlgorithm;
30
31 fn from_str(s: &str) -> Result<Self, Self::Err> {
32 match s.to_ascii_lowercase().as_str() {
33 "myers" | "default" => Ok(Self::Myers),
34 "minimal" => Ok(Self::Minimal),
35 "patience" => Ok(Self::Patience),
36 "histogram" => Ok(Self::Histogram),
37 other => Err(UnknownDiffAlgorithm(other.to_owned())),
38 }
39 }
40}
41
42impl DiffAlgorithm {
43 #[must_use]
45 pub fn similar_algorithm(self) -> similar::Algorithm {
46 match self {
47 Self::Myers | Self::Histogram => similar::Algorithm::Myers,
48 Self::Minimal => similar::Algorithm::Lcs,
49 Self::Patience => similar::Algorithm::Patience,
50 }
51 }
52
53 #[must_use]
55 pub fn uses_git_histogram(self) -> bool {
56 matches!(self, Self::Histogram)
57 }
58}
59
60pub const RENAME_DETECTION_DISABLED: u32 = 101;
63
64#[non_exhaustive]
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum RenameDetection {
68 Off,
70 Similarity(u8),
72}
73
74impl Default for RenameDetection {
75 fn default() -> Self {
77 Self::Similarity(50)
78 }
79}
80
81impl RenameDetection {
82 #[must_use]
84 pub fn threshold_percent(self) -> Option<u32> {
85 match self {
86 Self::Off => None,
87 Self::Similarity(p) => Some(u32::from(p)),
88 }
89 }
90}
91
92pub const WORD_LEVEL_INLINE_MAX_CHANGED_FILES: usize = 64;
94
95#[non_exhaustive]
97#[derive(Debug, Clone)]
98pub struct DiffOptions {
99 pub context_lines: usize,
101 pub inter_hunk_context: usize,
103 pub algorithm: DiffAlgorithm,
105 pub indent_heuristic: bool,
107 pub renames: RenameDetection,
110 pub filter: Option<DiffFilter>,
112 pub whitespace: WhitespaceMode,
114 pub word_level_inline: bool,
116 pub function_context: bool,
118 #[doc(hidden)]
120 pub blob_read_counter: Option<std::sync::Arc<std::sync::atomic::AtomicUsize>>,
121 pub(crate) no_index_stat_only: bool,
123}
124
125impl Default for DiffOptions {
126 fn default() -> Self {
127 Self {
128 context_lines: 3,
129 inter_hunk_context: 0,
130 algorithm: DiffAlgorithm::Myers,
131 indent_heuristic: true,
132 renames: RenameDetection::default(),
133 filter: None,
134 whitespace: WhitespaceMode::default(),
135 word_level_inline: true,
136 function_context: false,
137 blob_read_counter: None,
138 no_index_stat_only: false,
139 }
140 }
141}
142
143impl PartialEq for DiffOptions {
144 fn eq(&self, other: &Self) -> bool {
145 self.context_lines == other.context_lines
146 && self.inter_hunk_context == other.inter_hunk_context
147 && self.algorithm == other.algorithm
148 && self.indent_heuristic == other.indent_heuristic
149 && self.renames == other.renames
150 && self.filter == other.filter
151 && self.whitespace == other.whitespace
152 && self.word_level_inline == other.word_level_inline
153 && self.function_context == other.function_context
154 && self.no_index_stat_only == other.no_index_stat_only
155 }
156}
157
158impl DiffOptions {
159 #[must_use]
161 pub fn with_context_lines(mut self, context_lines: usize) -> Self {
162 self.context_lines = context_lines;
163 self
164 }
165
166 #[must_use]
168 pub fn with_inter_hunk_context(mut self, inter_hunk_context: usize) -> Self {
169 self.inter_hunk_context = inter_hunk_context;
170 self
171 }
172
173 #[must_use]
175 pub fn with_algorithm(mut self, algorithm: DiffAlgorithm) -> Self {
176 self.algorithm = algorithm;
177 self
178 }
179
180 #[must_use]
182 pub fn with_indent_heuristic(mut self, indent_heuristic: bool) -> Self {
183 self.indent_heuristic = indent_heuristic;
184 self
185 }
186
187 #[must_use]
189 pub fn with_renames(mut self, renames: RenameDetection) -> Self {
190 self.renames = renames;
191 self
192 }
193
194 #[must_use]
196 pub fn with_filter(mut self, filter: Option<DiffFilter>) -> Self {
197 self.filter = filter;
198 self
199 }
200
201 #[must_use]
203 pub fn rename_similarity_threshold(&self) -> Option<u32> {
204 match self.renames {
205 RenameDetection::Off => None,
206 RenameDetection::Similarity(p) => Some(u32::from(p)),
207 }
208 }
209
210 #[must_use]
212 pub fn rename_threshold_u32(&self) -> u32 {
213 self.rename_similarity_threshold()
214 .unwrap_or(RENAME_DETECTION_DISABLED)
215 }
216
217 #[must_use]
219 pub fn with_word_level_inline(mut self, word_level_inline: bool) -> Self {
220 self.word_level_inline = word_level_inline;
221 self
222 }
223
224 #[must_use]
226 pub fn with_whitespace(mut self, whitespace: WhitespaceMode) -> Self {
227 self.whitespace = whitespace;
228 self
229 }
230
231 #[must_use]
233 pub fn with_no_index_stat_only(mut self, stat_only: bool) -> Self {
234 self.no_index_stat_only = stat_only;
235 self
236 }
237
238 #[must_use]
241 pub fn cap_word_level_inline_for_changed_files(self, changed_files: usize) -> Self {
242 let mut opts = self;
243 if opts.word_level_inline && changed_files > WORD_LEVEL_INLINE_MAX_CHANGED_FILES {
244 opts.word_level_inline = false;
245 }
246 opts
247 }
248
249 #[must_use]
252 pub fn from_config(config: &ConfigSet) -> Self {
253 let mut opts = Self::default();
254 if let Some(name) = config.get("diff.algorithm") {
255 if let Ok(parsed) = name.parse::<DiffAlgorithm>() {
256 opts.algorithm = parsed;
257 }
258 }
259 if let Some(v) = config.get("diff.context") {
260 if let Ok(n) = v.parse::<usize>() {
261 opts.context_lines = n;
262 }
263 }
264 if let Some(v) = config.get("diff.interHunkContext") {
265 if let Ok(n) = v.parse::<usize>() {
266 opts.inter_hunk_context = n;
267 }
268 }
269 opts.indent_heuristic = indent_heuristic_from_config(config);
270 opts.renames = renames_from_config(config);
271 if config
272 .get_bool("diff.ignoreSpaceChange")
273 .and_then(|r| r.ok())
274 .unwrap_or(false)
275 {
276 opts.whitespace.ignore_space_change = true;
277 }
278 opts
279 }
280
281 #[must_use]
283 pub fn resolve_indent_heuristic(
284 config: &ConfigSet,
285 cli_indent_heuristic: bool,
286 cli_no_indent_heuristic: bool,
287 ) -> bool {
288 resolve_indent_heuristic(config, cli_indent_heuristic, cli_no_indent_heuristic)
289 }
290}
291
292#[must_use]
294pub fn renames_from_config(config: &ConfigSet) -> RenameDetection {
295 match config.get("diff.renames") {
296 Some(v) => {
297 let v = v.trim();
298 if v.eq_ignore_ascii_case("false")
299 || v.eq_ignore_ascii_case("no")
300 || v.eq_ignore_ascii_case("off")
301 || v == "0"
302 {
303 RenameDetection::Off
304 } else {
305 RenameDetection::default()
306 }
307 }
308 None => RenameDetection::default(),
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use crate::config::{ConfigFile, ConfigScope, ConfigSet};
316
317 fn config_from(snippet: &str) -> ConfigSet {
318 let file = ConfigFile::parse(std::path::Path::new("test"), snippet, ConfigScope::Local)
319 .expect("parse");
320 let mut set = ConfigSet::new();
321 set.merge(&file);
322 set
323 }
324
325 #[test]
326 fn diff_algorithm_from_str_roundtrip_and_error() {
327 for (s, expected) in [
328 ("myers", DiffAlgorithm::Myers),
329 ("default", DiffAlgorithm::Myers),
330 ("MYERS", DiffAlgorithm::Myers),
331 ("minimal", DiffAlgorithm::Minimal),
332 ("patience", DiffAlgorithm::Patience),
333 ("histogram", DiffAlgorithm::Histogram),
334 ] {
335 assert_eq!(s.parse::<DiffAlgorithm>().expect("parse"), expected);
336 }
337 assert!(matches!(
338 "bogus".parse::<DiffAlgorithm>(),
339 Err(UnknownDiffAlgorithm(_))
340 ));
341 }
342
343 #[test]
344 fn diff_options_from_config_reads_algorithm_context_renames() {
345 let cfg = config_from(
346 "[diff]\n\
347 \talgorithm = patience\n\
348 \tcontext = 5\n\
349 \tinterHunkContext = 2\n\
350 \tindentHeuristic = false\n\
351 \trenames = false\n",
352 );
353 let opts = DiffOptions::from_config(&cfg);
354 assert_eq!(opts.algorithm, DiffAlgorithm::Patience);
355 assert_eq!(opts.context_lines, 5);
356 assert_eq!(opts.inter_hunk_context, 2);
357 assert!(!opts.indent_heuristic);
358 assert_eq!(opts.renames, RenameDetection::Off);
359
360 for on in [
361 "",
362 "[diff]\n\trenames = true\n",
363 "[diff]\n\trenames = copies\n",
364 ] {
365 assert_eq!(
366 DiffOptions::from_config(&config_from(on)).renames,
367 RenameDetection::Similarity(50),
368 "config {on:?}"
369 );
370 }
371 assert_eq!(
372 DiffOptions::default().renames,
373 RenameDetection::Similarity(50)
374 );
375
376 for off in ["no", "0", "off", "OFF"] {
377 let cfg = config_from(&format!("[diff]\n\trenames = {off}\n"));
378 assert_eq!(
379 DiffOptions::from_config(&cfg).renames,
380 RenameDetection::Off,
381 "renames={off}"
382 );
383 }
384 }
385
386 #[test]
387 fn cap_word_level_inline_for_changed_files() {
388 let on = DiffOptions {
389 word_level_inline: true,
390 ..DiffOptions::default()
391 };
392 assert!(
393 on.clone()
394 .cap_word_level_inline_for_changed_files(WORD_LEVEL_INLINE_MAX_CHANGED_FILES)
395 .word_level_inline
396 );
397 assert!(
398 !on.cap_word_level_inline_for_changed_files(WORD_LEVEL_INLINE_MAX_CHANGED_FILES + 1)
399 .word_level_inline
400 );
401 }
402}