mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-18 06:22:28 +03:00
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:
parent
deb1db188b
commit
31f1cb8607
37
app/src/lib/components/Avatar.svelte
Normal file
37
app/src/lib/components/Avatar.svelte
Normal 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>
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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} />
|
65
app/src/lib/components/BranchFooter.svelte
Normal file
65
app/src/lib/components/BranchFooter.svelte
Normal 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>
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -65,7 +65,7 @@
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
style="pop"
|
||||
style="neutral"
|
||||
kind="solid"
|
||||
grow
|
||||
loading={isCommitting}
|
||||
|
55
app/src/lib/components/CommitLines.svelte
Normal file
55
app/src/lib/components/CommitLines.svelte
Normal 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>
|
@ -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}
|
||||
{#if hasCommits}
|
||||
<div class="commit-list__content">
|
||||
{#if hasCommits}
|
||||
<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 {
|
||||
|
@ -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>
|
@ -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>
|
@ -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%;
|
||||
|
90
app/src/lib/components/LocalLine.svelte
Normal file
90
app/src/lib/components/LocalLine.svelte
Normal 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>
|
68
app/src/lib/components/PullRequestButton.svelte
Normal file
68
app/src/lib/components/PullRequestButton.svelte
Normal 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>
|
@ -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 {
|
||||
|
@ -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"
|
||||
<DropDownButton
|
||||
style="pop"
|
||||
kind="soft"
|
||||
loading={isLoading}
|
||||
bind:this={dropDown}
|
||||
{wide}
|
||||
@ -89,7 +65,7 @@
|
||||
on:click={() => {
|
||||
dispatch('trigger', { action });
|
||||
}}
|
||||
>
|
||||
>
|
||||
{$selection$?.label}
|
||||
<ContextMenu
|
||||
type="select"
|
||||
@ -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}
|
||||
</DropDownButton>
|
||||
|
@ -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}
|
||||
|
123
app/src/lib/components/RemoteLine.svelte
Normal file
123
app/src/lib/components/RemoteLine.svelte
Normal 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>
|
102
app/src/lib/components/ShadowLine.svelte
Normal file
102
app/src/lib/components/ShadowLine.svelte
Normal 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>
|
@ -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);
|
||||
|
@ -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<
|
||||
|
@ -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;
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user