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
// 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
  log       show a page's public history
  diff      show what changed in a revision
  doctor    check the config, the content and the database
  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 "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