1 Commits

Author SHA1 Message Date
k3nny 5d0489dd71 feat(releaser): CHANGELOG auto-update, --init, and --changelog-file flags
ci / vet, staticcheck, test, build (push) Successful in 3m30s
release / Build and publish release (push) Successful in 4m39s
- Automatically write/update CHANGELOG.md on every release, grouped by
  Breaking Changes / Added / Fixed; file is created if missing and the
  new section is committed alongside pom.xml in the release commit
- Add --init flag: scaffolds a default .releaser.yml in the repo root
- Add --changelog-file flag: override the default CHANGELOG.md path
- Add CommitFiles() to gitutil so pom.xml and CHANGELOG.md are staged
  in a single commit
- Fix HTTPS push when no token is set: delegate to the git CLI so that
  system credential helpers, SSH agents, and netrc are honoured

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 11:18:27 +02:00
8 changed files with 419 additions and 32 deletions
+13
View File
@@ -3,6 +3,19 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [1.1.0] - 2026-07-07
### Added
- **CHANGELOG.md auto-update** — every release now writes a new dated section to `CHANGELOG.md` (grouped by Breaking Changes / Added / Fixed), committed alongside `pom.xml` in the release commit; file is created if it does not exist
- **`--changelog-file` flag** — override the default `CHANGELOG.md` path (e.g. `--changelog-file CHANGES.md`)
- **`--init` flag** — scaffolds a fully-commented default `.releaser.yml` in the repository root; errors if the file already exists
- **`CommitFiles`** in `gitutil` — internal helper that stages multiple files before a single commit, used to bundle `pom.xml` + `CHANGELOG.md` in one release commit
### Fixed
- **Push without token** — go-git's HTTPS transport does not use the system credential store; when `GITLAB_TOKEN` is unset the push now delegates to the `git` CLI so credential helpers, SSH agents, and `netrc` all work as expected
## [0.4.2] - 2026-07-07 ## [0.4.2] - 2026-07-07
### Fixed ### Fixed
+12 -3
View File
@@ -1,6 +1,6 @@
# releaser # releaser
![release](https://img.shields.io/badge/release-v0.4.2-blue.svg) ![release](https://img.shields.io/badge/release-v1.1.0-blue.svg)
A CI-friendly release automation tool for GitFlow workflows using Conventional Commits. A CI-friendly release automation tool for GitFlow workflows using Conventional Commits.
@@ -38,16 +38,22 @@ release/1.2 branch
## Usage ## Usage
```bash ```bash
# Scaffold a default .releaser.yml in the current repository
releaser --init
# Simulate next version (no side effects) # Simulate next version (no side effects)
releaser --dry-run releaser --dry-run
# Full release: bump pom.xml, commit, tag, push, GitLab release # Full release: update pom.xml + CHANGELOG.md, commit, tag, push, GitLab release
releaser releaser
# Commit and tag locally — skip push and GitLab release # Commit and tag locally — skip push and GitLab release
releaser --no-push releaser --no-push
# Update pom.xml but stop before committing (review first) # Push commit and tag but skip creating the GitLab release
releaser --no-release
# Update files but stop before committing (review first)
releaser --no-commit releaser --no-commit
# … then commit manually and re-run: # … then commit manually and re-run:
releaser --tag-only releaser --tag-only
@@ -55,6 +61,9 @@ releaser --tag-only
# Explicitly target a branch (useful in detached HEAD CI) # Explicitly target a branch (useful in detached HEAD CI)
releaser --branch release/1.2 releaser --branch release/1.2
# Write changelog to a custom file
releaser --changelog-file CHANGES.md
# Target a specific pom.xml # Target a specific pom.xml
releaser --pom path/to/pom.xml releaser --pom path/to/pom.xml
+5 -3
View File
@@ -55,10 +55,12 @@
- [x] Gitea release workflow (5-platform cross-compilation, release asset upload) - [x] Gitea release workflow (5-platform cross-compilation, release asset upload)
- [x] 96% test coverage with real in-memory git repos and fuzz tests for all parsers - [x] 96% test coverage with real in-memory git repos and fuzz tests for all parsers
## v0.5 — Changelog ## v0.5 — Changelog
- [ ] `CHANGELOG.md` generation / append (grouped by commit type) - [x] `CHANGELOG.md` generation / append (grouped by commit type: Breaking Changes / Added / Fixed)
- [ ] `--changelog-file` flag - [x] `--changelog-file` flag to use a custom filename
- [x] `--init` flag to scaffold a default `.releaser.yml`
- [x] Push falls back to system `git` CLI when no token is set (uses credential helpers, SSH, netrc)
## v1.0 — Production ready ## v1.0 — Production ready
+97 -22
View File
@@ -12,6 +12,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"git.k3nny.fr/releaser/internal/branch" "git.k3nny.fr/releaser/internal/branch"
"git.k3nny.fr/releaser/internal/changelog"
"git.k3nny.fr/releaser/internal/commits" "git.k3nny.fr/releaser/internal/commits"
"git.k3nny.fr/releaser/internal/config" "git.k3nny.fr/releaser/internal/config"
"git.k3nny.fr/releaser/internal/glclient" "git.k3nny.fr/releaser/internal/glclient"
@@ -21,6 +22,45 @@ import (
semver "git.k3nny.fr/releaser/internal/version" semver "git.k3nny.fr/releaser/internal/version"
) )
const defaultConfigTemplate = `# .releaser.yml — configuration for git.k3nny.fr/releaser
# All fields are optional. Uncomment and adjust what you need.
# CLI flags always take precedence over values set here.
git:
# Prefix prepended to every version tag.
# tag_prefix: "v"
# Regex that identifies release branches. Must contain exactly two capture
# groups: group 1 = major version, group 2 = minor version.
# branch_pattern: "^(?:.*/)?release/(\\d+)\\.(\\d+)$"
# Template for the version-bump commit message.
# {version} is replaced with the full tag name (e.g. "v1.2.3").
# commit_message: "chore(release): {version} [skip ci]"
# Override the git commit author. When omitted, releaser reads user.name
# and user.email from the repository's git config.
# author_name: ""
# author_email: ""
maven:
# Path to pom.xml, relative to the repository root.
# pom_path: "pom.xml"
gitlab:
# GitLab instance URL. Falls back to the CI_SERVER_URL environment variable.
# url: "https://gitlab.example.com"
# Personal or CI access token with api scope.
# Falls back to the GITLAB_TOKEN environment variable.
# Tip: never commit a real token here — use the environment variable instead.
# token: ""
# Numeric project ID or "namespace/project" path.
# Falls back to CI_PROJECT_ID, then CI_PROJECT_PATH environment variables.
# project: ""
`
var ( var (
version = "dev" // overridden at build time via -ldflags "-X main.version=..." version = "dev" // overridden at build time via -ldflags "-X main.version=..."
errNothingToRelease = errors.New("nothing to release") errNothingToRelease = errors.New("nothing to release")
@@ -31,6 +71,7 @@ var exitFn = os.Exit
func newRootCmd() *cobra.Command { func newRootCmd() *cobra.Command {
var ( var (
init_ bool
dryRun bool dryRun bool
noPush bool noPush bool
noRelease bool noRelease bool
@@ -39,6 +80,7 @@ func newRootCmd() *cobra.Command {
branchOverride string branchOverride string
repoPath string repoPath string
pomOverride string pomOverride string
changelogFile string
tagPrefixFlag string tagPrefixFlag string
tagPrefixSet bool tagPrefixSet bool
patternFlag string patternFlag string
@@ -54,9 +96,11 @@ func newRootCmd() *cobra.Command {
tagPrefixSet = cmd.Flags().Changed("tag-prefix") tagPrefixSet = cmd.Flags().Changed("tag-prefix")
patternSet = cmd.Flags().Changed("branch-pattern") patternSet = cmd.Flags().Changed("branch-pattern")
return run(options{ return run(options{
init: init_,
repoPath: repoPath, repoPath: repoPath,
branchOverride: branchOverride, branchOverride: branchOverride,
pomOverride: pomOverride, pomOverride: pomOverride,
changelogFile: changelogFile,
tagPrefixFlag: tagPrefixFlag, tagPrefixFlag: tagPrefixFlag,
tagPrefixSet: tagPrefixSet, tagPrefixSet: tagPrefixSet,
patternFlag: patternFlag, patternFlag: patternFlag,
@@ -70,14 +114,16 @@ func newRootCmd() *cobra.Command {
}, },
} }
root.Flags().BoolVar(&init_, "init", false, "create a default .releaser.yml in the repository and exit")
root.Flags().BoolVar(&dryRun, "dry-run", false, "print next version without making changes") root.Flags().BoolVar(&dryRun, "dry-run", false, "print next version without making changes")
root.Flags().BoolVar(&noPush, "no-push", false, "create commit and tag locally without pushing or creating a GitLab release") root.Flags().BoolVar(&noPush, "no-push", false, "create commit and tag locally without pushing or creating a GitLab release")
root.Flags().BoolVar(&noRelease, "no-release", false, "push commit and tag but skip creating the GitLab release") root.Flags().BoolVar(&noRelease, "no-release", false, "push commit and tag but skip creating the GitLab release")
root.Flags().BoolVar(&noCommit, "no-commit", false, "update pom.xml but do not commit, tag, or push") root.Flags().BoolVar(&noCommit, "no-commit", false, "update files but do not commit, tag, or push")
root.Flags().BoolVar(&tagOnly, "tag-only", false, "tag HEAD without updating pom.xml (assumes version was already committed)") root.Flags().BoolVar(&tagOnly, "tag-only", false, "tag HEAD without updating files (assumes version was already committed)")
root.Flags().StringVar(&branchOverride, "branch", "", "override branch name detection (required in detached HEAD)") root.Flags().StringVar(&branchOverride, "branch", "", "override branch name detection (required in detached HEAD)")
root.Flags().StringVar(&repoPath, "repo", ".", "path to git repository") root.Flags().StringVar(&repoPath, "repo", ".", "path to git repository")
root.Flags().StringVar(&pomOverride, "pom", "", "override maven.pom_path from config") root.Flags().StringVar(&pomOverride, "pom", "", "override maven.pom_path from config")
root.Flags().StringVar(&changelogFile, "changelog-file", "CHANGELOG.md", "path to changelog file relative to repo root")
root.Flags().StringVar(&tagPrefixFlag, "tag-prefix", "", "override git.tag_prefix from config") root.Flags().StringVar(&tagPrefixFlag, "tag-prefix", "", "override git.tag_prefix from config")
root.Flags().StringVar(&patternFlag, "branch-pattern", "", "override git.branch_pattern from config") root.Flags().StringVar(&patternFlag, "branch-pattern", "", "override git.branch_pattern from config")
@@ -95,9 +141,11 @@ func main() {
} }
type options struct { type options struct {
init bool
repoPath string repoPath string
branchOverride string branchOverride string
pomOverride string pomOverride string
changelogFile string
tagPrefixFlag string tagPrefixFlag string
tagPrefixSet bool tagPrefixSet bool
patternFlag string patternFlag string
@@ -109,6 +157,18 @@ type options struct {
tagOnly bool tagOnly bool
} }
func initConfig(absRepo string) error {
path := filepath.Join(absRepo, ".releaser.yml")
if _, err := os.Stat(path); err == nil {
return fmt.Errorf(".releaser.yml already exists in %s — delete it first if you want to reset", absRepo)
}
if err := os.WriteFile(path, []byte(defaultConfigTemplate), 0644); err != nil {
return fmt.Errorf("write .releaser.yml: %w", err)
}
fmt.Printf("created %s\n", path)
return nil
}
func run(o options) error { func run(o options) error {
// --- Config --- // --- Config ---
absRepo, err := filepath.Abs(o.repoPath) absRepo, err := filepath.Abs(o.repoPath)
@@ -116,6 +176,10 @@ func run(o options) error {
return fmt.Errorf("resolve repo path: %w", err) return fmt.Errorf("resolve repo path: %w", err)
} }
if o.init {
return initConfig(absRepo)
}
cfg, err := config.Load(absRepo) cfg, err := config.Load(absRepo)
if err != nil { if err != nil {
return err return err
@@ -207,27 +271,41 @@ func run(o options) error {
return nil return nil
} }
// --- pom.xml (skipped with --tag-only or when the file does not exist) --- // --- pom.xml + CHANGELOG.md (skipped with --tag-only) ---
pomPath := filepath.Join(absRepo, cfg.Maven.PomPath) if !o.tagOnly {
_, statErr := os.Stat(pomPath) var filesToCommit []string
hasPom := !errors.Is(statErr, os.ErrNotExist)
if statErr != nil && hasPom {
return fmt.Errorf("check pom path: %w", statErr)
}
if !o.tagOnly && hasPom { // pom.xml
currentPomVersion, err := maven.ReadVersion(pomPath) pomPath := filepath.Join(absRepo, cfg.Maven.PomPath)
if err != nil { _, statErr := os.Stat(pomPath)
return fmt.Errorf("read pom version: %w", err) hasPom := !errors.Is(statErr, os.ErrNotExist)
if statErr != nil && hasPom {
return fmt.Errorf("check pom path: %w", statErr)
}
if hasPom {
currentPomVersion, err := maven.ReadVersion(pomPath)
if err != nil {
return fmt.Errorf("read pom version: %w", err)
}
fmt.Fprintf(os.Stderr, "info: pom.xml: %s → %s\n", currentPomVersion, nextVersion)
if err := maven.WriteVersion(pomPath, currentPomVersion, nextVersion); err != nil {
return fmt.Errorf("update pom version: %w", err)
}
filesToCommit = append(filesToCommit, cfg.Maven.PomPath)
} else {
fmt.Fprintln(os.Stderr, "info: no pom.xml found — skipping version bump")
} }
fmt.Fprintf(os.Stderr, "info: pom.xml: %s → %s\n", currentPomVersion, nextVersion)
if err := maven.WriteVersion(pomPath, currentPomVersion, nextVersion); err != nil { // CHANGELOG.md
return fmt.Errorf("update pom version: %w", err) changelogAbsPath := filepath.Join(absRepo, o.changelogFile)
if err := changelog.Update(changelogAbsPath, nextTag, nextVersion, messages); err != nil {
return fmt.Errorf("update changelog: %w", err)
} }
fmt.Fprintf(os.Stderr, "info: %s updated\n", o.changelogFile)
filesToCommit = append(filesToCommit, o.changelogFile)
if o.noCommit { if o.noCommit {
fmt.Printf("pom.xml updated to %s — commit manually then re-run with --tag-only\n", nextVersion) fmt.Printf("files updated to %s — commit manually then re-run with --tag-only\n", nextVersion)
return nil return nil
} }
@@ -239,14 +317,11 @@ func run(o options) error {
if cfg.Git.AuthorEmail != "" { if cfg.Git.AuthorEmail != "" {
authorEmail = cfg.Git.AuthorEmail authorEmail = cfg.Git.AuthorEmail
} }
commitMsg := strings.ReplaceAll(cfg.Git.CommitMessage, "{version}", nextTag) commitMsg := strings.ReplaceAll(cfg.Git.CommitMessage, "{version}", nextTag)
if _, err := gitutil.CommitFile(repo, cfg.Maven.PomPath, commitMsg, authorName, authorEmail); err != nil { if _, err := gitutil.CommitFiles(repo, filesToCommit, commitMsg, authorName, authorEmail); err != nil {
return fmt.Errorf("commit pom.xml: %w", err) return fmt.Errorf("commit: %w", err)
} }
fmt.Fprintf(os.Stderr, "info: committed: %s\n", commitMsg) fmt.Fprintf(os.Stderr, "info: committed: %s\n", commitMsg)
} else if !o.tagOnly && !hasPom {
fmt.Fprintln(os.Stderr, "info: no pom.xml found — skipping version bump commit")
} }
// --- Git tag --- // --- Git tag ---
+69
View File
@@ -6,6 +6,7 @@ import (
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"time" "time"
@@ -587,3 +588,71 @@ func TestMainError(t *testing.T) {
t.Errorf("expected exit code 1 for general error, got %d", gotCode) t.Errorf("expected exit code 1 for general error, got %d", gotCode)
} }
} }
func TestRunInit(t *testing.T) {
dir := t.TempDir()
err := execCmd(t, "--init", "--repo", dir)
if err != nil {
t.Fatalf("--init: unexpected error: %v", err)
}
data, err := os.ReadFile(filepath.Join(dir, ".releaser.yml"))
if err != nil {
t.Fatal("expected .releaser.yml to be created")
}
if len(data) == 0 {
t.Error("expected non-empty .releaser.yml")
}
}
func TestRunInitAlreadyExists(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, ".releaser.yml"), []byte("existing"), 0644)
err := execCmd(t, "--init", "--repo", dir)
if err == nil {
t.Fatal("expected error when .releaser.yml already exists")
}
}
func TestRunChangelogCreated(t *testing.T) {
_, dir := setupRepo(t)
addFile(t, dir, "x.go", "// feat")
repo, _ := gogit.PlainOpen(dir)
w, _ := repo.Worktree()
w.Add("x.go")
w.Commit("feat: add shiny feature", &gogit.CommitOptions{Author: testSig()})
err := execCmd(t, "--no-push", "--branch", "release/1.2", "--repo", dir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
data, err := os.ReadFile(filepath.Join(dir, "CHANGELOG.md"))
if err != nil {
t.Fatal("expected CHANGELOG.md to be created")
}
s := string(data)
if !strings.Contains(s, "## [1.2.0]") {
t.Error("expected version header in CHANGELOG")
}
if !strings.Contains(s, "add shiny feature") {
t.Error("expected feat subject in CHANGELOG")
}
}
func TestRunChangelogFile(t *testing.T) {
_, dir := setupRepo(t)
addFile(t, dir, "x.go", "// fix")
repo, _ := gogit.PlainOpen(dir)
w, _ := repo.Worktree()
w.Add("x.go")
w.Commit("fix: something", &gogit.CommitOptions{Author: testSig()})
err := execCmd(t, "--no-push", "--branch", "release/1.2", "--repo", dir, "--changelog-file", "CHANGES.md")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, "CHANGES.md")); err != nil {
t.Error("expected CHANGES.md to be created")
}
}
+101
View File
@@ -0,0 +1,101 @@
package changelog
import (
"errors"
"fmt"
"os"
"regexp"
"strings"
"time"
"git.k3nny.fr/releaser/internal/commits"
)
var headerSubjectRe = regexp.MustCompile(`(?i)^\w+(?:\([^)]*\))?!?\s*:\s*(.+)`)
// Update inserts a new release section into the CHANGELOG file at path.
// If the file does not exist it is created with a standard header.
// Only commits with a releasable type (fix, feat, breaking) produce bullets;
// if none are found the file is left untouched.
func Update(path, tag, version string, messages []string) error {
section := buildSection(version, messages)
if section == "" {
return nil
}
existing := ""
data, err := os.ReadFile(path)
if err == nil {
existing = string(data)
} else if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("read %s: %w", path, err)
}
var out string
if existing == "" {
out = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n" +
section + "\n"
} else {
// Insert above the first ## [ heading so newest release is always at top.
if idx := strings.Index(existing, "\n## ["); idx >= 0 {
out = existing[:idx+1] + section + "\n\n" + existing[idx+1:]
} else {
out = strings.TrimRight(existing, "\n") + "\n\n" + section + "\n"
}
}
return os.WriteFile(path, []byte(out), 0644)
}
func buildSection(version string, messages []string) string {
var breaking, feats, fixes []string
for _, msg := range messages {
t := commits.Parse(msg)
if t == commits.TypeNone {
continue
}
first := strings.SplitN(strings.TrimSpace(msg), "\n", 2)[0]
subject := extractSubject(first)
switch t {
case commits.TypeBreaking:
breaking = append(breaking, subject)
case commits.TypeFeat:
feats = append(feats, subject)
case commits.TypeFix:
fixes = append(fixes, subject)
}
}
if len(breaking)+len(feats)+len(fixes) == 0 {
return ""
}
date := time.Now().Format("2006-01-02")
var sb strings.Builder
fmt.Fprintf(&sb, "## [%s] - %s\n", version, date)
writeSection(&sb, "Breaking Changes", breaking)
writeSection(&sb, "Added", feats)
writeSection(&sb, "Fixed", fixes)
return strings.TrimRight(sb.String(), "\n")
}
func writeSection(sb *strings.Builder, title string, items []string) {
if len(items) == 0 {
return
}
fmt.Fprintf(sb, "\n### %s\n", title)
for _, item := range items {
fmt.Fprintf(sb, "- %s\n", item)
}
}
func extractSubject(header string) string {
m := headerSubjectRe.FindStringSubmatch(header)
if m != nil {
return strings.TrimSpace(m[1])
}
return strings.TrimSpace(header)
}
+111
View File
@@ -0,0 +1,111 @@
package changelog
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestUpdateNewFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "CHANGELOG.md")
if err := Update(path, "v1.2.0", "1.2.0", []string{
"feat: add widget",
"fix: off-by-one in parser",
}); err != nil {
t.Fatal(err)
}
data, _ := os.ReadFile(path)
s := string(data)
if !strings.Contains(s, "## [1.2.0]") {
t.Error("expected version header")
}
if !strings.Contains(s, "### Added") {
t.Error("expected Added section")
}
if !strings.Contains(s, "### Fixed") {
t.Error("expected Fixed section")
}
if !strings.Contains(s, "add widget") {
t.Error("expected feat subject")
}
if !strings.Contains(s, "off-by-one in parser") {
t.Error("expected fix subject")
}
}
func TestUpdateExistingFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "CHANGELOG.md")
// Seed with an older release.
os.WriteFile(path, []byte("# Changelog\n\n## [1.1.0] - 2026-01-01\n\n### Added\n- old stuff\n"), 0644)
if err := Update(path, "v1.2.0", "1.2.0", []string{"feat: new thing"}); err != nil {
t.Fatal(err)
}
data, _ := os.ReadFile(path)
s := string(data)
newIdx := strings.Index(s, "## [1.2.0]")
oldIdx := strings.Index(s, "## [1.1.0]")
if newIdx < 0 || oldIdx < 0 {
t.Fatal("both versions should appear in CHANGELOG")
}
if newIdx > oldIdx {
t.Error("new version should appear before old version")
}
}
func TestUpdateNoReleasableCommits(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "CHANGELOG.md")
if err := Update(path, "v1.0.1", "1.0.1", []string{
"chore: update deps",
"docs: fix typo",
}); err != nil {
t.Fatal(err)
}
// File should NOT have been created.
if _, err := os.Stat(path); err == nil {
t.Error("file should not be created when there are no releasable commits")
}
}
func TestUpdateBreakingSection(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "CHANGELOG.md")
if err := Update(path, "v2.0.0", "2.0.0", []string{
"feat!: redesign API",
"fix(core): nil panic",
}); err != nil {
t.Fatal(err)
}
data, _ := os.ReadFile(path)
s := string(data)
if !strings.Contains(s, "### Breaking Changes") {
t.Error("expected Breaking Changes section")
}
if !strings.Contains(s, "redesign API") {
t.Error("expected breaking subject")
}
}
func TestUpdateReadError(t *testing.T) {
dir := t.TempDir()
// Create a directory where the file should be — ReadFile will error.
os.Mkdir(filepath.Join(dir, "CHANGELOG.md"), 0755)
err := Update(filepath.Join(dir, "CHANGELOG.md"), "v1.0.0", "1.0.0", []string{"fix: something"})
if err == nil {
t.Error("expected error when path is a directory")
}
}
+11 -4
View File
@@ -196,15 +196,17 @@ func AuthorFromConfig(repo *gogit.Repository) (name, email string) {
return return
} }
// CommitFile stages filePath (relative to worktree root) and creates a commit. // CommitFiles stages all filePaths (relative to worktree root) and creates a commit.
func CommitFile(repo *gogit.Repository, filePath, message, authorName, authorEmail string) (plumbing.Hash, error) { func CommitFiles(repo *gogit.Repository, filePaths []string, message, authorName, authorEmail string) (plumbing.Hash, error) {
w, err := repo.Worktree() w, err := repo.Worktree()
if err != nil { if err != nil {
return plumbing.ZeroHash, err return plumbing.ZeroHash, err
} }
if _, err := w.Add(filePath); err != nil { for _, p := range filePaths {
return plumbing.ZeroHash, fmt.Errorf("git add %s: %w", filePath, err) if _, err := w.Add(p); err != nil {
return plumbing.ZeroHash, fmt.Errorf("git add %s: %w", p, err)
}
} }
hash, err := w.Commit(message, &gogit.CommitOptions{ hash, err := w.Commit(message, &gogit.CommitOptions{
@@ -220,6 +222,11 @@ func CommitFile(repo *gogit.Repository, filePath, message, authorName, authorEma
return hash, nil return hash, nil
} }
// CommitFile stages a single file and creates a commit.
func CommitFile(repo *gogit.Repository, filePath, message, authorName, authorEmail string) (plumbing.Hash, error) {
return CommitFiles(repo, []string{filePath}, message, authorName, authorEmail)
}
// CreateTag creates a lightweight tag on HEAD. // CreateTag creates a lightweight tag on HEAD.
func CreateTag(repo *gogit.Repository, tagName string) error { func CreateTag(repo *gogit.Repository, tagName string) error {
head, err := repo.Head() head, err := repo.Head()