Skip to content

Commit

Permalink
Adds and updates footer in stacked pull request descriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
mtsgrd committed Nov 2, 2024
1 parent e2042d4 commit 204d241
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 1 deletion.
23 changes: 23 additions & 0 deletions apps/desktop/src/lib/branch/BranchLaneContextMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
import { getForgePrService } from '$lib/forge/interface/forgePrService';
import { updatePrDescriptionTables } from '$lib/forge/shared/stackFooter';
import { User } from '$lib/stores/user';
import { BranchController } from '$lib/vbranches/branchController';
import { VirtualBranch } from '$lib/vbranches/types';
import { getContext, getContextStore } from '@gitbutler/shared/context';
import Button from '@gitbutler/ui/Button.svelte';
import Modal from '@gitbutler/ui/Modal.svelte';
import Toggle from '@gitbutler/ui/Toggle.svelte';
import Tooltip from '@gitbutler/ui/Tooltip.svelte';
import { isDefined } from '@gitbutler/ui/utils/typeguards';
interface Props {
prUrl?: string;
Expand All @@ -26,6 +30,8 @@
const branchStore = getContextStore(VirtualBranch);
const branchController = getContext(BranchController);
const prService = getForgePrService();
const user = getContextStore(User);
let deleteBranchModal: Modal;
let allowRebasing = $state<boolean>();
Expand All @@ -37,6 +43,8 @@
allowRebasing = branch.allowRebasing;
});
const allPrIds = $derived(branch.series.map((series) => series.forgeId).filter(isDefined));
async function toggleAllowRebasing() {
branchController.updateBranchAllowRebasing(branch.id, !allowRebasing);
}
Expand Down Expand Up @@ -113,6 +121,21 @@
}}
/>
</ContextMenuSection>
{#if $user && $user.role?.includes('admin')}
<ContextMenuSection label="admin only">
<ContextMenuItem
label="Update PR footers"
disabled={allPrIds.length === 0}
onclick={() => {
if ($prService && branch) {
const allPrIds = branch.series.map((series) => series.forgeId).filter(isDefined);
updatePrDescriptionTables($prService, allPrIds);
}
contextMenuEl?.close();
}}
/>
</ContextMenuSection>
{/if}
</ContextMenu>

<Modal
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
<script lang="ts">
import type { Snippet } from 'svelte';
const { label, children }: { label?: string; children: Snippet } = $props();
</script>

<div class="context-menu-section">
<slot />
{#if label}
<div class="label text-12">{label}</div>
{/if}
{@render children()}
</div>

<style lang="postcss">
Expand All @@ -17,4 +23,10 @@
border-top: 1px solid var(--clr-border-2);
}
}
.label {
padding: 6px 8px;
color: var(--clr-scale-ntrl-50);
user-select: none;
}
</style>
11 changes: 11 additions & 0 deletions apps/desktop/src/lib/forge/github/githubPrService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ export class GitHubPrService implements ForgePrService {
if (!isGitHubPr(id)) throw INVALID_PR_TYPE;
return new GitHubPrMonitor(this, id);
}

async update(id: PullRequestId, details: { description?: string }) {
if (!isGitHubPr(id)) throw INVALID_PR_TYPE;
const { description } = details;
await this.octokit.pulls.update({
owner: this.repo.owner,
repo: this.repo.name,
pull_number: id.subject.prNumber,
body: description
});
}
}

function isGitHubPr(
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/lib/forge/interface/forgePrService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export interface ForgePrService {
merge(method: MergeMethod, id: PullRequestId): Promise<void>;
reopen(id: PullRequestId): Promise<void>;
prMonitor(id: PullRequestId): ForgePrMonitor;
update(id: PullRequestId, details: { description?: string }): Promise<void>;
}
56 changes: 56 additions & 0 deletions apps/desktop/src/lib/forge/shared/stackFooter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { equalPrId } from './pullRequestId';
import {
ForgeName,
type DetailedPullRequest,
type PullRequestId
} from '$lib/forge/interface/types';
import type { ForgePrService } from '../interface/forgePrService';

export const GITBUTLER_FOOTER_BOUNDARY = '<!-- GitButler Footer Boundary -->';

export async function updatePrDescriptionTables(prService: ForgePrService, prIds: PullRequestId[]) {
if (prService && prIds.length > 1) {
const prs = await Promise.all(prIds.map(async (prId) => await prService.get(prId)));
await Promise.all(
prIds.map(async (prId) => {
const pr = prs.find((p) => equalPrId(p.id, prId)) as DetailedPullRequest;
const currentDescription = pr.body ? stripFooter(pr.body.trim()) : '';
await prService.update(pr.id, {
description: currentDescription + '\n' + generateFooter(prId, prs)
});
})
);
}
}

/**
* Generates a footer for use in pull request descriptions when part of a stack.
*/
export function generateFooter(id: PullRequestId, all: DetailedPullRequest[]) {
const stackIndex = all.findIndex((pr) => equalPrId(pr.id, id));
let footer = '';
footer += GITBUTLER_FOOTER_BOUNDARY + '\n\n';
footer += `| # | PR |\n`;
footer += '| --- | --- |\n';
all.forEach((pr, i) => {
if (pr.id.type !== ForgeName.GitHub) {
throw `Unsupported Forge: ${pr.id.type}`;
}
const current = i === stackIndex;
const rankNumber = all.length - i;
const rankStr = current ? bold(rankNumber) : rankNumber;
const prNumber = `#${pr.id.subject.prNumber}`;
const prStr = current ? bold(prNumber) : prNumber;
footer += `| ${rankStr} | ${prStr} |\n`;
});
return footer;
}

function stripFooter(description: string) {
console.log(description.split(GITBUTLER_FOOTER_BOUNDARY));
return description.split(GITBUTLER_FOOTER_BOUNDARY)[0];
}

function bold(text: string | number) {
return `**${text}**`;
}
13 changes: 13 additions & 0 deletions apps/desktop/src/lib/pr/PrDetailsModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import { getForge } from '$lib/forge/interface/forge';
import { getForgePrService } from '$lib/forge/interface/forgePrService';
import { type DetailedPullRequest, type PullRequest } from '$lib/forge/interface/types';
import { updatePrDescriptionTables } from '$lib/forge/shared/stackFooter';
import { showError, showToast } from '$lib/notifications/toasts';
import { isFailure } from '$lib/result';
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
Expand All @@ -40,6 +41,7 @@
import Textarea from '@gitbutler/ui/Textarea.svelte';
import Textbox from '@gitbutler/ui/Textbox.svelte';
import ToggleButton from '@gitbutler/ui/ToggleButton.svelte';
import { isDefined } from '@gitbutler/ui/utils/typeguards';
import { tick } from 'svelte';
interface BaseProps {
Expand Down Expand Up @@ -165,6 +167,9 @@
error('Pull request service not available');
return;
}
if (props.type !== 'preview-series') {
return;
}
isLoading = true;
try {
Expand Down Expand Up @@ -203,6 +208,9 @@
return;
}
// All ids that exists prior to creating a new one.
const priorIds = branch.series.map((series) => series.forgeId).filter(isDefined);
const pr = await $prService.createPr({
title: params.title,
body: params.body,
Expand All @@ -213,6 +221,11 @@
if (props.type === 'preview-series') {
await branchController.updateSeriesForgeId(props.stackId, props.currentSeries.name, pr.id);
}
// If we now have two or more pull requests we add a stack table to the description.
if (priorIds.length > 0) {
updatePrDescriptionTables($prService, priorIds.concat([pr.id]));
}
} catch (err: any) {
console.error(err);
const toast = mapErrorToToast(err);
Expand Down

0 comments on commit 204d241

Please sign in to comment.