diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 0000000..e7fdef7 --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +8.4.2 diff --git a/.bcr/metadata.template.json b/.bcr/metadata.template.json new file mode 100644 index 0000000..134e923 --- /dev/null +++ b/.bcr/metadata.template.json @@ -0,0 +1,13 @@ +{ + "homepage": "https://github.com/skydio/revup", + "maintainers": [ + { + "email": "jerry@skydio.com", + "github": "skydio", + "name": "Jerry Zhang" + } + ], + "repository": ["github:skydio/revup"], + "versions": [], + "yanked_versions": {} +} diff --git a/.bcr/presubmit.yml b/.bcr/presubmit.yml new file mode 100644 index 0000000..bfd9a72 --- /dev/null +++ b/.bcr/presubmit.yml @@ -0,0 +1,11 @@ +matrix: + platform: ["debian11", "macos", "ubuntu2004"] + bazel: ["8.x"] + +tasks: + verify_targets: + name: Build the revup wheel + platform: ${{ platform }} + bazel: ${{ bazel }} + build_targets: + - "@revup//:revup" diff --git a/.bcr/source.template.json b/.bcr/source.template.json new file mode 100644 index 0000000..98a9be3 --- /dev/null +++ b/.bcr/source.template.json @@ -0,0 +1,5 @@ +{ + "url": "https://github.com/skydio/revup/releases/download/v{VERSION}/revup-v{VERSION}.tar.gz", + "integrity": "**leave this alone**", + "strip_prefix": "revup-{VERSION}" +} diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..98feca4 Binary files /dev/null and b/.coverage differ diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml new file mode 100644 index 0000000..c5d19a1 --- /dev/null +++ b/.github/workflows/bazel.yml @@ -0,0 +1,25 @@ +name: Bazel build + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ "*" ] + +jobs: + build: + runs-on: ubuntu-latest + name: bazel build //:revup + steps: + - name: Install pandoc + run: sudo apt-get install -y pandoc + - uses: actions/checkout@v4 + name: Checkout the repo + - uses: bazel-contrib/setup-bazel@0.9.1 + name: Set up Bazel + with: + bazelisk-cache: true + disk-cache: ${{ github.workflow }} + repository-cache: true + - name: Build the revup wheel + run: bazel build //:revup diff --git a/.gitignore b/.gitignore index 8a84de6..eee4f52 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ build/ revup.egg-info/ revup/man1/ __pycache__ +bazel-* +MODULE.bazel.lock diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000..b6cbfeb --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,197 @@ +load("@rules_license//rules:license.bzl", "license") +load("@rules_python//python:defs.bzl", "py_library") +load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") +load("@rules_shell//shell:sh_binary.bzl", "sh_binary") +load(":version.bzl", "REVUP_VERSION") + +package(default_applicable_licenses = [":license"]) + +license( + name = "license", + package_name = "revup", + license_kinds = ["@rules_license//licenses/spdx:MIT"], + license_text = "LICENSE", +) + +REVUP_DATE = "Apr 21, 2021" + +REVUP_VERSION_HASH = "main" + +# Man page sources, one per docs/*.md. +MAN_PAGES = [ + "amend", + "cherry-pick", + "config", + "restack", + "revup", + "upload", +] + +# Driver script that takes (name, version, date, input.md, output.1.gz) and +# emits the gzipped man page. Honors $M4 / $PANDOC env vars supplied below +# so the action runs hermetically off the prebuilt rules_m4 / pandoc archives. +sh_binary( + name = "build_manpage", + srcs = ["scripts/build_manpage.sh"], +) + +# Per-platform config settings used to pick a pandoc binary. +[ + config_setting( + name = "is_{}_{}".format(os, cpu), + constraint_values = [ + "@platforms//os:{}".format(os), + "@platforms//cpu:{}".format(cpu), + ], + ) + for os, cpu in [ + ("linux", "x86_64"), + ("linux", "arm64"), + ("macos", "x86_64"), + ("macos", "arm64"), + ("windows", "x86_64"), + ] +] + +# Per-platform pandoc binary, fetched in MODULE.bazel. +alias( + name = "pandoc", + actual = select({ + ":is_linux_x86_64": "@pandoc_linux_amd64//:bin/pandoc", + ":is_linux_arm64": "@pandoc_linux_arm64//:bin/pandoc", + ":is_macos_x86_64": "@pandoc_macos_amd64//:bin/pandoc", + ":is_macos_arm64": "@pandoc_macos_arm64//:bin/pandoc", + ":is_windows_x86_64": "@pandoc_windows_amd64//:pandoc.exe", + }), +) + +# Generate revup/man1/.1.gz from docs/.md. Uses a genrule (rather +# than run_binary) so that the m4 make-variable from rules_m4's toolchain +# expands correctly. +[ + genrule( + name = "man1_{}".format(page), + srcs = ["docs/{}.md".format(page)], + outs = ["revup/man1/{}.1.gz".format(page)], + cmd = ( + "M4=$(M4) PANDOC=$(execpath :pandoc) " + + "$(execpath :build_manpage) " + + "{page} {version} '{date}' $(SRCS) $@" + ).format( + date = REVUP_DATE, + page = page, + version = REVUP_VERSION, + ), + tools = [ + ":build_manpage", + ":pandoc", + ], + toolchains = ["@rules_m4//m4:current_m4_toolchain"], + ) + for page in MAN_PAGES +] + +filegroup( + name = "man1", + srcs = ["revup/man1/{}.1.gz".format(page) for page in MAN_PAGES], +) + +# Rewrite docs/images/* links in README.md to absolute GitHub URLs, matching +# the substitution that setup.py performs at sdist build time. +genrule( + name = "long_description", + srcs = ["README.md"], + outs = ["README.pypi.md"], + cmd = """\ +python3 - "$<" "$@" <<'PY' +import re, sys +src, dst = sys.argv[1], sys.argv[2] +text = open(src).read() +text = re.sub( + r"((?!PYPI_REMOVE).)*", + "", + text, + flags=re.MULTILINE | re.DOTALL, +) +text = text.replace( + "docs/images/", + "https://raw.githubusercontent.com/skydio/revup/{hash}/docs/images/", +) +open(dst, "w").write(text) +PY +""".format(hash = REVUP_VERSION_HASH), +) + +py_library( + name = "revup_lib", + srcs = glob(["revup/**/*.py"]), + data = [":man1"], + imports = ["."], + visibility = ["//visibility:public"], +) + +py_package( + name = "revup_pkg", + packages = ["revup"], + deps = [":revup_lib"], +) + +py_wheel( + name = "revup", + abi = "none", + author = "Jerry Zhang", + author_email = "jerry@skydio.com", + classifiers = [ + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Version Control", + "Topic :: Software Development :: Version Control :: Git", + "Programming Language :: Python", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3 :: Only", + "Environment :: Console", + "Typing :: Typed", + ], + description_content_type = "text/markdown", + description_file = ":long_description", + distribution = "revup", + entry_points = { + "console_scripts": ["revup = revup.__main__:_main"], + }, + extra_distinfo_files = { + "//:LICENSE": "LICENSE", + }, + homepage = "https://github.com/skydio/revup", + license = "MIT", + platform = "any", + project_urls = { + "Source": "https://github.com/skydio/revup", + "Bug Tracker": "https://github.com/skydio/revup/issues", + }, + python_requires = ">=3.10", + python_tag = "py3", + requires = [ + "aiohttp", + "argcomplete", + "async_lru", + "attrs", + "requests", + "rich", + "multidict", + "yarl", + "async_timeout", + "charset_normalizer", + "aiosignal", + ], + summary = "Revolutionary github tools. Effortlessly create multiple branches and pull requests.", + version = REVUP_VERSION, + visibility = ["//visibility:public"], + deps = [":revup_pkg"], +) diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..f2f6869 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,74 @@ +module( + name = "revup", + # Keep this in sync with revup/version.py REVUP_VERSION. MODULE.bazel does + # not support `load`, so the value cannot be imported from version.bzl. + version = "0.4.0", + compatibility_level = 1, +) + +bazel_dep(name = "bazel_skylib", version = "1.7.1") +bazel_dep(name = "platforms", version = "0.0.10") +bazel_dep(name = "rules_license", version = "1.0.0") +bazel_dep(name = "rules_m4", version = "0.3.bcr.1") +bazel_dep(name = "rules_python", version = "0.36.0") +bazel_dep(name = "rules_shell", version = "0.3.0") + +# Pandoc isn't on BCR, so we fetch the official prebuilt binaries directly. +# One http_archive per (os, arch); BUILD.bazel selects the right one. +http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +PANDOC_VERSION = "3.10" + +_PANDOC_BUILD = """\ +exports_files(["bin/pandoc"], visibility = ["//visibility:public"]) +""" + +_PANDOC_WINDOWS_BUILD = """\ +exports_files(["pandoc.exe"], visibility = ["//visibility:public"]) +""" + +http_archive( + name = "pandoc_linux_amd64", + url = "https://github.com/jgm/pandoc/releases/download/{v}/pandoc-{v}-linux-amd64.tar.gz".format(v = PANDOC_VERSION), + sha256 = "e0f8af62d0f267d22baa5bcefe6d5dda3a097ccc60de794b759fe03159923244", + strip_prefix = "pandoc-{}".format(PANDOC_VERSION), + build_file_content = _PANDOC_BUILD, +) + +http_archive( + name = "pandoc_linux_arm64", + url = "https://github.com/jgm/pandoc/releases/download/{v}/pandoc-{v}-linux-arm64.tar.gz".format(v = PANDOC_VERSION), + sha256 = "55413dfb0c1aec861641fe858f1f73e84848f3db497b1c0c02e62887ea76f4a4", + strip_prefix = "pandoc-{}".format(PANDOC_VERSION), + build_file_content = _PANDOC_BUILD, +) + +http_archive( + name = "pandoc_macos_amd64", + url = "https://github.com/jgm/pandoc/releases/download/{v}/pandoc-{v}-x86_64-macOS.zip".format(v = PANDOC_VERSION), + sha256 = "6334f4d9af7c9e37e761dfad56fa5507685f6d29724ebf31c4be6d5c654a3161", + strip_prefix = "pandoc-{}-x86_64".format(PANDOC_VERSION), + build_file_content = _PANDOC_BUILD, +) + +http_archive( + name = "pandoc_macos_arm64", + url = "https://github.com/jgm/pandoc/releases/download/{v}/pandoc-{v}-arm64-macOS.zip".format(v = PANDOC_VERSION), + sha256 = "d9cad01d96ae774a0dc8c8c45bb1ad3e4c5ff2cc2e24f45958f5f9b7974aee34", + strip_prefix = "pandoc-{}-arm64".format(PANDOC_VERSION), + build_file_content = _PANDOC_BUILD, +) + +http_archive( + name = "pandoc_windows_amd64", + url = "https://github.com/jgm/pandoc/releases/download/{v}/pandoc-{v}-windows-x86_64.zip".format(v = PANDOC_VERSION), + sha256 = "bb808d00fd58762299d64582a9b4c3e4b106cd929e62c5f19bcdcb496f1e54ae", + strip_prefix = "pandoc-{}".format(PANDOC_VERSION), + build_file_content = _PANDOC_WINDOWS_BUILD, +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain( + is_default = True, + python_version = "3.12", +) diff --git a/Makefile b/Makefile index 3fc6a08..84ab558 100644 --- a/Makefile +++ b/Makefile @@ -35,18 +35,8 @@ clean: rm -rf $(BUILD_DIR) rm -rf .mypy_cache -REVUP_VERSION:=$(shell $(PYTHON) revup/__init__.py) +REVUP_VERSION:=$(shell $(PYTHON) revup/version.py) REVUP_DATE ?= Apr 21, 2021 -define REVUP_HEADER ---- -title: TITLE -section: 1 -header: Revup Manual -footer: revup VERSION -date: DATE ---- -endef -export REVUP_HEADER REVUP_VERSION_HASH?=${shell git rev-parse --short v$(REVUP_VERSION) || echo main} @@ -66,13 +56,11 @@ upload: $(PYTHON) -m twine upload build/revup-$(REVUP_VERSION).tar.gz man: - mkdir -p revup/man1 ; \ - cd docs ; \ - for file in *.md ; do \ - CMD_NAME=`echo $${file} | awk -F'[.]' '{print $$1}'` ; \ - echo "$${REVUP_HEADER}" | m4 -DTITLE=$${CMD_NAME} -DVERSION=$(REVUP_VERSION) -DDATE="$(REVUP_DATE)" - | \ - cat - $${file} | pandoc -f markdown-smart -s -t man > ../revup/man1/$${CMD_NAME}.1 || exit 1 ; \ - gzip -n -f -k ../revup/man1/$${CMD_NAME}.1 || exit 1 ; \ + mkdir -p revup/man1 + @for src in docs/*.md ; do \ + name=$$(basename $${src} .md) ; \ + scripts/build_manpage.sh "$${name}" "$(REVUP_VERSION)" "$(REVUP_DATE)" \ + "$${src}" "revup/man1/$${name}.1.gz" || exit 1 ; \ done test: diff --git a/revup/__init__.py b/revup/__init__.py index 007c867..e69de29 100644 --- a/revup/__init__.py +++ b/revup/__init__.py @@ -1,5 +0,0 @@ -__version__ = "0.4.0" - -if __name__ == "__main__": - # So that Makefile can get the version without having to parse python - print(__version__) diff --git a/revup/revup.py b/revup/revup.py index 50d127e..2b1546d 100755 --- a/revup/revup.py +++ b/revup/revup.py @@ -8,7 +8,6 @@ import sys from typing import Any, List, Tuple -import revup from revup import config, git, logs, shell from revup.completion import ( ShellType, @@ -21,6 +20,7 @@ from revup.forge_utils import parse_forge_info from revup.topic_stack import PrBodySource from revup.types import RevupUsageException +from revup.version import REVUP_VERSION REVUP_CONFIG_ENV_VAR = "REVUP_CONFIG_PATH" CONFIG_FILE_NAME = ".revupconfig" @@ -47,9 +47,7 @@ def __call__(self, parser: Any, namespace: Any, values: Any, option_string: Any def make_toplevel_parser() -> RevupArgParser: revup_parser = RevupArgParser(add_help=False, prog="revup") revup_parser.add_argument("--help", "-h", action=HelpAction, nargs=0) - revup_parser.add_argument( - "--version", action="version", version=f"%(prog)s {revup.__version__}" - ) + revup_parser.add_argument("--version", action="version", version=f"%(prog)s {REVUP_VERSION}") revup_parser.add_argument("--proxy") revup_parser.add_argument("--forge-oauth", "--github-oauth") revup_parser.add_argument("--forge-url", "--github-url", default="github.com") diff --git a/revup/version.py b/revup/version.py new file mode 100644 index 0000000..c9de931 --- /dev/null +++ b/revup/version.py @@ -0,0 +1,5 @@ +REVUP_VERSION = "0.4.0" + +if __name__ == "__main__": + # Lets Makefile read the version without parsing a Python file. + print(REVUP_VERSION) diff --git a/scripts/build_manpage.sh b/scripts/build_manpage.sh new file mode 100755 index 0000000..ab47eb5 --- /dev/null +++ b/scripts/build_manpage.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Build a gzipped man page from a markdown source. +# +# Usage: build_manpage.sh +# +# Composes a pandoc-style YAML header with m4, prepends it to the markdown, +# pipes through pandoc, then gzip -n for reproducibility. Honors $M4 and +# $PANDOC env vars so Bazel can supply hermetic toolchains; falls back to PATH. +set -euo pipefail + +NAME=$1 +VERSION=$2 +DATE=$3 +INPUT=$4 +OUTPUT=$5 + +: "${M4:=m4}" +: "${PANDOC:=pandoc}" + +HEADER=$("$M4" \ + -DTITLE="$NAME" \ + -DVERSION="$VERSION" \ + -DDATE="$DATE" \ + <<'EOF' +--- +title: TITLE +section: 1 +header: Revup Manual +footer: revup VERSION +date: DATE +--- +EOF +) + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +printf '%s\n' "$HEADER" | cat - "$INPUT" \ + | "$PANDOC" -f markdown-smart -s -t man > "$TMP/page.1" +gzip -n -c "$TMP/page.1" > "$OUTPUT" diff --git a/setup.cfg b/setup.cfg index f05bc12..dbcf4ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = revup -version = attr: revup.__version__ +version = attr: revup.version.REVUP_VERSION author = Jerry Zhang author_email = jerry@skydio.com description = Revolutionary github tools. Effortlessly create multiple branches and pull requests. diff --git a/tests/test_version_parity.py b/tests/test_version_parity.py new file mode 100644 index 0000000..fcb4d1b --- /dev/null +++ b/tests/test_version_parity.py @@ -0,0 +1,35 @@ +"""The version is duplicated in revup/version.py and version.bzl because +MODULE.bazel cannot `load()` and Bazel needs a Starlark-readable form. This +test catches drift between the two.""" + +import re +from pathlib import Path + +from revup.version import REVUP_VERSION + + +def _read_starlark_version(path: Path) -> str: + text = path.read_text() + match = re.search(r'^REVUP_VERSION\s*=\s*"([^"]+)"', text, re.MULTILINE) + assert match, f"REVUP_VERSION not found in {path}" + return match.group(1) + + +def _read_module_bazel_version(path: Path) -> str: + text = path.read_text() + match = re.search(r'^\s*version\s*=\s*"([^"]+)"', text, re.MULTILINE) + assert match, f"module() version not found in {path}" + return match.group(1) + + +def test_version_files_match(): + repo_root = Path(__file__).parent.parent + bzl_version = _read_starlark_version(repo_root / "version.bzl") + module_version = _read_module_bazel_version(repo_root / "MODULE.bazel") + + assert REVUP_VERSION == bzl_version, ( + f"revup/version.py ({REVUP_VERSION}) and version.bzl ({bzl_version}) disagree." + ) + assert REVUP_VERSION == module_version, ( + f"revup/version.py ({REVUP_VERSION}) and MODULE.bazel ({module_version}) disagree." + ) diff --git a/version.bzl b/version.bzl new file mode 100644 index 0000000..ea7cca5 --- /dev/null +++ b/version.bzl @@ -0,0 +1,8 @@ +"""Single source of truth for the revup version number. + +This file is intentionally a real file (not a symlink) for Windows +compatibility. A test (tests/test_version_parity.py) ensures it stays in sync +with revup/version.py, which Python imports. +""" + +REVUP_VERSION = "0.4.0"