ModuleTrees reads each named path's object with one git ls-tree and answers their named tree, so the preview gives the value the indexer gives; a named path absent at the commit fails by name rather than pinning a source that does not exist.
124 lines
4.6 KiB
Go
124 lines
4.6 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
|
)
|
|
|
|
// ModuleTree is what a merge holds of one module directory: its tree id, the sha256 of its module.json and the sha256
|
|
// of each lock file it has (go.sum, package-lock.json), from which the checker and the indexer take the module's inputs
|
|
// (novox/hq ADR 0484 clause 3).
|
|
type ModuleTree struct {
|
|
Tree string `json:"tree"`
|
|
Manifest string `json:"manifest"`
|
|
Locks map[string]string `json:"locks,omitempty"`
|
|
// Paths are, of a module whose every artifact names its paths, each named path's git object id at the commit; Tree
|
|
// is then their delivery.NamedTree with Manifest (novox/hq ADR 0499 clause 2). Absent otherwise.
|
|
Paths map[string]string `json:"paths,omitempty"`
|
|
}
|
|
|
|
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 "."); for a module whose every artifact names its paths, the named tree of those paths and
|
|
// their objects (novox/hq ADR 0499 clause 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]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", firstLine(r.Err+" "+r.Stderr))
|
|
}
|
|
trees := map[string]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, firstLine(t.Err+" "+t.Stderr))
|
|
}
|
|
mt := 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 and its named Tree 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 reads the manifest here, as
|
|
// the indexer does, and no other build step reads paths from one (ADR 0495 clause 1). A manifest that does not parse
|
|
// 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 *ModuleTree) error {
|
|
m, err := catalogue.ParseManifest(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, firstLine(r.Err+" "+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
|
|
}
|
|
named, err := delivery.NamedTree(mt.Manifest, mt.Paths)
|
|
if err != nil {
|
|
return fmt.Errorf("the module %s: %w", mdir, err)
|
|
}
|
|
mt.Tree = named
|
|
return nil
|
|
}
|
|
|
|
func sha256Hex(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }
|