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