1use derive_builder::Builder;
10use git_checks_core::impl_prelude::*;
11use thiserror::Error;
12
13#[derive(Debug, Error)]
14enum BadCommitError {
15 #[error("failed to list topic refs from {} to {}: {}", base, commit, output)]
16 RevList {
17 commit: CommitId,
18 base: CommitId,
19 output: String,
20 },
21}
22
23impl BadCommitError {
24 fn rev_list(commit: CommitId, base: CommitId, output: &[u8]) -> Self {
25 BadCommitError::RevList {
26 commit,
27 base,
28 output: String::from_utf8_lossy(output).into(),
29 }
30 }
31}
32
33#[derive(Builder, Debug, Clone)]
35#[builder(field(private))]
36pub struct BadCommit {
37 #[builder(setter(into))]
41 commit: CommitId,
42 #[builder(setter(into))]
46 reason: String,
47}
48
49impl BadCommit {
50 pub fn builder() -> BadCommitBuilder {
52 Default::default()
53 }
54
55 fn apply(&self, result: &mut CheckResult) {
56 result
57 .add_error(format!(
58 "commit {} is not allowed {}.",
59 self.commit, self.reason,
60 ))
61 .add_alert(
62 format!("commit {} was pushed to the server.", self.commit),
63 true,
64 );
65 }
66}
67
68impl Check for BadCommit {
69 fn name(&self) -> &str {
70 "bad-commit"
71 }
72
73 fn check(&self, _: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
74 let mut result = CheckResult::new();
75
76 if self.commit == commit.sha1 {
77 self.apply(&mut result);
78 }
79
80 Ok(result)
81 }
82}
83
84impl TopicCheck for BadCommit {
85 fn name(&self) -> &str {
86 "bad-commit-topic"
87 }
88
89 fn check(&self, ctx: &CheckGitContext, topic: &Topic) -> Result<CheckResult, Box<dyn Error>> {
90 let rev_list = ctx
91 .git()
92 .arg("rev-list")
93 .arg("--reverse")
94 .arg("--topo-order")
95 .arg(topic.sha1.as_str())
96 .arg(format!("^{}", topic.base))
97 .output()
98 .map_err(|err| GitError::subcommand("rev-list", err))?;
99 if !rev_list.status.success() {
100 return Err(BadCommitError::rev_list(
101 topic.sha1.clone(),
102 topic.base.clone(),
103 &rev_list.stderr,
104 )
105 .into());
106 }
107
108 let refs = String::from_utf8_lossy(&rev_list.stdout);
109
110 let mut result = CheckResult::new();
111 if refs.lines().any(|rev| rev == self.commit.as_str()) {
112 self.apply(&mut result)
113 }
114 Ok(result)
115 }
116}
117
118#[cfg(feature = "config")]
119pub(crate) mod config {
120 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
121 use git_workarea::CommitId;
122 use serde::Deserialize;
123 #[cfg(test)]
124 use serde_json::json;
125
126 #[cfg(test)]
127 use crate::test;
128 use crate::BadCommit;
129
130 #[derive(Deserialize, Debug)]
147 pub struct BadCommitConfig {
148 commit: String,
149 reason: String,
150 }
151
152 impl IntoCheck for BadCommitConfig {
153 type Check = BadCommit;
154
155 fn into_check(self) -> Self::Check {
156 let mut builder = BadCommit::builder();
157 builder.commit(CommitId::new(self.commit));
158 builder.reason(self.reason);
159 builder
160 .build()
161 .expect("configuration mismatch for `BadCommit`")
162 }
163 }
164
165 register_checks! {
166 BadCommitConfig {
167 "bad_commit" => CommitCheckConfig,
168 "bad_commit/topic" => TopicCheckConfig,
169 },
170 }
171
172 #[test]
173 fn test_bad_commit_config_empty() {
174 let json = json!({});
175 let err = serde_json::from_value::<BadCommitConfig>(json).unwrap_err();
176 test::check_missing_json_field(err, "commit");
177 }
178
179 #[test]
180 fn test_bad_commit_config_commit_is_required() {
181 let reason = "because";
182 let json = json!({
183 "reason": reason,
184 });
185 let err = serde_json::from_value::<BadCommitConfig>(json).unwrap_err();
186 test::check_missing_json_field(err, "commit");
187 }
188
189 #[test]
190 fn test_bad_commit_config_reason_is_required() {
191 let commit = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
192 let json = json!({
193 "commit": commit,
194 });
195 let err = serde_json::from_value::<BadCommitConfig>(json).unwrap_err();
196 test::check_missing_json_field(err, "reason");
197 }
198
199 #[test]
200 fn test_bad_commit_config_minimum_fields() {
201 let commit = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
202 let reason = "because";
203 let json = json!({
204 "commit": commit,
205 "reason": reason,
206 });
207 let check: BadCommitConfig = serde_json::from_value(json).unwrap();
208
209 assert_eq!(check.commit, commit);
210 assert_eq!(check.reason, reason);
211
212 let check = check.into_check();
213
214 assert_eq!(check.commit, CommitId::new(commit));
215 assert_eq!(check.reason, reason);
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use git_checks_core::{Check, TopicCheck};
222 use git_workarea::CommitId;
223
224 use crate::test::*;
225 use crate::BadCommit;
226
227 const NO_EXIST_COMMIT: &str = "0000000000000000000000000000000000000000";
228 const GOOD_COMMIT: &str = "7b0c51ed98a23a32718ed7014d6d4a813423f1bd";
229 const BAD_COMMIT: &str = "029a00428913ee915ce5ee7250c023abfbc2aca3";
230 const BAD_TOPIC: &str = "3d535904b40868dcba6465cf2c3ce4358501880a";
231
232 #[test]
233 fn test_bad_commit_builder_default() {
234 assert!(BadCommit::builder().build().is_err());
235 }
236
237 #[test]
238 fn test_bad_commit_builder_reason_is_required() {
239 assert!(BadCommit::builder()
240 .commit(CommitId::new(NO_EXIST_COMMIT))
241 .build()
242 .is_err());
243 }
244
245 #[test]
246 fn test_bad_commit_builder_commit_is_required() {
247 assert!(BadCommit::builder().reason("because").build().is_err());
248 }
249
250 #[test]
251 fn test_bad_commit_builder_minimum_fields() {
252 assert!(BadCommit::builder()
253 .commit(CommitId::new(NO_EXIST_COMMIT))
254 .reason("because")
255 .build()
256 .is_ok());
257 }
258
259 #[test]
260 fn test_bad_commit_name_commit() {
261 let check = BadCommit::builder()
262 .commit(CommitId::new(NO_EXIST_COMMIT))
263 .reason("because")
264 .build()
265 .unwrap();
266 assert_eq!(Check::name(&check), "bad-commit");
267 }
268
269 #[test]
270 fn test_bad_commit_name_topic() {
271 let check = BadCommit::builder()
272 .commit(CommitId::new(NO_EXIST_COMMIT))
273 .reason("because")
274 .build()
275 .unwrap();
276 assert_eq!(TopicCheck::name(&check), "bad-commit-topic");
277 }
278
279 #[test]
280 fn test_bad_commit_good_commit() {
281 let check = BadCommit::builder()
282 .commit(CommitId::new(BAD_COMMIT))
283 .reason("because")
284 .build()
285 .unwrap();
286 run_check_ok("test_bad_commit_good_commit", GOOD_COMMIT, check);
287 }
288
289 #[test]
290 fn test_bad_commit_no_bad_commit() {
291 let check = BadCommit::builder()
292 .commit(CommitId::new(NO_EXIST_COMMIT))
294 .reason("because")
295 .build()
296 .unwrap();
297 run_check_ok("test_bad_commit_no_bad_commit", BAD_TOPIC, check);
298 }
299
300 #[test]
301 fn test_bad_commit_already_in_history() {
302 let check = BadCommit::builder()
303 .commit(CommitId::new(FILLER_COMMIT))
305 .reason("because")
306 .build()
307 .unwrap();
308 run_check_ok("test_bad_commit_already_in_history", BAD_TOPIC, check);
309 }
310
311 #[test]
312 fn test_bad_commit_not_already_in_history() {
313 let check = BadCommit::builder()
314 .commit(CommitId::new(BAD_COMMIT))
316 .reason("because")
317 .build()
318 .unwrap();
319 let result = run_check("test_bad_commit_not_already_in_history", BAD_TOPIC, check);
320
321 assert_eq!(result.warnings().len(), 0);
322 assert_eq!(result.alerts().len(), 1);
323 assert_eq!(
324 result.alerts()[0],
325 "commit 029a00428913ee915ce5ee7250c023abfbc2aca3 was pushed to the server.",
326 );
327 assert_eq!(result.errors().len(), 1);
328 assert_eq!(
329 result.errors()[0],
330 "commit 029a00428913ee915ce5ee7250c023abfbc2aca3 is not allowed because.",
331 );
332 assert!(!result.temporary());
333 assert!(!result.allowed());
334 assert!(!result.pass());
335 }
336
337 #[test]
338 fn test_bad_commit_topic_good_commit() {
339 let check = BadCommit::builder()
340 .commit(CommitId::new(BAD_COMMIT))
341 .reason("because")
342 .build()
343 .unwrap();
344 run_topic_check_ok("test_bad_commit_topic_good_commit", GOOD_COMMIT, check);
345 }
346
347 #[test]
348 fn test_bad_commit_topic_no_bad_commit() {
349 let check = BadCommit::builder()
350 .commit(CommitId::new(NO_EXIST_COMMIT))
352 .reason("because")
353 .build()
354 .unwrap();
355 run_topic_check_ok("test_bad_commit_topic_no_bad_commit", BAD_TOPIC, check);
356 }
357
358 #[test]
359 fn test_bad_commit_topic_already_in_history() {
360 let check = BadCommit::builder()
361 .commit(CommitId::new(FILLER_COMMIT))
363 .reason("because")
364 .build()
365 .unwrap();
366 run_topic_check_ok("test_bad_commit_topic_already_in_history", BAD_TOPIC, check);
367 }
368
369 #[test]
370 fn test_bad_commit_topic_not_already_in_history() {
371 let check = BadCommit::builder()
372 .commit(CommitId::new(BAD_COMMIT))
374 .reason("because")
375 .build()
376 .unwrap();
377 let result = run_topic_check(
378 "test_bad_commit_topic_not_already_in_history",
379 BAD_TOPIC,
380 check,
381 );
382
383 assert_eq!(result.warnings().len(), 0);
384 assert_eq!(result.alerts().len(), 1);
385 assert_eq!(
386 result.alerts()[0],
387 "commit 029a00428913ee915ce5ee7250c023abfbc2aca3 was pushed to the server.",
388 );
389 assert_eq!(result.errors().len(), 1);
390 assert_eq!(
391 result.errors()[0],
392 "commit 029a00428913ee915ce5ee7250c023abfbc2aca3 is not allowed because.",
393 );
394 assert!(!result.temporary());
395 assert!(!result.allowed());
396 assert!(!result.pass());
397 }
398}