From 85610cc299eaf84ea918bfd9856f0df8e86dbe22 Mon Sep 17 00:00:00 2001 From: Niclas van Eyk Date: Sun, 22 Dec 2024 14:10:14 +0100 Subject: [PATCH 1/9] a fresh start in rust --- .github/workflows/ci.yml | 29 -- .github/workflows/goreleaser.yml | 35 -- .../workflows/record_and_publish_demos.yml | 61 --- .gitignore | 12 +- .goreleaser.yaml | 80 ---- CHANGELOG.md | 36 -- Cargo.lock | 272 +++++++++++++ Cargo.toml | 3 + Makefile | 28 -- cmd/diff.go | 47 --- cmd/edit.go | 28 -- cmd/find.go | 33 -- cmd/init.go | 50 --- cmd/insert.go | 195 ---------- cmd/insert_test.go | 95 ----- cmd/release.go | 113 ------ cmd/root.go | 34 -- cmd/search.go | 34 -- cmd/show.go | 130 ------- cmd/yank.go | 68 ---- crates/keepac-cli/Cargo.toml | 12 + crates/keepac-cli/src/main.rs | 57 +++ crates/keepac/Cargo.toml | 6 + crates/keepac/src/lib.rs | 14 + go.mod | 56 --- go.sum | 105 ----- internal/changelog/changelog.go | 154 -------- internal/changelog/completions.go | 33 -- internal/changelog/finder.go | 75 ---- internal/changelog/finder_test.go | 12 - internal/changelog/formatter.go | 115 ------ internal/changelog/formatter_test.go | 85 ---- internal/changelog/inserter.go | 149 ------- internal/changelog/inserter_test.go | 365 ------------------ internal/changelog/parser.go | 249 ------------ internal/changelog/parser_test.go | 59 --- internal/changelog/search.go | 59 --- internal/changelog/search_test.go | 33 -- internal/changelog/show.go | 67 ---- internal/changelog/yank.go | 16 - internal/editor/editor.go | 80 ---- internal/tui/choice.go | 122 ------ main.go | 7 - scripts/generate-completions | 10 - scripts/record-tapes | 66 ---- tapes/.gitignore | 3 - tapes/dark/demo.tape | 86 ----- tapes/dark/find.tape | 51 --- tapes/dark/insert.tape | 36 -- tapes/dark/search.tape | 44 --- tapes/dark/show.tape | 17 - tapes/dummy.tape | 2 - tapes/examples/markdown.md | 31 -- tapes/light/.gitignore | 2 - tapes/partials/config.tape | 13 - tapes/recordings/.gitignore | 2 - 56 files changed, 365 insertions(+), 3311 deletions(-) delete mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/goreleaser.yml delete mode 100644 .github/workflows/record_and_publish_demos.yml delete mode 100644 .goreleaser.yaml delete mode 100644 CHANGELOG.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 Makefile delete mode 100644 cmd/diff.go delete mode 100644 cmd/edit.go delete mode 100644 cmd/find.go delete mode 100644 cmd/init.go delete mode 100644 cmd/insert.go delete mode 100644 cmd/insert_test.go delete mode 100644 cmd/release.go delete mode 100644 cmd/root.go delete mode 100644 cmd/search.go delete mode 100644 cmd/show.go delete mode 100644 cmd/yank.go create mode 100644 crates/keepac-cli/Cargo.toml create mode 100644 crates/keepac-cli/src/main.rs create mode 100644 crates/keepac/Cargo.toml create mode 100644 crates/keepac/src/lib.rs delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 internal/changelog/changelog.go delete mode 100644 internal/changelog/completions.go delete mode 100644 internal/changelog/finder.go delete mode 100644 internal/changelog/finder_test.go delete mode 100644 internal/changelog/formatter.go delete mode 100644 internal/changelog/formatter_test.go delete mode 100644 internal/changelog/inserter.go delete mode 100644 internal/changelog/inserter_test.go delete mode 100644 internal/changelog/parser.go delete mode 100644 internal/changelog/parser_test.go delete mode 100644 internal/changelog/search.go delete mode 100644 internal/changelog/search_test.go delete mode 100644 internal/changelog/show.go delete mode 100644 internal/changelog/yank.go delete mode 100644 internal/editor/editor.go delete mode 100644 internal/tui/choice.go delete mode 100644 main.go delete mode 100755 scripts/generate-completions delete mode 100755 scripts/record-tapes delete mode 100644 tapes/.gitignore delete mode 100644 tapes/dark/demo.tape delete mode 100644 tapes/dark/find.tape delete mode 100644 tapes/dark/insert.tape delete mode 100644 tapes/dark/search.tape delete mode 100644 tapes/dark/show.tape delete mode 100644 tapes/dummy.tape delete mode 100644 tapes/examples/markdown.md delete mode 100644 tapes/light/.gitignore delete mode 100644 tapes/partials/config.tape delete mode 100644 tapes/recordings/.gitignore 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..519af24 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,272 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[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", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[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", +] + +[[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", +] + +[[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 = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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" + +[[package]] +name = "keepac-cli" +version = "0.1.0" +dependencies = [ + "clap", + "clap-verbosity-flag", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[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 = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[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 = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[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..5f15a90 --- /dev/null +++ b/crates/keepac-cli/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "keepac-cli" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "changelog" +path = "src/main.rs" + +[dependencies] +clap = { version = "4.5.23", features = ["derive", "env", "unicode"] } +clap-verbosity-flag = "3.0.2" diff --git a/crates/keepac-cli/src/main.rs b/crates/keepac-cli/src/main.rs new file mode 100644 index 0000000..2655a82 --- /dev/null +++ b/crates/keepac-cli/src/main.rs @@ -0,0 +1,57 @@ +use clap::{Parser, Subcommand}; + +#[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 {}, + Yank {}, +} + +fn main() { + let args = Cli::parse(); + + let command = args.command.unwrap_or(Command::Show {}); + match command { + Command::Add {} => todo!(), + Command::Change {} => todo!(), + Command::Deprecate {} => todo!(), + Command::Diff {} => todo!(), + Command::Edit {} => todo!(), + Command::Find {} => todo!(), + Command::Fix {} => todo!(), + Command::Init {} => todo!(), + Command::Insert {} => todo!(), + Command::Release {} => todo!(), + Command::Remove {} => todo!(), + Command::Search {} => todo!(), + Command::Secure {} => todo!(), + Command::Show {} => todo!(), + Command::Yank {} => todo!(), + } +} diff --git a/crates/keepac/Cargo.toml b/crates/keepac/Cargo.toml new file mode 100644 index 0000000..5cdaa95 --- /dev/null +++ b/crates/keepac/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "keepac" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/crates/keepac/src/lib.rs b/crates/keepac/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/crates/keepac/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} 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 From f5847388ff51586bb13026ef10ddc817b3ed4091 Mon Sep 17 00:00:00 2001 From: Niclas van Eyk Date: Sun, 22 Dec 2024 22:41:27 +0100 Subject: [PATCH 2/9] implemented find and partial init --- Cargo.lock | 102 +++++++++++++++++++++++++ crates/keepac-cli/Cargo.toml | 5 ++ crates/keepac-cli/src/commands/find.rs | 16 ++++ crates/keepac-cli/src/commands/init.rs | 65 ++++++++++++++++ crates/keepac-cli/src/commands/mod.rs | 5 ++ crates/keepac-cli/src/main.rs | 65 ++++++++++++++-- crates/keepac/Cargo.toml | 3 + crates/keepac/src/find.rs | 89 +++++++++++++++++++++ crates/keepac/src/lib.rs | 15 +--- 9 files changed, 346 insertions(+), 19 deletions(-) create mode 100644 crates/keepac-cli/src/commands/find.rs create mode 100644 crates/keepac-cli/src/commands/init.rs create mode 100644 crates/keepac-cli/src/commands/mod.rs create mode 100644 crates/keepac/src/find.rs diff --git a/Cargo.lock b/Cargo.lock index 519af24..5fcf1de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + [[package]] name = "clap" version = "4.5.23" @@ -109,6 +115,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "heck" version = "0.5.0" @@ -124,15 +136,27 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "keepac" version = "0.1.0" +dependencies = [ + "tempdir", +] [[package]] name = "keepac-cli" version = "0.1.0" dependencies = [ + "anyhow", "clap", "clap-verbosity-flag", + "keepac", + "tempdir", ] +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + [[package]] name = "log" version = "0.4.22" @@ -157,6 +181,52 @@ 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 = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "strsim" version = "0.11.1" @@ -174,6 +244,16 @@ dependencies = [ "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 = "unicase" version = "2.8.0" @@ -198,6 +278,28 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[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-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.59.0" diff --git a/crates/keepac-cli/Cargo.toml b/crates/keepac-cli/Cargo.toml index 5f15a90..6c92171 100644 --- a/crates/keepac-cli/Cargo.toml +++ b/crates/keepac-cli/Cargo.toml @@ -8,5 +8,10 @@ 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/" } + +[dev-dependencies] +tempdir = "0.3.7" diff --git a/crates/keepac-cli/src/commands/find.rs b/crates/keepac-cli/src/commands/find.rs new file mode 100644 index 0000000..01fc2d4 --- /dev/null +++ b/crates/keepac-cli/src/commands/find.rs @@ -0,0 +1,16 @@ +use std::path::Path; + +use crate::{ErrorExitCode, KeepacCliError, SubcommandResult}; + +pub fn find(path: &Path) -> SubcommandResult { + match keepac::find::nearest_changelog_path(path) { + Some(changelog_path) => { + println!("{}", changelog_path.display()); + Ok(()) + } + None => Err(KeepacCliError { + message: String::from("Failed to find CHANGELOG.md"), + exit_code: ErrorExitCode::ChangelogNotFound, + }), + } +} diff --git a/crates/keepac-cli/src/commands/init.rs b/crates/keepac-cli/src/commands/init.rs new file mode 100644 index 0000000..66105e3 --- /dev/null +++ b/crates/keepac-cli/src/commands/init.rs @@ -0,0 +1,65 @@ +use anyhow::anyhow; +use std::{fs::File, io::Write, path::Path}; + +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").into()); + } + + let mut changelog = File::create_new(&changelog_path)?; + changelog.write_all(TEMPLATE.as_bytes())?; + + // TODO: Print _highlighted_ to console + println!( + "Initialized empty changelog at {}:", + changelog_path.display() + ); + println!("\n{}", TEMPLATE); + 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..2c7e25f --- /dev/null +++ b/crates/keepac-cli/src/commands/mod.rs @@ -0,0 +1,5 @@ +mod find; +mod init; + +pub(crate) use find::find; +pub(crate) use init::init; diff --git a/crates/keepac-cli/src/main.rs b/crates/keepac-cli/src/main.rs index 2655a82..da3184a 100644 --- a/crates/keepac-cli/src/main.rs +++ b/crates/keepac-cli/src/main.rs @@ -1,3 +1,7 @@ +pub mod commands; + +use std::{error::Error, fmt::Display, path::Path}; + use clap::{Parser, Subcommand}; #[derive(Debug, Parser)] @@ -33,19 +37,61 @@ enum Command { Yank {}, } -fn main() { - let args = Cli::parse(); +#[derive(Debug, Clone, Copy)] +enum ErrorExitCode { + Unknown = 1, + ChangelogNotFound = 2, +} + +#[derive(Debug)] +struct KeepacCliError { + pub message: String, + pub exit_code: ErrorExitCode, +} + +impl Display for KeepacCliError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl Error for KeepacCliError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + None + } +} - let command = args.command.unwrap_or(Command::Show {}); +impl From for KeepacCliError { + fn from(value: anyhow::Error) -> Self { + KeepacCliError { + message: format!("{}", value), + exit_code: ErrorExitCode::Unknown, + } + } +} + +impl From for KeepacCliError { + fn from(value: std::io::Error) -> Self { + KeepacCliError { + message: format!("{}", value), + exit_code: ErrorExitCode::Unknown, + } + } +} + +type SubcommandResult = Result<(), KeepacCliError>; + +fn run(cli: Cli, path: &Path) -> SubcommandResult { + let command = cli.command.unwrap_or(Command::Show {}); match command { Command::Add {} => todo!(), Command::Change {} => todo!(), Command::Deprecate {} => todo!(), Command::Diff {} => todo!(), Command::Edit {} => todo!(), - Command::Find {} => todo!(), + Command::Find {} => commands::find(path), Command::Fix {} => todo!(), - Command::Init {} => todo!(), + Command::Init {} => commands::init(path), Command::Insert {} => todo!(), Command::Release {} => todo!(), Command::Remove {} => todo!(), @@ -55,3 +101,12 @@ fn main() { Command::Yank {} => todo!(), } } + +fn main() { + let cwd = std::env::current_dir().unwrap(); + + if let Err(err) = run(Cli::parse(), &cwd) { + eprintln!("{}", err); + std::process::exit(err.exit_code as i32); + }; +} diff --git a/crates/keepac/Cargo.toml b/crates/keepac/Cargo.toml index 5cdaa95..5ae0d91 100644 --- a/crates/keepac/Cargo.toml +++ b/crates/keepac/Cargo.toml @@ -4,3 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] + +[dev-dependencies] +tempdir = "0.3.7" 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/lib.rs b/crates/keepac/src/lib.rs index 7d12d9a..9b5fc57 100644 --- a/crates/keepac/src/lib.rs +++ b/crates/keepac/src/lib.rs @@ -1,14 +1 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod find; From e7a76bc69b30a7308cce8501c3a2137554530a4d Mon Sep 17 00:00:00 2001 From: Niclas van Eyk Date: Sun, 29 Dec 2024 16:53:49 +0100 Subject: [PATCH 3/9] an experimental versions command --- Cargo.lock | 687 ++++++++++++++++++++- crates/keepac-cli/src/commands/find.rs | 2 +- crates/keepac-cli/src/commands/init.rs | 3 +- crates/keepac-cli/src/commands/mod.rs | 2 + crates/keepac-cli/src/commands/versions.rs | 32 + crates/keepac-cli/src/errors.rs | 52 ++ crates/keepac-cli/src/main.rs | 54 +- crates/keepac/Cargo.toml | 13 +- crates/keepac/src/lib.rs | 5 + crates/keepac/src/parse.rs | 164 +++++ crates/keepac/src/render.rs | 95 +++ 11 files changed, 1048 insertions(+), 61 deletions(-) create mode 100644 crates/keepac-cli/src/commands/versions.rs create mode 100644 crates/keepac-cli/src/errors.rs create mode 100644 crates/keepac/src/parse.rs create mode 100644 crates/keepac/src/render.rs diff --git a/Cargo.lock b/Cargo.lock index 5fcf1de..bb6f86f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # 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" @@ -38,7 +47,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -48,7 +57,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -57,6 +66,39 @@ 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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" +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" @@ -88,7 +130,7 @@ dependencies = [ "clap_lex", "strsim", "unicase", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] @@ -100,7 +142,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.91", ] [[package]] @@ -115,18 +157,170 @@ 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" @@ -137,7 +331,14 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" name = "keepac" version = "0.1.0" dependencies = [ + "cc", + "pulldown-cmark", + "streaming-iterator", "tempdir", + "termimad", + "textwrap", + "tree-sitter", + "tree-sitter-md", ] [[package]] @@ -151,18 +352,119 @@ dependencies = [ "tempdir", ] +[[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.91", +] + [[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" @@ -172,6 +474,25 @@ 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.37" @@ -218,6 +539,44 @@ 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" @@ -227,12 +586,136 @@ 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.91", +] + +[[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.91" @@ -254,6 +737,93 @@ dependencies = [ "remove_dir_all", ] +[[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", + "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", +] + +[[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.91", +] + +[[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.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f968c22a01010b83fc960455ae729db08dbeb6388617d9113897cb9204b030" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "unicase" version = "2.8.0" @@ -266,6 +836,18 @@ 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" @@ -278,6 +860,12 @@ 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" @@ -300,13 +888,46 @@ 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", + "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]] @@ -315,28 +936,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "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" @@ -349,24 +988,48 @@ 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" diff --git a/crates/keepac-cli/src/commands/find.rs b/crates/keepac-cli/src/commands/find.rs index 01fc2d4..e002e6a 100644 --- a/crates/keepac-cli/src/commands/find.rs +++ b/crates/keepac-cli/src/commands/find.rs @@ -1,6 +1,6 @@ use std::path::Path; -use crate::{ErrorExitCode, KeepacCliError, SubcommandResult}; +use crate::{errors::ErrorExitCode, KeepacCliError, SubcommandResult}; pub fn find(path: &Path) -> SubcommandResult { match keepac::find::nearest_changelog_path(path) { diff --git a/crates/keepac-cli/src/commands/init.rs b/crates/keepac-cli/src/commands/init.rs index 66105e3..64517af 100644 --- a/crates/keepac-cli/src/commands/init.rs +++ b/crates/keepac-cli/src/commands/init.rs @@ -1,4 +1,5 @@ use anyhow::anyhow; +use keepac::render::render; use std::{fs::File, io::Write, path::Path}; use crate::SubcommandResult; @@ -27,7 +28,7 @@ pub(crate) fn init(path: &Path) -> SubcommandResult { "Initialized empty changelog at {}:", changelog_path.display() ); - println!("\n{}", TEMPLATE); + render(TEMPLATE); Ok(()) } diff --git a/crates/keepac-cli/src/commands/mod.rs b/crates/keepac-cli/src/commands/mod.rs index 2c7e25f..49bf6ba 100644 --- a/crates/keepac-cli/src/commands/mod.rs +++ b/crates/keepac-cli/src/commands/mod.rs @@ -1,5 +1,7 @@ mod find; mod init; +mod versions; pub(crate) use find::find; pub(crate) use init::init; +pub(crate) use versions::versions; diff --git a/crates/keepac-cli/src/commands/versions.rs b/crates/keepac-cli/src/commands/versions.rs new file mode 100644 index 0000000..ef7a4d7 --- /dev/null +++ b/crates/keepac-cli/src/commands/versions.rs @@ -0,0 +1,32 @@ +use std::{fs::File, io::Read, path::Path}; + +use keepac::parse::{parse_versions, Changelog}; + +use crate::{errors::ErrorExitCode, KeepacCliError, SubcommandResult}; + +pub(crate) fn versions(path: &Path) -> SubcommandResult { + let Some(changelog_path) = keepac::find::nearest_changelog_path(path) else { + return Err(KeepacCliError { + message: String::from("Failed to find CHANGELOG.md"), + exit_code: ErrorExitCode::ChangelogNotFound, + }); + }; + + let mut changelog_file = File::open(changelog_path)?; + let mut changelog_source = String::new(); + changelog_file.read_to_string(&mut changelog_source)?; + + // TODO: don't unwrap here + let changelog = Changelog::try_from(changelog_source.as_str()).unwrap(); + let versions = parse_versions(&changelog); + + for version in versions { + println!( + "{} {}", + version.released_at.unwrap_or(" "), + version.name + ); + } + + Ok(()) +} diff --git a/crates/keepac-cli/src/errors.rs b/crates/keepac-cli/src/errors.rs new file mode 100644 index 0000000..a98916a --- /dev/null +++ b/crates/keepac-cli/src/errors.rs @@ -0,0 +1,52 @@ +use std::{error::Error, fmt::Display}; + +#[derive(Debug, Clone, Copy)] +pub enum ErrorExitCode { + Unknown = 1, + ChangelogNotFound = 2, +} + +#[derive(Debug)] +pub struct KeepacCliError { + pub message: String, + pub exit_code: ErrorExitCode, +} + +impl Display for KeepacCliError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl Error for KeepacCliError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + None + } +} + +impl From for KeepacCliError { + fn from(value: anyhow::Error) -> Self { + KeepacCliError { + message: format!("{}", value), + exit_code: ErrorExitCode::Unknown, + } + } +} + +impl From for KeepacCliError { + fn from(value: std::io::Error) -> Self { + KeepacCliError { + message: format!("{}", value), + exit_code: ErrorExitCode::Unknown, + } + } +} + +impl From> for KeepacCliError { + fn from(value: Box) -> Self { + KeepacCliError { + message: format!("{}", value), + exit_code: ErrorExitCode::Unknown, + } + } +} diff --git a/crates/keepac-cli/src/main.rs b/crates/keepac-cli/src/main.rs index da3184a..47035d0 100644 --- a/crates/keepac-cli/src/main.rs +++ b/crates/keepac-cli/src/main.rs @@ -1,8 +1,10 @@ pub mod commands; - -use std::{error::Error, fmt::Display, path::Path}; +pub mod errors; use clap::{Parser, Subcommand}; +use std::path::Path; + +use crate::errors::KeepacCliError; #[derive(Debug, Parser)] #[command( @@ -34,51 +36,10 @@ enum Command { Search {}, Secure {}, Show {}, + Versions {}, Yank {}, } -#[derive(Debug, Clone, Copy)] -enum ErrorExitCode { - Unknown = 1, - ChangelogNotFound = 2, -} - -#[derive(Debug)] -struct KeepacCliError { - pub message: String, - pub exit_code: ErrorExitCode, -} - -impl Display for KeepacCliError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.message) - } -} - -impl Error for KeepacCliError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - None - } -} - -impl From for KeepacCliError { - fn from(value: anyhow::Error) -> Self { - KeepacCliError { - message: format!("{}", value), - exit_code: ErrorExitCode::Unknown, - } - } -} - -impl From for KeepacCliError { - fn from(value: std::io::Error) -> Self { - KeepacCliError { - message: format!("{}", value), - exit_code: ErrorExitCode::Unknown, - } - } -} - type SubcommandResult = Result<(), KeepacCliError>; fn run(cli: Cli, path: &Path) -> SubcommandResult { @@ -98,6 +59,7 @@ fn run(cli: Cli, path: &Path) -> SubcommandResult { Command::Search {} => todo!(), Command::Secure {} => todo!(), Command::Show {} => todo!(), + Command::Versions {} => commands::versions(path), Command::Yank {} => todo!(), } } @@ -105,7 +67,9 @@ fn run(cli: Cli, path: &Path) -> SubcommandResult { fn main() { let cwd = std::env::current_dir().unwrap(); - if let Err(err) = run(Cli::parse(), &cwd) { + let result = run(Cli::parse(), &cwd); + + if let Err(err) = result { eprintln!("{}", err); std::process::exit(err.exit_code as i32); }; diff --git a/crates/keepac/Cargo.toml b/crates/keepac/Cargo.toml index 5ae0d91..fda138b 100644 --- a/crates/keepac/Cargo.toml +++ b/crates/keepac/Cargo.toml @@ -3,7 +3,16 @@ name = "keepac" version = "0.1.0" edition = "2021" -[dependencies] - [dev-dependencies] tempdir = "0.3.7" +pulldown-cmark = "0.12.2" + +[build-dependencies] +cc = "*" + +[dependencies] +streaming-iterator = "0.1.9" +termimad = "0.31.1" +textwrap = { version = "0.16.1", features = ["terminal_size"] } +tree-sitter = "0.24.6" +tree-sitter-md = "0.3.2" diff --git a/crates/keepac/src/lib.rs b/crates/keepac/src/lib.rs index 9b5fc57..98be92a 100644 --- a/crates/keepac/src/lib.rs +++ b/crates/keepac/src/lib.rs @@ -1 +1,6 @@ +/// Find CHANGELOG.md files on the filesystem tree. pub mod find; +/// Parse changelogs given a few structural constraints. +pub mod parse; +/// Render out changelogs to the terminal. +pub mod render; diff --git a/crates/keepac/src/parse.rs b/crates/keepac/src/parse.rs new file mode 100644 index 0000000..dbe30ee --- /dev/null +++ b/crates/keepac/src/parse.rs @@ -0,0 +1,164 @@ +use std::collections::HashMap; + +use streaming_iterator::StreamingIterator; +use tree_sitter::{Parser, Query, QueryCursor, QueryMatch, QueryMatches, Tree}; + +pub struct Changelog<'a> { + pub source: &'a str, + pub tree: Tree, +} + +impl<'a> Changelog<'a> { + pub fn query(&'a self, query: &str) -> ChangelogQueryCursor<'a> { + ChangelogQueryCursor { + query: Query::new(&tree_sitter_md::LANGUAGE.into(), query).unwrap(), + cursor: QueryCursor::new(), + changelog: self, + } + } +} + +pub struct ChangelogQueryCursor<'c> { + cursor: QueryCursor, + query: Query, + changelog: &'c Changelog<'c>, +} + +impl<'c> ChangelogQueryCursor<'c> { + pub fn matches(&'c mut self) -> QueryMatches<'_, '_, &[u8], &[u8]> { + self.cursor.matches( + &self.query, + self.changelog.tree.root_node(), + self.changelog.source.as_bytes(), + ) + } +} + +#[derive(Debug)] +pub enum MarkdownParserError { + FailedToLoadGrammar, + CouldNotParseTree, +} + +impl<'a> TryFrom<&'a str> for Changelog<'a> { + type Error = MarkdownParserError; + + fn try_from(value: &'a str) -> Result { + let mut parser = Parser::new(); + + let Ok(_) = parser.set_language(&tree_sitter_md::LANGUAGE.into()) else { + return Err(MarkdownParserError::FailedToLoadGrammar); + }; + + let Some(tree) = parser.parse(value, None) else { + return Err(MarkdownParserError::CouldNotParseTree); + }; + + Ok(Self { + source: value, + 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: &Changelog<'c>) -> Vec> { + let mut query = + changelog.query("(atx_heading (atx_h2_marker) heading_content: (inline) @content)"); + let mut matches = query.matches(); + + let mut versions = Vec::new(); + while let Some(m) = matches.next() { + for c in m.captures { + let text = c.node.utf8_text(changelog.source.as_bytes()).unwrap(); + let parts: Vec<&str> = text.split(" - ").collect(); + + versions.push(Version { + name: parts[0], + released_at: parts.get(1).copied(), + href: None, + }) + } + } + + versions +} + +pub fn gather_link_defs<'a>(changelog: Changelog<'a>) -> HashMap<&'a str, &'a str> { + let mut query = changelog + .query("(link_reference_definition (link_label) @label (link_destination) @destination)"); + let mut matches = query.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.source.as_bytes()) + .unwrap() + .strip_prefix('[') + .unwrap() + .strip_suffix(']') + .unwrap(); + let destination = captures + .get(1) + .unwrap() + .node + .utf8_text(changelog.source.as_bytes()) + .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/render.rs b/crates/keepac/src/render.rs new file mode 100644 index 0000000..7dc368a --- /dev/null +++ b/crates/keepac/src/render.rs @@ -0,0 +1,95 @@ +use std::{collections::HashMap, fmt::Display}; + +use tree_sitter::Node; + +use crate::parse::{gather_link_defs, Changelog}; + +struct ChangelogRenderer<'a, 'f> { + changelog: &'a Changelog<'a>, + f: &'a mut std::fmt::Formatter<'f>, + // Maps the [1.0.0] to their actual URLs at the bottom + // link_defs: HashMap<&'a str, &'a str>, +} + +impl<'a, 'f> ChangelogRenderer<'a, 'f> { + pub fn render(&mut self) -> std::fmt::Result { + let mut cursor = self.changelog.tree.walk(); + + // The first node is always the document one + cursor.goto_first_child(); + cursor.goto_first_child(); + cursor.goto_next_sibling(); + cursor.goto_next_sibling(); + + let node = cursor.node(); + writeln!(self.f, "NODE: {}", node)?; + + let contents = node.child_by_field_name("heading_content"); + match contents { + Some(c) => self.render_heading(c), + None => {} + }; + + Ok(()) + } + + fn render_heading(&self, node: Node) { + let res = node.utf8_text(self.changelog.source.as_bytes()).unwrap(); + println!("HEADING: {}", res); + } + + fn render_inline(&self, node: Node) {} +} + +impl<'a> Display for Changelog<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // let link_defs = gather_link_defs(&self.tree, self.source); + let mut renderer = ChangelogRenderer { + changelog: self, + f, + // link_defs, + }; + renderer.render() + } +} + +#[derive(Debug)] +enum MarkdownParserError { + FailedToLoadGrammar, + CouldNotParseTree, +} + +pub fn render(changelog: &str) { + let renderer = Changelog::try_from(changelog).unwrap(); + println!("{}", renderer); +} + +#[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_renders() { + render(A_CHANGELOG) + } +} From 00f15bca2cb7d91c868a05d23f496e8fa489c036 Mon Sep 17 00:00:00 2001 From: Niclas van Eyk Date: Tue, 31 Dec 2024 17:20:13 +0100 Subject: [PATCH 4/9] show (no highlighting) + edit --- crates/keepac-cli/src/commands/edit.rs | 42 ++++++++++++++++++++++ crates/keepac-cli/src/commands/mod.rs | 4 +++ crates/keepac-cli/src/commands/show.rs | 22 ++++++++++++ crates/keepac-cli/src/commands/versions.rs | 15 ++++++++ crates/keepac-cli/src/main.rs | 4 +-- crates/keepac/src/parse.rs | 1 + 6 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 crates/keepac-cli/src/commands/edit.rs create mode 100644 crates/keepac-cli/src/commands/show.rs diff --git a/crates/keepac-cli/src/commands/edit.rs b/crates/keepac-cli/src/commands/edit.rs new file mode 100644 index 0000000..8ec6ee7 --- /dev/null +++ b/crates/keepac-cli/src/commands/edit.rs @@ -0,0 +1,42 @@ +use std::{ + env, io, + path::Path, + process::{Child, Command}, +}; + +use crate::{errors::ErrorExitCode, KeepacCliError, SubcommandResult}; + +pub fn edit(path: &Path) -> SubcommandResult { + let Some(changelog_path) = keepac::find::nearest_changelog_path(path) else { + return Err(KeepacCliError { + message: String::from("Failed to find CHANGELOG.md"), + exit_code: ErrorExitCode::ChangelogNotFound, + }); + }; + + let mut process = try_terminal_editor(&changelog_path) + .unwrap_or_else(|| try_system_editor(&changelog_path))?; + let exit_status = process.wait()?; + + // TODO: 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/mod.rs b/crates/keepac-cli/src/commands/mod.rs index 49bf6ba..2705482 100644 --- a/crates/keepac-cli/src/commands/mod.rs +++ b/crates/keepac-cli/src/commands/mod.rs @@ -1,7 +1,11 @@ +mod edit; mod find; mod init; +mod show; mod versions; +pub(crate) use edit::edit; pub(crate) use find::find; pub(crate) use init::init; +pub(crate) use show::show; pub(crate) use versions::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..c4035bb --- /dev/null +++ b/crates/keepac-cli/src/commands/show.rs @@ -0,0 +1,22 @@ +use std::io::Read; +use std::{fs::File, path::Path}; + +use crate::{errors::ErrorExitCode, KeepacCliError, SubcommandResult}; + +pub fn show(path: &Path) -> SubcommandResult { + let Some(changelog_path) = keepac::find::nearest_changelog_path(path) else { + return Err(KeepacCliError { + message: String::from("Failed to find CHANGELOG.md"), + exit_code: ErrorExitCode::ChangelogNotFound, + }); + }; + + let mut changelog_file = File::open(changelog_path)?; + let mut changelog_source = String::new(); + changelog_file.read_to_string(&mut changelog_source)?; + + // TODO: Actually highlight + println!("{}", changelog_source); + + Ok(()) +} diff --git a/crates/keepac-cli/src/commands/versions.rs b/crates/keepac-cli/src/commands/versions.rs index ef7a4d7..cfc1305 100644 --- a/crates/keepac-cli/src/commands/versions.rs +++ b/crates/keepac-cli/src/commands/versions.rs @@ -20,7 +20,22 @@ pub(crate) fn versions(path: &Path) -> SubcommandResult { let changelog = Changelog::try_from(changelog_source.as_str()).unwrap(); 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(" "), diff --git a/crates/keepac-cli/src/main.rs b/crates/keepac-cli/src/main.rs index 47035d0..c7f6a40 100644 --- a/crates/keepac-cli/src/main.rs +++ b/crates/keepac-cli/src/main.rs @@ -49,7 +49,7 @@ fn run(cli: Cli, path: &Path) -> SubcommandResult { Command::Change {} => todo!(), Command::Deprecate {} => todo!(), Command::Diff {} => todo!(), - Command::Edit {} => todo!(), + Command::Edit {} => commands::edit(path), Command::Find {} => commands::find(path), Command::Fix {} => todo!(), Command::Init {} => commands::init(path), @@ -58,7 +58,7 @@ fn run(cli: Cli, path: &Path) -> SubcommandResult { Command::Remove {} => todo!(), Command::Search {} => todo!(), Command::Secure {} => todo!(), - Command::Show {} => todo!(), + Command::Show {} => commands::show(path), Command::Versions {} => commands::versions(path), Command::Yank {} => todo!(), } diff --git a/crates/keepac/src/parse.rs b/crates/keepac/src/parse.rs index dbe30ee..7e538e6 100644 --- a/crates/keepac/src/parse.rs +++ b/crates/keepac/src/parse.rs @@ -74,6 +74,7 @@ pub fn parse_versions<'c>(changelog: &Changelog<'c>) -> Vec> { let mut matches = query.matches(); let mut versions = Vec::new(); + while let Some(m) = matches.next() { for c in m.captures { let text = c.node.utf8_text(changelog.source.as_bytes()).unwrap(); From 8ca4c944a29540f61f3efb16597f13e5ded0b2b6 Mon Sep 17 00:00:00 2001 From: Niclas van Eyk Date: Wed, 1 Jan 2025 16:47:46 +0100 Subject: [PATCH 5/9] reorder some stuff --- crates/keepac-cli/src/commands/show.rs | 5 ++ crates/keepac-cli/src/commands/versions.rs | 3 +- crates/keepac-cli/src/main.rs | 12 ++--- crates/keepac/src/changelog.rs | 25 +++++++++ crates/keepac/src/changelog/from.rs | 55 ++++++++++++++++++++ crates/keepac/src/changelog/query.rs | 19 +++++++ crates/keepac/src/lib.rs | 4 ++ crates/keepac/src/parse.rs | 59 +--------------------- crates/keepac/src/render.rs | 10 +--- 9 files changed, 119 insertions(+), 73 deletions(-) create mode 100644 crates/keepac/src/changelog.rs create mode 100644 crates/keepac/src/changelog/from.rs create mode 100644 crates/keepac/src/changelog/query.rs diff --git a/crates/keepac-cli/src/commands/show.rs b/crates/keepac-cli/src/commands/show.rs index c4035bb..ccb60d2 100644 --- a/crates/keepac-cli/src/commands/show.rs +++ b/crates/keepac-cli/src/commands/show.rs @@ -3,6 +3,11 @@ use std::{fs::File, path::Path}; use crate::{errors::ErrorExitCode, KeepacCliError, SubcommandResult}; +pub struct ShowCommandOptions { + /// An optional version to show the changes for + version: Option, +} + pub fn show(path: &Path) -> SubcommandResult { let Some(changelog_path) = keepac::find::nearest_changelog_path(path) else { return Err(KeepacCliError { diff --git a/crates/keepac-cli/src/commands/versions.rs b/crates/keepac-cli/src/commands/versions.rs index cfc1305..636057a 100644 --- a/crates/keepac-cli/src/commands/versions.rs +++ b/crates/keepac-cli/src/commands/versions.rs @@ -1,6 +1,7 @@ use std::{fs::File, io::Read, path::Path}; -use keepac::parse::{parse_versions, Changelog}; +use keepac::parse::parse_versions; +use keepac::Changelog; use crate::{errors::ErrorExitCode, KeepacCliError, SubcommandResult}; diff --git a/crates/keepac-cli/src/main.rs b/crates/keepac-cli/src/main.rs index c7f6a40..80d899f 100644 --- a/crates/keepac-cli/src/main.rs +++ b/crates/keepac-cli/src/main.rs @@ -45,19 +45,19 @@ type SubcommandResult = Result<(), KeepacCliError>; fn run(cli: Cli, path: &Path) -> SubcommandResult { let command = cli.command.unwrap_or(Command::Show {}); match command { - Command::Add {} => todo!(), - Command::Change {} => todo!(), - Command::Deprecate {} => todo!(), + /* change */ Command::Add {} => todo!(), + /* change */ Command::Change {} => todo!(), + /* change */ Command::Deprecate {} => todo!(), Command::Diff {} => todo!(), Command::Edit {} => commands::edit(path), Command::Find {} => commands::find(path), - Command::Fix {} => todo!(), + /* change */ Command::Fix {} => todo!(), Command::Init {} => commands::init(path), Command::Insert {} => todo!(), Command::Release {} => todo!(), - Command::Remove {} => todo!(), + /* change */ Command::Remove {} => todo!(), Command::Search {} => todo!(), - Command::Secure {} => todo!(), + /* change */ Command::Secure {} => todo!(), Command::Show {} => commands::show(path), Command::Versions {} => commands::versions(path), Command::Yank {} => todo!(), diff --git a/crates/keepac/src/changelog.rs b/crates/keepac/src/changelog.rs new file mode 100644 index 0000000..e96260b --- /dev/null +++ b/crates/keepac/src/changelog.rs @@ -0,0 +1,25 @@ +use tree_sitter::{Query, QueryCursor, Tree}; + +use crate::changelog::query::ChangelogQueryCursor; + +/// Implementations of [From] and [TryFrom] for [Changelog] from various structs. +mod from; + +mod query; + +pub struct Changelog<'a> { + pub(crate) source: &'a str, + pub(crate) tree: Tree, +} + +impl<'a> Changelog<'a> { + pub fn query(&'a self, query: &str) -> ChangelogQueryCursor<'a> { + ChangelogQueryCursor { + query: Query::new(&tree_sitter_md::LANGUAGE.into(), query).unwrap(), + cursor: QueryCursor::new(), + changelog: self, + } + } + + // TODO: Version iterator +} diff --git a/crates/keepac/src/changelog/from.rs b/crates/keepac/src/changelog/from.rs new file mode 100644 index 0000000..ed4c00f --- /dev/null +++ b/crates/keepac/src/changelog/from.rs @@ -0,0 +1,55 @@ +use crate::Changelog; + +use tree_sitter::Parser; + +#[derive(Debug)] +pub enum MarkdownParserError { + FailedToLoadGrammar, + CouldNotParseTree, +} + +/// Get a Changelog from a string +impl<'a> TryFrom<&'a str> for Changelog<'a> { + type Error = MarkdownParserError; + + fn try_from(value: &'a str) -> Result { + let mut parser = Parser::new(); + + let Ok(_) = parser.set_language(&tree_sitter_md::LANGUAGE.into()) else { + return Err(MarkdownParserError::FailedToLoadGrammar); + }; + + let Some(tree) = parser.parse(value, None) else { + return Err(MarkdownParserError::CouldNotParseTree); + }; + + Ok(Self { + source: value, + tree, + }) + } +} + +// Get a Changelog from a file +// impl<'a> TryFrom<&'a str> for Changelog<'a> { +// type Error = MarkdownParserError; +// +// fn try_from(value: &'a str) -> Result { +// let mut parser = Parser::new(); +// +// let Ok(_) = parser.set_language(&tree_sitter_md::LANGUAGE.into()) else { +// return Err(MarkdownParserError::FailedToLoadGrammar); +// }; +// +// let Some(tree) = parser.parse(value, None) else { +// return Err(MarkdownParserError::CouldNotParseTree); +// }; +// +// Ok(Self { +// source: value, +// tree, +// }) +// } +// } + +// Get a Changelog from a pathbuf that implements the recursive upwards iteration diff --git a/crates/keepac/src/changelog/query.rs b/crates/keepac/src/changelog/query.rs new file mode 100644 index 0000000..f985f43 --- /dev/null +++ b/crates/keepac/src/changelog/query.rs @@ -0,0 +1,19 @@ +use tree_sitter::{Query, QueryCursor, QueryMatches}; + +use crate::Changelog; + +pub struct ChangelogQueryCursor<'c> { + pub(crate) cursor: QueryCursor, + pub(crate) query: Query, + pub(crate) changelog: &'c Changelog<'c>, +} + +impl<'c> ChangelogQueryCursor<'c> { + pub fn matches(&'c mut self) -> QueryMatches<'_, '_, &[u8], &[u8]> { + self.cursor.matches( + &self.query, + self.changelog.tree.root_node(), + self.changelog.source.as_bytes(), + ) + } +} diff --git a/crates/keepac/src/lib.rs b/crates/keepac/src/lib.rs index 98be92a..1ae9c3f 100644 --- a/crates/keepac/src/lib.rs +++ b/crates/keepac/src/lib.rs @@ -1,6 +1,10 @@ +/// 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; /// Render out changelogs to the terminal. pub mod render; + +pub use changelog::Changelog; diff --git a/crates/keepac/src/parse.rs b/crates/keepac/src/parse.rs index 7e538e6..6c4fc55 100644 --- a/crates/keepac/src/parse.rs +++ b/crates/keepac/src/parse.rs @@ -1,65 +1,8 @@ use std::collections::HashMap; use streaming_iterator::StreamingIterator; -use tree_sitter::{Parser, Query, QueryCursor, QueryMatch, QueryMatches, Tree}; -pub struct Changelog<'a> { - pub source: &'a str, - pub tree: Tree, -} - -impl<'a> Changelog<'a> { - pub fn query(&'a self, query: &str) -> ChangelogQueryCursor<'a> { - ChangelogQueryCursor { - query: Query::new(&tree_sitter_md::LANGUAGE.into(), query).unwrap(), - cursor: QueryCursor::new(), - changelog: self, - } - } -} - -pub struct ChangelogQueryCursor<'c> { - cursor: QueryCursor, - query: Query, - changelog: &'c Changelog<'c>, -} - -impl<'c> ChangelogQueryCursor<'c> { - pub fn matches(&'c mut self) -> QueryMatches<'_, '_, &[u8], &[u8]> { - self.cursor.matches( - &self.query, - self.changelog.tree.root_node(), - self.changelog.source.as_bytes(), - ) - } -} - -#[derive(Debug)] -pub enum MarkdownParserError { - FailedToLoadGrammar, - CouldNotParseTree, -} - -impl<'a> TryFrom<&'a str> for Changelog<'a> { - type Error = MarkdownParserError; - - fn try_from(value: &'a str) -> Result { - let mut parser = Parser::new(); - - let Ok(_) = parser.set_language(&tree_sitter_md::LANGUAGE.into()) else { - return Err(MarkdownParserError::FailedToLoadGrammar); - }; - - let Some(tree) = parser.parse(value, None) else { - return Err(MarkdownParserError::CouldNotParseTree); - }; - - Ok(Self { - source: value, - tree, - }) - } -} +use crate::Changelog; #[derive(Debug)] pub struct Version<'a> { diff --git a/crates/keepac/src/render.rs b/crates/keepac/src/render.rs index 7dc368a..eab7f55 100644 --- a/crates/keepac/src/render.rs +++ b/crates/keepac/src/render.rs @@ -1,8 +1,8 @@ -use std::{collections::HashMap, fmt::Display}; +use std::fmt::Display; use tree_sitter::Node; -use crate::parse::{gather_link_defs, Changelog}; +use crate::Changelog; struct ChangelogRenderer<'a, 'f> { changelog: &'a Changelog<'a>, @@ -53,12 +53,6 @@ impl<'a> Display for Changelog<'a> { } } -#[derive(Debug)] -enum MarkdownParserError { - FailedToLoadGrammar, - CouldNotParseTree, -} - pub fn render(changelog: &str) { let renderer = Changelog::try_from(changelog).unwrap(); println!("{}", renderer); From 5ca73646108744fa2d93c07b97ce71eb39e1745a Mon Sep 17 00:00:00 2001 From: Niclas van Eyk Date: Wed, 1 Jan 2025 17:39:10 +0100 Subject: [PATCH 6/9] more ergonomic construction of the changelog facade --- Cargo.lock | 25 ++++- crates/keepac-cli/src/commands/show.rs | 16 +--- crates/keepac-cli/src/commands/versions.rs | 14 +-- crates/keepac/Cargo.toml | 1 + crates/keepac/src/changelog.rs | 13 ++- crates/keepac/src/changelog/from.rs | 103 +++++++++++++-------- crates/keepac/src/parse.rs | 30 +++++- 7 files changed, 128 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb6f86f..8981594 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -337,6 +337,7 @@ dependencies = [ "tempdir", "termimad", "textwrap", + "thiserror 2.0.9", "tree-sitter", "tree-sitter-md", ] @@ -749,7 +750,7 @@ dependencies = [ "lazy-regex", "minimad", "serde", - "thiserror", + "thiserror 1.0.69", "unicode-width 0.1.14", ] @@ -781,7 +782,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "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]] @@ -795,6 +805,17 @@ dependencies = [ "syn 2.0.91", ] +[[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.91", +] + [[package]] name = "tree-sitter" version = "0.24.6" diff --git a/crates/keepac-cli/src/commands/show.rs b/crates/keepac-cli/src/commands/show.rs index ccb60d2..d8565b7 100644 --- a/crates/keepac-cli/src/commands/show.rs +++ b/crates/keepac-cli/src/commands/show.rs @@ -1,6 +1,8 @@ use std::io::Read; use std::{fs::File, path::Path}; +use keepac::Changelog; + use crate::{errors::ErrorExitCode, KeepacCliError, SubcommandResult}; pub struct ShowCommandOptions { @@ -9,19 +11,9 @@ pub struct ShowCommandOptions { } pub fn show(path: &Path) -> SubcommandResult { - let Some(changelog_path) = keepac::find::nearest_changelog_path(path) else { - return Err(KeepacCliError { - message: String::from("Failed to find CHANGELOG.md"), - exit_code: ErrorExitCode::ChangelogNotFound, - }); - }; - - let mut changelog_file = File::open(changelog_path)?; - let mut changelog_source = String::new(); - changelog_file.read_to_string(&mut changelog_source)?; + let changelog = Changelog::nearest(path)?; // TODO: Actually highlight - println!("{}", changelog_source); - + println!("{}", changelog.source); Ok(()) } diff --git a/crates/keepac-cli/src/commands/versions.rs b/crates/keepac-cli/src/commands/versions.rs index 636057a..9d9b850 100644 --- a/crates/keepac-cli/src/commands/versions.rs +++ b/crates/keepac-cli/src/commands/versions.rs @@ -6,19 +6,7 @@ use keepac::Changelog; use crate::{errors::ErrorExitCode, KeepacCliError, SubcommandResult}; pub(crate) fn versions(path: &Path) -> SubcommandResult { - let Some(changelog_path) = keepac::find::nearest_changelog_path(path) else { - return Err(KeepacCliError { - message: String::from("Failed to find CHANGELOG.md"), - exit_code: ErrorExitCode::ChangelogNotFound, - }); - }; - - let mut changelog_file = File::open(changelog_path)?; - let mut changelog_source = String::new(); - changelog_file.read_to_string(&mut changelog_source)?; - - // TODO: don't unwrap here - let changelog = Changelog::try_from(changelog_source.as_str()).unwrap(); + let changelog = Changelog::nearest(path)?; let versions = parse_versions(&changelog); let mut last_seen_year: Option = None; diff --git a/crates/keepac/Cargo.toml b/crates/keepac/Cargo.toml index fda138b..609b902 100644 --- a/crates/keepac/Cargo.toml +++ b/crates/keepac/Cargo.toml @@ -14,5 +14,6 @@ cc = "*" streaming-iterator = "0.1.9" termimad = "0.31.1" textwrap = { version = "0.16.1", features = ["terminal_size"] } +thiserror = "2.0.9" tree-sitter = "0.24.6" tree-sitter-md = "0.3.2" diff --git a/crates/keepac/src/changelog.rs b/crates/keepac/src/changelog.rs index e96260b..b4e49a9 100644 --- a/crates/keepac/src/changelog.rs +++ b/crates/keepac/src/changelog.rs @@ -1,18 +1,29 @@ +use std::{borrow::Cow, path::Path}; + use tree_sitter::{Query, QueryCursor, Tree}; use crate::changelog::query::ChangelogQueryCursor; +use self::from::{changelog_try_from_nearest, ChangelogFromPathError}; + /// Implementations of [From] and [TryFrom] for [Changelog] from various structs. mod from; mod query; pub struct Changelog<'a> { - pub(crate) source: &'a str, + pub source: Cow<'a, str>, pub(crate) tree: Tree, } 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 query(&'a self, query: &str) -> ChangelogQueryCursor<'a> { ChangelogQueryCursor { query: Query::new(&tree_sitter_md::LANGUAGE.into(), query).unwrap(), diff --git a/crates/keepac/src/changelog/from.rs b/crates/keepac/src/changelog/from.rs index ed4c00f..00da887 100644 --- a/crates/keepac/src/changelog/from.rs +++ b/crates/keepac/src/changelog/from.rs @@ -1,55 +1,76 @@ +use crate::find::nearest_changelog_path; +use crate::parse::{parse_markdown, MarkdownParserError}; use crate::Changelog; +use std::io::Read; +use std::{fs::File, path::Path}; +use thiserror::Error; -use tree_sitter::Parser; +/// Get a Changelog from a string +impl<'s> TryFrom<&'s str> for Changelog<'s> { + type Error = MarkdownParserError; -#[derive(Debug)] -pub enum MarkdownParserError { - FailedToLoadGrammar, - CouldNotParseTree, + fn try_from(value: &'s str) -> Result { + Ok(Self { + source: std::borrow::Cow::Borrowed(value), + tree: parse_markdown(value)?, + }) + } } /// Get a Changelog from a string -impl<'a> TryFrom<&'a str> for Changelog<'a> { +impl TryFrom for Changelog<'_> { type Error = MarkdownParserError; - fn try_from(value: &'a str) -> Result { - let mut parser = Parser::new(); - - let Ok(_) = parser.set_language(&tree_sitter_md::LANGUAGE.into()) else { - return Err(MarkdownParserError::FailedToLoadGrammar); - }; - - let Some(tree) = parser.parse(value, None) else { - return Err(MarkdownParserError::CouldNotParseTree); - }; - + fn try_from(value: String) -> Result { + let tree = parse_markdown(&value)?; Ok(Self { - source: value, + source: std::borrow::Cow::Owned(value), tree, }) } } -// Get a Changelog from a file -// impl<'a> TryFrom<&'a str> for Changelog<'a> { -// type Error = MarkdownParserError; -// -// fn try_from(value: &'a str) -> Result { -// let mut parser = Parser::new(); -// -// let Ok(_) = parser.set_language(&tree_sitter_md::LANGUAGE.into()) else { -// return Err(MarkdownParserError::FailedToLoadGrammar); -// }; -// -// let Some(tree) = parser.parse(value, None) else { -// return Err(MarkdownParserError::CouldNotParseTree); -// }; -// -// Ok(Self { -// source: value, -// tree, -// }) -// } -// } - -// Get a Changelog from a pathbuf that implements the recursive upwards iteration +#[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/parse.rs b/crates/keepac/src/parse.rs index 6c4fc55..08a5889 100644 --- a/crates/keepac/src/parse.rs +++ b/crates/keepac/src/parse.rs @@ -1,8 +1,28 @@ +use crate::Changelog; use std::collections::HashMap; - use streaming_iterator::StreamingIterator; +use thiserror::Error; +use tree_sitter::{LanguageError, Parser, Tree}; -use crate::Changelog; +#[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> { @@ -11,7 +31,7 @@ pub struct Version<'a> { pub href: Option<&'a str>, } -pub fn parse_versions<'c>(changelog: &Changelog<'c>) -> Vec> { +pub fn parse_versions<'c>(changelog: &'c Changelog<'c>) -> Vec> { let mut query = changelog.query("(atx_heading (atx_h2_marker) heading_content: (inline) @content)"); let mut matches = query.matches(); @@ -34,7 +54,7 @@ pub fn parse_versions<'c>(changelog: &Changelog<'c>) -> Vec> { versions } -pub fn gather_link_defs<'a>(changelog: Changelog<'a>) -> HashMap<&'a str, &'a str> { +pub fn gather_link_defs<'a>(changelog: &Changelog<'a>) -> HashMap<&'a str, &'a str> { let mut query = changelog .query("(link_reference_definition (link_label) @label (link_destination) @destination)"); let mut matches = query.matches(); @@ -94,7 +114,7 @@ blah blah blah fn it_can_gather_link_defs() { let r = Changelog::try_from(A_CHANGELOG).unwrap(); - let links = gather_link_defs(r); + let links = gather_link_defs(&r); assert_eq!( links.get("1.0.1").unwrap().to_owned(), From e5a3762393a267fdeb8da512f034606e8eecef21 Mon Sep 17 00:00:00 2001 From: Niclas van Eyk Date: Wed, 1 Jan 2025 21:06:33 +0100 Subject: [PATCH 7/9] refactor error handling --- crates/keepac-cli/src/commands/edit.rs | 13 +++--- crates/keepac-cli/src/commands/find.rs | 9 ++-- crates/keepac-cli/src/commands/init.rs | 2 +- crates/keepac-cli/src/commands/show.rs | 5 +-- crates/keepac-cli/src/commands/versions.rs | 4 +- crates/keepac-cli/src/errors.rs | 52 ---------------------- crates/keepac-cli/src/main.rs | 7 +-- crates/keepac/src/parse.rs | 2 +- 8 files changed, 19 insertions(+), 75 deletions(-) delete mode 100644 crates/keepac-cli/src/errors.rs diff --git a/crates/keepac-cli/src/commands/edit.rs b/crates/keepac-cli/src/commands/edit.rs index 8ec6ee7..a525e0a 100644 --- a/crates/keepac-cli/src/commands/edit.rs +++ b/crates/keepac-cli/src/commands/edit.rs @@ -4,21 +4,22 @@ use std::{ process::{Child, Command}, }; -use crate::{errors::ErrorExitCode, KeepacCliError, SubcommandResult}; +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(KeepacCliError { - message: String::from("Failed to find CHANGELOG.md"), - exit_code: ErrorExitCode::ChangelogNotFound, - }); + 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()?; - // TODO: exit_status + if !exit_status.success() { + return Err(anyhow!("Editor exited with status {exit_status}")); + } Ok(()) } diff --git a/crates/keepac-cli/src/commands/find.rs b/crates/keepac-cli/src/commands/find.rs index e002e6a..c49a03e 100644 --- a/crates/keepac-cli/src/commands/find.rs +++ b/crates/keepac-cli/src/commands/find.rs @@ -1,6 +1,8 @@ use std::path::Path; -use crate::{errors::ErrorExitCode, KeepacCliError, SubcommandResult}; +use anyhow::anyhow; + +use crate::SubcommandResult; pub fn find(path: &Path) -> SubcommandResult { match keepac::find::nearest_changelog_path(path) { @@ -8,9 +10,6 @@ pub fn find(path: &Path) -> SubcommandResult { println!("{}", changelog_path.display()); Ok(()) } - None => Err(KeepacCliError { - message: String::from("Failed to find CHANGELOG.md"), - exit_code: ErrorExitCode::ChangelogNotFound, - }), + 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 index 64517af..d1db121 100644 --- a/crates/keepac-cli/src/commands/init.rs +++ b/crates/keepac-cli/src/commands/init.rs @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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").into()); + return Err(anyhow!("CHANGELOG.md already exists")); } let mut changelog = File::create_new(&changelog_path)?; diff --git a/crates/keepac-cli/src/commands/show.rs b/crates/keepac-cli/src/commands/show.rs index d8565b7..babcaff 100644 --- a/crates/keepac-cli/src/commands/show.rs +++ b/crates/keepac-cli/src/commands/show.rs @@ -1,9 +1,8 @@ -use std::io::Read; -use std::{fs::File, path::Path}; +use std::path::Path; use keepac::Changelog; -use crate::{errors::ErrorExitCode, KeepacCliError, SubcommandResult}; +use crate::SubcommandResult; pub struct ShowCommandOptions { /// An optional version to show the changes for diff --git a/crates/keepac-cli/src/commands/versions.rs b/crates/keepac-cli/src/commands/versions.rs index 9d9b850..3221fbd 100644 --- a/crates/keepac-cli/src/commands/versions.rs +++ b/crates/keepac-cli/src/commands/versions.rs @@ -1,9 +1,9 @@ -use std::{fs::File, io::Read, path::Path}; +use std::path::Path; use keepac::parse::parse_versions; use keepac::Changelog; -use crate::{errors::ErrorExitCode, KeepacCliError, SubcommandResult}; +use crate::SubcommandResult; pub(crate) fn versions(path: &Path) -> SubcommandResult { let changelog = Changelog::nearest(path)?; diff --git a/crates/keepac-cli/src/errors.rs b/crates/keepac-cli/src/errors.rs deleted file mode 100644 index a98916a..0000000 --- a/crates/keepac-cli/src/errors.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::{error::Error, fmt::Display}; - -#[derive(Debug, Clone, Copy)] -pub enum ErrorExitCode { - Unknown = 1, - ChangelogNotFound = 2, -} - -#[derive(Debug)] -pub struct KeepacCliError { - pub message: String, - pub exit_code: ErrorExitCode, -} - -impl Display for KeepacCliError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.message) - } -} - -impl Error for KeepacCliError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - None - } -} - -impl From for KeepacCliError { - fn from(value: anyhow::Error) -> Self { - KeepacCliError { - message: format!("{}", value), - exit_code: ErrorExitCode::Unknown, - } - } -} - -impl From for KeepacCliError { - fn from(value: std::io::Error) -> Self { - KeepacCliError { - message: format!("{}", value), - exit_code: ErrorExitCode::Unknown, - } - } -} - -impl From> for KeepacCliError { - fn from(value: Box) -> Self { - KeepacCliError { - message: format!("{}", value), - exit_code: ErrorExitCode::Unknown, - } - } -} diff --git a/crates/keepac-cli/src/main.rs b/crates/keepac-cli/src/main.rs index 80d899f..56e3c28 100644 --- a/crates/keepac-cli/src/main.rs +++ b/crates/keepac-cli/src/main.rs @@ -1,11 +1,8 @@ pub mod commands; -pub mod errors; use clap::{Parser, Subcommand}; use std::path::Path; -use crate::errors::KeepacCliError; - #[derive(Debug, Parser)] #[command( version, @@ -40,7 +37,7 @@ enum Command { Yank {}, } -type SubcommandResult = Result<(), KeepacCliError>; +type SubcommandResult = Result<(), anyhow::Error>; fn run(cli: Cli, path: &Path) -> SubcommandResult { let command = cli.command.unwrap_or(Command::Show {}); @@ -71,6 +68,6 @@ fn main() { if let Err(err) = result { eprintln!("{}", err); - std::process::exit(err.exit_code as i32); + std::process::exit(1); }; } diff --git a/crates/keepac/src/parse.rs b/crates/keepac/src/parse.rs index 08a5889..e8c9ab6 100644 --- a/crates/keepac/src/parse.rs +++ b/crates/keepac/src/parse.rs @@ -54,7 +54,7 @@ pub fn parse_versions<'c>(changelog: &'c Changelog<'c>) -> Vec> { versions } -pub fn gather_link_defs<'a>(changelog: &Changelog<'a>) -> HashMap<&'a str, &'a str> { +pub fn gather_link_defs<'a>(changelog: &'a Changelog<'a>) -> HashMap<&'a str, &'a str> { let mut query = changelog .query("(link_reference_definition (link_label) @label (link_destination) @destination)"); let mut matches = query.matches(); From d7988409884cdb0a07ccea0c1abd9af5c0e28d1e Mon Sep 17 00:00:00 2001 From: Niclas van Eyk Date: Sun, 5 Jan 2025 15:24:47 +0100 Subject: [PATCH 8/9] Create a separate MarkdownDocument struct --- crates/keepac-cli/src/commands/show.rs | 10 +- crates/keepac/src/changelog.rs | 21 +--- crates/keepac/src/changelog/from.rs | 10 +- crates/keepac/src/changelog/query.rs | 19 ---- crates/keepac/src/lib.rs | 3 + crates/keepac/src/markdown.rs | 135 ++++++++++++++++++++++++ crates/keepac/src/parse.rs | 48 +++++---- crates/keepac/src/render.rs | 103 ++++++++++-------- crates/keepac/src/render/tree_sitter.rs | 69 ++++++++++++ 9 files changed, 303 insertions(+), 115 deletions(-) delete mode 100644 crates/keepac/src/changelog/query.rs create mode 100644 crates/keepac/src/markdown.rs create mode 100644 crates/keepac/src/render/tree_sitter.rs diff --git a/crates/keepac-cli/src/commands/show.rs b/crates/keepac-cli/src/commands/show.rs index babcaff..b1f9b5e 100644 --- a/crates/keepac-cli/src/commands/show.rs +++ b/crates/keepac-cli/src/commands/show.rs @@ -4,15 +4,15 @@ use keepac::Changelog; use crate::SubcommandResult; -pub struct ShowCommandOptions { - /// An optional version to show the changes for - version: Option, -} +// pub struct ShowCommandOptions { +// /// An optional version to show the changes for +// version: Option, +// } pub fn show(path: &Path) -> SubcommandResult { let changelog = Changelog::nearest(path)?; // TODO: Actually highlight - println!("{}", changelog.source); + println!("{}", changelog); Ok(()) } diff --git a/crates/keepac/src/changelog.rs b/crates/keepac/src/changelog.rs index b4e49a9..a373bde 100644 --- a/crates/keepac/src/changelog.rs +++ b/crates/keepac/src/changelog.rs @@ -1,19 +1,14 @@ -use std::{borrow::Cow, path::Path}; +use std::path::Path; -use tree_sitter::{Query, QueryCursor, Tree}; - -use crate::changelog::query::ChangelogQueryCursor; +use crate::markdown::MarkdownDocument; use self::from::{changelog_try_from_nearest, ChangelogFromPathError}; /// Implementations of [From] and [TryFrom] for [Changelog] from various structs. mod from; -mod query; - pub struct Changelog<'a> { - pub source: Cow<'a, str>, - pub(crate) tree: Tree, + pub document: MarkdownDocument<'a>, } impl<'a> Changelog<'a> { @@ -24,13 +19,5 @@ impl<'a> Changelog<'a> { changelog_try_from_nearest(path) } - pub fn query(&'a self, query: &str) -> ChangelogQueryCursor<'a> { - ChangelogQueryCursor { - query: Query::new(&tree_sitter_md::LANGUAGE.into(), query).unwrap(), - cursor: QueryCursor::new(), - changelog: self, - } - } - - // TODO: Version iterator + // 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 index 00da887..1021e92 100644 --- a/crates/keepac/src/changelog/from.rs +++ b/crates/keepac/src/changelog/from.rs @@ -1,5 +1,6 @@ use crate::find::nearest_changelog_path; -use crate::parse::{parse_markdown, MarkdownParserError}; +use crate::markdown::MarkdownDocument; +use crate::parse::MarkdownParserError; use crate::Changelog; use std::io::Read; use std::{fs::File, path::Path}; @@ -11,8 +12,7 @@ impl<'s> TryFrom<&'s str> for Changelog<'s> { fn try_from(value: &'s str) -> Result { Ok(Self { - source: std::borrow::Cow::Borrowed(value), - tree: parse_markdown(value)?, + document: MarkdownDocument::try_from(value)?, }) } } @@ -22,10 +22,8 @@ impl TryFrom for Changelog<'_> { type Error = MarkdownParserError; fn try_from(value: String) -> Result { - let tree = parse_markdown(&value)?; Ok(Self { - source: std::borrow::Cow::Owned(value), - tree, + document: MarkdownDocument::try_from(value)?, }) } } diff --git a/crates/keepac/src/changelog/query.rs b/crates/keepac/src/changelog/query.rs deleted file mode 100644 index f985f43..0000000 --- a/crates/keepac/src/changelog/query.rs +++ /dev/null @@ -1,19 +0,0 @@ -use tree_sitter::{Query, QueryCursor, QueryMatches}; - -use crate::Changelog; - -pub struct ChangelogQueryCursor<'c> { - pub(crate) cursor: QueryCursor, - pub(crate) query: Query, - pub(crate) changelog: &'c Changelog<'c>, -} - -impl<'c> ChangelogQueryCursor<'c> { - pub fn matches(&'c mut self) -> QueryMatches<'_, '_, &[u8], &[u8]> { - self.cursor.matches( - &self.query, - self.changelog.tree.root_node(), - self.changelog.source.as_bytes(), - ) - } -} diff --git a/crates/keepac/src/lib.rs b/crates/keepac/src/lib.rs index 1ae9c3f..fde5c74 100644 --- a/crates/keepac/src/lib.rs +++ b/crates/keepac/src/lib.rs @@ -7,4 +7,7 @@ pub mod parse; /// Render out changelogs to the terminal. pub mod render; +/// Treesitter utilities for markdown files +pub mod markdown; + 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 index e8c9ab6..69b8ebe 100644 --- a/crates/keepac/src/parse.rs +++ b/crates/keepac/src/parse.rs @@ -32,32 +32,36 @@ pub struct Version<'a> { } pub fn parse_versions<'c>(changelog: &'c Changelog<'c>) -> Vec> { - let mut query = - changelog.query("(atx_heading (atx_h2_marker) heading_content: (inline) @content)"); - let mut matches = query.matches(); - - let mut versions = Vec::new(); - - while let Some(m) = matches.next() { - for c in m.captures { - let text = c.node.utf8_text(changelog.source.as_bytes()).unwrap(); - let parts: Vec<&str> = text.split(" - ").collect(); - - versions.push(Version { + 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, - }) - } - } - - versions + } + }) + .collect() } pub fn gather_link_defs<'a>(changelog: &'a Changelog<'a>) -> HashMap<&'a str, &'a str> { - let mut query = changelog - .query("(link_reference_definition (link_label) @label (link_destination) @destination)"); - let mut matches = query.matches(); + 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() { @@ -67,7 +71,7 @@ pub fn gather_link_defs<'a>(changelog: &'a Changelog<'a>) -> HashMap<&'a str, &' .first() .unwrap() .node - .utf8_text(changelog.source.as_bytes()) + .utf8_text(changelog.document.source.as_ref()) .unwrap() .strip_prefix('[') .unwrap() @@ -77,7 +81,7 @@ pub fn gather_link_defs<'a>(changelog: &'a Changelog<'a>) -> HashMap<&'a str, &' .get(1) .unwrap() .node - .utf8_text(changelog.source.as_bytes()) + .utf8_text(changelog.document.source.as_ref()) .unwrap(); links.insert(label, destination); diff --git a/crates/keepac/src/render.rs b/crates/keepac/src/render.rs index eab7f55..c7e1c48 100644 --- a/crates/keepac/src/render.rs +++ b/crates/keepac/src/render.rs @@ -1,61 +1,72 @@ -use std::fmt::Display; +mod tree_sitter; -use tree_sitter::Node; +use std::fmt::Display; use crate::Changelog; -struct ChangelogRenderer<'a, 'f> { - changelog: &'a Changelog<'a>, - f: &'a mut std::fmt::Formatter<'f>, - // Maps the [1.0.0] to their actual URLs at the bottom - // link_defs: HashMap<&'a str, &'a str>, -} - -impl<'a, 'f> ChangelogRenderer<'a, 'f> { - pub fn render(&mut self) -> std::fmt::Result { - let mut cursor = self.changelog.tree.walk(); - - // The first node is always the document one - cursor.goto_first_child(); - cursor.goto_first_child(); - cursor.goto_next_sibling(); - cursor.goto_next_sibling(); - - let node = cursor.node(); - writeln!(self.f, "NODE: {}", node)?; - - let contents = node.child_by_field_name("heading_content"); - match contents { - Some(c) => self.render_heading(c), - None => {} - }; - - Ok(()) - } - - fn render_heading(&self, node: Node) { - let res = node.utf8_text(self.changelog.source.as_bytes()).unwrap(); - println!("HEADING: {}", res); - } - - fn render_inline(&self, node: Node) {} -} +// #[derive(Default, Debug)] +// pub struct ChangelogRenderContext {} +// +// #[derive(Default, Debug)] +// pub struct TerminalChangelogRenderer { +// context: ChangelogRenderContext, +// } +// +// impl TerminalChangelogRenderer { +// pub fn render(source: &str, formatter: &mut std::fmt::Formatter) -> std::io::Result<()> { +// Ok(()) +// } +// } + +// struct ChangelogRenderer<'a, 'f> { +// changelog: &'a Changelog<'a>, +// f: &'a mut std::fmt::Formatter<'f>, +// // Maps the [1.0.0] to their actual URLs at the bottom +// // link_defs: HashMap<&'a str, &'a str>, +// } + +// impl<'a, 'f> ChangelogRenderer<'a, 'f> { +// pub fn render(&mut self) -> std::fmt::Result { +// let mut cursor = self.changelog.tree.walk(); +// +// // The first node is always the document one +// cursor.goto_first_child(); +// cursor.goto_first_child(); +// cursor.goto_next_sibling(); +// cursor.goto_next_sibling(); +// +// let node = cursor.node(); +// writeln!(self.f, "NODE: {}", node)?; +// +// let contents = node.child_by_field_name("heading_content"); +// match contents { +// Some(c) => self.render_heading(c), +// None => {} +// }; +// +// Ok(()) +// } +// +// fn render_heading(&self, node: Node) { +// let res = node.utf8_text(self.changelog.source.as_bytes()).unwrap(); +// println!("HEADING: {}", res); +// } +// +// fn render_inline(&self, node: Node) {} +// } impl<'a> Display for Changelog<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // let link_defs = gather_link_defs(&self.tree, self.source); - let mut renderer = ChangelogRenderer { - changelog: self, + write!( f, - // link_defs, - }; - renderer.render() + "{}", + std::str::from_utf8(self.document.source.as_ref()).unwrap() + ) } } pub fn render(changelog: &str) { - let renderer = Changelog::try_from(changelog).unwrap(); - println!("{}", renderer); + println!("{}", Changelog::try_from(changelog).unwrap()); } #[cfg(test)] diff --git a/crates/keepac/src/render/tree_sitter.rs b/crates/keepac/src/render/tree_sitter.rs new file mode 100644 index 0000000..f0b120d --- /dev/null +++ b/crates/keepac/src/render/tree_sitter.rs @@ -0,0 +1,69 @@ +use std::{ + fmt::Formatter, + io::{Result, Write}, +}; + +use tree_sitter::Node; + +#[derive(Debug, Default)] +pub(crate) struct TreeSitterTerminalHighlighter {} + +impl TreeSitterTerminalHighlighter { + pub fn render_heading(&self, node: &Node, formatter: &mut Formatter) -> Result<()> { + Ok(()) + } + + pub fn render_inline_link( + &self, + node: &Node, + source: &[u8], + formatter: &mut impl Write, + ) -> Result<()> { + let link_text = node.child(0).unwrap().utf8_text(source).unwrap(); + let link_address = node.child(1).unwrap().utf8_text(source).unwrap(); + + write!( + formatter, + "\u{1b}]8;;{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\", + link_text, link_address + ) + } +} + +#[cfg(test)] +mod tests { + use tree_sitter::Query; + + use crate::parse::parse_markdown; + + use super::*; + + #[test] + pub fn it_renders_links() { + let highlighter = TreeSitterTerminalHighlighter::default(); + let source = "[Google](https://google.com)"; + let tree = parse_markdown(source).unwrap(); + println!("{:?}", tree); + + let root_node = tree.root_node(); + let section = root_node.child(0).unwrap(); + println!("{}", section.grammar_name()); + let paragraph = section.child(0).unwrap(); + let inline = paragraph.child(0).unwrap(); + + // let foo = markdown_query("(inline_link)"); + + println!("{:?}", inline); + let mut buffer = Vec::new(); + + highlighter + .render_inline_link(&inline, source.as_bytes(), &mut buffer) + .unwrap(); + let output = std::str::from_utf8(buffer.as_slice()).unwrap(); + + assert_eq!( + output, + "\u{1b}]8;;Google\u{1b}\\https://google.com\u{1b}]8;;\u{1b}\\" + ); + } +} From 9fa9767a26ef0b90e93ef1392ad145417c96b28d Mon Sep 17 00:00:00 2001 From: Niclas van Eyk Date: Sat, 11 Jan 2025 20:34:29 +0100 Subject: [PATCH 9/9] changelogs look kind of good, can reference explicit files via option --- Cargo.lock | 52 +++- crates/keepac-cli/Cargo.toml | 1 + crates/keepac-cli/src/commands/init.rs | 8 +- crates/keepac-cli/src/commands/mod.rs | 16 +- crates/keepac-cli/src/commands/show.rs | 39 ++- crates/keepac-cli/src/main.rs | 20 +- crates/keepac/Cargo.toml | 14 +- crates/keepac/src/changelog.rs | 6 +- crates/keepac/src/highlight.rs | 386 ++++++++++++++++++++++++ crates/keepac/src/lib.rs | 6 +- crates/keepac/src/render.rs | 100 ------ crates/keepac/src/render/tree_sitter.rs | 69 ----- crates/keepac/src/theme.rs | 14 + 13 files changed, 508 insertions(+), 223 deletions(-) create mode 100644 crates/keepac/src/highlight.rs delete mode 100644 crates/keepac/src/render.rs delete mode 100644 crates/keepac/src/render/tree_sitter.rs create mode 100644 crates/keepac/src/theme.rs diff --git a/Cargo.lock b/Cargo.lock index 8981594..7fdee83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,9 +86,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "cc" -version = "1.2.6" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" +checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" dependencies = [ "shlex", ] @@ -142,7 +142,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.95", ] [[package]] @@ -335,6 +335,7 @@ dependencies = [ "pulldown-cmark", "streaming-iterator", "tempdir", + "termcolor", "termimad", "textwrap", "thiserror 2.0.9", @@ -351,6 +352,7 @@ dependencies = [ "clap-verbosity-flag", "keepac", "tempdir", + "termcolor", ] [[package]] @@ -373,7 +375,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.91", + "syn 2.0.95", ] [[package]] @@ -496,9 +498,9 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -637,7 +639,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.95", ] [[package]] @@ -719,9 +721,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.91" +version = "2.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" dependencies = [ "proc-macro2", "quote", @@ -738,6 +740,15 @@ dependencies = [ "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" @@ -802,7 +813,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.95", ] [[package]] @@ -813,7 +824,7 @@ checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.95", ] [[package]] @@ -837,19 +848,19 @@ checksum = "c199356c799a8945965bb5f2c55b2ad9d9aa7c4b4f6e587fe9dea0bc715e5f9c" [[package]] name = "tree-sitter-md" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f968c22a01010b83fc960455ae729db08dbeb6388617d9113897cb9204b030" +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.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" @@ -903,6 +914,15 @@ 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" diff --git a/crates/keepac-cli/Cargo.toml b/crates/keepac-cli/Cargo.toml index 6c92171..88ea5dc 100644 --- a/crates/keepac-cli/Cargo.toml +++ b/crates/keepac-cli/Cargo.toml @@ -12,6 +12,7 @@ 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/init.rs b/crates/keepac-cli/src/commands/init.rs index d1db121..9fce1a4 100644 --- a/crates/keepac-cli/src/commands/init.rs +++ b/crates/keepac-cli/src/commands/init.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; -use keepac::render::render; use std::{fs::File, io::Write, path::Path}; +use termcolor::{BufferWriter, ColorChoice}; use crate::SubcommandResult; @@ -23,12 +23,14 @@ pub(crate) fn init(path: &Path) -> SubcommandResult { let mut changelog = File::create_new(&changelog_path)?; changelog.write_all(TEMPLATE.as_bytes())?; - // TODO: Print _highlighted_ to console println!( "Initialized empty changelog at {}:", changelog_path.display() ); - render(TEMPLATE); + let writer = BufferWriter::stdout(ColorChoice::Always); + let mut buffer = writer.buffer(); + keepac::highlight::render(&mut buffer, TEMPLATE)?; + writer.print(&buffer)?; Ok(()) } diff --git a/crates/keepac-cli/src/commands/mod.rs b/crates/keepac-cli/src/commands/mod.rs index 2705482..af35608 100644 --- a/crates/keepac-cli/src/commands/mod.rs +++ b/crates/keepac-cli/src/commands/mod.rs @@ -1,11 +1,5 @@ -mod edit; -mod find; -mod init; -mod show; -mod versions; - -pub(crate) use edit::edit; -pub(crate) use find::find; -pub(crate) use init::init; -pub(crate) use show::show; -pub(crate) use versions::versions; +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 index b1f9b5e..0609524 100644 --- a/crates/keepac-cli/src/commands/show.rs +++ b/crates/keepac-cli/src/commands/show.rs @@ -1,18 +1,41 @@ use std::path::Path; use keepac::Changelog; +use termcolor::{BufferWriter, ColorChoice}; use crate::SubcommandResult; -// pub struct ShowCommandOptions { -// /// An optional version to show the changes for -// version: Option, -// } +#[derive(Debug, clap::Args)] +pub struct ShowCommandOptions { + /// An optional version to show the changes for + version: Option, -pub fn show(path: &Path) -> SubcommandResult { - let changelog = Changelog::nearest(path)?; + /// 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)?, + }; - // TODO: Actually highlight - println!("{}", changelog); + 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/main.rs b/crates/keepac-cli/src/main.rs index 56e3c28..9424c6e 100644 --- a/crates/keepac-cli/src/main.rs +++ b/crates/keepac-cli/src/main.rs @@ -1,6 +1,7 @@ pub mod commands; use clap::{Parser, Subcommand}; +use commands::show::ShowCommandOptions; use std::path::Path; #[derive(Debug, Parser)] @@ -32,7 +33,10 @@ enum Command { Remove {}, Search {}, Secure {}, - Show {}, + Show { + #[command(flatten)] + options: commands::show::ShowCommandOptions, + }, Versions {}, Yank {}, } @@ -40,23 +44,25 @@ enum Command { type SubcommandResult = Result<(), anyhow::Error>; fn run(cli: Cli, path: &Path) -> SubcommandResult { - let command = cli.command.unwrap_or(Command::Show {}); + 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(path), - Command::Find {} => commands::find(path), + Command::Edit {} => commands::edit::edit(path), + Command::Find {} => commands::find::find(path), /* change */ Command::Fix {} => todo!(), - Command::Init {} => commands::init(path), + 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 {} => commands::show(path), - Command::Versions {} => commands::versions(path), + Command::Show { options } => commands::show::show(path, options), + Command::Versions {} => commands::versions::versions(path), Command::Yank {} => todo!(), } } diff --git a/crates/keepac/Cargo.toml b/crates/keepac/Cargo.toml index 609b902..2a0c882 100644 --- a/crates/keepac/Cargo.toml +++ b/crates/keepac/Cargo.toml @@ -11,9 +11,11 @@ pulldown-cmark = "0.12.2" cc = "*" [dependencies] -streaming-iterator = "0.1.9" -termimad = "0.31.1" -textwrap = { version = "0.16.1", features = ["terminal_size"] } -thiserror = "2.0.9" -tree-sitter = "0.24.6" -tree-sitter-md = "0.3.2" +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 index a373bde..1e752de 100644 --- a/crates/keepac/src/changelog.rs +++ b/crates/keepac/src/changelog.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{fs::File, path::Path}; use crate::markdown::MarkdownDocument; @@ -19,5 +19,9 @@ impl<'a> Changelog<'a> { 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/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 index fde5c74..7fd461d 100644 --- a/crates/keepac/src/lib.rs +++ b/crates/keepac/src/lib.rs @@ -4,10 +4,12 @@ pub mod changelog; pub mod find; /// Parse changelogs given a few structural constraints. pub mod parse; -/// Render out changelogs to the terminal. -pub mod render; /// Treesitter utilities for markdown files pub mod markdown; +pub mod highlight; + +pub mod theme; + pub use changelog::Changelog; diff --git a/crates/keepac/src/render.rs b/crates/keepac/src/render.rs deleted file mode 100644 index c7e1c48..0000000 --- a/crates/keepac/src/render.rs +++ /dev/null @@ -1,100 +0,0 @@ -mod tree_sitter; - -use std::fmt::Display; - -use crate::Changelog; - -// #[derive(Default, Debug)] -// pub struct ChangelogRenderContext {} -// -// #[derive(Default, Debug)] -// pub struct TerminalChangelogRenderer { -// context: ChangelogRenderContext, -// } -// -// impl TerminalChangelogRenderer { -// pub fn render(source: &str, formatter: &mut std::fmt::Formatter) -> std::io::Result<()> { -// Ok(()) -// } -// } - -// struct ChangelogRenderer<'a, 'f> { -// changelog: &'a Changelog<'a>, -// f: &'a mut std::fmt::Formatter<'f>, -// // Maps the [1.0.0] to their actual URLs at the bottom -// // link_defs: HashMap<&'a str, &'a str>, -// } - -// impl<'a, 'f> ChangelogRenderer<'a, 'f> { -// pub fn render(&mut self) -> std::fmt::Result { -// let mut cursor = self.changelog.tree.walk(); -// -// // The first node is always the document one -// cursor.goto_first_child(); -// cursor.goto_first_child(); -// cursor.goto_next_sibling(); -// cursor.goto_next_sibling(); -// -// let node = cursor.node(); -// writeln!(self.f, "NODE: {}", node)?; -// -// let contents = node.child_by_field_name("heading_content"); -// match contents { -// Some(c) => self.render_heading(c), -// None => {} -// }; -// -// Ok(()) -// } -// -// fn render_heading(&self, node: Node) { -// let res = node.utf8_text(self.changelog.source.as_bytes()).unwrap(); -// println!("HEADING: {}", res); -// } -// -// fn render_inline(&self, node: Node) {} -// } - -impl<'a> Display for Changelog<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - std::str::from_utf8(self.document.source.as_ref()).unwrap() - ) - } -} - -pub fn render(changelog: &str) { - println!("{}", Changelog::try_from(changelog).unwrap()); -} - -#[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_renders() { - render(A_CHANGELOG) - } -} diff --git a/crates/keepac/src/render/tree_sitter.rs b/crates/keepac/src/render/tree_sitter.rs deleted file mode 100644 index f0b120d..0000000 --- a/crates/keepac/src/render/tree_sitter.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::{ - fmt::Formatter, - io::{Result, Write}, -}; - -use tree_sitter::Node; - -#[derive(Debug, Default)] -pub(crate) struct TreeSitterTerminalHighlighter {} - -impl TreeSitterTerminalHighlighter { - pub fn render_heading(&self, node: &Node, formatter: &mut Formatter) -> Result<()> { - Ok(()) - } - - pub fn render_inline_link( - &self, - node: &Node, - source: &[u8], - formatter: &mut impl Write, - ) -> Result<()> { - let link_text = node.child(0).unwrap().utf8_text(source).unwrap(); - let link_address = node.child(1).unwrap().utf8_text(source).unwrap(); - - write!( - formatter, - "\u{1b}]8;;{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\", - link_text, link_address - ) - } -} - -#[cfg(test)] -mod tests { - use tree_sitter::Query; - - use crate::parse::parse_markdown; - - use super::*; - - #[test] - pub fn it_renders_links() { - let highlighter = TreeSitterTerminalHighlighter::default(); - let source = "[Google](https://google.com)"; - let tree = parse_markdown(source).unwrap(); - println!("{:?}", tree); - - let root_node = tree.root_node(); - let section = root_node.child(0).unwrap(); - println!("{}", section.grammar_name()); - let paragraph = section.child(0).unwrap(); - let inline = paragraph.child(0).unwrap(); - - // let foo = markdown_query("(inline_link)"); - - println!("{:?}", inline); - let mut buffer = Vec::new(); - - highlighter - .render_inline_link(&inline, source.as_bytes(), &mut buffer) - .unwrap(); - let output = std::str::from_utf8(buffer.as_slice()).unwrap(); - - assert_eq!( - output, - "\u{1b}]8;;Google\u{1b}\\https://google.com\u{1b}]8;;\u{1b}\\" - ); - } -} 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, +// }