-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.go
More file actions
536 lines (453 loc) · 17.1 KB
/
main.go
File metadata and controls
536 lines (453 loc) · 17.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
package main
import (
"flag"
"fmt"
"maps"
"os"
"regexp"
"runtime"
"runtime/debug"
"sort"
"strings"
"github.com/UnitVectorY-Labs/repver/internal/color"
"github.com/UnitVectorY-Labs/repver/internal/git"
"github.com/UnitVectorY-Labs/repver/internal/repver"
)
var Version = "dev" // This will be set by the build systems to the release version
const applicationName = "repver"
var semverRe = regexp.MustCompile(`^\d+\.\d+\.\d+`)
func buildVersionOutput(version string) string {
normalized := version
if semverRe.MatchString(normalized) && !strings.HasPrefix(normalized, "v") {
normalized = "v" + normalized
}
return fmt.Sprintf("%s version %s (%s, %s/%s)", applicationName, normalized, runtime.Version(), runtime.GOOS, runtime.GOARCH)
}
// main is the entry point for the repver command-line tool.
func main() {
// Set the build version from the build info if not set by the build system
if Version == "dev" || Version == "" {
if bi, ok := debug.ReadBuildInfo(); ok {
if bi.Main.Version != "" && bi.Main.Version != "(devel)" {
Version = bi.Main.Version
}
}
}
// Pre-parse static flags to handle --version and --exists before loading .repver
preParse := flag.NewFlagSet("preparse", flag.ContinueOnError)
preParse.SetOutput(os.Stderr)
preCommand := preParse.String("command", "", "Command to execute")
preExists := preParse.Bool("exists", false, "Check whether .repver exists and contains the specified command")
preVersion := preParse.Bool("version", false, "Print version")
preDebug := preParse.Bool("debug", false, "Enable debug mode")
preDryRun := preParse.Bool("dry-run", false, "Dry run mode")
preNoColor := preParse.Bool("no-color", false, "Disable colored output")
// Register param-* flags dynamically to avoid unknown flag errors during pre-parse
// We'll accept any --param-* flags here but not use them
for _, arg := range os.Args[1:] {
if strings.HasPrefix(arg, "--param-") {
parts := strings.SplitN(arg, "=", 2)
paramName := strings.TrimPrefix(parts[0], "--")
if preParse.Lookup(paramName) == nil {
preParse.String(paramName, "", "")
}
}
}
// Parse pre-parse flags - errors are handled by falling through to normal mode
_ = preParse.Parse(os.Args[1:])
// Handle --version early
if *preVersion {
fmt.Println(buildVersionOutput(Version))
return
}
// Handle --exists mode
if *preExists {
handleExistsMode(*preCommand)
return
}
// Set debug and dry-run from pre-parse for early debugging
repver.Debug = *preDebug
repver.DryRun = *preDryRun
// Disable color if --no-color flag was passed (NO_COLOR env var is
// handled by the color package init function)
if *preNoColor {
color.Enabled = false
}
repver.NoColor = !color.Enabled
// Initialization Phase
// Decision: .repver exists?
if _, err := os.Stat(".repver"); os.IsNotExist(err) {
printErrorAndExit(100, ".repver file not found")
}
// Process: Load .repver
config, err := repver.Load(".repver")
// Decision: Load successful?
if err != nil {
printErrorAndExit(101, ".repver failed to load")
}
// Process: Validate .repver
err = config.Validate()
// Decision: Validation Successful?
if err != nil {
printErrorAndExit(102, fmt.Sprintf(".repver validation failed\n%v", err))
}
// Process: Enumerate possible command line arguments from .repver
argumentNames, err := config.GetParameterNames()
if err != nil {
// This error is not on the flowchart because the previous validate step
// should prevent this from ever happening
printErrorAndExit(501, "Internal error compiling prevalidated parameters")
}
argumentFlags := make(map[string]*string)
for _, argumentName := range argumentNames {
argumentFlags[argumentName] = flag.String("param-"+argumentName, "", "Value for "+argumentName)
}
showVersion := flag.Bool("version", false, "Print version")
// Process: Parse command line arguments
repver.ParseParams()
// Sync color state after full parse in case the pre-parse could not
// process --no-color (e.g. flag parsing stopped early on unknown flags).
if repver.NoColor {
color.Enabled = false
}
if *showVersion {
fmt.Println(buildVersionOutput(Version))
return
}
// Decision: Command specified?
if repver.UserCommand == "" {
// Generate help message listing all available commands with their parameters
helpMessage := generateHelpMessage(config)
printErrorAndExit(103, "No command specified", helpMessage)
}
// Process: Retrieve command configuration
command, err := config.GetCommand(repver.UserCommand)
// Decision: Command found?
if err != nil {
helpMessage := generateHelpMessage(config)
printErrorAndExit(104, "Command not found", helpMessage)
}
// Process: Identify required arguments for command]
parameters, err := command.GetParameterNames()
if err != nil {
// This error is not on the flowchart because the previous validate step
// should prevent this from ever happening
printErrorAndExit(502, "Internal error compiling prevalidated parameters")
}
// Decision: All params provided?
argumentValues := make(map[string]string)
missingParams := []string{}
for _, parameter := range parameters {
// Check if the parameter is set
if val, ok := argumentFlags[parameter]; ok && *val != "" {
argumentValues[parameter] = *val
} else {
missingParams = append(missingParams, parameter)
}
}
if len(missingParams) > 0 {
// Create a targeted help message for the specific command
var helpBuilder strings.Builder
helpBuilder.WriteString(fmt.Sprintf("Command '%s' requires the following parameters:\n", repver.UserCommand))
for _, param := range missingParams {
helpBuilder.WriteString(fmt.Sprintf(" --param-%s=<value>\n", param))
}
helpBuilder.WriteString("\nComplete usage example:\n")
helpBuilder.WriteString(fmt.Sprintf(" repver --command=%s", repver.UserCommand))
for _, param := range parameters {
helpBuilder.WriteString(fmt.Sprintf(" --param-%s=<value>", param))
}
printErrorAndExit(105, "Missing required parameters", helpBuilder.String())
}
// Process: Validate params and extract named groups
extractedGroups := make(map[string]string)
for _, param := range command.Params {
value, exists := argumentValues[param.Name]
if exists {
// Validate the value against the param pattern
if err := param.ValidateValue(value); err != nil {
printErrorAndExit(108, fmt.Sprintf("Parameter '%s' validation failed: %v", param.Name, err))
}
// Extract named groups from the value
groups, err := param.ExtractNamedGroups(value)
if err != nil {
printErrorAndExit(109, fmt.Sprintf("Failed to extract groups from parameter '%s': %v", param.Name, err))
}
maps.Copy(extractedGroups, groups)
repver.Debugln("Extracted groups from param '%s': %v", param.Name, groups)
}
}
// Evaluate all target changes before performing any git operations so a no-op
// leaves the repository untouched.
executionPlans := make([]*repver.ExecutionPlan, 0, len(command.Targets))
anyFileModified := false
commitFiles := []string{}
for _, target := range command.Targets {
plan, err := target.Plan(argumentValues, extractedGroups)
if err != nil {
printErrorAndExit(202, "Failed to evaluate command on target")
}
executionPlans = append(executionPlans, plan)
if plan.Modified {
anyFileModified = true
commitFiles = append(commitFiles, target.Path)
}
}
if !anyFileModified {
fmt.Println(color.Green("No updates needed; target files already match the requested values."))
return
}
// If dry run mode is enabled, output that information only after confirming
// there is actual work to preview.
if repver.DryRun {
fmt.Println(color.Yellow("DRY RUN MODE ENABLED"))
}
// Decision: Git options specified?
useGit := command.GitOptions.GitOptionsSpecified()
if useGit && !repver.DryRun {
// Decision: In git root?
isGitRoot, err := git.IsGitRoot()
if err != nil {
// This error isn't in the flowchart because the failure here is
printErrorAndExit(503, "Internal error determining git root")
}
if !isGitRoot {
printErrorAndExit(106, "Not in git repository")
}
// Decision: Git workspace clean?
err = git.CheckGitClean()
if err != nil {
printErrorAndExit(107, "Git workspace not clean")
}
} else if useGit && repver.DryRun {
fmt.Println(color.Yellow("[DRYRUN] Git operations would be performed but are disabled in dry run mode"))
}
// Execution Phase
// Decision: Git options specified?
originalBranchName := ""
newBranchName := ""
if useGit && !repver.DryRun {
// Process: Get the current branch name
originalBranchName, err = git.GetCurrentBranch()
if err != nil {
// This error isn't in the flowchart because we previously checked we are in a git repo
printErrorAndExit(504, "Internal error could not get current branch name")
}
// Decision: Create new branch?
newBranchName = originalBranchName
if command.GitOptions.CreateBranch {
newBranchName = command.GitOptions.BuildBranchName(argumentValues)
// Decision: Branch already exists?
branchExists, err := git.BranchExists(newBranchName)
if err != nil {
printErrorAndExit(503, "Internal error checking if branch exists")
}
if branchExists {
printErrorAndExit(200, fmt.Sprintf("Branch '%s' already exists", newBranchName))
}
// Process: Create new branch
output, err := git.CreateAndSwitchBranch(newBranchName)
// Decision: Branch creation successful?
if err != nil {
printErrorAndExit(201, "Failed to create new branch")
}
repver.Debugln("Created and switched to new branch\n%s", output)
}
} else if useGit && repver.DryRun && command.GitOptions.CreateBranch {
// Process: Get the current branch name
originalBranchName, err = git.GetCurrentBranch()
if err != nil {
// This error isn't in the flowchart because we previously checked we are in a git repo
printErrorAndExit(504, "Internal error could not get current branch name")
}
// In dry run mode, just show what branch would be created
newBranchName = command.GitOptions.BuildBranchName(argumentValues)
fmt.Println(color.Yellowf("[DRYRUN] Would create and switch to branch: %s", newBranchName))
}
for i, target := range command.Targets {
// Process: Execute the previously planned update to target
_, err := target.ExecutePlan(executionPlans[i])
// Decision: Execution successful?
if err != nil {
printErrorAndExit(202, "Failed to execute command on target")
}
}
// Decision: Commit changes to git?
if !anyFileModified {
repver.Debugln("No files modified, skipping commit")
} else if command.GitOptions.Commit && !repver.DryRun {
// Process: Construct commit message
commitMessage := command.GitOptions.BuildCommitMessage(argumentValues)
// Process: Commit changes to git
output, err := git.AddAndCommitFiles(commitFiles, commitMessage)
if err != nil {
// This error isn't in the flowchart because we previously checked we are in a git repo
printErrorAndExit(505, "Internal error could not add and commit files")
}
repver.Debugln("Changes committed successfully\n%s", output)
// Decision: Push changes to remote?
if command.GitOptions.Push && newBranchName != "" {
remote := command.GitOptions.Remote
if remote == "" {
remote = "origin"
}
// Process: Push changes to remote
output, err = git.PushChanges(remote, newBranchName)
if err != nil {
// This error isn't in the flowchart because we previously checked we are in a git repo
printErrorAndExit(506, "Internal error failed to push changes")
}
repver.Debugln("Changes pushed successfully\n%s", output)
// Decision: Create pull request?
if command.GitOptions.PullRequest == "GITHUB_CLI" {
output, err = git.CreateGitHubPullRequest()
if err != nil {
printErrorAndExit(508, "Failed to create GitHub pull request")
}
repver.Debugln("Created GitHub pull request\n%s", output)
}
}
} else if command.GitOptions.Commit && repver.DryRun {
// In dry run mode, just show what would be committed
commitMessage := command.GitOptions.BuildCommitMessage(argumentValues)
fmt.Println(color.Yellowf("[DRYRUN] Would commit changes with message: \"%s\"", commitMessage))
fmt.Println(color.Yellow("[DRYRUN] Files that would be added to the commit:"))
for _, file := range commitFiles {
fmt.Printf(" - %s\n", file)
}
if command.GitOptions.Push {
remote := command.GitOptions.Remote
if remote == "" {
remote = "origin"
}
fmt.Println(color.Yellowf("[DRYRUN] Would push changes to remote '%s' branch '%s'", remote, newBranchName))
}
if command.GitOptions.PullRequest == "GITHUB_CLI" {
fmt.Println(color.Yellow("[DRYRUN] Would create GitHub pull request"))
}
}
// Decision: Return to original branch?
if command.GitOptions.ReturnToOriginalBranch && !repver.DryRun && anyFileModified {
// Process: Switch back to original branch
output, err := git.SwitchToBranch(originalBranchName)
if err != nil {
// This error isn't in the flowchart because we previously checked we are in a git repo
printErrorAndExit(507, "Internal error failed to switch back to original branch")
}
repver.Debugln("Returned to original branch\n%s", output)
// Decision: Delete new branch?
if command.GitOptions.DeleteBranch && command.GitOptions.CreateBranch {
// Process: Delete new branch
output, err = git.DeleteLocalBranch(newBranchName)
if err != nil {
// This error isn't in the flowchart because we previously checked we are in a git repo
printErrorAndExit(509, "Internal error failed to delete new branch")
}
repver.Debugln("Deleted branch\n%s", output)
}
} else if command.GitOptions.ReturnToOriginalBranch && repver.DryRun {
fmt.Println(color.Yellowf("[DRYRUN] Would switch back to original branch '%s'", originalBranchName))
if command.GitOptions.DeleteBranch && command.GitOptions.CreateBranch {
fmt.Println(color.Yellowf("[DRYRUN] Would delete branch '%s'", newBranchName))
}
}
}
// generateHelpMessage creates a formatted help message showing all available commands
// and their required parameters from the configuration
func generateHelpMessage(config *repver.RepverConfig) string {
var help strings.Builder
help.WriteString("USAGE:\n")
help.WriteString(" repver --command=<command_name> [--param-<n>=<value> ...] [OPTIONS]\n\n")
help.WriteString("AVAILABLE COMMANDS:\n")
if len(config.Commands) == 0 {
help.WriteString(" No commands defined in .repver configuration\n")
return help.String()
}
// Get the longest command name for proper padding
maxNameLen := 0
for _, cmd := range config.Commands {
if len(cmd.Name) > maxNameLen {
maxNameLen = len(cmd.Name)
}
}
// Sort the commands alphabetically for easier reading
cmdNames := make([]string, 0, len(config.Commands))
cmdMap := make(map[string]*repver.RepverCommand)
for i, cmd := range config.Commands {
cmdNames = append(cmdNames, cmd.Name)
cmdMap[cmd.Name] = &config.Commands[i]
}
sort.Strings(cmdNames)
// Print each command with its parameters
for _, name := range cmdNames {
cmd := cmdMap[name]
// Get parameters for this command
params, err := cmd.GetParameterNames()
if err != nil {
continue // Skip if we can't get parameters
}
// Format command name with padding
padding := strings.Repeat(" ", maxNameLen-len(name)+2)
help.WriteString(fmt.Sprintf(" %s%s", name, padding))
// Include example usage
if len(params) > 0 {
paramList := strings.Join(params, ", ")
help.WriteString(fmt.Sprintf("Parameters: [%s]\n", paramList))
// Add complete example
help.WriteString(fmt.Sprintf(" Example: repver --command=%s", name))
for _, param := range params {
help.WriteString(fmt.Sprintf(" --param-%s=<value>", param))
}
help.WriteString("\n\n")
} else {
help.WriteString("No parameters required\n")
help.WriteString(fmt.Sprintf(" Example: repver --command=%s\n\n", name))
}
}
help.WriteString("OPTIONS:\n")
help.WriteString(" --debug Enable debug output\n")
help.WriteString(" --dry-run Show what would be changed without modifying files or performing git operations\n")
help.WriteString(" --no-color Disable colored output (also respects NO_COLOR environment variable)\n")
return help.String()
}
func printErrorAndExit(errNum int, errMsg string, helpMsg ...string) {
fmt.Fprintf(os.Stderr, "%s %s\n", color.BoldRed(fmt.Sprintf("Error (%d):", errNum)), errMsg)
if len(helpMsg) > 0 && helpMsg[0] != "" {
fmt.Fprintln(os.Stderr, "\n"+helpMsg[0])
}
os.Exit(errNum)
}
// handleExistsMode handles the --exists flag behavior.
// It checks if .repver exists and contains the specified command.
// Exits with 0 if successful, 1 otherwise.
func handleExistsMode(command string) {
// Check if --command is provided
if command == "" {
fmt.Fprintln(os.Stderr, "--command is required with --exists")
os.Exit(1)
}
// Check if .repver exists
if _, err := os.Stat(".repver"); os.IsNotExist(err) {
fmt.Fprintln(os.Stderr, ".repver not found")
os.Exit(1)
}
// Load .repver
config, err := repver.Load(".repver")
if err != nil {
fmt.Fprintln(os.Stderr, "invalid .repver")
os.Exit(1)
}
// Validate .repver
if err := config.Validate(); err != nil {
fmt.Fprintln(os.Stderr, "invalid .repver")
os.Exit(1)
}
// Check if command exists
_, err = config.GetCommand(command)
if err != nil {
fmt.Fprintf(os.Stderr, "command not found: %s\n", command)
os.Exit(1)
}
// Success - command exists
os.Exit(0)
}