Files
glint/internal/graph/tree.go
T
k3nny 88f20165db feat(cli)!: subcommand CLI, graph tree mode, local include resolution
BREAKING CHANGES:
- `glint <file>` removed; use `glint check <file>`
- `--graph <mode>` removed; use `glint graph [mode]`
- `--graph-out` renamed to `--out` on `glint graph`

feat(cli): ruff-style subcommands — `glint check` and `glint graph [mode]`
feat(graph): `glint graph tree` — terminal job tree with context annotations
feat(graph): context flags (--branch/--tag/--source/--var) on `glint graph`
feat(resolver): recursive local include resolution from disk
fix(resolver): extends unknown base emits warning instead of fatal error
fix(model): script/before_script/after_script accept block scalar string form
test(linter): Samba project CI fixtures as integration tests
chore(build): fix .gitignore to not exclude cmd/glint/ directory
docs: update CHANGELOG, README, ROADMAP for v0.2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 00:27:28 +02:00

114 lines
2.6 KiB
Go

package graph
import (
"sort"
"strings"
"git.k3nny.fr/glint/internal/cicontext"
"git.k3nny.fr/glint/internal/model"
)
// Tree returns a tree-command-style text representation of pipeline jobs
// grouped by stage.
//
// Hidden template jobs (names starting with ".") are excluded.
// When ctx is non-nil and non-empty, each job is annotated with its evaluated
// state ([skipped] or [manual]); active jobs carry no annotation.
// Without a context, job-type annotations ([manual], [delayed], [trigger]) are
// shown instead.
func Tree(p *model.Pipeline, ctx *cicontext.Context) string {
var sb strings.Builder
// Collect visible jobs.
var visible []string
for name := range p.Jobs {
if !strings.HasPrefix(name, ".") {
visible = append(visible, name)
}
}
sort.Strings(visible)
// Group by stage.
byStage := make(map[string][]string)
for _, name := range visible {
stage := p.Jobs[name].Stage
if stage == "" {
stage = "test"
}
byStage[stage] = append(byStage[stage], name)
}
// Build ordered stage list: declared first, then extras.
seen := make(map[string]bool)
var stages []string
for _, s := range p.Stages {
if len(byStage[s]) > 0 && !seen[s] {
stages = append(stages, s)
seen[s] = true
}
}
for _, name := range visible {
stage := p.Jobs[name].Stage
if stage == "" {
stage = "test"
}
if !seen[stage] {
stages = append(stages, stage)
seen[stage] = true
}
}
sb.WriteString("pipeline\n")
for si, stage := range stages {
lastStage := si == len(stages)-1
stagePrefix, childPrefix := branchChars(lastStage)
sb.WriteString(stagePrefix + stage + "\n")
jobs := byStage[stage]
sort.Strings(jobs)
for ji, name := range jobs {
lastJob := ji == len(jobs)-1
jobBranch, _ := branchChars(lastJob)
sb.WriteString(childPrefix + jobBranch + jobLabel(p.Jobs[name], name, ctx) + "\n")
}
}
return sb.String()
}
func branchChars(last bool) (branch, continuation string) {
if last {
return "└── ", " "
}
return "├── ", "│ "
}
func jobLabel(job model.Job, name string, ctx *cicontext.Context) string {
if ctx != nil && !ctx.IsEmpty() {
switch cicontext.EvalJob(job, ctx) {
case cicontext.JobSkipped:
return name + " [skipped]"
case cicontext.JobManual:
return name + " [manual]"
}
return name
}
// No context: annotate by job type.
var tags []string
if job.When == "manual" {
tags = append(tags, "manual")
}
if job.When == "delayed" {
tags = append(tags, "delayed")
}
if job.Trigger != nil {
tags = append(tags, "trigger")
}
if len(tags) == 0 {
return name
}
return name + " [" + strings.Join(tags, ", ") + "]"
}