diff --git a/.tool-versions b/.tool-versions index d73a169..f4eb891 100644 --- a/.tool-versions +++ b/.tool-versions @@ -6,3 +6,4 @@ copilot-cli 1.0.69 squad-cli 0.10.0 gh 2.92.0 delta 0.19.2 +lazygit 0.63.0 diff --git a/scripts/linux/tools/lazygit.sh b/scripts/linux/tools/lazygit.sh new file mode 100644 index 0000000..d751391 --- /dev/null +++ b/scripts/linux/tools/lazygit.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# scripts/linux/tools/lazygit.sh -- Install lazygit (terminal UI for git) at pinned version +# +# Called by: scripts/linux/setup.sh +# Idempotent: yes -- version-aware; upgrades if installed version != pinned version. +# Opt-in: NOT in DEFAULT_TOOLS; only runs when requested via --only=lazygit. +# +# macOS: brew install lazygit (warns if brew version differs from pin). +# Linux: downloads the pinned release tarball from GitHub releases. +# Asset naming: lazygit__linux_x86_64.tar.gz (no leading v; lowercase os/arch). +# Binary is at the archive root -- extract and install to ~/.local/bin. +# Note: Ubuntu PPA lags significantly; tarball is primary for version accuracy. + +# shellcheck disable=SC1091 +. "$(dirname "${BASH_SOURCE[0]}")/../lib/log.sh" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LG_VERSION="$(sh "${SCRIPT_DIR}/../../lib/read-tool-version.sh" lazygit)" + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + set -euo pipefail + + # Detect installed version + INSTALLED_VERSION="" + if command -v lazygit &>/dev/null; then + INSTALLED_VERSION="$(lazygit --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" + fi + + if [ "${INSTALLED_VERSION}" = "${LG_VERSION}" ]; then + log_ok "lazygit already at pinned version ${LG_VERSION}" + exit 0 + fi + + if [ -n "${INSTALLED_VERSION}" ]; then + log_info "lazygit ${INSTALLED_VERSION} installed; upgrading to pinned ${LG_VERSION}..." + else + log_info "Installing lazygit ${LG_VERSION}..." + fi + + PLATFORM="$(uname -s)" + if [[ "$PLATFORM" == "Darwin" ]]; then + # Homebrew: versioned formulae for lazygit are not reliably pinnable. + if command -v lazygit &>/dev/null; then + brew upgrade lazygit || true + else + brew install lazygit + fi + ACTUAL_VERSION="$(lazygit --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo 'unknown')" + if [ "${ACTUAL_VERSION}" != "${LG_VERSION}" ]; then + log_warn "lazygit ${ACTUAL_VERSION} installed (pinned: ${LG_VERSION}); brew cannot guarantee exact version" + else + log_ok "lazygit installed at ${LG_VERSION}" + fi + else + # Linux: tarball from GitHub releases (asset names use lowercase os/arch, no leading v). + ARCH="$(uname -m)" + case "$ARCH" in + x86_64) ARCH_SUFFIX="x86_64" ;; + aarch64|arm64) ARCH_SUFFIX="arm64" ;; + *) log_error "Unsupported architecture: ${ARCH}"; exit 1 ;; + esac + + TARBALL="lazygit_${LG_VERSION}_linux_${ARCH_SUFFIX}.tar.gz" + TARBALL_URL="https://github.com/jesseduffield/lazygit/releases/download/v${LG_VERSION}/${TARBALL}" + INSTALL_DIR="${HOME}/.local/bin" + WORK_DIR="${HOME}/.local/share/dev-setup-install/lazygit" + + mkdir -p "$INSTALL_DIR" + mkdir -p "$WORK_DIR" + + log_info "Downloading ${TARBALL}..." + curl -fsSL "$TARBALL_URL" -o "${WORK_DIR}/${TARBALL}" + # Binary is at the archive root (no version-named subdirectory). + tar -xzf "${WORK_DIR}/${TARBALL}" -C "$WORK_DIR" + cp "${WORK_DIR}/lazygit" "${INSTALL_DIR}/lazygit" + chmod +x "${INSTALL_DIR}/lazygit" + rm -rf "$WORK_DIR" + + log_ok "lazygit ${LG_VERSION} installed to ${INSTALL_DIR}/lazygit" + fi +fi diff --git a/scripts/windows/setup.ps1 b/scripts/windows/setup.ps1 index c5bac3d..4634ce8 100644 --- a/scripts/windows/setup.ps1 +++ b/scripts/windows/setup.ps1 @@ -47,6 +47,7 @@ $ErrorActionPreference = 'Stop' . "$PSScriptRoot\tools\auth.ps1" . "$PSScriptRoot\tools\git-hook.ps1" . "$PSScriptRoot\tools\delta.ps1" +. "$PSScriptRoot\tools\lazygit.ps1" # --------------------------------------------------------------------------- # $DefaultTools -- single ordered source of truth for a no-arg default run. @@ -87,6 +88,7 @@ $ToolRegistry = [ordered]@{ 'profile' = { Write-PowerShellProfile } 'git-hook' = { Install-GitHook } 'delta' = { Install-Delta } + 'lazygit' = { Install-Lazygit } } # --------------------------------------------------------------------------- diff --git a/scripts/windows/tools/lazygit.ps1 b/scripts/windows/tools/lazygit.ps1 new file mode 100644 index 0000000..80ccfe1 --- /dev/null +++ b/scripts/windows/tools/lazygit.ps1 @@ -0,0 +1,53 @@ +# scripts/windows/tools/lazygit.ps1 - lazygit installer +# +# Installs lazygit at pinned version from .tool-versions. +# Opt-in: NOT in $DefaultTools; only runs when requested via -Only 'lazygit'. +# winget preferred (id JesseDuffield.lazygit); scoop fallback when winget unavailable. +# PS 5.1 ASCII-only: no smart quotes, em-dashes, or non-ASCII characters. + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. "$PSScriptRoot\..\lib\logging.ps1" +. "$PSScriptRoot\..\lib\path.ps1" +. "$PSScriptRoot\..\..\lib\Read-ToolVersion.ps1" + +function Install-Lazygit { + $LgVersion = Get-ToolVersion -Name 'lazygit' + + # Detect installed version + $InstalledVersion = '' + if (Get-Command lazygit -ErrorAction SilentlyContinue) { + $raw = (lazygit --version 2>&1) | Select-Object -First 1 | Out-String + $m = [regex]::Match($raw, '[0-9]+\.[0-9]+\.[0-9]+') + if ($m.Success) { $InstalledVersion = $m.Value } + } + + if ($InstalledVersion -eq $LgVersion) { + Write-Ok "lazygit already at pinned version $LgVersion" + return + } + + if ($InstalledVersion) { + Write-Info "lazygit $InstalledVersion installed; upgrading to pinned $LgVersion..." + } else { + Write-Info "Installing lazygit $LgVersion..." + } + + # winget preferred; fall back to scoop if winget is unavailable + if (Get-Command winget -ErrorAction SilentlyContinue) { + winget install --id JesseDuffield.lazygit --version $LgVersion --silent ` + --accept-source-agreements --accept-package-agreements + Assert-LastExit -ToolName "lazygit" -AllowedExitCodes @(0, -1978335189) + Refresh-SessionPath + Write-Ok "lazygit installed via winget at $LgVersion" + } elseif (Get-Command scoop -ErrorAction SilentlyContinue) { + Write-Info "winget not available; falling back to scoop..." + scoop install lazygit + Assert-LastExit -ToolName "lazygit (scoop)" + Write-Warn "scoop installed latest lazygit; version may differ from pinned $LgVersion" + } else { + Write-Err "Neither winget nor scoop available; cannot install lazygit" + throw "lazygit install failed: no supported package manager found" + } +} diff --git a/tests/fixtures/stub-tools/linux/lazygit.sh b/tests/fixtures/stub-tools/linux/lazygit.sh new file mode 100644 index 0000000..159cb70 --- /dev/null +++ b/tests/fixtures/stub-tools/linux/lazygit.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "lazygit" >> "$RUN_LOG" diff --git a/tests/fixtures/stub-tools/windows/lazygit.ps1 b/tests/fixtures/stub-tools/windows/lazygit.ps1 new file mode 100644 index 0000000..350414a --- /dev/null +++ b/tests/fixtures/stub-tools/windows/lazygit.ps1 @@ -0,0 +1,4 @@ +# stub: lazygit +param() +$n = 'lazygit' +if ($env:RUN_LOG) { Add-Content -Path $env:RUN_LOG -Value $n } diff --git a/tests/test_lazygit_installer.sh b/tests/test_lazygit_installer.sh new file mode 100644 index 0000000..d2f74af --- /dev/null +++ b/tests/test_lazygit_installer.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# tests/test_lazygit_installer.sh -- parity tests for lazygit opt-in installer (#467) +# +# Tests: +# T_lazygit_in_list -- lazygit appears in --list output (opt-in discoverable) +# T_lazygit_not_default -- lazygit is NOT installed by a default no-arg run +# T_lazygit_version_pin -- .tool-versions contains a lazygit pin +# T_lazygit_optin_stub -- --only=lazygit with stub dir runs only lazygit +# +# Usage: bash tests/test_lazygit_installer.sh +# Requires: bash 3.2+ (macOS compatible) + +set -uo pipefail + +PASS=0 +FAIL=0 +SKIP=0 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RESET='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +LINUX_SETUP="${REPO_ROOT}/scripts/linux/setup.sh" +TOOL_VERSIONS="${REPO_ROOT}/.tool-versions" +STUB_DIR="${REPO_ROOT}/tests/fixtures/stub-tools/linux" + +pass() { printf "${GREEN}PASS${RESET}: %s\n" "$1"; PASS=$((PASS + 1)); } +fail() { printf "${RED}FAIL${RESET}: %s\n" "$1"; FAIL=$((FAIL + 1)); } +# shellcheck disable=SC2329 +skip() { printf "${YELLOW}SKIP${RESET}: %s -- %s\n" "$1" "$2"; SKIP=$((SKIP + 1)); } + +# --------------------------------------------------------------------------- +# T_lazygit_in_list: --list on real tools dir includes 'lazygit' +# Fails RED when scripts/linux/tools/lazygit.sh does not exist. +# --------------------------------------------------------------------------- +echo "" +echo "--- T_lazygit_in_list ---" +list_out="$(bash "$LINUX_SETUP" --list 2>&1)" && list_exit=$? || list_exit=$? +if [ "$list_exit" -ne 0 ]; then + fail "T_lazygit_in_list: --list exited $list_exit (expected 0)" +elif echo "$list_out" | grep -qF "lazygit"; then + pass "T_lazygit_in_list: lazygit appears in --list output (opt-in discoverable)" +else + fail "T_lazygit_in_list: lazygit missing from --list output (is lazygit.sh in tools/?)" + echo " --list output: $list_out" +fi + +# --------------------------------------------------------------------------- +# T_lazygit_not_default: default no-arg run with stub dir does NOT run lazygit +# lazygit is opt-in; it must not appear in defaults.txt. +# --------------------------------------------------------------------------- +echo "" +echo "--- T_lazygit_not_default ---" +RUN_LOG="$(mktemp)" +export RUN_LOG +bash "$LINUX_SETUP" "--tools-dir=${STUB_DIR}" >/dev/null 2>&1 || true +if grep -qF "lazygit" "$RUN_LOG" 2>/dev/null; then + fail "T_lazygit_not_default: lazygit ran in a default no-arg install (must be opt-in only)" +else + pass "T_lazygit_not_default: lazygit does NOT run in a default install (correctly opt-in)" +fi +rm -f "$RUN_LOG" +unset RUN_LOG + +# --------------------------------------------------------------------------- +# T_lazygit_version_pin: .tool-versions contains a lazygit entry +# Fails RED before lazygit is added to .tool-versions. +# --------------------------------------------------------------------------- +echo "" +echo "--- T_lazygit_version_pin ---" +if grep -qE '^lazygit[[:space:]]' "$TOOL_VERSIONS" 2>/dev/null; then + ver="$(grep -E '^lazygit[[:space:]]' "$TOOL_VERSIONS" | awk '{print $2}')" + pass "T_lazygit_version_pin: lazygit pinned at ${ver} in .tool-versions" +else + fail "T_lazygit_version_pin: lazygit not found in .tool-versions -- RED (pre-implementation)" +fi + +# --------------------------------------------------------------------------- +# T_lazygit_optin_stub: --only=lazygit with stub dir runs only lazygit +# Fails RED when tests/fixtures/stub-tools/linux/lazygit.sh does not exist. +# --------------------------------------------------------------------------- +echo "" +echo "--- T_lazygit_optin_stub ---" +RUN_LOG2="$(mktemp)" +export RUN_LOG +RUN_LOG="$RUN_LOG2" +bash "$LINUX_SETUP" "--tools-dir=${STUB_DIR}" --only=lazygit 2>&1 | grep -q . || true +if grep -qF "lazygit" "$RUN_LOG2" 2>/dev/null; then + actual="$(cat "$RUN_LOG2")" + if [ "$actual" = "lazygit" ]; then + pass "T_lazygit_optin_stub: --only=lazygit runs only lazygit via stub" + else + fail "T_lazygit_optin_stub: --only=lazygit ran unexpected tools: ${actual}" + fi +else + fail "T_lazygit_optin_stub: --only=lazygit did not run lazygit (stub missing or tool unregistered)" +fi +rm -f "$RUN_LOG2" +unset RUN_LOG + +# --------------------------------------------------------------------------- +echo "" +echo "Results: ${PASS} passed, ${FAIL} failed, ${SKIP} skipped" +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi +exit 0 diff --git a/tests/test_lazygit_installer_pwsh.ps1 b/tests/test_lazygit_installer_pwsh.ps1 new file mode 100644 index 0000000..1d74dc9 --- /dev/null +++ b/tests/test_lazygit_installer_pwsh.ps1 @@ -0,0 +1,116 @@ +# tests/test_lazygit_installer_pwsh.ps1 -- parity tests for lazygit opt-in installer (#467) +# +# Tests: +# T_lazygit_in_list -- lazygit appears in -List output (opt-in discoverable) +# T_lazygit_not_default -- lazygit is NOT installed by a default no-arg run +# T_lazygit_version_pin -- .tool-versions contains a lazygit pin +# T_lazygit_optin_stub -- -Only 'lazygit' with stub dir runs only lazygit +# +# Usage: powershell -ExecutionPolicy Bypass -File tests\test_lazygit_installer_pwsh.ps1 +# PS 5.1 ASCII-only: no smart quotes, em-dashes, or non-ASCII characters. + +$ErrorActionPreference = 'Stop' +$TestsPassed = 0 +$TestsFailed = 0 +$TestsSkipped = 0 + +$RepoRoot = Split-Path $PSScriptRoot -Parent +$WinSetup = Join-Path $RepoRoot 'scripts\windows\setup.ps1' +$StubDir = Join-Path $RepoRoot 'tests\fixtures\stub-tools\windows' +$ToolVersions = Join-Path $RepoRoot '.tool-versions' + +function Test-Scenario { + param([string]$Name, [scriptblock]$Test) + Write-Host "`n=== TEST: $Name ===" -ForegroundColor Cyan + try { + & $Test + Write-Host "[PASS] $Name" -ForegroundColor Green + $script:TestsPassed++ + } + catch { + Write-Host "[FAIL] $Name" -ForegroundColor Red + Write-Host " Error: $_" -ForegroundColor Red + $script:TestsFailed++ + } +} + +# --------------------------------------------------------------------------- +# T_lazygit_in_list: -List on real registry includes 'lazygit' +# Fails RED when lazygit.ps1 is not dot-sourced + registered in setup.ps1. +# --------------------------------------------------------------------------- + +Test-Scenario "T_lazygit_in_list: lazygit appears in -List output (opt-in discoverable)" { + $out = powershell -NoProfile -ExecutionPolicy Bypass -File $WinSetup -List 2>&1 | Out-String + $ec = $LASTEXITCODE + if ($ec -ne 0) { throw "-List exited $ec (expected 0)" } + if ($out -notmatch '\blazygit\b') { + throw "lazygit missing from -List output; has lazygit.ps1 been registered in ToolRegistry?" + } +} + +# --------------------------------------------------------------------------- +# T_lazygit_not_default: default no-arg run with stub dir does NOT run lazygit +# --------------------------------------------------------------------------- + +Test-Scenario "T_lazygit_not_default: lazygit does NOT run in a default install (opt-in only)" { + $runLog = [System.IO.Path]::GetTempFileName() + $env:RUN_LOG = $runLog + try { + powershell -NoProfile -ExecutionPolicy Bypass -File $WinSetup ` + -ToolsDir $StubDir 2>&1 | Out-Null + $content = Get-Content $runLog -ErrorAction SilentlyContinue + if ($content -contains 'lazygit') { + throw "lazygit ran in a default no-arg install (must be opt-in only)" + } + } + finally { + $env:RUN_LOG = $null + Remove-Item $runLog -ErrorAction SilentlyContinue + } +} + +# --------------------------------------------------------------------------- +# T_lazygit_version_pin: .tool-versions contains a lazygit entry +# Fails RED before lazygit is added to .tool-versions. +# --------------------------------------------------------------------------- + +Test-Scenario "T_lazygit_version_pin: lazygit is pinned in .tool-versions" { + $lines = Get-Content $ToolVersions + $entry = $lines | Where-Object { $_ -match '^lazygit\s+' } + if (-not $entry) { + throw "lazygit not found in .tool-versions -- RED (pre-implementation)" + } + $ver = ($entry -split '\s+')[1] + Write-Host " lazygit pinned at $ver" -ForegroundColor DarkGray +} + +# --------------------------------------------------------------------------- +# T_lazygit_optin_stub: -Only 'lazygit' with stub dir runs only lazygit +# Fails RED when lazygit.ps1 stub is not in the stub dir OR not in ToolRegistry. +# --------------------------------------------------------------------------- + +Test-Scenario "T_lazygit_optin_stub: -Only 'lazygit' runs only lazygit via stub" { + $runLog = [System.IO.Path]::GetTempFileName() + $env:RUN_LOG = $runLog + try { + powershell -NoProfile -ExecutionPolicy Bypass -File $WinSetup ` + -ToolsDir $StubDir -Only 'lazygit' 2>&1 | Out-Null + $content = Get-Content $runLog -ErrorAction SilentlyContinue + if (-not $content) { throw "-Only 'lazygit' produced empty run-log" } + if ($content -notcontains 'lazygit') { + throw "-Only 'lazygit' did not run lazygit stub; log: $($content -join ', ')" + } + $extra = $content | Where-Object { $_ -ne 'lazygit' } + if ($extra) { throw "-Only 'lazygit' ran unexpected tools: $($extra -join ', ')" } + } + finally { + $env:RUN_LOG = $null + Remove-Item $runLog -ErrorAction SilentlyContinue + } +} + +# --------------------------------------------------------------------------- +Write-Host "" +Write-Host "Results: $TestsPassed passed, $TestsFailed failed, $TestsSkipped skipped" +if ($TestsFailed -gt 0) { exit 1 } +exit 0