package buildstep import ( "context" "crypto/sha256" "encoding/hex" "fmt" "os" "path" "path/filepath" "strings" sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" dforgekey "git.novox.be/novox/mesh-delivery/sdk/forgekey" "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", dforgekey.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, dforgekey.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) } objects := map[string]string{} for _, round := range unnested(full) { r := run(ctx, env, "git", append([]string{"-C", dir, "ls-tree", "-z", commit, "--"}, round...)...) if r.Status != 0 || r.Err != "" { return fmt.Errorf("git ls-tree %s of the module %s: %s", commit, mdir, dforgekey.Said(r.Err+"\n"+r.Stderr)) } 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 } // unnested splits paths into rounds of one git ls-tree each, none of whose paths lies inside another of its round: given // "go" and "go/seats/x" together, git ls-tree descends into go and lists its children instead of go itself, so the // object of go is missing from its answer (issue 1966: mesh-contracts' manifest names both). Usually one round; the // order of paths is kept within a round. func unnested(paths []string) [][]string { var rounds [][]string for _, p := range paths { placed := false for i, round := range rounds { if !nestsWithAny(p, round) { rounds[i] = append(round, p) placed = true break } } if !placed { rounds = append(rounds, []string{p}) } } return rounds } func nestsWithAny(p string, round []string) bool { for _, q := range round { if p == q || strings.HasPrefix(p, q+"/") || strings.HasPrefix(q, p+"/") || p == "." || q == "." { return true } } return false } func sha256Hex(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }