Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 52 additions & 47 deletions pkg/documentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"go/token"
"io/fs"
"os"

"path/filepath"
"regexp"
"strings"
Expand Down Expand Up @@ -44,59 +43,65 @@ func rewriteDocumentationLine(text, oldMod, newMod string, excludedModules ...st
if oldMod == "" || oldMod == newMod {
return text
}
_, oldMajor, _ := module.SplitPathVersion(oldMod)
return documentationToken.ReplaceAllStringFunc(text, func(reference string) string {
core := strings.TrimRight(reference, ".,;:!)]}*|")
punctuation := reference[len(core):]
prefix, target := "", core
badge := false
for _, host := range []string{"https://pkg.go.dev/badge/", "http://pkg.go.dev/badge/", "https://pkg.go.dev/", "http://pkg.go.dev/", "https://godoc.org/", "http://godoc.org/"} {
if strings.HasPrefix(target, host) {
prefix, target = host, strings.TrimPrefix(target, host)
badge = strings.Contains(host, "/badge/")
break
}
}
// Anchors and query parameters belong to the URL, not the module path.
path, suffix := target, ""
if i := strings.IndexAny(path, "?#"); i >= 0 {
path, suffix = target[:i], target[i:]
}
modulePath := strings.SplitN(path, "@", 2)[0]
if badge {
modulePath = strings.TrimSuffix(modulePath, ".svg")
}
if excludedModuleReference(modulePath, excludedModules) {
return reference
}
if !strings.HasPrefix(path, oldMod) {
return reference
}
if i := strings.IndexByte(path, '@'); i >= 0 {
version := strings.SplitN(path[i+1:], "/", 2)[0]
if badge {
version = strings.TrimSuffix(version, ".svg")
}
if version != "latest" {
return reference
}
}
rest := strings.TrimPrefix(path, oldMod)
if badge && rest == ".svg" {
return prefix + newMod + rest + suffix + punctuation
}
if rest != "" && !strings.HasPrefix(rest, "/") && !strings.HasPrefix(rest, "@latest") {
return reference
return rewriteDocumentationReference(reference, oldMod, newMod, excludedModules...)
})
}

// rewriteDocumentationReference updates one token while preserving URL decoration,
// pinned releases, other major versions, and references to independent modules.
func rewriteDocumentationReference(reference, oldMod, newMod string, excludedModules ...string) string {
_, oldMajor, _ := module.SplitPathVersion(oldMod)
core := strings.TrimRight(reference, ".,;:!)]}*|")
punctuation := reference[len(core):]
prefix, target := "", core
badge := false
for _, host := range []string{"https://pkg.go.dev/badge/", "http://pkg.go.dev/badge/", "https://pkg.go.dev/", "http://pkg.go.dev/", "https://godoc.org/", "http://godoc.org/"} {
if strings.HasPrefix(target, host) {
prefix, target = host, strings.TrimPrefix(target, host)
badge = strings.Contains(host, "/badge/")
break
}
majorPath := strings.SplitN(rest, "@", 2)[0]
}
// Anchors and query parameters belong to the URL, not the module path.
path, suffix := target, ""
if i := strings.IndexAny(path, "?#"); i >= 0 {
path, suffix = target[:i], target[i:]
}
modulePath := strings.SplitN(path, "@", 2)[0]
if badge {
modulePath = strings.TrimSuffix(modulePath, ".svg")
}
if excludedModuleReference(modulePath, excludedModules) {
return reference
}
if !strings.HasPrefix(path, oldMod) {
return reference
}
if i := strings.IndexByte(path, '@'); i >= 0 {
version := strings.SplitN(path[i+1:], "/", 2)[0]
if badge {
majorPath = strings.TrimSuffix(majorPath, ".svg")
version = strings.TrimSuffix(version, ".svg")
}
if oldMajor == "" && documentationMajor.MatchString(majorPath) {
if version != "latest" {
return reference
}
}
rest := strings.TrimPrefix(path, oldMod)
if badge && rest == ".svg" {
return prefix + newMod + rest + suffix + punctuation
})
}
if rest != "" && !strings.HasPrefix(rest, "/") && !strings.HasPrefix(rest, "@latest") {
return reference
}
majorPath := strings.SplitN(rest, "@", 2)[0]
if badge {
majorPath = strings.TrimSuffix(majorPath, ".svg")
}
if oldMajor == "" && documentationMajor.MatchString(majorPath) {
return reference
}
return prefix + newMod + rest + suffix + punctuation
}

// rewriteGoDocumentation edits comment byte ranges only, leaving literals and
Expand Down
78 changes: 78 additions & 0 deletions pkg/git_commit_workdir_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package goversion

import (
"os"
"path/filepath"
"slices"
"strings"
"testing"
)

// TestRunWithOptionsTracksIgnoredFilesOutsideWorkDir covers repository-wide tracked
// discovery and keeps automatic update reporting separate from extra-only staging.
func TestRunWithOptionsTracksIgnoredFilesOutsideWorkDir(t *testing.T) {
for _, bump := range []bool{false, true} {
name := "extra only"
if bump {
name = "bump and extra"
}
t.Run(name, func(t *testing.T) {
root, workDir := initNestedModuleRepo(t, "example.com/acme/repo/tools/widget", "1.2.3")
if err := os.Mkdir(filepath.Join(root, "release"), 0o755); err != nil {
t.Fatal(err)
}
tracked := filepath.Join(root, "release", "version.txt")
for path, content := range map[string]string{
tracked: "1.2.3\n",
filepath.Join(root, ".gitignore"): "/release/\n/build.txt\n",
} {
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
gitRun(t, root, "add", ".gitignore")
gitRun(t, root, "add", "-f", "release/version.txt")
gitRun(t, root, "commit", "-m", "add tracked ignored release file")
if err := os.WriteFile(filepath.Join(root, "build.txt"), []byte("untracked output\n"), 0o644); err != nil {
t.Fatal(err)
}

options := VersionOptions{
WorkDir: workDir,
ExtraFiles: []string{"../../release/version.txt", "../../release/version.txt", "version.go"},
}
wantUpdated := []string{filepath.Join(workDir, "go.mod"), filepath.Join(workDir, "version.go")}
wantRelease := "extra-only change"
if bump {
options.BumpFiles = []string{"../../release/version.txt", "../../release/version.txt"}
wantUpdated = append(wantUpdated, tracked)
wantRelease = "2.0.0"
} else if err := os.WriteFile(tracked, []byte(wantRelease+"\n"), 0o644); err != nil {
t.Fatal(err)
}

meta, err := RunWithOptions(options, "major")
if err != nil {
t.Fatal(err)
}
if !slices.Equal(meta.UpdatedFiles, wantUpdated) {
t.Fatalf("UpdatedFiles = %v; want %v", meta.UpdatedFiles, wantUpdated)
}
if got := strings.TrimSpace(gitOutput(t, root, "show", "HEAD:release/version.txt")); got != wantRelease {
t.Fatalf("committed release file = %q; want %q", got, wantRelease)
}
if got := strings.Fields(gitOutput(t, root, "diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD")); !slices.Equal(got, []string{"release/version.txt", "tools/widget/go.mod", "tools/widget/version.go"}) {
t.Fatalf("unexpected committed files: %v", got)
}
if got := gitOutput(t, root, "ls-files", "--", "build.txt"); got != "" {
t.Fatalf("ignored untracked output was staged: %q", got)
}
if got := gitOutput(t, root, "status", "--porcelain"); got != "" {
t.Fatalf("working tree is dirty after bump: %s", got)
}
if got := strings.TrimSpace(gitOutput(t, root, "rev-parse", meta.Tag)); got != strings.TrimSpace(gitOutput(t, root, "rev-parse", "HEAD")) {
t.Fatalf("tag %q does not point to the bump commit", meta.Tag)
}
})
}
}
25 changes: 10 additions & 15 deletions pkg/goversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ func gitCommit(workDir, newVersion, tag string, extraFiles []string) error {
return err
}
list := exec.Command("git", "ls-files", "--cached", "--full-name", "-z")
list.Dir = workDir
list.Dir = root
out, err := list.Output()
if err != nil {
return fmt.Errorf("list tracked files: %w", err)
Expand Down Expand Up @@ -596,7 +596,7 @@ func runWithOptions(options VersionOptions, versionArg string, confinePaths bool
if err != nil {
return meta, err
}
rewritten = uniquePaths(append(rewritten, docs...))
rewritten = append(rewritten, docs...)
}
}

Expand All @@ -619,24 +619,19 @@ func runWithOptions(options VersionOptions, versionArg string, confinePaths bool
}

// 7. Stage, commit, and tag
filesToCommit := make([]string, len(extraFiles))
copy(filesToCommit, extraFiles)
filesToCommit = append(filesToCommit, versionFilePath)
var changedFiles []string
if meta.BumpType == "major" && hasGoMod {
filesToCommit = append(filesToCommit, filepath.Join(modDir, "go.mod"))
changedFiles = append(changedFiles, filepath.Join(modDir, "go.mod"))
}
filesToCommit = append(filesToCommit, rewritten...)
filesToCommit = append(filesToCommit, bumpedFiles...)
if err := gitCommit(workDir, meta.NewVersion, meta.Tag, uniquePaths(filesToCommit)); err != nil {
changedFiles = append(changedFiles, versionFilePath)
changedFiles = append(changedFiles, rewritten...)
changedFiles = uniquePaths(append(changedFiles, bumpedFiles...))
filesToCommit := uniquePaths(append(append([]string{}, extraFiles...), changedFiles...))
if err := gitCommit(workDir, meta.NewVersion, meta.Tag, filesToCommit); err != nil {
return meta, err
}

meta.UpdatedFiles = append([]string{versionFilePath}, rewritten...)
meta.UpdatedFiles = append(meta.UpdatedFiles, bumpedFiles...)
if meta.BumpType == "major" && hasGoMod {
meta.UpdatedFiles = append([]string{filepath.Join(modDir, "go.mod")}, meta.UpdatedFiles...)
}
meta.UpdatedFiles = uniquePaths(meta.UpdatedFiles)
meta.UpdatedFiles = changedFiles

return meta, nil
}
Expand Down
5 changes: 0 additions & 5 deletions version_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"flag"
"fmt"
"io"
"slices"
"strings"

goversion "github.com/bcomnes/goversion/v2/pkg"
Expand Down Expand Up @@ -68,10 +67,6 @@ func runVersionCommand(arguments []string, output, errorOutput io.Writer) int {
return 1
}

if !slices.Contains(extraFiles, *versionFile) {
extraFiles = append(extraFiles, *versionFile)
}

var meta goversion.VersionMeta
var err error
options := goversion.VersionOptions{
Expand Down
16 changes: 11 additions & 5 deletions version_skip_docs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"testing"
)
Expand Down Expand Up @@ -44,6 +45,7 @@ func TestVersionSkipDocsWorkdir(t *testing.T) {
{"skipDocsDry", true, true},
{"skipDocsReal", true, false},
{"defaultUpdatesDocs", false, false},
{"defaultUpdatesDocsDry", false, true},
} {
t.Run(tc.name, func(t *testing.T) {
dir, before := newVersionSkipDocsFixture(t)
Expand Down Expand Up @@ -73,16 +75,20 @@ func TestVersionSkipDocsWorkdir(t *testing.T) {
if !found {
t.Fatalf("output missing %q:\n%s", heading, out)
}
listed := make(map[string]bool)
var listed, expected []string
for _, line := range strings.Split(strings.TrimSpace(files), "\n") {
listed[filepath.Base(strings.TrimSpace(line))] = true
listed = append(listed, strings.TrimSpace(line))
}
for _, name := range []string{"version.go", "go.mod", "consumer.go", "README.md", "doc.go"} {
want := !tc.skipDocs || (name != "README.md" && name != "doc.go")
if listed[name] != want {
t.Errorf("updated file list contains %s = %v, want %v:\n%s", name, listed[name], want, out)
if !tc.skipDocs || (name != "README.md" && name != "doc.go") {
expected = append(expected, filepath.Join(dir, name))
}
}
slices.Sort(listed)
slices.Sort(expected)
if !slices.Equal(listed, expected) {
t.Errorf("updated file list:\ngot: %q\nwant: %q\n%s", listed, expected, out)
}
for name, original := range before {
want := original
if !tc.dry {
Expand Down
Loading