207 lines
5.6 KiB
Go
207 lines
5.6 KiB
Go
package git
|
|
|
|
import (
|
|
"log/slog"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/data"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
|
|
)
|
|
|
|
// CommitGraphState is a CommitGraph's selection and its lane layout.
|
|
type CommitGraphState struct {
|
|
List data.ListState[string]
|
|
rows []commitGraphRow
|
|
// lanes is the widest the graph gets.
|
|
lanes int
|
|
src *Commit
|
|
n int
|
|
}
|
|
|
|
// CommitGraphOptions configure a CommitGraph.
|
|
type CommitGraphOptions struct {
|
|
// TimeFormat lays out commit times; empty means "2006-01-02 15:04".
|
|
TimeFormat string
|
|
// MaxLanes caps the graph width in lanes; zero means 8.
|
|
MaxLanes int
|
|
Status data.DataStatus
|
|
Error string
|
|
}
|
|
|
|
// commitGraphLink joins lane from to lane to within a row.
|
|
type commitGraphLink struct{ from, to, color int }
|
|
|
|
type commitGraphRow struct {
|
|
node, color int
|
|
merge bool
|
|
// top links enter from above; bottom links leave the node.
|
|
top, bottom []commitGraphLink
|
|
}
|
|
|
|
type commitGraphLane struct {
|
|
hash string
|
|
color int
|
|
}
|
|
|
|
// CommitGraph shows commits with branch lanes; give it a height.
|
|
func CommitGraph(c *ui.Context, s *CommitGraphState, commits []Commit, opts CommitGraphOptions) GitListResult {
|
|
maxLanes := opts.MaxLanes
|
|
if maxLanes == 0 {
|
|
maxLanes = 8
|
|
}
|
|
if maxLanes < 0 {
|
|
panic("mujica: CommitGraph MaxLanes is negative")
|
|
}
|
|
var first *Commit
|
|
if len(commits) > 0 {
|
|
first = &commits[0]
|
|
}
|
|
if first != s.src || len(commits) != s.n {
|
|
s.rows, s.lanes = commitGraphLayout(commits)
|
|
s.src, s.n = first, len(commits)
|
|
slog.Debug("mujica: commit graph layout", "commits", len(commits), "lanes", s.lanes)
|
|
}
|
|
layout := gitTimeLayout(opts.TimeFormat)
|
|
lane := core.Density(c).Unit() * 4
|
|
width := float32(min(max(s.lanes, 1), maxLanes)) * lane
|
|
h := core.Density(c).ControlHeight()
|
|
v := data.List(c, &s.List, len(commits), data.ListOptions[string]{
|
|
Key: func(i int) string { return gitCommitHash(commits[i]) },
|
|
Label: func(i int) string { return commits[i].Subject },
|
|
Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "commit_list.empty"),
|
|
}, func(i int) {
|
|
row := s.rows[i]
|
|
ui.Box(c).Size(width, h).Shrink(0).Clip().Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) {
|
|
commitGraphDraw(p, core.Tokens(c), r, row, lane)
|
|
})
|
|
commitListRow(c, commits[i], layout)
|
|
})
|
|
res := GitListResult{Element: v.Element, changed: v.Changed(), submitted: v.Submitted()}
|
|
if res.changed || res.submitted {
|
|
key, _ := s.List.Selected()
|
|
slog.Debug("mujica: commit graph choose", "hash", key, "open", res.submitted)
|
|
}
|
|
return res
|
|
}
|
|
|
|
// commitGraphLayout gives each commit a lane and parent links.
|
|
func commitGraphLayout(commits []Commit) ([]commitGraphRow, int) {
|
|
rows := make([]commitGraphRow, len(commits))
|
|
var lanes []commitGraphLane
|
|
colors, widest := 0, 0
|
|
free := func() int {
|
|
for j, l := range lanes {
|
|
if l.hash == "" {
|
|
return j
|
|
}
|
|
}
|
|
lanes = append(lanes, commitGraphLane{})
|
|
return len(lanes) - 1
|
|
}
|
|
for i, cm := range commits {
|
|
h := gitCommitHash(cm)
|
|
var row commitGraphRow
|
|
row.node = -1
|
|
for j, l := range lanes {
|
|
if l.hash == h && row.node < 0 {
|
|
row.node = j
|
|
}
|
|
}
|
|
for j, l := range lanes {
|
|
switch {
|
|
case l.hash == "":
|
|
case l.hash == h:
|
|
row.top = append(row.top, commitGraphLink{j, row.node, l.color})
|
|
default:
|
|
row.top = append(row.top, commitGraphLink{j, j, l.color})
|
|
}
|
|
}
|
|
if row.node < 0 {
|
|
row.node = free()
|
|
lanes[row.node] = commitGraphLane{h, colors % 6}
|
|
colors++
|
|
}
|
|
row.color = lanes[row.node].color
|
|
for j := range lanes {
|
|
if lanes[j].hash == h {
|
|
lanes[j].hash = ""
|
|
}
|
|
}
|
|
row.merge = len(cm.Parents) > 1
|
|
for pi, p := range cm.Parents {
|
|
if pi == 0 {
|
|
lanes[row.node] = commitGraphLane{p, row.color}
|
|
row.bottom = append(row.bottom, commitGraphLink{row.node, row.node, row.color})
|
|
continue
|
|
}
|
|
to := -1
|
|
for j, l := range lanes {
|
|
if l.hash == p {
|
|
to = j
|
|
break
|
|
}
|
|
}
|
|
if to < 0 {
|
|
to = free()
|
|
lanes[to] = commitGraphLane{p, colors % 6}
|
|
colors++
|
|
}
|
|
row.bottom = append(row.bottom, commitGraphLink{row.node, to, lanes[to].color})
|
|
}
|
|
for len(lanes) > 0 && lanes[len(lanes)-1].hash == "" {
|
|
lanes = lanes[:len(lanes)-1]
|
|
}
|
|
widest = max(widest, len(lanes), row.node+1)
|
|
rows[i] = row
|
|
}
|
|
return rows, widest
|
|
}
|
|
|
|
// commitGraphColors are the lane colors; place also tells lanes apart.
|
|
func commitGraphColors(k theme.Tokens) [6]ui.Color {
|
|
return [6]ui.Color{k.AccentText, k.Info, k.Success, k.Warning, k.Danger, k.TextMuted}
|
|
}
|
|
|
|
func commitGraphDraw(p *ui.Painter, k theme.Tokens, r ui.Rect, row commitGraphRow, lane float32) {
|
|
cols := commitGraphColors(k)
|
|
x := func(j int) float32 { return internal.Snap(p, r.X+lane/2+float32(j)*lane) }
|
|
top, mid, bot := internal.Snap(p, r.Y), internal.Snap(p, r.Y+r.H/2), internal.Snap(p, r.Y+r.H)
|
|
seg := func(l commitGraphLink, y0, y1 float32) {
|
|
var path ui.Path
|
|
xa, xb := x(l.from), x(l.to)
|
|
path.MoveTo(xa, y0)
|
|
if xa == xb {
|
|
path.LineTo(xb, y1)
|
|
} else {
|
|
m := (y0 + y1) / 2
|
|
path.CubeTo(xa, m, xb, m, xb, y1)
|
|
}
|
|
p.StrokePath(&path, 2, cols[l.color])
|
|
}
|
|
for _, l := range row.top {
|
|
if l.from == l.to && l.to != row.node {
|
|
seg(l, top, bot)
|
|
continue
|
|
}
|
|
seg(l, top, mid)
|
|
}
|
|
for _, l := range row.bottom {
|
|
seg(l, mid, bot)
|
|
}
|
|
cx, col := x(row.node), cols[row.color]
|
|
if row.merge {
|
|
p.FillPath(internal.Diamond(cx, mid, 11), k.Surface)
|
|
p.StrokePath(internal.Diamond(cx, mid, 9), 2, col)
|
|
return
|
|
}
|
|
var dot ui.Path
|
|
dot.Circle(cx, mid, 5.5)
|
|
p.FillPath(&dot, k.Surface)
|
|
var ring ui.Path
|
|
ring.Circle(cx, mid, 4)
|
|
p.FillPath(&ring, col)
|
|
}
|