gitxelectron 1.2.6 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/out/main/index.js CHANGED
@@ -4,6 +4,8 @@ const path = require("path");
4
4
  const fs = require("fs/promises");
5
5
  const simpleGit = require("simple-git");
6
6
  const crypto = require("crypto");
7
+ const child_process = require("child_process");
8
+ const readline = require("readline");
7
9
  const generativeAi = require("@google/generative-ai");
8
10
  const OpenAI = require("openai");
9
11
  const chokidar = require("chokidar");
@@ -26,6 +28,7 @@ function _interopNamespaceDefault(e) {
26
28
  }
27
29
  const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path);
28
30
  const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs);
31
+ const readline__namespace = /* @__PURE__ */ _interopNamespaceDefault(readline);
29
32
  const GITHUB_CLIENT_ID = "Ov23livWfiaPADGiwSXq";
30
33
  let accounts = [];
31
34
  const activeSessions = /* @__PURE__ */ new Map();
@@ -690,9 +693,56 @@ async function initializeRepo(sessionId, dir) {
690
693
  }
691
694
  }
692
695
  async function getStatus(sessionId) {
693
- const { git } = getGitSession(sessionId);
694
- const status = await git.status();
695
- return JSON.parse(JSON.stringify(status));
696
+ const { git, repoPath } = getGitSession(sessionId);
697
+ return new Promise((resolve, reject) => {
698
+ const child = child_process.spawn("git", ["status", "--porcelain"], { cwd: repoPath });
699
+ let count = 0;
700
+ let isMassive = false;
701
+ child.stdout.on("data", (chunk) => {
702
+ for (let i = 0; i < chunk.length; ++i) {
703
+ if (chunk[i] === 10) {
704
+ count++;
705
+ if (count > 1e4) {
706
+ isMassive = true;
707
+ child.kill();
708
+ break;
709
+ }
710
+ }
711
+ }
712
+ });
713
+ child.on("close", async () => {
714
+ if (isMassive) {
715
+ resolve({
716
+ not_added: [],
717
+ created: [],
718
+ deleted: [],
719
+ modified: [],
720
+ conflicted: [],
721
+ renamed: [],
722
+ staged: [],
723
+ files: [],
724
+ current: "main",
725
+ ahead: 0,
726
+ behind: 0,
727
+ tracking: null,
728
+ isMassive: true,
729
+ totalChanges: count
730
+ });
731
+ } else {
732
+ try {
733
+ const status = await git.status();
734
+ const result = JSON.parse(JSON.stringify(status));
735
+ result.isMassive = false;
736
+ result.totalChanges = count;
737
+ if (!result.staged) result.staged = [];
738
+ resolve(result);
739
+ } catch (e) {
740
+ reject(e);
741
+ }
742
+ }
743
+ });
744
+ child.on("error", reject);
745
+ });
696
746
  }
697
747
  async function stageFiles(sessionId, files) {
698
748
  const { git, repoPath } = getGitSession(sessionId);
@@ -727,7 +777,15 @@ async function stageFiles(sessionId, files) {
727
777
  const chunkSize = 50;
728
778
  for (let i = 0; i < fileArray.length; i += chunkSize) {
729
779
  const chunk = fileArray.slice(i, i + chunkSize);
730
- await git.add(["--force", ...chunk]);
780
+ try {
781
+ await git.raw(["-c", "core.safecrlf=false", "-c", "core.autocrlf=false", "add", "--force", ...chunk]);
782
+ } catch (e) {
783
+ if (e.message && e.message.includes("LF will be replaced by CRLF") && !e.message.includes("fatal:")) {
784
+ console.warn("Ignored CRLF warning during stage");
785
+ } else {
786
+ throw e;
787
+ }
788
+ }
731
789
  }
732
790
  return true;
733
791
  }
@@ -779,6 +837,123 @@ function injectAuthToken(sessionId, remoteUrl) {
779
837
  }
780
838
  return remoteUrl;
781
839
  }
840
+ async function pushMassiveRepository(sessionId, remote, branch, chunkSize = 5e3, onProgress, onDetailedProgress) {
841
+ const { repoPath } = getGitSession(sessionId);
842
+ if (onProgress) onProgress("Starting rapid file count to prepare massive chunking...");
843
+ const totalFiles = await new Promise((resolve) => {
844
+ const child2 = child_process.spawn("git", ["status", "--porcelain"], { cwd: repoPath });
845
+ let count = 0;
846
+ child2.stdout.on("data", (chunk) => {
847
+ for (let i = 0; i < chunk.length; ++i) {
848
+ if (chunk[i] === 10) count++;
849
+ }
850
+ });
851
+ child2.on("close", () => resolve(count));
852
+ child2.on("error", () => resolve(0));
853
+ });
854
+ if (totalFiles === 0) {
855
+ if (onProgress) onProgress("No files to push.");
856
+ return true;
857
+ }
858
+ if (onProgress) onProgress(`Total files to push: ${totalFiles}. Starting chunked upload...`);
859
+ const { git } = getGitSession(sessionId);
860
+ const remotes = await getRemotes(sessionId);
861
+ const remoteObj = remotes.find((r) => r.name === remote);
862
+ if (onProgress) onProgress("Starting massive repository push...");
863
+ await git.addConfig("http.postBuffer", "1048576000");
864
+ await git.addConfig("core.safecrlf", "false");
865
+ await git.addConfig("core.autocrlf", "false");
866
+ let authUrl = remote;
867
+ if (remoteObj && remoteObj.refs.push) {
868
+ authUrl = injectAuthToken(sessionId, remoteObj.refs.push);
869
+ }
870
+ const child = child_process.spawn("git", ["status", "--porcelain"], { cwd: repoPath });
871
+ const rl = readline__namespace.createInterface({
872
+ input: child.stdout,
873
+ crlfDelay: Infinity
874
+ });
875
+ let currentChunk = [];
876
+ let chunkCount = 0;
877
+ let processedFiles = 0;
878
+ const startTime = Date.now();
879
+ const processChunk = async (chunk) => {
880
+ chunkCount++;
881
+ processedFiles += chunk.length;
882
+ const percentage = Math.min(100, processedFiles / totalFiles * 100);
883
+ const elapsedMs = Date.now() - startTime;
884
+ const msPerFile = elapsedMs / processedFiles;
885
+ const remainingFiles = totalFiles - processedFiles;
886
+ const etaMs = msPerFile * remainingFiles;
887
+ const formatEta = (ms) => {
888
+ if (!isFinite(ms) || ms < 0) return "calculating...";
889
+ const totalSec = Math.floor(ms / 1e3);
890
+ const min = Math.floor(totalSec / 60);
891
+ const sec = totalSec % 60;
892
+ if (min > 0) return `${min}m ${sec}s left`;
893
+ return `${sec}s left`;
894
+ };
895
+ const etaStr = formatEta(etaMs);
896
+ if (onProgress) onProgress(`Processing chunk ${chunkCount} (${chunk.length} files)... ETA: ${etaStr}`);
897
+ if (onDetailedProgress) {
898
+ onDetailedProgress({
899
+ processed: processedFiles,
900
+ total: totalFiles,
901
+ percentage,
902
+ eta: etaStr
903
+ });
904
+ }
905
+ const tempFile = path__namespace.join(repoPath, ".git", "massive_chunk.txt");
906
+ await fs__namespace.writeFile(tempFile, chunk.join("\n"), "utf-8");
907
+ try {
908
+ await git.raw(["-c", "core.safecrlf=false", "add", "--force", "--pathspec-from-file=" + tempFile]);
909
+ } catch (e) {
910
+ if (e.message && e.message.includes("fatal:")) {
911
+ throw e;
912
+ }
913
+ } finally {
914
+ try {
915
+ await fs__namespace.unlink(tempFile);
916
+ } catch (e) {
917
+ }
918
+ }
919
+ try {
920
+ await git.commit(`Auto-sync chunk ${chunkCount} (Massive Push)`);
921
+ } catch (e) {
922
+ }
923
+ try {
924
+ await git.raw(["-c", "credential.helper=", "push", authUrl, branch]);
925
+ } catch (err) {
926
+ if (err.message && (err.message.includes("non-fast-forward") || err.message.includes("fetch first") || err.message.includes("branch is behind"))) {
927
+ await git.pull(authUrl, branch, { "--rebase": null, "--autostash": null });
928
+ await git.raw(["-c", "credential.helper=", "push", authUrl, branch]);
929
+ } else {
930
+ if (err.message && !err.message.includes("nothing to commit")) {
931
+ throw err;
932
+ }
933
+ }
934
+ }
935
+ };
936
+ for await (const line of rl) {
937
+ if (line.length < 3) continue;
938
+ let file = line.substring(3).trim();
939
+ if (file.startsWith('"') && file.endsWith('"')) {
940
+ file = file.substring(1, file.length - 1);
941
+ }
942
+ if (!file) continue;
943
+ currentChunk.push(file);
944
+ if (currentChunk.length >= chunkSize) {
945
+ rl.pause();
946
+ await processChunk(currentChunk);
947
+ currentChunk = [];
948
+ rl.resume();
949
+ }
950
+ }
951
+ if (currentChunk.length > 0) {
952
+ await processChunk(currentChunk);
953
+ }
954
+ if (onProgress) onProgress("✅ Massive push completed successfully!");
955
+ return true;
956
+ }
782
957
  async function pushToRemote(sessionId, remote, branch) {
783
958
  const remotes = await getRemotes(sessionId);
784
959
  const remoteObj = remotes.find((r) => r.name === remote);
@@ -1127,7 +1302,15 @@ async function commitUniversalWorkspace(workspacePath, message, branch) {
1127
1302
  GIT_TERMINAL_PROMPT: "0",
1128
1303
  GIT_ASKPASS: "echo"
1129
1304
  });
1130
- await git.add(["--sparse", "."]);
1305
+ try {
1306
+ await git.raw(["-c", "core.safecrlf=false", "-c", "core.autocrlf=false", "add", "--sparse", "."]);
1307
+ } catch (e) {
1308
+ if (e.message && e.message.includes("LF will be replaced by CRLF") && !e.message.includes("fatal:")) {
1309
+ console.warn("Ignored CRLF warning in universal workspace");
1310
+ } else {
1311
+ throw e;
1312
+ }
1313
+ }
1131
1314
  await git.commit(message);
1132
1315
  try {
1133
1316
  await git.raw(["-c", "credential.helper=", "push", "origin", branch]);
@@ -1517,13 +1700,15 @@ let isResolving = false;
1517
1700
  let globalOnLog = null;
1518
1701
  let globalOnErrorApproval = null;
1519
1702
  let globalOnResolveComplete = null;
1703
+ let globalOnMassiveProgress = null;
1520
1704
  const DEBOUNCE_MS = 3e3;
1521
1705
  const changedFiles = /* @__PURE__ */ new Set();
1522
- function toggleAutoSync(sessionId, repoPath, enabled, onLog, onErrorApproval, onResolveComplete) {
1706
+ function toggleAutoSync(sessionId, repoPath, enabled, onLog, onErrorApproval, onResolveComplete, onMassiveProgress) {
1523
1707
  isAutoSyncEnabled = enabled;
1524
1708
  globalOnLog = onLog;
1525
1709
  if (onErrorApproval) globalOnErrorApproval = onErrorApproval;
1526
1710
  if (onResolveComplete) globalOnResolveComplete = onResolveComplete;
1711
+ if (onMassiveProgress) globalOnMassiveProgress = onMassiveProgress;
1527
1712
  if (currentWatcher) {
1528
1713
  currentWatcher.close();
1529
1714
  currentWatcher = null;
@@ -1553,7 +1738,11 @@ function toggleAutoSync(sessionId, repoPath, enabled, onLog, onErrorApproval, on
1553
1738
  currentWatcher.on("all", (_event, filePath) => {
1554
1739
  if (!isAutoSyncEnabled || isResolving) return;
1555
1740
  const relativePath = path__namespace.relative(repoPath, filePath).replace(/\\/g, "/");
1556
- changedFiles.add(relativePath);
1741
+ if (changedFiles.size < 1e4) {
1742
+ changedFiles.add(relativePath);
1743
+ } else if (!changedFiles.has("__MASSIVE__")) {
1744
+ changedFiles.add("__MASSIVE__");
1745
+ }
1557
1746
  if (syncTimeout) {
1558
1747
  clearTimeout(syncTimeout);
1559
1748
  }
@@ -1574,9 +1763,29 @@ async function triggerSyncSequence(sessionId, _repoPath, onLog) {
1574
1763
  onLog("No valid files to sync (temp files filtered).");
1575
1764
  return;
1576
1765
  }
1577
- await stageFiles(sessionId, existingFiles);
1766
+ if (changedFiles.has("__MASSIVE__")) {
1767
+ onLog("Massive file changes detected in watcher. Checking status...");
1768
+ }
1578
1769
  const status = await getStatus(sessionId);
1579
- const branch = status.current || "main";
1770
+ if (status.isMassive) {
1771
+ onLog(`🚨 Massive repository change detected! Engaging chunked sync mode...`);
1772
+ const branch2 = status.current || "main";
1773
+ const success = await pushMassiveRepository(
1774
+ sessionId,
1775
+ "origin",
1776
+ branch2,
1777
+ 5e3,
1778
+ onLog,
1779
+ globalOnMassiveProgress || void 0
1780
+ );
1781
+ if (success) {
1782
+ onLog(`✅ Successfully pushed massive repository sync!`);
1783
+ }
1784
+ return;
1785
+ }
1786
+ await stageFiles(sessionId, existingFiles);
1787
+ const finalStatus = await getStatus(sessionId);
1788
+ const branch = finalStatus.current || "main";
1580
1789
  onLog("Generating AI commit message...");
1581
1790
  let commitMsg = `Auto-sync: update ${filesToSync.length > 1 ? filesToSync.length + " files" : filesToSync[0]}`;
1582
1791
  try {
@@ -1688,7 +1897,15 @@ async function applyAiFixAndResume(sessionId, resolvedCode) {
1688
1897
  if (resolvedCode) {
1689
1898
  for (const [file, content] of Object.entries(resolvedCode)) {
1690
1899
  await fs__namespace.writeFile(path__namespace.join(repoPath, file), content, "utf-8");
1691
- await git.add(["--force", file]);
1900
+ try {
1901
+ await git.raw(["-c", "core.safecrlf=false", "-c", "core.autocrlf=false", "add", "--force", file]);
1902
+ } catch (e) {
1903
+ if (e.message && e.message.includes("LF will be replaced by CRLF") && !e.message.includes("fatal:")) {
1904
+ console.warn("Ignored CRLF warning during AI fix apply");
1905
+ } else {
1906
+ throw e;
1907
+ }
1908
+ }
1692
1909
  }
1693
1910
  }
1694
1911
  try {
@@ -2073,15 +2290,10 @@ electron.app.whenReady().then(async () => {
2073
2290
  event.sender.id,
2074
2291
  repoPath,
2075
2292
  enabled,
2076
- (msg) => {
2077
- event.sender.send("auto-sync-log", msg);
2078
- },
2079
- (payload) => {
2080
- event.sender.send("auto-sync-error-approval", payload);
2081
- },
2082
- () => {
2083
- event.sender.send("auto-sync-resolved");
2084
- }
2293
+ (msg) => event.sender.send("auto-sync-log", msg),
2294
+ (payload) => event.sender.send("auto-sync-error-approval", payload),
2295
+ () => event.sender.send("auto-sync-resolved"),
2296
+ (data) => event.sender.send("massive-push-progress", data)
2085
2297
  );
2086
2298
  });
2087
2299
  electron.ipcMain.handle("approve-ai-fix", async (event, resolvedCode) => {
@@ -93,6 +93,10 @@ const api = {
93
93
  electron.ipcRenderer.removeAllListeners("auto-sync-resolved");
94
94
  electron.ipcRenderer.on("auto-sync-resolved", () => callback());
95
95
  },
96
+ onMassivePushProgress: (callback) => {
97
+ electron.ipcRenderer.removeAllListeners("massive-push-progress");
98
+ electron.ipcRenderer.on("massive-push-progress", (_event, data) => callback(data));
99
+ },
96
100
  getAppVersion: () => electron.ipcRenderer.invoke("get-app-version"),
97
101
  // AI Fixer API
98
102
  askAi: (provider, apiKey, errorMsg, gitStatus, repoPath) => electron.ipcRenderer.invoke("ask-ai", provider, apiKey, errorMsg, gitStatus, repoPath),
@@ -24165,6 +24165,70 @@ const CommitReceiptPrinter = ({ commit, onClose }) => {
24165
24165
  ] }) })
24166
24166
  ] });
24167
24167
  };
24168
+ const MassivePushLoader = ({ percentage, eta }) => {
24169
+ const clampedPercentage = Math.min(100, Math.max(0, percentage));
24170
+ const check1 = clampedPercentage >= 33;
24171
+ const check2 = clampedPercentage >= 66;
24172
+ const check3 = clampedPercentage >= 99;
24173
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center space-y-6 py-12", children: [
24174
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-center text-primary mb-2", children: [
24175
+ /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-bold", children: "Massive Push in Progress" }),
24176
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm opacity-70", children: "Chunking thousands of files to prevent freezing" })
24177
+ ] }),
24178
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative bg-[#3a3a3a] rounded-full h-4 w-[330px] shadow-inner", children: [
24179
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
24180
+ "div",
24181
+ {
24182
+ className: "absolute left-0 top-0 h-full rounded-full transition-all duration-500 ease-out",
24183
+ style: {
24184
+ width: `${clampedPercentage}%`,
24185
+ backgroundColor: "var(--color-brand, #00cd00)"
24186
+ }
24187
+ }
24188
+ ),
24189
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute -top-1 left-0 z-10 flex w-full justify-between px-1", children: [
24190
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6" }),
24191
+ " ",
24192
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Checkmark, { active: check1 }),
24193
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Checkmark, { active: check2 }),
24194
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Checkmark, { active: check3 })
24195
+ ] })
24196
+ ] }),
24197
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-center text-primary mt-4", children: [
24198
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "font-semibold text-2xl", children: [
24199
+ clampedPercentage.toFixed(1),
24200
+ "%"
24201
+ ] }),
24202
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-md opacity-80 mt-1", children: [
24203
+ "ETA: ",
24204
+ eta
24205
+ ] })
24206
+ ] })
24207
+ ] });
24208
+ };
24209
+ const Checkmark = ({ active }) => {
24210
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
24211
+ "div",
24212
+ {
24213
+ className: `rounded-full h-6 w-6 p-[3px] transition-all duration-500 flex items-center justify-center shadow-md ${active ? "scale-110" : "scale-90"}`,
24214
+ style: {
24215
+ backgroundColor: active ? "var(--color-brand, #00cd00)" : "#535353",
24216
+ opacity: active ? 1 : 0.5
24217
+ },
24218
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
24219
+ "svg",
24220
+ {
24221
+ stroke: "white",
24222
+ strokeWidth: 3,
24223
+ viewBox: "0 0 24 24",
24224
+ fill: "none",
24225
+ xmlns: "http://www.w3.org/2000/svg",
24226
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "m4.5 12.75 6 6 9-13.5", strokeLinejoin: "round", strokeLinecap: "round" })
24227
+ }
24228
+ )
24229
+ }
24230
+ );
24231
+ };
24168
24232
  const translations = {
24169
24233
  en: {
24170
24234
  settings: "Settings",
@@ -24745,6 +24809,7 @@ function App() {
24745
24809
  const [conflictQueue, setConflictQueue] = reactExports.useState([]);
24746
24810
  const [autoSyncErrorPayload, setAutoSyncErrorPayload] = reactExports.useState(null);
24747
24811
  const [failedFiles, setFailedFiles] = reactExports.useState([]);
24812
+ const [massivePushData, setMassivePushData] = reactExports.useState(null);
24748
24813
  const [isLocked, setIsLocked] = reactExports.useState(true);
24749
24814
  const [storedPin, setStoredPin] = reactExports.useState(localStorage.getItem("gitelectron-pin"));
24750
24815
  const [pinInput, setPinInput] = reactExports.useState("");
@@ -25115,6 +25180,11 @@ function App() {
25115
25180
  setFailedFiles([]);
25116
25181
  refreshRepoState();
25117
25182
  });
25183
+ if (window.api.onMassivePushProgress) {
25184
+ window.api.onMassivePushProgress((data) => {
25185
+ setMassivePushData(data);
25186
+ });
25187
+ }
25118
25188
  }, []);
25119
25189
  const handleToggleAutoSync = () => {
25120
25190
  const next = !autoSyncEnabled;
@@ -26586,6 +26656,7 @@ function App() {
26586
26656
  onReplace: handleConflictReplace
26587
26657
  }
26588
26658
  ),
26659
+ showUniversalWizard && /* @__PURE__ */ jsxRuntimeExports.jsx(UniversalPushWizard, { isOpen: showUniversalWizard, onClose: () => setShowUniversalWizard(false) }),
26589
26660
  showSetupWizard && repoPath && /* @__PURE__ */ jsxRuntimeExports.jsx(
26590
26661
  RepositoryWizard,
26591
26662
  {
@@ -27534,7 +27605,13 @@ function App() {
27534
27605
  )
27535
27606
  ] })
27536
27607
  ] }),
27537
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-auto p-4 custom-scrollbar", children: [
27608
+ status?.isMassive ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex flex-col items-center justify-center p-8 bg-canvas", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
27609
+ MassivePushLoader,
27610
+ {
27611
+ percentage: massivePushData?.percentage || 0,
27612
+ eta: massivePushData?.eta || "calculating..."
27613
+ }
27614
+ ) }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-auto p-4 custom-scrollbar", children: [
27538
27615
  changesTab === "changes" && /* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "space-y-1", children: [
27539
27616
  status.conflicted.map((f) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
27540
27617
  "li",