Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ File an issue at
For a bug, include the command you ran, what you expected, what happened, and
the output of `markfluence --version`. Re-running with `--debug` often shows the
request that failed. Redact your site URL, username, and token if you'd rather
not share them — but say which Confluence flavor you're on (Cloud or Data
Center), since the API differs.
not share them.

## Development setup

Expand All @@ -30,7 +29,8 @@ make build # produces ./bin/markfluence
make test
```

Run `make` with no target for the annotated list of rules.
Run `make` with no target for the annotated list of rules. `make check` is the
one to remember — see below.

To exercise the binary against a real Confluence site, put a `.env` in the
working directory — see [`.env.example`](.env.example) and the
Expand Down
17 changes: 16 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ GOLANGCI_LINT = $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION)

COMPLETIONS_DIR ?= completions

.PHONY: help all build build-linux install completions lint vet fmt fmt-check test check regen-regressions clean
.PHONY: help all build build-linux install completions lint vet fmt fmt-check test check regen-regressions docs docs-check clean

help: ## Show this help
@echo "Available rules:"
Expand Down Expand Up @@ -62,13 +62,28 @@ check: ## Run every check CI runs, in CI's order -- the pre-flight before calli
@# lint would otherwise race to populate ./bin.
$(MAKE) vet
$(MAKE) fmt-check
$(MAKE) docs-check
$(MAKE) test
$(MAKE) build
$(MAKE) lint

regen-regressions: ## Regenerate the converter regression goldens
go test ./internal/convert -run TestRegression -update

docs: ## Regenerate docs/commands/ from each command's --help
go run ./tools/gendocs

docs-check: ## Fail if docs/commands/ disagrees with the binary's --help
@# docs/commands/ is generated and checked in, so the command reference is
@# browsable on GitHub without installing anything. A checked-in copy of
@# help text is only safe if it cannot drift, which is what this is for --
@# the same role fmt-check plays for formatting.
@tmp="$$(mktemp -d)"; trap 'rm -rf "$$tmp"' EXIT; \
go run ./tools/gendocs "$$tmp" >/dev/null 2>&1; \
if ! diff -rq docs/commands "$$tmp" >/dev/null 2>&1; then \
echo "docs/commands/ is out of date; run: make docs"; \
diff -ru docs/commands "$$tmp" | head -40; exit 1; fi

clean: ## Remove build artifacts (bin/, dist/, completions/, ./markfluence)
rm -rf $(LOCALBIN) dist $(COMPLETIONS_DIR) markfluence

Expand Down
1,327 changes: 162 additions & 1,165 deletions README.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions cmd/attachmentdownload/attachmentdownload.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ var Cmd = &cobra.Command{
"attachment, since the path comes from an attachment comment anyone who\n" +
"can edit the page controls.\n\n" +
"A file that already exists is skipped unless --force.",
Example: " # Every attachment, to the paths they were published from\n" +
" markfluence attachment-download 1234567890 --dest ./out\n\n" +
" # Just one, by its stored name\n" +
" markfluence attachment-download 1234567890 diagram.png --dest ./out\n\n" +
" # Ignore recorded paths and write everything flat\n" +
" markfluence attachment-download 1234567890 --dest ./out --flat\n",
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: completion.PageThenNames,
RunE: run,
Expand Down
4 changes: 4 additions & 0 deletions cmd/attachmentlist/attachmentlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ var Cmd = &cobra.Command{
"SOURCE is a dash when no source path is recorded: the attachment was\n" +
"uploaded by hand, or it was published before markfluence recorded one.\n" +
"Use --json, whose managed field tells those two apart.",
Example: " # Every attachment on a page\n" +
" markfluence attachment-list 1234567890\n\n" +
" # By the file that publishes to it\n" +
" markfluence attachment-list docs/foo.md\n",
Args: cobra.ExactArgs(1),
ValidArgsFunction: completion.MarkdownFiles,
RunE: run,
Expand Down
7 changes: 7 additions & 0 deletions cmd/attachmentupload/attachmentupload.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ var Cmd = &cobra.Command{
"Two files whose base names agree cannot both be uploaded to one page,\n" +
"since an attachment name is unique per page; that is refused rather\n" +
"than silently overwriting.",
Example: " # Upload one file, or several\n" +
" markfluence attachment-upload 1234567890 diagram.png\n" +
" markfluence attachment-upload 1234567890 report.pdf notes.txt\n\n" +
" # Store it under the path a markdown image would reference\n" +
" markfluence attachment-upload 1234567890 img.png --name assets/diagram.png\n\n" +
" # Re-upload even though the checksum matches\n" +
" markfluence attachment-upload 1234567890 diagram.png --force\n",
Args: cobra.MinimumNArgs(2),
ValidArgsFunction: completion.PageThenFiles,
RunE: run,
Expand Down
4 changes: 4 additions & 0 deletions cmd/check/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ var Cmd = &cobra.Command{
"resolves to a real heading in the current file, but can't be turned into\n" +
"an absolute URL until this file itself has a page_id -- resolved by this\n" +
"file's own first publish, nothing to fix.",
Example: " # Validate a batch of files\n" +
" markfluence check docs/*.md\n\n" +
" # Show the storage HTML a publish would send\n" +
" markfluence check --show-html docs/one-page.md\n",
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: completion.MarkdownFiles,
RunE: run,
Expand Down
13 changes: 13 additions & 0 deletions cmd/children/children.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ var Cmd = &cobra.Command{
"would show nothing for a folder that contains folders.\n\n" +
"A folder counts as a level: at the default --depth 1 a child folder\n" +
"appears as a row, and --depth 2 shows what is inside it.",
Example: " # Direct children of a page\n" +
" markfluence children 1234567890\n\n" +
" # Deeper, or the whole subtree\n" +
" markfluence children 1234567890 --depth 3\n" +
" markfluence children 1234567890 --depth all\n\n" +
" # By folder URL, or by the file that publishes to a page\n" +
" markfluence children \"https://org.atlassian.net/wiki/spaces/ENG/folder/1234567890\"\n" +
" markfluence children docs/index.md\n\n" +
" # A whole space, and every page and folder in it\n" +
" markfluence children --space ENG\n" +
" markfluence children --space ENG --depth all\n\n" +
" # Just the page ids\n" +
" markfluence children 1234567890 --json | jq -r '.results[] | select(.type==\"page\") | .id'\n",
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: completion.MarkdownFiles,
RunE: run,
Expand Down
45 changes: 37 additions & 8 deletions cmd/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,44 @@ var Cmd = &cobra.Command{
Use: "create FILE...",
Short: "Create new Confluence pages from markdown files",
Long: "Create new Confluence pages from markdown FILEs.\n\n" +
"The title comes from frontmatter, or from --title, which overrides it and\n" +
"requires a single FILE. The space comes from --space or frontmatter. The\n" +
"parent comes from --parent or frontmatter and may be a page or a Cloud\n" +
"folder -- give a folder's id the same way you would a page's. Page width\n" +
"defaults to max.\n\n" +
"Every file is checked first -- including converting it -- and if any would\n" +
"fail, nothing is created.\n" +
"Otherwise a content-less stub is reserved for each, parents-first, before\n" +
"any of them is converted -- so a link between two files in the same batch\n" +
"resolves regardless of which direction it points, or whether the two link\n" +
"to each other. A parent cycle among the given files is rejected instead.\n" +
"--title and --page-width override the frontmatter (--title requires\n" +
"a single FILE). Unless --no-persist is given, each created page's\n" +
"title/space/parent/page_id/page_width are written back into the frontmatter.",
"fail, nothing is created. A page_id that resolves to nothing is a failure\n" +
"too, not a fresh page: create will not publish a second copy and overwrite\n" +
"an id it cannot explain. Remove the page_id to create a new page, or\n" +
"correct it.\n\n" +
"Once every file passes, a content-less stub is reserved for each,\n" +
"parents-first, before any of them is converted -- so a link between two\n" +
"files in the same batch resolves regardless of which direction it points,\n" +
"or whether the two link to each other. A parent cycle among the given\n" +
"files is rejected instead. A run interrupted after the reserve phase\n" +
"leaves an empty page version behind rather than no page; every id is\n" +
"already written back, so a plain update finishes the job.\n\n" +
"A whole tree can be created in one pass: give each child a parent: that\n" +
"points at its parent's .md file, and creation is ordered parents-first\n" +
"with the real ids filled in.\n\n" +
"Unless --no-persist is given, each created page's\n" +
"title/space/parent/page_id/page_width/labels are written back into the\n" +
"frontmatter.\n\n" +
"--dry-run makes the same checks as a real run, so it exits non-zero on the\n" +
"same failures and one unpublishable file aborts the preview for the whole\n" +
"batch. To lint several files independently, use check instead.",
Example: " # Create one page in a space\n" +
" markfluence create docs/new_page.md --space ENG\n\n" +
" # Create it under an existing parent page or folder\n" +
" markfluence create docs/child.md --space ENG --parent 123456\n\n" +
" # Create a whole tree, hierarchy taken from each file's parent: path\n" +
" markfluence create docs/*.md --space ENG\n\n" +
" # Override the title and width for a single file\n" +
" markfluence create note.md --space ENG --title \"Ad-hoc note\" --page-width wide\n\n" +
" # Create without writing page_id back into the file\n" +
" markfluence create note.md --space ENG --no-persist\n\n" +
" # Preview everything, write nothing\n" +
" markfluence create docs/*.md --space ENG --dry-run",
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: completion.MarkdownFiles,
RunE: run,
Expand Down
8 changes: 8 additions & 0 deletions cmd/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ var Cmd = &cobra.Command{
"and not per space. Only attachments the page references are exported;\n" +
"--all-attachments takes everything on the page.\n\n" +
"This is the one-command form of `read` plus `attachment-download`.",
Example: " # One page and the attachments it uses\n" +
" markfluence export 1234567890 --dest ./out\n\n" +
" # The page and its whole subtree, hierarchy mirrored on disk\n" +
" markfluence export 1234567890 --depth all --dest out\n\n" +
" # A whole space; --depth is required for a space walk\n" +
" markfluence export --space ENG --depth all --dest out\n\n" +
" # Re-export a tree whose pages changed upstream\n" +
" markfluence export 1234567890 --depth all --dest out --force\n",
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: completion.MarkdownFiles,
RunE: run,
Expand Down
6 changes: 6 additions & 0 deletions cmd/find/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ var Cmd = &cobra.Command{
"A folder does not reserve a title, so a folder hit is never a reason\n" +
"a page cannot be created -- it is there to be found, not to warn.\n\n" +
"Finding nothing is a success: the command says so and exits 0.",
Example: " # Every page, archived page and folder with this exact title\n" +
" markfluence find \"Deploy runbook\"\n\n" +
" # Scoped to one space\n" +
" markfluence find \"Deploy runbook\" --space ENG\n\n" +
" # Just the current page ids\n" +
" markfluence find \"Deploy runbook\" --json | jq -r '.results[] | select(.type==\"page\") | .id'\n",
Args: cobra.ExactArgs(1),
// Nothing here is completable: a title is free text and a space key lives
// on the server, which completion may not go ask for.
Expand Down
20 changes: 17 additions & 3 deletions cmd/fix/fix.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,28 @@ var Cmd = &cobra.Command{
Use: "fix FILE...",
Short: "Reconcile each markdown file's frontmatter to its live Confluence page",
Long: "Reconcile each markdown file's frontmatter to its live Confluence page.\n\n" +
"Populates/refreshes page_id, space, parent, and page_width (and fills a\n" +
"missing title) from the live page. Each file is processed independently;\n" +
"the command exits non-zero if any file failed.\n\n" +
"Populates/refreshes page_id, space, parent, page_width and labels (and\n" +
"fills a missing title) from the live page. The page is located by page_id,\n" +
"or by searching for the title when page_id is absent. fix never creates,\n" +
"updates or moves pages -- it is read-only on the server. Each file is\n" +
"processed independently; the command exits non-zero if any file failed.\n\n" +
"It writes a file when a field changed, and also when the frontmatter keys\n" +
"are out of canonical order (title, space, parent, page_id, then the rest\n" +
"alphabetically), which is reported separately as reordered. --dry-run\n" +
"reports both without writing.\n\n" +
"Labels are reconciled even for a file with no labels: line, which is how\n" +
"you adopt a page somebody labeled in the UI. That is the one place fix\n" +
"fills in a field update would have left alone, because fix reconciles the\n" +
"file to the page rather than the page to the file.\n\n" +
"parent is written as the live page's parent id. In a tree written by\n" +
"`export --depth`, where parent points at the parent's own .md file,\n" +
"fix therefore replaces that path with an id -- consistent with\n" +
"reconciling to the live page, and worth knowing before running it over\n" +
"an exported tree.",
Example: " # Reconcile a batch of files to their live pages\n" +
" markfluence fix docs/*.md\n\n" +
" # Report what would change, write nothing\n" +
" markfluence fix docs/foo.md --dry-run",
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: completion.MarkdownFiles,
RunE: run,
Expand Down
11 changes: 10 additions & 1 deletion cmd/info/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,17 @@ var Cmd = &cobra.Command{
Use: "info PAGE",
Short: "Print metadata about a Confluence page",
Long: "Print metadata about a Confluence page.\n\n" +
"Id, title, status, space, parent, version, page width, labels, the\n" +
"created/updated author stamps, and the page URL. An empty field is\n" +
"omitted rather than printed blank.\n\n" +
"PAGE is a numeric page id, a Confluence page URL, or a markdown file\n" +
"whose frontmatter has a page_id.",
"whose frontmatter has a page_id.\n\n" +
"--properties also lists every one of the page's content properties, which\n" +
"is where Confluence keeps things like the page width.",
Example: " # By page id\n" +
" markfluence info 1234567890\n\n" +
" # By the file that publishes to it, with content properties\n" +
" markfluence info docs/foo.md --properties",
Args: cobra.ExactArgs(1),
ValidArgsFunction: completion.MarkdownFiles,
RunE: run,
Expand Down
23 changes: 21 additions & 2 deletions cmd/read/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,27 @@ var Cmd = &cobra.Command{
"PAGE is a numeric page id, a Confluence page URL (the modern\n" +
"/wiki/.../pages/<id>/... form or a legacy ?pageId=<id> URL), or a\n" +
"markdown file whose frontmatter has a page_id.\n\n" +
"The default markdown output carries title/space/parent/page_id/page_width\n" +
"frontmatter and is a best-effort inverse of what create/update publish.",
"It composes with shell redirection.\n\n" +
"--format markdown (the default) carries\n" +
"title/space/parent/page_id/labels/page_width frontmatter and is a\n" +
"best-effort inverse of what create/update publish. The Confluence API has\n" +
"no markdown representation, so the storage body is converted here:\n" +
"constructs markfluence emits round-trip faithfully, while editor-authored\n" +
"content degrades gracefully -- a macro markfluence does not map, and a\n" +
"column layout, pass through as raw storage tags with their bodies kept as\n" +
"readable markdown, so they publish back unchanged. Some transforms are\n" +
"lossy (a table cell colour outside the named swatches comes back as a\n" +
"literal hex), so this is a reading aid rather than a guaranteed source\n" +
"round-trip.\n\n" +
"--format storage prints the raw storage-format XHTML exactly as stored.",
Example: " # Markdown, with frontmatter, to stdout\n" +
" markfluence read 1234567890\n\n" +
" # Save it as a file you can edit and publish back\n" +
" markfluence read 1234567890 > page.md\n\n" +
" # The raw storage Confluence holds\n" +
" markfluence read 1234567890 --format storage > page.storage.xml\n\n" +
" # By URL\n" +
" markfluence read \"https://org.atlassian.net/wiki/spaces/ENG/pages/1234567890/Title\"",
Args: cobra.ExactArgs(1),
ValidArgsFunction: completion.MarkdownFiles,
RunE: run,
Expand Down
9 changes: 9 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ func reportSecurityWarning(msg string) {
// carried code (1 operational, 2 config/usage). Any other error is
// cobra-generated (bad args/flags): a usage error, printed as a human line or a
// JSON error object under --json, exiting 2.
// Root returns the root command, for tooling that needs to walk the command
// tree rather than run it -- currently only the docs generator, which renders
// every command's --help into docs/commands/.
//
// Exported for that one caller rather than left unexported with the generator
// living inside this package: a main() in here would be built into the binary,
// and a _test.go that writes files into the repo is not a test.
func Root() *cobra.Command { return rootCmd }

func Execute() {
// Detect --json before parsing so that even a flag-parse failure (which
// short-circuits PersistentPreRunE, where SetJSON normally runs) is reported
Expand Down
29 changes: 29 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,35 @@ func TestSubcommandsCompleteArgs(t *testing.T) {
}
}

// TestSubcommandsDocumentThemselves is what makes `--help` usable as the
// reference. The README used to carry a per-command section explaining each
// command's reasoning and showing worked invocations; that moved into the
// commands themselves (#102), so the README could stop being a second copy
// that drifts.
//
// The cost of that move is a new way to regress: a command added with a
// one-line Long and no Example leaves its behaviour documented nowhere at all,
// and nothing else would notice. A length floor rather than a non-empty check,
// because "Create pages." satisfies non-empty and explains nothing.
func TestSubcommandsDocumentThemselves(t *testing.T) {
rootCmd.InitDefaultCompletionCmd()
for _, c := range rootCmd.Commands() {
// Cobra's own commands document themselves.
if c.Name() == "help" || c.Name() == "completion" {
continue
}
if len(c.Long) < 120 {
t.Errorf("subcommand %q has a Long of %d chars; it is the reference for "+
"what the command does and why, so it needs more than a restated Short",
c.Name(), len(c.Long))
}
if c.Example == "" {
t.Errorf("subcommand %q has no Example; a worked invocation is what a "+
"reader wants first, and the README no longer carries one", c.Name())
}
}
}

// TestSecurityWarnerIsWired pins the one line that makes the .env permission
// warning exist at runtime. Everything else about it is tested in
// internal/client (the predicate) and internal/ui (the output), each against
Expand Down
4 changes: 4 additions & 0 deletions cmd/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ var Cmd = &cobra.Command{
"validate real --json output read that same embedded copy.\n\n"+
"The output is the schema document itself, so --json changes nothing here.",
jsonout.SchemaVersion),
Example: " # Save the schema\n" +
" markfluence schema > schema.json\n\n" +
" # Which commands emit a --json envelope\n" +
" markfluence schema | jq -r '.properties.command.enum | join(\" \")'\n",
Args: cobra.NoArgs,
// The command takes no arguments; without this, completion would offer every
// file in the directory.
Expand Down
8 changes: 8 additions & 0 deletions cmd/search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ var Cmd = &cobra.Command{
"Neither are folders, which have no text to match -- use `find` for\n" +
"both of those.\n\n" +
"Finding nothing is a success: the command says so and exits 0.",
Example: " # Full-text search; every word must appear somewhere\n" +
" markfluence search \"deploy runbook\"\n\n" +
" # Scoped, with a bigger page of results\n" +
" markfluence search \"deploy runbook\" --space ENG --limit 25\n\n" +
" # Every match, ids only\n" +
" markfluence search deploy --limit all --json | jq -r '.results[].id'\n\n" +
" # Raw CQL, passed through untouched\n" +
" markfluence search 'type = page and label = \"runbook\"' --cql\n",
Args: cobra.ExactArgs(1),
// A query is free text and a space key lives on the server, which completion
// may not go ask for.
Expand Down
Loading