1use 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#[derive(Debug, Error)]
19#[non_exhaustive]
20pub enum AttributeError {
21 #[error("git error: {}", source)]
23 Git {
24 #[from]
26 source: GitError,
27 },
28 #[error(
30 "check-attr error: failed to check the {} attribute of {}: {}",
31 attribute,
32 path.display(),
33 output
34 )]
35 CheckAttr {
36 attribute: String,
38 path: PathBuf,
40 output: String,
42 },
43 #[error(
45 "check-attr error: unexpected git output format error: no value for {} on {}",
46 attribute,
47 path.display()
48 )]
49 MissingValue {
50 attribute: String,
52 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#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum AttributeState {
77 Unspecified,
79 Set,
81 Unset,
83 Value(String),
85}
86
87#[derive(Debug)]
89pub struct CheckGitContext {
90 workarea: GitWorkArea,
92 topic_owner: Identity,
94 configuration: BTreeMap<String, String>,
96}
97
98impl CheckGitContext {
99 pub fn new(workarea: GitWorkArea, topic_owner: Identity) -> Self {
101 Self {
102 workarea,
103 topic_owner,
104 configuration: BTreeMap::new(),
105 }
106 }
107
108 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 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 pub fn git(&self) -> Command {
138 self.workarea.git()
139 }
140
141 pub fn topic_owner(&self) -> &Identity {
143 &self.topic_owner
144 }
145
146 pub fn configuration(&self, key: &str) -> Option<&str> {
148 self.configuration.get(key).map(|v| v.as_str())
149 }
150
151 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 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 Ok(AttributeState::Value(attr_value.to_owned()))
184 }
185 }
186
187 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 pub fn workarea(&self) -> &GitWorkArea {
198 &self.workarea
199 }
200
201 pub fn workarea_mut(&mut self) -> &mut GitWorkArea {
203 &mut self.workarea
204 }
205
206 pub fn gitdir(&self) -> &Path {
208 self.workarea.gitdir()
209 }
210
211 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 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 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 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 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 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}