Files
mesh-catalog/modules/mesh-build-go/buildstep/trees.go
T
jschoubben 6f781abb57 A merge preview reports raw paths and objects, so only the indexer computes a named tree or reads packages (ADR 0593 §2)
ModuleTrees answers each module directory's git tree, its manifest and lock digests, and the git objects of the paths its
artifacts name; it no longer computes delivery.NamedTree or reads the mesh packages out of go.sum.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:28:22 +01:00

110 lines
4.0 KiB
Go

package buildstep
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path"
"path/filepath"
"strings"
sdkstep "git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
var lockFiles = []string{"go.sum", "package-lock.json"}
// ModuleTrees are the trees, in commit, of every directory of the workspace dir that holds a module.json, by directory
// (the repository's root is "."): the directory's git tree, the digests of its manifest and lock files, and, for a module
// whose every artifact names its paths, those paths' git objects (novox/hq ADR 0499 clause 2). It is raw: the indexer alone
// computes a module's named tree and reads its mesh packages from it (novox/hq ADR 0593 §2). The workspace is at commit, so
// the manifests and the lock files are read from it. env is the environment git runs with (nil for none). Both build
// modules' fetch steps answer it for a merge.
func ModuleTrees(ctx context.Context, run forgekey.Runner, env []string, dir, commit string) (map[string]sdkstep.ModuleTree, error) {
r := run(ctx, env, "git", "-C", dir, "ls-files", "--", "module.json", "*/module.json")
if r.Status != 0 || r.Err != "" {
return nil, fmt.Errorf("git ls-files: %s", forgekey.Said(r.Err+"\n"+r.Stderr))
}
trees := map[string]sdkstep.ModuleTree{}
for _, l := range strings.Split(r.Stdout, "\n") {
if l = strings.TrimSpace(l); l == "" {
continue
}
mdir := filepath.ToSlash(filepath.Dir(l))
spec := commit + ":" + mdir
if mdir == "." {
spec = commit + "^{tree}"
}
t := run(ctx, env, "git", "-C", dir, "rev-parse", spec)
if t.Status != 0 || t.Err != "" {
return nil, fmt.Errorf("git rev-parse %s: %s", spec, forgekey.Said(t.Err+"\n"+t.Stderr))
}
mt := sdkstep.ModuleTree{Tree: strings.TrimSpace(t.Stdout)}
raw, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(l)))
if err != nil {
return nil, err
}
mt.Manifest = sha256Hex(raw)
if err := narrow(ctx, run, env, dir, commit, mdir, raw, &mt); err != nil {
return nil, err
}
for _, lock := range lockFiles {
if raw, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(mdir), lock)); err == nil {
if mt.Locks == nil {
mt.Locks = map[string]string{}
}
mt.Locks[lock] = sha256Hex(raw)
}
}
trees[mdir] = mt
}
return trees, nil
}
// narrow sets mt's Paths when the module's manifest names paths on every artifact (novox/hq ADR 0499
// clause 2): one `git ls-tree -z` gives each named path's object at commit. The merge preview decodes the manifest here
// (catalogue.Decode: a build step judges no manifest, novox/hq ADR 0578 §3), and no other build step reads paths from one
// (ADR 0495 clause 1). A manifest that does not decode is not narrowed: the indexer gives it none. A named path absent at
// commit fails, naming the module and the path.
func narrow(ctx context.Context, run forgekey.Runner, env []string, dir, commit, mdir string, raw []byte, mt *sdkstep.ModuleTree) error {
m, err := catalogue.Decode(raw)
if err != nil {
return nil
}
paths, every := m.NamedPaths()
if !every {
return nil
}
full := make([]string, len(paths))
for i, p := range paths {
full[i] = path.Join(mdir, p)
}
r := run(ctx, env, "git", append([]string{"-C", dir, "ls-tree", "-z", commit, "--"}, full...)...)
if r.Status != 0 || r.Err != "" {
return fmt.Errorf("git ls-tree %s of the module %s: %s", commit, mdir, forgekey.Said(r.Err+"\n"+r.Stderr))
}
objects := map[string]string{}
for _, l := range strings.Split(r.Stdout, "\x00") {
meta, name, ok := strings.Cut(l, "\t")
fields := strings.Fields(meta)
if !ok || len(fields) != 3 {
continue
}
objects[name] = fields[2]
}
mt.Paths = map[string]string{}
for i, p := range paths {
object := objects[full[i]]
if object == "" {
return fmt.Errorf("the module %s names the path %q, which is not in %s", mdir, p, commit)
}
mt.Paths[p] = object
}
return nil
}
func sha256Hex(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }