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
@@ -74,6 +74,7 @@ const config: ForgeConfig = {
|
|||||||
},
|
},
|
||||||
asar: true,
|
asar: true,
|
||||||
ignore,
|
ignore,
|
||||||
|
extraResource: ["node_modules/dugite/git"],
|
||||||
// ignore: [/node_modules\/(?!(better-sqlite3|bindings|file-uri-to-path)\/)/],
|
// ignore: [/node_modules\/(?!(better-sqlite3|bindings|file-uri-to-path)\/)/],
|
||||||
},
|
},
|
||||||
rebuildConfig: {
|
rebuildConfig: {
|
||||||
|
|||||||
94
package-lock.json
generated
94
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "dyad",
|
"name": "dyad",
|
||||||
"version": "0.28.0",
|
"version": "0.29.0-beta.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "dyad",
|
"name": "dyad",
|
||||||
"version": "0.28.0",
|
"version": "0.29.0-beta.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "^3.0.15",
|
"@ai-sdk/amazon-bedrock": "^3.0.15",
|
||||||
@@ -59,6 +59,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"drizzle-orm": "^0.41.0",
|
"drizzle-orm": "^0.41.0",
|
||||||
|
"dugite": "^3.0.0",
|
||||||
"electron-log": "^5.3.3",
|
"electron-log": "^5.3.3",
|
||||||
"electron-playwright-helpers": "^1.7.1",
|
"electron-playwright-helpers": "^1.7.1",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
@@ -8090,6 +8091,20 @@
|
|||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/b4a": {
|
||||||
|
"version": "1.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
|
||||||
|
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native-b4a": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-b4a": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bail": {
|
"node_modules/bail": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
||||||
@@ -8106,6 +8121,20 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bare-events": {
|
||||||
|
"version": "2.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||||
|
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"bare-abort-controller": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-abort-controller": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@@ -10203,6 +10232,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dugite": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dugite/-/dugite-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-+q2i3y5TvlC2YaZofkdELHtmvHbT6yuBODimItxU6xEGtHqRt6rpApJzf6lAqtpo+y1gokhfsHyULH0yNZuTWQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"progress": "^2.0.3",
|
||||||
|
"tar-stream": "^3.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dugite/node_modules/tar-stream": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4",
|
||||||
|
"fast-fifo": "^1.2.0",
|
||||||
|
"streamx": "^2.15.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -11584,6 +11638,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/events-universal": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bare-events": "^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eventsource": {
|
"node_modules/eventsource": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||||
@@ -11808,6 +11871,12 @@
|
|||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-fifo": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||||
@@ -17970,7 +18039,6 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
@@ -19985,6 +20053,17 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/streamx": {
|
||||||
|
"version": "2.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
|
||||||
|
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"events-universal": "^1.0.0",
|
||||||
|
"fast-fifo": "^1.3.2",
|
||||||
|
"text-decoder": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
@@ -20524,6 +20603,15 @@
|
|||||||
"rimraf": "bin.js"
|
"rimraf": "bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/text-decoder": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
|
|||||||
@@ -135,6 +135,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"drizzle-orm": "^0.41.0",
|
"drizzle-orm": "^0.41.0",
|
||||||
|
"dugite": "^3.0.0",
|
||||||
"electron-log": "^5.3.3",
|
"electron-log": "^5.3.3",
|
||||||
"electron-playwright-helpers": "^1.7.1",
|
"electron-playwright-helpers": "^1.7.1",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import {
|
|||||||
hasUnclosedDyadWrite,
|
hasUnclosedDyadWrite,
|
||||||
} from "../ipc/handlers/chat_stream_handlers";
|
} from "../ipc/handlers/chat_stream_handlers";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import git from "isomorphic-git";
|
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import { cleanFullResponse } from "@/ipc/utils/cleanFullResponse";
|
import { cleanFullResponse } from "../ipc/utils/cleanFullResponse";
|
||||||
|
import { gitAdd, gitRemove, gitCommit } from "../ipc/utils/git_utils";
|
||||||
|
|
||||||
// Mock fs with default export
|
// Mock fs with default export
|
||||||
vi.mock("node:fs", async () => {
|
vi.mock("node:fs", async () => {
|
||||||
@@ -43,14 +43,19 @@ vi.mock("node:fs", async () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock isomorphic-git
|
// Mock Git utils
|
||||||
vi.mock("isomorphic-git", () => ({
|
vi.mock("../ipc/utils/git_utils", () => ({
|
||||||
default: {
|
gitAdd: vi.fn(),
|
||||||
add: vi.fn().mockResolvedValue(undefined),
|
gitCommit: vi.fn(),
|
||||||
remove: vi.fn().mockResolvedValue(undefined),
|
gitRemove: vi.fn(),
|
||||||
commit: vi.fn().mockResolvedValue(undefined),
|
gitRenameBranch: vi.fn(),
|
||||||
statusMatrix: vi.fn().mockResolvedValue([]),
|
gitCurrentBranch: vi.fn(),
|
||||||
},
|
gitLog: vi.fn(),
|
||||||
|
gitInit: vi.fn(),
|
||||||
|
gitPush: vi.fn(),
|
||||||
|
gitSetRemoteUrl: vi.fn(),
|
||||||
|
gitStatus: vi.fn().mockResolvedValue([]),
|
||||||
|
getGitUncommittedFiles: vi.fn().mockResolvedValue([]),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock paths module to control getDyadAppPath
|
// Mock paths module to control getDyadAppPath
|
||||||
@@ -703,12 +708,12 @@ describe("processFullResponse", () => {
|
|||||||
"/mock/user/data/path/mock-app-path/src/file1.js",
|
"/mock/user/data/path/mock-app-path/src/file1.js",
|
||||||
"console.log('Hello');",
|
"console.log('Hello');",
|
||||||
);
|
);
|
||||||
expect(git.add).toHaveBeenCalledWith(
|
expect(gitAdd).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
filepath: "src/file1.js",
|
filepath: "src/file1.js",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(git.commit).toHaveBeenCalled();
|
expect(gitCommit).toHaveBeenCalled();
|
||||||
expect(result).toEqual({ updatedFiles: true });
|
expect(result).toEqual({ updatedFiles: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -783,24 +788,24 @@ describe("processFullResponse", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Verify git operations were called for each file
|
// Verify git operations were called for each file
|
||||||
expect(git.add).toHaveBeenCalledWith(
|
expect(gitAdd).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
filepath: "src/file1.js",
|
filepath: "src/file1.js",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(git.add).toHaveBeenCalledWith(
|
expect(gitAdd).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
filepath: "src/utils/file2.js",
|
filepath: "src/utils/file2.js",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(git.add).toHaveBeenCalledWith(
|
expect(gitAdd).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
filepath: "src/components/Button.tsx",
|
filepath: "src/components/Button.tsx",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify commit was called once after all files were added
|
// Verify commit was called once after all files were added
|
||||||
expect(git.commit).toHaveBeenCalledTimes(1);
|
expect(gitCommit).toHaveBeenCalledTimes(1);
|
||||||
expect(result).toEqual({ updatedFiles: true });
|
expect(result).toEqual({ updatedFiles: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -825,17 +830,17 @@ describe("processFullResponse", () => {
|
|||||||
"/mock/user/data/path/mock-app-path/src/components/OldComponent.jsx",
|
"/mock/user/data/path/mock-app-path/src/components/OldComponent.jsx",
|
||||||
"/mock/user/data/path/mock-app-path/src/components/NewComponent.jsx",
|
"/mock/user/data/path/mock-app-path/src/components/NewComponent.jsx",
|
||||||
);
|
);
|
||||||
expect(git.add).toHaveBeenCalledWith(
|
expect(gitAdd).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
filepath: "src/components/NewComponent.jsx",
|
filepath: "src/components/NewComponent.jsx",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(git.remove).toHaveBeenCalledWith(
|
expect(gitRemove).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
filepath: "src/components/OldComponent.jsx",
|
filepath: "src/components/OldComponent.jsx",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(git.commit).toHaveBeenCalled();
|
expect(gitCommit).toHaveBeenCalled();
|
||||||
expect(result).toEqual({ updatedFiles: true });
|
expect(result).toEqual({ updatedFiles: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -852,7 +857,7 @@ describe("processFullResponse", () => {
|
|||||||
|
|
||||||
expect(fs.mkdirSync).toHaveBeenCalled();
|
expect(fs.mkdirSync).toHaveBeenCalled();
|
||||||
expect(fs.renameSync).not.toHaveBeenCalled();
|
expect(fs.renameSync).not.toHaveBeenCalled();
|
||||||
expect(git.commit).not.toHaveBeenCalled();
|
expect(gitCommit).not.toHaveBeenCalled();
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
updatedFiles: false,
|
updatedFiles: false,
|
||||||
extraFiles: undefined,
|
extraFiles: undefined,
|
||||||
@@ -875,12 +880,12 @@ describe("processFullResponse", () => {
|
|||||||
expect(fs.unlinkSync).toHaveBeenCalledWith(
|
expect(fs.unlinkSync).toHaveBeenCalledWith(
|
||||||
"/mock/user/data/path/mock-app-path/src/components/Unused.jsx",
|
"/mock/user/data/path/mock-app-path/src/components/Unused.jsx",
|
||||||
);
|
);
|
||||||
expect(git.remove).toHaveBeenCalledWith(
|
expect(gitRemove).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
filepath: "src/components/Unused.jsx",
|
filepath: "src/components/Unused.jsx",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(git.commit).toHaveBeenCalled();
|
expect(gitCommit).toHaveBeenCalled();
|
||||||
expect(result).toEqual({ updatedFiles: true });
|
expect(result).toEqual({ updatedFiles: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -896,8 +901,8 @@ describe("processFullResponse", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(fs.unlinkSync).not.toHaveBeenCalled();
|
expect(fs.unlinkSync).not.toHaveBeenCalled();
|
||||||
expect(git.remove).not.toHaveBeenCalled();
|
expect(gitRemove).not.toHaveBeenCalled();
|
||||||
expect(git.commit).not.toHaveBeenCalled();
|
expect(gitCommit).not.toHaveBeenCalled();
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
updatedFiles: false,
|
updatedFiles: false,
|
||||||
extraFiles: undefined,
|
extraFiles: undefined,
|
||||||
@@ -942,11 +947,11 @@ describe("processFullResponse", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Check git operations
|
// Check git operations
|
||||||
expect(git.add).toHaveBeenCalledTimes(2); // For the write and rename
|
expect(gitAdd).toHaveBeenCalledTimes(2); // For the write and rename
|
||||||
expect(git.remove).toHaveBeenCalledTimes(2); // For the rename and delete
|
expect(gitRemove).toHaveBeenCalledTimes(2); // For the rename and delete
|
||||||
|
|
||||||
// Check the commit message includes all operations
|
// Check the commit message includes all operations
|
||||||
expect(git.commit).toHaveBeenCalledWith(
|
expect(gitCommit).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
message: expect.stringContaining(
|
message: expect.stringContaining(
|
||||||
"wrote 1 file(s), renamed 1 file(s), deleted 1 file(s)",
|
"wrote 1 file(s), renamed 1 file(s), deleted 1 file(s)",
|
||||||
|
|||||||
60
src/ipc/git_types.ts
Normal file
60
src/ipc/git_types.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Type definitions for Git operations
|
||||||
|
export type GitCommit = {
|
||||||
|
oid: string;
|
||||||
|
commit: {
|
||||||
|
message: string;
|
||||||
|
author: {
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export interface GitBaseParams {
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
export interface GitCommitParams extends GitBaseParams {
|
||||||
|
message: string;
|
||||||
|
amend?: boolean;
|
||||||
|
}
|
||||||
|
export interface GitFileParams extends GitBaseParams {
|
||||||
|
filepath: string;
|
||||||
|
}
|
||||||
|
export interface GitCheckoutParams extends GitBaseParams {
|
||||||
|
ref: string;
|
||||||
|
}
|
||||||
|
export interface GitBranchRenameParams extends GitBaseParams {
|
||||||
|
oldBranch: string;
|
||||||
|
newBranch: string;
|
||||||
|
}
|
||||||
|
export interface GitCloneParams {
|
||||||
|
path: string; // destination
|
||||||
|
url: string;
|
||||||
|
depth?: number | null;
|
||||||
|
singleBranch?: boolean;
|
||||||
|
accessToken?: string;
|
||||||
|
}
|
||||||
|
export interface GitLogParams extends GitBaseParams {
|
||||||
|
depth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitResult {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
export interface GitPushParams extends GitBaseParams {
|
||||||
|
branch: string;
|
||||||
|
accessToken: string;
|
||||||
|
force?: boolean;
|
||||||
|
}
|
||||||
|
export interface GitFileAtCommitParams extends GitBaseParams {
|
||||||
|
filePath: string;
|
||||||
|
commitHash: string;
|
||||||
|
}
|
||||||
|
export interface GitSetRemoteUrlParams extends GitBaseParams {
|
||||||
|
remoteUrl: string;
|
||||||
|
}
|
||||||
|
export interface GitInitParams extends GitBaseParams {
|
||||||
|
ref?: string; // branch name, default = "main"
|
||||||
|
}
|
||||||
|
export interface GitStageToRevertParams extends GitBaseParams {
|
||||||
|
targetOid: string;
|
||||||
|
}
|
||||||
@@ -14,7 +14,6 @@ import fs from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { getDyadAppPath, getUserDataPath } from "../../paths/paths";
|
import { getDyadAppPath, getUserDataPath } from "../../paths/paths";
|
||||||
import { ChildProcess, spawn } from "node:child_process";
|
import { ChildProcess, spawn } from "node:child_process";
|
||||||
import git from "isomorphic-git";
|
|
||||||
import { promises as fsPromises } from "node:fs";
|
import { promises as fsPromises } from "node:fs";
|
||||||
|
|
||||||
// Import our utility modules
|
// Import our utility modules
|
||||||
@@ -44,7 +43,13 @@ import { getLanguageModelProviders } from "../shared/language_model_helpers";
|
|||||||
import { startProxy } from "../utils/start_proxy_server";
|
import { startProxy } from "../utils/start_proxy_server";
|
||||||
import { Worker } from "worker_threads";
|
import { Worker } from "worker_threads";
|
||||||
import { createFromTemplate } from "./createFromTemplate";
|
import { createFromTemplate } from "./createFromTemplate";
|
||||||
import { gitCommit } from "../utils/git_utils";
|
import {
|
||||||
|
gitCommit,
|
||||||
|
gitAdd,
|
||||||
|
gitInit,
|
||||||
|
gitListBranches,
|
||||||
|
gitRenameBranch,
|
||||||
|
} from "../utils/git_utils";
|
||||||
import { safeSend } from "../utils/safe_sender";
|
import { safeSend } from "../utils/safe_sender";
|
||||||
import { normalizePath } from "../../../shared/normalizePath";
|
import { normalizePath } from "../../../shared/normalizePath";
|
||||||
import { isServerFunction } from "@/supabase_admin/supabase_utils";
|
import { isServerFunction } from "@/supabase_admin/supabase_utils";
|
||||||
@@ -585,18 +590,11 @@ export function registerAppHandlers() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Initialize git repo and create first commit
|
// Initialize git repo and create first commit
|
||||||
await git.init({
|
|
||||||
fs: fs,
|
await gitInit({ path: fullAppPath, ref: "main" });
|
||||||
dir: fullAppPath,
|
|
||||||
defaultBranch: "main",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stage all files
|
// Stage all files
|
||||||
await git.add({
|
await gitAdd({ path: fullAppPath, filepath: "." });
|
||||||
fs: fs,
|
|
||||||
dir: fullAppPath,
|
|
||||||
filepath: ".",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create initial commit
|
// Create initial commit
|
||||||
const commitHash = await gitCommit({
|
const commitHash = await gitCommit({
|
||||||
@@ -657,18 +655,10 @@ export function registerAppHandlers() {
|
|||||||
|
|
||||||
if (!withHistory) {
|
if (!withHistory) {
|
||||||
// Initialize git repo and create first commit
|
// Initialize git repo and create first commit
|
||||||
await git.init({
|
await gitInit({ path: newAppPath, ref: "main" });
|
||||||
fs: fs,
|
|
||||||
dir: newAppPath,
|
|
||||||
defaultBranch: "main",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stage all files
|
// Stage all files
|
||||||
await git.add({
|
await gitAdd({ path: newAppPath, filepath: "." });
|
||||||
fs: fs,
|
|
||||||
dir: newAppPath,
|
|
||||||
filepath: ".",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create initial commit
|
// Create initial commit
|
||||||
await gitCommit({
|
await gitCommit({
|
||||||
@@ -1049,11 +1039,7 @@ export function registerAppHandlers() {
|
|||||||
|
|
||||||
// Check if git repository exists and commit the change
|
// Check if git repository exists and commit the change
|
||||||
if (fs.existsSync(path.join(appPath, ".git"))) {
|
if (fs.existsSync(path.join(appPath, ".git"))) {
|
||||||
await git.add({
|
await gitAdd({ path: appPath, filepath: filePath });
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
filepath: filePath,
|
|
||||||
});
|
|
||||||
|
|
||||||
await gitCommit({
|
await gitCommit({
|
||||||
path: appPath,
|
path: appPath,
|
||||||
@@ -1398,7 +1384,7 @@ export function registerAppHandlers() {
|
|||||||
return withLock(appId, async () => {
|
return withLock(appId, async () => {
|
||||||
try {
|
try {
|
||||||
// Check if the old branch exists
|
// Check if the old branch exists
|
||||||
const branches = await git.listBranches({ fs, dir: appPath });
|
const branches = await gitListBranches({ path: appPath });
|
||||||
if (!branches.includes(oldBranchName)) {
|
if (!branches.includes(oldBranchName)) {
|
||||||
throw new Error(`Branch '${oldBranchName}' not found.`);
|
throw new Error(`Branch '${oldBranchName}' not found.`);
|
||||||
}
|
}
|
||||||
@@ -1414,11 +1400,10 @@ export function registerAppHandlers() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await git.renameBranch({
|
await gitRenameBranch({
|
||||||
fs: fs,
|
path: appPath,
|
||||||
dir: appPath,
|
oldBranch: oldBranchName,
|
||||||
oldref: oldBranchName,
|
newBranch: newBranchName,
|
||||||
ref: newBranchName,
|
|
||||||
});
|
});
|
||||||
logger.info(
|
logger.info(
|
||||||
`Branch renamed from '${oldBranchName}' to '${newBranchName}' for app ${appId}`,
|
`Branch renamed from '${oldBranchName}' to '${newBranchName}' for app ${appId}`,
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ import { db } from "../../db";
|
|||||||
import { apps, chats, messages } from "../../db/schema";
|
import { apps, chats, messages } from "../../db/schema";
|
||||||
import { desc, eq, and, like } from "drizzle-orm";
|
import { desc, eq, and, like } from "drizzle-orm";
|
||||||
import type { ChatSearchResult, ChatSummary } from "../../lib/schemas";
|
import type { ChatSearchResult, ChatSummary } from "../../lib/schemas";
|
||||||
import * as git from "isomorphic-git";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import { createLoggedHandler } from "./safe_handle";
|
import { createLoggedHandler } from "./safe_handle";
|
||||||
|
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
import { getDyadAppPath } from "../../paths/paths";
|
import { getDyadAppPath } from "../../paths/paths";
|
||||||
import { UpdateChatParams } from "../ipc_types";
|
import { UpdateChatParams } from "../ipc_types";
|
||||||
|
import { getCurrentCommitHash } from "../utils/git_utils";
|
||||||
|
|
||||||
const logger = log.scope("chat_handlers");
|
const logger = log.scope("chat_handlers");
|
||||||
const handle = createLoggedHandler(logger);
|
const handle = createLoggedHandler(logger);
|
||||||
@@ -31,9 +30,8 @@ export function registerChatHandlers() {
|
|||||||
let initialCommitHash = null;
|
let initialCommitHash = null;
|
||||||
try {
|
try {
|
||||||
// Get the current git revision of main branch
|
// Get the current git revision of main branch
|
||||||
initialCommitHash = await git.resolveRef({
|
initialCommitHash = await getCurrentCommitHash({
|
||||||
fs,
|
path: getDyadAppPath(app.path),
|
||||||
dir: getDyadAppPath(app.path),
|
|
||||||
ref: "main",
|
ref: "main",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import git from "isomorphic-git";
|
|
||||||
import http from "isomorphic-git/http/node";
|
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { copyDirectoryRecursive } from "../utils/file_utils";
|
import { copyDirectoryRecursive } from "../utils/file_utils";
|
||||||
|
import { gitClone, getCurrentCommitHash } from "../utils/git_utils";
|
||||||
import { readSettings } from "@/main/settings";
|
import { readSettings } from "@/main/settings";
|
||||||
import { getTemplateOrThrow } from "../utils/template_utils";
|
import { getTemplateOrThrow } from "../utils/template_utils";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
@@ -35,9 +34,6 @@ export async function createFromTemplate({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function cloneRepo(repoUrl: string): Promise<string> {
|
async function cloneRepo(repoUrl: string): Promise<string> {
|
||||||
let orgName: string;
|
|
||||||
let repoName: string;
|
|
||||||
|
|
||||||
const url = new URL(repoUrl);
|
const url = new URL(repoUrl);
|
||||||
if (url.protocol !== "https:") {
|
if (url.protocol !== "https:") {
|
||||||
throw new Error("Repository URL must use HTTPS.");
|
throw new Error("Repository URL must use HTTPS.");
|
||||||
@@ -55,8 +51,8 @@ async function cloneRepo(repoUrl: string): Promise<string> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
orgName = pathParts[0];
|
const orgName = pathParts[0];
|
||||||
repoName = path.basename(pathParts[1], ".git"); // Remove .git suffix if present
|
const repoName = path.basename(pathParts[1], ".git"); // Remove .git suffix if present
|
||||||
|
|
||||||
if (!orgName || !repoName) {
|
if (!orgName || !repoName) {
|
||||||
// This case should ideally be caught by pathParts.length !== 2
|
// This case should ideally be caught by pathParts.length !== 2
|
||||||
@@ -83,41 +79,31 @@ async function cloneRepo(repoUrl: string): Promise<string> {
|
|||||||
const apiUrl = `https://api.github.com/repos/${orgName}/${repoName}/commits/HEAD`;
|
const apiUrl = `https://api.github.com/repos/${orgName}/${repoName}/commits/HEAD`;
|
||||||
logger.info(`Fetching remote SHA from ${apiUrl}`);
|
logger.info(`Fetching remote SHA from ${apiUrl}`);
|
||||||
|
|
||||||
let remoteSha: string | undefined;
|
// Use native fetch instead of isomorphic-git http.request
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
const response = await http.request({
|
|
||||||
url: apiUrl,
|
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": "Dyad", // GitHub API requires a User-Agent
|
"User-Agent": "Dyad", // GitHub API requires this
|
||||||
Accept: "application/vnd.github.v3+json",
|
Accept: "application/vnd.github.v3+json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Handle non-200 responses
|
||||||
if (response.statusCode === 200 && response.body) {
|
if (!response.ok) {
|
||||||
// Convert AsyncIterableIterator<Uint8Array> to string
|
throw new Error(
|
||||||
const chunks: Uint8Array[] = [];
|
`GitHub API request failed with status ${response.status}: ${response.statusText}`,
|
||||||
for await (const chunk of response.body) {
|
);
|
||||||
chunks.push(chunk);
|
|
||||||
}
|
}
|
||||||
const responseBodyStr = Buffer.concat(chunks).toString("utf8");
|
// Parse JSON directly (fetch handles streaming internally)
|
||||||
const commitData = JSON.parse(responseBodyStr);
|
const commitData = await response.json();
|
||||||
remoteSha = commitData.sha;
|
const remoteSha = commitData.sha;
|
||||||
if (!remoteSha) {
|
if (!remoteSha) {
|
||||||
throw new Error("SHA not found in GitHub API response.");
|
throw new Error("SHA not found in GitHub API response.");
|
||||||
}
|
}
|
||||||
logger.info(`Successfully fetched remote SHA: ${remoteSha}`);
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`GitHub API request failed with status ${response.statusCode}: ${response.statusMessage}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const localSha = await git.resolveRef({
|
logger.info(`Successfully fetched remote SHA: ${remoteSha}`);
|
||||||
fs,
|
|
||||||
dir: cachePath,
|
// Compare with local SHA
|
||||||
ref: "HEAD",
|
const localSha = await getCurrentCommitHash({ path: cachePath });
|
||||||
});
|
|
||||||
|
|
||||||
if (remoteSha === localSha) {
|
if (remoteSha === localSha) {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -129,7 +115,7 @@ async function cloneRepo(repoUrl: string): Promise<string> {
|
|||||||
`Local cache for ${repoName} (SHA: ${localSha}) is outdated (Remote SHA: ${remoteSha}). Removing and re-cloning.`,
|
`Local cache for ${repoName} (SHA: ${localSha}) is outdated (Remote SHA: ${remoteSha}). Removing and re-cloning.`,
|
||||||
);
|
);
|
||||||
fs.rmSync(cachePath, { recursive: true, force: true });
|
fs.rmSync(cachePath, { recursive: true, force: true });
|
||||||
// Proceed to clone
|
// Continue to clone…
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -144,14 +130,7 @@ async function cloneRepo(repoUrl: string): Promise<string> {
|
|||||||
|
|
||||||
logger.info(`Cloning ${repoUrl} to ${cachePath}`);
|
logger.info(`Cloning ${repoUrl} to ${cachePath}`);
|
||||||
try {
|
try {
|
||||||
await git.clone({
|
await gitClone({ path: cachePath, url: repoUrl, depth: 1 });
|
||||||
fs,
|
|
||||||
http,
|
|
||||||
dir: cachePath,
|
|
||||||
url: repoUrl,
|
|
||||||
singleBranch: true,
|
|
||||||
depth: 1,
|
|
||||||
});
|
|
||||||
logger.info(`Successfully cloned ${repoUrl} to ${cachePath}`);
|
logger.info(`Successfully cloned ${repoUrl} to ${cachePath}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to clone ${repoUrl} to ${cachePath}: `, err);
|
logger.error(`Failed to clone ${repoUrl} to ${cachePath}: `, err);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { ipcMain, BrowserWindow, IpcMainInvokeEvent } from "electron";
|
import { ipcMain, BrowserWindow, IpcMainInvokeEvent } from "electron";
|
||||||
import fetch from "node-fetch"; // Use node-fetch for making HTTP requests in main process
|
import fetch from "node-fetch"; // Use node-fetch for making HTTP requests in main process
|
||||||
import { writeSettings, readSettings } from "../../main/settings";
|
import { writeSettings, readSettings } from "../../main/settings";
|
||||||
import git, { clone } from "isomorphic-git";
|
import { gitSetRemoteUrl, gitPush, gitClone } from "../utils/git_utils";
|
||||||
import http from "isomorphic-git/http/node";
|
|
||||||
import * as schema from "../../db/schema";
|
import * as schema from "../../db/schema";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { getDyadAppPath } from "../../paths/paths";
|
import { getDyadAppPath } from "../../paths/paths";
|
||||||
@@ -575,25 +574,17 @@ async function handlePushToGithub(
|
|||||||
? `${GITHUB_GIT_BASE}/${app.githubOrg}/${app.githubRepo}.git`
|
? `${GITHUB_GIT_BASE}/${app.githubOrg}/${app.githubRepo}.git`
|
||||||
: `https://${accessToken}:x-oauth-basic@github.com/${app.githubOrg}/${app.githubRepo}.git`;
|
: `https://${accessToken}:x-oauth-basic@github.com/${app.githubOrg}/${app.githubRepo}.git`;
|
||||||
// Set or update remote URL using git config
|
// Set or update remote URL using git config
|
||||||
await git.setConfig({
|
await gitSetRemoteUrl({
|
||||||
fs,
|
path: appPath,
|
||||||
dir: appPath,
|
remoteUrl,
|
||||||
path: "remote.origin.url",
|
|
||||||
value: remoteUrl,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Push to GitHub
|
// Push to GitHub
|
||||||
await git.push({
|
await gitPush({
|
||||||
fs,
|
path: appPath,
|
||||||
http,
|
branch,
|
||||||
dir: appPath,
|
accessToken,
|
||||||
remote: "origin",
|
force,
|
||||||
ref: "main",
|
|
||||||
remoteRef: branch,
|
|
||||||
onAuth: () => ({
|
|
||||||
username: accessToken,
|
|
||||||
password: "x-oauth-basic",
|
|
||||||
}),
|
|
||||||
force: !!force,
|
|
||||||
});
|
});
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -673,9 +664,12 @@ async function handleCloneRepoFromUrl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const appPath = getDyadAppPath(finalAppName);
|
const appPath = getDyadAppPath(finalAppName);
|
||||||
|
// Ensure the app directory exists if native git is disabled
|
||||||
|
if (!settings.enableNativeGit) {
|
||||||
if (!fs.existsSync(appPath)) {
|
if (!fs.existsSync(appPath)) {
|
||||||
fs.mkdirSync(appPath, { recursive: true });
|
fs.mkdirSync(appPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Use authenticated URL if token exists, otherwise use public HTTPS URL
|
// Use authenticated URL if token exists, otherwise use public HTTPS URL
|
||||||
const cloneUrl = accessToken
|
const cloneUrl = accessToken
|
||||||
? IS_TEST_BUILD
|
? IS_TEST_BUILD
|
||||||
@@ -683,17 +677,10 @@ async function handleCloneRepoFromUrl(
|
|||||||
: `https://${accessToken}:x-oauth-basic@github.com/${owner}/${repoName}.git`
|
: `https://${accessToken}:x-oauth-basic@github.com/${owner}/${repoName}.git`
|
||||||
: `https://github.com/${owner}/${repoName}.git`; // Changed: use public HTTPS URL instead of original url
|
: `https://github.com/${owner}/${repoName}.git`; // Changed: use public HTTPS URL instead of original url
|
||||||
try {
|
try {
|
||||||
await clone({
|
await gitClone({
|
||||||
fs,
|
path: appPath,
|
||||||
http,
|
|
||||||
dir: appPath,
|
|
||||||
url: cloneUrl,
|
url: cloneUrl,
|
||||||
onAuth: accessToken
|
accessToken,
|
||||||
? () => ({
|
|
||||||
username: accessToken,
|
|
||||||
password: "x-oauth-basic",
|
|
||||||
})
|
|
||||||
: undefined,
|
|
||||||
singleBranch: false,
|
singleBranch: false,
|
||||||
});
|
});
|
||||||
} catch (cloneErr) {
|
} catch (cloneErr) {
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ import { apps } from "@/db/schema";
|
|||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { chats } from "@/db/schema";
|
import { chats } from "@/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import git from "isomorphic-git";
|
|
||||||
|
|
||||||
import { ImportAppParams, ImportAppResult } from "../ipc_types";
|
import { ImportAppParams, ImportAppResult } from "../ipc_types";
|
||||||
import { copyDirectoryRecursive } from "../utils/file_utils";
|
import { copyDirectoryRecursive } from "../utils/file_utils";
|
||||||
import { gitCommit } from "../utils/git_utils";
|
import { gitCommit, gitAdd, gitInit } from "../utils/git_utils";
|
||||||
|
|
||||||
const logger = log.scope("import-handlers");
|
const logger = log.scope("import-handlers");
|
||||||
const handle = createLoggedHandler(logger);
|
const handle = createLoggedHandler(logger);
|
||||||
@@ -106,18 +105,11 @@ export function registerImportHandlers() {
|
|||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
if (!isGitRepo) {
|
if (!isGitRepo) {
|
||||||
// Initialize git repo and create first commit
|
// Initialize git repo and create first commit
|
||||||
await git.init({
|
await gitInit({ path: destPath, ref: "main" });
|
||||||
fs: fs,
|
|
||||||
dir: destPath,
|
|
||||||
defaultBranch: "main",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stage all files
|
// Stage all files
|
||||||
await git.add({
|
|
||||||
fs: fs,
|
await gitAdd({ path: destPath, filepath: "." });
|
||||||
dir: destPath,
|
|
||||||
filepath: ".",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create initial commit
|
// Create initial commit
|
||||||
await gitCommit({
|
await gitCommit({
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import { apps } from "../../db/schema";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { getDyadAppPath } from "../../paths/paths";
|
import { getDyadAppPath } from "../../paths/paths";
|
||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
import fs from "node:fs";
|
import { gitCommit, gitAdd } from "../utils/git_utils";
|
||||||
import git from "isomorphic-git";
|
|
||||||
import { gitCommit } from "../utils/git_utils";
|
|
||||||
import { storeDbTimestampAtCurrentVersion } from "../utils/neon_timestamp_utils";
|
import { storeDbTimestampAtCurrentVersion } from "../utils/neon_timestamp_utils";
|
||||||
|
|
||||||
const logger = log.scope("portal_handlers");
|
const logger = log.scope("portal_handlers");
|
||||||
@@ -116,11 +114,7 @@ export function registerPortalHandlers() {
|
|||||||
|
|
||||||
// Stage all changes and commit
|
// Stage all changes and commit
|
||||||
try {
|
try {
|
||||||
await git.add({
|
await gitAdd({ path: appPath, filepath: "." });
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
filepath: ".",
|
|
||||||
});
|
|
||||||
|
|
||||||
const commitHash = await gitCommit({
|
const commitHash = await gitCommit({
|
||||||
path: appPath,
|
path: appPath,
|
||||||
|
|||||||
@@ -7,15 +7,23 @@ import type {
|
|||||||
RevertVersionParams,
|
RevertVersionParams,
|
||||||
RevertVersionResponse,
|
RevertVersionResponse,
|
||||||
} from "../ipc_types";
|
} from "../ipc_types";
|
||||||
|
import type { GitCommit } from "../git_types";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { getDyadAppPath } from "../../paths/paths";
|
import { getDyadAppPath } from "../../paths/paths";
|
||||||
import git, { type ReadCommitResult } from "isomorphic-git";
|
|
||||||
import { withLock } from "../utils/lock_utils";
|
import { withLock } from "../utils/lock_utils";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
import { createLoggedHandler } from "./safe_handle";
|
import { createLoggedHandler } from "./safe_handle";
|
||||||
import { gitCheckout, gitCommit, gitStageToRevert } from "../utils/git_utils";
|
|
||||||
import { deployAllSupabaseFunctions } from "../../supabase_admin/supabase_utils";
|
import { deployAllSupabaseFunctions } from "../../supabase_admin/supabase_utils";
|
||||||
|
import {
|
||||||
|
gitCheckout,
|
||||||
|
gitCommit,
|
||||||
|
gitStageToRevert,
|
||||||
|
getCurrentCommitHash,
|
||||||
|
gitCurrentBranch,
|
||||||
|
gitLog,
|
||||||
|
} from "../utils/git_utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getNeonClient,
|
getNeonClient,
|
||||||
@@ -80,11 +88,9 @@ export function registerVersionHandlers() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const commits = await git.log({
|
const commits = await gitLog({
|
||||||
fs,
|
path: appPath,
|
||||||
dir: appPath,
|
depth: 100_000, // KEEP UP TO DATE WITH ChatHeader.tsx
|
||||||
// KEEP UP TO DATE WITH ChatHeader.tsx
|
|
||||||
depth: 100_000, // Limit to last 100_000 commits for performance
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get all snapshots for this app to match with commits
|
// Get all snapshots for this app to match with commits
|
||||||
@@ -104,7 +110,7 @@ export function registerVersionHandlers() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return commits.map((commit: ReadCommitResult) => {
|
return commits.map((commit: GitCommit) => {
|
||||||
const snapshotInfo = snapshotMap.get(commit.oid);
|
const snapshotInfo = snapshotMap.get(commit.oid);
|
||||||
return {
|
return {
|
||||||
oid: commit.oid,
|
oid: commit.oid,
|
||||||
@@ -134,11 +140,7 @@ export function registerVersionHandlers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentBranch = await git.currentBranch({
|
const currentBranch = await gitCurrentBranch({ path: appPath });
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
fullname: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
branch: currentBranch || "<no-branch>",
|
branch: currentBranch || "<no-branch>",
|
||||||
@@ -169,9 +171,8 @@ export function registerVersionHandlers() {
|
|||||||
|
|
||||||
const appPath = getDyadAppPath(app.path);
|
const appPath = getDyadAppPath(app.path);
|
||||||
// Get the current commit hash before reverting
|
// Get the current commit hash before reverting
|
||||||
const currentCommitHash = await git.resolveRef({
|
const currentCommitHash = await getCurrentCommitHash({
|
||||||
fs,
|
path: appPath,
|
||||||
dir: appPath,
|
|
||||||
ref: "main",
|
ref: "main",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -518,6 +518,7 @@ export interface GithubRepository {
|
|||||||
full_name: string;
|
full_name: string;
|
||||||
private: boolean;
|
private: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CloneRepoReturnType =
|
export type CloneRepoReturnType =
|
||||||
| {
|
| {
|
||||||
app: App;
|
app: App;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { getDyadAppPath } from "../../paths/paths";
|
import { getDyadAppPath } from "../../paths/paths";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import git from "isomorphic-git";
|
|
||||||
import { safeJoin } from "../utils/path_utils";
|
import { safeJoin } from "../utils/path_utils";
|
||||||
|
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
@@ -16,7 +15,13 @@ import {
|
|||||||
} from "../../supabase_admin/supabase_management_client";
|
} from "../../supabase_admin/supabase_management_client";
|
||||||
import { isServerFunction } from "../../supabase_admin/supabase_utils";
|
import { isServerFunction } from "../../supabase_admin/supabase_utils";
|
||||||
import { UserSettings } from "../../lib/schemas";
|
import { UserSettings } from "../../lib/schemas";
|
||||||
import { gitCommit } from "../utils/git_utils";
|
import {
|
||||||
|
gitCommit,
|
||||||
|
gitAdd,
|
||||||
|
gitRemove,
|
||||||
|
gitAddAll,
|
||||||
|
getGitUncommittedFiles,
|
||||||
|
} from "../utils/git_utils";
|
||||||
import { readSettings } from "@/main/settings";
|
import { readSettings } from "@/main/settings";
|
||||||
import { writeMigrationFile } from "../utils/file_utils";
|
import { writeMigrationFile } from "../utils/file_utils";
|
||||||
import {
|
import {
|
||||||
@@ -265,11 +270,7 @@ export async function processFullResponseActions(
|
|||||||
|
|
||||||
// Remove the file from git
|
// Remove the file from git
|
||||||
try {
|
try {
|
||||||
await git.remove({
|
await gitRemove({ path: appPath, filepath: filePath });
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
filepath: filePath,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`Failed to git remove deleted file ${filePath}:`, error);
|
logger.warn(`Failed to git remove deleted file ${filePath}:`, error);
|
||||||
// Continue even if remove fails as the file was still deleted
|
// Continue even if remove fails as the file was still deleted
|
||||||
@@ -308,17 +309,9 @@ export async function processFullResponseActions(
|
|||||||
renamedFiles.push(tag.to);
|
renamedFiles.push(tag.to);
|
||||||
|
|
||||||
// Add the new file and remove the old one from git
|
// Add the new file and remove the old one from git
|
||||||
await git.add({
|
await gitAdd({ path: appPath, filepath: tag.to });
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
filepath: tag.to,
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
await git.remove({
|
await gitRemove({ path: appPath, filepath: tag.from });
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
filepath: tag.from,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`Failed to git remove old file ${tag.from}:`, error);
|
logger.warn(`Failed to git remove old file ${tag.from}:`, error);
|
||||||
// Continue even if remove fails as the file was still renamed
|
// Continue even if remove fails as the file was still renamed
|
||||||
@@ -469,11 +462,7 @@ export async function processFullResponseActions(
|
|||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
// Stage all written files
|
// Stage all written files
|
||||||
for (const file of writtenFiles) {
|
for (const file of writtenFiles) {
|
||||||
await git.add({
|
await gitAdd({ path: appPath, filepath: file });
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
filepath: file,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create commit with details of all changes
|
// Create commit with details of all changes
|
||||||
@@ -502,18 +491,11 @@ export async function processFullResponseActions(
|
|||||||
logger.log(`Successfully committed changes: ${changes.join(", ")}`);
|
logger.log(`Successfully committed changes: ${changes.join(", ")}`);
|
||||||
|
|
||||||
// Check for any uncommitted changes after the commit
|
// Check for any uncommitted changes after the commit
|
||||||
const statusMatrix = await git.statusMatrix({ fs, dir: appPath });
|
uncommittedFiles = await getGitUncommittedFiles({ path: appPath });
|
||||||
uncommittedFiles = statusMatrix
|
|
||||||
.filter((row) => row[1] !== 1 || row[2] !== 1 || row[3] !== 1)
|
|
||||||
.map((row) => row[0]); // Get just the file paths
|
|
||||||
|
|
||||||
if (uncommittedFiles.length > 0) {
|
if (uncommittedFiles.length > 0) {
|
||||||
// Stage all changes
|
// Stage all changes
|
||||||
await git.add({
|
await gitAddAll({ path: appPath });
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
filepath: ".",
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
commitHash = await gitCommit({
|
commitHash = await gitCommit({
|
||||||
path: appPath,
|
path: appPath,
|
||||||
|
|||||||
@@ -1,43 +1,68 @@
|
|||||||
import { getGitAuthor } from "./git_author";
|
import { getGitAuthor } from "./git_author";
|
||||||
import git from "isomorphic-git";
|
import git from "isomorphic-git";
|
||||||
|
import http from "isomorphic-git/http/node";
|
||||||
|
import { exec } from "dugite";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { promises as fsPromises } from "node:fs";
|
import { promises as fsPromises } from "node:fs";
|
||||||
import pathModule from "node:path";
|
import pathModule from "node:path";
|
||||||
import { exec } from "node:child_process";
|
|
||||||
import { promisify } from "node:util";
|
|
||||||
import { readSettings } from "../../main/settings";
|
import { readSettings } from "../../main/settings";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
const logger = log.scope("git_utils");
|
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,
|
* Helper function that wraps exec and throws an error if the exit code is non-zero
|
||||||
): Promise<{ stdout: string; stderr: string }> {
|
*/
|
||||||
try {
|
async function execOrThrow(
|
||||||
return await execAsync(command);
|
args: string[],
|
||||||
} catch (error: any) {
|
path: string,
|
||||||
let errorMessage = `Error: ${error.message}`;
|
errorMessage?: string,
|
||||||
if (error.stdout) {
|
): Promise<void> {
|
||||||
errorMessage += `\nStdout: ${error.stdout}`;
|
const result = await exec(args, path);
|
||||||
}
|
if (result.exitCode !== 0) {
|
||||||
if (error.stderr) {
|
const errorDetails = result.stderr.trim() || result.stdout.trim();
|
||||||
errorMessage += `\nStderr: ${error.stderr}`;
|
const error = errorMessage
|
||||||
}
|
? `${errorMessage}. ${errorDetails}`
|
||||||
throw new Error(errorMessage);
|
: `Git command failed: ${args.join(" ")}. ${errorDetails}`;
|
||||||
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCurrentCommitHash({
|
export async function getCurrentCommitHash({
|
||||||
path,
|
path,
|
||||||
}: {
|
ref = "HEAD",
|
||||||
path: string;
|
}: GitInitParams): Promise<string> {
|
||||||
}): 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({
|
return await git.resolveRef({
|
||||||
fs,
|
fs,
|
||||||
dir: path,
|
dir: path,
|
||||||
ref: "HEAD",
|
ref,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function isGitStatusClean({
|
export async function isGitStatusClean({
|
||||||
path,
|
path,
|
||||||
@@ -46,8 +71,15 @@ export async function isGitStatusClean({
|
|||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
if (settings.enableNativeGit) {
|
if (settings.enableNativeGit) {
|
||||||
const { stdout } = await execAsync(`git -C "${path}" status --porcelain`);
|
const result = await exec(["status", "--porcelain"], path);
|
||||||
return stdout.trim() === "";
|
|
||||||
|
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 {
|
} else {
|
||||||
const statusMatrix = await git.statusMatrix({ fs, dir: path });
|
const statusMatrix = await git.statusMatrix({ fs, dir: path });
|
||||||
return statusMatrix.every(
|
return statusMatrix.every(
|
||||||
@@ -60,21 +92,31 @@ export async function gitCommit({
|
|||||||
path,
|
path,
|
||||||
message,
|
message,
|
||||||
amend,
|
amend,
|
||||||
}: {
|
}: GitCommitParams): Promise<string> {
|
||||||
path: string;
|
|
||||||
message: string;
|
|
||||||
amend?: boolean;
|
|
||||||
}): Promise<string> {
|
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
if (settings.enableNativeGit) {
|
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) {
|
if (amend) {
|
||||||
command += " --amend";
|
args.push("--amend");
|
||||||
}
|
}
|
||||||
|
await execOrThrow(args, path, "Failed to create commit");
|
||||||
await verboseExecAsync(command);
|
// Get the new commit hash
|
||||||
const { stdout } = await execAsync(`git -C "${path}" rev-parse HEAD`);
|
const result = await exec(["rev-parse", "HEAD"], path);
|
||||||
return stdout.trim();
|
if (result.exitCode !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to get commit hash: ${result.stderr.trim() || result.stdout.trim()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result.stdout.trim();
|
||||||
} else {
|
} else {
|
||||||
return git.commit({
|
return git.commit({
|
||||||
fs: fs,
|
fs: fs,
|
||||||
@@ -89,13 +131,14 @@ export async function gitCommit({
|
|||||||
export async function gitCheckout({
|
export async function gitCheckout({
|
||||||
path,
|
path,
|
||||||
ref,
|
ref,
|
||||||
}: {
|
}: GitCheckoutParams): Promise<void> {
|
||||||
path: string;
|
|
||||||
ref: string;
|
|
||||||
}): Promise<void> {
|
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
if (settings.enableNativeGit) {
|
if (settings.enableNativeGit) {
|
||||||
await execAsync(`git -C "${path}" checkout "${ref.replace(/"/g, '\\"')}"`);
|
await execOrThrow(
|
||||||
|
["checkout", ref],
|
||||||
|
path,
|
||||||
|
`Failed to checkout ref '${ref}'`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
return git.checkout({ fs, dir: path, ref });
|
return git.checkout({ fs, dir: path, ref });
|
||||||
@@ -105,17 +148,18 @@ export async function gitCheckout({
|
|||||||
export async function gitStageToRevert({
|
export async function gitStageToRevert({
|
||||||
path,
|
path,
|
||||||
targetOid,
|
targetOid,
|
||||||
}: {
|
}: GitStageToRevertParams): Promise<void> {
|
||||||
path: string;
|
|
||||||
targetOid: string;
|
|
||||||
}): Promise<void> {
|
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
if (settings.enableNativeGit) {
|
if (settings.enableNativeGit) {
|
||||||
// Get the current HEAD commit hash
|
// Get the current HEAD commit hash
|
||||||
const { stdout: currentHead } = await execAsync(
|
const currentHeadResult = await exec(["rev-parse", "HEAD"], path);
|
||||||
`git -C "${path}" rev-parse HEAD`,
|
if (currentHeadResult.exitCode !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to get current commit: ${currentHeadResult.stderr.trim() || currentHeadResult.stdout.trim()}`,
|
||||||
);
|
);
|
||||||
const currentCommit = currentHead.trim();
|
}
|
||||||
|
|
||||||
|
const currentCommit = currentHeadResult.stdout.trim();
|
||||||
|
|
||||||
// If we're already at the target commit, nothing to do
|
// If we're already at the target commit, nothing to do
|
||||||
if (currentCommit === targetOid) {
|
if (currentCommit === targetOid) {
|
||||||
@@ -123,20 +167,31 @@ export async function gitStageToRevert({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Safety: refuse to run if the work-tree isn't clean.
|
// Safety: refuse to run if the work-tree isn't clean.
|
||||||
const { stdout: wtStatus } = await execAsync(
|
const statusResult = await exec(["status", "--porcelain"], path);
|
||||||
`git -C "${path}" status --porcelain`,
|
if (statusResult.exitCode !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to get status: ${statusResult.stderr.trim() || statusResult.stdout.trim()}`,
|
||||||
);
|
);
|
||||||
if (wtStatus.trim() !== "") {
|
}
|
||||||
|
if (statusResult.stdout.trim() !== "") {
|
||||||
throw new Error("Cannot revert: working tree has uncommitted changes.");
|
throw new Error("Cannot revert: working tree has uncommitted changes.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the working directory and index to match the target commit state
|
// Reset the working directory and index to match the target commit state
|
||||||
// This effectively undoes all changes since the target commit
|
// 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
|
// 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
|
// 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 {
|
} else {
|
||||||
// Get status matrix comparing the target commit (previousVersionId as HEAD) with current working directory
|
// Get status matrix comparing the target commit (previousVersionId as HEAD) with current working directory
|
||||||
const matrix = await git.statusMatrix({
|
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();
|
const settings = readSettings();
|
||||||
if (settings.enableNativeGit) {
|
if (settings.enableNativeGit) {
|
||||||
await execAsync(`git -C "${path}" add .`);
|
await execOrThrow(["add", "."], path, "Failed to stage all files");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
return git.add({ fs, dir: path, filepath: "." });
|
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({
|
export async function getFileAtCommit({
|
||||||
path,
|
path,
|
||||||
filePath,
|
filePath,
|
||||||
commitHash,
|
commitHash,
|
||||||
}: {
|
}: GitFileAtCommitParams): Promise<string | null> {
|
||||||
path: string;
|
|
||||||
filePath: string;
|
|
||||||
commitHash: string;
|
|
||||||
}): Promise<string | null> {
|
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
if (settings.enableNativeGit) {
|
if (settings.enableNativeGit) {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execAsync(
|
const result = await exec(["show", `${commitHash}:${filePath}`], path);
|
||||||
`git -C "${path}" show "${commitHash}:${filePath}"`,
|
if (result.exitCode !== 0) {
|
||||||
);
|
// File doesn't exist at this commit or other error
|
||||||
return stdout;
|
return null;
|
||||||
|
}
|
||||||
|
return result.stdout;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error getting file at commit ${commitHash}: ${error.message}`,
|
`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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { db } from "../../db";
|
import { db } from "../../db";
|
||||||
import { versions, apps } from "../../db/schema";
|
import { versions, apps } from "../../db/schema";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import fs from "node:fs";
|
|
||||||
import git from "isomorphic-git";
|
|
||||||
import { getDyadAppPath } from "../../paths/paths";
|
import { getDyadAppPath } from "../../paths/paths";
|
||||||
import { neon } from "@neondatabase/serverless";
|
import { neon } from "@neondatabase/serverless";
|
||||||
|
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
import { getNeonClient } from "@/neon_admin/neon_management_client";
|
import { getNeonClient } from "@/neon_admin/neon_management_client";
|
||||||
|
import { getCurrentCommitHash } from "./git_utils";
|
||||||
|
|
||||||
const logger = log.scope("neon_timestamp_utils");
|
const logger = log.scope("neon_timestamp_utils");
|
||||||
|
|
||||||
@@ -62,11 +61,7 @@ export async function storeDbTimestampAtCurrentVersion({
|
|||||||
|
|
||||||
// 2. Get the current commit hash
|
// 2. Get the current commit hash
|
||||||
const appPath = getDyadAppPath(app.path);
|
const appPath = getDyadAppPath(app.path);
|
||||||
const currentCommitHash = await git.resolveRef({
|
const currentCommitHash = await getCurrentCommitHash({ path: appPath });
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
ref: "HEAD",
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(`Current commit hash: ${currentCommitHash}`);
|
logger.info(`Current commit hash: ${currentCommitHash}`);
|
||||||
|
|
||||||
|
|||||||
17
src/main.ts
17
src/main.ts
@@ -24,6 +24,7 @@ import {
|
|||||||
AddPromptDataSchema,
|
AddPromptDataSchema,
|
||||||
AddPromptPayload,
|
AddPromptPayload,
|
||||||
} from "./ipc/deep_link_data";
|
} from "./ipc/deep_link_data";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
log.errorHandler.startCatching();
|
log.errorHandler.startCatching();
|
||||||
log.eventLogger.startLogging();
|
log.eventLogger.startLogging();
|
||||||
@@ -42,6 +43,22 @@ if (started) {
|
|||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decide the git directory depending on environment
|
||||||
|
function resolveLocalGitDirectory() {
|
||||||
|
if (!app.isPackaged) {
|
||||||
|
// Dev: app.getAppPath() is the project root
|
||||||
|
return path.join(app.getAppPath(), "node_modules/dugite/git");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packaged app: git is bundled via extraResource
|
||||||
|
return path.join(process.resourcesPath, "git");
|
||||||
|
}
|
||||||
|
|
||||||
|
const gitDir = resolveLocalGitDirectory();
|
||||||
|
if (fs.existsSync(gitDir)) {
|
||||||
|
process.env.LOCAL_GIT_DIRECTORY = gitDir;
|
||||||
|
}
|
||||||
|
|
||||||
// https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app#main-process-mainjs
|
// https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app#main-process-mainjs
|
||||||
if (process.defaultApp) {
|
if (process.defaultApp) {
|
||||||
if (process.argv.length >= 2) {
|
if (process.argv.length >= 2) {
|
||||||
|
|||||||
@@ -163,18 +163,8 @@ export default function SettingsPage() {
|
|||||||
<Label htmlFor="enable-native-git">Enable Native Git</Label>
|
<Label htmlFor="enable-native-git">Enable Native Git</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
Native Git offers faster performance but requires{" "}
|
This doesn't require any external Git installation and offers
|
||||||
<a
|
a faster, native-Git performance experience.
|
||||||
onClick={() => {
|
|
||||||
IpcClient.getInstance().openExternalUrl(
|
|
||||||
"https://git-scm.com/downloads",
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
className="text-blue-600 hover:underline dark:text-blue-400"
|
|
||||||
>
|
|
||||||
installing Git
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import fsAsync from "node:fs/promises";
|
import fsAsync from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { isIgnored } from "isomorphic-git";
|
import { gitIsIgnored } from "../ipc/utils/git_utils";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
import { IS_TEST_BUILD } from "../ipc/utils/test_utils";
|
import { IS_TEST_BUILD } from "../ipc/utils/test_utils";
|
||||||
import { glob } from "glob";
|
import { glob } from "glob";
|
||||||
@@ -176,9 +175,8 @@ async function isGitIgnored(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const relativePath = path.relative(baseDir, filePath);
|
const relativePath = path.relative(baseDir, filePath);
|
||||||
const result = await isIgnored({
|
const result = await gitIsIgnored({
|
||||||
fs,
|
path: baseDir,
|
||||||
dir: baseDir,
|
|
||||||
filepath: relativePath,
|
filepath: relativePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user