git_checks/
reject_conflict_paths.rs

1// Copyright Kitware, Inc.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use derive_builder::Builder;
10use git_checks_core::impl_prelude::*;
11use lazy_static::lazy_static;
12use regex::Regex;
13
14/// A check which denies paths which look like merge conflict resolution paths.
15///
16/// Sometimes after a merge, the files written to assist in resolving the conflict will be added
17/// accidentally.
18#[derive(Builder, Debug, Clone, Copy)]
19#[builder(field(private))]
20pub struct RejectConflictPaths {
21    /// Require that the base file exist for the check to enforce paths.
22    ///
23    /// If the base exists, then the conflict path patterns will always fire. However, this misses
24    /// the case where the base file was deleted and the conflict files left behind. Passing `true`
25    /// here rejects any path matching the conflict patterns.
26    ///
27    /// Configuration: Optional
28    /// Default: `true`
29    #[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    /// Create a new builder.
46    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    /// Configuration for the `CheckEndOfLine` check.
158    ///
159    /// No configuration available.
160    ///
161    /// This check is registered as a commit check with the name `"check_end_of_line"` and a topic
162    /// check with the name `"check_end_of_line/topic"`.
163    #[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}