Compute a module's narrowed tree in one function, so every caller gives the merge's value (issue 1758, S2, ADR 0499 §2)
delivery.NamedTree hashes the manifest digest with each named path's git object; catalogue.Manifest.NamedPaths gives the union of the paths, only when every artifact names some. The named: prefix keeps the value apart from a git tree id.
This commit is contained in:
@@ -399,6 +399,33 @@ func (b *Build) problems(module string) []string {
|
||||
return problems
|
||||
}
|
||||
|
||||
// NamedPaths are the paths a module's fingerprint takes its tree from (novox/hq ADR 0499 clause 2): the union of the
|
||||
// paths its artifacts name, cleaned and sorted, and every true only when the module builds something and every artifact
|
||||
// names paths; otherwise none, and the module keeps its directory's tree.
|
||||
//
|
||||
// **Read by the core alone** (ADR 0495 clause 1, the lead's decision L8 of R4e): the indexer and its merge preview
|
||||
// (buildstep.ModuleTrees) call it. A build module's fetch step is handed the paths by the builder, from the indexer's
|
||||
// kept inputs (delivery.KeptInputs2.Paths), and reads no manifest for them.
|
||||
func (m Manifest) NamedPaths() (paths []string, every bool) {
|
||||
if m.Build == nil || len(m.Build.Artifacts) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, a := range m.Build.Artifacts {
|
||||
if len(a.Paths) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
for _, p := range a.Paths {
|
||||
seen[path.Clean(p)] = true
|
||||
}
|
||||
}
|
||||
for p := range seen {
|
||||
paths = append(paths, p)
|
||||
}
|
||||
sort.Strings(paths)
|
||||
return paths, true
|
||||
}
|
||||
|
||||
// pathsProblems is what is wrong with an artifact's `paths` (novox/hq ADR 0499 clause 1): named on a kind whose build
|
||||
// source is derived or named otherwise, or a path that leaves the module's directory, is its whole directory, or is a
|
||||
// pattern.
|
||||
|
||||
@@ -44,3 +44,24 @@ func TestAnArtifactNamesThePathsItIsBuiltFrom(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The paths a module's tree is taken from are the union its artifacts name, cleaned and sorted, and only when every
|
||||
// artifact names some (novox/hq ADR 0499 clause 2); a module with an artifact that names none, or with none at all,
|
||||
// keeps its directory's tree.
|
||||
func TestAModulesNamedPathsAreTheUnionWhenEveryArtifactNamesThem(t *testing.T) {
|
||||
m := Manifest{Module: "a", Build: &Build{Artifacts: []Artifact{
|
||||
{Name: "lib", Kind: ArtifactPackage, Paths: []string{"src/", "package.json"}},
|
||||
{Name: "tools", Kind: ArtifactBundle, Paths: []string{"./src", "tools"}},
|
||||
}}}
|
||||
paths, every := m.NamedPaths()
|
||||
if !every || strings.Join(paths, ",") != "package.json,src,tools" {
|
||||
t.Fatalf("NamedPaths = %v, %v; want [package.json src tools], true", paths, every)
|
||||
}
|
||||
m.Build.Artifacts = append(m.Build.Artifacts, Artifact{Name: "img", Kind: ArtifactImage, From: "Dockerfile"})
|
||||
if paths, every := m.NamedPaths(); every || paths != nil {
|
||||
t.Fatalf("a module with an artifact naming no path gave %v, %v", paths, every)
|
||||
}
|
||||
if paths, every := (Manifest{Module: "b"}).NamedPaths(); every || paths != nil {
|
||||
t.Fatalf("a module that builds nothing gave %v, %v", paths, every)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NamedTreePrefix opens the tree input of a module whose every artifact names its paths, so such a value is never read
|
||||
// as a git tree id and a reader of a kept Tree can tell the two apart.
|
||||
const NamedTreePrefix = "named:"
|
||||
|
||||
// NamedTree is the tree input of the new-form fingerprint of a module whose every artifact names the paths it is built
|
||||
// from (novox/hq ADR 0499 clause 2): `named:` and the hex sha256 of the lines "manifest <digest>" and, sorted,
|
||||
// "path <path> <object>", joined by a newline. manifest is the module's ManifestDigest (the sha256 of its module.json's
|
||||
// exact bytes, as Module.Manifest and the merge preview carry it); paths are the named paths, each clean and relative to
|
||||
// the module's directory as the manifest names them (catalogue.Manifest.NamedPaths), with the git object id each has at
|
||||
// the commit (a blob for a file, a tree for a directory).
|
||||
//
|
||||
// **One function computes it** (clause 2): the indexer, its merge preview (buildstep.ModuleTrees) and the build modules
|
||||
// call this one, and no second computation exists. It refuses no path, a named path with no object (absent at the
|
||||
// commit), a path that is not clean, and a manifest that is not a digest, naming them: the indexer then gives the
|
||||
// module none, with that reason (clause 3).
|
||||
func NamedTree(manifest string, paths map[string]string) (string, error) {
|
||||
if _, err := ManifestsKey(manifest); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(paths) == 0 {
|
||||
return "", fmt.Errorf("delivery: the module names no path, so it has no named tree")
|
||||
}
|
||||
lines := make([]string, 0, len(paths))
|
||||
for p, object := range paths {
|
||||
if p == "" || p != path.Clean(p) || strings.HasPrefix(p, "/") || p == ".." || strings.HasPrefix(p, "../") {
|
||||
return "", fmt.Errorf("delivery: the named path %q is not a clean path inside the module's directory", p)
|
||||
}
|
||||
if object == "" {
|
||||
return "", fmt.Errorf("delivery: the named path %q has no git object at the commit", p)
|
||||
}
|
||||
lines = append(lines, "path "+p+" "+object)
|
||||
}
|
||||
sort.Strings(lines)
|
||||
sum := sha256.Sum256([]byte("manifest " + manifest + "\n" + strings.Join(lines, "\n")))
|
||||
return NamedTreePrefix + hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func sha256Of(s string) string { h := sha256.Sum256([]byte(s)); return hex.EncodeToString(h[:]) }
|
||||
|
||||
// A module whose every artifact names its paths takes its tree from its manifest and those paths alone (novox/hq ADR
|
||||
// 0499 clause 2): a change outside them leaves the value equal, a change to a named path or to the manifest changes it,
|
||||
// and the order the paths are given in changes nothing.
|
||||
func TestTheNamedTreeIsOfTheManifestAndTheNamedPathsAlone(t *testing.T) {
|
||||
manifest := sha256Of(`{"module":"mesh-sdk"}`)
|
||||
paths := map[string]string{"src": strings.Repeat("a", 40), "package.json": strings.Repeat("b", 40)}
|
||||
v, err := NamedTree(manifest, paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(v, NamedTreePrefix) || !isHex64(strings.TrimPrefix(v, NamedTreePrefix)) {
|
||||
t.Fatalf("the value %q is not %s and 64 hex digits", v, NamedTreePrefix)
|
||||
}
|
||||
// A change outside the named paths is no input at all: the same inputs give the same value, in any map order.
|
||||
again, _ := NamedTree(manifest, map[string]string{"package.json": strings.Repeat("b", 40), "src": strings.Repeat("a", 40)})
|
||||
if again != v {
|
||||
t.Fatalf("the same inputs gave %q and %q", v, again)
|
||||
}
|
||||
changed, _ := NamedTree(manifest, map[string]string{"src": strings.Repeat("c", 40), "package.json": strings.Repeat("b", 40)})
|
||||
if changed == v {
|
||||
t.Fatal("a changed named path left the value equal")
|
||||
}
|
||||
otherManifest, _ := NamedTree(sha256Of(`{"module":"mesh-sdk","version":"2"}`), paths)
|
||||
if otherManifest == v {
|
||||
t.Fatal("a changed manifest left the value equal")
|
||||
}
|
||||
renamed, _ := NamedTree(manifest, map[string]string{"lib": strings.Repeat("a", 40), "package.json": strings.Repeat("b", 40)})
|
||||
if renamed == v {
|
||||
t.Fatal("a path named differently with the same object left the value equal")
|
||||
}
|
||||
|
||||
// The lines are fixed, so every computation of it agrees: pinned here.
|
||||
want := "named:" + sha256Of("manifest "+manifest+"\npath package.json "+strings.Repeat("b", 40)+"\npath src "+strings.Repeat("a", 40))
|
||||
if v != want {
|
||||
t.Fatalf("NamedTree = %q, want %q", v, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheNamedTreeRefusesWhatItCannotPin(t *testing.T) {
|
||||
manifest := sha256Of("{}")
|
||||
for _, c := range []struct {
|
||||
manifest string
|
||||
paths map[string]string
|
||||
says string
|
||||
}{
|
||||
{manifest, nil, "names no path"},
|
||||
{manifest, map[string]string{"src": ""}, `the named path "src" has no git object`},
|
||||
{"abc", map[string]string{"src": "x"}, "not a manifest digest"},
|
||||
{manifest, map[string]string{"src/../x": "x"}, "is not a clean path"},
|
||||
{manifest, map[string]string{"": "x"}, "is not a clean path"},
|
||||
{manifest, map[string]string{"/src": "x"}, "is not a clean path"},
|
||||
} {
|
||||
if _, err := NamedTree(c.manifest, c.paths); err == nil || !strings.Contains(err.Error(), c.says) {
|
||||
t.Errorf("NamedTree(%q, %v) = %v, wanted a refusal saying %q", c.manifest, c.paths, err, c.says)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user