gitjournal/
lib.rs

1#![deny(missing_docs)]
2//! # The Git Commit Message and Changelog Generation Framework
3//!
4//! This crate contains the library for the
5//! [`git-journal`](https://github.com/saschagrunert/git-journal) executable. It handles all the
6//! parsing and commit message modification stuff which is provided by the
7//! executable.
8//!
9//! ### Example usage
10//!
11//! ```
12//! use gitjournal::GitJournal;
13//! let mut journal = GitJournal::new(".").unwrap();
14//! journal.parse_log("HEAD", "rc", &1, &false, &true, None);
15//! journal
16//!     .print_log(true, None, None)
17//!     .expect("Could not print short log.");
18//! ```
19//!
20//! Simply create a new git-journal struct from a given path (`.` in this
21//! example). Then parse the log between a given commit range or a single
22//! commit. In this example we want to retrieve everything included in the last
23//! git tag, which does not
24//! represent a release candidate
25//! (contains `"rc"`). After that parsing the log will be printed in the
26//! shortest possible format.
27
28pub 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
47/// The main structure of git-journal.
48pub struct GitJournal {
49    /// The configuration structure
50    pub config: Config,
51    parser: Parser,
52    path: String,
53    tags: Vec<(Oid, String)>,
54}
55
56impl GitJournal {
57    /// Constructs a new `GitJournal`. Searches upwards if the given path does
58    /// not contain the `.git` directory.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use gitjournal::GitJournal;
64    ///
65    /// let journal = GitJournal::new(".").unwrap();
66    /// ```
67    ///
68    /// # Errors
69    /// When not providing a path with a valid git repository ('.git' folder or
70    /// the initial parsing of the git tags failed.
71    pub fn new(path: &str) -> Result<Self, Error> {
72        // Search upwards for the .git directory
73        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        // Open the repository
91        let repo = Repository::open(&path_buf)?;
92
93        // Get all available tags in some vector of tuples
94        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        // Search for config in path and load
112        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        // Setup the logger if not already set
118        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        // Create a new parser with empty results
133        let new_parser = Parser {
134            config: new_config.clone(),
135            result: vec![],
136        };
137
138        // Return the git journal object
139        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    /// Does the setup on the target git repository.
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// use gitjournal::GitJournal;
153    ///
154    /// let journal = GitJournal::new(".").unwrap();
155    /// journal.setup().expect("Setup error");
156    /// ```
157    ///
158    /// Creates a `.gitjournal` file with the default values inside the given
159    /// path, which looks like:
160    ///
161    /// ```toml
162    /// # Specifies the available categories for the commit message, allowed regular expressions.
163    /// categories = ["Added", "Changed", "Fixed", "Improved", "Removed"]
164    ///
165    /// # Set the characters where the categories are wrapped in
166    /// category_delimiters = ["[", "]"]
167    ///
168    /// # Set to false if the output should not be colored
169    /// colored_output = true
170    ///
171    /// # Specifies the default template. Will be used for tag validation and printing. Can be
172    /// # removed from the configuration file as well.
173    /// default_template = "CHANGELOG.toml"
174    ///
175    /// # Show or hide the debug messages like `[OKAY] ...` or `[INFO] ...`
176    /// enable_debug = true
177    ///
178    /// # Excluded tags in an array, e.g. "internal"
179    /// excluded_commit_tags = []
180    ///
181    /// # Enable or disable the output and accumulation of commit footers.
182    /// enable_footers = false
183    ///
184    /// # Show or hide the commit hash for every entry
185    /// show_commit_hash = false
186    ///
187    /// # Show or hide the commit message prefix, e.g. JIRA-1234
188    /// show_prefix = false
189    ///
190    /// # Sort the commits during the output by "date" (default) or "name"
191    /// sort_by = "date"
192    ///
193    /// # Commit message template prefix which will be added during commit preparation.
194    /// template_prefix = "JIRA-1234"
195    /// ```
196    ///
197    /// It also creates a symlinks for the commit message validation and
198    /// preparation hook inside the given git repository.
199    ///
200    /// # Errors
201    /// - When the writing of the default configuration fails.
202    /// - When installation of the commit message (preparation) hook fails.
203    pub fn setup(&self) -> Result<(), Error> {
204        // Save the default config
205        let output_file = Config::new().save_default_config(&self.path)?;
206        info!("Defaults written to '{}' file.", output_file);
207
208        // Install commit message hook
209        self.install_git_hook("commit-msg", "git journal v $1\n")?;
210
211        // Install the prepare commit message hook
212        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    /// Prepare a commit message before the user edits it. This includes also a
265    /// verification of the commit message, e.g. for amended commits.
266    ///
267    /// # Examples
268    ///
269    /// ```
270    /// use gitjournal::GitJournal;
271    ///
272    /// let journal = GitJournal::new(".").unwrap();
273    /// journal
274    ///     .prepare("./tests/commit_messages/success_1", None)
275    ///     .expect("Commit message preparation error");
276    /// ```
277    ///
278    /// # Errors
279    /// When the path is not available or writing the commit message fails.
280    pub fn prepare(
281        &self,
282        path: &str,
283        commit_type: Option<&str>,
284    ) -> Result<(), Error> {
285        // If the message is not valid, assume a new commit and provide the
286        // template.
287        if let Err(error) = self.verify(path) {
288            // But if the message is provided via the cli with `-m`, then abort
289            // since the user can not edit this message any more.
290            if let Some(commit_type) = commit_type {
291                if commit_type == "message" {
292                    return Err(error);
293                }
294            }
295
296            // Read the file contents to get the actual commit message string
297            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            // Write the new generated content to the file
302            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    /// Verify a given commit message against the parsing rules of
338    /// [RFC0001](https://github.com/saschagrunert/git-journal/blob/master/rfc/0001-commit-msg.md)
339    ///
340    /// # Examples
341    ///
342    /// ```
343    /// use gitjournal::GitJournal;
344    ///
345    /// let journal = GitJournal::new(".").unwrap();
346    /// journal
347    ///     .verify("tests/commit_messages/success_1")
348    ///     .expect("Commit message verification error");
349    /// ```
350    ///
351    /// # Errors
352    /// When the commit message is not valid due to RFC0001 or opening of the
353    /// given file failed.
354    pub fn verify(&self, path: &str) -> Result<(), Error> {
355        // Open the file and read to string
356        let mut file = File::open(path)?;
357        let mut commit_message = String::new();
358        file.read_to_string(&mut commit_message)?;
359
360        // Parse the commit and extract the tags
361        let parsed_commit =
362            self.parser.parse_commit_message(&commit_message, None)?;
363        let tags = parsed_commit.get_tags_unique(vec![]);
364
365        // Check if the tags within the commit also occur in the default
366        // template and error if not.
367        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            // Deserialize the toml
375            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    /// Parses a revision range for a `GitJournal`.
393    ///
394    /// # Examples
395    ///
396    /// ```
397    /// use gitjournal::GitJournal;
398    ///
399    /// let mut journal = GitJournal::new(".").unwrap();
400    /// journal.parse_log("HEAD", "rc", &1, &false, &false, None);
401    /// ```
402    ///
403    /// # Errors
404    /// When something during the parsing fails, for example if the revision
405    /// range is invalid.
406    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        // Fill the revwalk with the selected revisions.
420        let revspec = repo.revparse(revision_range)?;
421        if revspec.mode().contains(git2::RevparseMode::SINGLE) {
422            // A single commit was given
423            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            // A specific commit range was given
429            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        // Iterate over the git objects and collect them in a vector of tuples
445        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                // Parsing entries of the last tag done
462                if !current_tag.message_ids.is_empty() {
463                    self.parser.result.push(current_tag.clone());
464                }
465
466                // If a single revision is given stop at the first seen tag
467                if !all && index > 0 && num_parsed_tags > *max_tags_count {
468                    break 'revloop;
469                }
470
471                // Format the tag and set as current
472                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            // Do not parse if we want to skip commits which do not belong to
483            // any release
484            if *skip_unreleased && current_tag.name == unreleased_str {
485                continue;
486            }
487
488            // Add the commit message to the parser work to be done, the `id`
489            // represents the index within the worker vector
490            let message = commit.message().ok_or_else(|| {
491                git2::Error::from_str("Commit message error.")
492            })?;
493            let id = worker_vec.len();
494
495            // The worker_vec contains the commit message and the parsed commit
496            // (currently none)
497            worker_vec.push((message.to_owned(), oid, None));
498            current_tag.message_ids.push(id);
499        }
500
501        // Add the last element as well if needed
502        if !current_tag.message_ids.is_empty()
503            && !self.parser.result.contains(&current_tag)
504        {
505            self.parser.result.push(current_tag);
506        }
507
508        // Process with the full CPU power
509        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                                // Filter out ignored tags
516                                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        // Assemble results together via the message_id
529        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    /// Generates an output template from the current parsing results.
561    ///
562    /// # Examples
563    ///
564    /// ```
565    /// use gitjournal::GitJournal;
566    ///
567    /// let mut journal = GitJournal::new(".").unwrap();
568    /// journal.parse_log("HEAD", "rc", &1, &false, &false, None);
569    /// journal
570    ///     .generate_template()
571    ///     .expect("Template generation failed.");
572    /// ```
573    ///
574    /// # Errors
575    /// If the generation of the template was impossible.
576    pub fn generate_template(&self) -> Result<(), Error> {
577        let mut tags = vec![parser::TOML_DEFAULT_KEY.to_owned()];
578
579        // Get all the tags
580        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        // Create the toml representation
591        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        // Write toml to file
632        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    /// Prints the resulting log in a short or detailed variant. Will use the
643    /// template as an output formatter if provided.
644    ///
645    /// # Examples
646    ///
647    /// ```
648    /// use gitjournal::GitJournal;
649    ///
650    /// let mut journal = GitJournal::new(".").unwrap();
651    /// journal.parse_log("HEAD", "rc", &1, &false, &false, None);
652    /// journal
653    ///     .print_log(true, None, None)
654    ///     .expect("Could not print short log.");
655    /// journal
656    ///     .print_log(false, None, None)
657    ///     .expect("Could not print detailed log.");
658    /// ```
659    ///
660    /// # Errors
661    /// If some commit message could not be print.
662    pub fn print_log(
663        &self,
664        compact: bool,
665        template: Option<&str>,
666        output: Option<&str>,
667    ) -> Result<(), Error> {
668        // Choose the template
669        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        // Print the log
697        let output_vec = self.parser.print(&compact, used_template)?;
698
699        // Print the log to the file if necessary
700        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}