code.pablomurad.com

patchwork

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

cmd/patchwork/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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
package main

import (
	"context"
	"fmt"
	"os"
	"path"
	"strconv"
	"strings"
	"time"
)

// pageAndNumber parses "<slug|path> <n>" arguments.
func (a *app) pageAndNumber(ctx context.Context, args []string) (int64, int, error) {
	if len(args) != 2 {
		return 0, 0, fmt.Errorf("want <slug|path> <version number>")
	}
	p, err := a.findPage(ctx, args[0])
	if err != nil {
		return 0, 0, err
	}
	n, err := strconv.Atoi(strings.TrimPrefix(args[1], "#"))
	if err != nil {
		return 0, 0, fmt.Errorf("version %q is not a number", args[1])
	}
	return p.ID, n, nil
}

func cmdRetract(ctx context.Context, args []string) error {
	fs := newFlags("retract", "[-undo] <slug|path> <n>")
	cfgPath := configFlag(fs)
	undo := fs.Bool("undo", false, "publish a retracted revision again")
	if err := fs.Parse(args); err != nil {
		return err
	}
	a, err := openApp(*cfgPath)
	if err != nil {
		return err
	}
	defer a.Close()
	id, n, err := a.pageAndNumber(ctx, fs.Args())
	if err != nil {
		return err
	}
	if err := a.engine.Retract(ctx, id, n, *undo); err != nil {
		return err
	}
	if *undo {
		fmt.Fprintf(stdout, "#%d is public again\n", n)
	} else {
		fmt.Fprintf(stdout, "#%d retracted: it's gone from the feeds and the API. Its text stays in the database; the current page is unchanged.\n", n)
	}
	return nil
}

func cmdAmend(ctx context.Context, args []string) error {
	fs := newFlags("amend", "-note TEXT [-kind KIND] <slug|path> <n>")
	cfgPath := configFlag(fs)
	note := fs.String("note", "", "new revision note")
	kind := fs.String("kind", "", "new kind: correction, expansion, position or reorganization")
	if err := fs.Parse(args); err != nil {
		return err
	}
	if *note == "" && *kind == "" {
		fs.Usage()
		return errProblems
	}
	a, err := openApp(*cfgPath)
	if err != nil {
		return err
	}
	defer a.Close()
	id, n, err := a.pageAndNumber(ctx, fs.Args())
	if err != nil {
		return err
	}
	if err := a.engine.Amend(ctx, id, n, *note, *kind); err != nil {
		return err
	}
	fmt.Fprintf(stdout, "#%d amended. The note in the file still wins on its next upload, so change it there too.\n", n)
	return nil
}

func cmdSnapshot(ctx context.Context, args []string) error {
	fs := newFlags("snapshot", "-path RELPATH -file COPY -at TIME")
	cfgPath := configFlag(fs)
	rel := fs.String("path", "", "the page's path inside the content folder")
	file := fs.String("file", "", "an older copy of that page")
	at := fs.String("at", "", "when that copy was current (RFC 3339, or YYYY-MM-DD)")
	if err := fs.Parse(args); err != nil {
		return err
	}
	if *rel == "" || *file == "" || *at == "" {
		fs.Usage()
		return errProblems
	}
	a, err := openApp(*cfgPath)
	if err != nil {
		return err
	}
	defer a.Close()
	t, err := time.Parse(time.RFC3339, *at)
	if err != nil {
		t, err = time.ParseInLocation("2006-01-02", *at, a.cfg.Location())
		if err != nil {
			return fmt.Errorf("-at %q: want RFC 3339 or YYYY-MM-DD", *at)
		}
	}
	raw, err := os.ReadFile(*file)
	if err != nil {
		return err
	}
	res, err := a.engine.Backfill(ctx, path.Clean(strings.ReplaceAll(*rel, `\`, "/")), raw, t)
	if err != nil {
		return err
	}
	fmt.Fprintln(stdout, formatResult(res))
	for _, w := range res.Warnings {
		fmt.Fprintf(os.Stderr, "  warning: %s\n", w)
	}
	return nil
}

func cmdRebuildDiffs(ctx context.Context, args []string) error {
	fs := newFlags("rebuild-diffs", "[-force] [slug|path]")
	cfgPath := configFlag(fs)
	force := fs.Bool("force", false, "recompute every diff, not only outdated ones")
	if err := fs.Parse(args); err != nil {
		return err
	}
	a, err := openApp(*cfgPath)
	if err != nil {
		return err
	}
	defer a.Close()
	var id int64
	if fs.NArg() == 1 {
		p, err := a.findPage(ctx, fs.Arg(0))
		if err != nil {
			return err
		}
		id = p.ID
	}
	n, err := a.engine.RebuildDiffs(ctx, id, *force)
	if err != nil {
		return err
	}
	fmt.Fprintf(stdout, "%d revision diff(s) updated\n", n)
	return nil
}

func cmdBackup(ctx context.Context, args []string) error {
	fs := newFlags("backup", "-to FILE")
	cfgPath := configFlag(fs)
	to := fs.String("to", "", "file to write (must not exist)")
	if err := fs.Parse(args); err != nil {
		return err
	}
	if *to == "" {
		fs.Usage()
		return errProblems
	}
	a, err := openApp(*cfgPath)
	if err != nil {
		return err
	}
	defer a.Close()
	if err := a.store.Backup(ctx, *to); err != nil {
		return err
	}
	fmt.Fprintf(stdout, "backed up %s to %s\n", a.cfg.DB, *to)
	return nil
}