mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-25 10:33:21 +03:00
Cleanup experimental stacking feature
- initial "new stacking branch" card - persist setting for show stacking details - separates out individual branch / pr header - stack files in separate folder - implements upstream commits accordion
This commit is contained in:
parent
d262b230a8
commit
8bdeb3f635
@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import BranchHeader from './BranchHeader.svelte';
|
||||
import StackedBranchHeader from './StackedBranchHeader.svelte';
|
||||
import EmptyStatePlaceholder from '../components/EmptyStatePlaceholder.svelte';
|
||||
import PullRequestCard from '../pr/PullRequestCard.svelte';
|
||||
import InfoMessage from '../shared/InfoMessage.svelte';
|
||||
@ -12,9 +11,7 @@
|
||||
import Dropzones from '$lib/branch/Dropzones.svelte';
|
||||
import CommitDialog from '$lib/commit/CommitDialog.svelte';
|
||||
import CommitList from '$lib/commit/CommitList.svelte';
|
||||
import StackingCommitList from '$lib/commit/StackingCommitList.svelte';
|
||||
import { projectAiGenEnabled } from '$lib/config/config';
|
||||
import { stackingFeature } from '$lib/config/uiFeatureFlags';
|
||||
import BranchFiles from '$lib/file/BranchFiles.svelte';
|
||||
import { getGitHostChecksMonitor } from '$lib/gitHost/interface/gitHostChecksMonitor';
|
||||
import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
|
||||
@ -28,7 +25,6 @@
|
||||
import { User } from '$lib/stores/user';
|
||||
import { getContext, getContextStore, getContextStoreBySymbol } from '$lib/utils/context';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { groupCommitsByRef } from '$lib/vbranches/commitGroups';
|
||||
import {
|
||||
getIntegratedCommits,
|
||||
getLocalAndRemoteCommits,
|
||||
@ -162,12 +158,12 @@
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<BranchHeader {isLaneCollapsed} onGenerateBranchName={generateBranchName} />
|
||||
{#if !$stackingFeature && branch.upstream?.givenName}
|
||||
{#if branch.upstream?.givenName}
|
||||
<PullRequestCard upstreamName={branch.upstream.givenName} />
|
||||
{/if}
|
||||
<div class:card-no-stacking={!$stackingFeature} class:card-stacking={$stackingFeature}>
|
||||
<div class="branch-card__files-wrapper">
|
||||
{#if branch.files?.length > 0}
|
||||
<div class="branch-card__files" class:card={$stackingFeature}>
|
||||
<div class="branch-card__files">
|
||||
<Dropzones>
|
||||
<BranchFiles
|
||||
isUnapplied={false}
|
||||
@ -202,7 +198,7 @@
|
||||
</div>
|
||||
{:else if branch.commits.length === 0}
|
||||
<Dropzones>
|
||||
<div class="new-branch" class:card={$stackingFeature}>
|
||||
<div class="new-branch">
|
||||
<EmptyStatePlaceholder image={laneNewSvg} width="11rem">
|
||||
<svelte:fragment slot="title">This is a new branch</svelte:fragment>
|
||||
<svelte:fragment slot="caption">
|
||||
@ -213,7 +209,7 @@
|
||||
</Dropzones>
|
||||
{:else}
|
||||
<Dropzones>
|
||||
<div class="no-changes" class:card={$stackingFeature}>
|
||||
<div class="no-changes">
|
||||
<EmptyStatePlaceholder image={noChangesSvg} width="11rem" hasBottomMargin={false}>
|
||||
<svelte:fragment slot="caption"
|
||||
>No uncommitted changes on this branch</svelte:fragment
|
||||
@ -238,45 +234,16 @@
|
||||
{branch.requiresForce ? 'Force push' : 'Push'}
|
||||
</Button>
|
||||
{/snippet}
|
||||
{#if $stackingFeature}
|
||||
{@const groups = groupCommitsByRef(branch.commits)}
|
||||
{#each groups as group (group.ref)}
|
||||
<div class="commit-group" class:stacking={$stackingFeature}>
|
||||
{#if group.branchName}
|
||||
<StackedBranchHeader upstreamName={group.branchName} />
|
||||
<PullRequestCard upstreamName={group.branchName} />
|
||||
{/if}
|
||||
<StackingCommitList
|
||||
localCommits={group.localCommits}
|
||||
localAndRemoteCommits={group.remoteCommits}
|
||||
integratedCommits={group.integratedCommits}
|
||||
remoteCommits={[]}
|
||||
isUnapplied={false}
|
||||
{localCommitsConflicted}
|
||||
{localAndRemoteCommitsConflicted}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{#if $integratedCommits.length === 0 && $localCommits.length > 0}
|
||||
{@render pushButton({
|
||||
disabled:
|
||||
localCommitsConflicted ||
|
||||
localAndRemoteCommitsConflicted ||
|
||||
$localCommits.length === 0
|
||||
})}
|
||||
{/if}
|
||||
{:else}
|
||||
<CommitList
|
||||
localCommits={$localCommits}
|
||||
localAndRemoteCommits={$localAndRemoteCommits}
|
||||
integratedCommits={$integratedCommits}
|
||||
remoteCommits={$remoteCommits}
|
||||
isUnapplied={false}
|
||||
{localCommitsConflicted}
|
||||
{localAndRemoteCommitsConflicted}
|
||||
{pushButton}
|
||||
/>
|
||||
{/if}
|
||||
<CommitList
|
||||
localCommits={$localCommits}
|
||||
localAndRemoteCommits={$localAndRemoteCommits}
|
||||
integratedCommits={$integratedCommits}
|
||||
remoteCommits={$remoteCommits}
|
||||
isUnapplied={false}
|
||||
{localCommitsConflicted}
|
||||
{localAndRemoteCommitsConflicted}
|
||||
{pushButton}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
@ -329,35 +296,6 @@
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.branch-card__files.card,
|
||||
.no-changes.card,
|
||||
.new-branch.card {
|
||||
border-radius: 0 0 var(--radius-m) var(--radius-m) !important;
|
||||
}
|
||||
|
||||
/* Stacking */
|
||||
.card-no-stacking {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--clr-border-2);
|
||||
border-radius: var(--radius-m);
|
||||
background: var(--clr-bg-1);
|
||||
}
|
||||
|
||||
.card-stacking {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.commit-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.branch-card__files {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -371,7 +309,7 @@
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.card-no-stacking {
|
||||
.branch-card__files-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -8,7 +8,6 @@
|
||||
import { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
|
||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||
import { stackingFeature } from '$lib/config/uiFeatureFlags';
|
||||
import { mapErrorToToast } from '$lib/gitHost/github/errorMap';
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
|
||||
@ -219,10 +218,9 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="header__wrapper" class:header__wrapper--stacking={$stackingFeature}>
|
||||
<div class="header__wrapper">
|
||||
<div
|
||||
class="header card"
|
||||
class:header_card--stacking={$stackingFeature}
|
||||
class:header_target-branch={branch.selectedForChanges}
|
||||
class:header_target-branch-animation={isTargetBranchAnimated && branch.selectedForChanges}
|
||||
>
|
||||
@ -231,101 +229,73 @@
|
||||
<Icon name="draggable" />
|
||||
</div>
|
||||
|
||||
<div class:header__info={!$stackingFeature} class:stacking-header__info={$stackingFeature}>
|
||||
<div class="header__info">
|
||||
<BranchLabel name={branch.name} onChange={(name) => handleBranchNameChange(name)} />
|
||||
{#if $stackingFeature}
|
||||
<span class="button-group">
|
||||
<DefaultTargetButton
|
||||
selectedForChanges={branch.selectedForChanges}
|
||||
onclick={async () => {
|
||||
isTargetBranchAnimated = true;
|
||||
await branchController.setSelectedForChanges(branch.id);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
bind:el={meatballButtonEl}
|
||||
style="ghost"
|
||||
icon="kebab"
|
||||
onclick={() => {
|
||||
contextMenu?.toggle();
|
||||
}}
|
||||
/>
|
||||
<BranchLaneContextMenu
|
||||
bind:contextMenuEl={contextMenu}
|
||||
target={meatballButtonEl}
|
||||
onCollapse={collapseLane}
|
||||
{onGenerateBranchName}
|
||||
/>
|
||||
</span>
|
||||
{:else}
|
||||
<div class="header__remote-branch">
|
||||
<ActiveBranchStatus
|
||||
{hasIntegratedCommits}
|
||||
remoteExists={!!branch.upstream}
|
||||
isLaneCollapsed={$isLaneCollapsed}
|
||||
/>
|
||||
<div class="header__remote-branch">
|
||||
<ActiveBranchStatus
|
||||
{hasIntegratedCommits}
|
||||
remoteExists={!!branch.upstream}
|
||||
isLaneCollapsed={$isLaneCollapsed}
|
||||
/>
|
||||
|
||||
{#await branch.isMergeable then isMergeable}
|
||||
{#if !isMergeable}
|
||||
<Button
|
||||
size="tag"
|
||||
clickable={false}
|
||||
icon="locked-small"
|
||||
style="warning"
|
||||
tooltip="Applying this branch will add merge conflict markers that you will have to resolve"
|
||||
>
|
||||
Conflict
|
||||
</Button>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
{#await branch.isMergeable then isMergeable}
|
||||
{#if !isMergeable}
|
||||
<Button
|
||||
size="tag"
|
||||
clickable={false}
|
||||
icon="locked-small"
|
||||
style="warning"
|
||||
tooltip="Applying this branch will add merge conflict markers that you will have to resolve"
|
||||
>
|
||||
Conflict
|
||||
</Button>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !$stackingFeature}
|
||||
<div class="header__actions">
|
||||
<div class="header__actions">
|
||||
<div class="header__buttons">
|
||||
<DefaultTargetButton
|
||||
selectedForChanges={branch.selectedForChanges}
|
||||
onclick={async () => {
|
||||
isTargetBranchAnimated = true;
|
||||
await branchController.setSelectedForChanges(branch.id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div class="header__buttons">
|
||||
<DefaultTargetButton
|
||||
selectedForChanges={branch.selectedForChanges}
|
||||
onclick={async () => {
|
||||
isTargetBranchAnimated = true;
|
||||
await branchController.setSelectedForChanges(branch.id);
|
||||
{#if !$pr}
|
||||
<PullRequestButton
|
||||
click={async ({ draft }) => await createPr({ draft })}
|
||||
disabled={branch.commits.length === 0 || !$gitHost || !$prService}
|
||||
tooltip={!$gitHost || !$prService
|
||||
? 'You can enable git host integration in the settings'
|
||||
: ''}
|
||||
loading={isLoading}
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
bind:el={meatballButtonEl}
|
||||
style="ghost"
|
||||
outline
|
||||
icon="kebab"
|
||||
onclick={() => {
|
||||
contextMenu?.toggle();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div class="header__buttons">
|
||||
{#if !$pr}
|
||||
<PullRequestButton
|
||||
click={async ({ draft }) => await createPr({ draft })}
|
||||
disabled={branch.commits.length === 0 || !$gitHost || !$prService}
|
||||
tooltip={!$gitHost || !$prService
|
||||
? 'You can enable git host integration in the settings'
|
||||
: ''}
|
||||
loading={isLoading}
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
bind:el={meatballButtonEl}
|
||||
style="ghost"
|
||||
outline
|
||||
icon="kebab"
|
||||
onclick={() => {
|
||||
contextMenu?.toggle();
|
||||
}}
|
||||
/>
|
||||
<BranchLaneContextMenu
|
||||
bind:contextMenuEl={contextMenu}
|
||||
target={meatballButtonEl}
|
||||
onCollapse={collapseLane}
|
||||
{onGenerateBranchName}
|
||||
/>
|
||||
</div>
|
||||
<BranchLaneContextMenu
|
||||
bind:contextMenuEl={contextMenu}
|
||||
target={meatballButtonEl}
|
||||
onCollapse={collapseLane}
|
||||
{onGenerateBranchName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="header__top-overlay" data-remove-from-draggable data-tauri-drag-region></div>
|
||||
</div>
|
||||
@ -338,18 +308,6 @@
|
||||
top: 12px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.header__wrapper--stacking {
|
||||
padding-bottom: unset !important;
|
||||
|
||||
& .header__info-wrapper .draggable {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
.header_card--stacking {
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
.header {
|
||||
z-index: var(--z-lifted);
|
||||
position: relative;
|
||||
@ -408,20 +366,6 @@
|
||||
overflow: hidden;
|
||||
gap: 10px;
|
||||
}
|
||||
/* TODO: Remove me after stacking feature toggle has been removed. */
|
||||
.stacking-header__info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.header__actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
@ -2,6 +2,7 @@
|
||||
import BranchCard from './BranchCard.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { projectLaneCollapsed } from '$lib/config/config';
|
||||
import { stackingFeature } from '$lib/config/uiFeatureFlags';
|
||||
import FileCard from '$lib/file/FileCard.svelte';
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import { createGitHostChecksMonitorStore } from '$lib/gitHost/interface/gitHostChecksMonitor';
|
||||
@ -11,6 +12,7 @@
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
import { SETTINGS, type Settings } from '$lib/settings/userSettings';
|
||||
import Resizer from '$lib/shared/Resizer.svelte';
|
||||
import Stack from '$lib/stack/Stack.svelte';
|
||||
import { getContext, getContextStoreBySymbol, createContextStore } from '$lib/utils/context';
|
||||
import {
|
||||
createIntegratedCommitsContextStore,
|
||||
@ -109,7 +111,11 @@
|
||||
</script>
|
||||
|
||||
<div class="wrapper" data-tauri-drag-region>
|
||||
<BranchCard {commitBoxOpen} {isLaneCollapsed} />
|
||||
{#if $stackingFeature}
|
||||
<Stack {commitBoxOpen} {isLaneCollapsed} />
|
||||
{:else}
|
||||
<BranchCard {commitBoxOpen} {isLaneCollapsed} />
|
||||
{/if}
|
||||
|
||||
{#if selected}
|
||||
<div
|
||||
|
@ -1,53 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
|
||||
import { getGitHostPrService } from '$lib/gitHost/interface/gitHostPrService';
|
||||
import PullRequestButton from '$lib/pr/PullRequestButton.svelte';
|
||||
|
||||
interface Props {
|
||||
upstreamName: string | undefined;
|
||||
}
|
||||
|
||||
const { upstreamName }: Props = $props();
|
||||
|
||||
const hostedListingServiceStore = getGitHostListingService();
|
||||
const prStore = $derived($hostedListingServiceStore?.prs);
|
||||
const prs = $derived(prStore ? $prStore : undefined);
|
||||
|
||||
const listedPr = $derived(prs?.find((pr) => pr.sourceBranch === upstreamName));
|
||||
const prNumber = $derived(listedPr?.number);
|
||||
|
||||
const prService = getGitHostPrService();
|
||||
const prMonitor = $derived(prNumber ? $prService?.prMonitor(prNumber) : undefined);
|
||||
|
||||
const pr = $derived(prMonitor?.pr);
|
||||
|
||||
let loading = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="branch-info">
|
||||
<div class="branch-name text-14 text-bold">
|
||||
<span class="remote-name">origin/</span>{upstreamName}
|
||||
</div>
|
||||
{#if !pr}
|
||||
<PullRequestButton {loading} click={() => {}} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.branch-info {
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.branch-name {
|
||||
}
|
||||
.remote-name {
|
||||
color: var(--clr-scale-ntrl-60);
|
||||
}
|
||||
</style>
|
222
apps/desktop/src/lib/branch/StackingBranchHeader.svelte
Normal file
222
apps/desktop/src/lib/branch/StackingBranchHeader.svelte
Normal file
@ -0,0 +1,222 @@
|
||||
<script lang="ts">
|
||||
import BranchLabel from './BranchLabel.svelte';
|
||||
import StackingStatusIcon from './StackingStatusIcon.svelte';
|
||||
import { getColorFromBranchType, type BranchColor } from './stackingUtils';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
|
||||
import { mapErrorToToast } from '$lib/gitHost/github/errorMap';
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
|
||||
import { getGitHostPrMonitor } from '$lib/gitHost/interface/gitHostPrMonitor';
|
||||
import { getGitHostPrService } from '$lib/gitHost/interface/gitHostPrService';
|
||||
import { showError, showToast } from '$lib/notifications/toasts';
|
||||
import PullRequestButton from '$lib/pr/PullRequestButton.svelte';
|
||||
import StackingPullRequestCard from '$lib/pr/StackingPullRequestCard.svelte';
|
||||
import { getBranchNameFromRef } from '$lib/utils/branch';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
import { sleep } from '$lib/utils/sleep';
|
||||
import { error } from '$lib/utils/toasts';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { VirtualBranch } from '$lib/vbranches/types';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import type { PullRequest } from '$lib/gitHost/interface/types';
|
||||
|
||||
let isLoading = $state(false);
|
||||
|
||||
const branchStore = getContextStore(VirtualBranch);
|
||||
const branch = $derived($branchStore);
|
||||
|
||||
const branchController = getContext(BranchController);
|
||||
const baseBranch = getContextStore(BaseBranch);
|
||||
const prMonitor = getGitHostPrMonitor();
|
||||
const baseBranchService = getContext(BaseBranchService);
|
||||
const prService = getGitHostPrService();
|
||||
const gitListService = getGitHostListingService();
|
||||
const gitHost = getGitHost();
|
||||
const project = getContext(Project);
|
||||
|
||||
const baseBranchName = $derived($baseBranch.shortName);
|
||||
const pr = $derived($prMonitor?.pr);
|
||||
|
||||
// TODO: Get Branch Status
|
||||
const branchType = $state<BranchColor>('integrated');
|
||||
const lineColor = $derived(getColorFromBranchType(branchType));
|
||||
|
||||
interface CreatePrOpts {
|
||||
draft: boolean;
|
||||
}
|
||||
|
||||
const defaultPrOpts: CreatePrOpts = {
|
||||
draft: true
|
||||
};
|
||||
|
||||
async function createPr(createPrOpts: CreatePrOpts): Promise<PullRequest | undefined> {
|
||||
const opts = { ...defaultPrOpts, ...createPrOpts };
|
||||
if (!$gitHost) {
|
||||
error('Pull request service not available');
|
||||
return;
|
||||
}
|
||||
|
||||
let title: string;
|
||||
let body: string;
|
||||
|
||||
let pullRequestTemplateBody: string | undefined;
|
||||
const prTemplatePath = project.git_host.pullRequestTemplatePath;
|
||||
|
||||
if (prTemplatePath) {
|
||||
pullRequestTemplateBody = await $prService?.pullRequestTemplateContent(
|
||||
prTemplatePath,
|
||||
project.id
|
||||
);
|
||||
}
|
||||
|
||||
if (pullRequestTemplateBody) {
|
||||
title = branch.name;
|
||||
body = pullRequestTemplateBody;
|
||||
} else {
|
||||
// In case of a single commit, use the commit summary and description for the title and
|
||||
// description of the PR.
|
||||
if (branch.commits.length === 1) {
|
||||
const commit = branch.commits[0];
|
||||
title = commit?.descriptionTitle ?? '';
|
||||
body = commit?.descriptionBody ?? '';
|
||||
} else {
|
||||
title = branch.name;
|
||||
body = '';
|
||||
}
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
try {
|
||||
let upstreamBranchName = branch.upstreamName;
|
||||
|
||||
if (branch.commits.some((c) => !c.isRemote)) {
|
||||
const firstPush = !branch.upstream;
|
||||
const { refname, remote } = await branchController.pushBranch(
|
||||
branch.id,
|
||||
branch.requiresForce
|
||||
);
|
||||
upstreamBranchName = getBranchNameFromRef(refname, remote);
|
||||
|
||||
if (firstPush) {
|
||||
// TODO: fix this hack for reactively available prService.
|
||||
await sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
if (!baseBranchName) {
|
||||
error('No base branch name determined');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!upstreamBranchName) {
|
||||
error('No upstream branch name determined');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$prService) {
|
||||
error('Pull request service not available');
|
||||
return;
|
||||
}
|
||||
|
||||
await $prService.createPr({
|
||||
title,
|
||||
body,
|
||||
draft: opts.draft,
|
||||
baseBranchName,
|
||||
upstreamName: upstreamBranchName
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
const toast = mapErrorToToast(err);
|
||||
if (toast) showToast(toast);
|
||||
else showError('Error while creating pull request', err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
await $gitListService?.refresh();
|
||||
baseBranchService.fetchFromRemotes();
|
||||
}
|
||||
|
||||
function editTitle(title: string) {
|
||||
branchController.updateBranchName(branch.id, title);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="branch-header">
|
||||
<div class="branch-info">
|
||||
<StackingStatusIcon icon="tick-small" color={branchType} gap={false} lineTop />
|
||||
<div class="text-14 text-bold branch-info__name">
|
||||
<span class="remote-name">{$baseBranch.remoteName ?? 'origin'}/</span>
|
||||
<BranchLabel name={branch.name} onChange={(name) => editTitle(name)} />
|
||||
</div>
|
||||
<div class="branch-info__btns">
|
||||
<Button icon="description" outline type="ghost" color="neutral" />
|
||||
<Button icon="edit-text" outline type="ghost" color="neutral" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="branch-action">
|
||||
<div class="branch-action__line" style:--bg-color={lineColor}></div>
|
||||
<div class="branch-action__body">
|
||||
{#if $pr && branch.upstream?.givenName}
|
||||
<StackingPullRequestCard upstreamName={branch.upstream.givenName} />
|
||||
{:else}
|
||||
<PullRequestButton
|
||||
click={async ({ draft }) => await createPr({ draft })}
|
||||
disabled={branch.commits.length === 0 || !$gitHost || !$prService}
|
||||
tooltip={!$gitHost || !$prService
|
||||
? 'You can enable git host integration in the settings'
|
||||
: ''}
|
||||
loading={isLoading}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.branch-header {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.branch-info {
|
||||
padding: 0 13px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
& .branch-info__name {
|
||||
padding: 8px 16px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
& .branch-info__btns {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.remote-name {
|
||||
color: var(--clr-scale-ntrl-60);
|
||||
margin-right: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.branch-action {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
|
||||
.branch-action__line {
|
||||
margin: 0 22px 0 22.5px;
|
||||
border-left: 2px solid var(--bg-color, var(--clr-border-3));
|
||||
}
|
||||
.branch-action__body {
|
||||
width: 100%;
|
||||
padding: 4px 12px 12px 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
90
apps/desktop/src/lib/branch/StackingNewStackCard.svelte
Normal file
90
apps/desktop/src/lib/branch/StackingNewStackCard.svelte
Normal file
@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import StackingStatusIcon from './StackingStatusIcon.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { projectShowStackingCardDetails } from '$lib/config/config';
|
||||
import Link from '$lib/shared/Link.svelte';
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Icon from '@gitbutler/ui/Icon.svelte';
|
||||
|
||||
interface Props {
|
||||
addSeries: (e: MouseEvent) => void;
|
||||
branchId: string;
|
||||
}
|
||||
|
||||
const { branchId: _branchId, addSeries }: Props = $props();
|
||||
|
||||
const project = getContext(Project);
|
||||
|
||||
const showStackingCardDetails = projectShowStackingCardDetails(project.id);
|
||||
let showDetails = $state($showStackingCardDetails);
|
||||
|
||||
function closeStackingCard() {
|
||||
showStackingCardDetails.set(false);
|
||||
showDetails = false;
|
||||
}
|
||||
|
||||
async function addSeriesToStack(e: MouseEvent) {
|
||||
addSeries(e);
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="card">
|
||||
{#if showDetails}
|
||||
<button tabindex="0" class="card__close" onclick={closeStackingCard}>
|
||||
<Icon name="cross-small" />
|
||||
</button>
|
||||
<div class="card__body">
|
||||
<h2 class="text-16 text-bold">New branch stacking</h2>
|
||||
<p class="text-12 card__description">
|
||||
Allows you to add a branch that depends on previous branches. This helps you create smaller
|
||||
PRs that are reviewed and merged in order.
|
||||
<Link href="https://docs.gitbutler.com/stacking" target="_blank">Read more</Link>
|
||||
</p>
|
||||
</div>
|
||||
<Spacer />
|
||||
{/if}
|
||||
<section class="card__action" class:showDetails={!showDetails}>
|
||||
<StackingStatusIcon icon="plus-small" color="neutral" gap={true} />
|
||||
<Button grow style="neutral" onclick={addSeriesToStack}>Add a branch to the stack</Button>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card__body {
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
.card__close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
|
||||
color: var(--clr-scale-ntrl-60);
|
||||
}
|
||||
|
||||
.card__description {
|
||||
color: var(--clr-scale-ntrl-50);
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.card__action {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: around;
|
||||
align-items: flex-start;
|
||||
padding: 0 13px;
|
||||
gap: 1rem;
|
||||
|
||||
&.showDetails {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
54
apps/desktop/src/lib/branch/StackingStatusIcon.svelte
Normal file
54
apps/desktop/src/lib/branch/StackingStatusIcon.svelte
Normal file
@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { getColorFromBranchType, type BranchColor } from './stackingUtils';
|
||||
import Icon from '@gitbutler/ui/Icon.svelte';
|
||||
|
||||
interface Props {
|
||||
icon: 'plus-small' | 'tick-small';
|
||||
color: BranchColor;
|
||||
gap?: boolean;
|
||||
lineTop?: boolean;
|
||||
}
|
||||
|
||||
const { icon, color, gap = false, lineTop = false }: Props = $props();
|
||||
|
||||
// TODO: Better handle colors
|
||||
const bgColor = $derived(getColorFromBranchType(color));
|
||||
</script>
|
||||
|
||||
<div class="stack__status gap" class:gap>
|
||||
{#if lineTop}
|
||||
<div class="stack__status--bar" style:--bg-color={bgColor}></div>
|
||||
{/if}
|
||||
<div class="stack__status--icon" style:--bg-color={bgColor}>
|
||||
<Icon name={icon} />
|
||||
</div>
|
||||
<div class="stack__status--bar" style:--bg-color={bgColor}></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stack__status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.gap {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
& .stack__status--icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 21px;
|
||||
height: 28px;
|
||||
border-radius: 30%;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--clr-text-1);
|
||||
}
|
||||
& .stack__status--bar {
|
||||
height: 10px;
|
||||
border-right: 2px solid var(--bg-color, var(--clr-border-3));
|
||||
}
|
||||
}
|
||||
</style>
|
10
apps/desktop/src/lib/branch/stackingUtils.ts
Normal file
10
apps/desktop/src/lib/branch/stackingUtils.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export type BranchColor = 'neutral' | 'integrated';
|
||||
|
||||
const colorMap = {
|
||||
neutral: 'var(--clr-scale-ntrl-80)',
|
||||
integrated: 'var(--clr-commit-integrated)'
|
||||
};
|
||||
|
||||
export function getColorFromBranchType(type: BranchColor) {
|
||||
return colorMap[type];
|
||||
}
|
@ -442,6 +442,7 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
background-color: var(--clr-bg-1);
|
||||
|
||||
transition: background-color var(--transition-fast);
|
||||
@ -505,6 +506,7 @@
|
||||
|
||||
.commit__title {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
display: block;
|
||||
color: var(--clr-text-1);
|
||||
width: 100%;
|
||||
|
@ -2,6 +2,7 @@
|
||||
import CommitAction from './CommitAction.svelte';
|
||||
import StackingCommitCard from './StackingCommitCard.svelte';
|
||||
import StackingCommitDragItem from './StackingCommitDragItem.svelte';
|
||||
import StackingUpstreamCommitsAccordion from './StackingUpstreamCommitsAccordion.svelte';
|
||||
import { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
import { transformAnyCommit } from '$lib/commitLines/transformers';
|
||||
import InsertEmptyCommitAction from '$lib/components/InsertEmptyCommitAction.svelte';
|
||||
@ -62,9 +63,10 @@
|
||||
? [...remoteCommits.map(transformAnyCommit), { id: LineSpacer.Remote }]
|
||||
: []
|
||||
);
|
||||
|
||||
const mappedLocalCommits = $derived(
|
||||
localCommits.length > 0 ? localCommits.map(transformAnyCommit) : []
|
||||
localCommits.length > 0
|
||||
? [...localCommits.map(transformAnyCommit), { id: LineSpacer.Local }]
|
||||
: []
|
||||
);
|
||||
const mappedLocalAndRemoteCommits = $derived(
|
||||
localAndRemoteCommits.length > 0
|
||||
@ -128,9 +130,8 @@
|
||||
<div class="commits">
|
||||
<!-- UPSTREAM COMMITS -->
|
||||
|
||||
{#if remoteCommits.length > 0}
|
||||
<!-- To make the sticky position work, commits should be wrapped in a div -->
|
||||
<div class="commits-group">
|
||||
{#if hasRemoteCommits}
|
||||
<StackingUpstreamCommitsAccordion remoteCommitCount={remoteCommits.length}>
|
||||
{#each remoteCommits as commit, idx (commit.id)}
|
||||
<StackingCommitCard
|
||||
type="remote"
|
||||
@ -146,30 +147,27 @@
|
||||
{/snippet}
|
||||
</StackingCommitCard>
|
||||
{/each}
|
||||
|
||||
<CommitAction>
|
||||
{#snippet lines()}
|
||||
<Line line={lineManager.get(LineSpacer.Remote)} />
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<Button
|
||||
style="warning"
|
||||
kind="solid"
|
||||
loading={isIntegratingCommits}
|
||||
onclick={async () => {
|
||||
isIntegratingCommits = true;
|
||||
try {
|
||||
await branchController.mergeUpstream($branch.id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
isIntegratingCommits = false;
|
||||
}
|
||||
}}>Integrate upstream</Button
|
||||
>
|
||||
{/snippet}
|
||||
</CommitAction>
|
||||
</div>
|
||||
{#snippet action()}
|
||||
<Button
|
||||
style="warning"
|
||||
kind="solid"
|
||||
grow
|
||||
loading={isIntegratingCommits}
|
||||
onclick={async () => {
|
||||
isIntegratingCommits = true;
|
||||
try {
|
||||
await branchController.mergeUpstream($branch.id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
isIntegratingCommits = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
Integrate upstream
|
||||
</Button>
|
||||
{/snippet}
|
||||
</StackingUpstreamCommitsAccordion>
|
||||
{/if}
|
||||
|
||||
<!-- LOCAL COMMITS -->
|
||||
@ -288,9 +286,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--clr-bg-2);
|
||||
border-radius: var(--radius-m);
|
||||
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--clr-border-2);
|
||||
}
|
||||
|
||||
.commits-group {
|
||||
|
@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@gitbutler/ui/Icon.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
action: Snippet;
|
||||
remoteCommitCount: number;
|
||||
}
|
||||
|
||||
const { children, action, remoteCommitCount }: Props = $props();
|
||||
|
||||
let isOpen = $state(false);
|
||||
|
||||
function toggle() {
|
||||
isOpen = !isOpen;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="accordion" onclick={toggle}>
|
||||
<div class="accordion-row">
|
||||
<div class="accordion-row__line dots">
|
||||
{#each new Array(remoteCommitCount) as _, idx}
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
class="upstream-dot"
|
||||
style="--dot: {idx + 1}; --dotCount: {remoteCommitCount + 1};"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="1.76782" y="1.76764" width="10.535" height="10.535" rx="3" stroke-width="2" />
|
||||
</svg>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="accordion-row__right">
|
||||
<h5 class="text-13 text-body text-semibold title">Upstream commits</h5>
|
||||
<Icon name={isOpen ? 'chevron-up' : 'chevron-down'} />
|
||||
</div>
|
||||
</div>
|
||||
{#if isOpen}
|
||||
<div class="accordion-row">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<div class="accordion-row unthemed">
|
||||
<div class="accordion-row__line"></div>
|
||||
<div class="accordion-row__right">
|
||||
{@render action()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.accordion {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--clr-theme-warn-bg);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
align-items: stretch;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
|
||||
&.unthemed {
|
||||
background-color: var(--clr-bg-1);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
}
|
||||
|
||||
& .accordion-row__line {
|
||||
margin: 0 10px 0 22.5px;
|
||||
border-right: 2px solid var(--clr-commit-upstream);
|
||||
|
||||
&.dots {
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
& .upstream-dot {
|
||||
position: absolute;
|
||||
fill: var(--clr-commit-upstream);
|
||||
stroke: var(--clr-theme-warn-bg);
|
||||
transform: translateX(-6px) translateY(calc(var(--dot) * 7.5px)) rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
& .accordion-row__right {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
margin: 0 8px;
|
||||
align-items: center;
|
||||
color: var(--clr-text-2);
|
||||
}
|
||||
|
||||
& .title {
|
||||
flex: 1;
|
||||
display: block;
|
||||
color: var(--clr-text-1);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -49,3 +49,7 @@ export function projectLaneCollapsed(projectId: string, laneId: string): Persist
|
||||
export function persistedCommitMessage(projectId: string, branchId: string): Persisted<string> {
|
||||
return persisted('', 'projectCurrentCommitMessage_' + projectId + '_' + branchId);
|
||||
}
|
||||
|
||||
export function projectShowStackingCardDetails(projectId: string): Persisted<boolean> {
|
||||
return persisted(true, 'showStackingCardDetails_' + projectId);
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
import InfoMessage from '../shared/InfoMessage.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
|
||||
import { stackingFeature } from '$lib/config/uiFeatureFlags';
|
||||
import { getGitHostChecksMonitor } from '$lib/gitHost/interface/gitHostChecksMonitor';
|
||||
import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
|
||||
import { getGitHostPrService } from '$lib/gitHost/interface/gitHostPrService';
|
||||
@ -174,11 +173,7 @@
|
||||
</script>
|
||||
|
||||
{#if $pr}
|
||||
<div
|
||||
class:card={!$stackingFeature}
|
||||
class:pr-card={!$stackingFeature}
|
||||
class:stacked-pr={$stackingFeature}
|
||||
>
|
||||
<div class="card pr-card">
|
||||
<div class="floating-button">
|
||||
<Button
|
||||
icon="update-small"
|
||||
@ -193,15 +188,11 @@
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class:pr-title={!$stackingFeature}
|
||||
class:stacked-pr-title={$stackingFeature}
|
||||
class="text-13 text-semibold"
|
||||
>
|
||||
<div class="pr-title text-13 text-semibold">
|
||||
<span style="color: var(--clr-scale-ntrl-50)">PR #{$pr?.number}:</span>
|
||||
{$pr.title}
|
||||
</div>
|
||||
<div class:pr-tags={!$stackingFeature} class:stacked-pr-tags={$stackingFeature}>
|
||||
<div class="pr-tags">
|
||||
<Button
|
||||
size="tag"
|
||||
clickable={false}
|
||||
@ -234,7 +225,7 @@
|
||||
immediately.
|
||||
-->
|
||||
{#if $pr}
|
||||
<div class:pr-actions={!$stackingFeature} class:stacked-pr-actions={$stackingFeature}>
|
||||
<div class="pr-actions">
|
||||
{#if infoProps}
|
||||
<InfoMessage icon={infoProps.icon} filled outlined={false} style={infoProps.messageStyle}>
|
||||
<svelte:fragment slot="content">
|
||||
@ -279,12 +270,6 @@
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.stacked-pr {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pr-card {
|
||||
position: relative;
|
||||
padding: 14px;
|
||||
@ -299,24 +284,11 @@
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.stacked-pr-title {
|
||||
color: var(--clr-scale-ntrl-0);
|
||||
padding: 14px 14px 12px 14px;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.pr-tags {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stacked-pr-tags {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 0 14px 12px 14px;
|
||||
}
|
||||
|
||||
.pr-actions {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
@ -324,13 +296,6 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stacked-pr-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 0 14px 12px 14px;
|
||||
}
|
||||
|
||||
.floating-button {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
|
306
apps/desktop/src/lib/pr/StackingPullRequestCard.svelte
Normal file
306
apps/desktop/src/lib/pr/StackingPullRequestCard.svelte
Normal file
@ -0,0 +1,306 @@
|
||||
<script lang="ts">
|
||||
import MergeButton from './MergeButton.svelte';
|
||||
import ViewPrButton from './ViewPrButton.svelte';
|
||||
import InfoMessage from '../shared/InfoMessage.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
|
||||
import { getGitHostChecksMonitor } from '$lib/gitHost/interface/gitHostChecksMonitor';
|
||||
import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
|
||||
import { getGitHostPrService } from '$lib/gitHost/interface/gitHostPrService';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import { type ComponentColor } from '@gitbutler/ui/utils/colorTypes';
|
||||
import { createTimeAgoStore } from '@gitbutler/ui/utils/timeAgo';
|
||||
import type { ChecksStatus } from '$lib/gitHost/interface/types';
|
||||
import type { MessageStyle } from '$lib/shared/InfoMessage.svelte';
|
||||
import type iconsJson from '@gitbutler/ui/data/icons.json';
|
||||
|
||||
interface Props {
|
||||
upstreamName: string;
|
||||
}
|
||||
|
||||
const { upstreamName }: Props = $props();
|
||||
|
||||
type StatusInfo = {
|
||||
text: string;
|
||||
icon: keyof typeof iconsJson | undefined;
|
||||
style?: ComponentColor;
|
||||
messageStyle?: MessageStyle;
|
||||
};
|
||||
|
||||
const vbranchService = getContext(VirtualBranchService);
|
||||
const baseBranchService = getContext(BaseBranchService);
|
||||
const project = getContext(Project);
|
||||
|
||||
const gitHostListingService = getGitHostListingService();
|
||||
const prStore = $derived($gitHostListingService?.prs);
|
||||
const prs = $derived(prStore ? $prStore : undefined);
|
||||
|
||||
const listedPr = $derived(prs?.find((pr) => pr.sourceBranch === upstreamName));
|
||||
const prNumber = $derived(listedPr?.number);
|
||||
|
||||
const prService = getGitHostPrService();
|
||||
const prMonitor = $derived(prNumber ? $prService?.prMonitor(prNumber) : undefined);
|
||||
|
||||
const checksMonitor = getGitHostChecksMonitor();
|
||||
// This PR has been loaded on demand, and contains more details than the version
|
||||
// obtained when listing them.
|
||||
const pr = $derived(prMonitor?.pr);
|
||||
const checks = $derived($checksMonitor?.status);
|
||||
|
||||
// While the pr monitor is set to fetch updates by interval, we want
|
||||
// frequent updates while checks are running.
|
||||
$effect(() => {
|
||||
if ($checks) prMonitor?.refresh();
|
||||
});
|
||||
|
||||
let isMerging = $state(false);
|
||||
|
||||
const lastFetch = $derived(prMonitor?.lastFetch);
|
||||
const timeAgo = $derived($lastFetch ? createTimeAgoStore($lastFetch) : undefined);
|
||||
|
||||
const mrLoading = $derived(prMonitor?.loading);
|
||||
const checksLoading = $derived($checksMonitor?.loading);
|
||||
|
||||
const checksError = $derived($checksMonitor?.error);
|
||||
const detailsError = $derived(prMonitor?.error);
|
||||
|
||||
function getChecksCount(status: ChecksStatus): string {
|
||||
if (!status) return 'Running checks';
|
||||
|
||||
const finished = status.finished || 0;
|
||||
const skipped = status.skipped || 0;
|
||||
const total = (status.totalCount || 0) - skipped;
|
||||
|
||||
return `Checks completed ${finished}/${total}`;
|
||||
}
|
||||
|
||||
const checksTagInfo: StatusInfo | undefined = $derived.by(() => {
|
||||
if ($checksError || $detailsError) {
|
||||
return { style: 'error', icon: 'warning-small', text: 'Failed to load' };
|
||||
}
|
||||
|
||||
if ($checks) {
|
||||
const style = $checks.completed ? ($checks.success ? 'success' : 'error') : 'warning';
|
||||
const icon =
|
||||
$checks.completed && !$checksLoading
|
||||
? $checks.success
|
||||
? 'success-small'
|
||||
: 'error-small'
|
||||
: 'spinner';
|
||||
const text = $checks.completed
|
||||
? $checks.success
|
||||
? 'Checks passed'
|
||||
: 'Checks failed'
|
||||
: getChecksCount($checks);
|
||||
return { style, icon, text };
|
||||
}
|
||||
if ($checksLoading) {
|
||||
return { style: 'neutral', icon: 'spinner', text: ' Checks' };
|
||||
}
|
||||
});
|
||||
|
||||
const prStatusInfo: StatusInfo = $derived.by(() => {
|
||||
if (!$pr) {
|
||||
return { text: 'Status', icon: 'spinner', style: 'neutral' };
|
||||
}
|
||||
|
||||
if ($pr?.mergedAt) {
|
||||
return { text: 'Merged', icon: 'merged-pr-small', style: 'purple' };
|
||||
}
|
||||
|
||||
if ($pr?.closedAt) {
|
||||
return { text: 'Closed', icon: 'closed-pr-small', style: 'error' };
|
||||
}
|
||||
|
||||
if ($pr?.draft) {
|
||||
return { text: 'Draft', icon: 'draft-pr-small', style: 'neutral' };
|
||||
}
|
||||
|
||||
return { text: 'Open', icon: 'pr-small', style: 'success' };
|
||||
});
|
||||
|
||||
const infoProps: StatusInfo | undefined = $derived.by(() => {
|
||||
const mergeableState = $pr?.mergeableState;
|
||||
if (mergeableState === 'blocked' && !$checks && !$checksLoading) {
|
||||
return {
|
||||
icon: 'error',
|
||||
messageStyle: 'error',
|
||||
text: 'Merge is blocked due to pending reviews or missing dependencies. Resolve the issues before merging.'
|
||||
};
|
||||
}
|
||||
|
||||
if ($checks?.completed) {
|
||||
if ($pr?.draft) {
|
||||
return {
|
||||
icon: 'warning',
|
||||
messageStyle: 'neutral',
|
||||
text: 'This pull request is still a work in progress. Draft pull requests cannot be merged.'
|
||||
};
|
||||
}
|
||||
|
||||
if (mergeableState === 'unstable') {
|
||||
return {
|
||||
icon: 'warning',
|
||||
messageStyle: 'warning',
|
||||
text: 'Your PR is causing instability or errors in the build or tests. Review the checks and fix the issues before merging.'
|
||||
};
|
||||
}
|
||||
|
||||
if (mergeableState === 'dirty') {
|
||||
return {
|
||||
icon: 'warning',
|
||||
messageStyle: 'warning',
|
||||
text: 'Your PR has conflicts that must be resolved before merging.'
|
||||
};
|
||||
}
|
||||
if (
|
||||
mergeableState === 'blocked' &&
|
||||
!$checksLoading &&
|
||||
$checks?.failed &&
|
||||
$checks.failed > 0
|
||||
) {
|
||||
return {
|
||||
icon: 'error',
|
||||
messageStyle: 'error',
|
||||
text: 'Merge is blocked due to failing checks. Resolve the issues before merging.'
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $pr}
|
||||
<div class="pr-header">
|
||||
<div class="floating-button">
|
||||
<Button
|
||||
icon="update-small"
|
||||
size="tag"
|
||||
style="ghost"
|
||||
outline
|
||||
loading={$mrLoading || $checksLoading}
|
||||
tooltip={$timeAgo ? 'Updated ' + $timeAgo : ''}
|
||||
onclick={async () => {
|
||||
$checksMonitor?.update();
|
||||
prMonitor?.refresh();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="text-13 text-semibold pr-header-title">
|
||||
<span style="color: var(--clr-scale-ntrl-50)">PR #{$pr?.number}:</span>
|
||||
{$pr.title}
|
||||
</div>
|
||||
<div class="pr-header-tags">
|
||||
<Button
|
||||
size="tag"
|
||||
clickable={false}
|
||||
icon={prStatusInfo.icon}
|
||||
style={prStatusInfo.style}
|
||||
kind={prStatusInfo.text !== 'Open' && prStatusInfo.text !== 'Status' ? 'solid' : 'soft'}
|
||||
>
|
||||
{prStatusInfo.text}
|
||||
</Button>
|
||||
{#if !$pr.closedAt && checksTagInfo}
|
||||
<Button
|
||||
size="tag"
|
||||
clickable={false}
|
||||
icon={checksTagInfo.icon}
|
||||
style={checksTagInfo.style}
|
||||
kind={checksTagInfo.icon === 'success-small' ? 'solid' : 'soft'}
|
||||
>
|
||||
{checksTagInfo.text}
|
||||
</Button>
|
||||
{/if}
|
||||
<ViewPrButton url={$pr.htmlUrl} />
|
||||
</div>
|
||||
|
||||
<!--
|
||||
We can't show the merge button until we've waited for checks
|
||||
|
||||
We use a octokit.checks.listForRef to find checks running for a PR, but right after
|
||||
creation this request succeeds but returns an empty array. So we need a better way
|
||||
determining "no checks will run for this PR" such that we can show the merge button
|
||||
immediately.
|
||||
-->
|
||||
{#if $pr}
|
||||
<div class="pr-header-actions">
|
||||
{#if infoProps}
|
||||
<InfoMessage icon={infoProps.icon} filled outlined={false} style={infoProps.messageStyle}>
|
||||
<svelte:fragment slot="content">
|
||||
{infoProps.text}
|
||||
</svelte:fragment>
|
||||
</InfoMessage>
|
||||
{/if}
|
||||
|
||||
<MergeButton
|
||||
wide
|
||||
projectId={project.id}
|
||||
disabled={$mrLoading ||
|
||||
$checksLoading ||
|
||||
$pr.draft ||
|
||||
!$pr.mergeable ||
|
||||
['dirty', 'unknown', 'blocked', 'behind'].includes($pr.mergeableState)}
|
||||
loading={isMerging}
|
||||
on:click={async (e) => {
|
||||
if (!$pr) return;
|
||||
isMerging = true;
|
||||
const method = e.detail.method;
|
||||
try {
|
||||
await $prService?.merge(method, $pr.number);
|
||||
await baseBranchService.fetchFromRemotes();
|
||||
await Promise.all([
|
||||
prMonitor?.refresh(),
|
||||
$gitHostListingService?.refresh(),
|
||||
vbranchService.refresh(),
|
||||
baseBranchService.refresh()
|
||||
]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toasts.error('Failed to merge pull request');
|
||||
} finally {
|
||||
isMerging = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.pr-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--clr-border-2);
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
|
||||
.pr-header-title {
|
||||
color: var(--clr-scale-ntrl-0);
|
||||
padding: 14px 14px 12px 14px;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.pr-header-tags {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 0 14px 12px 14px;
|
||||
}
|
||||
|
||||
.pr-header-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 0 14px 12px 14px;
|
||||
}
|
||||
|
||||
.floating-button {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
}
|
||||
</style>
|
@ -31,9 +31,9 @@
|
||||
onthumbdrag
|
||||
}: Props = $props();
|
||||
|
||||
let viewport: HTMLDivElement | undefined = $state();
|
||||
let contents: HTMLDivElement | undefined = $state();
|
||||
let scrollable: boolean | undefined = $state();
|
||||
let viewport = $state<HTMLDivElement>();
|
||||
let contents = $state<HTMLDivElement>();
|
||||
let scrollable = $state<boolean>();
|
||||
|
||||
let observer: ResizeObserver;
|
||||
|
||||
|
385
apps/desktop/src/lib/stack/Stack.svelte
Normal file
385
apps/desktop/src/lib/stack/Stack.svelte
Normal file
@ -0,0 +1,385 @@
|
||||
<script lang="ts">
|
||||
import StackHeader from './StackHeader.svelte';
|
||||
import StackSeries from './StackSeries.svelte';
|
||||
import EmptyStatePlaceholder from '../components/EmptyStatePlaceholder.svelte';
|
||||
import InfoMessage from '../shared/InfoMessage.svelte';
|
||||
import { PromptService } from '$lib/ai/promptService';
|
||||
import { AIService } from '$lib/ai/service';
|
||||
import laneNewSvg from '$lib/assets/empty-state/lane-new.svg?raw';
|
||||
import noChangesSvg from '$lib/assets/empty-state/lane-no-changes.svg?raw';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
import Dropzones from '$lib/branch/Dropzones.svelte';
|
||||
import StackingNewStackCard from '$lib/branch/StackingNewStackCard.svelte';
|
||||
import CommitDialog from '$lib/commit/CommitDialog.svelte';
|
||||
import { projectAiGenEnabled } from '$lib/config/config';
|
||||
import BranchFiles from '$lib/file/BranchFiles.svelte';
|
||||
import { getGitHostChecksMonitor } from '$lib/gitHost/interface/gitHostChecksMonitor';
|
||||
import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
|
||||
import { getGitHostPrMonitor } from '$lib/gitHost/interface/gitHostPrMonitor';
|
||||
import { showError } from '$lib/notifications/toasts';
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
import { isFailure } from '$lib/result';
|
||||
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
|
||||
import { SETTINGS, type Settings } from '$lib/settings/userSettings';
|
||||
import Resizer from '$lib/shared/Resizer.svelte';
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
import { User } from '$lib/stores/user';
|
||||
import { getContext, getContextStore, getContextStoreBySymbol } from '$lib/utils/context';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import {
|
||||
// getIntegratedCommits,
|
||||
getLocalAndRemoteCommits,
|
||||
getLocalCommits
|
||||
// getRemoteCommits
|
||||
} from '$lib/vbranches/contexts';
|
||||
import { FileIdSelection } from '$lib/vbranches/fileIdSelection';
|
||||
import { VirtualBranch } from '$lib/vbranches/types';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import lscache from 'lscache';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
const {
|
||||
isLaneCollapsed,
|
||||
commitBoxOpen
|
||||
}: { isLaneCollapsed: Writable<boolean>; commitBoxOpen: Writable<boolean> } = $props();
|
||||
|
||||
const branchController = getContext(BranchController);
|
||||
const fileIdSelection = getContext(FileIdSelection);
|
||||
const branchStore = getContextStore(VirtualBranch);
|
||||
const project = getContext(Project);
|
||||
const user = getContextStore(User);
|
||||
const baseBranch = getContextStore(BaseBranch);
|
||||
|
||||
const branch = $derived($branchStore);
|
||||
|
||||
const aiGenEnabled = projectAiGenEnabled(project.id);
|
||||
|
||||
const aiService = getContext(AIService);
|
||||
const promptService = getContext(PromptService);
|
||||
|
||||
const userSettings = getContextStoreBySymbol<Settings>(SETTINGS);
|
||||
const defaultBranchWidthRem = persisted<number>(24, 'defaulBranchWidth' + project.id);
|
||||
const laneWidthKey = 'laneWidth_';
|
||||
|
||||
let laneWidth: number | undefined = $state();
|
||||
|
||||
let commitDialog = $state<CommitDialog>();
|
||||
let scrollViewport = $state<HTMLElement>();
|
||||
let rsViewport = $state<HTMLElement>();
|
||||
|
||||
$effect(() => {
|
||||
if ($commitBoxOpen && branch.files.length === 0) {
|
||||
commitBoxOpen.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
async function generateBranchName() {
|
||||
if (!aiGenEnabled) return;
|
||||
|
||||
const hunks = branch.files.flatMap((f) => f.hunks);
|
||||
|
||||
const prompt = promptService.selectedBranchPrompt(project.id);
|
||||
const messageResult = await aiService.summarizeBranch({
|
||||
hunks,
|
||||
userToken: $user?.access_token,
|
||||
branchTemplate: prompt
|
||||
});
|
||||
|
||||
if (isFailure(messageResult)) {
|
||||
console.error(messageResult.failure);
|
||||
showError('Failed to generate branch name', messageResult.failure);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const message = messageResult.value;
|
||||
|
||||
if (message && message !== branch.name) {
|
||||
branch.name = message;
|
||||
branchController.updateBranchName(branch.id, branch.name);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
laneWidth = lscache.get(laneWidthKey + branch.id);
|
||||
});
|
||||
|
||||
const localCommits = getLocalCommits();
|
||||
const localAndRemoteCommits = getLocalAndRemoteCommits();
|
||||
// const integratedCommits = getIntegratedCommits();
|
||||
// const remoteCommits = getRemoteCommits();
|
||||
|
||||
let isPushingCommits = $state(false);
|
||||
const localCommitsConflicted = $derived($localCommits.some((commit) => commit.conflicted));
|
||||
const localAndRemoteCommitsConflicted = $derived(
|
||||
$localAndRemoteCommits.some((commit) => commit.conflicted)
|
||||
);
|
||||
|
||||
const listingService = getGitHostListingService();
|
||||
const prMonitor = getGitHostPrMonitor();
|
||||
const checksMonitor = getGitHostChecksMonitor();
|
||||
|
||||
async function push() {
|
||||
isPushingCommits = true;
|
||||
try {
|
||||
await branchController.pushBranch(branch.id, branch.requiresForce, true);
|
||||
$listingService?.refresh();
|
||||
$prMonitor?.refresh();
|
||||
$checksMonitor?.update();
|
||||
} finally {
|
||||
isPushingCommits = false;
|
||||
}
|
||||
}
|
||||
|
||||
function addSeries(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
console.log('CREATE SERIES.BRANCH', { branch, baseBranchName: $baseBranch.remoteName });
|
||||
const topChangeId = branch.commits.at(-1)?.changeId;
|
||||
if (topChangeId) {
|
||||
branchController.createChangeReference(
|
||||
branch?.id || '',
|
||||
'refs/remotes/' +
|
||||
$baseBranch.remoteName +
|
||||
'/' +
|
||||
`series-${Math.floor(Math.random() * 1000)}`,
|
||||
topChangeId
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $isLaneCollapsed}
|
||||
<div class="collapsed-lane-container">
|
||||
<StackHeader
|
||||
uncommittedChanges={branch.files.length}
|
||||
onGenerateBranchName={generateBranchName}
|
||||
{isLaneCollapsed}
|
||||
/>
|
||||
<div class="collapsed-lane-divider" data-remove-from-draggable></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="resizer-wrapper" bind:this={scrollViewport}>
|
||||
<div class="branch-card hide-native-scrollbar" class:target-branch={branch.selectedForChanges}>
|
||||
<ScrollableContainer
|
||||
wide
|
||||
padding={{
|
||||
top: 12,
|
||||
bottom: 12
|
||||
}}
|
||||
>
|
||||
<div
|
||||
bind:this={rsViewport}
|
||||
style:width={`${laneWidth || $defaultBranchWidthRem}rem`}
|
||||
class="branch-card__contents"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<StackHeader {isLaneCollapsed} onGenerateBranchName={generateBranchName} />
|
||||
<div class="card-stacking">
|
||||
{#if branch.files?.length > 0}
|
||||
<div class="branch-card__files card">
|
||||
<Dropzones>
|
||||
<BranchFiles
|
||||
isUnapplied={false}
|
||||
files={branch.files}
|
||||
showCheckboxes={$commitBoxOpen}
|
||||
allowMultiple
|
||||
commitDialogExpanded={commitBoxOpen}
|
||||
focusCommitDialog={() => commitDialog?.focus()}
|
||||
/>
|
||||
{#if branch.conflicted}
|
||||
<div class="card-notifications">
|
||||
<InfoMessage filled outlined={false} style="error">
|
||||
<svelte:fragment slot="title">
|
||||
{#if branch.files.some((f) => f.conflicted)}
|
||||
This virtual branch conflicts with upstream changes. Please resolve all
|
||||
conflicts and commit before you can continue.
|
||||
{:else}
|
||||
Please commit your resolved conflicts to continue.
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</InfoMessage>
|
||||
</div>
|
||||
{/if}
|
||||
</Dropzones>
|
||||
|
||||
<CommitDialog
|
||||
bind:this={commitDialog}
|
||||
projectId={project.id}
|
||||
expanded={commitBoxOpen}
|
||||
hasSectionsAfter={branch.commits.length > 0}
|
||||
/>
|
||||
</div>
|
||||
{:else if branch.commits.length === 0}
|
||||
<Dropzones>
|
||||
<div class="new-branch card">
|
||||
<EmptyStatePlaceholder image={laneNewSvg} width="11rem">
|
||||
<svelte:fragment slot="title">This is a new branch</svelte:fragment>
|
||||
<svelte:fragment slot="caption">
|
||||
You can drag and drop files or parts of files here.
|
||||
</svelte:fragment>
|
||||
</EmptyStatePlaceholder>
|
||||
</div>
|
||||
</Dropzones>
|
||||
{:else}
|
||||
<Dropzones>
|
||||
<div class="no-changes card">
|
||||
<EmptyStatePlaceholder image={noChangesSvg} width="11rem" hasBottomMargin={false}>
|
||||
<svelte:fragment slot="caption">
|
||||
No uncommitted changes on this branch
|
||||
</svelte:fragment>
|
||||
</EmptyStatePlaceholder>
|
||||
</div>
|
||||
</Dropzones>
|
||||
{/if}
|
||||
<Spacer dotted />
|
||||
<div class="lane-branches">
|
||||
<StackingNewStackCard branchId={branch.id} {addSeries} />
|
||||
<StackSeries {branch} />
|
||||
</div>
|
||||
<!-- TODO: Sticky styling -->
|
||||
<div class="lane-branches__action">
|
||||
<Button
|
||||
style="pop"
|
||||
kind="solid"
|
||||
wide
|
||||
loading={isPushingCommits}
|
||||
disabled={localCommitsConflicted || localAndRemoteCommitsConflicted}
|
||||
tooltip={localCommitsConflicted
|
||||
? 'In order to push, please resolve any conflicted commits.'
|
||||
: undefined}
|
||||
onclick={push}
|
||||
>
|
||||
{branch.requiresForce ? 'Force push' : 'Push'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div class="divider-line">
|
||||
{#if rsViewport}
|
||||
<Resizer
|
||||
viewport={rsViewport}
|
||||
direction="right"
|
||||
minWidth={380}
|
||||
sticky
|
||||
defaultLineColor={$fileIdSelection.length === 1 ? 'transparent' : 'var(--clr-border-2)'}
|
||||
on:width={(e) => {
|
||||
laneWidth = e.detail / (16 * $userSettings.zoom);
|
||||
lscache.set(laneWidthKey + branch.id, laneWidth, 7 * 1440); // 7 day ttl
|
||||
$defaultBranchWidthRem = laneWidth;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.resizer-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.branch-card {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.lane-branches {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:global(.lane-branches > *) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.lane-branches__action {
|
||||
z-index: var(--z-lifted);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
z-index: var(--z-lifted);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.branch-card__contents {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 100%;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.card-stacking {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.branch-card__files.card,
|
||||
.no-changes.card,
|
||||
.new-branch.card {
|
||||
border-radius: 0 0 var(--radius-m) var(--radius-m) !important;
|
||||
}
|
||||
|
||||
.branch-card__files {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-notifications {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.new-branch,
|
||||
.no-changes {
|
||||
flex-grow: 1;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: var(--clr-scale-ntrl-60);
|
||||
justify-content: center;
|
||||
cursor: default; /* was defaulting to text cursor */
|
||||
}
|
||||
|
||||
/* COLLAPSED LANE */
|
||||
.collapsed-lane-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.collapsed-lane-divider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: var(--clr-border-2);
|
||||
}
|
||||
</style>
|
302
apps/desktop/src/lib/stack/StackHeader.svelte
Normal file
302
apps/desktop/src/lib/stack/StackHeader.svelte
Normal file
@ -0,0 +1,302 @@
|
||||
<script lang="ts">
|
||||
import ActiveBranchStatus from '$lib/branch/ActiveBranchStatus.svelte';
|
||||
import BranchLabel from '$lib/branch/BranchLabel.svelte';
|
||||
import BranchLaneContextMenu from '$lib/branch/BranchLaneContextMenu.svelte';
|
||||
import DefaultTargetButton from '$lib/branch/DefaultTargetButton.svelte';
|
||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { VirtualBranch } from '$lib/vbranches/types';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Icon from '@gitbutler/ui/Icon.svelte';
|
||||
import type { Persisted } from '$lib/persisted/persisted';
|
||||
|
||||
interface Props {
|
||||
uncommittedChanges?: number;
|
||||
isLaneCollapsed: Persisted<boolean>;
|
||||
onGenerateBranchName: () => void;
|
||||
}
|
||||
|
||||
const { uncommittedChanges = 0, isLaneCollapsed, onGenerateBranchName }: Props = $props();
|
||||
|
||||
const branchController = getContext(BranchController);
|
||||
const branchStore = getContextStore(VirtualBranch);
|
||||
const branch = $derived($branchStore);
|
||||
|
||||
let contextMenu = $state<ReturnType<typeof ContextMenu>>();
|
||||
let meatballButtonEl = $state<HTMLDivElement>();
|
||||
let isTargetBranchAnimated = $state(false);
|
||||
|
||||
function handleBranchNameChange(title: string) {
|
||||
if (title === '') return;
|
||||
|
||||
branchController.updateBranchName(branch.id, title);
|
||||
}
|
||||
|
||||
function expandLane() {
|
||||
$isLaneCollapsed = false;
|
||||
}
|
||||
|
||||
function collapseLane() {
|
||||
$isLaneCollapsed = true;
|
||||
}
|
||||
|
||||
const hasIntegratedCommits = $derived(branch.commits?.some((b) => b.isIntegrated));
|
||||
|
||||
let headerInfoHeight = $state(0);
|
||||
</script>
|
||||
|
||||
{#if $isLaneCollapsed}
|
||||
<div
|
||||
class="card collapsed-lane"
|
||||
class:collapsed-lane_target-branch={branch.selectedForChanges}
|
||||
onkeydown={(e) => e.key === 'Enter' && expandLane()}
|
||||
tabindex="0"
|
||||
role="button"
|
||||
>
|
||||
<div class="collapsed-lane__actions">
|
||||
<div class="draggable" data-drag-handle>
|
||||
<Icon name="draggable" />
|
||||
</div>
|
||||
<Button style="ghost" outline icon="unfold-lane" tooltip="Expand lane" onclick={expandLane} />
|
||||
</div>
|
||||
|
||||
<div class="collapsed-lane__info-wrap" bind:clientHeight={headerInfoHeight}>
|
||||
<div class="collapsed-lane__info" style="width: {headerInfoHeight}px">
|
||||
<div class="collapsed-lane__label-wrap">
|
||||
<h3 class="collapsed-lane__label text-13 text-bold">
|
||||
{branch.name}
|
||||
</h3>
|
||||
{#if uncommittedChanges > 0}
|
||||
<Button
|
||||
size="tag"
|
||||
clickable={false}
|
||||
style="warning"
|
||||
kind="soft"
|
||||
tooltip="Uncommitted changes"
|
||||
>
|
||||
{uncommittedChanges}
|
||||
{uncommittedChanges === 1 ? 'change' : 'changes'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="collapsed-lane__info__details">
|
||||
<ActiveBranchStatus
|
||||
{hasIntegratedCommits}
|
||||
remoteExists={!!branch.upstream}
|
||||
isLaneCollapsed={$isLaneCollapsed}
|
||||
/>
|
||||
{#if branch.selectedForChanges}
|
||||
<Button style="pop" kind="soft" size="tag" clickable={false} icon="target">
|
||||
Default branch
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="header__wrapper">
|
||||
<div
|
||||
class="header card"
|
||||
class:header_target-branch={branch.selectedForChanges}
|
||||
class:header_target-branch-animation={isTargetBranchAnimated && branch.selectedForChanges}
|
||||
>
|
||||
<div class="header__info-wrapper">
|
||||
<div class="draggable" data-drag-handle>
|
||||
<Icon name="draggable" />
|
||||
</div>
|
||||
|
||||
<div class="header__info">
|
||||
<BranchLabel name={branch.name} onChange={(name) => handleBranchNameChange(name)} />
|
||||
<span class="button-group">
|
||||
<DefaultTargetButton
|
||||
selectedForChanges={branch.selectedForChanges}
|
||||
onclick={async () => {
|
||||
isTargetBranchAnimated = true;
|
||||
await branchController.setSelectedForChanges(branch.id);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
bind:el={meatballButtonEl}
|
||||
style="ghost"
|
||||
icon="kebab"
|
||||
onclick={() => {
|
||||
contextMenu?.toggle();
|
||||
}}
|
||||
/>
|
||||
<BranchLaneContextMenu
|
||||
bind:contextMenuEl={contextMenu}
|
||||
target={meatballButtonEl}
|
||||
onCollapse={collapseLane}
|
||||
{onGenerateBranchName}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header__top-overlay" data-remove-from-draggable data-tauri-drag-region></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.header__wrapper {
|
||||
z-index: var(--z-lifted);
|
||||
top: 12px;
|
||||
padding-bottom: unset !important;
|
||||
& .draggable {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.header.card {
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
.header {
|
||||
z-index: var(--z-lifted);
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
transition:
|
||||
border-color 0.12s ease-in-out,
|
||||
box-shadow 0.12s ease-in-out;
|
||||
}
|
||||
.header_target-branch {
|
||||
border-color: var(--clr-theme-pop-element);
|
||||
box-shadow: 0 4px 0 var(--clr-theme-pop-element);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.header_target-branch-animation {
|
||||
animation: setTargetAnimation 0.3s ease-in-out forwards;
|
||||
}
|
||||
@keyframes setTargetAnimation {
|
||||
0% {
|
||||
}
|
||||
40% {
|
||||
transform: scale(1.017) rotate(1deg);
|
||||
}
|
||||
50% {
|
||||
border-color: var(--clr-theme-pop-element);
|
||||
box-shadow: 0 4px 0 var(--clr-theme-pop-element);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
70%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
border-color: var(--clr-theme-pop-element);
|
||||
box-shadow: 0 4px 0 var(--clr-theme-pop-element);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.header__top-overlay {
|
||||
z-index: var(--z-ground);
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: var(--clr-bg-2);
|
||||
}
|
||||
.header__info-wrapper {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 10px;
|
||||
}
|
||||
.header__info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.draggable {
|
||||
display: flex;
|
||||
height: fit-content;
|
||||
align-items: center;
|
||||
cursor: grab;
|
||||
padding: 2px 2px 0 0;
|
||||
color: var(--clr-scale-ntrl-50);
|
||||
transition: color var(--transition-slow);
|
||||
|
||||
&:hover {
|
||||
color: var(--clr-scale-ntrl-40);
|
||||
}
|
||||
}
|
||||
|
||||
/* COLLAPSIBLE LANE */
|
||||
|
||||
.collapsed-lane {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 48px;
|
||||
overflow: hidden;
|
||||
gap: 8px;
|
||||
padding: 8px 8px 20px;
|
||||
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsed-lane_target-branch {
|
||||
border-color: var(--clr-theme-pop-element);
|
||||
}
|
||||
|
||||
.collapsed-lane__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* */
|
||||
|
||||
.collapsed-lane__info-wrap {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.collapsed-lane__info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
transform: rotate(-90deg);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
/* */
|
||||
|
||||
.collapsed-lane__info__details {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.collapsed-lane__label-wrap {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.collapsed-lane__label {
|
||||
color: var(--clr-scale-ntrl-0);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
56
apps/desktop/src/lib/stack/StackSeries.svelte
Normal file
56
apps/desktop/src/lib/stack/StackSeries.svelte
Normal file
@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import StackingBranchHeader from '$lib/branch/StackingBranchHeader.svelte';
|
||||
import StackingCommitList from '$lib/commit/StackingCommitList.svelte';
|
||||
import {
|
||||
getLocalAndRemoteCommits,
|
||||
getLocalCommits,
|
||||
getRemoteCommits
|
||||
} from '$lib/vbranches/contexts';
|
||||
import type { VirtualBranch } from '$lib/vbranches/types';
|
||||
// import type { Series } from './types';
|
||||
|
||||
interface Props {
|
||||
// series: Series[];
|
||||
branch: VirtualBranch;
|
||||
}
|
||||
|
||||
const { branch }: Props = $props();
|
||||
|
||||
const localCommits = getLocalCommits();
|
||||
const localAndRemoteCommits = getLocalAndRemoteCommits();
|
||||
const remoteCommits = getRemoteCommits();
|
||||
|
||||
const localCommitsConflicted = $derived($localCommits.some((commit) => commit.conflicted));
|
||||
const localAndRemoteCommitsConflicted = $derived(
|
||||
$localAndRemoteCommits.some((commit) => commit.conflicted)
|
||||
);
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add connecting line on background between NewStackCard above and branches below -->
|
||||
{#each branch.series as currentSeries}
|
||||
<div class="branch-group">
|
||||
<StackingBranchHeader
|
||||
commits={currentSeries.patches}
|
||||
name={currentSeries.name}
|
||||
upstreamName={currentSeries.name}
|
||||
/>
|
||||
<StackingCommitList
|
||||
localCommits={currentSeries.localCommits}
|
||||
localAndRemoteCommits={currentSeries.remoteCommits}
|
||||
integratedCommits={currentSeries.integratedCommits}
|
||||
remoteCommits={$remoteCommits}
|
||||
isUnapplied={false}
|
||||
{localCommitsConflicted}
|
||||
{localAndRemoteCommitsConflicted}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.branch-group {
|
||||
border: 1px solid var(--clr-border-2);
|
||||
border-radius: var(--radius-m);
|
||||
background: var(--clr-bg-1);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
17
apps/desktop/src/lib/stack/types.ts
Normal file
17
apps/desktop/src/lib/stack/types.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { DetailedCommit, VirtualBranch } from '$lib/vbranches/types';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class PatchSeries {
|
||||
name?: string;
|
||||
description?: string;
|
||||
upstreamReference?: string;
|
||||
|
||||
@Type(() => DetailedCommit)
|
||||
patches!: DetailedCommit[];
|
||||
@Type(() => DetailedCommit)
|
||||
upstreamPatches!: DetailedCommit[];
|
||||
}
|
||||
|
||||
export class Stack extends VirtualBranch {
|
||||
series!: PatchSeries[];
|
||||
}
|
@ -7,6 +7,8 @@ import type { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
import type { BranchPushResult, Hunk, LocalFile } from './types';
|
||||
import type { VirtualBranchService } from './virtualBranch';
|
||||
|
||||
type CommitIdOrChangeId = { CommitId: string } | { ChangeId: string };
|
||||
|
||||
export class BranchController {
|
||||
constructor(
|
||||
readonly projectId: string,
|
||||
@ -96,6 +98,25 @@ export class BranchController {
|
||||
}
|
||||
}
|
||||
|
||||
async createPatchSeries(
|
||||
branchId: string,
|
||||
referenceName: string,
|
||||
commitIdOrChangeId?: CommitIdOrChangeId
|
||||
) {
|
||||
try {
|
||||
await invoke<void>('create_series', {
|
||||
projectId: this.projectId,
|
||||
branchId: branchId,
|
||||
request: {
|
||||
target_patch: commitIdOrChangeId,
|
||||
name: referenceName
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
showError('Failed to create branch reference', err);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates a new GitButler change reference associated with a branch.
|
||||
* @param branchId
|
||||
@ -261,9 +282,14 @@ export class BranchController {
|
||||
}
|
||||
}
|
||||
|
||||
async pushBranch(branchId: string, withForce: boolean): Promise<BranchPushResult> {
|
||||
async pushBranch(
|
||||
branchId: string,
|
||||
withForce: boolean,
|
||||
stack: boolean = false
|
||||
): Promise<BranchPushResult> {
|
||||
try {
|
||||
const pushResult = await invoke<BranchPushResult>('push_virtual_branch', {
|
||||
const command = stack ? 'push_stack' : 'push_virtual_branch';
|
||||
const pushResult = await invoke<BranchPushResult>(command, {
|
||||
projectId: this.projectId,
|
||||
branchId,
|
||||
withForce
|
||||
|
@ -4,6 +4,7 @@ import { hashCode } from '$lib/utils/string';
|
||||
import { isDefined, notNull } from '@gitbutler/ui/utils/typeguards';
|
||||
import { Type, Transform } from 'class-transformer';
|
||||
import type { PullRequest } from '$lib/gitHost/interface/types';
|
||||
import type { PatchSeries } from '$lib/stack/types';
|
||||
|
||||
export type ChangeType =
|
||||
/// Entry does not exist in old version
|
||||
@ -143,6 +144,9 @@ export class VirtualBranch {
|
||||
refname!: string;
|
||||
tree!: string;
|
||||
|
||||
// Used in the stacking context where VirtualBranch === Stack
|
||||
series!: PatchSeries[];
|
||||
|
||||
get localCommits() {
|
||||
return this.commits.filter((c) => c.status === 'local');
|
||||
}
|
||||
|
@ -69,6 +69,7 @@ export class VirtualBranchService {
|
||||
);
|
||||
|
||||
this.branches.set(branches);
|
||||
|
||||
this.branchesError.set(undefined);
|
||||
this.logMetrics(branches);
|
||||
|
||||
|
@ -35,16 +35,18 @@
|
||||
|
||||
<div class="container">
|
||||
{#if type === 'Local'}
|
||||
<svg
|
||||
class="local-commit-dot"
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="10" height="10" rx="5" />
|
||||
</svg>
|
||||
<Tooltip text={hoverText}>
|
||||
<svg
|
||||
class="local-commit-dot"
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="10" height="10" rx="5" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
{:else if type === 'LocalShadow'}
|
||||
<div class="local-shadow-commit-dot">
|
||||
<Tooltip text={hoverTextShadow}>
|
||||
|
@ -30,7 +30,7 @@
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
align-items: flex-end;
|
||||
width: 24px;
|
||||
width: 25px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
@ -22,9 +22,9 @@ function generateLineData({
|
||||
const integratedBranchGroups = mapToCommitLineGroupPair(integratedCommits);
|
||||
|
||||
remoteBranchGroups.forEach(({ commit, line }) => {
|
||||
line.top.type = 'LocalRemote';
|
||||
line.bottom.type = 'LocalRemote';
|
||||
line.commitNode = { type: 'LocalRemote', commit };
|
||||
line.top.type = 'Upstream';
|
||||
line.bottom.type = 'Upstream';
|
||||
line.commitNode = { type: 'Upstream', commit };
|
||||
|
||||
// If there are local commits we want to fill in a local dashed line
|
||||
if (localBranchGroups.length > 0) {
|
||||
|
@ -18,6 +18,7 @@
|
||||
"copy-small": "M7.10832 4.75H7.66667C9.18545 4.75 10.4167 5.98122 10.4167 7.5V9.75H11C11.6903 9.75 12.25 9.19036 12.25 8.5V5C12.25 4.30964 11.6903 3.75 11 3.75H8.33331C7.72857 3.75 7.22413 4.17944 7.10832 4.75ZM10.4055 11.25C10.2791 12.6516 9.10118 13.75 7.66667 13.75H5C3.48121 13.75 2.25 12.5188 2.25 11V7.5C2.25 5.98122 3.48122 4.75 5 4.75H5.59452C5.72083 3.34837 6.8988 2.25 8.33331 2.25H11C12.5188 2.25 13.75 3.48122 13.75 5V8.5C13.75 10.0188 12.5188 11.25 11 11.25H10.4055ZM3.75 7.5C3.75 6.80964 4.30964 6.25 5 6.25H7.66667C8.35702 6.25 8.91667 6.80964 8.91667 7.5V11C8.91667 11.6904 8.35702 12.25 7.66667 12.25H5C4.30964 12.25 3.75 11.6904 3.75 11V7.5Z",
|
||||
"cross": "M8.00001 9.06065L12.4194 13.4801L13.4801 12.4194L9.06067 7.99999L13.4801 3.58057L12.4194 2.51991L8.00001 6.93933L3.58059 2.51991L2.51993 3.58057L6.93935 7.99999L2.51993 12.4194L3.58059 13.4801L8.00001 9.06065Z",
|
||||
"cross-small": "M8 9.06066L11.4697 12.5303L12.5303 11.4697L9.06066 8L12.5303 4.53033L11.4697 3.46967L8 6.93934L4.53033 3.46967L3.46967 4.53033L6.93934 8L3.46967 11.4697L4.53033 12.5303L8 9.06066Z",
|
||||
"description": "M3 1.75C2.30964 1.75 1.75 2.30964 1.75 3L1.75 10.5C1.75 11.1904 2.30964 11.75 3 11.75L13 11.75C13.6904 11.75 14.25 11.1904 14.25 10.5L14.25 3C14.25 2.30964 13.6904 1.75 13 1.75L3 1.75ZM0.25 3C0.25 1.48122 1.48122 0.25 3 0.25L13 0.25C14.5188 0.25 15.75 1.48122 15.75 3L15.75 10.5C15.75 12.0188 14.5188 13.25 13 13.25L3 13.25C1.48122 13.25 0.25 12.0188 0.25 10.5L0.25 3ZM12 5.75L4 5.75L4 4.25L12 4.25V5.75ZM4 8.75L12 8.75L12 7.25L4 7.25V8.75Z",
|
||||
"discord": "M13.5535 3.08875C12.5178 2.58012 11.4104 2.21048 10.2526 2C10.1104 2.26983 9.94429 2.63275 9.82976 2.92145C8.599 2.72718 7.37956 2.72718 6.17144 2.92145C6.05693 2.63275 5.88704 2.26983 5.74357 2C4.58454 2.21048 3.47584 2.58148 2.44013 3.09144C0.351095 6.40485 -0.215207 9.63596 0.0679444 12.8212C1.4535 13.9072 2.79627 14.567 4.11638 14.9987C4.44233 14.5278 4.73302 14.0273 4.98345 13.4998C4.5065 13.3096 4.04969 13.0748 3.61805 12.8023C3.73256 12.7133 3.84457 12.6202 3.95279 12.5244C6.58546 13.8168 9.44593 13.8168 12.0472 12.5244C12.1566 12.6202 12.2686 12.7133 12.3819 12.8023C11.949 13.0762 11.4909 13.3109 11.014 13.5012C11.2644 14.0273 11.5538 14.5292 11.881 15C13.2024 14.5683 14.5464 13.9086 15.932 12.8212C16.2642 9.1287 15.3644 5.92726 13.5535 3.08875ZM5.34212 10.8623C4.55181 10.8623 3.9037 10.0879 3.9037 9.14488C3.9037 8.20186 4.53797 7.42612 5.34212 7.42612C6.14628 7.42612 6.79437 8.2005 6.78053 9.14488C6.78178 10.0879 6.14628 10.8623 5.34212 10.8623ZM10.6578 10.8623C9.86752 10.8623 9.21941 10.0879 9.21941 9.14488C9.21941 8.20186 9.85366 7.42612 10.6578 7.42612C11.462 7.42612 12.1101 8.2005 12.0962 9.14488C12.0962 10.0879 11.462 10.8623 10.6578 10.8623Z",
|
||||
"discord-outline": "M7.1875 8.09375C7.1875 9.07427 6.48095 9.86914 5.60937 9.86914C4.7378 9.86914 4.03125 9.07427 4.03125 8.09375C4.03125 7.11323 4.7378 6.31836 5.60937 6.31836C6.48095 6.31836 7.1875 7.11323 7.1875 8.09375Z M10.4062 9.86914C11.2778 9.86914 11.9844 9.07427 11.9844 8.09375C11.9844 7.11323 11.2778 6.31836 10.4062 6.31836C9.53467 6.31836 8.82812 7.11323 8.82812 8.09375C8.82812 9.07427 9.53467 9.86914 10.4062 9.86914Z M9.57024 2.16805C10.8879 2.3997 12.16 2.75884 13.3701 3.33354C15.1841 6.08312 15.9619 9.27384 15.6567 12.5544C14.2682 13.6081 12.7593 14.3558 11.1039 14.8788C10.6836 14.294 10.2765 13.6972 9.95775 13.0496C8.66794 13.3387 7.34539 13.3302 6.05146 13.025C5.72793 13.6841 5.32063 14.2832 4.89477 14.8781C3.24161 14.3553 1.72443 13.6016 0.34286 12.5543C0.0412364 9.27319 0.824091 6.09492 2.62211 3.33711C3.8387 2.75784 5.10281 2.40058 6.42434 2.16851C6.56957 2.43265 6.71893 2.69484 6.85253 2.96515C7.61301 2.88657 8.37771 2.88689 9.1458 2.96572C9.27778 2.69492 9.42614 2.43248 9.57024 2.16805ZM3.25609 12.649C3.55846 11.9971 3.84358 11.339 4.12816 10.6791C6.76234 12.0881 9.31057 12.0966 12.0049 10.6594C12.2571 11.3294 12.5205 11.9613 12.8609 12.5902C13.3088 12.359 13.7614 12.0845 14.2204 11.7557C14.4057 9.02811 13.713 6.64929 12.348 4.51097C11.7228 4.22909 11.0672 4.00661 10.3877 3.84973C10.2747 4.09991 10.1752 4.35621 10.071 4.61012C8.67283 4.3967 7.32313 4.39516 5.93062 4.6117C5.82612 4.35693 5.72686 4.09939 5.61219 3.84895C4.93138 4.00599 4.27444 4.22923 3.64855 4.51206C2.08732 6.98426 1.61928 9.38089 1.77978 11.7556C2.27892 12.1128 2.77032 12.406 3.25609 12.649Z",
|
||||
"docs": "M12 5.75H4V4.25H12V5.75Z M4 8.75H10V7.25H4V8.75Z M5.35241e-05 12.9852L0 3.22C0 2.08102 0 1.51153 0.225173 1.07805C0.414924 0.712765 0.712765 0.414924 1.07805 0.225173C1.51153 0 2.08102 0 3.22 0H16V16H2C1.46143 16 0.972588 15.7871 0.613012 15.4409C0.587144 15.416 0.56195 15.3904 0.537457 15.3642C0.204038 15.0069 0 14.5273 0 14V13L5.35241e-05 12.9852ZM3.22 1.5H14.5V11H2C1.82735 11 1.65981 11.0219 1.5 11.063V3.22C1.5 2.62533 1.50121 2.27105 1.523 2.0086C1.53628 1.84867 1.55378 1.78152 1.55949 1.76346C1.60596 1.67691 1.67691 1.60596 1.76346 1.55949C1.78152 1.55378 1.84867 1.53628 2.0086 1.523C2.27105 1.50121 2.62533 1.5 3.22 1.5ZM3.22 14.5C2.62533 14.5 2.27105 14.4988 2.0086 14.477C1.84867 14.4637 1.78152 14.4462 1.76346 14.4405C1.67691 14.394 1.60596 14.3231 1.55949 14.2365C1.55378 14.2185 1.53628 14.1513 1.523 13.9914C1.50391 13.7615 1.50061 13.4611 1.50009 12.9903C1.50527 12.7186 1.7271 12.5 2 12.5H14.5L14.5 14.5H3.22Z",
|
||||
|
@ -30,6 +30,10 @@
|
||||
--border-color: var(--clr-commit-shadow);
|
||||
}
|
||||
|
||||
&.upstream {
|
||||
--border-color: var(--clr-commit-upstream);
|
||||
}
|
||||
|
||||
&.integrated {
|
||||
--border-color: var(--clr-commit-shadow);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user