code.pablomurad.com

patchwork

Publishes the revisions of your pages as a feed, with notes and readable diffs, written in Go

internal/engine/admin.go raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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
}