This commit is contained in:
277
src/main.go
277
src/main.go
@@ -1,14 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
git "github.com/go-git/go-git/v5"
|
||||
"github.com/go-ini/ini"
|
||||
)
|
||||
|
||||
@@ -23,6 +26,18 @@ type BumpConfig struct {
|
||||
FilePaths []string
|
||||
}
|
||||
|
||||
type resolvedFlag struct {
|
||||
value bool
|
||||
source string
|
||||
}
|
||||
|
||||
type fileStatus struct {
|
||||
path string
|
||||
exists bool
|
||||
contains bool
|
||||
err error
|
||||
}
|
||||
|
||||
// getBumpConfig загружает конфигурацию из указанного файла
|
||||
func getBumpConfig(cfg_name string) (*BumpConfig, error) {
|
||||
cfg, err := ini.Load(cfg_name)
|
||||
@@ -186,25 +201,55 @@ func versionString() string {
|
||||
return fmt.Sprintf("%s (%s)", AppVersion, AppCommit)
|
||||
}
|
||||
|
||||
func normalizeArgs(args []string) ([]string, error) {
|
||||
if len(args) == 0 {
|
||||
return args, nil
|
||||
func extractPart(args []string) (string, []string, error) {
|
||||
var part string
|
||||
flagArgs := make([]string, 0, len(args))
|
||||
|
||||
for _, arg := range args {
|
||||
trimmed := strings.TrimSpace(arg)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "-") {
|
||||
flagArgs = append(flagArgs, arg)
|
||||
continue
|
||||
}
|
||||
|
||||
value := strings.ToLower(trimmed)
|
||||
switch value {
|
||||
case "major", "minor", "patch":
|
||||
if part != "" {
|
||||
return "", nil, fmt.Errorf("multiple positional arguments provided (expected only one major/minor/patch)")
|
||||
}
|
||||
part = value
|
||||
default:
|
||||
return "", nil, fmt.Errorf("unknown positional argument %q (expected major/minor/patch or flags)", trimmed)
|
||||
}
|
||||
}
|
||||
first := strings.TrimSpace(args[0])
|
||||
if first == "" || strings.HasPrefix(first, "-") {
|
||||
return args, nil
|
||||
|
||||
if part == "" {
|
||||
return "", nil, fmt.Errorf("version part argument required (major|minor|patch)")
|
||||
}
|
||||
switch strings.ToLower(first) {
|
||||
case "major", "minor", "patch":
|
||||
return append([]string{"-part=" + strings.ToLower(first)}, args[1:]...), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown positional argument %q (expected major/minor/patch or flags)", first)
|
||||
|
||||
return part, flagArgs, nil
|
||||
}
|
||||
|
||||
func hasHelpFlag(args []string) bool {
|
||||
for _, arg := range args {
|
||||
switch arg {
|
||||
case "-h", "--help", "-help":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func main() {
|
||||
const cfg_name = ".bumpversion.cfg"
|
||||
args := os.Args[1:]
|
||||
|
||||
flag.Usage = usage(cfg_name)
|
||||
|
||||
// Проверяем аргументы командной строки
|
||||
if len(args) > 0 && strings.ToLower(args[0]) == "--version" {
|
||||
fmt.Printf("%s version %s\n", AppName, versionString())
|
||||
@@ -214,26 +259,33 @@ func main() {
|
||||
// Печатаем название и версию при старте
|
||||
fmt.Printf("Starting %s version %s\n", AppName, versionString())
|
||||
|
||||
normalizedArgs, err := normalizeArgs(args)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
bc, err := getBumpConfig(cfg_name)
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading bumpversion configuration: %v", err)
|
||||
}
|
||||
|
||||
if hasHelpFlag(args) {
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
partValue, flagArgs, err := extractPart(args)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// Парсинг аргументов командной строки
|
||||
part := flag.String("part", "patch", "Part of the version to bump (major/minor/patch). Positional form is also supported: bumpversion.run major")
|
||||
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")
|
||||
noFatal := flag.Bool("no-fatal", false, "Do not fail if a configured file does not contain the current version")
|
||||
push := flag.Bool("push", false, "Force push to repository")
|
||||
if err := flag.CommandLine.Parse(normalizedArgs); err != nil {
|
||||
dryRun := flag.Bool("dry-run", false, "Plan only: show what will be changed without touching files or git")
|
||||
verbose := flag.Bool("verbose", false, "Print detailed output")
|
||||
|
||||
if err := flag.CommandLine.Parse(flagArgs); err != nil {
|
||||
log.Fatalf("Error parsing flags: %v", err)
|
||||
}
|
||||
|
||||
@@ -243,22 +295,21 @@ func main() {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
partValue := strings.ToLower(strings.TrimSpace(*part))
|
||||
if partValue != "major" && partValue != "minor" && partValue != "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)
|
||||
shouldCommit, err := resolveFlagVerbose("commit", commit, noCommit, false, "default (--no-commit)")
|
||||
if err != nil {
|
||||
log.Fatalf("Error resolving commit flags: %v", err)
|
||||
}
|
||||
shouldTag, err := resolveFlag(tag, noTag, bc.Tag)
|
||||
shouldTag, err := resolveFlagVerbose("tag", tag, noTag, false, "default (--no-tag)")
|
||||
if err != nil {
|
||||
log.Fatalf("Error resolving tag flags: %v", err)
|
||||
}
|
||||
|
||||
if *verbose {
|
||||
printConfigSummary(cfg_name, bc)
|
||||
printEffectiveParameters(partValue, bc, shouldCommit, shouldTag, *push, !*noFatal, *dryRun)
|
||||
}
|
||||
|
||||
newVersion, err := bumpVersion(bc, partValue)
|
||||
if err != nil {
|
||||
log.Fatalf("Error bumping version: %v", err)
|
||||
@@ -266,6 +317,15 @@ func main() {
|
||||
fmt.Printf("Current version: %s\n", bc.CurrentVersion)
|
||||
fmt.Printf("New version: %s\n", newVersion)
|
||||
|
||||
if *verbose {
|
||||
printFileStatuses(bc.FilePaths, bc.CurrentVersion)
|
||||
}
|
||||
|
||||
if *dryRun {
|
||||
printPlanOnly(partValue, newVersion, bc, shouldCommit, shouldTag, *push, !*noFatal)
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем файлы, указанные в конфигурации
|
||||
if err := updateFiles(bc.FilePaths, bc.CurrentVersion, newVersion, !*noFatal); err != nil {
|
||||
log.Fatalf("Error updating files: %v", err)
|
||||
@@ -279,12 +339,12 @@ func main() {
|
||||
}
|
||||
|
||||
// Выполняем git commit и tag, если требуется
|
||||
if shouldCommit {
|
||||
if shouldCommit.value {
|
||||
gitCommit(bc, newVersion, cfg_name)
|
||||
}
|
||||
|
||||
// Выполняем git commit и tag, если требуется
|
||||
if shouldTag {
|
||||
if shouldTag.value {
|
||||
gitTag(bc, newVersion)
|
||||
}
|
||||
|
||||
@@ -292,3 +352,162 @@ func main() {
|
||||
gitPush(bc, newVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func printConfigSummary(cfgPath string, bc *BumpConfig) {
|
||||
fmt.Println("Configuration:")
|
||||
fmt.Printf(" config_file: %s\n", cfgPath)
|
||||
fmt.Printf(" current_version: %s\n", bc.CurrentVersion)
|
||||
fmt.Printf(" parse: %s\n", bc.Parse)
|
||||
fmt.Printf(" serialize: %s\n", bc.Serialize)
|
||||
fmt.Printf(" commit (config): %t\n", bc.Commit)
|
||||
fmt.Printf(" tag (config): %t\n", bc.Tag)
|
||||
fmt.Printf(" tag_name template: %s\n", dashIfEmpty(bc.TagName))
|
||||
fmt.Printf(" message template: %s\n", dashIfEmpty(bc.Message))
|
||||
fmt.Printf(" files: %s\n", strings.Join(bc.FilePaths, ", "))
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func printEffectiveParameters(part string, bc *BumpConfig, commit resolvedFlag, tag resolvedFlag, push bool, fatal bool, dryRun bool) {
|
||||
fmt.Println("Effective parameters:")
|
||||
fmt.Printf(" part: %s\n", part)
|
||||
fmt.Printf(" commit: %t (source: %s)\n", commit.value, commit.source)
|
||||
fmt.Printf(" tag: %t (source: %s)\n", tag.value, tag.source)
|
||||
fmt.Printf(" push: %t\n", push)
|
||||
fmt.Printf(" fatal_on_missing: %t\n", fatal)
|
||||
fmt.Printf(" dry_run: %t\n", dryRun)
|
||||
fmt.Printf(" git: %s\n", gitHint())
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func printFileStatuses(paths []string, needle string) {
|
||||
fmt.Println("Target files status:")
|
||||
statuses := collectFileStatuses(paths, needle)
|
||||
for _, st := range statuses {
|
||||
switch {
|
||||
case st.err != nil:
|
||||
fmt.Printf(" %s: error: %v\n", st.path, st.err)
|
||||
case !st.exists:
|
||||
fmt.Printf(" %s: missing\n", st.path)
|
||||
case st.contains:
|
||||
fmt.Printf(" %s: present, contains current version\n", st.path)
|
||||
default:
|
||||
fmt.Printf(" %s: present, current version not found\n", st.path)
|
||||
}
|
||||
}
|
||||
if len(statuses) == 0 {
|
||||
fmt.Println(" (no files configured)")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func collectFileStatuses(paths []string, needle string) []fileStatus {
|
||||
result := make([]fileStatus, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
continue
|
||||
}
|
||||
st := fileStatus{path: path}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
st.exists = false
|
||||
} else {
|
||||
st.err = err
|
||||
}
|
||||
result = append(result, st)
|
||||
continue
|
||||
}
|
||||
st.exists = true
|
||||
st.contains = strings.Contains(string(data), needle)
|
||||
result = append(result, st)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func printPlanOnly(part, newVersion string, bc *BumpConfig, commit resolvedFlag, tag resolvedFlag, push bool, fatal bool) {
|
||||
fmt.Println("Plan (dry-run):")
|
||||
fmt.Printf(" bump %s -> %s using parse=%q serialize=%q\n", bc.CurrentVersion, newVersion, bc.Parse, bc.Serialize)
|
||||
for _, path := range bc.FilePaths {
|
||||
fmt.Printf(" would update file: %s (fatal_if_missing=%t)\n", path, fatal)
|
||||
}
|
||||
fmt.Printf(" would update config: %s -> current_version=%s\n", ".bumpversion.cfg", newVersion)
|
||||
if commit.value {
|
||||
fmt.Printf(" would commit with message template: %s\n", bc.Message)
|
||||
}
|
||||
if tag.value {
|
||||
fmt.Printf(" would tag: %s\n", strings.ReplaceAll(bc.TagName, "{new_version}", newVersion))
|
||||
}
|
||||
if push {
|
||||
fmt.Println(" would push branch and tag to origin")
|
||||
}
|
||||
fmt.Println(" no changes were made (dry-run)")
|
||||
}
|
||||
|
||||
func dashIfEmpty(s string) string {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return "—"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func usage(cfgPath string) func() {
|
||||
return func() {
|
||||
fmt.Println("Usage:")
|
||||
fmt.Println(" bumpversion.run <major|minor|patch> [--commit|--no-commit] [--tag|--no-tag] [--push] [--no-fatal] [--dry-run]")
|
||||
fmt.Println()
|
||||
fmt.Println("Flags:")
|
||||
fmt.Println(" major|minor|patch Required positional argument selecting which part to bump")
|
||||
fmt.Println(" --commit / --no-commit Toggle commit creation (default: --no-commit)")
|
||||
fmt.Println(" --tag / --no-tag Toggle tag creation (default: --no-tag)")
|
||||
fmt.Println(" --push Push branch+tag to origin after bump")
|
||||
fmt.Println(" --no-fatal Do not fail if old version not found in target files")
|
||||
fmt.Println(" --dry-run Show plan and detected files, do not touch anything")
|
||||
fmt.Println(" --verbose Print detailed configuration and status")
|
||||
fmt.Printf("Config: reads %s for defaults (current_version, parse/serialize, files, tag/message).\n", cfgPath)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveFlagVerbose(name string, positive, negative *bool, defaultValue bool, defaultSource string) (resolvedFlag, error) {
|
||||
if positive == nil || negative == nil {
|
||||
return resolvedFlag{}, fmt.Errorf("flag pointers must not be nil for %s", name)
|
||||
}
|
||||
if *positive && *negative {
|
||||
return resolvedFlag{}, fmt.Errorf("conflicting flags: both --%s and --no-%s are set", name, name)
|
||||
}
|
||||
if *positive {
|
||||
return resolvedFlag{value: true, source: "--" + name}, nil
|
||||
}
|
||||
if *negative {
|
||||
return resolvedFlag{value: false, source: "--no-" + name}, nil
|
||||
}
|
||||
return resolvedFlag{value: defaultValue, source: defaultSource}, nil
|
||||
}
|
||||
|
||||
func gitHint() string {
|
||||
repo, err := git.PlainOpen(".")
|
||||
if err != nil {
|
||||
return "repo not found"
|
||||
}
|
||||
head, err := repo.Head()
|
||||
if err != nil {
|
||||
return "cannot read HEAD"
|
||||
}
|
||||
branch := head.Name().Short()
|
||||
w, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return fmt.Sprintf("branch %s (worktree error)", branch)
|
||||
}
|
||||
status, err := w.Status()
|
||||
if err != nil {
|
||||
return fmt.Sprintf("branch %s (status error)", branch)
|
||||
}
|
||||
state := "dirty"
|
||||
if status.IsClean() {
|
||||
state = "clean"
|
||||
}
|
||||
remote := "(origin not set)"
|
||||
if r, err := repo.Remote("origin"); err == nil && len(r.Config().URLs) > 0 {
|
||||
remote = r.Config().URLs[0]
|
||||
}
|
||||
return fmt.Sprintf("branch %s, %s, remote=%s", branch, state, remote)
|
||||
}
|
||||
|
||||
@@ -123,30 +123,33 @@ func TestBumpVersion(t *testing.T) {
|
||||
|
||||
func TestNormalizeArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in []string
|
||||
want []string
|
||||
wantErr bool
|
||||
name string
|
||||
in []string
|
||||
wantPart string
|
||||
wantFlagLen int
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "empty", in: nil, want: nil},
|
||||
{name: "flags_only", in: []string{"--tag"}, want: []string{"--tag"}},
|
||||
{name: "positional_major", in: []string{"major"}, want: []string{"-part=major"}},
|
||||
{name: "positional_minor_plus_flags", in: []string{"minor", "--tag"}, want: []string{"-part=minor", "--tag"}},
|
||||
{name: "positional_patch", in: []string{"patch"}, want: []string{"-part=patch"}},
|
||||
{name: "major_only", in: []string{"major"}, wantPart: "major", wantFlagLen: 0},
|
||||
{name: "minor_with_flags", in: []string{"minor", "--tag"}, wantPart: "minor", wantFlagLen: 1},
|
||||
{name: "multiple_parts", in: []string{"major", "patch"}, wantErr: true},
|
||||
{name: "unknown_positional", in: []string{"wat"}, wantErr: true},
|
||||
{name: "missing_part", in: []string{"--tag"}, wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := normalizeArgs(tt.in)
|
||||
part, flags, err := extractPart(tt.in)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("normalizeArgs error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Fatalf("extractPart error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("normalizeArgs = %#v, want %#v", got, tt.want)
|
||||
if part != tt.wantPart {
|
||||
t.Fatalf("extractPart part = %q, want %q", part, tt.wantPart)
|
||||
}
|
||||
if len(flags) != tt.wantFlagLen {
|
||||
t.Fatalf("extractPart flags len = %d, want %d", len(flags), tt.wantFlagLen)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user