Skip to content
quarry Artifact [a35b50e56f]
Login

Artifact [a35b50e56f]

Artifact a35b50e56f5de62f6dc83fb195aa9d7c09927153c99c7d3a95025aa56107100a:


package ingest

import (
	"strings"

	"craftdesign.group/quarry/internal/artifact"
	"craftdesign.group/quarry/internal/evt"
)

// raised is one vocabulary event a folded artifact produced.
type raised struct {
	Kind    evt.Kind
	Payload map[string]any
}

// symPrefix marks a branch name among the tags. Fossil keeps the T
// card's operator on the name — `*` propagates, `+` applies to one
// artifact, `-` cancels — so the operator is read off the front rather
// than tracked separately.
const symPrefix = "sym-"

// derive names the vocabulary events one folded artifact produces
// (RFC 0005 §2.1: these are the DERIVED events, the ones zit's DAG
// creates rather than a forge op). It reads only the artifact, so it
// cannot lie about state it did not look at — anything needing index
// state is deliberately absent, see branchKind below.
func derive(hash string, a *artifact.Artifact) []raised {
	if a == nil {
		return nil
	}
	base := func() map[string]any {
		return map[string]any{"hash": hash, "actor": a.User, "date": a.Date}
	}
	var out []raised
	switch a.Type {
	case artifact.Manifest:
		p := base()
		p["summary"] = firstLine(a.Comment)
		if b := branches(a.Tags); len(b) > 0 {
			p["branch"] = b[0]
		}
		out = append(out, raised{evt.CheckinCreated, p})
	case artifact.Control:
		// A control artifact carries nothing but tags; the check-in it
		// targets is the subject of every event it produces.
	default:
		// Every other type is already raised at the op that made it.
		// Raising here too would double every wiki edit and forum post
		// that went through the forge, and the op knows more than the
		// artifact does (it knows whether a ticket CLOSED, which needs
		// the previous status). See the note in fold().
		return nil
	}
	for _, t := range a.Tags {
		if len(t.Name) < 2 {
			continue // no operator, no name: nothing to say about it
		}
		op, name := t.Name[:1], t.Name[1:]
		if !strings.HasPrefix(name, symPrefix) {
			// A plain tag. `-` cancels one, anything else applies it.
			k := evt.TagCreated
			if op == "-" {
				k = evt.TagRemoved
			}
			p := base()
			p["tag"] = name
			p["target"] = target(t, hash)
			out = append(out, raised{k, p})
			continue
		}
		if op == "-" {
			// Cancelling a branch tag is not closing the branch — the
			// `closed` tag does that, and it is a plain tag handled
			// above. Nothing in the vocabulary names this, so nothing
			// is raised rather than something approximate.
			continue
		}
		// A `sym-` tag on a check-in is that branch coming into
		// existence. branch.moved for the SAME check-in comes from the
		// pipeline once branches are recomputed (see branchKind), so
		// raising it here as well would double it.
		if a.Type == artifact.Control {
			p := base()
			p["branch"] = strings.TrimPrefix(name, symPrefix)
			p["target"] = target(t, hash)
			out = append(out, raised{evt.BranchCreated, p})
		}
	}
	return out
}

// branchKind is deliberately not a function here, because a check-in's
// branch is not in the check-in. Only a branch's FIRST check-in wears a
// `sym-` tag; the hundredth commit to trunk says nothing about trunk, so
// deriving branch.moved from tags alone would fire it on branch creation
// and never again — the opposite of what the name promises. The index
// computes the branch during the fold, and the pipeline raises
// branch.moved from that answer once it exists.

// target is the artifact a tag applies to: a control artifact names it,
// a manifest tags itself.
func target(t artifact.Tag, self string) string {
	if t.Target != "" && t.Target != "*" {
		return t.Target
	}
	return self
}

// branches returns the propagating branch names among a check-in's tags.
func branches(tags []artifact.Tag) []string {
	var out []string
	for _, t := range tags {
		if len(t.Name) > 1 && strings.HasPrefix(t.Name[1:], symPrefix) && t.Name[:1] != "-" {
			out = append(out, strings.TrimPrefix(t.Name[1:], symPrefix))
		}
	}
	return out
}

func firstLine(s string) string {
	if i := strings.IndexByte(s, '\n'); i >= 0 {
		return s[:i]
	}
	return s
}