Files
bumpversion/src/main.go
Ruslan Popov 027b69c1f6
Some checks failed
continuous-integration/drone/push Build is failing
Bump version
2025-12-24 12:01:49 +03:00

254 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}