Skip to content

Commit 00a4433

Browse files
authored
[Blueprints] Preserve the first char of all filenames sourced from GitDirectoryReference (#2070)
Adjust GitDirectoryReference to stop cutting off the first character from each filename when the repository path starts with a slash. Closes #2061 Closes #1999 ## Implementation The problem was passing the wring prefix length to `fileName.substring()` (user input vs path without the leading slash). This PR adjusts that. In addition, it updates git-sparse-checkout.ts to use `commitHash` and not ref name for fetching files. This way you can checkout a specific commit, and that's convenient for testing and developers consuming the API. It also modified the CORS proxy to allow 'git-protocol' in headers. ## Testing Go to http://127.0.0.1:5400/website-server/#{%22landingPage%22:%22/wp-admin/%22,%22preferredVersions%22:{%22php%22:%227.4%22,%22wp%22:%225.9%22},%22steps%22:[{%22step%22:%22login%22,%22username%22:%22admin%22},{%22step%22:%22installPlugin%22,%22pluginData%22:{%22resource%22:%22git:directory%22,%22url%22:%22https://wordpress-playground-cors-proxy.net/cors-proxy.php?https://github.com/Automattic/page-optimize.git%22,%22ref%22:%22master%22,%22path%22:%22/%22},%22options%22:{%22activate%22:true,%22targetFolderName%22:%22page-optimize-from-git%22}}]} and confirm you see wp-admin (without CSS).
1 parent bbf5645 commit 00a4433

File tree

6 files changed

+119
-25
lines changed

6 files changed

+119
-25
lines changed

packages/playground/blueprints/src/lib/resources.spec.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { UrlResource } from './resources';
1+
import { UrlResource, GitDirectoryResource } from './resources';
22

33
describe('UrlResource', () => {
44
it('should create a new instance of UrlResource', () => {
@@ -21,3 +21,29 @@ describe('UrlResource', () => {
2121
);
2222
});
2323
});
24+
25+
describe('GitDirectoryResource', () => {
26+
describe('resolve', () => {
27+
it.each([
28+
'packages/docs/site/docs/blueprints/tutorial',
29+
'/packages/docs/site/docs/blueprints/tutorial',
30+
])(
31+
'should return a list of files in the directory (path: %s)',
32+
async (path) => {
33+
const resource = new GitDirectoryResource({
34+
resource: 'git:directory',
35+
url: 'https://github.com/WordPress/wordpress-playground',
36+
ref: '05138293dd39e25a9fa8e43a9cc775d6fb780e37',
37+
path,
38+
});
39+
const { files } = await resource.resolve();
40+
expect(Object.keys(files)).toEqual([
41+
'01-what-are-blueprints-what-you-can-do-with-them.md',
42+
'02-how-to-load-run-blueprints.md',
43+
'03-build-your-first-blueprint.md',
44+
'index.md',
45+
]);
46+
}
47+
);
48+
});
49+
});

packages/playground/blueprints/src/lib/resources.ts

+17-11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { dirname, Semaphore } from '@php-wasm/util';
77
import {
88
listDescendantFiles,
99
listGitFiles,
10+
resolveCommitHash,
1011
sparseCheckout,
1112
} from '@wp-playground/storage';
1213
import { zipNameToHumanName } from './utils/zip-name-to-human-name';
@@ -461,21 +462,20 @@ export class GitDirectoryResource extends Resource<Directory> {
461462
const repoUrl = this.options?.corsProxy
462463
? `${this.options.corsProxy}${this.reference.url}`
463464
: this.reference.url;
464-
const ref = ['', 'HEAD'].includes(this.reference.ref)
465-
? 'HEAD'
466-
: `refs/heads/${this.reference.ref}`;
467-
const allFiles = await listGitFiles(repoUrl, ref);
465+
466+
const commitHash = await resolveCommitHash(repoUrl, {
467+
value: this.reference.ref,
468+
type: 'infer',
469+
});
470+
const allFiles = await listGitFiles(repoUrl, commitHash);
468471

469472
const requestedPath = this.reference.path.replace(/^\/+/, '');
470473
const filesToClone = listDescendantFiles(allFiles, requestedPath);
471-
let files = await sparseCheckout(repoUrl, ref, filesToClone);
474+
let files = await sparseCheckout(repoUrl, commitHash, filesToClone);
475+
472476
// Remove the path prefix from the cloned file names.
473-
files = Object.fromEntries(
474-
Object.entries(files).map(([name, contents]) => {
475-
name = name.substring(this.reference.path.length);
476-
name = name.replace(/^\/+/, '');
477-
return [name, contents];
478-
})
477+
files = mapKeys(files, (name) =>
478+
name.substring(requestedPath.length).replace(/^\/+/, '')
479479
);
480480
return {
481481
name:
@@ -493,6 +493,12 @@ export class GitDirectoryResource extends Resource<Directory> {
493493
}
494494
}
495495

496+
function mapKeys(obj: Record<string, any>, fn: (key: string) => string) {
497+
return Object.fromEntries(
498+
Object.entries(obj).map(([key, value]) => [fn(key), value])
499+
);
500+
}
501+
496502
/**
497503
* A `Resource` that represents a git directory.
498504
*/

packages/playground/php-cors-proxy/cors-proxy.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
header('Access-Control-Allow-Origin: ' . $origin);
2020
header('Access-Control-Allow-Credentials: true');
2121
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
22-
header('Access-Control-Allow-Headers: Accept, Authorization, Content-Type');
22+
header('Access-Control-Allow-Headers: Accept, Authorization, Content-Type, git-protocol');
2323
}
2424
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
2525
header("Allow: GET, POST, OPTIONS");

packages/playground/storage/src/lib/git-sparse-checkout.spec.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
listGitRefs,
33
sparseCheckout,
44
listGitFiles,
5+
resolveCommitHash,
56
} from './git-sparse-checkout';
67

78
describe('listRefs', () => {
@@ -18,9 +19,16 @@ describe('listRefs', () => {
1819

1920
describe('sparseCheckout', () => {
2021
it('should retrieve the requested files from a git repo', async () => {
22+
const commitHash = await resolveCommitHash(
23+
'https://github.com/WordPress/wordpress-playground.git',
24+
{
25+
value: 'trunk',
26+
type: 'branch',
27+
}
28+
);
2129
const files = await sparseCheckout(
2230
'https://github.com/WordPress/wordpress-playground.git',
23-
'refs/heads/trunk',
31+
commitHash,
2432
['README.md']
2533
);
2634
expect(files).toEqual({
@@ -32,9 +40,16 @@ describe('sparseCheckout', () => {
3240

3341
describe('listGitFiles', () => {
3442
it('should list the files in a git repo', async () => {
43+
const commitHash = await resolveCommitHash(
44+
'https://github.com/WordPress/wordpress-playground.git',
45+
{
46+
value: 'trunk',
47+
type: 'branch',
48+
}
49+
);
3550
const files = await listGitFiles(
3651
'https://github.com/WordPress/wordpress-playground.git',
37-
'refs/heads/trunk'
52+
commitHash
3853
);
3954
expect(files).toEqual(
4055
expect.arrayContaining([

packages/playground/storage/src/lib/git-sparse-checkout.ts

+50-10
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,9 @@ if (typeof window !== 'undefined') {
3939
*/
4040
export async function sparseCheckout(
4141
repoUrl: string,
42-
fullyQualifiedBranchName: string,
42+
commitHash: string,
4343
filesPaths: string[]
4444
) {
45-
const refs = await listGitRefs(repoUrl, fullyQualifiedBranchName);
46-
const commitHash = refs[fullyQualifiedBranchName];
4745
const treesIdx = await fetchWithoutBlobs(repoUrl, commitHash);
4846
const objects = await resolveObjects(treesIdx, commitHash, filesPaths);
4947

@@ -75,24 +73,24 @@ export type FileTreeFolder = {
7573
};
7674
export type FileTree = FileTreeFile | FileTreeFolder;
7775

76+
export type GitRef = {
77+
value: string;
78+
type?: 'branch' | 'commit' | 'refname' | 'infer';
79+
};
80+
7881
/**
7982
* Lists all files in a git repository.
8083
*
8184
* See https://git-scm.com/book/en/v2/Git-Internals-Git-Objects for more information.
8285
*
8386
* @param repoUrl The URL of the git repository.
84-
* @param fullyQualifiedBranchName The full name of the branch to fetch from (e.g., 'refs/heads/main').
87+
* @param commitHash The commit hash to fetch from.
8588
* @returns A list of all files in the repository.
8689
*/
8790
export async function listGitFiles(
8891
repoUrl: string,
89-
fullyQualifiedBranchName: string
92+
commitHash: string
9093
): Promise<FileTree[]> {
91-
const refs = await listGitRefs(repoUrl, fullyQualifiedBranchName);
92-
if (!(fullyQualifiedBranchName in refs)) {
93-
throw new Error(`Branch ${fullyQualifiedBranchName} not found`);
94-
}
95-
const commitHash = refs[fullyQualifiedBranchName];
9694
const treesIdx = await fetchWithoutBlobs(repoUrl, commitHash);
9795
const rootTree = await resolveAllObjects(treesIdx, commitHash);
9896
if (!rootTree?.object) {
@@ -102,6 +100,48 @@ export async function listGitFiles(
102100
return gitTreeToFileTree(rootTree);
103101
}
104102

103+
/**
104+
* Resolves a ref description, e.g. a branch name, to a commit hash.
105+
*
106+
* @param repoUrl The URL of the git repository.
107+
* @param ref The branch name or commit hash.
108+
* @returns The commit hash.
109+
*/
110+
export async function resolveCommitHash(repoUrl: string, ref: GitRef) {
111+
if (ref.type === 'infer' || ref.type === undefined) {
112+
if (['', 'HEAD'].includes(ref.value)) {
113+
ref = {
114+
value: ref.value,
115+
type: 'refname',
116+
};
117+
} else if (typeof ref.value === 'string' && ref.value.length === 40) {
118+
ref = {
119+
value: ref.value,
120+
type: 'commit',
121+
};
122+
}
123+
}
124+
if (ref.type === 'branch') {
125+
ref = {
126+
value: `refs/heads/${ref.value}`,
127+
type: 'refname',
128+
};
129+
}
130+
switch (ref.type) {
131+
case 'commit':
132+
return ref.value;
133+
case 'refname': {
134+
const refs = await listGitRefs(repoUrl, ref.value);
135+
if (!(ref.value in refs)) {
136+
throw new Error(`Branch ${ref.value} not found`);
137+
}
138+
return refs[ref.value];
139+
}
140+
default:
141+
throw new Error(`Invalid ref type: ${ref.type}`);
142+
}
143+
}
144+
105145
function gitTreeToFileTree(tree: GitTree): FileTree[] {
106146
return tree.object
107147
.map((branch) => {

packages/playground/storage/src/lib/paths.ts

+7
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,10 @@ export function listDescendantFiles(files: FileTree[], selectedPath: string) {
4646
}
4747
return descendants;
4848
}
49+
50+
export function removePathPrefix(path: string, prefix: string) {
51+
if (path.startsWith(prefix)) {
52+
return path.substring(prefix.length);
53+
}
54+
return path;
55+
}

0 commit comments

Comments
 (0)