package main import ( "fmt" "log" "os" "strings" "time" git "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" ) // getenvDefault возвращает значение из окружения по ключу, или заданное // по умолчанию func getenvDefault(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def } // normalizePath возвращает нормализованный путь func normalizePath(p string) string { p = strings.TrimSpace(p) p = strings.TrimPrefix(p, "./") return p } func pushRefSpec(headRef *plumbing.Reference) (config.RefSpec, error) { if headRef == nil || !headRef.Name().IsBranch() { return "", fmt.Errorf("cannot determine branch to push from HEAD") } branch := headRef.Name().String() return config.RefSpec(branch + ":" + branch), nil } func signatureFromEnv() *object.Signature { return &object.Signature{ Name: getenvDefault("GIT_USERNAME", "bumpversion"), Email: getenvDefault("GIT_EMAIL", "bumpversion@deploy"), When: time.Now().UTC(), } } // gitCommit выполняет коммит с внесёнными изменениями func gitCommit(bc *BumpConfig, newVersion string, configPath string) { // Открываем локальный репозиторий (предполагается, что он существует в папке ".") repo, err := git.PlainOpen(".") if err != nil { log.Fatalf("Repository open error: %v", err) } // получаем рабочее дерево worktree, err := repo.Worktree() if err != nil { log.Fatalf("Work directory open error: %v", err) } // создаём список файлов, которые должны быть включены в коммит targets := make([]string, 0, len(bc.FilePaths)+1) targets = append(targets, bc.FilePaths...) targets = append(targets, configPath) // проверяем на наличие изменений status, err := worktree.Status() if err != nil { log.Fatalf("Status error: %v", err) } changed := false for _, p := range targets { if p == "" { continue } p = normalizePath(p) // пропускаем отсутствующие файлы тихо if _, err := os.Stat(p); err != nil { if os.IsNotExist(err) { log.Printf("Skip missing file: %s", p) continue } log.Fatalf("Stat error for %s: %v", p, err) } st, ok := status[p] if !ok || st == nil { // nothing to stage for this file continue } if st.Worktree != git.Unmodified { if _, err := worktree.Add(p); err != nil { log.Fatalf("Add %s error: %v", p, err) } changed = true continue } } if !changed { log.Printf("No changes detected in configured files; skipping commit.") return } // формируем сообщение коммита commitMsg := strings. NewReplacer( "{current_version}", bc.CurrentVersion, "{new_version}", newVersion, ).Replace(bc.Message) author := signatureFromEnv() hash, err := worktree.Commit(commitMsg, &git.CommitOptions{ Author: author, Committer: author, }) if err != nil { log.Fatalf("Commit error: %v", err) } // Получаем объект коммита (по его хэшу) commitObj, err := repo.CommitObject(hash) if err != nil { log.Fatalf("Commit object error: %v", err) } log.Printf("Committed as %s\n", commitObj.Hash) } // gitTag ставит тэг на текущий коммит func gitTag(bc *BumpConfig, newVersion string) { // Открываем локальный репозиторий (предполагается, что он существует в папке ".") repo, err := git.PlainOpen(".") if err != nil { log.Fatalf("Repository open error: %v", err) } // Получаем текущий HEAD (он должен совпадать с только что созданным коммитом) headRef, err := repo.Head() if err != nil { log.Fatalf("HEAD open error: %v", err) } log.Printf("Current HEAD is %s\n", headRef.Hash()) // Создаем тег на текущем коммите (HEAD) commitMsg := strings.ReplaceAll(bc.Message, "{current_version}", bc.CurrentVersion) commitMsg = strings.ReplaceAll(commitMsg, "{new_version}", newVersion) tagName := strings.ReplaceAll(bc.TagName, "{new_version}", newVersion) _, err = repo.CreateTag(tagName, headRef.Hash(), &git.CreateTagOptions{ Tagger: signatureFromEnv(), Message: commitMsg, }) if err != nil { log.Fatalf("Tag creation error: %v", err) } log.Printf("Tag '%s' is created on commit %s\n", tagName, headRef.Hash()) } func gitPush(bc *BumpConfig, newVersion string) { // Открываем локальный репозиторий (предполагается, что он существует в папке ".") repo, err := git.PlainOpen(".") if err != nil { log.Fatalf("Repository open error: %v", err) } tagName := strings.ReplaceAll(bc.TagName, "{new_version}", newVersion) headRef, err := repo.Head() if err != nil { log.Fatalf("HEAD open error: %v", err) } branchSpec, err := pushRefSpec(headRef) if err != nil { log.Fatalf("Push branch detection error: %v", err) } // (Опционально) Выполняем push на удаленный репозиторий tagSpec := config.RefSpec("refs/tags/" + tagName + ":refs/tags/" + tagName) err = repo.Push(&git.PushOptions{ RemoteName: "origin", RefSpecs: []config.RefSpec{ branchSpec, tagSpec, }, }) if err != nil { log.Fatalf("Push error: %v", err) } log.Println("Changes pushed successfully") }