254 lines
7.5 KiB
Go
254 lines
7.5 KiB
Go
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)
|
||
}
|
||
}
|