| 1 | // Copyright (c) 2019-2024 Subhomoy Haldar. All rights reserved. |
| 2 | // Use of this source code is governed by an MIT license that can be found in the LICENSE file. |
| 3 | module main |
| 4 | |
| 5 | import flag |
| 6 | import os |
| 7 | import regex |
| 8 | import semver |
| 9 | |
| 10 | const tool_name = os.file_name(os.executable()) |
| 11 | const tool_version = '0.1.0' |
| 12 | const tool_description = '\n Bump the semantic version of the v.mod and/or specified files. |
| 13 | |
| 14 | The first instance of a version number is replaced with the new version. |
| 15 | Additionally, the line affected must contain the word "version" in any |
| 16 | form of capitalization. For instance, the following lines will be |
| 17 | recognized by the heuristic: |
| 18 | |
| 19 | tool_version = \'1.2.1\' |
| 20 | version: \'0.2.42\' |
| 21 | VERSION = "1.23.8" |
| 22 | |
| 23 | If certain lines need to be skipped, use the --skip option. For instance, |
| 24 | the following command will skip lines containing "tool-version": |
| 25 | |
| 26 | v bump --patch --skip "tool-version" [files...] |
| 27 | |
| 28 | Examples: |
| 29 | Bump the patch version in v.mod if it exists |
| 30 | v bump --patch |
| 31 | Bump the major version in v.mod and vls.v |
| 32 | v bump --major v.mod vls.v |
| 33 | Upgrade the minor version in sample.v only |
| 34 | v bump --minor sample.v |
| 35 | ' |
| 36 | |
| 37 | const semver_query = r'((0)|([1-9]\d*)\.){2}(0)|([1-9]\d*)(\-[\w\d\.\-_]+)?(\+[\w\d\.\-_]+)?' |
| 38 | |
| 39 | struct Options { |
| 40 | show_help bool |
| 41 | major bool |
| 42 | minor bool |
| 43 | patch bool |
| 44 | skip string |
| 45 | } |
| 46 | |
| 47 | type ReplacementFunction = fn (re regex.RE, input string, start int, end int) string |
| 48 | |
| 49 | fn replace_with_increased_patch_version(_re regex.RE, input string, start int, end int) string { |
| 50 | version := semver.from(input[start..end]) or { return input } |
| 51 | return version.increment(.patch).str() |
| 52 | } |
| 53 | |
| 54 | fn replace_with_increased_minor_version(_re regex.RE, input string, start int, end int) string { |
| 55 | version := semver.from(input[start..end]) or { return input } |
| 56 | return version.increment(.minor).str() |
| 57 | } |
| 58 | |
| 59 | fn replace_with_increased_major_version(_re regex.RE, input string, start int, end int) string { |
| 60 | version := semver.from(input[start..end]) or { return input } |
| 61 | return version.increment(.major).str() |
| 62 | } |
| 63 | |
| 64 | fn get_replacement_function(options Options) ReplacementFunction { |
| 65 | if options.patch { |
| 66 | return replace_with_increased_patch_version |
| 67 | } else if options.minor { |
| 68 | return replace_with_increased_minor_version |
| 69 | } else if options.major { |
| 70 | return replace_with_increased_major_version |
| 71 | } |
| 72 | return replace_with_increased_patch_version |
| 73 | } |
| 74 | |
| 75 | fn process_file(input_file string, options Options) ! { |
| 76 | lines := os.read_lines(input_file) or { return error('Failed to read file: ${input_file}') } |
| 77 | |
| 78 | mut re := regex.regex_opt(semver_query) or { return error('Could not create a RegEx parser.') } |
| 79 | |
| 80 | repl_fn := get_replacement_function(options) |
| 81 | |
| 82 | mut new_lines := []string{cap: lines.len} |
| 83 | mut replacement_complete := false |
| 84 | |
| 85 | for line in lines { |
| 86 | // Copy over the remaining lines normally if the replacement is complete |
| 87 | if replacement_complete { |
| 88 | new_lines << line |
| 89 | continue |
| 90 | } |
| 91 | |
| 92 | // Check if replacement is necessary |
| 93 | updated_line := if line.to_lower().contains('version') && !(options.skip != '' |
| 94 | && line.contains(options.skip)) { |
| 95 | replacement_complete = true |
| 96 | re.replace_by_fn(line, repl_fn) |
| 97 | } else { |
| 98 | line |
| 99 | } |
| 100 | new_lines << updated_line |
| 101 | } |
| 102 | |
| 103 | // Add a trailing newline |
| 104 | new_lines << '' |
| 105 | |
| 106 | backup_file := input_file + '.cache' |
| 107 | |
| 108 | // Remove the backup file if it exists. |
| 109 | os.rm(backup_file) or {} |
| 110 | |
| 111 | // Rename the original to the backup. |
| 112 | os.mv(input_file, backup_file) or { return error('Failed to move file: ${input_file}') } |
| 113 | |
| 114 | // Process the old file and write it back to the original. |
| 115 | os.write_file(input_file, new_lines.join_lines()) or { |
| 116 | return error('Failed to write file: ${input_file}') |
| 117 | } |
| 118 | |
| 119 | // Remove the backup file. |
| 120 | os.rm(backup_file) or {} |
| 121 | |
| 122 | if replacement_complete { |
| 123 | println('Bumped version in ${input_file}') |
| 124 | } else { |
| 125 | println('No changes made in ${input_file}') |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | fn main() { |
| 130 | if os.args.len < 2 { |
| 131 | eprintln('Usage: ${tool_name} [options] [file1 file2 ...] |
| 132 | ${tool_description} |
| 133 | Try ${tool_name} -h for more help...') |
| 134 | exit(1) |
| 135 | } |
| 136 | |
| 137 | mut fp := flag.new_flag_parser(os.args) |
| 138 | |
| 139 | fp.application(tool_name) |
| 140 | fp.version(tool_version) |
| 141 | fp.description(tool_description) |
| 142 | fp.arguments_description('[file1 file2 ...]') |
| 143 | fp.skip_executable() |
| 144 | |
| 145 | options := Options{ |
| 146 | show_help: fp.bool('help', `h`, false, 'Show this help text.') |
| 147 | patch: fp.bool('patch', `p`, false, 'Bump the patch version.') |
| 148 | minor: fp.bool('minor', `n`, false, 'Bump the minor version.') |
| 149 | major: fp.bool('major', `m`, false, 'Bump the major version.') |
| 150 | skip: fp.string('skip', `s`, '', 'Skip lines matching this substring.').trim_space() |
| 151 | } |
| 152 | |
| 153 | remaining := fp.finalize() or { |
| 154 | eprintln(fp.usage()) |
| 155 | exit(1) |
| 156 | } |
| 157 | |
| 158 | if options.show_help { |
| 159 | println(fp.usage()) |
| 160 | exit(0) |
| 161 | } |
| 162 | |
| 163 | validate_options(options) or { |
| 164 | eprintln(fp.usage()) |
| 165 | exit(1) |
| 166 | } |
| 167 | |
| 168 | files := remaining[1..] |
| 169 | |
| 170 | if files.len == 0 { |
| 171 | if !os.exists('v.mod') { |
| 172 | eprintln('v.mod does not exist. You can create one using "v init".') |
| 173 | exit(1) |
| 174 | } |
| 175 | process_file('v.mod', options) or { |
| 176 | eprintln('Failed to process v.mod: ${err}') |
| 177 | exit(1) |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | for input_file in files { |
| 182 | if !os.exists(input_file) { |
| 183 | eprintln('File not found: ${input_file}') |
| 184 | exit(1) |
| 185 | } |
| 186 | process_file(input_file, options) or { |
| 187 | eprintln('Failed to process ${input_file}: ${err}') |
| 188 | exit(1) |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | fn validate_options(options Options) ! { |
| 194 | if options.patch && options.major { |
| 195 | return error('Cannot specify both --patch and --major.') |
| 196 | } |
| 197 | |
| 198 | if options.patch && options.minor { |
| 199 | return error('Cannot specify both --patch and --minor.') |
| 200 | } |
| 201 | |
| 202 | if options.major && options.minor { |
| 203 | return error('Cannot specify both --major and --minor.') |
| 204 | } |
| 205 | |
| 206 | if !(options.patch || options.major || options.minor) { |
| 207 | return error('Must specify one of --patch, --major, or --minor.') |
| 208 | } |
| 209 | } |
| 210 | |