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/LICENSE +21 -0
- package/bin/ai-setup.js +1 -1
- package/bin/cli.js +4 -1
- package/out/main/index.js +421 -111
- package/out/preload/index.js +1 -0
- package/out/renderer/assets/index-CbRfLxpa.css +2989 -0
- package/out/renderer/assets/{index-7yJPiur0.js → index-CmvtfcrN.js} +605 -85
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/out/renderer/assets/index-CI-_rJcn.css +0 -2989
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
|
-
|
|
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
|
-
|
|
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=
|
|
1402
|
+
"pack.windowMemory=512m",
|
|
1348
1403
|
"-c",
|
|
1349
|
-
"pack.packSizeLimit=
|
|
1404
|
+
"pack.packSizeLimit=1024m",
|
|
1350
1405
|
"-c",
|
|
1351
|
-
"http.postBuffer=
|
|
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
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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.
|
|
1474
|
+
await git.raw(["-c", "core.safecrlf=false", "-c", "core.autocrlf=false", "add", "-A"]);
|
|
1415
1475
|
} catch {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
1665
|
+
"http.postBuffer=1048576000",
|
|
1509
1666
|
"-c",
|
|
1510
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
const
|
|
1561
|
-
if (
|
|
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
|
|
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
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
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
|
-
|
|
2414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2569
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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 {
|
|
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("
|
|
3162
|
+
onLog("Syncing incoming remote changes from team...");
|
|
2864
3163
|
const status2 = await getStatus(sessionId);
|
|
2865
3164
|
const currentBranch = status2.current || "main";
|
|
2866
|
-
await
|
|
2867
|
-
|
|
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
|
|
3173
|
+
onLog(`Pull notice: ${pullErr.message}`);
|
|
2879
3174
|
}
|
|
2880
3175
|
const status = await getStatus(sessionId);
|
|
2881
3176
|
if (status.conflicted.length === 0) {
|
|
2882
|
-
onLog("
|
|
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:
|
|
2887
|
-
proposedFix: "
|
|
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
|
-
|
|
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
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
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
|
|
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
|
});
|