Refactor commits section of branch lanes

Fix remote lane line issue
Drop unused file
Fix criteria for showing push button
Fix alignment of avatar for local commit
Fix criteria for showing pus/rebase button
Show tooltip on shadow commit marker hover
Add missing type param to CommitCard
Fix lint
Remove extraneous parameter
Drop a couple of console.log() calls
This commit is contained in:
Mattias Granlund 2024-05-16 00:59:33 +02:00
parent deb1db188b
commit 31f1cb8607
27 changed files with 903 additions and 511 deletions

View File

@ -0,0 +1,37 @@
<script lang="ts">
import type { Author, CommitStatus } from '$lib/vbranches/types';
export let author: Author;
export let status: CommitStatus;
</script>
<img
class="avatar"
title={author.name}
alt="Gravatar for {author.email}"
srcset="{author.gravatarUrl} 2x"
width="100"
height="100"
class:remote={status == 'remote'}
class:local={status == 'local'}
class:upstream={status == 'upstream'}
on:error
/>
<style lang="postcss">
.avatar {
width: var(--size-16);
height: var(--size-16);
border-radius: var(--radius-m);
}
.local {
border: var(--size-2) solid var(--clr-commit-local);
}
.remote {
border: var(--size-2) solid var(--clr-commit-remote);
}
.upstream {
border: var(--size-2) solid var(--clr-commit-upstream);
}
</style>

View File

@ -48,7 +48,12 @@
</Button>
<div class="commits-list">
{#each base.upstreamCommits as commit}
<CommitCard {commit} isUnapplied={true} commitUrl={base.commitUrl(commit.id)} />
<CommitCard
{commit}
isUnapplied={true}
commitUrl={base.commitUrl(commit.id)}
type="upstream"
/>
{/each}
</div>
<Spacer margin={2} />
@ -62,7 +67,7 @@
Local
</h1>
{#each base.recentCommits as commit}
<CommitCard {commit} isUnapplied={true} commitUrl={base.commitUrl(commit.id)} />
<CommitCard {commit} isUnapplied={true} commitUrl={base.commitUrl(commit.id)} type="remote" />
{/each}
</div>
</div>

View File

@ -1,8 +1,9 @@
<script lang="ts">
import BranchCommits from './BranchCommits.svelte';
import BranchFiles from './BranchFiles.svelte';
import BranchFooter from './BranchFooter.svelte';
import BranchHeader from './BranchHeader.svelte';
import CommitDialog from './CommitDialog.svelte';
import CommitList from './CommitList.svelte';
import DropzoneOverlay from './DropzoneOverlay.svelte';
import InfoMessage from './InfoMessage.svelte';
import PullRequestCard from './PullRequestCard.svelte';
@ -282,7 +283,8 @@
{/if}
</div>
<BranchCommits {isUnapplied} />
<CommitList {isUnapplied} />
<BranchFooter {isUnapplied} />
</div>
</ScrollableContainer>
<div class="divider-line">
@ -338,7 +340,6 @@
flex-direction: column;
flex: 1;
min-height: 100%;
gap: var(--size-8);
padding: var(--size-12);
}

View File

@ -1,23 +0,0 @@
<script lang="ts">
import CommitList from './CommitList.svelte';
import {
getIntegratedCommits,
getLocalCommits,
getRemoteCommits,
getUnknownCommits
} from '$lib/vbranches/contexts';
export let isUnapplied: boolean;
const localCommits = getLocalCommits();
const remoteCommits = getRemoteCommits();
const integratedCommits = getIntegratedCommits();
const unknownCommits = getUnknownCommits();
</script>
{#if $unknownCommits && $unknownCommits.length > 0}
<CommitList {isUnapplied} commits={$unknownCommits} type="upstream" />
{/if}
<CommitList {isUnapplied} type="local" commits={$localCommits} />
<CommitList {isUnapplied} type="remote" commits={$remoteCommits} />
<CommitList {isUnapplied} type="integrated" commits={$integratedCommits} />

View File

@ -0,0 +1,65 @@
<script lang="ts">
import PassphraseBox from './PassphraseBox.svelte';
import PushButton, { BranchAction } from './PushButton.svelte';
import { PromptService } from '$lib/backend/prompt';
import { getContext, getContextStore } from '$lib/utils/context';
import { BranchController } from '$lib/vbranches/branchController';
import { getLocalCommits } from '$lib/vbranches/contexts';
import { Branch } from '$lib/vbranches/types';
export let isUnapplied: boolean;
const branchController = getContext(BranchController);
const promptService = getContext(PromptService);
const branch = getContextStore(Branch);
const [prompt, promptError] = promptService.reactToPrompt({
branchId: $branch.id,
timeoutMs: 30000
});
const localCommits = getLocalCommits();
let isLoading: boolean;
$: isPushed = $localCommits.length == 0;
</script>
{#if !isUnapplied}
<div class="actions">
{#if !isPushed}
{#if $prompt}
<PassphraseBox prompt={$prompt} error={$promptError} />
{/if}
<PushButton
wide
branch={$branch}
{isLoading}
on:trigger={async (e) => {
try {
if (e.detail.action == BranchAction.Push) {
isLoading = true;
await branchController.pushBranch($branch.id, $branch.requiresForce);
isLoading = false;
} else if (e.detail.action == BranchAction.Rebase) {
isLoading = true;
await branchController.mergeUpstream($branch.id);
isLoading = false;
}
} catch (e) {
console.error(e);
}
}}
/>
{:else}
Branch origin/second-branch is up to date with the remote.
{/if}
</div>
{/if}
<style lang="postcss">
.actions {
background: var(--clr-bg-1);
padding: var(--size-16);
border-radius: 0 0 var(--radius-m) var(--radius-m);
border: 1px solid var(--clr-border-2);
}
</style>

View File

@ -2,16 +2,21 @@
import ActiveBranchStatus from './ActiveBranchStatus.svelte';
import BranchLabel from './BranchLabel.svelte';
import BranchLanePopupMenu from './BranchLanePopupMenu.svelte';
import PullRequestButton from './PullRequestButton.svelte';
import Tag from './Tag.svelte';
import { Project } from '$lib/backend/projects';
import { BranchService } from '$lib/branches/service';
import { clickOutside } from '$lib/clickOutside';
import Button from '$lib/components/Button.svelte';
import Icon from '$lib/components/Icon.svelte';
import { GitHubService } from '$lib/github/service';
import { showError } from '$lib/notifications/toasts';
import { normalizeBranchName } from '$lib/utils/branch';
import { getContext, getContextStore } from '$lib/utils/context';
import { BranchController } from '$lib/vbranches/branchController';
import { Branch } from '$lib/vbranches/types';
import { BaseBranch, Branch } from '$lib/vbranches/types';
import toast from 'svelte-french-toast';
import type { PullRequest } from '$lib/github/types';
import type { Persisted } from '$lib/persisted/persisted';
import { goto } from '$app/navigation';
@ -20,16 +25,21 @@
export let isLaneCollapsed: Persisted<boolean>;
const branchController = getContext(BranchController);
const githubService = getContext(GitHubService);
const branchStore = getContextStore(Branch);
const project = getContext(Project);
const branchService = getContext(BranchService);
const baseBranch = getContextStore(BaseBranch);
$: branch = $branchStore;
$: hasPullRequest = branch.upstreamName && githubService.hasPr(branch.upstreamName);
let meatballButton: HTMLDivElement;
let visible = false;
let isApplying = false;
let isDeleting = false;
let branchName = branch?.upstreamName || normalizeBranchName($branchStore.name);
let isLoading: boolean;
function handleBranchNameChange(title: string) {
if (title == '') return;
@ -49,6 +59,34 @@
$: hasIntegratedCommits = branch.commits?.some((b) => b.isIntegrated);
let headerInfoHeight = 0;
interface CreatePrOpts {
draft: boolean;
}
const defaultPrOpts: CreatePrOpts = {
draft: true
};
async function createPr(createPrOpts: CreatePrOpts): Promise<PullRequest | undefined> {
const opts = { ...defaultPrOpts, ...createPrOpts };
if (!githubService.isEnabled) {
toast.error('Cannot create PR without GitHub credentials');
return;
}
if (!$baseBranch?.shortName) {
toast.error('Cannot create PR without base branch');
return;
}
isLoading = true;
try {
return await branchService.createPr(branch, $baseBranch.shortName, opts.draft);
} finally {
isLoading = false;
}
}
</script>
{#if $isLaneCollapsed}
@ -215,6 +253,12 @@
</Button>
{:else}
<div class="header__buttons">
{#if !hasPullRequest}
<PullRequestButton
on:click={async (e) => await createPr({ draft: e.detail.action == 'draft' })}
loading={isLoading}
/>
{/if}
<Button
style="ghost"
kind="solid"
@ -253,6 +297,7 @@
z-index: var(--z-lifted);
position: sticky;
top: var(--size-12);
padding-bottom: var(--size-8);
}
.header {
z-index: var(--z-lifted);

View File

@ -10,7 +10,6 @@
import { getContext, getContextStoreBySymbol, createContextStore } from '$lib/utils/context';
import { isDefined } from '$lib/utils/typeguards';
import {
createIntegratedContextStore,
createLocalContextStore,
createRemoteContextStore,
createSelectedFiles,
@ -18,7 +17,7 @@
} from '$lib/vbranches/contexts';
import { FileIdSelection } from '$lib/vbranches/fileIdSelection';
import { Ownership } from '$lib/vbranches/ownership';
import { RemoteFile, Branch } from '$lib/vbranches/types';
import { RemoteFile, Branch, commitCompare, RemoteCommit } from '$lib/vbranches/types';
import lscache from 'lscache';
import { setContext } from 'svelte';
import { quintOut } from 'svelte/easing';
@ -40,11 +39,8 @@
const remoteCommits = createRemoteContextStore(branch.remoteCommits);
$: remoteCommits.set(branch.remoteCommits);
const integratedCommits = createIntegratedContextStore(branch.integratedCommits);
$: integratedCommits.set(branch.integratedCommits);
// Set the store immediately so it can be updated later.
const unknownCommits = createUnknownContextStore([]);
const upstreamCommits = createUnknownContextStore([]);
$: if (branch.upstream?.name) loadRemoteBranch(branch.upstream?.name);
const fileIdSelection = new FileIdSelection();
@ -63,11 +59,20 @@
$: displayedFile = $selectedFiles.length == 1 ? $selectedFiles[0] : undefined;
async function loadRemoteBranch(name: string) {
const remoteBranchData = await getRemoteBranchData(project.id, name);
const commits = remoteBranchData?.commits.filter(
(remoteCommit) => !branch.commits.find((commit) => remoteCommit.id == commit.id)
);
unknownCommits.set(commits);
const upstream = await getRemoteBranchData(project.id, name);
if (!upstream.commits) return;
const unknownCommits: RemoteCommit[] = [];
upstream?.commits.forEach((upstreamCommit) => {
let match = branch.commits.find((commit) => commitCompare(upstreamCommit, commit));
if (match) {
match.relatedTo = upstreamCommit;
} else unknownCommits.push(upstreamCommit);
});
if (upstream.commits.length != unknownCommits.length) {
// Force update since we've mutated local commits
localCommits.set($localCommits);
}
upstreamCommits.set(unknownCommits);
}
const project = getContext(Project);
@ -88,12 +93,7 @@
}
</script>
<div
class="wrapper"
data-tauri-drag-region
class:target-branch={branch.active && branch.selectedForChanges}
class:file-selected={displayedFile}
>
<div class="wrapper" data-tauri-drag-region class:file-selected={displayedFile}>
<BranchCard {isUnapplied} {commitBoxOpen} bind:isLaneCollapsed />
{#if displayedFile}
@ -140,10 +140,6 @@
background-color: var(--target-branch-background);
}
.target-branch {
--target-branch-background: color-mix(in srgb, var(--clr-scale-pop-60) 20%, var(--clr-bg-2));
}
.file-preview {
display: flex;
position: relative;

View File

@ -37,7 +37,6 @@
$: branch = $branchStore;
$: commits = branch.commits;
$: setAIConfigurationValid($user);
$: hasIntegratedCommits = branch.integratedCommits.length > 0;
async function setAIConfigurationValid(user: User | undefined) {
aiConfigurationValid = await aiService.validateConfiguration(user?.access_token);
@ -92,7 +91,7 @@
<ContextMenuSection>
<ContextMenuItem
label="Set remote branch name"
disabled={isUnapplied || hasIntegratedCommits}
disabled={isUnapplied}
on:click={() => {
newRemoteName = branch.upstreamName || normalizeBranchName(branch.name) || '';
close();

View File

@ -1,11 +1,11 @@
<script lang="ts">
import BranchFilesList from './BranchFilesList.svelte';
import Icon from './Icon.svelte';
import { Project } from '$lib/backend/projects';
import Button from '$lib/components/Button.svelte';
import CommitMessageInput from '$lib/components/CommitMessageInput.svelte';
import Modal from '$lib/components/Modal.svelte';
import Tag from '$lib/components/Tag.svelte';
import TimeAgo from '$lib/components/TimeAgo.svelte';
import { persistedCommitMessage } from '$lib/config/config';
import { featureAdvancedCommitOperations } from '$lib/config/uiFeatureFlags';
import { draggable } from '$lib/dragging/draggable';
@ -16,7 +16,14 @@
import { createCommitStore, getSelectedFiles } from '$lib/vbranches/contexts';
import { FileIdSelection } from '$lib/vbranches/fileIdSelection';
import { listRemoteCommitFiles } from '$lib/vbranches/remoteCommits';
import { RemoteCommit, Commit, RemoteFile, Branch, BaseBranch } from '$lib/vbranches/types';
import {
RemoteCommit,
Commit,
RemoteFile,
Branch,
BaseBranch,
type CommitStatus
} from '$lib/vbranches/types';
import { slide } from 'svelte/transition';
export let branch: Branch | undefined = undefined;
@ -24,6 +31,9 @@
export let commitUrl: string | undefined = undefined;
export let isHeadCommit: boolean = false;
export let isUnapplied = false;
export let first = false;
export let last = false;
export let type: CommitStatus;
const branchController = getContext(BranchController);
const baseBranch = getContextStore(BaseBranch);
@ -140,8 +150,30 @@
: nonDraggable()}
class="commit"
class:is-commit-open={showFiles}
class:is-first={first}
class:is-last={last}
>
<div
class="accent"
class:is-first={first}
class:is-last={last}
class:local={type == 'local'}
class:remote={type == 'remote'}
class:upstream={type == 'upstream'}
></div>
<div class="commit__header" on:click={toggleFiles} on:keyup={onKeyup} role="button" tabindex="0">
{#if first}
<div class="commit__type text-semibold text-base-12">
{#if type == 'remote'}
Local and remote <Icon name="local-remote" />
{:else if type == 'local'}
Local <Icon name="local" />
{:else if type == 'upstream'}
Remote upstream <Icon name="remote" />
{/if}
</div>
{/if}
<div class="commit__message">
{#if $advancedCommitOperations}
<div class="commit__id">
@ -204,23 +236,9 @@
{/if}
{/if}
</div>
<div class="commit__row">
<div class="commit__author">
<img
class="commit__avatar"
title="Gravatar for {commit.author.email}"
alt="Gravatar for {commit.author.email}"
srcset="{commit.author.gravatarUrl} 2x"
width="100"
height="100"
on:error
/>
<span class="commit__author-name text-base-12 truncate">{commit.author.name}</span>
</div>
<span class="commit__time text-base-11">
<!-- <span class="commit__time text-base-11">
<TimeAgo date={commit.createdAt} />
</span>
</div>
</span> -->
</div>
{#if showFiles}
@ -308,19 +326,51 @@
.commit {
display: flex;
position: relative;
flex-direction: column;
border-radius: var(--size-6);
background-color: var(--clr-bg-1);
border: 1px solid var(--clr-border-2);
overflow: hidden;
transition: background-color var(--transition-fast);
&.is-first {
border-top-left-radius: var(--radius-m);
border-top-right-radius: var(--radius-m);
}
&.is-last {
border-bottom-left-radius: var(--radius-m);
border-bottom-right-radius: var(--radius-m);
}
&:not(.is-first):not(.is-commit-open) {
border-top: none;
}
&:not(.is-commit-open):hover {
background-color: var(--clr-bg-2);
}
}
.accent {
position: absolute;
width: var(--size-4);
height: 100%;
&.local {
background-color: var(--clr-commit-local);
}
&.remote {
background-color: var(--clr-commit-remote);
}
&.upstream {
background-color: var(--clr-commit-upstream);
}
&.is-first {
border-top-left-radius: var(--radius-m);
}
&.is-last {
border-bottom-left-radius: var(--radius-m);
}
}
.commit__header {
cursor: pointer;
display: flex;
@ -329,8 +379,13 @@
padding: var(--size-14);
}
.commit__type {
opacity: 0.4;
}
.is-commit-open {
background-color: var(--clr-bg-2);
margin: 0.5rem 0;
& .commit__header {
padding-bottom: var(--size-16);
@ -390,29 +445,6 @@
margin-bottom: -8px;
}
.commit__author {
display: block;
flex: 1;
display: flex;
align-items: center;
gap: var(--size-6);
}
.commit__avatar {
width: var(--size-16);
height: var(--size-16);
border-radius: 100%;
}
.commit__author-name {
max-width: calc(100% - var(--size-16));
}
.commit__time,
.commit__author-name {
color: var(--clr-scale-ntrl-50);
}
.files-container {
background-color: var(--clr-bg-1);
padding: 0 var(--size-14) var(--size-14);

View File

@ -65,7 +65,7 @@
</Button>
{/if}
<Button
style="pop"
style="neutral"
kind="solid"
grow
loading={isCommitting}

View File

@ -0,0 +1,55 @@
<script lang="ts">
import LocalLine from './LocalLine.svelte';
import RemoteLine from './RemoteLine.svelte';
import ShadowLine from './ShadowLine.svelte';
import type { Commit, RemoteCommit } from '$lib/vbranches/types';
export let hasShadowColumn = false;
export let hasLocalColumn = false;
export let localCommit: Commit | undefined = undefined;
export let remoteCommit: RemoteCommit | undefined = undefined;
export let first = false;
export let localLine = false;
export let remoteLine = false;
export let upstreamLine = false;
export let shadowLine = false;
export let base = false;
</script>
<div class="lines" class:base>
{#if hasShadowColumn}
<ShadowLine
line={shadowLine}
dashed={base}
{upstreamLine}
{remoteCommit}
{localCommit}
{first}
/>
{/if}
<RemoteLine
commit={localCommit?.status == 'remote' ? localCommit : undefined}
line={localCommit?.status == 'remote' || remoteLine}
root={localLine}
{first}
{base}
/>
{#if hasLocalColumn}
<LocalLine
commit={localCommit?.status == 'local' ? localCommit : undefined}
dashed={localLine}
{first}
/>
{/if}
</div>
<style lang="postcss">
.lines {
display: flex;
align-items: stretch;
min-height: var(--size-16);
&.base {
height: var(--size-40);
}
}
</style>

View File

@ -1,65 +1,128 @@
<script lang="ts">
import CommitListFooter from './CommitListFooter.svelte';
import CommitListHeader from './CommitListHeader.svelte';
import CommitCard from './CommitCard.svelte';
import CommitLines from './CommitLines.svelte';
import CommitListItem from './CommitListItem.svelte';
import { getContext, getContextStore } from '$lib/utils/context';
import { Branch, type Commit, type CommitStatus, type RemoteCommit } from '$lib/vbranches/types';
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
import { map } from 'rxjs';
import { getContextStore } from '$lib/utils/context';
import { getLocalCommits, getRemoteCommits, getUnknownCommits } from '$lib/vbranches/contexts';
import { BaseBranch, Branch } from '$lib/vbranches/types';
export let type: CommitStatus;
export let isUnapplied: boolean;
export let commits: Commit[] | RemoteCommit[];
const branchService = getContext(VirtualBranchService);
const branch = getContextStore(Branch);
const localCommits = getLocalCommits();
const remoteCommits = getRemoteCommits();
const unknownCommits = getUnknownCommits();
const baseBranch = getContextStore(BaseBranch);
let headerHeight: number;
let expanded = true;
$: headCommit = $branch.commits[0];
$: hasCommits = commits && commits.length > 0;
$: remoteRequiresForcePush = type === 'remote' && $branch.requiresForce;
$: branchCount = branchService.activeBranches$.pipe(map((branches) => branches?.length || 0));
$: hasShadowColumn = $localCommits.some((c) => !!c.relatedTo);
$: hasLocalColumn = $localCommits.length > 0;
$: hasCommits = $branch.commits && $branch.commits.length > 0;
$: headCommit = $branch.commits.at(0);
$: hasUnknownCommits = $unknownCommits.length > 0;
</script>
{#if hasCommits || remoteRequiresForcePush}
<div class="commit-list card" class:upstream={type == 'upstream'}>
<CommitListHeader
{type}
bind:expanded
bind:height={headerHeight}
isExpandable={hasCommits}
commitCount={commits.length}
/>
{#if expanded}
<div class="commit-list__content">
{#if hasCommits}
<div class="commit-list__content">
<div class="title text-base-13 text-semibold"></div>
<div class="commits">
{#each commits as commit, idx (commit.id)}
<CommitListItem
{commit}
{isUnapplied}
isChained={idx != commits.length - 1}
isHeadCommit={commit.id === headCommit?.id}
{#if $unknownCommits.length > 0}
<CommitLines {hasShadowColumn} {hasLocalColumn} localLine />
{#each $unknownCommits as commit, idx (commit.id)}
<div class="flex">
<CommitLines
{hasLocalColumn}
{hasShadowColumn}
first={idx == 0}
localLine={hasLocalColumn}
remoteCommit={commit}
upstreamLine
/>
<CommitListItem {commit}>
<CommitCard
branch={$branch}
{commit}
commitUrl={$baseBranch?.commitUrl(commit.id)}
isHeadCommit={commit.id === headCommit?.id}
{isUnapplied}
first={idx == 0}
last={idx == $unknownCommits.length - 1}
type="upstream"
/>
</CommitListItem>
</div>
{/each}
</div>
{/if}
{#if type == 'upstream' && $branchCount > 1}
<div class="upstream-message text-base-body-11">
You have {$branchCount} active branches. To merge upstream work, we will unapply all other
branches.
</div>{/if}
<CommitListFooter {type} {isUnapplied} {hasCommits} />
{#if $localCommits.length > 0}
<CommitLines
{hasShadowColumn}
{hasLocalColumn}
upstreamLine={hasUnknownCommits}
localLine
/>
{#each $localCommits as commit, idx (commit.id)}
<div class="flex">
<CommitLines
{hasLocalColumn}
{hasShadowColumn}
localCommit={commit}
shadowLine={hasShadowColumn}
first={idx == 0}
upstreamLine={hasUnknownCommits}
/>
<CommitListItem {commit}>
<CommitCard
branch={$branch}
{commit}
commitUrl={$baseBranch?.commitUrl(commit.id)}
isHeadCommit={commit.id === headCommit?.id}
{isUnapplied}
first={idx == 0}
last={idx == $localCommits.length - 1}
type="local"
/>
</CommitListItem>
</div>
{/each}
{/if}
{#if $remoteCommits.length > 0}
<CommitLines {hasShadowColumn} {hasLocalColumn} localLine />
{#each $remoteCommits as commit, idx (commit.id)}
<div class="flex">
<CommitLines
{hasLocalColumn}
{hasShadowColumn}
localCommit={commit}
first={idx == 0}
upstreamLine={hasUnknownCommits}
/>
<CommitListItem {commit}>
<CommitCard
branch={$branch}
{commit}
commitUrl={$baseBranch?.commitUrl(commit.id)}
isHeadCommit={commit.id === headCommit?.id}
{isUnapplied}
first={idx == 0}
last={idx == $remoteCommits.length - 1}
type="remote"
/>
</CommitListItem>
</div>
{/each}
{/if}
<CommitLines
{hasShadowColumn}
localLine={$remoteCommits.length == 0 && $localCommits.length > 0}
remoteLine={$remoteCommits.length > 0}
shadowLine={hasShadowColumn}
base
/>
</div>
</div>
{/if}
<style lang="postcss">
.commit-list {
/* .commit-list {
&.upstream {
background-color: var(--clr-bg-2);
}
@ -68,19 +131,10 @@
flex-direction: column;
position: relative;
flex-shrink: 0;
}
} */
.commit-list__content {
display: flex;
flex-direction: column;
padding: 0 var(--size-14) var(--size-14) var(--size-14);
gap: var(--size-8);
}
.upstream-message {
color: var(--clr-scale-warn-30);
border-radius: var(--radius-m);
background: var(--clr-scale-warn-80);
padding: var(--size-12);
margin-left: var(--size-16);
}
.commits {

View File

@ -1,149 +0,0 @@
<script lang="ts">
import PassphraseBox from './PassphraseBox.svelte';
import PushButton, { BranchAction } from './PushButton.svelte';
import { PromptService } from '$lib/backend/prompt';
import { BranchService } from '$lib/branches/service';
import Button from '$lib/components/Button.svelte';
import { GitHubService } from '$lib/github/service';
import { getContext, getContextStore } from '$lib/utils/context';
import { BranchController } from '$lib/vbranches/branchController';
import { BaseBranch, Branch, type CommitStatus } from '$lib/vbranches/types';
import toast from 'svelte-french-toast';
import type { PullRequest } from '$lib/github/types';
export let type: CommitStatus;
export let isUnapplied: boolean;
export let hasCommits: boolean;
const branchService = getContext(BranchService);
const githubService = getContext(GitHubService);
const branchController = getContext(BranchController);
const promptService = getContext(PromptService);
const baseBranch = getContextStore(BaseBranch);
const branch = getContextStore(Branch);
const [prompt, promptError] = promptService.reactToPrompt({
branchId: $branch.id,
timeoutMs: 30000
});
$: githubServiceState$ = githubService.getState($branch.id);
$: pr$ = githubService.getPr$($branch.upstreamName);
let isPushing: boolean;
let isMerging: boolean;
interface CreatePrOpts {
draft: boolean;
}
const defaultPrOpts: CreatePrOpts = {
draft: true
};
async function push() {
isPushing = true;
await branchController.pushBranch($branch.id, $branch.requiresForce);
isPushing = false;
}
async function createPr(createPrOpts: CreatePrOpts): Promise<PullRequest | undefined> {
const opts = { ...defaultPrOpts, ...createPrOpts };
if (!githubService.isEnabled) {
toast.error('Cannot create PR without GitHub credentials');
return;
}
if (!$baseBranch?.shortName) {
toast.error('Cannot create PR without base branch');
return;
}
isPushing = true;
try {
return await branchService.createPr($branch, $baseBranch.shortName, opts.draft);
} finally {
isPushing = false;
}
}
</script>
{#if !isUnapplied && type != 'integrated'}
<div class="actions" class:hasCommits>
{#if $prompt && type == 'local'}
<PassphraseBox prompt={$prompt} error={$promptError} />
{:else if githubService.isEnabled && (type == 'local' || type == 'remote')}
<PushButton
wide
isLoading={isPushing || $githubServiceState$?.busy}
isPr={!!$pr$}
{type}
branch={$branch}
githubEnabled={true}
on:trigger={async (e) => {
try {
if (e.detail.action == BranchAction.Pr) {
await createPr({ draft: false });
} else if (e.detail.action == BranchAction.DraftPr) {
await createPr({ draft: true });
} else {
await push();
}
} catch (e) {
console.error(e);
}
}}
/>
{:else if type == 'local'}
<Button
style="ghost"
kind="solid"
wide
loading={isPushing}
on:click={async () => {
try {
await push();
} catch {
toast.error('Failed to push');
}
}}
>
{#if $branch.requiresForce}
Force Push
{:else}
Push
{/if}
</Button>
{:else if type == 'upstream'}
<Button
style="warning"
kind="solid"
wide
loading={isMerging}
on:click={async () => {
isMerging = true;
try {
await branchController.mergeUpstream($branch.id);
} catch (err) {
toast.error('Failed to merge upstream commits');
} finally {
isMerging = false;
}
}}
>
Merge upstream commits
</Button>
{/if}
</div>
{/if}
<style lang="postcss">
.hasCommits {
padding-left: var(--size-16);
}
.actions {
&:empty {
display: none;
}
}
</style>

View File

@ -1,64 +0,0 @@
<script lang="ts">
import Icon from '$lib/components/Icon.svelte';
import { onMount } from 'svelte';
import type { CommitStatus } from '$lib/vbranches/types';
export let isExpandable = true;
export let expanded: boolean;
export let type: CommitStatus;
export let height: number | undefined;
export let commitCount = 0;
let element: HTMLButtonElement | undefined = undefined;
onMount(() => (height = element?.offsetHeight));
</script>
<button class="header" bind:this={element} on:click={() => (expanded = !expanded)}>
<div class="title text-base-13 text-semibold">
{#if type == 'local'}
Local
{:else if type == 'remote'}
Remote branch
{:else if type == 'integrated'}
Integrated
{:else if type == 'upstream'}
{commitCount} upstream {commitCount == 1 ? 'commit' : 'commits'}
<Icon name="warning" color="warning" />
{/if}
</div>
{#if isExpandable}
<div class="expander">
<Icon name={expanded ? 'chevron-down' : 'chevron-top'} />
</div>
{/if}
</button>
<style lang="postcss">
.header {
display: flex;
align-items: center;
padding: var(--size-16) var(--size-14) var(--size-16) var(--size-14);
justify-content: space-between;
gap: var(--size-8);
&:hover {
& .expander {
opacity: 1;
}
}
}
.title {
display: flex;
align-items: center;
color: var(--clr-scale-ntrl-0);
gap: var(--size-8);
overflow-x: hidden;
}
.expander {
color: var(--clr-scale-ntrl-50);
opacity: 0.5;
transition: opacity var(--transition-fast);
}
</style>

View File

@ -1,5 +1,4 @@
<script lang="ts">
import CommitCard from './CommitCard.svelte';
import DropzoneOverlay from './DropzoneOverlay.svelte';
import { Project } from '$lib/backend/projects';
import { DraggableCommit, DraggableFile, DraggableHunk } from '$lib/dragging/draggables';
@ -7,22 +6,11 @@
import { getContext, getContextStore } from '$lib/utils/context';
import { BranchController } from '$lib/vbranches/branchController';
import { filesToOwnership, filesToSimpleOwnership } from '$lib/vbranches/ownership';
import {
RemoteCommit,
Branch,
type Commit,
BaseBranch,
LocalFile,
RemoteFile
} from '$lib/vbranches/types';
import { RemoteCommit, Branch, Commit, LocalFile, RemoteFile } from '$lib/vbranches/types';
export let commit: Commit | RemoteCommit;
export let isHeadCommit: boolean;
export let isChained: boolean;
export let isUnapplied = false;
const branchController = getContext(BranchController);
const baseBranch = getContextStore(BaseBranch);
const project = getContext(Project);
const branch = getContextStore(Branch);
@ -107,10 +95,6 @@
</script>
<div class="commit-list-item">
{#if isChained}
<div class="line" />
{/if}
<div class="connector" />
<div
class="commit-card-wrapper"
use:dropzone={{
@ -130,45 +114,21 @@
<DropzoneOverlay class="amend-dz-marker" label="Amend" />
<DropzoneOverlay class="squash-dz-marker" label="Squash" />
<CommitCard
branch={$branch}
{commit}
commitUrl={$baseBranch?.commitUrl(commit.id)}
{isHeadCommit}
{isUnapplied}
/>
<slot />
</div>
</div>
<style>
.commit-list-item {
display: flex;
padding: 0 0 var(--size-6) var(--size-16);
position: relative;
padding: 0;
gap: var(--size-8);
flex-grow: 1;
&:last-child {
padding-bottom: 0;
}
}
.line {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 1px;
background-color: var(--clr-border-2);
}
.connector {
width: 1rem;
height: 1.125rem;
position: absolute;
top: 0;
left: 0;
border-bottom: 1px solid var(--clr-border-2);
border-left: 1px solid var(--clr-border-2);
border-radius: 0 0 0 0.5rem;
}
.commit-card-wrapper {
position: relative;
width: 100%;

View File

@ -0,0 +1,90 @@
<script lang="ts">
import Avatar from './Avatar.svelte';
import type { Commit } from '$lib/vbranches/types';
export let dashed: boolean;
export let commit: Commit | undefined;
export let first: boolean;
$: hasRoot = isRoot(commit);
function isRoot(commit: Commit | undefined): boolean {
return !!commit && (commit.parent == undefined || commit.parent?.status == 'remote');
}
</script>
<div class="local-column">
{#if !commit && dashed}
<div class="local-line dashed"></div>
{:else if commit}
{#if first}
<div class="local-line dashed tip" />
{/if}
<div class="local-line" class:has-root={hasRoot} class:short={first} />
{/if}
{#if commit}
{@const author = commit.author}
<div class="avatar" class:first>
<Avatar {author} status={commit.status} />
</div>
{/if}
{#if hasRoot}
<div class="root" class:long-root={commit?.parent} />
{/if}
</div>
<style lang="postcss">
.local-column {
position: relative;
width: var(--size-16);
}
.avatar {
position: absolute;
top: var(--size-12);
left: calc(-1 * var(--size-4));
&.first {
top: 2.625rem;
}
}
.local-line {
position: absolute;
width: var(--size-2);
background-color: var(--clr-commit-local);
left: var(--size-4);
top: 0;
bottom: 0;
&.dashed {
background: repeating-linear-gradient(
0,
transparent,
transparent 0.1875rem,
var(--clr-commit-local) 0.1875rem,
var(--clr-commit-local) 0.4375rem
);
}
&.has-root {
bottom: var(--size-8);
}
&.tip {
bottom: calc(100% - 2.625rem);
}
&.short {
top: 2.625rem;
}
}
.root {
position: absolute;
width: var(--size-10);
top: calc(100% - var(--size-12));
left: calc(-1 * var(--size-4));
bottom: calc(-1 * var(--size-2));
border-radius: 0 0 var(--radius-l) 0;
border-color: var(--clr-commit-local);
border-width: 0 var(--size-2) var(--size-2) 0;
&.long-root {
bottom: -3rem;
}
}
</style>

View File

@ -0,0 +1,68 @@
<script lang="ts">
import DropDownButton from './DropDownButton.svelte';
import ContextMenu from './contextmenu/ContextMenu.svelte';
import ContextMenuItem from './contextmenu/ContextMenuItem.svelte';
import ContextMenuSection from './contextmenu/ContextMenuSection.svelte';
import { persisted, type Persisted } from '$lib/persisted/persisted';
import * as toasts from '$lib/utils/toasts';
import { createEventDispatcher } from 'svelte';
enum Action {
Create = 'create',
Draft = 'draft'
}
const dispatch = createEventDispatcher<{ click: { action: Action } }>();
const action = defaultAction();
export let loading = false;
let dropDown: DropDownButton;
let contextMenu: ContextMenu;
$: selection$ = contextMenu?.selection$;
function defaultAction(): Persisted<Action> {
const key = 'projectDefaultPrAction';
return persisted<Action>(Action.Create, key);
}
</script>
<DropDownButton
style="ghost"
kind="solid"
{loading}
bind:this={dropDown}
on:click={() => {
dispatch('click', { action: $action });
}}
>
{$selection$?.label}
<ContextMenu
type="select"
slot="context-menu"
bind:this={contextMenu}
on:select={(e) => {
// TODO: Refactor to use generics if/when that works with Svelte
switch (e.detail?.id) {
case Action.Create:
$action = Action.Create;
break;
case Action.Draft:
$action = Action.Draft;
break;
default:
toasts.error('Unknown merge method');
}
dropDown.close();
}}
>
<ContextMenuSection>
<ContextMenuItem id={Action.Create} label="Create PR" selected={$action == Action.Create} />
<ContextMenuItem
id={Action.Draft}
label="Create Draft PR"
selected={$action == Action.Draft}
/>
</ContextMenuSection>
</ContextMenu>
</DropDownButton>

View File

@ -255,7 +255,7 @@
}}
/>
</div>
<div class="pr-title text-base-13 font-semibold">
<div class="pr-title text-base-13 text-semibold">
<span style="color: var(--clr-scale-ntrl-50)">PR #{pr.number}:</span>
{pr.title}
</div>
@ -350,6 +350,7 @@
.pr-card {
position: relative;
padding: var(--size-14);
margin-bottom: var(--size-8);
}
.pr-title {

View File

@ -1,14 +1,12 @@
<script lang="ts" context="module">
export enum BranchAction {
Push = 'push',
Pr = 'pr',
DraftPr = 'draftPr'
Rebase = 'rebase'
}
</script>
<script lang="ts">
import { Project } from '$lib/backend/projects';
import Button from '$lib/components/Button.svelte';
import DropDownButton from '$lib/components/DropDownButton.svelte';
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
@ -16,17 +14,17 @@
import { persisted, type Persisted } from '$lib/persisted/persisted';
import { getContext } from '$lib/utils/context';
import * as toasts from '$lib/utils/toasts';
import { getLocalCommits, getUnknownCommits } from '$lib/vbranches/contexts';
import { createEventDispatcher } from 'svelte';
import type { Branch } from '$lib/vbranches/types';
export let type: string;
export let isLoading = false;
export let githubEnabled: boolean;
export let wide = false;
export let branch: Branch;
export let isPr = false;
const project = getContext(Project);
const localCommits = getLocalCommits();
const unknownCommits = getUnknownCommits();
function defaultAction(): Persisted<BranchAction> {
const key = 'projectDefaultAction_';
@ -39,49 +37,27 @@
let contextMenu: ContextMenu;
let dropDown: DropDownButton;
let disabled = false;
let isPushed = $localCommits.length == 0 && !branch.requiresForce;
$: canBeRebased = $unknownCommits.length > 0;
$: selection$ = contextMenu?.selection$;
$: action = selectAction(isPushed, $preferredAction);
let action!: BranchAction;
$: {
isPushed; // selectAction is dependant on isPushed
action = selectAction($preferredAction);
}
function selectAction(preferredAction: BranchAction) {
if (isPushed && !githubEnabled) {
function selectAction(isPushed: boolean, preferredAction: BranchAction) {
// TODO: Refactor such that this is not necessary
console.log('No push actions possible');
return BranchAction.Push;
} else if (isPushed) {
if (preferredAction == BranchAction.Push) return BranchAction.Pr;
return preferredAction;
} else if (!githubEnabled) {
if (isPushed) {
return BranchAction.Rebase;
} else if (!branch.requiresForce) {
return BranchAction.Push;
}
return preferredAction;
}
$: pushLabel = branch.requiresForce ? 'Force push to remote' : 'Push to remote';
$: commits = branch.commits.filter((c) => c.status == type);
$: isPushed = type === 'remote' && !branch.requiresForce;
$: pushLabel = branch.requiresForce ? 'Force push' : 'Push';
</script>
{#if (isPr || commits.length === 0) && !isPushed}
<Button
style="ghost"
kind="solid"
{wide}
disabled={isPushed}
loading={isLoading}
on:click={() => {
dispatch('trigger', { action: BranchAction.Push });
}}>{pushLabel}</Button
>
{:else if !isPr}
<DropDownButton
style="ghost"
kind="solid"
style="pop"
kind="soft"
loading={isLoading}
bind:this={dropDown}
{wide}
@ -101,11 +77,8 @@
case BranchAction.Push:
$preferredAction = BranchAction.Push;
break;
case BranchAction.Pr:
$preferredAction = BranchAction.Pr;
break;
case BranchAction.DraftPr:
$preferredAction = BranchAction.DraftPr;
case BranchAction.Rebase:
$preferredAction = BranchAction.Rebase;
break;
default:
toasts.error('Uknown branch action');
@ -114,25 +87,30 @@
}}
>
<ContextMenuSection>
{#if !isPushed}
<ContextMenuItem
id="push"
label={pushLabel}
selected={action == BranchAction.Push}
disabled={isPushed}
/>
{/if}
{#if !branch.requiresForce}
<ContextMenuItem
id="pr"
label="Create pull request"
disabled={!githubEnabled}
selected={action == BranchAction.Pr}
id="rebase"
label="Rebase upstream"
selected={action == BranchAction.Rebase}
disabled={isPushed}
/>
{/if}
{#if canBeRebased}
<ContextMenuItem
id="draftPr"
label="Create draft pull request"
disabled={!githubEnabled}
selected={action == BranchAction.DraftPr}
id="rebase"
label="Rebase upstream"
selected={action == BranchAction.Rebase}
disabled={isPushed}
/>
{/if}
</ContextMenuSection>
</ContextMenu>
</DropDownButton>
{/if}

View File

@ -68,7 +68,7 @@
{#if branchData.commits && branchData.commits.length > 0}
<div class="branch-preview__commits-list">
{#each branchData.commits as commit (commit.id)}
<CommitCard {commit} commitUrl={$baseBranch?.commitUrl(commit.id)} />
<CommitCard {commit} commitUrl={$baseBranch?.commitUrl(commit.id)} type="remote" />
{/each}
</div>
{/if}

View File

@ -0,0 +1,123 @@
<script lang="ts">
import Avatar from './Avatar.svelte';
import type { Commit } from '$lib/vbranches/types';
export let commit: Commit | undefined;
export let base: boolean;
export let first: boolean;
export let line: boolean;
export let root: boolean;
$: hasRoot = isRoot(commit);
function isRoot(commit: Commit | undefined): boolean {
return commit?.status == 'remote' && commit?.children?.[0]?.status == 'local';
}
</script>
<div class="remote-column" class:has-root={hasRoot}>
{#if base}
<div class="remote-line dashed" class:short={!line} />
{#if root}
<div class="root base" />
{/if}
<div class="commit-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.32501 7.25C4.67247 5.53832 6.18578 4.25 8 4.25C9.81422 4.25 11.3275 5.53832 11.675 7.25H14V8.75H11.675C11.3275 10.4617 9.81422 11.75 8 11.75C6.18578 11.75 4.67247 10.4617 4.32501 8.75H2V7.25H4.32501ZM8 5.75C6.75736 5.75 5.75 6.75736 5.75 8C5.75 9.24264 6.75736 10.25 8 10.25C9.24264 10.25 10.25 9.24264 10.25 8C10.25 6.75736 9.24264 5.75 8 5.75Z"
fill="white"
/>
</svg>
</div>
{:else}
{#if line}
<div class="remote-line" class:first></div>
{/if}
{#if hasRoot}
<div class="root" />
{/if}
{#if commit}
{@const author = commit.author}
<div class="avatar" class:first>
<Avatar {author} status={commit.status} />
</div>
{/if}
{/if}
</div>
<style lang="postcss">
.remote-column {
position: relative;
width: var(--size-24);
}
.remote-line {
position: absolute;
width: var(--size-2);
background-color: var(--clr-commit-remote);
left: calc(var(--size-10) + var(--size-1));
bottom: 0;
top: 0;
&.first {
top: calc(var(--size-40) + var(--size-2));
}
&.short {
top: 1rem;
}
&.dashed {
background: repeating-linear-gradient(
0,
transparent,
transparent 0.1875rem,
var(--clr-commit-remote) 0.1875rem,
var(--clr-commit-remote) 0.4375rem
);
}
}
.avatar {
position: absolute;
top: var(--size-10);
left: var(--size-4);
&.first {
top: calc(var(--size-40) + var(--size-2));
}
}
.root {
position: absolute;
width: var(--size-10);
top: 1.875rem;
border-radius: var(--radius-l) 0 0 0;
height: var(--size-10);
left: calc(var(--size-10) + var(--size-1));
border-color: var(--clr-commit-local);
border-width: var(--size-2) 0 0 var(--size-2);
&.base {
top: 0;
}
}
.commit-icon {
display: inline-block;
position: absolute;
border-radius: 6px;
left: var(--size-4);
background: var(--clr-commit-remote);
height: var(--size-16);
width: var(--size-16);
top: var(--size-10);
& svg {
height: var(--size-16);
width: var(--size-16);
}
}
</style>

View File

@ -0,0 +1,102 @@
<script lang="ts">
import { tooltip } from '$lib/utils/tooltip';
import type { Commit, RemoteCommit } from '$lib/vbranches/types';
export let line: boolean;
export let first: boolean;
export let remoteCommit: RemoteCommit | undefined;
export let localCommit: Commit | undefined;
export let dashed: boolean;
export let upstreamLine: boolean;
</script>
<div class="shadow-column">
{#if line}
{#if upstreamLine}
<div class="shadow-line tip" class:upstream={upstreamLine}></div>
{/if}
<div class="shadow-line" class:dashed class:short={first} />
{:else if upstreamLine}
<div class="shadow-line upstream" class:short={first} />
{/if}
{#if localCommit}
<div class="shadow-marker" class:first use:tooltip={localCommit.descriptionTitle}></div>
{/if}
{#if remoteCommit}
{@const author = remoteCommit.author}
<img
class="avatar avatar"
class:first
title={author.name}
alt="Gravatar for {author.email}"
srcset="{author.gravatarUrl} 2x"
width="100"
height="100"
on:error
/>
{/if}
</div>
<style lang="postcss">
.shadow-column {
position: relative;
}
.shadow-column {
width: var(--size-16);
}
.shadow-line {
position: absolute;
height: 100%;
width: var(--size-2);
background-color: var(--clr-commit-shadow);
left: 75%;
bottom: 0;
top: 0;
&.short {
top: 3rem;
}
&.dashed {
background: repeating-linear-gradient(
0,
transparent,
transparent 0.1875rem,
var(--clr-commit-shadow) 0.1875rem,
var(--clr-commit-shadow) 0.4375rem
);
}
&.tip {
bottom: calc(100% - 2.625rem);
}
&.upstream {
background-color: var(--clr-commit-upstream);
}
}
.shadow-marker {
position: absolute;
width: var(--size-10);
height: var(--size-10);
border-radius: 100%;
background-color: var(--clr-commit-shadow);
top: var(--size-14);
left: 50%;
&.first {
top: 2.75rem;
}
}
.avatar {
position: absolute;
width: var(--size-16);
height: var(--size-16);
border-radius: var(--radius-m);
top: var(--size-10);
left: var(--size-4);
border: var(--size-2) solid var(--clr-commit-upstream);
&.first {
top: var(--size-40);
}
}
</style>

View File

@ -151,7 +151,7 @@ export class GitHubService {
if (!skipCache) {
const cachedRsp = lscache.get(key);
if (cachedRsp) subscriber.next(cachedRsp.data.map(ghResponseToInstance));
if (cachedRsp?.data) subscriber.next(cachedRsp.data.map(ghResponseToInstance));
}
try {
@ -239,6 +239,10 @@ export class GitHubService {
return this.prs$.pipe(map((prs) => prs.find((pr) => pr.targetBranch == branch)));
}
hasPr(branch: string): boolean {
return !!this.prs$.value.find((pr) => pr.targetBranch == branch);
}
/* TODO: Figure out a way to cleanup old behavior subjects */
getState(branchId: string) {
let state$ = this.stateMap.get(branchId);

View File

@ -8,8 +8,6 @@ export const [getLocalCommits, createLocalContextStore] =
buildContextStore<Commit[]>('localCommits');
export const [getRemoteCommits, createRemoteContextStore] =
buildContextStore<Commit[]>('remoteCommits');
export const [getIntegratedCommits, createIntegratedContextStore] =
buildContextStore<Commit[]>('integratedCommits');
export const [getUnknownCommits, createUnknownContextStore] =
buildContextStore<RemoteCommit[]>('unknownCommits');
export const [getSelectedFiles, createSelectedFiles] = buildContextStore<

View File

@ -131,10 +131,6 @@ export class Branch {
get remoteCommits() {
return this.commits.filter((c) => c.status == 'remote');
}
get integratedCommits() {
return this.commits.filter((c) => c.status == 'integrated');
}
}
// Used for dependency injection
@ -148,7 +144,7 @@ export type ComponentColor =
| 'error'
| 'warning'
| 'purple';
export type CommitStatus = 'local' | 'remote' | 'integrated' | 'upstream';
export type CommitStatus = 'local' | 'remote' | 'upstream';
export class Commit {
id!: string;
@ -164,6 +160,7 @@ export class Commit {
branchId!: string;
changeId!: string;
isSigned!: boolean;
relatedTo?: RemoteCommit;
parent?: Commit;
children?: Commit[];
@ -172,14 +169,9 @@ export class Commit {
return !this.isRemote && !this.isIntegrated;
}
get status() {
if (!this.isIntegrated && !this.isRemote) {
get status(): CommitStatus {
if (this.isRemote) return 'remote';
return 'local';
} else if (!this.isIntegrated && this.isRemote) {
return 'remote';
} else if (this.isIntegrated) {
return 'integrated';
}
}
get descriptionTitle(): string | undefined {
@ -195,6 +187,10 @@ export class Commit {
}
}
export function isLocalCommit(obj: any): obj is Commit {
return obj instanceof Commit;
}
export class RemoteCommit {
id!: string;
author!: Author;
@ -218,6 +214,14 @@ export class RemoteCommit {
get descriptionBody(): string | undefined {
return splitMessage(this.description).description || undefined;
}
get status(): CommitStatus {
return 'upstream';
}
}
export function isRemoteCommit(obj: any): obj is RemoteCommit {
return obj instanceof RemoteCommit;
}
export type AnyCommit = Commit | RemoteCommit;
@ -227,6 +231,14 @@ export const REMOTE_COMMITS = Symbol('RemoteCommits');
export const INTEGRATED_COMMITS = Symbol('IntegratedCommits');
export const UNKNOWN_COMMITS = Symbol('UnknownCommits');
export function commitCompare(left: AnyCommit, right: AnyCommit): boolean {
if (left.id == right.id) return true;
if (left.description != right.description) return false;
if (left.author.name != right.author.name) return false;
if (left.author.email != right.author.email) return false;
return true;
}
export class RemoteHunk {
diff!: string;
hash?: string;

View File

@ -58,11 +58,13 @@ export class VirtualBranchService {
const commits = branch.commits;
for (let j = 0; j < commits.length; j++) {
const commit = commits[j];
if (j != 0) {
commit.parent = commits[j - 1];
if (j == 0) {
commit.children = [];
} else {
commit.children = [commits[j - 1]];
}
if (j != commits.length - 1) {
commit.children = [commits[j + 1]];
commit.parent = commits[j + 1];
}
}
}

View File

@ -189,6 +189,7 @@
--radius-l: 0.75rem;
--radius-m: 0.375rem;
--radius-s: 0.25rem;
--size-1: 0.06125rem;
--size-2: 0.125rem;
--size-4: 0.25rem;
--size-6: 0.375rem;