gitxelectron 1.4.3 → 1.4.4

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
@@ -678,6 +678,31 @@ function getGitBinary() {
678
678
  cachedGitBinary = "git";
679
679
  return "git";
680
680
  }
681
+ async function ensureGitIdentity(sessionId, git) {
682
+ try {
683
+ const account = getActiveAccount(sessionId);
684
+ if (account && account.username) {
685
+ const email2 = account.id ? `${account.id}+${account.username}@users.noreply.github.com` : `${account.username}@users.noreply.github.com`;
686
+ const curName = await git.getConfig("user.name");
687
+ const curEmail = await git.getConfig("user.email");
688
+ const isPlaceholderName = !curName.value || curName.value === "GitElectron User" || curName.value === "gitelectron" || curName.value === "GitQuickPush User";
689
+ const isPlaceholderEmail = !curEmail.value || curEmail.value === "auto@gitelectron.local" || curEmail.value.includes("gitelectron.local") || curEmail.value.includes("gitquickpush.local");
690
+ if (isPlaceholderName) {
691
+ await git.addConfig("user.name", account.username);
692
+ }
693
+ if (isPlaceholderEmail) {
694
+ await git.addConfig("user.email", email2);
695
+ }
696
+ return;
697
+ }
698
+ const name = await git.getConfig("user.name");
699
+ if (!name.value) await git.addConfig("user.name", "GitQuickPush User");
700
+ const email = await git.getConfig("user.email");
701
+ if (!email.value) await git.addConfig("user.email", "noreply@gitquickpush.local");
702
+ } catch (e) {
703
+ console.warn("Failed to ensure git identity:", e);
704
+ }
705
+ }
681
706
  function initGit(sessionId, baseDir) {
682
707
  recordSessionPath(sessionId, baseDir);
683
708
  const options = {
@@ -710,14 +735,7 @@ function initGit(sessionId, baseDir) {
710
735
  });
711
736
  (async () => {
712
737
  try {
713
- const name = await git.getConfig("user.name");
714
- if (!name.value) {
715
- await git.addConfig("user.name", "GitElectron User");
716
- }
717
- const email = await git.getConfig("user.email");
718
- if (!email.value) {
719
- await git.addConfig("user.email", "auto@gitelectron.local");
720
- }
738
+ await ensureGitIdentity(sessionId, git);
721
739
  } catch (e) {
722
740
  console.warn("Failed to ensure git identity:", e);
723
741
  }
@@ -1119,19 +1137,7 @@ async function unstageFiles(sessionId, files) {
1119
1137
  async function commitChanges(sessionId, message) {
1120
1138
  const { git } = getGitSession(sessionId);
1121
1139
  await ensureAiWeightsUseLfs(git);
1122
- try {
1123
- const nameConfig = await git.getConfig("user.name");
1124
- const emailConfig = await git.getConfig("user.email");
1125
- if (!nameConfig.value || !emailConfig.value) {
1126
- const account = getActiveAccount(sessionId);
1127
- const name = account?.username || "GitQuickPush User";
1128
- const email = account ? `${account.username}@users.noreply.github.com` : "noreply@gitquickpush.local";
1129
- if (!nameConfig.value) await git.addConfig("user.name", name);
1130
- if (!emailConfig.value) await git.addConfig("user.email", email);
1131
- }
1132
- } catch (e) {
1133
- console.warn("Failed to check or set git author identity", e);
1134
- }
1140
+ await ensureGitIdentity(sessionId, git);
1135
1141
  return await git.commit(message);
1136
1142
  }
1137
1143
  function injectAuthToken(sessionId, remoteUrl) {
@@ -1333,6 +1339,53 @@ async function pushMassiveRepository(sessionId, remote, branch, chunkSize = 5e3,
1333
1339
  if (onProgress) onProgress("✅ Massive push completed successfully!");
1334
1340
  return true;
1335
1341
  }
1342
+ function cleanGitErrorMessage(rawMsg, token, branch) {
1343
+ if (!rawMsg) return "Unknown Git error";
1344
+ let msg = typeof rawMsg === "string" ? rawMsg : rawMsg?.message || String(rawMsg);
1345
+ if (token) {
1346
+ msg = msg.split(token).join("***");
1347
+ }
1348
+ msg = msg.replace(/https:\/\/[^@]+@github\.com/g, "https://github.com");
1349
+ msg = msg.replace(/ghp_[A-Za-z0-9_]{30,}/g, "***");
1350
+ msg = msg.replace(/github_pat_[A-Za-z0-9_]{30,}/g, "***");
1351
+ msg = msg.replace(/^Error invoking remote method '[^']+':\s*/i, "");
1352
+ msg = msg.replace(/^Error:\s*/i, "");
1353
+ msg = msg.replace(/^fatal:\s*/i, "");
1354
+ const lines = msg.split(/[\r\n]+/);
1355
+ const meaningfulLines = lines.filter((line) => {
1356
+ const trimmed = line.trim();
1357
+ if (!trimmed) return false;
1358
+ if (/^(?:Enumerating|Counting|Compressing|Writing)\s+objects:\s*\d+%/i.test(trimmed))
1359
+ return false;
1360
+ if (/^Delta compression using up to \d+ threads/i.test(trimmed)) return false;
1361
+ if (/^Total \d+ \(delta \d+\), reused \d+/i.test(trimmed)) return false;
1362
+ if (/^POST git-(?:receive|upload)-pack \([^)]+\)/i.test(trimmed)) return false;
1363
+ if (/^To https:\/\/github\.com/i.test(trimmed)) return false;
1364
+ return true;
1365
+ });
1366
+ const cleaned = meaningfulLines.join("\n").trim();
1367
+ if (cleaned.includes("Protected branch update failed") || cleaned.includes("GH006") || cleaned.includes("Cannot force-push to a protected branch")) {
1368
+ const b = branch ? ` "${branch}"` : "";
1369
+ return `GitHub rejected push: Protected branch policy is enabled for${b}. Force-pushing to this branch is blocked in your repository settings on GitHub.`;
1370
+ }
1371
+ if (cleaned.includes("GH001") || cleaned.includes("exceeds GitHub's file size limit")) {
1372
+ const match = cleaned.match(/File\s+([^\s]+)\s+is\s+([0-9.]+)\s+MB/i);
1373
+ if (match) {
1374
+ return `Large file rejected by GitHub: "${match[1]}" is ${match[2]} MB, which exceeds GitHub's 100 MB limit. Track this file with Git LFS or add it to .gitignore.`;
1375
+ }
1376
+ return `Large file rejected by GitHub: Files exceeding 100 MB must be tracked with Git LFS or excluded in .gitignore.`;
1377
+ }
1378
+ if (cleaned.includes("RPC failed") || cleaned.includes("the remote end hung up unexpectedly") || cleaned.includes("early EOF") || cleaned.includes("curl 55")) {
1379
+ return "Push connection interrupted or payload too large. (Buffer increased to 1GB). Check network stability or use Git LFS if pushing large binaries.";
1380
+ }
1381
+ if (cleaned.includes("terminal prompts disabled") || cleaned.includes("Authentication failed") || cleaned.includes("could not read Username") || cleaned.includes("Permission to") && cleaned.includes("denied")) {
1382
+ return "Authentication failed or permission denied for remote repository. Verify your GitHub account has write access.";
1383
+ }
1384
+ if (cleaned.includes("non-fast-forward") || cleaned.includes("fetch first") || cleaned.includes("Updates were rejected because the remote contains work")) {
1385
+ return "Push rejected: Remote repository has new incoming changes. Pull latest changes first to synchronize.";
1386
+ }
1387
+ return cleaned || "Git operation failed. Please check repository state.";
1388
+ }
1336
1389
  async function pushToRemote(sessionId, remote, branch) {
1337
1390
  const remotes = await getRemotes(sessionId);
1338
1391
  const remoteObj = remotes.find((r) => r.name === remote);
@@ -1342,13 +1395,21 @@ async function pushToRemote(sessionId, remote, branch) {
1342
1395
  "-c",
1343
1396
  "credential.helper=",
1344
1397
  "-c",
1398
+ "core.longpaths=true",
1399
+ "-c",
1345
1400
  "lfs.allowincompletepush=true",
1346
1401
  "-c",
1347
- "pack.windowMemory=256m",
1402
+ "pack.windowMemory=512m",
1348
1403
  "-c",
1349
- "pack.packSizeLimit=500m",
1404
+ "pack.packSizeLimit=1024m",
1350
1405
  "-c",
1351
- "http.postBuffer=524288000",
1406
+ "http.postBuffer=1048576000",
1407
+ "-c",
1408
+ "http.maxRequestBuffer=1048576000",
1409
+ "-c",
1410
+ "http.lowSpeedLimit=1000",
1411
+ "-c",
1412
+ "http.lowSpeedTime=180",
1352
1413
  "-c",
1353
1414
  "lfs.concurrenttransfers=8",
1354
1415
  "push",
@@ -1366,18 +1427,17 @@ async function pushToRemote(sessionId, remote, branch) {
1366
1427
  return await doPush();
1367
1428
  } catch (err) {
1368
1429
  if (err.message && (err.message.includes("non-fast-forward") || err.message.includes("fetch first") || err.message.includes("branch is behind"))) {
1369
- console.log("Push rejected because branch is behind. Auto-pulling with rebase...");
1370
- const { git } = getGitSession(sessionId);
1371
- const options = { "--rebase": "false", "--autostash": null, "-X": "ours" };
1372
- if (remoteObj && remoteObj.refs.fetch) {
1373
- const authUrl = injectAuthToken(sessionId, remoteObj.refs.fetch);
1374
- await git.pull(authUrl, branch, options);
1375
- } else {
1376
- await git.pull(remote, branch, options);
1430
+ console.log("Push rejected because branch is behind. Auto-pulling before retry...");
1431
+ try {
1432
+ await pullFromRemote(sessionId, remote, branch);
1433
+ return await doPush();
1434
+ } catch (pullErr) {
1435
+ throw new Error(
1436
+ cleanGitErrorMessage(pullErr.message, getToken(sessionId) || void 0, branch)
1437
+ );
1377
1438
  }
1378
- return await doPush();
1379
1439
  }
1380
- throw err;
1440
+ throw new Error(cleanGitErrorMessage(err.message, getToken(sessionId) || void 0, branch));
1381
1441
  }
1382
1442
  }
1383
1443
  async function masterForcePush(sessionId, remote, branch) {
@@ -1411,18 +1471,14 @@ async function masterForcePush(sessionId, remote, branch) {
1411
1471
  }
1412
1472
  await ensureAiWeightsUseLfs(git);
1413
1473
  try {
1414
- await git.add(".");
1474
+ await git.raw(["-c", "core.safecrlf=false", "-c", "core.autocrlf=false", "add", "-A"]);
1415
1475
  } catch {
1416
- await git.raw(["add", "-A"]);
1417
- }
1418
- try {
1419
- const name = await git.getConfig("user.name");
1420
- if (!name.value) await git.addConfig("user.name", "GitElectron User");
1421
- const email = await git.getConfig("user.email");
1422
- if (!email.value) await git.addConfig("user.email", "auto@gitelectron.local");
1423
- } catch (e) {
1424
- console.warn("Failed to ensure git identity before master push:", e);
1476
+ try {
1477
+ await git.add(".");
1478
+ } catch {
1479
+ }
1425
1480
  }
1481
+ await ensureGitIdentity(sessionId, git);
1426
1482
  try {
1427
1483
  await git.commit("Master Force Push - Sync changes");
1428
1484
  } catch (e) {
@@ -1430,11 +1486,24 @@ async function masterForcePush(sessionId, remote, branch) {
1430
1486
  console.warn("Commit note:", e.message);
1431
1487
  }
1432
1488
  }
1433
- const tempGit = createSafeGit(repoPath, sessionId);
1434
1489
  const pushArgs = [
1435
1490
  "-c",
1436
1491
  "credential.helper=",
1437
1492
  "-c",
1493
+ "core.longpaths=true",
1494
+ "-c",
1495
+ "http.postBuffer=1048576000",
1496
+ "-c",
1497
+ "http.maxRequestBuffer=1048576000",
1498
+ "-c",
1499
+ "pack.windowMemory=512m",
1500
+ "-c",
1501
+ "pack.packSizeLimit=1024m",
1502
+ "-c",
1503
+ "http.lowSpeedLimit=1000",
1504
+ "-c",
1505
+ "http.lowSpeedTime=180",
1506
+ "-c",
1438
1507
  "filter.lfs.required=false",
1439
1508
  "-c",
1440
1509
  "lfs.allowincompletepush=true",
@@ -1444,11 +1513,75 @@ async function masterForcePush(sessionId, remote, branch) {
1444
1513
  "--no-verify",
1445
1514
  "-f"
1446
1515
  ];
1447
- if (remoteObj && remoteObj.refs?.push) {
1448
- const authUrl = injectAuthToken(sessionId, remoteObj.refs.push);
1449
- return await tempGit.raw([...pushArgs, authUrl, branch]);
1516
+ const targetRemoteUrl = remoteObj && remoteObj.refs?.push ? injectAuthToken(sessionId, remoteObj.refs.push) : remote || "origin";
1517
+ let currentActiveBranch = branch;
1518
+ try {
1519
+ const status = await git.status();
1520
+ if (status.current && !status.current.includes("(no") && status.current !== "HEAD") {
1521
+ currentActiveBranch = status.current;
1522
+ }
1523
+ } catch {
1524
+ }
1525
+ const targetBranch = branch || currentActiveBranch || "main";
1526
+ const pushRefSpec = currentActiveBranch ? `${currentActiveBranch}:${targetBranch}` : targetBranch;
1527
+ try {
1528
+ return await new Promise((resolve, reject) => {
1529
+ const child = child_process.spawn(getGitBinary(), [...pushArgs, targetRemoteUrl, pushRefSpec], {
1530
+ cwd: repoPath,
1531
+ env: {
1532
+ ...process.env,
1533
+ GCM_INTERACTIVE: "false",
1534
+ GIT_TERMINAL_PROMPT: "0",
1535
+ GIT_ASKPASS: "echo"
1536
+ }
1537
+ });
1538
+ let stdout = "";
1539
+ let stderr = "";
1540
+ const wc = electron.webContents.fromId(sessionId);
1541
+ child.stdout.on("data", (d) => {
1542
+ stdout += d.toString();
1543
+ });
1544
+ child.stderr.on("data", (d) => {
1545
+ const text = d.toString();
1546
+ stderr += text;
1547
+ const match = text.match(
1548
+ /(Writing objects|Compressing objects|Counting objects|Enumerating objects):\s+(\d+)%/i
1549
+ );
1550
+ if (match && wc) {
1551
+ const stage = match[1];
1552
+ const progress = parseInt(match[2], 10);
1553
+ try {
1554
+ wc.send("git-progress", { method: "push", stage, progress });
1555
+ } catch {
1556
+ }
1557
+ }
1558
+ });
1559
+ child.on("close", (code) => {
1560
+ if (code === 0) {
1561
+ resolve(stdout || stderr || "Force push successful");
1562
+ } else {
1563
+ const combined = (stderr + "\n" + stdout).trim();
1564
+ const clean = cleanGitErrorMessage(
1565
+ combined,
1566
+ getToken(sessionId) || void 0,
1567
+ targetBranch
1568
+ );
1569
+ reject(new Error(clean));
1570
+ }
1571
+ });
1572
+ child.on("error", (err) => {
1573
+ reject(
1574
+ new Error(
1575
+ cleanGitErrorMessage(err.message, getToken(sessionId) || void 0, targetBranch)
1576
+ )
1577
+ );
1578
+ });
1579
+ });
1580
+ } catch (err) {
1581
+ throw new Error(
1582
+ cleanGitErrorMessage(err.message, getToken(sessionId) || void 0, targetBranch)
1583
+ );
1450
1584
  }
1451
- return await tempGit.raw([...pushArgs, remote || "origin", branch]);
1452
1585
  }
1453
1586
  async function pullFromRemote(sessionId, remote, branch) {
1454
1587
  const { git, repoPath } = getGitSession(sessionId);
@@ -1458,14 +1591,7 @@ async function pullFromRemote(sessionId, remote, branch) {
1458
1591
  }
1459
1592
  const remoteObj = (remote ? remotes.find((r) => r.name === remote) : null) || remotes[0];
1460
1593
  const targetRemote = remoteObj.name;
1461
- try {
1462
- const name = await git.getConfig("user.name");
1463
- if (!name.value) await git.addConfig("user.name", "GitElectron User");
1464
- const email = await git.getConfig("user.email");
1465
- if (!email.value) await git.addConfig("user.email", "auto@gitelectron.local");
1466
- } catch (e) {
1467
- console.warn("Failed to ensure git identity before pull:", e);
1468
- }
1594
+ await ensureGitIdentity(sessionId, git);
1469
1595
  let targetBranch = branch;
1470
1596
  if (!targetBranch) {
1471
1597
  const status = await git.status();
@@ -1480,6 +1606,37 @@ async function pullFromRemote(sessionId, remote, branch) {
1480
1606
  }
1481
1607
  }
1482
1608
  const authUrl = remoteObj.refs?.fetch ? injectAuthToken(sessionId, remoteObj.refs.fetch) : targetRemote;
1609
+ let headBefore = null;
1610
+ try {
1611
+ headBefore = (await git.raw(["rev-parse", "HEAD"])).trim();
1612
+ } catch {
1613
+ }
1614
+ try {
1615
+ const fetchArgs = [
1616
+ "-c",
1617
+ "credential.helper=",
1618
+ "-c",
1619
+ "core.longpaths=true",
1620
+ "fetch",
1621
+ "--prune",
1622
+ authUrl,
1623
+ targetBranch
1624
+ ];
1625
+ await new Promise((resolve) => {
1626
+ const child = child_process.spawn(getGitBinary(), fetchArgs, {
1627
+ cwd: repoPath,
1628
+ env: {
1629
+ ...process.env,
1630
+ GCM_INTERACTIVE: "false",
1631
+ GIT_TERMINAL_PROMPT: "0",
1632
+ GIT_ASKPASS: "echo"
1633
+ }
1634
+ });
1635
+ child.on("close", () => resolve());
1636
+ child.on("error", () => resolve());
1637
+ });
1638
+ } catch {
1639
+ }
1483
1640
  let didStash = false;
1484
1641
  try {
1485
1642
  const status = await git.status();
@@ -1505,16 +1662,14 @@ async function pullFromRemote(sessionId, remote, branch) {
1505
1662
  "-c",
1506
1663
  "core.longpaths=true",
1507
1664
  "-c",
1508
- "filter.lfs.required=false",
1665
+ "http.postBuffer=1048576000",
1509
1666
  "-c",
1510
- "filter.lfs.smudge=",
1511
- "-c",
1512
- "http.postBuffer=524288000",
1667
+ "http.maxRequestBuffer=1048576000",
1513
1668
  "pull",
1669
+ "--progress",
1514
1670
  "--no-rebase",
1515
1671
  "--allow-unrelated-histories",
1516
- "-X",
1517
- "ours",
1672
+ "--stat",
1518
1673
  authUrl,
1519
1674
  targetBranch
1520
1675
  ];
@@ -1523,18 +1678,30 @@ async function pullFromRemote(sessionId, remote, branch) {
1523
1678
  cwd: repoPath,
1524
1679
  env: {
1525
1680
  ...process.env,
1526
- GIT_LFS_SKIP_SMUDGE: "1",
1527
1681
  GCM_INTERACTIVE: "false",
1528
1682
  GIT_TERMINAL_PROMPT: "0",
1529
1683
  GIT_ASKPASS: "echo"
1530
1684
  }
1531
1685
  });
1532
1686
  let out = "";
1687
+ const wc = electron.webContents.fromId(sessionId);
1533
1688
  child.stdout.on("data", (d) => {
1534
1689
  out += d.toString();
1535
1690
  });
1536
1691
  child.stderr.on("data", (d) => {
1537
- out += d.toString();
1692
+ const text = d.toString();
1693
+ out += text;
1694
+ const match = text.match(
1695
+ /(Receiving objects|Resolving deltas|Compressing objects|Counting objects):\s+(\d+)%/i
1696
+ );
1697
+ if (match && wc) {
1698
+ const stage = match[1];
1699
+ const progress = parseInt(match[2], 10);
1700
+ try {
1701
+ wc.send("git-progress", { method: "pull", stage, progress });
1702
+ } catch {
1703
+ }
1704
+ }
1538
1705
  });
1539
1706
  child.on("close", (code) => {
1540
1707
  if (code === 0) {
@@ -1553,15 +1720,30 @@ async function pullFromRemote(sessionId, remote, branch) {
1553
1720
  }
1554
1721
  }
1555
1722
  const alreadyUpToDate = pullOutput.includes("Already up to date.") || pullOutput.includes("Already up-to-date.");
1556
- const lines = pullOutput.split(/[\r\n]+/).map((l) => l.trim());
1557
- const changedFiles2 = [];
1558
- for (const l of lines) {
1559
- if (l.includes("|") && (l.includes("+") || l.includes("-"))) {
1560
- const filePart = l.split("|")[0].trim();
1561
- if (filePart) changedFiles2.push(filePart);
1723
+ let changedFiles2 = [];
1724
+ if (headBefore && !alreadyUpToDate) {
1725
+ try {
1726
+ const diffTree = await git.raw(["diff", "--name-only", headBefore, "HEAD"]);
1727
+ const files = diffTree.split(/[\r\n]+/).map((f) => f.trim()).filter(Boolean);
1728
+ if (files.length > 0) {
1729
+ changedFiles2 = files;
1730
+ }
1731
+ } catch {
1732
+ }
1733
+ }
1734
+ if (changedFiles2.length === 0 && !alreadyUpToDate) {
1735
+ const lines = pullOutput.split(/[\r\n]+/).map((l) => l.trim());
1736
+ for (const l of lines) {
1737
+ if (l.includes("|") && (l.includes("+") || l.includes("-"))) {
1738
+ const filePart = l.split("|")[0].trim();
1739
+ if (filePart) changedFiles2.push(filePart);
1740
+ }
1562
1741
  }
1563
1742
  }
1564
- const message = alreadyUpToDate ? "Already up to date with remote." : changedFiles2.length > 0 ? `Successfully pulled ${changedFiles2.length} file(s) from team!` : "Successfully pulled latest changes from team!";
1743
+ const summaryMatch = pullOutput.match(/(\d+)\s+files?\s+changed/i);
1744
+ const totalFilesChanged = summaryMatch ? parseInt(summaryMatch[1], 10) : changedFiles2.length;
1745
+ const displayCount = Math.max(totalFilesChanged, changedFiles2.length);
1746
+ const message = alreadyUpToDate ? "Already up to date with remote." : displayCount > 0 ? `Successfully pulled ${displayCount} file(s) from team!` : "Successfully pulled latest changes from team!";
1565
1747
  return {
1566
1748
  success: true,
1567
1749
  alreadyUpToDate,
@@ -1576,12 +1758,11 @@ async function pullFromRemote(sessionId, remote, branch) {
1576
1758
  } catch {
1577
1759
  }
1578
1760
  }
1579
- const token = getToken(sessionId);
1580
- let cleanMsg = err?.message || String(err);
1581
- if (token) {
1582
- cleanMsg = cleanMsg.split(token).join("***");
1583
- }
1584
- cleanMsg = cleanMsg.replace(/https:\/\/[^@]+@github\.com/g, "https://github.com");
1761
+ const cleanMsg = cleanGitErrorMessage(
1762
+ err?.message || String(err),
1763
+ getToken(sessionId) || void 0,
1764
+ targetBranch
1765
+ );
1585
1766
  throw new Error(cleanMsg);
1586
1767
  }
1587
1768
  }
@@ -1629,6 +1810,111 @@ async function checkRemoteUpdates(sessionId) {
1629
1810
  return { behind: 0, ahead: 0, current: "" };
1630
1811
  }
1631
1812
  }
1813
+ async function masterForcePull(sessionId, remote, branch) {
1814
+ const { git, repoPath } = getGitSession(sessionId);
1815
+ const remotes = await getRemotes(sessionId);
1816
+ if (!remotes || remotes.length === 0) {
1817
+ throw new Error("No remote repository configured to force pull from.");
1818
+ }
1819
+ const remoteObj = (remote ? remotes.find((r) => r.name === remote) : null) || remotes[0];
1820
+ const targetRemote = remoteObj.name;
1821
+ await ensureGitIdentity(sessionId, git);
1822
+ let targetBranch = branch;
1823
+ if (!targetBranch) {
1824
+ const status = await git.status();
1825
+ targetBranch = status.current || "main";
1826
+ if (targetBranch.includes("(no") || targetBranch === "HEAD") {
1827
+ try {
1828
+ const remoteHead = await git.raw(["rev-parse", "--abbrev-ref", `${targetRemote}/HEAD`]);
1829
+ targetBranch = remoteHead.trim().replace(`${targetRemote}/`, "");
1830
+ } catch {
1831
+ targetBranch = "main";
1832
+ }
1833
+ }
1834
+ }
1835
+ const authUrl = remoteObj.refs?.fetch ? injectAuthToken(sessionId, remoteObj.refs.fetch) : targetRemote;
1836
+ let headBefore = null;
1837
+ try {
1838
+ headBefore = (await git.raw(["rev-parse", "HEAD"])).trim();
1839
+ } catch {
1840
+ }
1841
+ try {
1842
+ const fetchArgs = [
1843
+ "-c",
1844
+ "credential.helper=",
1845
+ "-c",
1846
+ "core.longpaths=true",
1847
+ "-c",
1848
+ "http.postBuffer=1048576000",
1849
+ "-c",
1850
+ "http.maxRequestBuffer=1048576000",
1851
+ "fetch",
1852
+ "--progress",
1853
+ "--prune",
1854
+ authUrl,
1855
+ targetBranch
1856
+ ];
1857
+ await new Promise((resolve, reject) => {
1858
+ const child = child_process.spawn(getGitBinary(), fetchArgs, {
1859
+ cwd: repoPath,
1860
+ env: {
1861
+ ...process.env,
1862
+ GCM_INTERACTIVE: "false",
1863
+ GIT_TERMINAL_PROMPT: "0",
1864
+ GIT_ASKPASS: "echo"
1865
+ }
1866
+ });
1867
+ let errOut = "";
1868
+ const wc = electron.webContents.fromId(sessionId);
1869
+ child.stderr.on("data", (d) => {
1870
+ const text = d.toString();
1871
+ errOut += text;
1872
+ const match = text.match(
1873
+ /(Receiving objects|Resolving deltas|Compressing objects|Counting objects):\s+(\d+)%/i
1874
+ );
1875
+ if (match && wc) {
1876
+ const stage = match[1];
1877
+ const progress = parseInt(match[2], 10);
1878
+ try {
1879
+ wc.send("git-progress", { method: "pull", stage, progress });
1880
+ } catch {
1881
+ }
1882
+ }
1883
+ });
1884
+ child.on("close", (code) => {
1885
+ if (code === 0) resolve();
1886
+ else reject(new Error(errOut || `Fetch failed with exit code ${code}`));
1887
+ });
1888
+ child.on("error", (err) => reject(err));
1889
+ });
1890
+ await git.reset(["--hard", "FETCH_HEAD"]);
1891
+ try {
1892
+ await git.clean("f", ["-d"]);
1893
+ } catch {
1894
+ }
1895
+ let changedFiles2 = [];
1896
+ if (headBefore) {
1897
+ try {
1898
+ const diffTree = await git.raw(["diff", "--name-only", headBefore, "HEAD"]);
1899
+ changedFiles2 = diffTree.split(/[\r\n]+/).map((f) => f.trim()).filter(Boolean);
1900
+ } catch {
1901
+ }
1902
+ }
1903
+ const message = changedFiles2.length > 0 ? `Force Pull successful! Synced ${changedFiles2.length} file(s) from GitHub.` : "Force Pull successful! All files are perfectly matched with GitHub.";
1904
+ return {
1905
+ success: true,
1906
+ message,
1907
+ filesChanged: changedFiles2
1908
+ };
1909
+ } catch (err) {
1910
+ const cleanMsg = cleanGitErrorMessage(
1911
+ err?.message || String(err),
1912
+ getToken(sessionId) || void 0,
1913
+ targetBranch
1914
+ );
1915
+ throw new Error(cleanMsg);
1916
+ }
1917
+ }
1632
1918
  async function resetToRemote(sessionId, remote, branch) {
1633
1919
  const { git } = getGitSession(sessionId);
1634
1920
  const remotes = await getRemotes(sessionId);
@@ -2383,6 +2669,19 @@ async function generateWithGemini(genAI, contents) {
2383
2669
  }
2384
2670
  throw lastErr || new Error("All Gemini model fallbacks failed");
2385
2671
  }
2672
+ function extractJson(text) {
2673
+ const cleaned = text.replace(/```json/gi, "").replace(/```/g, "").trim();
2674
+ try {
2675
+ return JSON.parse(cleaned);
2676
+ } catch {
2677
+ const firstBrace = cleaned.indexOf("{");
2678
+ const lastBrace = cleaned.lastIndexOf("}");
2679
+ if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
2680
+ return JSON.parse(cleaned.substring(firstBrace, lastBrace + 1));
2681
+ }
2682
+ throw new Error("No valid JSON object found in response");
2683
+ }
2684
+ }
2386
2685
  async function analyzeGitError(provider, apiKeyInput, errorMsg, gitStatus, repoPath, githubToken) {
2387
2686
  const keys = apiKeyInput ? apiKeyInput.split(",").map((k) => k.trim()).filter((k) => k) : getBackupApiKeys();
2388
2687
  const prompt = `
@@ -2410,9 +2709,8 @@ ${errorMsg}
2410
2709
  ],
2411
2710
  response_format: { type: "json_object" }
2412
2711
  });
2413
- let responseText = response.choices[0].message.content || "{}";
2414
- responseText = responseText.replace(/```json/g, "").replace(/```/g, "").trim();
2415
- const parsed = JSON.parse(responseText);
2712
+ const responseText = response.choices[0].message.content || "{}";
2713
+ const parsed = extractJson(responseText);
2416
2714
  if (parsed.explanation && Array.isArray(parsed.commandsToRun)) {
2417
2715
  console.log("[AI Service] Successfully used GitHub Copilot Models API!");
2418
2716
  return parsed;
@@ -2445,8 +2743,7 @@ ${errorMsg}
2445
2743
  } else {
2446
2744
  throw new Error(`Unsupported AI Provider: ${provider}`);
2447
2745
  }
2448
- responseText = responseText.replace(/```json/g, "").replace(/```/g, "").trim();
2449
- const parsed = JSON.parse(responseText);
2746
+ const parsed = extractJson(responseText);
2450
2747
  if (!parsed.explanation || !Array.isArray(parsed.commandsToRun)) {
2451
2748
  throw new Error("AI returned an invalid response format.");
2452
2749
  }
@@ -2565,9 +2862,8 @@ ${content}
2565
2862
  ],
2566
2863
  response_format: { type: "json_object" }
2567
2864
  });
2568
- let responseText = response.choices[0].message.content || "{}";
2569
- responseText = responseText.replace(/```json/g, "").replace(/```/g, "").trim();
2570
- const parsed = JSON.parse(responseText);
2865
+ const responseText = response.choices[0].message.content || "{}";
2866
+ const parsed = extractJson(responseText);
2571
2867
  if (parsed.reason && parsed.proposedFix) {
2572
2868
  return parsed;
2573
2869
  }
@@ -2585,8 +2881,7 @@ ${content}
2585
2881
  { text: prompt }
2586
2882
  ]);
2587
2883
  }
2588
- responseText = responseText.replace(/```json/g, "").replace(/```/g, "").trim();
2589
- const parsed = JSON.parse(responseText);
2884
+ const parsed = extractJson(responseText);
2590
2885
  if (!parsed.reason || !parsed.proposedFix) {
2591
2886
  throw new Error("AI returned an invalid response format.");
2592
2887
  }
@@ -2738,7 +3033,7 @@ function toggleAutoSync(sessionId, repoPath, enabled, onLog, onErrorApproval, on
2738
3033
  }
2739
3034
  } catch {
2740
3035
  }
2741
- }, 25e3);
3036
+ }, 1e4);
2742
3037
  currentWatcher = chokidar.watch(repoPath, {
2743
3038
  ignored: [
2744
3039
  /(^|[/\\])\.git([/\\]|$)/,
@@ -2837,14 +3132,18 @@ async function triggerSyncSequence(sessionId, _repoPath, onLog) {
2837
3132
  if (globalOnErrorApproval && !isResolving) {
2838
3133
  isResolving = true;
2839
3134
  onLog("Attempting to resolve push failure...");
2840
- handleAutoSyncError(sessionId, error.message, onLog).catch(
2841
- (err) => onLog(`Resolution failed: ${err.message}`)
2842
- );
3135
+ handleAutoSyncError(sessionId, error.message, onLog).catch((err) => {
3136
+ onLog(`Resolution failed: ${err.message}`);
3137
+ }).finally(() => {
3138
+ setTimeout(() => {
3139
+ isResolving = false;
3140
+ }, 3e4);
3141
+ });
2843
3142
  }
2844
3143
  }
2845
3144
  }
2846
3145
  async function handleAutoSyncError(sessionId, errorMsg, onLog) {
2847
- const { git, repoPath } = getGitSession(sessionId);
3146
+ const { repoPath } = getGitSession(sessionId);
2848
3147
  if (!repoPath) {
2849
3148
  isResolving = false;
2850
3149
  return;
@@ -2860,31 +3159,32 @@ async function handleAutoSyncError(sessionId, errorMsg, onLog) {
2860
3159
  return;
2861
3160
  }
2862
3161
  try {
2863
- onLog("Pulling remote changes with rebase...");
3162
+ onLog("Syncing incoming remote changes from team...");
2864
3163
  const status2 = await getStatus(sessionId);
2865
3164
  const currentBranch = status2.current || "main";
2866
- await git.pull("origin", currentBranch, {
2867
- "--rebase": "false",
2868
- "--autostash": null,
2869
- "-X": "ours"
2870
- });
2871
- onLog("Pull successful, attempting push again...");
3165
+ const pullResult = await pullFromRemote(sessionId, "origin", currentBranch);
3166
+ onLog(`Pull result: ${pullResult.message}. Resuming push...`);
2872
3167
  await pushToRemote(sessionId, "origin", currentBranch);
2873
3168
  onLog("✅ Error resolved and push resumed!");
2874
3169
  isResolving = false;
2875
3170
  if (globalOnResolveComplete) globalOnResolveComplete();
2876
3171
  return;
2877
3172
  } catch (pullErr) {
2878
- onLog(`Pull failed (possible conflict): ${pullErr.message}`);
3173
+ onLog(`Pull notice: ${pullErr.message}`);
2879
3174
  }
2880
3175
  const status = await getStatus(sessionId);
2881
3176
  if (status.conflicted.length === 0) {
2882
- onLog("No conflicts found but resolution failed. Giving up.");
3177
+ onLog("Auto-push paused: Remote rejected push or requires review.");
2883
3178
  isResolving = false;
2884
3179
  if (globalOnErrorApproval) {
3180
+ const cleanReason = cleanGitErrorMessage(
3181
+ errorMsg,
3182
+ getToken(sessionId) || void 0,
3183
+ status.current || "main"
3184
+ );
2885
3185
  globalOnErrorApproval({
2886
- reason: "Failed to resolve error automatically. No file conflicts found.",
2887
- proposedFix: "Manual intervention required.",
3186
+ reason: cleanReason || "Remote repository rejected the push request.",
3187
+ proposedFix: 'Click "Approve & Fix" to attempt auto-reconciliation and push your changes.',
2888
3188
  files: []
2889
3189
  });
2890
3190
  }
@@ -2936,7 +3236,8 @@ async function applyAiFixAndResume(sessionId, resolvedCode) {
2936
3236
  if (!repoPath) return false;
2937
3237
  if (globalOnLog) globalOnLog("Applying AI resolution to conflicted files...");
2938
3238
  try {
2939
- if (resolvedCode) {
3239
+ const hasResolvedFiles = resolvedCode && Object.keys(resolvedCode).length > 0;
3240
+ if (hasResolvedFiles) {
2940
3241
  for (const [file, content] of Object.entries(resolvedCode)) {
2941
3242
  await fs__namespace.writeFile(path__namespace.join(repoPath, file), content, "utf-8");
2942
3243
  try {
@@ -2957,14 +3258,20 @@ async function applyAiFixAndResume(sessionId, resolvedCode) {
2957
3258
  }
2958
3259
  }
2959
3260
  }
2960
- }
2961
- try {
2962
- await git.rebase({ "--continue": null });
2963
- } catch {
2964
- await git.commit("Auto-resolved merge conflict by AI");
3261
+ try {
3262
+ await git.rebase({ "--continue": null });
3263
+ } catch {
3264
+ await ensureGitIdentity(sessionId, git);
3265
+ await git.commit("Auto-resolved merge conflict by AI");
3266
+ }
3267
+ } else {
3268
+ try {
3269
+ await pullFromRemote(sessionId);
3270
+ } catch {
3271
+ }
2965
3272
  }
2966
3273
  const status = await getStatus(sessionId);
2967
- if (globalOnLog) globalOnLog("Pushing AI-resolved changes...");
3274
+ if (globalOnLog) globalOnLog("Pushing changes...");
2968
3275
  await pushToRemote(sessionId, "origin", status.current || "main");
2969
3276
  if (globalOnLog) globalOnLog("✅ Push successful!");
2970
3277
  isResolving = false;
@@ -3246,6 +3553,9 @@ electron.app.whenReady().then(async () => {
3246
3553
  electron.ipcMain.handle("master-push", async (event, remote, branch) => {
3247
3554
  return await masterForcePush(event.sender.id, remote, branch);
3248
3555
  });
3556
+ electron.ipcMain.handle("master-pull", async (event, remote, branch) => {
3557
+ return await masterForcePull(event.sender.id, remote, branch);
3558
+ });
3249
3559
  electron.ipcMain.handle("pull-from-remote", async (event, remote, branch) => {
3250
3560
  return await pullFromRemote(event.sender.id, remote, branch);
3251
3561
  });