This commit is contained in:
277
src/main.go
277
src/main.go
@@ -1,14 +1,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
git "github.com/go-git/go-git/v5"
|
||||||
"github.com/go-ini/ini"
|
"github.com/go-ini/ini"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,6 +26,18 @@ type BumpConfig struct {
|
|||||||
FilePaths []string
|
FilePaths []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type resolvedFlag struct {
|
||||||
|
value bool
|
||||||
|
source string
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileStatus struct {
|
||||||
|
path string
|
||||||
|
exists bool
|
||||||
|
contains bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
// getBumpConfig загружает конфигурацию из указанного файла
|
// getBumpConfig загружает конфигурацию из указанного файла
|
||||||
func getBumpConfig(cfg_name string) (*BumpConfig, error) {
|
func getBumpConfig(cfg_name string) (*BumpConfig, error) {
|
||||||
cfg, err := ini.Load(cfg_name)
|
cfg, err := ini.Load(cfg_name)
|
||||||
@@ -186,25 +201,55 @@ func versionString() string {
|
|||||||
return fmt.Sprintf("%s (%s)", AppVersion, AppCommit)
|
return fmt.Sprintf("%s (%s)", AppVersion, AppCommit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeArgs(args []string) ([]string, error) {
|
func extractPart(args []string) (string, []string, error) {
|
||||||
if len(args) == 0 {
|
var part string
|
||||||
return args, nil
|
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, "-") {
|
if part == "" {
|
||||||
return args, nil
|
return "", nil, fmt.Errorf("version part argument required (major|minor|patch)")
|
||||||
}
|
}
|
||||||
switch strings.ToLower(first) {
|
|
||||||
case "major", "minor", "patch":
|
return part, flagArgs, nil
|
||||||
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)
|
func hasHelpFlag(args []string) bool {
|
||||||
|
for _, arg := range args {
|
||||||
|
switch arg {
|
||||||
|
case "-h", "--help", "-help":
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
const cfg_name = ".bumpversion.cfg"
|
const cfg_name = ".bumpversion.cfg"
|
||||||
args := os.Args[1:]
|
args := os.Args[1:]
|
||||||
|
|
||||||
|
flag.Usage = usage(cfg_name)
|
||||||
|
|
||||||
// Проверяем аргументы командной строки
|
// Проверяем аргументы командной строки
|
||||||
if len(args) > 0 && strings.ToLower(args[0]) == "--version" {
|
if len(args) > 0 && strings.ToLower(args[0]) == "--version" {
|
||||||
fmt.Printf("%s version %s\n", AppName, versionString())
|
fmt.Printf("%s version %s\n", AppName, versionString())
|
||||||
@@ -214,26 +259,33 @@ func main() {
|
|||||||
// Печатаем название и версию при старте
|
// Печатаем название и версию при старте
|
||||||
fmt.Printf("Starting %s version %s\n", AppName, versionString())
|
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)
|
bc, err := getBumpConfig(cfg_name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error reading bumpversion configuration: %v", err)
|
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")
|
commit := flag.Bool("commit", false, "Create a commit")
|
||||||
noCommit := flag.Bool("no-commit", false, "Do not create a commit")
|
noCommit := flag.Bool("no-commit", false, "Do not create a commit")
|
||||||
tag := flag.Bool("tag", false, "Add a git tag")
|
tag := flag.Bool("tag", false, "Add a git tag")
|
||||||
noTag := flag.Bool("no-tag", false, "Do not 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")
|
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")
|
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)
|
log.Fatalf("Error parsing flags: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,22 +295,21 @@ func main() {
|
|||||||
os.Exit(2)
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("Error resolving commit flags: %v", err)
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("Error resolving tag flags: %v", err)
|
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)
|
newVersion, err := bumpVersion(bc, partValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error bumping version: %v", err)
|
log.Fatalf("Error bumping version: %v", err)
|
||||||
@@ -266,6 +317,15 @@ func main() {
|
|||||||
fmt.Printf("Current version: %s\n", bc.CurrentVersion)
|
fmt.Printf("Current version: %s\n", bc.CurrentVersion)
|
||||||
fmt.Printf("New version: %s\n", newVersion)
|
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 {
|
if err := updateFiles(bc.FilePaths, bc.CurrentVersion, newVersion, !*noFatal); err != nil {
|
||||||
log.Fatalf("Error updating files: %v", err)
|
log.Fatalf("Error updating files: %v", err)
|
||||||
@@ -279,12 +339,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Выполняем git commit и tag, если требуется
|
// Выполняем git commit и tag, если требуется
|
||||||
if shouldCommit {
|
if shouldCommit.value {
|
||||||
gitCommit(bc, newVersion, cfg_name)
|
gitCommit(bc, newVersion, cfg_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Выполняем git commit и tag, если требуется
|
// Выполняем git commit и tag, если требуется
|
||||||
if shouldTag {
|
if shouldTag.value {
|
||||||
gitTag(bc, newVersion)
|
gitTag(bc, newVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,3 +352,162 @@ func main() {
|
|||||||
gitPush(bc, newVersion)
|
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) {
|
func TestNormalizeArgs(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
in []string
|
in []string
|
||||||
want []string
|
wantPart string
|
||||||
wantErr bool
|
wantFlagLen int
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{name: "empty", in: nil, want: nil},
|
{name: "major_only", in: []string{"major"}, wantPart: "major", wantFlagLen: 0},
|
||||||
{name: "flags_only", in: []string{"--tag"}, want: []string{"--tag"}},
|
{name: "minor_with_flags", in: []string{"minor", "--tag"}, wantPart: "minor", wantFlagLen: 1},
|
||||||
{name: "positional_major", in: []string{"major"}, want: []string{"-part=major"}},
|
{name: "multiple_parts", in: []string{"major", "patch"}, wantErr: true},
|
||||||
{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: "unknown_positional", in: []string{"wat"}, wantErr: true},
|
{name: "unknown_positional", in: []string{"wat"}, wantErr: true},
|
||||||
|
{name: "missing_part", in: []string{"--tag"}, wantErr: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := normalizeArgs(tt.in)
|
part, flags, err := extractPart(tt.in)
|
||||||
if (err != nil) != tt.wantErr {
|
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
if part != tt.wantPart {
|
||||||
t.Fatalf("normalizeArgs = %#v, want %#v", got, tt.want)
|
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