From c23104f2ee4d01b04829920e60865ff1de060339 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 9 Jun 2025 23:13:04 +0300 Subject: [PATCH 1/4] add claude.md --- CLAUDE.md | 211 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..824abff --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,211 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**git-undo** is a CLI tool that provides a universal "Ctrl+Z" for Git commands. It tracks every mutating Git operation and can reverse them with a single `git undo` command. + +## Essential Commands + +### Development +```bash +make build # Compile binary with version info to ./build/git-undo +make test # Run unit tests +make test-all # Run unit tests + integration tests (dev mode) +make integration-test-dev # BATS integration tests (test current changes) +make integration-test-prod # BATS integration tests (test user experience) +make lint # Run golangci-lint (auto-installs if needed) +make tidy # Format, vet, and tidy Go modules +``` + +### Installation & Management +```bash +make install # Full installation (binary + shell hooks) +make uninstall # Complete removal +./install.sh # Direct installation script +``` + +### Testing Specific Components +```bash +go test ./internal/app # Test main application logic +go test ./internal/githelpers # Test git command parsing +go test ./internal/git-undo/logging # Test command logging +go test -v ./internal/app -run TestSequentialUndo # Run specific failing test +``` + +## Core Architecture + +### Component Relationships +``` +User Git Command → Shell Hook (captures command) ┐ + ↓ ├→ Deduplication Logic → git-undo --hook → Logger → .git/git-undo/commands + Git Operation → Git Hook (post-operation) ┘ ↓ + │ +User runs `git undo` → App.Run() → Undoer Factory → Command-specific Undoer → Git Execution │ + ↓ │ + Logger.ToggleEntry() ←────────────────────┘ +``` + +### Key Packages + +**`/internal/app/`** - Central orchestrator +- `app.go`: Main application logic, command routing, self-management +- Handles: `git undo`, `git undo undo` (redo), `--verbose`, `--dry-run`, `--log` +- Self-management: `git undo self update/uninstall/version` + +**`/internal/git-undo/undoer/`** - Undo command generation +- Interface-based design with factory pattern +- Command-specific implementations: `add.go`, `commit.go`, `merge.go`, `branch.go`, `stash.go`, `checkout.go` +- Each undoer analyzes git state and generates appropriate inverse commands + +**`/internal/git-undo/logging/`** - Command tracking system +- Logs format: `TIMESTAMP|REF|COMMAND` in `.git/git-undo/commands` +- Supports marking entries as undone with `#` prefix +- Deduplication between shell hooks and git hooks using flag files and command identifiers +- Per-repository, per-branch tracking + +**`/internal/githelpers/`** - Git integration layer +- `gitcommand.go`: Command parsing with `github.com/mattn/go-shellwords` +- `git_reference.go`: Command classification (porcelain/plumbing, read-only/mutating) +- `githelper.go`: Git execution wrapper with proper error handling + +### Dual Hook System Architecture: Shell + Git Hooks + +**git-undo** uses a sophisticated dual hook system that combines shell hooks and git hooks to capture git operations from different contexts: + +#### Shell Hooks (Primary) +- **Bash**: Uses `DEBUG` trap + `PROMPT_COMMAND` to capture command-line git operations +- **Zsh**: Uses `preexec` + `precmd` hooks +- **Coverage**: Captures user-typed commands with exact flags/arguments (e.g., `git add file1.txt file2.txt`) +- **Advantage**: Preserves original command context for precise undo operations + +#### Git Hooks (Secondary/Fallback) +- **Hooks**: `post-commit` and `post-merge` via `/scripts/git-undo-git-hook.sh` +- **Coverage**: Captures git operations that bypass shell (IDEs, scripts, git commands run by other tools) +- **Command Reconstruction**: Since hooks run after the fact, commands are reconstructed from git state: + - `post-commit`: Extracts commit message → `git commit -m "message"` + - `post-merge`: Detects merge type → `git merge --squash/--no-ff/--ff` + +#### Deduplication Strategy +**Problem**: Both hooks often fire for the same operation, risking duplicate logging. + +**Solution**: Smart deduplication via command normalization and timing: + +1. **Command Normalization**: Both hooks normalize commands to canonical form + - `git commit -m "test" --verbose` → `git commit -m "test"` + - `git merge feature --no-ff` → `git merge --no-ff feature` + - Handles variations in flag order, quotes, and extra flags + +2. **Timing + Hashing**: Creates SHA1 identifier from `normalized_command + git_ref + truncated_timestamp` + - 2-second time window for duplicate detection + - Git hook runs first, marks command as logged via flag file + - Shell hook checks flag file, skips if already logged + +3. **Hook Priority**: **Git hook wins** when both detect the same operation + - Git hooks are more reliable for detecting actual git state changes + - Shell hooks can capture commands that don't change state (failed commands) + +#### When Each Hook System Is Useful + +**Shell Hooks Excel At:** +- `git add` operations (exact file lists preserved) +- Commands with complex flag combinations +- Failed commands that still need tracking for user context +- Interactive git operations with user input + +**Git Hooks Excel At:** +- IDE-triggered commits/merges (VS Code, IntelliJ, etc.) +- Script-automated git operations +- Git operations from other tools (CI/CD, deployment scripts) +- Operations that bypass shell entirely + +#### Installation Process +1. **Binary**: Installs to `$(go env GOPATH)/bin` +2. **Git Hooks**: + - Sets global `core.hooksPath` to `~/.config/git-undo/hooks/` OR + - Integrates with existing hooks by appending to existing hook files + - Copies dispatcher script (`git-undo-git-hook.sh`) to hooks directory + - Creates `post-commit` and `post-merge` hooks (symlinks preferred, fallback to standalone scripts) +3. **Shell Hooks**: Places in `~/.config/git-undo/` and sources from shell rc files + +## Testing Strategy + +### Unit Tests +- Uses `github.com/stretchr/testify` with suite pattern +- `testutil.GitTestSuite`: Base suite with git repository setup/teardown +- Mock interfaces for git operations to enable isolated testing +- Export pattern: `export_test.go` files expose internal functions for testing + +### Integration Tests +- **BATS Framework**: Bash Automated Testing System +- **Dev Mode** (`--dev`): Tests current working directory changes +- **Prod Mode** (`--prod`): Tests real user installation experience +- Real git repository creation and cleanup +- End-to-end workflow verification + +### Current Test Issues +- `TestSequentialUndo` failing on both main and feature branches (see `todo.md`) +- Test expects alternating commit/add undo sequence but fails on second undo + +## Command Support & Undo Logic + +### Supported Commands +- **commit** → `git reset --soft HEAD~1` (preserves staged changes) +- **add** → `git restore --staged ` or `git reset ` +- **branch** → `git branch -d ` +- **checkout -b** → `git branch -d ` +- **stash** → `git stash pop` + cleanup +- **merge** → `git reset --merge ORIG_HEAD` + +### Undo Command Generation +- Context-aware: checks HEAD existence, merge state, tags +- Handles edge cases: initial commits, amended commits, tagged commits +- Provides warnings for potentially destructive operations +- Uses git state analysis to determine appropriate reset strategy + +## Build System & Versioning + +### Version Management +- Uses `./scripts/pseudo_version.sh` for development builds +- Build-time version injection via `-ldflags "-X main.version=$(VERSION)"` +- Priority: git tags → build-time version → "unknown" + +### Dependencies +- **Runtime**: Only `github.com/mattn/go-shellwords` for safe command parsing +- **Testing**: `github.com/stretchr/testify` +- **Linting**: `golangci-lint` (auto-installed via Makefile) + +## Important Implementation Details + +### Command Logging Format +``` +2025-01-09 14:30:45|main|git commit -m "test message" +#2025-01-09 14:25:30|main|git add file.txt # undoed entry (prefixed with #) +``` + +### Hook Detection & Environment Variables +- **Git Hook Detection**: + - Primary: `GIT_UNDO_GIT_HOOK_MARKER=1` (set by git hook script) + - Secondary: `GIT_HOOK_NAME` (contains hook name like "post-commit") + - Fallback: `GIT_DIR` environment variable presence +- **Shell Hook Detection**: `GIT_UNDO_INTERNAL_HOOK=1` (set by shell hooks) +- **Flag Files**: `.git/git-undo/.git-hook-` for marking git hook execution + +### Command Normalization Details +- **Supported Commands**: `commit`, `merge`, `rebase`, `cherry-pick` +- **commit**: Extracts `-m message` and `--amend`, ignores flags like `--verbose`, `--signoff` +- **merge**: Extracts merge strategy (`--squash`, `--no-ff`, `--ff`) and branch name +- **Purpose**: Ensures equivalent commands generate identical identifiers for deduplication + +### Error Handling Patterns +- Graceful degradation when not in git repository +- Panic recovery in main application loop +- Git command validation before execution +- Comprehensive error wrapping with context + +### Security Considerations +- Safe command parsing with `go-shellwords` +- Git command validation against known command whitelist +- No arbitrary command execution +- Proper file permissions for hook installation \ No newline at end of file From e9f9ebdc7f321fa29e8e74b1d79b711bfbbc026f Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 9 Jun 2025 23:30:00 +0300 Subject: [PATCH 2/4] refactor: scripts/src files to be separated --- install.sh | 2 +- scripts/build.sh | 8 ++++---- scripts/{ => src}/install.src.sh | 0 scripts/{ => src}/uninstall.src.sh | 0 scripts/{ => src}/update.src.sh | 0 uninstall.sh | 2 +- update.sh | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) rename scripts/{ => src}/install.src.sh (100%) rename scripts/{ => src}/uninstall.src.sh (100%) rename scripts/{ => src}/update.src.sh (100%) diff --git a/install.sh b/install.sh index 15ad1d6..0b74f23 100755 --- a/install.sh +++ b/install.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This file is auto-generated by scripts/build.sh -# DO NOT EDIT - modify scripts/*.src.sh instead and run 'make buildscripts' +# DO NOT EDIT - modify scripts/src/*.src.sh instead and run 'make buildscripts' # ── Embedded hook files ── that's a base64 of scripts/git-undo-hook.bash ──── EMBEDDED_BASH_HOOK='IyBWYXJpYWJsZSB0byBzdG9yZSB0aGUgZ2l0IGNvbW1hbmQgdGVtcG9yYXJpbHkKR0lUX0NPTU1BTkRfVE9fTE9HPSIiCgojIEZ1bmN0aW9uIHRvIHN0b3JlIHRoZSBnaXQgY29tbWFuZCB0ZW1wb3JhcmlseQpzdG9yZV9naXRfY29tbWFuZCgpIHsKICBsb2NhbCByYXdfY21kPSIkMSIKICBsb2NhbCBoZWFkPSR7cmF3X2NtZCUlICp9CiAgbG9jYWwgcmVzdD0ke3Jhd19jbWQjIiRoZWFkIn0KCiAgIyBDaGVjayBpZiB0aGUgY29tbWFuZCBpcyBhbiBhbGlhcyBhbmQgZXhwYW5kIGl0CiAgaWYgYWxpYXMgIiRoZWFkIiAmPi9kZXYvbnVsbDsgdGhlbgogICAgbG9jYWwgZGVmPSQoYWxpYXMgIiRoZWFkIikKICAgICMgRXh0cmFjdCB0aGUgZXhwYW5zaW9uIGZyb20gYWxpYXMgb3V0cHV0IChmb3JtYXQ6IGFsaWFzIG5hbWU9J2V4cGFuc2lvbicpCiAgICBsb2NhbCBleHBhbnNpb249JHtkZWYjKlwnfQogICAgZXhwYW5zaW9uPSR7ZXhwYW5zaW9uJVwnfQogICAgcmF3X2NtZD0iJHtleHBhbnNpb259JHtyZXN0fSIKICBmaQoKICAjIE9ubHkgc3RvcmUgaWYgaXQncyBhIGdpdCBjb21tYW5kCiAgW1sgIiRyYXdfY21kIiA9PSBnaXRcICogXV0gfHwgcmV0dXJuCiAgR0lUX0NPTU1BTkRfVE9fTE9HPSIkcmF3X2NtZCIKfQoKIyBGdW5jdGlvbiB0byBsb2cgdGhlIGNvbW1hbmQgb25seSBpZiBpdCB3YXMgc3VjY2Vzc2Z1bApsb2dfc3VjY2Vzc2Z1bF9naXRfY29tbWFuZCgpIHsKICAjIENoZWNrIGlmIHdlIGhhdmUgYSBnaXQgY29tbWFuZCB0byBsb2cgYW5kIGlmIHRoZSBwcmV2aW91cyBjb21tYW5kIHdhcyBzdWNjZXNzZnVsCiAgaWYgW1sgLW4gIiRHSVRfQ09NTUFORF9UT19MT0ciICYmICQ/IC1lcSAwIF1dOyB0aGVuCiAgICBHSVRfVU5ET19JTlRFUk5BTF9IT09LPTEgY29tbWFuZCBnaXQtdW5kbyAtLWhvb2s9IiRHSVRfQ09NTUFORF9UT19MT0ciCiAgZmkKICAjIENsZWFyIHRoZSBzdG9yZWQgY29tbWFuZAogIEdJVF9DT01NQU5EX1RPX0xPRz0iIgp9CgojIHRyYXAgZG9lcyB0aGUgYWN0dWFsIGhvb2tpbmc6IG1ha2luZyBhbiBleHRyYSBnaXQtdW5kbyBjYWxsIGZvciBldmVyeSBnaXQgY29tbWFuZC4KdHJhcCAnc3RvcmVfZ2l0X2NvbW1hbmQgIiRCQVNIX0NPTU1BTkQiJyBERUJVRwoKIyBTZXQgdXAgUFJPTVBUX0NPTU1BTkQgdG8gbG9nIHN1Y2Nlc3NmdWwgY29tbWFuZHMgYWZ0ZXIgZXhlY3V0aW9uCmlmIFtbIC16ICIkUFJPTVBUX0NPTU1BTkQiIF1dOyB0aGVuCiAgUFJPTVBUX0NPTU1BTkQ9ImxvZ19zdWNjZXNzZnVsX2dpdF9jb21tYW5kIgplbHNlCiAgUFJPTVBUX0NPTU1BTkQ9IiRQUk9NUFRfQ09NTUFORDsgbG9nX3N1Y2Nlc3NmdWxfZ2l0X2NvbW1hbmQiCmZp' diff --git a/scripts/build.sh b/scripts/build.sh index 0deb053..775e5ef 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -8,9 +8,9 @@ COMMON_FILE="$SCRIPT_DIR/common.sh" BASH_HOOK_FILE="$SCRIPT_DIR/git-undo-hook.bash" BASH_TEST_HOOK_FILE="$SCRIPT_DIR/git-undo-hook.test.bash" ZSH_HOOK_FILE="$SCRIPT_DIR/git-undo-hook.zsh" -SRC_INSTALL="$SCRIPT_DIR/install.src.sh" -SRC_UNINSTALL="$SCRIPT_DIR/uninstall.src.sh" -SRC_UPDATE="$SCRIPT_DIR/update.src.sh" +SRC_INSTALL="$SCRIPT_DIR/src/install.src.sh" +SRC_UNINSTALL="$SCRIPT_DIR/src/uninstall.src.sh" +SRC_UPDATE="$SCRIPT_DIR/src/update.src.sh" OUT_INSTALL="$SCRIPT_DIR/../install.sh" OUT_UNINSTALL="$SCRIPT_DIR/../uninstall.sh" OUT_UPDATE="$SCRIPT_DIR/../update.sh" @@ -36,7 +36,7 @@ build_script() { cat > "$out_file" << 'EOF' #!/usr/bin/env bash # This file is auto-generated by scripts/build.sh -# DO NOT EDIT - modify scripts/*.src.sh instead and run 'make buildscripts' +# DO NOT EDIT - modify scripts/src/*.src.sh instead and run 'make buildscripts' EOF # If building install.sh, add embedded hook files diff --git a/scripts/install.src.sh b/scripts/src/install.src.sh similarity index 100% rename from scripts/install.src.sh rename to scripts/src/install.src.sh diff --git a/scripts/uninstall.src.sh b/scripts/src/uninstall.src.sh similarity index 100% rename from scripts/uninstall.src.sh rename to scripts/src/uninstall.src.sh diff --git a/scripts/update.src.sh b/scripts/src/update.src.sh similarity index 100% rename from scripts/update.src.sh rename to scripts/src/update.src.sh diff --git a/uninstall.sh b/uninstall.sh index 886e6dd..1b52797 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This file is auto-generated by scripts/build.sh -# DO NOT EDIT - modify scripts/*.src.sh instead and run 'make buildscripts' +# DO NOT EDIT - modify scripts/src/*.src.sh instead and run 'make buildscripts' set -euo pipefail # ── Inlined content from common.sh ────────────────────────────────────────── diff --git a/update.sh b/update.sh index 9167a56..673360c 100755 --- a/update.sh +++ b/update.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This file is auto-generated by scripts/build.sh -# DO NOT EDIT - modify scripts/*.src.sh instead and run 'make buildscripts' +# DO NOT EDIT - modify scripts/src/*.src.sh instead and run 'make buildscripts' set -e # ── Inlined content from common.sh ────────────────────────────────────────── From d7a10a26be1cf91b9f0c693b4f74edabd503e759 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 10 Jun 2025 00:15:15 +0300 Subject: [PATCH 3/4] refactor: improve shell script quality and organization - Fix all shellcheck warnings (SC2155, SC2129, SC2015, SC1091, SC2034) - Separate variable declaration and assignment to avoid masking return values - Group multiple file redirects using brace syntax for better performance - Replace ambiguous A && B || C patterns with explicit if-then statements - Add shellcheck disable directives for unavoidable warnings - Export or properly handle unused variables - Reorganize scripts directory structure for better separation of concerns - Move colors.sh and common.sh to scripts/src/ alongside template files - Update all path references in build.sh and source templates - Refactor _get_script_dir function to handle new common.sh location - Standardize color variable usage by removing duplicate RESET/NC variables - Remove RESET variable and use NC consistently across all scripts - Update all template files and logging functions - Add shellcheck task to Makefile as 'make sc' for continuous quality checking - Includes proper error handling to not fail build on warnings - Covers all shell script types (.sh, .bash, .zsh) - Improve zsh hook file compatibility - Add proper shebang to git-undo-hook.zsh - Disable shellcheck for zsh-specific syntax not supported by shellcheck --- Makefile | 6 ++ install.sh | 97 +++++++++++---------- scripts/build.sh | 39 +++++---- scripts/colors.sh | 33 -------- scripts/git-undo-hook.bash | 3 +- scripts/git-undo-hook.test.bash | 3 +- scripts/git-undo-hook.zsh | 5 +- scripts/integration/setup-and-test-dev.sh | 4 +- scripts/integration/setup-and-test-prod.sh | 4 +- scripts/pseudo_version.sh | 2 +- scripts/run-integration.sh | 3 +- scripts/src/colors.sh | 30 +++++++ scripts/{ => src}/common.sh | 36 ++++---- scripts/src/install.src.sh | 40 ++++----- scripts/src/uninstall.src.sh | 37 ++++---- scripts/src/update.src.sh | 47 ++++++----- uninstall.sh | 88 +++++++++++-------- update.sh | 98 ++++++++++++---------- 18 files changed, 321 insertions(+), 254 deletions(-) delete mode 100644 scripts/colors.sh create mode 100644 scripts/src/colors.sh rename scripts/{ => src}/common.sh (90%) diff --git a/Makefile b/Makefile index 0f2569c..6b8019d 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,12 @@ lint-install: lint: lint-install $(shell which golangci-lint) run +# Check shell scripts using shellcheck +.PHONY: sc +sc: + @echo "Running shellcheck on all shell scripts..." + @find scripts/ -name "*.sh" -o -name "*.bash" -o -name "*.zsh" | xargs shellcheck || true + # Install the binary globally with custom version info .PHONY: binary-install binary-install: diff --git a/install.sh b/install.sh index 0b74f23..028d69e 100755 --- a/install.sh +++ b/install.sh @@ -3,45 +3,44 @@ # DO NOT EDIT - modify scripts/src/*.src.sh instead and run 'make buildscripts' # ── Embedded hook files ── that's a base64 of scripts/git-undo-hook.bash ──── -EMBEDDED_BASH_HOOK='IyBWYXJpYWJsZSB0byBzdG9yZSB0aGUgZ2l0IGNvbW1hbmQgdGVtcG9yYXJpbHkKR0lUX0NPTU1BTkRfVE9fTE9HPSIiCgojIEZ1bmN0aW9uIHRvIHN0b3JlIHRoZSBnaXQgY29tbWFuZCB0ZW1wb3JhcmlseQpzdG9yZV9naXRfY29tbWFuZCgpIHsKICBsb2NhbCByYXdfY21kPSIkMSIKICBsb2NhbCBoZWFkPSR7cmF3X2NtZCUlICp9CiAgbG9jYWwgcmVzdD0ke3Jhd19jbWQjIiRoZWFkIn0KCiAgIyBDaGVjayBpZiB0aGUgY29tbWFuZCBpcyBhbiBhbGlhcyBhbmQgZXhwYW5kIGl0CiAgaWYgYWxpYXMgIiRoZWFkIiAmPi9kZXYvbnVsbDsgdGhlbgogICAgbG9jYWwgZGVmPSQoYWxpYXMgIiRoZWFkIikKICAgICMgRXh0cmFjdCB0aGUgZXhwYW5zaW9uIGZyb20gYWxpYXMgb3V0cHV0IChmb3JtYXQ6IGFsaWFzIG5hbWU9J2V4cGFuc2lvbicpCiAgICBsb2NhbCBleHBhbnNpb249JHtkZWYjKlwnfQogICAgZXhwYW5zaW9uPSR7ZXhwYW5zaW9uJVwnfQogICAgcmF3X2NtZD0iJHtleHBhbnNpb259JHtyZXN0fSIKICBmaQoKICAjIE9ubHkgc3RvcmUgaWYgaXQncyBhIGdpdCBjb21tYW5kCiAgW1sgIiRyYXdfY21kIiA9PSBnaXRcICogXV0gfHwgcmV0dXJuCiAgR0lUX0NPTU1BTkRfVE9fTE9HPSIkcmF3X2NtZCIKfQoKIyBGdW5jdGlvbiB0byBsb2cgdGhlIGNvbW1hbmQgb25seSBpZiBpdCB3YXMgc3VjY2Vzc2Z1bApsb2dfc3VjY2Vzc2Z1bF9naXRfY29tbWFuZCgpIHsKICAjIENoZWNrIGlmIHdlIGhhdmUgYSBnaXQgY29tbWFuZCB0byBsb2cgYW5kIGlmIHRoZSBwcmV2aW91cyBjb21tYW5kIHdhcyBzdWNjZXNzZnVsCiAgaWYgW1sgLW4gIiRHSVRfQ09NTUFORF9UT19MT0ciICYmICQ/IC1lcSAwIF1dOyB0aGVuCiAgICBHSVRfVU5ET19JTlRFUk5BTF9IT09LPTEgY29tbWFuZCBnaXQtdW5kbyAtLWhvb2s9IiRHSVRfQ09NTUFORF9UT19MT0ciCiAgZmkKICAjIENsZWFyIHRoZSBzdG9yZWQgY29tbWFuZAogIEdJVF9DT01NQU5EX1RPX0xPRz0iIgp9CgojIHRyYXAgZG9lcyB0aGUgYWN0dWFsIGhvb2tpbmc6IG1ha2luZyBhbiBleHRyYSBnaXQtdW5kbyBjYWxsIGZvciBldmVyeSBnaXQgY29tbWFuZC4KdHJhcCAnc3RvcmVfZ2l0X2NvbW1hbmQgIiRCQVNIX0NPTU1BTkQiJyBERUJVRwoKIyBTZXQgdXAgUFJPTVBUX0NPTU1BTkQgdG8gbG9nIHN1Y2Nlc3NmdWwgY29tbWFuZHMgYWZ0ZXIgZXhlY3V0aW9uCmlmIFtbIC16ICIkUFJPTVBUX0NPTU1BTkQiIF1dOyB0aGVuCiAgUFJPTVBUX0NPTU1BTkQ9ImxvZ19zdWNjZXNzZnVsX2dpdF9jb21tYW5kIgplbHNlCiAgUFJPTVBUX0NPTU1BTkQ9IiRQUk9NUFRfQ09NTUFORDsgbG9nX3N1Y2Nlc3NmdWxfZ2l0X2NvbW1hbmQiCmZp' -EMBEDDED_BASH_TEST_HOOK='IyBWYXJpYWJsZSB0byBzdG9yZSB0aGUgZ2l0IGNvbW1hbmQgdGVtcG9yYXJpbHkKR0lUX0NPTU1BTkRfVE9fTE9HPSIiCgojIEZ1bmN0aW9uIHRvIHN0b3JlIHRoZSBnaXQgY29tbWFuZCB0ZW1wb3JhcmlseQpzdG9yZV9naXRfY29tbWFuZCgpIHsKICBsb2NhbCByYXdfY21kPSIkMSIKICBsb2NhbCBoZWFkPSR7cmF3X2NtZCUlICp9CiAgbG9jYWwgcmVzdD0ke3Jhd19jbWQjIiRoZWFkIn0KCiAgIyBDaGVjayBpZiB0aGUgY29tbWFuZCBpcyBhbiBhbGlhcyBhbmQgZXhwYW5kIGl0CiAgaWYgYWxpYXMgIiRoZWFkIiAmPi9kZXYvbnVsbDsgdGhlbgogICAgbG9jYWwgZGVmPSQoYWxpYXMgIiRoZWFkIikKICAgICMgRXh0cmFjdCB0aGUgZXhwYW5zaW9uIGZyb20gYWxpYXMgb3V0cHV0IChmb3JtYXQ6IGFsaWFzIG5hbWU9J2V4cGFuc2lvbicpCiAgICBsb2NhbCBleHBhbnNpb249JHtkZWYjKlwnfQogICAgZXhwYW5zaW9uPSR7ZXhwYW5zaW9uJVwnfQogICAgcmF3X2NtZD0iJHtleHBhbnNpb259JHtyZXN0fSIKICBmaQoKICAjIE9ubHkgc3RvcmUgaWYgaXQncyBhIGdpdCBjb21tYW5kCiAgW1sgIiRyYXdfY21kIiA9PSBnaXRcICogXV0gfHwgcmV0dXJuCiAgR0lUX0NPTU1BTkRfVE9fTE9HPSIkcmF3X2NtZCIKfQoKIyBGdW5jdGlvbiB0byBsb2cgdGhlIGNvbW1hbmQgb25seSBpZiBpdCB3YXMgc3VjY2Vzc2Z1bApsb2dfc3VjY2Vzc2Z1bF9naXRfY29tbWFuZCgpIHsKICAjIENoZWNrIGlmIHdlIGhhdmUgYSBnaXQgY29tbWFuZCB0byBsb2cgYW5kIGlmIHRoZSBwcmV2aW91cyBjb21tYW5kIHdhcyBzdWNjZXNzZnVsCiAgaWYgW1sgLW4gIiRHSVRfQ09NTUFORF9UT19MT0ciICYmICQ/IC1lcSAwIF1dOyB0aGVuCiAgICBHSVRfVU5ET19JTlRFUk5BTF9IT09LPTEgY29tbWFuZCBnaXQtdW5kbyAtLWhvb2s9IiRHSVRfQ09NTUFORF9UT19MT0ciCiAgZmkKICAjIENsZWFyIHRoZSBzdG9yZWQgY29tbWFuZAogIEdJVF9DT01NQU5EX1RPX0xPRz0iIgp9CgoKIyBUZXN0IG1vZGU6IHByb3ZpZGUgYSBtYW51YWwgd2F5IHRvIGNhcHR1cmUgY29tbWFuZHMKIyBUaGlzIGlzIG9ubHkgdXNlZCBmb3IgaW50ZWdyYXRpb24tdGVzdC5iYXRzLiAKZ2l0KCkgewogICAgY29tbWFuZCBnaXQgIiRAIgogICAgbG9jYWwgZXhpdF9jb2RlPSQ/CiAgICBpZiBbWyAkZXhpdF9jb2RlIC1lcSAwIF1dOyB0aGVuCiAgICAgICAgR0lUX1VORE9fSU5URVJOQUxfSE9PSz0xIGNvbW1hbmQgZ2l0LXVuZG8gLS1ob29rPSJnaXQgJCoiCiAgICBmaQogICAgcmV0dXJuICRleGl0X2NvZGUKfQoKCiMgU2V0IHVwIFBST01QVF9DT01NQU5EIHRvIGxvZyBzdWNjZXNzZnVsIGNvbW1hbmRzIGFmdGVyIGV4ZWN1dGlvbgppZiBbWyAteiAiJFBST01QVF9DT01NQU5EIiBdXTsgdGhlbgogIFBST01QVF9DT01NQU5EPSJsb2dfc3VjY2Vzc2Z1bF9naXRfY29tbWFuZCIKZWxzZQogIFBST01QVF9DT01NQU5EPSIkUFJPTVBUX0NPTU1BTkQ7IGxvZ19zdWNjZXNzZnVsX2dpdF9jb21tYW5kIgpmaQo=' -EMBEDDED_ZSH_HOOK='IyBGdW5jdGlvbiB0byBzdG9yZSB0aGUgZ2l0IGNvbW1hbmQgdGVtcG9yYXJpbHkKc3RvcmVfZ2l0X2NvbW1hbmQoKSB7CiAgbG9jYWwgcmF3X2NtZD0iJDEiCiAgbG9jYWwgaGVhZD0ke3Jhd19jbWQlJSAqfQogIGxvY2FsIHJlc3Q9JHtyYXdfY21kIyIkaGVhZCJ9CiAgaWYgYWxpYXMgIiRoZWFkIiAmPi9kZXYvbnVsbDsgdGhlbgogICAgbG9jYWwgZGVmPSQoYWxpYXMgIiRoZWFkIikKICAgIGxvY2FsIGV4cGFuc2lvbj0ke2RlZiMqXCd9CiAgICBleHBhbnNpb249JHtleHBhbnNpb24lXCd9CiAgICByYXdfY21kPSIke2V4cGFuc2lvbn0ke3Jlc3R9IgogIGZpCiAgW1sgIiRyYXdfY21kIiA9PSBnaXRcICogXV0gfHwgcmV0dXJuCiAgR0lUX0NPTU1BTkRfVE9fTE9HPSIkcmF3X2NtZCIKfQoKIyBGdW5jdGlvbiB0byBsb2cgdGhlIGNvbW1hbmQgb25seSBpZiBpdCB3YXMgc3VjY2Vzc2Z1bApsb2dfc3VjY2Vzc2Z1bF9naXRfY29tbWFuZCgpIHsKICAjIENoZWNrIGlmIHdlIGhhdmUgYSBnaXQgY29tbWFuZCB0byBsb2cgYW5kIGlmIHRoZSBwcmV2aW91cyBjb21tYW5kIHdhcyBzdWNjZXNzZnVsCiAgaWYgW1sgLW4gIiRHSVRfQ09NTUFORF9UT19MT0ciICYmICQ/IC1lcSAwIF1dOyB0aGVuCiAgICBHSVRfVU5ET19JTlRFUk5BTF9IT09LPTEgY29tbWFuZCBnaXQtdW5kbyAtLWhvb2s9IiRHSVRfQ09NTUFORF9UT19MT0ciCiAgZmkKICAjIENsZWFyIHRoZSBzdG9yZWQgY29tbWFuZAogIEdJVF9DT01NQU5EX1RPX0xPRz0iIgp9CgphdXRvbG9hZCAtVSBhZGQtenNoLWhvb2sKYWRkLXpzaC1ob29rIHByZWV4ZWMgc3RvcmVfZ2l0X2NvbW1hbmQKYWRkLXpzaC1ob29rIHByZWNtZCBsb2dfc3VjY2Vzc2Z1bF9naXRfY29tbWFuZAo=' +EMBEDDED_BASH_HOOK='IyBWYXJpYWJsZSB0byBzdG9yZSB0aGUgZ2l0IGNvbW1hbmQgdGVtcG9yYXJpbHkKR0lUX0NPTU1BTkRfVE9fTE9HPSIiCgojIEZ1bmN0aW9uIHRvIHN0b3JlIHRoZSBnaXQgY29tbWFuZCB0ZW1wb3JhcmlseQpzdG9yZV9naXRfY29tbWFuZCgpIHsKICBsb2NhbCByYXdfY21kPSIkMSIKICBsb2NhbCBoZWFkPSR7cmF3X2NtZCUlICp9CiAgbG9jYWwgcmVzdD0ke3Jhd19jbWQjIiRoZWFkIn0KCiAgIyBDaGVjayBpZiB0aGUgY29tbWFuZCBpcyBhbiBhbGlhcyBhbmQgZXhwYW5kIGl0CiAgaWYgYWxpYXMgIiRoZWFkIiAmPi9kZXYvbnVsbDsgdGhlbgogICAgbG9jYWwgZGVmCiAgICBkZWY9JChhbGlhcyAiJGhlYWQiKQogICAgIyBFeHRyYWN0IHRoZSBleHBhbnNpb24gZnJvbSBhbGlhcyBvdXRwdXQgKGZvcm1hdDogYWxpYXMgbmFtZT0nZXhwYW5zaW9uJykKICAgIGxvY2FsIGV4cGFuc2lvbj0ke2RlZiMqXCd9CiAgICBleHBhbnNpb249JHtleHBhbnNpb24lXCd9CiAgICByYXdfY21kPSIke2V4cGFuc2lvbn0ke3Jlc3R9IgogIGZpCgogICMgT25seSBzdG9yZSBpZiBpdCdzIGEgZ2l0IGNvbW1hbmQKICBbWyAiJHJhd19jbWQiID09IGdpdFwgKiBdXSB8fCByZXR1cm4KICBHSVRfQ09NTUFORF9UT19MT0c9IiRyYXdfY21kIgp9CgojIEZ1bmN0aW9uIHRvIGxvZyB0aGUgY29tbWFuZCBvbmx5IGlmIGl0IHdhcyBzdWNjZXNzZnVsCmxvZ19zdWNjZXNzZnVsX2dpdF9jb21tYW5kKCkgewogICMgQ2hlY2sgaWYgd2UgaGF2ZSBhIGdpdCBjb21tYW5kIHRvIGxvZyBhbmQgaWYgdGhlIHByZXZpb3VzIGNvbW1hbmQgd2FzIHN1Y2Nlc3NmdWwKICBpZiBbWyAtbiAiJEdJVF9DT01NQU5EX1RPX0xPRyIgJiYgJD8gLWVxIDAgXV07IHRoZW4KICAgIEdJVF9VTkRPX0lOVEVSTkFMX0hPT0s9MSBjb21tYW5kIGdpdC11bmRvIC0taG9vaz0iJEdJVF9DT01NQU5EX1RPX0xPRyIKICBmaQogICMgQ2xlYXIgdGhlIHN0b3JlZCBjb21tYW5kCiAgR0lUX0NPTU1BTkRfVE9fTE9HPSIiCn0KCiMgdHJhcCBkb2VzIHRoZSBhY3R1YWwgaG9va2luZzogbWFraW5nIGFuIGV4dHJhIGdpdC11bmRvIGNhbGwgZm9yIGV2ZXJ5IGdpdCBjb21tYW5kLgp0cmFwICdzdG9yZV9naXRfY29tbWFuZCAiJEJBU0hfQ09NTUFORCInIERFQlVHCgojIFNldCB1cCBQUk9NUFRfQ09NTUFORCB0byBsb2cgc3VjY2Vzc2Z1bCBjb21tYW5kcyBhZnRlciBleGVjdXRpb24KaWYgW1sgLXogIiRQUk9NUFRfQ09NTUFORCIgXV07IHRoZW4KICBQUk9NUFRfQ09NTUFORD0ibG9nX3N1Y2Nlc3NmdWxfZ2l0X2NvbW1hbmQiCmVsc2UKICBQUk9NUFRfQ09NTUFORD0iJFBST01QVF9DT01NQU5EOyBsb2dfc3VjY2Vzc2Z1bF9naXRfY29tbWFuZCIKZmk=' +EMBEDDED_BASH_TEST_HOOK='IyBWYXJpYWJsZSB0byBzdG9yZSB0aGUgZ2l0IGNvbW1hbmQgdGVtcG9yYXJpbHkKR0lUX0NPTU1BTkRfVE9fTE9HPSIiCgojIEZ1bmN0aW9uIHRvIHN0b3JlIHRoZSBnaXQgY29tbWFuZCB0ZW1wb3JhcmlseQpzdG9yZV9naXRfY29tbWFuZCgpIHsKICBsb2NhbCByYXdfY21kPSIkMSIKICBsb2NhbCBoZWFkPSR7cmF3X2NtZCUlICp9CiAgbG9jYWwgcmVzdD0ke3Jhd19jbWQjIiRoZWFkIn0KCiAgIyBDaGVjayBpZiB0aGUgY29tbWFuZCBpcyBhbiBhbGlhcyBhbmQgZXhwYW5kIGl0CiAgaWYgYWxpYXMgIiRoZWFkIiAmPi9kZXYvbnVsbDsgdGhlbgogICAgbG9jYWwgZGVmCiAgICBkZWY9JChhbGlhcyAiJGhlYWQiKQogICAgIyBFeHRyYWN0IHRoZSBleHBhbnNpb24gZnJvbSBhbGlhcyBvdXRwdXQgKGZvcm1hdDogYWxpYXMgbmFtZT0nZXhwYW5zaW9uJykKICAgIGxvY2FsIGV4cGFuc2lvbj0ke2RlZiMqXCd9CiAgICBleHBhbnNpb249JHtleHBhbnNpb24lXCd9CiAgICByYXdfY21kPSIke2V4cGFuc2lvbn0ke3Jlc3R9IgogIGZpCgogICMgT25seSBzdG9yZSBpZiBpdCdzIGEgZ2l0IGNvbW1hbmQKICBbWyAiJHJhd19jbWQiID09IGdpdFwgKiBdXSB8fCByZXR1cm4KICBHSVRfQ09NTUFORF9UT19MT0c9IiRyYXdfY21kIgp9CgojIEZ1bmN0aW9uIHRvIGxvZyB0aGUgY29tbWFuZCBvbmx5IGlmIGl0IHdhcyBzdWNjZXNzZnVsCmxvZ19zdWNjZXNzZnVsX2dpdF9jb21tYW5kKCkgewogICMgQ2hlY2sgaWYgd2UgaGF2ZSBhIGdpdCBjb21tYW5kIHRvIGxvZyBhbmQgaWYgdGhlIHByZXZpb3VzIGNvbW1hbmQgd2FzIHN1Y2Nlc3NmdWwKICBpZiBbWyAtbiAiJEdJVF9DT01NQU5EX1RPX0xPRyIgJiYgJD8gLWVxIDAgXV07IHRoZW4KICAgIEdJVF9VTkRPX0lOVEVSTkFMX0hPT0s9MSBjb21tYW5kIGdpdC11bmRvIC0taG9vaz0iJEdJVF9DT01NQU5EX1RPX0xPRyIKICBmaQogICMgQ2xlYXIgdGhlIHN0b3JlZCBjb21tYW5kCiAgR0lUX0NPTU1BTkRfVE9fTE9HPSIiCn0KCgojIFRlc3QgbW9kZTogcHJvdmlkZSBhIG1hbnVhbCB3YXkgdG8gY2FwdHVyZSBjb21tYW5kcwojIFRoaXMgaXMgb25seSB1c2VkIGZvciBpbnRlZ3JhdGlvbi10ZXN0LmJhdHMuIApnaXQoKSB7CiAgICBjb21tYW5kIGdpdCAiJEAiCiAgICBsb2NhbCBleGl0X2NvZGU9JD8KICAgIGlmIFtbICRleGl0X2NvZGUgLWVxIDAgXV07IHRoZW4KICAgICAgICBHSVRfVU5ET19JTlRFUk5BTF9IT09LPTEgY29tbWFuZCBnaXQtdW5kbyAtLWhvb2s9ImdpdCAkKiIKICAgIGZpCiAgICByZXR1cm4gJGV4aXRfY29kZQp9CgoKIyBTZXQgdXAgUFJPTVBUX0NPTU1BTkQgdG8gbG9nIHN1Y2Nlc3NmdWwgY29tbWFuZHMgYWZ0ZXIgZXhlY3V0aW9uCmlmIFtbIC16ICIkUFJPTVBUX0NPTU1BTkQiIF1dOyB0aGVuCiAgUFJPTVBUX0NPTU1BTkQ9ImxvZ19zdWNjZXNzZnVsX2dpdF9jb21tYW5kIgplbHNlCiAgUFJPTVBUX0NPTU1BTkQ9IiRQUk9NUFRfQ09NTUFORDsgbG9nX3N1Y2Nlc3NmdWxfZ2l0X2NvbW1hbmQiCmZpCg==' +EMBEDDED_ZSH_HOOK='IyEvdXNyL2Jpbi9lbnYgenNoCiMgc2hlbGxjaGVjayBkaXNhYmxlPWFsbAojIEZ1bmN0aW9uIHRvIHN0b3JlIHRoZSBnaXQgY29tbWFuZCB0ZW1wb3JhcmlseQpzdG9yZV9naXRfY29tbWFuZCgpIHsKICBsb2NhbCByYXdfY21kPSIkMSIKICBsb2NhbCBoZWFkPSR7cmF3X2NtZCUlICp9CiAgbG9jYWwgcmVzdD0ke3Jhd19jbWQjIiRoZWFkIn0KICBpZiBhbGlhcyAiJGhlYWQiICY+L2Rldi9udWxsOyB0aGVuCiAgICBsb2NhbCBkZWYKICAgIGRlZj0kKGFsaWFzICIkaGVhZCIpCiAgICBsb2NhbCBleHBhbnNpb249JHtkZWYjKlwnfQogICAgZXhwYW5zaW9uPSR7ZXhwYW5zaW9uJVwnfQogICAgcmF3X2NtZD0iJHtleHBhbnNpb259JHtyZXN0fSIKICBmaQogIFtbICIkcmF3X2NtZCIgPT0gZ2l0XCAqIF1dIHx8IHJldHVybgogIEdJVF9DT01NQU5EX1RPX0xPRz0iJHJhd19jbWQiCn0KCiMgRnVuY3Rpb24gdG8gbG9nIHRoZSBjb21tYW5kIG9ubHkgaWYgaXQgd2FzIHN1Y2Nlc3NmdWwKbG9nX3N1Y2Nlc3NmdWxfZ2l0X2NvbW1hbmQoKSB7CiAgIyBDaGVjayBpZiB3ZSBoYXZlIGEgZ2l0IGNvbW1hbmQgdG8gbG9nIGFuZCBpZiB0aGUgcHJldmlvdXMgY29tbWFuZCB3YXMgc3VjY2Vzc2Z1bAogIGlmIFtbIC1uICIkR0lUX0NPTU1BTkRfVE9fTE9HIiAmJiAkPyAtZXEgMCBdXTsgdGhlbgogICAgR0lUX1VORE9fSU5URVJOQUxfSE9PSz0xIGNvbW1hbmQgZ2l0LXVuZG8gLS1ob29rPSIkR0lUX0NPTU1BTkRfVE9fTE9HIgogIGZpCiAgIyBDbGVhciB0aGUgc3RvcmVkIGNvbW1hbmQKICBHSVRfQ09NTUFORF9UT19MT0c9IiIKfQoKYXV0b2xvYWQgLVUgYWRkLXpzaC1ob29rCmFkZC16c2gtaG9vayBwcmVleGVjIHN0b3JlX2dpdF9jb21tYW5kCmFkZC16c2gtaG9vayBwcmVjbWQgbG9nX3N1Y2Nlc3NmdWxfZ2l0X2NvbW1hbmQK' # ── End of embedded hook files ────────────────────────────────────────────── set -e +# shellcheck disable=SC1091 # ── Inlined content from common.sh ────────────────────────────────────────── + # Color definitions - shared across all scripts GRAY='\033[90m' GREEN='\033[32m' YELLOW='\033[33m' RED='\033[31m' BLUE='\033[34m' -RESET='\033[0m' - -# Alternative name for compatibility -NC="$RESET" # No Color (used in some scripts) +NC='\033[0m' # No Color # Basic logging functions log() { - echo -e "${GRAY}git-undo:${RESET} $1" + echo -e "${GRAY}git-undo:${NC} $1" } log_info() { - echo -e "${BLUE}[INFO]${RESET} $*" + echo -e "${BLUE}[INFO]${NC} $*" } log_success() { - echo -e "${GREEN}[SUCCESS]${RESET} $*" + echo -e "${GREEN}[SUCCESS]${NC} $*" } log_error() { - echo -e "${RED}[ERROR]${RESET} $*" + echo -e "${RED}[ERROR]${NC} $*" } log_warning() { - echo -e "${YELLOW}[WARNING]${RESET} $*" + echo -e "${YELLOW}[WARNING]${NC} $*" } # _get_script_dir determines the directory of this file (common.sh) in a POSIX-portable way @@ -63,10 +62,13 @@ _get_script_dir() { # physical directory of the file itself local dir dir=$(cd -P -- "$(dirname -- "$src")" && pwd) - # Because of how we store scripts: built executable scripts are in root - # but helpers and sources are in `scripts` dir. - # so always append scripts - printf '%s/scripts' "$dir" + # Since common.sh is now in scripts/src/, we need to go up one level to get scripts/ + # Remove /src suffix if present, otherwise assume we're already in scripts/ + if [[ "$dir" == */src ]]; then + printf '%s' "${dir%/src}" + else + printf '%s' "$dir" + fi } if [[ -n "${BASH_SOURCE[0]:-}" ]]; then # Bash (she-bang path) @@ -78,25 +80,26 @@ unset -f _get_script_dir echo "SCRIPT DIR $SCRIPT_DIR" # Coloring helpers +# shellcheck disable=SC1091 # Git-undo specific configuration BIN_NAME="git-undo" BIN_DIR=$(go env GOBIN 2>/dev/null || true) [[ -z "$BIN_DIR" ]] && BIN_DIR="$(go env GOPATH)/bin" -BIN_PATH="$BIN_DIR/$BIN_NAME" +export BIN_PATH="$BIN_DIR/$BIN_NAME" CFG_DIR="$HOME/.config/git-undo" -BASH_HOOK="$CFG_DIR/git-undo-hook.bash" -ZSH_HOOK="$CFG_DIR/git-undo-hook.zsh" +export BASH_HOOK="$CFG_DIR/git-undo-hook.bash" +export ZSH_HOOK="$CFG_DIR/git-undo-hook.zsh" GIT_HOOKS_DIR="$CFG_DIR/hooks" DISPATCHER_FILE="$GIT_HOOKS_DIR/git-hooks.sh" DISPATCHER_SRC="$SCRIPT_DIR/git-undo-git-hook.sh" REPO_OWNER="amberpixels" REPO_NAME="git-undo" -GITHUB_REPO_URL="github.com/$REPO_OWNER/$REPO_NAME" +export GITHUB_REPO_URL="github.com/$REPO_OWNER/$REPO_NAME" GITHUB_API_URL="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME" -INSTALL_URL="https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/install.sh" +export INSTALL_URL="https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/install.sh" detect_shell() { # Method 1: Check $SHELL environment variable (most reliable for login shell) @@ -154,12 +157,16 @@ version_compare() { version2=${version2#v} # Extract base version (everything before the first dash) - local base1=$(echo "$version1" | cut -d'-' -f1) - local base2=$(echo "$version2" | cut -d'-' -f1) + local base1 + local base2 + base1=$(echo "$version1" | cut -d'-' -f1) + base2=$(echo "$version2" | cut -d'-' -f1) # Convert base versions to comparable format (e.g., 1.2.3 -> 001002003) - local v1=$(echo "$base1" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') - local v2=$(echo "$base2" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') + local v1 + local v2 + v1=$(echo "$base1" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') + v2=$(echo "$base2" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') # Compare base versions first if [[ "$v1" < "$v2" ]]; then @@ -312,6 +319,7 @@ EOF log "Hook installation completed for: $target" return 0 } + # ── End of inlined content ────────────────────────────────────────────────── # Function to write an embedded hook file @@ -405,7 +413,7 @@ main() { local detected_go="go-unknown" if ! command -v go >/dev/null 2>&1; then - echo -e "${GRAY}git-undo:${RESET} 1. Installing Go binary... ${RED}FAILED${RESET} Go not found. ${RED}Go 1.22+ is required to build the binary.${RESET}" + echo -e "${GRAY}git-undo:${NC} 1. Installing Go binary... ${RED}FAILED${NC} Go not found. ${RED}Go 1.22+ is required to build the binary.${NC}" skip_binary=true else # Extract major & minor (works for go1.xx and goX.YY) @@ -416,27 +424,27 @@ main() { detected_go="$ver_raw" if (( ver_major < 1 )) || { (( ver_major == 1 )) && (( ver_minor < 22 )); }; then - echo -e "${GRAY}git-undo:${RESET} 1. Installing Go binary... ${RED}FAILED${RESET} Detected Go ${YELLOW}${ver_raw}${RESET}, but Go ${RED}≥ 1.22${RESET} is required." + echo -e "${GRAY}git-undo:${NC} 1. Installing Go binary... ${RED}FAILED${NC} Detected Go ${YELLOW}${ver_raw}${NC}, but Go ${RED}≥ 1.22${NC} is required." skip_binary=true fi fi if ! $skip_binary; then # 1) Install the binary - echo -en "${GRAY}git-undo:${RESET} 1. Installing Go binary (${BLUE}${detected_go}${RESET}) ..." + echo -en "${GRAY}git-undo:${NC} 1. Installing Go binary (${BLUE}${detected_go}${NC}) ..." # Check if we're in dev mode with local source available if [[ "${GIT_UNDO_DEV_MODE:-}" == "true" && -d "./cmd/git-undo" && -f "./Makefile" ]]; then - echo -e " ${YELLOW}(dev mode)${RESET}" + echo -e " ${YELLOW}(dev mode)${NC}" log "Building from local source using Makefile..." # Use Makefile's binary-install target which has proper version logic if make binary-install &>/dev/null; then # Get the version that was just installed INSTALLED_VERSION=$(git-undo --version 2>/dev/null || echo "unknown") - echo -e "${GRAY}git-undo:${RESET} Binary installed with version: ${BLUE}$INSTALLED_VERSION${RESET}" + echo -e "${GRAY}git-undo:${NC} Binary installed with version: ${BLUE}$INSTALLED_VERSION${NC}" else - echo -e "${GRAY}git-undo:${RESET} ${RED}Failed to build from source using Makefile${RESET}" + echo -e "${GRAY}git-undo:${NC} ${RED}Failed to build from source using Makefile${NC}" exit 1 fi else @@ -444,16 +452,16 @@ main() { if go install "$GITHUB_REPO_URL/cmd/$BIN_NAME@latest" 2>/dev/null; then BIN_PATH=$(command -v git-undo || echo "$BIN_DIR/$BIN_NAME") INSTALLED_VERSION=$(git-undo --version 2>/dev/null || echo "unknown") - echo -e " ${GREEN}OK${RESET} (installed at ${BLUE}${BIN_PATH}${RESET} | version=${BLUE}${INSTALLED_VERSION}${RESET})" + echo -e " ${GREEN}OK${NC} (installed at ${BLUE}${BIN_PATH}${NC} | version=${BLUE}${INSTALLED_VERSION}${NC})" else - echo -e " ${RED}FAILED${RESET}" + echo -e " ${RED}FAILED${NC}" exit 1 fi fi fi # 2) Git hooks integration - echo -en "${GRAY}git-undo:${RESET} 2. Git integration..." + echo -en "${GRAY}git-undo:${NC} 2. Git integration..." current_hooks_path=$(git config --global --get core.hooksPath || echo "") target_hooks_path="$GIT_HOOKS_DIR" @@ -461,45 +469,46 @@ main() { if [[ -z "$current_hooks_path" ]]; then git config --global core.hooksPath "$target_hooks_path" install_dispatcher_into "$target_hooks_path" - echo -e " ${GREEN}OK${RESET} (set core.hooksPath)" + echo -e " ${GREEN}OK${NC} (set core.hooksPath)" elif [[ "$current_hooks_path" == "$target_hooks_path" ]]; then install_dispatcher_into "$target_hooks_path" - echo -e " ${YELLOW}SKIP${RESET} (already configured)" + echo -e " ${YELLOW}SKIP${NC} (already configured)" else install_dispatcher_into "$current_hooks_path" - echo -e " ${YELLOW}SHARED${RESET} (pig-backed on $current_hooks_path)" + echo -e " ${YELLOW}SHARED${NC} (pig-backed on $current_hooks_path)" fi # 3) Shell integration local current_shell current_shell=$(detect_shell) - echo -en "${GRAY}git-undo:${RESET} 3. Shell integration (${BLUE}$current_shell${RESET})..." + echo -en "${GRAY}git-undo:${NC} 3. Shell integration (${BLUE}$current_shell${NC})..." # Temporarily disable set -e to capture non-zero exit codes set +e local hook_output + # shellcheck disable=SC2034 hook_output=$(install_shell_hook "$current_shell" 2>&1) local hook_status=$? set -e case $hook_status in 0) - echo -e " ${GREEN}OK${RESET}" + echo -e " ${GREEN}OK${NC}" ;; 2) - echo -e " ${YELLOW}SKIP${RESET} (already configured)" + echo -e " ${YELLOW}SKIP${NC} (already configured)" ;; *) - echo -e " ${RED}FAILED${RESET}" - log "You can manually source the appropriate hook file from ${YELLOW}$CFG_DIR${RESET}" + echo -e " ${RED}FAILED${NC}" + log "You can manually source the appropriate hook file from ${YELLOW}$CFG_DIR${NC}" exit 1 ;; esac # 3) Final message - log "${GREEN}Installation completed successfully!${RESET}" + log "${GREEN}Installation completed successfully!${NC}" echo -e "" - echo -e "Please restart your shell or run '${YELLOW}source ~/.${current_shell}rc${RESET}' to activate ${BLUE}git-undo${RESET}" + echo -e "Please restart your shell or run '${YELLOW}source ~/.${current_shell}rc${NC}' to activate ${BLUE}git-undo${NC}" } main "$@" diff --git a/scripts/build.sh b/scripts/build.sh index 775e5ef..f53cdf6 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -3,8 +3,8 @@ set -e SCRIPT_DIR="$(dirname "$0")" -COLORS_FILE="$SCRIPT_DIR/colors.sh" -COMMON_FILE="$SCRIPT_DIR/common.sh" +COLORS_FILE="$SCRIPT_DIR/src/colors.sh" +COMMON_FILE="$SCRIPT_DIR/src/common.sh" BASH_HOOK_FILE="$SCRIPT_DIR/git-undo-hook.bash" BASH_TEST_HOOK_FILE="$SCRIPT_DIR/git-undo-hook.test.bash" ZSH_HOOK_FILE="$SCRIPT_DIR/git-undo-hook.zsh" @@ -28,7 +28,8 @@ encode_hook_file() { build_script() { local src_file="$1" local out_file="$2" - local script_name="$(basename "$out_file")" + local script_name + script_name="$(basename "$out_file")" # echo "Building $script_name..." @@ -41,13 +42,15 @@ EOF # If building install.sh, add embedded hook files if [[ "$script_name" == "install.sh" ]]; then - echo "" >> "$out_file" - echo "# ── Embedded hook files ── that's a base64 of scripts/git-undo-hook.bash ────" >> "$out_file" - encode_hook_file "$BASH_HOOK_FILE" "EMBEDDED_BASH_HOOK" >> "$out_file" - encode_hook_file "$BASH_TEST_HOOK_FILE" "EMBEDDED_BASH_TEST_HOOK" >> "$out_file" - encode_hook_file "$ZSH_HOOK_FILE" "EMBEDDED_ZSH_HOOK" >> "$out_file" - echo "# ── End of embedded hook files ──────────────────────────────────────────────" >> "$out_file" - echo "" >> "$out_file" + { + echo "" + echo "# ── Embedded hook files ── that's a base64 of scripts/git-undo-hook.bash ────" + encode_hook_file "$BASH_HOOK_FILE" "EMBEDDED_BASH_HOOK" + encode_hook_file "$BASH_TEST_HOOK_FILE" "EMBEDDED_BASH_TEST_HOOK" + encode_hook_file "$ZSH_HOOK_FILE" "EMBEDDED_ZSH_HOOK" + echo "# ── End of embedded hook files ──────────────────────────────────────────────" + echo "" + } >> "$out_file" fi # Process the source file line by line @@ -59,13 +62,15 @@ EOF # Replace the common.sh source line with actual content if [[ "$line" =~ source.*common\.sh ]]; then - echo "# ── Inlined content from common.sh ──────────────────────────────────────────" >> "$out_file" - - # First inline colors.sh content (without shebang and without sourcing line) - tail -n +2 "$COLORS_FILE" | grep -v '^#!/' >> "$out_file" - tail -n +2 "$COMMON_FILE" | grep -v '^#!/' | grep -v 'source.*colors\.sh' | grep -v '^SCRIPT_DIR=' | grep -v '^#.*Source shared colors' >> "$out_file" - - echo "# ── End of inlined content ──────────────────────────────────────────────────" >> "$out_file" + { + echo "# ── Inlined content from common.sh ──────────────────────────────────────────" + echo "" + # First inline colors.sh content (without shebang and without sourcing line) + tail -n +2 "$COLORS_FILE" | grep -v '^#!/' + tail -n +2 "$COMMON_FILE" | grep -v '^#!/' | grep -v 'source.*colors\.sh' | grep -v '^SCRIPT_DIR=' | grep -v '^#.*Source shared colors' + echo "" + echo "# ── End of inlined content ──────────────────────────────────────────────────" + } >> "$out_file" else echo "$line" >> "$out_file" fi diff --git a/scripts/colors.sh b/scripts/colors.sh deleted file mode 100644 index 93bc1d8..0000000 --- a/scripts/colors.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -# Color definitions - shared across all scripts -GRAY='\033[90m' -GREEN='\033[32m' -YELLOW='\033[33m' -RED='\033[31m' -BLUE='\033[34m' -RESET='\033[0m' - -# Alternative name for compatibility -NC="$RESET" # No Color (used in some scripts) - -# Basic logging functions -log() { - echo -e "${GRAY}git-undo:${RESET} $1" -} - -log_info() { - echo -e "${BLUE}[INFO]${RESET} $*" -} - -log_success() { - echo -e "${GREEN}[SUCCESS]${RESET} $*" -} - -log_error() { - echo -e "${RED}[ERROR]${RESET} $*" -} - -log_warning() { - echo -e "${YELLOW}[WARNING]${RESET} $*" -} \ No newline at end of file diff --git a/scripts/git-undo-hook.bash b/scripts/git-undo-hook.bash index 5529984..7dd9a6f 100644 --- a/scripts/git-undo-hook.bash +++ b/scripts/git-undo-hook.bash @@ -9,7 +9,8 @@ store_git_command() { # Check if the command is an alias and expand it if alias "$head" &>/dev/null; then - local def=$(alias "$head") + local def + def=$(alias "$head") # Extract the expansion from alias output (format: alias name='expansion') local expansion=${def#*\'} expansion=${expansion%\'} diff --git a/scripts/git-undo-hook.test.bash b/scripts/git-undo-hook.test.bash index 60e87e3..c679151 100644 --- a/scripts/git-undo-hook.test.bash +++ b/scripts/git-undo-hook.test.bash @@ -9,7 +9,8 @@ store_git_command() { # Check if the command is an alias and expand it if alias "$head" &>/dev/null; then - local def=$(alias "$head") + local def + def=$(alias "$head") # Extract the expansion from alias output (format: alias name='expansion') local expansion=${def#*\'} expansion=${expansion%\'} diff --git a/scripts/git-undo-hook.zsh b/scripts/git-undo-hook.zsh index d0e5fa3..ba464e0 100644 --- a/scripts/git-undo-hook.zsh +++ b/scripts/git-undo-hook.zsh @@ -1,10 +1,13 @@ +#!/usr/bin/env zsh +# shellcheck disable=all # Function to store the git command temporarily store_git_command() { local raw_cmd="$1" local head=${raw_cmd%% *} local rest=${raw_cmd#"$head"} if alias "$head" &>/dev/null; then - local def=$(alias "$head") + local def + def=$(alias "$head") local expansion=${def#*\'} expansion=${expansion%\'} raw_cmd="${expansion}${rest}" diff --git a/scripts/integration/setup-and-test-dev.sh b/scripts/integration/setup-and-test-dev.sh index 7e10080..c761783 100644 --- a/scripts/integration/setup-and-test-dev.sh +++ b/scripts/integration/setup-and-test-dev.sh @@ -11,7 +11,9 @@ chmod +x install.sh echo "Installation completed, setting up PATH and sourcing shell configuration..." # Ensure Go binary path is in PATH BEFORE sourcing .bashrc (needed for hooks) -export PATH="$(go env GOPATH)/bin:$PATH" +GOPATH_BIN="$(go env GOPATH)/bin" +export PATH="$GOPATH_BIN:$PATH" +# shellcheck disable=SC1090 source ~/.bashrc cd /home/testuser diff --git a/scripts/integration/setup-and-test-prod.sh b/scripts/integration/setup-and-test-prod.sh index 762309b..e4c0127 100644 --- a/scripts/integration/setup-and-test-prod.sh +++ b/scripts/integration/setup-and-test-prod.sh @@ -9,7 +9,9 @@ curl -fsSL https://raw.githubusercontent.com/amberpixels/git-undo/main/install.s echo "Installation completed, setting up PATH and sourcing shell configuration..." # Ensure Go binary path is in PATH BEFORE sourcing .bashrc (needed for hooks) -export PATH="$(go env GOPATH)/bin:$PATH" +GOPATH_BIN="$(go env GOPATH)/bin" +export PATH="$GOPATH_BIN:$PATH" +# shellcheck disable=SC1090 source ~/.bashrc echo "Running integration tests..." diff --git a/scripts/pseudo_version.sh b/scripts/pseudo_version.sh index aea87dd..7cf6a99 100755 --- a/scripts/pseudo_version.sh +++ b/scripts/pseudo_version.sh @@ -2,7 +2,7 @@ set -euo pipefail pseudo_version() { - local tag pkg_ts ts hash dirty base major minor patch next + local tag ts hash dirty base major minor patch next # 1. last semver tag (falls back to v0.0.0) tag=$(git describe --tags --abbrev=0 --match 'v[0-9]*' 2>/dev/null || echo v0.0.0) diff --git a/scripts/run-integration.sh b/scripts/run-integration.sh index 1974d5e..c984e93 100755 --- a/scripts/run-integration.sh +++ b/scripts/run-integration.sh @@ -8,7 +8,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Source shared colors and logging functions -source "$SCRIPT_DIR/colors.sh" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/src/colors.sh" # Parse command line arguments MODE="dev" # Default to dev mode for local testing diff --git a/scripts/src/colors.sh b/scripts/src/colors.sh new file mode 100644 index 0000000..fda621f --- /dev/null +++ b/scripts/src/colors.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# Color definitions - shared across all scripts +GRAY='\033[90m' +GREEN='\033[32m' +YELLOW='\033[33m' +RED='\033[31m' +BLUE='\033[34m' +NC='\033[0m' # No Color + +# Basic logging functions +log() { + echo -e "${GRAY}git-undo:${NC} $1" +} + +log_info() { + echo -e "${BLUE}[INFO]${NC} $*" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $*" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $*" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $*" +} \ No newline at end of file diff --git a/scripts/common.sh b/scripts/src/common.sh similarity index 90% rename from scripts/common.sh rename to scripts/src/common.sh index b355fff..835b67f 100644 --- a/scripts/common.sh +++ b/scripts/src/common.sh @@ -19,10 +19,13 @@ _get_script_dir() { # physical directory of the file itself local dir dir=$(cd -P -- "$(dirname -- "$src")" && pwd) - # Because of how we store scripts: built executable scripts are in root - # but helpers and sources are in `scripts` dir. - # so always append scripts - printf '%s/scripts' "$dir" + # Since common.sh is now in scripts/src/, we need to go up one level to get scripts/ + # Remove /src suffix if present, otherwise assume we're already in scripts/ + if [[ "$dir" == */src ]]; then + printf '%s' "${dir%/src}" + else + printf '%s' "$dir" + fi } if [[ -n "${BASH_SOURCE[0]:-}" ]]; then # Bash (she-bang path) @@ -34,26 +37,27 @@ unset -f _get_script_dir echo "SCRIPT DIR $SCRIPT_DIR" # Coloring helpers -source "$SCRIPT_DIR/colors.sh" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/src/colors.sh" # Git-undo specific configuration BIN_NAME="git-undo" BIN_DIR=$(go env GOBIN 2>/dev/null || true) [[ -z "$BIN_DIR" ]] && BIN_DIR="$(go env GOPATH)/bin" -BIN_PATH="$BIN_DIR/$BIN_NAME" +export BIN_PATH="$BIN_DIR/$BIN_NAME" CFG_DIR="$HOME/.config/git-undo" -BASH_HOOK="$CFG_DIR/git-undo-hook.bash" -ZSH_HOOK="$CFG_DIR/git-undo-hook.zsh" +export BASH_HOOK="$CFG_DIR/git-undo-hook.bash" +export ZSH_HOOK="$CFG_DIR/git-undo-hook.zsh" GIT_HOOKS_DIR="$CFG_DIR/hooks" DISPATCHER_FILE="$GIT_HOOKS_DIR/git-hooks.sh" DISPATCHER_SRC="$SCRIPT_DIR/git-undo-git-hook.sh" REPO_OWNER="amberpixels" REPO_NAME="git-undo" -GITHUB_REPO_URL="github.com/$REPO_OWNER/$REPO_NAME" +export GITHUB_REPO_URL="github.com/$REPO_OWNER/$REPO_NAME" GITHUB_API_URL="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME" -INSTALL_URL="https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/install.sh" +export INSTALL_URL="https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/install.sh" detect_shell() { # Method 1: Check $SHELL environment variable (most reliable for login shell) @@ -111,12 +115,16 @@ version_compare() { version2=${version2#v} # Extract base version (everything before the first dash) - local base1=$(echo "$version1" | cut -d'-' -f1) - local base2=$(echo "$version2" | cut -d'-' -f1) + local base1 + local base2 + base1=$(echo "$version1" | cut -d'-' -f1) + base2=$(echo "$version2" | cut -d'-' -f1) # Convert base versions to comparable format (e.g., 1.2.3 -> 001002003) - local v1=$(echo "$base1" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') - local v2=$(echo "$base2" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') + local v1 + local v2 + v1=$(echo "$base1" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') + v2=$(echo "$base2" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') # Compare base versions first if [[ "$v1" < "$v2" ]]; then diff --git a/scripts/src/install.src.sh b/scripts/src/install.src.sh index f65d539..94b4534 100755 --- a/scripts/src/install.src.sh +++ b/scripts/src/install.src.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -e +# shellcheck disable=SC1091 source "$(dirname "$0")/common.sh" # Function to write an embedded hook file @@ -94,7 +95,7 @@ main() { local detected_go="go-unknown" if ! command -v go >/dev/null 2>&1; then - echo -e "${GRAY}git-undo:${RESET} 1. Installing Go binary... ${RED}FAILED${RESET} Go not found. ${RED}Go 1.22+ is required to build the binary.${RESET}" + echo -e "${GRAY}git-undo:${NC} 1. Installing Go binary... ${RED}FAILED${NC} Go not found. ${RED}Go 1.22+ is required to build the binary.${NC}" skip_binary=true else # Extract major & minor (works for go1.xx and goX.YY) @@ -105,27 +106,27 @@ main() { detected_go="$ver_raw" if (( ver_major < 1 )) || { (( ver_major == 1 )) && (( ver_minor < 22 )); }; then - echo -e "${GRAY}git-undo:${RESET} 1. Installing Go binary... ${RED}FAILED${RESET} Detected Go ${YELLOW}${ver_raw}${RESET}, but Go ${RED}≥ 1.22${RESET} is required." + echo -e "${GRAY}git-undo:${NC} 1. Installing Go binary... ${RED}FAILED${NC} Detected Go ${YELLOW}${ver_raw}${NC}, but Go ${RED}≥ 1.22${NC} is required." skip_binary=true fi fi if ! $skip_binary; then # 1) Install the binary - echo -en "${GRAY}git-undo:${RESET} 1. Installing Go binary (${BLUE}${detected_go}${RESET}) ..." + echo -en "${GRAY}git-undo:${NC} 1. Installing Go binary (${BLUE}${detected_go}${NC}) ..." # Check if we're in dev mode with local source available if [[ "${GIT_UNDO_DEV_MODE:-}" == "true" && -d "./cmd/git-undo" && -f "./Makefile" ]]; then - echo -e " ${YELLOW}(dev mode)${RESET}" + echo -e " ${YELLOW}(dev mode)${NC}" log "Building from local source using Makefile..." # Use Makefile's binary-install target which has proper version logic if make binary-install &>/dev/null; then # Get the version that was just installed INSTALLED_VERSION=$(git-undo --version 2>/dev/null || echo "unknown") - echo -e "${GRAY}git-undo:${RESET} Binary installed with version: ${BLUE}$INSTALLED_VERSION${RESET}" + echo -e "${GRAY}git-undo:${NC} Binary installed with version: ${BLUE}$INSTALLED_VERSION${NC}" else - echo -e "${GRAY}git-undo:${RESET} ${RED}Failed to build from source using Makefile${RESET}" + echo -e "${GRAY}git-undo:${NC} ${RED}Failed to build from source using Makefile${NC}" exit 1 fi else @@ -133,16 +134,16 @@ main() { if go install "$GITHUB_REPO_URL/cmd/$BIN_NAME@latest" 2>/dev/null; then BIN_PATH=$(command -v git-undo || echo "$BIN_DIR/$BIN_NAME") INSTALLED_VERSION=$(git-undo --version 2>/dev/null || echo "unknown") - echo -e " ${GREEN}OK${RESET} (installed at ${BLUE}${BIN_PATH}${RESET} | version=${BLUE}${INSTALLED_VERSION}${RESET})" + echo -e " ${GREEN}OK${NC} (installed at ${BLUE}${BIN_PATH}${NC} | version=${BLUE}${INSTALLED_VERSION}${NC})" else - echo -e " ${RED}FAILED${RESET}" + echo -e " ${RED}FAILED${NC}" exit 1 fi fi fi # 2) Git hooks integration - echo -en "${GRAY}git-undo:${RESET} 2. Git integration..." + echo -en "${GRAY}git-undo:${NC} 2. Git integration..." current_hooks_path=$(git config --global --get core.hooksPath || echo "") target_hooks_path="$GIT_HOOKS_DIR" @@ -150,45 +151,46 @@ main() { if [[ -z "$current_hooks_path" ]]; then git config --global core.hooksPath "$target_hooks_path" install_dispatcher_into "$target_hooks_path" - echo -e " ${GREEN}OK${RESET} (set core.hooksPath)" + echo -e " ${GREEN}OK${NC} (set core.hooksPath)" elif [[ "$current_hooks_path" == "$target_hooks_path" ]]; then install_dispatcher_into "$target_hooks_path" - echo -e " ${YELLOW}SKIP${RESET} (already configured)" + echo -e " ${YELLOW}SKIP${NC} (already configured)" else install_dispatcher_into "$current_hooks_path" - echo -e " ${YELLOW}SHARED${RESET} (pig-backed on $current_hooks_path)" + echo -e " ${YELLOW}SHARED${NC} (pig-backed on $current_hooks_path)" fi # 3) Shell integration local current_shell current_shell=$(detect_shell) - echo -en "${GRAY}git-undo:${RESET} 3. Shell integration (${BLUE}$current_shell${RESET})..." + echo -en "${GRAY}git-undo:${NC} 3. Shell integration (${BLUE}$current_shell${NC})..." # Temporarily disable set -e to capture non-zero exit codes set +e local hook_output + # shellcheck disable=SC2034 hook_output=$(install_shell_hook "$current_shell" 2>&1) local hook_status=$? set -e case $hook_status in 0) - echo -e " ${GREEN}OK${RESET}" + echo -e " ${GREEN}OK${NC}" ;; 2) - echo -e " ${YELLOW}SKIP${RESET} (already configured)" + echo -e " ${YELLOW}SKIP${NC} (already configured)" ;; *) - echo -e " ${RED}FAILED${RESET}" - log "You can manually source the appropriate hook file from ${YELLOW}$CFG_DIR${RESET}" + echo -e " ${RED}FAILED${NC}" + log "You can manually source the appropriate hook file from ${YELLOW}$CFG_DIR${NC}" exit 1 ;; esac # 3) Final message - log "${GREEN}Installation completed successfully!${RESET}" + log "${GREEN}Installation completed successfully!${NC}" echo -e "" - echo -e "Please restart your shell or run '${YELLOW}source ~/.${current_shell}rc${RESET}' to activate ${BLUE}git-undo${RESET}" + echo -e "Please restart your shell or run '${YELLOW}source ~/.${current_shell}rc${NC}' to activate ${BLUE}git-undo${NC}" } main "$@" diff --git a/scripts/src/uninstall.src.sh b/scripts/src/uninstall.src.sh index a8ce32e..0b5df7a 100755 --- a/scripts/src/uninstall.src.sh +++ b/scripts/src/uninstall.src.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -euo pipefail +# shellcheck disable=SC1091 source "$(dirname "$0")/common.sh" scrub_rc() { @@ -32,40 +33,46 @@ main() { log "Starting uninstallation..." # 1) Remove binary - echo -en "${GRAY}git-undo:${RESET} 1. Removing binary..." + echo -en "${GRAY}git-undo:${NC} 1. Removing binary..." if [[ -f "$BIN_PATH" ]]; then rm -f "$BIN_PATH" - echo -e " ${GREEN}OK${RESET}" + echo -e " ${GREEN}OK${NC}" else - echo -e " ${YELLOW}SKIP${RESET} (not found)" + echo -e " ${YELLOW}SKIP${NC} (not found)" fi # 2) Clean shell configuration files - echo -en "${GRAY}git-undo:${RESET} 2. Cleaning shell configurations..." + echo -en "${GRAY}git-undo:${NC} 2. Cleaning shell configurations..." local cleaned_files=0 # Check each rc file and count successful cleanings - scrub_rc "$HOME/.zshrc" && ((cleaned_files++)) || true - scrub_rc "$HOME/.bashrc" && ((cleaned_files++)) || true - scrub_rc "$HOME/.bash_profile" && ((cleaned_files++)) || true + if scrub_rc "$HOME/.zshrc"; then + ((cleaned_files++)) + fi + if scrub_rc "$HOME/.bashrc"; then + ((cleaned_files++)) + fi + if scrub_rc "$HOME/.bash_profile"; then + ((cleaned_files++)) + fi if [ $cleaned_files -gt 0 ]; then - echo -e " ${GREEN}OK${RESET} ($cleaned_files files)" + echo -e " ${GREEN}OK${NC} ($cleaned_files files)" else - echo -e " ${YELLOW}SKIP${RESET} (no hook lines found)" + echo -e " ${YELLOW}SKIP${NC} (no hook lines found)" fi # 3) Remove config directory - echo -en "${GRAY}git-undo:${RESET} 3. Removing config directory..." + echo -en "${GRAY}git-undo:${NC} 3. Removing config directory..." if [[ -d "$CFG_DIR" ]]; then rm -rf "$CFG_DIR" - echo -e " ${GREEN}OK${RESET}" + echo -e " ${GREEN}OK${NC}" else - echo -e " ${YELLOW}SKIP${RESET} (not found)" + echo -e " ${YELLOW}SKIP${NC} (not found)" fi # 4) Git hooks - echo -en "${GRAY}git-undo:${RESET} 4. Cleaning git hooks…" + echo -en "${GRAY}git-undo:${NC} 4. Cleaning git hooks…" if [[ "$(git config --global --get core.hooksPath)" == "$GIT_HOOKS_DIR" ]]; then git config --global --unset core.hooksPath fi @@ -77,10 +84,10 @@ main() { done done rm -f "$DISPATCHER_FILE" - echo -e " ${GREEN}OK${RESET}" + echo -e " ${GREEN}OK${NC}" # 5) Final message - log "${GREEN}Uninstallation completed successfully!${RESET}" + log "${GREEN}Uninstallation completed successfully!${NC}" } main "$@" diff --git a/scripts/src/update.src.sh b/scripts/src/update.src.sh index 62ad48d..ce42032 100755 --- a/scripts/src/update.src.sh +++ b/scripts/src/update.src.sh @@ -1,62 +1,63 @@ #!/usr/bin/env bash set -e +# shellcheck disable=SC1091 source "$(dirname "$0")/common.sh" main() { log "Checking for updates..." # 1) Get current version from the binary itself - echo -en "${GRAY}git-undo:${RESET} 1. Current version..." + echo -en "${GRAY}git-undo:${NC} 1. Current version..." local current_version if ! current_version=$(git-undo version 2>/dev/null | awk '{print $2}'); then - echo -e " ${RED}FAILED${RESET}" + echo -e " ${RED}FAILED${NC}" log "Could not determine current version. Is git-undo installed?" exit 1 fi if [[ -z "$current_version" || "$current_version" == "unknown" ]]; then - echo -e " ${YELLOW}UNKNOWN${RESET}" + echo -e " ${YELLOW}UNKNOWN${NC}" log "No version information found. Reinstall git-undo." exit 1 else - echo -e " ${BLUE}$current_version${RESET}" + echo -e " ${BLUE}$current_version${NC}" fi # 2) Get latest release version - echo -en "${GRAY}git-undo:${RESET} 2. Checking latest release..." + echo -en "${GRAY}git-undo:${NC} 2. Checking latest release..." local latest_version if ! latest_version=$(get_latest_version); then - echo -e " ${RED}FAILED${RESET}" + echo -e " ${RED}FAILED${NC}" log "Failed to check latest version. Check your internet connection." exit 1 fi - echo -e " ${BLUE}$latest_version${RESET}" + echo -e " ${BLUE}$latest_version${NC}" # 3) Compare versions - echo -en "${GRAY}git-undo:${RESET} 3. Comparing releases..." + echo -en "${GRAY}git-undo:${NC} 3. Comparing releases..." local comparison comparison=$(version_compare "$current_version" "$latest_version") case "$comparison" in "same") - echo -e " ${GREEN}UP TO DATE${RESET}" - log "You're already running the latest release (${BLUE}$current_version${RESET})" + echo -e " ${GREEN}UP TO DATE${NC}" + log "You're already running the latest release (${BLUE}$current_version${NC})" exit 0 ;; "newer") - echo -e " ${YELLOW}NEWER${RESET}" - log "You're running a newer release than available (${BLUE}$current_version${RESET} > ${BLUE}$latest_version${RESET})" + echo -e " ${YELLOW}NEWER${NC}" + log "You're running a newer release than available (${BLUE}$current_version${NC} > ${BLUE}$latest_version${NC})" exit 0 ;; "older") - echo -e " ${YELLOW}UPDATE AVAILABLE${RESET}" + echo -e " ${YELLOW}UPDATE AVAILABLE${NC}" ;; esac # 4) Ask for confirmation echo -e "" - echo -e "Update available: ${BLUE}$current_version${RESET} → ${GREEN}$latest_version${RESET}" + echo -e "Update available: ${BLUE}$current_version${NC} → ${GREEN}$latest_version${NC}" echo -en "Do you want to update? [Y/n]: " read -r response @@ -70,28 +71,28 @@ main() { esac # 5) Download and run new installer - echo -en "${GRAY}git-undo:${RESET} 4. Downloading latest installer..." + echo -en "${GRAY}git-undo:${NC} 4. Downloading latest installer..." local temp_installer temp_installer=$(mktemp) if command -v curl >/dev/null 2>&1; then if curl -sL "$INSTALL_URL" -o "$temp_installer"; then - echo -e " ${GREEN}OK${RESET}" + echo -e " ${GREEN}OK${NC}" else - echo -e " ${RED}FAILED${RESET}" + echo -e " ${RED}FAILED${NC}" rm -f "$temp_installer" exit 1 fi elif command -v wget >/dev/null 2>&1; then if wget -qO "$temp_installer" "$INSTALL_URL"; then - echo -e " ${GREEN}OK${RESET}" + echo -e " ${GREEN}OK${NC}" else - echo -e " ${RED}FAILED${RESET}" + echo -e " ${RED}FAILED${NC}" rm -f "$temp_installer" exit 1 fi else - echo -e " ${RED}FAILED${RESET}" + echo -e " ${RED}FAILED${NC}" log "curl or wget required for update" exit 1 fi @@ -105,10 +106,10 @@ main() { rm -f "$temp_installer" if [[ $install_status -eq 0 ]]; then - log "${GREEN}Update completed successfully!${RESET}" - log "Updated to version ${GREEN}$latest_version${RESET}" + log "${GREEN}Update completed successfully!${NC}" + log "Updated to version ${GREEN}$latest_version${NC}" else - log "${RED}Update failed.${RESET}" + log "${RED}Update failed.${NC}" exit 1 fi } diff --git a/uninstall.sh b/uninstall.sh index 1b52797..a683543 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -3,38 +3,37 @@ # DO NOT EDIT - modify scripts/src/*.src.sh instead and run 'make buildscripts' set -euo pipefail +# shellcheck disable=SC1091 # ── Inlined content from common.sh ────────────────────────────────────────── + # Color definitions - shared across all scripts GRAY='\033[90m' GREEN='\033[32m' YELLOW='\033[33m' RED='\033[31m' BLUE='\033[34m' -RESET='\033[0m' - -# Alternative name for compatibility -NC="$RESET" # No Color (used in some scripts) +NC='\033[0m' # No Color # Basic logging functions log() { - echo -e "${GRAY}git-undo:${RESET} $1" + echo -e "${GRAY}git-undo:${NC} $1" } log_info() { - echo -e "${BLUE}[INFO]${RESET} $*" + echo -e "${BLUE}[INFO]${NC} $*" } log_success() { - echo -e "${GREEN}[SUCCESS]${RESET} $*" + echo -e "${GREEN}[SUCCESS]${NC} $*" } log_error() { - echo -e "${RED}[ERROR]${RESET} $*" + echo -e "${RED}[ERROR]${NC} $*" } log_warning() { - echo -e "${YELLOW}[WARNING]${RESET} $*" + echo -e "${YELLOW}[WARNING]${NC} $*" } # _get_script_dir determines the directory of this file (common.sh) in a POSIX-portable way @@ -56,10 +55,13 @@ _get_script_dir() { # physical directory of the file itself local dir dir=$(cd -P -- "$(dirname -- "$src")" && pwd) - # Because of how we store scripts: built executable scripts are in root - # but helpers and sources are in `scripts` dir. - # so always append scripts - printf '%s/scripts' "$dir" + # Since common.sh is now in scripts/src/, we need to go up one level to get scripts/ + # Remove /src suffix if present, otherwise assume we're already in scripts/ + if [[ "$dir" == */src ]]; then + printf '%s' "${dir%/src}" + else + printf '%s' "$dir" + fi } if [[ -n "${BASH_SOURCE[0]:-}" ]]; then # Bash (she-bang path) @@ -71,25 +73,26 @@ unset -f _get_script_dir echo "SCRIPT DIR $SCRIPT_DIR" # Coloring helpers +# shellcheck disable=SC1091 # Git-undo specific configuration BIN_NAME="git-undo" BIN_DIR=$(go env GOBIN 2>/dev/null || true) [[ -z "$BIN_DIR" ]] && BIN_DIR="$(go env GOPATH)/bin" -BIN_PATH="$BIN_DIR/$BIN_NAME" +export BIN_PATH="$BIN_DIR/$BIN_NAME" CFG_DIR="$HOME/.config/git-undo" -BASH_HOOK="$CFG_DIR/git-undo-hook.bash" -ZSH_HOOK="$CFG_DIR/git-undo-hook.zsh" +export BASH_HOOK="$CFG_DIR/git-undo-hook.bash" +export ZSH_HOOK="$CFG_DIR/git-undo-hook.zsh" GIT_HOOKS_DIR="$CFG_DIR/hooks" DISPATCHER_FILE="$GIT_HOOKS_DIR/git-hooks.sh" DISPATCHER_SRC="$SCRIPT_DIR/git-undo-git-hook.sh" REPO_OWNER="amberpixels" REPO_NAME="git-undo" -GITHUB_REPO_URL="github.com/$REPO_OWNER/$REPO_NAME" +export GITHUB_REPO_URL="github.com/$REPO_OWNER/$REPO_NAME" GITHUB_API_URL="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME" -INSTALL_URL="https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/install.sh" +export INSTALL_URL="https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/install.sh" detect_shell() { # Method 1: Check $SHELL environment variable (most reliable for login shell) @@ -147,12 +150,16 @@ version_compare() { version2=${version2#v} # Extract base version (everything before the first dash) - local base1=$(echo "$version1" | cut -d'-' -f1) - local base2=$(echo "$version2" | cut -d'-' -f1) + local base1 + local base2 + base1=$(echo "$version1" | cut -d'-' -f1) + base2=$(echo "$version2" | cut -d'-' -f1) # Convert base versions to comparable format (e.g., 1.2.3 -> 001002003) - local v1=$(echo "$base1" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') - local v2=$(echo "$base2" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') + local v1 + local v2 + v1=$(echo "$base1" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') + v2=$(echo "$base2" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') # Compare base versions first if [[ "$v1" < "$v2" ]]; then @@ -305,6 +312,7 @@ EOF log "Hook installation completed for: $target" return 0 } + # ── End of inlined content ────────────────────────────────────────────────── scrub_rc() { @@ -336,40 +344,46 @@ main() { log "Starting uninstallation..." # 1) Remove binary - echo -en "${GRAY}git-undo:${RESET} 1. Removing binary..." + echo -en "${GRAY}git-undo:${NC} 1. Removing binary..." if [[ -f "$BIN_PATH" ]]; then rm -f "$BIN_PATH" - echo -e " ${GREEN}OK${RESET}" + echo -e " ${GREEN}OK${NC}" else - echo -e " ${YELLOW}SKIP${RESET} (not found)" + echo -e " ${YELLOW}SKIP${NC} (not found)" fi # 2) Clean shell configuration files - echo -en "${GRAY}git-undo:${RESET} 2. Cleaning shell configurations..." + echo -en "${GRAY}git-undo:${NC} 2. Cleaning shell configurations..." local cleaned_files=0 # Check each rc file and count successful cleanings - scrub_rc "$HOME/.zshrc" && ((cleaned_files++)) || true - scrub_rc "$HOME/.bashrc" && ((cleaned_files++)) || true - scrub_rc "$HOME/.bash_profile" && ((cleaned_files++)) || true + if scrub_rc "$HOME/.zshrc"; then + ((cleaned_files++)) + fi + if scrub_rc "$HOME/.bashrc"; then + ((cleaned_files++)) + fi + if scrub_rc "$HOME/.bash_profile"; then + ((cleaned_files++)) + fi if [ $cleaned_files -gt 0 ]; then - echo -e " ${GREEN}OK${RESET} ($cleaned_files files)" + echo -e " ${GREEN}OK${NC} ($cleaned_files files)" else - echo -e " ${YELLOW}SKIP${RESET} (no hook lines found)" + echo -e " ${YELLOW}SKIP${NC} (no hook lines found)" fi # 3) Remove config directory - echo -en "${GRAY}git-undo:${RESET} 3. Removing config directory..." + echo -en "${GRAY}git-undo:${NC} 3. Removing config directory..." if [[ -d "$CFG_DIR" ]]; then rm -rf "$CFG_DIR" - echo -e " ${GREEN}OK${RESET}" + echo -e " ${GREEN}OK${NC}" else - echo -e " ${YELLOW}SKIP${RESET} (not found)" + echo -e " ${YELLOW}SKIP${NC} (not found)" fi # 4) Git hooks - echo -en "${GRAY}git-undo:${RESET} 4. Cleaning git hooks…" + echo -en "${GRAY}git-undo:${NC} 4. Cleaning git hooks…" if [[ "$(git config --global --get core.hooksPath)" == "$GIT_HOOKS_DIR" ]]; then git config --global --unset core.hooksPath fi @@ -381,10 +395,10 @@ main() { done done rm -f "$DISPATCHER_FILE" - echo -e " ${GREEN}OK${RESET}" + echo -e " ${GREEN}OK${NC}" # 5) Final message - log "${GREEN}Uninstallation completed successfully!${RESET}" + log "${GREEN}Uninstallation completed successfully!${NC}" } main "$@" diff --git a/update.sh b/update.sh index 673360c..231e394 100755 --- a/update.sh +++ b/update.sh @@ -3,38 +3,37 @@ # DO NOT EDIT - modify scripts/src/*.src.sh instead and run 'make buildscripts' set -e +# shellcheck disable=SC1091 # ── Inlined content from common.sh ────────────────────────────────────────── + # Color definitions - shared across all scripts GRAY='\033[90m' GREEN='\033[32m' YELLOW='\033[33m' RED='\033[31m' BLUE='\033[34m' -RESET='\033[0m' - -# Alternative name for compatibility -NC="$RESET" # No Color (used in some scripts) +NC='\033[0m' # No Color # Basic logging functions log() { - echo -e "${GRAY}git-undo:${RESET} $1" + echo -e "${GRAY}git-undo:${NC} $1" } log_info() { - echo -e "${BLUE}[INFO]${RESET} $*" + echo -e "${BLUE}[INFO]${NC} $*" } log_success() { - echo -e "${GREEN}[SUCCESS]${RESET} $*" + echo -e "${GREEN}[SUCCESS]${NC} $*" } log_error() { - echo -e "${RED}[ERROR]${RESET} $*" + echo -e "${RED}[ERROR]${NC} $*" } log_warning() { - echo -e "${YELLOW}[WARNING]${RESET} $*" + echo -e "${YELLOW}[WARNING]${NC} $*" } # _get_script_dir determines the directory of this file (common.sh) in a POSIX-portable way @@ -56,10 +55,13 @@ _get_script_dir() { # physical directory of the file itself local dir dir=$(cd -P -- "$(dirname -- "$src")" && pwd) - # Because of how we store scripts: built executable scripts are in root - # but helpers and sources are in `scripts` dir. - # so always append scripts - printf '%s/scripts' "$dir" + # Since common.sh is now in scripts/src/, we need to go up one level to get scripts/ + # Remove /src suffix if present, otherwise assume we're already in scripts/ + if [[ "$dir" == */src ]]; then + printf '%s' "${dir%/src}" + else + printf '%s' "$dir" + fi } if [[ -n "${BASH_SOURCE[0]:-}" ]]; then # Bash (she-bang path) @@ -71,25 +73,26 @@ unset -f _get_script_dir echo "SCRIPT DIR $SCRIPT_DIR" # Coloring helpers +# shellcheck disable=SC1091 # Git-undo specific configuration BIN_NAME="git-undo" BIN_DIR=$(go env GOBIN 2>/dev/null || true) [[ -z "$BIN_DIR" ]] && BIN_DIR="$(go env GOPATH)/bin" -BIN_PATH="$BIN_DIR/$BIN_NAME" +export BIN_PATH="$BIN_DIR/$BIN_NAME" CFG_DIR="$HOME/.config/git-undo" -BASH_HOOK="$CFG_DIR/git-undo-hook.bash" -ZSH_HOOK="$CFG_DIR/git-undo-hook.zsh" +export BASH_HOOK="$CFG_DIR/git-undo-hook.bash" +export ZSH_HOOK="$CFG_DIR/git-undo-hook.zsh" GIT_HOOKS_DIR="$CFG_DIR/hooks" DISPATCHER_FILE="$GIT_HOOKS_DIR/git-hooks.sh" DISPATCHER_SRC="$SCRIPT_DIR/git-undo-git-hook.sh" REPO_OWNER="amberpixels" REPO_NAME="git-undo" -GITHUB_REPO_URL="github.com/$REPO_OWNER/$REPO_NAME" +export GITHUB_REPO_URL="github.com/$REPO_OWNER/$REPO_NAME" GITHUB_API_URL="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME" -INSTALL_URL="https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/install.sh" +export INSTALL_URL="https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/install.sh" detect_shell() { # Method 1: Check $SHELL environment variable (most reliable for login shell) @@ -147,12 +150,16 @@ version_compare() { version2=${version2#v} # Extract base version (everything before the first dash) - local base1=$(echo "$version1" | cut -d'-' -f1) - local base2=$(echo "$version2" | cut -d'-' -f1) + local base1 + local base2 + base1=$(echo "$version1" | cut -d'-' -f1) + base2=$(echo "$version2" | cut -d'-' -f1) # Convert base versions to comparable format (e.g., 1.2.3 -> 001002003) - local v1=$(echo "$base1" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') - local v2=$(echo "$base2" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') + local v1 + local v2 + v1=$(echo "$base1" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') + v2=$(echo "$base2" | awk -F. '{ printf("%03d%03d%03d\n", $1, $2, $3); }') # Compare base versions first if [[ "$v1" < "$v2" ]]; then @@ -305,62 +312,63 @@ EOF log "Hook installation completed for: $target" return 0 } + # ── End of inlined content ────────────────────────────────────────────────── main() { log "Checking for updates..." # 1) Get current version from the binary itself - echo -en "${GRAY}git-undo:${RESET} 1. Current version..." + echo -en "${GRAY}git-undo:${NC} 1. Current version..." local current_version if ! current_version=$(git-undo version 2>/dev/null | awk '{print $2}'); then - echo -e " ${RED}FAILED${RESET}" + echo -e " ${RED}FAILED${NC}" log "Could not determine current version. Is git-undo installed?" exit 1 fi if [[ -z "$current_version" || "$current_version" == "unknown" ]]; then - echo -e " ${YELLOW}UNKNOWN${RESET}" + echo -e " ${YELLOW}UNKNOWN${NC}" log "No version information found. Reinstall git-undo." exit 1 else - echo -e " ${BLUE}$current_version${RESET}" + echo -e " ${BLUE}$current_version${NC}" fi # 2) Get latest release version - echo -en "${GRAY}git-undo:${RESET} 2. Checking latest release..." + echo -en "${GRAY}git-undo:${NC} 2. Checking latest release..." local latest_version if ! latest_version=$(get_latest_version); then - echo -e " ${RED}FAILED${RESET}" + echo -e " ${RED}FAILED${NC}" log "Failed to check latest version. Check your internet connection." exit 1 fi - echo -e " ${BLUE}$latest_version${RESET}" + echo -e " ${BLUE}$latest_version${NC}" # 3) Compare versions - echo -en "${GRAY}git-undo:${RESET} 3. Comparing releases..." + echo -en "${GRAY}git-undo:${NC} 3. Comparing releases..." local comparison comparison=$(version_compare "$current_version" "$latest_version") case "$comparison" in "same") - echo -e " ${GREEN}UP TO DATE${RESET}" - log "You're already running the latest release (${BLUE}$current_version${RESET})" + echo -e " ${GREEN}UP TO DATE${NC}" + log "You're already running the latest release (${BLUE}$current_version${NC})" exit 0 ;; "newer") - echo -e " ${YELLOW}NEWER${RESET}" - log "You're running a newer release than available (${BLUE}$current_version${RESET} > ${BLUE}$latest_version${RESET})" + echo -e " ${YELLOW}NEWER${NC}" + log "You're running a newer release than available (${BLUE}$current_version${NC} > ${BLUE}$latest_version${NC})" exit 0 ;; "older") - echo -e " ${YELLOW}UPDATE AVAILABLE${RESET}" + echo -e " ${YELLOW}UPDATE AVAILABLE${NC}" ;; esac # 4) Ask for confirmation echo -e "" - echo -e "Update available: ${BLUE}$current_version${RESET} → ${GREEN}$latest_version${RESET}" + echo -e "Update available: ${BLUE}$current_version${NC} → ${GREEN}$latest_version${NC}" echo -en "Do you want to update? [Y/n]: " read -r response @@ -374,28 +382,28 @@ main() { esac # 5) Download and run new installer - echo -en "${GRAY}git-undo:${RESET} 4. Downloading latest installer..." + echo -en "${GRAY}git-undo:${NC} 4. Downloading latest installer..." local temp_installer temp_installer=$(mktemp) if command -v curl >/dev/null 2>&1; then if curl -sL "$INSTALL_URL" -o "$temp_installer"; then - echo -e " ${GREEN}OK${RESET}" + echo -e " ${GREEN}OK${NC}" else - echo -e " ${RED}FAILED${RESET}" + echo -e " ${RED}FAILED${NC}" rm -f "$temp_installer" exit 1 fi elif command -v wget >/dev/null 2>&1; then if wget -qO "$temp_installer" "$INSTALL_URL"; then - echo -e " ${GREEN}OK${RESET}" + echo -e " ${GREEN}OK${NC}" else - echo -e " ${RED}FAILED${RESET}" + echo -e " ${RED}FAILED${NC}" rm -f "$temp_installer" exit 1 fi else - echo -e " ${RED}FAILED${RESET}" + echo -e " ${RED}FAILED${NC}" log "curl or wget required for update" exit 1 fi @@ -409,10 +417,10 @@ main() { rm -f "$temp_installer" if [[ $install_status -eq 0 ]]; then - log "${GREEN}Update completed successfully!${RESET}" - log "Updated to version ${GREEN}$latest_version${RESET}" + log "${GREEN}Update completed successfully!${NC}" + log "Updated to version ${GREEN}$latest_version${NC}" else - log "${RED}Update failed.${RESET}" + log "${RED}Update failed.${NC}" exit 1 fi } From 01af43bcfd633f85e897b2af8f8906c726d90438 Mon Sep 17 00:00:00 2001 From: Test User Date: Fri, 13 Jun 2025 13:06:07 +0300 Subject: [PATCH 4/4] Revert "add claude.md" This reverts commit c23104f2ee4d01b04829920e60865ff1de060339. --- CLAUDE.md | 211 ------------------------------------------------------ 1 file changed, 211 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 824abff..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,211 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -**git-undo** is a CLI tool that provides a universal "Ctrl+Z" for Git commands. It tracks every mutating Git operation and can reverse them with a single `git undo` command. - -## Essential Commands - -### Development -```bash -make build # Compile binary with version info to ./build/git-undo -make test # Run unit tests -make test-all # Run unit tests + integration tests (dev mode) -make integration-test-dev # BATS integration tests (test current changes) -make integration-test-prod # BATS integration tests (test user experience) -make lint # Run golangci-lint (auto-installs if needed) -make tidy # Format, vet, and tidy Go modules -``` - -### Installation & Management -```bash -make install # Full installation (binary + shell hooks) -make uninstall # Complete removal -./install.sh # Direct installation script -``` - -### Testing Specific Components -```bash -go test ./internal/app # Test main application logic -go test ./internal/githelpers # Test git command parsing -go test ./internal/git-undo/logging # Test command logging -go test -v ./internal/app -run TestSequentialUndo # Run specific failing test -``` - -## Core Architecture - -### Component Relationships -``` -User Git Command → Shell Hook (captures command) ┐ - ↓ ├→ Deduplication Logic → git-undo --hook → Logger → .git/git-undo/commands - Git Operation → Git Hook (post-operation) ┘ ↓ - │ -User runs `git undo` → App.Run() → Undoer Factory → Command-specific Undoer → Git Execution │ - ↓ │ - Logger.ToggleEntry() ←────────────────────┘ -``` - -### Key Packages - -**`/internal/app/`** - Central orchestrator -- `app.go`: Main application logic, command routing, self-management -- Handles: `git undo`, `git undo undo` (redo), `--verbose`, `--dry-run`, `--log` -- Self-management: `git undo self update/uninstall/version` - -**`/internal/git-undo/undoer/`** - Undo command generation -- Interface-based design with factory pattern -- Command-specific implementations: `add.go`, `commit.go`, `merge.go`, `branch.go`, `stash.go`, `checkout.go` -- Each undoer analyzes git state and generates appropriate inverse commands - -**`/internal/git-undo/logging/`** - Command tracking system -- Logs format: `TIMESTAMP|REF|COMMAND` in `.git/git-undo/commands` -- Supports marking entries as undone with `#` prefix -- Deduplication between shell hooks and git hooks using flag files and command identifiers -- Per-repository, per-branch tracking - -**`/internal/githelpers/`** - Git integration layer -- `gitcommand.go`: Command parsing with `github.com/mattn/go-shellwords` -- `git_reference.go`: Command classification (porcelain/plumbing, read-only/mutating) -- `githelper.go`: Git execution wrapper with proper error handling - -### Dual Hook System Architecture: Shell + Git Hooks - -**git-undo** uses a sophisticated dual hook system that combines shell hooks and git hooks to capture git operations from different contexts: - -#### Shell Hooks (Primary) -- **Bash**: Uses `DEBUG` trap + `PROMPT_COMMAND` to capture command-line git operations -- **Zsh**: Uses `preexec` + `precmd` hooks -- **Coverage**: Captures user-typed commands with exact flags/arguments (e.g., `git add file1.txt file2.txt`) -- **Advantage**: Preserves original command context for precise undo operations - -#### Git Hooks (Secondary/Fallback) -- **Hooks**: `post-commit` and `post-merge` via `/scripts/git-undo-git-hook.sh` -- **Coverage**: Captures git operations that bypass shell (IDEs, scripts, git commands run by other tools) -- **Command Reconstruction**: Since hooks run after the fact, commands are reconstructed from git state: - - `post-commit`: Extracts commit message → `git commit -m "message"` - - `post-merge`: Detects merge type → `git merge --squash/--no-ff/--ff` - -#### Deduplication Strategy -**Problem**: Both hooks often fire for the same operation, risking duplicate logging. - -**Solution**: Smart deduplication via command normalization and timing: - -1. **Command Normalization**: Both hooks normalize commands to canonical form - - `git commit -m "test" --verbose` → `git commit -m "test"` - - `git merge feature --no-ff` → `git merge --no-ff feature` - - Handles variations in flag order, quotes, and extra flags - -2. **Timing + Hashing**: Creates SHA1 identifier from `normalized_command + git_ref + truncated_timestamp` - - 2-second time window for duplicate detection - - Git hook runs first, marks command as logged via flag file - - Shell hook checks flag file, skips if already logged - -3. **Hook Priority**: **Git hook wins** when both detect the same operation - - Git hooks are more reliable for detecting actual git state changes - - Shell hooks can capture commands that don't change state (failed commands) - -#### When Each Hook System Is Useful - -**Shell Hooks Excel At:** -- `git add` operations (exact file lists preserved) -- Commands with complex flag combinations -- Failed commands that still need tracking for user context -- Interactive git operations with user input - -**Git Hooks Excel At:** -- IDE-triggered commits/merges (VS Code, IntelliJ, etc.) -- Script-automated git operations -- Git operations from other tools (CI/CD, deployment scripts) -- Operations that bypass shell entirely - -#### Installation Process -1. **Binary**: Installs to `$(go env GOPATH)/bin` -2. **Git Hooks**: - - Sets global `core.hooksPath` to `~/.config/git-undo/hooks/` OR - - Integrates with existing hooks by appending to existing hook files - - Copies dispatcher script (`git-undo-git-hook.sh`) to hooks directory - - Creates `post-commit` and `post-merge` hooks (symlinks preferred, fallback to standalone scripts) -3. **Shell Hooks**: Places in `~/.config/git-undo/` and sources from shell rc files - -## Testing Strategy - -### Unit Tests -- Uses `github.com/stretchr/testify` with suite pattern -- `testutil.GitTestSuite`: Base suite with git repository setup/teardown -- Mock interfaces for git operations to enable isolated testing -- Export pattern: `export_test.go` files expose internal functions for testing - -### Integration Tests -- **BATS Framework**: Bash Automated Testing System -- **Dev Mode** (`--dev`): Tests current working directory changes -- **Prod Mode** (`--prod`): Tests real user installation experience -- Real git repository creation and cleanup -- End-to-end workflow verification - -### Current Test Issues -- `TestSequentialUndo` failing on both main and feature branches (see `todo.md`) -- Test expects alternating commit/add undo sequence but fails on second undo - -## Command Support & Undo Logic - -### Supported Commands -- **commit** → `git reset --soft HEAD~1` (preserves staged changes) -- **add** → `git restore --staged ` or `git reset ` -- **branch** → `git branch -d ` -- **checkout -b** → `git branch -d ` -- **stash** → `git stash pop` + cleanup -- **merge** → `git reset --merge ORIG_HEAD` - -### Undo Command Generation -- Context-aware: checks HEAD existence, merge state, tags -- Handles edge cases: initial commits, amended commits, tagged commits -- Provides warnings for potentially destructive operations -- Uses git state analysis to determine appropriate reset strategy - -## Build System & Versioning - -### Version Management -- Uses `./scripts/pseudo_version.sh` for development builds -- Build-time version injection via `-ldflags "-X main.version=$(VERSION)"` -- Priority: git tags → build-time version → "unknown" - -### Dependencies -- **Runtime**: Only `github.com/mattn/go-shellwords` for safe command parsing -- **Testing**: `github.com/stretchr/testify` -- **Linting**: `golangci-lint` (auto-installed via Makefile) - -## Important Implementation Details - -### Command Logging Format -``` -2025-01-09 14:30:45|main|git commit -m "test message" -#2025-01-09 14:25:30|main|git add file.txt # undoed entry (prefixed with #) -``` - -### Hook Detection & Environment Variables -- **Git Hook Detection**: - - Primary: `GIT_UNDO_GIT_HOOK_MARKER=1` (set by git hook script) - - Secondary: `GIT_HOOK_NAME` (contains hook name like "post-commit") - - Fallback: `GIT_DIR` environment variable presence -- **Shell Hook Detection**: `GIT_UNDO_INTERNAL_HOOK=1` (set by shell hooks) -- **Flag Files**: `.git/git-undo/.git-hook-` for marking git hook execution - -### Command Normalization Details -- **Supported Commands**: `commit`, `merge`, `rebase`, `cherry-pick` -- **commit**: Extracts `-m message` and `--amend`, ignores flags like `--verbose`, `--signoff` -- **merge**: Extracts merge strategy (`--squash`, `--no-ff`, `--ff`) and branch name -- **Purpose**: Ensures equivalent commands generate identical identifiers for deduplication - -### Error Handling Patterns -- Graceful degradation when not in git repository -- Panic recovery in main application loop -- Git command validation before execution -- Comprehensive error wrapping with context - -### Security Considerations -- Safe command parsing with `go-shellwords` -- Git command validation against known command whitelist -- No arbitrary command execution -- Proper file permissions for hook installation \ No newline at end of file