package engine import ( "context" "errors" "fmt" "time" "git.pablomurad.com/pablo/patchwork/internal/diff" "git.pablomurad.com/pablo/patchwork/internal/doc" "git.pablomurad.com/pablo/patchwork/internal/store" ) // Retract hides a published revision from the feeds and the API, or brings // it back with undo. The revisions after it get their diffs recomputed, // because their base changes. func (e *Engine) Retract(ctx context.Context, pageID int64, number int, undo bool) error { return e.Store.Tx(ctx, false, func(q *store.Queries) error { r, err := q.RevisionByNumber(ctx, pageID, number) if err != nil { return fmt.Errorf("version %d: %w", number, err) } if r.Baseline() { return errors.New("the first version can't be retracted; it's the base everything else is compared with") } if undo { r.RetractedAt = nil } else { now := e.Now() r.RetractedAt = &now } if err := q.UpdateRevision(ctx, r); err != nil { return err } _, err = e.rebuildPage(ctx, q, pageID, true) return err }) } // Amend rewrites a published revision's note and, when kind isn't empty, // its kind. It's the CLI twin of editing the note in the file. func (e *Engine) Amend(ctx context.Context, pageID int64, number int, note, kind string) error { if kind != "" && !doc.Valid(kind, doc.Kinds) { return fmt.Errorf("kind %q is not one of %v", kind, doc.Kinds) } return e.Store.Tx(ctx, false, func(q *store.Queries) error { r, err := q.RevisionByNumber(ctx, pageID, number) if err != nil { return fmt.Errorf("version %d: %w", number, err) } if r.Baseline() { return errors.New("the first version has no public note to amend") } if note != "" { r.Note = note } if kind != "" { r.Kind = kind } r.UpdatedAt = e.Now() return q.UpdateRevision(ctx, r) }) } // RebuildDiffs recomputes cached diffs: all of them with force, otherwise // only those made by an older diff algorithm. pageID 0 means every page. // It returns how many revisions changed. func (e *Engine) RebuildDiffs(ctx context.Context, pageID int64, force bool) (int, error) { n := 0 err := e.Store.Tx(ctx, false, func(q *store.Queries) error { var ids []int64 if pageID != 0 { ids = []int64{pageID} } else { pages, err := q.Pages(ctx, e.Source, true) if err != nil { return err } for _, p := range pages { ids = append(ids, p.ID) } } for _, id := range ids { c, err := e.rebuildPage(ctx, q, id, force) if err != nil { return err } n += c } return nil }) return n, err } func (e *Engine) rebuildPage(ctx context.Context, q *store.Queries, pageID int64, force bool) (int, error) { revs, err := q.Revisions(ctx, pageID) if err != nil { return 0, err } changed := 0 var base *store.Snapshot for i := range revs { r := &revs[i] if r.RetractedAt != nil { continue } snap, err := q.Snapshot(ctx, r.SnapshotID) if err != nil { return changed, err } if base != nil && (force || r.DiffVersion != diff.Version) { before := r.DiffJSON e.setDiff(r, base, doc.Parse([]byte(snap.Raw), e.opts)) if r.DiffJSON != before { if err := q.UpdateRevision(ctx, r); err != nil { return changed, err } changed++ } } base = snap } return changed, nil } // Backfill imports an older copy of a file as a snapshot at a past time, // for building history from copies made before Patchwork ran. Copies must // be imported oldest first and before any newer snapshot of the page; // history can be extended into the past only at its start, never rewritten // in the middle. func (e *Engine) Backfill(ctx context.Context, relPath string, raw []byte, at time.Time) (Result, error) { if at.IsZero() { return Result{}, errors.New("backfill needs a time") } if at.After(e.Now()) { return Result{}, errors.New("backfill time is in the future") } var res Result err := e.Store.Tx(ctx, false, func(q *store.Queries) error { if p, err := q.PageByPath(ctx, e.Source, relPath); err == nil { last, err := q.LastSnapshot(ctx, p.ID) if err != nil { return err } if !at.After(last.ObservedAt) { return fmt.Errorf("%s already has a snapshot from %s; import older copies first", relPath, last.ObservedAt.Format(time.RFC3339)) } } else if !errors.Is(err, store.ErrNotFound) { return err } var err error res, err = e.ingest(ctx, q, Observation{Path: relPath, Raw: raw, At: at}) return err }) return res, err }