#!/bin/bash # # Regenerates the generated pages of the gt (Gitea CLI) documentation from the # gitea/cli sources: one command reference page per command and help topic. # # Usage: # # ./update_cli_docs.sh develop docs, from the main branch # ./update_cli_docs.sh --released every documented release series # ./update_cli_docs.sh one ref into one directory # # --released needs no version list: the documented series are the # cli-docs_versioned_docs/version-/reference directories, and the tag # each one is generated from is the newest stable v.x tag of gitea/cli, # looked up through the Gitea API. A new series is documented by running # `make cut-version PRODUCT=cli VERSION=`, nothing here has to be edited. set -euo pipefail CLI_REMOTE="${CLI_REMOTE:-https://gitea.com/gitea/cli.git}" CLI_API="${CLI_API:-https://gitea.com/api/v1/repos/gitea/cli}" VERSIONED_DOCS="cli-docs_versioned_docs" SRC_DIR=".tmp/upstream-cli" OUT_DIR="$PWD/.tmp/cli-generated" TAGS_FILE=".tmp/cli-tags.txt" MARKER='Generated by update_cli_docs.sh from the gitea/cli sources, do not edit.' mkdir -p .tmp # prints every tag name of the cli repository, one per line list_cli_tags() { local page=1 names while :; do names="$(curl --silent --show-error --fail \ "$CLI_API/tags?limit=50&page=$page" | grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d '"' -f 4)" [ -n "$names" ] || return 0 printf '%s\n' "$names" page=$((page + 1)) done } # latest_stable_tag , e.g. "0.1" -> "v0.1.2", empty if the series has # no release yet. Pre-release tags (v0.1.0-rc1) are ignored on purpose. latest_stable_tag() { local series="$1" if [ ! -s "$TAGS_FILE" ]; then list_cli_tags > "$TAGS_FILE" fi { grep -E "^v${series//./\\.}(\.[0-9]+)+$" "$TAGS_FILE" || true; } | sed 's/^v//' | sort -t . -k1,1n -k2,2n -k3,3n | tail -n 1 | sed 's/^/v/' } # generate_pages , writes gt's markdown reference to $OUT_DIR/reference generate_pages() { local ref="$1" format=sha1 head # gitea/cli uses sha256 object names, which a fetch into a sha1 repository rejects head="$(git ls-remote "$CLI_REMOTE" HEAD | awk '$2 == "HEAD" { print $1; exit }')" [ "${#head}" -ne 64 ] || format=sha256 if [ -d "$SRC_DIR/.git" ] && [ "$(git -C "$SRC_DIR" rev-parse --show-object-format)" = "$format" ]; then git -C "$SRC_DIR" remote set-url origin "$CLI_REMOTE" else rm -rf "$SRC_DIR" git init --quiet --object-format="$format" "$SRC_DIR" git -C "$SRC_DIR" remote add origin "$CLI_REMOTE" fi git -C "$SRC_DIR" fetch --quiet --depth 1 origin "$ref" git -C "$SRC_DIR" checkout --quiet --detach FETCH_HEAD rm -rf "$OUT_DIR" (cd "$SRC_DIR" && go run ./tools/cli-docs --out "$OUT_DIR") } # Cobra names a page after the command path, gt_pr_review_list.md. On the site # a command with subcommands becomes a directory with an index page, so the # sidebar groups it, and the help topics, which gt.md does not list, go to help/. # command path of a cobra page name: gt_pr_review_list.md -> "pr review list" command_of() { local name="${1%.md}" name="${name#gt}" name="${name#_}" printf '%s\n' "${name//_/ }" } is_topic() { [[ " $TOPICS " == *" $1 "* ]] } # page path of a command, relative to the reference directory page_of() { local command="$1" if [ -z "$command" ]; then echo index.md elif is_topic "$command"; then echo "help/$command.md" elif grep -qx "$command .*" <<< "$COMMANDS_LIST"; then echo "${command// //}/index.md" else echo "${command// //}.md" fi } # sidebar position of a command among its siblings, in the alphabetical order of gt --help position_of() { local command="$1" parent if [ -z "$command" ]; then echo 0 return fi if is_topic "$command"; then awk -v topic="$command" '$0 == topic { print NR }' <<< "$TOPICS_LIST" return fi parent="${command% *}" [ "$parent" != "$command" ] || parent="" awk -v parent="$parent" -v command="$command" ' { p = $0 if (!sub(/ [^ ]+$/, "", p)) p = "" if (p == parent) { count++ if ($0 == command) { print count; exit } } }' <<< "$COMMANDS_LIST" } # write_page write_page() { local source="$1" target="$2" command page depth up title position description command="$(command_of "$(basename "$source")")" page="$(page_of "$command")" depth="$(tr -cd / <<< "$page" | wc -c | tr -d ' ')" up="" for ((i = 0; i < depth; i++)); do up="../$up"; done position="$(position_of "$command")" # the line below the heading is the short description of the command description="$(sed -n '3p' "$source")" if [ -z "$command" ]; then title="Command reference" elif is_topic "$command"; then title="gt help $command" else title="gt $command" fi mkdir -p "$target/$(dirname "$page")" { printf -- '---\nsidebar_position: %s\ndescription: "%s"\n---\n\n' "$position" "${description//\"/\\\"}" printf '# %s\n\n\n\n' "$title" "$MARKER" if [ -z "$command" ]; then cat <<'EOF' Every `gt` command and flag, generated from the `--help` output of gt's main branch or release. Global flags work with every command; [gt help environment](help/environment.md) describes them together with the environment variables gt reads. EOF fi # drop the cobra heading, move the other headings one level up, fence the # synopsis and point the links between pages at their new paths. The # synopsis is help text written for a terminal: its tables are indented by # two spaces and its placeholders, such as , would be read as HTML. COMMANDS_LIST="$COMMANDS_LIST" awk -v up="$up" -v topics="$TOPICS" ' BEGIN { n = split(ENVIRON["COMMANDS_LIST"], list, "\n") for (i = 1; i <= n; i++) { c = list[i] sub(/ [^ ]+$/, "", c) if (c != list[i]) parents[c] = 1 } m = split(topics, names, " ") for (i = 1; i <= m; i++) topic[names[i]] = 1 } function target(file, c, index_page) { c = file sub(/\.md$/, "", c) sub(/^gt_?/, "", c) gsub(/_/, " ", c) if (c == "") return up "index.md" if (c in topic) return up "help/" c ".md" index_page = c in parents gsub(/ /, "/", c) return up c (index_page ? "/index.md" : ".md") } function flush_synopsis( first, last, i) { first = 1 while (first <= lines && synopsis[first] == "") first++ last = lines while (last >= first && synopsis[last] == "") last-- if (first <= last) { print "```text" for (i = first; i <= last; i++) print synopsis[i] print "```" print "" } in_synopsis = 0 } NR == 1 && /^## / { skip = 1; next } skip && /^$/ { skip = 0; next } in_synopsis && !/^```/ && !/^### (Examples|Options|Options inherited from parent commands|SEE ALSO)$/ { synopsis[++lines] = $0 next } in_synopsis { flush_synopsis() } /^### Synopsis$/ { print "## Synopsis"; print ""; in_synopsis = 1; lines = 0; next } /^```/ { fenced = !fenced } !fenced && /^#+ / { sub(/^#/, "") } /^\* \[/ { sub(/\)\t - /, "): ") } { line = $0 out = "" while (match(line, /\]\(gt[^)]*\.md\)/)) { file = substr(line, RSTART + 2, RLENGTH - 3) out = out substr(line, 1, RSTART + 1) target(file) ")" line = substr(line, RSTART + RLENGTH) } print out line } END { if (in_synopsis) flush_synopsis() } ' "$source" } > "$target/$page" } # generate_reference generate_reference() { local ref="$1" target="$2" source command generate_pages "$ref" local reference="$OUT_DIR/reference" if [ ! -s "$reference/gt.md" ]; then echo "$CLI_REMOTE@$ref did not generate $reference/gt.md" >&2 exit 1 fi # every command, in the alphabetical order gt --help lists them in COMMANDS_LIST="$(for source in "$reference"/gt_*.md; do command_of "$(basename "$source")"; done | LC_ALL=C sort)" # the help topics are the pages gt.md does not link to TOPICS_LIST="$(for source in "$reference"/gt_*.md; do command="$(command_of "$(basename "$source")")" [[ "$command" == *" "* ]] && continue grep -qF "](gt_$command.md)" "$reference/gt.md" || echo "$command" done)" TOPICS="$(tr '\n' ' ' <<< "$TOPICS_LIST")" COMMANDS_LIST="$(grep -vxF -f <(printf '%s\n' $TOPICS) <<< "$COMMANDS_LIST" || true)" # remove the pages of an earlier run, so a removed command does not linger if [ -d "$target" ]; then { grep -rlF --include='*.md' "$MARKER" "$target" || true; } | while IFS= read -r source; do rm "$source"; done find "$target" -depth -type d -empty -delete fi mkdir -p "$target" write_page "$reference/gt.md" "$target" for source in "$reference"/gt_*.md; do write_page "$source" "$target" done if [ -n "$TOPICS" ]; then { printf -- '---\nsidebar_position: %s\ndescription: "%s"\n---\n\n' \ "$(($(grep -cv ' ' <<< "$COMMANDS_LIST") + 1))" \ "Guides built into gt, read with gt help ." printf '# Help topics\n\n\n\n' "$MARKER" printf 'Guides built into gt, read with `gt help `:\n\n' for command in $TOPICS; do printf -- '- [gt help %s](%s.md): %s\n' "$command" "$command" "$(sed -n '3p' "$reference/gt_$command.md")" done } > "$target/help/index.md" fi echo "wrote $(grep -rlF --include='*.md' "$MARKER" "$target" | wc -l | tr -d ' ') pages to $target from $CLI_REMOTE@$ref" } # regenerates every documented release series from its newest stable tag generate_released() { local dir series tag found=0 for dir in "$VERSIONED_DOCS"/version-*/reference; do [ -d "$dir" ] || continue found=1 series="${dir#"$VERSIONED_DOCS"/version-}" series="${series%/reference}" tag="$(latest_stable_tag "$series")" if [ -z "$tag" ]; then echo "no stable v$series tag in $CLI_API, skipping $dir" >&2 continue fi generate_reference "$tag" "$dir" done # before the first release only the develop docs exist, which is not an error if [ "$found" -eq 0 ]; then echo "no $VERSIONED_DOCS/version-*/reference directory, no released series to regenerate" fi } case "${1:-}" in --released) generate_released ;; -h | --help) sed -n '3,17s/^#\{1,2\} \{0,1\}//p' "$0" ;; *) generate_reference "${1:-main}" "${2:-cli-docs/reference}" ;; esac