diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index b961486..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Build & Test - -on: - push: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up Go - uses: actions/setup-go@v4 - with: - cache: true - - name: Download Go modules - run: go mod download - - name: Build - run: go build ./... - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - - name: Test - run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml deleted file mode 100644 index 137f988..0000000 --- a/.github/workflows/goreleaser.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Build, Test, Release - -on: - push: - tags: - - "*" - -permissions: - contents: write - -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up Go - uses: actions/setup-go@v4 - with: - cache: true - - name: Setup keepac - run: go build -o changelog . - - name: Generate Release Notes - run: ./changelog show --plain $GITHUB_REF_NAME | tail -n +3 > release-notes.md && cat release-notes.md - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v4 - with: - version: latest - args: release --clean --release-notes="release-notes.md" - env: - GITHUB_TOKEN: ${{ secrets.GH_PAT }} - # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution - # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} diff --git a/.github/workflows/record_and_publish_demos.yml b/.github/workflows/record_and_publish_demos.yml deleted file mode 100644 index 8fcc81c..0000000 --- a/.github/workflows/record_and_publish_demos.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Record Demo Workflows And Publish Them To GitHub Pages - -on: - push: - tags: - - "*" - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Pages - uses: actions/configure-pages@v3 - - uses: actions/setup-go@v4 - - id: go-cache-paths - run: | - echo "::set-output name=go-build::$(go env GOCACHE)" - echo "::set-output name=go-mod::$(go env GOMODCACHE)" - - name: Go Build Cache - uses: actions/cache@v2 - with: - path: ${{ steps.go-cache-paths.outputs.go-build }} - key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - - name: Go Mod Cache - uses: actions/cache@v2 - with: - path: ${{ steps.go-cache-paths.outputs.go-mod }} - key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - - name: Build keepac - run: go build -o changelog - - uses: charmbracelet/vhs-action@v1 - with: - path: "tapes/dummy.tape" - - name: Record tapes - run: ./scripts/record-tapes - - run: ls -alh ${{ github.workspace }}/tapes/recordings - - name: Upload artifact - uses: actions/upload-pages-artifact@v1 - with: - path: ${{ github.workspace }}/tapes/recordings - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v2 diff --git a/.gitignore b/.gitignore index 0687fbc..ea8c4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1 @@ -# Binaries -/changelog -/keepac - -# GoReleaser output -dist/ -completions/ - -# Gets generated during the goreleaser workflow and if it is not ignored, -# goreleaser will complain, that git is in a dirty state. -/release-notes.md +/target diff --git a/.goreleaser.yaml b/.goreleaser.yaml deleted file mode 100644 index 06811c1..0000000 --- a/.goreleaser.yaml +++ /dev/null @@ -1,80 +0,0 @@ -# This is an example .goreleaser.yml file with some sensible defaults. -# Make sure to check the documentation at https://goreleaser.com -before: - hooks: - # You may remove this if you don't use go modules. - - go mod tidy - # you may remove this if you don't need go generate - - go generate ./... - - ./scripts/generate-completions -builds: - - main: main.go - binary: changelog - env: - - CGO_ENABLED=0 - goos: - - linux - - windows - - darwin - -archives: - - format: tar.gz - # this name template makes the OS and Arch compatible with the results of uname. - name_template: >- - {{ .ProjectName }}_ - {{- title .Os }}_ - {{- if eq .Arch "amd64" }}x86_64 - {{- else if eq .Arch "386" }}i386 - {{- else }}{{ .Arch }}{{ end }} - {{- if .Arm }}v{{ .Arm }}{{ end }} - # use zip for windows archives - format_overrides: - - goos: windows - format: zip - files: - - completions/* - -checksum: - name_template: "checksums.txt" -snapshot: - name_template: "{{ incpatch .Version }}-next" -changelog: - sort: asc - filters: - exclude: - - "^docs:" - - "^test:" -brews: - - homepage: "https://github.com/NiclasvanEyk/keepac" - tap: - owner: NiclasvanEyk - name: keepac-homebrew-tap - install: |- - bin.install "changelog" - bash_completion.install "completions/changelog.bash" => "changelog" - zsh_completion.install "completions/changelog.zsh" => "_changelog" - fish_completion.install "completions/changelog.fish" -nfpms: - - section: utils - formats: - - apk - - deb - - rpm - - archlinux - contents: - - src: ./completions/changelog.bash - dst: /usr/share/bash-completion/completions/changelog - file_info: - mode: 0644 - - src: ./completions/changelog.fish - dst: /usr/share/fish/vendor_completions.d/changelog.fish - file_info: - mode: 0644 - - src: ./completions/changelog.zsh - dst: /usr/share/zsh/vendor-completions/_changelog - file_info: - mode: 0644 -# The lines beneath this are called `modelines`. See `:help modeline` -# Feel free to remove those if you don't want/use them. -# yaml-language-server: $schema=https://goreleaser.com/static/schema.json -# vim: set ts=2 sw=2 tw=0 fo=cnqoj diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index b84ae17..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,36 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [0.1.0] - 2024-02-11 - -### Added - -- The option to _manually_ specify the version when using the `release` sub-command using the a new `--version` option - -### Changed - -- Update to changelog 1.1.0 link in the init template - -### Fixed - -- Empty bullet points don't lead to panics when the inserting a new change -- Closing the editor or providing an empty response does not insert empty bulletpoints or sections anymore - -## [0.0.9] - 2023-10-17 - -### Added - -- `changelog path` as an alias of `changelog find` -- The column at which text wraps can be configured using the `KEEPAC_WRAP_AT` environment variable - -### Changed - -- The output now wraps after 85 characters - -### Fixed - -- `changelog release` omitting a few characters at the end of the printed summary diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7fdee83 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1078 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "cc" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap-verbosity-flag" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" +dependencies = [ + "clap", + "log", +] + +[[package]] +name = "clap_builder" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "unicase", + "unicode-width 0.2.0", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.95", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "coolor" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "691defa50318376447a73ced869862baecfab35f6aabaa91a4cd726b315bfe1a" +dependencies = [ + "crossterm", +] + +[[package]] +name = "crokey" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520e83558f4c008ac06fa6a86e5c1d4357be6f994cce7434463ebcdaadf47bb1" +dependencies = [ + "crokey-proc_macros", + "crossterm", + "once_cell", + "serde", + "strict", +] + +[[package]] +name = "crokey-proc_macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370956e708a1ce65fe4ac5bb7185791e0ece7485087f17736d54a23a0895049f" +dependencies = [ + "crossterm", + "proc-macro2", + "quote", + "strict", + "syn 1.0.109", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.42", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width 0.1.14", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "keepac" +version = "0.1.0" +dependencies = [ + "cc", + "pulldown-cmark", + "streaming-iterator", + "tempdir", + "termcolor", + "termimad", + "textwrap", + "thiserror 2.0.9", + "tree-sitter", + "tree-sitter-md", +] + +[[package]] +name = "keepac-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "clap-verbosity-flag", + "keepac", + "tempdir", + "termcolor", +] + +[[package]] +name = "lazy-regex" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.95", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimad" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2" +dependencies = [ + "once_cell", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags 2.6.0", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.95", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "strict" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand", + "remove_dir_all", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termimad" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a5d4cf55d9f1cb04fcda48f725772d0733ae34e030dfc4dd36e738a5965f4" +dependencies = [ + "coolor", + "crokey", + "crossbeam", + "lazy-regex", + "minimad", + "serde", + "thiserror 1.0.69", + "unicode-width 0.1.14", +] + +[[package]] +name = "terminal_size" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +dependencies = [ + "rustix 0.37.27", + "windows-sys 0.48.0", +] + +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", + "terminal_size", + "unicode-linebreak", + "unicode-width 0.1.14", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +dependencies = [ + "thiserror-impl 2.0.9", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.95", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.95", +] + +[[package]] +name = "tree-sitter" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2434c86ba59ed15af56039cc5bf1acf8ba76ce301e32ef08827388ef285ec5" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c199356c799a8945965bb5f2c55b2ad9d9aa7c4b4f6e587fe9dea0bc715e5f9c" + +[[package]] +name = "tree-sitter-md" +version = "0.4.0" +source = "git+https://github.com/tree-sitter-grammars/tree-sitter-markdown?branch=split_parser#192407ab5a24bfc24f13332979b5e7967518754a" +dependencies = [ + "cc", + "tree-sitter", + "tree-sitter-language", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d767148 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["crates/*"] +resolver = "2" diff --git a/Makefile b/Makefile deleted file mode 100644 index 05ce75c..0000000 --- a/Makefile +++ /dev/null @@ -1,28 +0,0 @@ -all: binary shell-completions - -uninstall: uninstall-binary uninstall-shell-completions - - -binary: build-binary install-binary - -build-binary: - go build -o changelog - -install-binary: - cp changelog "${HOME}/.local/bin/changelog" - -uninstall-binary: - rm "${HOME}/.local/bin/changelog" - - -shell-completions: generate-shell-completions install-shell-completions - -install-shell-completions: - cp ./completions/changelog.zsh /usr/local/share/zsh/site-functions/_changelog - -uninstall-shell-completions: - rm /usr/local/share/zsh/site-functions/_changelog - -generate-shell-completions: - ./scripts/generate-completions - diff --git a/cmd/diff.go b/cmd/diff.go deleted file mode 100644 index 8b39867..0000000 --- a/cmd/diff.go +++ /dev/null @@ -1,47 +0,0 @@ -package cmd - -import ( - clog "github.com/niclasvaneyk/keepac/internal/changelog" - "github.com/spf13/cobra" -) - -var ( - merge bool - prefix bool -) - -var diffCmd = &cobra.Command{ - Use: "diff [flags] from to", - Aliases: []string{"compare"}, - Short: "View the logs between two versions", - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - changelog, _, err := clog.ResolveChangelog() - if err != nil { - return err - } - - from, to := args[0], args[1] - - var contents string - if merge { - contents, err = changelog.Merge(from, to, prefix) - if err != nil { - return err - } - } else { - contents, err = changelog.Diff(from, to) - if err != nil { - return err - } - } - - return clog.Show(contents) - }, -} - -func init() { - rootCmd.AddCommand(diffCmd) - diffCmd.Flags().BoolVarP(&merge, "merged", "", false, "Merge sections into a single continuous") - diffCmd.Flags().BoolVarP(&prefix, "prefixed", "", false, "When --merged is passed, prefix each change with its version number") -} diff --git a/cmd/edit.go b/cmd/edit.go deleted file mode 100644 index f51f1b3..0000000 --- a/cmd/edit.go +++ /dev/null @@ -1,28 +0,0 @@ -package cmd - -import ( - clog "github.com/niclasvaneyk/keepac/internal/changelog" - "github.com/niclasvaneyk/keepac/internal/editor" - - "github.com/spf13/cobra" -) - -var editCmd = &cobra.Command{ - Use: "edit", - Short: "Opens the nearest changelog in a text editor.", - Long: `Opens the nearest changelog in a text editor. - - Honors the $EDITOR environment variable and falls back to xdg-open (linux), open (mac) or cmd /c start (windows).`, - RunE: func(cmd *cobra.Command, args []string) error { - filename, err := clog.ResolvePathToChangelog() - if err != nil { - return err - } - - return editor.Open(filename) - }, -} - -func init() { - rootCmd.AddCommand(editCmd) -} diff --git a/cmd/find.go b/cmd/find.go deleted file mode 100644 index 9290a7c..0000000 --- a/cmd/find.go +++ /dev/null @@ -1,33 +0,0 @@ -package cmd - -import ( - "fmt" - - clog "github.com/niclasvaneyk/keepac/internal/changelog" - - "github.com/spf13/cobra" -) - -var findCmd = &cobra.Command{ - Use: "find", - Aliases: []string{"path"}, - Short: "Attempts to find the nearest CHANGELOG.md file relative to the current working directory", - RunE: func(cmd *cobra.Command, args []string) error { - path, err := clog.ResolvePathToChangelog() - if err != nil { - return err - } - - if err != nil { - return err - } - - fmt.Println(path) - - return nil - }, -} - -func init() { - rootCmd.AddCommand(findCmd) -} diff --git a/cmd/init.go b/cmd/init.go deleted file mode 100644 index 14929ac..0000000 --- a/cmd/init.go +++ /dev/null @@ -1,50 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "path" - - clog "github.com/niclasvaneyk/keepac/internal/changelog" - - "github.com/spf13/cobra" -) - -var initCmd = &cobra.Command{ - Use: "init", - Short: "Creates a CHANGELOG.md with an empty [Unreleased] section", - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return err - } - - changelogPath, wasFound := clog.FindChangelogIn(cwd) - if wasFound { - return fmt.Errorf("changelog already exists at %s", changelogPath) - } - - changelogPath = path.Join(cwd, "CHANGELOG.md") - changelogContents := `# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] -` - - err = os.WriteFile(changelogPath, []byte(changelogContents), 0o774) - if err != nil { - return err - } - - fmt.Printf("Initialized empty changelog at %s:\n", changelogPath) - return clog.Show(changelogContents) - }, -} - -func init() { - rootCmd.AddCommand(initCmd) -} diff --git a/cmd/insert.go b/cmd/insert.go deleted file mode 100644 index efcd007..0000000 --- a/cmd/insert.go +++ /dev/null @@ -1,195 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "strings" - - clog "github.com/niclasvaneyk/keepac/internal/changelog" - "github.com/niclasvaneyk/keepac/internal/editor" - "github.com/niclasvaneyk/keepac/internal/tui" - - "github.com/spf13/cobra" -) - -var ( - changeTypeAdded bool - changeTypeChanged bool - changeTypeDeprecated bool - changeTypeRemoved bool - changeTypeFixed bool - changeTypeSecurity bool -) - -func runInsertCmd(changelog *clog.Changelog, args []string, filename string, changeType clog.ChangeType) error { - response, err := promptForDescription(args) - if err != nil { - return err - } - - newSource := changelog.AddItem(changeType, response) - err = os.WriteFile(filename, []byte(newSource), 0o774) - if err != nil { - return err - } - - return clog.Show(viewAfterInsertion(newSource, changeType)) -} - -func promptForDescription(args []string) (string, error) { - var response string - var err error - - if len(args) > 0 { - response = strings.Join(args, " ") - } else { - response, err = editor.Prompt("- ", "") - if err != nil { - return "", err - } - - if response == "-" { - return "", fmt.Errorf("no description provided") - } - } - - return normalized(response), err -} - -func viewAfterInsertion(newSource string, changeType clog.ChangeType) string { - newChangelog := clog.Parse([]byte(newSource)) - editedSection := newChangelog.Releases.Next.FindSection(changeType) - if editedSection == nil { - return "" - } - - items := make([]string, 0) - const MAX_ITEMS_SHOWN = 4 - - offset := 0 - if len(editedSection.Items) > MAX_ITEMS_SHOWN { - offset = 1 - items = append(items, "- ...") - } - - index := (len(editedSection.Items) - MAX_ITEMS_SHOWN) + offset - if index < 0 { - index = 0 - } - for ; len(items) < MAX_ITEMS_SHOWN && index < len(editedSection.Items); index++ { - bounds := editedSection.Items[index].Bounds - items = append(items, "- "+newChangelog.ContentWithin(&bounds)) - } - - headline := "### " + clog.ChangeTypeLabel(changeType) - - return headline + "\n" + strings.Join(items, "\n") -} - -var insertCmd = &cobra.Command{ - Use: "insert", - Aliases: []string{"i"}, - Short: "Inserts a new entry to a specified section of the next release", - Long: `Inserts a new entry to a specified section of the next release using your preferred editor. - -Honors the $EDITOR environment variable and falls back to xdg-open (linux), open (mac) or cmd /c start (windows). - -If you prefer using a shorter approach to using flags, there are a few shortcut commands available that -insert a new entry into a specific section (values in [] can be ommitted): -- add -- fix -- cha[nge] -- rem[ove] (rm is also available) -- dep[recate] -- sec[ure]`, - RunE: func(cmd *cobra.Command, args []string) error { - changelog, filename, err := clog.ResolveChangelog() - if err != nil { - return err - } - - var changeType clog.ChangeType - if changeTypeAdded { - changeType = clog.Added - } else if changeTypeChanged { - changeType = clog.Changed - } else if changeTypeDeprecated { - changeType = clog.Deprecated - } else if changeTypeRemoved { - changeType = clog.Removed - } else if changeTypeFixed { - changeType = clog.Fixed - } else if changeTypeSecurity { - changeType = clog.Security - } else { - changeType = chooseChangeType() - if changeType == clog.Unknown { - return nil - } - } - - return runInsertCmd(changelog, args, filename, changeType) - }, -} - -func init() { - rootCmd.AddCommand(insertCmd) - rootCmd.AddCommand(alias("add", nil, clog.Added)) - rootCmd.AddCommand(alias("fix", nil, clog.Fixed)) - rootCmd.AddCommand(alias("change", []string{"cha"}, clog.Changed)) - rootCmd.AddCommand(alias("remove", []string{"rm", "rem"}, clog.Removed)) - rootCmd.AddCommand(alias("deprecate", []string{"dep"}, clog.Deprecated)) - rootCmd.AddCommand(alias("secure", []string{"sec", "security"}, clog.Security)) - - insertCmd.Flags().BoolVarP(&changeTypeAdded, "added", "a", false, "Adds the change to the 'Added' section.") - insertCmd.Flags().BoolVarP(&changeTypeChanged, "changed", "c", false, "Adds the change to the 'Changed' section.") - insertCmd.Flags().BoolVarP(&changeTypeDeprecated, "deprecated", "d", false, "Adds the change to the 'Deprecated' section.") - insertCmd.Flags().BoolVarP(&changeTypeRemoved, "removed", "r", false, "Adds the change to the 'Removed' section.") - insertCmd.Flags().BoolVarP(&changeTypeFixed, "fixed", "f", false, "Adds the change to the 'Fixed' section.") - insertCmd.Flags().BoolVarP(&changeTypeSecurity, "security", "s", false, "Adds the change to the 'Security' section.") - insertCmd.MarkFlagsMutuallyExclusive("added", "changed", "deprecated", "removed", "fixed", "security") -} - -func chooseChangeType() clog.ChangeType { - choice, _ := tui.Choice("What type of change do you want to document?", []string{ - "Added", - "Changed", - "Deprecated", - "Removed", - "Fixed", - "Security", - }) - - return clog.ParseChangeType(choice) -} - -func normalized(response string) string { - normalized := strings.TrimSpace(response) - - if strings.HasPrefix(normalized, "- ") { - return normalized - } - - return "- " + normalized -} - -func alias(command string, aliases []string, changeType clog.ChangeType) *cobra.Command { - section := clog.ChangeTypeLabel(changeType) - - return &cobra.Command{ - Use: command, - Aliases: aliases, - Short: fmt.Sprintf(`Inserts a new entry into the "%s" section of the next release`, section), - Long: fmt.Sprintf(`Inserts a new entry into the "%s" section of the next release using your preferred editor. - - Honors the $EDITOR environment variable and falls back to xdg-open (linux), open (mac) or cmd /c start (windows).`, section), - RunE: func(cmd *cobra.Command, args []string) error { - changelog, filename, err := clog.ResolveChangelog() - if err != nil { - return err - } - - return runInsertCmd(changelog, args, filename, changeType) - }, - } -} diff --git a/cmd/insert_test.go b/cmd/insert_test.go deleted file mode 100644 index c82f3ea..0000000 --- a/cmd/insert_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package cmd - -import ( - "strings" - "testing" - - clog "github.com/niclasvaneyk/keepac/internal/changelog" - "gotest.tools/assert" -) - -func Test_viewAfterInsertion(t *testing.T) { - tests := []struct { - name string - changeType clog.ChangeType - newSource string - want string - }{ - { - name: "Truncated", - changeType: clog.Added, - newSource: ` -# Changelog - -## [Unreleased] - -### Added - -- First Entry -- Second Entry -- Third Entry -- Fourth Entry -- Fifth Entry -- Sixth Entry -- Seventh Entry -- New Entry -`, - want: ` -### Added -- ... -- Sixth Entry -- Seventh Entry -- New Entry -`, - }, - { - name: "First Entry", - changeType: clog.Added, - newSource: ` -# Changelog - -## [Unreleased] - -### Added - -- New Entry -`, - want: ` -### Added -- New Entry -`, - }, - { - name: "Max Items", - changeType: clog.Added, - newSource: ` -# Changelog - -## [Unreleased] - -### Added - -- First Entry -- Second Entry -- Third Entry -- New Entry -`, - want: ` -### Added -- First Entry -- Second Entry -- Third Entry -- New Entry -`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := viewAfterInsertion(tt.newSource, tt.changeType); got != tt.want { - expected := strings.TrimSpace(tt.want) - actual := strings.TrimSpace(got) - assert.Equal(t, expected, actual) - } - }) - } -} diff --git a/cmd/release.go b/cmd/release.go deleted file mode 100644 index 43633bf..0000000 --- a/cmd/release.go +++ /dev/null @@ -1,113 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "time" - - "github.com/blang/semver/v4" - clog "github.com/niclasvaneyk/keepac/internal/changelog" - "github.com/niclasvaneyk/keepac/internal/tui" - - "github.com/spf13/cobra" -) - -var ( - isMajor bool - isMinor bool - isPatch bool - manuallySpecifiedVersion string -) - -var releaseCmd = &cobra.Command{ - Use: "release", - Short: "Turns the [Unreleased] section into a proper release", - RunE: func(_ *cobra.Command, _ []string) error { - changelog, changelogPath, err := clog.ResolveChangelog() - if err != nil { - return err - } - - nextRelease := changelog.Releases.Next - if nextRelease == nil { - return fmt.Errorf("%s does not contain an [Unreleased] section", changelogPath) - } - - if !clog.HasChanges(&nextRelease.Sections) { - return fmt.Errorf("the [Unreleased] section of %s does not contain any changes", changelogPath) - } - - version := manuallySpecifiedVersion - if manuallySpecifiedVersion == "" { - version = getVersion(changelog) - } - timestamp := time.Now().Format(time.DateOnly) - newHeadline := fmt.Sprintf("[%s] - %s", version, timestamp) - - bounds := nextRelease.HeadlineBounds - newSource := changelog.ReplacedWithinBounds(bounds, newHeadline) - - err = os.WriteFile(changelogPath, []byte(newSource), 0o774) - if err != nil { - return err - } - - // Since we replaced the headline, simply using the old bounds would cut - // of some letters at the end - stop := nextRelease.Bounds.Stop + changelog.DiffLen(newSource) - relevantSection := newSource[nextRelease.Bounds.Start:stop] - - return clog.Show(relevantSection) - }, -} - -func init() { - rootCmd.AddCommand(releaseCmd) - - releaseCmd.Flags().BoolVar(&isMajor, "major", false, "Release a new major version") - releaseCmd.Flags().BoolVar(&isMinor, "minor", false, "Release a new minor version") - releaseCmd.Flags().BoolVar(&isPatch, "patch", false, "Release a new patch version") - releaseCmd.Flags().StringVarP(&manuallySpecifiedVersion, "version", "v", "", "Manually specify the a version") - releaseCmd.MarkFlagsMutuallyExclusive("major", "minor", "patch", "version") -} - -func getVersion(changelog *clog.Changelog) string { - var prev semver.Version - if len(changelog.Releases.Past) > 0 { - rawVersion := changelog.Releases.Past[0].Version - prev = semver.MustParse(rawVersion) - } else { - prev = semver.Version{Major: 0, Minor: 0, Patch: 0} - } - - nextMajor := semver.Version{Major: prev.Major + 1, Minor: 0, Patch: 0}.String() - nextMinor := semver.Version{Major: prev.Major, Minor: prev.Minor + 1, Patch: 0}.String() - nextPatch := semver.Version{Major: prev.Major, Minor: prev.Minor, Patch: prev.Patch + 1}.String() - - var index int - if !(isMajor || isMinor || isPatch) { - _, index = tui.Choice("What type of release do you want to create?", []string{ - fmt.Sprintf("Major (%s)", nextMajor), - fmt.Sprintf("Minor (%s)", nextMinor), - fmt.Sprintf("Patch (%s)", nextPatch), - }) - } else { - if isMajor { - index = 0 - } else if isMinor { - index = 1 - } else { - index = 2 - } - } - - if index == 0 { - return nextMajor - } - - if index == 1 { - return nextMinor - } - - return nextPatch -} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index 5d2f6e5..0000000 --- a/cmd/root.go +++ /dev/null @@ -1,34 +0,0 @@ -package cmd - -import ( - "os" - - "github.com/spf13/cobra" -) - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "changelog", - SilenceUsage: true, - Version: "0.0.6", - Long: `keepac provides useful tools for working with changelogs. - -You can show, search, compare or add new changes from anywhere within your project, -without the needing to open up any editors and manually inserting new markdown -sections.`, - RunE: showCmd.RunE, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } -} - -func init() { - rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) - rootCmd.CompletionOptions.HiddenDefaultCmd = true -} diff --git a/cmd/search.go b/cmd/search.go deleted file mode 100644 index 857661e..0000000 --- a/cmd/search.go +++ /dev/null @@ -1,34 +0,0 @@ -package cmd - -import ( - "fmt" - - clog "github.com/niclasvaneyk/keepac/internal/changelog" - "github.com/spf13/cobra" -) - -var searchCmd = &cobra.Command{ - Use: "search [query]", - Aliases: []string{"grep"}, - Short: "Searches for strings in the nearest changelog and prints matches within their context", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - changelog, _, err := clog.ResolveChangelog() - if err != nil { - return err - } - - query := args[0] - result := clog.Search(changelog, query) - - if result == "" { - fmt.Println("Nothing matched your query!") - } - - return clog.Show(result + "\n") - }, -} - -func init() { - rootCmd.AddCommand(searchCmd) -} diff --git a/cmd/show.go b/cmd/show.go deleted file mode 100644 index f1f92db..0000000 --- a/cmd/show.go +++ /dev/null @@ -1,130 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - clog "github.com/niclasvaneyk/keepac/internal/changelog" - - "github.com/spf13/cobra" -) - -var shouldShowPlain bool - -var showCmd = &cobra.Command{ - Use: "show [VERSION|latest|next|unreleased]", - Short: "Displays the contents of the nearest changelog.", - Long: `Displays the contents of the nearest changelog. - - If a VERSION (e.g. "1.2.3") is specified, only the release notes for that given version will be shown. - Instead of a specific version you can also use one of the following aliases: - - "latest" will show the latest release - - "next" or "unreleased" will show the contents of the [Unreleased] - `, - Args: cobra.MaximumNArgs(1), - ValidArgsFunction: func( - cmd *cobra.Command, - args []string, - toComplete string, - ) ([]string, cobra.ShellCompDirective) { - matchingVersions, directive := clog.CompleteReleasesAsFirstArgument(cmd, args, toComplete) - if matchingVersions == nil { - return matchingVersions, directive - } - - // We also support 'latest' as an alias - matchingVersions = append(matchingVersions, "latest") - matchingVersions = append(matchingVersions, "next") - matchingVersions = append(matchingVersions, "unreleased") - - return matchingVersions, cobra.ShellCompDirectiveNoFileComp - }, - RunE: func(cmd *cobra.Command, args []string) error { - path, err := clog.ResolvePathToChangelog() - if err != nil { - return err - } - - source, err := os.ReadFile(path) - if err != nil { - return err - } - - if len(args) == 0 { - if shouldShowPlain { - fmt.Print(string(source)) - return nil - } - - return clog.Show(string(source)) - } - - changelog := clog.Parse(source) - bounds, err := findReleaseBounds(args[0], &changelog) - if err != nil { - return err - } - - contents := changelog.ContentWithin(bounds) - - if shouldShowPlain { - fmt.Print(contents) - return nil - } - - return clog.Show(contents) - }, -} - -func init() { - rootCmd.AddCommand(showCmd) - showCmd.Flags().BoolVarP(&shouldShowPlain, "plain", "p", false, "Only print the raw contents, without terminal decorations") -} - -func isAlias(alias string) bool { - versionAliases := []string{ - "latest", - "next", - "unreleased", - } - for _, knownAlias := range versionAliases { - if alias == knownAlias { - return true - } - } - - return false -} - -func findReleaseBounds(versionOrAlias string, changelog *clog.Changelog) (*clog.Bounds, error) { - if isAlias(versionOrAlias) { - alias := versionOrAlias - - if alias == "latest" { - if len(changelog.Releases.Past) < 1 { - return nil, fmt.Errorf("cannot show latest release, since there are none") - } - - return &changelog.Releases.Past[0].Bounds, nil - } - - if alias == "next" || alias == "unreleased" { - nextRelease := changelog.Releases.Next - if nextRelease == nil { - return nil, fmt.Errorf("cannot show next release, since there is none") - } - - return &nextRelease.Bounds, nil - } - - return nil, fmt.Errorf("unknown version or alias '%s'", alias) - } - - version := versionOrAlias - release := changelog.FindRelease(version) - if release == nil { - return nil, fmt.Errorf("release '%s' not found", version) - } - - return &release.Bounds, nil -} diff --git a/cmd/yank.go b/cmd/yank.go deleted file mode 100644 index 5940586..0000000 --- a/cmd/yank.go +++ /dev/null @@ -1,68 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - - clog "github.com/niclasvaneyk/keepac/internal/changelog" -) - -var yankCmd = &cobra.Command{ - Use: "yank", - Aliases: []string{"yeet"}, - Short: "Marks the specified release as yanked", - Long: `As described by https://keepachangelog.com, yanked releases are versions that had to be pulled because of a serious bug or security issue.`, - Args: cobra.ExactArgs(1), - ValidArgsFunction: func( - cmd *cobra.Command, - args []string, - toComplete string, - ) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - - changelog, _, err := clog.ResolveChangelog() - if err != nil { - return nil, cobra.ShellCompDirectiveNoFileComp - } - - matchingVersions := make([]string, 0) - for _, release := range changelog.Releases.Past { - if !release.Yanked && strings.HasPrefix(release.Version, toComplete) { - matchingVersions = append(matchingVersions, release.Version) - } - } - - return matchingVersions, cobra.ShellCompDirectiveNoFileComp - }, - RunE: func(cmd *cobra.Command, args []string) error { - changelog, changelogPath, err := clog.ResolveChangelog() - if err != nil { - return err - } - - target := args[0] - - newSource, err := changelog.Yank(target) - if err != nil { - return err - } - - err = os.WriteFile(changelogPath, []byte(newSource), 0o774) - if err != nil { - return err - } - - fmt.Printf("Marked '%s' as yanked!\n", target) - - return nil - }, -} - -func init() { - rootCmd.AddCommand(yankCmd) -} diff --git a/crates/keepac-cli/Cargo.toml b/crates/keepac-cli/Cargo.toml new file mode 100644 index 0000000..88ea5dc --- /dev/null +++ b/crates/keepac-cli/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "keepac-cli" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "changelog" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.95" +clap = { version = "4.5.23", features = ["derive", "env", "unicode"] } +clap-verbosity-flag = "3.0.2" +keepac = { path = "../keepac/" } +termcolor = "1.4.1" + +[dev-dependencies] +tempdir = "0.3.7" diff --git a/crates/keepac-cli/src/commands/edit.rs b/crates/keepac-cli/src/commands/edit.rs new file mode 100644 index 0000000..a525e0a --- /dev/null +++ b/crates/keepac-cli/src/commands/edit.rs @@ -0,0 +1,43 @@ +use std::{ + env, io, + path::Path, + process::{Child, Command}, +}; + +use anyhow::anyhow; + +use crate::SubcommandResult; + +pub fn edit(path: &Path) -> SubcommandResult { + let Some(changelog_path) = keepac::find::nearest_changelog_path(path) else { + return Err(anyhow!("Failed to find CHANGELOG.md")); + }; + + let mut process = try_terminal_editor(&changelog_path) + .unwrap_or_else(|| try_system_editor(&changelog_path))?; + let exit_status = process.wait()?; + + if !exit_status.success() { + return Err(anyhow!("Editor exited with status {exit_status}")); + } + + Ok(()) +} + +pub fn try_terminal_editor(path: &Path) -> Option> { + Some( + Command::new(env::var_os("EDITOR")?) + .arg(path.to_str().unwrap()) + .spawn(), + ) +} + +pub fn try_system_editor(path: &Path) -> io::Result { + let path_str = path.to_str().unwrap(); + + match env::consts::OS { + "windows" => Command::new("cmd").args(["/c", "start", path_str]).spawn(), + "macos" => Command::new("open").arg(path_str).spawn(), + _ => Command::new("xdg-open").arg(path_str).spawn(), + } +} diff --git a/crates/keepac-cli/src/commands/find.rs b/crates/keepac-cli/src/commands/find.rs new file mode 100644 index 0000000..c49a03e --- /dev/null +++ b/crates/keepac-cli/src/commands/find.rs @@ -0,0 +1,15 @@ +use std::path::Path; + +use anyhow::anyhow; + +use crate::SubcommandResult; + +pub fn find(path: &Path) -> SubcommandResult { + match keepac::find::nearest_changelog_path(path) { + Some(changelog_path) => { + println!("{}", changelog_path.display()); + Ok(()) + } + None => Err(anyhow!("Failed to find CHANGELOG.md")), + } +} diff --git a/crates/keepac-cli/src/commands/init.rs b/crates/keepac-cli/src/commands/init.rs new file mode 100644 index 0000000..9fce1a4 --- /dev/null +++ b/crates/keepac-cli/src/commands/init.rs @@ -0,0 +1,68 @@ +use anyhow::anyhow; +use std::{fs::File, io::Write, path::Path}; +use termcolor::{BufferWriter, ColorChoice}; + +use crate::SubcommandResult; + +static TEMPLATE: &str = r#"# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +"#; + +pub(crate) fn init(path: &Path) -> SubcommandResult { + let changelog_path = path.join("CHANGELOG.md"); + if changelog_path.exists() { + return Err(anyhow!("CHANGELOG.md already exists")); + } + + let mut changelog = File::create_new(&changelog_path)?; + changelog.write_all(TEMPLATE.as_bytes())?; + + println!( + "Initialized empty changelog at {}:", + changelog_path.display() + ); + let writer = BufferWriter::stdout(ColorChoice::Always); + let mut buffer = writer.buffer(); + keepac::highlight::render(&mut buffer, TEMPLATE)?; + writer.print(&buffer)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::fs::File; + + use tempdir::TempDir; + + use super::*; + + #[test] + fn it_inits_if_empty() { + let dir = TempDir::new("it_inits_if_empty").unwrap(); + let path = dir.path(); + + let result = init(path); + assert!(result.is_ok()); + + let changelog_path = path.join("CHANGELOG.md"); + assert!(changelog_path.is_file()); + } + + #[test] + fn it_fails_if_changelog_already_exists() { + let dir = TempDir::new("it_fails_if_changelog_already_exists").unwrap(); + let path = dir.path(); + + let changelog_path = path.join("CHANGELOG.md"); + File::create(changelog_path).unwrap(); + + let result = init(path); + assert!(result.is_err()); + } +} diff --git a/crates/keepac-cli/src/commands/mod.rs b/crates/keepac-cli/src/commands/mod.rs new file mode 100644 index 0000000..af35608 --- /dev/null +++ b/crates/keepac-cli/src/commands/mod.rs @@ -0,0 +1,5 @@ +pub mod edit; +pub mod find; +pub mod init; +pub mod show; +pub mod versions; diff --git a/crates/keepac-cli/src/commands/show.rs b/crates/keepac-cli/src/commands/show.rs new file mode 100644 index 0000000..0609524 --- /dev/null +++ b/crates/keepac-cli/src/commands/show.rs @@ -0,0 +1,41 @@ +use std::path::Path; + +use keepac::Changelog; +use termcolor::{BufferWriter, ColorChoice}; + +use crate::SubcommandResult; + +#[derive(Debug, clap::Args)] +pub struct ShowCommandOptions { + /// An optional version to show the changes for + version: Option, + + /// An optional explicit path to a changelog file to use + #[arg(short, long)] + file: Option, +} + +impl Default for ShowCommandOptions { + fn default() -> Self { + ShowCommandOptions { + version: None, + file: None, + } + } +} + +pub fn show(path: &Path, options: ShowCommandOptions) -> SubcommandResult { + let changelog = match options.file { + Some(file) => Changelog::try_from_file_at(file)?, + None => Changelog::nearest(path)?, + }; + + let writer = BufferWriter::stdout(ColorChoice::Always); + let mut buffer = writer.buffer(); + keepac::highlight::render( + &mut buffer, + std::str::from_utf8(changelog.document.source.as_ref())?, + )?; + writer.print(&buffer)?; + Ok(()) +} diff --git a/crates/keepac-cli/src/commands/versions.rs b/crates/keepac-cli/src/commands/versions.rs new file mode 100644 index 0000000..3221fbd --- /dev/null +++ b/crates/keepac-cli/src/commands/versions.rs @@ -0,0 +1,36 @@ +use std::path::Path; + +use keepac::parse::parse_versions; +use keepac::Changelog; + +use crate::SubcommandResult; + +pub(crate) fn versions(path: &Path) -> SubcommandResult { + let changelog = Changelog::nearest(path)?; + let versions = parse_versions(&changelog); + + let mut last_seen_year: Option = None; + for version in versions { + if version.released_at.is_none() { + continue; + } + + let year = version + .released_at + .and_then(|released_at| released_at.split('-').next()) + .and_then(|year| year.parse::().ok()); + + if year != last_seen_year { + println!(); + last_seen_year = year; + } + + println!( + "{} {}", + version.released_at.unwrap_or(" "), + version.name + ); + } + + Ok(()) +} diff --git a/crates/keepac-cli/src/main.rs b/crates/keepac-cli/src/main.rs new file mode 100644 index 0000000..9424c6e --- /dev/null +++ b/crates/keepac-cli/src/main.rs @@ -0,0 +1,79 @@ +pub mod commands; + +use clap::{Parser, Subcommand}; +use commands::show::ShowCommandOptions; +use std::path::Path; + +#[derive(Debug, Parser)] +#[command( + version, + about = "keepac provides useful tools for working with changelogs.", + long_about = "You can show, search, compare or add new changes from anywhere within your project, without the needing to open up any editors and manually inserting new markdown sections." +)] +struct Cli { + #[command(flatten)] + verbose: clap_verbosity_flag::Verbosity, + + #[command(subcommand)] + command: Option, +} + +#[derive(Debug, Subcommand)] +enum Command { + Add {}, + Change {}, + Deprecate {}, + Diff {}, + Edit {}, + Find {}, + Fix {}, + Init {}, + Insert {}, + Release {}, + Remove {}, + Search {}, + Secure {}, + Show { + #[command(flatten)] + options: commands::show::ShowCommandOptions, + }, + Versions {}, + Yank {}, +} + +type SubcommandResult = Result<(), anyhow::Error>; + +fn run(cli: Cli, path: &Path) -> SubcommandResult { + let command = cli.command.unwrap_or(Command::Show { + options: ShowCommandOptions::default(), + }); + match command { + /* change */ Command::Add {} => todo!(), + /* change */ Command::Change {} => todo!(), + /* change */ Command::Deprecate {} => todo!(), + Command::Diff {} => todo!(), + Command::Edit {} => commands::edit::edit(path), + Command::Find {} => commands::find::find(path), + /* change */ Command::Fix {} => todo!(), + Command::Init {} => commands::init::init(path), + Command::Insert {} => todo!(), + Command::Release {} => todo!(), + /* change */ Command::Remove {} => todo!(), + Command::Search {} => todo!(), + /* change */ Command::Secure {} => todo!(), + Command::Show { options } => commands::show::show(path, options), + Command::Versions {} => commands::versions::versions(path), + Command::Yank {} => todo!(), + } +} + +fn main() { + let cwd = std::env::current_dir().unwrap(); + + let result = run(Cli::parse(), &cwd); + + if let Err(err) = result { + eprintln!("{}", err); + std::process::exit(1); + }; +} diff --git a/crates/keepac/Cargo.toml b/crates/keepac/Cargo.toml new file mode 100644 index 0000000..2a0c882 --- /dev/null +++ b/crates/keepac/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "keepac" +version = "0.1.0" +edition = "2021" + +[dev-dependencies] +tempdir = "0.3.7" +pulldown-cmark = "0.12.2" + +[build-dependencies] +cc = "*" + +[dependencies] +pulldown-cmark = "0.12.2" +streaming-iterator = "0.1" +termcolor = "1.4.1" +termimad = "0.31" +textwrap = { version = "0.16", features = ["terminal_size"] } +thiserror = "2.0" +tree-sitter = "0.24" +tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", branch = "split_parser", version = "0.4.0", features = ["parser"] } diff --git a/crates/keepac/src/changelog.rs b/crates/keepac/src/changelog.rs new file mode 100644 index 0000000..1e752de --- /dev/null +++ b/crates/keepac/src/changelog.rs @@ -0,0 +1,27 @@ +use std::{fs::File, path::Path}; + +use crate::markdown::MarkdownDocument; + +use self::from::{changelog_try_from_nearest, ChangelogFromPathError}; + +/// Implementations of [From] and [TryFrom] for [Changelog] from various structs. +mod from; + +pub struct Changelog<'a> { + pub document: MarkdownDocument<'a>, +} + +impl<'a> Changelog<'a> { + /// Tries to find and open a changelog near [path]. + /// If none is found, it recursively hikes the directory tree until a `CHANGELOG.md` file is + /// found. + pub fn nearest(path: &Path) -> Result { + changelog_try_from_nearest(path) + } + + pub fn try_from_file_at(path: String) -> Result, ChangelogFromPathError> { + Ok(Changelog::try_from(&mut File::open(Path::new(&path))?)?) + } + + // TODO: Version section iterator, maybe even structured in some way? +} diff --git a/crates/keepac/src/changelog/from.rs b/crates/keepac/src/changelog/from.rs new file mode 100644 index 0000000..1021e92 --- /dev/null +++ b/crates/keepac/src/changelog/from.rs @@ -0,0 +1,74 @@ +use crate::find::nearest_changelog_path; +use crate::markdown::MarkdownDocument; +use crate::parse::MarkdownParserError; +use crate::Changelog; +use std::io::Read; +use std::{fs::File, path::Path}; +use thiserror::Error; + +/// Get a Changelog from a string +impl<'s> TryFrom<&'s str> for Changelog<'s> { + type Error = MarkdownParserError; + + fn try_from(value: &'s str) -> Result { + Ok(Self { + document: MarkdownDocument::try_from(value)?, + }) + } +} + +/// Get a Changelog from a string +impl TryFrom for Changelog<'_> { + type Error = MarkdownParserError; + + fn try_from(value: String) -> Result { + Ok(Self { + document: MarkdownDocument::try_from(value)?, + }) + } +} + +#[derive(Error, Debug)] +pub enum ChangelogFromFileError { + #[error("Failed to read the contents of the changelog file")] + FileError(#[from] std::io::Error), + + #[error("Failed to parse the markdown file")] + MarkdownParserError(#[from] MarkdownParserError), +} + +/// Get a Changelog from a file +impl TryFrom<&mut File> for Changelog<'_> { + type Error = ChangelogFromFileError; + + fn try_from(value: &mut File) -> Result { + let mut source = String::new(); + value.read_to_string(&mut source)?; + + Ok(Changelog::try_from(source)?) + } +} + +#[derive(Error, Debug)] +pub enum ChangelogFromPathError { + #[error("Nearest changelog could not be found")] + NoChangelogFound, + + #[error("Failed to open the changelog file")] + FileError(#[from] std::io::Error), + + #[error("Failed to parse the markdown file")] + FailedToOpenChangelog(#[from] ChangelogFromFileError), +} + +pub(crate) fn changelog_try_from_nearest( + value: &Path, +) -> Result { + let Some(nearest) = nearest_changelog_path(value) else { + return Err(ChangelogFromPathError::NoChangelogFound); + }; + + let mut changelog_file = File::open(nearest)?; + + Ok(Changelog::try_from(&mut changelog_file)?) +} diff --git a/crates/keepac/src/find.rs b/crates/keepac/src/find.rs new file mode 100644 index 0000000..a28c7c5 --- /dev/null +++ b/crates/keepac/src/find.rs @@ -0,0 +1,89 @@ +/// Functions related to finding the CHANGELOG.md +use std::path::{Path, PathBuf}; + +/// Iterates upwards the directory tree, until a CHANGELOG.md file is found.CHANGELOG +/// +/// Right now, this is case-sensitive to improve performance. +pub fn nearest_changelog_path(origin: &Path) -> Option { + let mut current_directory = origin; + loop { + if let Some(changelog_path) = get_changelog_in(current_directory) { + return Some(changelog_path); + }; + + match current_directory.parent() { + Some(parent) => current_directory = parent, + None => return None, + } + } +} + +/// Checks whether there is a CHANGELOG.md in the current [directory]. +fn get_changelog_in(directory: &Path) -> Option { + let changelog_path = directory.join("CHANGELOG.md"); + if changelog_path.is_file() { + Some(changelog_path) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use std::fs::{create_dir, File}; + + use tempdir::TempDir; + + use super::*; + + #[test] + fn it_finds_if_present_in_the_current_directory() { + let dir = TempDir::new("it_finds_if_present_in_the_current_directory").unwrap(); + let path = dir.path(); + let actual_changelog_path = path.join("CHANGELOG.md"); + File::create(&actual_changelog_path).unwrap(); + + let found_changelog_path = nearest_changelog_path(path).unwrap(); + assert_eq!(actual_changelog_path, found_changelog_path); + } + + #[test] + fn it_finds_if_present_in_the_parent_directory() { + let root_dir = TempDir::new("it_finds_if_present_in_the_parent_directory").unwrap(); + let root_dir_path = root_dir.path(); + + let actual_changelog_path = root_dir_path.join("CHANGELOG.md"); + File::create(&actual_changelog_path).unwrap(); + + let child_dir_path = root_dir_path.join("child"); + create_dir(&child_dir_path).unwrap(); + + let found_changelog_path = nearest_changelog_path(&child_dir_path).unwrap(); + assert_eq!(actual_changelog_path, found_changelog_path); + } + + #[test] + fn it_actually_chooses_the_nearest_changelog() { + let root_dir = TempDir::new("it_finds_if_present_in_the_parent_directory").unwrap(); + let root_dir_path = root_dir.path(); + + let parent_changelog_path = root_dir_path.join("CHANGELOG.md"); + File::create(parent_changelog_path).unwrap(); + + let child_dir_path = root_dir_path.join("child"); + create_dir(&child_dir_path).unwrap(); + + let child_changelog_path = child_dir_path.join("CHANGELOG.md"); + File::create(&child_changelog_path).unwrap(); + + let found_changelog_path = nearest_changelog_path(&child_dir_path).unwrap(); + assert_eq!(child_changelog_path, found_changelog_path); + } + + #[test] + fn it_can_return_nothing_if_no_changelog_is_present() { + let root_dir = TempDir::new("it_can_return_nothing_if_no_changelog_is_present").unwrap(); + let found_changelog_path = nearest_changelog_path(root_dir.path()); + assert!(found_changelog_path.is_none()); + } +} diff --git a/crates/keepac/src/highlight.rs b/crates/keepac/src/highlight.rs new file mode 100644 index 0000000..1fa5c49 --- /dev/null +++ b/crates/keepac/src/highlight.rs @@ -0,0 +1,386 @@ +use std::io::{Result, Write}; +use termcolor::{Buffer, Color, ColorSpec, HyperlinkSpec, WriteColor}; +use textwrap::{fill, Options as WrapOptions}; + +use pulldown_cmark::{Event, HeadingLevel, Options, Parser, Tag, TagEnd}; + +// struct RenderOptions { +// width: usize, +// padding_top: usize, +// padding_left: usize, +// } + +// impl Default for RenderOptions { +// fn default() -> Self { +// RenderOptions { +// width: 80, +// padding_top: 1, +// padding_left: 2, +// } +// } +// } + +fn flush_to_source( + buffer: &mut Buffer, + source_buffer: &mut Buffer, + wrap_options: &WrapOptions, + flush_str: &str, +) -> Result<()> { + let str = std::str::from_utf8(buffer.as_slice()).unwrap(); + let wrapped = fill(str, wrap_options); + + source_buffer.write_all(wrapped.as_bytes())?; + source_buffer.write_all(flush_str.as_bytes())?; + + source_buffer.flush()?; + buffer.clear(); + + Ok(()) +} + +enum RenderContext { + VersionHeading, + ChangeCategoryHeading, +} + +enum ChangeCategory { + /// New features. + Added, + /// Changes in existing functionality. + Changed, + /// Soon-to-be removed features. + Deprecated, + /// Now removed features. + Removed, + /// Any bug fixes. + Fixed, + /// In case of vulnerabilities. + Security, +} + +impl ChangeCategory { + fn parse(value: &str) -> Option { + match value.to_lowercase().as_str() { + "added" => Some(Self::Added), + "changed" => Some(Self::Changed), + "deprecated" => Some(Self::Deprecated), + "removed" => Some(Self::Removed), + "fixed" => Some(Self::Fixed), + "security" => Some(Self::Security), + _ => None, + } + } +} + +fn horizontal_rule(width: usize, buffer: &mut Buffer) -> Result<()> { + buffer.set_color( + ColorSpec::new() + .set_fg(Some(Color::Ansi256(250))) + .set_dimmed(true), + )?; + let rule = '┈'.to_string().repeat(width); + write!(buffer, "{}", rule)?; + buffer.reset() +} + +pub fn render(source_buffer: &mut Buffer, source: &str) -> Result<()> { + let parser = Parser::new_ext(source, Options::all()); + let mut render_context: Option = None; + let mut inside_link = false; + + let width = 80; + let mut buffer = source_buffer.clone(); + write!(source_buffer, "\n")?; + + let mut colors = ColorSpec::new(); + buffer.set_color(&colors)?; + + for (event, range) in parser.into_offset_iter() { + // eprintln!("{:?} {:?}", range, event); + + match event { + Event::Start(start) => match start { + Tag::Paragraph => {} + Tag::Heading { + level, + id: _, + classes: _, + attrs: _, + } => match level { + HeadingLevel::H1 => { + colors.set_fg(Some(Color::Blue)); + colors.set_bold(true); + buffer.set_color(&colors)?; + } + HeadingLevel::H2 => { + horizontal_rule(width, source_buffer)?; + write!(source_buffer, "\n\n")?; + render_context = Some(RenderContext::VersionHeading); + colors.set_bold(false); + colors.set_fg(Some(Color::Cyan)); + buffer.set_color(&colors)?; + } + HeadingLevel::H3 => { + render_context = Some(RenderContext::ChangeCategoryHeading); + colors.set_bold(false); + colors.set_fg(Some(Color::Green)); + buffer.set_color(&colors)?; + } + HeadingLevel::H4 => { + colors.set_fg(Some(Color::Magenta)); + buffer.set_color(&colors)?; + } + HeadingLevel::H5 => { + colors.set_fg(Some(Color::Red)); + buffer.set_color(&colors)?; + } + HeadingLevel::H6 => { + colors.set_fg(Some(Color::Red)); + buffer.set_color(&colors)?; + } + }, + Tag::BlockQuote(_) => {} + Tag::CodeBlock(_) => {} + Tag::HtmlBlock => {} + Tag::List(_) => {} + Tag::Item => {} + Tag::FootnoteDefinition(_) => {} + Tag::DefinitionList => {} + Tag::DefinitionListTitle => {} + Tag::DefinitionListDefinition => {} + Tag::Table(_) => {} + Tag::TableHead => {} + Tag::TableRow => {} + Tag::TableCell => {} + Tag::Emphasis => {} + Tag::Strong => {} + Tag::Strikethrough => {} + Tag::Link { + link_type: _, + dest_url, + title: _, + id: _, + } => { + inside_link = true; + buffer.set_hyperlink(&HyperlinkSpec::open(dest_url.as_bytes()))?; + colors.set_underline(true); + colors.set_fg(Some(Color::Cyan)); + buffer.set_color(&colors)?; + } + Tag::Image { + link_type: _, + dest_url: _, + title: _, + id: _, + } => {} + Tag::MetadataBlock(_) => {} + }, + Event::End(end) => match end { + TagEnd::Paragraph => { + buffer.reset()?; + flush_to_source( + &mut buffer, + source_buffer, + &WrapOptions::new(width) + .initial_indent(" ") + .subsequent_indent(" "), + "\n\n", + )? + } + TagEnd::Heading(_) => { + render_context = None; + buffer.reset()?; + flush_to_source( + &mut buffer, + source_buffer, + &WrapOptions::new(width) + .initial_indent(" ") + .subsequent_indent(" "), + "\n\n", + )?; + } + TagEnd::BlockQuote(_) => {} + TagEnd::CodeBlock => {} + TagEnd::HtmlBlock => {} + TagEnd::List(_) => { + buffer.reset()?; + write!(source_buffer, "\n")?; + } + TagEnd::Item => { + buffer.reset()?; + flush_to_source( + &mut buffer, + source_buffer, + &WrapOptions::new(width) + .initial_indent(" - ") + .subsequent_indent(" "), + "\n", + )?; + } + TagEnd::FootnoteDefinition => {} + TagEnd::DefinitionList => {} + TagEnd::DefinitionListTitle => {} + TagEnd::DefinitionListDefinition => {} + TagEnd::Table => {} + TagEnd::TableHead => {} + TagEnd::TableRow => {} + TagEnd::TableCell => {} + TagEnd::Emphasis => {} + TagEnd::Strong => {} + TagEnd::Strikethrough => {} + TagEnd::Link => { + buffer.set_hyperlink(&HyperlinkSpec::close())?; + colors.set_underline(false); + buffer.set_color(&colors)?; + inside_link = false; + } + TagEnd::Image => {} + TagEnd::MetadataBlock(_) => {} + }, + Event::Text(text) => match render_context { + None => write!(buffer, "{}", text)?, + Some(ref context) => match context { + RenderContext::VersionHeading => { + if text.starts_with(" - ") { + // Release date + let date = text.strip_prefix(" - "); + match date { + Some(date) => write!(buffer, " - \u{eab0} {}", date)?, + // Fallback + None => write!(buffer, "{}", text)?, + } + } else { + // Version tag + write!(buffer, "\u{ea66} {}", text)? + } + } + RenderContext::ChangeCategoryHeading => { + let category = ChangeCategory::parse(text.as_ref()); + let icon = match &category { + Some(c) => match c { + ChangeCategory::Added => Some("\u{eadc}"), + ChangeCategory::Changed => Some("\u{eae0}"), + ChangeCategory::Deprecated => Some("\u{ea98}"), + ChangeCategory::Removed => Some("\u{eadf}"), + ChangeCategory::Fixed => Some("\u{ead8}"), + ChangeCategory::Security => Some("\u{eb53}"), + }, + None => None, + }; + + let colors_before = colors.clone(); + + match &category { + Some(c) => match c { + ChangeCategory::Added => { + colors.set_fg(Some(Color::Green)); + } + ChangeCategory::Changed => { + colors.set_fg(Some(Color::Yellow)); + } + ChangeCategory::Deprecated => { + colors.set_fg(Some(Color::Yellow)); + } + ChangeCategory::Removed => { + colors.set_fg(Some(Color::Red)); + } + ChangeCategory::Fixed => { + colors.set_fg(Some(Color::Cyan)); + } + ChangeCategory::Security => { + colors.set_fg(Some(Color::Blue)); + } + }, + None => {} + }; + + buffer.set_color(&colors)?; + match icon { + Some(icon) => write!(buffer, "{} {}", icon, text)?, + None => write!(buffer, "{}", text)?, + }; + + colors = colors_before; + } + }, + }, + Event::Code(code) => write!(buffer, "{}", code)?, + Event::InlineMath(_) => {} + Event::DisplayMath(_) => {} + Event::Html(_) => {} + Event::InlineHtml(_) => {} + Event::FootnoteReference(_) => {} + Event::SoftBreak => write!(buffer, " ")?, + Event::HardBreak => {} + Event::Rule => write!(buffer, "\n----------\n")?, + Event::TaskListMarker(checked) => match checked { + true => write!(buffer, "☑︎")?, + false => write!(buffer, "☐")?, + }, + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use termcolor::{BufferWriter, ColorChoice}; + + use super::*; + + /// Renders the source and compares it to the expected result. + fn assert_renders(source: &str, expected_result: &str) { + let writer = BufferWriter::stdout(ColorChoice::Always); + let mut buffer = writer.buffer(); + + render(&mut buffer, source).unwrap(); + + let rendered = std::str::from_utf8(buffer.as_slice()).unwrap(); + + assert_eq!(rendered, expected_result); + } + + /// Renders the source and compares it to the expected result. + fn assert_renders_plain(source: &str, expected_result: &str) { + let writer = BufferWriter::stdout(ColorChoice::Never); + let mut buffer = writer.buffer(); + + render(&mut buffer, source).unwrap(); + + let rendered = std::str::from_utf8(buffer.as_slice()).unwrap(); + + assert_eq!(rendered, expected_result); + } + + #[test] + pub fn it_renders_links() { + assert_renders( + "[Google](https://google.com)", + "\u{1b}]8;;Google\u{1b}\\https://google.com\u{1b}]8;;\u{1b}\\", + ); + } + + #[test] + pub fn single_word() { + assert_renders_plain("word", " \n word\n \n "); + } + + #[test] + pub fn lists() { + assert_renders_plain("- This is a really long list item, that spans multiple lines and should be wrapped correctly. It even has punctuation!", ""); + } + + #[test] + pub fn it_correctly_wraps_text() { + assert_renders_plain( + r#" +This is one paragraph. +This is is another paragraph. +The sentences should not be organized by line, but grouped together inside a paragraph. +"# + .trim(), + " This is one paragraph. This is is another paragraph. The sentences should not\n be organized by line, but grouped together inside a paragraph.\n \n ", + ); + } +} diff --git a/crates/keepac/src/lib.rs b/crates/keepac/src/lib.rs new file mode 100644 index 0000000..7fd461d --- /dev/null +++ b/crates/keepac/src/lib.rs @@ -0,0 +1,15 @@ +/// The main entry point for manipulating things with the changelog / the "public" API. +pub mod changelog; +/// Find CHANGELOG.md files on the filesystem tree. +pub mod find; +/// Parse changelogs given a few structural constraints. +pub mod parse; + +/// Treesitter utilities for markdown files +pub mod markdown; + +pub mod highlight; + +pub mod theme; + +pub use changelog::Changelog; diff --git a/crates/keepac/src/markdown.rs b/crates/keepac/src/markdown.rs new file mode 100644 index 0000000..e97d540 --- /dev/null +++ b/crates/keepac/src/markdown.rs @@ -0,0 +1,135 @@ +use std::borrow::Cow; + +use streaming_iterator::StreamingIterator; +use tree_sitter::{Node, Query, QueryCursor, QueryMatch, QueryMatches, Tree}; + +use crate::parse::{parse_markdown, MarkdownParserError}; + +pub struct MarkdownDocument<'md> { + pub source: Cow<'md, [u8]>, + pub(crate) tree: Tree, +} + +impl<'md> MarkdownDocument<'md> { + pub fn query_cursor( + &self, + query: &str, + ) -> Result { + Ok(MarkdownDocumentQueryCursor { + cursor: QueryCursor::new(), + query: Query::new(&tree_sitter_md::LANGUAGE.into(), query)?, + document: self, + }) + } +} + +pub struct MarkdownDocumentQueryCursor<'md> { + cursor: QueryCursor, + query: Query, + document: &'md MarkdownDocument<'md>, +} + +impl<'md> MarkdownDocumentQueryCursor<'md> { + pub fn matches(&mut self) -> QueryMatches<'_, '_, &[u8], &[u8]> { + self.cursor.matches( + &self.query, + self.document.tree.root_node(), + self.document.source.as_ref(), + ) + } + + pub fn for_each)>(&mut self, mut callback: F) { + let mut matches = self.matches(); + while let Some(res) = matches.next() { + callback(res); + } + } + + pub fn captured_nodes(&mut self) -> Vec> { + let mut matches = self.matches(); + let mut nodes = Vec::new(); + while let Some(query_match) = matches.next() { + for capture in query_match.captures { + nodes.push(capture.node); + } + } + + nodes + } +} + +impl<'md> TryFrom<&'md str> for MarkdownDocument<'md> { + type Error = MarkdownParserError; + + fn try_from(value: &'md str) -> Result { + let tree = parse_markdown(value)?; + + Ok(MarkdownDocument { + source: Cow::Borrowed(value.as_ref()), + tree, + }) + } +} + +impl<'md> TryFrom for MarkdownDocument<'md> { + type Error = MarkdownParserError; + + fn try_from(value: String) -> Result { + let tree = parse_markdown(&value)?; + + Ok(MarkdownDocument { + source: Cow::Owned(value.into()), + tree, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const EXAMPLE_CHANGELOG: &str = r#"# Changelog + +This is a changelog. + +## The second version + +This is the second version. + +## The first version + +This is the first version. +"#; + + #[test] + fn it_can_iterate_results() { + let document = MarkdownDocument::try_from(EXAMPLE_CHANGELOG).unwrap(); + let mut query = document.query_cursor("(paragraph) @par").unwrap(); + + let mut foo: Vec = Vec::new(); + query.for_each(|res| { + for capt in res.captures { + foo.push( + capt.node + .utf8_text(EXAMPLE_CHANGELOG.as_bytes()) + .unwrap() + .into(), + ); + } + }); + + assert_eq!(foo.get(0).unwrap().trim(), "This is a changelog."); + assert_eq!(foo.get(1).unwrap().trim(), "This is the second version."); + assert_eq!(foo.get(2).unwrap().trim(), "This is the first version."); + } + + #[test] + pub fn it_can_iterate_nodes() { + let document = MarkdownDocument::try_from(EXAMPLE_CHANGELOG).unwrap(); + let mut cursor = document.query_cursor("(paragraph) @par").unwrap(); + + for node in cursor.captured_nodes() { + assert_eq!(node.grammar_name(), "paragraph"); + } + } +} diff --git a/crates/keepac/src/parse.rs b/crates/keepac/src/parse.rs new file mode 100644 index 0000000..69b8ebe --- /dev/null +++ b/crates/keepac/src/parse.rs @@ -0,0 +1,132 @@ +use crate::Changelog; +use std::collections::HashMap; +use streaming_iterator::StreamingIterator; +use thiserror::Error; +use tree_sitter::{LanguageError, Parser, Tree}; + +#[derive(Error, Debug)] +pub enum MarkdownParserError { + #[error("Failed to load markdown grammar")] + FailedToLoadGrammar(#[from] LanguageError), + + #[error("Failed to parse the changelog markdown file")] + CouldNotParseTree, +} + +pub(crate) fn parse_markdown(value: impl AsRef<[u8]>) -> Result { + let mut parser = Parser::new(); + parser.set_language(&tree_sitter_md::LANGUAGE.into())?; + + let Some(tree) = parser.parse(value, None) else { + return Err(MarkdownParserError::CouldNotParseTree); + }; + + Ok(tree) +} + +#[derive(Debug)] +pub struct Version<'a> { + pub name: &'a str, + pub released_at: Option<&'a str>, + pub href: Option<&'a str>, +} + +pub fn parse_versions<'c>(changelog: &'c Changelog<'c>) -> Vec> { + changelog + .document + .query_cursor("(atx_heading (atx_h2_marker) heading_content: (inline) @content)") + .unwrap() + .captured_nodes() + .iter() + .map(|heading_node| { + let parts: Vec<&str> = heading_node + .utf8_text(changelog.document.source.as_ref()) + .unwrap() + .split(" - ") + .collect(); + + Version { + name: parts[0], + released_at: parts.get(1).copied(), + href: None, + } + }) + .collect() +} + +pub fn gather_link_defs<'a>(changelog: &'a Changelog<'a>) -> HashMap<&'a str, &'a str> { + let mut cursor = changelog + .document + .query_cursor( + "(link_reference_definition (link_label) @label (link_destination) @destination)", + ) + .unwrap(); + let mut matches = cursor.matches(); + + let mut links = HashMap::new(); + while let Some(m) = matches.next() { + let captures = m.captures; + + let label = captures + .first() + .unwrap() + .node + .utf8_text(changelog.document.source.as_ref()) + .unwrap() + .strip_prefix('[') + .unwrap() + .strip_suffix(']') + .unwrap(); + let destination = captures + .get(1) + .unwrap() + .node + .utf8_text(changelog.document.source.as_ref()) + .unwrap(); + + links.insert(label, destination); + } + + links +} + +#[cfg(test)] +mod tests { + use super::*; + + static A_CHANGELOG: &str = r#"# Changelog + +blah blah blah + +## [1.0.1] - 2024-12-12 + +### Added + +- Something cool with `fancy` _annotations_ + +## [1.0.0] - 2024-12-10 + +### Added + +- Something plain + +[1.0.1]: https://google.com +[1.0.0]: https://google.com +"#; + + #[test] + fn it_can_gather_link_defs() { + let r = Changelog::try_from(A_CHANGELOG).unwrap(); + + let links = gather_link_defs(&r); + + assert_eq!( + links.get("1.0.1").unwrap().to_owned(), + "https://google.com".to_string() + ); + assert_eq!( + links.get("1.0.0").unwrap().to_owned(), + "https://google.com".to_string() + ); + } +} diff --git a/crates/keepac/src/theme.rs b/crates/keepac/src/theme.rs new file mode 100644 index 0000000..31c1050 --- /dev/null +++ b/crates/keepac/src/theme.rs @@ -0,0 +1,14 @@ +// use termcolor::ColorSpec; + +// struct Headings { +// h1: ColorSpec, +// h2: ColorSpec, +// h3: ColorSpec, +// h4: ColorSpec, +// h5: ColorSpec, +// } + +// struct Theme { +// pub defaults: ColorSpec, +// pub headings: Headings, +// } diff --git a/go.mod b/go.mod deleted file mode 100644 index c07aa4a..0000000 --- a/go.mod +++ /dev/null @@ -1,56 +0,0 @@ -module github.com/niclasvaneyk/keepac - -go 1.21 - -toolchain go1.22.4 - -require ( - github.com/spf13/cobra v1.8.1 - github.com/yuin/goldmark v1.7.4 - golang.org/x/term v0.21.0 -) - -require ( - github.com/alecthomas/chroma/v2 v2.14.0 // indirect - github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/aymerick/douceur v0.2.0 // indirect - github.com/charmbracelet/x/ansi v0.1.2 // indirect - github.com/charmbracelet/x/input v0.1.2 // indirect - github.com/charmbracelet/x/term v0.1.1 // indirect - github.com/charmbracelet/x/windows v0.1.2 // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/gorilla/css v1.0.1 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/microcosm-cc/bluemonday v1.0.26 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.2 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/sahilm/fuzzy v0.1.1 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark-emoji v1.0.3 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect -) - -require ( - github.com/blang/semver/v4 v4.0.0 - github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.26.6 - github.com/charmbracelet/glamour v0.7.0 - github.com/charmbracelet/lipgloss v0.11.0 - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - gotest.tools v2.2.0+incompatible -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 9cd685a..0000000 --- a/go.sum +++ /dev/null @@ -1,105 +0,0 @@ -github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= -github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= -github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= -github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= -github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s= -github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk= -github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= -github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= -github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= -github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= -github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= -github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/input v0.1.2 h1:QJAZr33eOhDowkkEQ24rsJy4Llxlm+fRDf/cQrmqJa0= -github.com/charmbracelet/x/input v0.1.2/go.mod h1:LGBim0maUY4Pitjn/4fHnuXb4KirU3DODsyuHuXdOyA= -github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= -github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= -github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= -github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= -github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= -github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= -github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= -github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= -github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/internal/changelog/changelog.go b/internal/changelog/changelog.go deleted file mode 100644 index 9a7b630..0000000 --- a/internal/changelog/changelog.go +++ /dev/null @@ -1,154 +0,0 @@ -package changelog - -type Changelog struct { - source string - Title string - Releases Releases -} - -// Returns the end index of the changelog -func (changelog *Changelog) Stop() int { - return len(changelog.source) -} - -type NextRelease struct { - HeadlineBounds Bounds - Bounds Bounds - Sections []Section -} - -type Releases struct { - Next *NextRelease - Past []Release -} - -func (next *NextRelease) FindSection(changeType ChangeType) *Section { - for _, section := range next.Sections { - if section.Type == changeType { - return §ion - } - } - - return nil -} - -type Release struct { - Sections []Section - Date string - Yanked bool - Version string - - HeadlineBounds Bounds - Bounds Bounds -} - -func NewRelease(version string, date string) Release { - return Release{ - Version: version, - Date: date, - Yanked: false, - Sections: make([]Section, 0), - } -} - -type ChangeType int64 - -const ( - Added ChangeType = iota - Changed - Deprecated - Fixed - Removed - Security - Unknown -) - -func KnownChangeTypes() []ChangeType { - return []ChangeType{ - Added, - Changed, - Deprecated, - Fixed, - Removed, - Security, - } -} - -func LastChangeType() ChangeType { - return Security -} - -func ChangeTypeLabel(changeType ChangeType) string { - switch changeType { - case Added: - return "Added" - case Changed: - return "Changed" - case Deprecated: - return "Deprecated" - case Fixed: - return "Fixed" - case Removed: - return "Removed" - case Security: - return "Security" - } - return "Unknown" -} - -func ParseChangeType(name string) ChangeType { - switch name { - case "Added": - return Added - case "Changed": - return Changed - case "Deprecated": - return Deprecated - case "Fixed": - return Fixed - case "Removed": - return Removed - case "Security": - return Security - } - return Unknown -} - -type Bounds struct { - Start int - Stop int -} - -func EmptyBounds() Bounds { - return Bounds{Start: -1, Stop: -1} -} - -type Section struct { - Type ChangeType - Items []Item - Bounds Bounds -} - -type Item struct { - Bounds Bounds -} - -func HasChanges(sections *([]Section)) bool { - for _, section := range *sections { - for range section.Items { - return true - } - } - - return false -} - -func (changelog *Changelog) FindRelease(version string) *Release { - for _, release := range changelog.Releases.Past { - if release.Version == version { - return &release - } - } - - return nil -} diff --git a/internal/changelog/completions.go b/internal/changelog/completions.go deleted file mode 100644 index 5013902..0000000 --- a/internal/changelog/completions.go +++ /dev/null @@ -1,33 +0,0 @@ -package changelog - -import ( - "strings" - - "github.com/spf13/cobra" -) - -// Can be passed as ValidArgsFunction to support custom completions if the -// first argument is a released version. -func CompleteReleasesAsFirstArgument( - cmd *cobra.Command, - args []string, - toComplete string, -) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - - changelog, _, err := ResolveChangelog() - if err != nil { - return nil, cobra.ShellCompDirectiveNoFileComp - } - - matchingVersions := make([]string, 0) - for _, release := range changelog.Releases.Past { - if strings.HasPrefix(release.Version, toComplete) { - matchingVersions = append(matchingVersions, release.Version) - } - } - - return matchingVersions, cobra.ShellCompDirectiveNoFileComp -} diff --git a/internal/changelog/finder.go b/internal/changelog/finder.go deleted file mode 100644 index 613b229..0000000 --- a/internal/changelog/finder.go +++ /dev/null @@ -1,75 +0,0 @@ -package changelog - -import ( - "fmt" - "io/fs" - "os" - "path/filepath" -) - -func FindChangelogIn(directory string) (string, bool) { - changelogPath := filepath.Join(directory, "CHANGELOG.md") - _, err := os.Stat(changelogPath) - if err != nil { - return "", false - } - - return changelogPath, true -} - -func hikeDir(start string, callback func(string) error) { - directory := start - for { - result := callback(directory) - if result == fs.SkipAll { - break - } - - parent := filepath.Dir(directory) - rootDirectoryReached := parent == directory - if rootDirectoryReached { - break - } - - directory = parent - } -} - -func ResolvePathToChangelog() (string, error) { - cwd, err := os.Getwd() - if err != nil { - return "", err - } - - changelogPath, wasFound := "", false - hikeDir(cwd, func(directory string) error { - changelogPath, wasFound = FindChangelogIn(directory) - if wasFound { - return fs.SkipAll - } - return nil - }) - - if wasFound { - return changelogPath, nil - } - - return "", fmt.Errorf("CHANGELOG.md not found") -} - -func ResolveChangelog() (*Changelog, string, error) { - filename, err := ResolvePathToChangelog() - if err != nil { - // TODO: if none exists, maybe ask to create one? Might not make sense though - return nil, "", err - } - - contents, err := os.ReadFile(filename) - if err != nil { - return nil, "", err - } - - changelog := Parse(contents) - - return &changelog, filename, nil -} diff --git a/internal/changelog/finder_test.go b/internal/changelog/finder_test.go deleted file mode 100644 index 71a057c..0000000 --- a/internal/changelog/finder_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package changelog - -import "testing" - -func TestFinder(t *testing.T) { - path, err := ResolvePathToChangelog() - if err != nil { - t.Errorf(err.Error()) - } - - println(path) -} diff --git a/internal/changelog/formatter.go b/internal/changelog/formatter.go deleted file mode 100644 index 0ce7683..0000000 --- a/internal/changelog/formatter.go +++ /dev/null @@ -1,115 +0,0 @@ -package changelog - -import ( - "fmt" - "strings" -) - -// Shows everything between the two given versions. -func (changelog *Changelog) Diff(from string, to string) (string, error) { - releases, err := releasesFromTo(changelog, from, to) - if err != nil { - return "", err - } - - start := releases[0].Bounds.Start - stop := releases[len(releases)-1].Bounds.Stop - - return changelog.ContentWithin(&Bounds{Start: start, Stop: stop}), nil -} - -// Merges -func (changelog *Changelog) Merge(from string, to string, prefixItemsWithVersion bool) (string, error) { - releases, err := releasesFromTo(changelog, from, to) - if err != nil { - return "", err - } - - merged := emptyMergedSections() - for _, release := range releases { - merged.merge(release) - } - - rendered := merged.render(changelog, prefixItemsWithVersion) - - return fmt.Sprintf("# %s -> %s\n\n%s", from, to, rendered), nil -} - -// Returns the releases between from and to (inclusively) -func releasesFromTo(changelog *Changelog, from string, to string) ([]Release, error) { - between := make([]Release, 0) - beginTracking := false - - for _, release := range changelog.Releases.Past { - if !beginTracking && release.Version == to { - beginTracking = true - } - - if beginTracking { - between = append(between, release) - } - - if release.Version == from { - return between, nil - } - } - - if !beginTracking { - return between, fmt.Errorf("version '%s' does not exist", to) - } - - return between, fmt.Errorf("version '%s' does not exist", from) -} - -type mergedItem struct { - Release Release - Item *Item -} - -type mergedSections struct { - sections [][]mergedItem -} - -func emptyMergedSections() mergedSections { - return mergedSections{ - sections: make([][]mergedItem, int(LastChangeType())+1), - } -} - -func (merged *mergedSections) merge(release Release) { - for _, section := range release.Sections { - index := int(section.Type) - if merged.sections[index] == nil { - merged.sections[index] = make([]mergedItem, 0) - } - for _, item := range section.Items { - merged.sections[index] = append(merged.sections[index], mergedItem{ - Release: release, - Item: &item, - }) - } - } -} - -func (merged *mergedSections) render(changelog *Changelog, doPrefix bool) string { - sections := make([]string, int(LastChangeType())+1) - for _, changeType := range KnownChangeTypes() { - items := merged.sections[int(changeType)] - if items == nil { - continue - } - - renderedItems := make([]string, len(items)) - for i, item := range items { - prefix := "" - if doPrefix { - prefix = fmt.Sprintf("[%s] ", item.Release.Version) - } - renderedItems[i] = "- " + prefix + changelog.ContentWithin(&item.Item.Bounds) + "\n" - } - - sections[int(changeType)] = "## " + ChangeTypeLabel(changeType) + "\n" + strings.Join(renderedItems, "\n") - } - - return strings.TrimSpace(strings.Join(sections, "\n\n")) -} diff --git a/internal/changelog/formatter_test.go b/internal/changelog/formatter_test.go deleted file mode 100644 index af00a1d..0000000 --- a/internal/changelog/formatter_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package changelog - -import ( - "testing" - - "gotest.tools/assert" - is "gotest.tools/assert/cmp" -) - -func TestDiff(t *testing.T) { - source := `# Changelog - -## [0.0.3] - 2020-01-01 - -### Added - -- The third version - -## [0.0.2] - 2020-01-01 - -### Added - -- The second version - -## [0.0.1] - 2020-01-01 - -### Added - -- The first version` - - changelog := Parse([]byte(source)) - - diff, err := changelog.Diff("0.0.2", "0.0.3") - assert.Assert(t, is.Nil(err)) - - expected := `## [0.0.3] - 2020-01-01 - -### Added - -- The third version - -## [0.0.2] - 2020-01-01 - -### Added - -- The second version - -` - assert.Equal(t, expected, diff) -} - -func TestMerge(t *testing.T) { - source := `# Changelog - -## [0.0.3] - 2020-01-01 - -### Added - -- The third version - -## [0.0.2] - 2020-01-01 - -### Added - -- The second version - -## [0.0.1] - 2020-01-01 - -### Added - -- The first version` - - changelog := Parse([]byte(source)) - - diff, err := changelog.Merge("0.0.2", "0.0.3", false) - assert.Assert(t, is.Nil(err)) - - expected := `# 0.0.2 -> 0.0.3 - -## Added -- The third version - -- The second version` - assert.Equal(t, expected, diff) -} diff --git a/internal/changelog/inserter.go b/internal/changelog/inserter.go deleted file mode 100644 index d88a2ef..0000000 --- a/internal/changelog/inserter.go +++ /dev/null @@ -1,149 +0,0 @@ -package changelog - -import ( - "strings" - "unicode" -) - -func (changelog *Changelog) AddItem(changeType ChangeType, contents string) string { - parts := make([]string, 0) - - shouldCreateNextRelease := changelog.Releases.Next == nil - if shouldCreateNextRelease { - parts = append(parts, "## [Unreleased]") - } - - shouldAddSection := shouldCreateNextRelease || changelog.Releases.Next.FindSection(changeType) == nil - if shouldAddSection { - parts = append(parts, "### "+ChangeTypeLabel(changeType)) - } - - parts = append(parts, contents) - newContent := strings.Join(parts, "\n\n") - - insertionPoint, padding := determineInsertionPoint(changeType, changelog) - - return padding.Join( - changelog.source[:insertionPoint], - newContent, - changelog.source[insertionPoint:], - ) -} - -type NewLines struct { - Before int - After int -} - -func ensureNewlinesBetween(lines int, left string, right string) string { - leftSanitized := strings.TrimRightFunc(left, unicode.IsSpace) - rightSanatized := strings.TrimLeftFunc(right, unicode.IsSpace) - - return leftSanitized + strings.Repeat("\n", lines) + rightSanatized -} - -func (p *NewLines) Join(before string, subject string, after string) string { - finalString := "" - - if p.Before > 0 { - finalString = ensureNewlinesBetween(p.Before, before, subject) - } else { - finalString = before + subject - } - - if p.After > 0 { - finalString = ensureNewlinesBetween(p.After, finalString, after) - } else { - finalString = finalString + after - } - - return finalString -} - -// Returns the index at which to insert and the amount of Padding -func determineInsertionPoint(changeType ChangeType, changelog *Changelog) (int, NewLines) { - nextRelease := changelog.Releases.Next - if nextRelease == nil { - if len(changelog.Releases.Past) == 0 { - // We have an empty changelog with just the title: - // # Changelog <-- Add here - return changelog.Stop(), NewLines{Before: 2, After: 0} - } - - // We have some releases, but no next one: - // # Changelog - // - // ## [1.1.0] - 2020-01-01 <-- Add before this line - // - // ### Added - return changelog.Releases.Past[0].Bounds.Start, NewLines{Before: 0, After: 2} - } - - // At this point we can be sure that we need to insert somewhere inside the - // [Unreleased] section: - // # Changelog - // - // ## [Unreleased] - // - // ### Added - // - // - Something - // - Something else <-- Add after this line - // - // ## [1.1.0] - 2020-01-01 - existingSection := nextRelease.FindSection(changeType) - if existingSection != nil { - return existingSection.Bounds.Stop, NewLines{Before: 1, After: 0} - } - - // Now we know, that the section does not exist yet. - // - // To make things easy we first handle the edge case where there is an - // [Unreleased] heading, without any actual sections in it. - if len(nextRelease.Sections) == 0 { - return nextRelease.HeadlineBounds.Stop, NewLines{Before: 2, After: 2} - } - - // Now with all other edge cases handled we can shift our focus to adding a - // new section in the right position. It would be nice if all of our sections - // would follow the same order (the one mentioned by keepachangelog, which is - // also followed in the definition of the `ChangeType` enum). This would be - // easy if we could assume that the sections are guaranteed to be in proper - // order, however this is a bold assumption to make given a fair share of - // changelogs are edited by hand. - - // This is rather simple, if we can simply prepend it before an existing - // section that the section to insert would preceed. - for _, section := range nextRelease.Sections { - if int(changeType) < int(section.Type) { - return section.Bounds.Start, NewLines{Before: 0, After: 2} - } - } - - // Now the only thing left is the case where we need to append the new - // section at the very end of the [Unreleased] section. Another way of - // framing this is inserting it before the latest release. First we handle - // the case where this one does not exist... - if len(changelog.Releases.Past) == 0 { - return changelog.Stop(), NewLines{2, 0} - } - - // ... and otherwise we insert it before the latest release. - return changelog.Releases.Past[0].Bounds.Start, NewLines{Before: 0, After: 2} -} - -func (changelog *Changelog) ReplacedWithinBounds(bounds Bounds, replacement string) string { - source := changelog.source - - return source[:bounds.Start] + replacement + source[bounds.Stop:] -} - -func (changelog *Changelog) DiffLen(other string) int { - return len(other) - len(changelog.source) -} - -func (changelog *Changelog) WithAddition(insertionPoint int, addition string) string { - source := changelog.source - - return source[:insertionPoint] + addition + source[insertionPoint:] -} diff --git a/internal/changelog/inserter_test.go b/internal/changelog/inserter_test.go deleted file mode 100644 index bfbe6be..0000000 --- a/internal/changelog/inserter_test.go +++ /dev/null @@ -1,365 +0,0 @@ -package changelog - -import ( - "testing" - - "gotest.tools/assert" -) - -func scenario( - t *testing.T, - source string, - changeType ChangeType, - addition string, - expected string, -) { - changelog := Parse([]byte(source)) - after := changelog.AddItem(changeType, addition) - - assert.Equal(t, expected, after) - // if after != expected { - // t.Errorf("Resulting changelog did not match expectations:\n\n'%v'\n\n is not\n\n'%v'", after, expected) - // } -} - -func TestAddToCompletelyEmptyChangelog(t *testing.T) { - source := "# Changelog" - changeType := Added - addition := "- New Thing" - expected := `# Changelog - -## [Unreleased] - -### Added - -- New Thing` - - scenario(t, source, changeType, addition, expected) -} - -func TestAddToJustAfterRelease(t *testing.T) { - source := `# Changelog - -## [1.0.0] - 2020-01-01 - -### Added - -- New Thing` - - changeType := Added - addition := "- Another New Thing" - expected := `# Changelog - -## [Unreleased] - -### Added - -- Another New Thing - -## [1.0.0] - 2020-01-01 - -### Added - -- New Thing` - - scenario(t, source, changeType, addition, expected) -} - -func TestAddToExistingSectionInNextRelease(t *testing.T) { - source := `# Changelog - -## [Unreleased] - -### Added - -- Something` - changeType := Added - addition := "- Another New Thing" - expected := `# Changelog - -## [Unreleased] - -### Added - -- Something -- Another New Thing` - - scenario(t, source, changeType, addition, expected) -} - -func TestAppendToExistingSectionInNextReleaseWithoutPastReleases(t *testing.T) { - source := `# Changelog - -## [Unreleased] - -### Added - -- First -- Second -- Third` - changeType := Added - addition := "- Fourth" - expected := `# Changelog - -## [Unreleased] - -### Added - -- First -- Second -- Third -- Fourth` - - scenario(t, source, changeType, addition, expected) -} - -func TestAddToNewSectionInNextReleaseWithoutPastReleases(t *testing.T) { - source := `# Changelog - -## [Unreleased] - -### Added - -- Something` - changeType := Changed - addition := "- Another New Thing" - expected := `# Changelog - -## [Unreleased] - -### Added - -- Something - -### Changed - -- Another New Thing` - - scenario(t, source, changeType, addition, expected) -} - -func TestAddNewAddedSectionAboveRemovedOne(t *testing.T) { - source := `# Changelog - -## [Unreleased] - -### Removed - -- Something -- Something else - -## [1.1.0] - 2020-01-01 - -### Added - -- Something` - changeType := Added - addition := "- Something new" - expected := `# Changelog - -## [Unreleased] - -### Added - -- Something new - -### Removed - -- Something -- Something else - -## [1.1.0] - 2020-01-01 - -### Added - -- Something` - - scenario(t, source, changeType, addition, expected) -} - -func TestAddNewDeprecatedSectionBetweenAddedAndRemovedOnes(t *testing.T) { - source := `# Changelog - -## [Unreleased] - -### Added - -- Something new - -### Removed - -- Something -- Something else - -## [1.1.0] - 2020-01-01 - -### Added - -- Something` - changeType := Deprecated - addition := "- Something that will be removed" - expected := `# Changelog - -## [Unreleased] - -### Added - -- Something new - -### Deprecated - -- Something that will be removed - -### Removed - -- Something -- Something else - -## [1.1.0] - 2020-01-01 - -### Added - -- Something` - - scenario(t, source, changeType, addition, expected) -} - -func TestInsertsAfterEmptyButExistingUnreleasedSection(t *testing.T) { - source := `# Changelog - -## [Unreleased] - -## [1.1.0] - 2020-01-01 - -### Added - -- Something` - changeType := Added - addition := "- Something" - expected := `# Changelog - -## [Unreleased] - -### Added - -- Something - -## [1.1.0] - 2020-01-01 - -### Added - -- Something` - - scenario(t, source, changeType, addition, expected) -} - -func TestInsertsAfterEmptyButExistingUnreleasedSectionWithoutAnyPastReleases(t *testing.T) { - source := `# Changelog - -## [Unreleased] -` - changeType := Added - addition := "- Something" - expected := `# Changelog - -## [Unreleased] - -### Added - -- Something - -` - - scenario(t, source, changeType, addition, expected) -} - -func TestInsertsCorrectlyRegressionTest(t *testing.T) { - source := `# Changelog - -## [Unreleased] - -### Added - -- The initial version -- Something` - - addition := "- New item at the end" - expected := `# Changelog - -## [Unreleased] - -### Added - -- The initial version -- Something -- New item at the end` - - scenario(t, source, Added, addition, expected) -} - -func TestInsertsCorrectlyWhenEmptyUnreleasedSectionPreceedsTwoPastReleases(t *testing.T) { - source := `# Changelog - -## [Unreleased] - -## 0.0.8 - 2023-07-21 - -### Changed - -- The generated release notes on GitHub now omit the redundant release heading - -## [0.0.1] - 2020-01-01 - -### Added - -- The first version` - - addition := "- Something" - expected := `# Changelog - -## [Unreleased] - -### Removed - -- Something - -## 0.0.8 - 2023-07-21 - -### Changed - -- The generated release notes on GitHub now omit the redundant release heading - -## [0.0.1] - 2020-01-01 - -### Added - -- The first version` - - scenario(t, source, Removed, addition, expected) -} - -func TestDoesNotCrashOnEmptyBulletPoints(t *testing.T) { - source := `# Changelog - -## [Unreleased] - -### Added - -- - -` - addition := "- Something" - - expected := `# Changelog - -## [Unreleased] - -### Added - -- - - -### Removed - -- Something` - - scenario(t, source, Removed, addition, expected) -} diff --git a/internal/changelog/parser.go b/internal/changelog/parser.go deleted file mode 100644 index 4409996..0000000 --- a/internal/changelog/parser.go +++ /dev/null @@ -1,249 +0,0 @@ -package changelog - -import ( - "fmt" - "math" - "regexp" - "strings" - - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/text" -) - -func Parse(source []byte) Changelog { - reader := text.NewReader(source) - parser := goldmark.DefaultParser() - root := parser.Parse(reader) - - run := parserRun{ - title: "", - currentRelease: nil, - currentReleaseIsNextRelease: false, - nextRelease: nil, - source: source, - } - - err := ast.Walk(root, func(node ast.Node, entering bool) (ast.WalkStatus, error) { - if !entering { - return ast.WalkContinue, nil - } - - if node.Kind() == ast.KindHeading { - heading := node.(*ast.Heading) - - if run.title == "" && heading.Level == 1 { - run.handleTitle(heading) - } - - if heading.Level == 2 { - run.handleNewRelease(heading) - } - - if heading.Level == 3 { - run.handleNewReleaseSection(heading) - } - } - - // The following operations require a section of a release to be present - if run.currentSection() == nil { - return ast.WalkContinue, nil - } - - if node.Kind() == ast.KindList { - run.handleListOfChanges(node.(*ast.List)) - } - - if node.Kind() == ast.KindListItem { - run.handleListOfChangesItem(node.(*ast.ListItem)) - } - - return ast.WalkContinue, nil - }) - if err != nil { - fmt.Printf("Warning: %s\n", err.Error()) - } - - run.finalizeCurrentRelease(len(source)) - - changelog := Changelog{ - Title: run.title, - source: string(source), - Releases: Releases{ - Next: run.nextRelease, - Past: run.releases, - }, - } - - return changelog -} - -type parserRun struct { - source []byte - title string - releases []Release - currentRelease *Release - currentReleaseIsNextRelease bool - nextRelease *NextRelease -} - -func (run *parserRun) currentSection() *Section { - if run.currentRelease == nil { - return nil - } - - if len(run.currentRelease.Sections) < 1 { - return nil - } - - return &run.currentRelease.Sections[len(run.currentRelease.Sections)-1] -} - -func (run *parserRun) finalizeCurrentRelease(stop int) { - if run.currentRelease == nil { - return - } - - if run.currentReleaseIsNextRelease { - run.finalizeCurrentAsNextRelease(stop) - } else { - run.currentRelease.Bounds.Stop = stop - run.finalizeCurrentAsPastRelease() - } -} - -func computeBounds(node ast.Node) Bounds { - start := math.MaxInt - stop := -1 - - current := node - for current.Lines().Len() <= 0 { - if !current.HasChildren() { - break - } - current = current.FirstChild() - } - - lines := current.Lines() - for _, line := range lines.Sliced(0, lines.Len()) { - start = min(start, line.Start) - stop = max(stop, line.Stop) - } - - if start == math.MaxInt { - start = 0 - } - - if stop == -1 { - stop = 0 - } - - return Bounds{Start: start, Stop: stop} -} - -func min(a, b int) int { - if a < b { - return a - } - return b -} - -func max(a, b int) int { - if a > b { - return a - } - return b -} - -func (run *parserRun) finalizeCurrentAsNextRelease(stop int) { - run.nextRelease = &NextRelease{ - Bounds: Bounds{ - Start: run.currentRelease.HeadlineBounds.Start - 3, - Stop: stop, - }, - HeadlineBounds: run.currentRelease.HeadlineBounds, - Sections: run.currentRelease.Sections, - } - run.currentRelease = nil -} - -func (run *parserRun) finalizeCurrentAsPastRelease() { - run.releases = append(run.releases, *run.currentRelease) - run.currentRelease = nil -} - -// Sets the title -func (run *parserRun) handleTitle(heading *ast.Heading) { - run.title = string(heading.Text(run.source)) -} - -// Finalizes the current release and prepares a new one -func (run *parserRun) handleNewRelease(heading *ast.Heading) { - headingBounds := computeBounds(heading) - - // Close current release if necessary - run.finalizeCurrentRelease(headingBounds.Start - 3) - - // Prepare a new one - line := string(heading.Text(run.source)) - r := NewRelease(parseVersion(line), parseDate(line)) - r.Yanked = strings.Contains(line, "[YANKED]") - r.HeadlineBounds = headingBounds - r.Bounds = Bounds{ - // we subtract the length of "## " to achieve better insertion points - Start: r.HeadlineBounds.Start - 3, - Stop: r.HeadlineBounds.Stop, // This will be incremented later - } - - // Set the prepared one as active - run.currentRelease = &r - run.currentReleaseIsNextRelease = line == "[Unreleased]" || line == "Unreleased" -} - -// Finalizes the current section and prepares a new one -func (run *parserRun) handleNewReleaseSection(heading *ast.Heading) { - bounds := computeBounds(heading) - bounds.Start = bounds.Start - 4 // we subtract the length of "### " to achieve better insertion points - section := Section{ - Type: ParseChangeType(string(heading.Text(run.source))), - Items: make([]Item, 0), - Bounds: bounds, - } - run.currentRelease.Sections = append(run.currentRelease.Sections, section) -} - -// Can be used to widen the bounds of a section -func (run *parserRun) handleListOfChanges(list *ast.List) { - bounds := computeBounds(list) - run.currentSection().Bounds.Stop = bounds.Stop -} - -func (run *parserRun) handleListOfChangesItem(item *ast.ListItem) { - bounds := computeBounds(item) - - currentSection := run.currentSection() - currentSection.Bounds.Stop = bounds.Stop - currentSection.Items = append(currentSection.Items, Item{Bounds: bounds}) -} - -func parseVersion(line string) string { - semver := regexp.MustCompile(`(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?`) - slice := semver.Find([]byte(line)) - - if slice != nil { - return string(slice) - } - - return "" -} - -func parseDate(line string) string { - semver := regexp.MustCompile("[0-9]{4}-[0-9]-{2}-[0-9]{2}") - slice := semver.Find([]byte(line)) - - if slice != nil { - return string(slice) - } - - return "" -} diff --git a/internal/changelog/parser_test.go b/internal/changelog/parser_test.go deleted file mode 100644 index 2632d3e..0000000 --- a/internal/changelog/parser_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package changelog - -import ( - "fmt" - "testing" - - "gotest.tools/assert" - is "gotest.tools/assert/cmp" -) - -const source = `# Changelog - - ## [1.1.1] - 2023-03-05 - - ### Added - - - added something - - added another - - ### Removed - - - removed something - - ### Changed - - - also changed something` - -func TestContentWithinParsedBoundsEqualsSource(t *testing.T) { - parsed := Parse([]byte(source)) - expected := `## [1.1.1] - 2023-03-05 - - ### Added - - - added something - - added another - - ### Removed - - - removed something - - ### Changed - - - also changed something` - - latestRelease := parsed.Releases.Past[0] - parsedContent := parsed.ContentWithin(&latestRelease.Bounds) - fmt.Printf("%v", latestRelease.Bounds) - - assert.Equal(t, expected, parsedContent) -} - -func TestParser(t *testing.T) { - parsed := Parse([]byte(source)) - - assert.Equal(t, "Changelog", parsed.Title) - assert.Assert(t, is.Nil(parsed.Releases.Next)) - - assert.Equal(t, 1, len(parsed.Releases.Past)) -} diff --git a/internal/changelog/search.go b/internal/changelog/search.go deleted file mode 100644 index 8dfd1e1..0000000 --- a/internal/changelog/search.go +++ /dev/null @@ -1,59 +0,0 @@ -package changelog - -import ( - "strings" -) - -func Search(changelog *Changelog, query string) string { - output := make([]string, 0) - - nextRelease := changelog.Releases.Next - if nextRelease != nil { - includedRelease := false - for _, section := range nextRelease.Sections { - includedSection := false - for _, item := range section.Items { - text := changelog.ContentWithin(&item.Bounds) - if strings.Contains(text, query) { - if !includedRelease { - includedRelease = true - output = append(output, "## "+changelog.ContentWithin(&nextRelease.HeadlineBounds)) - } - - if !includedSection { - includedSection = true - output = append(output, "### "+ChangeTypeLabel(section.Type)) - } - - output = append(output, "- "+text) - } - } - } - } - - for _, release := range changelog.Releases.Past { - includedRelease := false - for _, section := range release.Sections { - includedSection := false - for _, item := range section.Items { - text := changelog.ContentWithin(&item.Bounds) - if strings.Contains(text, query) { - if !includedRelease { - includedRelease = true - output = append(output, "## "+changelog.ContentWithin(&release.HeadlineBounds)) - output = append(output, "") - } - - if !includedSection { - includedSection = true - output = append(output, "### "+ChangeTypeLabel(section.Type)) - output = append(output, "") - } - output = append(output, "- "+text) - } - } - } - } - - return strings.Join(output, "\n") -} diff --git a/internal/changelog/search_test.go b/internal/changelog/search_test.go deleted file mode 100644 index b485798..0000000 --- a/internal/changelog/search_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package changelog - -import ( - "strings" - "testing" -) - -func TestItCanSearchForItems(t *testing.T) { - changelog := Parse([]byte(`# Changelog - -## [Unreleased] - -### Added - -- Something cool -- Another thing - -### Removed - -- Support for Go > 1.0 -- Support for Windows - `)) - - actual := Search(&changelog, "Windows") - expected := `## [Unreleased] -### Removed -- Support for Windows - ` - - if strings.TrimSpace(expected) != strings.TrimSpace(actual) { - t.Errorf("Expected does not match actual:\n\n%s", actual) - } -} diff --git a/internal/changelog/show.go b/internal/changelog/show.go deleted file mode 100644 index 8580e35..0000000 --- a/internal/changelog/show.go +++ /dev/null @@ -1,67 +0,0 @@ -package changelog - -import ( - "fmt" - "os" - "strconv" - - "github.com/charmbracelet/glamour" - "golang.org/x/term" -) - -func (changelog *Changelog) ContentWithin(bounds *Bounds) string { - return changelog.source[bounds.Start:bounds.Stop] -} - -func Show(contents string) error { - renderer, err := glamour.NewTermRenderer( - glamour.WithAutoStyle(), - glamour.WithEnvironmentConfig(), - glamour.WithWordWrap(getWordWrapLimit()), - ) - if err != nil { - return err - } - - out, err := renderer.Render(contents) - if err != nil { - return err - } - - fmt.Print(out) - return nil -} - -func getWordWrapLimit() int { - current := int(os.Stdin.Fd()) - // When wrapping at exatcly 85, the default contents generated - // `changelog init` wrap nicely: - // - // Changelog - // - // All notable changes to this project will be documented in this file. - // - // The format is based on Keep a Changelog https://keepachangelog.com/en/1.0.0/, and - // this project adheres to Semantic Versioning https://semver.org/spec/v2.0.0.html. - // - // Anything less than 85 leads to the links being broken up, which does not - // look very good. - fallback := 85 - - preferredRaw := os.Getenv("KEEPAC_WRAP_AT") - preferred, err := strconv.Atoi(preferredRaw) - if err != nil { - preferred = fallback - } - - width, _, err := term.GetSize(current) - if err != nil { - return fallback - } - - if width > preferred { - return preferred - } - - return width -} diff --git a/internal/changelog/yank.go b/internal/changelog/yank.go deleted file mode 100644 index 5fabc33..0000000 --- a/internal/changelog/yank.go +++ /dev/null @@ -1,16 +0,0 @@ -package changelog - -import "fmt" - -func (changelog *Changelog) Yank(version string) (string, error) { - release := changelog.FindRelease(version) - if release == nil { - return "", fmt.Errorf("Release '%s' not found", version) - } - - if release.Yanked { - return "", fmt.Errorf("Release '%s' was already yanked", version) - } - - return changelog.WithAddition(release.HeadlineBounds.Stop, " [YANKED]"), nil -} diff --git a/internal/editor/editor.go b/internal/editor/editor.go deleted file mode 100644 index b3dec81..0000000 --- a/internal/editor/editor.go +++ /dev/null @@ -1,80 +0,0 @@ -package editor - -import ( - "fmt" - "os" - "os/exec" - "runtime" - "strings" -) - -func Open(path string) error { - editor := os.Getenv("EDITOR") - if editor != "" { - editorCommand := exec.Command(editor, path) - editorCommand.Stdout = os.Stdout - editorCommand.Stdin = os.Stdin - editorCommand.Stderr = os.Stderr - err := editorCommand.Run() - if err != nil { - return fmt.Errorf("failed to open file with $EDITOR: %s", err) - } - return nil - } - - var openCommand *exec.Cmd - switch runtime.GOOS { - case "darwin": - openCommand = exec.Command("open", path) - case "linux": - openCommand = exec.Command("xdg-open", path) - case "windows": - openCommand = exec.Command("cmd", "/c", "start", path) - default: - return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) - } - - err := openCommand.Start() - if err != nil { - return fmt.Errorf("failed to open file with default command: %s", err) - } - - return nil -} - -func Prompt(initialContents string, description string) (string, error) { - tempFile, err := os.CreateTemp("", "keepac-input.txt") - if err != nil { - return "", fmt.Errorf("failed to create temporary file: %s", err) - } - defer os.Remove(tempFile.Name()) - - contents := initialContents + "\n\n\n" + description - _, err = tempFile.WriteString(contents) - if err != nil { - return "", fmt.Errorf("failed to write prompt to temporary file: %s", err) - } - - if err = tempFile.Close(); err != nil { - return "", fmt.Errorf("failed to close temporary file: %s", err) - } - - err = Open(tempFile.Name()) - if err != nil { - return "", err - } - - data, err := os.ReadFile(tempFile.Name()) - if err != nil { - return "", fmt.Errorf("failed to read file: %s", err) - } - - response := string(data) - response = strings.TrimSpace(response) - if strings.HasSuffix(response, description) { - response = strings.TrimSuffix(response, description) - response = strings.TrimSpace(response) - } - - return response, nil -} diff --git a/internal/tui/choice.go b/internal/tui/choice.go deleted file mode 100644 index 3070516..0000000 --- a/internal/tui/choice.go +++ /dev/null @@ -1,122 +0,0 @@ -package tui - -import ( - "fmt" - "io" - "os" - "strings" - - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -var ( - titleStyle = lipgloss.NewStyle().MarginLeft(2).MarginTop(1) - itemStyle = lipgloss.NewStyle().PaddingLeft(4) - selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Bold(true) -) - -type item struct { - value string - index int -} - -func (i item) FilterValue() string { return "" } - -type itemDelegate struct{} - -func (d itemDelegate) Height() int { return 1 } -func (d itemDelegate) Spacing() int { return 0 } -func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil } -func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { - i, ok := listItem.(item) - if !ok { - return - } - - fn := itemStyle.Render - if index == m.Index() { - fn = func(s ...string) string { - return selectedItemStyle.Render("> " + strings.Join(s, " ")) - } - } - - fmt.Fprint(w, fn(i.value)) -} - -type model struct { - list list.Model - onSelect func(item) -} - -func (m model) Init() tea.Cmd { - return nil -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.list.SetWidth(msg.Width) - return m, nil - - case tea.KeyMsg: - switch keypress := msg.String(); keypress { - case "ctrl+c": - return m, tea.Quit - - case "enter": - i, ok := m.list.SelectedItem().(item) - if ok { - m.onSelect(i) - } - return m, tea.Quit - } - } - - var cmd tea.Cmd - m.list, cmd = m.list.Update(msg) - return m, cmd -} - -func (m model) View() string { - return m.list.View() -} - -func Choice(title string, options []string) (string, int) { - items := make([]list.Item, 0) - for index, option := range options { - items = append(items, item{value: option, index: index}) - } - - const defaultWidth = 20 - const listHeight = 14 - l := list.New(items, itemDelegate{}, defaultWidth, listHeight) - - l.DisableQuitKeybindings() - l.SetShowStatusBar(false) - l.SetFilteringEnabled(false) - l.SetShowHelp(false) - - l.Title = title - l.Styles.Title = titleStyle - - choice := "" - index := -1 - m := model{list: l, onSelect: func(i item) { - choice = i.value - index = i.index - }} - - if _, err := tea.NewProgram(m).Run(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } - - if index == -1 { - fmt.Println("Aborted") - os.Exit(0) - } - - return choice, index -} diff --git a/main.go b/main.go deleted file mode 100644 index a9bc2de..0000000 --- a/main.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "github.com/niclasvaneyk/keepac/cmd" - -func main() { - cmd.Execute() -} diff --git a/scripts/generate-completions b/scripts/generate-completions deleted file mode 100755 index 22dbfd9..0000000 --- a/scripts/generate-completions +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -set -e -rm -rf completions -mkdir completions -for sh in bash zsh fish; do - go run main.go completion "$sh" >"completions/changelog.$sh" -done - - diff --git a/scripts/record-tapes b/scripts/record-tapes deleted file mode 100755 index a0f74dc..0000000 --- a/scripts/record-tapes +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 - -from os import path -import os -import pathlib -import sys -import subprocess - -project_root_folder = pathlib.Path(path.dirname(__file__), '..').resolve() -tapes_folder = project_root_folder / 'tapes' -dark_tapes_folder = tapes_folder / 'dark' -light_tapes_folder = tapes_folder / 'light' -gifs_folder = tapes_folder / 'recordings' - - -def main() -> int: - create_light_tapes() - record_tapes() - - return 0 - - -def create_light_tapes() -> None: - for dark_tape in dark_tapes_folder.glob('*.tape'): - with open(light_tapes_folder.joinpath(dark_tape.name), 'w') as light_tape: - print(f"Creating light version of {light_tape.name}...") - light_tape.write("# This file is auto-generated and will be overridden!\n\n") - light_tape.write("Set Theme \"Builtin Light\"\n") - light_tape.write(dark_tape.read_text()) - - -def record_tapes() -> None: - recorded_tapes = 0 - - - - for tapes in [light_tapes_folder, dark_tapes_folder]: - colorscheme = tapes.name - - local_env = os.environ - # Uses locally built version of keepac - local_env["PATH"] = str(project_root_folder) + ":" + local_env["PATH"] - # Auto-colorscheme detection does not work within vhs, so we manually - # set the style here - local_env["GLAMOUR_STYLE"] = colorscheme - - print(f"Recording {colorscheme} tapes...\n") - for tape in tapes.glob('*.tape'): - subprocess.run(['vhs', tape], env=local_env, cwd=tapes) - - recording = tapes / 'out.gif' - tape_name = path.splitext(tape.name)[0] - destination = gifs_folder / f'{tape_name}.{colorscheme}.gif' - - print(f"Moving {recording} to {destination}...") - recording.rename(destination) - recorded_tapes += 1 - - print("") - - print("Done!") - print(f"Recorded {recorded_tapes} tapes.") - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/tapes/.gitignore b/tapes/.gitignore deleted file mode 100644 index 0582d64..0000000 --- a/tapes/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.gif -*.md -!examples/*.md diff --git a/tapes/dark/demo.tape b/tapes/dark/demo.tape deleted file mode 100644 index 465205f..0000000 --- a/tapes/dark/demo.tape +++ /dev/null @@ -1,86 +0,0 @@ -Source ../partials/config.tape - -Hide -Type "mkdir keepac-demo" -Enter -Type "cd keepac-demo" -Enter -Type "clear" -Enter -Show - -Type "# Imagine you start a new project and want to keep a proper changelog." -Enter -Sleep 2500ms - -Type "# keepac's `changelog` CLI makes this simple:" -Enter -Sleep 2500ms - -Type "changelog init" -Enter -Sleep 4000ms - -Type "clear" -Enter -Sleep 1500ms - -Type "# That looked kinda empty. Let's change that!" -Enter -Sleep 2500ms - -Type "changelog add A beeeeauuuutiful, shiny new feature" -Enter -Sleep 1500ms - -Type "# Good job on that feature btw!" -Enter -Sleep 2500ms - -Type "# We can view our changelog anytime:" -Enter -Sleep 1500ms - -Type "changelog show" -Enter -Sleep 2500ms - -Type "# Let's add a few more changes!" -Enter -Sleep 1500ms - -Type "changelog fix A gnarly, disastrous bug" -Enter -Sleep 1500ms - -Type "changelog change Something that breaks backwards compatibility" -Enter -Sleep 2500ms - -Type "clear" -Enter -Sleep 1500ms - -Type "# I feel like we have enough changes to finally launch our project. Let's release v1!" -Enter -Sleep 2500ms - -Type "changelog release --major" -Enter -Sleep 5000ms - -Type "# Congrats! Now keep documenting your features." -Enter -Type "# Keepac has more functionality, explore it by reading this Readme or run" -Enter -Sleep 1500ms -Type "changelog --help" -Enter - -Sleep 10000ms - -Hide -Type "cd ../" -Enter -Type "rm -rf keepac-demo" -Enter diff --git a/tapes/dark/find.tape b/tapes/dark/find.tape deleted file mode 100644 index a221caf..0000000 --- a/tapes/dark/find.tape +++ /dev/null @@ -1,51 +0,0 @@ -Source ../partials/config.tape -Require tree - -Hide -Type "mkdir init-demo && cd init-demo" -Sleep 100ms -Enter -Type "changelog init" -Sleep 100ms -Enter -Type "clear" -Sleep 100ms -Enter -Show - -Type "ls" -Sleep 100ms -Enter -Sleep 2500ms - -Type "changelog find" -Sleep 100ms -Enter -Sleep 2500ms - -Type "mkdir -p some/deeply/nested/directory" -Sleep 100ms -Enter -Sleep 2500ms - -Type "cd some/deeply/nested/directory" -Sleep 100ms -Enter -Sleep 2500ms - -Type "changelog find" -Sleep 100ms -Enter -Sleep 2500ms - -Type "# Still finds the 'nearest' CHANGELOG.md!" -Sleep 5000ms -Enter - -Hide -Type "cd ../../../.." -Sleep 100ms -Enter -Type "rm -rf init-demo" -Sleep 100ms -Enter diff --git a/tapes/dark/insert.tape b/tapes/dark/insert.tape deleted file mode 100644 index 7eb33fb..0000000 --- a/tapes/dark/insert.tape +++ /dev/null @@ -1,36 +0,0 @@ -Source ../partials/config.tape - -Hide -# Create new file -Type "changelog init" -Sleep 500ms -Enter -Sleep 1500ms -Show - -# Trigger section selection -Type "changelog insert" -Sleep 500ms -Enter -Sleep 1500ms - -# Select "Fixed" -Down -Sleep 500ms -Down -Sleep 500ms -Down -Sleep 500ms -Down -Sleep 1500ms -Enter - -Type "That one annoying bug" -Enter - -Sleep 1500ms - -# Cleanup -Hide -Type rm CHANGELOG.md -Enter diff --git a/tapes/dark/search.tape b/tapes/dark/search.tape deleted file mode 100644 index 605f37d..0000000 --- a/tapes/dark/search.tape +++ /dev/null @@ -1,44 +0,0 @@ -Source ../partials/config.tape -Require wget - -Set Width 1500 -Set Height 800 - -Hide -# We use the tailwindcss changelog as an example -Type "wget https://github.com/tailwindlabs/tailwindcss/raw/master/CHANGELOG.md" -Sleep 250ms -Enter -Sleep 250ms -Type "clear" -Enter -Show - -Type "# Imagine you are trying to search for changes related to a specific feature." -Sleep 500ms -Enter - -Type "# Lets use TailwindCSS as an example. Let's try searching for changes related to flexbox" -Sleep 500ms -Enter - -Type "cat CHANGELOG.md | grep flex" -Sleep 500ms -Enter - -Type "# This works, but we can do better:" -Sleep 500ms -Enter - -Type "changelog search flex" -Sleep 500ms -Enter -Sleep 2500ms - -Type "# Much better! More context, nicer output and easier to remember." -Sleep 10000ms - -Hide -Type "rm CHANGELOG.md" -Enter - diff --git a/tapes/dark/show.tape b/tapes/dark/show.tape deleted file mode 100644 index 61345e8..0000000 --- a/tapes/dark/show.tape +++ /dev/null @@ -1,17 +0,0 @@ -Source ../partials/config.tape - -Hide -Type "cp ../examples/markdown.md ./CHANGELOG.md" -Enter -Sleep 250ms -Show - -Type "changelog show" -Sleep 1000ms -Enter -Sleep 15000ms - -Hide -Type "rm CHANGELOG.md" -Enter - diff --git a/tapes/dummy.tape b/tapes/dummy.tape deleted file mode 100644 index 36a52b6..0000000 --- a/tapes/dummy.tape +++ /dev/null @@ -1,2 +0,0 @@ -Type "test" -Enter \ No newline at end of file diff --git a/tapes/examples/markdown.md b/tapes/examples/markdown.md deleted file mode 100644 index c56fb52..0000000 --- a/tapes/examples/markdown.md +++ /dev/null @@ -1,31 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The changes describe a contrived project to demonstrate the markdown output of `keepac` / `changelog`. - -## [1.1.0] - 2022-04-04 - -> This release is sponsored by [our GitHub sponsors](https://github.com/sponsors/NiclasvanEyk) ❤️ - -### Added - -- A **whole new** way of doing the thing -- A new cool syntax highlighter: - ```rs - // Inline code snippets! - let foo = "bar" - ``` - -## [1.0.1] - 2022-02-03 - -### Fixed - -- A bug when calling `foo` without previously calling `bar` -- A _really_ complicated deadlock which is better explained in [#123](https://github.com/NiclasvanEyk/keepac/pull/123) - -## [1.0.0] - 2022-02-02 - -### Added - -- The initial version of the thing diff --git a/tapes/light/.gitignore b/tapes/light/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/tapes/light/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/tapes/partials/config.tape b/tapes/partials/config.tape deleted file mode 100644 index d8e8a4a..0000000 --- a/tapes/partials/config.tape +++ /dev/null @@ -1,13 +0,0 @@ -# This tape just houses shared configuration and is included/sourced into all -# other tapes - -Require changelog - -Set Padding 5 - - -Set TypingSpeed 75ms - -Set FontSize 18 -Set Width 1280 -Set Height 860 diff --git a/tapes/recordings/.gitignore b/tapes/recordings/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/tapes/recordings/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore