|
| 1 | +use crate::*; |
| 2 | + |
| 3 | +/// Parse a version string into Version struct |
| 4 | +/// |
| 5 | +/// # Arguments |
| 6 | +/// |
| 7 | +/// - `&str`: The version string to parse (e.g., "0.1.2" or "0.1.2-alpha") |
| 8 | +/// |
| 9 | +/// # Returns |
| 10 | +/// |
| 11 | +/// - `Option<Version>`: Parsed version if successful, None otherwise |
| 12 | +fn parse_version(version_str: &str) -> Option<Version> { |
| 13 | + let parts: Vec<&str> = version_str.split('-').collect(); |
| 14 | + let version_part: &str = parts.first()?; |
| 15 | + let prerelease: Option<String> = parts.get(1).map(|s: &&str| s.to_string()); |
| 16 | + let nums: Vec<&str> = version_part.split('.').collect(); |
| 17 | + if nums.len() != 3 { |
| 18 | + return None; |
| 19 | + } |
| 20 | + let major: u64 = nums.first()?.parse().ok()?; |
| 21 | + let minor: u64 = nums.get(1)?.parse().ok()?; |
| 22 | + let patch: u64 = nums.get(2)?.parse().ok()?; |
| 23 | + Some(Version { |
| 24 | + major, |
| 25 | + minor, |
| 26 | + patch, |
| 27 | + prerelease, |
| 28 | + }) |
| 29 | +} |
| 30 | + |
| 31 | +/// Parse pre-release identifier to extract type and number |
| 32 | +/// |
| 33 | +/// # Arguments |
| 34 | +/// |
| 35 | +/// - `&str`: The pre-release string (e.g., "alpha", "alpha.1", "beta.2") |
| 36 | +/// |
| 37 | +/// # Returns |
| 38 | +/// |
| 39 | +/// - `Option<(&str, u64)>`: Tuple of (pre_release_type, number) if parsed successfully |
| 40 | +fn parse_prerelease(prerelease: &str) -> Option<(&str, u64)> { |
| 41 | + let parts: Vec<&str> = prerelease.split('.').collect(); |
| 42 | + let pre_type: &str = parts.first()?; |
| 43 | + let number: u64 = parts |
| 44 | + .get(1) |
| 45 | + .and_then(|s: &&str| s.parse().ok()) |
| 46 | + .unwrap_or(0); |
| 47 | + Some((pre_type, number)) |
| 48 | +} |
| 49 | + |
| 50 | +/// Get the next pre-release version string |
| 51 | +/// |
| 52 | +/// # Arguments |
| 53 | +/// |
| 54 | +/// - `Option<&String>`: Current pre-release identifier |
| 55 | +/// - `&str`: Target pre-release type ("alpha", "beta", "rc") |
| 56 | +/// |
| 57 | +/// # Returns |
| 58 | +/// |
| 59 | +/// - `String`: The new pre-release identifier |
| 60 | +fn get_next_prerelease(current: Option<&String>, target_type: &str) -> String { |
| 61 | + match current { |
| 62 | + Some(pre) => { |
| 63 | + if let Some((pre_type, number)) = parse_prerelease(pre) { |
| 64 | + if pre_type == target_type && number > 0 { |
| 65 | + return format!("{}.{}", target_type, number + 1); |
| 66 | + } |
| 67 | + } |
| 68 | + format!("{target_type}.1") |
| 69 | + } |
| 70 | + None => target_type.to_string(), |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +/// Convert Version back to string representation |
| 75 | +/// |
| 76 | +/// # Arguments |
| 77 | +/// |
| 78 | +/// - `&Version`: The Version struct to convert |
| 79 | +/// |
| 80 | +/// # Returns |
| 81 | +/// |
| 82 | +/// - `String`: Version string (e.g., "0.1.2" or "0.1.2-alpha") |
| 83 | +fn version_to_string(version: &Version) -> String { |
| 84 | + let base: String = format!("{}.{}.{}", version.major, version.minor, version.patch); |
| 85 | + match &version.prerelease { |
| 86 | + Some(pre) => format!("{base}-{pre}"), |
| 87 | + None => base, |
| 88 | + } |
| 89 | +} |
| 90 | + |
| 91 | +/// Apply version bump according to the specified type |
| 92 | +/// |
| 93 | +/// # Arguments |
| 94 | +/// |
| 95 | +/// - `&Version`: The current version |
| 96 | +/// - `BumpVersionType`: The type of version bump to apply |
| 97 | +/// |
| 98 | +/// # Returns |
| 99 | +/// |
| 100 | +/// - `Version`: The new version after bumping |
| 101 | +fn bump_version(version: &Version, bump_type: BumpVersionType) -> Version { |
| 102 | + match bump_type { |
| 103 | + BumpVersionType::Patch => Version { |
| 104 | + major: version.major, |
| 105 | + minor: version.minor, |
| 106 | + patch: version.patch + 1, |
| 107 | + prerelease: None, |
| 108 | + }, |
| 109 | + BumpVersionType::Minor => Version { |
| 110 | + major: version.major, |
| 111 | + minor: version.minor + 1, |
| 112 | + patch: 0, |
| 113 | + prerelease: None, |
| 114 | + }, |
| 115 | + BumpVersionType::Major => Version { |
| 116 | + major: version.major + 1, |
| 117 | + minor: 0, |
| 118 | + patch: 0, |
| 119 | + prerelease: None, |
| 120 | + }, |
| 121 | + BumpVersionType::Release => Version { |
| 122 | + major: version.major, |
| 123 | + minor: version.minor, |
| 124 | + patch: version.patch, |
| 125 | + prerelease: None, |
| 126 | + }, |
| 127 | + BumpVersionType::Alpha => { |
| 128 | + let prerelease: String = get_next_prerelease(version.prerelease.as_ref(), "alpha"); |
| 129 | + Version { |
| 130 | + major: version.major, |
| 131 | + minor: version.minor, |
| 132 | + patch: version.patch, |
| 133 | + prerelease: Some(prerelease), |
| 134 | + } |
| 135 | + } |
| 136 | + BumpVersionType::Beta => { |
| 137 | + let prerelease: String = get_next_prerelease(version.prerelease.as_ref(), "beta"); |
| 138 | + Version { |
| 139 | + major: version.major, |
| 140 | + minor: version.minor, |
| 141 | + patch: version.patch, |
| 142 | + prerelease: Some(prerelease), |
| 143 | + } |
| 144 | + } |
| 145 | + BumpVersionType::Rc => { |
| 146 | + let prerelease: String = get_next_prerelease(version.prerelease.as_ref(), "rc"); |
| 147 | + Version { |
| 148 | + major: version.major, |
| 149 | + minor: version.minor, |
| 150 | + patch: version.patch, |
| 151 | + prerelease: Some(prerelease), |
| 152 | + } |
| 153 | + } |
| 154 | + } |
| 155 | +} |
| 156 | + |
| 157 | +/// Find version value position in a line |
| 158 | +/// |
| 159 | +/// # Arguments |
| 160 | +/// |
| 161 | +/// - `&str`: The line to search |
| 162 | +/// |
| 163 | +/// # Returns |
| 164 | +/// |
| 165 | +/// - `Option<(usize, usize)>`: Start and end positions of version string within quotes |
| 166 | +fn find_version_position(line: &str) -> Option<(usize, usize)> { |
| 167 | + let trimmed: &str = line.trim(); |
| 168 | + if !trimmed.starts_with("version") || !trimmed.contains('=') { |
| 169 | + return None; |
| 170 | + } |
| 171 | + let eq_pos: usize = line.find('=')?; |
| 172 | + let after_eq: &str = &line[eq_pos + 1..]; |
| 173 | + let quote_start: usize = after_eq.find('"')?; |
| 174 | + let after_first_quote: &str = &after_eq[quote_start + 1..]; |
| 175 | + let quote_end: usize = after_first_quote.find('"')?; |
| 176 | + let version_start: usize = eq_pos + 1 + quote_start + 1; |
| 177 | + let version_end: usize = version_start + quote_end; |
| 178 | + Some((version_start, version_end)) |
| 179 | +} |
| 180 | + |
| 181 | +/// Read and update version in Cargo.toml |
| 182 | +/// |
| 183 | +/// # Arguments |
| 184 | +/// |
| 185 | +/// - `&str`: Path to Cargo.toml file |
| 186 | +/// - `BumpVersionType`: Type of version bump to apply |
| 187 | +/// |
| 188 | +/// # Returns |
| 189 | +/// |
| 190 | +/// - `Result<String, Box<dyn std::error::Error>>`: The new version string or an error |
| 191 | +pub(crate) fn execute_bump( |
| 192 | + manifest_path: &str, |
| 193 | + bump_type: BumpVersionType, |
| 194 | +) -> Result<String, Box<dyn std::error::Error>> { |
| 195 | + let path: &Path = Path::new(manifest_path); |
| 196 | + let content: String = read_to_string(path)?; |
| 197 | + let mut new_version: Option<String> = None; |
| 198 | + let mut found_version: bool = false; |
| 199 | + let mut updated_content: String = content.clone(); |
| 200 | + for line in content.lines() { |
| 201 | + if found_version { |
| 202 | + break; |
| 203 | + } |
| 204 | + if let Some((version_start, version_end)) = find_version_position(line) { |
| 205 | + let version_str: &str = &line[version_start..version_end]; |
| 206 | + if let Some(version) = parse_version(version_str) { |
| 207 | + let bumped: Version = bump_version(&version, bump_type); |
| 208 | + let version_string: String = version_to_string(&bumped); |
| 209 | + new_version = Some(version_string.clone()); |
| 210 | + let new_line: String = format!( |
| 211 | + "{}{}{}", |
| 212 | + &line[..version_start], |
| 213 | + version_string, |
| 214 | + &line[version_end..] |
| 215 | + ); |
| 216 | + updated_content = updated_content.replacen(line, &new_line, 1); |
| 217 | + found_version = true; |
| 218 | + } |
| 219 | + } |
| 220 | + } |
| 221 | + if !found_version { |
| 222 | + return Err("version field not found in Cargo.toml".into()); |
| 223 | + } |
| 224 | + write(path, updated_content)?; |
| 225 | + match new_version { |
| 226 | + Some(v) => Ok(v), |
| 227 | + None => Err("failed to bump version".into()), |
| 228 | + } |
| 229 | +} |
0 commit comments