| 1 |
|
package service |
| 2 |
|
|
| 3 |
|
import ( |
| 4 |
|
"context" |
| 5 |
|
"fmt" |
| 6 |
|
"sync" |
| 7 |
|
|
| 8 |
|
"github.com/go-git/go-git/v5/plumbing" |
| 9 |
|
|
| 10 |
|
"sourcecraft.dev/bigbes/sr-ht-spec/core" |
| 11 |
|
"sourcecraft.dev/bigbes/sr-ht-spec/doc" |
| 12 |
|
"sourcecraft.dev/bigbes/sr-ht-spec/gitx" |
| 13 |
|
) |
| 14 |
|
|
| 15 |
|
// archiveRenderer is shared across calls: doc.Renderer is documented as |
| 16 |
|
// reusable and concurrency-safe, and building one per archive would rebuild the |
| 17 |
|
// whole goldmark pipeline on every read. search/ shares one the same way. |
| 18 |
|
var archiveRenderer = sync.OnceValue(doc.NewRenderer) |
| 19 |
|
|
| 20 |
|
// Archive builds a space's addressable document set at a revision, together |
| 21 |
|
// with every document's bytes, from one tree walk. |
| 22 |
|
// |
| 23 |
|
// This is the read every surface above this layer needs: the archive owns the |
| 24 |
|
// design's addressing rule (a document's id when it is well-formed and unique |
| 25 |
|
// in the space, otherwise its path), resolves the [[wikilinks]] between |
| 26 |
|
// documents, and carries the link graph backlinks are read from. Before it |
| 27 |
|
// existed, web/ scanned the git tree itself — reaching past this layer into |
| 28 |
|
// gitx, which the layering rule forbids — and mcpsrv/ converted this package's |
| 29 |
|
// documents back into git ones. Two workarounds around one missing accessor is |
| 30 |
|
// how three agent-facing surfaces stop being behaviourally identical. |
| 31 |
|
// |
| 32 |
|
// The revision is resolved first and the documents are read at the resolved |
| 33 |
|
// sha, so the archive and the bodies are the same revision by construction: a |
| 34 |
|
// merge landing mid-read cannot make one answer describe two revisions. The |
| 35 |
|
// resolved sha is the archive's Rev, and is what a caller hands back as the |
| 36 |
|
// pinned ?rev=. |
| 37 |
|
// |
| 38 |
|
// bodies is keyed by tree path — the key doc.Page.Path carries — and holds the |
| 39 |
|
// whole document, frontmatter included. |
| 40 |
3 |
func (s *Service) Archive(ctx context.Context, sp *Space, rev string) (*doc.Archive, map[string][]byte, error) { |
| 41 |
3 |
commit, resolved, err := s.resolveRev(ctx, sp, rev) |
| 42 |
3 |
if err != nil { |
| 43 |
1 |
return nil, nil, err |
| 44 |
1 |
} |
| 45 |
2 |
docs, err := sp.Repo.ListDocuments(ctx, resolved) |
| 46 |
2 |
if err != nil { |
| 47 |
0 |
return nil, nil, readErr(err, "list documents at %s in %s", resolved, sp.Ref) |
| 48 |
0 |
} |
| 49 |
2 |
return archiveOf(sp.Ref, commit.String(), docs) |
| 50 |
|
} |
| 51 |
|
|
| 52 |
|
// ArchiveFrom builds the same archive out of documents this package already |
| 53 |
|
// returned, without touching git. |
| 54 |
|
// |
| 55 |
|
// It is the seam for a caller that holds a [Document] set — a test double |
| 56 |
|
// standing in for this service, a future surface that has already listed a |
| 57 |
|
// revision — and it exists so that the one conversion from a hex object name |
| 58 |
|
// back to a git hash lives here rather than in each of them. A malformed sha is |
| 59 |
|
// an error, not a zero hash: plumbing.NewHash silently yields the zero value |
| 60 |
|
// for anything it cannot parse, and a document whose render-cache key is zero |
| 61 |
|
// is a cache collision waiting to happen. |
| 62 |
|
// |
| 63 |
|
// rev must be the resolved commit the documents were read at, for the same |
| 64 |
|
// reason [Service.Archive] resolves before it reads. |
| 65 |
1 |
func ArchiveFrom(sp core.SpaceRef, rev string, docs []Document) (*doc.Archive, map[string][]byte, error) { |
| 66 |
1 |
converted := make([]gitx.Document, 0, len(docs)) |
| 67 |
1 |
for _, d := range docs { |
| 68 |
1 |
if !plumbing.IsHash(d.Blob) { |
| 69 |
1 |
return nil, nil, fmt.Errorf("service: document %q carries a malformed blob id %q", d.Path, d.Blob) |
| 70 |
1 |
} |
| 71 |
0 |
converted = append(converted, gitx.Document{Path: d.Path, Blob: plumbing.NewHash(d.Blob), Data: d.Data}) |
| 72 |
|
} |
| 73 |
0 |
return archiveOf(sp, rev, converted) |
| 74 |
|
} |
| 75 |
|
|
| 76 |
|
// archiveOf assembles the archive, the bodies and the link graph out of one |
| 77 |
|
// revision's documents. |
| 78 |
|
// |
| 79 |
|
// The link pass runs here rather than at each caller. doc.Page.Links is what |
| 80 |
|
// Archive.Backlinks reads, and an archive handed out without it looks like a |
| 81 |
|
// space where nothing links to anything — which is what every surface saw |
| 82 |
|
// until web/ grew its own pass and re-rendered the revision on each page view. |
| 83 |
|
// Filling it once, in the accessor, is the difference between a link graph and |
| 84 |
|
// a per-surface convention. It costs one render of each document; at the |
| 85 |
|
// confirmed volume — tens of documents a day — that is the same walk's data |
| 86 |
|
// being parsed once more, not a second read. |
| 87 |
2 |
func archiveOf(sp core.SpaceRef, rev string, docs []gitx.Document) (*doc.Archive, map[string][]byte, error) { |
| 88 |
2 |
arc := doc.FromDocuments(sp, rev, docs) |
| 89 |
2 |
bodies := make(map[string][]byte, len(docs)) |
| 90 |
4 |
for _, d := range docs { |
| 91 |
4 |
bodies[d.Path] = d.Data |
| 92 |
4 |
} |
| 93 |
2 |
if err := arc.LinkPass(archiveRenderer(), bodies); err != nil { |
| 94 |
0 |
return nil, nil, fmt.Errorf("service: archive of %s at %s: %w", sp, rev, err) |
| 95 |
0 |
} |
| 96 |
2 |
return arc, bodies, nil |
| 97 |
|
} |