Skip to content

Commit 21b7863

Browse files
committed
Add stack footer
1 parent 76d4313 commit 21b7863

File tree

6 files changed

+104
-2
lines changed

6 files changed

+104
-2
lines changed

apps/desktop/src/lib/branch/BranchLaneContextMenu.svelte

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
33
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
44
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
5+
import { getForgePrService } from '$lib/forge/interface/forgePrService';
6+
import { updatePrDescriptionTables } from '$lib/forge/shared/stackFooter';
7+
import { User } from '$lib/stores/user';
58
import { BranchController } from '$lib/vbranches/branchController';
69
import { VirtualBranch } from '$lib/vbranches/types';
710
import { getContext, getContextStore } from '@gitbutler/shared/context';
811
import Button from '@gitbutler/ui/Button.svelte';
912
import Modal from '@gitbutler/ui/Modal.svelte';
1013
import Toggle from '@gitbutler/ui/Toggle.svelte';
1114
import Tooltip from '@gitbutler/ui/Tooltip.svelte';
15+
import { isDefined } from '@gitbutler/ui/utils/typeguards';
1216
1317
interface Props {
1418
prUrl?: string;
@@ -26,6 +30,8 @@
2630
2731
const branchStore = getContextStore(VirtualBranch);
2832
const branchController = getContext(BranchController);
33+
const prService = getForgePrService();
34+
const user = getContextStore(User);
2935
3036
let deleteBranchModal: Modal;
3137
let allowRebasing = $state<boolean>();
@@ -37,6 +43,8 @@
3743
allowRebasing = branch.allowRebasing;
3844
});
3945
46+
const allPrIds = $derived(branch.series.map((series) => series.forgeId).filter(isDefined));
47+
4048
async function toggleAllowRebasing() {
4149
branchController.updateBranchAllowRebasing(branch.id, !allowRebasing);
4250
}
@@ -113,6 +121,21 @@
113121
}}
114122
/>
115123
</ContextMenuSection>
124+
{#if $user && $user.role?.includes('admin')}
125+
<ContextMenuSection label="admin only">
126+
<ContextMenuItem
127+
label="Update PR footers"
128+
disabled={allPrIds.length === 0}
129+
onclick={() => {
130+
if ($prService && branch) {
131+
const allPrIds = branch.series.map((series) => series.forgeId).filter(isDefined);
132+
updatePrDescriptionTables($prService, allPrIds);
133+
}
134+
contextMenuEl?.close();
135+
}}
136+
/>
137+
</ContextMenuSection>
138+
{/if}
116139
</ContextMenu>
117140

118141
<Modal

apps/desktop/src/lib/components/contextmenu/ContextMenuSection.svelte

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
<script lang="ts">
2+
import type { Snippet } from 'svelte';
3+
4+
const { label, children }: { label?: string; children: Snippet } = $props();
25
</script>
36

47
<div class="context-menu-section">
5-
<slot />
8+
{#if label}
9+
<div class="label text-12">{label}</div>
10+
{/if}
11+
{@render children()}
612
</div>
713

814
<style lang="postcss">
@@ -17,4 +23,10 @@
1723
border-top: 1px solid var(--clr-border-2);
1824
}
1925
}
26+
27+
.label {
28+
padding: 6px 8px;
29+
color: var(--clr-scale-ntrl-50);
30+
user-select: none;
31+
}
2032
</style>

apps/desktop/src/lib/forge/github/githubPrService.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,14 @@ export class GitHubPrService implements ForgePrService {
9696
prMonitor(prNumber: number): GitHubPrMonitor {
9797
return new GitHubPrMonitor(this, prNumber);
9898
}
99+
100+
async update(prNumber: number, details: { description?: string }) {
101+
const { description } = details;
102+
await this.octokit.pulls.update({
103+
owner: this.repo.owner,
104+
repo: this.repo.name,
105+
pull_number: prNumber,
106+
body: description
107+
});
108+
}
99109
}

apps/desktop/src/lib/forge/interface/forgePrService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export interface ForgePrService {
2020
merge(method: MergeMethod, prNumber: number): Promise<void>;
2121
reopen(prNumber: number): Promise<void>;
2222
prMonitor(prNumber: number): ForgePrMonitor;
23+
update(prNumber: number, details: { description?: string }): Promise<void>;
2324
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { type DetailedPullRequest } from '$lib/forge/interface/types';
2+
import type { ForgePrService } from '../interface/forgePrService';
3+
4+
export const GITBUTLER_FOOTER_BOUNDARY = '<!-- GitButler Footer Boundary -->';
5+
6+
export async function updatePrDescriptionTables(prService: ForgePrService, prIds: number[]) {
7+
if (prService && prIds.length > 1) {
8+
const prs = await Promise.all(prIds.map(async (prId) => await prService.get(prId)));
9+
await Promise.all(
10+
prIds.map(async (prId) => {
11+
const pr = prs.find((p) => p.number === prId) as DetailedPullRequest;
12+
const currentDescription = pr.body ? stripFooter(pr.body.trim()) : '';
13+
await prService.update(pr.id, {
14+
description: currentDescription + '\n' + generateFooter(prId, prs)
15+
});
16+
})
17+
);
18+
}
19+
}
20+
21+
/**
22+
* Generates a footer for use in pull request descriptions when part of a stack.
23+
*/
24+
export function generateFooter(id: number, all: DetailedPullRequest[]) {
25+
const stackIndex = all.findIndex((pr) => pr.id === id);
26+
let footer = '';
27+
footer += GITBUTLER_FOOTER_BOUNDARY + '\n\n';
28+
footer += `| # | PR |\n`;
29+
footer += '| --- | --- |\n';
30+
all.forEach((pr, i) => {
31+
const current = i === stackIndex;
32+
const rankNumber = all.length - i;
33+
const rankStr = current ? bold(rankNumber) : rankNumber;
34+
const prNumber = `#${pr.number}`;
35+
const prStr = current ? bold(prNumber) : prNumber;
36+
footer += `| ${rankStr} | ${prStr} |\n`;
37+
});
38+
return footer;
39+
}
40+
41+
function stripFooter(description: string) {
42+
console.log(description.split(GITBUTLER_FOOTER_BOUNDARY));
43+
return description.split(GITBUTLER_FOOTER_BOUNDARY)[0];
44+
}
45+
46+
function bold(text: string | number) {
47+
return `**${text}**`;
48+
}

apps/desktop/src/lib/pr/PrDetailsModal.svelte

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import { mapErrorToToast } from '$lib/forge/github/errorMap';
2222
import { getForge } from '$lib/forge/interface/forge';
2323
import { getForgePrService } from '$lib/forge/interface/forgePrService';
24+
import { type DetailedPullRequest, type PullRequest } from '$lib/forge/interface/types';
25+
import { updatePrDescriptionTables } from '$lib/forge/shared/stackFooter';
2426
import { showError, showToast } from '$lib/notifications/toasts';
2527
import { isFailure } from '$lib/result';
2628
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
@@ -39,8 +41,8 @@
3941
import Textarea from '@gitbutler/ui/Textarea.svelte';
4042
import Textbox from '@gitbutler/ui/Textbox.svelte';
4143
import ToggleButton from '@gitbutler/ui/ToggleButton.svelte';
44+
import { isDefined } from '@gitbutler/ui/utils/typeguards';
4245
import { tick } from 'svelte';
43-
import type { DetailedPullRequest, PullRequest } from '$lib/forge/interface/types';
4446
4547
interface BaseProps {
4648
type: 'display' | 'preview' | 'preview-series';
@@ -165,6 +167,9 @@
165167
error('Pull request service not available');
166168
return;
167169
}
170+
if (props.type !== 'preview-series') {
171+
return;
172+
}
168173
169174
isLoading = true;
170175
try {
@@ -203,6 +208,9 @@
203208
return;
204209
}
205210
211+
// All ids that exists prior to creating a new one.
212+
const priorIds = branch.series.map((series) => series.forgeId).filter(isDefined);
213+
206214
const pr = await $prService.createPr({
207215
title: params.title,
208216
body: params.body,

0 commit comments

Comments
 (0)