mit_commit_message_lints/external/
git2.rs1use std::{collections::BTreeMap, convert::TryFrom, path::PathBuf};
2
3use git2::{Config, Repository, RepositoryState};
4use miette::{miette, IntoDiagnostic, Report, Result};
5
6use crate::{
7 external::{vcs::RepoState, Vcs},
8 mit::{Author, Authors},
9};
10
11#[allow(missing_debug_implementations)]
13pub struct Git2 {
14 config_snapshot: Config,
15 config_live: Config,
16 state: Option<RepositoryState>,
17}
18
19impl Git2 {
20 pub fn new(mut config: Config, state: Option<RepositoryState>) -> Result<Self> {
24 Ok(Self {
25 config_snapshot: config.snapshot().into_diagnostic()?,
26 config_live: config,
27 state,
28 })
29 }
30
31 fn config_defined(&self, lint_name: &str) -> Result<bool> {
32 Ok(self
33 .config_snapshot
34 .entries(Some(lint_name))
35 .into_diagnostic()?
36 .next()
37 .is_some())
38 }
39
40 fn refresh_snapshot(&mut self) -> Result<()> {
41 self.config_snapshot = self.config_live.snapshot().into_diagnostic()?;
42 Ok(())
43 }
44}
45
46impl Vcs for Git2 {
47 fn entries(&self, glob: Option<&str>) -> Result<Vec<String>> {
48 let mut entries = vec![];
49 let mut item = self.config_snapshot.entries(glob).into_diagnostic()?;
50 while let Some(entry) = item.next() {
51 if let Ok(name) = entry.into_diagnostic()?.name() {
52 entries.push(name.into());
53 }
54 }
55
56 Ok(entries)
57 }
58
59 fn get_bool(&self, name: &str) -> Result<Option<bool>> {
60 if self.config_defined(name)? {
61 Ok(Some(self.config_snapshot.get_bool(name).into_diagnostic()?))
62 } else {
63 Ok(None)
64 }
65 }
66
67 fn get_str(&self, name: &str) -> Result<Option<&str>> {
68 let defined = self.config_defined(name)?;
69
70 if defined {
71 self.config_snapshot
72 .get_str(name)
73 .map(Some)
74 .into_diagnostic()
75 } else {
76 Ok(None)
77 }
78 }
79
80 fn get_i64(&self, name: &str) -> Result<Option<i64>> {
81 let defined = self.config_defined(name)?;
82
83 if defined {
84 self.config_snapshot
85 .get_i64(name)
86 .map(Some)
87 .into_diagnostic()
88 } else {
89 Ok(None)
90 }
91 }
92
93 fn set_str(&mut self, name: &str, value: &str) -> Result<()> {
94 self.config_live.set_str(name, value).into_diagnostic()?;
95 self.refresh_snapshot()
96 }
97
98 fn set_i64(&mut self, name: &str, value: i64) -> Result<()> {
99 self.config_live.set_i64(name, value).into_diagnostic()?;
100 self.refresh_snapshot()
101 }
102
103 fn remove(&mut self, name: &str) -> Result<()> {
104 self.config_live.remove(name).into_diagnostic()?;
105 self.refresh_snapshot()
106 }
107
108 fn state(&self) -> Option<RepoState> {
109 match self.state {
110 None => None,
111 Some(RepositoryState::ApplyMailbox) => Some(RepoState::ApplyMailbox),
112 Some(RepositoryState::Clean) => Some(RepoState::Clean),
113 Some(RepositoryState::Merge) => Some(RepoState::Merge),
114 Some(RepositoryState::Revert) => Some(RepoState::Revert),
115 Some(RepositoryState::RevertSequence) => Some(RepoState::RevertSequence),
116 Some(RepositoryState::CherryPick) => Some(RepoState::CherryPick),
117 Some(RepositoryState::CherryPickSequence) => Some(RepoState::CherryPickSequence),
118 Some(RepositoryState::Bisect) => Some(RepoState::Bisect),
119 Some(RepositoryState::Rebase) => Some(RepoState::Rebase),
120 Some(RepositoryState::RebaseInteractive) => Some(RepoState::RebaseInteractive),
121 Some(RepositoryState::RebaseMerge) => Some(RepoState::RebaseMerge),
122 Some(RepositoryState::ApplyMailboxOrRebase) => Some(RepoState::ApplyMailboxOrRebase),
123 }
124 }
125}
126
127impl TryFrom<PathBuf> for Git2 {
128 type Error = Report;
129
130 fn try_from(current_dir: PathBuf) -> Result<Self, Self::Error> {
131 let (config, state) = Repository::discover(current_dir)
132 .and_then(|repo| {
133 let state = repo.state();
134 repo.config().map(|config| (config, Some(state)))
135 })
136 .or_else(|_| Config::open_default().map(|config| (config, None)))
137 .into_diagnostic()?;
138 Self::new(config, state)
139 }
140}
141
142fn parse_initial_and_part(config_key: &str) -> Result<(String, String)> {
153 let stripped = config_key.trim_start_matches("mit.author.config.");
154 let fragments: Vec<&str> = stripped.split_terminator('.').collect();
155 if fragments.len() < 2 {
156 return Err(miette!("Malformed config key: {config_key}"));
157 }
158 let part = String::from(fragments[fragments.len() - 1]);
159 let initial = fragments[..fragments.len() - 1].join(".");
160 Ok((initial, part))
161}
162
163impl TryFrom<&'_ Git2> for Authors<'_> {
164 type Error = Report;
165
166 fn try_from(vcs: &'_ Git2) -> Result<Self, Self::Error> {
167 let raw_entries: BTreeMap<String, BTreeMap<String, String>> = vcs
168 .entries(Some("mit.author.config.*"))?
169 .iter()
170 .try_fold::<_, _, Result<_, Self::Error>>(BTreeMap::new(), |mut acc, key| {
171 let (initial, part) = parse_initial_and_part(key)?;
172 let mut existing: BTreeMap<String, String> =
173 acc.get(&initial).cloned().unwrap_or_default();
174 existing.insert(part, String::from(vcs.get_str(key)?.unwrap()));
175
176 acc.insert(initial, existing);
177 Ok(acc)
178 })?;
179
180 Ok(Self::new(
181 raw_entries
182 .iter()
183 .filter_map(|(key, cfg)| {
184 let name = cfg.get("name").cloned();
185 let email = cfg.get("email").cloned();
186 let signingkey: Option<String> = cfg.get("signingkey").cloned();
187
188 match (name, email, signingkey) {
189 (Some(name), Some(email), None) => {
190 Some((key, Author::new(name.into(), email.into(), None)))
191 }
192 (Some(name), Some(email), Some(signingkey)) => Some((
193 key,
194 Author::new(name.into(), email.into(), Some(signingkey.into())),
195 )),
196 _ => None,
197 }
198 })
199 .map(|(key, value): (&String, Author<'_>)| (key.clone(), value))
200 .collect(),
201 ))
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::parse_initial_and_part;
208
209 #[test]
210 fn parses_simple_initials() {
211 let (initial, part) =
212 parse_initial_and_part("mit.author.config.bt.email").expect("should parse");
213 assert_eq!(initial, "bt");
214 assert_eq!(part, "email");
215 }
216
217 #[test]
218 fn parses_initials_containing_dots() {
219 let (initial, part) =
220 parse_initial_and_part("mit.author.config.b.t.email").expect("should parse");
221 assert_eq!(initial, "b.t");
222 assert_eq!(part, "email");
223 }
224
225 #[test]
226 fn parses_signingkey_with_dotted_initials() {
227 let (initial, part) =
228 parse_initial_and_part("mit.author.config.b.t.signingkey").expect("should parse");
229 assert_eq!(initial, "b.t");
230 assert_eq!(part, "signingkey");
231 }
232
233 #[test]
234 fn errors_on_malformed_key() {
235 parse_initial_and_part("mit.author.config.bt").expect_err("should fail");
236 }
237}