From 6d69722034cb0778cf73572b53ef4129945b2269 Mon Sep 17 00:00:00 2001 From: an-lee Date: Mon, 1 Jun 2026 14:47:52 +0800 Subject: [PATCH] feat(git): add selectable git aliases via Phase 2 TUI Introduce a YAML catalog of common shortcuts (git st, git co, etc.), let developers preview and multi-select during setup, and apply them through a Botstrap-owned gitconfig include with uninstall support. Co-authored-by: Cursor --- configs/git/aliases.yaml | 90 ++++++++++ docs/CONFIGURATION.md | 5 +- docs/DEFAULTS_AND_CUSTOMIZATION.md | 4 +- docs/REFERENCE.md | 6 +- install/phase-2-tui.ps1 | 82 +++++++++ install/phase-2-tui.sh | 81 +++++++++ install/phase-3-configure.ps1 | 3 + install/phase-3-configure.sh | 4 + install/uninstall.ps1 | 8 + install/uninstall.sh | 8 + lib/git-aliases.ps1 | 249 ++++++++++++++++++++++++++ lib/git-aliases.sh | 271 +++++++++++++++++++++++++++++ 12 files changed, 808 insertions(+), 3 deletions(-) create mode 100644 configs/git/aliases.yaml create mode 100644 lib/git-aliases.ps1 create mode 100644 lib/git-aliases.sh diff --git a/configs/git/aliases.yaml b/configs/git/aliases.yaml new file mode 100644 index 0000000..f86e7c9 --- /dev/null +++ b/configs/git/aliases.yaml @@ -0,0 +1,90 @@ +# Git alias catalog for Botstrap Phase 2 / Phase 3. +# Applied via ~/.config/botstrap/git-aliases (included from ~/.gitconfig). + +defaults: + enabled: true + +aliases: + - id: st + name: st + command: status -sb + description: Short status with branch info + default: true + + - id: co + name: co + command: checkout + description: Switch branches + default: true + + - id: br + name: br + command: branch -vv + description: List branches with tracking info + default: true + + - id: ci + name: ci + command: commit + description: Create a commit + default: true + + - id: ca + name: ca + command: commit --amend + description: Amend the last commit + default: true + + - id: cane + name: cane + command: commit --amend --no-edit + description: Amend without editing the message + default: true + + - id: unstage + name: unstage + command: restore --staged + description: Unstage files + default: true + + - id: undo + name: undo + command: reset HEAD~1 --mixed + description: Undo last commit, keep changes + default: true + + - id: last + name: last + command: log -1 HEAD + description: Show the latest commit + default: true + + - id: lg + name: lg + command: log --oneline --graph --decorate -20 + description: Compact recent history graph + default: true + + - id: df + name: df + command: diff + description: Show unstaged diff + default: true + + - id: dc + name: dc + command: diff --cached + description: Show staged diff + default: true + + - id: pullr + name: pullr + command: pull --rebase + description: Pull with rebase + default: true + + - id: pushf + name: pushf + command: push --force-with-lease + description: Safer force push + default: true diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f1a0e41..8f0b0ff 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -10,8 +10,9 @@ For **defaults**, **automation env vars**, and **how to customize** (reconfigure |-----------------|----------------------| | `configs/git/gitconfig` | Copied to **`~/.gitconfig`** only when that file does **not** already exist (Unix Phase 3). | | `configs/git/gitignore_global` | Copied to **`~/.gitignore_global`**; `git config --global core.excludesfile` set to that path when Git is available. | +| `configs/git/aliases.yaml` | Catalog of recommended git aliases (not copied verbatim). Phase 2 uses it for preview and multi-select; Phase 3 writes selected aliases to **`~/.config/botstrap/git-aliases`** and adds a **`[include]`** in **`~/.gitconfig`** (marker **`# botstrap git-aliases`**). | -Global **`user.name`** and **`user.email`** are set from **`BOTSTRAP_GIT_NAME`** and **`BOTSTRAP_GIT_EMAIL`** when non-empty (not from static files). +Global **`user.name`** and **`user.email`** are set from **`BOTSTRAP_GIT_NAME`** and **`BOTSTRAP_GIT_EMAIL`** when non-empty (not from static files). Git alias selection is driven by **`BOTSTRAP_GIT_ALIASES`** (see [Reference — Phase 2 selection variables](./REFERENCE.md#phase-2-selection-variables-unix)). ## `configs/shell/` @@ -57,6 +58,8 @@ Phase 3 also writes: - **`~/.config/botstrap/theme.env`** — `theme=` - **`~/.config/botstrap/editor.env`** — `editor=` +- **`~/.config/botstrap/git-aliases.env`** — `selected=` and `managed=` alias id lists +- **`~/.config/botstrap/git-aliases`** — generated `[alias]` fragment included from **`~/.gitconfig`** ## `themes/` diff --git a/docs/DEFAULTS_AND_CUSTOMIZATION.md b/docs/DEFAULTS_AND_CUSTOMIZATION.md index e2ac449..4f2fb4b 100644 --- a/docs/DEFAULTS_AND_CUSTOMIZATION.md +++ b/docs/DEFAULTS_AND_CUSTOMIZATION.md @@ -9,6 +9,7 @@ When **gum** is not on `PATH`, Phase 2 (`install/phase-2-tui.sh` / `phase-2-tui. | Variable | Default when gum is missing | |----------|-----------------------------| | `BOTSTRAP_GIT_NAME` / `BOTSTRAP_GIT_EMAIL` | Empty unless already set in the environment. | +| `BOTSTRAP_GIT_ALIASES` | All catalog entries with **`default: true`** when **`configs/git/aliases.yaml`** has **`defaults.enabled: true`**, unless **`git-aliases.env`** already has **`selected=`** (reconfigure). Set to **`none`** to skip. | | `BOTSTRAP_EDITOR` | `none` | | `BOTSTRAP_LANGUAGES` | Empty (no optional language installs from TUI). | | `BOTSTRAP_DATABASES` | Empty. | @@ -22,7 +23,7 @@ You can **pre-set any `BOTSTRAP_*`** before running `install.sh`, `install.ps1`, Phase 3 copies or merges from **`configs/`** and runs optional installs from **`registry/optional.yaml`**. Typical defaults: -- **Git:** `configs/git/gitconfig` is copied to **`~/.gitconfig`** only if that file **does not** already exist. Global **`user.name`** / **`user.email`** are set from **`BOTSTRAP_GIT_NAME`** / **`BOTSTRAP_GIT_EMAIL`** when non-empty. +- **Git:** `configs/git/gitconfig` is copied to **`~/.gitconfig`** only if that file **does not** already exist. Global **`user.name`** / **`user.email`** are set from **`BOTSTRAP_GIT_NAME`** / **`BOTSTRAP_GIT_EMAIL`** when non-empty. **Git shortcuts** (`git st`, `git co`, …) from **`configs/git/aliases.yaml`** are written to **`~/.config/botstrap/git-aliases`** and included from **`~/.gitconfig`** when selected in Phase 2 (or when **`BOTSTRAP_GIT_ALIASES`** is preset). - **Git ignore:** `gitignore_global` is copied and **`core.excludesfile`** is pointed at it when Git is available. - **Starship:** `configs/shell/prompt.toml` → **`~/.config/starship.toml`** (overwrites when the repo file exists). - **Shell rc files:** `aliases` and `functions` are appended **once** inside `# botstrap aliases` / `# botstrap functions` blocks to **`~/.zshrc`** and **`~/.bashrc`** (Unix). The PATH snippet sources **`~/.config/botstrap/env.sh`**. @@ -50,6 +51,7 @@ Export **`BOTSTRAP_*`** variables before the orchestrator or before sourcing Pha ### 4. Manual edits in your home directory - Prefer editing **`~/.config/...`** files directly for Starship, editor configs, and Git excludes when you do not want to flow changes through the repo. +- **Git aliases:** edit **`~/.config/botstrap/git-aliases`** directly, or run **`botstrap reconfigure`** to change the Phase 2 selection. To add custom aliases outside Botstrap, set them in **`~/.gitconfig`** (not in the include file) so reconfigure does not overwrite them. - For **`~/.zshrc`** / **`~/.bashrc`**, avoid duplicating Botstrap blocks: either edit **outside** the `# botstrap …` sections or adjust **`configs/shell/*`** in the clone and re-run Phase 3 so a single marked block stays canonical. ### 5. Fork or extend the registry diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 4e8061a..6bf404a 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -15,7 +15,7 @@ Operational facts: **CLI**, **environment variables**, and **artifacts** Botstra | `botstrap update` | **Interactive (TTY + `gum`):** **`gum choose`** among **Botstrap repo only**, **Installed tools only**, **Both**, or **Cancel**. **Non-interactive, no flags:** repo-only pull (backward compatible) and prints a one-line hint about **`--tools`**, **`--all`**, and **`self-update`**. **Flags:** **`--self`** — git pull only; **`--tools`** — run **`install/update-tools.sh`** (Bash) or **`install/update-tools.ps1`** (PowerShell) to upgrade **prerequisites**, **selected core**, and **persisted optional** selections per registry **`update`** snippets; **`--all`** — self then tools. Does **not** re-run full Phase 3 except what each **`update`** snippet does. | | `botstrap reconfigure` | **Bash:** sets **`BOTSTRAP_ROOT`**, runs **`lib/detect`**, then sources **`install/phase-2-tui.sh`** and **`install/phase-3-configure.sh`** only. **PowerShell:** sets **`BOTSTRAP_ROOT`**, dot-sources **`install/phase-2-tui.ps1`** and **`install/phase-3-configure.ps1`**. | | `botstrap doctor` | **Bash:** prints a short **status** header (`BOTSTRAP_ROOT`, semver, optional git head, whether **`~/.config/botstrap/env.sh`** exists), then runs **`install/phase-4-verify.sh`**. **PowerShell:** similar header (whether the **`# botstrap PATH`** hook is present in **each** known profile path—see Windows Phase 3 artifacts), then dot-sources **`install/phase-4-verify.ps1`**. Exits **0** if every verify passes, **1** if **`yq`** is missing or any verify fails. | -| `botstrap uninstall` | Removes **Phase 3 shell integration** only (see **`install/uninstall.sh`** / **`install/uninstall.ps1`**). **Unix:** strips **`# botstrap PATH`**, **`# botstrap aliases`**, and **`# botstrap functions`** blocks from **`~/.zshrc`** and **`~/.bashrc`**, and deletes **`~/.config/botstrap/env.sh`**. **Windows:** removes **`# botstrap PATH`**, **`# botstrap starship`**, **`# botstrap zoxide`**, and **`# botstrap aliases`** regions from **each** PowerShell profile path Botstrap manages (same set as Phase 3). **Does not** uninstall packages (Homebrew, apt, winget, mise, etc.) or remove files Botstrap copied elsewhere (e.g. **`~/.config/starship.toml`**, editor settings, **`~/.gitconfig`**). **Exit 1** if the user cancels, a guard refuses **`--remove-checkout`**, or usage is invalid. | +| `botstrap uninstall` | Removes **Phase 3 shell integration** and **Botstrap-managed git aliases** (see **`install/uninstall.sh`** / **`install/uninstall.ps1`**). **Unix:** strips **`# botstrap PATH`**, **`# botstrap aliases`**, and **`# botstrap functions`** blocks from **`~/.zshrc`** and **`~/.bashrc`**, removes the **`# botstrap git-aliases`** **`[include]`** block from **`~/.gitconfig`**, deletes **`~/.config/botstrap/git-aliases`**, and deletes **`~/.config/botstrap/env.sh`**. **Windows:** removes **`# botstrap PATH`**, **`# botstrap starship`**, **`# botstrap zoxide`**, and **`# botstrap aliases`** regions from **each** PowerShell profile path Botstrap manages (same set as Phase 3), plus the git alias include and fragment as on Unix. **Does not** uninstall packages (Homebrew, apt, winget, mise, etc.) or remove other files Botstrap copied elsewhere (e.g. **`~/.config/starship.toml`**, editor settings, the rest of **`~/.gitconfig`**). With **`--purge`**, also deletes **`git-aliases.env`**. **Exit 1** if the user cancels, a guard refuses **`--remove-checkout`**, or usage is invalid. | | `botstrap` (no arguments) | If **stdin** and **stdout** are TTYs (**Bash**) or the console is not redirected (**PowerShell**) **and** **`gum`** is on **`PATH`**, shows a **`gum choose`** menu for **`update`**, **`self-update`**, **`reconfigure`**, **`doctor`**, **`uninstall`**, **`version`**, or **`quit`** (exit **0**). Otherwise prints **usage** and exits **1**. For automation and **AI agents**, pass an explicit subcommand (e.g. **`botstrap doctor`**, **`botstrap update --all`**) instead of relying on the menu. | | Any other first argument | Prints **usage** and exits with code **1**. | @@ -46,6 +46,7 @@ Set by **`install/phase-2-tui.sh`** (or defaults when gum is missing). Group ids |----------|---------| | `BOTSTRAP_GIT_NAME` | Global Git `user.name` (Phase 3). | | `BOTSTRAP_GIT_EMAIL` | Global Git `user.email` (Phase 3). | +| `BOTSTRAP_GIT_ALIASES` | Comma-separated alias **`id`** values from **`configs/git/aliases.yaml`** (e.g. `st,co,lg`), or **`none`** to skip. Interactive Phase 2 shows a preview and multi-select; non-interactive default: all catalog entries with **`default: true`** when **`defaults.enabled`** is true (or persisted **`selected=`** from **`git-aliases.env`** on reconfigure). | | `BOTSTRAP_CORE_TOOLS` | Comma-separated tool **`name`** values from **`registry/core.yaml`** to install in Phase 3 (registry order). Set by the TUI (default: all names) or non-interactive defaults; may be preset for automation. | | `BOTSTRAP_EDITOR` | One of: `cursor`, `vscode`, `neovim`, `zed`, `none`. | | `BOTSTRAP_LANGUAGES` | Comma-separated mise-related choices: `node`, `python`, `ruby`, `go`, `rust`, `java`, `elixir`, `php`, `none`, … | @@ -72,6 +73,8 @@ Unless otherwise noted, paths are under **`$HOME`**. | `~/.config/starship.toml` | Overwritten from `configs/shell/prompt.toml` when that file exists in the repo. | | `~/.gitignore_global` | Copied from `configs/git/gitignore_global`; `core.excludesfile` set globally. | | Git user.name / user.email | Set from `BOTSTRAP_GIT_*` when non-empty. | +| Git aliases | Selected ids from **`BOTSTRAP_GIT_ALIASES`** written to **`~/.config/botstrap/git-aliases`**; **`~/.gitconfig`** gets a one-time **`# botstrap git-aliases`** **`[include]`** pointing at that file. Existing global aliases with the same name are skipped unless Botstrap previously managed them. | +| `~/.config/botstrap/git-aliases.env` | **`selected=`** and **`managed=`** comma-separated alias ids (Phase 3) for reconfigure TUI defaults and conflict detection. | | `~/.zshrc`, `~/.bashrc` | Appended **once** (marker-guarded) with contents of `configs/shell/aliases`, `configs/shell/functions`, and `configs/shell/env_path_snippet.bash` when those repo files exist. The PATH snippet sources **`~/.config/botstrap/env.sh`**. | | `~/.config/botstrap/core-tools.env` | **`core_tools=`** comma-separated list (persisted Phase 3) for **`botstrap doctor`** / reconfigure default core selection when **`BOTSTRAP_CORE_TOOLS`** is not set in the shell. | | `~/.config/botstrap/optional-selections.env` | **`languages=`**, **`databases=`**, **`ai_tools=`**, **`optional_apps=`** (Phase 3) for **`botstrap update --tools`** and TUI **`--selected`** defaults on reconfigure. | @@ -88,6 +91,7 @@ Paths use **`%USERPROFILE%`** where relevant. |--------|-----------| | **`%USERPROFILE%\.config\botstrap\core-tools.env`** | **`core_tools=`** persisted list (Phase 3) for **`doctor`** / TUI defaults when **`BOTSTRAP_CORE_TOOLS`** is unset. | | **`%USERPROFILE%\.config\botstrap\optional-selections.env`** | **`languages=`**, **`databases=`**, **`ai_tools=`**, **`optional_apps=`** for **`update --tools`** and reconfigure TUI defaults. | +| Git aliases | Same as Unix: **`%USERPROFILE%\.config\botstrap\git-aliases`**, include block in **`%USERPROFILE%\.gitconfig`**, **`git-aliases.env`** for persistence. | | Editor configs (**`BOTSTRAP_EDITOR`**) | **cursor** / **vscode:** under **`%USERPROFILE%`** as in [Configuration file map](./CONFIGURATION.md). **neovim:** **`%LOCALAPPDATA%\nvim`** — LazyVim from core tool **`neovim`** (**`install/modules/lazyvim.ps1`**); minimal **`init.lua`** only when **`lua\config\lazy.lua`** is missing. | | Zellij **`config.kdl`** (`default_shell`) | When **`zellij`** is in **`BOTSTRAP_CORE_TOOLS`**: Phase 3 runs **`install/modules/zellij.ps1`** which sets **`default_shell`** to the resolved `pwsh`/`powershell` path in **`config.kdl`** (location from `zellij setup --check`; fallback **`%USERPROFILE%\.config\zellij\config.kdl`**). Marker comment `// botstrap: default_shell (Windows)` guards repeated runs. | | PowerShell **profiles** (dual host): **`Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1`** (Windows PowerShell 5.1) and **`Documents\PowerShell\Microsoft.PowerShell_profile.ps1`** (**`pwsh`** 7+), plus **`$PROFILE`** when it differs (e.g. VS Code host) | Each file is updated **once** (marker-guarded) with the same blocks: **`# botstrap PATH`** sets **`$env:BOTSTRAP_ROOT`**, prepends **`$BOTSTRAP_ROOT\bin`** to **`$env:PATH`**, and defines **`function Global:botstrap`** invoking **`bin\botstrap.ps1`**. Also **`# botstrap starship`**, **`# botstrap zoxide`**, and **`# botstrap aliases`** when missing. | diff --git a/install/phase-2-tui.ps1 b/install/phase-2-tui.ps1 index ed136a7..9af1ec5 100644 --- a/install/phase-2-tui.ps1 +++ b/install/phase-2-tui.ps1 @@ -45,6 +45,22 @@ if (-not (Get-Command gum -ErrorAction SilentlyContinue)) { if ($om.Count -gt 0) { $env:BOTSTRAP_OPTIONAL_APPS = ($om[0] -replace '^\s*optional_apps=', '').Trim() } } if (-not $env:BOTSTRAP_OPTIONAL_APPS) { $env:BOTSTRAP_OPTIONAL_APPS = '' } + . (Join-Path $env:BOTSTRAP_ROOT 'lib\git-aliases.ps1') + if (-not $env:BOTSTRAP_GIT_ALIASES) { + $gitAliasesEnv = Get-BotstrapGitAliasesEnvPath + if (Test-Path -LiteralPath $gitAliasesEnv) { + $gaMatch = @(Get-Content -LiteralPath $gitAliasesEnv -ErrorAction SilentlyContinue | Where-Object { $_ -match '^\s*selected=' } | Select-Object -First 1) + if ($gaMatch.Count -gt 0) { + $env:BOTSTRAP_GIT_ALIASES = ($gaMatch[0] -replace '^\s*selected=', '').Trim() + } + else { + $env:BOTSTRAP_GIT_ALIASES = Get-BotstrapGitAliasesDefaultCsv + } + } + else { + $env:BOTSTRAP_GIT_ALIASES = Get-BotstrapGitAliasesDefaultCsv + } + } return } @@ -83,6 +99,72 @@ if (-not $env:BOTSTRAP_GIT_EMAIL) { $env:BOTSTRAP_GIT_EMAIL = & gum input --placeholder $gitEmailPlaceholder @emailArgs } +. (Join-Path $env:BOTSTRAP_ROOT 'lib\git-aliases.ps1') +if (-not $env:BOTSTRAP_GIT_ALIASES) { + $gitAliasLabels = @(Get-BotstrapGitAliasesChooseLabels) + if ($gitAliasLabels.Count -gt 0) { + $previewLines = @(Get-BotstrapGitAliasesPreviewLines) + $previewText = ($previewLines -join "`n") + $ErrorActionPreference = 'Continue' + & gum style --border normal --padding '0 1' --foreground 212 ` + 'Git shortcuts' '' ` + 'Run git st, git co, and similar aliases from configs/git/aliases.yaml.' '' ` + $previewText + $ErrorActionPreference = 'Stop' + $installAliases = $false + $ErrorActionPreference = 'Continue' + if (& gum confirm 'Install git shortcuts? (git st, git co, …)') { + $installAliases = $true + } + $ErrorActionPreference = 'Stop' + if ($installAliases) { + $gitAliasEnvFile = Get-BotstrapGitAliasesEnvPath + $gitAliasGumArgs = @('choose', '--no-limit', '--ordered', '--header', 'Git shortcuts (git )') + $seedCsv = '' + if (Test-Path -LiteralPath $gitAliasEnvFile) { + $gaSeed = @(Get-Content -LiteralPath $gitAliasEnvFile -ErrorAction SilentlyContinue | Where-Object { $_ -match '^\s*selected=' } | Select-Object -First 1) + if ($gaSeed.Count -gt 0) { + $seedCsv = ($gaSeed[0] -replace '^\s*selected=', '').Trim() + } + } + if ($seedCsv -eq 'none') { + # no pre-selection + } + elseif ($seedCsv) { + foreach ($rawId in $seedCsv.Split(',')) { + $id = $rawId.Trim() + if (-not $id) { continue } + foreach ($label in $gitAliasLabels) { + if ($label.StartsWith("$id → ")) { + $gitAliasGumArgs += @('--selected', $label) + } + } + } + } + else { + $gitAliasGumArgs += @('--selected', '*') + } + $gitAliasGumArgs += $gitAliasLabels + $ErrorActionPreference = 'Continue' + $aliasLines = @( & gum @gitAliasGumArgs ) + $ErrorActionPreference = 'Stop' + $aliasIds = @($aliasLines | ForEach-Object { Get-BotstrapGitAliasIdFromLabel -Label "$_".Trim() } | Where-Object { $_ }) + if ($aliasIds.Count -gt 0) { + $env:BOTSTRAP_GIT_ALIASES = ($aliasIds -join ',') + } + else { + $env:BOTSTRAP_GIT_ALIASES = 'none' + } + } + else { + $env:BOTSTRAP_GIT_ALIASES = 'none' + } + } + else { + $env:BOTSTRAP_GIT_ALIASES = 'none' + } +} + $ErrorActionPreference = 'Stop' $coreYaml = Join-Path $env:BOTSTRAP_ROOT 'registry\core.yaml' $coreNames = @(& yq -r '.tools[].name' $coreYaml 2>$null | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) diff --git a/install/phase-2-tui.sh b/install/phase-2-tui.sh index 7a187ea..fa4774d 100755 --- a/install/phase-2-tui.sh +++ b/install/phase-2-tui.sh @@ -43,6 +43,21 @@ if ! command -v gum &>/dev/null; then export BOTSTRAP_OPTIONAL_APPS="$(grep -m1 '^optional_apps=' "${_opt_sel}" 2>/dev/null | sed 's/^optional_apps=//' || true)" fi export BOTSTRAP_OPTIONAL_APPS="${BOTSTRAP_OPTIONAL_APPS:-}" + # shellcheck source=lib/git-aliases.sh + source "${BOTSTRAP_ROOT}/lib/git-aliases.sh" + _git_aliases_env="$(botstrap_git_aliases_env_path)" + if [[ -z "${BOTSTRAP_GIT_ALIASES:-}" ]]; then + if [[ -f "${_git_aliases_env}" ]]; then + _ga_ln="$(grep -m1 '^selected=' "${_git_aliases_env}" 2>/dev/null || true)" + if [[ -n "${_ga_ln}" ]]; then + export BOTSTRAP_GIT_ALIASES="${_ga_ln#selected=}" + else + export BOTSTRAP_GIT_ALIASES="$(botstrap_git_aliases_default_csv)" + fi + else + export BOTSTRAP_GIT_ALIASES="$(botstrap_git_aliases_default_csv)" + fi + fi exit 0 fi @@ -73,6 +88,72 @@ fi # shellcheck disable=SC2086 # bash32-nounset-empty-array: optional gum args export BOTSTRAP_GIT_EMAIL="${BOTSTRAP_GIT_EMAIL:-$(gum input --placeholder "${_git_email_placeholder}" ${_git_email_args[@]+"${_git_email_args[@]}"})}" +# shellcheck source=lib/git-aliases.sh +source "${BOTSTRAP_ROOT}/lib/git-aliases.sh" +if [[ -z "${BOTSTRAP_GIT_ALIASES:-}" ]]; then + _git_alias_env_file="$(botstrap_git_aliases_env_path)" + _git_alias_labels=() + botstrap_read_lines_to_array _git_alias_labels < <(botstrap_git_aliases_choose_labels) + if [[ ${#_git_alias_labels[@]} -gt 0 ]]; then + _git_alias_preview=() + botstrap_read_lines_to_array _git_alias_preview < <(botstrap_git_aliases_preview_lines) + _git_alias_preview_text="" + if [[ ${#_git_alias_preview[@]} -gt 0 ]]; then + _git_alias_preview_text="$(printf '%s\n' "${_git_alias_preview[@]}")" + fi + gum style --border normal --padding "0 1" --foreground 212 \ + "Git shortcuts" "" \ + "Run git st, git co, and similar aliases from configs/git/aliases.yaml." "" \ + "${_git_alias_preview_text}" + + if gum confirm "Install git shortcuts? (git st, git co, …)"; then + _git_alias_gum_args=() + _git_alias_seed_csv="" + if [[ -f "${_git_alias_env_file}" ]]; then + _ga_seed_ln="$(grep -m1 '^selected=' "${_git_alias_env_file}" 2>/dev/null || true)" + [[ -n "${_ga_seed_ln}" ]] && _git_alias_seed_csv="${_ga_seed_ln#selected=}" + fi + if [[ "${_git_alias_seed_csv}" == "none" ]]; then + : + elif [[ -n "${_git_alias_seed_csv}" ]]; then + IFS=',' read -ra _ga_seed_ids <<<"${_git_alias_seed_csv}" + for _ga_id in "${_ga_seed_ids[@]}"; do + _ga_id="${_ga_id//[[:space:]]/}" + [[ -n "${_ga_id}" ]] || continue + for _ga_label in "${_git_alias_labels[@]}"; do + if [[ "${_ga_label}" == "${_ga_id} → "* ]]; then + _git_alias_gum_args+=(--selected "${_ga_label}") + fi + done + done + else + _git_alias_gum_args=(--selected '*') + fi + _git_alias_lines="$( + # shellcheck disable=SC2086 # bash32-nounset-empty-array: optional gum args + gum choose --no-limit --ordered --header "Git shortcuts (git )" \ + ${_git_alias_gum_args[@]+"${_git_alias_gum_args[@]}"} \ + ${_git_alias_labels[@]+"${_git_alias_labels[@]}"} || true + )" + _git_alias_ids=() + while IFS= read -r _ga_line || [[ -n "${_ga_line}" ]]; do + [[ -n "${_ga_line}" ]] || continue + _git_alias_ids+=("$(botstrap_git_aliases_id_from_label "${_ga_line}")") + done < <(printf '%s\n' "${_git_alias_lines}") + if [[ ${#_git_alias_ids[@]} -gt 0 ]]; then + _git_alias_csv="$(printf '%s,' "${_git_alias_ids[@]}")" + export BOTSTRAP_GIT_ALIASES="${_git_alias_csv%,}" + else + export BOTSTRAP_GIT_ALIASES="none" + fi + else + export BOTSTRAP_GIT_ALIASES="none" + fi + else + export BOTSTRAP_GIT_ALIASES="none" + fi +fi + _core_yaml="${BOTSTRAP_ROOT}/registry/core.yaml" _core_tool_names=() botstrap_read_lines_to_array _core_tool_names < <(yq -r '.tools[].name' "${_core_yaml}") diff --git a/install/phase-3-configure.ps1 b/install/phase-3-configure.ps1 index 725e79c..6789144 100644 --- a/install/phase-3-configure.ps1 +++ b/install/phase-3-configure.ps1 @@ -87,6 +87,9 @@ if (Get-Command git -ErrorAction SilentlyContinue) { } } +. (Join-Path $root 'lib\git-aliases.ps1') +Install-BotstrapGitAliases + $themeStarship = Join-Path $root "themes\$($env:BOTSTRAP_THEME)\starship.toml" $promptTpl = Join-Path $root 'configs\shell\prompt.toml' $starshipOut = Join-Path $configBase 'starship.toml' diff --git a/install/phase-3-configure.sh b/install/phase-3-configure.sh index c62f49f..38ce087 100755 --- a/install/phase-3-configure.sh +++ b/install/phase-3-configure.sh @@ -67,6 +67,10 @@ if [[ -n "${BOTSTRAP_GIT_EMAIL:-}" ]]; then git config --global user.email "${BOTSTRAP_GIT_EMAIL}" 2>/dev/null || true fi +# shellcheck source=lib/git-aliases.sh +source "${BOTSTRAP_ROOT}/lib/git-aliases.sh" +botstrap_git_aliases_apply + _append_block() { local file="$1" local marker="$2" diff --git a/install/uninstall.ps1 b/install/uninstall.ps1 index ec4cbb7..343f36a 100644 --- a/install/uninstall.ps1 +++ b/install/uninstall.ps1 @@ -117,6 +117,14 @@ foreach ($profilePath in (Get-BotstrapWindowsPowerShellProfilePaths)) { Remove-BotstrapProfileBlocks -ProfilePath $profilePath } +. (Join-Path $Root 'lib\git-aliases.ps1') +if ($Purge) { + Uninstall-BotstrapGitAliases -Purge +} +else { + Uninstall-BotstrapGitAliases +} + if ($Purge) { $cfg = Join-Path $env:USERPROFILE '.config\botstrap' if (Test-Path -LiteralPath $cfg) { diff --git a/install/uninstall.sh b/install/uninstall.sh index ade2b59..47a8d5a 100644 --- a/install/uninstall.sh +++ b/install/uninstall.sh @@ -158,6 +158,14 @@ if [[ -f "${env_sh}" ]]; then botstrap_log_info "Removed ${env_sh}" fi +# shellcheck source=lib/git-aliases.sh +source "${ROOT}/lib/git-aliases.sh" +if [[ "${PURGE}" == true ]]; then + botstrap_git_aliases_uninstall --purge +else + botstrap_git_aliases_uninstall +fi + if [[ "${PURGE}" == true ]]; then cfg="${HOME}/.config/botstrap" if [[ -d "${cfg}" ]]; then diff --git a/lib/git-aliases.ps1 b/lib/git-aliases.ps1 new file mode 100644 index 0000000..fede4e0 --- /dev/null +++ b/lib/git-aliases.ps1 @@ -0,0 +1,249 @@ +#requires -Version 5.1 +# Git alias catalog helpers and Phase 3 apply logic (Windows). + +function Get-BotstrapGitAliasesYaml { + Join-Path $env:BOTSTRAP_ROOT 'configs\git\aliases.yaml' +} + +function Get-BotstrapGitAliasesConfigDir { + Join-Path $env:USERPROFILE '.config\botstrap' +} + +function Get-BotstrapGitAliasesFragmentPath { + Join-Path (Get-BotstrapGitAliasesConfigDir) 'git-aliases' +} + +function Get-BotstrapGitAliasesEnvPath { + Join-Path (Get-BotstrapGitAliasesConfigDir) 'git-aliases.env' +} + +function Get-BotstrapGitAliasesUserGitconfig { + Join-Path $env:USERPROFILE '.gitconfig' +} + +function Get-BotstrapGitAliasesDefaultCsv { + $yaml = Get-BotstrapGitAliasesYaml + if (-not (Test-Path -LiteralPath $yaml)) { return '' } + $enabled = & yq -r '.defaults.enabled // true' $yaml 2>$null + if ("$enabled" -ne 'true') { return '' } + $ids = @(& yq -r '.aliases[] | select(.default == true) | .id' $yaml 2>$null | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) + return ($ids -join ',') +} + +function Get-BotstrapGitAliasEntry { + param([Parameter(Mandatory)][string]$Id) + $yaml = Get-BotstrapGitAliasesYaml + if (-not (Test-Path -LiteralPath $yaml)) { return $null } + $env:BOTSTRAP_ALIAS_ID = $Id + $name = & yq -r '.aliases[] | select(.id == strenv(BOTSTRAP_ALIAS_ID)) | .name' $yaml 2>$null + $command = & yq -r '.aliases[] | select(.id == strenv(BOTSTRAP_ALIAS_ID)) | .command' $yaml 2>$null + Remove-Item Env:BOTSTRAP_ALIAS_ID -ErrorAction SilentlyContinue + if ([string]::IsNullOrWhiteSpace("$name")) { return $null } + return [pscustomobject]@{ Name = "$name".Trim(); Command = "$command".Trim() } +} + +function Get-BotstrapGitAliasesPreviewLines { + $yaml = Get-BotstrapGitAliasesYaml + if (-not (Test-Path -LiteralPath $yaml)) { return @() } + return @(& yq -r '.aliases[] | "\(.id) → \(.command) — \(.description)"' $yaml 2>$null | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) +} + +function Get-BotstrapGitAliasesChooseLabels { + $yaml = Get-BotstrapGitAliasesYaml + if (-not (Test-Path -LiteralPath $yaml)) { return @() } + return @(& yq -r '.aliases[] | "\(.id) → \(.command)"' $yaml 2>$null | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) +} + +function Get-BotstrapGitAliasIdFromLabel { + param([Parameter(Mandatory)][string]$Label) + if ($Label -match ' → ') { + return $Label.Split(' → ', 2)[0].Trim() + } + return $Label.Trim() +} + +function Get-BotstrapGitAliasesManagedCsv { + $envFile = Get-BotstrapGitAliasesEnvPath + if (-not (Test-Path -LiteralPath $envFile)) { return '' } + $line = @(Get-Content -LiteralPath $envFile -ErrorAction SilentlyContinue | Where-Object { $_ -match '^\s*managed=' } | Select-Object -First 1) + if ($line.Count -eq 0) { return '' } + return ($line[0] -replace '^\s*managed=', '').Trim() +} + +function Test-BotstrapGitAliasesCsvContains { + param( + [string]$Csv, + [Parameter(Mandatory)][string]$Needle + ) + if ([string]::IsNullOrWhiteSpace($Csv)) { return $false } + foreach ($part in ($Csv -split ',')) { + if ($part.Trim() -eq $Needle) { return $true } + } + return $false +} + +function Add-BotstrapGitAliasesInclude { + $gitconfig = Get-BotstrapGitAliasesUserGitconfig + $fragment = Get-BotstrapGitAliasesFragmentPath + $marker = '# botstrap git-aliases' + if (Test-Path -LiteralPath $gitconfig) { + $raw = Get-Content -LiteralPath $gitconfig -Raw -ErrorAction SilentlyContinue + if ($raw -and $raw.Contains($marker)) { return } + } + $block = @" + +$marker +[include] + path = $fragment +"@ + Add-Content -LiteralPath $gitconfig -Value $block -Encoding utf8 + Write-BotstrapInfo "Added git alias include to $gitconfig" +} + +function Remove-BotstrapGitAliasesInclude { + param([Parameter(Mandatory)][string]$GitconfigPath) + if (-not (Test-Path -LiteralPath $GitconfigPath)) { return } + $lines = @(Get-Content -LiteralPath $GitconfigPath -ErrorAction SilentlyContinue) + if ($lines.Count -eq 0) { return } + $out = New-Object System.Collections.Generic.List[string] + $skip = $false + foreach ($line in $lines) { + if ($line -eq '# botstrap git-aliases') { + $skip = $true + continue + } + if ($skip) { + if ($line -match '^\[include\]') { continue } + if ($line -match '^\s*path\s*=') { + $skip = $false + continue + } + if ([string]::IsNullOrWhiteSpace($line)) { + $skip = $false + continue + } + $skip = $false + } + [void]$out.Add($line) + } + $newText = ($out -join "`n").TrimEnd() + $oldText = ($lines -join "`n").TrimEnd() + if ($newText -ne $oldText) { + Set-Content -LiteralPath $GitconfigPath -Value $newText -Encoding utf8 + Write-BotstrapInfo "Removed git alias include from $GitconfigPath" + } +} + +function Install-BotstrapGitAliases { + $selection = $env:BOTSTRAP_GIT_ALIASES + $yaml = Get-BotstrapGitAliasesYaml + $fragment = Get-BotstrapGitAliasesFragmentPath + $envFile = Get-BotstrapGitAliasesEnvPath + $configDir = Get-BotstrapGitAliasesConfigDir + $managedCsv = Get-BotstrapGitAliasesManagedCsv + $gitconfig = Get-BotstrapGitAliasesUserGitconfig + + New-Item -ItemType Directory -Force -Path $configDir | Out-Null + + if ($selection -eq 'none') { + Remove-Item -LiteralPath $fragment -Force -ErrorAction SilentlyContinue + Remove-BotstrapGitAliasesInclude -GitconfigPath $gitconfig + @( + '# Generated by Botstrap phase 3; git alias selection for reconfigure.', + 'selected=none', + 'managed=' + ) | Set-Content -LiteralPath $envFile -Encoding utf8 + Write-BotstrapInfo 'Git aliases skipped (BOTSTRAP_GIT_ALIASES=none).' + return + } + + if ([string]::IsNullOrWhiteSpace($selection)) { + $selection = Get-BotstrapGitAliasesDefaultCsv + } + + if ([string]::IsNullOrWhiteSpace($selection)) { + Remove-Item -LiteralPath $fragment -Force -ErrorAction SilentlyContinue + Remove-BotstrapGitAliasesInclude -GitconfigPath $gitconfig + @( + '# Generated by Botstrap phase 3; git alias selection for reconfigure.', + 'selected=', + 'managed=' + ) | Set-Content -LiteralPath $envFile -Encoding utf8 + return + } + + if (-not (Test-Path -LiteralPath $yaml)) { + Write-BotstrapWarn "Git alias catalog missing: $yaml" + return + } + + if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + Write-BotstrapWarn 'git not on PATH; skipping git alias setup.' + return + } + + $applied = New-Object System.Collections.Generic.List[string] + $iniLines = New-Object System.Collections.Generic.List[string] + [void]$iniLines.Add('[alias]') + + foreach ($id in ($selection -split ',')) { + $id = $id.Trim() + if (-not $id) { continue } + $entry = Get-BotstrapGitAliasEntry -Id $id + if (-not $entry) { + Write-BotstrapWarn "Unknown git alias id: $id" + continue + } + $prevEa = $ErrorActionPreference + $ErrorActionPreference = 'SilentlyContinue' + $existing = & git config --global --get "alias.$($entry.Name)" 2>$null + $ErrorActionPreference = $prevEa + if ($existing -and -not (Test-BotstrapGitAliasesCsvContains -Csv $managedCsv -Needle $entry.Name)) { + Write-BotstrapWarn "Skipping git alias $($entry.Name): already set globally (not managed by Botstrap)." + continue + } + [void]$iniLines.Add("`t$($entry.Name) = $($entry.Command)") + [void]$applied.Add($entry.Name) + } + + if ($applied.Count -eq 0) { + Remove-Item -LiteralPath $fragment -Force -ErrorAction SilentlyContinue + Remove-BotstrapGitAliasesInclude -GitconfigPath $gitconfig + @( + '# Generated by Botstrap phase 3; git alias selection for reconfigure.', + "selected=$selection", + 'managed=' + ) | Set-Content -LiteralPath $envFile -Encoding utf8 + Write-BotstrapInfo 'No git aliases applied (conflicts or empty selection).' + return + } + + $iniLines | Set-Content -LiteralPath $fragment -Encoding utf8 + Add-BotstrapGitAliasesInclude + + $managedOut = ($applied -join ',') + @( + '# Generated by Botstrap phase 3; git alias selection for reconfigure.', + "selected=$selection", + "managed=$managedOut" + ) | Set-Content -LiteralPath $envFile -Encoding utf8 + + Write-BotstrapInfo "Applied $($applied.Count) git alias(es) to $fragment" +} + +function Uninstall-BotstrapGitAliases { + param([switch]$Purge) + $fragment = Get-BotstrapGitAliasesFragmentPath + $envFile = Get-BotstrapGitAliasesEnvPath + $gitconfig = Get-BotstrapGitAliasesUserGitconfig + + Remove-BotstrapGitAliasesInclude -GitconfigPath $gitconfig + if (Test-Path -LiteralPath $fragment) { + Remove-Item -LiteralPath $fragment -Force + Write-BotstrapInfo "Removed $fragment" + } + if ($Purge -and (Test-Path -LiteralPath $envFile)) { + Remove-Item -LiteralPath $envFile -Force + Write-BotstrapInfo "Removed $envFile" + } +} diff --git a/lib/git-aliases.sh b/lib/git-aliases.sh new file mode 100644 index 0000000..3e3975d --- /dev/null +++ b/lib/git-aliases.sh @@ -0,0 +1,271 @@ +#!/usr/bin/env bash +# Git alias catalog helpers and Phase 3 apply logic (Unix). + +: "${BOTSTRAP_ROOT:?BOTSTRAP_ROOT must be set}" + +# shellcheck source=lib/log.sh +source "${BOTSTRAP_ROOT}/lib/log.sh" +# shellcheck source=lib/bash-compat.sh +source "${BOTSTRAP_ROOT}/lib/bash-compat.sh" + +botstrap_git_aliases_yaml() { + printf '%s/configs/git/aliases.yaml' "${BOTSTRAP_ROOT}" +} + +botstrap_git_aliases_config_dir() { + printf '%s/.config/botstrap' "${HOME}" +} + +botstrap_git_aliases_fragment_path() { + printf '%s/.config/botstrap/git-aliases' "${HOME}" +} + +botstrap_git_aliases_env_path() { + printf '%s/.config/botstrap/git-aliases.env' "${HOME}" +} + +botstrap_git_aliases_user_gitconfig() { + printf '%s/.gitconfig' "${HOME}" +} + +# CSV of alias ids with default: true when defaults.enabled is true; empty otherwise. +botstrap_git_aliases_default_csv() { + local yaml enabled + yaml="$(botstrap_git_aliases_yaml)" + [[ -f "${yaml}" ]] || return 0 + enabled="$(yq -r '.defaults.enabled // true' "${yaml}" 2>/dev/null || echo true)" + [[ "${enabled}" == "true" ]] || return 0 + yq -r '.aliases[] | select(.default == true) | .id' "${yaml}" 2>/dev/null | paste -sd, - +} + +# Resolve id to "namecommand" from catalog; prints nothing if unknown. +botstrap_git_aliases_lookup() { + local id="$1" + local yaml + yaml="$(botstrap_git_aliases_yaml)" + [[ -f "${yaml}" ]] || return 1 + yq -r ".aliases[] | select(.id == \"${id}\") | [.name, .command] | @tsv" "${yaml}" 2>/dev/null || true +} + +# Build preview lines: "id → command — description" +botstrap_git_aliases_preview_lines() { + local yaml + yaml="$(botstrap_git_aliases_yaml)" + [[ -f "${yaml}" ]] || return 0 + yq -r '.aliases[] | "\(.id) → \(.command) — \(.description)"' "${yaml}" 2>/dev/null || true +} + +# gum choose labels: "id → command" +botstrap_git_aliases_choose_labels() { + local yaml + yaml="$(botstrap_git_aliases_yaml)" + [[ -f "${yaml}" ]] || return 0 + yq -r '.aliases[] | "\(.id) → \(.command)"' "${yaml}" 2>/dev/null || true +} + +# Extract alias id from a gum choose label ("id → command"). +botstrap_git_aliases_id_from_label() { + local label="$1" + printf '%s' "${label%% → *}" +} + +# Read managed= csv from git-aliases.env (may be empty). +botstrap_git_aliases_read_managed_csv() { + local env_file line + env_file="$(botstrap_git_aliases_env_path)" + [[ -f "${env_file}" ]] || return 0 + line="$(grep -m1 '^managed=' "${env_file}" 2>/dev/null || true)" + [[ -n "${line}" ]] || return 0 + printf '%s' "${line#managed=}" +} + +botstrap_git_aliases_csv_contains() { + local csv="$1" + local needle="$2" + local part + IFS=',' read -ra _parts <<<"${csv}" + for part in "${_parts[@]}"; do + part="${part//[[:space:]]/}" + [[ "${part}" == "${needle}" ]] && return 0 + done + return 1 +} + +botstrap_git_aliases_ensure_include() { + local gitconfig fragment marker + gitconfig="$(botstrap_git_aliases_user_gitconfig)" + fragment="$(botstrap_git_aliases_fragment_path)" + marker='# botstrap git-aliases' + + if grep -qF "${marker}" "${gitconfig}" 2>/dev/null; then + return 0 + fi + + { + printf '\n%s\n' "${marker}" + printf '[include]\n' + printf '\tpath = %s\n' "${fragment}" + } >>"${gitconfig}" + botstrap_log_info "Added git alias include to ${gitconfig}" +} + +botstrap_git_aliases_remove_include() { + local gitconfig="$1" + local tmp skip line + + [[ -f "${gitconfig}" ]] || return 0 + tmp="$(mktemp)" + skip=false + while IFS= read -r line || [[ -n "${line}" ]]; do + if [[ "${line}" == '# botstrap git-aliases' ]]; then + skip=true + continue + fi + if [[ "${skip}" == true ]]; then + if [[ "${line}" =~ ^\[include\] ]]; then + continue + fi + if [[ "${line}" =~ ^[[:space:]]*path[[:space:]]*= ]]; then + skip=false + continue + fi + if [[ -z "${line}" ]]; then + skip=false + continue + fi + skip=false + fi + printf '%s\n' "${line}" + done <"${gitconfig}" >"${tmp}" + if ! cmp -s "${gitconfig}" "${tmp}"; then + mv -f "${tmp}" "${gitconfig}" + botstrap_log_info "Removed git alias include from ${gitconfig}" + else + rm -f "${tmp}" + fi +} + +botstrap_git_aliases_apply() { + local selection="${BOTSTRAP_GIT_ALIASES:-}" + local yaml fragment env_file config_dir managed_csv + local -a applied_ids=() + local id lookup name command existing + + yaml="$(botstrap_git_aliases_yaml)" + fragment="$(botstrap_git_aliases_fragment_path)" + env_file="$(botstrap_git_aliases_env_path)" + config_dir="$(botstrap_git_aliases_config_dir)" + managed_csv="$(botstrap_git_aliases_read_managed_csv)" + + mkdir -p "${config_dir}" + + if [[ "${selection}" == "none" ]]; then + rm -f "${fragment}" + botstrap_git_aliases_remove_include "$(botstrap_git_aliases_user_gitconfig)" + { + printf '%s\n' '# Generated by Botstrap phase 3; git alias selection for reconfigure.' + printf 'selected=none\n' + printf 'managed=\n' + } >"${env_file}" + botstrap_log_info 'Git aliases skipped (BOTSTRAP_GIT_ALIASES=none).' + return 0 + fi + + if [[ -z "${selection}" ]]; then + selection="$(botstrap_git_aliases_default_csv)" + fi + + if [[ -z "${selection}" ]]; then + rm -f "${fragment}" + botstrap_git_aliases_remove_include "$(botstrap_git_aliases_user_gitconfig)" + { + printf '%s\n' '# Generated by Botstrap phase 3; git alias selection for reconfigure.' + printf 'selected=\n' + printf 'managed=\n' + } >"${env_file}" + return 0 + fi + + if [[ ! -f "${yaml}" ]]; then + botstrap_log_warn "Git alias catalog missing: ${yaml}" + return 0 + fi + + if ! command -v git &>/dev/null; then + botstrap_log_warn 'git not on PATH; skipping git alias setup.' + return 0 + fi + + { + printf '%s\n' '[alias]' + } >"${fragment}.tmp" + + IFS=',' read -ra _sel_ids <<<"${selection}" + for id in "${_sel_ids[@]}"; do + id="${id//[[:space:]]/}" + [[ -n "${id}" ]] || continue + + lookup="$(botstrap_git_aliases_lookup "${id}")" + if [[ -z "${lookup}" ]]; then + botstrap_log_warn "Unknown git alias id: ${id}" + continue + fi + name="${lookup%%$'\t'*}" + command="${lookup#*$'\t'}" + + existing="$(git config --global --get "alias.${name}" 2>/dev/null || true)" + if [[ -n "${existing}" ]] && ! botstrap_git_aliases_csv_contains "${managed_csv}" "${name}"; then + botstrap_log_warn "Skipping git alias ${name}: already set globally (not managed by Botstrap)." + continue + fi + + printf '\t%s = %s\n' "${name}" "${command}" >>"${fragment}.tmp" + applied_ids+=("${name}") + done + + if [[ ${#applied_ids[@]} -eq 0 ]]; then + rm -f "${fragment}.tmp" + rm -f "${fragment}" + botstrap_git_aliases_remove_include "$(botstrap_git_aliases_user_gitconfig)" + { + printf '%s\n' '# Generated by Botstrap phase 3; git alias selection for reconfigure.' + printf 'selected=%s\n' "${selection}" + printf 'managed=\n' + } >"${env_file}" + botstrap_log_info 'No git aliases applied (conflicts or empty selection).' + return 0 + fi + + mv -f "${fragment}.tmp" "${fragment}" + botstrap_git_aliases_ensure_include + + local applied_csv managed_out + applied_csv="$(printf '%s,' "${applied_ids[@]}")" + applied_csv="${applied_csv%,}" + managed_out="${applied_csv}" + + { + printf '%s\n' '# Generated by Botstrap phase 3; git alias selection for reconfigure.' + printf 'selected=%s\n' "${selection}" + printf 'managed=%s\n' "${managed_out}" + } >"${env_file}" + + botstrap_log_info "Applied ${#applied_ids[@]} git alias(es) to ${fragment}" +} + +botstrap_git_aliases_uninstall() { + local fragment env_file gitconfig + fragment="$(botstrap_git_aliases_fragment_path)" + env_file="$(botstrap_git_aliases_env_path)" + gitconfig="$(botstrap_git_aliases_user_gitconfig)" + + botstrap_git_aliases_remove_include "${gitconfig}" + if [[ -f "${fragment}" ]]; then + rm -f "${fragment}" + botstrap_log_info "Removed ${fragment}" + fi + if [[ "${1:-}" == "--purge" && -f "${env_file}" ]]; then + rm -f "${env_file}" + botstrap_log_info "Removed ${env_file}" + fi +}