179 lines
4.6 KiB
Go
179 lines
4.6 KiB
Go
package docrevision
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
)
|
|
|
|
func (s *Service) GetRevisionState(ctx context.Context, docID string) (*RevisionState, error) {
|
|
if err := requireDocID(docID); err != nil {
|
|
return nil, err
|
|
}
|
|
// T20 B3: doc 级锁 → 全局内存锁;hydrate 的 PG RTT 只阻塞同文档。
|
|
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
|
|
}
|
|
root, ok := s.roots[docID]
|
|
if !ok || root == nil || root.LedgerVersionID == nil || root.HeadSnapshotID == nil {
|
|
return nil, ErrNotFound
|
|
}
|
|
ver := s.versions[*root.LedgerVersionID]
|
|
head := s.snapshots[*root.HeadSnapshotID]
|
|
if ver == nil || head == nil {
|
|
return nil, ErrNotFound
|
|
}
|
|
caps := []string{"read"}
|
|
if root.LifecycleState == "drafting" {
|
|
caps = append(caps, "edit")
|
|
}
|
|
if s.DiffUI {
|
|
caps = append(caps, "diff", "overlay", "history")
|
|
}
|
|
st := &RevisionState{
|
|
DocID: docID,
|
|
CurrentVersionNo: ver.VersionNo,
|
|
VersionStatus: ver.Status,
|
|
BaselineSnapshotID: ver.BaselineSnapshotID,
|
|
HeadSnapshotID: ver.HeadSnapshotID,
|
|
HeadRevisionNo: root.HeadRevisionNo,
|
|
HeadSHA256: head.ContentSHA256,
|
|
ManualChangeCount: ver.HumanRevisionCount,
|
|
Capabilities: caps,
|
|
ETag: fmt.Sprintf("%d:%d", ver.HeadSnapshotID, root.HeadRevisionNo),
|
|
HeadBlockMap: append([]BlockNode(nil), head.BlockMap...),
|
|
}
|
|
s.attachPendingCandidateLocked(st, docID)
|
|
return st, nil
|
|
}
|
|
|
|
func (s *Service) ListVersions(ctx context.Context, docID string) ([]Version, error) {
|
|
if err := requireDocID(docID); err != nil {
|
|
return nil, err
|
|
}
|
|
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
|
|
}
|
|
var out []Version
|
|
for _, v := range s.versions {
|
|
if v.DocID == docID {
|
|
out = append(out, *v)
|
|
}
|
|
}
|
|
for i := 0; i < len(out); i++ {
|
|
for j := i + 1; j < len(out); j++ {
|
|
if out[j].VersionNo < out[i].VersionNo {
|
|
out[i], out[j] = out[j], out[i]
|
|
}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Service) GetVersionContent(ctx context.Context, docID string, versionNo int, view string) (string, string, error) {
|
|
if err := requireDocID(docID); err != nil {
|
|
return "", "", err
|
|
}
|
|
g := s.lockDoc(docID)
|
|
defer g.Release()
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if err := s.hydrateDocLocked(ctx, docID); err != nil {
|
|
return "", "", err
|
|
}
|
|
var ver *Version
|
|
for _, v := range s.versions {
|
|
if v.DocID == docID && v.VersionNo == versionNo {
|
|
ver = v
|
|
break
|
|
}
|
|
}
|
|
if ver == nil {
|
|
return "", "", ErrNotFound
|
|
}
|
|
sid := ver.HeadSnapshotID
|
|
if view == "baseline" {
|
|
sid = ver.BaselineSnapshotID
|
|
}
|
|
snap := s.snapshots[sid]
|
|
if snap == nil || s.blobs[snap.ContentSHA256] == nil {
|
|
return "", "", ErrNotFound
|
|
}
|
|
return s.blobs[snap.ContentSHA256].ContentMarkdown, snap.ContentSHA256, nil
|
|
}
|
|
|
|
func (s *Service) DiffVersions(ctx context.Context, docID string, fromNo, toNo int, fromView, toView string) (*DiffResult, error) {
|
|
fromMD, _, err := s.GetVersionContent(ctx, docID, fromNo, fromView)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
toMD, _, err := s.GetVersionContent(ctx, docID, toNo, toView)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fromBlocks := BuildBlockMap(fromMD, nil)
|
|
toBlocks := BuildBlockMap(toMD, fromBlocks)
|
|
d := ComputeDiff(fromMD, toMD, fromBlocks, toBlocks)
|
|
return &d, nil
|
|
}
|
|
|
|
func (s *Service) ManualOverlay(ctx context.Context, docID string) (*DiffResult, error) {
|
|
if err := requireDocID(docID); err != nil {
|
|
return nil, err
|
|
}
|
|
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
|
|
}
|
|
d := s.manualOverlayLocked(docID)
|
|
return &d, nil
|
|
}
|
|
|
|
func (s *Service) GetCandidate(ctx context.Context, docID string, candidateID int64) (*AgentCandidate, error) {
|
|
if err := requireDocID(docID); err != nil {
|
|
return nil, err
|
|
}
|
|
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
|
|
}
|
|
c, ok := s.candidates[candidateID]
|
|
if !ok || c.DocID != docID {
|
|
return nil, ErrNotFound
|
|
}
|
|
cp := *c
|
|
return &cp, nil
|
|
}
|
|
|
|
func (s *Service) GetChangeReport(ctx context.Context, docID string, reportID int64) (*ChangeReport, error) {
|
|
if err := requireDocID(docID); err != nil {
|
|
return nil, err
|
|
}
|
|
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
|
|
}
|
|
r, ok := s.reports[reportID]
|
|
if !ok || r.DocID != docID {
|
|
return nil, ErrNotFound
|
|
}
|
|
cp := *r
|
|
return &cp, nil
|
|
}
|