88f20165db
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>
114 lines
2.6 KiB
Go
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, ", ") + "]"
|
|
}
|