Skip to content

Commit

Permalink
Merge pull request stackblitz-labs#24 from goncaloalves/main
Browse files Browse the repository at this point in the history
Added GitHub push functionality
  • Loading branch information
coleam00 authored Oct 24, 2024
2 parents 47a7932 + 059933a commit 5a10894
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 6 deletions.
25 changes: 25 additions & 0 deletions app/components/workbench/Workbench.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,31 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
<div className="i-ph:terminal" />
Toggle Terminal
</PanelHeaderButton>
<PanelHeaderButton
className="mr-1 text-sm"
onClick={() => {
const repoName = prompt("Please enter a name for your new GitHub repository:", "bolt-generated-project");
if (!repoName) {
alert("Repository name is required. Push to GitHub cancelled.");
return;
}
const githubUsername = prompt("Please enter your GitHub username:");
if (!githubUsername) {
alert("GitHub username is required. Push to GitHub cancelled.");
return;
}
const githubToken = prompt("Please enter your GitHub personal access token:");
if (!githubToken) {
alert("GitHub token is required. Push to GitHub cancelled.");
return;
}

workbenchStore.pushToGitHub(repoName, githubUsername, githubToken);
}}
>
<div className="i-ph:github-logo" />
Push to GitHub
</PanelHeaderButton>
</>
)}
<IconButton
Expand Down
106 changes: 106 additions & 0 deletions app/lib/stores/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { PreviewsStore } from './previews';
import { TerminalStore } from './terminal';
import JSZip from 'jszip';
import { saveAs } from 'file-saver';
import { Octokit } from "@octokit/rest";

export interface ArtifactState {
id: string;
Expand Down Expand Up @@ -303,6 +304,111 @@ export class WorkbenchStore {
const content = await zip.generateAsync({ type: 'blob' });
saveAs(content, 'project.zip');
}

async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) {

try {
// Get the GitHub auth token from environment variables
const githubToken = ghToken;

const owner = githubUsername;

if (!githubToken) {
throw new Error('GitHub token is not set in environment variables');
}

// Initialize Octokit with the auth token
const octokit = new Octokit({ auth: githubToken });

// Check if the repository already exists before creating it
let repo
try {
repo = await octokit.repos.get({ owner: owner, repo: repoName });
} catch (error) {
if (error instanceof Error && 'status' in error && error.status === 404) {
// Repository doesn't exist, so create a new one
const { data: newRepo } = await octokit.repos.createForAuthenticatedUser({
name: repoName,
private: false,
auto_init: true,
});
repo = newRepo;
} else {
console.log('cannot create repo!');
throw error; // Some other error occurred
}
}

// Get all files
const files = this.files.get();
if (!files || Object.keys(files).length === 0) {
throw new Error('No files found to push');
}

// Create blobs for each file
const blobs = await Promise.all(
Object.entries(files).map(async ([filePath, dirent]) => {
if (dirent?.type === 'file' && dirent.content) {
const { data: blob } = await octokit.git.createBlob({
owner: repo.owner.login,
repo: repo.name,
content: Buffer.from(dirent.content).toString('base64'),
encoding: 'base64',
});
return { path: filePath.replace(/^\/home\/project\//, ''), sha: blob.sha };
}
})
);

const validBlobs = blobs.filter(Boolean); // Filter out any undefined blobs

if (validBlobs.length === 0) {
throw new Error('No valid files to push');
}

// Get the latest commit SHA (assuming main branch, update dynamically if needed)
const { data: ref } = await octokit.git.getRef({
owner: repo.owner.login,
repo: repo.name,
ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
});
const latestCommitSha = ref.object.sha;

// Create a new tree
const { data: newTree } = await octokit.git.createTree({
owner: repo.owner.login,
repo: repo.name,
base_tree: latestCommitSha,
tree: validBlobs.map((blob) => ({
path: blob!.path,
mode: '100644',
type: 'blob',
sha: blob!.sha,
})),
});

// Create a new commit
const { data: newCommit } = await octokit.git.createCommit({
owner: repo.owner.login,
repo: repo.name,
message: 'Initial commit from your app',
tree: newTree.sha,
parents: [latestCommitSha],
});

// Update the reference
await octokit.git.updateRef({
owner: repo.owner.login,
repo: repo.name,
ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
sha: newCommit.sha,
});

alert(`Repository created and code pushed: ${repo.html_url}`);
} catch (error) {
console.error('Error pushing to GitHub:', error instanceof Error ? error.message : String(error));
}
}
}

export const workbenchStore = new WorkbenchStore();
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
"@iconify-json/svg-spinners": "^1.1.2",
"@lezer/highlight": "^1.2.0",
"@nanostores/react": "^0.7.2",
"@octokit/rest": "^21.0.2",
"@octokit/types": "^13.6.1",
"@openrouter/ai-sdk-provider": "^0.0.5",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
Expand Down
136 changes: 130 additions & 6 deletions pnpm-lock.yaml

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

0 comments on commit 5a10894

Please sign in to comment.