git_checks/
restricted_path.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::*;
11
12/// A check which denies commits which modify files underneath certain path.
13#[derive(Builder, Debug, Clone)]
14#[builder(field(private))]
15pub struct RestrictedPath {
16    /// The path which may not be edited.
17    ///
18    /// Configuration: Required
19    #[builder(setter(into))]
20    path: String,
21    /// Whether the check is an error or a warning.
22    ///
23    /// Configuration: Optional
24    /// Default: `true`
25    #[builder(default = "true")]
26    required: bool,
27}
28
29impl RestrictedPath {
30    /// Create a new builder.
31    pub fn builder() -> RestrictedPathBuilder {
32        Default::default()
33    }
34}
35
36impl ContentCheck for RestrictedPath {
37    fn name(&self) -> &str {
38        "restricted-path"
39    }
40
41    fn check(
42        &self,
43        _: &CheckGitContext,
44        content: &dyn Content,
45    ) -> Result<CheckResult, Box<dyn Error>> {
46        let mut result = CheckResult::new();
47
48        let is_restricted = content
49            .diffs()
50            .iter()
51            .map(|diff| diff.name.as_path())
52            .any(|path| path.starts_with(&self.path));
53
54        if is_restricted {
55            if self.required {
56                result.add_error(format!(
57                    "{}the `{}` path is restricted.",
58                    commit_prefix_str(content, "not allowed;"),
59                    self.path,
60                ));
61            } else {
62                result.add_warning(format!(
63                    "{}the `{}` path is restricted.",
64                    commit_prefix_str(content, "should be inspected;"),
65                    self.path,
66                ));
67            };
68        }
69
70        Ok(result)
71    }
72}
73
74#[cfg(feature = "config")]
75pub(crate) mod config {
76    use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
77    use serde::Deserialize;
78    #[cfg(test)]
79    use serde_json::json;
80
81    #[cfg(test)]
82    use crate::test;
83    use crate::RestrictedPath;
84
85    /// Configuration for the `RestrictedPath` check.
86    ///
87    /// The `restricted_path` key is a string with the path to the content which should be watched.
88    /// The `required` key is a boolean which defaults to `true` which indicates whether modifying
89    /// the path is an error or a warning.
90    ///
91    /// This check is registered as a commit check with the name `"restricted_path"` and as a topic
92    /// check with the name `"restricted_path/topic"`.
93    ///
94    /// # Example
95    ///
96    /// ```json
97    /// {
98    ///     "restricted_path": "path/to/restricted/content",
99    ///     "required": false
100    /// }
101    /// ```
102    #[derive(Deserialize, Debug)]
103    pub struct RestrictedPathConfig {
104        path: String,
105        #[serde(default)]
106        required: Option<bool>,
107    }
108
109    impl IntoCheck for RestrictedPathConfig {
110        type Check = RestrictedPath;
111
112        fn into_check(self) -> Self::Check {
113            let mut builder = RestrictedPath::builder();
114
115            builder.path(self.path);
116
117            if let Some(required) = self.required {
118                builder.required(required);
119            }
120
121            builder
122                .build()
123                .expect("configuration mismatch for `RestrictedPath`")
124        }
125    }
126
127    register_checks! {
128        RestrictedPathConfig {
129            "restricted_path" => CommitCheckConfig,
130            "restricted_path/topic" => TopicCheckConfig,
131        },
132    }
133
134    #[test]
135    fn test_restricted_path_config_empty() {
136        let json = json!({});
137        let err = serde_json::from_value::<RestrictedPathConfig>(json).unwrap_err();
138        test::check_missing_json_field(err, "path");
139    }
140
141    #[test]
142    fn test_restricted_path_config_minimum_fields() {
143        let exp_restricted_path = "path/to/restricted/content";
144        let json = json!({
145            "path": exp_restricted_path,
146        });
147        let check: RestrictedPathConfig = serde_json::from_value(json).unwrap();
148
149        assert_eq!(check.path, exp_restricted_path);
150        assert_eq!(check.required, None);
151
152        let check = check.into_check();
153
154        assert_eq!(check.path, exp_restricted_path);
155        assert!(check.required);
156    }
157
158    #[test]
159    fn test_restricted_path_config_all_fields() {
160        let exp_restricted_path = "path/to/restricted/content";
161        let json = json!({
162            "path": exp_restricted_path,
163            "required": false,
164        });
165        let check: RestrictedPathConfig = serde_json::from_value(json).unwrap();
166
167        assert_eq!(check.path, exp_restricted_path);
168        assert_eq!(check.required, Some(false));
169
170        let check = check.into_check();
171
172        assert_eq!(check.path, exp_restricted_path);
173        assert!(!check.required);
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use git_checks_core::{Check, TopicCheck};
180
181    use crate::test::*;
182    use crate::RestrictedPath;
183
184    const BAD_TOPIC: &str = "e845fa2521c17bdd31d5891c1c644fb17f0629db";
185    const FIX_TOPIC: &str = "d8a2f22943cdcca373f00892a23b85f3a6ba1196";
186
187    #[test]
188    fn test_restricted_path_builder_default() {
189        assert!(RestrictedPath::builder().build().is_err());
190    }
191
192    #[test]
193    fn test_restricted_path_builder_minimum_fields() {
194        assert!(RestrictedPath::builder().path("restricted").build().is_ok());
195    }
196
197    #[test]
198    fn test_restricted_path_name_commit() {
199        let check = RestrictedPath::builder()
200            .path("restricted")
201            .build()
202            .unwrap();
203        assert_eq!(Check::name(&check), "restricted-path");
204    }
205
206    #[test]
207    fn test_restricted_path_name_topic() {
208        let check = RestrictedPath::builder()
209            .path("restricted")
210            .build()
211            .unwrap();
212        assert_eq!(TopicCheck::name(&check), "restricted-path");
213    }
214
215    #[test]
216    fn test_restricted_path() {
217        let check = RestrictedPath::builder()
218            .path("restricted")
219            .build()
220            .unwrap();
221        let result = run_check("test_restricted_path", BAD_TOPIC, check);
222        test_result_errors(result, &[
223            "commit e845fa2521c17bdd31d5891c1c644fb17f0629db not allowed; the `restricted` path \
224             is restricted.",
225        ]);
226    }
227
228    #[test]
229    fn test_restricted_path_topic() {
230        let check = RestrictedPath::builder()
231            .path("restricted")
232            .build()
233            .unwrap();
234        let result = run_topic_check("test_restricted_path_topic", BAD_TOPIC, check);
235        test_result_errors(result, &["the `restricted` path is restricted."]);
236    }
237
238    #[test]
239    fn test_restricted_path_warning() {
240        let check = RestrictedPath::builder()
241            .path("restricted")
242            .required(false)
243            .build()
244            .unwrap();
245        let result = run_check("test_restricted_path_warning", BAD_TOPIC, check);
246        test_result_warnings(
247            result,
248            &[
249                "commit e845fa2521c17bdd31d5891c1c644fb17f0629db should be inspected; the \
250                 `restricted` path is restricted.",
251            ],
252        );
253    }
254
255    #[test]
256    fn test_restricted_path_warning_topic() {
257        let check = RestrictedPath::builder()
258            .path("restricted")
259            .required(false)
260            .build()
261            .unwrap();
262        let result = run_topic_check("test_restricted_path_warning_topic", BAD_TOPIC, check);
263        test_result_warnings(result, &["the `restricted` path is restricted."]);
264    }
265
266    #[test]
267    fn test_restricted_path_topic_fixed() {
268        let check = RestrictedPath::builder()
269            .path("restricted")
270            .build()
271            .unwrap();
272        run_topic_check_ok("test_restricted_path_topic_fixed", FIX_TOPIC, check);
273    }
274}