package main import ( "flag" "fmt" "log" "os" "regexp" "strconv" "strings" "github.com/go-ini/ini" ) type BumpConfig struct { CurrentVersion string Commit bool Tag bool TagName string Parse string Serialize string Message string FilePaths []string } // getBumpConfig загружает конфигурацию из указанного файла func getBumpConfig(cfg_name string) (*BumpConfig, error) { cfg, err := ini.Load(cfg_name) if err != nil { return nil, fmt.Errorf("error loading config: %w", err) } sec, err := cfg.GetSection("bumpversion") if err != nil { return nil, fmt.Errorf("missing [bumpversion] section: %w", err) } bc := &BumpConfig{ CurrentVersion: sec.Key("current_version").String(), Commit: sec.Key("commit").MustBool(false), Tag: sec.Key("tag").MustBool(false), TagName: sec.Key("tag_name").String(), Parse: sec.Key("parse").String(), Serialize: strings.TrimSpace(sec.Key("serialize").String()), Message: sec.Key("message").String(), } // Получим список файлов для обновления из секций, начинающихся с "bumpversion:file:" for _, s := range cfg.Sections() { if strings.HasPrefix(s.Name(), "bumpversion:file:") { // Имя файла после последнего двоеточия: parts := strings.SplitN(s.Name(), ":", 3) if len(parts) == 3 { filePath := strings.TrimSpace(parts[2]) bc.FilePaths = append(bc.FilePaths, filePath) } } } return bc, nil } // bumpVersion парсит current_version, увеличивает нужную часть и возвращает новую версию. func bumpVersion(bc *BumpConfig, part string) (string, error) { re, err := regexp.Compile(bc.Parse) if err != nil { return "", fmt.Errorf("failed to compile parse regex: %w", err) } matches := re.FindStringSubmatch(bc.CurrentVersion) if matches == nil { return "", fmt.Errorf("current version %s does not match parse pattern", bc.CurrentVersion) } // Создадим карту групп по именам groupNames := re.SubexpNames() var major, minor, patch int var hasMajor, hasMinor, hasPatch bool for i, name := range groupNames { switch name { case "major": major, err = strconv.Atoi(matches[i]) if err != nil { return "", err } hasMajor = true case "minor": minor, err = strconv.Atoi(matches[i]) if err != nil { return "", err } hasMinor = true case "patch": patch, err = strconv.Atoi(matches[i]) if err != nil { return "", err } hasPatch = true } } if !hasMajor || !hasMinor || !hasPatch { return "", fmt.Errorf("parse pattern must contain major, minor, and patch groups") } switch part { case "major": major++ minor = 0 patch = 0 case "minor": minor++ patch = 0 case "patch": patch++ default: return "", fmt.Errorf("unknown bump part: %s", part) } newVersion := bc.Serialize newVersion = strings.ReplaceAll(newVersion, "{major}", fmt.Sprintf("%d", major)) newVersion = strings.ReplaceAll(newVersion, "{minor}", fmt.Sprintf("%d", minor)) newVersion = strings.ReplaceAll(newVersion, "{patch}", fmt.Sprintf("%d", patch)) return newVersion, nil } // updateFiles обновляет версию в файле func updateFiles(filePaths []string, oldVersion, newVersion string) { for _, path := range filePaths { data, err := os.ReadFile(path) if err != nil { log.Printf("Unable to read file %s: %v", path, err) continue } newData := strings.ReplaceAll(string(data), oldVersion, newVersion) if err := os.WriteFile(path, []byte(newData), 0644); err != nil { log.Printf("Unable to write file %s: %v", path, err) } else { log.Printf("Updated file: %s", path) } } } // updateConfigFile обновляет исходный конфигурационный файл func updateConfigFile(configPath string, newVersion string) error { cfg, err := ini.Load(configPath) if err != nil { return fmt.Errorf("failed to load config: %w", err) } sec, err := cfg.GetSection("bumpversion") if err != nil { return fmt.Errorf("section [bumpversion] not found: %w", err) } sec.Key("current_version").SetValue(newVersion) return cfg.SaveTo(configPath) } // resolveFlag проверяет два логических флага: positive и negative. // Если оба заданы как true, вызывается ошибка; если задан negative, возвращается false; // если задан positive, возвращается true; иначе возвращается defaultValue. func resolveFlag(positive, negative *bool, defaultValue bool) (bool, error) { if *positive && *negative { return false, fmt.Errorf("conflicting flags: both positive and negative are set") } if *negative { return false, nil } if *positive { return true, nil } return defaultValue, nil } // Версия приложения const AppName = "BumpVersion" var ( AppVersion = "1.0.0" AppCommit = "unknown" ) func versionString() string { if AppCommit == "" || AppCommit == "unknown" { return AppVersion } return fmt.Sprintf("%s (%s)", AppVersion, AppCommit) } func main() { const cfg_name = ".bumpversion.cfg" args := os.Args[1:] // Проверяем аргументы командной строки if len(args) > 0 && strings.ToLower(args[0]) == "--version" { fmt.Printf("%s version %s\n", AppName, versionString()) return } // Печатаем название и версию при старте fmt.Printf("Starting %s version %s\n", AppName, versionString()) bc, err := getBumpConfig(cfg_name) if err != nil { log.Fatalf("Error reading bumpversion configuration: %v", err) } // Парсинг аргументов командной строки part := flag.String("part", "patch", "Part of the version to bump (major/minor/patch)") commit := flag.Bool("commit", false, "Create a commit") noCommit := flag.Bool("no-commit", false, "Do not create a commit") tag := flag.Bool("tag", false, "Add a git tag") noTag := flag.Bool("no-tag", false, "Do not add a git tag") push := flag.Bool("push", false, "Force push to repository") flag.Parse() // Обработка флагов if *part != "major" && *part != "minor" && *part != "patch" { fmt.Printf("Error: part must be one of 'major', 'minor', or 'patch'. Got '%s'\n", *part) os.Exit(1) } // Разрешаем флаги: shouldCommit, err := resolveFlag(commit, noCommit, bc.Commit) if err != nil { log.Fatalf("Error resolving commit flags: %v", err) } shouldTag, err := resolveFlag(tag, noTag, bc.Tag) if err != nil { log.Fatalf("Error resolving tag flags: %v", err) } newVersion, err := bumpVersion(bc, *part) if err != nil { log.Fatalf("Error bumping version: %v", err) } fmt.Printf("Current version: %s\n", bc.CurrentVersion) fmt.Printf("New version: %s\n", newVersion) // Обновляем файлы, указанные в конфигурации updateFiles(bc.FilePaths, bc.CurrentVersion, newVersion) // Обновляем конфигурационный файл if err := updateConfigFile(cfg_name, newVersion); err != nil { log.Printf("Error updating config file: %v", err) } else { log.Printf("Config file %s updated to version %s", cfg_name, newVersion) } // Выполняем git commit и tag, если требуется if shouldCommit { gitCommit(bc, newVersion, cfg_name) } // Выполняем git commit и tag, если требуется if shouldTag { gitTag(bc, newVersion) } if *push { gitPush(bc, newVersion) } }