git_checks_core/
context.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 std::collections::BTreeMap;
10use std::ffi::OsStr;
11use std::path::{Path, PathBuf};
12use std::process::Command;
13
14use git_workarea::{GitError, GitWorkArea, Identity, SubmoduleConfig};
15use thiserror::Error;
16
17/// Errors which can occur when querying an attribute.
18#[derive(Debug, Error)]
19#[non_exhaustive]
20pub enum AttributeError {
21    /// Command preparation failure.
22    #[error("git error: {}", source)]
23    Git {
24        /// The cause of the error.
25        #[from]
26        source: GitError,
27    },
28    /// Failure when getting the attribute from git.
29    #[error(
30        "check-attr error: failed to check the {} attribute of {}: {}",
31        attribute,
32        path.display(),
33        output
34    )]
35    CheckAttr {
36        /// The attribute being queried.
37        attribute: String,
38        /// The path being queried.
39        path: PathBuf,
40        /// Git's output for the error.
41        output: String,
42    },
43    /// Failure to parse Git's attribute output.
44    #[error(
45        "check-attr error: unexpected git output format error: no value for {} on {}",
46        attribute,
47        path.display()
48    )]
49    MissingValue {
50        /// The attribute being queried.
51        attribute: String,
52        /// The path being queried.
53        path: PathBuf,
54    },
55}
56
57impl AttributeError {
58    fn check_attr(attr: &str, path: &OsStr, output: &[u8]) -> Self {
59        AttributeError::CheckAttr {
60            attribute: attr.into(),
61            path: path.into(),
62            output: String::from_utf8_lossy(output).into(),
63        }
64    }
65
66    fn missing_value(attr: &str, path: &OsStr) -> Self {
67        AttributeError::MissingValue {
68            attribute: attr.into(),
69            path: path.into(),
70        }
71    }
72}
73
74/// States attributes may be in.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum AttributeState {
77    /// The attribute is neither set nor unset.
78    Unspecified,
79    /// The attribute is set.
80    Set,
81    /// The attribute is unset.
82    Unset,
83    /// The attribute is set with the given value.
84    Value(String),
85}
86
87/// Git context for use in checks.
88#[derive(Debug)]
89pub struct CheckGitContext {
90    /// The workarea for the check.
91    workarea: GitWorkArea,
92    /// The owner of the topic.
93    topic_owner: Identity,
94    /// Configuration lookup callback.
95    configuration: BTreeMap<String, String>,
96}
97
98impl CheckGitContext {
99    /// Create a new git context for checking a commit.
100    pub fn new(workarea: GitWorkArea, topic_owner: Identity) -> Self {
101        Self {
102            workarea,
103            topic_owner,
104            configuration: BTreeMap::new(),
105        }
106    }
107
108    /// Add a configuration value for use by checks.
109    ///
110    /// Any existing configuration value is overwritten.
111    pub fn add_configuration<K, V>(&mut self, key: K, value: V) -> &mut Self
112    where
113        K: Into<String>,
114        V: Into<String>,
115    {
116        self.configuration.insert(key.into(), value.into());
117
118        self
119    }
120
121    /// Add a sequence of configuration values for use by checks.
122    ///
123    /// Any existing configuration values are overwritten.
124    pub fn add_configurations<I, K, V>(&mut self, iter: I) -> &mut Self
125    where
126        I: IntoIterator<Item = (K, V)>,
127        K: Into<String>,
128        V: Into<String>,
129    {
130        self.configuration
131            .extend(iter.into_iter().map(|(k, v)| (k.into(), v.into())));
132
133        self
134    }
135
136    /// Create a git command for use in checks.
137    pub fn git(&self) -> Command {
138        self.workarea.git()
139    }
140
141    /// The publisher of the branch.
142    pub fn topic_owner(&self) -> &Identity {
143        &self.topic_owner
144    }
145
146    /// Query for a configuration value.
147    pub fn configuration(&self, key: &str) -> Option<&str> {
148        self.configuration.get(key).map(|v| v.as_str())
149    }
150
151    /// Check an attribute of the given path.
152    fn check_attr_impl(&self, attr: &str, path: &OsStr) -> Result<AttributeState, AttributeError> {
153        let check_attr = self
154            .workarea
155            .git()
156            .arg("--literal-pathspecs")
157            .arg("check-attr")
158            .arg(attr)
159            .arg("--")
160            .arg(path)
161            .output()
162            .map_err(|err| GitError::subcommand("check-attr", err))?;
163        if !check_attr.status.success() {
164            return Err(AttributeError::check_attr(attr, path, &check_attr.stderr));
165        }
166        let attr_line = String::from_utf8_lossy(&check_attr.stdout);
167
168        // So the output format here is ambiguous. The `gitattributes(5)` format does not support
169        // spaces in the values of attributes, so split on whitespace and take the last element.
170        let attr_value = attr_line
171            .split_whitespace()
172            .last()
173            .ok_or_else(|| AttributeError::missing_value(attr, path))?;
174        if attr_value == "set" {
175            Ok(AttributeState::Set)
176        } else if attr_value == "unset" {
177            Ok(AttributeState::Unset)
178        } else if attr_value == "unspecified" {
179            Ok(AttributeState::Unspecified)
180        } else {
181            // Attribute values which match one of the above are ambiguous. `git-check-attr(1)`
182            // states that is ambiguous and leaves it at that.
183            Ok(AttributeState::Value(attr_value.to_owned()))
184        }
185    }
186
187    /// Check an attribute of the given path.
188    pub fn check_attr<A, P>(&self, attr: A, path: P) -> Result<AttributeState, AttributeError>
189    where
190        A: AsRef<str>,
191        P: AsRef<OsStr>,
192    {
193        self.check_attr_impl(attr.as_ref(), path.as_ref())
194    }
195
196    /// The workarea used for check operations.
197    pub fn workarea(&self) -> &GitWorkArea {
198        &self.workarea
199    }
200
201    /// The workarea used for check operations.
202    pub fn workarea_mut(&mut self) -> &mut GitWorkArea {
203        &mut self.workarea
204    }
205
206    /// The path to the git repository.
207    pub fn gitdir(&self) -> &Path {
208        self.workarea.gitdir()
209    }
210
211    /// The submodule configuration for the repository.
212    pub fn submodule_config(&self) -> &SubmoduleConfig {
213        self.workarea.submodule_config()
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use std::path::Path;
220
221    use git_workarea::{CommitId, GitContext, Identity};
222
223    use crate::context::*;
224
225    fn make_context() -> GitContext {
226        let gitdir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.git"));
227        if !gitdir.exists() {
228            panic!("The tests must be run from a git checkout.");
229        }
230
231        GitContext::new(gitdir)
232    }
233
234    #[test]
235    fn test_commit_attrs() {
236        let ctx = make_context();
237
238        // A commit with attributes set on some paths.
239        let sha1 = "85b9551a672a34e1926d5010a9c9075eda0a6107";
240        let prep_ctx = ctx.prepare(&CommitId::new(sha1)).unwrap();
241
242        let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
243        let check_ctx = CheckGitContext::new(prep_ctx, ben);
244
245        assert_eq!(
246            check_ctx.check_attr("foo", "file1").unwrap(),
247            AttributeState::Value("bar".to_owned()),
248        );
249        assert_eq!(
250            check_ctx.check_attr("attr_set", "file1").unwrap(),
251            AttributeState::Set,
252        );
253        assert_eq!(
254            check_ctx.check_attr("attr_unset", "file1").unwrap(),
255            AttributeState::Unspecified,
256        );
257        assert_eq!(
258            check_ctx.check_attr("text", "file1").unwrap(),
259            AttributeState::Unspecified,
260        );
261
262        assert_eq!(
263            check_ctx.check_attr("foo", "file2").unwrap(),
264            AttributeState::Unspecified,
265        );
266        assert_eq!(
267            check_ctx.check_attr("attr_set", "file2").unwrap(),
268            AttributeState::Set,
269        );
270        assert_eq!(
271            check_ctx.check_attr("attr_unset", "file2").unwrap(),
272            AttributeState::Unset,
273        );
274        assert_eq!(
275            check_ctx.check_attr("text", "file2").unwrap(),
276            AttributeState::Unspecified,
277        );
278
279        assert_eq!(
280            check_ctx.check_attr("foo", "file3").unwrap(),
281            AttributeState::Unspecified,
282        );
283        assert_eq!(
284            check_ctx.check_attr("attr_set", "file3").unwrap(),
285            AttributeState::Unspecified,
286        );
287        assert_eq!(
288            check_ctx.check_attr("attr_unset", "file3").unwrap(),
289            AttributeState::Unspecified,
290        );
291        assert_eq!(
292            check_ctx.check_attr("text", "file3").unwrap(),
293            AttributeState::Value("yes".to_owned()),
294        );
295    }
296
297    #[test]
298    fn test_commit_attrs_literal_pathspecs() {
299        let ctx = make_context();
300
301        // A commit with attributes set on some paths.
302        let sha1 = "9055e6f31ee5e7de8cdce0ca57452c38f433fd89";
303        let prep_ctx = ctx.prepare(&CommitId::new(sha1)).unwrap();
304
305        let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
306        let check_ctx = CheckGitContext::new(prep_ctx, ben);
307
308        assert_eq!(
309            check_ctx.check_attr("custom-attr", "foo.attr").unwrap(),
310            AttributeState::Set,
311        );
312        assert_eq!(
313            check_ctx.check_attr("custom-attr", "*.attr").unwrap(),
314            AttributeState::Set,
315        );
316    }
317
318    #[test]
319    fn test_commit_attrs_no_path() {
320        let ctx = make_context();
321
322        // A commit with attributes set on some paths.
323        let sha1 = "9055e6f31ee5e7de8cdce0ca57452c38f433fd89";
324        let prep_ctx = ctx.prepare(&CommitId::new(sha1)).unwrap();
325
326        let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
327        let check_ctx = CheckGitContext::new(prep_ctx, ben);
328
329        assert_eq!(
330            check_ctx.check_attr("noexist", "noattr").unwrap(),
331            AttributeState::Unspecified,
332        );
333    }
334
335    #[test]
336    fn test_context_apis() {
337        let ctx = make_context();
338
339        // A commit with attributes set on some paths.
340        let sha1 = "9055e6f31ee5e7de8cdce0ca57452c38f433fd89";
341        let prep_ctx = ctx.prepare(&CommitId::new(sha1)).unwrap();
342
343        let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
344        let check_ctx = CheckGitContext::new(prep_ctx, ben);
345        let _ = check_ctx.workarea();
346
347        let mut check_ctx = check_ctx;
348        let _ = check_ctx.workarea_mut();
349    }
350
351    #[test]
352    fn test_context_configuration_overwrite() {
353        let ctx = make_context();
354
355        // A commit with attributes set on some paths.
356        let sha1 = "9055e6f31ee5e7de8cdce0ca57452c38f433fd89";
357        let prep_ctx = ctx.prepare(&CommitId::new(sha1)).unwrap();
358
359        let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
360        let check_ctx = {
361            let mut check_ctx = CheckGitContext::new(prep_ctx, ben);
362            check_ctx
363                .add_configuration("key1", "value1")
364                .add_configuration("key2", "value2")
365                .add_configuration("key3", "value3")
366                .add_configuration("key1", "value1_overwrite")
367                .add_configurations([("key2", "value2_overwrite"), ("key4", "value4")]);
368            check_ctx
369        };
370
371        let expect = [
372            ("key1", Some("value1_overwrite")),
373            ("key2", Some("value2_overwrite")),
374            ("key3", Some("value3")),
375            ("key4", Some("value4")),
376            ("key5", None),
377        ];
378
379        for (k, v) in expect {
380            assert_eq!(check_ctx.configuration(k), v);
381        }
382        assert_eq!(check_ctx.configuration.len(), 4);
383    }
384}