From 3da68e5541ac04b4469a376b14a030130461dc57 Mon Sep 17 00:00:00 2001 From: Lucas Mendelowski Date: Sat, 15 Aug 2026 16:32:40 +0200 Subject: [PATCH 1/6] Simplify dotfile utilities --- .github/workflows/ci.yml | 62 ++++++++ README.md | 10 ++ bin/git-fe | 12 ++ bin/gw | 303 --------------------------------------- bin/{rp => photon} | 25 ++-- fish/completions/gw.fish | 22 --- git/config | 2 +- 7 files changed, 95 insertions(+), 341 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100755 bin/git-fe delete mode 100755 bin/gw rename bin/{rp => photon} (79%) delete mode 100644 fish/completions/gw.fish diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4dd2d41 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install linters + run: | + sudo apt-get update + sudo apt-get install --yes fish shellcheck + + - name: Check Bash + run: | + bash -n bin/* bootstrap/linux/* bootstrap/macos/* + shellcheck --external-sources --exclude=SC2155 bin/* bootstrap/linux/* bootstrap/macos/* + + - name: Check Fish + run: fish -n fish/config.fish fish/conf.d/*.fish fish/functions/*.fish + + - name: Check PowerShell + shell: pwsh + run: | + $tokens = $null + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path ./windows.ps1), + [ref]$tokens, + [ref]$errors + ) | Out-Null + + if ($errors.Count -gt 0) { + $errors | ForEach-Object { Write-Error $_ } + exit 1 + } + + - name: Check TOML + run: | + python3 - <<'PY' + import pathlib + import tomllib + + for path in pathlib.Path('.').rglob('*.toml'): + if '.git' not in path.parts and 'tmux/plugins/tpm' not in path.as_posix(): + with path.open('rb') as config: + tomllib.load(config) + PY + + - name: Check whitespace + run: git diff --check diff --git a/README.md b/README.md index 313ca42..0fc267a 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,13 @@ Preview bootstrap changes or inspect current state: mise bootstrap --dry-run mise bootstrap status ``` + +## Utilities + +Rename photos from their EXIF timestamps, without changing anything first: + +```sh +photon /path/to/photos --dry-run +``` + +Remove `--dry-run` to apply names in the `IMG_yyyyMMdd_HHmmss` format. Duplicate timestamps receive a numeric suffix. diff --git a/bin/git-fe b/bin/git-fe new file mode 100755 index 0000000..d8a8a11 --- /dev/null +++ b/bin/git-fe @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# Fetch remotes and delete local branches whose upstream no longer exists. + +set -euo pipefail + +git fetch --prune +git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads | + awk '$NF == "[gone]" {print $1}' | + while IFS= read -r branch; do + git branch -D -- "$branch" + done diff --git a/bin/gw b/bin/gw deleted file mode 100755 index 29f1dd9..0000000 --- a/bin/gw +++ /dev/null @@ -1,303 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Die with 1 exit code (defined first so the variable block below can use it). -die() { - echo "gw (error): $*" >&2 - exit 1 -} - -# Variables -#------------------------------------------------------------------------------ -CURRENT_WORKTREE="$(git rev-parse --show-toplevel 2>/dev/null || die "not inside a git worktree")" -# Worktree name pool, also the WORKTREE_ID space (assign_slot indexes into it). -# 16 names map onto Redis's default 16 databases (0-15); WORKTREE_ID is a DB. -NAME_POOL=(black blue brown cyan gray green lime magenta navy olive orange pink purple red white yellow) -SYMLINK_DIRS=(node_modules vendor/bundle) -REPO_ROOT="$(git worktree list --porcelain | sed -n '1s/^worktree //p')" -WORKTREES_DIR="$REPO_ROOT/.worktrees" - -# Worktree utilities -#------------------------------------------------------------------------------ -# Check if a directory is in the list of symlinked directories (e.g. node_modules). -is_symlink_dir() { - local candidate="$1" - local dir - - for dir in "${SYMLINK_DIRS[@]}"; do - [[ "$candidate" == "$dir" ]] && return 0 - done - - return 1 -} - -# Assign name for branch: prefer the hash of the branch, fallback to the first available name. -# Stable per branch (reclaims its preferred slot when free) and never collides -# with a live worktree. -assign_slot() { - local branch="$1" - local size=${#NAME_POOL[@]} - local sum preferred offset index - - sum="$(printf '%s' "$branch" | cksum | cut -d' ' -f1)" - preferred=$(( sum % size )) - - for (( offset = 0; offset < size; offset++ )); do - index=$(( (preferred + offset) % size )) - [[ -e "$WORKTREES_DIR/${NAME_POOL[$index]}" ]] || { echo "$index"; return; } - done - - die "all worktree slots are in use" -} - -# Print current branch for a worktree path (empty on detached HEAD). -worktree_branch() { - [[ -d "$1" ]] || return 1 - git -C "$1" branch --show-current 2>/dev/null || true -} - -# Find worktree name associated with $1 path -worktree_name() { - local path="$1" - - # Verify worktrees path - [[ "$path" == "$REPO_ROOT/.worktrees/"* ]] || return 1 - - # Remove worktrees path prefix. - path="${path#"$REPO_ROOT/.worktrees/"}" - # After removing the .worktrees prefix, expect a non-empty single path segment. - [[ "$path" != */* && -n "$path" ]] || return 1 - - echo "$path" -} - -# Return path to worktree with $1 name -worktree_path() { - local name="$1" - - [[ -d "$WORKTREES_DIR/$name" ]] || die "no worktree named '$name'" - echo "$WORKTREES_DIR/$name" -} - -# Remove a linked worktree and delete its local branch when present. -worktree_remove() { - local path="$1" - local branch - - branch="$(worktree_branch "$path")" - - # Run the remove hook while the worktree still exists (e.g. drop its database). - run_task_hook "$path" "worktree:remove" - - cd "$REPO_ROOT" - git worktree remove --force "$path" >&2 - if [[ -n "$branch" ]] && git show-ref --verify --quiet "refs/heads/$branch"; then - git branch -D "$branch" >&2 - fi - - echo "$REPO_ROOT" -} - -# Worktree setup -#------------------------------------------------------------------------------ -# Print entries listed in .worktreeinclude, falling back to mise vars.worktreeinclude. -included_file_entries() { - local manifest="$REPO_ROOT/.worktreeinclude" - - if [[ -f "$manifest" ]]; then - cat "$manifest" - return 0 - fi - - - # Defined as multi-line string in mise config file - # [vars] - # worktreeinclude = """ - # .env - # .env.local - # mise.toml - # node_modules - # vendor/bundle - # """ - command -v mise >/dev/null 2>&1 || return 0 - mise config get -C "$REPO_ROOT" vars.worktreeinclude 2>/dev/null || true -} - -# Copy entries listed in .worktreeinclude or mise vars.worktreeinclude (if present). -copy_included_files() { - local target_root="$1" - local entry - - while IFS= read -r entry || [[ -n "$entry" ]]; do - # Trim leading/trailing whitespace. - entry="$(printf '%s' "$entry" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" - - [[ -z "$entry" ]] && continue - - if [[ -e "$REPO_ROOT/$entry" ]]; then - mkdir -p "$(dirname "$target_root/$entry")" - if [[ -d "$REPO_ROOT/$entry" ]]; then - if is_symlink_dir "$entry"; then - ln -s "$REPO_ROOT/$entry" "$target_root/$entry" - else - cp -R "$REPO_ROOT/$entry/." "$target_root/$entry" - fi - else - cp -R "$REPO_ROOT/$entry" "$target_root/$entry" - fi - else - echo "gw (warning): included entry '$entry' does not exist" >&2 - fi - done < <(included_file_entries) -} - -# Trust the mise config and install its tools when a mise.toml is present. -setup_mise() { - local path="$1" - - [[ -f "$path/mise.toml" ]] || return 0 - command -v mise >/dev/null 2>&1 || { - echo "gw (warning): mise.toml found but mise is not installed; skipping" >&2 - return 0 - } - - mise trust "$path/mise.toml" >&2 - (cd "$path" && mise install) >&2 -} - -# Write a gitignored mise.local.toml exposing this worktree's identity as vars. -write_mise_local() { - [[ -f "$1/mise.toml" ]] || return 0 - - { - printf '[env]\n' - printf 'WORKTREE_ID = "%s"\n' "$2" - printf 'WORKTREE_NAME = "%s"\n' "$3" - } > "$1/mise.local.toml" -} - -# Run the mise task associated with the worktree hook (e.g. worktree:create) -run_task_hook() { - local path="$1" - local task="$2" - local branch - - command -v mise >/dev/null 2>&1 || return 0 - [[ -f "$path/mise.toml" ]] || return 0 - (cd "$path" && mise tasks info "$task" >/dev/null 2>&1) || return 0 - - branch="$(worktree_branch "$path")" - (cd "$path" && mise run "$task" >&2) || echo "gw (warning): mise task '$task' failed" >&2 -} - -# Commands -#------------------------------------------------------------------------------ -# Add new worktree -cmd_add() { - [[ $# -eq 1 ]] || cmd_usage - - # Accept a remote-qualified name (e.g. origin/feature) and use the short branch name locally. - local branch="${1#origin/}" - local name - local path - local index - - index="$(assign_slot "$branch")" - name="${NAME_POOL[$index]}" - path="$WORKTREES_DIR/$name" - - mkdir -p "$WORKTREES_DIR" - cd "$REPO_ROOT" - # Reuse an existing local branch. - if git show-ref --verify --quiet "refs/heads/$branch"; then - git worktree add -- "$path" "$branch" >&2 - # If only the remote branch exists, create a local tracking branch for it. - elif git show-ref --verify --quiet "refs/remotes/origin/$branch"; then - git worktree add --track -b "$branch" -- "$path" "origin/$branch" >&2 - # Otherwise create a new local branch from the current HEAD. - else - git worktree add -b "$branch" -- "$path" >&2 - fi - - copy_included_files "$path" - write_mise_local "$path" "$index" "$name" - setup_mise "$path" - - run_task_hook "$path" "worktree:create" - - cmd_open "$name" -} - -# Print the path to worktree $1 so the caller can cd into it. -cmd_open() { - [[ $# -eq 1 ]] || cmd_usage - - worktree_path "$1" -} - -# Remove worktree with $1 name or in current directory -cmd_remove() { - [[ $# -le 1 ]] || cmd_usage - - local name="${1:-}" - local path - - if [[ -n "$name" ]]; then - path="$(worktree_path "$name")" - else - path="$CURRENT_WORKTREE" - name="$(worktree_name "$path" || true)" - [[ -n "$name" ]] || die "refusing to remove the primary worktree; pass a linked worktree name" - fi - - worktree_remove "$path" -} - -# List all worktrees -cmd_list() { - [[ $# -eq 0 ]] || cmd_usage - - local path - local name - local branch - local marker - local worktrees - worktrees="$(git worktree list --porcelain | sed -n 's/^worktree //p')" - - while IFS= read -r path; do - name="$(worktree_name "$path" || true)" - [[ -n "$name" ]] || continue - - branch="$(worktree_branch "$path")" - marker=" " - [[ "$path" == "$CURRENT_WORKTREE" ]] && marker="*" - printf '%s %-10s %-30s %s\n' "$marker" "$name" "${branch:-detached}" "$path" - done <<< "$worktrees" -} - -cmd_usage() { - cat >&2 <<'USAGE' -Usage: - gw add - gw remove [] - gw open - gw list -USAGE - exit 1 -} - -# Entrypoint -#------------------------------------------------------------------------------ -[[ $# -gt 0 ]] || cmd_usage - -COMMAND="$1" -shift - -case "$COMMAND" in -add) cmd_add "$@" ;; -remove) cmd_remove "$@" ;; -open) cmd_open "$@" ;; -list) cmd_list "$@" ;; -*) cmd_usage ;; -esac diff --git a/bin/rp b/bin/photon similarity index 79% rename from bin/rp rename to bin/photon index ffaf37c..e8975e3 100755 --- a/bin/rp +++ b/bin/photon @@ -3,7 +3,7 @@ # Batch rename photos to IMG_yyyyMMdd_HHmmss format using EXIF data. # Dependencies: exiftool (brew install exiftool / apt install libimage-exiftool-perl) # -# Usage: rp [--dry-run] +# Usage: photon [--dry-run] set -euo pipefail @@ -11,7 +11,7 @@ DIR="" DRY_RUN=false usage() { - echo "Usage: rp [--dry-run]" + echo "Usage: photon [--dry-run]" exit 1 } @@ -24,8 +24,7 @@ check_deps() { get_datetime() { local file="$1" - local datetime - datetime=$(exiftool -s3 -d '%Y%m%d_%H%M%S' -DateTimeOriginal "$file" 2>/dev/null) + local datetime=$(exiftool -s3 -d '%Y%m%d_%H%M%S' -DateTimeOriginal "$file" 2>/dev/null) [[ -z "$datetime" ]] && datetime=$(exiftool -s3 -d '%Y%m%d_%H%M%S' -CreateDate "$file" 2>/dev/null) [[ -z "$datetime" ]] && datetime=$(exiftool -s3 -d '%Y%m%d_%H%M%S' -FileModifyDate "$file" 2>/dev/null) @@ -39,8 +38,7 @@ build_new_path() { # datetime format: "20240115_143045" -> "IMG_20240115_143045" local new_name="IMG_${datetime}" - local ext - ext=$(echo "${file##*.}" | tr '[:upper:]' '[:lower:]') + local ext=$(echo "${file##*.}" | tr '[:upper:]' '[:lower:]') local new_path="$DIR/${new_name}.${ext}" # Handle duplicates by appending _1, _2, etc. @@ -60,25 +58,22 @@ rename_photos() { for file in "$DIR"/*; do [[ -f "$file" ]] || continue - local mime - mime=$(file --brief --mime-type "$file") + local mime=$(file --brief --mime-type "$file") [[ "$mime" == image/* ]] || continue - local datetime - datetime=$(get_datetime "$file") + local datetime=$(get_datetime "$file") if [[ -z "$datetime" || "$datetime" == "00000000_000000" ]]; then echo "SKIP (no date): $file" - ((skipped++)) + ((++skipped)) continue fi - local new_path - new_path=$(build_new_path "$file" "$datetime") + local new_path=$(build_new_path "$file" "$datetime") if [[ -e "$new_path" && "$file" -ef "$new_path" ]]; then echo "SKIP (already named): $file" - ((skipped++)) + ((++skipped)) continue fi @@ -88,7 +83,7 @@ rename_photos() { mv "$file" "$new_path" echo "RENAMED: $file -> $new_path" fi - ((renamed++)) + ((++renamed)) done echo "" diff --git a/fish/completions/gw.fish b/fish/completions/gw.fish deleted file mode 100644 index 607f3b6..0000000 --- a/fish/completions/gw.fish +++ /dev/null @@ -1,22 +0,0 @@ -# Completions for gw (git worktree helper) - -function __gw_worktree_names - git worktree list --porcelain 2>/dev/null | string replace -r --filter '^worktree .*/\.worktrees/([^/]+)$' '$1' -end - -function __gw_needs_worktree_name - set -l tokens (commandline -opc) - test (count $tokens) -eq 2; and contains -- $tokens[2] open remove -end - -# No file completions by default. -complete -c gw -f - -set -l commands add remove open list - -complete -c gw -n "not __fish_seen_subcommand_from $commands" -a 'add' -d 'Add a new worktree' -complete -c gw -n "not __fish_seen_subcommand_from $commands" -a 'remove' -d 'Remove a worktree' -complete -c gw -n "not __fish_seen_subcommand_from $commands" -a 'open' -d 'Open a worktree' -complete -c gw -n "not __fish_seen_subcommand_from $commands" -a 'list' -d 'List worktrees' - -complete -c gw -n '__gw_needs_worktree_name' -a '(__gw_worktree_names)' diff --git a/git/config b/git/config index d3d3ad7..7b5b1a4 100644 --- a/git/config +++ b/git/config @@ -34,7 +34,7 @@ co = checkout cob = checkout -b # Fetch and delete branches that have been deleted on the remote - fe = "!f() { git fetch -p && git branch -vv | awk '/: gone]/{print $1}' | xargs git br -D; }; f" + fe = !git-fe his = log --oneline --graph --decorate pl = pull --rebase ps = push From 0ebf5683b50958e431e3b6848294a894f2aafe56 Mon Sep 17 00:00:00 2001 From: Lucas Mendelowski Date: Sat, 15 Aug 2026 17:17:54 +0200 Subject: [PATCH 2/6] Fix shellcheck warnings --- bin/worktree-setup | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/worktree-setup b/bin/worktree-setup index 5427e3a..3c58e53 100755 --- a/bin/worktree-setup +++ b/bin/worktree-setup @@ -46,7 +46,6 @@ included_file_entries() { is_checkout() { local prev_head="${1:-}" - local new_head="${2:-}" local checkout_flags="${3:-}" [[ "$prev_head" == "$NULL_SHA" && "$checkout_flags" == "1" ]] @@ -82,7 +81,7 @@ next_worktree_id() { while IFS= read -r path; do id="$(git -C "$path" config --worktree --get worktreeSetup.id 2>/dev/null || true)" if [[ "$id" =~ ^[0-9]+$ ]] && (( id >= 1 && id <= 16 )); then - used[$id]=1 + used[id]=1 fi done < <(git worktree list --porcelain | sed -n 's/^worktree //p') From be54e305bc4a7806ec030666b8690c1f66a01f4e Mon Sep 17 00:00:00 2001 From: Lucas Mendelowski Date: Sat, 15 Aug 2026 18:50:35 +0200 Subject: [PATCH 3/6] Refine update command and CI --- .github/workflows/ci.yml | 13 +++++++++---- bin/{git-fe => get-update} | 2 +- bin/worktree-setup | 10 +++++----- git/config | 5 +++-- 4 files changed, 18 insertions(+), 12 deletions(-) rename bin/{git-fe => get-update} (74%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4dd2d41..ad0f007 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,12 +50,17 @@ jobs: run: | python3 - <<'PY' import pathlib + import subprocess import tomllib - for path in pathlib.Path('.').rglob('*.toml'): - if '.git' not in path.parts and 'tmux/plugins/tpm' not in path.as_posix(): - with path.open('rb') as config: - tomllib.load(config) + paths = [pathlib.Path(filename) for filename in subprocess.check_output( + ['git', 'ls-files', '--', '*.toml'], + text=True, + ).splitlines()] + + for path in paths: + with path.open('rb') as config: + tomllib.load(config) PY - name: Check whitespace diff --git a/bin/git-fe b/bin/get-update similarity index 74% rename from bin/git-fe rename to bin/get-update index d8a8a11..c0ebe4f 100755 --- a/bin/git-fe +++ b/bin/get-update @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Fetch remotes and delete local branches whose upstream no longer exists. +# Fetch remote updates and delete local branches whose upstream no longer exists. set -euo pipefail diff --git a/bin/worktree-setup b/bin/worktree-setup index 3c58e53..c4f1ac3 100755 --- a/bin/worktree-setup +++ b/bin/worktree-setup @@ -11,6 +11,9 @@ WORKTREE_PATH="$PWD" REPO_ROOT="$(git worktree list --porcelain | sed -n '1s/^worktree //p')" SYMLINK_DIRS=(deps node_modules vendor/bundle) ENV_SECTION_PATTERN='^[[:space:]]*\[env\][[:space:]]*(#.*)?$' +PREVIOUS_HEAD="${1:-}" +NEW_HEAD="${2:-}" +BRANCH_CHECKOUT="${3:-}" copy_included_files() { local entry @@ -45,10 +48,7 @@ included_file_entries() { } is_checkout() { - local prev_head="${1:-}" - local checkout_flags="${3:-}" - - [[ "$prev_head" == "$NULL_SHA" && "$checkout_flags" == "1" ]] + [[ "$PREVIOUS_HEAD" == "$NULL_SHA" && "$NEW_HEAD" != "$NULL_SHA" && "$BRANCH_CHECKOUT" == "1" ]] } is_symlink_dir() { @@ -154,7 +154,7 @@ warn() { trap 'warn "setup did not complete"; exit 0' ERR -is_checkout "$@" || exit 0 +is_checkout || exit 0 is_worktree || exit 0 non_bare_repo || exit 0 diff --git a/git/config b/git/config index 7b5b1a4..6b034f1 100644 --- a/git/config +++ b/git/config @@ -33,8 +33,8 @@ cma = !git add -A && git commit --amend --no-edit --reset-author co = checkout cob = checkout -b - # Fetch and delete branches that have been deleted on the remote - fe = !git-fe + # Deprecated alias for `git up` + fe = "!f() { echo 'git fe is deprecated; use git up' >&2; git up \"$@\"; }; f" his = log --oneline --graph --decorate pl = pull --rebase ps = push @@ -42,6 +42,7 @@ sw = !git checkout $(git br | fzf) swa = !git checkout $(git br --all | fzf) un = reset HEAD~1 --soft + up = !get-update wip = !git add -A && git commit -m "WIP" # Open modified or added files in neovim edit = "!git status --porcelain | grep -v '^D' | awk '{print $2 \":1:1: [\" $1 \"]\"}' | nvim -q - +copen" From 438eeb2c599c885b592abb77ff1730361c680e96 Mon Sep 17 00:00:00 2001 From: Lucas Mendelowski Date: Sat, 15 Aug 2026 19:10:53 +0200 Subject: [PATCH 4/6] Rename update script --- bin/{get-update => git-update} | 2 +- git/config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename bin/{get-update => git-update} (74%) diff --git a/bin/get-update b/bin/git-update similarity index 74% rename from bin/get-update rename to bin/git-update index c0ebe4f..b1e1107 100755 --- a/bin/get-update +++ b/bin/git-update @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Fetch remote updates and delete local branches whose upstream no longer exists. +# Fetch updates and delete local branches whose upstream no longer exists. set -euo pipefail diff --git a/git/config b/git/config index 6b034f1..72b682e 100644 --- a/git/config +++ b/git/config @@ -42,7 +42,7 @@ sw = !git checkout $(git br | fzf) swa = !git checkout $(git br --all | fzf) un = reset HEAD~1 --soft - up = !get-update + up = !git-update wip = !git add -A && git commit -m "WIP" # Open modified or added files in neovim edit = "!git status --porcelain | grep -v '^D' | awk '{print $2 \":1:1: [\" $1 \"]\"}' | nvim -q - +copen" From dc6e48e69e45298804688e3b82d30d0693735d1d Mon Sep 17 00:00:00 2001 From: Lucas Mendelowski Date: Sat, 15 Aug 2026 19:19:55 +0200 Subject: [PATCH 5/6] Clarify worktree guards --- bin/worktree-setup | 21 ++++++++++++++------- git/config | 1 - 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/bin/worktree-setup b/bin/worktree-setup index c4f1ac3..5488044 100755 --- a/bin/worktree-setup +++ b/bin/worktree-setup @@ -48,7 +48,11 @@ included_file_entries() { } is_checkout() { - [[ "$PREVIOUS_HEAD" == "$NULL_SHA" && "$NEW_HEAD" != "$NULL_SHA" && "$BRANCH_CHECKOUT" == "1" ]] + local previous_head="$1" + local new_head="$2" + local branch_checkout="$3" + + [[ "$previous_head" == "$NULL_SHA" && "$new_head" != "$NULL_SHA" && "$branch_checkout" == "1" ]] } is_symlink_dir() { @@ -63,15 +67,18 @@ is_symlink_dir() { } is_worktree() { - local git_dir="$(git rev-parse --absolute-git-dir)" - local common_dir="$(cd "$(git rev-parse --git-common-dir)" && pwd)" + local worktree_path="$1" + local git_dir="$(git -C "$worktree_path" rev-parse --absolute-git-dir)" + local common_dir="$(git -C "$worktree_path" rev-parse --path-format=absolute --git-common-dir)" # Git worktrees keep their git dir under the main worktree's .git/worktrees [[ "$git_dir" != "$common_dir" ]] } non_bare_repo() { - [[ "$(git -C "$REPO_ROOT" rev-parse --is-bare-repository)" == "false" ]] + local repo_root="$1" + + [[ "$(git -C "$repo_root" rev-parse --is-bare-repository)" == "false" ]] } next_worktree_id() { @@ -154,9 +161,9 @@ warn() { trap 'warn "setup did not complete"; exit 0' ERR -is_checkout || exit 0 -is_worktree || exit 0 -non_bare_repo || exit 0 +is_checkout "$PREVIOUS_HEAD" "$NEW_HEAD" "$BRANCH_CHECKOUT" || exit 0 +is_worktree "$WORKTREE_PATH" || exit 0 +non_bare_repo "$REPO_ROOT" || exit 0 copy_included_files setup_mise diff --git a/git/config b/git/config index 72b682e..ff521d3 100644 --- a/git/config +++ b/git/config @@ -33,7 +33,6 @@ cma = !git add -A && git commit --amend --no-edit --reset-author co = checkout cob = checkout -b - # Deprecated alias for `git up` fe = "!f() { echo 'git fe is deprecated; use git up' >&2; git up \"$@\"; }; f" his = log --oneline --graph --decorate pl = pull --rebase From 0a2d17789423ca5dc6f88a12828a810f4c7204a4 Mon Sep 17 00:00:00 2001 From: Lucas Mendelowski Date: Sat, 15 Aug 2026 19:29:59 +0200 Subject: [PATCH 6/6] Simplify worktree guards --- bin/worktree-setup | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/bin/worktree-setup b/bin/worktree-setup index 5488044..64eb168 100755 --- a/bin/worktree-setup +++ b/bin/worktree-setup @@ -4,16 +4,18 @@ # Copy all files listed in .worktreeinclude from the main worktree. # Dependency directories such as deps, node_modules and vendor/bundle are symlinked to the main worktree. +# Usage: worktree-setup + set -Eeuo pipefail +BRANCH_CHECKOUT="${3:-}" +ENV_SECTION_PATTERN='^[[:space:]]*\[env\][[:space:]]*(#.*)?$' +NEW_HEAD="${2:-}" NULL_SHA=0000000000000000000000000000000000000000 -WORKTREE_PATH="$PWD" +PREVIOUS_HEAD="${1:-}" REPO_ROOT="$(git worktree list --porcelain | sed -n '1s/^worktree //p')" SYMLINK_DIRS=(deps node_modules vendor/bundle) -ENV_SECTION_PATTERN='^[[:space:]]*\[env\][[:space:]]*(#.*)?$' -PREVIOUS_HEAD="${1:-}" -NEW_HEAD="${2:-}" -BRANCH_CHECKOUT="${3:-}" +WORKTREE_PATH="$PWD" copy_included_files() { local entry @@ -48,11 +50,7 @@ included_file_entries() { } is_checkout() { - local previous_head="$1" - local new_head="$2" - local branch_checkout="$3" - - [[ "$previous_head" == "$NULL_SHA" && "$new_head" != "$NULL_SHA" && "$branch_checkout" == "1" ]] + [[ "$PREVIOUS_HEAD" == "$NULL_SHA" && "$NEW_HEAD" != "$NULL_SHA" && "$BRANCH_CHECKOUT" == "1" ]] } is_symlink_dir() { @@ -67,18 +65,15 @@ is_symlink_dir() { } is_worktree() { - local worktree_path="$1" - local git_dir="$(git -C "$worktree_path" rev-parse --absolute-git-dir)" - local common_dir="$(git -C "$worktree_path" rev-parse --path-format=absolute --git-common-dir)" + local git_dir="$(git -C "$WORKTREE_PATH" rev-parse --absolute-git-dir)" + local common_dir="$(git -C "$WORKTREE_PATH" rev-parse --path-format=absolute --git-common-dir)" # Git worktrees keep their git dir under the main worktree's .git/worktrees [[ "$git_dir" != "$common_dir" ]] } non_bare_repo() { - local repo_root="$1" - - [[ "$(git -C "$repo_root" rev-parse --is-bare-repository)" == "false" ]] + [[ "$(git -C "$REPO_ROOT" rev-parse --is-bare-repository)" == "false" ]] } next_worktree_id() { @@ -161,9 +156,9 @@ warn() { trap 'warn "setup did not complete"; exit 0' ERR -is_checkout "$PREVIOUS_HEAD" "$NEW_HEAD" "$BRANCH_CHECKOUT" || exit 0 -is_worktree "$WORKTREE_PATH" || exit 0 -non_bare_repo "$REPO_ROOT" || exit 0 +is_checkout || exit 0 +is_worktree || exit 0 +non_bare_repo || exit 0 copy_included_files setup_mise