1#![deny(missing_docs)]
2pub use crate::config::Config;
29use crate::parser::{ParsedTag, Parser, Print, Tags};
30use chrono::{offset::Utc, TimeZone};
31use failure::{bail, Error};
32use git2::{ObjectType, Oid, Repository};
33use log::{info, warn, LevelFilter};
34use rayon::prelude::*;
35use std::{
36 collections::BTreeMap,
37 env,
38 fs::{self, File, OpenOptions},
39 io::prelude::*,
40 path::{Path, PathBuf},
41};
42use toml::Value;
43
44pub mod config;
45mod parser;
46
47pub struct GitJournal {
49 pub config: Config,
51 parser: Parser,
52 path: String,
53 tags: Vec<(Oid, String)>,
54}
55
56impl GitJournal {
57 pub fn new(path: &str) -> Result<Self, Error> {
72 let mut path_buf = if path != "." {
74 PathBuf::from(path)
75 } else {
76 env::current_dir()?
77 };
78 'git_search: loop {
79 for dir in fs::read_dir(&path_buf)? {
80 let dir_path = dir?.path();
81 if dir_path.ends_with(".git") {
82 break 'git_search;
83 }
84 }
85 if !path_buf.pop() {
86 break;
87 }
88 }
89
90 let repo = Repository::open(&path_buf)?;
92
93 let mut new_tags = vec![];
95 for name in repo.tag_names(None)?.iter() {
96 let name = name.ok_or_else(|| {
97 git2::Error::from_str("Could not receive tag name")
98 })?;
99 let obj = repo.revparse_single(name)?;
100 if let Ok(tag) = obj.into_tag() {
101 let tag_name = tag
102 .name()
103 .ok_or_else(|| {
104 git2::Error::from_str("Could not parse tag name")
105 })?
106 .to_owned();
107 new_tags.push((tag.target_id(), tag_name));
108 }
109 }
110
111 let mut new_config = Config::new();
113 if let Err(e) = new_config.load(path) {
114 println!("Can't load configuration file, using default one: {}", e);
115 }
116
117 if new_config.enable_debug {
119 if new_config.colored_output {
120 if mowl::init_with_level(LevelFilter::Info).is_err() {
121 warn!("Logger already set.");
122 };
123 } else {
124 if mowl::init_with_level_and_without_colors(LevelFilter::Info)
125 .is_err()
126 {
127 warn!("Logger already set.");
128 };
129 }
130 }
131
132 let new_parser = Parser {
134 config: new_config.clone(),
135 result: vec![],
136 };
137
138 Ok(GitJournal {
140 config: new_config,
141 parser: new_parser,
142 path: path_buf.to_str().unwrap_or("").to_owned(),
143 tags: new_tags,
144 })
145 }
146
147 pub fn setup(&self) -> Result<(), Error> {
204 let output_file = Config::new().save_default_config(&self.path)?;
206 info!("Defaults written to '{}' file.", output_file);
207
208 self.install_git_hook("commit-msg", "git journal v $1\n")?;
210
211 self.install_git_hook("prepare-commit-msg", "git journal p $1 $2\n")?;
213
214 Ok(())
215 }
216
217 fn install_git_hook(&self, name: &str, content: &str) -> Result<(), Error> {
218 let mut hook_path = PathBuf::from(&self.path);
219 hook_path.push(".git/hooks");
220 hook_path.push(name);
221 let mut hook_file: File;
222
223 if hook_path.exists() {
224 warn!(
225 "There is already a hook available in '{}'. Please verifiy \
226 the hook by hand after the installation.",
227 hook_path.display()
228 );
229 hook_file = OpenOptions::new()
230 .read(true)
231 .append(true)
232 .open(&hook_path)?;
233 let mut hook_content = String::new();
234 hook_file.read_to_string(&mut hook_content)?;
235 if hook_content.contains(content) {
236 info!(
237 "Hook already installed, nothing changed in existing hook."
238 );
239 return Ok(());
240 }
241 } else {
242 hook_file = File::create(&hook_path)?;
243 hook_file.write_all(b"#!/usr/bin/env sh\n")?;
244 }
245 hook_file.write_all(content.as_bytes())?;
246 self.chmod(&hook_path, 0o755)?;
247
248 info!("Git hook installed to '{}'.", hook_path.display());
249 Ok(())
250 }
251
252 #[cfg(unix)]
253 fn chmod(&self, path: &Path, perms: u32) -> Result<(), Error> {
254 use std::os::unix::prelude::PermissionsExt;
255 fs::set_permissions(path, fs::Permissions::from_mode(perms))?;
256 Ok(())
257 }
258
259 #[cfg(windows)]
260 fn chmod(&self, _path: &Path, _perms: u32) -> Result<(), Error> {
261 Ok(())
262 }
263
264 pub fn prepare(
281 &self,
282 path: &str,
283 commit_type: Option<&str>,
284 ) -> Result<(), Error> {
285 if let Err(error) = self.verify(path) {
288 if let Some(commit_type) = commit_type {
291 if commit_type == "message" {
292 return Err(error);
293 }
294 }
295
296 let mut read_file = File::open(path)?;
298 let mut commit_message = String::new();
299 read_file.read_to_string(&mut commit_message)?;
300
301 let mut file = OpenOptions::new().write(true).open(path)?;
303 let mut old_msg_vec = commit_message
304 .lines()
305 .filter_map(|line| {
306 if !line.is_empty() {
307 if line.starts_with('#') {
308 Some(line.to_owned())
309 } else {
310 Some("# ".to_owned() + line)
311 }
312 } else {
313 None
314 }
315 })
316 .collect::<Vec<_>>();
317 if !old_msg_vec.is_empty() {
318 old_msg_vec
319 .insert(0, "# The provided commit message:".to_owned());
320 }
321 let prefix = if self.config.template_prefix.is_empty() {
322 "".to_owned()
323 } else {
324 self.config.template_prefix.clone() + " "
325 };
326 let new_content = prefix
327 + &self.config.categories[0]
328 + " ...\n\n# Add a more detailed description if needed\n\n# - "
329 + &self.config.categories.join("\n# - ")
330 + "\n\n"
331 + &old_msg_vec.join("\n");
332 file.write_all(new_content.as_bytes())?;
333 }
334 Ok(())
335 }
336
337 pub fn verify(&self, path: &str) -> Result<(), Error> {
355 let mut file = File::open(path)?;
357 let mut commit_message = String::new();
358 file.read_to_string(&mut commit_message)?;
359
360 let parsed_commit =
362 self.parser.parse_commit_message(&commit_message, None)?;
363 let tags = parsed_commit.get_tags_unique(vec![]);
364
365 if let Some(ref template) = self.config.default_template {
368 let mut path_buf = PathBuf::from(&self.path);
369 path_buf.push(template);
370 let mut file = File::open(path_buf)?;
371 let mut toml_string = String::new();
372 file.read_to_string(&mut toml_string)?;
373
374 let toml = toml::from_str(&toml_string)?;
376 let toml_tags = self.parser.get_tags_from_toml(&toml, vec![]);
377 let invalid_tags = tags
378 .into_iter()
379 .filter(|tag| !toml_tags.contains(tag))
380 .collect::<Vec<String>>();
381 if !invalid_tags.is_empty() {
382 warn!(
383 "These tags are not part of the default template: '{}'.",
384 invalid_tags.join(", ")
385 );
386 bail!("Not all tags exists in the default template.");
387 }
388 }
389 Ok(())
390 }
391
392 pub fn parse_log(
407 &mut self,
408 revision_range: &str,
409 tag_skip_pattern: &str,
410 max_tags_count: &u32,
411 all: &bool,
412 skip_unreleased: &bool,
413 ignore_tags: Option<Vec<&str>>,
414 ) -> Result<(), Error> {
415 let repo = Repository::open(&self.path)?;
416 let mut revwalk = repo.revwalk()?;
417 revwalk.set_sorting(git2::Sort::TIME);
418
419 let revspec = repo.revparse(revision_range)?;
421 if revspec.mode().contains(git2::RevparseMode::SINGLE) {
422 let from = revspec.from().ok_or_else(|| {
424 git2::Error::from_str("Could not set revision range start")
425 })?;
426 revwalk.push(from.id())?;
427 } else {
428 let from = revspec.from().ok_or_else(|| {
430 git2::Error::from_str("Could not set revision range start")
431 })?;
432 let to = revspec.to().ok_or_else(|| {
433 git2::Error::from_str("Could not set revision range end")
434 })?;
435 revwalk.push(to.id())?;
436 if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) {
437 let base = repo.merge_base(from.id(), to.id())?;
438 let o = repo.find_object(base, Some(ObjectType::Commit))?;
439 revwalk.push(o.id())?;
440 }
441 revwalk.hide(from.id())?;
442 }
443
444 let mut num_parsed_tags: u32 = 1;
446 let unreleased_str = "Unreleased";
447 let mut current_tag = ParsedTag {
448 name: unreleased_str.to_owned(),
449 date: Utc::today(),
450 commits: vec![],
451 message_ids: vec![],
452 };
453 let mut worker_vec = vec![];
454 'revloop: for (index, id) in revwalk.enumerate() {
455 let oid = id?;
456 let commit = repo.find_commit(oid)?;
457 for tag in self.tags.iter().filter(|tag| {
458 tag.0.as_bytes() == oid.as_bytes()
459 && !tag.1.contains(tag_skip_pattern)
460 }) {
461 if !current_tag.message_ids.is_empty() {
463 self.parser.result.push(current_tag.clone());
464 }
465
466 if !all && index > 0 && num_parsed_tags > *max_tags_count {
468 break 'revloop;
469 }
470
471 num_parsed_tags += 1;
473 let date = Utc.timestamp(commit.time().seconds(), 0).date();
474 current_tag = ParsedTag {
475 name: tag.1.clone(),
476 date,
477 commits: vec![],
478 message_ids: vec![],
479 };
480 }
481
482 if *skip_unreleased && current_tag.name == unreleased_str {
485 continue;
486 }
487
488 let message = commit.message().ok_or_else(|| {
491 git2::Error::from_str("Commit message error.")
492 })?;
493 let id = worker_vec.len();
494
495 worker_vec.push((message.to_owned(), oid, None));
498 current_tag.message_ids.push(id);
499 }
500
501 if !current_tag.message_ids.is_empty()
503 && !self.parser.result.contains(¤t_tag)
504 {
505 self.parser.result.push(current_tag);
506 }
507
508 worker_vec.par_iter_mut().for_each(
510 |&mut (ref message, ref oid, ref mut result)| {
511 match self.parser.parse_commit_message(message, Some(*oid)) {
512 Ok(parsed_message) => match ignore_tags {
513 Some(ref tags) => {
514 for tag in tags {
515 if !parsed_message.contains_tag(Some(tag)) {
517 *result = Some(parsed_message.clone())
518 }
519 }
520 }
521 _ => *result = Some(parsed_message),
522 },
523 Err(e) => warn!("Skipping commit: {}", e),
524 }
525 },
526 );
527
528 self.parser.result = self
530 .parser
531 .result
532 .clone()
533 .into_iter()
534 .filter_map(|mut parsed_tag| {
535 for id in &parsed_tag.message_ids {
536 if let Some(parsed_commit) = worker_vec[*id].2.clone() {
537 parsed_tag.commits.push(parsed_commit);
538 }
539 }
540 if parsed_tag.commits.is_empty() {
541 None
542 } else {
543 if self.config.sort_by == "name" {
544 parsed_tag.commits.sort_by(|l, r| {
545 l.summary.category.cmp(&r.summary.category)
546 });
547 }
548 Some(parsed_tag)
549 }
550 })
551 .collect::<Vec<ParsedTag>>();
552
553 info!(
554 "Parsing done. Processed {} commit messages.",
555 worker_vec.len()
556 );
557 Ok(())
558 }
559
560 pub fn generate_template(&self) -> Result<(), Error> {
577 let mut tags = vec![parser::TOML_DEFAULT_KEY.to_owned()];
578
579 for parsed_tag in &self.parser.result {
581 tags = parsed_tag.get_tags_unique(tags);
582 }
583
584 if tags.len() > 1 {
585 info!("Found tags: '{}'.", tags[1..].join(", "));
586 } else {
587 warn!("No tags found.");
588 }
589
590 let mut toml_map = BTreeMap::new();
592 let toml_tags = tags
593 .iter()
594 .map(|tag| {
595 let mut map = BTreeMap::new();
596 map.insert(
597 parser::TOML_TAG.to_owned(),
598 Value::String(tag.to_owned()),
599 );
600 map.insert(
601 parser::TOML_NAME_KEY.to_owned(),
602 Value::String(tag.to_owned()),
603 );
604 map.insert(
605 parser::TOML_FOOTERS_KEY.to_owned(),
606 Value::Array(vec![]),
607 );
608 Value::Table(map)
609 })
610 .collect::<Vec<Value>>();
611 toml_map.insert("tags".to_owned(), Value::Array(toml_tags));
612
613 let mut header_footer_map = BTreeMap::new();
614 header_footer_map
615 .insert(parser::TOML_ONCE_KEY.to_owned(), Value::Boolean(false));
616 header_footer_map.insert(
617 parser::TOML_TEXT_KEY.to_owned(),
618 Value::String(String::new()),
619 );
620 toml_map.insert(
621 parser::TOML_HEADER_KEY.to_owned(),
622 Value::Table(header_footer_map.clone()),
623 );
624 toml_map.insert(
625 parser::TOML_FOOTER_KEY.to_owned(),
626 Value::Table(header_footer_map),
627 );
628
629 let toml = Value::Table(toml_map);
630
631 let mut path_buf = PathBuf::from(&self.path);
633 path_buf.push("template.toml");
634 let toml_string = toml::to_string(&toml)?;
635 let mut toml_file = File::create(&path_buf)?;
636 toml_file.write_all(toml_string.as_bytes())?;
637
638 info!("Template written to '{}'", path_buf.display());
639 Ok(())
640 }
641
642 pub fn print_log(
663 &self,
664 compact: bool,
665 template: Option<&str>,
666 output: Option<&str>,
667 ) -> Result<(), Error> {
668 let mut default_template = PathBuf::from(&self.path);
670 let used_template = match self.config.default_template {
671 Some(ref default_template_file) => {
672 default_template.push(default_template_file);
673
674 match template {
675 None => {
676 if default_template.exists() {
677 info!(
678 "Using default template '{}'.",
679 default_template.display()
680 );
681 default_template.to_str()
682 } else {
683 warn!(
684 "The default template '{}' does not exist.",
685 default_template.display()
686 );
687 None
688 }
689 }
690 Some(t) => Some(t),
691 }
692 }
693 None => template,
694 };
695
696 let output_vec = self.parser.print(&compact, used_template)?;
698
699 if let Some(output) = output {
701 let mut output_file =
702 OpenOptions::new().create(true).append(true).open(output)?;
703 output_file.write_all(&output_vec)?;
704 info!("Output written to '{}'.", output);
705 }
706
707 Ok(())
708 }
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714
715 #[test]
716 fn new() {
717 assert!(GitJournal::new(".").is_ok());
718 let res = GitJournal::new("/dev/null");
719 assert!(res.is_err());
720 if let Err(e) = res {
721 println!("{}", e);
722 }
723 }
724
725 #[test]
726 fn setup_succeed() {
727 let path = ".";
728 let journal = GitJournal::new(path);
729 assert!(journal.is_ok());
730 assert!(journal.unwrap().setup().is_ok());
731 assert!(GitJournal::new(path).is_ok());
732 }
733
734 #[test]
735 fn setup_failed() {
736 let journal = GitJournal::new("./tests/test_repo");
737 assert!(journal.is_ok());
738 let res = journal.unwrap().setup();
739 assert!(res.is_err());
740 if let Err(e) = res {
741 println!("{}", e);
742 }
743 }
744
745 #[test]
746 fn verify_commit_msg_summary_success_1() {
747 let journal = GitJournal::new(".").unwrap();
748 assert!(journal.verify("./tests/commit_messages/success_1").is_ok());
749 }
750
751 #[test]
752 fn verify_commit_msg_summary_success_2() {
753 let journal = GitJournal::new(".").unwrap();
754 assert!(journal.verify("./tests/commit_messages/success_2").is_ok());
755 }
756
757 #[test]
758 fn verify_commit_msg_summary_success_3() {
759 let journal = GitJournal::new(".").unwrap();
760 assert!(journal.verify("./tests/commit_messages/success_3").is_ok());
761 }
762
763 #[test]
764 fn verify_commit_msg_summary_success_4() {
765 let journal = GitJournal::new(".").unwrap();
766 assert!(journal.verify("./tests/commit_messages/success_4").is_ok());
767 }
768
769 fn verify_failure(path: &str) {
770 let journal = GitJournal::new(".").unwrap();
771 let res = journal.verify(path);
772 assert!(res.is_err());
773 if let Err(e) = res {
774 println!("{}", e);
775 }
776 }
777
778 #[test]
779 fn verify_commit_msg_summary_failure_1() {
780 verify_failure("./tests/commit_messages/failure_1");
781 }
782
783 #[test]
784 fn verify_commit_msg_summary_failure_2() {
785 verify_failure("./tests/commit_messages/failure_2");
786 }
787
788 #[test]
789 fn verify_commit_msg_summary_failure_3() {
790 verify_failure("./tests/commit_messages/failure_3");
791 }
792
793 #[test]
794 fn verify_commit_msg_paragraph_failure_1() {
795 verify_failure("./tests/commit_messages/failure_4");
796 }
797
798 #[test]
799 fn verify_commit_msg_paragraph_failure_2() {
800 verify_failure("./tests/commit_messages/failure_5");
801 }
802
803 #[test]
804 fn verify_commit_msg_paragraph_failure_3() {
805 verify_failure("./tests/commit_messages/failure_6");
806 }
807
808 #[test]
809 fn verify_commit_msg_summary_failure_tag() {
810 let journal = GitJournal::new("./tests/test_repo2").unwrap();
811 assert!(journal.verify("./tests/commit_messages/success_1").is_err());
812 assert!(journal.verify("./tests/commit_messages/success_3").is_err());
813 }
814
815 #[test]
816 fn parse_and_print_log_1() {
817 let mut journal = GitJournal::new("./tests/test_repo").unwrap();
818 assert_eq!(journal.tags.len(), 2);
819 assert_eq!(journal.parser.result.len(), 0);
820 assert_eq!(journal.config.show_prefix, false);
821 assert_eq!(journal.config.colored_output, true);
822 assert_eq!(journal.config.show_commit_hash, false);
823 assert_eq!(journal.config.excluded_commit_tags.len(), 0);
824 assert!(journal
825 .parse_log("HEAD", "rc", &0, &true, &false, None)
826 .is_ok());
827 assert_eq!(journal.parser.result.len(), journal.tags.len() + 1);
828 assert_eq!(journal.parser.result[0].commits.len(), 15);
829 assert_eq!(journal.parser.result[1].commits.len(), 1);
830 assert_eq!(journal.parser.result[2].commits.len(), 2);
831 assert!(journal.print_log(false, None, Some("CHANGELOG.md")).is_ok());
832 assert!(journal.print_log(true, None, Some("CHANGELOG.md")).is_ok());
833 assert!(journal
834 .print_log(
835 false,
836 Some("./tests/template.toml"),
837 Some("CHANGELOG.md")
838 )
839 .is_ok());
840 assert!(journal
841 .print_log(
842 true,
843 Some("./tests/template.toml"),
844 Some("CHANGELOG.md")
845 )
846 .is_ok());
847 }
848
849 #[test]
850 fn parse_and_print_log_2() {
851 let mut journal = GitJournal::new("./tests/test_repo").unwrap();
852 assert!(journal
853 .parse_log("HEAD", "rc", &1, &false, &false, None)
854 .is_ok());
855 assert_eq!(journal.parser.result.len(), 2);
856 assert_eq!(journal.parser.result[0].name, "Unreleased");
857 assert_eq!(journal.parser.result[1].name, "v2");
858 assert!(journal.print_log(false, None, Some("CHANGELOG.md")).is_ok());
859 assert!(journal.print_log(true, None, Some("CHANGELOG.md")).is_ok());
860 assert!(journal
861 .print_log(
862 false,
863 Some("./tests/template.toml"),
864 Some("CHANGELOG.md")
865 )
866 .is_ok());
867 assert!(journal
868 .print_log(
869 true,
870 Some("./tests/template.toml"),
871 Some("CHANGELOG.md")
872 )
873 .is_ok());
874 }
875
876 #[test]
877 fn parse_and_print_log_3() {
878 let mut journal = GitJournal::new("./tests/test_repo").unwrap();
879 assert!(journal
880 .parse_log("HEAD", "rc", &1, &false, &true, None)
881 .is_ok());
882 assert_eq!(journal.parser.result.len(), 1);
883 assert_eq!(journal.parser.result[0].name, "v2");
884 assert!(journal.print_log(false, None, Some("CHANGELOG.md")).is_ok());
885 assert!(journal.print_log(true, None, Some("CHANGELOG.md")).is_ok());
886 assert!(journal
887 .print_log(
888 false,
889 Some("./tests/template.toml"),
890 Some("CHANGELOG.md")
891 )
892 .is_ok());
893 assert!(journal
894 .print_log(
895 true,
896 Some("./tests/template.toml"),
897 Some("CHANGELOG.md")
898 )
899 .is_ok());
900 }
901
902 #[test]
903 fn parse_and_print_log_4() {
904 let mut journal = GitJournal::new("./tests/test_repo").unwrap();
905 assert!(journal
906 .parse_log("HEAD", "rc", &2, &false, &true, None)
907 .is_ok());
908 assert_eq!(journal.parser.result.len(), 2);
909 assert_eq!(journal.parser.result[0].name, "v2");
910 assert_eq!(journal.parser.result[1].name, "v1");
911 assert!(journal.print_log(false, None, Some("CHANGELOG.md")).is_ok());
912 assert!(journal.print_log(true, None, Some("CHANGELOG.md")).is_ok());
913 assert!(journal
914 .print_log(
915 false,
916 Some("./tests/template.toml"),
917 Some("CHANGELOG.md")
918 )
919 .is_ok());
920 assert!(journal
921 .print_log(
922 true,
923 Some("./tests/template.toml"),
924 Some("CHANGELOG.md")
925 )
926 .is_ok());
927 }
928
929 #[test]
930 fn parse_and_print_log_5() {
931 let mut journal = GitJournal::new("./tests/test_repo").unwrap();
932 assert!(journal
933 .parse_log("v1..v2", "rc", &0, &true, &false, None)
934 .is_ok());
935 assert_eq!(journal.parser.result.len(), 1);
936 assert_eq!(journal.parser.result[0].name, "v2");
937 assert!(journal.print_log(false, None, Some("CHANGELOG.md")).is_ok());
938 assert!(journal.print_log(true, None, Some("CHANGELOG.md")).is_ok());
939 assert!(journal
940 .print_log(
941 false,
942 Some("./tests/template.toml"),
943 Some("CHANGELOG.md")
944 )
945 .is_ok());
946 assert!(journal
947 .print_log(
948 true,
949 Some("./tests/template.toml"),
950 Some("CHANGELOG.md")
951 )
952 .is_ok());
953 }
954
955 #[test]
956 fn parse_and_print_log_6() {
957 let mut journal = GitJournal::new("./tests/test_repo2").unwrap();
958 assert!(journal
959 .parse_log("HEAD", "rc", &0, &true, &false, None)
960 .is_ok());
961 assert!(journal.print_log(false, None, Some("CHANGELOG.md")).is_ok());
962 }
963
964 #[test]
965 fn prepare_message_success_1() {
966 let journal = GitJournal::new(".").unwrap();
967 assert!(journal.prepare("./tests/COMMIT_EDITMSG", None).is_ok());
968 }
969
970 #[test]
971 fn prepare_message_success_2() {
972 let journal = GitJournal::new(".").unwrap();
973 assert!(journal
974 .prepare("./tests/commit_messages/prepare_1", None)
975 .is_ok());
976 }
977
978 #[test]
979 fn prepare_message_success_3() {
980 let journal = GitJournal::new(".").unwrap();
981 assert!(journal
982 .prepare("./tests/commit_messages/prepare_2", None)
983 .is_ok());
984 }
985
986 #[test]
987 fn prepare_message_success_4() {
988 let journal = GitJournal::new(".").unwrap();
989 assert!(journal
990 .prepare("./tests/commit_messages/prepare_4", None)
991 .is_ok());
992 }
993
994 #[test]
995 fn prepare_message_failure_1() {
996 let journal = GitJournal::new(".").unwrap();
997 assert!(journal.prepare("TEST", None).is_err());
998 assert!(journal.prepare("TEST", Some("message")).is_err());
999 }
1000
1001 #[test]
1002 fn prepare_message_failure_2() {
1003 let journal = GitJournal::new(".").unwrap();
1004 assert!(journal
1005 .prepare("./tests/commit_messages/prepare_3", Some("message"))
1006 .is_err());
1007 }
1008
1009 #[test]
1010 fn install_git_hook() {
1011 let journal = GitJournal::new(".").unwrap();
1012 assert!(journal.install_git_hook("test", "echo 1\n").is_ok());
1013 assert!(journal.install_git_hook("test", "echo 1\n").is_ok());
1014 assert!(journal.install_git_hook("test", "echo 2\n").is_ok());
1015 }
1016
1017 #[test]
1018 fn generate_template_1() {
1019 let mut journal = GitJournal::new("./tests/test_repo").unwrap();
1020 assert!(journal.generate_template().is_ok());
1021 assert!(journal
1022 .parse_log("HEAD", "rc", &0, &true, &false, None)
1023 .is_ok());
1024 assert!(journal.generate_template().is_ok());
1025 }
1026
1027 #[test]
1028 fn path_failure() {
1029 assert!(GitJournal::new("/etc/").is_err());
1030 }
1031
1032}