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