diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d91a2f..0b00f65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,23 +56,23 @@ jobs: include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu - artifact_name: git-issue - asset_name: git-issue-linux-x86_64 + cli_asset_name: git-issue-linux-x86_64 + web_asset_name: git-issue-web-linux-x86_64 - os: macos-15-intel # Intel runner for x86_64 target: x86_64-apple-darwin - artifact_name: git-issue - asset_name: git-issue-macos-x86_64 + cli_asset_name: git-issue-macos-x86_64 + web_asset_name: git-issue-web-macos-x86_64 - os: macos-latest # Apple Silicon runner for arm64 target: aarch64-apple-darwin - artifact_name: git-issue - asset_name: git-issue-macos-aarch64 + cli_asset_name: git-issue-macos-aarch64 + web_asset_name: git-issue-web-macos-aarch64 - os: windows-latest target: x86_64-pc-windows-msvc - artifact_name: git-issue.exe - asset_name: git-issue-windows-x86_64.exe + cli_asset_name: git-issue-windows-x86_64.exe + web_asset_name: git-issue-web-windows-x86_64.exe steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -82,21 +82,31 @@ jobs: - name: Install target run: rustup target add ${{ matrix.target }} - - name: Build - run: cargo build --release --target ${{ matrix.target }} + - name: Build binaries + run: cargo build --release --target ${{ matrix.target }} --bins - - name: Rename artifact (Unix) + - name: Rename CLI artifact (Unix) if: runner.os != 'Windows' - run: mv ./target/${{ matrix.target }}/release/${{ matrix.artifact_name }} ./target/${{ matrix.target }}/release/${{ matrix.asset_name }} + run: mv ./target/${{ matrix.target }}/release/git-issue ./target/${{ matrix.target }}/release/${{ matrix.cli_asset_name }} - - name: Rename artifact (Windows) + - name: Rename CLI artifact (Windows) if: runner.os == 'Windows' - run: Move-Item -Path ./target/${{ matrix.target }}/release/${{ matrix.artifact_name }} -Destination ./target/${{ matrix.target }}/release/${{ matrix.asset_name }} + run: Move-Item -Path ./target/${{ matrix.target }}/release/git-issue.exe -Destination ./target/${{ matrix.target }}/release/${{ matrix.cli_asset_name }} - - name: Upload Release Asset + - name: Rename web artifact (Unix) + if: runner.os != 'Windows' + run: mv ./target/${{ matrix.target }}/release/git-issue-web ./target/${{ matrix.target }}/release/${{ matrix.web_asset_name }} + + - name: Rename web artifact (Windows) + if: runner.os == 'Windows' + run: Move-Item -Path ./target/${{ matrix.target }}/release/git-issue-web.exe -Destination ./target/${{ matrix.target }}/release/${{ matrix.web_asset_name }} + + - name: Upload Release Assets uses: softprops/action-gh-release@v1 with: - files: ./target/${{ matrix.target }}/release/${{ matrix.asset_name }} + files: | + ./target/${{ matrix.target }}/release/${{ matrix.cli_asset_name }} + ./target/${{ matrix.target }}/release/${{ matrix.web_asset_name }} publish: name: Publish to crates.io @@ -106,5 +116,8 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Verify packaged binaries + run: cargo package --allow-dirty --list + - name: Publish to crates.io run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index bd018fb..1dbeed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,17 @@ 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] +## [v0.8.0] - 2026-08-01 + +### Added + - [web] added simple web server to list and show issues ### Changed + - [cmd/show] ensured metadata table is left aligned - [cmd/show] removed title from metadata table in markdown (#59) + - [cmd/show] removed id from metadata table in markdown + - [cmd/show] added `#` to issue ids in relationships in markdown - [config] moved `export_csv_separator` from config to settings --> YAML schema v2 (#58) - [structure] added lib, implemented separation between cmd and cli diff --git a/Cargo.lock b/Cargo.lock index d76b492..b055c48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,7 +56,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -67,15 +67,134 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "askama" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7125972258312e79827b60c9eb93938334100245081cf701a2dee981b17427" +dependencies = [ + "askama_macros", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba5e7259a1580c61571e3116ebaaa01e3c001b2132b17c4cc5c70780ca3e994" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "askama_macros" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "236ce20b77cb13506eaf5024899f4af6e12e8825f390bd943c4c37fd8f322e46" +dependencies = [ + "askama_derive", +] + +[[package]] +name = "askama_parser" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c63392767bb2df6aa65a6e1e3b80fd89bb7af6d58359b924c0695620f1512e" +dependencies = [ + "rustc-hash", + "serde", + "serde_derive", + "unicode-ident", + "winnow", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -88,6 +207,12 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + [[package]] name = "cc" version = "1.2.50" @@ -182,7 +307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -197,6 +322,48 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -211,17 +378,21 @@ dependencies = [ [[package]] name = "git-issue" -version = "0.7.2" +version = "0.8.0" dependencies = [ "anstyle", + "askama", + "axum", "chrono", "clap", "indexmap", "regex", "serde", + "serde_json", "serde_yaml", "shell-words", "tempfile", + "tokio", ] [[package]] @@ -236,6 +407,88 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -312,12 +565,35 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -339,6 +615,24 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "proc-macro2" version = "1.0.103" @@ -392,6 +686,12 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "1.1.3" @@ -402,7 +702,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -447,6 +747,42 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -472,6 +808,22 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "strsim" version = "0.11.1" @@ -489,6 +841,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "tempfile" version = "3.23.0" @@ -499,7 +857,80 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", ] [[package]] @@ -520,6 +951,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -633,6 +1070,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -642,8 +1088,88 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zmij" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/Cargo.toml b/Cargo.toml index 5cbde3c..fe3d74f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "git-issue" -version = "0.7.2" +version = "0.8.0" edition = "2024" rust-version = "1.88" description = "Git-native issue tracker" @@ -19,6 +19,10 @@ path = "src/lib.rs" name = "git-issue" path = "src/cli/main.rs" +[[bin]] +name = "git-issue-web" +path = "src/web/main.rs" + [dependencies] chrono = "0.4.42" clap = { version = "4.5.53", features = ["derive"] } @@ -28,6 +32,10 @@ regex = "1.12.2" serde = { version = "1.0.228", features = ["derive"] } serde_yaml = "0.9.34" shell-words = "1.1" +axum = "0.8.8" +tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros"] } +serde_json = "1.0.149" +askama = "0.15.1" [dev-dependencies] tempfile = "3" diff --git a/README.md b/README.md index 7288a7d..24e6486 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Issues live alongside your code inside `.gitissues/`, making them platform-indep - ✅ External editor renders issue information as markdown - ✅ Git-integration: auto-commit of changes - ✅ Possibility to export issue list into CSV file +- ✅ Small web server to graphically list and show the issues - ✅ Automated integration tests - 🚧 Comments / discussions @@ -33,20 +34,32 @@ Different installation approaches are explained here. #### 2.1.1) GitHub Release -Download the latest release from GitHub and put the binary on your PATH. - -1) Go to the Releases page and download the binary for your platform: - - `git-issue-linux-x86_64` - - `git-issue-macos-x86_64` or `git-issue-macos-aarch64` - - `git-issue-windows-x86_64.exe` +Download the latest release from GitHub and put the desired binary on your PATH. + +1) Go to the Releases page and download the binaries for your platform: + - CLI: `git-issue-` + - Web UI: `git-issue-web-` + - Examples: + - `git-issue-linux-x86_64` + - `git-issue-windows-x86_64.exe` + - `git-issue-macos-x86_64` or `git-issue-macos-aarch64` + - `git-issue-web-linux-x86_64` + - `git-issue-web-windows-x86_64.exe` + - `git-issue-web-macos-x86_64` or `git-issue-web-macos-aarch64` 2) Rename to the canonical name and place on your PATH - - Linux/macOS: + - CLI binary: ```bash mv git-issue- git-issue chmod +x git-issue sudo mv git-issue /usr/local/bin/ ``` - - Windows: rename `git-issue-windows-x86_64.exe` to `git-issue.exe` and move it to a directory on your PATH. + - Web binary: + ```bash + mv git-issue-web- git-issue-web + chmod +x git-issue-web + sudo mv git-issue-web /usr/local/bin/ + ``` + - Windows: rename the downloaded `.exe` files to `git-issue.exe` or `git-issue-web.exe` and move them to a directory on your PATH. 3) Verify: ```bash git issue -h @@ -64,10 +77,16 @@ cargo install --git https://github.com/timonburkard/git-issue Package is available on https://crates.io/crates/git-issue, so it can be installed as follows: -``` +```bash cargo install git-issue ``` +To install the web UI binary from crates.io instead of the CLI binary, use: + +```bash +cargo install git-issue --bin git-issue-web +``` + ### 2.2) How To Lets imagine this is the structure of your git repo, for which you want to add issue tracking: @@ -107,7 +126,7 @@ This is the suggested content for the `.gitignore`: .gitissues/settings.yaml ``` -### 2.3) Commands +### 2.3) CLI ```bash # Help page @@ -171,7 +190,19 @@ git issue link 1234 --add related=5678,3333 parent=9999 --remove child=7777 # b git issue edit 1234 ``` -### 2.4) Example +### 2.4) WEB + +For users which prefer graphical representation, there also exists a small web server. It only supports reading/displaying and not editing. + +It can: +- List issues: `http://localhost:7878/` (same as `http://localhost:7878/list/`) + - Supports filters + - Supports columns + - ID is a hyperlink to `http://localhost:7878/show/{id}/` +- Show issue: `http://localhost:7878/show/{id}/` + - Renders markdown info + +### 2.5) Example Example projects to see how `git-issue` is used in a repo: @@ -445,19 +476,21 @@ cargo test - `lib.rs` -- Public library - `model.rs` -- Shared data types, functions and utilities - `cmd/` -- Core of the application: Commands (CRUD) - - `edit.rs` -- Edit issue description (markdown) with external text editor - - `init.rs` -- Initialize `.gitissues/` directory and copy default config - - `link.rs` -- Change relationships between issues - - `list.rs` -- List all issues - - `new.rs` -- Create new issues - - `set.rs` -- Change issue meta fields - - `show.rs` -- Show all issue information (markdown) with external text editor - - `util.rs` -- Utility functions for CMD + - `edit.rs` -- Edit issue description (markdown) with external text editor + - `init.rs` -- Initialize `.gitissues/` directory and copy default config + - `link.rs` -- Change relationships between issues + - `list.rs` -- List all issues + - `new.rs` -- Create new issues + - `set.rs` -- Change issue meta fields + - `show.rs` -- Show all issue information (markdown) with external text editor + - `util.rs` -- Utility functions for CMD - `cli/` -- Binary: CLI -- Command Line Interface - - `main.rs` -- Main entry for CLI: parsing with clap - - `cli.rs` -- Functionality for CLI - - `util.rs` -- Utility functions for CLI - - `web/` -- Binary: WEB -- Local web server (🚧) + - `main.rs` -- Main entry for CLI: parsing with clap + - `cli.rs` -- Functionality for CLI + - `util.rs` -- Utility functions for CLI + - `web/` -- Binary: WEB -- Local web server + - `main.rs` -- Main entry for WEB + - `templates/` -- HTML templates - `tests/` -- Automated tests ### 5.3) Dependencies @@ -466,7 +499,10 @@ cargo test - `chrono` -- Timestamp generation - `serde` -- Serialization framework - `serde_yaml` -- YAML parsing for meta.yaml files +- `serde_json` -- JSON parsing - `shell-words` -- Process command line according to parsing rules of Unix shell - `regex` -- Regular expressions - `indexmap` -- Provides IndexMap datatype - `anstyle` -- Terminal output coloring +- `tokio` -- TCP listener +- `askama` -- HTML template rendering diff --git a/askama.toml b/askama.toml new file mode 100644 index 0000000..44971b8 --- /dev/null +++ b/askama.toml @@ -0,0 +1,2 @@ +[general] +dirs = ["src/web/templates"] diff --git a/src/cmd/init.rs b/src/cmd/init.rs index 8869fbe..6824cdf 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -36,13 +36,9 @@ pub fn init(no_commit: bool) -> Cmd<()> { fs::write(&desc_dst, DEFAULT_DESC).map_err(|e| format!("Failed to write default description to {}: {e}", desc_dst.display()))?; if !no_commit { - match git_commit_non_templated("init") { - Ok(infos_commit) => { - infos.extend(infos_commit); - return Ok(CmdResult { value: (), infos }); - } - Err(e) => return Err(e), - } + let infos_commit = git_commit_non_templated("init")?; + infos.extend(infos_commit); + return Ok(CmdResult { value: (), infos }); } Ok(CmdResult { value: (), infos }) diff --git a/src/cmd/show.rs b/src/cmd/show.rs index 9efc172..5dd28b5 100644 --- a/src/cmd/show.rs +++ b/src/cmd/show.rs @@ -63,8 +63,7 @@ fn generate_content_metadata(id: u32, meta: &Meta) -> String { content.push_str("## Meta Data\n"); content.push('\n'); content.push_str(&format!("| **field** | {:width$} |\n", "**value**")); - content.push_str(&format!("| ----------------- | {} |\n", "-".repeat(width))); - content.push_str(&format!("| **id** | {:width$} |\n", values["id"])); + content.push_str(&format!("| :---------------- | :{} |\n", "-".repeat(width - 1))); content.push_str(&format!("| **state** | {:width$} |\n", values["state"])); content.push_str(&format!("| **type** | {:width$} |\n", values["type"])); content.push_str(&format!("| **labels** | {:width$} |\n", values["labels"])); @@ -82,7 +81,6 @@ fn generate_content_metadata(id: u32, meta: &Meta) -> String { fn get_values(meta: &Meta) -> (usize, HashMap) { let mut values = HashMap::new(); - values.insert("id".to_string(), meta.id.to_string()); values.insert("state".to_string(), meta.state.clone()); values.insert("type".to_string(), dash_if_empty(&meta.type_)); values.insert("labels".to_string(), dash_if_empty(&meta.labels.join(","))); @@ -117,7 +115,7 @@ fn content_relationships(relationships: &IndexMap>, min_width: let mut first = true; for (rel_type, ids) in relationships { - let ids_str = ids.iter().map(|id| id.to_string()).collect::>().join(", "); + let ids_str = ids.iter().map(|id| format!("#{id}")).collect::>().join(", "); let string = format!("{}: {}", rel_type, ids_str); if string.len() > max_width { diff --git a/src/web/favicon.ico b/src/web/favicon.ico new file mode 100644 index 0000000..2008cd6 Binary files /dev/null and b/src/web/favicon.ico differ diff --git a/src/web/main.rs b/src/web/main.rs new file mode 100644 index 0000000..c18426a --- /dev/null +++ b/src/web/main.rs @@ -0,0 +1,234 @@ +#![deny(warnings, clippy::unwrap_used, clippy::expect_used)] +use askama::Template; +use axum::extract::Query; +use axum::http::header; +use axum::{Json, Router, extract::Path, response::Html, response::IntoResponse, routing::get}; +use regex::Regex; +use serde::Deserialize; +use serde_json::{self, json}; +use std::fs; +use std::str::FromStr; + +use git_issue::model::{Filter, load_settings}; + +enum ApiError { + NotFound, + BadRequest(String), + InternalServerError, +} + +impl IntoResponse for ApiError { + fn into_response(self) -> axum::response::Response { + let (status, error_message) = match self { + ApiError::NotFound => (axum::http::StatusCode::NOT_FOUND, "Resource not found".to_string()), + ApiError::BadRequest(msg) => (axum::http::StatusCode::BAD_REQUEST, format!("Bad Request: {}", msg)), + ApiError::InternalServerError => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string()), + }; + + let body = Json(json!({ + "error": error_message, + })); + + (status, body).into_response() + } +} + +async fn ping() -> impl IntoResponse { + Json(json!({ + "status": "ok", + "message": "Service is running", + })) +} + +struct Data { + key: String, + value: String, +} + +#[derive(Template)] +#[template(path = "list.html")] +struct ListTemplate { + ids: Vec, + rows: Vec>, + user: String, + columns: Vec, + filters: Vec, +} + +#[derive(Template)] +#[template(path = "show.html")] +struct ShowTemplate { + id: u32, + content: String, +} + +#[derive(Deserialize)] +struct ListColumnsQuery { + #[serde(default, deserialize_with = "comma_separated")] + columns: Vec, +} + +#[derive(Deserialize)] +struct ListFiltersQuery { + #[serde(default, deserialize_with = "comma_separated")] + filters: Vec, +} + +fn comma_separated<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let s = Option::::deserialize(deserializer)?; + Ok(s.unwrap_or_default().split(',').map(|s| s.to_string()).collect()) +} + +async fn list(Query(columns): Query, Query(filters): Query) -> Result, ApiError> { + let columns = if columns.columns.is_empty() { None } else { Some(columns.columns) }; + + let mut filters_parsed: Vec = Vec::new(); + + for filter in &filters.filters { + // Skip empty filter strings + if filter.trim().is_empty() { + continue; + } + + let parsed = Filter::from_str(filter); + match parsed { + Ok(f) => filters_parsed.push(f), + Err(_) => { + return Err(ApiError::BadRequest(format!("Invalid filter format: {}", filter))); + } + } + } + + let result = git_issue::list(columns, Some(filters_parsed), None); + + let result = match result { + Ok(result) => result, + Err(_) => { + return Err(ApiError::InternalServerError); + } + }; + + for info in result.infos { + println!("{}", info); + } + + let columns = result.value.columns; + + let mut ids: Vec = Vec::new(); + let mut rows: Vec> = Vec::new(); + + for issue in &result.value.issues { + ids.push(issue.id); + + let mut issue_rows: Vec = Vec::new(); + + for col in &columns { + issue_rows.push(Data { + key: col.clone(), + value: issue.data.get(col).cloned().unwrap_or_default(), + }); + } + + rows.push(issue_rows); + } + + let (settings, _) = match load_settings() { + Ok(settings) => settings, + Err(_) => { + return Err(ApiError::InternalServerError); + } + }; + + let issue_collection = ListTemplate { + ids, + rows, + user: settings.user.to_string(), + columns, + filters: filters.filters, + }; + + let html = issue_collection.render().map_err(|_| ApiError::InternalServerError)?; + + Ok(Html(html)) +} + +async fn show(Path(id): Path) -> Result, ApiError> { + if id == 0 { + return Err(ApiError::BadRequest("ID does not exist".to_string())); + } + + let result = git_issue::show(id); + + let md_path = match result { + Ok(result) => result.value, + Err(_) => { + return Err(ApiError::InternalServerError); + } + }; + + let mut content = match fs::read_to_string(&md_path) { + Ok(content) => content, + Err(_) => { + return Err(ApiError::InternalServerError); + } + }; + + let md_start = "\n\n# Issue "; + + // Prevent title from being formatted --> remove # from title issue id + let re = Regex::new(&format!("^({md_start})#(\\d+)")).map_err(|_| ApiError::InternalServerError)?; + content = re.replace(&content, "$1$2").to_string(); + + // Format issue ids with links to the corresponding show page + let re = Regex::new(r"#(\d+)").map_err(|_| ApiError::InternalServerError)?; + content = re.replace_all(&content, "[#$1](http://localhost:7878/show/$1)").to_string(); + + // Put back the # in front of the title issue id + let re = Regex::new(&format!("^({md_start})(\\d+)")).map_err(|_| ApiError::InternalServerError)?; + content = re.replace(&content, "$1#$2").to_string(); + + let template = ShowTemplate { id, content }; + let html = template.render().map_err(|_| ApiError::InternalServerError)?; + + Ok(Html(html)) +} + +async fn favicon() -> impl IntoResponse { + let bytes = include_bytes!("favicon.ico"); + ([(header::CONTENT_TYPE, "image/x-icon")], bytes.as_slice()).into_response() +} + +async fn not_found() -> impl IntoResponse { + ApiError::NotFound +} + +fn create_app() -> Router { + Router::new() + .route("/ping", get(ping)) + .route("/", get(list)) + .route("/list", get(list)) + .route("/show/{id}", get(show)) + .route("/favicon.ico", get(favicon)) + .fallback(not_found) +} + +#[tokio::main] +async fn main() { + let app = create_app(); + + let listener = match tokio::net::TcpListener::bind("127.0.0.1:7878").await { + Ok(listener) => listener, + Err(err) => { + eprintln!("Failed to bind listener: {err}"); + std::process::exit(1); + } + }; + + if let Err(err) = axum::serve(listener, app).await { + eprintln!("Failed to start server: {err}"); + std::process::exit(1); + } +} diff --git a/src/web/templates/list.html b/src/web/templates/list.html new file mode 100644 index 0000000..87a98cc --- /dev/null +++ b/src/web/templates/list.html @@ -0,0 +1,256 @@ + + + + + git-issue + + + + + + + + + +
+
+
+

🗂️ git-issue

+

Browse and manage your Issues tracked with git-issue +

+
+
+ +
+

Columns

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+

Filters

+ +
+ + + + +
+
+ +
+ + + + + {% for column in columns %} + + {% endfor %} + + + + {% for (idx, id) in ids.iter().enumerate() %} + + {% for data in rows[idx] %} + {% match data.key.as_str() %} + {% when "id" %} + + {% when "state" %} + + {% when "assignee" | "reporter" %} + {% if data.value == user %} + + {% else %} + + {% endif %} + {% else %} + + {% endmatch %} + {% endfor %} + + {% endfor %} + +
Issue list
{{ column }}
{{ data.value }}{{ data.value }}{{ data.value }}{{ data.value }}{{ data.value }}
+
+
+ + + + + diff --git a/src/web/templates/show.html b/src/web/templates/show.html new file mode 100644 index 0000000..6a38049 --- /dev/null +++ b/src/web/templates/show.html @@ -0,0 +1,125 @@ + + + + + git-issue #{{ id }} + + + + + + +
+
+
+

🗂️ git-issue

+

Browse and manage your Issues tracked with git-issue +

+
+
+ +
+ +
{% if content.is_empty() %}No description available.{% endif %}
+
+
+ + + + + + diff --git a/tests/includes/show_empty.md b/tests/includes/show_empty.md index 65cdefb..d447f5e 100644 --- a/tests/includes/show_empty.md +++ b/tests/includes/show_empty.md @@ -5,8 +5,7 @@ ## Meta Data | **field** | **value** | -| ----------------- | -------------------- | -| **id** | 1 | +| :---------------- | :------------------- | | **state** | new | | **type** | - | | **labels** | - | diff --git a/tests/includes/show_metadata.md b/tests/includes/show_metadata.md index 745cc46..d10685d 100644 --- a/tests/includes/show_metadata.md +++ b/tests/includes/show_metadata.md @@ -5,8 +5,7 @@ ## Meta Data | **field** | **value** | -| ----------------- | -------------------- | -| **id** | 1 | +| :---------------- | :------------------- | | **state** | active | | **type** | feature | | **labels** | ui,driver | @@ -14,8 +13,8 @@ | **assignee** | bob | | **priority** | P1 | | **due_date** | 2026-06-24 | -| **relationships** | related: 2 | -| | child: 2, 3 | +| **relationships** | related: #2 | +| | child: #2, #3 | | **created** | __CREATED__ | | **updated** | __UPDATED__ | diff --git a/tests/includes/show_metadata_wider.md b/tests/includes/show_metadata_wider.md index cdb5418..3dcde1f 100644 --- a/tests/includes/show_metadata_wider.md +++ b/tests/includes/show_metadata_wider.md @@ -5,8 +5,7 @@ ## Meta Data | **field** | **value** | -| ----------------- | ------------------------------------------ | -| **id** | 1 | +| :---------------- | :----------------------------------------- | | **state** | active | | **type** | feature | | **labels** | ui,driver,backend,api,performance,security | @@ -14,8 +13,8 @@ | **assignee** | bob | | **priority** | P1 | | **due_date** | 2026-06-24 | -| **relationships** | related: 2 | -| | child: 2, 3 | +| **relationships** | related: #2 | +| | child: #2, #3 | | **created** | __CREATED__ | | **updated** | __UPDATED__ |