fix(stats): count every worktree of a repo as that repo (#809) - #813
Conversation
The PR description includes sufficient real-CLI end-to-end verification for this runtime change. |
ed7143f to
69e9788
Compare
|
Re: #813 (comment) Thanks, both confirmed and fixed in 69e9788.
|
The two earlier findings are resolved in the current diff: bare-repository worktrees now use the repository name, and equivalent remote-host keys canonicalize to one row. The PR description includes sufficient real-CLI end-to-end verification. |
`stats --by-repo`, the dashboard, `session save` and the wiki slug of `import --dir` / `codebase --extract` derived the project from a per-worktree path, so each worktree became its own project, and once a worktree was removed git could no longer map its path back to the repo. The hook already resolves the repo's main checkout to find the config and dropped it. Both event writers (the hook-dispatch handler and the legacy `dashboard-report`) now store it as `projectAnchor`, resolved for the event's cwd and skipped when that directory is gone. An event with no cwd (Copilot, which keeps paths out of its events) gets none either. The dashboard's process_exit event copies the session's anchor. `resolveAnchors` remembers each directory's non-null result for the life of the process, so the second lookup runs no git. - repoKeys: a session's key is the last anchor it recorded, else its last cwd. repoLabel names a key by its repo (repoName), qualified by the parent when two path keys share a name, and by the whole key if that still collides. repoName is the directory's name, except for a bare git directory (HEAD and objects/, no .git): `repo.git` -> repo, and `repo/.bare` -> repo; keys named by the same directory share a label. A key that is a repo keeps its name even when attributeRepo reserves it for non-project directories (a repo in ~/workspace). Remote-form keys keep their canonical owner/repo, so the same repo on two hosts stays one row. - stats --by-repo groups sessions under repoLabel(repoKey); directories that are not a repo still share one no_repo row. - dashboard: an event belongs to the project rooted at its anchor, else the one holding its cwd; discovery uses the anchor and asks git only for events without one, or whose anchor holds no config (a legacy .teamai/ in a worktree). Sessions carry repoKey/repoLabel, which the Repository filter uses; rows keep showing the cwd. - session save: Project: is the repo label, Directory: the cwd. - wiki slug: without --project, a linked worktree's root takes the repo's name (repoName), so a bare repo's worktrees share it too. Any other directory keeps its own name, including subdirectories, so per-package slugs in a monorepo do not change. The issue's rule would have renamed those too.
69e9788 to
8c72294
Compare
|
Re: #813 (comment) Fixed in 8c72294:
|
The earlier bare-repository, reserved-name, remote-host canonicalization, and bilingual documentation findings are resolved. The PR description includes sufficient real-CLI end-to-end verification. |
|
Follow-ups from this PR's Notes and its last review pass are tracked in #823 (Cleanup: the symlinked main-checkout slug at |
Merges Tencent#817 (docs mirror), Tencent#788/Tencent#790 and Tencent#809/Tencent#813. The docs mirror now targets the member's resolved docs set: it copies only the delivered files and prunes every local entry the team repo does not have, but a local copy of a team doc in an inactive namespace follows the Tencent#707 rule (removed when byte-equal, kept and named when edited). Withdrawal runs inside the mirror, after the single-repo early return, so it never deletes from the team repo's own docs/. Doctor expects the delivered set and does not report a withheld namespace's team doc as stale. Tencent#817's failure handling (docsSyncFailed, cleared revision, result.completed) is kept.
Summary
stats --by-repo, the dashboard,session saveand the wiki slug treated each git worktree as its own project, because events recorded only theircwd. Events now record the repo's main checkout, and every consumer attributes sessions to it.stats --by-repoin the repomy-repo 2/wt-demo 2/src 1sessmy-repo 5 sessapiapi 2 sesswork/api 1/personal/api 1session save(worktree)Project: `/…/wt-demo`Project: `my-repo`+Directory:import --dir ./codebase --extractin the worktreewt-demomy-reporepo/.bare+main,featuremain,feature; rowsmain,featurerepogit worktree removemy-repo, listed once in the filterType of Change
Test Plan
npx tsc --noEmitpassesnpx vitest runpasses: 307 files, 4737 passed, 1 skippedrepo-attribution-worktrees-809.test.ts; unit cases inanchors,session-analytics,dashboard-workspaces,dashboard-report-scope,dashboard-uinpm run test:e2epasses: 48 files (3 skipped), 252 passed, 26 skipped (on69e97889; later: labels/docs only, [bug] stats, dashboard, session save and wiki slugs split one repo into one project per worktree #809 e2e 6/6)The e2e test drives the built CLI (sandbox HOME, local team repo, a worktree outside the checkout, hook payloads on stdin):
On
origin/mainall five fail, e.g.expected [ [ 'my-repo', 2 ], …(2) ] to deeply equal [ [ 'my-repo', 5 ] ]; with this PR all pass.Removing any one of 30 parts (writers, memo, filter, slug,
repoName, remote-key rules) and rebuilding fails a test.Reproduced with the real CLI, git mode: worktree
Reproduced with the real CLI: bare repo (repo/.bare + repo/main + repo/feature)
From reading the code, not reproduced with the real CLI:
filterEventsByScope's legacy branch (src/team-push.ts:415) is unchanged.Not run:
gitlab/githubproviders and other agents; attribution depends on neither (left to CI).Related Issues
Closes #809
Notes for Reviewers
src/hook-handlers.ts:189), not onlydashboardReport. Both calleventProjectAnchor, which skips a missingcwdwithout git, so [bug] hook events after a worktree is deleted fall back to the user scope #810 changes one place.process_exitcopies the anchor, likedataHome.resolveAnchorskeeps non-null results for the process's life, so the hook's second lookup runs no git inside a repo.nullis not cached, per the issue. A long-lived dashboard keeps a removed worktree's answer, which attribution wants.--extract packages/apistaysapi).repoNamedetects a bare git dir on disk (HEAD andobjects/, no.git). Gap: a main checkout reached through a differently named symlink keeps that name.no_repodirs still share a row; a repo keeps its name even in a dir like~/workspace. Only path keys get a parent prefix, then the whole path on a second collision; remote keys keepowner/repo, one row across hosts.session savelabels against the whole log.cwd.github.com/acme/apiand local/x/acme/apiboth labelacme/api(src/utils/repo-attribution.ts:138-141); hooks never write remote keys. Unrelated flake:local-agent.test.ts > emits hint only once per sessionIdshares anos.tmpdir()marker keyed by a fixed session id (src/local-agent.ts:1191).filterEventsByScopebranch, already-pushed wiki entries, [bug] hook events after a worktree is deleted fall back to the user scope #810, a per-worktree breakdown.data-directory-layout.md(projectAnchor, plus the fix(report): each scope keeps its own reported dashboard snapshots (#786) #795 scope note),dashboard-unified{,.zh-CN}.mdandusage-guide{,.zh-CN}.md(repository filter, slug),--projecthelp and regeneratedcommands.md.