guito 1.0.4 → 1.0.5
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/CHANGELOG.md +20 -0
- package/README.md +2 -0
- package/bin/guito-server.js +424 -72
- package/bin/ui/browser/index.html +1 -1
- package/bin/ui/browser/main-LODECJV3.js +67 -0
- package/guito-1.0.5.vsix +0 -0
- package/package.json +1 -1
- package/bin/ui/browser/main-DE53IE7Q.js +0 -67
package/bin/guito-server.js
CHANGED
|
@@ -401,6 +401,84 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
401
401
|
mutationQueue = result.catch(() => { });
|
|
402
402
|
return result;
|
|
403
403
|
};
|
|
404
|
+
// ==================== Mutation option helpers ====================
|
|
405
|
+
// Shared guards for the option fields the Git operation dialogs send.
|
|
406
|
+
// Every value is allowlisted before it reaches a Git argument array.
|
|
407
|
+
/** Reads a trimmed string body field; non-strings become ''. */
|
|
408
|
+
const strField = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
409
|
+
/** Reads a boolean body field; only an explicit true enables the option. */
|
|
410
|
+
const boolField = (value) => value === true;
|
|
411
|
+
/** Sends `{ success: true }`, adding warnings for partially-failed composites. */
|
|
412
|
+
const mutationResult = (resp, warnings) => resp.type('application/json').send({
|
|
413
|
+
success: true,
|
|
414
|
+
...(warnings.length ? { warnings } : {}),
|
|
415
|
+
});
|
|
416
|
+
/**
|
|
417
|
+
* Resolves the remote a remote-capable action should use: an explicit name
|
|
418
|
+
* (which must be configured), else "origin", else the first remote.
|
|
419
|
+
*/
|
|
420
|
+
const resolveRemote = async (requested) => {
|
|
421
|
+
const names = (await git.raw(['remote']))
|
|
422
|
+
.split(/\r?\n/)
|
|
423
|
+
.map((name) => name.trim())
|
|
424
|
+
.filter(Boolean);
|
|
425
|
+
if (names.length === 0)
|
|
426
|
+
throw new Error('No remotes are configured for this repository.');
|
|
427
|
+
if (requested) {
|
|
428
|
+
if (!names.includes(requested))
|
|
429
|
+
throw new Error(`Remote "${requested}" is not configured.`);
|
|
430
|
+
return requested;
|
|
431
|
+
}
|
|
432
|
+
return names.includes('origin') ? 'origin' : names[0];
|
|
433
|
+
};
|
|
434
|
+
/** Verifies a branch name Git would accept, before it is used as an argument. */
|
|
435
|
+
const verifyBranchName = async (name) => {
|
|
436
|
+
await git.raw(['check-ref-format', '--branch', name]);
|
|
437
|
+
};
|
|
438
|
+
/** Verifies a tag name Git would accept (rejects "", "..", "~", "^", ":"...). */
|
|
439
|
+
const verifyTagName = async (name) => {
|
|
440
|
+
await git.raw(['check-ref-format', `refs/tags/${name}`]);
|
|
441
|
+
};
|
|
442
|
+
/** Parent hashes of a commit, used for merge-commit mainline validation. */
|
|
443
|
+
const commitParents = async (commit) => {
|
|
444
|
+
const raw = await git.raw(['rev-list', '--parents', '-n', '1', commit]);
|
|
445
|
+
return raw.trim().split(/\s+/).slice(1).filter(Boolean);
|
|
446
|
+
};
|
|
447
|
+
/** Validates a mainline parent selection for a merge commit. */
|
|
448
|
+
const mainlineArg = (mainline, parents) => {
|
|
449
|
+
const parsed = Number(mainline);
|
|
450
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > parents.length) {
|
|
451
|
+
throw new Error(`Choose a mainline parent between 1 and ${parents.length}.`);
|
|
452
|
+
}
|
|
453
|
+
return String(parsed);
|
|
454
|
+
};
|
|
455
|
+
/** Rejects tracked changes that would block a history-rewriting operation. */
|
|
456
|
+
const requireCleanWorktree = async () => {
|
|
457
|
+
const status = await git.raw(['status', '--porcelain']);
|
|
458
|
+
const dirty = status
|
|
459
|
+
.split(/\r?\n/)
|
|
460
|
+
.filter(Boolean)
|
|
461
|
+
.filter((line) => !line.startsWith('??'));
|
|
462
|
+
if (dirty.length > 0) {
|
|
463
|
+
throw new Error('The working tree has uncommitted changes. Commit or stash them first.');
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
/** Requires HEAD to sit on a branch (not detached), returning its name. */
|
|
467
|
+
const currentBranch = async () => {
|
|
468
|
+
// simple-git may swallow the non-zero exit of a detached symbolic-ref, so
|
|
469
|
+
// parse the output instead of relying on the rejection.
|
|
470
|
+
let head = '';
|
|
471
|
+
try {
|
|
472
|
+
head = await git.raw(['symbolic-ref', '--quiet', 'HEAD']);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
head = '';
|
|
476
|
+
}
|
|
477
|
+
const branch = head.trim().replace(/^refs\/heads\//, '');
|
|
478
|
+
if (!branch)
|
|
479
|
+
throw new Error('HEAD is detached. Check out a branch first.');
|
|
480
|
+
return branch;
|
|
481
|
+
};
|
|
404
482
|
const repoRoot = async () => (await git.revparse(['--show-toplevel'])).trim();
|
|
405
483
|
// ==================== Azure DevOps settings ====================
|
|
406
484
|
// Settings live in the repository's git directory so they are per-repo and
|
|
@@ -794,9 +872,25 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
794
872
|
});
|
|
795
873
|
app.post('/api/revert', async (req, resp) => {
|
|
796
874
|
try {
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
875
|
+
const hash = strField(req.body?.commit);
|
|
876
|
+
if (!hash)
|
|
877
|
+
throw new Error('A commit is required.');
|
|
878
|
+
const noCommit = boolField(req.body?.noCommit);
|
|
879
|
+
const signoff = boolField(req.body?.signoff);
|
|
880
|
+
await mutate(async () => {
|
|
881
|
+
const parents = await commitParents(hash);
|
|
882
|
+
const args = ['revert'];
|
|
883
|
+
// Merge commits must pick the parent the changes are reverted against.
|
|
884
|
+
if (parents.length > 1)
|
|
885
|
+
args.push('--mainline', mainlineArg(req.body?.mainline, parents));
|
|
886
|
+
if (noCommit)
|
|
887
|
+
args.push('--no-commit');
|
|
888
|
+
if (signoff)
|
|
889
|
+
args.push('--signoff');
|
|
890
|
+
args.push(hash);
|
|
891
|
+
await git.raw(args);
|
|
892
|
+
});
|
|
893
|
+
return mutationResult(resp, []);
|
|
800
894
|
}
|
|
801
895
|
catch (err) {
|
|
802
896
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -804,8 +898,28 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
804
898
|
});
|
|
805
899
|
app.post('/api/cherry-pick', async (req, resp) => {
|
|
806
900
|
try {
|
|
807
|
-
|
|
808
|
-
|
|
901
|
+
const hash = strField(req.body?.commit);
|
|
902
|
+
if (!hash)
|
|
903
|
+
throw new Error('A commit is required.');
|
|
904
|
+
const noCommit = boolField(req.body?.noCommit);
|
|
905
|
+
const recordSource = boolField(req.body?.recordSource);
|
|
906
|
+
const signoff = boolField(req.body?.signoff);
|
|
907
|
+
await mutate(async () => {
|
|
908
|
+
const parents = await commitParents(hash);
|
|
909
|
+
const args = ['cherry-pick'];
|
|
910
|
+
// Merge commits must pick the parent the changes are picked against.
|
|
911
|
+
if (parents.length > 1)
|
|
912
|
+
args.push('--mainline', mainlineArg(req.body?.mainline, parents));
|
|
913
|
+
if (noCommit)
|
|
914
|
+
args.push('--no-commit');
|
|
915
|
+
if (recordSource)
|
|
916
|
+
args.push('-x');
|
|
917
|
+
if (signoff)
|
|
918
|
+
args.push('--signoff');
|
|
919
|
+
args.push(hash);
|
|
920
|
+
await git.raw(args);
|
|
921
|
+
});
|
|
922
|
+
return mutationResult(resp, []);
|
|
809
923
|
}
|
|
810
924
|
catch (err) {
|
|
811
925
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -813,8 +927,32 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
813
927
|
});
|
|
814
928
|
app.post('/api/commit/drop', async (req, resp) => {
|
|
815
929
|
try {
|
|
816
|
-
|
|
817
|
-
|
|
930
|
+
const hash = strField(req.body?.commit);
|
|
931
|
+
if (!hash)
|
|
932
|
+
throw new Error('A commit is required.');
|
|
933
|
+
await mutate(async () => {
|
|
934
|
+
// Dropping rewrites history: only a clean, branch-bound working tree
|
|
935
|
+
// can safely replay the descendants of the selected commit.
|
|
936
|
+
await currentBranch();
|
|
937
|
+
await requireCleanWorktree();
|
|
938
|
+
const parents = await commitParents(hash);
|
|
939
|
+
if (parents.length === 0)
|
|
940
|
+
throw new Error('The root commit cannot be dropped.');
|
|
941
|
+
if (parents.length > 1)
|
|
942
|
+
throw new Error('Merge commits cannot be dropped.');
|
|
943
|
+
// The commit must be part of the current branch's history: when it is
|
|
944
|
+
// an ancestor of HEAD, it is also the merge base of the two.
|
|
945
|
+
const base = (await git.raw(['merge-base', hash, 'HEAD'])).trim();
|
|
946
|
+
if (base !== hash) {
|
|
947
|
+
throw new Error('The commit is not reachable from the current branch.');
|
|
948
|
+
}
|
|
949
|
+
// Replay every descendant onto the dropped commit's parent, removing
|
|
950
|
+
// only the selected commit (a hard reset would lose the descendants).
|
|
951
|
+
// Rebase by branch name: rebasing HEAD detaches it instead of moving
|
|
952
|
+
// the branch ref.
|
|
953
|
+
await git.raw(['rebase', '--onto', `${hash}~1`, hash, await currentBranch()]);
|
|
954
|
+
});
|
|
955
|
+
return mutationResult(resp, []);
|
|
818
956
|
}
|
|
819
957
|
catch (err) {
|
|
820
958
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -822,9 +960,12 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
822
960
|
});
|
|
823
961
|
app.post('/api/reset-commit', async (req, resp) => {
|
|
824
962
|
try {
|
|
963
|
+
const hash = strField(req.body?.commit);
|
|
964
|
+
if (!hash)
|
|
965
|
+
throw new Error('A commit is required.');
|
|
825
966
|
const mode = ['soft', 'mixed', 'hard'].includes(req.body?.mode) ? req.body.mode : 'hard';
|
|
826
|
-
await git.raw(['reset', `--${mode}`,
|
|
827
|
-
return resp
|
|
967
|
+
await mutate(() => git.raw(['reset', `--${mode}`, hash]));
|
|
968
|
+
return mutationResult(resp, []);
|
|
828
969
|
}
|
|
829
970
|
catch (err) {
|
|
830
971
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -905,9 +1046,36 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
905
1046
|
});
|
|
906
1047
|
app.post('/api/branch/create', async (req, resp) => {
|
|
907
1048
|
try {
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
1049
|
+
const name = strField(req.body?.name);
|
|
1050
|
+
if (!name)
|
|
1051
|
+
throw new Error('A branch name is required.');
|
|
1052
|
+
await verifyBranchName(name);
|
|
1053
|
+
const startPoint = strField(req.body?.startPoint);
|
|
1054
|
+
const checkout = boolField(req.body?.checkout);
|
|
1055
|
+
const warnings = [];
|
|
1056
|
+
await mutate(async () => {
|
|
1057
|
+
// Resolve the remote up front so a bad configuration fails before any
|
|
1058
|
+
// local branch is created.
|
|
1059
|
+
const publishRemote = boolField(req.body?.publish)
|
|
1060
|
+
? await resolveRemote(strField(req.body?.remote))
|
|
1061
|
+
: '';
|
|
1062
|
+
if (checkout) {
|
|
1063
|
+
await git.raw(['checkout', '-b', name, ...(startPoint ? [startPoint] : [])]);
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
await git.branch([...(startPoint ? [name, startPoint] : [name])]);
|
|
1067
|
+
}
|
|
1068
|
+
// Publishing is a follow-up step: keep the created branch when it fails.
|
|
1069
|
+
if (publishRemote) {
|
|
1070
|
+
try {
|
|
1071
|
+
await git.push(['-u', publishRemote, name]);
|
|
1072
|
+
}
|
|
1073
|
+
catch (err) {
|
|
1074
|
+
warnings.push(`Branch "${name}" was created but could not be published to "${publishRemote}": ${err.message}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
return mutationResult(resp, warnings);
|
|
911
1079
|
}
|
|
912
1080
|
catch (err) {
|
|
913
1081
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -915,9 +1083,29 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
915
1083
|
});
|
|
916
1084
|
app.post('/api/branch/delete', async (req, resp) => {
|
|
917
1085
|
try {
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
1086
|
+
const name = strField(req.body?.name);
|
|
1087
|
+
if (!name)
|
|
1088
|
+
throw new Error('A branch name is required.');
|
|
1089
|
+
const force = boolField(req.body?.force);
|
|
1090
|
+
const warnings = [];
|
|
1091
|
+
await mutate(async () => {
|
|
1092
|
+
// Resolve the remote up front so a bad configuration fails before the
|
|
1093
|
+
// local branch is deleted.
|
|
1094
|
+
const deleteRemote = boolField(req.body?.deleteRemote)
|
|
1095
|
+
? await resolveRemote(strField(req.body?.remote))
|
|
1096
|
+
: '';
|
|
1097
|
+
await git.deleteLocalBranch(name, force);
|
|
1098
|
+
// Deleting on the remote is a follow-up: keep the local deletion when it fails.
|
|
1099
|
+
if (deleteRemote) {
|
|
1100
|
+
try {
|
|
1101
|
+
await git.push([deleteRemote, '--delete', `refs/heads/${name}`]);
|
|
1102
|
+
}
|
|
1103
|
+
catch (err) {
|
|
1104
|
+
warnings.push(`Branch "${name}" was deleted locally but could not be deleted on "${deleteRemote}": ${err.message}`);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
return mutationResult(resp, warnings);
|
|
921
1109
|
}
|
|
922
1110
|
catch (err) {
|
|
923
1111
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -925,9 +1113,13 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
925
1113
|
});
|
|
926
1114
|
app.post('/api/branch/delete-remote', async (req, resp) => {
|
|
927
1115
|
try {
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
1116
|
+
const remote = strField(req.body?.remote);
|
|
1117
|
+
const branch = strField(req.body?.branch);
|
|
1118
|
+
if (!remote || !branch)
|
|
1119
|
+
throw new Error('A remote and branch are required.');
|
|
1120
|
+
const resolved = await resolveRemote(remote);
|
|
1121
|
+
await mutate(() => git.push([resolved, '--delete', `refs/heads/${branch}`]));
|
|
1122
|
+
return mutationResult(resp, []);
|
|
931
1123
|
}
|
|
932
1124
|
catch (err) {
|
|
933
1125
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -935,9 +1127,41 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
935
1127
|
});
|
|
936
1128
|
app.post('/api/branch/rename', async (req, resp) => {
|
|
937
1129
|
try {
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
1130
|
+
const oldName = strField(req.body?.oldName);
|
|
1131
|
+
const newName = strField(req.body?.newName);
|
|
1132
|
+
if (!oldName || !newName)
|
|
1133
|
+
throw new Error('The current and new branch names are required.');
|
|
1134
|
+
await verifyBranchName(newName);
|
|
1135
|
+
const publish = boolField(req.body?.publish);
|
|
1136
|
+
const deleteOld = boolField(req.body?.deleteRemoteOld);
|
|
1137
|
+
const warnings = [];
|
|
1138
|
+
await mutate(async () => {
|
|
1139
|
+
// Resolve the remote up front so a bad configuration fails before the
|
|
1140
|
+
// branch is renamed.
|
|
1141
|
+
const needsRemote = publish || deleteOld;
|
|
1142
|
+
const remote = needsRemote ? await resolveRemote(strField(req.body?.remote)) : '';
|
|
1143
|
+
await git.branch(['-m', oldName, newName]);
|
|
1144
|
+
// Publishing/deleting the old ref are follow-ups: keep the rename when they fail.
|
|
1145
|
+
if (remote) {
|
|
1146
|
+
if (publish) {
|
|
1147
|
+
try {
|
|
1148
|
+
await git.push(['-u', remote, newName]);
|
|
1149
|
+
}
|
|
1150
|
+
catch (err) {
|
|
1151
|
+
warnings.push(`Branch "${newName}" was renamed but could not be published to "${remote}": ${err.message}`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (deleteOld) {
|
|
1155
|
+
try {
|
|
1156
|
+
await git.push([remote, '--delete', `refs/heads/${oldName}`]);
|
|
1157
|
+
}
|
|
1158
|
+
catch (err) {
|
|
1159
|
+
warnings.push(`The old branch "${oldName}" was renamed locally but could not be deleted on "${remote}": ${err.message}`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
return mutationResult(resp, warnings);
|
|
941
1165
|
}
|
|
942
1166
|
catch (err) {
|
|
943
1167
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1039,9 +1263,44 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1039
1263
|
});
|
|
1040
1264
|
app.post('/api/checkout', async (req, resp) => {
|
|
1041
1265
|
try {
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
1266
|
+
const ref = strField(req.body?.ref);
|
|
1267
|
+
if (!ref)
|
|
1268
|
+
throw new Error('A ref is required.');
|
|
1269
|
+
const newBranch = strField(req.body?.newBranch);
|
|
1270
|
+
const detach = boolField(req.body?.detach);
|
|
1271
|
+
const track = boolField(req.body?.track);
|
|
1272
|
+
const warnings = [];
|
|
1273
|
+
await mutate(async () => {
|
|
1274
|
+
// Resolve the remote up front so a bad configuration fails before the
|
|
1275
|
+
// checkout happens.
|
|
1276
|
+
const publishRemote = newBranch && boolField(req.body?.publish)
|
|
1277
|
+
? await resolveRemote(strField(req.body?.remote))
|
|
1278
|
+
: '';
|
|
1279
|
+
if (newBranch) {
|
|
1280
|
+
await verifyBranchName(newBranch);
|
|
1281
|
+
// --track sets the upstream when checking out a remote-tracking ref.
|
|
1282
|
+
await git.raw([
|
|
1283
|
+
'checkout',
|
|
1284
|
+
'-b',
|
|
1285
|
+
newBranch,
|
|
1286
|
+
...(track ? ['--track'] : []),
|
|
1287
|
+
ref,
|
|
1288
|
+
]);
|
|
1289
|
+
}
|
|
1290
|
+
else {
|
|
1291
|
+
await git.raw(['checkout', ...(detach ? ['--detach'] : []), ref]);
|
|
1292
|
+
}
|
|
1293
|
+
// Publishing the new branch is a follow-up: keep the checkout when it fails.
|
|
1294
|
+
if (publishRemote) {
|
|
1295
|
+
try {
|
|
1296
|
+
await git.push(['-u', publishRemote, newBranch]);
|
|
1297
|
+
}
|
|
1298
|
+
catch (err) {
|
|
1299
|
+
warnings.push(`Branch "${newBranch}" was checked out but could not be published to "${publishRemote}": ${err.message}`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
return mutationResult(resp, warnings);
|
|
1045
1304
|
}
|
|
1046
1305
|
catch (err) {
|
|
1047
1306
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1050,9 +1309,28 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1050
1309
|
// ==================== Merge & Rebase ====================
|
|
1051
1310
|
app.post('/api/merge', async (req, resp) => {
|
|
1052
1311
|
try {
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1312
|
+
const branch = strField(req.body?.branch);
|
|
1313
|
+
if (!branch)
|
|
1314
|
+
throw new Error('A branch or commit is required.');
|
|
1315
|
+
const mode = ['default', 'no-ff', 'ff-only', 'squash'].includes(req.body?.mode)
|
|
1316
|
+
? req.body.mode
|
|
1317
|
+
: 'default';
|
|
1318
|
+
// --no-commit only applies to modes that would create a commit.
|
|
1319
|
+
const noCommit = boolField(req.body?.noCommit) && (mode === 'default' || mode === 'no-ff');
|
|
1320
|
+
const args = ['merge'];
|
|
1321
|
+
if (mode === 'no-ff')
|
|
1322
|
+
args.push('--no-ff');
|
|
1323
|
+
if (mode === 'ff-only')
|
|
1324
|
+
args.push('--ff-only');
|
|
1325
|
+
if (mode === 'squash')
|
|
1326
|
+
args.push('--squash');
|
|
1327
|
+
if (noCommit)
|
|
1328
|
+
args.push('--no-commit');
|
|
1329
|
+
if (boolField(req.body?.autostash))
|
|
1330
|
+
args.push('--autostash');
|
|
1331
|
+
args.push(branch);
|
|
1332
|
+
await mutate(() => git.raw(args));
|
|
1333
|
+
return mutationResult(resp, []);
|
|
1056
1334
|
}
|
|
1057
1335
|
catch (err) {
|
|
1058
1336
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1080,9 +1358,18 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1080
1358
|
});
|
|
1081
1359
|
app.post('/api/rebase', async (req, resp) => {
|
|
1082
1360
|
try {
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1361
|
+
const branch = strField(req.body?.branch);
|
|
1362
|
+
if (!branch)
|
|
1363
|
+
throw new Error('A branch or commit is required.');
|
|
1364
|
+
await mutate(() => git.raw([
|
|
1365
|
+
'rebase',
|
|
1366
|
+
...(boolField(req.body?.autostash) ? ['--autostash'] : []),
|
|
1367
|
+
// --preserve-merges was removed in Git 2.35+; --rebase-merges is the
|
|
1368
|
+
// supported way to keep merge commits while rebasing.
|
|
1369
|
+
...(boolField(req.body?.preserveMerges) ? ['--rebase-merges'] : []),
|
|
1370
|
+
branch,
|
|
1371
|
+
]));
|
|
1372
|
+
return mutationResult(resp, []);
|
|
1086
1373
|
}
|
|
1087
1374
|
catch (err) {
|
|
1088
1375
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1131,23 +1418,24 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1131
1418
|
});
|
|
1132
1419
|
app.post('/api/stash/save', async (req, resp) => {
|
|
1133
1420
|
try {
|
|
1134
|
-
const
|
|
1421
|
+
const message = strField(req.body?.message);
|
|
1422
|
+
const scope = ['staged', 'unstaged', 'all'].includes(req.body?.scope) ? req.body.scope : 'all';
|
|
1135
1423
|
// 'staged' → index only (--staged, git ≥ 2.35); 'unstaged' → working tree only
|
|
1136
1424
|
// (--keep-index leaves staged changes staged); 'all' → index + working tree.
|
|
1137
|
-
// Untracked files ride along
|
|
1425
|
+
// Untracked files ride along unless includeUntracked opts out (the legacy
|
|
1426
|
+
// callers kept them for 'all'/'unstaged', which stay the defaults).
|
|
1427
|
+
const includeUntracked = typeof req.body?.includeUntracked === 'boolean' ? req.body.includeUntracked : scope !== 'staged';
|
|
1138
1428
|
const args = ['push'];
|
|
1139
|
-
if (scope === 'staged')
|
|
1429
|
+
if (scope === 'staged')
|
|
1140
1430
|
args.push('--staged');
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
args.push('--keep-index');
|
|
1431
|
+
if (scope === 'unstaged')
|
|
1432
|
+
args.push('--keep-index');
|
|
1433
|
+
if (includeUntracked)
|
|
1145
1434
|
args.push('-u');
|
|
1146
|
-
}
|
|
1147
1435
|
if (message)
|
|
1148
1436
|
args.push('-m', message);
|
|
1149
|
-
await git.stash(args);
|
|
1150
|
-
return resp
|
|
1437
|
+
await mutate(() => git.stash(args));
|
|
1438
|
+
return mutationResult(resp, []);
|
|
1151
1439
|
}
|
|
1152
1440
|
catch (err) {
|
|
1153
1441
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1155,9 +1443,13 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1155
1443
|
});
|
|
1156
1444
|
app.post('/api/stash/apply', async (req, resp) => {
|
|
1157
1445
|
try {
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1446
|
+
const index = Number(req.body?.index);
|
|
1447
|
+
if (!Number.isInteger(index) || index < 0)
|
|
1448
|
+
throw new Error('A stash index is required.');
|
|
1449
|
+
// --index restores the staged/unstaged split recorded by the stash.
|
|
1450
|
+
const args = ['apply', ...(boolField(req.body?.restoreIndex) ? ['--index'] : []), `stash@{${index}}`];
|
|
1451
|
+
await mutate(() => git.stash(args));
|
|
1452
|
+
return mutationResult(resp, []);
|
|
1161
1453
|
}
|
|
1162
1454
|
catch (err) {
|
|
1163
1455
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1165,9 +1457,12 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1165
1457
|
});
|
|
1166
1458
|
app.post('/api/stash/pop', async (req, resp) => {
|
|
1167
1459
|
try {
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
|
|
1460
|
+
const index = Number(req.body?.index);
|
|
1461
|
+
if (!Number.isInteger(index) || index < 0)
|
|
1462
|
+
throw new Error('A stash index is required.');
|
|
1463
|
+
const args = ['pop', ...(boolField(req.body?.restoreIndex) ? ['--index'] : []), `stash@{${index}}`];
|
|
1464
|
+
await mutate(() => git.stash(args));
|
|
1465
|
+
return mutationResult(resp, []);
|
|
1171
1466
|
}
|
|
1172
1467
|
catch (err) {
|
|
1173
1468
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1176,8 +1471,8 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1176
1471
|
app.post('/api/stash/drop', async (req, resp) => {
|
|
1177
1472
|
try {
|
|
1178
1473
|
const { index } = req.body;
|
|
1179
|
-
await git.stash(['drop', `stash@{${index}}`]);
|
|
1180
|
-
return resp
|
|
1474
|
+
await mutate(() => git.stash(['drop', `stash@{${index}}`]));
|
|
1475
|
+
return mutationResult(resp, []);
|
|
1181
1476
|
}
|
|
1182
1477
|
catch (err) {
|
|
1183
1478
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1217,14 +1512,41 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1217
1512
|
});
|
|
1218
1513
|
app.post('/api/tag/create', async (req, resp) => {
|
|
1219
1514
|
try {
|
|
1220
|
-
const
|
|
1221
|
-
if (
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1515
|
+
const name = strField(req.body?.name);
|
|
1516
|
+
if (!name)
|
|
1517
|
+
throw new Error('A tag name is required.');
|
|
1518
|
+
await verifyTagName(name);
|
|
1519
|
+
const message = strField(req.body?.message);
|
|
1520
|
+
// A message always produces an annotated tag (legacy callers relied on it);
|
|
1521
|
+
// the dialog sends annotate explicitly and must then include a message.
|
|
1522
|
+
const annotate = boolField(req.body?.annotate) || !!message;
|
|
1523
|
+
if (annotate && !message)
|
|
1524
|
+
throw new Error('An annotated tag requires a message.');
|
|
1525
|
+
const commit = strField(req.body?.commit);
|
|
1526
|
+
const warnings = [];
|
|
1527
|
+
await mutate(async () => {
|
|
1528
|
+
// Resolve the remote up front so a bad configuration fails before the
|
|
1529
|
+
// tag is created.
|
|
1530
|
+
const pushRemote = boolField(req.body?.push)
|
|
1531
|
+
? await resolveRemote(strField(req.body?.remote))
|
|
1532
|
+
: '';
|
|
1533
|
+
if (annotate) {
|
|
1534
|
+
await git.tag(['-a', name, '-m', message, ...(commit ? [commit] : [])]);
|
|
1535
|
+
}
|
|
1536
|
+
else {
|
|
1537
|
+
await git.tag([name, ...(commit ? [commit] : [])]);
|
|
1538
|
+
}
|
|
1539
|
+
// Pushing is a follow-up: keep the created tag when it fails.
|
|
1540
|
+
if (pushRemote) {
|
|
1541
|
+
try {
|
|
1542
|
+
await git.push([pushRemote, `refs/tags/${name}`]);
|
|
1543
|
+
}
|
|
1544
|
+
catch (err) {
|
|
1545
|
+
warnings.push(`Tag "${name}" was created but could not be pushed to "${pushRemote}": ${err.message}`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
return mutationResult(resp, warnings);
|
|
1228
1550
|
}
|
|
1229
1551
|
catch (err) {
|
|
1230
1552
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1232,9 +1554,28 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1232
1554
|
});
|
|
1233
1555
|
app.post('/api/tag/delete', async (req, resp) => {
|
|
1234
1556
|
try {
|
|
1235
|
-
const
|
|
1236
|
-
|
|
1237
|
-
|
|
1557
|
+
const name = strField(req.body?.name);
|
|
1558
|
+
if (!name)
|
|
1559
|
+
throw new Error('A tag name is required.');
|
|
1560
|
+
const warnings = [];
|
|
1561
|
+
await mutate(async () => {
|
|
1562
|
+
// Resolve the remote up front so a bad configuration fails before the
|
|
1563
|
+
// local tag is deleted.
|
|
1564
|
+
const deleteRemote = boolField(req.body?.deleteRemote)
|
|
1565
|
+
? await resolveRemote(strField(req.body?.remote))
|
|
1566
|
+
: '';
|
|
1567
|
+
await git.tag(['-d', name]);
|
|
1568
|
+
// Deleting on the remote is a follow-up: keep the local deletion when it fails.
|
|
1569
|
+
if (deleteRemote) {
|
|
1570
|
+
try {
|
|
1571
|
+
await git.push([deleteRemote, '--delete', `refs/tags/${name}`]);
|
|
1572
|
+
}
|
|
1573
|
+
catch (err) {
|
|
1574
|
+
warnings.push(`Tag "${name}" was deleted locally but could not be deleted on "${deleteRemote}": ${err.message}`);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
return mutationResult(resp, warnings);
|
|
1238
1579
|
}
|
|
1239
1580
|
catch (err) {
|
|
1240
1581
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1242,12 +1583,12 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1242
1583
|
});
|
|
1243
1584
|
app.post('/api/tag/push', async (req, resp) => {
|
|
1244
1585
|
try {
|
|
1245
|
-
const
|
|
1246
|
-
if (!name)
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
await git.push(remote
|
|
1250
|
-
return resp
|
|
1586
|
+
const name = strField(req.body?.name);
|
|
1587
|
+
if (!name)
|
|
1588
|
+
throw new Error('A tag name is required.');
|
|
1589
|
+
const remote = await resolveRemote(strField(req.body?.remote));
|
|
1590
|
+
await mutate(() => git.push([remote, ...(boolField(req.body?.force) ? ['-f'] : []), `refs/tags/${name}`]));
|
|
1591
|
+
return mutationResult(resp, []);
|
|
1251
1592
|
}
|
|
1252
1593
|
catch (err) {
|
|
1253
1594
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1266,9 +1607,20 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1266
1607
|
});
|
|
1267
1608
|
app.post('/api/pull', async (req, resp) => {
|
|
1268
1609
|
try {
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1610
|
+
const remote = strField(req.body?.remote);
|
|
1611
|
+
const branch = strField(req.body?.branch);
|
|
1612
|
+
// 'mode' supersedes the legacy 'rebase' boolean (kept for older callers).
|
|
1613
|
+
const mode = ['merge', 'rebase', 'ff-only'].includes(req.body?.mode) ? req.body.mode : 'merge';
|
|
1614
|
+
const rebase = boolField(req.body?.rebase) || mode === 'rebase';
|
|
1615
|
+
const ffOnly = mode === 'ff-only';
|
|
1616
|
+
// An explicit remote must be configured; without one, keep the legacy
|
|
1617
|
+
// 'origin' default so upstream-based pulls keep working.
|
|
1618
|
+
const resolvedRemote = remote ? await resolveRemote(remote) : 'origin';
|
|
1619
|
+
await mutate(() => git.pull(resolvedRemote, branch || undefined, {
|
|
1620
|
+
...(rebase ? { '--rebase': null } : {}),
|
|
1621
|
+
...(ffOnly ? { '--ff-only': null } : {}),
|
|
1622
|
+
}));
|
|
1623
|
+
return mutationResult(resp, []);
|
|
1272
1624
|
}
|
|
1273
1625
|
catch (err) {
|
|
1274
1626
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -2858,8 +3210,8 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
2858
3210
|
// ==================== Working Tree Actions ====================
|
|
2859
3211
|
app.post('/api/reset', async (_req, resp) => {
|
|
2860
3212
|
try {
|
|
2861
|
-
await git.raw(['reset', '--hard', 'HEAD']);
|
|
2862
|
-
return resp
|
|
3213
|
+
await mutate(() => git.raw(['reset', '--hard', 'HEAD']));
|
|
3214
|
+
return mutationResult(resp, []);
|
|
2863
3215
|
}
|
|
2864
3216
|
catch (err) {
|
|
2865
3217
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -2867,8 +3219,8 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
2867
3219
|
});
|
|
2868
3220
|
app.post('/api/clean', async (_req, resp) => {
|
|
2869
3221
|
try {
|
|
2870
|
-
await git.raw(['clean', '-fd']);
|
|
2871
|
-
return resp
|
|
3222
|
+
await mutate(() => git.raw(['clean', '-fd']));
|
|
3223
|
+
return mutationResult(resp, []);
|
|
2872
3224
|
}
|
|
2873
3225
|
catch (err) {
|
|
2874
3226
|
return resp.status(400).type('application/json').send({ error: err.message });
|