@agentics.org/sparc2
Version:
SPARC 2.0 - Autonomous Vector Coding Agent + MCP. SPARC 2.0, vectorized AI code analysis, is an intelligent coding agent framework built to automate and streamline software development. It combines secure execution environments, and version control into a
414 lines (354 loc) • 12.7 kB
text/typescript
/**
* GitIntegration module for SPARC 2.0
* Provides functions to interact with Git and GitHub for autonomous diff-based code editing
*/
import { logMessage } from "../logger.ts";
/**
* Options for creating a commit
*/
export interface CommitOptions {
/** Whether to push the commit to the remote repository */
push?: boolean;
/** The remote repository to push to */
remote?: string;
/** Additional commit options */
options?: string[];
}
/**
* Create a commit using Git CLI commands
* @param branch Branch name
* @param filePath File that was modified
* @param commitMessage Commit message
* @param options Additional options for the commit
* @returns The commit hash
*/
export async function createCommit(
branch: string,
filePath: string | string[],
commitMessage: string,
options: CommitOptions = {},
): Promise<string> {
// Convert filePath to array if it's a string
const filePaths = Array.isArray(filePath) ? filePath : [filePath];
// Stage the files
for (const path of filePaths) {
const addCmd = new Deno.Command("git", {
args: ["add", path],
stdout: "piped",
stderr: "piped",
});
const addOutput = await addCmd.output();
if (!addOutput.success) {
const errorOutput = new TextDecoder().decode(addOutput.stderr);
await logMessage("error", `Git add failed for ${path}: ${errorOutput}`, { path });
throw new Error(`Git add failed for ${path}: ${errorOutput}`);
}
}
// Create the commit
const commitArgs = ["commit", "-m", commitMessage];
// Add any additional options
if (options.options) {
commitArgs.push(...options.options);
}
const commitCmd = new Deno.Command("git", {
args: commitArgs,
stdout: "piped",
stderr: "piped",
});
const commitOutput = await commitCmd.output();
if (!commitOutput.success) {
const errorOutput = new TextDecoder().decode(commitOutput.stderr);
await logMessage("error", `Git commit failed: ${errorOutput}`, { branch, filePaths });
throw new Error(`Git commit failed: ${errorOutput}`);
}
const commitText = new TextDecoder().decode(commitOutput.stdout);
const commitHashMatch = commitText.match(/\[([^\]]+)\s+([a-f0-9]+)\]/);
const commitHash = commitHashMatch ? commitHashMatch[2] : "";
await logMessage("info", `Commit created for ${filePaths.join(", ")}`, {
commitHash,
output: commitText.trim(),
});
// Push the commit if requested
if (options.push) {
const remote = options.remote || "origin";
const pushCmd = new Deno.Command("git", {
args: ["push", remote, branch],
stdout: "piped",
stderr: "piped",
});
const pushOutput = await pushCmd.output();
if (!pushOutput.success) {
const errorOutput = new TextDecoder().decode(pushOutput.stderr);
await logMessage("error", `Git push failed: ${errorOutput}`, { branch, remote });
throw new Error(`Git push failed: ${errorOutput}`);
}
await logMessage("info", `Pushed commit to ${remote}/${branch}`, { commitHash });
}
return commitHash;
}
/**
* Options for rolling back changes
*/
export interface RollbackOptions {
/** Whether to create a new branch for the rollback */
newBranch?: string;
/** Whether to push the rollback to the remote repository */
push?: boolean;
/** The remote repository to push to */
remote?: string;
}
/**
* Roll back changes to a specific commit
* @param commit Commit hash to roll back to
* @param message Message for the rollback commit
* @param options Additional options for the rollback
* @returns The new commit hash
*/
export async function rollbackChanges(
commit: string,
message: string = `Rollback to ${commit}`,
options: RollbackOptions = {},
): Promise<string> {
// Check if the commit exists
const revParseCmd = new Deno.Command("git", {
args: ["rev-parse", "--verify", commit],
stdout: "piped",
stderr: "piped",
});
const revParseOutput = await revParseCmd.output();
if (!revParseOutput.success) {
const errorOutput = new TextDecoder().decode(revParseOutput.stderr);
await logMessage("error", `Commit ${commit} not found: ${errorOutput}`, { commit });
throw new Error(`Commit ${commit} not found: ${errorOutput}`);
}
// Create a new branch if requested
if (options.newBranch) {
const branchCmd = new Deno.Command("git", {
args: ["checkout", "-b", options.newBranch],
stdout: "piped",
stderr: "piped",
});
const branchOutput = await branchCmd.output();
if (!branchOutput.success) {
const errorOutput = new TextDecoder().decode(branchOutput.stderr);
await logMessage("error", `Creating new branch failed: ${errorOutput}`, {
branch: options.newBranch,
});
throw new Error(`Creating new branch failed: ${errorOutput}`);
}
}
// Reset to the specified commit
const resetCmd = new Deno.Command("git", {
args: ["reset", "--hard", commit],
stdout: "piped",
stderr: "piped",
});
const resetOutput = await resetCmd.output();
if (!resetOutput.success) {
const errorOutput = new TextDecoder().decode(resetOutput.stderr);
await logMessage("error", `Git reset failed: ${errorOutput}`, { commit });
throw new Error(`Git reset failed: ${errorOutput}`);
}
const resetText = new TextDecoder().decode(resetOutput.stdout);
await logMessage("info", `Reset to commit ${commit}`, { output: resetText.trim() });
// Push the changes if requested
if (options.push) {
const remote = options.remote || "origin";
const branch = options.newBranch || await getCurrentBranch();
const pushCmd = new Deno.Command("git", {
args: ["push", "--force", remote, branch],
stdout: "piped",
stderr: "piped",
});
const pushOutput = await pushCmd.output();
if (!pushOutput.success) {
const errorOutput = new TextDecoder().decode(pushOutput.stderr);
await logMessage("error", `Git push failed: ${errorOutput}`, { branch, remote });
throw new Error(`Git push failed: ${errorOutput}`);
}
await logMessage("info", `Pushed rollback to ${remote}/${branch}`, { commit });
}
return commit;
}
/**
* Options for reverting changes
*/
export interface RevertOptions {
/** Whether to create a new branch for the revert */
newBranch?: string;
/** Whether to push the revert to the remote repository */
push?: boolean;
/** The remote repository to push to */
remote?: string;
}
/**
* Revert changes since a specific date
* @param target Date or commit to revert to
* @param options Additional options for the revert
*/
export async function revertChanges(
target: string,
options: RevertOptions = {},
): Promise<void> {
// Check if the target is a valid date or commit
try {
// Try to parse as a date
new Date(target);
} catch (error) {
// If not a date, check if it's a valid commit
const revParseCmd = new Deno.Command("git", {
args: ["rev-parse", "--verify", target],
stdout: "piped",
stderr: "piped",
});
const revParseOutput = await revParseCmd.output();
if (!revParseOutput.success) {
const errorOutput = new TextDecoder().decode(revParseOutput.stderr);
await logMessage("error", `Invalid target: ${errorOutput}`, { target });
throw new Error(`Invalid target: ${errorOutput}`);
}
}
// Get commits since the target date
const logCmd = new Deno.Command("git", {
args: ["log", "--since", target, "--format=%H"],
stdout: "piped",
stderr: "piped",
});
const logOutput = await logCmd.output();
if (!logOutput.success) {
const errorOutput = new TextDecoder().decode(logOutput.stderr);
await logMessage("error", `Git log failed: ${errorOutput}`, { target });
throw new Error(`Git log failed: ${errorOutput}`);
}
const logText = new TextDecoder().decode(logOutput.stdout);
const commits = logText.trim().split("\n").filter(Boolean);
if (commits.length === 0) {
await logMessage("info", `No commits found since ${target}`);
return;
}
// Create a new branch if requested
if (options.newBranch) {
const branchCmd = new Deno.Command("git", {
args: ["checkout", "-b", options.newBranch],
stdout: "piped",
stderr: "piped",
});
const branchOutput = await branchCmd.output();
if (!branchOutput.success) {
const errorOutput = new TextDecoder().decode(branchOutput.stderr);
await logMessage("error", `Creating new branch failed: ${errorOutput}`, {
branch: options.newBranch,
});
throw new Error(`Creating new branch failed: ${errorOutput}`);
}
}
// Create a revert commit for each commit
for (const commit of commits) {
const revertCmd = new Deno.Command("git", {
args: ["revert", "--no-edit", commit],
stdout: "piped",
stderr: "piped",
});
const revertOutput = await revertCmd.output();
if (!revertOutput.success) {
const errorOutput = new TextDecoder().decode(revertOutput.stderr);
await logMessage("error", `Git revert failed for ${commit}: ${errorOutput}`, { commit });
throw new Error(`Git revert failed for ${commit}: ${errorOutput}`);
}
}
// Push the changes if requested
if (options.push) {
const remote = options.remote || "origin";
const branch = options.newBranch || await getCurrentBranch();
const pushCmd = new Deno.Command("git", {
args: ["push", remote, branch],
stdout: "piped",
stderr: "piped",
});
const pushOutput = await pushCmd.output();
if (!pushOutput.success) {
const errorOutput = new TextDecoder().decode(pushOutput.stderr);
await logMessage("error", `Git push failed: ${errorOutput}`, { branch, remote });
throw new Error(`Git push failed: ${errorOutput}`);
}
}
}
/**
* Create a checkpoint tag
* @param name Name of the checkpoint
* @returns The commit hash of the checkpoint
*/
export async function createCheckpoint(name: string): Promise<string> {
// Add a timestamp to make the tag name unique
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const uniqueName = `${name}-${timestamp}`;
// Create a tag for the checkpoint
const tagCmd = new Deno.Command("git", {
args: ["tag", uniqueName],
stdout: "piped",
stderr: "piped",
});
const tagOutput = await tagCmd.output();
if (!tagOutput.success) {
const errorOutput = new TextDecoder().decode(tagOutput.stderr);
await logMessage("error", `Creating checkpoint tag failed: ${errorOutput}`, {
name: uniqueName,
});
throw new Error(`Creating checkpoint tag failed: ${errorOutput}`);
}
// Get the commit hash for the tag
const revParseCmd = new Deno.Command("git", {
args: ["rev-parse", uniqueName],
stdout: "piped",
stderr: "piped",
});
const revParseOutput = await revParseCmd.output();
if (!revParseOutput.success) {
const errorOutput = new TextDecoder().decode(revParseOutput.stderr);
await logMessage("error", `Getting checkpoint commit hash failed: ${errorOutput}`, {
name: uniqueName,
});
throw new Error(`Getting checkpoint commit hash failed: ${errorOutput}`);
}
const commitHash = new TextDecoder().decode(revParseOutput.stdout).trim();
await logMessage("info", `Created checkpoint ${uniqueName} at commit ${commitHash}`);
return commitHash;
}
/**
* Get the current branch name
* @returns The current branch name
*/
export async function getCurrentBranch(): Promise<string> {
const cmd = new Deno.Command("git", {
args: ["rev-parse", "--abbrev-ref", "HEAD"],
stdout: "piped",
stderr: "piped",
});
const output = await cmd.output();
if (!output.success) {
const errorOutput = new TextDecoder().decode(output.stderr);
await logMessage("error", `Getting current branch failed: ${errorOutput}`);
throw new Error(`Getting current branch failed: ${errorOutput}`);
}
const branch = new TextDecoder().decode(output.stdout).trim();
return branch;
}
/**
* Check if the repository is clean (no uncommitted changes)
* @returns True if the repository is clean
*/
export async function isRepoClean(): Promise<boolean> {
const cmd = new Deno.Command("git", {
args: ["status", "--porcelain"],
stdout: "piped",
stderr: "piped",
});
const output = await cmd.output();
if (!output.success) {
const errorOutput = new TextDecoder().decode(output.stderr);
await logMessage("error", `Checking repo status failed: ${errorOutput}`);
throw new Error(`Checking repo status failed: ${errorOutput}`);
}
const statusText = new TextDecoder().decode(output.stdout);
return statusText.trim() === "";
}