1use derive_builder::Builder;
10use git_checks_core::impl_prelude::*;
11use itertools::Itertools;
12use lazy_static::lazy_static;
13use regex::Regex;
14
15#[derive(Builder, Debug, Clone)]
26#[builder(field(private))]
27pub struct CommitSubject {
28 #[builder(default = "8")]
33 min_summary: usize,
34 #[builder(default = "78")]
39 max_summary: usize,
40
41 #[builder(default = "true")]
49 check_work_in_progress: bool,
50
51 #[builder(default = "true")]
60 check_rebase_commands: bool,
61
62 #[builder(default = "false")]
71 check_suggestion_subjects: bool,
72
73 #[builder(private)]
74 #[builder(setter(name = "_tolerated_prefixes"))]
75 #[builder(default)]
76 tolerated_prefixes: Vec<Regex>,
77 #[builder(private)]
78 #[builder(setter(name = "_allowed_prefixes"))]
79 #[builder(default)]
80 allowed_prefixes: Vec<String>,
81 #[builder(private)]
82 #[builder(setter(name = "_disallowed_prefixes"))]
83 #[builder(default)]
84 disallowed_prefixes: Vec<String>,
85}
86
87lazy_static! {
88 static ref SUGGESTION_SUBJECTS: Vec<Regex> = vec![
89 Regex::new(r"Apply \d* suggestion\(s\) to \d* file\(s\)").unwrap(),
91 Regex::new("Apply suggestions from code review").unwrap(),
94 ];
95}
96
97impl CommitSubjectBuilder {
98 pub fn tolerated_prefixes<I, P>(&mut self, patterns: I) -> &mut Self
106 where
107 I: IntoIterator<Item = P>,
108 P: Into<Regex>,
109 {
110 self.tolerated_prefixes = Some(patterns.into_iter().map(Into::into).collect());
111 self
112 }
113
114 pub fn allowed_prefixes<I, P>(&mut self, prefixes: I) -> &mut Self
122 where
123 I: IntoIterator<Item = P>,
124 P: Into<String>,
125 {
126 self.allowed_prefixes = Some(prefixes.into_iter().map(Into::into).collect());
127 self
128 }
129
130 pub fn disallowed_prefixes<I, P>(&mut self, prefixes: I) -> &mut Self
138 where
139 I: IntoIterator<Item = P>,
140 P: Into<String>,
141 {
142 self.disallowed_prefixes = Some(prefixes.into_iter().map(Into::into).collect());
143 self
144 }
145}
146
147impl CommitSubject {
148 pub fn builder() -> CommitSubjectBuilder {
150 Default::default()
151 }
152
153 fn is_generated_subject(summary: &str) -> bool {
158 summary.starts_with("Merge ") || summary.starts_with("Revert ")
159 }
160}
161
162impl Default for CommitSubject {
163 fn default() -> Self {
164 CommitSubject {
165 min_summary: 8,
166 max_summary: 78,
167 check_work_in_progress: true,
168 check_rebase_commands: true,
169 check_suggestion_subjects: false,
170 tolerated_prefixes: Vec::new(),
171 allowed_prefixes: Vec::new(),
172 disallowed_prefixes: Vec::new(),
173 }
174 }
175}
176
177impl Check for CommitSubject {
178 fn name(&self) -> &str {
179 "commit-subject"
180 }
181
182 fn check(&self, _: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
183 let mut result = CheckResult::new();
184 let lines = commit.message.trim().lines().collect::<Vec<_>>();
185
186 if lines.is_empty() {
187 result.add_error(format!(
188 "commit {} has an invalid commit subject; it is empty.",
189 commit.sha1,
190 ));
191 return Ok(result);
192 }
193
194 let summary = &lines[0];
195 let summary_len = summary.len();
196
197 if summary_len < self.min_summary {
198 result.add_error(format!(
199 "commit {} has an invalid commit subject; the first line must be at least {} \
200 characters.",
201 commit.sha1, self.min_summary,
202 ));
203 }
204
205 let is_generated = Self::is_generated_subject(summary);
206 if !is_generated && self.max_summary < summary_len {
207 result.add_error(format!(
208 "commit {} has an invalid commit subject; the first line must be no longer than \
209 {} characters.",
210 commit.sha1, self.max_summary,
211 ));
212 }
213
214 if lines.len() >= 2 {
215 if lines.len() >= 2 && !lines[1].is_empty() {
216 result.add_error(format!(
217 "commit {} has an invalid commit subject; the second line must be empty.",
218 commit.sha1,
219 ));
220 }
221
222 if lines.len() == 2 {
223 result.add_error(format!(
224 "commit {} has an invalid commit subject; it cannot be exactly two lines.",
225 commit.sha1,
226 ));
227 } else if lines[2].is_empty() {
228 result.add_error(format!(
229 "commit {} has an invalid commit subject; the third line must not be empty.",
230 commit.sha1,
231 ));
232 }
233 }
234
235 const WIP_PREFIXES: &[&str] = &[
236 "WIP", "wip", "Draft:", "draft:", "[Draft]", "[draft]", "(Draft)", "(draft)",
237 ];
238
239 if self.check_work_in_progress
240 && WIP_PREFIXES
241 .iter()
242 .any(|prefix| summary.starts_with(prefix))
243 {
244 result.add_error(format!(
245 "commit {} cannot be merged; it is marked as a work-in-progress (WIP).",
246 commit.sha1,
247 ));
248 }
249
250 if self.check_rebase_commands {
251 if summary.starts_with("fixup! ") {
252 result.add_error(format!(
253 "commit {} cannot be merged; it is marked as a fixup commit.",
254 commit.sha1,
255 ));
256 } else if summary.starts_with("squash! ") {
257 result.add_error(format!(
258 "commit {} cannot be merged; it is marked as a commit to be squashed.",
259 commit.sha1,
260 ));
261 } else if summary.starts_with("amend! ") {
262 result.add_error(format!(
263 "commit {} cannot be merged; it is marked as an amending commit.",
264 commit.sha1,
265 ));
266 }
267 }
268
269 if self.check_suggestion_subjects {
270 for subject in SUGGESTION_SUBJECTS.iter() {
271 if subject.is_match(summary) {
272 result.add_error(format!(
273 "commit {} cannot be merged; its commit summary appears to have been \
274 automatically generated by a suggestion application mechanism. Please \
275 squash the suggestion(s) into the relevant commit(s) using `git rebase`.",
276 commit.sha1,
277 ));
278 }
279 }
280 }
281
282 if !is_generated {
283 let is_tolerated = self.tolerated_prefixes.iter().any(|regex| {
284 regex
285 .find(summary)
286 .map(|found| found.start() == 0)
287 .unwrap_or(false)
288 });
289 if !is_tolerated {
290 if !self.allowed_prefixes.is_empty() {
291 let is_ok = self
292 .allowed_prefixes
293 .iter()
294 .any(|prefix| summary.starts_with(prefix));
295 if !is_ok {
296 result.add_error(format!(
297 "commit {} cannot be merged; it must start with one of the following \
298 prefixes: `{}`.",
299 commit.sha1,
300 self.allowed_prefixes.iter().format("`, `"),
301 ));
302 }
303 }
304
305 let is_ok = self
306 .disallowed_prefixes
307 .iter()
308 .all(|prefix| !summary.starts_with(prefix));
309 if !is_ok {
310 result.add_error(format!(
311 "commit {} cannot be merged; it cannot start with any of the following \
312 prefixes: `{}`.",
313 commit.sha1,
314 self.disallowed_prefixes.iter().format("`, `"),
315 ));
316 }
317 }
318 }
319
320 Ok(result)
321 }
322}
323
324#[cfg(feature = "config")]
325pub(crate) mod config {
326 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck};
327 use regex::Regex;
328 use serde::de::{Deserializer, Error as SerdeError};
329 use serde::Deserialize;
330 #[cfg(test)]
331 use serde_json::json;
332
333 use crate::CommitSubject;
334
335 #[derive(Debug)]
336 struct RegexConfig(Regex);
337
338 impl<'de> Deserialize<'de> for RegexConfig {
339 fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
340 where
341 D: Deserializer<'de>,
342 {
343 let regex_str = <String as Deserialize>::deserialize(deserializer)?;
344 let regex = Regex::new(®ex_str)
345 .map_err(|err| D::Error::custom(format!("'{}': {}", regex_str, err)))?;
346 Ok(RegexConfig(regex))
347 }
348 }
349
350 impl From<RegexConfig> for Regex {
351 fn from(regex_config: RegexConfig) -> Self {
352 regex_config.0
353 }
354 }
355
356 #[derive(Deserialize, Debug)]
396 pub struct CommitSubjectConfig {
397 #[serde(default)]
398 min_summary: Option<usize>,
399 #[serde(default)]
400 max_summary: Option<usize>,
401
402 #[serde(default)]
403 check_work_in_progress: Option<bool>,
404 #[serde(default)]
405 check_rebase_commands: Option<bool>,
406 #[serde(default)]
407 check_suggestion_subjects: Option<bool>,
408
409 #[serde(default)]
410 tolerated_prefixes: Option<Vec<RegexConfig>>,
411 #[serde(default)]
412 allowed_prefixes: Option<Vec<String>>,
413 #[serde(default)]
414 disallowed_prefixes: Option<Vec<String>>,
415 }
416
417 impl IntoCheck for CommitSubjectConfig {
418 type Check = CommitSubject;
419
420 fn into_check(self) -> Self::Check {
421 let mut builder = CommitSubject::builder();
422
423 if let Some(min_summary) = self.min_summary {
424 builder.min_summary(min_summary);
425 }
426
427 if let Some(max_summary) = self.max_summary {
428 builder.max_summary(max_summary);
429 }
430
431 if let Some(check_work_in_progress) = self.check_work_in_progress {
432 builder.check_work_in_progress(check_work_in_progress);
433 }
434
435 if let Some(check_rebase_commands) = self.check_rebase_commands {
436 builder.check_rebase_commands(check_rebase_commands);
437 }
438
439 if let Some(check_suggestion_subjects) = self.check_suggestion_subjects {
440 builder.check_suggestion_subjects(check_suggestion_subjects);
441 }
442
443 if let Some(tolerated_prefixes) = self.tolerated_prefixes {
444 builder.tolerated_prefixes(tolerated_prefixes);
445 }
446
447 if let Some(allowed_prefixes) = self.allowed_prefixes {
448 builder.allowed_prefixes(allowed_prefixes);
449 }
450
451 if let Some(disallowed_prefixes) = self.disallowed_prefixes {
452 builder.disallowed_prefixes(disallowed_prefixes);
453 }
454
455 builder
456 .build()
457 .expect("configuration mismatch for `CommitSubject`")
458 }
459 }
460
461 register_checks! {
462 CommitSubjectConfig {
463 "commit_subject" => CommitCheckConfig,
464 },
465 }
466
467 #[test]
468 fn test_commit_subject_config_empty() {
469 let json = json!({});
470 let check: CommitSubjectConfig = serde_json::from_value(json).unwrap();
471
472 assert_eq!(check.min_summary, None);
473 assert_eq!(check.max_summary, None);
474 assert_eq!(check.check_work_in_progress, None);
475 assert_eq!(check.check_rebase_commands, None);
476 assert_eq!(check.check_suggestion_subjects, None);
477 assert!(check.tolerated_prefixes.is_none());
478 assert_eq!(check.allowed_prefixes, None);
479 assert_eq!(check.disallowed_prefixes, None);
480
481 let check = check.into_check();
482
483 assert_eq!(check.min_summary, 8);
484 assert_eq!(check.max_summary, 78);
485 assert!(check.check_work_in_progress);
486 assert!(check.check_rebase_commands);
487 assert!(!check.check_suggestion_subjects);
488 assert!(check.tolerated_prefixes.is_empty());
489 itertools::assert_equal(&check.allowed_prefixes, &[] as &[&str]);
490 itertools::assert_equal(&check.disallowed_prefixes, &[] as &[&str]);
491 }
492
493 #[test]
494 fn test_commit_subject_config_all_fields() {
495 let exp_tprefix: String = "tolerated".into();
496 let exp_aprefix: String = "allowed".into();
497 let exp_dprefix: String = "disallowed".into();
498 let json = json!({
499 "min_summary": 1,
500 "max_summary": 100,
501 "check_work_in_progress": false,
502 "check_rebase_commands": false,
503 "check_suggestion_subjects": true,
504 "tolerated_prefixes": [exp_tprefix],
505 "allowed_prefixes": [exp_aprefix],
506 "disallowed_prefixes": [exp_dprefix],
507 });
508 let check: CommitSubjectConfig = serde_json::from_value(json).unwrap();
509
510 assert_eq!(check.min_summary, Some(1));
511 assert_eq!(check.max_summary, Some(100));
512 assert_eq!(check.check_work_in_progress, Some(false));
513 assert_eq!(check.check_rebase_commands, Some(false));
514 assert_eq!(check.check_suggestion_subjects, Some(true));
515 itertools::assert_equal(
516 check
517 .tolerated_prefixes
518 .as_ref()
519 .unwrap()
520 .iter()
521 .map(|re| re.0.as_str()),
522 &[exp_tprefix.clone()],
523 );
524 itertools::assert_equal(&check.allowed_prefixes, &Some([exp_aprefix.clone()]));
525 itertools::assert_equal(&check.disallowed_prefixes, &Some([exp_dprefix.clone()]));
526
527 let check = check.into_check();
528
529 assert_eq!(check.min_summary, 1);
530 assert_eq!(check.max_summary, 100);
531 assert!(!check.check_work_in_progress);
532 assert!(!check.check_rebase_commands);
533 assert!(check.check_suggestion_subjects);
534 itertools::assert_equal(
535 check.tolerated_prefixes.iter().map(|re| re.as_str()),
536 &[exp_tprefix],
537 );
538 itertools::assert_equal(&check.allowed_prefixes, &[exp_aprefix]);
539 itertools::assert_equal(&check.disallowed_prefixes, &[exp_dprefix]);
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use git_checks_core::Check;
546 use regex::Regex;
547
548 use crate::test::*;
549 use crate::CommitSubject;
550
551 const BAD_TOPIC: &str = "891db15952303d4f18ca23070c1bf054bc51a15c";
552
553 #[test]
554 fn test_commit_subject_builder_default() {
555 assert!(CommitSubject::builder().build().is_ok());
556 }
557
558 #[test]
559 fn test_commit_subject_name_commit() {
560 let check = CommitSubject::default();
561 assert_eq!(Check::name(&check), "commit-subject");
562 }
563
564 #[test]
565 fn test_commit_subject() {
566 let check = CommitSubject::default();
567 let result = run_check("test_commit_subject", BAD_TOPIC, check);
568 test_result_errors(result, &[
569 "commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c has an invalid commit subject; the \
570 first line must be at least 8 characters.",
571 "commit 1afc6b3584580488917fc61aa5e5298e98583805 has an invalid commit subject; the \
572 first line must be no longer than 78 characters.",
573 "commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; the \
574 second line must be empty.",
575 "commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; it \
576 cannot be exactly two lines.",
577 "commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
578 second line must be empty.",
579 "commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
580 third line must not be empty.",
581 "commit e478f630b586e331753477eba88059d644927be8 cannot be merged; it is marked as a \
582 work-in-progress (WIP).",
583 "commit 9039b9a4813fa019229e960033fe1ae8514a0c8e cannot be merged; it is marked as a \
584 work-in-progress (WIP).",
585 "commit 11dbbbff3f32445d74d1a8d96df0a49381c81ba0 cannot be merged; it is marked as a \
586 work-in-progress (WIP).",
587 "commit 54d673ff559a72ce6343bd9526a950d79034b24e cannot be merged; it is marked as a \
588 fixup commit.",
589 "commit 5f7284fe1599265c90550b681a4bf0763bc1de21 cannot be merged; it is marked as a \
590 commit to be squashed.",
591 "commit c4a9d2f34a687134e0d5d47ff576d34966381f56 has an invalid commit subject; it is \
592 empty.",
593 "commit 12b564db852a80190950caf5b363a4939b822730 cannot be merged; it is marked as an \
594 amending commit.",
595 ]);
596 }
597
598 #[test]
599 fn test_commit_subject_with_suggestions() {
600 let check = CommitSubject::builder()
601 .check_work_in_progress(false)
602 .check_rebase_commands(false)
603 .check_suggestion_subjects(true)
604 .build()
605 .unwrap();
606 let result = run_check("test_commit_subject_with_suggestions", BAD_TOPIC, check);
607 test_result_errors(result, &[
608 "commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c has an invalid commit subject; the \
609 first line must be at least 8 characters.",
610 "commit 1afc6b3584580488917fc61aa5e5298e98583805 has an invalid commit subject; the \
611 first line must be no longer than 78 characters.",
612 "commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; the \
613 second line must be empty.",
614 "commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; it \
615 cannot be exactly two lines.",
616 "commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
617 second line must be empty.",
618 "commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
619 third line must not be empty.",
620 "commit c4a9d2f34a687134e0d5d47ff576d34966381f56 has an invalid commit subject; it is \
621 empty.",
622 "commit c52a84631f35561043cb7cca6c136f94bd9fc757 cannot be merged; its commit summary \
623 appears to have been automatically generated by a suggestion application mechanism. \
624 Please squash the suggestion(s) into the relevant commit(s) using `git rebase`.",
625 "commit 891db15952303d4f18ca23070c1bf054bc51a15c cannot be merged; its commit summary \
626 appears to have been automatically generated by a suggestion application mechanism. \
627 Please squash the suggestion(s) into the relevant commit(s) using `git rebase`.",
628 ]);
629 }
630
631 #[test]
632 fn test_commit_subject_allowed_prefixes() {
633 let check = CommitSubject::builder()
634 .check_work_in_progress(false)
635 .check_rebase_commands(false)
636 .allowed_prefixes(["commit message "].iter().cloned())
637 .build()
638 .unwrap();
639 let result = run_check("test_commit_subject_allowed_prefixes", BAD_TOPIC, check);
640 test_result_errors(result, &[
641 "commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c has an invalid commit subject; the \
642 first line must be at least 8 characters.",
643 "commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c cannot be merged; it must start with one of the following prefixes: `commit message `.",
644 "commit 1afc6b3584580488917fc61aa5e5298e98583805 has an invalid commit subject; the \
645 first line must be no longer than 78 characters.",
646 "commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; the \
647 second line must be empty.",
648 "commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; it \
649 cannot be exactly two lines.",
650 "commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
651 second line must be empty.",
652 "commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
653 third line must not be empty.",
654 "commit e478f630b586e331753477eba88059d644927be8 cannot be merged; it must start with \
655 one of the following prefixes: `commit message `.",
656 "commit 9039b9a4813fa019229e960033fe1ae8514a0c8e cannot be merged; it must start with \
657 one of the following prefixes: `commit message `.",
658 "commit 11dbbbff3f32445d74d1a8d96df0a49381c81ba0 cannot be merged; it must start with \
659 one of the following prefixes: `commit message `.",
660 "commit 54d673ff559a72ce6343bd9526a950d79034b24e cannot be merged; it must start with \
661 one of the following prefixes: `commit message `.",
662 "commit 5f7284fe1599265c90550b681a4bf0763bc1de21 cannot be merged; it must start with \
663 one of the following prefixes: `commit message `.",
664 "commit c4a9d2f34a687134e0d5d47ff576d34966381f56 has an invalid commit subject; it is \
665 empty.",
666 "commit 12b564db852a80190950caf5b363a4939b822730 cannot be merged; it must start with \
667 one of the following prefixes: `commit message `.",
668 "commit c52a84631f35561043cb7cca6c136f94bd9fc757 cannot be merged; it must start with \
669 one of the following prefixes: `commit message `.",
670 "commit 891db15952303d4f18ca23070c1bf054bc51a15c cannot be merged; it must start with \
671 one of the following prefixes: `commit message `.",
672 ]);
673 }
674
675 #[test]
676 fn test_commit_subject_disallowed_prefixes() {
677 let check = CommitSubject::builder()
678 .check_work_in_progress(false)
679 .check_rebase_commands(false)
680 .disallowed_prefixes(["commit message "].iter().cloned())
681 .build()
682 .unwrap();
683 let result = run_check("test_commit_subject_disallowed_prefixes", BAD_TOPIC, check);
684 test_result_errors(result, &[
685 "commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c has an invalid commit subject; the \
686 first line must be at least 8 characters.",
687 "commit 1afc6b3584580488917fc61aa5e5298e98583805 has an invalid commit subject; the \
688 first line must be no longer than 78 characters.",
689 "commit 1afc6b3584580488917fc61aa5e5298e98583805 cannot be merged; it cannot start \
690 with any of the following prefixes: `commit message `.",
691 "commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; the \
692 second line must be empty.",
693 "commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; it \
694 cannot be exactly two lines.",
695 "commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 cannot be merged; it cannot start \
696 with any of the following prefixes: `commit message `.",
697 "commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
698 second line must be empty.",
699 "commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
700 third line must not be empty.",
701 "commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 cannot be merged; it cannot start \
702 with any of the following prefixes: `commit message `.",
703 "commit c4a9d2f34a687134e0d5d47ff576d34966381f56 has an invalid commit subject; it is \
704 empty.",
705 ]);
706 }
707
708 #[test]
709 fn test_commit_subject_tolerated_prefixes() {
710 let check = CommitSubject::builder()
711 .check_work_in_progress(false)
712 .check_rebase_commands(false)
713 .tolerated_prefixes(
714 [
715 "^(commit message )",
716 "^([Ww][Ii][Pp]|fixup|squash|amend)",
717 "Apply",
718 "hort",
721 ]
722 .iter()
723 .map(|patt| Regex::new(patt).unwrap()),
724 )
725 .allowed_prefixes(["allowed prefix "].iter().cloned())
726 .disallowed_prefixes(["commit message "].iter().cloned())
727 .build()
728 .unwrap();
729 let result = run_check("test_commit_subject_tolerated_prefixes", BAD_TOPIC, check);
730 test_result_errors(
731 result,
732 &[
733 "commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c has an invalid commit subject; \
734 the first line must be at least 8 characters.",
735 "commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c cannot be merged; it must start \
736 with one of the following prefixes: `allowed prefix `.",
737 "commit 1afc6b3584580488917fc61aa5e5298e98583805 has an invalid commit subject; \
738 the first line must be no longer than 78 characters.",
739 "commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; \
740 the second line must be empty.",
741 "commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; \
742 it cannot be exactly two lines.",
743 "commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; \
744 the second line must be empty.",
745 "commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; \
746 the third line must not be empty.",
747 "commit c4a9d2f34a687134e0d5d47ff576d34966381f56 has an invalid commit subject; \
748 it is empty.",
749 ],
750 );
751 }
752}