Browse Source

fix(pigeon): Worktree sessions resolve to main repo, never overwrite

Bug: a pigeon session running from a git worktree would register
the project with the WORKTREE's path and basename rather than the
main repo's. Because projects.hash is keyed on the git root commit
(same across worktrees) and register_project uses INSERT OR
REPLACE, a worktree session would clobber the main repo's row —
turning 'claude-mods' into 'feat-foo-abc123' (or whatever the
worktree dirname happened to be).

Concrete symptom: 'pigeon send claude-mods ...' from another
project would either fail to find the project by name or hit a
stale worktree path that no longer exists.

Fix: new resolve_main_repo() helper uses 'git rev-parse
--git-common-dir' to walk a worktree back to its canonical main
repo. project_name() and register_project() now route through
resolve_main_repo() instead of canonical_path() for the
this-project case, so the main repo's identity is preserved no
matter which worktree (or how many worktrees) ran pigeon from
inside it.

Mechanism: --git-common-dir returns the main repo's .git
directory regardless of whether you're in a worktree or the main
repo itself. Stripping the trailing /.git gives the canonical
top-level. Bare repos / non-git dirs fall back to canonical_path
unchanged.

Dogfooded: created a real worktree at /tmp/pigeon-worktree-test
from this repo. Before fix, 'mail-db.sh id' from inside would
report 'pigeon-worktree-test 7663d6'. After fix, it correctly
reports 'claude-mods 7663d6' — same name + same hash as the main
repo, no clobbering.

The hash itself was already correct (project_hash uses root
commit which is identical for worktrees + main); only the name +
path columns were susceptible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0xDarkMatter 1 month ago
parent
commit
006f1fce74
1 changed files with 45 additions and 4 deletions
  1. 45 4
      skills/pigeon/scripts/mail-db.sh

+ 45 - 4
skills/pigeon/scripts/mail-db.sh

@@ -22,6 +22,45 @@ canonical_path() {
   fi
 }
 
+# Resolve to the canonical main-repository root. If the given dir is inside
+# a git worktree, returns the MAIN repo's top-level directory rather than the
+# worktree's. This prevents pigeon from registering a worktree as if it were
+# the project (a worktree session would otherwise INSERT OR REPLACE the main
+# repo's projects row with the worktree's name + path).
+#
+# Mechanism:
+#   - `git rev-parse --git-common-dir` returns the canonical .git directory:
+#       - For a main repo: same as --git-dir (e.g. /repo/.git)
+#       - For a worktree: the main repo's .git (e.g. /repo/.git, NOT
+#         /repo/.git/worktrees/<wt-name>)
+#   - Strip trailing /.git to get the main repo's top-level directory.
+#   - Bare repos / non-git dirs fall back to canonical_path.
+resolve_main_repo() {
+  local dir="${1:-$PWD}"
+  if [ ! -d "$dir" ]; then
+    canonical_path "$dir"
+    return
+  fi
+  local commondir
+  commondir=$(git -C "$dir" rev-parse --git-common-dir 2>/dev/null)
+  if [ -z "$commondir" ]; then
+    # Not a git repo — fall through
+    canonical_path "$dir"
+    return
+  fi
+  # commondir may be relative to $dir; make it absolute and canonical.
+  case "$commondir" in
+    /*) ;;  # absolute
+    *)  commondir=$(cd "$dir" && cd "$commondir" 2>/dev/null && pwd -P) ;;
+  esac
+  # Strip trailing /.git to get the main repo's top-level (non-bare repos).
+  # Bare repos: commondir IS the repo top-level, no /.git suffix.
+  case "$commondir" in
+    */.git)  dirname "$commondir" ;;
+    *)       printf '%s' "$commondir" ;;
+  esac
+}
+
 # Generate 6-char project ID
 # Priority: git root commit hash > canonical path hash
 project_hash() {
@@ -43,9 +82,9 @@ project_hash() {
   printf '%s' "$path" | shasum -a 256 | cut -c1-6
 }
 
-# Get display name (basename of canonical path)
+# Get display name (basename of the MAIN-REPO top-level — never a worktree's).
 project_name() {
-  basename "$(canonical_path "${1:-$PWD}")"
+  basename "$(resolve_main_repo "${1:-$PWD}")"
 }
 
 # ============================================================================
@@ -113,12 +152,14 @@ read_body() {
   fi
 }
 
-# Register current project in the projects table (idempotent)
+# Register current project in the projects table (idempotent).
+# Always registers the main repo's top-level — a worktree session must NOT
+# overwrite the main repo's row with the worktree's path/name.
 register_project() {
   local hash name path
   hash=$(project_hash "${1:-$PWD}")
   name=$(sql_escape "$(project_name "${1:-$PWD}")")
-  path=$(sql_escape "$(canonical_path "${1:-$PWD}")")
+  path=$(sql_escape "$(resolve_main_repo "${1:-$PWD}")")
   sqlite3 "$MAIL_DB" \
     "INSERT OR REPLACE INTO projects (hash, name, path) VALUES ('${hash}', '${name}', '${path}');"
 }