v / cmd / tools / vbump.v
209 lines · 168 sloc · 5.62 KB · 3d60410b605d001e54f280070d5f952da9de1112
Raw
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.
3module main
4
5import flag
6import os
7import regex
8import semver
9
10const tool_name = os.file_name(os.executable())
11const tool_version = '0.1.0'
12const 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
28Examples:
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
37const semver_query = r'((0)|([1-9]\d*)\.){2}(0)|([1-9]\d*)(\-[\w\d\.\-_]+)?(\+[\w\d\.\-_]+)?'
38
39struct Options {
40 show_help bool
41 major bool
42 minor bool
43 patch bool
44 skip string
45}
46
47type ReplacementFunction = fn (re regex.RE, input string, start int, end int) string
48
49fn 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
54fn 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
59fn 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
64fn 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
75fn 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
129fn main() {
130 if os.args.len < 2 {
131 eprintln('Usage: ${tool_name} [options] [file1 file2 ...]
132${tool_description}
133Try ${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
193fn 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