From f0969698e276c15d14da996a11367505db87bfc5 Mon Sep 17 00:00:00 2001 From: Subhomoy Haldar Date: Sun, 12 Dec 2021 01:47:01 +0530 Subject: [PATCH] cmd: add v bump (#12798) --- cmd/tools/vbump.v | 188 +++++++++++++++++++++++++++++++++++++++++ cmd/tools/vbump_test.v | 95 +++++++++++++++++++++ cmd/tools/vcomplete.v | 1 + cmd/v/help/bump.txt | 28 ++++++ cmd/v/v.v | 1 + 5 files changed, 313 insertions(+) create mode 100644 cmd/tools/vbump.v create mode 100644 cmd/tools/vbump_test.v create mode 100644 cmd/v/help/bump.txt diff --git a/cmd/tools/vbump.v b/cmd/tools/vbump.v new file mode 100644 index 0000000000..88b8721d2b --- /dev/null +++ b/cmd/tools/vbump.v @@ -0,0 +1,188 @@ +// Copyright (c) 2019-2021 Subhomoy Haldar. All rights reserved. +// Use of this source code is governed by an MIT license that can be found in the LICENSE file. +module main + +import flag +import os +import regex +import semver + +const ( + tool_name = os.file_name(os.executable()) + tool_version = '0.0.1' + tool_description = '\n Bump the semantic version of the v.mod and/or specified files. + + The first instance of a version number is replaced with the new version. + Additionally, the line affected must contain the word "version" in any + form of capitalization. For instance, the following lines will be + recognized by the heuristic: + + tool_version = \'1.2.1\' + version: \'0.2.42\' + VERSION = "1.23.8" + +Examples: + Bump the patch version in v.mod if it exists + v bump --patch + Bump the major version in v.mod and vls.v + v bump --major v.mod vls.v + Upgrade the minor version in sample.v only + v bump --minor sample.v +' + semver_query = r'((0)|([1-9]\d*)\.){2}(0)|([1-9]\d*)(\-[\w\d\.\-_]+)?(\+[\w\d\.\-_]+)?' +) + +struct Options { + show_help bool + major bool + minor bool + patch bool +} + +type ReplacementFunction = fn (re regex.RE, input string, start int, end int) string + +fn replace_with_increased_patch_version(re regex.RE, input string, start int, end int) string { + version := semver.from(input[start..end]) or { return input } + return version.increment(.patch).str() +} + +fn replace_with_increased_minor_version(re regex.RE, input string, start int, end int) string { + version := semver.from(input[start..end]) or { return input } + return version.increment(.minor).str() +} + +fn replace_with_increased_major_version(re regex.RE, input string, start int, end int) string { + version := semver.from(input[start..end]) or { return input } + return version.increment(.major).str() +} + +fn get_replacement_function(options Options) ReplacementFunction { + if options.patch { + return replace_with_increased_patch_version + } else if options.minor { + return replace_with_increased_minor_version + } else if options.major { + return replace_with_increased_major_version + } + return replace_with_increased_patch_version +} + +fn process_file(input_file string, options Options) { + lines := os.read_lines(input_file) or { panic('Failed to read file: $input_file') } + + mut re := regex.regex_opt(semver_query) or { panic('Could not create a RegEx parser.') } + + repl_fn := get_replacement_function(options) + + mut new_lines := []string{cap: lines.len} + mut replacement_complete := false + + for line in lines { + // Copy over the remaining lines normally if the replacement is complete + if replacement_complete { + new_lines << line + continue + } + + // Check if replacement is necessary + updated_line := if line.to_lower().contains('version') { + replacement_complete = true + re.replace_by_fn(line, repl_fn) + } else { + line + } + new_lines << updated_line + } + + // Add a trailing newline + new_lines << '' + + backup_file := input_file + '.cache' + + // Remove the backup file if it exists. + os.rm(backup_file) or {} + + // Rename the original to the backup. + os.mv(input_file, backup_file) or { panic('Failed to copy file: $input_file') } + + // Process the old file and write it back to the original. + os.write_file(input_file, new_lines.join_lines()) or { + panic('Failed to write file: $input_file') + } + + // Remove the backup file. + os.rm(backup_file) or {} + + if replacement_complete { + println('Bumped version in $input_file') + } else { + println('No changes made in $input_file') + } +} + +fn main() { + if os.args.len < 2 { + println('Usage: $tool_name [options] [file1 file2 ...] +$tool_description +Try $tool_name -h for more help...') + exit(1) + } + + mut fp := flag.new_flag_parser(os.args) + + fp.application(tool_name) + fp.version(tool_version) + fp.description(tool_description) + fp.arguments_description('[file1 file2 ...]') + fp.skip_executable() + + options := Options{ + show_help: fp.bool('help', `h`, false, 'Show this help text.') + patch: fp.bool('patch', `p`, false, 'Bump the patch version.') + minor: fp.bool('minor', `n`, false, 'Bump the minor version.') + major: fp.bool('major', `m`, false, 'Bump the major version.') + } + + if options.show_help { + println(fp.usage()) + exit(0) + } + + validate_options(options) or { panic(err) } + + files := os.args[3..] + + if files.len == 0 { + if !os.exists('v.mod') { + println('v.mod does not exist. You can create one using "v init".') + exit(1) + } + process_file('v.mod', options) + } + + for input_file in files { + if !os.exists(input_file) { + println('File not found: $input_file') + exit(1) + } + process_file(input_file, options) + } +} + +fn validate_options(options Options) ? { + if options.patch && options.major { + return error('Cannot specify both --patch and --major.') + } + + if options.patch && options.minor { + return error('Cannot specify both --patch and --minor.') + } + + if options.major && options.minor { + return error('Cannot specify both --major and --minor.') + } + + if !(options.patch || options.major || options.minor) { + return error('Must specify one of --patch, --major, or --minor.') + } +} diff --git a/cmd/tools/vbump_test.v b/cmd/tools/vbump_test.v new file mode 100644 index 0000000000..ad532fde1a --- /dev/null +++ b/cmd/tools/vbump_test.v @@ -0,0 +1,95 @@ +import os + +struct BumpTestCase { + file_name string + contents string + line int + expected_patch string + expected_minor string + expected_major string +} + +const test_cases = [ + BumpTestCase{ + file_name: 'v.mod' + contents: "Module { + name: 'Sample' + description: 'Sample project' + version: '1.2.6' + license: 'MIT' + dependencies: [] +} + +" + line: 3 + expected_patch: " version: '1.2.7'" + expected_minor: " version: '1.3.0'" + expected_major: " version: '2.0.0'" + }, + BumpTestCase{ + file_name: 'random_versions.vv' + contents: " +1.1.2 +1.2.5 +3.21.73 +version = '1.5.1' + +" + line: 4 + expected_patch: "version = '1.5.2'" + expected_minor: "version = '1.6.0'" + expected_major: "version = '2.0.0'" + }, + BumpTestCase{ + file_name: 'sample_tool.v' + contents: "// Module comment and copyright information +import os +import flag + +const ( + tool_name = os.file_name(os.executable()) + tool_version = '0.1.33' +) +fn main() { + // stuff +} + " + line: 6 + expected_patch: " tool_version = '0.1.34'" + expected_minor: " tool_version = '0.2.0'" + expected_major: " tool_version = '1.0.0'" + }, +] + +fn run_individual_test(case BumpTestCase) ? { + vexe := @VEXE + + temp_dir := os.temp_dir() + test_file := os.join_path_single(temp_dir, case.file_name) + + os.rm(test_file) or {} + os.write_file(test_file, case.contents) ? + + { + os.execute_or_exit('$vexe bump --patch $test_file') + patch_lines := os.read_lines(test_file) ? + assert patch_lines[case.line] == case.expected_patch + } + { + os.execute_or_exit('$vexe bump --minor $test_file') + minor_lines := os.read_lines(test_file) ? + assert minor_lines[case.line] == case.expected_minor + } + { + os.execute_or_exit('$vexe bump --major $test_file') + major_lines := os.read_lines(test_file) ? + assert major_lines[case.line] == case.expected_major + } + os.rm(test_file) ? +} + +fn test_all_bump_cases() { + for case in test_cases { + run_individual_test(case) or { panic(err) } + } +} diff --git a/cmd/tools/vcomplete.v b/cmd/tools/vcomplete.v index a847269a87..29b0322d00 100644 --- a/cmd/tools/vcomplete.v +++ b/cmd/tools/vcomplete.v @@ -59,6 +59,7 @@ const ( 'build-examples', 'build-tools', 'build-vbinaries', + 'bump', 'check-md', 'complete', 'compress', diff --git a/cmd/v/help/bump.txt b/cmd/v/help/bump.txt new file mode 100644 index 0000000000..9885ed3445 --- /dev/null +++ b/cmd/v/help/bump.txt @@ -0,0 +1,28 @@ +Usage: v bump [options] [file1 file2 ...] + +Description: + Bump the semantic version of the v.mod and/or specified files. + + The first instance of a version number is replaced with the new version. + Additionally, the line affected must contain the word "version" in any + form of capitalization. For instance, the following lines will be + recognized by the heuristic: + + tool_version = '1.2.1' + version: '0.2.42' + VERSION = "1.23.8" + +Examples: + Bump the patch version in v.mod if it exists + v bump --patch + Bump the major version in v.mod and vls.v + v bump --major v.mod vls.v + Upgrade the minor version in sample.v only + v bump --minor sample.v + + +Options: + -h, --help Show this help text. + -m, --major Bump the major version. + -n, --minor Bump the minor version. + -p, --patch Bump the patch version. diff --git a/cmd/v/v.v b/cmd/v/v.v index adde3b9d93..7ac6e4e0cf 100644 --- a/cmd/v/v.v +++ b/cmd/v/v.v @@ -19,6 +19,7 @@ const ( 'build-examples', 'build-tools', 'build-vbinaries', + 'bump', 'check-md', 'complete', 'compress',