From 718e8c436bd478bf174d85c698788355316f2c88 Mon Sep 17 00:00:00 2001 From: Kenta Okamoto Date: Sun, 30 Mar 2025 12:50:01 +0900 Subject: [PATCH] Rewritten in Rust --- .devcontainer/devcontainer.json | 4 + .github/workflows/build.yaml | 29 - .github/workflows/ci.yml | 47 ++ .github/workflows/release.yaml | 24 - .github/workflows/release_linux.yml | 36 ++ .github/workflows/release_macos.yml | 32 ++ .github/workflows/release_windows.yml | 36 ++ .gitignore | 3 +- Cargo.lock | 788 ++++++++++++++++++++++++++ Cargo.toml | 10 + Makefile | 51 +- README.md | 150 ++--- branch.go | 32 -- branch_test.go | 15 - git.go | 20 - go.mod | 5 - go.sum | 2 - input.go | 42 -- main.go | 203 ------- parser.go | 89 --- parser_test.go | 33 -- src/branch.rs | 235 ++++++++ src/main.rs | 177 ++++++ 23 files changed, 1473 insertions(+), 590 deletions(-) create mode 100644 .devcontainer/devcontainer.json delete mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/release_linux.yml create mode 100644 .github/workflows/release_macos.yml create mode 100644 .github/workflows/release_windows.yml create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 branch.go delete mode 100644 branch_test.go delete mode 100644 git.go delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 input.go delete mode 100644 main.go delete mode 100644 parser.go delete mode 100644 parser_test.go create mode 100644 src/branch.rs create mode 100644 src/main.rs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..d1fd027 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Rust", + "image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye" +} diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index 94f91a6..0000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: build - -on: - push: - branches: - - master - pull_request: - types: [opened, synchronize, reopened] - -jobs: - test: - name: Test - - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - - steps: - - uses: actions/checkout@v2 - - name: Setup Go - uses: actions/setup-go@v2 - with: - go-version: '^1.16' - - name: Test - run: make test - - name: Lint - run: make lint diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f5fdb72 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + strategy: + matrix: + rust: [stable] + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + components: rustfmt, clippy + profile: minimal + cache: 'cargo' + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: "." + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Clippy + run: cargo clippy -- -D warnings + + - name: Build + run: cargo build --verbose + + - name: Run tests + run: cargo test --verbose + diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 0d2b8a7..0000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: release - -on: - push: - tags: - - '*' - -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Setup Go - uses: actions/setup-go@v2 - with: - go-version: '^1.16' - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 - with: - version: latest - args: release --rm-dist - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release_linux.yml b/.github/workflows/release_linux.yml new file mode 100644 index 0000000..6879354 --- /dev/null +++ b/.github/workflows/release_linux.yml @@ -0,0 +1,36 @@ +name: Build and upload release (linux) + +on: + push: + tags: + - 'v*' + +env: + APP_NAME: buranko + ARCHIVE_NAME: buranko_linux-x64.tar.gz + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build application + run: | + docker run --rm -t -v $HOME/.cargo/registry/:/root/.cargo/registry -v "$(pwd)":/volume clux/muslrust:stable cargo build --release + + - name: Create archive + run: | + tar -czf $ARCHIVE_NAME -C target/x86_64-unknown-linux-musl/release $APP_NAME + + - name: Upload release asset + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ env.ARCHIVE_NAME }} + asset_name: ${{ env.ARCHIVE_NAME }} + tag: ${{ github.ref }} + overwrite: true + diff --git a/.github/workflows/release_macos.yml b/.github/workflows/release_macos.yml new file mode 100644 index 0000000..97f85b6 --- /dev/null +++ b/.github/workflows/release_macos.yml @@ -0,0 +1,32 @@ +name: Build and upload release (macos) + +on: + push: + tags: + - 'v*' + +env: + APP_NAME: buranko + ARCHIVE_NAME: buranko_macos-x64.tar.gz + +jobs: + build: + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build application + run: | + cargo build --release + tar -czf $ARCHIVE_NAME -C target/release $APP_NAME + + - name: Upload release asset + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ env.ARCHIVE_NAME }} + asset_name: ${{ env.ARCHIVE_NAME }} + tag: ${{ github.ref }} + overwrite: true diff --git a/.github/workflows/release_windows.yml b/.github/workflows/release_windows.yml new file mode 100644 index 0000000..84d587b --- /dev/null +++ b/.github/workflows/release_windows.yml @@ -0,0 +1,36 @@ +name: Build and upload release (windows) + +on: + push: + tags: + - 'v*' + +env: + APP_NAME: buranko + ARCHIVE_NAME: buranko_windows-x64.zip + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build application + run: | + cargo build --release + cd target/release + ls + 7z a -tzip $ARCHIVE_NAME $APP_NAME.exe + mv $ARCHIVE_NAME ../../ + shell: bash + + - name: Upload release asset + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ env.ARCHIVE_NAME }} + asset_name: ${{ env.ARCHIVE_NAME }} + tag: ${{ github.ref }} + overwrite: true diff --git a/.gitignore b/.gitignore index d6a0601..ea8c4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -*.test -/buranko +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bf88fda --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,788 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "buranko" +version = "3.0.0" +dependencies = [ + "atty", + "clap", + "git2", + "regex", +] + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "clap" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +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 = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "git2" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5220b8ba44c68a9a7f7a7659e864dd73692e417ef0211bea133c7b74e031eeb9" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[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.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "libgit2-sys" +version = "0.18.1+1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "once_cell" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[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 = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[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.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[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" +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" + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a85a424 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "buranko" +version = "3.0.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5.32", features = ["derive"] } +git2 = "0.20.1" +regex = "1.10.3" +atty = "0.2.14" diff --git a/Makefile b/Makefile index e43f68d..3815dcb 100644 --- a/Makefile +++ b/Makefile @@ -1,31 +1,36 @@ -BIN := buranko -VERBOSE_FLAG = $(if $(VERBOSE),-v) -GOBIN ?= $(shell go env GOPATH)/bin +.PHONY: build test clean run check format lint -.PHONY: all -all: clean build +all: build -testdeps: - go get -d -t $(VERBOSE_FLAG) +build: + cargo build -test: testdeps - go test $(VERBOSE_FLAG) ./... +release: + cargo build --release -.PHONY: test testdeps +test: + cargo test -.PHONY: lint -lint: $(GOBIN)/golint - go vet ./... - golint -set_exit_status ./... +clean: + cargo clean -$(GOBIN)/golint: - cd && go get golang.org/x/lint/golint +run: + cargo run -.PHONY: build -build: - go build -o $(BIN) +check: + cargo check -.PHONY: clean -clean: - rm $(BIN) - go clean +format: + cargo fmt + +lint: + cargo clippy + +update: + cargo update + +install: + cargo install --path . + +uninstall: + cargo uninstall buranko diff --git a/README.md b/README.md index ddf281f..84a931b 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,122 @@ -# buranko +# Buranko -![Build Status](https://github.com/chocoby/buranko/workflows/build/badge.svg?branch=master) +![Build Status](https://github.com/chocoby/buranko/workflows/build/badge.svg?branch=main) -A tool for parse a git branch name +Buranko is a CLI tool for parsing and formatting Git branch names. + +## Features + +- Git branch name parsing +- Output formatting using templates +- Field extraction + +## Installation + +Download a binary from [releases page](https://github.com/chocoby/buranko/releases) and place it in `$PATH` directory. ## Usage -`buranko` outputs the `ID` field by default. +### Basic Usage -``` -$ git checkout -b feature/1234_foo-bar -$ buranko -1234 +Display the current branch ID: + +```bash +buranko ``` -Specify an output field. +### Options -``` -$ buranko -output LinkID -#1234 -$ buranko -output Name -foo-bar -``` +#### `-t, --template` -Parse a branch name from stdin. +Format the output using a configured template: -``` -$ echo 'feature/1234_foo-bar' | buranko -1234 +```bash +buranko --template ``` -## Configuration +Template configuration: -Configuration uses 'git-config' variables. +```bash +git config buranko.template "{Action}/{ID}-{Description}" +``` -***buranko.template*** +Available template variables: -The template can use the values defined in the following Fields. +- `{FullName}`: Full branch name +- `{Action}`: Action (feature, bugfix, etc.) +- `{ID}`: Branch ID +- `{LinkID}`: Link ID (with #) +- `{Description}`: Branch description -This option is useful for pointing to issues in other GitHub repositories, or for software that requires a prefix in the issue number. +This option is useful for referencing issues in other GitHub repositories, or for software that requires a prefix in the issue number. -``` +```bash $ git checkout -b feature/1234_foo-bar -$ git config buranko.template ABC-{{.ID}} -$ buranko -template +$ git config buranko.template ABC-{ID} +$ buranko --template ABC-1234 -$ git config buranko.template foo-org/bar-repo#{{.ID}} -$ buranko -template +$ git config buranko.template foo-org/bar-repo#{ID} +$ buranko --template foo-org/bar-repo#1234 ``` -## Fields - -* `FullName`: Full branch name -* `Action`: Action type -* `ID`: Issue ID -* `LinkID`: Issue ID with a leading `#` -* `Description`: Description +#### `--output ` -## Parse patterns +Output only a specific field: -### `feature/1234_foo-bar` +```bash +buranko --output FullName +``` -* `FullName`: `feature/1234_foo-bar` -* `Action`: `feature` -* `ID`: `1234` -* `LinkID`: `#1234` -* `Description`: `foo-bar` +Available fields: -### `foo-bar` +- `FullName`: Full branch name +- `Action`: Action +- `ID`: Branch ID +- `LinkID`: Link ID +- `Description`: Branch description -* `FullName`: `foo-bar` -* `Description`: `foo-bar` +#### `-v, --verbose` -More patterns at [`parser_test.go`](https://github.com/chocoby/buranko/blob/master/parser_test.go). +Display all branch information: -## Integrate with `prepare-commit-msg` +```bash +buranko --verbose +``` -Add an issue ID to commit comment using git hook. +### Input from Pipe -`GIT-REPO/.git/hooks/prepare-commit-msg` +You can read branch names from stdin: -```sh -if [ "$2" == "" ]; then - mv $1 $1.tmp - echo `buranko -output LinkID -template` > $1 - cat $1.tmp >> $1 -fi +```bash +echo "feature/123-test" | buranko ``` -## Install +## Branch Name Format -To install, use `go get`: +Buranko parses branch names in the following format: -```bash -$ go get github.com/chocoby/buranko +``` +/- ``` -Or you can download a binary from [releases page](https://github.com/chocoby/buranko/releases) and place it in `$PATH` directory. +Examples: +- `feature/123-add-login` +- `bugfix/456-fix-crash` +- `hotfix/789-security-patch` -## Contribution +More patterns at [`main.rs`](https://github.com/chocoby/buranko/blob/main/src/main.rs). -1. Fork ([https://github.com/chocoby/buranko/fork](https://github.com/chocoby/buranko/fork)) -1. Create a feature branch -1. Commit your changes -1. Rebase your local changes against the master branch -1. Run test suite with the `go test ./...` command and confirm that it passes -1. Run `gofmt -s` -1. Create a new Pull Request +## Integrate with `prepare-commit-msg` + +Add an issue ID to the commit message using a git hook. -## GitHub +`[Git repository]/.git/hooks/prepare-commit-msg` -https://github.com/chocoby/buranko +```bash +if [ "$2" == "" ]; then + mv $1 $1.tmp + echo `buranko -output LinkID -template` > $1 + cat $1.tmp >> $1 +fi +``` diff --git a/branch.go b/branch.go deleted file mode 100644 index ce0228a..0000000 --- a/branch.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -var ( - fullName string = "" - action string = "" - id string = "" - description string = "" -) - -// Branch is a branch information -type Branch struct { - FullName string - Action string - ID string - Description string -} - -// NewBranch returns Branch -func NewBranch() *Branch { - return &Branch{ - FullName: fullName, - Action: action, - ID: id, - Description: description, - } -} - -// LinkID returns ID with a leading # -// This can be used to link the issue ID to the commit message -func (b *Branch) LinkID() string { - return "#" + b.ID -} diff --git a/branch_test.go b/branch_test.go deleted file mode 100644 index 2197b95..0000000 --- a/branch_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "testing" -) - -func TestBranch_LinkID(t *testing.T) { - branch := NewBranch() - branch.ID = "1234" - expected := "#1234" - - if branch.LinkID() != expected { - t.Fatalf("Expected %v, but %v:", expected, branch.LinkID()) - } -} diff --git a/git.go b/git.go deleted file mode 100644 index 74d41a4..0000000 --- a/git.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "strings" - - pipeline "github.com/mattn/go-pipeline" -) - -// GetTemplate returns a configured template. -func GetTemplate() string { - out, err := pipeline.Output( - []string{"git", "config", "--get", "buranko.template"}, - ) - - if err != nil { - return "" - } - - return strings.TrimRight(string(out), "\n") -} diff --git a/go.mod b/go.mod deleted file mode 100644 index 8f47f6f..0000000 --- a/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/chocoby/buranko - -go 1.13 - -require github.com/mattn/go-pipeline v0.0.0-20190323144519-32d779b32768 diff --git a/go.sum b/go.sum deleted file mode 100644 index 862a2b1..0000000 --- a/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/mattn/go-pipeline v0.0.0-20190323144519-32d779b32768 h1:uZ41sUUU0/vDwBwMVz7O1jtcSyLFn/AeUdMr9MKcDsc= -github.com/mattn/go-pipeline v0.0.0-20190323144519-32d779b32768/go.mod h1:THCMZVX5asLpinN+6hFlR1xKFcFsaDpAtUltGqZauBM= diff --git a/input.go b/input.go deleted file mode 100644 index 256cc8d..0000000 --- a/input.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "os" - - pipeline "github.com/mattn/go-pipeline" -) - -// GetBranchNameFromStdin returns branch name from stdin. -func GetBranchNameFromStdin() string { - out := "" - - scanner := bufio.NewScanner(os.Stdin) - - for scanner.Scan() { - out = scanner.Text() - break - } - - if err := scanner.Err(); err != nil { - fmt.Println(err) - os.Exit(1) - } - - return out -} - -// GetBranchNameFromGitCommand returns branch name from git command. -func GetBranchNameFromGitCommand() string { - out, err := pipeline.Output( - []string{"git", "rev-parse", "--abbrev-ref", "HEAD"}, - ) - - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - return string(out) -} diff --git a/main.go b/main.go deleted file mode 100644 index 09ea0df..0000000 --- a/main.go +++ /dev/null @@ -1,203 +0,0 @@ -package main - -import ( - "bufio" - "flag" - "fmt" - "io" - "log" - "os" - "strings" - "text/template" -) - -// Version returns release version -const Version string = "2.0.0" - -var ( - output string - useTemplate bool -) - -// A Command is an implementation of a buranko command -type Command struct { - // Run runs the command. - // The args are the arguments after the command name. - Run func(args []string) int - - // UsageLine is the one-line usage message. - // The first word in the line is taken to be the command name. - UsageLine string - - // Short is the short description shown in the 'buranko help' output. - Short string - - // Long is the long message shown in the 'buranko help ' output. - Long string - - // Flag is a set of flags specific to this command. - Flag flag.FlagSet -} - -// Name returns the command's name: the first word in the usage line. -func (c *Command) Name() string { - name := c.UsageLine - i := strings.Index(name, " ") - if i >= 0 { - name = name[:i] - } - return name -} - -// Usage returns the commands usage. -func (c *Command) Usage() { - fmt.Fprintf(os.Stderr, "usage: %s\n\n", c.UsageLine) - fmt.Fprintf(os.Stderr, "%s\n", strings.TrimSpace(c.Long)) - os.Exit(2) -} - -// Commands lists the available commands and help topics. -// The order here is the order in which they are printed by 'buranko help'. -var commands = []*Command{} - -func main() { - flag.StringVar(&output, "output", "ID", "Output field") - flag.BoolVar(&useTemplate, "template", false, "Use the configured template in the output") - flag.Usage = usage - flag.Parse() - log.SetFlags(0) - - args := flag.Args() - - if len(args) < 1 { - doOutput() - return - } - - if args[0] == "help" { - help(args[1:]) - return - } - - if args[0] == "version" { - version() - return - } -} - -var usageTemplate = `buranko is a tool for parse a git branch name - -Usage: - - buranko commands [arguments] - -The commands are: - help Show this help - version Output version information - -Options: - -output - Specify an output field. - Available fields are FullName, Action, ID, LinkID, Description. - - -template - Use the configured template in the output. -` - -var helpTemplate = `usage: buranko {{.UsageLine}} - -{{.Long | trim}} -` - -// tmpl executes the given template text on data, writing the result to w. -func tmpl(w io.Writer, text string, data interface{}) { - t := template.New("top") - t.Funcs(template.FuncMap{"trim": strings.TrimSpace}) - template.Must(t.Parse(text)) - if err := t.Execute(w, data); err != nil { - panic(err) - } -} - -func printUsage(w io.Writer) { - bw := bufio.NewWriter(w) - tmpl(bw, usageTemplate, commands) - bw.Flush() -} - -func usage() { - printUsage(os.Stderr) - os.Exit(2) -} - -// help implements the 'help' command. -func help(args []string) { - if len(args) == 0 { - printUsage(os.Stdout) - // not exit 2: succeeded at 'buranko help'. - return - } - if len(args) != 1 { - fmt.Fprintf(os.Stderr, "usage: buranko help command\n\nToo many arguments given.\n") - os.Exit(2) // failed at 'buranko help' - } - - arg := args[0] - - for _, cmd := range commands { - if cmd.Name() == arg { - tmpl(os.Stdout, helpTemplate, cmd) - // not exit 2: succeeded at 'buranko help cmd'. - return - } - } - - fmt.Fprintf(os.Stderr, "Unknown help topic %#q. Run 'buranko help'.\n", arg) - os.Exit(2) // failed at 'buranko help cmd' -} - -func version() { - fmt.Fprintf(os.Stdout, "branko version v%s\n", Version) -} - -func doOutput() { - branchName := "" - - stat, _ := os.Stdin.Stat() - if (stat.Mode() & os.ModeCharDevice) == 0 { - branchName = GetBranchNameFromStdin() - } else { - branchName = GetBranchNameFromGitCommand() - } - - branch := Parse(branchName) - - templateText := GetTemplate() - if useTemplate && len(templateText) > 0 { - tmpl, err := template.New("Format").Parse(templateText) - if err != nil { - log.Fatal(err) - } - writer := new(strings.Builder) - tmpl.Execute(writer, branch) - - fmt.Print(writer.String()) - - return - } - - switch output { - case "FullName": - fmt.Print(branch.FullName) - case "Action": - fmt.Print(branch.Action) - case "ID": - fmt.Print(branch.ID) - case "LinkID": - fmt.Print(branch.LinkID()) - case "Description": - fmt.Print(branch.Description) - default: - fmt.Print("") - } -} diff --git a/parser.go b/parser.go deleted file mode 100644 index 6ba65d0..0000000 --- a/parser.go +++ /dev/null @@ -1,89 +0,0 @@ -package main - -import ( - "regexp" -) - -// Parse returns parsed branch name. -func Parse(fullName string) *Branch { - branch := NewBranch() - - re := regexp.MustCompile(`(\S+)\/(\d+)_(\S+)`) - matches := re.FindStringSubmatch(fullName) - - if len(matches) > 0 { - branch.FullName = fullName - branch.Action = matches[1] - branch.ID = matches[2] - branch.Description = matches[3] - - return branch - } - - re = regexp.MustCompile(`(\S+)\/(\d+)-(\S+)`) - matches = re.FindStringSubmatch(fullName) - - if len(matches) > 0 { - branch.FullName = fullName - branch.Action = matches[1] - branch.ID = matches[2] - branch.Description = matches[3] - - return branch - } - - re = regexp.MustCompile(`(\S+)\/(\d+)`) - matches = re.FindStringSubmatch(fullName) - - if len(matches) > 0 { - branch.FullName = fullName - branch.Action = matches[1] - branch.ID = matches[2] - - return branch - } - - re = regexp.MustCompile(`(\S+)\/(\S+)`) - matches = re.FindStringSubmatch(fullName) - - if len(matches) > 0 { - branch.FullName = fullName - branch.Action = matches[1] - branch.Description = matches[2] - - return branch - } - - re = regexp.MustCompile(`#(\d+)-(\S+)`) - matches = re.FindStringSubmatch(fullName) - - if len(matches) > 0 { - branch.FullName = fullName - branch.ID = matches[1] - branch.Description = matches[2] - - return branch - } - - re = regexp.MustCompile(`(\d+)`) - matches = re.FindStringSubmatch(fullName) - - if len(matches) > 0 { - branch.FullName = fullName - branch.ID = matches[1] - - return branch - } - - re = regexp.MustCompile(`(\S+)`) - matches = re.FindStringSubmatch(fullName) - - if len(matches) > 0 { - branch.FullName = fullName - branch.Description = matches[1] - - return branch - } - - return branch -} diff --git a/parser_test.go b/parser_test.go deleted file mode 100644 index d9b4023..0000000 --- a/parser_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "reflect" - "testing" -) - -func TestParse(t *testing.T) { - testcases := []struct { - line string - expected *Branch - }{ - {`feature/1234_foo`, &Branch{FullName: "feature/1234_foo", Action: "feature", ID: "1234", Description: "foo"}}, - {`feature/1234_foo-bar`, &Branch{FullName: "feature/1234_foo-bar", Action: "feature", ID: "1234", Description: "foo-bar"}}, - {`feature/1234_foo_bar`, &Branch{FullName: "feature/1234_foo_bar", Action: "feature", ID: "1234", Description: "foo_bar"}}, - {`feature/1234-foo`, &Branch{FullName: "feature/1234-foo", Action: "feature", ID: "1234", Description: "foo"}}, - {`feature/1234`, &Branch{FullName: "feature/1234", Action: "feature", ID: "1234", Description: ""}}, - {`feature/foo`, &Branch{FullName: "feature/foo", Action: "feature", ID: "", Description: "foo"}}, - {`#1234-foo-bar`, &Branch{FullName: "#1234-foo-bar", Action: "", ID: "1234", Description: "foo-bar"}}, - {`JRA-1234`, &Branch{FullName: "JRA-1234", Action: "", ID: "1234", Description: ""}}, - {`foo`, &Branch{FullName: "foo", Action: "", ID: "", Description: "foo"}}, - {`foo-bar`, &Branch{FullName: "foo-bar", Action: "", ID: "", Description: "foo-bar"}}, - {`1234`, &Branch{FullName: "1234", Action: "", ID: "1234", Description: ""}}, - {``, &Branch{FullName: "", Action: "", ID: "", Description: ""}}, - } - - for _, testcase := range testcases { - parser := Parse(testcase.line) - if !reflect.DeepEqual(parser, testcase.expected) { - t.Fatalf("Expected %v, but %v:", testcase.expected, parser) - } - } -} diff --git a/src/branch.rs b/src/branch.rs new file mode 100644 index 0000000..5362299 --- /dev/null +++ b/src/branch.rs @@ -0,0 +1,235 @@ +#[derive(Debug, PartialEq)] +pub struct Branch { + pub full_name: String, + pub action: String, + pub id: String, + pub description: String, +} + +use regex::Regex; +use std::sync::LazyLock; + +static FEATURE_BRANCH: LazyLock = + LazyLock::new(|| Regex::new(r"^([\w-]+)/(?:(\d+)[_-](.+)|(\d+)$|(.+))$").unwrap()); + +static JIRA_FEATURE: LazyLock = + LazyLock::new(|| Regex::new(r"^([\w-]+)/([A-Z]+-\d+)-(.+)$").unwrap()); + +static TICKET_REF: LazyLock = LazyLock::new(|| Regex::new(r"^#?(\d+)-(.+)$").unwrap()); + +static JIRA_TICKET: LazyLock = LazyLock::new(|| Regex::new(r"^([A-Z]+-(\d+))$").unwrap()); + +static PURE_NUMBER: LazyLock = LazyLock::new(|| Regex::new(r"^(\d+)$").unwrap()); + +impl Branch { + pub fn parse(branch_name: &str) -> Self { + if branch_name.is_empty() { + return Self::empty(); + } + + // Case 1: feature/JRA-1234-foo + if let Some(caps) = JIRA_FEATURE.captures(branch_name) { + return Self { + full_name: branch_name.to_string(), + action: caps.get(1).map_or("", |m| m.as_str()).to_string(), + id: caps + .get(2) + .and_then(|m| extract_jira_id(m.as_str())) + .unwrap_or_default(), + description: caps.get(3).map_or("", |m| m.as_str()).to_string(), + }; + } + + // Case 2: feature/1234_foo or feature/1234-foo or feature/1234 or feature/foo + if let Some(caps) = FEATURE_BRANCH.captures(branch_name) { + let action = caps.get(1).map_or("", |m| m.as_str()).to_string(); + if let Some(id) = caps.get(2) { + // Case: feature/1234_foo or feature/1234-foo + return Self { + full_name: branch_name.to_string(), + action, + id: id.as_str().to_string(), + description: caps.get(3).map_or("", |m| m.as_str()).to_string(), + }; + } else if let Some(pure_id) = caps.get(4) { + // Case: feature/1234 + return Self { + full_name: branch_name.to_string(), + action, + id: pure_id.as_str().to_string(), + description: String::new(), + }; + } else { + // Case: feature/foo + return Self { + full_name: branch_name.to_string(), + action, + id: String::new(), + description: caps.get(5).map_or("", |m| m.as_str()).to_string(), + }; + } + } + + // Case 3: #1234-foo-bar + if let Some(caps) = TICKET_REF.captures(branch_name) { + return Self { + full_name: branch_name.to_string(), + action: String::new(), + id: caps.get(1).map_or("", |m| m.as_str()).to_string(), + description: caps.get(2).map_or("", |m| m.as_str()).to_string(), + }; + } + + // Case 4: JRA-1234 + if let Some(caps) = JIRA_TICKET.captures(branch_name) { + return Self { + full_name: branch_name.to_string(), + action: String::new(), + id: caps.get(2).map_or("", |m| m.as_str()).to_string(), + description: String::new(), + }; + } + + // Case 5: Pure number + if let Some(caps) = PURE_NUMBER.captures(branch_name) { + return Self { + full_name: branch_name.to_string(), + action: String::new(), + id: caps.get(1).map_or("", |m| m.as_str()).to_string(), + description: String::new(), + }; + } + + // Case 6: Simple text + Self { + full_name: branch_name.to_string(), + action: String::new(), + id: String::new(), + description: branch_name.to_string(), + } + } + + fn empty() -> Self { + Self { + full_name: String::new(), + action: String::new(), + id: String::new(), + description: String::new(), + } + } +} + +// Helper function to extract JIRA-style IDs (e.g., JRA-1234) +fn extract_jira_id(text: &str) -> Option { + let parts: Vec<&str> = text.split('-').collect(); + if parts.len() >= 2 && parts[1].chars().all(|c| c.is_ascii_digit()) { + Some(parts[1].to_string()) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_branch_name() { + let test_cases = vec![ + ( + "feature/1234_foo", + Branch { + full_name: "feature/1234_foo".to_string(), + action: "feature".to_string(), + id: "1234".to_string(), + description: "foo".to_string(), + }, + ), + ( + "feature/1234_foo-bar", + Branch { + full_name: "feature/1234_foo-bar".to_string(), + action: "feature".to_string(), + id: "1234".to_string(), + description: "foo-bar".to_string(), + }, + ), + ( + "feature/1234-foo-bar", + Branch { + full_name: "feature/1234-foo-bar".to_string(), + action: "feature".to_string(), + id: "1234".to_string(), + description: "foo-bar".to_string(), + }, + ), + ( + "feature/1234", + Branch { + full_name: "feature/1234".to_string(), + action: "feature".to_string(), + id: "1234".to_string(), + description: "".to_string(), + }, + ), + ( + "feature/foo", + Branch { + full_name: "feature/foo".to_string(), + action: "feature".to_string(), + id: "".to_string(), + description: "foo".to_string(), + }, + ), + ( + "#1234-foo-bar", + Branch { + full_name: "#1234-foo-bar".to_string(), + action: "".to_string(), + id: "1234".to_string(), + description: "foo-bar".to_string(), + }, + ), + ( + "feature/JRA-1234-foo", + Branch { + full_name: "feature/JRA-1234-foo".to_string(), + action: "feature".to_string(), + id: "1234".to_string(), + description: "foo".to_string(), + }, + ), + ( + "JRA-1234", + Branch { + full_name: "JRA-1234".to_string(), + action: "".to_string(), + id: "1234".to_string(), + description: "".to_string(), + }, + ), + ( + "foo", + Branch { + full_name: "foo".to_string(), + action: "".to_string(), + id: "".to_string(), + description: "foo".to_string(), + }, + ), + ( + "", + Branch { + full_name: "".to_string(), + action: "".to_string(), + id: "".to_string(), + description: "".to_string(), + }, + ), + ]; + + for (input, expected) in test_cases { + assert_eq!(Branch::parse(input), expected); + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cf66cca --- /dev/null +++ b/src/main.rs @@ -0,0 +1,177 @@ +use clap::Parser; +use git2::Repository; +use std::io::{self, Read}; + +mod branch; +use branch::Branch; + +/// CLI tool to parse and format git branch names +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Format output using template + #[arg(short, long)] + template: bool, + + /// Select output field (FullName, Action, ID, LinkID, Description) + #[arg(long, value_parser = ["FullName", "Action", "ID", "LinkID", "Description"], default_value = "ID")] + output: String, + + /// Show all branch information + #[arg(short, long)] + verbose: bool, +} + +fn read_from_stdin() -> io::Result { + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer)?; + Ok(buffer.trim().to_string()) +} + +fn get_current_branch() -> Result { + let repo = Repository::discover(".")?; + let head = repo.head()?; + let branch_name = head.shorthand().unwrap_or("HEAD detached").to_string(); + Ok(Branch::parse(&branch_name)) +} + +fn get_template_from_config() -> Result { + let repo = Repository::discover(".")?; + let config = repo.config()?; + let template = config.get_string("buranko.template").unwrap_or_default(); + Ok(template) +} + +fn format_with_template(branch: &Branch, template: &str) -> String { + let mut result = template.to_string(); + result = result.replace("{FullName}", &branch.full_name); + result = result.replace("{Action}", &branch.action); + result = result.replace("{ID}", &branch.id); + result = result.replace("{LinkID}", &format!("#{}", &branch.id)); + result = result.replace("{Description}", &branch.description); + result +} + +fn format_verbose_output(branch: &Branch) -> String { + format!( + "Full Name: {}\nAction: {}\nID: {}\nDescription: {}", + branch.full_name, branch.action, branch.id, branch.description + ) +} + +fn main() { + let args = Args::parse(); + + // Check if input is coming from a pipe + let branch = if !atty::is(atty::Stream::Stdin) { + // Read from stdin + match read_from_stdin() { + Ok(input) => Branch::parse(&input), + Err(e) => { + eprintln!("Failed to read from stdin: {}", e); + return; + } + } + } else { + // Get current git branch + match get_current_branch() { + Ok(branch) => branch, + Err(e) => { + eprintln!("Failed to get branch name: {}", e); + return; + } + } + }; + + if args.verbose { + println!("{}", format_verbose_output(&branch)); + } else if args.template { + match get_template_from_config() { + Ok(template) => { + if !template.is_empty() { + let formatted = format_with_template(&branch, &template); + print!("{}", formatted); + } else { + println!( + "No template configured. Use 'git config buranko.template' to set a template." + ); + } + } + Err(e) => eprintln!("Failed to get template: {}", e), + } + } else { + let output = match args.output.as_str() { + "FullName" => branch.full_name, + "Action" => branch.action, + "ID" => branch.id, + "LinkID" => format!("#{}", branch.id), + "Description" => branch.description, + _ => unreachable!("Invalid output field - clap should prevent this"), + }; + print!("{}", output); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_args_output_field() { + let test_branch = Branch { + full_name: "feature/123-test".to_string(), + action: "feature".to_string(), + id: "123".to_string(), + description: "test".to_string(), + }; + + // Test each output field + let cases = vec![ + ("FullName", "feature/123-test"), + ("Action", "feature"), + ("ID", "123"), + ("LinkID", "#123"), + ("Description", "test"), + ]; + + for (field, expected) in cases { + let output = match field { + "FullName" => test_branch.full_name.clone(), + "Action" => test_branch.action.clone(), + "ID" => test_branch.id.clone(), + "LinkID" => format!("#{}", test_branch.id), + "Description" => test_branch.description.clone(), + _ => panic!("Invalid test case"), + }; + assert_eq!(output, expected); + } + } + + #[test] + fn test_default_output_field() { + use clap::Parser; + + // Parse empty args (no --output specified) + let args = Args::try_parse_from(["buranko"]).unwrap(); + + // Check that default value is "ID" + assert_eq!(args.output, "ID"); + } + + #[test] + fn test_verbose_output() { + let test_branch = Branch { + full_name: "feature/123-test".to_string(), + action: "feature".to_string(), + id: "123".to_string(), + description: "test".to_string(), + }; + + let expected = "Full Name: feature/123-test\n\ + Action: feature\n\ + ID: 123\n\ + Description: test"; + + assert_eq!(format_verbose_output(&test_branch), expected); + } +}