211 lines
7.4 KiB
Go
211 lines
7.4 KiB
Go
package docrevision
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// EnsureBootstrap creates a ledger from fact-source markdown when none exists (idempotent).
|
|
func (s *Service) EnsureBootstrap(ctx context.Context, req BootstrapRequest) (*RevisionState, error) {
|
|
if err := requireDocID(req.DocID); err != nil {
|
|
return nil, err
|
|
}
|
|
if st, err := s.GetRevisionState(ctx, req.DocID); err == nil {
|
|
return st, nil
|
|
} else if err != nil && !errors.Is(err, ErrNotFound) {
|
|
return nil, err
|
|
}
|
|
md := strings.TrimSpace(req.Markdown)
|
|
if md == "" {
|
|
return nil, ErrNeedsContent
|
|
}
|
|
kind := req.Kind
|
|
if kind == "" {
|
|
kind = "controlled"
|
|
}
|
|
life := req.LifecycleState
|
|
if life == "" {
|
|
life = "drafting"
|
|
}
|
|
st, err := s.OpenTemplateVersion(ctx, req.DocID, md, req.Actor, req.ProjectID, req.WorkDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
g := s.lockDoc(req.DocID)
|
|
s.mu.Lock()
|
|
if root := s.roots[req.DocID]; root != nil {
|
|
root.Kind = kind
|
|
root.LifecycleState = life
|
|
}
|
|
s.mu.Unlock()
|
|
g.Release()
|
|
if req.WorkDir != "" {
|
|
if pErr := s.ProjectOutputMarkdownIfSafe(req.DocID, s.DataDir); pErr != nil {
|
|
return st, pErr
|
|
}
|
|
}
|
|
return st, nil
|
|
}
|
|
|
|
func (s *Service) CommitHumanRevision(ctx context.Context, req HumanRevisionRequest) (*HumanRevisionResult, error) {
|
|
if err := requireDocID(req.DocID); err != nil {
|
|
return nil, err
|
|
}
|
|
if req.IdempotencyKey == "" {
|
|
return nil, fmt.Errorf("docrevision: idempotency key required")
|
|
}
|
|
if req.LifecycleState != "" && req.LifecycleState != "drafting" {
|
|
return nil, ErrLifecycleLocked
|
|
}
|
|
canon := CanonicalizeMarkdown(req.Markdown)
|
|
if len(canon) > MaxMarkdownBytes {
|
|
return nil, ErrTooLarge
|
|
}
|
|
sha := SHA256Hex(canon)
|
|
|
|
g := s.lockDoc(req.DocID)
|
|
defer g.Release()
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if err := s.hydrateDocLocked(ctx, req.DocID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if eid, ok := s.idemp[req.IdempotencyKey]; ok {
|
|
ev := s.events[eid]
|
|
if ev != nil {
|
|
if snap := s.snapshots[ev.ToSnapshotID]; snap != nil && snap.ContentSHA256 != sha {
|
|
return nil, ErrIdempotencyConflict
|
|
}
|
|
}
|
|
st, _ := s.revisionStateLocked(req.DocID)
|
|
overlay := s.manualOverlayLocked(req.DocID)
|
|
return &HumanRevisionResult{State: *st, Event: *ev, Overlay: overlay}, nil
|
|
}
|
|
|
|
root := s.roots[req.DocID]
|
|
if root == nil {
|
|
kind := "controlled"
|
|
if req.WorkDir == "" && req.ProjectID > 0 {
|
|
kind = "controlled"
|
|
}
|
|
root = &DocRootPointers{DocID: req.DocID, Kind: kind, LifecycleState: req.LifecycleState, ProjectID: req.ProjectID, RevisionSchemaVer: SchemaVersion, WorkDir: req.WorkDir}
|
|
s.roots[req.DocID] = root
|
|
}
|
|
if req.WorkDir != "" {
|
|
root.WorkDir = req.WorkDir
|
|
}
|
|
if req.ProjectID > 0 {
|
|
root.ProjectID = req.ProjectID
|
|
}
|
|
|
|
if root.LedgerVersionID == nil {
|
|
if !req.AllowCreateBaseline {
|
|
return nil, ErrNoOpenVersion
|
|
}
|
|
snap := s.putSnapshotLocked(req.DocID, canon, sha, nil, SourceHuman, &req.ActorUserID, "", req.ActorSnapshot, "", req.ClientBlockMap)
|
|
ver := s.openVersionLocked(req.DocID, OriginTemplate, snap.ID, snap.ID, "")
|
|
root.LedgerVersionID = &ver.ID
|
|
root.HeadSnapshotID = &snap.ID
|
|
root.HeadRevisionNo = 0
|
|
root.ContentSHA256 = sha
|
|
root.RowRevision++
|
|
ev := s.putEventLocked(req.DocID, ver.ID, 1, nil, snap.ID, ChangeImport, ActorHuman, &req.ActorUserID, "", req.ActorSnapshot, nil, req.EditSessionID, req.RequestID, req.IdempotencyKey)
|
|
rep := s.formalCommitReportLocked(ctx, req.DocID, 0, snap.ID, nil)
|
|
if err := s.persistHumanRevisionLocked(ctx, root, ver, snap, ev, true, rep); err != nil {
|
|
s.forgetDocLocked(req.DocID)
|
|
return nil, err
|
|
}
|
|
st := s.mustStateLocked(req.DocID)
|
|
return &HumanRevisionResult{State: *st, Event: *ev, Report: rep}, nil
|
|
}
|
|
|
|
ver := s.versions[*root.LedgerVersionID]
|
|
if ver.Status != VersionOpen {
|
|
return nil, ErrNoOpenVersion
|
|
}
|
|
if req.ExpectedHeadSnapshot != nil && *req.ExpectedHeadSnapshot != ver.HeadSnapshotID {
|
|
cur := s.snapshots[ver.HeadSnapshotID]
|
|
return &HumanRevisionResult{Conflict: &ConflictInfo{
|
|
Code: ErrCodeHeadConflict, BaseSnapshotID: ver.BaselineSnapshotID,
|
|
CurrentSnapshotID: ver.HeadSnapshotID, CurrentRevisionNo: root.HeadRevisionNo, CurrentSHA256: cur.ContentSHA256,
|
|
}}, ErrHeadConflict
|
|
}
|
|
if req.ExpectedRevisionNo != nil && *req.ExpectedRevisionNo != root.HeadRevisionNo {
|
|
cur := s.snapshots[ver.HeadSnapshotID]
|
|
return &HumanRevisionResult{Conflict: &ConflictInfo{
|
|
Code: ErrCodeHeadConflict, BaseSnapshotID: ver.BaselineSnapshotID,
|
|
CurrentSnapshotID: ver.HeadSnapshotID, CurrentRevisionNo: root.HeadRevisionNo, CurrentSHA256: cur.ContentSHA256,
|
|
}}, ErrHeadConflict
|
|
}
|
|
|
|
parentID := ver.HeadSnapshotID
|
|
parent := s.snapshots[parentID]
|
|
if parent.ContentSHA256 == sha {
|
|
st := s.mustStateLocked(req.DocID)
|
|
overlay := s.manualOverlayLocked(req.DocID)
|
|
return &HumanRevisionResult{State: *st, Overlay: overlay}, nil
|
|
}
|
|
|
|
prevBlocks := parent.BlockMap
|
|
snap := s.putSnapshotLocked(req.DocID, canon, sha, &parentID, SourceHuman, &req.ActorUserID, "", req.ActorSnapshot, "", req.ClientBlockMap)
|
|
if len(req.ClientBlockMap) == 0 {
|
|
snap.BlockMap = BuildBlockMap(canon, prevBlocks)
|
|
} else {
|
|
snap.BlockMap = BuildBlockMap(canon, req.ClientBlockMap)
|
|
}
|
|
diff := ComputeDiff(s.blobs[parent.ContentSHA256].ContentMarkdown, canon, prevBlocks, snap.BlockMap)
|
|
ver.HeadSnapshotID = snap.ID
|
|
ver.HumanRevisionCount++
|
|
root.HeadSnapshotID = &snap.ID
|
|
root.HeadRevisionNo++
|
|
root.ContentSHA256 = sha
|
|
root.RowRevision++
|
|
eventNo := s.nextEventNoLocked(req.DocID)
|
|
ev := s.putEventLocked(req.DocID, ver.ID, eventNo, &parentID, snap.ID, ChangeHumanRevision, ActorHuman, &req.ActorUserID, "", req.ActorSnapshot, diff.Hunks, req.EditSessionID, req.RequestID, req.IdempotencyKey)
|
|
rep := s.formalCommitReportLocked(ctx, req.DocID, parentID, snap.ID, diff.Hunks)
|
|
if err := s.persistHumanRevisionLocked(ctx, root, ver, snap, ev, false, rep); err != nil {
|
|
s.forgetDocLocked(req.DocID)
|
|
return nil, err
|
|
}
|
|
st := s.mustStateLocked(req.DocID)
|
|
baseMD := s.blobs[s.snapshots[ver.BaselineSnapshotID].ContentSHA256].ContentMarkdown
|
|
overlay := ComputeDiff(baseMD, canon, s.snapshots[ver.BaselineSnapshotID].BlockMap, snap.BlockMap)
|
|
overlay.FromSnapshotID = ver.BaselineSnapshotID
|
|
overlay.ToSnapshotID = snap.ID
|
|
return &HumanRevisionResult{State: *st, Event: *ev, Overlay: overlay, Report: rep}, nil
|
|
}
|
|
|
|
func (s *Service) OpenTemplateVersion(ctx context.Context, docID string, markdown string, actor ActorSnapshot, projectID int64, workDir string) (*RevisionState, error) {
|
|
if err := requireDocID(docID); err != nil {
|
|
return nil, err
|
|
}
|
|
canon := CanonicalizeMarkdown(markdown)
|
|
sha := SHA256Hex(canon)
|
|
g := s.lockDoc(docID)
|
|
defer g.Release()
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if err := s.hydrateDocLocked(ctx, docID); err != nil {
|
|
return nil, err
|
|
}
|
|
if root, ok := s.roots[docID]; ok && root.LedgerVersionID != nil {
|
|
return s.mustStateLocked(docID), nil
|
|
}
|
|
if s.usingPG() {
|
|
return s.openTemplatePGLocked(ctx, docID, canon, sha, actor, projectID, workDir)
|
|
}
|
|
snap := s.putSnapshotLocked(docID, canon, sha, nil, SourceTemplate, nil, "template", actor, "", nil)
|
|
ver := s.openVersionLocked(docID, OriginTemplate, snap.ID, snap.ID, "")
|
|
root := &DocRootPointers{
|
|
DocID: docID, Kind: "controlled", ProjectID: projectID, WorkDir: workDir,
|
|
LedgerVersionID: &ver.ID, HeadSnapshotID: &snap.ID, HeadRevisionNo: 0,
|
|
ContentSHA256: sha, LifecycleState: "drafting", RevisionSchemaVer: SchemaVersion,
|
|
}
|
|
s.roots[docID] = root
|
|
_ = s.putEventLocked(docID, ver.ID, 1, nil, snap.ID, ChangeImport, ActorTemplate, nil, "template", actor, nil, "", "", fmt.Sprintf("template:%s:%s", docID, sha))
|
|
return s.mustStateLocked(docID), nil
|
|
}
|