guito 1.0.3 → 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 +29 -0
- package/README.md +2 -0
- package/bin/guito-server.js +435 -73
- 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-H44AMFJL.js +0 -67
package/bin/guito-server.js
CHANGED
|
@@ -235,7 +235,7 @@ const azureErrorMessage = (result) => {
|
|
|
235
235
|
return result.body;
|
|
236
236
|
}
|
|
237
237
|
};
|
|
238
|
-
export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 8080, apiToken, azureDevOpsUrl, prBranchNameTemplate, autoReload, diffViewer, showGraph, showStashes, showTags, showRemoteBranches, issueRegex, issueUrl, allowMerge, fileListView, refListView, searchMode, searchCaseSensitive, azureRequestImpl, avatarFetchImpl, onLog, }) {
|
|
238
|
+
export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 8080, apiToken, azureDevOpsUrl, prBranchNameTemplate, autoReload, diffViewer, showGraph, showStashes, showTags, showRemoteBranches, issueRegex, issueUrl, allowMerge, fileListView, refListView, sidePanelSectionsExpanded, searchMode, searchCaseSensitive, azureRequestImpl, avatarFetchImpl, onLog, }) {
|
|
239
239
|
// Initialize server
|
|
240
240
|
const app = fastify({
|
|
241
241
|
logger: false,
|
|
@@ -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
|
|
@@ -583,6 +661,9 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
583
661
|
const fileShowRemoteBranches = typeof file.showRemoteBranches === 'boolean' ? file.showRemoteBranches : undefined;
|
|
584
662
|
const fileFileListView = file.fileListView === 'tree' || file.fileListView === 'flat' ? file.fileListView : undefined;
|
|
585
663
|
const fileRefListView = file.refListView === 'tree' || file.refListView === 'flat' ? file.refListView : undefined;
|
|
664
|
+
const fileSidePanelSectionsExpanded = typeof file.sidePanelSectionsExpanded === 'boolean'
|
|
665
|
+
? file.sidePanelSectionsExpanded
|
|
666
|
+
: undefined;
|
|
586
667
|
const fileSearchMode = file.searchMode === 'filter' || file.searchMode === 'navigate' ? file.searchMode : undefined;
|
|
587
668
|
const fileAllowMerge = typeof file.allowMerge === 'boolean' ? file.allowMerge : undefined;
|
|
588
669
|
const fileSearchCaseSensitive = typeof file.searchCaseSensitive === 'boolean' ? file.searchCaseSensitive : undefined;
|
|
@@ -632,6 +713,9 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
632
713
|
: (fileShowRemoteBranches ?? true),
|
|
633
714
|
fileListView: fileListView ?? fileFileListView ?? 'flat',
|
|
634
715
|
refListView: refListView ?? fileRefListView ?? 'flat',
|
|
716
|
+
sidePanelSectionsExpanded: typeof sidePanelSectionsExpanded === 'boolean'
|
|
717
|
+
? sidePanelSectionsExpanded
|
|
718
|
+
: (fileSidePanelSectionsExpanded ?? true),
|
|
635
719
|
searchMode: searchMode ?? fileSearchMode ?? 'navigate',
|
|
636
720
|
searchCaseSensitive: typeof searchCaseSensitive === 'boolean'
|
|
637
721
|
? searchCaseSensitive
|
|
@@ -788,9 +872,25 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
788
872
|
});
|
|
789
873
|
app.post('/api/revert', async (req, resp) => {
|
|
790
874
|
try {
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
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, []);
|
|
794
894
|
}
|
|
795
895
|
catch (err) {
|
|
796
896
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -798,8 +898,28 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
798
898
|
});
|
|
799
899
|
app.post('/api/cherry-pick', async (req, resp) => {
|
|
800
900
|
try {
|
|
801
|
-
|
|
802
|
-
|
|
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, []);
|
|
803
923
|
}
|
|
804
924
|
catch (err) {
|
|
805
925
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -807,8 +927,32 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
807
927
|
});
|
|
808
928
|
app.post('/api/commit/drop', async (req, resp) => {
|
|
809
929
|
try {
|
|
810
|
-
|
|
811
|
-
|
|
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, []);
|
|
812
956
|
}
|
|
813
957
|
catch (err) {
|
|
814
958
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -816,9 +960,12 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
816
960
|
});
|
|
817
961
|
app.post('/api/reset-commit', async (req, resp) => {
|
|
818
962
|
try {
|
|
963
|
+
const hash = strField(req.body?.commit);
|
|
964
|
+
if (!hash)
|
|
965
|
+
throw new Error('A commit is required.');
|
|
819
966
|
const mode = ['soft', 'mixed', 'hard'].includes(req.body?.mode) ? req.body.mode : 'hard';
|
|
820
|
-
await git.raw(['reset', `--${mode}`,
|
|
821
|
-
return resp
|
|
967
|
+
await mutate(() => git.raw(['reset', `--${mode}`, hash]));
|
|
968
|
+
return mutationResult(resp, []);
|
|
822
969
|
}
|
|
823
970
|
catch (err) {
|
|
824
971
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -899,9 +1046,36 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
899
1046
|
});
|
|
900
1047
|
app.post('/api/branch/create', async (req, resp) => {
|
|
901
1048
|
try {
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
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);
|
|
905
1079
|
}
|
|
906
1080
|
catch (err) {
|
|
907
1081
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -909,9 +1083,29 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
909
1083
|
});
|
|
910
1084
|
app.post('/api/branch/delete', async (req, resp) => {
|
|
911
1085
|
try {
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
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);
|
|
915
1109
|
}
|
|
916
1110
|
catch (err) {
|
|
917
1111
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -919,9 +1113,13 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
919
1113
|
});
|
|
920
1114
|
app.post('/api/branch/delete-remote', async (req, resp) => {
|
|
921
1115
|
try {
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
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, []);
|
|
925
1123
|
}
|
|
926
1124
|
catch (err) {
|
|
927
1125
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -929,9 +1127,41 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
929
1127
|
});
|
|
930
1128
|
app.post('/api/branch/rename', async (req, resp) => {
|
|
931
1129
|
try {
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
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);
|
|
935
1165
|
}
|
|
936
1166
|
catch (err) {
|
|
937
1167
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1033,9 +1263,44 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1033
1263
|
});
|
|
1034
1264
|
app.post('/api/checkout', async (req, resp) => {
|
|
1035
1265
|
try {
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
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);
|
|
1039
1304
|
}
|
|
1040
1305
|
catch (err) {
|
|
1041
1306
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1044,9 +1309,28 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1044
1309
|
// ==================== Merge & Rebase ====================
|
|
1045
1310
|
app.post('/api/merge', async (req, resp) => {
|
|
1046
1311
|
try {
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
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, []);
|
|
1050
1334
|
}
|
|
1051
1335
|
catch (err) {
|
|
1052
1336
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1074,9 +1358,18 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1074
1358
|
});
|
|
1075
1359
|
app.post('/api/rebase', async (req, resp) => {
|
|
1076
1360
|
try {
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
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, []);
|
|
1080
1373
|
}
|
|
1081
1374
|
catch (err) {
|
|
1082
1375
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1125,23 +1418,24 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1125
1418
|
});
|
|
1126
1419
|
app.post('/api/stash/save', async (req, resp) => {
|
|
1127
1420
|
try {
|
|
1128
|
-
const
|
|
1421
|
+
const message = strField(req.body?.message);
|
|
1422
|
+
const scope = ['staged', 'unstaged', 'all'].includes(req.body?.scope) ? req.body.scope : 'all';
|
|
1129
1423
|
// 'staged' → index only (--staged, git ≥ 2.35); 'unstaged' → working tree only
|
|
1130
1424
|
// (--keep-index leaves staged changes staged); 'all' → index + working tree.
|
|
1131
|
-
// 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';
|
|
1132
1428
|
const args = ['push'];
|
|
1133
|
-
if (scope === 'staged')
|
|
1429
|
+
if (scope === 'staged')
|
|
1134
1430
|
args.push('--staged');
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
args.push('--keep-index');
|
|
1431
|
+
if (scope === 'unstaged')
|
|
1432
|
+
args.push('--keep-index');
|
|
1433
|
+
if (includeUntracked)
|
|
1139
1434
|
args.push('-u');
|
|
1140
|
-
}
|
|
1141
1435
|
if (message)
|
|
1142
1436
|
args.push('-m', message);
|
|
1143
|
-
await git.stash(args);
|
|
1144
|
-
return resp
|
|
1437
|
+
await mutate(() => git.stash(args));
|
|
1438
|
+
return mutationResult(resp, []);
|
|
1145
1439
|
}
|
|
1146
1440
|
catch (err) {
|
|
1147
1441
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1149,9 +1443,13 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1149
1443
|
});
|
|
1150
1444
|
app.post('/api/stash/apply', async (req, resp) => {
|
|
1151
1445
|
try {
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
|
|
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, []);
|
|
1155
1453
|
}
|
|
1156
1454
|
catch (err) {
|
|
1157
1455
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1159,9 +1457,12 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1159
1457
|
});
|
|
1160
1458
|
app.post('/api/stash/pop', async (req, resp) => {
|
|
1161
1459
|
try {
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
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, []);
|
|
1165
1466
|
}
|
|
1166
1467
|
catch (err) {
|
|
1167
1468
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1170,8 +1471,8 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1170
1471
|
app.post('/api/stash/drop', async (req, resp) => {
|
|
1171
1472
|
try {
|
|
1172
1473
|
const { index } = req.body;
|
|
1173
|
-
await git.stash(['drop', `stash@{${index}}`]);
|
|
1174
|
-
return resp
|
|
1474
|
+
await mutate(() => git.stash(['drop', `stash@{${index}}`]));
|
|
1475
|
+
return mutationResult(resp, []);
|
|
1175
1476
|
}
|
|
1176
1477
|
catch (err) {
|
|
1177
1478
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1211,14 +1512,41 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1211
1512
|
});
|
|
1212
1513
|
app.post('/api/tag/create', async (req, resp) => {
|
|
1213
1514
|
try {
|
|
1214
|
-
const
|
|
1215
|
-
if (
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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);
|
|
1222
1550
|
}
|
|
1223
1551
|
catch (err) {
|
|
1224
1552
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1226,9 +1554,28 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1226
1554
|
});
|
|
1227
1555
|
app.post('/api/tag/delete', async (req, resp) => {
|
|
1228
1556
|
try {
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
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);
|
|
1232
1579
|
}
|
|
1233
1580
|
catch (err) {
|
|
1234
1581
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1236,12 +1583,12 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1236
1583
|
});
|
|
1237
1584
|
app.post('/api/tag/push', async (req, resp) => {
|
|
1238
1585
|
try {
|
|
1239
|
-
const
|
|
1240
|
-
if (!name)
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
await git.push(remote
|
|
1244
|
-
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, []);
|
|
1245
1592
|
}
|
|
1246
1593
|
catch (err) {
|
|
1247
1594
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1260,9 +1607,20 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1260
1607
|
});
|
|
1261
1608
|
app.post('/api/pull', async (req, resp) => {
|
|
1262
1609
|
try {
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
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, []);
|
|
1266
1624
|
}
|
|
1267
1625
|
catch (err) {
|
|
1268
1626
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -1448,6 +1806,9 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1448
1806
|
if (body.refListView === 'tree' || body.refListView === 'flat') {
|
|
1449
1807
|
settings.refListView = body.refListView;
|
|
1450
1808
|
}
|
|
1809
|
+
if (typeof body.sidePanelSectionsExpanded === 'boolean') {
|
|
1810
|
+
settings.sidePanelSectionsExpanded = body.sidePanelSectionsExpanded;
|
|
1811
|
+
}
|
|
1451
1812
|
if (body.searchMode === 'filter' || body.searchMode === 'navigate') {
|
|
1452
1813
|
settings.searchMode = body.searchMode;
|
|
1453
1814
|
}
|
|
@@ -2849,8 +3210,8 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
2849
3210
|
// ==================== Working Tree Actions ====================
|
|
2850
3211
|
app.post('/api/reset', async (_req, resp) => {
|
|
2851
3212
|
try {
|
|
2852
|
-
await git.raw(['reset', '--hard', 'HEAD']);
|
|
2853
|
-
return resp
|
|
3213
|
+
await mutate(() => git.raw(['reset', '--hard', 'HEAD']));
|
|
3214
|
+
return mutationResult(resp, []);
|
|
2854
3215
|
}
|
|
2855
3216
|
catch (err) {
|
|
2856
3217
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -2858,8 +3219,8 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
2858
3219
|
});
|
|
2859
3220
|
app.post('/api/clean', async (_req, resp) => {
|
|
2860
3221
|
try {
|
|
2861
|
-
await git.raw(['clean', '-fd']);
|
|
2862
|
-
return resp
|
|
3222
|
+
await mutate(() => git.raw(['clean', '-fd']));
|
|
3223
|
+
return mutationResult(resp, []);
|
|
2863
3224
|
}
|
|
2864
3225
|
catch (err) {
|
|
2865
3226
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
@@ -2877,6 +3238,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
2877
3238
|
showGraph = settings.showGraph;
|
|
2878
3239
|
showStashes = settings.showStashes;
|
|
2879
3240
|
fileListView = settings.fileListView;
|
|
3241
|
+
sidePanelSectionsExpanded = settings.sidePanelSectionsExpanded;
|
|
2880
3242
|
searchMode = settings.searchMode;
|
|
2881
3243
|
searchCaseSensitive = settings.searchCaseSensitive;
|
|
2882
3244
|
showTags = settings.showTags;
|