1use derive_builder::Builder;
10use git_checks_core::impl_prelude::*;
11use lazy_static::lazy_static;
12use regex::Regex;
13
14#[derive(Builder, Debug, Clone, Copy)]
19#[builder(field(private))]
20pub struct RejectConflictPaths {
21 #[builder(default = "true")]
30 require_base_exist: bool,
31}
32
33lazy_static! {
34 static ref CONFLICT_FILE_PATH: Regex = Regex::new(
35 "^(?P<base>.*)\
36 (?P<kind>_(BACKUP|BASE|LOCAL|REMOTE))\
37 (?P<pid>_[0-9]+)\
38 (?P<ext>(\\..*)?)$",
39 )
40 .unwrap();
41}
42const ORIG_SUFFIX: &str = ".orig";
43
44impl RejectConflictPaths {
45 pub fn builder() -> RejectConflictPathsBuilder {
47 Default::default()
48 }
49
50 fn check_conflict_path_name(
51 self,
52 ctx: &CheckGitContext,
53 path: &str,
54 ) -> Result<bool, CommitError> {
55 if let Some(file_path) = CONFLICT_FILE_PATH.captures(path) {
56 if !self.require_base_exist {
57 return Ok(true);
58 }
59
60 let base = file_path
61 .name("base")
62 .expect("the conflict file path regex should have a 'base' group");
63 let ext = file_path
64 .name("ext")
65 .expect("the conflict file path regex should have a 'ext' group");
66
67 let basepath = format!("{}{}", base.as_str(), ext.as_str());
68 Self::check_for_path(ctx, &basepath)
69 } else {
70 Ok(false)
71 }
72 }
73
74 fn check_orig_path_name(self, ctx: &CheckGitContext, path: &str) -> Result<bool, CommitError> {
75 if path.ends_with(ORIG_SUFFIX) {
76 if !self.require_base_exist {
77 return Ok(true);
78 }
79
80 let basepath = path.trim_end_matches(ORIG_SUFFIX);
81 Self::check_for_path(ctx, basepath)
82 } else {
83 Ok(false)
84 }
85 }
86
87 fn check_for_path(ctx: &CheckGitContext, path: &str) -> Result<bool, CommitError> {
88 let cat_file = ctx
89 .git()
90 .arg("cat-file")
91 .arg("-e")
92 .arg(format!(":{}", path))
93 .output()
94 .map_err(|err| GitError::subcommand("cat-file -e", err))?;
95 Ok(cat_file.status.success())
96 }
97}
98
99impl Default for RejectConflictPaths {
100 fn default() -> Self {
101 RejectConflictPaths {
102 require_base_exist: true,
103 }
104 }
105}
106
107impl ContentCheck for RejectConflictPaths {
108 fn name(&self) -> &str {
109 "reject-conflict-paths"
110 }
111
112 fn check(
113 &self,
114 ctx: &CheckGitContext,
115 content: &dyn Content,
116 ) -> Result<CheckResult, Box<dyn Error>> {
117 let mut result = CheckResult::new();
118
119 for diff in content.diffs() {
120 match diff.status {
121 StatusChange::Added | StatusChange::Modified(_) => (),
122 _ => continue,
123 }
124
125 if self.check_conflict_path_name(ctx, diff.name.as_str())? {
126 result.add_error(format!(
127 "{}it appears as though `{}` is a merge conflict resolution file and cannot \
128 be added.",
129 commit_prefix_str(content, "not allowed;"),
130 diff.name,
131 ));
132 }
133
134 if self.check_orig_path_name(ctx, diff.name.as_str())? {
135 result.add_error(format!(
136 "{}it appears as though `{}` is a merge conflict backup file and cannot be \
137 added.",
138 commit_prefix_str(content, "not allowed;"),
139 diff.name,
140 ));
141 }
142 }
143
144 Ok(result)
145 }
146}
147
148#[cfg(feature = "config")]
149pub(crate) mod config {
150 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
151 use serde::Deserialize;
152 #[cfg(test)]
153 use serde_json::json;
154
155 use crate::RejectConflictPaths;
156
157 #[derive(Deserialize, Debug)]
164 pub struct RejectConflictPathsConfig {
165 #[serde(default)]
166 require_base_exist: Option<bool>,
167 }
168
169 impl IntoCheck for RejectConflictPathsConfig {
170 type Check = RejectConflictPaths;
171
172 fn into_check(self) -> Self::Check {
173 let mut builder = RejectConflictPaths::builder();
174
175 if let Some(require_base_exist) = self.require_base_exist {
176 builder.require_base_exist(require_base_exist);
177 }
178
179 builder
180 .build()
181 .expect("configuration mismatch for `RejectConflictPaths`")
182 }
183 }
184
185 register_checks! {
186 RejectConflictPathsConfig {
187 "reject_conflict_paths" => CommitCheckConfig,
188 "reject_conflict_paths/topic" => TopicCheckConfig,
189 },
190 }
191
192 #[test]
193 fn test_reject_conflict_paths_config_empty() {
194 let json = json!({});
195 let check: RejectConflictPathsConfig = serde_json::from_value(json).unwrap();
196
197 assert_eq!(check.require_base_exist, None);
198
199 let check = check.into_check();
200
201 assert!(check.require_base_exist);
202 }
203
204 #[test]
205 fn test_reject_conflict_paths_config_all_fields() {
206 let json = json!({
207 "require_base_exist": false,
208 });
209 let check: RejectConflictPathsConfig = serde_json::from_value(json).unwrap();
210
211 assert_eq!(check.require_base_exist, Some(false));
212
213 let check = check.into_check();
214
215 assert!(!check.require_base_exist);
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use git_checks_core::{Check, TopicCheck};
222
223 use crate::test::*;
224 use crate::RejectConflictPaths;
225
226 const MERGE_CONFLICT_NO_BASE: &str = "52710df4a433731545ef99440edeb431b3160fc6";
227 const MERGE_CONFLICT_ORIG_NO_BASE: &str = "672a6a32b045bec6f93b9b1b5252611f40437a07";
228 const DELETE_CONFLICT_NO_BASE: &str = "7ed69370ce3ba45bca9c4565d142b81685088247";
229
230 const MERGE_CONFLICT_NO_EXT: &str = "f31b2e2cb75083d174097db7054cbc9e5836bad7";
231 const MERGE_CONFLICT_WITH_EXT: &str = "59f02bdb6c404c8f7cfb32700645c149148c089b";
232 const MERGE_CONFLICT_TWO_EXT: &str = "e9421eadfcac3c67a090444ef2ac859e86a8a2e0";
233 const MERGE_CONFLICT_ORIG_EXT: &str = "4abcf323b81c757e668ce1936f475b085d6852e8";
234
235 const MERGE_CONFLICT_NO_EXT_FIXED: &str = "63c923b5be729f672eddcd4b1e979a7d3d22606f";
236 const MERGE_CONFLICT_WITH_EXT_FIXED: &str = "8fcdd0d920ef47d0294229fdad4b3abd3ebdc43b";
237 const MERGE_CONFLICT_TWO_EXT_FIXED: &str = "0bf291f1f8a320abfb63776c56e1d1497231e24e";
238 const MERGE_CONFLICT_ORIG_EXT_FIXED: &str = "e1fd80490cd7f6556120aad6309d8fb95818b6ef";
239
240 #[test]
241 fn test_reject_conflict_paths_builder_default() {
242 assert!(RejectConflictPaths::builder().build().is_ok());
243 }
244
245 #[test]
246 fn test_reject_conflict_paths_name_commit() {
247 let check = RejectConflictPaths::default();
248 assert_eq!(Check::name(&check), "reject-conflict-paths");
249 }
250
251 #[test]
252 fn test_reject_conflict_paths_name_topic() {
253 let check = RejectConflictPaths::default();
254 assert_eq!(TopicCheck::name(&check), "reject-conflict-paths");
255 }
256
257 #[test]
258 fn test_reject_conflict_paths_no_base() {
259 let check = RejectConflictPaths::default();
260 run_check_ok(
261 "test_reject_conflict_paths_no_base",
262 MERGE_CONFLICT_NO_BASE,
263 check,
264 );
265 }
266
267 #[test]
268 fn test_reject_conflict_paths_no_base_topic() {
269 let check = RejectConflictPaths::default();
270 run_topic_check_ok(
271 "test_reject_conflict_paths_no_base_topic",
272 MERGE_CONFLICT_NO_BASE,
273 check,
274 );
275 }
276
277 #[test]
278 fn test_reject_conflict_paths_no_base_require() {
279 let check = RejectConflictPaths::builder()
280 .require_base_exist(false)
281 .build()
282 .unwrap();
283 let result = run_check(
284 "test_reject_conflict_paths_no_base_require",
285 MERGE_CONFLICT_NO_BASE,
286 check,
287 );
288 test_result_errors(result, &[
289 "commit 52710df4a433731545ef99440edeb431b3160fc6 not allowed; it appears as though \
290 `no_base_BACKUP_12345.ext` is a merge conflict resolution file and cannot be added.",
291 "commit 52710df4a433731545ef99440edeb431b3160fc6 not allowed; it appears as though \
292 `no_base_BASE_12345.ext` is a merge conflict resolution file and cannot be added.",
293 "commit 52710df4a433731545ef99440edeb431b3160fc6 not allowed; it appears as though \
294 `no_base_LOCAL_12345.ext` is a merge conflict resolution file and cannot be added.",
295 "commit 52710df4a433731545ef99440edeb431b3160fc6 not allowed; it appears as though \
296 `no_base_REMOTE_12345.ext` is a merge conflict resolution file and cannot be added.",
297 ]);
298 }
299
300 #[test]
301 fn test_reject_conflict_paths_no_base_require_topic() {
302 let check = RejectConflictPaths::builder()
303 .require_base_exist(false)
304 .build()
305 .unwrap();
306 let result = run_topic_check(
307 "test_reject_conflict_paths_no_base_require_topic",
308 MERGE_CONFLICT_NO_BASE,
309 check,
310 );
311 test_result_errors(result, &[
312 "it appears as though `no_base_BACKUP_12345.ext` is a merge conflict resolution file \
313 and cannot be added.",
314 "it appears as though `no_base_BASE_12345.ext` is a merge conflict resolution file \
315 and cannot be added.",
316 "it appears as though `no_base_LOCAL_12345.ext` is a merge conflict resolution file \
317 and cannot be added.",
318 "it appears as though `no_base_REMOTE_12345.ext` is a merge conflict resolution file \
319 and cannot be added.",
320 ]);
321 }
322
323 #[test]
324 fn test_reject_conflict_paths_orig_no_base() {
325 let check = RejectConflictPaths::default();
326 run_check_ok(
327 "test_reject_conflict_paths_orig_no_base",
328 MERGE_CONFLICT_ORIG_NO_BASE,
329 check,
330 );
331 }
332
333 #[test]
334 fn test_reject_conflict_paths_orig_no_base_topic() {
335 let check = RejectConflictPaths::default();
336 run_topic_check_ok(
337 "test_reject_conflict_paths_orig_no_base_topic",
338 MERGE_CONFLICT_ORIG_NO_BASE,
339 check,
340 );
341 }
342
343 #[test]
344 fn test_reject_conflict_paths_delete_file() {
345 let check = RejectConflictPaths::builder()
346 .require_base_exist(false)
347 .build()
348 .unwrap();
349 let conf = make_check_conf(&check);
350
351 let result = test_check_base(
352 "test_reject_conflict_paths_delete_file",
353 DELETE_CONFLICT_NO_BASE,
354 MERGE_CONFLICT_NO_BASE,
355 &conf,
356 );
357 test_result_ok(result);
358 }
359
360 #[test]
361 fn test_reject_conflict_paths_delete_file_topic() {
362 let check = RejectConflictPaths::builder()
363 .require_base_exist(false)
364 .build()
365 .unwrap();
366 run_topic_check_ok(
367 "test_reject_conflict_paths_delete_file_topic",
368 DELETE_CONFLICT_NO_BASE,
369 check,
370 );
371 }
372
373 #[test]
374 fn test_reject_conflict_paths_orig_no_base_require() {
375 let check = RejectConflictPaths::builder()
376 .require_base_exist(false)
377 .build()
378 .unwrap();
379 let result = run_check(
380 "test_reject_conflict_paths_orig_no_base_require",
381 MERGE_CONFLICT_ORIG_NO_BASE,
382 check,
383 );
384 test_result_errors(result, &[
385 "commit 672a6a32b045bec6f93b9b1b5252611f40437a07 not allowed; it appears as though \
386 `orig_file_valid.orig` is a merge conflict backup file and cannot be added.",
387 ]);
388 }
389
390 #[test]
391 fn test_reject_conflict_paths_orig_no_base_require_topic() {
392 let check = RejectConflictPaths::builder()
393 .require_base_exist(false)
394 .build()
395 .unwrap();
396 let result = run_topic_check(
397 "test_reject_conflict_paths_orig_no_base_require_topic",
398 MERGE_CONFLICT_ORIG_NO_BASE,
399 check,
400 );
401 test_result_errors(
402 result,
403 &[
404 "it appears as though `orig_file_valid.orig` is a merge conflict backup file and \
405 cannot be added.",
406 ],
407 );
408 }
409
410 #[test]
411 fn test_reject_conflict_paths_no_ext() {
412 let check = RejectConflictPaths::default();
413 let result = run_check(
414 "test_reject_conflict_paths_no_ext",
415 MERGE_CONFLICT_NO_EXT,
416 check,
417 );
418 test_result_errors(result, &[
419 "commit f31b2e2cb75083d174097db7054cbc9e5836bad7 not allowed; it appears as though \
420 `no_ext_BACKUP_12345` is a merge conflict resolution file and cannot be added.",
421 "commit f31b2e2cb75083d174097db7054cbc9e5836bad7 not allowed; it appears as though \
422 `no_ext_BASE_12345` is a merge conflict resolution file and cannot be added.",
423 "commit f31b2e2cb75083d174097db7054cbc9e5836bad7 not allowed; it appears as though \
424 `no_ext_LOCAL_12345` is a merge conflict resolution file and cannot be added.",
425 "commit f31b2e2cb75083d174097db7054cbc9e5836bad7 not allowed; it appears as though \
426 `no_ext_REMOTE_12345` is a merge conflict resolution file and cannot be added.",
427 ]);
428 }
429
430 #[test]
431 fn test_reject_conflict_paths_no_ext_topic() {
432 let check = RejectConflictPaths::default();
433 let result = run_topic_check(
434 "test_reject_conflict_paths_no_ext_topic",
435 MERGE_CONFLICT_NO_EXT,
436 check,
437 );
438 test_result_errors(result, &[
439 "it appears as though `no_ext_BACKUP_12345` is a merge conflict resolution file and \
440 cannot be added.",
441 "it appears as though `no_ext_BASE_12345` is a merge conflict resolution file and \
442 cannot be added.",
443 "it appears as though `no_ext_LOCAL_12345` is a merge conflict resolution file and \
444 cannot be added.",
445 "it appears as though `no_ext_REMOTE_12345` is a merge conflict resolution file and \
446 cannot be added.",
447 ]);
448 }
449
450 #[test]
451 fn test_reject_conflict_paths_no_ext_topic_fixed() {
452 let check = RejectConflictPaths::default();
453 run_topic_check_ok(
454 "test_reject_conflict_paths_no_ext_topic_fixed",
455 MERGE_CONFLICT_NO_EXT_FIXED,
456 check,
457 );
458 }
459
460 #[test]
461 fn test_reject_conflict_paths_with_ext() {
462 let check = RejectConflictPaths::default();
463 let result = run_check(
464 "test_reject_conflict_paths_with_ext",
465 MERGE_CONFLICT_WITH_EXT,
466 check,
467 );
468 test_result_errors(result, &[
469 "commit 59f02bdb6c404c8f7cfb32700645c149148c089b not allowed; it appears as though \
470 `conflict_with_BACKUP_12345.ext` is a merge conflict resolution file and cannot be \
471 added.",
472 "commit 59f02bdb6c404c8f7cfb32700645c149148c089b not allowed; it appears as though \
473 `conflict_with_BASE_12345.ext` is a merge conflict resolution file and cannot be \
474 added.",
475 "commit 59f02bdb6c404c8f7cfb32700645c149148c089b not allowed; it appears as though \
476 `conflict_with_LOCAL_12345.ext` is a merge conflict resolution file and cannot be \
477 added.",
478 "commit 59f02bdb6c404c8f7cfb32700645c149148c089b not allowed; it appears as though \
479 `conflict_with_REMOTE_12345.ext` is a merge conflict resolution file and cannot be \
480 added.",
481 ]);
482 }
483
484 #[test]
485 fn test_reject_conflict_paths_with_ext_topic() {
486 let check = RejectConflictPaths::default();
487 let result = run_topic_check(
488 "test_reject_conflict_paths_with_ext_topic",
489 MERGE_CONFLICT_WITH_EXT,
490 check,
491 );
492 test_result_errors(result, &[
493 "it appears as though `conflict_with_BACKUP_12345.ext` is a merge conflict resolution \
494 file and cannot be added.",
495 "it appears as though `conflict_with_BASE_12345.ext` is a merge conflict resolution \
496 file and cannot be added.",
497 "it appears as though `conflict_with_LOCAL_12345.ext` is a merge conflict resolution \
498 file and cannot be added.",
499 "it appears as though `conflict_with_REMOTE_12345.ext` is a merge conflict resolution \
500 file and cannot be added.",
501 ]);
502 }
503
504 #[test]
505 fn test_reject_conflict_paths_with_ext_topic_fixed() {
506 let check = RejectConflictPaths::default();
507 run_topic_check_ok(
508 "test_reject_conflict_paths_with_ext_topic_fixed",
509 MERGE_CONFLICT_WITH_EXT_FIXED,
510 check,
511 );
512 }
513
514 #[test]
515 fn test_reject_conflict_paths_two_ext() {
516 let check = RejectConflictPaths::default();
517 let result = run_check(
518 "test_reject_conflict_paths_two_ext",
519 MERGE_CONFLICT_TWO_EXT,
520 check,
521 );
522 test_result_errors(result, &[
523 "commit e9421eadfcac3c67a090444ef2ac859e86a8a2e0 not allowed; it appears as though \
524 `conflict_with.two_BACKUP_12345.ext` is a merge conflict resolution file and cannot \
525 be added.",
526 "commit e9421eadfcac3c67a090444ef2ac859e86a8a2e0 not allowed; it appears as though \
527 `conflict_with.two_BASE_12345.ext` is a merge conflict resolution file and cannot be \
528 added.",
529 "commit e9421eadfcac3c67a090444ef2ac859e86a8a2e0 not allowed; it appears as though \
530 `conflict_with.two_LOCAL_12345.ext` is a merge conflict resolution file and cannot be \
531 added.",
532 "commit e9421eadfcac3c67a090444ef2ac859e86a8a2e0 not allowed; it appears as though \
533 `conflict_with.two_REMOTE_12345.ext` is a merge conflict resolution file and cannot \
534 be added.",
535 ]);
536 }
537
538 #[test]
539 fn test_reject_conflict_paths_two_ext_topic() {
540 let check = RejectConflictPaths::default();
541 let result = run_topic_check(
542 "test_reject_conflict_paths_two_ext_topic",
543 MERGE_CONFLICT_TWO_EXT,
544 check,
545 );
546 test_result_errors(
547 result,
548 &[
549 "it appears as though `conflict_with.two_BACKUP_12345.ext` is a merge conflict \
550 resolution file and cannot be added.",
551 "it appears as though `conflict_with.two_BASE_12345.ext` is a merge conflict \
552 resolution file and cannot be added.",
553 "it appears as though `conflict_with.two_LOCAL_12345.ext` is a merge conflict \
554 resolution file and cannot be added.",
555 "it appears as though `conflict_with.two_REMOTE_12345.ext` is a merge conflict \
556 resolution file and cannot be added.",
557 ],
558 );
559 }
560
561 #[test]
562 fn test_reject_conflict_paths_two_ext_topic_fixed() {
563 let check = RejectConflictPaths::default();
564 run_topic_check_ok(
565 "test_reject_conflict_paths_two_ext_topic_fixed",
566 MERGE_CONFLICT_TWO_EXT_FIXED,
567 check,
568 );
569 }
570
571 #[test]
572 fn test_reject_conflict_paths_orig_ext() {
573 let check = RejectConflictPaths::default();
574 let result = run_check(
575 "test_reject_conflict_paths_orig_ext",
576 MERGE_CONFLICT_ORIG_EXT,
577 check,
578 );
579 test_result_errors(result, &[
580 "commit 4abcf323b81c757e668ce1936f475b085d6852e8 not allowed; it appears as though \
581 `orig_file.ext.orig` is a merge conflict backup file and cannot be added.",
582 ]);
583 }
584
585 #[test]
586 fn test_reject_conflict_paths_orig_ext_topic() {
587 let check = RejectConflictPaths::default();
588 let result = run_topic_check(
589 "test_reject_conflict_paths_orig_ext_topic",
590 MERGE_CONFLICT_ORIG_EXT,
591 check,
592 );
593 test_result_errors(result, &[
594 "it appears as though `orig_file.ext.orig` is a merge conflict backup file and cannot \
595 be added.",
596 ]);
597 }
598
599 #[test]
600 fn test_reject_conflict_paths_orig_ext_topic_fixed() {
601 let check = RejectConflictPaths::default();
602 run_topic_check_ok(
603 "test_reject_conflict_paths_orig_ext_topic_fixed",
604 MERGE_CONFLICT_ORIG_EXT_FIXED,
605 check,
606 );
607 }
608}