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. |
3 | module main |
4 | |
5 | import flag |
6 | import os |
7 | import regex |
8 | import semver |
9 | |
10 | const ( |
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 | |
29 | Examples: |
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 | |
40 | struct Options { |
41 | show_help bool |
42 | major bool |
43 | minor bool |
44 | patch bool |
45 | skip string |
46 | } |
47 | |
48 | type ReplacementFunction = fn (re regex.RE, input string, start int, end int) string |
49 | |
50 | fn 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 | |
55 | fn 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 | |
60 | fn 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 | |
65 | fn 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 | |
76 | fn 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 | |
130 | fn main() { |
131 | if os.args.len < 2 { |
132 | eprintln('Usage: ${tool_name} [options] [file1 file2 ...] |
133 | ${tool_description} |
134 | Try ${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 | |
194 | fn 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 | } |