Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/managing-chezmoi/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This repo is edited from inside a nono sandbox. Treat chezmoi source files as th

## Default Workflow

1. **Confirm source ownership**: before editing a file under `~`, use `chezmoi source-path <target>` or inspect whether the source uses `dot_*`, `private_dot_*`, `dot_config/`, `private_dot_config/`, or a template source
1. **Confirm source ownership**: before editing a file under `~`, use `chezmoi source-path <target>` or inspect whether the source uses `dot_*`, `private_dot_*`, `dot_config/`, `private_dot_config/`, or a template source. When `source-path` reports "not managed", search the `run_*` scripts at the repo root for the target path before treating the file as unmanaged: some scripts own keys inside target config files, and the next apply overwrites manual edits to those keys
2. **Inspect state**: use `chezmoi status` and `chezmoi diff` for managed targets; use `git diff` for repo-only files that chezmoi does not manage as targets
3. **Edit source directly**: modify files in this repo (the source state)
4. **Validate safely**: run `chezmoi diff` and `chezmoi apply --dry-run -v` for managed targets; stop at source review and git diff for unmanaged repo files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ This file is a *practical* quick reference for `chezmoi` operations in a dotfile
### 7) Repo-only files

- Some files in this repo are not managed as chezmoi targets.
- If `chezmoi source-path <target>` reports "not managed", validate with source review and `git diff` instead of trying to apply.
- `chezmoi source-path <target>` only knows targets that map to a source file. A target that a `run_*` script writes or edits also reports "not managed".
- Before treating a target as unmanaged, search the `run_*` scripts at the repo root for its path, for example `grep -l 'worktrunk' run_*`. If a script owns the setting, change the value in the script, not in the target file: the script rewrites the target on every apply.
- For repo files that no target or script uses, validate with source review and `git diff` instead of trying to apply.

## Day-to-day commands

Expand Down
22 changes: 21 additions & 1 deletion dot_agents/shell/nono.zsh.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,27 @@ function _nono_agent_run() {
local -a git_common_dir_args=()
local git_common_dir
if git_common_dir="$(command git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"; then
git_common_dir_args=(--allow "$git_common_dir")
# The folder holding .git is the main checkout, or in the bare worktree
# layout (git wtclone) the container of every worktree. Never widen to
# $HOME or / when a repository lives there.
if [[ "${git_common_dir:t}" == .git && "${git_common_dir:h}" != "$HOME" && "${git_common_dir:h}" != / ]]; then
git_common_dir_args=(--allow "${git_common_dir:h}")
else
git_common_dir_args=(--allow "$git_common_dir")
fi

# A classic (non-bare) clone keeps its worktrees in a central folder keyed
# by the main checkout path, matching the worktree-path template that
# run_after_ensure-worktrunk-config.sh writes for worktrunk.
if [[ "$(command git --git-dir="$git_common_dir" config --bool core.bare 2>/dev/null)" != true ]]; then
local main_checkout
main_checkout="$(command git worktree list --porcelain 2>/dev/null | command sed -n '1s/^worktree //p')"
if [[ -n "$main_checkout" ]]; then
local worktrees_dir="$HOME/.local/share/worktrees/${${main_checkout//[\/\\]/-}//./-}"
command mkdir -p "$worktrees_dir" &&
git_common_dir_args+=(--allow "$worktrees_dir")
fi
fi
fi

local -a acp_args=()
Expand Down
125 changes: 125 additions & 0 deletions private_dot_local/bin/executable_git-wtclone
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
set -euo pipefail

# Clone a repository into the bare worktree layout:
# <dir>/.git bare repository
# <dir>/<branch> worktree of the default branch, submodules initialized
# Arguments match git clone: git wtclone [<clone options>] <repo> [<dir>]

die() {
echo "git-wtclone: $*" >&2
exit 1
}

usage() {
echo "usage: git wtclone [<git clone options>] <repo> [<dir>]" >&2
exit 2
}

# git clone options that take their value as a separate argument.
takes_value() {
case "$1" in
-o | --origin | -b | --branch | -u | --upload-pack | --reference | --reference-if-able | \
--depth | --shallow-since | --shallow-exclude | -c | --config | --server-option | \
-j | --jobs | --filter | --bundle-uri | --template | --ref-format | --revision)
return 0
;;
esac
return 1
}

clone_opts=()
positional=()
single_branch=false
while (( $# > 0 )); do
case "$1" in
--)
shift
positional+=("$@")
break
;;
-h | --help)
usage
;;
--bare | --mirror | --separate-git-dir | --separate-git-dir=*)
die "$1 conflicts with the worktree layout"
;;
--recurse-submodules | --recurse-submodules=* | --recursive)
# Submodules are always initialized in the worktree after cloning.
shift
;;
--single-branch)
single_branch=true
clone_opts+=("$1")
shift
;;
-*)
if takes_value "$1"; then
(( $# >= 2 )) || usage
clone_opts+=("$1" "$2")
shift 2
else
clone_opts+=("$1")
shift
fi
;;
*)
positional+=("$1")
shift
;;
esac
done

(( ${#positional[@]} >= 1 && ${#positional[@]} <= 2 )) || usage
repo="${positional[0]}"
dir="${positional[1]:-}"

if [[ -z "$dir" ]]; then
# Same naming rule as git clone: last path component without .git.
dir="${repo%/}"
dir="${dir%/.git}"
dir="${dir%.git}"
dir="${dir##*/}"
dir="${dir##*:}"
[[ -n "$dir" ]] || die "cannot derive a directory name from '$repo'; pass <dir>"
fi

if [[ -e "$dir" && -n "$(ls -A "$dir")" ]]; then
die "destination path '$dir' already exists and is not an empty directory"
fi

git clone --bare ${clone_opts[@]+"${clone_opts[@]}"} -- "$repo" "$dir/.git"

git_dir="$(cd "$dir/.git" && pwd -P)"
container="${git_dir%/.git}"

g() {
git --git-dir="$git_dir" "$@"
}

remote="$(g remote | head -n 1)"
branch="$(g symbolic-ref --short HEAD)"

# A bare clone maps remote branches straight onto local branches and sets no
# fetch refspec. Track them as remote-tracking branches instead, like a normal
# clone, and keep only the checked-out branch local.
if [[ "$single_branch" == true ]]; then
g config "remote.$remote.fetch" "+refs/heads/$branch:refs/remotes/$remote/$branch"
else
g config "remote.$remote.fetch" "+refs/heads/*:refs/remotes/$remote/*"
fi
g fetch --quiet "$remote"
g remote set-head "$remote" --auto >/dev/null 2>&1 ||
echo "git-wtclone: could not set $remote/HEAD; run 'git remote set-head $remote --auto' later" >&2

g for-each-ref --format='%(refname)' refs/heads/ | while IFS= read -r ref; do
[[ "$ref" == "refs/heads/$branch" ]] || g update-ref -d "$ref"
done
g branch --quiet --set-upstream-to="$remote/$branch" "$branch"

# Folder name follows worktrunk's sanitize filter: / and \ become -.
worktree="$container/${branch//[\/\\]/-}"
g worktree add --quiet "$worktree" "$branch"
git -C "$worktree" submodule update --init --recursive --quiet

echo "Cloned into $worktree"
130 changes: 130 additions & 0 deletions private_dot_local/bin/executable_git-wtmigrate
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
set -euo pipefail

# Convert a regular clone into the bare worktree layout used by git wtclone:
# <repo>/.git the former .git directory, now a bare repository
# <repo>/<branch> the former main checkout, now a linked worktree
# Linked worktrees on a branch move to <repo>/<branch> as well. Uncommitted,
# staged, untracked, and ignored files stay in place.
# Usage: git wtmigrate [<path>] (default: the repository of the current directory)

die() {
echo "git-wtmigrate: $*" >&2
exit 1
}

warn() {
echo "git-wtmigrate: $*" >&2
}

# Folder name follows worktrunk's sanitize filter: / and \ become -.
sanitize() {
local name="$1"
echo "${name//[\/\\]/-}"
}

(( $# <= 1 )) || die "usage: git wtmigrate [<path>]"
target="${1:-.}"

common="$(git -C "$target" rev-parse --path-format=absolute --git-common-dir)" ||
die "'$target' is not inside a git repository"
[[ "$(basename "$common")" == .git && -d "$common" ]] ||
die "expected the repository data in a .git directory, found $common"
[[ "$(git --git-dir="$common" config --bool --get core.bare || echo false)" == false ]] ||
die "$(dirname "$common") already uses the bare layout"
[[ -z "$(git --git-dir="$common" config --get core.worktree || true)" ]] ||
die "core.worktree is set; this layout is not supported"
[[ ! -d "$common/modules" ]] ||
die "repositories with initialized submodules are not supported"

for state in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD BISECT_LOG rebase-merge rebase-apply sequencer; do
[[ ! -e "$common/$state" ]] ||
die "an operation is in progress ($state); finish or abort it first"
done

repo="$(dirname "$common")"
branch="$(git --git-dir="$common" symbolic-ref --quiet --short HEAD)" ||
die "the main checkout has a detached HEAD; check out a branch first"
main_dir="$(sanitize "$branch")"

# Collect linked worktrees that sit on a branch; others stay where they are.
linked_paths=()
linked_dests=()
used_names="$main_dir"
wt_path=""
wt_branch=""
wt_skip=false
flush_worktree() {
if [[ -n "$wt_path" && "$wt_path" != "$repo" ]]; then
if [[ "$wt_path" == "$repo"/* ]]; then
die "worktree $wt_path is nested inside the main checkout; move it out first"
fi
if [[ "$wt_skip" == true || -z "$wt_branch" ]]; then
warn "leaving $wt_path in place (detached, locked, or missing)"
else
local name
name="$(sanitize "$wt_branch")"
if grep -Fxq -- "$name" <<<"$used_names"; then
die "two worktrees map to the folder name '$name'"
fi
used_names+=$'\n'"$name"
linked_paths+=("$wt_path")
linked_dests+=("$repo/$name")
fi
fi
wt_path=""
wt_branch=""
wt_skip=false
}
while IFS= read -r line; do
case "$line" in
"worktree "*) wt_path="${line#worktree }" ;;
"branch refs/heads/"*) wt_branch="${line#branch refs/heads/}" ;;
locked* | prunable*) wt_skip=true ;;
"") flush_worktree ;;
esac
done < <(git --git-dir="$common" worktree list --porcelain)
flush_worktree

[[ "$main_dir" != .git ]] || die "a branch named '.git' cannot become a folder"

parent="$(dirname "$repo")"
tmp="$(mktemp -d "$parent/.$(basename "$repo").wtmigrate.XXXXXX")"
trap 'warn "failed partway; pieces of the repository may be in $repo and $tmp"' ERR

# Move the main checkout aside, then put its .git back as the bare repository.
mv "$repo" "$tmp/checkout"
mkdir "$repo"
mv "$tmp/checkout/.git" "$common"
git --git-dir="$common" config core.bare true

# Register the checkout as a linked worktree. Keep its index (staged changes)
# and HEAD reflog, then swap the real files in under the new .git file.
[[ ! -f "$common/index" ]] || mv "$common/index" "$tmp/index"
git --git-dir="$common" worktree add --quiet --no-checkout "$repo/$main_dir" "$branch"
admin="$(git -C "$repo/$main_dir" rev-parse --path-format=absolute --git-dir)"
[[ ! -f "$tmp/index" ]] || mv "$tmp/index" "$admin/index"
if [[ -f "$common/logs/HEAD" ]]; then
mkdir -p "$admin/logs"
mv "$common/logs/HEAD" "$admin/logs/HEAD"
fi
mv "$repo/$main_dir/.git" "$tmp/checkout/.git"
rmdir "$repo/$main_dir"
mv "$tmp/checkout" "$repo/$main_dir"
rmdir "$tmp"
trap - ERR

git -C "$repo/$main_dir" update-index -q --refresh >/dev/null || true

for (( i = 0; i < ${#linked_paths[@]}; i++ )); do
src="${linked_paths[$i]}"
dest="${linked_dests[$i]}"
if [[ -e "$dest" ]]; then
warn "leaving $src in place; $dest already exists"
elif ! git --git-dir="$common" worktree move "$src" "$dest"; then
warn "could not move $src to $dest"
fi
done

echo "Migrated $repo; the former main checkout is now $repo/$main_dir"
echo "Shells inside the old checkout should cd to the new path."
Loading
Loading