|
| 1 | +use crate::util::{get_crates, Crate}; |
| 2 | +use anyhow::{anyhow, Context, Result}; |
| 3 | +use std::fs; |
| 4 | +use std::process::Command; |
| 5 | +use structopt::StructOpt; |
| 6 | + |
| 7 | +#[derive(Debug, StructOpt)] |
| 8 | +#[structopt(name = "bump")] |
| 9 | +pub struct BumpCommand { |
| 10 | + /// Do not modify the Cargo.toml files; instead, simply print the actions that would have been |
| 11 | + /// taken. |
| 12 | + #[structopt(long = "dry-run")] |
| 13 | + dry_run: bool, |
| 14 | + /// Add a conventional Git commit message for the bump changes; equivalent to `git commit -a -m |
| 15 | + /// 'Release v[bumped version]'`. |
| 16 | + #[structopt(long)] |
| 17 | + git: bool, |
| 18 | + /// What part of the semver version to change: major | minor | patch | <version string> |
| 19 | + #[structopt(name = "KIND")] |
| 20 | + bump: Bump, |
| 21 | +} |
| 22 | + |
| 23 | +impl BumpCommand { |
| 24 | + pub fn execute(&self) -> Result<()> { |
| 25 | + // Find the publishable crates. |
| 26 | + let publishable_crates: Vec<Crate> = |
| 27 | + get_crates()?.into_iter().filter(|c| c.publish).collect(); |
| 28 | + |
| 29 | + // Check that all of the versions are the same. |
| 30 | + if !publishable_crates |
| 31 | + .windows(2) |
| 32 | + .all(|w| w[0].version == w[1].version) |
| 33 | + { |
| 34 | + anyhow!( |
| 35 | + "Not all crate versions are the same: {:?}", |
| 36 | + publishable_crates |
| 37 | + ); |
| 38 | + } |
| 39 | + |
| 40 | + // Change the version. |
| 41 | + let mut next_version = semver::Version::parse(&publishable_crates[0].version)?; |
| 42 | + match &self.bump { |
| 43 | + Bump::Major => next_version.increment_major(), |
| 44 | + Bump::Minor => next_version.increment_minor(), |
| 45 | + Bump::Patch => next_version.increment_patch(), |
| 46 | + Bump::Custom(v) => next_version = semver::Version::parse(v)?, |
| 47 | + } |
| 48 | + |
| 49 | + // Update the Cargo.toml files. |
| 50 | + let next_version_str = &next_version.to_string(); |
| 51 | + for c in publishable_crates.iter() { |
| 52 | + update_version(c, &publishable_crates, next_version_str, self.dry_run)?; |
| 53 | + } |
| 54 | + |
| 55 | + // Update the Cargo.lock file. |
| 56 | + if !self.dry_run { |
| 57 | + assert!(Command::new("cargo").arg("fetch").status()?.success()); |
| 58 | + } |
| 59 | + |
| 60 | + // Add a Git commit. |
| 61 | + let commit_message = format!("'Release v{}'", next_version_str); |
| 62 | + if self.git { |
| 63 | + println!("> add Git commit: {}", &commit_message); |
| 64 | + if !self.dry_run && self.git { |
| 65 | + assert!(Command::new("git") |
| 66 | + .arg("commit") |
| 67 | + .arg("-a") |
| 68 | + .arg("-m") |
| 69 | + .arg(&commit_message) |
| 70 | + .status() |
| 71 | + .with_context(|| format!("failed to run `git commit` command"))? |
| 72 | + .success()); |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + Ok(()) |
| 77 | + } |
| 78 | +} |
| 79 | + |
| 80 | +/// Enumerate the ways a version can change. |
| 81 | +#[derive(Debug)] |
| 82 | +pub enum Bump { |
| 83 | + Major, |
| 84 | + Minor, |
| 85 | + Patch, |
| 86 | + Custom(String), |
| 87 | +} |
| 88 | + |
| 89 | +impl std::str::FromStr for Bump { |
| 90 | + type Err = anyhow::Error; |
| 91 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
| 92 | + Ok(match s { |
| 93 | + "major" => Self::Major, |
| 94 | + "minor" => Self::Minor, |
| 95 | + "patch" => Self::Patch, |
| 96 | + _ => Self::Custom(s.into()), |
| 97 | + }) |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +/// Update the version of `krate` and any dependencies in `crates` to match the version passed in |
| 102 | +/// `next_version`. Adapted from |
| 103 | +/// https://github.com/bytecodealliance/wasmtime/blob/main/scripts/publish.rs |
| 104 | +fn update_version( |
| 105 | + krate: &Crate, |
| 106 | + crates: &[Crate], |
| 107 | + next_version: &str, |
| 108 | + dry_run: bool, |
| 109 | +) -> Result<()> { |
| 110 | + let contents = fs::read_to_string(&krate.path)?; |
| 111 | + let mut new_contents = String::new(); |
| 112 | + let mut reading_dependencies = false; |
| 113 | + for line in contents.lines() { |
| 114 | + let mut rewritten = false; |
| 115 | + |
| 116 | + // Update top-level `version = "..."` line. |
| 117 | + if !reading_dependencies && line.starts_with("version =") { |
| 118 | + println!( |
| 119 | + "> bump `{}` {} => {}", |
| 120 | + krate.name, krate.version, next_version |
| 121 | + ); |
| 122 | + new_contents.push_str(&line.replace(&krate.version.to_string(), next_version)); |
| 123 | + rewritten = true; |
| 124 | + } |
| 125 | + |
| 126 | + // Check whether we have reached the `[dependencies]` section. |
| 127 | + reading_dependencies = if line.starts_with("[") { |
| 128 | + line.contains("dependencies") |
| 129 | + } else { |
| 130 | + reading_dependencies |
| 131 | + }; |
| 132 | + |
| 133 | + // Find dependencies and update them as well. |
| 134 | + for other in crates { |
| 135 | + if !reading_dependencies || !line.starts_with(&format!("{} ", other.name)) { |
| 136 | + continue; |
| 137 | + } |
| 138 | + if !line.contains(&other.version.to_string()) { |
| 139 | + if !line.contains("version =") { |
| 140 | + continue; |
| 141 | + } |
| 142 | + panic!( |
| 143 | + "{:?} has a dependency on {} but doesn't list version {}", |
| 144 | + krate.path, other.name, other.version |
| 145 | + ); |
| 146 | + } |
| 147 | + println!( |
| 148 | + "> bump dependency `{}` {} => {}", |
| 149 | + other.name, other.version, next_version |
| 150 | + ); |
| 151 | + rewritten = true; |
| 152 | + new_contents.push_str(&line.replace(&other.version, next_version)); |
| 153 | + break; |
| 154 | + } |
| 155 | + |
| 156 | + // All other lines are printed as-is. |
| 157 | + if !rewritten { |
| 158 | + new_contents.push_str(line); |
| 159 | + } |
| 160 | + |
| 161 | + new_contents.push_str("\n"); |
| 162 | + } |
| 163 | + |
| 164 | + if !dry_run { |
| 165 | + fs::write(&krate.path, new_contents)?; |
| 166 | + } |
| 167 | + |
| 168 | + Ok(()) |
| 169 | +} |
0 commit comments