Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
"@types/ini": "4.1.0",
"@types/jest": "29.5.14",
"@types/node": "20.14.8",
"@types/pako": "1.0.4",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.0",
"@types/react-modal": "3.16.3",
Expand Down
2 changes: 2 additions & 0 deletions packages/docs/site/docs/blueprints/04-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type GitDirectoryReference = {
url: string; // Repository URL (https://, ssh git@..., etc.)
path?: string; // Optional subdirectory inside the repository
ref?: string; // Optional branch, tag, or commit SHA
'.git'?: boolean; // Experimental: include a .git directory with fetched metadata
};
```

Expand All @@ -84,6 +85,7 @@ type GitDirectoryReference = {
- Playground automatically detects providers like GitHub and GitLab.
- It handles CORS-proxied fetches and sparse checkouts, so you can use URLs that point to specific subdirectories or branches.
- This resource can be used with steps like [`installPlugin`](/blueprints/steps#InstallPluginStep) and [`installTheme`](/blueprints/steps#InstallThemeStep).
- Set `".git": true` to include a `.git` folder containing packfiles and refs so Git-aware tooling can detect the checkout. This currently mirrors a shallow clone of the selected ref.

### CoreThemeReference

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4028,6 +4028,11 @@ const schema25 = {
description:
'The path to the directory in the git repository. Defaults to the repo root.',
},
'.git': {
type: 'boolean',
description:
'When true, include a .git directory in the cloned files',
},
},
required: ['resource', 'url', 'ref'],
additionalProperties: false,
Expand Down Expand Up @@ -4070,7 +4075,8 @@ function validate19(
key0 === 'url' ||
key0 === 'ref' ||
key0 === 'refType' ||
key0 === 'path'
key0 === 'path' ||
key0 === '.git'
)
) {
validate19.errors = [
Expand Down Expand Up @@ -4224,6 +4230,35 @@ function validate19(
} else {
var valid0 = true;
}
if (valid0) {
if (data['.git'] !== undefined) {
const _errs13 = errors;
if (
typeof data['.git'] !==
'boolean'
) {
validate19.errors = [
{
instancePath:
instancePath +
'/.git',
schemaPath:
'#/properties/.git/type',
keyword: 'type',
params: {
type: 'boolean',
},
message:
'must be boolean',
},
];
return false;
}
var valid0 = _errs13 === errors;
} else {
var valid0 = true;
}
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/playground/blueprints/public/blueprint-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,10 @@
"path": {
"type": "string",
"description": "The path to the directory in the git repository. Defaults to the repo root."
},
".git": {
"type": "boolean",
"description": "When true, include a .git directory in the cloned files"
}
},
"required": ["resource", "url", "ref"],
Expand Down
110 changes: 110 additions & 0 deletions packages/playground/blueprints/src/lib/v1/resources.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {
} from './resources';
import { expect, describe, it, vi, beforeEach } from 'vitest';
import { StreamedFile } from '@php-wasm/stream-compression';
import { mkdtemp, rm, writeFile, mkdir } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { execSync, type ExecSyncOptions } from 'child_process';

describe('UrlResource', () => {
it('should create a new instance of UrlResource', () => {
Expand Down Expand Up @@ -75,6 +79,112 @@ describe('GitDirectoryResource', () => {
);
expect(files['dependabot.yml']).toBeInstanceOf(Uint8Array);
});

it('includes a .git directory when requested', async () => {
const commit = '05138293dd39e25a9fa8e43a9cc775d6fb780e37';
const resource = new GitDirectoryResource({
resource: 'git:directory',
url: 'https://github.com/WordPress/wordpress-playground',
ref: commit,
refType: 'commit',
path: 'packages/docs/site/docs/blueprints/tutorial',
'.git': true,
});

const { files } = await resource.resolve();

// Create a temporary directory and write all files to disk
const tmpDir = await mkdtemp(join(tmpdir(), 'git-test-'));
try {
// Write all files to the temporary directory
for (const [path, content] of Object.entries(files)) {
const fullPath = join(tmpDir, path);
const dir = join(fullPath, '..');
await mkdir(dir, { recursive: true });

if (typeof content === 'string') {
await writeFile(fullPath, content, 'utf8');
} else {
await writeFile(fullPath, content);
}
}

// Run git commands to verify the repository state
const gitEnv: ExecSyncOptions = {
cwd: tmpDir,
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024, // 10MB buffer to handle large output
stdio: ['pipe', 'pipe', 'ignore'], // Suppress stderr to avoid buffer overflow
};

// Verify we're on the expected commit
const currentCommit = execSync('git rev-parse HEAD', gitEnv)
.toString()
.trim();
expect(currentCommit).toBe(commit);

// Verify the remote is configured correctly
const remoteUrl = execSync('git remote get-url origin', gitEnv)
.toString()
.trim();
expect(remoteUrl).toBe(
'https://github.com/WordPress/wordpress-playground'
);

// Verify this is a shallow clone
const isShallow = execSync(
'git rev-parse --is-shallow-repository',
gitEnv
)
.toString()
.trim();
expect(isShallow).toBe('true');

// Verify the shallow file contains the expected commit
const shallowCommit = execSync('cat .git/shallow', gitEnv)
.toString()
.trim();
expect(shallowCommit).toBe(commit);

// Verify the expected files exist in the git index
const lsFiles = execSync('git ls-files', gitEnv)
.toString()
.trim()
.split('\n')
.filter((f) => f.length > 0)
.sort();
expect(lsFiles).toEqual([
'01-what-are-blueprints-what-you-can-do-with-them.md',
'02-how-to-load-run-blueprints.md',
'03-build-your-first-blueprint.md',
'index.md',
]);

// Verify we can run git log to see commit history
const logOutput = execSync('git log --oneline -n 1', gitEnv)
.toString()
.trim();
expect(logOutput).toContain(commit.substring(0, 7));

// Update the git index to match the actual files on disk
execSync('git add -A', gitEnv);

// Modify a file and verify git status detects the change
const fileToModify = join(tmpDir, 'index.md');
await writeFile(fileToModify, 'modified content\n', 'utf8');
const statusAfterModification = execSync(
'git status --porcelain',
gitEnv
)
.toString()
.trim();
// Git status should show the file as modified (can be ' M' or 'M ')
expect(statusAfterModification).toMatch(/M.*index\.md/);
} finally {
// Clean up the temporary directory
await rm(tmpDir, { recursive: true, force: true });
}
});
});

describe('name', () => {
Expand Down
28 changes: 27 additions & 1 deletion packages/playground/blueprints/src/lib/v1/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { zipNameToHumanName } from '../utils/zip-name-to-human-name';
import { fetchWithCorsProxy } from '@php-wasm/web';
import { StreamedFile } from '@php-wasm/stream-compression';
import type { StreamBundledFile } from './types';
import { createDotGitDirectory } from '@wp-playground/storage';

export type { FileTree };
export const ResourceTypes = [
Expand Down Expand Up @@ -74,6 +75,8 @@ export type GitDirectoryReference = {
refType?: GitDirectoryRefType;
/** The path to the directory in the git repository. Defaults to the repo root. */
path?: string;
/** When true, include a `.git` directory with Git metadata (experimental). */
'.git'?: boolean;
};
export interface Directory {
files: FileTree;
Expand Down Expand Up @@ -579,12 +582,35 @@ export class GitDirectoryResource extends Resource<Directory> {

const requestedPath = (this.reference.path ?? '').replace(/^\/+/, '');
const filesToClone = listDescendantFiles(allFiles, requestedPath);
let files = await sparseCheckout(repoUrl, commitHash, filesToClone);
const checkout = await sparseCheckout(
repoUrl,
commitHash,
filesToClone,
{
withObjects: this.reference['.git'],
}
);
let files = checkout.files;

// Remove the path prefix from the cloned file names.
files = mapKeys(files, (name) =>
name.substring(requestedPath.length).replace(/^\/+/, '')
);
if (this.reference['.git']) {
const gitFiles = await createDotGitDirectory({
repoUrl: this.reference.url,
commitHash,
ref: this.reference.ref,
refType: this.reference.refType,
objects: checkout.objects ?? [],
fileOids: checkout.fileOids ?? {},
pathPrefix: requestedPath,
});
files = {
...gitFiles,
...files,
};
}
return {
name: this.filename,
files,
Expand Down
5 changes: 4 additions & 1 deletion packages/playground/blueprints/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"include": [
"src/**/*.ts",
"../storage/src/lib/git-create-dotgit-directory.ts"
],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}
4 changes: 2 additions & 2 deletions packages/playground/components/src/demos/GitBrowserDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ export default function GitBrowserDemo() {
Object.keys(filesToCheckout)
);
const checkedOutFiles: Record<string, string> = {};
for (const filename in result) {
for (const filename in result.files) {
checkedOutFiles[filename] = new TextDecoder().decode(
result[filename]
result.files[filename]
);
}
setCheckedOutFiles(checkedOutFiles);
Expand Down
1 change: 1 addition & 0 deletions packages/playground/storage/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export * from './lib/changeset';
export * from './lib/playground';
export * from './lib/browser-fs';
export * from './lib/git-sparse-checkout';
export * from './lib/git-create-dotgit-directory';
export * from './lib/paths';
export * from './lib/filesystems';
Loading