1use 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#[derive(Debug, Error)]
18#[non_exhaustive]
19pub enum AttributeError {
20 #[error("git error: {}", source)]
22 Git {
23 #[from]
25 source: GitError,
26 },
27 #[error(
29 "check-attr error: failed to check the {} attribute of {}: {}",
30 attribute,
31 path.display(),
32 output
33 )]
34 CheckAttr {
35 attribute: String,
37 path: PathBuf,
39 output: String,
41 },
42 #[error(
44 "check-attr error: unexpected git output format error: no value for {} on {}",
45 attribute,
46 path.display()
47 )]
48 MissingValue {
49 attribute: String,
51 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#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum AttributeState {
76 Unspecified,
78 Set,
80 Unset,
82 Value(String),
84}
85
86#[derive(Debug)]
88pub struct CheckGitContext {
89 workarea: GitWorkArea,
91 topic_owner: Identity,
93}
94
95impl CheckGitContext {
96 pub fn new(workarea: GitWorkArea, topic_owner: Identity) -> Self {
98 Self {
99 workarea,
100 topic_owner,
101 }
102 }
103
104 pub fn git(&self) -> Command {
106 self.workarea.git()
107 }
108
109 pub fn topic_owner(&self) -> &Identity {
111 &self.topic_owner
112 }
113
114 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 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 Ok(AttributeState::Value(attr_value.to_owned()))
147 }
148 }
149
150 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 pub fn workarea(&self) -> &GitWorkArea {
161 &self.workarea
162 }
163
164 pub fn workarea_mut(&mut self) -> &mut GitWorkArea {
166 &mut self.workarea
167 }
168
169 pub fn gitdir(&self) -> &Path {
171 self.workarea.gitdir()
172 }
173
174 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 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 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 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 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}