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
25 changes: 0 additions & 25 deletions cmd/seq.go

This file was deleted.

44 changes: 10 additions & 34 deletions cmd/app.go → internal/cmd/concur.go
Original file line number Diff line number Diff line change
@@ -1,42 +1,31 @@
package cmd

import (
"context"
"fmt"
"sync"
"time"

"github.com/hashmap-kz/relimpact/internal/diffs"
"github.com/hashmap-kz/relimpact/internal/gitutils"
"github.com/hashmap-kz/relimpact/internal/loggr"
)

type ReportFormat string

const (
ReportFormatMarkdown ReportFormat = "markdown"
ReportFormatHTML ReportFormat = "html"
)

func CreateChangelog(repoDir, oldRef, newRef string) string {
return CreateAPIReport(repoDir, oldRef, newRef, ReportFormatMarkdown)
}

func CreateAPIReport(repoDir, oldRef, newRef string, format ReportFormat) string {
func CreateAPIReportConcurrently(ctx context.Context, repoDir, oldRef, newRef string, format ReportFormat) string {
// 1. Concurrent checkout old/new worktrees.
tmpOld, tmpNew := checkout(repoDir, oldRef, newRef)
tmpOld, tmpNew := checkout(ctx, repoDir, oldRef, newRef)
defer gitutils.CleanupWorktree(repoDir, tmpOld)
defer gitutils.CleanupWorktree(repoDir, tmpNew)

// 2. Concurrent API snapshots.
oldAPI, newAPI := snap(tmpOld, tmpNew)
oldAPI, newAPI := snap(ctx, tmpOld, tmpNew)

// 3. Render API-only report.
apiDiff := diffs.DiffAPI(oldAPI, newAPI)
return renderAPIReport(apiDiff, repoDir, oldRef, newRef, format)
}

//nolint:gocritic
func checkout(repoDir, oldRef, newRef string) (string, string) {
func checkout(ctx context.Context, repoDir, oldRef, newRef string) (string, string) {
type worktreeResult struct {
which string
path string
Expand All @@ -51,7 +40,7 @@ func checkout(repoDir, oldRef, newRef string) (string, string) {
worktreeCh <- worktreeResult{"old", "", fmt.Errorf("checkout old failed: %v", r)}
}
}()
path := gitutils.CheckoutWorktree(repoDir, oldRef)
path := gitutils.CheckoutWorktree(ctx, repoDir, oldRef)
worktreeCh <- worktreeResult{"old", path, nil}
}()

Expand All @@ -61,7 +50,7 @@ func checkout(repoDir, oldRef, newRef string) (string, string) {
worktreeCh <- worktreeResult{"new", "", fmt.Errorf("checkout new failed: %v", r)}
}
}()
path := gitutils.CheckoutWorktree(repoDir, newRef)
path := gitutils.CheckoutWorktree(ctx, repoDir, newRef)
worktreeCh <- worktreeResult{"new", path, nil}
}()

Expand All @@ -84,19 +73,19 @@ func checkout(repoDir, oldRef, newRef string) (string, string) {
}

//nolint:gocritic
func snap(tmpOld, tmpNew string) (map[string]diffs.APIPackage, map[string]diffs.APIPackage) {
func snap(ctx context.Context, tmpOld, tmpNew string) (map[string]diffs.APIPackage, map[string]diffs.APIPackage) {
var wgSnapshots sync.WaitGroup
apiOldCh := make(chan map[string]diffs.APIPackage, 1)
apiNewCh := make(chan map[string]diffs.APIPackage, 1)

wgSnapshots.Add(2)
go func() {
defer wgSnapshots.Done()
apiOldCh <- diffs.SnapshotAPI(tmpOld)
apiOldCh <- diffs.SnapshotAPI(ctx, tmpOld)
}()
go func() {
defer wgSnapshots.Done()
apiNewCh <- diffs.SnapshotAPI(tmpNew)
apiNewCh <- diffs.SnapshotAPI(ctx, tmpNew)
}()

wgSnapshots.Wait()
Expand All @@ -108,16 +97,3 @@ func snap(tmpOld, tmpNew string) (map[string]diffs.APIPackage, map[string]diffs.

return oldAPI, newAPI
}

func renderAPIReport(apiDiff *diffs.APIDiff, repoDir, oldRef, newRef string, format ReportFormat) string {
meta := diffs.ReportMetadata{
Repo: repoDir,
OldRef: oldRef,
NewRef: newRef,
Now: time.Now(),
}
if format == ReportFormatHTML {
return apiDiff.HTML(meta)
}
return apiDiff.Markdown(meta)
}
27 changes: 27 additions & 0 deletions internal/cmd/render.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package cmd

import (
"time"

"github.com/hashmap-kz/relimpact/internal/diffs"
)

type ReportFormat string

const (
ReportFormatMarkdown ReportFormat = "markdown"
ReportFormatHTML ReportFormat = "html"
)

func renderAPIReport(apiDiff *diffs.APIDiff, repoDir, oldRef, newRef string, format ReportFormat) string {
meta := diffs.ReportMetadata{
Repo: repoDir,
OldRef: oldRef,
NewRef: newRef,
Now: time.Now(),
}
if format == ReportFormatHTML {
return apiDiff.HTML(meta)
}
return apiDiff.Markdown(meta)
}
23 changes: 23 additions & 0 deletions internal/cmd/seq.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package cmd

import (
"context"

"github.com/hashmap-kz/relimpact/internal/diffs"
"github.com/hashmap-kz/relimpact/internal/gitutils"
)

func CreateAPIReportSequential(ctx context.Context, repoDir, oldRef, newRef string, format ReportFormat) string {
// Checkout old/new worktrees.
tmpOld := gitutils.CheckoutWorktree(ctx, repoDir, oldRef)
defer gitutils.CleanupWorktree(repoDir, tmpOld)

tmpNew := gitutils.CheckoutWorktree(ctx, repoDir, newRef)
defer gitutils.CleanupWorktree(repoDir, tmpNew)

oldAPI := diffs.SnapshotAPI(ctx, tmpOld)
newAPI := diffs.SnapshotAPI(ctx, tmpNew)

apiDiff := diffs.DiffAPI(oldAPI, newAPI)
return renderAPIReport(apiDiff, repoDir, oldRef, newRef, format)
}
20 changes: 11 additions & 9 deletions internal/diffs/api.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package diffs

import (
"context"
"encoding/json"
"fmt"
"go/token"
Expand Down Expand Up @@ -72,10 +73,10 @@ func getCacheDir() string {
return filepath.Join(os.TempDir(), "relimpact-api-cache") // fallback for local runs
}

func SnapshotAPI(dir string) map[string]APIPackage {
func SnapshotAPI(ctx context.Context, dir string) map[string]APIPackage {
// TODO: debuglog

sha := getGitCommitSHA(dir)
sha := getGitCommitSHA(ctx, dir)
cachePath := filepath.Join(getCacheDir(), apiCacheSchemaVersion+"-"+sha+".json")
loggr.Debugf("cache path: %s", cachePath)

Expand All @@ -101,8 +102,9 @@ func SnapshotAPI(dir string) map[string]APIPackage {
// }

cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedTypes | packages.NeedImports,
Dir: dir,
Context: ctx,
Mode: packages.NeedName | packages.NeedTypes | packages.NeedImports,
Dir: dir,
}

// NOTE: this is the most expensive routine in the whole app.
Expand All @@ -116,7 +118,7 @@ func SnapshotAPI(dir string) map[string]APIPackage {

loggr.Debugf("packages load. time=%s, sha=%s", time.Since(loadStart).String(), sha)

modulePath := getModulePath(dir)
modulePath := getModulePath(ctx, dir)
api := make(map[string]APIPackage)

for _, pkg := range pkgs {
Expand Down Expand Up @@ -381,8 +383,8 @@ func diffList(label, path string, oldList, newList []string) (added, removed []D
return added, removed
}

func getModulePath(dir string) string {
cmd := exec.Command("go", "list", "-m")
func getModulePath(ctx context.Context, dir string) string {
cmd := exec.CommandContext(ctx, "go", "list", "-m")
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
Expand All @@ -391,8 +393,8 @@ func getModulePath(dir string) string {
return strings.TrimSpace(string(out))
}

func getGitCommitSHA(dir string) string {
cmd := exec.Command("git", "rev-parse", "HEAD")
func getGitCommitSHA(ctx context.Context, dir string) string {
cmd := exec.CommandContext(ctx, "git", "rev-parse", "HEAD")
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
Expand Down
11 changes: 10 additions & 1 deletion internal/diffs/api_html.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"regexp"
"sort"
"strings"
"sync"
"time"
)

Expand Down Expand Up @@ -1243,8 +1244,14 @@ var importPathRE = regexp.MustCompile(
`([A-Za-z0-9_./~-]+/[A-Za-z0-9_./~-]+)\.([A-Za-z_][A-Za-z0-9_]*)`,
)

var abbreviateCache sync.Map

func abbreviateImportPaths(s string) string {
return importPathRE.ReplaceAllStringFunc(s, func(match string) string {
if v, ok := abbreviateCache.Load(s); ok {
//nolint:errcheck
return v.(string)
}
result := importPathRE.ReplaceAllStringFunc(s, func(match string) string {
idx := strings.LastIndex(match, ".")
if idx < 0 {
return match
Expand All @@ -1256,6 +1263,8 @@ func abbreviateImportPaths(s string) string {
}
return path + "." + ident
})
abbreviateCache.Store(s, result)
return result
}

func cleanAPILabel(fallback, label string) string {
Expand Down
13 changes: 8 additions & 5 deletions internal/gitutils/gitutils.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
package gitutils

import (
"context"
"io"
"log"
"os"
"os/exec"
)

func CheckoutWorktree(repoDir, ref string) string {
func CheckoutWorktree(ctx context.Context, repoDir, ref string) string {
tmpDir, err := os.MkdirTemp("", "apidiff-"+ref)
if err != nil {
log.Fatal(err)
}
runGitInDir(repoDir, "worktree", "add", "--detach", tmpDir, ref)
runGitInDir(ctx, repoDir, "worktree", "add", "--detach", tmpDir, ref)
return tmpDir
}

// CleanupWorktree removes the worktree unconditionally; it uses a fresh
// context so that an expired deadline does not prevent cleanup.
func CleanupWorktree(repoDir, path string) {
runGitInDir(repoDir, "worktree", "remove", "--force", path)
runGitInDir(context.Background(), repoDir, "worktree", "remove", "--force", path)
}

func runGitInDir(dir string, args ...string) {
cmd := exec.Command("git", args...)
func runGitInDir(ctx context.Context, dir string, args ...string) {
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = dir
cmd.Stdout = io.Discard
cmd.Stderr = io.Discard
Expand Down
3 changes: 2 additions & 1 deletion internal/gitutils/gitutils_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gitutils

import (
"context"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -39,7 +40,7 @@ func TestCheckoutAndCleanupWorktree(t *testing.T) {
// git worktree must be run inside repo!
require.NoError(t, os.Chdir(tmpDir))

worktreeDir := CheckoutWorktree(tmpDir, "v1")
worktreeDir := CheckoutWorktree(context.Background(), tmpDir, "v1")

// Verify worktree dir exists and contains file.txt
_, err = os.Stat(filepath.Join(worktreeDir, "file.txt"))
Expand Down
23 changes: 17 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package main

import (
"context"
"flag"
"fmt"
"os"
"strings"
"time"

"github.com/hashmap-kz/relimpact/internal/cmd"

"github.com/hashmap-kz/relimpact/cmd"
"github.com/hashmap-kz/relimpact/internal/loggr"
"github.com/hashmap-kz/relimpact/internal/x/fmtx"
)
Expand All @@ -18,6 +21,7 @@ func main() {
format := flag.String("format", "markdown", "report format: markdown or html")
outputFile := flag.String("output", "", "write report to file instead of stdout")
greedy := flag.Bool("greedy", false, "use maximum concurrency")
timeoutFlag := flag.Duration("timeout", 10*time.Minute, `operation timeout, e.g. "5m", "30s" (default "10m")`)

flag.Usage = usage

Expand All @@ -41,11 +45,14 @@ func main() {
// TODO: make log level configurable via env/CLI.
loggr.Init(loggr.LevelTrace, "relimpact")

ctx, cancel := context.WithTimeout(context.Background(), *timeoutFlag)
defer cancel()

var report string
if *greedy {
report = cmd.CreateAPIReport(*dir, *oldRef, *newRef, reportFormat)
report = cmd.CreateAPIReportConcurrently(ctx, *dir, *oldRef, *newRef, reportFormat)
} else {
report = cmd.CreateAPIReportSequential(*dir, *oldRef, *newRef, reportFormat)
report = cmd.CreateAPIReportSequential(ctx, *dir, *oldRef, *newRef, reportFormat)
}

if *outputFile == "" {
Expand All @@ -70,12 +77,16 @@ Required:
--new string new git ref, tag, branch, or commit

Flags:
--dir string path to git repository (default ".")
--format string report format: markdown or html (default "markdown")
--output string write report to file instead of stdout
--dir string path to git repository (default ".")
--format string report format: markdown or html (default "markdown")
--output string write report to file instead of stdout
--timeout duration overall operation timeout, e.g. "5m", "30s" (default "10m")
--greedy use maximum concurrency
-h, --help show help

Environment:
RELIMPACT_API_CACHE_DIR override the directory used for API snapshot cache

Examples:
relimpact --old v1.0.0 --new HEAD

Expand Down
Loading