Skip to content

Commit b198044

Browse files
committed
fix(@angular/build): preserve nested workspace path for git worktree cache
When an Angular workspace is located in a subdirectory of a Git repository rather than at the repository root, resolving the shared cache path in a Git worktree previously walked up to the worktree root to find the `.git` file and then resolved the cache path directly against the main repository root (`dirname(commonGitDir)`). This discarded the relative path between the worktree root and the workspace root. `getCacheBasePath` now computes the relative path from the worktree root to `workspaceRoot` and resolves the target workspace directory in the main repository. If the corresponding workspace directory exists in the main repository, the cache path is resolved relative to it; otherwise, it falls back to the worktree-local workspace root to avoid creating untracked directories in the main repository. Additionally, the directory traversal now stops at the nearest `.git` boundary. Fixes #34215 (cherry picked from commit aa53b97)
1 parent d12414d commit b198044

4 files changed

Lines changed: 128 additions & 10 deletions

File tree

‎packages/angular/build/src/utils/normalize-cache.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { existsSync, readFileSync, statSync } from 'node:fs';
10-
import { dirname, isAbsolute, join, resolve } from 'node:path';
10+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
1111

1212
/** Version placeholder is replaced during the build process with actual package version */
1313
const VERSION = '0.0.0-PLACEHOLDER';
@@ -75,11 +75,16 @@ function getCacheBasePath(workspaceRoot: string, cachePathSetting: string): stri
7575
// It's a git worktree
7676
const commondir = readFileSync(commondirPath, 'utf8').trim();
7777
const commonGitDir = resolve(gitdir, commondir);
78-
79-
return resolve(dirname(commonGitDir), cachePathSetting);
78+
const relativeWorkspacePath = relative(currentDir, workspaceRoot);
79+
const mainWorkspaceRoot = resolve(dirname(commonGitDir), relativeWorkspacePath);
80+
if (existsSync(mainWorkspaceRoot)) {
81+
return resolve(mainWorkspaceRoot, cachePathSetting);
82+
}
8083
}
8184
}
8285
}
86+
87+
break;
8388
}
8489
const parentDir = dirname(currentDir);
8590
if (parentDir === currentDir) {

‎packages/angular/build/src/utils/normalize-cache_spec.ts‎

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import assert from 'node:assert';
910
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
10-
import { tmpdir } from 'node:os';
1111
import { join, resolve } from 'node:path';
1212
import { normalizeCacheOptions } from './normalize-cache';
1313

1414
describe('normalizeCacheOptions', () => {
1515
let tempDir: string;
1616

1717
beforeEach(async () => {
18-
tempDir = await mkdtemp(join(tmpdir(), 'angular-cache-spec-'));
18+
const baseTmpDir = process.env['TEST_TMPDIR'];
19+
assert(baseTmpDir, 'TEST_TMPDIR is not set');
20+
tempDir = await mkdtemp(join(baseTmpDir, 'angular-cache-spec-'));
1921
});
2022

2123
afterEach(async () => {
@@ -58,6 +60,64 @@ describe('normalizeCacheOptions', () => {
5860
expect(options.localPath).toBe(resolve(worktreeRoot, '.angular/cache', '0.0.0-PLACEHOLDER'));
5961
});
6062

63+
it('should resolve cache path relative to corresponding nested workspace in main repository for a git worktree', async () => {
64+
const mainRepoRoot = join(tempDir, 'main-repo');
65+
const mainGitDir = join(mainRepoRoot, '.git');
66+
const mainWorkspaceRoot = join(mainRepoRoot, 'Site1/ClientApp');
67+
const worktreeRoot = join(tempDir, 'worktree');
68+
const worktreeWorkspaceRoot = join(worktreeRoot, 'Site1/ClientApp');
69+
70+
// Create main repo with a nested Angular workspace directory
71+
await mkdir(mainGitDir, { recursive: true });
72+
await mkdir(mainWorkspaceRoot, { recursive: true });
73+
74+
// Create worktree with the same nested Angular workspace structure and a .git file at the worktree root
75+
const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1');
76+
await mkdir(worktreeMetadataDir, { recursive: true });
77+
await mkdir(worktreeWorkspaceRoot, { recursive: true });
78+
await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`);
79+
80+
// Point the worktree metadata back to the main .git directory
81+
await writeFile(join(worktreeMetadataDir, 'commondir'), '../..');
82+
83+
const options = normalizeCacheOptions({}, worktreeWorkspaceRoot);
84+
85+
expect(options.basePath).toBe(resolve(mainWorkspaceRoot, '.angular/cache'));
86+
expect(options.path).toBe(resolve(mainWorkspaceRoot, '.angular/cache', '0.0.0-PLACEHOLDER'));
87+
expect(options.localBasePath).toBe(resolve(worktreeWorkspaceRoot, '.angular/cache'));
88+
expect(options.localPath).toBe(
89+
resolve(worktreeWorkspaceRoot, '.angular/cache', '0.0.0-PLACEHOLDER'),
90+
);
91+
});
92+
93+
it('should fall back to worktree workspace root when nested workspace does not exist in main repository', async () => {
94+
const mainRepoRoot = join(tempDir, 'main-repo');
95+
const mainGitDir = join(mainRepoRoot, '.git');
96+
const worktreeRoot = join(tempDir, 'worktree');
97+
const worktreeWorkspaceRoot = join(worktreeRoot, 'NewSite/ClientApp');
98+
99+
// Create main repo without the 'NewSite/ClientApp' subdirectory (e.g., added only on the worktree branch)
100+
await mkdir(mainGitDir, { recursive: true });
101+
102+
// Create worktree with the new nested Angular workspace and link it to the main repo's .git directory
103+
const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1');
104+
await mkdir(worktreeMetadataDir, { recursive: true });
105+
await mkdir(worktreeWorkspaceRoot, { recursive: true });
106+
await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`);
107+
await writeFile(join(worktreeMetadataDir, 'commondir'), '../..');
108+
109+
const options = normalizeCacheOptions({}, worktreeWorkspaceRoot);
110+
111+
expect(options.basePath).toBe(resolve(worktreeWorkspaceRoot, '.angular/cache'));
112+
expect(options.path).toBe(
113+
resolve(worktreeWorkspaceRoot, '.angular/cache', '0.0.0-PLACEHOLDER'),
114+
);
115+
expect(options.localBasePath).toBe(resolve(worktreeWorkspaceRoot, '.angular/cache'));
116+
expect(options.localPath).toBe(
117+
resolve(worktreeWorkspaceRoot, '.angular/cache', '0.0.0-PLACEHOLDER'),
118+
);
119+
});
120+
61121
it('should resolve local cache path relative to worktree root with custom relative path', async () => {
62122
const mainRepoRoot = join(tempDir, 'main-repo');
63123
const mainGitDir = join(mainRepoRoot, '.git');

‎packages/angular/cli/src/commands/cache/utilities.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { isJsonObject } from '@angular-devkit/core';
1010
import { existsSync, readFileSync, statSync } from 'node:fs';
11-
import { dirname, isAbsolute, join, resolve } from 'node:path';
11+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
1212
import { Cache, Environment } from '../../../lib/config/workspace-schema';
1313
import { AngularWorkspace } from '../../utilities/config';
1414

@@ -47,11 +47,16 @@ function getCacheBasePath(workspaceRoot: string, cachePathSetting: string): stri
4747
// It's a git worktree
4848
const commondir = readFileSync(commondirPath, 'utf8').trim();
4949
const commonGitDir = resolve(gitdir, commondir);
50-
51-
return resolve(dirname(commonGitDir), cachePathSetting);
50+
const relativeWorkspacePath = relative(currentDir, workspaceRoot);
51+
const mainWorkspaceRoot = resolve(dirname(commonGitDir), relativeWorkspacePath);
52+
if (existsSync(mainWorkspaceRoot)) {
53+
return resolve(mainWorkspaceRoot, cachePathSetting);
54+
}
5255
}
5356
}
5457
}
58+
59+
break;
5560
}
5661
const parentDir = dirname(currentDir);
5762
if (parentDir === currentDir) {

‎packages/angular/cli/src/commands/cache/utilities_spec.ts‎

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
*/
88

99
import { workspaces } from '@angular-devkit/core';
10+
import assert from 'node:assert';
1011
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
11-
import { tmpdir } from 'node:os';
1212
import { join, resolve } from 'node:path';
1313
import { AngularWorkspace } from '../../utilities/config';
1414
import { getCacheConfig } from './utilities';
@@ -17,7 +17,9 @@ describe('CLI cache config utilities', () => {
1717
let tempDir: string;
1818

1919
beforeEach(async () => {
20-
tempDir = await mkdtemp(join(tmpdir(), 'angular-cli-cache-spec-'));
20+
const baseTmpDir = process.env['TEST_TMPDIR'];
21+
assert(baseTmpDir, 'TEST_TMPDIR is not set');
22+
tempDir = await mkdtemp(join(baseTmpDir, 'angular-cli-cache-spec-'));
2123
});
2224

2325
afterEach(async () => {
@@ -67,6 +69,52 @@ describe('CLI cache config utilities', () => {
6769
expect(config.path).toBe(resolve(mainRepoRoot, '.angular/cache'));
6870
});
6971

72+
it('should resolve default cache path relative to corresponding nested workspace in main repository for a git worktree', async () => {
73+
const mainRepoRoot = join(tempDir, 'main-repo');
74+
const mainGitDir = join(mainRepoRoot, '.git');
75+
const mainWorkspaceRoot = join(mainRepoRoot, 'Site1/ClientApp');
76+
const worktreeRoot = join(tempDir, 'worktree');
77+
const worktreeWorkspaceRoot = join(worktreeRoot, 'Site1/ClientApp');
78+
79+
// Create main repo with a nested Angular workspace directory
80+
await mkdir(mainGitDir, { recursive: true });
81+
await mkdir(mainWorkspaceRoot, { recursive: true });
82+
83+
// Create worktree with the same nested Angular workspace structure and a .git file at the worktree root
84+
const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1');
85+
await mkdir(worktreeMetadataDir, { recursive: true });
86+
await mkdir(worktreeWorkspaceRoot, { recursive: true });
87+
await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`);
88+
89+
// Point the worktree metadata back to the main .git directory
90+
await writeFile(join(worktreeMetadataDir, 'commondir'), '../..');
91+
92+
const config = getCacheConfig(mockWorkspace(worktreeWorkspaceRoot));
93+
94+
expect(config.path).toBe(resolve(mainWorkspaceRoot, '.angular/cache'));
95+
});
96+
97+
it('should fall back to worktree workspace basePath when nested workspace does not exist in main repository', async () => {
98+
const mainRepoRoot = join(tempDir, 'main-repo');
99+
const mainGitDir = join(mainRepoRoot, '.git');
100+
const worktreeRoot = join(tempDir, 'worktree');
101+
const worktreeWorkspaceRoot = join(worktreeRoot, 'NewSite/ClientApp');
102+
103+
// Create main repo without the 'NewSite/ClientApp' subdirectory (e.g., added only on the worktree branch)
104+
await mkdir(mainGitDir, { recursive: true });
105+
106+
// Create worktree with the new nested Angular workspace and link it to the main repo's .git directory
107+
const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1');
108+
await mkdir(worktreeMetadataDir, { recursive: true });
109+
await mkdir(worktreeWorkspaceRoot, { recursive: true });
110+
await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`);
111+
await writeFile(join(worktreeMetadataDir, 'commondir'), '../..');
112+
113+
const config = getCacheConfig(mockWorkspace(worktreeWorkspaceRoot));
114+
115+
expect(config.path).toBe(resolve(worktreeWorkspaceRoot, '.angular/cache'));
116+
});
117+
70118
it('should resolve custom relative cache path relative to main repository root in a git worktree', async () => {
71119
const mainRepoRoot = join(tempDir, 'main-repo');
72120
const mainGitDir = join(mainRepoRoot, '.git');

0 commit comments

Comments
 (0)