-
Notifications
You must be signed in to change notification settings - Fork 0
dot notation #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
dot notation #5
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,11 +12,13 @@ import ( | |
|
|
||
| // writeOutput serialises result in the requested format and writes it to | ||
| // outputPath (or stdout if outputPath is empty). | ||
| func writeOutput(result *types.Result, format, outputPath string) { | ||
| func writeOutput(result *types.Result, graph *types.CallGraph, format, outputPath string) { | ||
| var output []byte | ||
| switch format { | ||
| case "text": | ||
| output = []byte(formatText(result)) | ||
| case "dot": | ||
| output = []byte(formatDot(result, graph)) | ||
| default: | ||
| var marshalErr error | ||
| output, marshalErr = json.MarshalIndent(result, "", " ") | ||
|
|
@@ -67,3 +69,139 @@ func formatText(result *types.Result) string { | |
| } | ||
| return sb.String() | ||
| } | ||
|
|
||
| // formatDot renders the impact as a Graphviz DOT digraph. | ||
| // | ||
| // Layout: | ||
| // - One cluster (subgraph) per service that needs to be rebuilt. | ||
| // - Nodes are short function names; changed functions are filled red, | ||
| // transitively-affected functions are filled orange. | ||
| // - Edges represent caller → callee relationships drawn from the call graph, | ||
| // restricted to nodes that appear in the impact set. | ||
|
Comment on lines
+73
to
+80
|
||
| func formatDot(result *types.Result, graph *types.CallGraph) string { | ||
| // Index changed functions for quick lookup. | ||
| changed := make(map[string]bool, len(result.Changes)) | ||
| for _, c := range result.Changes { | ||
| changed[c.Function] = true | ||
| } | ||
|
|
||
| // Collect all affected functions across all owners. | ||
| affected := make(map[string]bool) | ||
| for _, fns := range result.Impact.AffectedFunctions { | ||
| for _, fn := range fns { | ||
| affected[fn] = true | ||
| } | ||
| } | ||
| // Changed functions are implicitly affected too. | ||
| for fn := range changed { | ||
| affected[fn] = true | ||
| } | ||
|
|
||
| // Build a set of services to rebuild for quick lookup. | ||
| rebuiltServices := make(map[string]bool, len(result.Impact.ServicesToBuild)) | ||
| for _, s := range result.Impact.ServicesToBuild { | ||
| rebuiltServices[s] = true | ||
| } | ||
|
|
||
| // dotID converts a fully-qualified function name to a safe DOT node ID. | ||
| dotID := func(fn string) string { | ||
| r := strings.NewReplacer(".", "_", "/", "_", "-", "_", "(", "_", ")", "_") | ||
| return r.Replace(fn) | ||
| } | ||
|
|
||
| // shortLabel strips the module prefix for readability. | ||
| shortLabel := func(fn string) string { | ||
| // Keep only "package.Func" — last two dot-separated segments. | ||
| parts := strings.Split(fn, "/") | ||
| if len(parts) == 0 { | ||
| return fn | ||
| } | ||
| last := parts[len(parts)-1] | ||
| return last | ||
|
Comment on lines
+112
to
+120
|
||
| } | ||
|
|
||
| sb := &strings.Builder{} | ||
| fmt.Fprintln(sb, "digraph buildgraph {") | ||
| fmt.Fprintln(sb, ` rankdir=LR;`) | ||
| fmt.Fprintln(sb, ` node [fontname="Helvetica", fontsize=11, style=filled, fillcolor=white];`) | ||
| fmt.Fprintln(sb, ` edge [fontsize=9];`) | ||
| fmt.Fprintln(sb) | ||
|
|
||
| // Emit one cluster per owner that has affected functions. | ||
| // Sort owners for deterministic output. | ||
| owners := make([]string, 0, len(result.Impact.AffectedFunctions)) | ||
| for owner := range result.Impact.AffectedFunctions { | ||
| owners = append(owners, owner) | ||
| } | ||
| sort.Strings(owners) | ||
|
|
||
| clusterIdx := 0 | ||
| for _, owner := range owners { | ||
| fns := result.Impact.AffectedFunctions[owner] | ||
| if len(fns) == 0 { | ||
| continue | ||
| } | ||
|
|
||
| // Determine cluster label — append "(rebuild)" for services being rebuilt. | ||
| label := owner | ||
| // Extract the last path segment as the service short name. | ||
| parts := strings.Split(owner, "/") | ||
| shortOwner := parts[len(parts)-1] | ||
| if rebuiltServices[shortOwner] { | ||
| label = owner + " [rebuild]" | ||
| } | ||
|
|
||
| fmt.Fprintf(sb, " subgraph cluster_%d {\n", clusterIdx) | ||
| fmt.Fprintf(sb, " label=%q;\n", label) | ||
| fmt.Fprintln(sb, ` style=rounded;`) | ||
| if rebuiltServices[shortOwner] { | ||
| fmt.Fprintln(sb, ` color=red;`) | ||
| } else { | ||
| fmt.Fprintln(sb, ` color=orange;`) | ||
| } | ||
| fmt.Fprintln(sb) | ||
|
|
||
| seen := make(map[string]bool) | ||
| for _, fn := range fns { | ||
| if seen[fn] { | ||
| continue | ||
| } | ||
| seen[fn] = true | ||
| id := dotID(fn) | ||
| lbl := shortLabel(fn) | ||
| if changed[fn] { | ||
| fmt.Fprintf(sb, " %s [label=%q, fillcolor=\"#ff6b6b\", fontcolor=white];\n", id, lbl) | ||
| } else { | ||
| fmt.Fprintf(sb, " %s [label=%q, fillcolor=\"#ffd580\"];\n", id, lbl) | ||
| } | ||
| } | ||
|
Comment on lines
+164
to
+177
|
||
| fmt.Fprintln(sb, " }") | ||
| fmt.Fprintln(sb) | ||
| clusterIdx++ | ||
| } | ||
|
|
||
| // Emit edges: for every affected function, draw edges to its callees | ||
| // that are also in the affected set, using the call graph. | ||
| fmt.Fprintln(sb, " // edges") | ||
| edgesSeen := make(map[string]bool) | ||
| for fn := range affected { | ||
| node, ok := graph.Nodes[fn] | ||
| if !ok { | ||
| continue | ||
| } | ||
| for _, dep := range node.Deps { | ||
| if !affected[dep.FullName] { | ||
| continue | ||
| } | ||
| key := dotID(fn) + "->" + dotID(dep.FullName) | ||
| if edgesSeen[key] { | ||
| continue | ||
| } | ||
| edgesSeen[key] = true | ||
| fmt.Fprintf(sb, " %s -> %s;\n", dotID(fn), dotID(dep.FullName)) | ||
| } | ||
| } | ||
|
Comment on lines
+185
to
+203
|
||
|
|
||
| fmt.Fprintln(sb, "}") | ||
| return sb.String() | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A new output format (“dot” / formatDot) is introduced, but there are no tests exercising it (e.g., ensuring clusters/nodes/edges are emitted and that changed nodes are styled correctly). Since cli/output_test.go already covers text formatting, adding a focused unit test for DOT output would help prevent regressions.