mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Add branch management functionality for extensions
This commit is contained in:
@ -661,6 +661,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
|
|||||||
let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" data-i18n="[title]Delete" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';
|
let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" data-i18n="[title]Delete" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';
|
||||||
let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : '';
|
let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : '';
|
||||||
let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" data-i18n="[title]Move" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';
|
let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" data-i18n="[title]Move" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';
|
||||||
|
let branchButton = isExternal && isUserAdmin ? `<button class="btn_branch menu_button" data-name="${externalId}" data-i18n="[title]Switch branch" title="Switch branch"><i class="fa-solid fa-code-branch fa-fw"></i></button>` : '';
|
||||||
let modulesInfo = '';
|
let modulesInfo = '';
|
||||||
|
|
||||||
if (isActive && Array.isArray(manifest.optional)) {
|
if (isActive && Array.isArray(manifest.optional)) {
|
||||||
@ -701,6 +702,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
|
|||||||
|
|
||||||
<div class="extension_actions flex-container alignItemsCenter">
|
<div class="extension_actions flex-container alignItemsCenter">
|
||||||
${updateButton}
|
${updateButton}
|
||||||
|
${branchButton}
|
||||||
${moveButton}
|
${moveButton}
|
||||||
${deleteButton}
|
${deleteButton}
|
||||||
</div>
|
</div>
|
||||||
@ -944,6 +946,44 @@ async function onDeleteClick() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onBranchClick() {
|
||||||
|
const extensionName = $(this).data('name');
|
||||||
|
const isCurrentUserAdmin = isAdmin();
|
||||||
|
const isGlobal = getExtensionType(extensionName) === 'global';
|
||||||
|
if (isGlobal && !isCurrentUserAdmin) {
|
||||||
|
toastr.error(t`You don't have permission to switch branch.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newBranch = '';
|
||||||
|
|
||||||
|
const branches = await getExtensionBranches(extensionName, isGlobal);
|
||||||
|
const selectElement = document.createElement('select');
|
||||||
|
selectElement.classList.add('text_pole', 'wide100p');
|
||||||
|
selectElement.addEventListener('change', function () {
|
||||||
|
newBranch = this.value;
|
||||||
|
});
|
||||||
|
for (const branch of branches) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = branch.name;
|
||||||
|
option.textContent = `${branch.name} (${branch.commit}) [${branch.label}]`;
|
||||||
|
option.selected = branch.current;
|
||||||
|
selectElement.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
const popup = new Popup(selectElement, POPUP_TYPE.CONFIRM, '', {
|
||||||
|
okButton: t`Switch`,
|
||||||
|
cancelButton: t`Cancel`,
|
||||||
|
});
|
||||||
|
const popupResult = await popup.show();
|
||||||
|
|
||||||
|
if (!popupResult || !newBranch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await switchExtensionBranch(extensionName, isGlobal, newBranch);
|
||||||
|
}
|
||||||
|
|
||||||
async function onMoveClick() {
|
async function onMoveClick() {
|
||||||
const extensionName = $(this).data('name');
|
const extensionName = $(this).data('name');
|
||||||
const isCurrentUserAdmin = isAdmin();
|
const isCurrentUserAdmin = isAdmin();
|
||||||
@ -1055,6 +1095,76 @@ async function getExtensionVersion(extensionName, abortSignal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the list of branches for a specific extension.
|
||||||
|
* @param {string} extensionName The name of the extension
|
||||||
|
* @param {boolean} isGlobal Whether the extension is global or not
|
||||||
|
* @returns {Promise<ExtensionBranch[]>} List of branches for the extension
|
||||||
|
* @typedef {object} ExtensionBranch
|
||||||
|
* @property {string} name The name of the branch
|
||||||
|
* @property {string} commit The commit hash of the branch
|
||||||
|
* @property {boolean} current Whether this branch is the current one
|
||||||
|
* @property {string} label The commit label of the branch
|
||||||
|
*/
|
||||||
|
async function getExtensionBranches(extensionName, isGlobal) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/extensions/branches', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
extensionName,
|
||||||
|
global: isGlobal,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
toastr.error(text || response.statusText, t`Extension branches fetch failed`);
|
||||||
|
console.error('Extension branches fetch failed', response.status, response.statusText, text);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches the branch of an extension.
|
||||||
|
* @param {string} extensionName The name of the extension
|
||||||
|
* @param {boolean} isGlobal If the extension is global
|
||||||
|
* @param {string} branch Branch name to switch to
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function switchExtensionBranch(extensionName, isGlobal, branch) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/extensions/switch', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
extensionName,
|
||||||
|
branch,
|
||||||
|
global: isGlobal,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
toastr.error(text || response.statusText, t`Extension branch switch failed`);
|
||||||
|
console.error('Extension branch switch failed', response.status, response.statusText, text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toastr.success(t`Extension ${extensionName} switched to ${branch}`);
|
||||||
|
await loadExtensionSettings({}, false, false);
|
||||||
|
void showExtensionsDetails();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs a third-party extension via the API.
|
* Installs a third-party extension via the API.
|
||||||
* @param {string} url Extension repository URL
|
* @param {string} url Extension repository URL
|
||||||
@ -1443,6 +1553,7 @@ export async function initExtensions() {
|
|||||||
$(document).on('click', '.extensions_info .extension_block .btn_update', onUpdateClick);
|
$(document).on('click', '.extensions_info .extension_block .btn_update', onUpdateClick);
|
||||||
$(document).on('click', '.extensions_info .extension_block .btn_delete', onDeleteClick);
|
$(document).on('click', '.extensions_info .extension_block .btn_delete', onDeleteClick);
|
||||||
$(document).on('click', '.extensions_info .extension_block .btn_move', onMoveClick);
|
$(document).on('click', '.extensions_info .extension_block .btn_move', onMoveClick);
|
||||||
|
$(document).on('click', '.extensions_info .extension_block .btn_branch', onBranchClick);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the click event for the third-party extension import button.
|
* Handles the click event for the third-party extension import button.
|
||||||
|
@ -158,6 +158,113 @@ router.post('/update', async (request, response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/branches', async (request, response) => {
|
||||||
|
try {
|
||||||
|
const git = simpleGit();
|
||||||
|
|
||||||
|
const { extensionName, global } = request.body;
|
||||||
|
|
||||||
|
if (!extensionName) {
|
||||||
|
return response.status(400).send('Bad Request: extensionName is required in the request body.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global && !request.user.profile.admin) {
|
||||||
|
console.error(`User ${request.user.profile.handle} does not have permission to update global extensions.`);
|
||||||
|
return response.status(403).send('Forbidden: No permission to update global extensions.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
|
||||||
|
const extensionPath = path.join(basePath, extensionName);
|
||||||
|
|
||||||
|
if (!fs.existsSync(extensionPath)) {
|
||||||
|
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unshallow the repository if it is shallow
|
||||||
|
const isShallow = await git.cwd(extensionPath).revparse(['--is-shallow-repository']) === 'true';
|
||||||
|
if (isShallow) {
|
||||||
|
console.info(`Unshallowing the repository at ${extensionPath}`);
|
||||||
|
await git.cwd(extensionPath).fetch('origin', ['--unshallow']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all branches
|
||||||
|
await git.cwd(extensionPath).remote(['set-branches', 'origin', '*']);
|
||||||
|
await git.cwd(extensionPath).fetch('origin');
|
||||||
|
const localBranches = await git.cwd(extensionPath).branchLocal();
|
||||||
|
const remoteBranches = await git.cwd(extensionPath).branch(['-r', '--list', 'origin/*']);
|
||||||
|
const result = [
|
||||||
|
...Object.values(localBranches.branches),
|
||||||
|
...Object.values(remoteBranches.branches),
|
||||||
|
].map(b => ({ current: b.current, commit: b.commit, name: b.name, label: b.label }));
|
||||||
|
|
||||||
|
return response.send(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Getting branches failed', error);
|
||||||
|
return response.status(500).send('Internal Server Error. Try again later.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/switch', async (request, response) => {
|
||||||
|
try {
|
||||||
|
const git = simpleGit();
|
||||||
|
|
||||||
|
const { extensionName, branch, global } = request.body;
|
||||||
|
|
||||||
|
if (!extensionName || !branch) {
|
||||||
|
return response.status(400).send('Bad Request: extensionName and branch are required in the request body.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global && !request.user.profile.admin) {
|
||||||
|
console.error(`User ${request.user.profile.handle} does not have permission to update global extensions.`);
|
||||||
|
return response.status(403).send('Forbidden: No permission to update global extensions.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
|
||||||
|
const extensionPath = path.join(basePath, extensionName);
|
||||||
|
|
||||||
|
if (!fs.existsSync(extensionPath)) {
|
||||||
|
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const branches = await git.cwd(extensionPath).branchLocal();
|
||||||
|
|
||||||
|
if (String(branch).startsWith('origin/')) {
|
||||||
|
const localBranch = branch.replace('origin/', '');
|
||||||
|
if (branches.all.includes(localBranch)) {
|
||||||
|
console.info(`Branch ${localBranch} already exists locally, checking it out`);
|
||||||
|
await git.cwd(extensionPath).checkout(localBranch);
|
||||||
|
return response.sendStatus(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(`Branch ${localBranch} does not exist locally, creating it from ${branch}`);
|
||||||
|
await git.cwd(extensionPath).checkoutBranch(localBranch, branch);
|
||||||
|
return response.sendStatus(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!branches.all.includes(branch)) {
|
||||||
|
console.error(`Branch ${branch} does not exist locally`);
|
||||||
|
return response.status(404).send(`Branch ${branch} does not exist locally`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the branch is already checked out
|
||||||
|
const currentBranch = await git.cwd(extensionPath).branch();
|
||||||
|
if (currentBranch.current === branch) {
|
||||||
|
console.info(`Branch ${branch} is already checked out`);
|
||||||
|
return response.sendStatus(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkout the branch
|
||||||
|
await git.cwd(extensionPath).checkout(branch);
|
||||||
|
console.info(`Checked out branch ${branch} at ${extensionPath}`);
|
||||||
|
|
||||||
|
return response.sendStatus(204);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Switching branches failed', error);
|
||||||
|
return response.status(500).send('Internal Server Error. Check the server logs for more details.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.post('/move', async (request, response) => {
|
router.post('/move', async (request, response) => {
|
||||||
try {
|
try {
|
||||||
const { extensionName, source, destination } = request.body;
|
const { extensionName, source, destination } = request.body;
|
||||||
|
Reference in New Issue
Block a user