Skip to content

fix(core): give unborn git repos their own project id - #61249

Open
nanoscopic wants to merge 1 commit into
anomalyco:devfrom
nanoscopic:unborn-project-identity
Open

nanoscopic wants to merge 1 commit into
anomalyco:devfrom
nanoscopic:unborn-project-identity

Conversation

@nanoscopic

@nanoscopic nanoscopic commented Nov 2, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #55133
Refs #50979

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

A repository with an unborn HEAD has no remote and no root commit, so Project.resolve fell back to the shared global project. Every freshly git init-ed directory therefore shared one session pool, and the desktop showed the first resolved directory instead of the new project.

Give unborn repositories the same directory-derived id that markerless directories already use (Hash.fast("directory:" + path)). Unrelated fresh repositories now resolve to distinct project ids and show up as their own projects.

When a remote or root commit appears later, the directory id is reported as previous. The existing worktree.resolved projection moves sessions from previous onto the new id, then the superseded directory-id project row is archived so the same directory is not listed twice. The global project is no longer written for these directories, which also stops it from being re-pointed at whichever empty repo resolved first.

How did you verify your code works?

  • New Project.resolve coverage: an unborn repo gets a directory-derived id, two unrelated unborn repos get different ids and both appear in project.list, and the directory project is archived once a commit upgrades the id.
  • Updated the reftable test to assert the id upgrades to the root commit with the unborn id as previous.
  • Updated the session adoption test: sessions created in a directory before git init keep their project when the repo appears, then move to the remote-derived id after the first commit and remote.
  • packages/core: bun run script/test.ts test/project.test.ts test/session-create.test.ts test/session-projector.test.ts test/session-move.test.ts → 124 pass, 1 skip.
  • packages/core: bun typecheck → no errors.
  • Root bun run check → all 35 tasks pass. Locally I had to replace the Windows checkout of the packages/app/src/custom-elements.d.ts symlink with its target content; this clone has core.symlinks=false, so the symlink is a text file and fails typecheck before the change too.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Fixes #50979

Related: #61009, #57985, #61104, #59371

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix works. I ran a small throwaway test against both checkouts: on the base, two fresh git init folders both resolve to global; with this change they get separate project ids and both show up in project.list. After the first commit the id moves to the root commit, and the old directory id is reported as previous, so the sessions follow it. The project, session-create and session-projector tests pass (107), and typecheck:tsc is clean.

Public API: unchanged. No new endpoints, events or schema fields. Only the project id that git init folders with no commits resolve to changes, from global to Hash.fast("directory:" + path).

Sessions already filed under global for an unborn repo are moved over on the next resolve, since the projector always checks global. Repos that already have an id now also get the directory id as previous, which only adds one empty session lookup when a folder's worktree row is first recorded.

One concern is left inline: after the first commit, the unborn project row stays behind as a second, empty project for the same folder.

Fixes #50979
Related: #61009, #57985, #61104, #59371

const resolved: Resolved = {
previous,
id: id ?? ID.global,
previous: previous ?? (identity ? directoryID : undefined),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sessions move from the directory id to the new id, but the directory-id project row stays. I checked: git init → resolve → first commit → resolve, then project.list returns two projects with the same canonical, the new root-commit one and the old empty one. The old row's directory still exists, so the archive sweep never removes it. On the base the leftover row was global, which the app filters out (project.id === "global"). This one isn't filtered, so every fresh repo will show up twice in the desktop project list after its first commit (similar to #61009 / #61104). Could the previous row be archived or merged once its sessions have been moved, at least when previous is this directory id?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed in f993152: once the worktree event has moved sessions from the directory id, the superseded project row is archived (not deleted). A new test covers git init -> resolve -> first commit -> resolve and asserts the old id disappears from project.list and is listed with ime.archived. A marker-file previous is left alone; only the directory-derived id is archived.

A repository with an unborn HEAD has no remote or root commit, so Project.resolve fell back to the shared global project. Every fresh 'git init' directory then shared one session pool, and the desktop showed the first resolved directory instead of the project.

Use the same directory-derived identity as markerless directories for unborn repos. When a remote or root commit appears later, expose the directory id as 'previous' so the session projector moves existing sessions onto the upgraded identity, then archive the placeholder project row so the directory is not listed twice.
@nanoscopic
nanoscopic force-pushed the unborn-project-identity branch from 81ad377 to f993152 Compare November 2, 2026 13:09

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commits fix the leftover-project concern. After the first commit, the old unborn-repo project is now archived, so the folder isn't listed twice. In my throwaway test (two git init folders, a commit in one, resolve again), project.list returns one project for that folder, the old row appears only with archived: true, and the other repo stays active. The project, session-create, session-projector and session-move tests pass (124, 1 skipped), and typecheck:tsc is clean.

Public API: unchanged. The archive uses the existing project-updated event; nothing is added or changed.

No new concerns.

@BoYanZh

BoYanZh commented Nov 7, 2026

Copy link
Copy Markdown

Heads-up: the same shared-global collapse exists in hgDiscover just below in the same file (packages/core/src/project.ts):

const id = previous ?? (yield* hgRoot(worktree))
...
id: id ?? ID.global,

A Mercurial checkout with no cached id and no root changeset (e.g. fresh hg init, or unreadable metadata) resolves to the shared global project, so unrelated empty hg repos share one session pool and whichever resolves first pins global's canonical — the same mechanism as #55133. The project.test.ts case "returns global id for unreadable mercurial metadata" pins that behavior.

The same treatment applied here (directory-derived identity + reporting it as previous when a stronger identity appears) would close that hole too. Happy to split it into a follow-up if you'd rather keep this PR git-scoped.

Independent confirmation of the approach: I reproduced the git-side collapse from real machine state (20+ unrelated directories sharing global with a stale / canonical, including an unborn-HEAD repo) and converged on the identical directory:-hash + previous shape before finding this PR — so +1 on the direction, including archiving the superseded row.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

2 participants