code.pablomurad.com

patchwork

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

cmd/patchwork/main.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
174
175
176
177
178
// Command patchwork publishes revision feeds for living pages.
package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"io"
	"log/slog"
	"os"
	"os/signal"

	"git.pablomurad.com/pablo/patchwork/internal/config"
	"git.pablomurad.com/pablo/patchwork/internal/engine"
	"git.pablomurad.com/pablo/patchwork/internal/source/folder"
	"git.pablomurad.com/pablo/patchwork/internal/store"
)

// version is set at build time with -ldflags "-X main.version=...".
var version = "dev"

const usage = `patchwork publishes revision feeds for living pages.

Usage:
  patchwork <command> [flags]

Commands:
  init           write a commented patchwork.yaml
  scan           scan the content folder once and apply the publish rules
  watch          keep scanning as files change
  serve          watch, and serve the API and feeds over HTTP
  export         write the API and feeds to a directory
  log            show a page's public history
  diff           show what changed in a revision
  retract        hide a published revision (or bring it back with -undo)
  amend          rewrite a published revision's note or kind
  snapshot       import an older copy of a page, to build history
  rebuild-diffs  recompute cached diffs
  backup         copy the database
  doctor         check the config, the content and the database
  health         check that a running serve answers (for containers)
  version        print the version

Every command except init and version reads patchwork.yaml from the
current directory, or the file named by --config or PATCHWORK_CONFIG.
Run "patchwork <command> -h" for a command's flags.
`

func main() {
	if len(os.Args) < 2 {
		fmt.Fprint(os.Stderr, usage)
		os.Exit(2)
	}
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	cmd, args := os.Args[1], os.Args[2:]
	var err error
	switch cmd {
	case "init":
		err = cmdInit(args)
	case "scan":
		err = cmdScan(ctx, args)
	case "watch":
		err = cmdWatch(ctx, args)
	case "log":
		err = cmdLog(ctx, args)
	case "diff":
		err = cmdDiff(ctx, args)
	case "doctor":
		err = cmdDoctor(ctx, args)
	case "serve":
		err = cmdServe(ctx, args)
	case "export":
		err = cmdExport(ctx, args)
	case "retract":
		err = cmdRetract(ctx, args)
	case "amend":
		err = cmdAmend(ctx, args)
	case "snapshot":
		err = cmdSnapshot(ctx, args)
	case "rebuild-diffs":
		err = cmdRebuildDiffs(ctx, args)
	case "backup":
		err = cmdBackup(ctx, args)
	case "health":
		err = cmdHealth(args)
	case "version", "-v", "--version":
		fmt.Println("patchwork", version)
	case "help", "-h", "--help":
		fmt.Print(usage)
	default:
		fmt.Fprintf(os.Stderr, "patchwork: unknown command %q\n\n%s", cmd, usage)
		os.Exit(2)
	}
	if err != nil {
		if errors.Is(err, flag.ErrHelp) {
			os.Exit(2)
		}
		if errors.Is(err, errProblems) {
			os.Exit(1)
		}
		fmt.Fprintln(os.Stderr, "patchwork:", err)
		os.Exit(1)
	}
}

// errProblems makes a command exit 1 after it has printed its own report.
var errProblems = errors.New("problems found")

func newFlags(name, args string) *flag.FlagSet {
	fs := flag.NewFlagSet(name, flag.ContinueOnError)
	fs.Usage = func() {
		fmt.Fprintf(fs.Output(), "Usage: patchwork %s %s\n", name, args)
		fs.PrintDefaults()
	}
	return fs
}

func configFlag(fs *flag.FlagSet) *string {
	def := os.Getenv("PATCHWORK_CONFIG")
	if def == "" {
		def = "patchwork.yaml"
	}
	return fs.String("config", def, "path to patchwork.yaml")
}

// app is what most commands need: config, store and engine.
type app struct {
	cfg    *config.Config
	store  *store.Store
	engine *engine.Engine
	log    *slog.Logger
}

func openApp(configPath string) (*app, error) {
	cfg, err := config.Load(configPath)
	if err != nil {
		return nil, err
	}
	st, err := store.Open(cfg.DB)
	if err != nil {
		return nil, err
	}
	e, err := engine.New(st, cfg)
	if err != nil {
		st.Close()
		return nil, err
	}
	return &app{cfg: cfg, store: st, engine: e, log: slog.New(slog.NewTextHandler(os.Stderr, nil))}, nil
}

func (a *app) Close() error { return a.store.Close() }

func (a *app) runner() *folder.Runner {
	m := folder.Matcher{Include: a.cfg.Source.Include, Exclude: a.cfg.Source.Exclude}
	return folder.NewRunner(a.cfg.Source.Path, m, a.engine, a.log)
}

func cmdInit(args []string) error {
	fs := newFlags("init", "[-o patchwork.yaml] [-force]")
	out := fs.String("o", "patchwork.yaml", "file to write")
	force := fs.Bool("force", false, "overwrite an existing file")
	if err := fs.Parse(args); err != nil {
		return err
	}
	if _, err := os.Stat(*out); err == nil && !*force {
		return fmt.Errorf("%s already exists (use -force to overwrite)", *out)
	}
	if err := os.WriteFile(*out, config.Example, 0o644); err != nil {
		return err
	}
	fmt.Printf("wrote %s; set site.base_url and source.path, then run: patchwork doctor\n", *out)
	return nil
}

var stdout io.Writer = os.Stdout