From 6f4f2e216bfc62b8bff99df945a3d9e32a862a30 Mon Sep 17 00:00:00 2001 From: Alberto De Bortoli Date: Fri, 12 Jun 2026 17:07:32 +0100 Subject: [PATCH] Handle no-TTY gracefully in post-checkout hook GUI Git clients (Fork, Sourcetree, Xcode, VS Code, etc.) run post-checkout hooks without a TTY, so sudo cannot prompt for a password when install.sh needs to write to /usr/local/bin. Previously this caused the hook to fail with a cryptic sudo error. The fix is surgical: only the Luca self-update path (curl | bash install.sh) requires sudo and therefore needs a TTY. The luca install step that syncs tools from the Lucafile does not require elevated privileges and should still run regardless. Behaviour with no TTY: - Luca absent: log a clear warning and exit 0 (nothing more can be done without the binary) - Version mismatch: log a warning naming the installed and required versions, then fall through to `luca install` so Lucafile tools are still synced using whatever version is currently installed Behaviour with a TTY: unchanged. The check uses `[ ! -t 0 ]` (stdin is not a terminal) rather than stdout, since sudo reads the password from stdin. Tests updated to reflect that bats runs without a TTY (so the no-TTY path is the natural path exercised). The old "luca absent: curl called to install" test was asserting behaviour that cannot run in a headless test environment; it is replaced by assertions on the no-TTY skip output. A new test confirms that a version mismatch with no TTY still proceeds to `luca install`. README diagrams for the post-checkout hook (flowchart and sequence diagram) updated to show the TTY decision point and its two outcomes. --- README.md | 38 ++++++++++++++++++++++----------- post-checkout | 45 ++++++++++++++++++++++++++-------------- tests/post_checkout.bats | 26 +++++++++++++++++++---- 3 files changed, 77 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 9cf5917..4f37e51 100644 --- a/README.md +++ b/README.md @@ -190,13 +190,21 @@ flowchart TD FindRoot --> CheckLucafile{Lucafile exists
in repo root?} CheckLucafile -->|No| Exit2([Exit: no Lucafile]) - CheckLucafile -->|Yes| CheckLuca{luca command
available?} + CheckLucafile -->|Yes| CheckLuca{luca installed &
version correct?} - CheckLuca -->|No| LogNotFound["Log: not found, installing"] - CheckLuca -->|Yes| CheckVersion{".luca-version"
matches installed?} + CheckLuca -->|Yes| RunInstall + CheckLuca -->|No| CheckTTY{Interactive
terminal?} - CheckVersion -->|Yes or no file| RunInstall - CheckVersion -->|No| LogMismatch["Log: version mismatch
(installed vs required)"] + CheckTTY -->|"No TTY (e.g. GUI Git client)"| LucaMissing{luca
installed?} + LucaMissing -->|No| WarnNoTTYAbsent["⚠️ Non-interactive: luca absent
skipping install — run from terminal"] + LucaMissing -->|"Yes, wrong version"| WarnNoTTYMismatch["⚠️ Non-interactive: version mismatch
skipping self-update — run from terminal"] + + WarnNoTTYAbsent --> Exit3([Exit: skip — no binary available]) + WarnNoTTYMismatch --> RunInstall + + CheckTTY -->|TTY present| LucaMissing2{luca
installed?} + LucaMissing2 -->|No| LogNotFound["Log: not found, installing"] + LucaMissing2 -->|"Yes, wrong version"| LogMismatch["Log: version mismatch
(installed vs required)"] LogNotFound --> Download[Download & run
install.sh] LogMismatch --> Download @@ -211,16 +219,16 @@ flowchart TD CheckResult -->|Yes| Success[✅ Tools synchronized] CheckResult -->|No| Warning[⚠️ Some tools may
have failed] - Success --> NotifyPath - Warning --> NotifyPath - - NotifyPath["ℹ️ PATH will update
on next prompt"] - NotifyPath --> Exit3([Exit: done]) + Success --> Exit4([Exit: done]) + Warning --> Exit4 style Exit1 fill:#FFE4B5 style Exit2 fill:#FFE4B5 + style Exit3 fill:#FFE4B5 style ErrorExit fill:#FFB6C1 style Success fill:#90EE90 + style WarnNoTTYAbsent fill:#FFE4B5 + style WarnNoTTYMismatch fill:#FFE4B5 ``` --- @@ -336,8 +344,14 @@ sequenceDiagram G->>PC: Trigger post-checkout PC->>PC: Check for Lucafile PC->>PC: Check luca version vs .luca-version - PC->>IS: Run install.sh if missing or version mismatch - IS-->>PC: Done + alt TTY present and update needed + PC->>IS: Run install.sh + IS-->>PC: Done + else No TTY and luca absent + PC->>PC: ⚠️ Skip — non-interactive, no binary + else No TTY and version mismatch + PC->>PC: ⚠️ Skip self-update — non-interactive + end PC->>L: luca install --quiet L->>L: Install/update tools L-->>PC: Done diff --git a/post-checkout b/post-checkout index 164d77b..8c852cf 100755 --- a/post-checkout +++ b/post-checkout @@ -129,27 +129,40 @@ log_info "Found $FOUND_SPEC, synchronizing tools..." # Check if Luca is installed and version is correct, install/update if not if ! is_luca_installed || ! is_luca_version_correct; then - if ! is_luca_installed; then - log_info "Luca not found, installing..." + if [ ! -t 0 ]; then + # No TTY — sudo cannot prompt for a password (e.g. GUI Git client). + # Skip the self-update and fall through to luca install below. + if ! is_luca_installed; then + log_warning "Non-interactive environment detected (e.g. GUI Git client). Luca is not installed — skipping automatic installation. Please run the setup from a terminal." + exit 0 + else + REQUIRED=$(cat "$VERSION_FILE") + INSTALLED=$(luca --version 2>/dev/null) + log_warning "Non-interactive environment detected (e.g. GUI Git client). Luca version mismatch (installed: $INSTALLED, required: $REQUIRED) — skipping automatic update. Please run the setup from a terminal to apply the correct version." + fi else - REQUIRED=$(cat "$VERSION_FILE") - INSTALLED=$(luca --version 2>/dev/null) - log_info "Luca version mismatch (installed: $INSTALLED, required: $REQUIRED), updating..." - fi + if ! is_luca_installed; then + log_info "Luca not found, installing..." + else + REQUIRED=$(cat "$VERSION_FILE") + INSTALLED=$(luca --version 2>/dev/null) + log_info "Luca version mismatch (installed: $INSTALLED, required: $REQUIRED), updating..." + fi - if command -v curl >/dev/null 2>&1; then - /bin/bash -c "$(curl -fsSL $INSTALL_SCRIPT_URL)" - INSTALL_RESULT=$? + if command -v curl >/dev/null 2>&1; then + /bin/bash -c "$(curl -fsSL $INSTALL_SCRIPT_URL)" + INSTALL_RESULT=$? - if [ $INSTALL_RESULT -ne 0 ]; then - log_error "Failed to install Luca" + if [ $INSTALL_RESULT -ne 0 ]; then + log_error "Failed to install Luca" + exit 1 + fi + + log_success "Luca installed" + else + log_error "curl is required to install Luca" exit 1 fi - - log_success "Luca installed" - else - log_error "curl is required to install Luca" - exit 1 fi fi diff --git a/tests/post_checkout.bats b/tests/post_checkout.bats index 55e0096..1284932 100644 --- a/tests/post_checkout.bats +++ b/tests/post_checkout.bats @@ -86,7 +86,7 @@ setup() { fi } -@test "luca absent: curl called to install when luca not in PATH" { +@test "luca absent, no-tty: skips installation with informative message and exits 0" { cp "$FIXTURE_DIR/Lucafile" "$FAKE_REPO/Lucafile" # Build a PATH that has git/curl mocks but NOT luca local no_luca_bin="$BATS_TEST_TMPDIR/no_luca_bin" @@ -95,11 +95,15 @@ setup() { ln -sf "$TESTS_DIR/test_helper/mocks/$mock" "$no_luca_bin/$mock" done - # Intentionally omit /usr/local/bin to ensure real luca is not found + # Intentionally omit /usr/local/bin to ensure real luca is not found. + # bats runs without a TTY so the non-interactive path is exercised. run env PATH="$no_luca_bin:/usr/bin:/bin" \ "$REPO_ROOT/post-checkout" prev_ref new_ref 1 - assert_output --partial "installing" + assert_success + assert_output --partial "Non-interactive environment" + assert_output --partial "skipping automatic installation" + assert_output --partial "terminal" } # --------------------------------------------------------------------------- @@ -142,15 +146,29 @@ setup() { refute_output --partial "installing" } -@test "version check: luca version mismatch triggers reinstall with mismatch message" { +@test "version check, no-tty: version mismatch shows warning with installed and required versions" { cp "$FIXTURE_DIR/Lucafile" "$FAKE_REPO/Lucafile" echo "v2.0.0" > "$FAKE_REPO/.luca-version" # differs from MOCK_LUCA_VERSION (v1.0.0) run "$REPO_ROOT/post-checkout" prev_ref new_ref 1 + assert_output --partial "Non-interactive environment" assert_output --partial "version mismatch" assert_output --partial "v1.0.0" assert_output --partial "v2.0.0" + assert_output --partial "terminal" +} + +@test "version check, no-tty: version mismatch still runs luca install" { + cp "$FIXTURE_DIR/Lucafile" "$FAKE_REPO/Lucafile" + echo "v2.0.0" > "$FAKE_REPO/.luca-version" # differs from MOCK_LUCA_VERSION (v1.0.0) + + run "$REPO_ROOT/post-checkout" prev_ref new_ref 1 + + assert_success + assert_output --partial "Tools synchronized successfully" + run grep "luca install --quiet --no-install-post-checkout-git-hook" "$MOCK_LUCA_CALL_LOG" + assert_success } @test "version check: no .luca-version file skips version check" {