diff --git a/.github/workflows/aur-git.yml b/.github/workflows/aur-git.yml new file mode 100644 index 0000000..418c34b --- /dev/null +++ b/.github/workflows/aur-git.yml @@ -0,0 +1,61 @@ +name: Publish AUR Git Package + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + publish-aur-git: + runs-on: ubuntu-latest + container: archlinux:base-devel + + steps: + - name: Install packaging tools + run: pacman -Sy --noconfirm --needed gettext git openssh + + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create build user + run: | + useradd --create-home --shell /bin/bash builder + chown -R builder:builder "$GITHUB_WORKSPACE" + + - name: Configure AUR SSH + env: + AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + run: | + test -n "${AUR_SSH_PRIVATE_KEY}" + install -d -m 700 /home/builder/.ssh + printf '%s\n' "${AUR_SSH_PRIVATE_KEY}" > /home/builder/.ssh/aur + chmod 600 /home/builder/.ssh/aur + ssh-keyscan -H aur.archlinux.org >> /home/builder/.ssh/known_hosts + cat <<'EOF' > /home/builder/.ssh/config + Host aur.archlinux.org + User aur + IdentityFile ~/.ssh/aur + EOF + chmod 600 /home/builder/.ssh/config + chown -R builder:builder /home/builder/.ssh + + - name: Compute ghfetch-rs-git version + id: meta + run: | + pkgver="$(su builder -c "cd '$GITHUB_WORKSPACE' && scripts/compute-git-pkgver.sh .")" + echo "pkgver=${pkgver}" >> "$GITHUB_OUTPUT" + + - name: Render ghfetch-rs-git + run: | + su builder -c "cd '$GITHUB_WORKSPACE' && \ + scripts/render-aur-package.sh ghfetch-rs-git /tmp/ghfetch-rs-git \ + --pkgver '${{ steps.meta.outputs.pkgver }}'" + + - name: Publish ghfetch-rs-git + run: | + su builder -c "cd '$GITHUB_WORKSPACE' && \ + scripts/publish-aur.sh ghfetch-rs-git /tmp/ghfetch-rs-git \ + 'chore: update ghfetch-rs-git to ${{ steps.meta.outputs.pkgver }}'" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ed2665..f5877a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,3 +28,25 @@ jobs: - name: Test run: cargo test + + aur-metadata: + runs-on: ubuntu-latest + container: archlinux:base-devel + + steps: + - name: Install packaging tools + run: pacman -Sy --noconfirm --needed gettext git + + - name: Check out code + uses: actions/checkout@v4 + + - name: Create build user + run: | + useradd --create-home --shell /bin/bash builder + chown -R builder:builder "$GITHUB_WORKSPACE" + + - name: Render AUR package metadata + run: | + su builder -c "cd '$GITHUB_WORKSPACE' && \ + scripts/render-aur-package.sh ghfetch-rs-git /tmp/ghfetch-rs-git --pkgver 0.1.0.r1.gabcdef0 && \ + scripts/render-aur-package.sh ghfetch-rs-bin /tmp/ghfetch-rs-bin --pkgver 0.1.0 --sha256 0000000000000000000000000000000000000000000000000000000000000000" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..80954e9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,105 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.meta.outputs.version }} + archive_name: ${{ steps.meta.outputs.archive_name }} + archive_sha256: ${{ steps.meta.outputs.archive_sha256 }} + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust artifacts + uses: Swatinem/rust-cache@v2 + + - name: Validate tag version + run: | + tag_version="${GITHUB_REF_NAME#v}" + cargo_version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n1)" + test "${tag_version}" = "${cargo_version}" + + - name: Run tests + run: cargo test --locked + + - name: Build release archive + id: meta + run: | + version="${GITHUB_REF_NAME#v}" + archive_path="$(scripts/build-release-archive.sh "${version}" dist)" + archive_name="$(basename "${archive_path}")" + archive_sha256="$(cut -d' ' -f1 "${archive_path}.sha256")" + + { + echo "version=${version}" + echo "archive_name=${archive_name}" + echo "archive_sha256=${archive_sha256}" + } >> "$GITHUB_OUTPUT" + + - name: Publish GitHub release + uses: softprops/action-gh-release@v2 + with: + files: | + dist/${{ steps.meta.outputs.archive_name }} + dist/${{ steps.meta.outputs.archive_name }}.sha256 + generate_release_notes: true + + publish-aur-bin: + runs-on: ubuntu-latest + container: archlinux:base-devel + needs: release + + steps: + - name: Install packaging tools + run: pacman -Sy --noconfirm --needed gettext git openssh + + - name: Check out code + uses: actions/checkout@v4 + + - name: Create build user + run: | + useradd --create-home --shell /bin/bash builder + chown -R builder:builder "$GITHUB_WORKSPACE" + + - name: Configure AUR SSH + env: + AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + run: | + test -n "${AUR_SSH_PRIVATE_KEY}" + install -d -m 700 /home/builder/.ssh + printf '%s\n' "${AUR_SSH_PRIVATE_KEY}" > /home/builder/.ssh/aur + chmod 600 /home/builder/.ssh/aur + ssh-keyscan -H aur.archlinux.org >> /home/builder/.ssh/known_hosts + cat <<'EOF' > /home/builder/.ssh/config + Host aur.archlinux.org + User aur + IdentityFile ~/.ssh/aur + EOF + chmod 600 /home/builder/.ssh/config + chown -R builder:builder /home/builder/.ssh + + - name: Render ghfetch-rs-bin + run: | + su builder -c "cd '$GITHUB_WORKSPACE' && \ + scripts/render-aur-package.sh ghfetch-rs-bin /tmp/ghfetch-rs-bin \ + --pkgver '${{ needs.release.outputs.version }}' \ + --sha256 '${{ needs.release.outputs.archive_sha256 }}'" + + - name: Publish ghfetch-rs-bin + run: | + su builder -c "cd '$GITHUB_WORKSPACE' && \ + scripts/publish-aur.sh ghfetch-rs-bin /tmp/ghfetch-rs-bin \ + 'chore: update ghfetch-rs-bin to ${{ needs.release.outputs.version }}'" diff --git a/README.md b/README.md index 3ac5811..a36d7c7 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,13 @@ GitHub stats in the terminal, neofetch-style. cargo install ghfetch ``` +### From the AUR + +```bash +paru -S ghfetch-rs-bin +paru -S ghfetch-rs-git +``` + ### From source ```bash @@ -50,6 +57,7 @@ Unauthenticated mode still works for public data, but GitHub rate limits are muc ghfetch octocat ghfetch user octocat --all ghfetch repo rust-lang/rust +ghfetch repo https://github.com/rust-lang/rust ghfetch org rust-lang --languages ghfetch octocat --json ghfetch repo rust-lang/rust --theme latte @@ -59,7 +67,7 @@ ghfetch repo rust-lang/rust --theme latte - `ghfetch [username]` - `ghfetch user ` -- `ghfetch repo ` +- `ghfetch repo ` - `ghfetch org ` ### Common flags @@ -72,7 +80,7 @@ ghfetch repo rust-lang/rust --theme latte ## Notes - `ghfetch user ` shows a compact summary by default. Use `--all` to include every section, or specific section flags like `--repos` or `--languages`. -- `ghfetch org ` and `ghfetch repo ` show language summaries by default. +- `ghfetch org ` and `ghfetch repo ` show language summaries by default. - Detailed language mode (`--languages`) prints a wider table instead of the card view. ## Development @@ -83,6 +91,32 @@ cargo clippy --all-targets --all-features -- -D warnings cargo test ``` +## Release Automation + +This repository ships two AUR packages: + +- `ghfetch-rs-bin` for tagged GitHub release artifacts +- `ghfetch-rs-git` for the live `main` branch + +The package names are suffixed with `-rs` because `ghfetch` is already taken in the AUR, but both packages still install the `ghfetch` executable. + +### GitHub Actions flow + +- Pushing a `vX.Y.Z` tag builds `ghfetch-X.Y.Z-x86_64-unknown-linux-gnu.tar.gz`, publishes a GitHub Release, and updates `ghfetch-rs-bin` +- Pushing to `main` refreshes `ghfetch-rs-git` +- CI also renders both AUR package definitions to catch metadata regressions early + +### Required repository secret + +Add `AUR_SSH_PRIVATE_KEY` to the GitHub repository secrets. It should be the private key for the AUR account `notes`, with the matching public key registered in that AUR account. + +The generated AUR commits use: + +- `Jonatan Jonasson` +- `notes@madeingotland.com` + +The AUR templates and publish scripts live under [`packaging/aur`](/home/notes/Projects/ghfetch/packaging/aur) and [`scripts`](/home/notes/Projects/ghfetch/scripts). + ## License MIT. See [LICENSE](LICENSE). diff --git a/packaging/aur/ghfetch-rs-bin/PKGBUILD.in b/packaging/aur/ghfetch-rs-bin/PKGBUILD.in new file mode 100644 index 0000000..439c5ea --- /dev/null +++ b/packaging/aur/ghfetch-rs-bin/PKGBUILD.in @@ -0,0 +1,19 @@ +# Maintainer: ${AUR_MAINTAINER_NAME} <${AUR_MAINTAINER_EMAIL}> + +pkgname=${AUR_PKGNAME} +pkgver=${AUR_PKGVER} +pkgrel=${AUR_PKGREL} +pkgdesc='GitHub stats in the terminal, neofetch-style (prebuilt binary)' +arch=('x86_64') +url='https://github.com/OneNoted/ghfetch' +license=('MIT') +depends=('gcc-libs' 'glibc' 'openssl') +provides=('ghfetch') +conflicts=('ghfetch' 'ghfetch-rs-git') +source=("${AUR_ARCHIVE_NAME}::https://github.com/${AUR_REPO_SLUG}/releases/download/v${AUR_PKGVER}/${AUR_ARCHIVE_NAME}") +sha256sums=('${AUR_ARCHIVE_SHA256}') + +package() { + install -Dm755 "${srcdir}/${AUR_ARCHIVE_ROOT}/ghfetch" "${pkgdir}/usr/bin/ghfetch" + install -Dm644 "${srcdir}/${AUR_ARCHIVE_ROOT}/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +} diff --git a/packaging/aur/ghfetch-rs-git/PKGBUILD.in b/packaging/aur/ghfetch-rs-git/PKGBUILD.in new file mode 100644 index 0000000..8ce7e9c --- /dev/null +++ b/packaging/aur/ghfetch-rs-git/PKGBUILD.in @@ -0,0 +1,54 @@ +# Maintainer: ${AUR_MAINTAINER_NAME} <${AUR_MAINTAINER_EMAIL}> + +pkgname=${AUR_PKGNAME} +pkgver=${AUR_PKGVER} +pkgrel=${AUR_PKGREL} +pkgdesc='GitHub stats in the terminal, neofetch-style (development snapshot)' +arch=('x86_64') +url='https://github.com/OneNoted/ghfetch' +license=('MIT') +depends=('gcc-libs' 'glibc' 'openssl') +makedepends=('git' 'rust') +provides=('ghfetch') +conflicts=('ghfetch' 'ghfetch-rs-bin') +_target='x86_64-unknown-linux-gnu' +source=('ghfetch::git+${AUR_REPO_URL}#branch=main') +sha256sums=('SKIP') + +pkgver() { + cd "${srcdir}/ghfetch" + + local cargo_version + local description + description="$(git describe --long --tags --abbrev=7 --match 'v[0-9]*' 2>/dev/null || true)" + + if [[ -n "${description}" ]]; then + description="${description#v}" + printf '%s\n' "${description}" | sed 's/\([^-]*-g\)/r\1/; s/-/./g' + return + fi + + cargo_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)" + + printf '%s.r%s.g%s\n' \ + "${cargo_version:-0.0.0}" \ + "$(git rev-list --count HEAD)" \ + "$(git rev-parse --short=7 HEAD)" +} + +prepare() { + cd "${srcdir}/ghfetch" + cargo fetch --locked --target "${_target}" +} + +build() { + cd "${srcdir}/ghfetch" + export CARGO_TARGET_DIR=target + cargo build --frozen --release --target "${_target}" +} + +package() { + cd "${srcdir}/ghfetch" + install -Dm755 "target/${_target}/release/ghfetch" "${pkgdir}/usr/bin/ghfetch" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +} diff --git a/scripts/build-release-archive.sh b/scripts/build-release-archive.sh new file mode 100755 index 0000000..73b3662 --- /dev/null +++ b/scripts/build-release-archive.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "usage: $0 " >&2 + exit 1 +fi + +version="$1" +output_dir="$2" +target_triple="${TARGET_TRIPLE:-x86_64-unknown-linux-gnu}" +source_date_epoch="${SOURCE_DATE_EPOCH:-0}" + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +archive_root="ghfetch-${version}-${target_triple}" +archive_path="${output_dir}/${archive_root}.tar.gz" +tar_path="${output_dir}/${archive_root}.tar" + +cd "${repo_root}" + +mkdir -p "${output_dir}" + +cargo build --locked --release --target "${target_triple}" + +staging_dir="$(mktemp -d)" +trap 'rm -rf "${staging_dir}"' EXIT + +install -Dm755 "target/${target_triple}/release/ghfetch" "${staging_dir}/${archive_root}/ghfetch" +install -Dm644 LICENSE "${staging_dir}/${archive_root}/LICENSE" + +tar \ + --sort=name \ + --owner=0 \ + --group=0 \ + --numeric-owner \ + --mtime="@${source_date_epoch}" \ + -cf "${tar_path}" \ + -C "${staging_dir}" \ + "${archive_root}" + +gzip -n -f "${tar_path}" +sha256sum "${archive_path}" > "${archive_path}.sha256" + +printf '%s\n' "${archive_path}" diff --git a/scripts/compute-git-pkgver.sh b/scripts/compute-git-pkgver.sh new file mode 100755 index 0000000..90275cd --- /dev/null +++ b/scripts/compute-git-pkgver.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="${1:-.}" + +cd "${repo_root}" + +description="$(git describe --long --tags --abbrev=7 --match 'v[0-9]*' 2>/dev/null || true)" + +if [[ -n "${description}" ]]; then + description="${description#v}" + printf '%s\n' "${description}" | sed 's/\([^-]*-g\)/r\1/; s/-/./g' + exit 0 +fi + +cargo_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)" + +printf '%s.r%s.g%s\n' \ + "${cargo_version}" \ + "$(git rev-list --count HEAD)" \ + "$(git rev-parse --short=7 HEAD)" diff --git a/scripts/publish-aur.sh b/scripts/publish-aur.sh new file mode 100755 index 0000000..1415a8c --- /dev/null +++ b/scripts/publish-aur.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -lt 2 || $# -gt 3 ]]; then + echo "usage: $0 [commit-message]" >&2 + exit 1 +fi + +package_name="$1" +rendered_dir="$2" +commit_message="${3:-chore: update ${package_name}}" +author_name="${AUR_GIT_AUTHOR_NAME:-Jonatan Jonasson}" +author_email="${AUR_GIT_AUTHOR_EMAIL:-notes@madeingotland.com}" + +if [[ ! -f "${rendered_dir}/PKGBUILD" || ! -f "${rendered_dir}/.SRCINFO" ]]; then + echo "rendered package directory must contain PKGBUILD and .SRCINFO" >&2 + exit 1 +fi + +checkout_dir="$(mktemp -d)" +trap 'rm -rf "${checkout_dir}"' EXIT + +git clone "ssh://aur@aur.archlinux.org/${package_name}.git" "${checkout_dir}" + +find "${checkout_dir}" -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} + + +shopt -s dotglob nullglob +for path in "${rendered_dir}"/* "${rendered_dir}"/.*; do + name="$(basename "${path}")" + case "${name}" in + .|..|*.in) + continue + ;; + esac + cp -a "${path}" "${checkout_dir}/${name}" +done +shopt -u dotglob nullglob + +cd "${checkout_dir}" + +git config user.name "${author_name}" +git config user.email "${author_email}" +git add --all + +if git diff --cached --quiet; then + echo "No AUR changes to publish for ${package_name}" + exit 0 +fi + +git commit -m "${commit_message}" +git push origin HEAD:master diff --git a/scripts/render-aur-package.sh b/scripts/render-aur-package.sh new file mode 100755 index 0000000..be11296 --- /dev/null +++ b/scripts/render-aur-package.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'EOF' >&2 +usage: + render-aur-package.sh ghfetch-rs-git [--pkgver VERSION] [--pkgrel N] + render-aur-package.sh ghfetch-rs-bin --pkgver VERSION --sha256 SHA256 [--pkgrel N] +EOF + exit 1 +} + +if [[ $# -lt 2 ]]; then + usage +fi + +package_name="$1" +output_dir="$2" +shift 2 + +pkgver="" +pkgrel="1" +archive_sha256="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --pkgver) + pkgver="$2" + shift 2 + ;; + --pkgrel) + pkgrel="$2" + shift 2 + ;; + --sha256) + archive_sha256="$2" + shift 2 + ;; + *) + usage + ;; + esac +done + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +template_path="${repo_root}/packaging/aur/${package_name}/PKGBUILD.in" + +if [[ ! -f "${template_path}" ]]; then + echo "unknown package template: ${package_name}" >&2 + exit 1 +fi + +if [[ -z "${pkgver}" ]]; then + if [[ "${package_name}" == "ghfetch-rs-git" ]]; then + pkgver="$("${repo_root}/scripts/compute-git-pkgver.sh" "${repo_root}")" + else + echo "--pkgver is required for ${package_name}" >&2 + exit 1 + fi +fi + +if [[ "${package_name}" == "ghfetch-rs-bin" && -z "${archive_sha256}" ]]; then + echo "--sha256 is required for ghfetch-rs-bin" >&2 + exit 1 +fi + +maintainer_name="${AUR_MAINTAINER_NAME:-Jonatan Jonasson}" +maintainer_email="${AUR_MAINTAINER_EMAIL:-notes@madeingotland.com}" +repo_slug="${AUR_REPO_SLUG:-OneNoted/ghfetch}" +repo_url="${AUR_REPO_URL:-https://github.com/OneNoted/ghfetch.git}" +archive_root="ghfetch-${pkgver}-x86_64-unknown-linux-gnu" +archive_name="${archive_root}.tar.gz" + +mkdir -p "${output_dir}" + +export AUR_MAINTAINER_NAME="${maintainer_name}" +export AUR_MAINTAINER_EMAIL="${maintainer_email}" +export AUR_PKGNAME="${package_name}" +export AUR_PKGVER="${pkgver}" +export AUR_PKGREL="${pkgrel}" +export AUR_REPO_SLUG="${repo_slug}" +export AUR_REPO_URL="${repo_url}" +export AUR_ARCHIVE_ROOT="${archive_root}" +export AUR_ARCHIVE_NAME="${archive_name}" +export AUR_ARCHIVE_SHA256="${archive_sha256}" + +envsubst \ + '${AUR_MAINTAINER_NAME} ${AUR_MAINTAINER_EMAIL} ${AUR_PKGNAME} ${AUR_PKGVER} ${AUR_PKGREL} ${AUR_REPO_SLUG} ${AUR_REPO_URL} ${AUR_ARCHIVE_ROOT} ${AUR_ARCHIVE_NAME} ${AUR_ARCHIVE_SHA256}' \ + < "${template_path}" \ + > "${output_dir}/PKGBUILD" + +( + cd "${output_dir}" + makepkg --printsrcinfo > .SRCINFO +) diff --git a/src/cli.rs b/src/cli.rs index e127253..558f1a2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -48,7 +48,7 @@ pub enum Command { /// Display repository stats Repo { - /// Repository in owner/repo format + /// Repository in owner/repo, GitHub URL, or GitHub SSH format repo: String, #[command(flatten)] diff --git a/src/data/repo.rs b/src/data/repo.rs index f9d1734..766095d 100644 --- a/src/data/repo.rs +++ b/src/data/repo.rs @@ -1,9 +1,12 @@ use crate::api::client::GhClient; use crate::cli::RepoOpts; use crate::data::languages::LanguageBreakdown; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; +use reqwest::Url; use serde::Serialize; +const REPO_INPUT_HELP: &str = "Repository must be in owner/repo, GitHub URL, or GitHub SSH format"; + #[derive(Debug, Serialize)] pub struct RepoProfile { pub name: String, @@ -26,19 +29,85 @@ pub struct RepoProfile { pub languages: Option, } +fn parse_repo_path(repo_path: &str) -> Result<(String, String)> { + let repo_path = repo_path.trim(); + + if repo_path.is_empty() { + bail!("{REPO_INPUT_HELP}"); + } + + if let Some(path) = repo_path + .strip_prefix("git@github.com:") + .or_else(|| repo_path.strip_prefix("git@www.github.com:")) + .or_else(|| repo_path.strip_prefix("github.com/")) + .or_else(|| repo_path.strip_prefix("www.github.com/")) + { + return parse_owner_repo(path); + } + + if repo_path.contains("://") { + return parse_repo_url(repo_path); + } + + parse_owner_repo(repo_path) +} + +fn parse_repo_url(repo_url: &str) -> Result<(String, String)> { + let url = Url::parse(repo_url).context(REPO_INPUT_HELP)?; + let host = url.host_str().context(REPO_INPUT_HELP)?; + + if host != "github.com" && host != "www.github.com" { + bail!("Repository URL must point to github.com"); + } + + let mut segments = url + .path_segments() + .into_iter() + .flatten() + .filter(|segment| !segment.is_empty()); + let owner = segments.next().context(REPO_INPUT_HELP)?; + let repo = segments.next().context(REPO_INPUT_HELP)?; + + Ok((owner.to_string(), strip_git_suffix(repo).to_string())) +} + +fn parse_owner_repo(repo_path: &str) -> Result<(String, String)> { + let parts = repo_path + .trim() + .trim_matches('/') + .split('/') + .filter(|segment| !segment.is_empty()) + .collect::>(); + + if parts.len() != 2 { + bail!("{REPO_INPUT_HELP}"); + } + + let owner = parts[0]; + let repo = strip_git_suffix(parts[1]); + + if owner.is_empty() || repo.is_empty() { + bail!("{REPO_INPUT_HELP}"); + } + + Ok((owner.to_string(), repo.to_string())) +} + +fn strip_git_suffix(repo: &str) -> &str { + repo.strip_suffix(".git").unwrap_or(repo) +} + pub async fn fetch_repo_profile( client: &GhClient, repo_path: &str, opts: &RepoOpts, ) -> Result { - let (owner, repo) = repo_path - .split_once('/') - .context("Repository must be in owner/repo format")?; + let (owner, repo) = parse_repo_path(repo_path)?; - let detail = client.get_repo_detail(owner, repo).await?; + let detail = client.get_repo_detail(&owner, &repo).await?; let languages = if opts.show_languages() { - let lang_bytes = client.get_repo_languages(owner, repo).await?; + let lang_bytes = client.get_repo_languages(&owner, &repo).await?; let lang_map = std::collections::HashMap::from([( repo.to_string(), lang_bytes.into_iter().collect::>(), @@ -72,3 +141,38 @@ pub async fn fetch_repo_profile( languages, }) } + +#[cfg(test)] +mod tests { + use super::parse_repo_path; + + #[test] + fn accepts_owner_repo_path() { + let repo = parse_repo_path("lazyvim/lazyvim").unwrap(); + assert_eq!(repo, ("lazyvim".to_string(), "lazyvim".to_string())); + } + + #[test] + fn accepts_github_repo_url() { + let repo = parse_repo_path("https://github.com/lazyvim/lazyvim").unwrap(); + assert_eq!(repo, ("lazyvim".to_string(), "lazyvim".to_string())); + } + + #[test] + fn accepts_github_ssh_remote() { + let repo = parse_repo_path("git@github.com:LazyVim/LazyVim.git").unwrap(); + assert_eq!(repo, ("LazyVim".to_string(), "LazyVim".to_string())); + } + + #[test] + fn accepts_ssh_url_with_extra_path_segments() { + let repo = parse_repo_path("ssh://git@github.com/lazyvim/lazyvim.git/tree/main").unwrap(); + assert_eq!(repo, ("lazyvim".to_string(), "lazyvim".to_string())); + } + + #[test] + fn rejects_non_github_repo_url() { + let err = parse_repo_path("https://gitlab.com/lazyvim/lazyvim").unwrap_err(); + assert_eq!(err.to_string(), "Repository URL must point to github.com"); + } +}