Skip to content

Commit

Permalink
Add feature to delete, and publish the contribution
Browse files Browse the repository at this point in the history
Signed-off-by: Anil Vishnoi <[email protected]>
  • Loading branch information
vishnoianil committed Dec 12, 2024
1 parent a8f5fad commit 4f5189d
Show file tree
Hide file tree
Showing 6 changed files with 425 additions and 71 deletions.
3 changes: 2 additions & 1 deletion src/app/api/envConfig/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export async function GET() {
UPSTREAM_REPO_NAME: process.env.NEXT_PUBLIC_TAXONOMY_REPO || '',
DEPLOYMENT_TYPE: process.env.IL_UI_DEPLOYMENT || '',
ENABLE_DEV_MODE: process.env.IL_ENABLE_DEV_MODE || '',
EXPERIMENTAL_FEATURES: process.env.NEXT_PUBLIC_EXPERIMENTAL_FEATURES || ''
EXPERIMENTAL_FEATURES: process.env.NEXT_PUBLIC_EXPERIMENTAL_FEATURES || '',
TAXONOMY_REPO_DIR: process.env.NEXT_PUBLIC_TAXONOMY_REPO_DIR || ''
};

return NextResponse.json(envConfig);
Expand Down
8 changes: 4 additions & 4 deletions src/app/api/native/clone-repo/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import fs from 'fs';
import path from 'path';

// Retrieve the base directory from the environment variable
const TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || './.instructlab-ui';
const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || './.instructlab-ui';
const TAXONOMY_REPO_URL = process.env.NEXT_PUBLIC_TAXONOMY_REPO_URL || 'https://github.com/instructlab/taxonomy.git';

export async function POST() {
const taxonomyDirectoryPath = path.join(TAXONOMY_ROOT_DIR, '/taxonomy');
const taxonomyDirectoryPath = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy');

if (fs.existsSync(taxonomyDirectoryPath)) {
const files = fs.readdirSync(taxonomyDirectoryPath);
Expand All @@ -32,8 +32,8 @@ export async function POST() {
});

// Include the full path in the response for client display
console.log(`Repository cloned successfully to ${TAXONOMY_ROOT_DIR}.`);
return NextResponse.json({ message: `Repository cloned successfully to ${TAXONOMY_ROOT_DIR}.` }, { status: 200 });
console.log(`Repository cloned successfully to ${LOCAL_TAXONOMY_ROOT_DIR}.`);
return NextResponse.json({ message: `Repository cloned successfully to ${LOCAL_TAXONOMY_ROOT_DIR}.` }, { status: 200 });
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
console.error(`Failed to clone taxonomy repository: ${errorMessage}`);
Expand Down
255 changes: 201 additions & 54 deletions src/app/api/native/git/branches/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import fs from 'fs';
import path from 'path';

// Get the repository path from the environment variable
const TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || './.instructlab-ui';
const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || './.instructlab-ui';
interface Diffs {
file: string;
status: string;
}

export async function GET() {
const REPO_DIR = path.join(TAXONOMY_ROOT_DIR, '/taxonomy');
const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy');
try {
// Ensure the repository path exists
if (!fs.existsSync(REPO_DIR)) {
Expand Down Expand Up @@ -41,91 +45,234 @@ export async function GET() {

// Handle POST requests for merge or branch comparison
export async function POST(req: NextRequest) {
const REPO_DIR = path.join(TAXONOMY_ROOT_DIR, '/taxonomy');
const { branchName, action } = await req.json();
const LOCAL_TAXONOMY_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy');
const { branchName, action, remoteTaxonomyRepoDir } = await req.json();
console.log('Received POST request:', { branchName, action, remoteTaxonomyRepoDir });

if (action === 'delete') {
return handleDelete(branchName, LOCAL_TAXONOMY_DIR);
}

if (action === 'diff') {
return handleDiff(branchName, LOCAL_TAXONOMY_DIR);
}

if (action === 'publish') {
return handlePublish(branchName, LOCAL_TAXONOMY_DIR, remoteTaxonomyRepoDir);
}
return NextResponse.json({ error: 'Invalid action specified' }, { status: 400 });
}

async function handleDelete(branchName: string, localTaxonomyDir: string) {
try {
if (action === 'merge') {
// Ensure valid branch name
if (!branchName || branchName === 'main') {
return NextResponse.json({ error: 'Invalid branch name for merge' }, { status: 400 });
}
if (!branchName || branchName === 'main') {
return NextResponse.json({ error: 'Invalid branch name for deletion' }, { status: 400 });
}

// Initialize the repository and checkout main branch
await git.init({ fs, dir: REPO_DIR });
await git.checkout({ fs, dir: REPO_DIR, ref: 'main' });
// Delete the target branch
await git.deleteBranch({ fs, dir: localTaxonomyDir, ref: branchName });

// Perform the merge
await git.merge({
fs,
dir: REPO_DIR,
ours: 'main',
theirs: branchName,
author: {
name: 'Instruct Lab Local',
email: '[email protected]'
}
});
return NextResponse.json({ message: `Successfully deleted branch ${branchName}.` }, { status: 200 });
} catch (error) {
console.error(`Failed to delete contribution ${branchName}:`, error);
return NextResponse.json(
{
error: `Failed to delete contribution ${branchName}`
},
{ status: 500 }
);
} finally {
// Ensure switching back to 'main' branch after any operation
try {
await git.checkout({ fs, dir: localTaxonomyDir, ref: 'main' });
} catch (checkoutError) {
console.error('Failed to switch back to main branch:', checkoutError);
}
}
}

async function handleDiff(branchName: string, localTaxonomyDir: string) {
try {
// Ensure valid branch name
if (!branchName || branchName === 'main') {
return NextResponse.json({ error: 'Invalid branch name for comparison' }, { status: 400 });
}

const changes = await findDiff(branchName, localTaxonomyDir);
return NextResponse.json({ changes }, { status: 200 });
} catch (error) {
console.error(`Failed to show contribution changes ${branchName}:`, error);
return NextResponse.json(
{
error: `Failed to show contribution changes for ${branchName}`
},
{ status: 500 }
);
} finally {
// Ensure switching back to 'main' branch after any operation
try {
await git.checkout({ fs, dir: localTaxonomyDir, ref: 'main' });
} catch (checkoutError) {
console.error('Failed to switch back to main branch:', checkoutError);
}
}
}

async function findDiff(branchName: string, localTaxonomyDir: string): Promise<Diffs[]> {
// Fetch the commit SHA for `main` and the target branch
const mainCommit = await git.resolveRef({ fs, dir: localTaxonomyDir, ref: 'main' });
const branchCommit = await git.resolveRef({ fs, dir: localTaxonomyDir, ref: branchName });

return NextResponse.json({ message: `Successfully merged ${branchName} into main.` }, { status: 200 });
} else if (action === 'diff') {
// Ensure valid branch name
if (!branchName || branchName === 'main') {
return NextResponse.json({ error: 'Invalid branch name for comparison' }, { status: 400 });
const mainFiles = await getFilesFromTree(mainCommit);
const branchFiles = await getFilesFromTree(branchCommit);

// Create an array of Diffs to store changes
const changes: Diffs[] = [];
// Identify modified and deleted files
for (const file in mainFiles) {
if (branchFiles[file]) {
if (mainFiles[file] !== branchFiles[file]) {
changes.push({ file, status: 'modified' });
}
} else {
changes.push({ file, status: 'deleted' });
}
}

// Identify added files
for (const file in branchFiles) {
if (!mainFiles[file]) {
changes.push({ file, status: 'added' });
}
}
return changes;
}

// Fetch the commit SHA for `main` and the target branch
const mainCommit = await git.resolveRef({ fs, dir: REPO_DIR, ref: 'main' });
const branchCommit = await git.resolveRef({ fs, dir: REPO_DIR, ref: branchName });
async function getTopCommitDetails(dir: string, ref: string = 'HEAD') {
try {
// Fetch the top commit (latest commit on the branch)
const [topCommit] = await git.log({
fs,
dir,
ref,
depth: 1 // Only fetch the latest commit
});

const mainFiles = await getFilesFromTree(mainCommit);
const branchFiles = await getFilesFromTree(branchCommit);
if (!topCommit) {
throw new Error('No commits found in the repository.');
}

const changes = [];
// Extract commit message
const commitMessage = topCommit.commit.message;

// Identify modified and deleted files
for (const file in mainFiles) {
if (branchFiles[file]) {
if (mainFiles[file] !== branchFiles[file]) {
changes.push({ file, status: 'modified' });
}
} else {
changes.push({ file, status: 'deleted' });
}
// Check for Signed-off-by line
const signoffMatch = commitMessage.match(/^Signed-off-by: (.+)$/m);
const signoff = signoffMatch ? signoffMatch[1] : null;

return {
message: commitMessage,
signoff
};
} catch (error) {
console.error('Error reading top commit details:', error);
throw error;
}
}
async function handlePublish(branchName: string, localTaxonomyDir: string, remoteTaxonomyDir: string) {
try {
if (!branchName || branchName === 'main') {
return NextResponse.json({ error: 'Invalid branch name for publish' }, { status: 400 });
}

console.log(`Publishing contribution from ${branchName} to remote taxonomy repo at ${remoteTaxonomyDir}`);
const changes = await findDiff(branchName, localTaxonomyDir);

// Check if there are any changes to publish, create a new branch at remoteTaxonomyDir and copy all the files listed in the changes array to the new branch and create a commit
if (changes.length > 0) {
const remoteBranchName = branchName;
await git.checkout({ fs, dir: localTaxonomyDir, ref: branchName });
// Read the commit message of the top commit from the branch
const details = await getTopCommitDetails(localTaxonomyDir);

// Check if the remote branch exists, if not create it
const remoteBranchExists = await git.listBranches({ fs, dir: remoteTaxonomyDir });
if (remoteBranchExists.includes(remoteBranchName)) {
console.log(`Branch ${remoteBranchName} exist in remote taxonomy, deleting it.`);
// Delete the remote branch if it exists, we will recreate it
await git.deleteBranch({ fs, dir: remoteTaxonomyDir, ref: remoteBranchName });
} else {
console.log(`Branch ${remoteBranchName} does not exist in remote taxonomy, creating a new branch.`);
}

// Identify added files
for (const file in branchFiles) {
if (!mainFiles[file]) {
changes.push({ file, status: 'added' });
await git.checkout({ fs, dir: remoteTaxonomyDir, ref: 'main' });
await git.branch({ fs, dir: remoteTaxonomyDir, ref: remoteBranchName });
await git.checkout({ fs, dir: remoteTaxonomyDir, ref: remoteBranchName });

// Copy the files listed in the changes array to the remote branch and if the directories do not exist, create them
for (const change of changes) {
console.log(`Copying ${change.file} to remote branch`);
const filePath = path.join(localTaxonomyDir, change.file);
const remoteFilePath = path.join(remoteTaxonomyDir, change.file);
const remoteFileDir = path.dirname(remoteFilePath);
if (!fs.existsSync(remoteFileDir)) {
fs.mkdirSync(remoteFileDir, { recursive: true });
}
fs.copyFileSync(filePath, remoteFilePath);
}

return NextResponse.json({ changes }, { status: 200 });
await git.add({ fs, dir: remoteTaxonomyDir, filepath: '.' });

const authorInfo = details.signoff!.match(/(.*?) <(.*?)>/);
let authorName = '';
let authorEmail = '';
if (authorInfo) {
console.log(`Author information found in signoff: ${authorInfo}`);
authorName = authorInfo[1];
authorEmail = authorInfo[2];
} else {
return NextResponse.json({ message: `Author information is not present in the contribution ${branchName}.` }, { status: 500 });
}
// Create a commit with the same message and signoff as the top commit from the local branch
await git.commit({
fs,
dir: remoteTaxonomyDir,
message: details.message,
author: {
name: authorName,
email: authorEmail
}
});
console.log(`Successfully published contribution from ${branchName} to remote taxonomy repo at ${remoteTaxonomyDir}`);
return NextResponse.json({ message: `Successfully published contribution to ${remoteTaxonomyDir}.` }, { status: 200 });
} else {
return NextResponse.json({ error: 'Invalid action specified' }, { status: 400 });
return NextResponse.json({ message: `No changes to publish from ${branchName}.` }, { status: 200 });
}
} catch (error) {
console.error(`Failed to ${action === 'merge' ? 'merge branch' : 'compare branches'}:`, error);
console.error(`Failed to publish contribution from ${branchName}:`, error);
return NextResponse.json(
{
error: `Failed to ${action === 'merge' ? 'merge branch' : 'compare branches'}`
error: `Failed to publish contribution from ${branchName}`
},
{ status: 500 }
);
} finally {
// Ensure switching back to 'main' branch after any operation
try {
await git.checkout({ fs, dir: REPO_DIR, ref: 'main' });
await git.checkout({ fs, dir: localTaxonomyDir, ref: 'main' });
} catch (checkoutError) {
console.error('Failed to switch back to main branch:', checkoutError);
console.error('Failed to switch back to main branch in local taxonomy repo:', checkoutError);
}
try {
await git.checkout({ fs, dir: remoteTaxonomyDir, ref: 'main' });
} catch (checkoutError) {
console.error('Failed to switch back to main branch in remote taxonomy repo:', checkoutError);
}
}
}

// Helper function to recursively gather file paths and their oids from a tree
async function getFilesFromTree(commitOid: string) {
const REPO_DIR = path.join(TAXONOMY_ROOT_DIR, '/taxonomy');
const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy');
const fileMap: Record<string, string> = {};

async function walkTree(dir: string) {
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/native/pr/knowledge/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import { KnowledgeYamlData } from '@/types';
import yaml from 'js-yaml';

// Define paths and configuration
const TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || './.instructlab-ui';
const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || './.instructlab-ui';

const KNOWLEDGE_DIR = 'knowledge';

export async function POST(req: NextRequest) {
const REPO_DIR = path.join(TAXONOMY_ROOT_DIR, '/taxonomy');
const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy');
try {
// Extract the data from the request body
const { content, attribution, name, email, submissionSummary, filePath } = await req.json();
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/native/pr/skill/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import path from 'path';
import yaml from 'js-yaml';

// Define paths and configuration
const TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || './.instructlab-ui';
const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || './.instructlab-ui';

const SKILLS_DIR = 'compositional_skills';

export async function POST(req: NextRequest) {
const REPO_DIR = path.join(TAXONOMY_ROOT_DIR, '/taxonomy');
const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy');
try {
// Extract the QnA data from the request body TODO: what is documentOutline?
const { content, attribution, name, email, submissionSummary, documentOutline, filePath } = await req.json(); // eslint-disable-line @typescript-eslint/no-unused-vars
Expand Down
Loading

0 comments on commit 4f5189d

Please sign in to comment.