feat: delete merged source branches safely (Closes #1364)
This commit is contained in:
1 parent
f3876d359d
commit
07f5770aa8
9 files changed
+540
-7
No files matched your search
@@ -35,7 +35,14 @@ header { position: sticky; top: 0; z-index: 20; padding: 12px 16px; display:flex
|
||||
.release-watchlist-item { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:10px; padding:10px; border:1px solid #334155; border-radius:10px; }
|
||||
.release-watchlist-item > div { display:grid; min-width:0; gap:3px; overflow-wrap:anywhere; }
|
||||
.release-watchlist-item > button { width:auto; min-width:88px; }
|
||||
@media (max-width:359px) { .release-receipt-actions { grid-template-columns:1fr; } }
|
||||
.release-watchlist-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; min-width:min(240px,46vw); }
|
||||
.release-watchlist-actions > button { min-height:44px; width:100%; }
|
||||
.release-branch-cleanup-status { color:#cbd5e1; }
|
||||
@media (max-width:359px) {
|
||||
.release-receipt-actions, .release-watchlist-actions { grid-template-columns:1fr; }
|
||||
.release-watchlist-item { grid-template-columns:1fr; }
|
||||
.release-watchlist-actions { min-width:0; width:100%; }
|
||||
}
|
||||
.issue-filing-receipt-panel { box-sizing:border-box; width:min(560px,100%); max-height:100dvh; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #4ade80; border-radius:18px 18px 0 0; background:#0b1526; overflow-wrap:anywhere; }
|
||||
.issue-filing-receipt-panel h2, .issue-filing-receipt-panel h3 { margin:.25rem 0; }
|
||||
.issue-filing-receipt-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:16px; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function createReleaseReceipt({ storage, getLogin, fetchJson, launcher = null, dialog = null, statusNode = null, checksNode = null, releaseNode = null, listNode = null, documentRef = null, windowRef = null, setTimer = setTimeout, clearTimer = clearTimeout, pollMs = 30000 }) {
|
||||
function createReleaseReceipt({ storage, getLogin, fetchJson, launcher = null, dialog = null, statusNode = null, checksNode = null, releaseNode = null, listNode = null, documentRef = null, windowRef = null, confirmAction = null, setTimer = setTimeout, clearTimer = clearTimeout, pollMs = 30000 }) {
|
||||
const prefix = 'stackchain.release-receipt.v1:';
|
||||
const limit = 12;
|
||||
let entries = [];
|
||||
@@ -62,6 +62,17 @@ function createReleaseReceipt({ storage, getLogin, fetchJson, launcher = null, d
|
||||
number: Number(item.number),
|
||||
key: item.key || item.repository + '#' + item.number,
|
||||
commit_sha: commitSha,
|
||||
...(
|
||||
mergeResult?.source_repository === item.repository
|
||||
&& mergeResult?.source_branch
|
||||
&& mergeResult?.source_head_sha
|
||||
? {
|
||||
source_branch: String(mergeResult.source_branch),
|
||||
source_head_sha: String(mergeResult.source_head_sha),
|
||||
cleanup: { state: 'available', message: 'Merged branch retained.' },
|
||||
}
|
||||
: {}
|
||||
),
|
||||
status: null,
|
||||
captured_at: new Date().toISOString(),
|
||||
};
|
||||
@@ -121,6 +132,14 @@ function createReleaseReceipt({ storage, getLogin, fetchJson, launcher = null, d
|
||||
state.className = 'small';
|
||||
state.textContent = entry.status?.label || 'Checking the exact merge commit…';
|
||||
copy.append(title, state);
|
||||
if (entry.source_branch && entry.source_head_sha) {
|
||||
const branch = document.createElement('span');
|
||||
branch.className = 'small release-branch-cleanup-status';
|
||||
branch.textContent = entry.cleanup?.state === 'deleted'
|
||||
? 'Branch ' + entry.source_branch + ' · deleted'
|
||||
: 'Branch ' + entry.source_branch + ' · ' + entry.source_head_sha.slice(0, 8);
|
||||
copy.append(branch);
|
||||
}
|
||||
if (entry.status?.release?.url) {
|
||||
const link = document.createElement('a');
|
||||
link.href = entry.status.release.url;
|
||||
@@ -132,7 +151,25 @@ function createReleaseReceipt({ storage, getLogin, fetchJson, launcher = null, d
|
||||
button.textContent = 'Dismiss';
|
||||
button.setAttribute('aria-label', 'Dismiss ' + entry.key + ' from release tracking');
|
||||
button.addEventListener('click', () => dismiss(entry.repository, entry.commit_sha));
|
||||
row.append(copy, button);
|
||||
if (entry.source_branch && entry.cleanup?.state !== 'deleted') {
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'release-watchlist-actions';
|
||||
const cleanup = document.createElement('button');
|
||||
cleanup.type = 'button';
|
||||
cleanup.textContent = entry.cleanup?.state === 'deleting' ? 'Deleting…' : 'Delete source branch';
|
||||
cleanup.disabled = entry.cleanup?.state === 'deleting' || entry.cleanup?.state === 'advanced';
|
||||
cleanup.setAttribute(
|
||||
'aria-label',
|
||||
'Delete merged source branch ' + entry.source_branch + ' at ' + entry.source_head_sha.slice(0, 8),
|
||||
);
|
||||
cleanup.addEventListener('click', () => deleteBranch(entry.repository, entry.commit_sha).catch(error => {
|
||||
if (statusNode) statusNode.textContent = String(error?.message || error);
|
||||
}));
|
||||
actions.append(cleanup, button);
|
||||
row.append(copy, actions);
|
||||
} else {
|
||||
row.append(copy, button);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
listNode.replaceChildren(...rows);
|
||||
@@ -168,6 +205,46 @@ function createReleaseReceipt({ storage, getLogin, fetchJson, launcher = null, d
|
||||
finally { refreshing = null; }
|
||||
}
|
||||
|
||||
async function deleteBranch(repository, commitSha) {
|
||||
const entry = entries.find(value => identity(value) === repository + '@' + commitSha);
|
||||
if (!entry?.source_branch || !entry?.source_head_sha || entry.cleanup?.state === 'deleted') {
|
||||
throw new Error('Source branch cleanup is unavailable.');
|
||||
}
|
||||
const approve = confirmAction || (windowRef?.confirm ? message => windowRef.confirm(message) : () => false);
|
||||
if (!approve('Delete ' + entry.source_branch + ' at ' + entry.source_head_sha.slice(0, 8) + '?')) return false;
|
||||
entry.cleanup = { state: 'deleting', message: 'Deleting source branch…' };
|
||||
persist();
|
||||
render();
|
||||
try {
|
||||
await fetchJson(
|
||||
'api/v1/repos/' + entry.repository.split('/').map(encodeURIComponent).join('/')
|
||||
+ '/pulls/' + encodeURIComponent(entry.number) + '/source-branch',
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({
|
||||
source_branch: entry.source_branch,
|
||||
expected_head_sha: entry.source_head_sha,
|
||||
}),
|
||||
},
|
||||
);
|
||||
entry.cleanup = { state: 'deleted', message: 'Source branch deleted.' };
|
||||
persist();
|
||||
render();
|
||||
return true;
|
||||
} catch (error) {
|
||||
entry.cleanup = {
|
||||
state: error?.status === 409 ? 'advanced' : 'available',
|
||||
message: error?.status === 409
|
||||
? 'Source branch has newer commits and was retained.'
|
||||
: 'Branch retained. Retry when connected.',
|
||||
};
|
||||
persist();
|
||||
render();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function dismiss(repository, commitSha) {
|
||||
if (repository && commitSha) entries = entries.filter(entry => identity(entry) !== repository + '@' + commitSha);
|
||||
else entries = [];
|
||||
@@ -195,7 +272,7 @@ function createReleaseReceipt({ storage, getLogin, fetchJson, launcher = null, d
|
||||
schedule();
|
||||
}
|
||||
|
||||
return { capture, restore, refresh, dismiss, bind, fetchJson };
|
||||
return { capture, restore, refresh, deleteBranch, dismiss, bind, fetchJson };
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = createReleaseReceipt;
|
||||
Reference in new issue
Block a user