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,
109 pub filter: Option<DiffFilter>,
111 pub whitespace: WhitespaceMode,
113 pub word_level_inline: bool,
115 pub function_context: bool,
117 #[doc(hidden)]
119 pub blob_read_counter: Option<std::sync::Arc<std::sync::atomic::AtomicUsize>>,
120 pub(crate) no_index_stat_only: bool,
122}
123
124impl Default for DiffOptions {
125 fn default() -> Self {
126 Self {
127 context_lines: 3,
128 inter_hunk_context: 0,
129 algorithm: DiffAlgorithm::Myers,
130 indent_heuristic: true,
131 renames: RenameDetection::default(),
132 filter: None,
133 whitespace: WhitespaceMode::default(),
134 word_level_inline: true,
135 function_context: false,
136 blob_read_counter: None,
137 no_index_stat_only: false,
138 }
139 }
140}
141
142impl PartialEq for DiffOptions {
143 fn eq(&self, other: &Self) -> bool {
144 self.context_lines == other.context_lines
145 && self.inter_hunk_context == other.inter_hunk_context
146 && self.algorithm == other.algorithm
147 && self.indent_heuristic == other.indent_heuristic
148 && self.renames == other.renames
149 && self.filter == other.filter
150 && self.whitespace == other.whitespace
151 && self.word_level_inline == other.word_level_inline
152 && self.function_context == other.function_context
153 && self.no_index_stat_only == other.no_index_stat_only
154 }
155}
156
157impl DiffOptions {
158 #[must_use]
160 pub fn with_context_lines(mut self, context_lines: usize) -> Self {
161 self.context_lines = context_lines;
162 self
163 }
164
165 #[must_use]
167 pub fn with_inter_hunk_context(mut self, inter_hunk_context: usize) -> Self {
168 self.inter_hunk_context = inter_hunk_context;
169 self
170 }
171
172 #[must_use]
174 pub fn with_algorithm(mut self, algorithm: DiffAlgorithm) -> Self {
175 self.algorithm = algorithm;
176 self
177 }
178
179 #[must_use]
181 pub fn with_indent_heuristic(mut self, indent_heuristic: bool) -> Self {
182 self.indent_heuristic = indent_heuristic;
183 self
184 }
185
186 #[must_use]
188 pub fn with_renames(mut self, renames: RenameDetection) -> Self {
189 self.renames = renames;
190 self
191 }
192
193 #[must_use]
195 pub fn with_filter(mut self, filter: Option<DiffFilter>) -> Self {
196 self.filter = filter;
197 self
198 }
199
200 #[must_use]
202 pub fn rename_similarity_threshold(&self) -> Option<u32> {
203 match self.renames {
204 RenameDetection::Off => None,
205 RenameDetection::Similarity(p) => Some(u32::from(p)),
206 }
207 }
208
209 #[must_use]
211 pub fn rename_threshold_u32(&self) -> u32 {
212 self.rename_similarity_threshold()
213 .unwrap_or(RENAME_DETECTION_DISABLED)
214 }
215
216 #[must_use]
218 pub fn with_word_level_inline(mut self, word_level_inline: bool) -> Self {
219 self.word_level_inline = word_level_inline;
220 self
221 }
222
223 #[must_use]
225 pub fn with_whitespace(mut self, whitespace: WhitespaceMode) -> Self {
226 self.whitespace = whitespace;
227 self
228 }
229
230 #[must_use]
232 pub fn with_no_index_stat_only(mut self, stat_only: bool) -> Self {
233 self.no_index_stat_only = stat_only;
234 self
235 }
236
237 #[must_use]
240 pub fn cap_word_level_inline_for_changed_files(self, changed_files: usize) -> Self {
241 let mut opts = self;
242 if opts.word_level_inline && changed_files > WORD_LEVEL_INLINE_MAX_CHANGED_FILES {
243 opts.word_level_inline = false;
244 }
245 opts
246 }
247
248 #[must_use]
251 pub fn from_config(config: &ConfigSet) -> Self {
252 let mut opts = Self::default();
253 if let Some(name) = config.get("diff.algorithm") {
254 if let Ok(parsed) = name.parse::<DiffAlgorithm>() {
255 opts.algorithm = parsed;
256 }
257 }
258 if let Some(v) = config.get("diff.context") {
259 if let Ok(n) = v.parse::<usize>() {
260 opts.context_lines = n;
261 }
262 }
263 if let Some(v) = config.get("diff.interHunkContext") {
264 if let Ok(n) = v.parse::<usize>() {
265 opts.inter_hunk_context = n;
266 }
267 }
268 opts.indent_heuristic = indent_heuristic_from_config(config);
269 opts.renames = renames_from_config(config);
270 if config
271 .get_bool("diff.ignoreSpaceChange")
272 .and_then(|r| r.ok())
273 .unwrap_or(false)
274 {
275 opts.whitespace.ignore_space_change = true;
276 }
277 opts
278 }
279
280 #[must_use]
282 pub fn resolve_indent_heuristic(
283 config: &ConfigSet,
284 cli_indent_heuristic: bool,
285 cli_no_indent_heuristic: bool,
286 ) -> bool {
287 resolve_indent_heuristic(config, cli_indent_heuristic, cli_no_indent_heuristic)
288 }
289}
290
291#[must_use]
293pub fn renames_from_config(config: &ConfigSet) -> RenameDetection {
294 match config.get("diff.renames") {
295 Some(v) => {
296 let v = v.trim();
297 if v.eq_ignore_ascii_case("false")
298 || v.eq_ignore_ascii_case("no")
299 || v.eq_ignore_ascii_case("off")
300 || v == "0"
301 {
302 RenameDetection::Off
303 } else {
304 RenameDetection::default()
305 }
306 }
307 None => RenameDetection::default(),
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use crate::config::{ConfigFile, ConfigScope, ConfigSet};
315
316 fn config_from(snippet: &str) -> ConfigSet {
317 let file = ConfigFile::parse(std::path::Path::new("test"), snippet, ConfigScope::Local)
318 .expect("parse");
319 let mut set = ConfigSet::new();
320 set.merge(&file);
321 set
322 }
323
324 #[test]
325 fn diff_algorithm_from_str_roundtrip_and_error() {
326 for (s, expected) in [
327 ("myers", DiffAlgorithm::Myers),
328 ("default", DiffAlgorithm::Myers),
329 ("MYERS", DiffAlgorithm::Myers),
330 ("minimal", DiffAlgorithm::Minimal),
331 ("patience", DiffAlgorithm::Patience),
332 ("histogram", DiffAlgorithm::Histogram),
333 ] {
334 assert_eq!(s.parse::<DiffAlgorithm>().expect("parse"), expected);
335 }
336 assert!(matches!(
337 "bogus".parse::<DiffAlgorithm>(),
338 Err(UnknownDiffAlgorithm(_))
339 ));
340 }
341
342 #[test]
343 fn diff_options_from_config_reads_algorithm_context_renames() {
344 let cfg = config_from(
345 "[diff]\n\
346 \talgorithm = patience\n\
347 \tcontext = 5\n\
348 \tinterHunkContext = 2\n\
349 \tindentHeuristic = false\n\
350 \trenames = false\n",
351 );
352 let opts = DiffOptions::from_config(&cfg);
353 assert_eq!(opts.algorithm, DiffAlgorithm::Patience);
354 assert_eq!(opts.context_lines, 5);
355 assert_eq!(opts.inter_hunk_context, 2);
356 assert!(!opts.indent_heuristic);
357 assert_eq!(opts.renames, RenameDetection::Off);
358
359 for on in [
360 "",
361 "[diff]\n\trenames = true\n",
362 "[diff]\n\trenames = copies\n",
363 ] {
364 assert_eq!(
365 DiffOptions::from_config(&config_from(on)).renames,
366 RenameDetection::Similarity(50),
367 "config {on:?}"
368 );
369 }
370 assert_eq!(
371 DiffOptions::default().renames,
372 RenameDetection::Similarity(50)
373 );
374
375 for off in ["no", "0", "off", "OFF"] {
376 let cfg = config_from(&format!("[diff]\n\trenames = {off}\n"));
377 assert_eq!(
378 DiffOptions::from_config(&cfg).renames,
379 RenameDetection::Off,
380 "renames={off}"
381 );
382 }
383 }
384
385 #[test]
386 fn cap_word_level_inline_for_changed_files() {
387 let on = DiffOptions {
388 word_level_inline: true,
389 ..DiffOptions::default()
390 };
391 assert!(
392 on.clone()
393 .cap_word_level_inline_for_changed_files(WORD_LEVEL_INLINE_MAX_CHANGED_FILES)
394 .word_level_inline
395 );
396 assert!(
397 !on.cap_word_level_inline_for_changed_files(WORD_LEVEL_INLINE_MAX_CHANGED_FILES + 1)
398 .word_level_inline
399 );
400 }
401}