conventional_semver_rs/release/
mod.rs

1extern crate custom_error;
2use custom_error::custom_error;
3use std::num::TryFromIntError;
4use std::fs::File;
5use std::io::{self, Write};
6use std::path::Path;
7use git2::{Signature, Oid};
8use regex::Regex;
9use git2::Commit;
10
11use crate::config::ConventionalSemverConfig;
12use crate::ConventionalRepo;
13
14custom_error! { pub Error
15    VersionFileError{source: io::Error, file: String} = "Version file error({file}): {source}.",
16    VersionMatchError{file: String} = "Unable find version in version file {file}",
17    SignatureError{source: TryFromIntError} = "Encountered error when attempting to create git signature timpstamp {source}",
18    GitError{source: git2::Error} = "An error occurred when performing a Git action: {source}",
19}
20
21static SEMVER_MATCHER: &str = r"[vV]?\d+\.\d+\.\d+[-+\w\.]*";
22
23#[derive(Debug)]
24pub struct VersionFile {
25    relative_path: String,
26    matcher: Regex,
27    v: bool,
28}
29impl VersionFile {
30    pub fn new(path: String, version_prefix: String, version_postfix: String, v: bool) -> Result<Self, regex::Error> {
31        let regex = construct_matcher(version_prefix, version_postfix)?;
32        Ok(VersionFile{
33            relative_path: path,
34            matcher: regex,
35            v,
36        })
37    }
38
39    pub fn config_to_version_files(config: &ConventionalSemverConfig) -> anyhow::Result<Vec<VersionFile>> {
40        match &config.version_files {
41            None => Ok(vec![]),
42            Some(version_files) => {
43                version_files.iter().map(|v_file| -> anyhow::Result<VersionFile> {
44                    Ok(VersionFile::new(
45                        v_file.path.clone(),
46                        v_file.version_prefix.as_ref()
47                            .unwrap_or(&String::from("")).clone(),
48                        v_file.version_postfix.as_ref()
49                            .unwrap_or(&String::from("")).clone(),
50                        v_file.v
51                    )?)
52                }).collect()
53            }
54        }
55    }
56}
57
58/// Compiles the provided prefix and postfix into a Regex with the SEMVER_MATCHER constant
59/// Example: `version_prefix: "version = \\""`, `version_postfix: "\\"[^,]"`
60/// Compiled: `(version = \\"){SEMVER_MATCHER}(\\"[^,])`
61/// Matches: `version = "2.12.18"`
62fn construct_matcher(prefix: String, postfix: String) -> Result<regex::Regex, regex::Error> {
63    Ok(Regex::new(&format!("({}){}({})", prefix, SEMVER_MATCHER, postfix))?)
64}
65
66/// Update versions in various version files.
67/// package.josn, cargo.toml, etc.
68pub fn bump_version_files(repo_path: &str, version: &str, files: &Vec<VersionFile>) -> Vec<Error> {
69    let version = match version.strip_prefix("v") {
70        Some(v) => v,
71        None => version,
72    };
73
74    files.iter().filter_map(|f| -> Option<Error> {
75        // Get file based on relative path
76        let str_pth = format!("{}/{}", repo_path, f.relative_path).to_string();
77        let pth = Path::new(&str_pth);
78        let contents = match std::fs::read_to_string(pth) {
79            Ok(c) => c,
80            Err(e) => return Some(Error::VersionFileError{source: e, file: f.relative_path.clone()}),
81        };
82
83        // Scan file contents with matcher regex
84        let cap = match f.matcher.captures(&contents) {
85            Some(c) => c,
86            None => return Some(Error::VersionMatchError{file: f.relative_path.clone()}),
87        };
88
89        let fmt_str = match f.v {
90            true => format!("{}v{}{}", cap[1].to_string(), version, cap[2].to_string()),
91            false => format!("{}{}{}", cap[1].to_string(), version, cap[2].to_string()),
92        };
93        let cow = f.matcher.replace_all(&contents, fmt_str);
94
95        // Don't write to file until files have been updated.
96        // Update file
97        match File::options().write(true).open(pth) {
98            Ok(mut out_file) => {
99                out_file.write_all(cow.as_ref().as_bytes()).err()?;
100            },
101            Err(e) => return Some(Error::VersionFileError{source: e, file: f.relative_path.clone()}),
102        }
103        None
104    }).collect()
105}
106
107/// Tag Head commit of Repository, with the provided version.
108pub fn tag_release(repo: &ConventionalRepo, version: &str) -> Result<Oid, Error> {
109    // Tag the repository with a version
110    let sig = Signature::now(
111        &repo.config.commit_signature.name,
112        &repo.config.commit_signature.email)?;
113    let head = repo.repo.head()?.peel_to_commit()?;
114    Ok(repo.repo.tag(&version.to_string(), head.as_object(), &sig, "", false)?)
115}
116
117pub fn commit_version_files(
118    repo: &ConventionalRepo,
119    version: &str,
120    version_files: &Vec<VersionFile>
121) -> Result<Oid, Error> {
122    let sig = Signature::now(
123        &repo.config.commit_signature.name,
124        &repo.config.commit_signature.email)?;
125
126    let head = repo.repo.head()?;
127    let commit = head.peel_to_commit()?;
128    let parent_commits: [&Commit; 1] = [&commit];
129
130    let mut index = repo.repo.index()?;
131    version_files.iter().for_each(|v: &VersionFile| {
132        if let Err(e) = index.add_path(&Path::new(&v.relative_path)) {
133            eprintln!("Error Encountered {}", e);
134        }
135    });
136    index.write()?;
137
138    // Regrab index from repo, to prevent staging old changes.
139    let mut index = repo.repo.index()?;
140    let oid = index.write_tree()?;
141    let commit_tree = repo.repo.find_tree(oid)?;
142
143    Ok(repo.repo.commit(
144        Some("HEAD"),
145        &sig,
146        &sig,
147        &format!("chore(release): created release {}", version).to_owned(),
148        &commit_tree,
149        &parent_commits
150    )?)
151}
152