Skip to content

Diff viewer is empty when an environment's path is a subdirectory of its git repository #3905

Description

@tempor1s

Summary

When an environment's path is a subdirectory of the git repository that contains it (a package inside a monorepo, for example), the Changes panel lists the changed files correctly but every file shows "No renderable diff", and expanding context shows "Couldn't load surrounding context". Environments rooted at the repository top level are unaffected.

Versions and environment

  • bb 0.43.1, desktop app, macOS (Apple Silicon)
  • git 2.55
  • Both code paths below are unchanged on main as of 0bb64f3. I reproduced on the 0.43.1 release and confirmed the cause by reading main. I did not run a build of main.

Steps to reproduce

mkdir -p /tmp/bb-subdir-repro/packages/app && cd /tmp/bb-subdir-repro
git init -q -b main
echo one > packages/app/file.txt
git add -A && git commit -q -m init
echo two >> packages/app/file.txt
  1. Add a project whose checkout path is /tmp/bb-subdir-repro/packages/app (the subdirectory, not the repo root).
  2. Start a thread in it and open the Changes panel.
  3. Select packages/app/file.txt.

Expected vs actual

Expected: the one-line diff for file.txt, with expandable context.

Actual: the file is listed as modified, its diff body says "No renderable diff", and context expansion says "Couldn't load surrounding context".

Evidence

The mismatch reproduces with git alone:

cd /tmp/bb-subdir-repro/packages/app

# The file listing is repo-root-relative even though cwd is the subdirectory:
git diff --name-status HEAD --
# M	packages/app/file.txt

# bb then asks for that path as a pathspec, which git resolves against cwd:
git diff HEAD -- packages/app/file.txt | wc -c
# 0

# Anchoring the pathspec at the top of the working tree fixes it:
git diff HEAD -- ':(top,literal)packages/app/file.txt' | wc -c
# 169

1. Empty patches. packages/host-workspace/src/workspace.ts runs every git command with cwd: this.path. The changed-file listing (--name-status, --numstat) prints paths relative to the repository root regardless of cwd. Those paths are then passed back as pathspecs through withDiffPathspec, and git resolves a plain pathspec relative to cwd. In a subdirectory environment packages/app/file.txt is looked up as packages/app/packages/app/file.txt, matches nothing, and the patch comes back empty. Because zero sections looks the same as zero entries, the per-file fallback never runs.

2. "Couldn't load surrounding context". routes.diffFile in apps/server/src/routes/environments.ts builds the working-tree path as path.join(environment.path, repoRelativePath). That is the same double prefix, so host.read_file returns ENOENT for the working-tree side. The committed side works, because git show <ref>:<path> is already root-relative.

Proposed fix

For (1), anchor the pathspecs at the top of the working tree. :(top) makes the pathspec independent of cwd, and literal matches what the file already does for its other pathspecs (:(literal)${relativePath}), so paths containing [, * or ? keep working.

--- a/packages/host-workspace/src/workspace.ts
+++ b/packages/host-workspace/src/workspace.ts
@@ function withDiffPathspec(
   const withoutSeparator =
     args[args.length - 1] === "--" ? args.slice(0, -1) : args;
-  return [...withoutSeparator, "--", ...paths];
+  return [
+    ...withoutSeparator,
+    "--",
+    ...paths.map((path) => `:(top,literal)${path}`),
+  ];
 }

This is a no-op for every environment rooted at the repository top level: there, top-of-tree and cwd are the same directory, and I checked that the two forms produce byte-identical output. It only changes behaviour in the case that is currently broken. The other two :(literal) call sites in the same file (the index diff loop and the ls-files --others lookup) have the same cwd assumption and would want :(top,literal) plus ls-files --full-name to be consistent.

For (2), the smallest change I found is in the daemon's host.read_file handler: when the read fails with ENOENT, a rootPath was supplied and there is no ref, resolve git -C <rootPath> rev-parse --show-toplevel and retry with join(top, relative(rootPath, path)). It only runs on a read that would otherwise fail, so existing behaviour is untouched. The cleaner alternative is for the server to send the repo-relative path and let the daemon resolve it against the repository root, but that changes the command contract.

What you ruled out

  • Not a large-repo or timeout problem: the two-file repository above reproduces it.
  • Not a rendering problem: the patch is already empty when it leaves the host daemon.
  • Not specific to worktrees, a custom environment provider, or a merge-base setting: a plain project pointed at a subdirectory is enough.
  • Top-level environments: the proposed pathspec form produces byte-identical git diff output there, so they should see no change.

Checks

  • I reproduced this on the latest release or on main, or I say above that I could not.
  • I searched open and closed issues for the same problem.
  • If an agent wrote this, the body ends with > AGENT GENERATED and links the thread or report.

AGENT GENERATED. Investigated and drafted with Claude Code, reviewed by me before filing. The thread is local to my machine, so the reproduction above is the full report.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    confirmed-reproBug reproduced again from a clean trusted checkout; see linked reportworkspacesWorktrees, environments, git, shells

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions