Skip to content

Commit

Permalink
refac(submit): extract stack navigation rendering (#562)
Browse files Browse the repository at this point in the history
Add a new forge/stacknav package that renders stack navigation comments
as a Markdown list.
  • Loading branch information
abhinav authored Feb 1, 2025
1 parent e2cc7a3 commit 936260b
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 58 deletions.
117 changes: 117 additions & 0 deletions internal/forge/stacknav/nav.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Package stacknav provides support for creating stack navigation comments
// and descriptions.
package stacknav

import (
"fmt"
"io"
)

const (
// _marker is the marker to use for the current change.
_marker = "◀"

// indent is a Markdown itemized list indentation.
_indent = " "
)

// Node is a single item in the stack navigation list.
// It usually represents a change in the Forge.
type Node interface {
// Value returns the text to display for the node.
// This will be rendered verbatim.
Value() string

// BaseIdx returns the index of the node below this one.
// Use -1 if this is the bottom-most node of its stack.
//
// If the value is not -1, it MUST be a valid index in the nodes list
// or the program will panic.
BaseIdx() int
}

// Print visualizes a stack of changes in a Forge
// using a Markdown itemized list.
//
// For example:
//
// This change is part of the following stack:
//
// - #123
// - #124 ◀
// - #125
//
// currentIdx is the index of the current node in the nodes list.
// It will be marked with [Printer.Marker].
//
// All Write errors are ignored. Use a Writer that doesn't fail.
func Print[N Node](w io.Writer, nodes []N, currentIdx int) {
// aboves[i] holds indexes of nodes that are above nodes[i].
aboves := make([][]int, len(nodes))
for idx, node := range nodes {
baseIdx := node.BaseIdx()
if baseIdx >= 0 {
aboves[baseIdx] = append(aboves[baseIdx], idx)
}
}

writeNode := func(nodeIdx, indent int) {
node := nodes[nodeIdx]
for range indent {
_, _ = io.WriteString(w, _indent)
}

_, _ = fmt.Fprintf(w, "- %v", node.Value())
if nodeIdx == currentIdx {
_, _ = fmt.Fprintf(w, " %v", _marker)
}

_, _ = io.WriteString(w, "\n")
}

// The graph is a DAG, so we don't expect cycles.
// Guard against it anyway.
visited := make([]bool, len(nodes))
ok := func(i int) bool {
if i < 0 || i >= len(nodes) || visited[i] {
return false
}
visited[i] = true
return true
}

// Write the downstacks, not including the current node.
// This will change the indent level.
// The downstacks leading up to the current branch are always linear.
var indent int
{
var downstacks []int
for base := nodes[currentIdx].BaseIdx(); ok(base); base = nodes[base].BaseIdx() {
downstacks = append(downstacks, base)
}

// Reverse order to print from base to current.
for i := len(downstacks) - 1; i >= 0; i-- {
writeNode(downstacks[i], indent)
indent++
}
}

// For the upstacks, we'll need to traverse the graph
// and recursively write the upstacks.
// Indentation will increase for each subtree.
var visit func(int, int)
visit = func(nodeIdx, indent int) {
if !ok(nodeIdx) {
return
}

writeNode(nodeIdx, indent)
for _, aboveIdx := range aboves[nodeIdx] {
visit(aboveIdx, indent+1)
}
}

// Current branch and its upstacks.
visit(currentIdx, indent)
}
112 changes: 112 additions & 0 deletions internal/forge/stacknav/nav_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package stacknav

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestPrinter(t *testing.T) {
tests := []struct {
name string
graph []Item
current int
want string
}{
{
name: "Single",
graph: []Item{
{value: "#123", base: -1},
},
current: 0,
want: joinLines(
"- #123 ◀",
),
},
{
name: "Downstack",
graph: []Item{
{value: "#123", base: -1},
{value: "#124", base: 0},
{value: "#125", base: 1},
},
current: 2,
want: joinLines(
"- #123",
" - #124",
" - #125 ◀",
),
},
{
name: "Upstack/Linear",
graph: []Item{
{value: "#123", base: -1},
{value: "#124", base: 0},
{value: "#125", base: 1},
},
current: 0,
want: joinLines(
"- #123 ◀",
" - #124",
" - #125",
),
},
{
name: "Upstack/NonLinear",
graph: []Item{
{value: "#123", base: -1},
{value: "#124", base: 0}, // 1
{value: "#125", base: 0}, // 2
{value: "#126", base: 1},
{value: "#127", base: 2},
},
current: 0,
want: joinLines(
"- #123 ◀",
" - #124",
" - #126",
" - #125",
" - #127",
),
},
{
name: "MidStack",
graph: []Item{
{value: "#123", base: -1}, // 0
{value: "#124", base: 0}, // 1
{value: "#125", base: 1}, // 2
{value: "#126", base: 0}, // 3
{value: "#127", base: 3}, // 4
},
// 1 has a sibling (3), but that won't be shown
// as it's not in the path to the current branch.
current: 1,
want: joinLines(
"- #123",
" - #124 ◀",
" - #125",
),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got strings.Builder
Print(&got, tt.graph, tt.current)
assert.Equal(t, tt.want, got.String())
})
}
}

type Item struct {
value string
base int
}

func (i Item) Value() string { return i.value }
func (i Item) BaseIdx() int { return i.base }

func joinLines(lines ...string) string {
return strings.Join(lines, "\n") + "\n"
}
69 changes: 11 additions & 58 deletions submit.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/charmbracelet/log"
"go.abhg.dev/gs/internal/forge"
"go.abhg.dev/gs/internal/forge/stacknav"
"go.abhg.dev/gs/internal/git"
"go.abhg.dev/gs/internal/secret"
"go.abhg.dev/gs/internal/spice"
Expand Down Expand Up @@ -400,6 +401,14 @@ type stackedChange struct {
Aboves []int
}

var _ stacknav.Node = (*stackedChange)(nil)

func (s *stackedChange) BaseIdx() int { return s.Base }

func (s *stackedChange) Value() string {
return s.Change.String()
}

const (
_commentHeader = "This change is part of the following stack:"
_commentFooter = "<sub>Change managed by [git-spice](https://abhinav.github.io/git-spice/).</sub>"
Expand All @@ -421,70 +430,14 @@ func generateStackNavigationComment(
var sb strings.Builder
sb.WriteString(_commentHeader)
sb.WriteString("\n\n")
write := func(nodeIdx, indent int) {
node := nodes[nodeIdx]
for range indent {
sb.WriteString(" ")
}
fmt.Fprintf(&sb, "- %v", node.Change)
if nodeIdx == current {
sb.WriteString(" ◀")
}
sb.WriteString("\n")
}

// The graph is a DAG, so we don't expect cycles.
// Guard against it anyway.
visited := make([]bool, len(nodes))
ok := func(i int) bool {
if i < 0 || i >= len(nodes) || visited[i] {
return false
}
visited[i] = true
return true
}

// Write the downstacks, not including the current node.
// This will change the indent level.
// The downstacks leading up to the current branch are always linear.
var indent int
{
var downstacks []int
for base := nodes[current].Base; ok(base); base = nodes[base].Base {
downstacks = append(downstacks, base)
}
stacknav.Print(&sb, nodes, current)

// Reverse order to print from base to current.
for i := len(downstacks) - 1; i >= 0; i-- {
write(downstacks[i], indent)
indent++
}
}

// For the upstacks, we'll need to traverse the graph
// and recursively write the upstacks.
// Indentation will increase for each subtree.
var visit func(int, int)
visit = func(nodeIdx, indent int) {
if !ok(nodeIdx) {
return
}

write(nodeIdx, indent)
for _, aboveIdx := range nodes[nodeIdx].Aboves {
visit(aboveIdx, indent+1)
}
}

// Current branch and its upstacks.
visit(current, indent)
sb.WriteString("\n")

sb.WriteString(_commentFooter)
sb.WriteString("\n")

sb.WriteString("\n")
sb.WriteString(_commentMarker)
sb.WriteString("\n")

return sb.String()
}

0 comments on commit 936260b

Please # to comment.