Replace native Git with Dugite to support users without Git installed (#1760)
I moved all isomorphic-git usage into a single git_utils.ts file and added Dugite as an alternative Git provider. The app now checks the user’s settings and uses dugite when user enabled native git for all isomorphic-git commands. This makes it easy to fully remove isomorphic-git in the future by updating only git_utils.ts. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds Dugite-based native Git (bundled binary) and refactors all Git calls to a unified git_utils API, replacing direct isomorphic-git usage across the app. > > - **Git Platform Abstraction**: > - Introduces `dugite` and bundles Git via Electron Forge (`extraResource`) with `LOCAL_GIT_DIRECTORY` setup in `src/main.ts`. > - Adds `src/ipc/git_types.ts` and a comprehensive `src/ipc/utils/git_utils.ts` wrapper supporting both Dugite (native) and `isomorphic-git` (fallback): `commit`, `add`/`addAll`, `remove`, `init`, `clone`, `push`, `setRemoteUrl`, `currentBranch`, `listBranches`, `renameBranch`, `log`, `isIgnored`, `getCurrentCommitHash`, `getGitUncommittedFiles`, `getFileAtCommit`, `checkout`, `stageToRevert`. > - **Refactors (switch to git_utils)**: > - Replaces direct `isomorphic-git` imports in handlers and processors: `app_handlers`, `chat_handlers`, `createFromTemplate`, `github_handlers`, `import_handlers`, `portal_handlers`, `version_handlers`, `response_processor`, `neon_timestamp_utils`, `utils/codebase`. > - Updates tests to mock `git_utils` (`src/__tests__/chat_stream_handlers.test.ts`). > - **Behavioral/Feature Updates**: > - `createFromTemplate` uses `fetch` for GitHub API and `gitClone` for cloning with cache validation. > - GitHub integration uses `gitSetRemoteUrl`/`gitPush`/`gitClone`, handling public vs token URLs and directory creation when native Git is disabled. > - Versioning, imports, app file edits, migrations now stage/commit via `git_utils`. > - **UI/Copy**: > - Updates Settings description for “Enable Native Git”. > - **Config/Version**: > - Bumps version to `0.29.0-beta.1`; adds `dugite` dependency. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ba098f7f25d85fc6330a41dc718fbfd43fff2d6c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Will Chen <willchen90@gmail.com>
This commit is contained in:
committed by
GitHub
parent
a7bcec220a
commit
d3f3ac3ae1
@@ -1,42 +1,67 @@
|
||||
import { getGitAuthor } from "./git_author";
|
||||
import git from "isomorphic-git";
|
||||
import http from "isomorphic-git/http/node";
|
||||
import { exec } from "dugite";
|
||||
import fs from "node:fs";
|
||||
import { promises as fsPromises } from "node:fs";
|
||||
import pathModule from "node:path";
|
||||
import { exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { readSettings } from "../../main/settings";
|
||||
import log from "electron-log";
|
||||
const logger = log.scope("git_utils");
|
||||
const execAsync = promisify(exec);
|
||||
import type {
|
||||
GitBaseParams,
|
||||
GitFileParams,
|
||||
GitCheckoutParams,
|
||||
GitBranchRenameParams,
|
||||
GitCloneParams,
|
||||
GitCommitParams,
|
||||
GitLogParams,
|
||||
GitFileAtCommitParams,
|
||||
GitSetRemoteUrlParams,
|
||||
GitStageToRevertParams,
|
||||
GitInitParams,
|
||||
GitPushParams,
|
||||
GitCommit,
|
||||
} from "../git_types";
|
||||
|
||||
async function verboseExecAsync(
|
||||
command: string,
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
try {
|
||||
return await execAsync(command);
|
||||
} catch (error: any) {
|
||||
let errorMessage = `Error: ${error.message}`;
|
||||
if (error.stdout) {
|
||||
errorMessage += `\nStdout: ${error.stdout}`;
|
||||
}
|
||||
if (error.stderr) {
|
||||
errorMessage += `\nStderr: ${error.stderr}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
/**
|
||||
* Helper function that wraps exec and throws an error if the exit code is non-zero
|
||||
*/
|
||||
async function execOrThrow(
|
||||
args: string[],
|
||||
path: string,
|
||||
errorMessage?: string,
|
||||
): Promise<void> {
|
||||
const result = await exec(args, path);
|
||||
if (result.exitCode !== 0) {
|
||||
const errorDetails = result.stderr.trim() || result.stdout.trim();
|
||||
const error = errorMessage
|
||||
? `${errorMessage}. ${errorDetails}`
|
||||
: `Git command failed: ${args.join(" ")}. ${errorDetails}`;
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCurrentCommitHash({
|
||||
path,
|
||||
}: {
|
||||
path: string;
|
||||
}): Promise<string> {
|
||||
return await git.resolveRef({
|
||||
fs,
|
||||
dir: path,
|
||||
ref: "HEAD",
|
||||
});
|
||||
ref = "HEAD",
|
||||
}: GitInitParams): Promise<string> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
const result = await exec(["rev-parse", ref], path);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to resolve ref '${ref}': ${result.stderr.trim() || result.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
return result.stdout.trim();
|
||||
} else {
|
||||
return await git.resolveRef({
|
||||
fs,
|
||||
dir: path,
|
||||
ref,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function isGitStatusClean({
|
||||
@@ -46,8 +71,15 @@ export async function isGitStatusClean({
|
||||
}): Promise<boolean> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
const { stdout } = await execAsync(`git -C "${path}" status --porcelain`);
|
||||
return stdout.trim() === "";
|
||||
const result = await exec(["status", "--porcelain"], path);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Failed to get status: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// If output is empty, working directory is clean (no changes)
|
||||
const isClean = result.stdout.trim().length === 0;
|
||||
return isClean;
|
||||
} else {
|
||||
const statusMatrix = await git.statusMatrix({ fs, dir: path });
|
||||
return statusMatrix.every(
|
||||
@@ -60,21 +92,31 @@ export async function gitCommit({
|
||||
path,
|
||||
message,
|
||||
amend,
|
||||
}: {
|
||||
path: string;
|
||||
message: string;
|
||||
amend?: boolean;
|
||||
}): Promise<string> {
|
||||
}: GitCommitParams): Promise<string> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
let command = `git -C "${path}" commit -m "${message.replace(/"/g, '\\"')}"`;
|
||||
// Get author info to match isomorphic-git behavior
|
||||
const author = await getGitAuthor();
|
||||
// Perform the commit using dugite with --author flag
|
||||
const args = [
|
||||
"commit",
|
||||
"-m",
|
||||
message,
|
||||
"--author",
|
||||
`${author.name} <${author.email}>`,
|
||||
];
|
||||
if (amend) {
|
||||
command += " --amend";
|
||||
args.push("--amend");
|
||||
}
|
||||
|
||||
await verboseExecAsync(command);
|
||||
const { stdout } = await execAsync(`git -C "${path}" rev-parse HEAD`);
|
||||
return stdout.trim();
|
||||
await execOrThrow(args, path, "Failed to create commit");
|
||||
// Get the new commit hash
|
||||
const result = await exec(["rev-parse", "HEAD"], path);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get commit hash: ${result.stderr.trim() || result.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
return result.stdout.trim();
|
||||
} else {
|
||||
return git.commit({
|
||||
fs: fs,
|
||||
@@ -89,13 +131,14 @@ export async function gitCommit({
|
||||
export async function gitCheckout({
|
||||
path,
|
||||
ref,
|
||||
}: {
|
||||
path: string;
|
||||
ref: string;
|
||||
}): Promise<void> {
|
||||
}: GitCheckoutParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
await execAsync(`git -C "${path}" checkout "${ref.replace(/"/g, '\\"')}"`);
|
||||
await execOrThrow(
|
||||
["checkout", ref],
|
||||
path,
|
||||
`Failed to checkout ref '${ref}'`,
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
return git.checkout({ fs, dir: path, ref });
|
||||
@@ -105,17 +148,18 @@ export async function gitCheckout({
|
||||
export async function gitStageToRevert({
|
||||
path,
|
||||
targetOid,
|
||||
}: {
|
||||
path: string;
|
||||
targetOid: string;
|
||||
}): Promise<void> {
|
||||
}: GitStageToRevertParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
// Get the current HEAD commit hash
|
||||
const { stdout: currentHead } = await execAsync(
|
||||
`git -C "${path}" rev-parse HEAD`,
|
||||
);
|
||||
const currentCommit = currentHead.trim();
|
||||
const currentHeadResult = await exec(["rev-parse", "HEAD"], path);
|
||||
if (currentHeadResult.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get current commit: ${currentHeadResult.stderr.trim() || currentHeadResult.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const currentCommit = currentHeadResult.stdout.trim();
|
||||
|
||||
// If we're already at the target commit, nothing to do
|
||||
if (currentCommit === targetOid) {
|
||||
@@ -123,20 +167,31 @@ export async function gitStageToRevert({
|
||||
}
|
||||
|
||||
// Safety: refuse to run if the work-tree isn't clean.
|
||||
const { stdout: wtStatus } = await execAsync(
|
||||
`git -C "${path}" status --porcelain`,
|
||||
);
|
||||
if (wtStatus.trim() !== "") {
|
||||
const statusResult = await exec(["status", "--porcelain"], path);
|
||||
if (statusResult.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get status: ${statusResult.stderr.trim() || statusResult.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
if (statusResult.stdout.trim() !== "") {
|
||||
throw new Error("Cannot revert: working tree has uncommitted changes.");
|
||||
}
|
||||
|
||||
// Reset the working directory and index to match the target commit state
|
||||
// This effectively undoes all changes since the target commit
|
||||
await execAsync(`git -C "${path}" reset --hard "${targetOid}"`);
|
||||
await execOrThrow(
|
||||
["reset", "--hard", targetOid],
|
||||
path,
|
||||
`Failed to reset to target commit '${targetOid}'`,
|
||||
);
|
||||
|
||||
// Reset back to the original HEAD but keep the working directory as it is
|
||||
// This stages all the changes needed to revert to the target state
|
||||
await execAsync(`git -C "${path}" reset --soft "${currentCommit}"`);
|
||||
await execOrThrow(
|
||||
["reset", "--soft", currentCommit],
|
||||
path,
|
||||
"Failed to reset back to original HEAD",
|
||||
);
|
||||
} else {
|
||||
// Get status matrix comparing the target commit (previousVersionId as HEAD) with current working directory
|
||||
const matrix = await git.statusMatrix({
|
||||
@@ -187,32 +242,111 @@ export async function gitStageToRevert({
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitAddAll({ path }: { path: string }): Promise<void> {
|
||||
export async function gitAddAll({ path }: GitBaseParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
await execAsync(`git -C "${path}" add .`);
|
||||
await execOrThrow(["add", "."], path, "Failed to stage all files");
|
||||
return;
|
||||
} else {
|
||||
return git.add({ fs, dir: path, filepath: "." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitAdd({ path, filepath }: GitFileParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
await execOrThrow(
|
||||
["add", "--", filepath],
|
||||
path,
|
||||
`Failed to stage file '${filepath}'`,
|
||||
);
|
||||
} else {
|
||||
await git.add({
|
||||
fs,
|
||||
dir: path,
|
||||
filepath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitInit({
|
||||
path,
|
||||
ref = "main",
|
||||
}: GitInitParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
await execOrThrow(
|
||||
["init", "-b", ref],
|
||||
path,
|
||||
`Failed to initialize git repository with branch '${ref}'`,
|
||||
);
|
||||
} else {
|
||||
await git.init({
|
||||
fs,
|
||||
dir: path,
|
||||
defaultBranch: ref,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitRemove({
|
||||
path,
|
||||
filepath,
|
||||
}: GitFileParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
await execOrThrow(
|
||||
["rm", "-f", "--", filepath],
|
||||
path,
|
||||
`Failed to remove file '${filepath}'`,
|
||||
);
|
||||
} else {
|
||||
await git.remove({
|
||||
fs,
|
||||
dir: path,
|
||||
filepath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGitUncommittedFiles({
|
||||
path,
|
||||
}: GitBaseParams): Promise<string[]> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
const result = await exec(["status", "--porcelain"], path);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get uncommitted files: ${result.stderr.trim() || result.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
return result.stdout
|
||||
.toString()
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() !== "")
|
||||
.map((line) => line.slice(3).trim());
|
||||
} else {
|
||||
const statusMatrix = await git.statusMatrix({ fs, dir: path });
|
||||
return statusMatrix
|
||||
.filter((row) => row[1] !== 1 || row[2] !== 1 || row[3] !== 1)
|
||||
.map((row) => row[0]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFileAtCommit({
|
||||
path,
|
||||
filePath,
|
||||
commitHash,
|
||||
}: {
|
||||
path: string;
|
||||
filePath: string;
|
||||
commitHash: string;
|
||||
}): Promise<string | null> {
|
||||
}: GitFileAtCommitParams): Promise<string | null> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`git -C "${path}" show "${commitHash}:${filePath}"`,
|
||||
);
|
||||
return stdout;
|
||||
const result = await exec(["show", `${commitHash}:${filePath}`], path);
|
||||
if (result.exitCode !== 0) {
|
||||
// File doesn't exist at this commit or other error
|
||||
return null;
|
||||
}
|
||||
return result.stdout;
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`Error getting file at commit ${commitHash}: ${error.message}`,
|
||||
@@ -238,3 +372,312 @@ export async function getFileAtCommit({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitListBranches({
|
||||
path,
|
||||
}: GitBaseParams): Promise<string[]> {
|
||||
const settings = readSettings();
|
||||
|
||||
if (settings.enableNativeGit) {
|
||||
const result = await exec(["branch", "--list"], path);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(result.stderr.toString());
|
||||
}
|
||||
// Parse output:
|
||||
// e.g. "* main\n feature/login"
|
||||
return result.stdout
|
||||
.toString()
|
||||
.split("\n")
|
||||
.map((line) => line.replace("*", "").trim())
|
||||
.filter((line) => line.length > 0);
|
||||
} else {
|
||||
return await git.listBranches({
|
||||
fs,
|
||||
dir: path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitRenameBranch({
|
||||
path,
|
||||
oldBranch,
|
||||
newBranch,
|
||||
}: GitBranchRenameParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
|
||||
if (settings.enableNativeGit) {
|
||||
// git branch -m oldBranch newBranch
|
||||
const result = await exec(["branch", "-m", oldBranch, newBranch], path);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(result.stderr.toString());
|
||||
}
|
||||
} else {
|
||||
await git.renameBranch({
|
||||
fs,
|
||||
dir: path,
|
||||
oldref: oldBranch,
|
||||
ref: newBranch,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitClone({
|
||||
path,
|
||||
url,
|
||||
accessToken,
|
||||
singleBranch = true,
|
||||
depth,
|
||||
}: GitCloneParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
// Dugite version (real Git)
|
||||
// Build authenticated URL if accessToken is provided and URL doesn't already have auth
|
||||
const finalUrl =
|
||||
accessToken && !url.includes("@")
|
||||
? url.replace("https://", `https://${accessToken}:x-oauth-basic@`)
|
||||
: url;
|
||||
const args = ["clone"];
|
||||
if (depth && depth > 0) {
|
||||
args.push("--depth", String(depth));
|
||||
}
|
||||
if (singleBranch) {
|
||||
args.push("--single-branch");
|
||||
}
|
||||
args.push(finalUrl, path);
|
||||
const result = await exec(args, ".");
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(result.stderr.toString());
|
||||
}
|
||||
} else {
|
||||
// isomorphic-git version
|
||||
// Strip any embedded auth from URL since isomorphic-git uses onAuth
|
||||
const cleanUrl = url.replace(/https:\/\/[^@]+@/, "https://");
|
||||
await git.clone({
|
||||
fs,
|
||||
http,
|
||||
dir: path,
|
||||
url: cleanUrl,
|
||||
onAuth: accessToken
|
||||
? () => ({
|
||||
username: accessToken,
|
||||
password: "x-oauth-basic",
|
||||
})
|
||||
: undefined,
|
||||
singleBranch,
|
||||
depth: depth ?? undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitSetRemoteUrl({
|
||||
path,
|
||||
remoteUrl,
|
||||
}: GitSetRemoteUrlParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
|
||||
if (settings.enableNativeGit) {
|
||||
// Dugite version
|
||||
try {
|
||||
// Try to add the remote
|
||||
const result = await exec(["remote", "add", "origin", remoteUrl], path);
|
||||
|
||||
// If remote already exists, update it instead
|
||||
if (result.exitCode !== 0 && result.stderr.includes("already exists")) {
|
||||
const updateResult = await exec(
|
||||
["remote", "set-url", "origin", remoteUrl],
|
||||
path,
|
||||
);
|
||||
|
||||
if (updateResult.exitCode !== 0) {
|
||||
throw new Error(`Failed to update remote: ${updateResult.stderr}`);
|
||||
}
|
||||
} else if (result.exitCode !== 0) {
|
||||
// Handle other errors
|
||||
throw new Error(`Failed to add remote: ${result.stderr}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("Error setting up remote:", error);
|
||||
throw error; // or handle as needed
|
||||
}
|
||||
} else {
|
||||
//isomorphic-git version
|
||||
await git.setConfig({
|
||||
fs,
|
||||
dir: path,
|
||||
path: "remote.origin.url",
|
||||
value: remoteUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitPush({
|
||||
path,
|
||||
branch,
|
||||
accessToken,
|
||||
force,
|
||||
}: GitPushParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
|
||||
if (settings.enableNativeGit) {
|
||||
// Dugite version
|
||||
try {
|
||||
// Push using the configured origin remote (which already has auth in URL)
|
||||
const args = ["push", "origin", `main:${branch}`];
|
||||
if (force) {
|
||||
args.push("--force");
|
||||
}
|
||||
const result = await exec(args, path);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
const errorMsg = result.stderr.toString() || result.stdout.toString();
|
||||
throw new Error(`Git push failed: ${errorMsg}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("Error during git push:", error);
|
||||
throw new Error(`Git push failed: ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
// isomorphic-git version
|
||||
await git.push({
|
||||
fs,
|
||||
http,
|
||||
dir: path,
|
||||
remote: "origin",
|
||||
ref: "main",
|
||||
remoteRef: branch,
|
||||
onAuth: () => ({
|
||||
username: accessToken,
|
||||
password: "x-oauth-basic",
|
||||
}),
|
||||
force: !!force,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitCurrentBranch({
|
||||
path,
|
||||
}: GitBaseParams): Promise<string | null> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
// Dugite version
|
||||
const result = await exec(["branch", "--show-current"], path);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get current branch: ${result.stderr.trim() || result.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
const branch = result.stdout.trim() || null;
|
||||
return branch;
|
||||
} else {
|
||||
// isomorphic-git version returns string | undefined
|
||||
const branch = await git.currentBranch({
|
||||
fs,
|
||||
dir: path,
|
||||
fullname: false,
|
||||
});
|
||||
return branch ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitLog({
|
||||
path,
|
||||
depth = 100_000,
|
||||
}: GitLogParams): Promise<GitCommit[]> {
|
||||
const settings = readSettings();
|
||||
|
||||
if (settings.enableNativeGit) {
|
||||
return await gitLogNative(path, depth);
|
||||
} else {
|
||||
// isomorphic-git fallback: this already returns the same structure
|
||||
return await git.log({
|
||||
fs,
|
||||
dir: path,
|
||||
depth,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitIsIgnored({
|
||||
path,
|
||||
filepath,
|
||||
}: GitFileParams): Promise<boolean> {
|
||||
const settings = readSettings();
|
||||
|
||||
if (settings.enableNativeGit) {
|
||||
// Dugite version
|
||||
// git check-ignore file
|
||||
const result = await exec(["check-ignore", filepath], path);
|
||||
|
||||
// If exitCode == 0 → file is ignored
|
||||
if (result.exitCode === 0) return true;
|
||||
|
||||
// If exitCode == 1 → not ignored
|
||||
if (result.exitCode === 1) return false;
|
||||
|
||||
// Other exit codes are actual errors
|
||||
throw new Error(result.stderr.toString());
|
||||
} else {
|
||||
// isomorphic-git version
|
||||
return await git.isIgnored({
|
||||
fs,
|
||||
dir: path,
|
||||
filepath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitLogNative(
|
||||
path: string,
|
||||
depth = 100_000,
|
||||
): Promise<GitCommit[]> {
|
||||
// Use git log with custom format to get all data in a single process
|
||||
// Format: %H = commit hash, %at = author timestamp (unix), %B = raw body (message)
|
||||
// Using null byte as field separator and custom delimiter between commits
|
||||
const logArgs = [
|
||||
"log",
|
||||
"--max-count",
|
||||
String(depth),
|
||||
"--format=%H%x00%at%x00%B%x00---END-COMMIT---",
|
||||
"HEAD",
|
||||
];
|
||||
|
||||
const logResult = await exec(logArgs, path);
|
||||
|
||||
if (logResult.exitCode !== 0) {
|
||||
throw new Error(logResult.stderr.toString());
|
||||
}
|
||||
|
||||
const output = logResult.stdout.toString().trim();
|
||||
if (!output) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Split by commit delimiter (without newline since trim() removes trailing newline)
|
||||
const commitChunks = output.split("\x00---END-COMMIT---").filter(Boolean);
|
||||
const entries: GitCommit[] = [];
|
||||
|
||||
for (const chunk of commitChunks) {
|
||||
// Split by null byte: [oid, timestamp, message]
|
||||
const parts = chunk.split("\x00");
|
||||
if (parts.length >= 3) {
|
||||
const oid = parts[0].trim();
|
||||
const timestamp = Number(parts[1]);
|
||||
// Message is everything after the second null byte, may contain null bytes itself
|
||||
const message = parts.slice(2).join("\x00");
|
||||
|
||||
entries.push({
|
||||
oid,
|
||||
commit: {
|
||||
message: message,
|
||||
author: {
|
||||
timestamp: timestamp,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user