Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9354ec3
Moved manifest_diff to bufcasdiff in a new pkg
unmultimedio Mar 30, 2026
3b7c581
Move run logic
unmultimedio Mar 30, 2026
512c38d
Merge output funcs
unmultimedio Mar 30, 2026
ad0753f
Lint
unmultimedio Mar 30, 2026
3c1cc44
Update output tests
unmultimedio Mar 30, 2026
a1cfedf
Call casdiff pkg instead of shelling out
unmultimedio Mar 30, 2026
4989d6b
Merge remote-tracking branch 'origin/main' into jfigueroa/refactor-ca…
unmultimedio Mar 30, 2026
773129d
lint
unmultimedio Mar 30, 2026
b5d85a4
Collapse casdiff individual transition comments.
unmultimedio Mar 30, 2026
3a0fc6c
Summary comment
unmultimedio Apr 8, 2026
b8e8256
Merge remote-tracking branch 'origin/main' into jfigueroa/summary-cas…
unmultimedio Apr 8, 2026
77d70bf
Merge remote-tracking branch 'origin/main' into jfigueroa/summary-cas…
unmultimedio Apr 8, 2026
8e940d5
Remove some states
unmultimedio Apr 8, 2026
9c64388
Merge branch 'jfigueroa/mockbase' into jfigueroa/summary-casdiff-comment
unmultimedio Apr 8, 2026
fe5b153
Revert "Remove some states"
unmultimedio Apr 8, 2026
a8bdf4a
Run GHA
unmultimedio Apr 8, 2026
e8c73c3
state
unmultimedio Apr 8, 2026
4e7974c
Label intermediate and global transitions, escape nested backticks in…
unmultimedio Apr 8, 2026
3e9685f
Nit command
unmultimedio Apr 8, 2026
7758f76
Extra space when in details
unmultimedio Apr 8, 2026
e384b72
Revert GHA changes
unmultimedio Apr 8, 2026
7c553ee
Revert "state"
unmultimedio Apr 8, 2026
ec357df
Consistent naming
unmultimedio Apr 9, 2026
9b6a11e
Merge remote-tracking branch 'origin/main' into jfigueroa/summary-cas…
unmultimedio Apr 9, 2026
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
1 change: 1 addition & 0 deletions .github/workflows/auto-casdiff-comment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
branches:
- main
paths:
- modules/sync/state.json
- modules/sync/*/*/state.json

permissions:
Expand Down
18 changes: 12 additions & 6 deletions cmd/commentprcasdiff/casdiff_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,18 @@ func runCASDiff(ctx context.Context, transition stateTransition) casDiffResult {
return result
}

result.output = fmt.Sprintf(
"```sh\n$ casdiff %s %s --format=markdown\n```\n\n%s",
transition.fromRef,
transition.toRef,
mdiff.String(bufcasdiff.ManifestDiffOutputFormatMarkdown),
)
cmd := fmt.Sprintf("```sh\n$ casdiff %s \\\n %s \\\n --format=markdown\n```", transition.fromRef, transition.toRef)
diffOutput := mdiff.String(bufcasdiff.ManifestDiffOutputFormatMarkdown)
if transition.isOverallTransition {
result.output = "### Overall transition\n\n" + cmd + "\n\n" + diffOutput
} else {
result.output = fmt.Sprintf(
"**Intermediate transition**\n\n%s\n<details><summary>%s</summary>\n<p>\n\n%s\n</p>\n</details>",
cmd,
mdiff.Summary(),
diffOutput,
)
}
return result
}

Expand Down
11 changes: 11 additions & 0 deletions cmd/commentprcasdiff/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,17 @@ func run(ctx context.Context, flags *flags) error {
}
}

overallTransitions, err := getOverallTransitions(ctx, stateRW, baseRef, headRef)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to get overall transitions from global state: %v\n", err)
} else if len(overallTransitions) > 0 {
fmt.Fprintf(os.Stdout, "Found %d overall transition(s) in global state:\n", len(overallTransitions))
for _, t := range overallTransitions {
fmt.Fprintf(os.Stdout, " %s: %s -> %s\n", strings.TrimPrefix(t.modulePath, "modules/sync/"), t.fromRef, t.toRef)
}
allTransitions = append(allTransitions, overallTransitions...)
}

if len(allTransitions) == 0 {
fmt.Fprintf(os.Stdout, "No digest transitions found\n")
return nil
Expand Down
114 changes: 100 additions & 14 deletions cmd/commentprcasdiff/state_analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ import (

// stateTransition represents a digest change in a module's state.json file.
type stateTransition struct {
modulePath string // e.g., "modules/sync/bufbuild/protovalidate"
filePath string // e.g., "modules/sync/bufbuild/protovalidate/state.json"
fromRef string // Last reference with old digest (e.g., "v1.1.0")
toRef string // First reference with new digest (e.g., "v1.2.0")
fromDigest string // Old digest
toDigest string // New digest
lineNumber int // Line in diff where new digest first appears
modulePath string // e.g., "modules/sync/bufbuild/protovalidate"
filePath string // The module where the transition happened, can be an individual module like "modules/sync/bufbuild/protovalidate/state.json" or the global state file "modules/sync/state.json"
fromRef string // Old git reference (e.g., "v1.1.0")
toRef string // New git reference (e.g., "v1.2.0")
fromDigest string // Old digest
toDigest string // New digest
lineNumber int // Line in diff where the new reference or digest appears.
isOverallTransition bool // True for overall transitions on the global state.json file.
}

// getStateFileTransitions reads state.json from base and head branches, compares the JSON arrays to
Expand Down Expand Up @@ -99,13 +100,14 @@ func getStateFileTransitions(
lineNumber = lineNumbers[i]
}
transitions = append(transitions, stateTransition{
modulePath: modulePath,
filePath: filePath,
fromRef: currentRef,
toRef: appendedRef.GetName(),
fromDigest: currentDigest,
toDigest: appendedRef.GetDigest(),
lineNumber: lineNumber,
modulePath: modulePath,
filePath: filePath,
fromRef: currentRef,
toRef: appendedRef.GetName(),
fromDigest: currentDigest,
toDigest: appendedRef.GetDigest(),
lineNumber: lineNumber,
isOverallTransition: false,
})
currentDigest = appendedRef.GetDigest()
}
Expand Down Expand Up @@ -151,6 +153,90 @@ func resolveAppendedRefs(
return baseRefs[len(baseRefs)-1], headRefs[len(baseRefs):]
}

// getOverallTransitions reads modules/sync/state.json from both base and head, compares the
// two, and returns one stateTransition per module whose latest_reference changed. Modules that were
// added or removed between base and head are ignored.
func getOverallTransitions(
ctx context.Context,
stateRW *bufstate.ReadWriter,
baseRef string,
headRef string,
) ([]stateTransition, error) {
const globalStatePath = "modules/sync/state.json"

baseContent, err := readFileAtRef(ctx, globalStatePath, baseRef)
if err != nil {
return nil, fmt.Errorf("read base global state: %w", err)
}
headContent, err := readFileAtRef(ctx, globalStatePath, headRef)
if err != nil {
return nil, fmt.Errorf("read head global state: %w", err)
}

baseGlobalState, err := stateRW.ReadGlobalState(io.NopCloser(bytes.NewReader(baseContent)))
if err != nil {
return nil, fmt.Errorf("parse base global state: %w", err)
}
headGlobalState, err := stateRW.ReadGlobalState(io.NopCloser(bytes.NewReader(headContent)))
if err != nil {
return nil, fmt.Errorf("parse head global state: %w", err)
}

baseLatestRefs := make(map[string]string, len(baseGlobalState.GetModules()))
for _, mod := range baseGlobalState.GetModules() {
baseLatestRefs[mod.GetModuleName()] = mod.GetLatestReference()
}

var transitions []stateTransition
for _, mod := range headGlobalState.GetModules() {
moduleName := mod.GetModuleName()
toRef := mod.GetLatestReference()
fromRef, existsInBase := baseLatestRefs[moduleName]
if !existsInBase || fromRef == toRef {
continue // it is a new module, or the reference did not change.
}
lineNumber, err := findLatestReferenceLineInGlobalState(headContent, moduleName)
if err != nil {
return nil, fmt.Errorf("find line number for %q: %w", moduleName, err)
}
transitions = append(transitions, stateTransition{
modulePath: "modules/sync/" + moduleName,
filePath: globalStatePath,
fromRef: fromRef,
toRef: toRef,
lineNumber: lineNumber,
isOverallTransition: true,
})
}
return transitions, nil
}

// findLatestReferenceLineInGlobalState scans the raw JSON of modules/sync/state.json and returns
// the 1-based line number of the "latest_reference" field for the given module.
func findLatestReferenceLineInGlobalState(content []byte, moduleName string) (int, error) {
quotedName := `"` + moduleName + `"`
scanner := bufio.NewScanner(bytes.NewReader(content))
var (
lineNum int
foundModule bool
)
for scanner.Scan() {
lineNum++
line := scanner.Text()
if !foundModule {
if strings.Contains(line, `"module_name"`) && strings.Contains(line, quotedName) {
foundModule = true
}
} else if strings.Contains(line, `"latest_reference"`) {
return lineNum, nil
}
}
if err := scanner.Err(); err != nil {
return 0, fmt.Errorf("scan global state: %w", err)
}
return 0, fmt.Errorf("latest_reference for module %q not found in global state", moduleName)
}

// readFileAtRef reads a file's content at a specific git ref using git show.
func readFileAtRef(ctx context.Context, filePath string, ref string) ([]byte, error) {
cmd := exec.CommandContext(ctx, "git", "show", fmt.Sprintf("%s:%s", ref, filePath)) //nolint:gosec
Expand Down
37 changes: 27 additions & 10 deletions internal/bufcasdiff/manifest_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"context"
"encoding/hex"
"fmt"
"strings"

"buf.build/go/standard/xslices"
"github.com/bufbuild/buf/private/pkg/cas"
Expand Down Expand Up @@ -139,6 +140,20 @@ func buildManifestDiff(
return diff, nil
}

// Summary returns a manifest diff summary in the shape of:
//
// %d files changed: %d removed, %d renamed, %d added, %d changed content.
func (d *ManifestDiff) Summary() string {
return fmt.Sprintf(
"%d files changed: %d removed, %d renamed, %d added, %d changed content.",
len(d.pathsRemoved)+len(d.pathsRenamed)+len(d.pathsAdded)+len(d.pathsChangedContent),
len(d.pathsRemoved),
len(d.pathsRenamed),
len(d.pathsAdded),
len(d.pathsChangedContent),
)
}

// String returns the diff output in the given format. On invalid or unknown format, this function
// defaults to ManifestDiffOutputFormatText.
func (d *ManifestDiff) String(format ManifestDiffOutputFormat) string {
Expand All @@ -147,15 +162,7 @@ func (d *ManifestDiff) String(format ManifestDiffOutputFormat) string {
if isMarkdown {
b.WriteString("> ")
}
fmt.Fprintf(
&b,
"%d files changed: %d removed, %d renamed, %d added, %d changed content\n",
len(d.pathsRemoved)+len(d.pathsRenamed)+len(d.pathsAdded)+len(d.pathsChangedContent),
len(d.pathsRemoved),
len(d.pathsRenamed),
len(d.pathsAdded),
len(d.pathsChangedContent),
)
b.WriteString(d.Summary() + "\n")
if len(d.pathsRemoved) > 0 {
b.WriteString("\n")
if isMarkdown {
Expand Down Expand Up @@ -219,7 +226,7 @@ func (d *ManifestDiff) String(format ManifestDiffOutputFormat) string {
b.WriteString(fdiff.from.Path() + ":\n")
}
if isMarkdown {
b.WriteString("```diff\n" + fdiff.diff + "\n```\n")
b.WriteString(markdownFencedDiff(fdiff.diff))
} else {
b.WriteString(fdiff.diff + "\n")
}
Expand All @@ -228,6 +235,16 @@ func (d *ManifestDiff) String(format ManifestDiffOutputFormat) string {
return b.String()
}

// markdownFencedDiff wraps content in a ```diff code fence, using a longer fence if the content
// itself contains backtick runs that would break the fence.
func markdownFencedDiff(content string) string {
fence := "```"
for strings.Contains(content, fence) {
fence += "`"
}
return fence + "diff\n" + content + "\n" + fence + "\n"
}

func calculateFileNodeDiff(
ctx context.Context,
from cas.FileNode,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
> 9 files changed: 1 removed, 6 renamed, 1 added, 1 changed content
> 9 files changed: 1 removed, 6 renamed, 1 added, 1 changed content.

# Files removed:

Expand Down
2 changes: 1 addition & 1 deletion internal/bufcasdiff/testdata/manifest_diff/text.golden.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
9 files changed: 1 removed, 6 renamed, 1 added, 1 changed content
9 files changed: 1 removed, 6 renamed, 1 added, 1 changed content.

Files removed:

Expand Down
Loading