diff --git a/docs/reference.md b/docs/reference.md index 20c7cbb..80a1062 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -390,10 +390,29 @@ relative to the base of the repo; other paths are relative to that root arbitrary labels to you projects, which is useful when using the `info` command. - `hooks`: (optional) A set of hooks that run at certain points of the - release process. Currently, only the `post_write` hook is supported: - this hook runs after local file changes are made, but before any VCS - commits/push/tagging is performed; it's useful to make additional - file changes that need to be committed with the release. + release process. Hooks are not run if the `dry-run` or + `changelog-only` flags are set. Note that commits, tags, and other + release operations are not executed on a project-by-project basis, + so all project hooks of the same type will be run together. Here are + the hooks you can use: + - `pre_begin`: runs before any other release activity is performed. + You can use this hook to perform any preperation for the release + process. + - `pre_write`: runs before any data is written to the local + filesystem; you can use this to prep files or record the fs state. + - `post_write`: runs after local file changes are made, + but before any VCS commits/push/tagging is performed; it's useful + to make additional file changes that need to be committed with the + release. + - `post_commit`: runs after local file changes are committed and/or + pushed to the VCS, but before tags are added/changed. Use this if + you want to perform additional VCS operations before tagging. + - `post_tag`: runs after tags are added/changed and pushed to the + VCS. Useful if you want to add your own tags, or perform final VCS + operations. + - `post_end`: runs after any other release activity. Use this if you + have cleanup operations that you want to run after your release is + done. - `commit` diff --git a/docs/use_cases.md b/docs/use_cases.md index a710426..8f76eb6 100644 --- a/docs/use_cases.md +++ b/docs/use_cases.md @@ -145,6 +145,31 @@ directories or files listed in any `.gitignore` files. If you want to include projects in hidden or ignored locations, you'll have to add those by hand to the resulting `.versio.yaml` file. +## Perform a Release + +This is the most commonly-used command: `release` will scan for +conventional commits, increment all project version numbers accordingly, +update changelogs, add and move tags, and commit and push all changes. + +``` +$ versio release +``` + +### Release dry-run + +You can run `versio -l local -c release -d` to run a "dry-run" release, +which won't actually write or commit anything, but will print out the +new version numbers that are expected. Using `-l local -c` will cause +versio to run only on the local repository, and ignore problems with +local changes: no network or remote operations will be consulted (see +[VCS Levels](./vcs_levels.md)). This command can be used while +performing edits to see that changes have the expected effect on a +project version. + +Note that the actual release may calculate different version numbers +than any given dry-run, especially if the configuration, network, or +commit/PR history is different than found in the dry-run environment. + ## Gitflow / Oneflow If you're using @@ -286,6 +311,7 @@ jobs: run: versio check - name: Print changes run: versio plan + - any other pr actions... ``` ## CI Release @@ -325,4 +351,5 @@ jobs: run: git fetch --unshallow - name: Generate release run: versio release + - any other release actions... ``` diff --git a/src/commands.rs b/src/commands.rs index 24f27b1..0cd0c58 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,7 +6,7 @@ use crate::errors::{Context as _, Result}; use crate::git::Repo; use crate::mono::{Mono, Plan}; use crate::output::{Output, ProjLine}; -use crate::state::{CommitState, StateRead}; +use crate::state::{CommitState, ResumeArgs, StateRead}; use crate::template::read_template; use crate::vcs::{VcsLevel, VcsRange, VcsState}; use schemars::schema_for; @@ -397,6 +397,7 @@ pub async fn release( pub fn resume(user_pref_vcs: Option) -> Result<()> { let vcs = combine_vcs(user_pref_vcs, VcsLevel::None, VcsLevel::Smart, VcsLevel::Local, VcsLevel::Smart)?; + let output = Output::new(); let mut output = output.resume(); @@ -409,8 +410,10 @@ pub fn resume(user_pref_vcs: Option) -> Result<()> { remove_file(".versio-paused")?; commit }; + let repo = Repo::open(".", VcsState::new(vcs.max(), false), commit.commit_config().clone())?; - commit.resume(&repo)?; + let file = ConfigFile::from_dir(repo.root())?; + commit.resume(&repo, ResumeArgs::new(&file.hooks()))?; output.write_done()?; output.commit()?; diff --git a/src/config.rs b/src/config.rs index cfe5afa..e3955e0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -783,7 +783,12 @@ impl HookSet { Ok(()) } + pub fn execute_pre_begin(&self, root: &Option<&String>) -> Result<()> { self.execute("pre_begin", root) } + pub fn execute_pre_write(&self, root: &Option<&String>) -> Result<()> { self.execute("pre_write", root) } pub fn execute_post_write(&self, root: &Option<&String>) -> Result<()> { self.execute("post_write", root) } + pub fn execute_post_commit(&self, root: &Option<&String>) -> Result<()> { self.execute("post_commit", root) } + pub fn execute_post_tag(&self, root: &Option<&String>) -> Result<()> { self.execute("post_tag", root) } + pub fn execute_post_end(&self, root: &Option<&String>) -> Result<()> { self.execute("post_end", root) } } impl<'de> Deserialize<'de> for HookSet { diff --git a/src/git.rs b/src/git.rs index abe37b0..464f9bd 100644 --- a/src/git.rs +++ b/src/git.rs @@ -50,6 +50,7 @@ impl Repo { // returns successfully at the Smart level. pub fn commit_config(&self) -> &CommitConfig { &self.commit_config } + pub fn root(&self) -> &Path { self.vcs.root() } /// Return the vcs level that this repository can support. pub fn detect>(path: P) -> Result { @@ -822,6 +823,15 @@ impl GitVcsLevel { VcsLevel::Smart => GitVcsLevel::Smart { repo, branch_name, remote_name, fetches } } } + + fn root(&self) -> &Path { + match self { + Self::None { root } => root, + Self::Local { repo, .. } => repo.workdir().unwrap(), + Self::Remote { repo, .. } => repo.workdir().unwrap(), + Self::Smart { repo, .. } => repo.workdir().unwrap() + } + } } /// A git commit hash-like (hash, branch, tag, etc) to revwalk "from" (a.k.a. "hide"), or none if the hash-like diff --git a/src/state.rs b/src/state.rs index ca86cfe..9ffe57e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -190,7 +190,19 @@ impl StateWrite { Ok(()) } - pub fn commit(&mut self, repo: &Repo, data: CommitArgs) -> Result<()> { + pub fn commit(&mut self, repo: &Repo, args: CommitArgs) -> Result<()> { + for proj_id in &self.proj_writes { + if let Some((root, hooks)) = args.hooks.get(proj_id) { + hooks.execute_pre_begin(root)?; + } + } + + for proj_id in &self.proj_writes { + if let Some((root, hooks)) = args.hooks.get(proj_id) { + hooks.execute_pre_write(root)?; + } + } + for write in &self.writes { write.write()?; } @@ -198,30 +210,30 @@ impl StateWrite { self.writes.clear(); for proj_id in &self.proj_writes { - if let Some((root, hooks)) = data.hooks.get(proj_id) { + if let Some((root, hooks)) = args.hooks.get(proj_id) { hooks.execute_post_write(root)?; } } let me = take(self); - let prev_tag = data.prev_tag.to_string(); - let last_commits = data.last_commits.clone(); - let old_tags = data.old_tags.clone(); + let prev_tag = args.prev_tag.to_string(); + let last_commits = args.last_commits.clone(); + let old_tags = args.old_tags.clone(); let mut commit_state = CommitState::new( me, did_write, prev_tag, last_commits, old_tags, - data.advance_prev, + args.advance_prev, repo.commit_config().clone() ); - if data.pause { + if args.pause { let file = OpenOptions::new().create(true).write(true).truncate(true).open(".versio-paused")?; Ok(serde_json::to_writer(file, &commit_state)?) } else { - commit_state.resume(repo) + commit_state.resume(repo, args.into_resume_args()) } } } @@ -242,6 +254,18 @@ impl<'a> CommitArgs<'a> { ) -> CommitArgs<'a> { CommitArgs { prev_tag, last_commits, old_tags, advance_prev, hooks, pause } } + + pub fn into_resume_args(self) -> ResumeArgs<'a> { ResumeArgs { hooks: self.hooks } } +} + +pub struct ResumeArgs<'a> { + hooks: &'a HashMap, &'a HookSet)> +} + +impl<'a> ResumeArgs<'a> { + pub fn new(hooks: &'a HashMap, &'a HookSet)>) -> ResumeArgs<'a> { + ResumeArgs { hooks } + } } fn fill_from_old(old: &HashMap, new_tags: &mut HashMap) { @@ -274,7 +298,7 @@ impl CommitState { pub fn commit_config(&self) -> &CommitConfig { &self.commit_config } - pub fn resume(&mut self, repo: &Repo) -> Result<()> { + pub fn resume(&mut self, repo: &Repo, args: ResumeArgs) -> Result<()> { if self.did_write { trace!("Wrote files, so committing."); repo.commit()?; @@ -282,6 +306,12 @@ impl CommitState { trace!("No files written, so not committing."); } + for proj_id in &self.write.proj_writes { + if let Some((root, hooks)) = args.hooks.get(proj_id) { + hooks.execute_post_commit(root)?; + } + } + for tag in &self.write.tag_head { repo.update_tag_head(tag)?; } @@ -298,7 +328,6 @@ impl CommitState { } } self.write.tag_head_or_last.clear(); - self.write.proj_writes.clear(); for (tag, oid) in &self.write.tag_commit { repo.update_tag(tag, oid)?; @@ -311,6 +340,20 @@ impl CommitState { repo.update_tag_head_anno(&self.prev_tag, &msg)?; } + for proj_id in &self.write.proj_writes { + if let Some((root, hooks)) = args.hooks.get(proj_id) { + hooks.execute_post_tag(root)?; + } + } + + for proj_id in &self.write.proj_writes { + if let Some((root, hooks)) = args.hooks.get(proj_id) { + hooks.execute_post_end(root)?; + } + } + + self.write.proj_writes.clear(); + Ok(()) } }