mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-09-11 12:25:42 +03:00
Update sidebar grouping of branches
This commit is contained in:
parent
443d8d6b71
commit
c6badd9f3e
@ -1,12 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '@gitbutler/ui/utils/tooltip';
|
||||
|
||||
export let name: 'remote-branch' | 'virtual-branch' | 'pr' | 'pr-draft' | 'pr-closed' | undefined;
|
||||
export let name:
|
||||
| 'remote-branch'
|
||||
| 'local-branch'
|
||||
| 'virtual-branch'
|
||||
| 'pr'
|
||||
| 'pr-draft'
|
||||
| 'pr-closed'
|
||||
| undefined;
|
||||
export let help: string | undefined;
|
||||
|
||||
function getIconColor(name: string | undefined) {
|
||||
if (name === 'remote-branch') return 'neutral';
|
||||
if (name === 'virtual-branch') return 'virtual';
|
||||
if (name === 'virtual-branch' || name === 'local-branch') return 'virtual';
|
||||
if (name === 'pr') return 'success';
|
||||
if (name === 'pr-draft') return 'purple';
|
||||
if (name === 'pr-closed') return 'neutral';
|
||||
@ -24,7 +31,7 @@
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
{#if name === 'remote-branch'}
|
||||
{#if name === 'remote-branch' || name === 'local-branch'}
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.75 9.99973V4H4.25V16H5.75V13C5.75 11.7574 6.75736 10.75 8 10.75C10.0711 10.75 11.75 9.07107 11.75 7V4H10.25V7C10.25 8.24264 9.24264 9.25 8 9.25C7.1558 9.25 6.37675 9.52896 5.75 9.99973Z"
|
||||
|
@ -13,10 +13,13 @@
|
||||
import type { Branch } from '$lib/vbranches/types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let branch: Branch;
|
||||
export let localBranch: Branch | undefined;
|
||||
export let remoteBranch: Branch | undefined;
|
||||
export let base: BaseBranch | undefined | null;
|
||||
export let pr: PullRequest | undefined;
|
||||
|
||||
$: branch = remoteBranch || localBranch!;
|
||||
|
||||
const branchController = getContext(BranchController);
|
||||
const project = getContext(Project);
|
||||
|
||||
@ -26,29 +29,36 @@
|
||||
<div class="header__wrapper">
|
||||
<div class="header card">
|
||||
<div class="header__info">
|
||||
<BranchLabel disabled bind:name={branch.name} />
|
||||
<BranchLabel disabled name={branch.name} />
|
||||
<div class="header__remote-branch">
|
||||
<div
|
||||
class="status-tag text-base-11 text-semibold remote"
|
||||
use:tooltip={'At least some of your changes have been pushed'}
|
||||
>
|
||||
<Icon name="remote-branch-small" /> remote
|
||||
</div>
|
||||
<Button
|
||||
size="tag"
|
||||
icon="open-link"
|
||||
style="ghost"
|
||||
outline
|
||||
shrinkable
|
||||
on:click={(e) => {
|
||||
const url = base?.branchUrl(branch.name);
|
||||
if (url) openExternalUrl(url);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{branch.displayName}
|
||||
</Button>
|
||||
{#if remoteBranch}
|
||||
<div
|
||||
class="status-tag text-base-11 text-semibold remote"
|
||||
use:tooltip={'At least some of your changes have been pushed'}
|
||||
>
|
||||
<Icon name="remote-branch-small" />
|
||||
{localBranch ? 'local and remote' : 'remote'}
|
||||
</div>
|
||||
<Button
|
||||
size="tag"
|
||||
icon="open-link"
|
||||
style="ghost"
|
||||
outline
|
||||
shrinkable
|
||||
on:click={(e) => {
|
||||
const url = base?.branchUrl(branch.name);
|
||||
if (url) openExternalUrl(url);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{branch.displayName}
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="status-tag text-base-11 text-semibold remote">
|
||||
<Icon name="remote-branch-small" /> local
|
||||
</div>
|
||||
{/if}
|
||||
{#if pr?.htmlUrl}
|
||||
<Button
|
||||
size="tag"
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { CombinedBranch } from '$lib/branches/types';
|
||||
import { buildContextStore } from '$lib/utils/context';
|
||||
import { groupBy } from '$lib/utils/groupBy';
|
||||
import { derived, readable, writable, type Readable } from 'svelte/store';
|
||||
import type { NameNormalizationService } from '$lib/branches/nameNormalizationService';
|
||||
import type { GitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
|
||||
import type { PullRequest } from '$lib/gitHost/interface/types';
|
||||
import type { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
@ -18,7 +20,8 @@ export class BranchService {
|
||||
constructor(
|
||||
vbranchService: VirtualBranchService,
|
||||
remoteBranchService: RemoteBranchService,
|
||||
gitPrService: GitHostListingService | undefined
|
||||
gitPrService: GitHostListingService | undefined,
|
||||
nameNormalizationService: NameNormalizationService
|
||||
) {
|
||||
const vbranches = vbranchService.branches;
|
||||
const branches = remoteBranchService.branches;
|
||||
@ -26,44 +29,73 @@ export class BranchService {
|
||||
|
||||
this.branches = derived(
|
||||
[vbranches, branches, prs],
|
||||
([vbranches, remoteBranches, pullRequests]) => {
|
||||
return mergeBranchesAndPrs(vbranches, pullRequests, remoteBranches || []);
|
||||
}
|
||||
([vbranches, remoteBranches, pullRequests], set) => {
|
||||
// derived with a set does not allow you to return a promise
|
||||
mergeBranchesAndPrs(
|
||||
vbranches || [],
|
||||
pullRequests || [],
|
||||
remoteBranches || [],
|
||||
nameNormalizationService
|
||||
).then((combinedBranches) => {
|
||||
set(combinedBranches);
|
||||
});
|
||||
},
|
||||
[] as CombinedBranch[] // Use an empty array as the default, with sufficient typing
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mergeBranchesAndPrs(
|
||||
_vbranches: VirtualBranch[] | undefined,
|
||||
pullRequests: PullRequest[] | undefined,
|
||||
remoteBranches: Branch[] | undefined
|
||||
): CombinedBranch[] {
|
||||
async function mergeBranchesAndPrs(
|
||||
virtualBranches: VirtualBranch[],
|
||||
pullRequests: PullRequest[],
|
||||
branches: Branch[],
|
||||
nameNormalizationService: NameNormalizationService
|
||||
): Promise<CombinedBranch[]> {
|
||||
const contributions: CombinedBranch[] = [];
|
||||
|
||||
// Then remote branches that have no virtual branch, combined with pull requests if present
|
||||
if (remoteBranches) {
|
||||
contributions.push(
|
||||
...remoteBranches.map((rb) => {
|
||||
const pr = pullRequests?.find((pr) => pr.sha === rb.sha);
|
||||
return new CombinedBranch({ remoteBranch: rb, pr });
|
||||
})
|
||||
);
|
||||
const groupedBranches = groupBy(branches, (branch) => branch.givenName);
|
||||
|
||||
for (const [_, branches] of Object.entries(groupedBranches)) {
|
||||
// There should only ever be one local reference for a particular given name
|
||||
const localBranch = branches.find((branch) => !branch.isRemote);
|
||||
const remoteBranches = branches.filter((branch) => branch.isRemote);
|
||||
|
||||
// There must be a local branch if there are no remote branches
|
||||
if (remoteBranches.length === 0) {
|
||||
contributions.push(new CombinedBranch({ localBranch }));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
remoteBranches.forEach((remoteBranch) => {
|
||||
contributions.push(new CombinedBranch({ remoteBranch, localBranch }));
|
||||
});
|
||||
}
|
||||
|
||||
// And finally pull requests that lack any corresponding branch
|
||||
if (pullRequests) {
|
||||
contributions.push(
|
||||
...pullRequests
|
||||
.filter((pr) => !contributions.some((cb) => pr.sha === cb.upstreamSha))
|
||||
.map((pr) => {
|
||||
return new CombinedBranch({ pr });
|
||||
})
|
||||
contributions.forEach((contribution) => {
|
||||
const pullRequest = pullRequests.find(
|
||||
// This may be over-sensitive in rare cases, but is preferable to using the head sha
|
||||
(pullRequest) => contribution.remoteBranch?.givenName === pullRequest.sourceBranch
|
||||
);
|
||||
}
|
||||
|
||||
if (pullRequest) {
|
||||
contribution.pr = pullRequest;
|
||||
}
|
||||
});
|
||||
|
||||
const normalizedVirtualBranchNames = new Set(
|
||||
await Promise.all(
|
||||
virtualBranches.map(
|
||||
async (virtualBranch) => await nameNormalizationService.normalize(virtualBranch.name)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// This should be everything considered a branch in one list
|
||||
const filtered = contributions.sort((a, b) => {
|
||||
return (a.modifiedAt || new Date(0)) < (b.modifiedAt || new Date(0)) ? 1 : -1;
|
||||
});
|
||||
const filtered = contributions
|
||||
.filter((combinedBranch) => !normalizedVirtualBranchNames.has(combinedBranch.branch!.givenName))
|
||||
.sort((a, b) => {
|
||||
return (a.modifiedAt || new Date(0)) < (b.modifiedAt || new Date(0)) ? 1 : -1;
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
|
@ -4,19 +4,23 @@ import type { Author, VirtualBranch, Branch } from '$lib/vbranches/types';
|
||||
export class CombinedBranch {
|
||||
pr?: PullRequest;
|
||||
remoteBranch?: Branch;
|
||||
localBranch?: Branch;
|
||||
vbranch?: VirtualBranch;
|
||||
|
||||
constructor({
|
||||
vbranch,
|
||||
remoteBranch,
|
||||
localBranch,
|
||||
pr
|
||||
}: {
|
||||
vbranch?: VirtualBranch;
|
||||
remoteBranch?: Branch;
|
||||
localBranch?: Branch;
|
||||
pr?: PullRequest;
|
||||
}) {
|
||||
this.vbranch = vbranch;
|
||||
this.remoteBranch = remoteBranch;
|
||||
this.localBranch = localBranch;
|
||||
this.pr = pr;
|
||||
}
|
||||
|
||||
@ -24,6 +28,7 @@ export class CombinedBranch {
|
||||
return (
|
||||
this.pr?.sha ||
|
||||
this.remoteBranch?.sha ||
|
||||
this.localBranch?.sha ||
|
||||
this.vbranch?.upstream?.sha ||
|
||||
this.vbranch?.head ||
|
||||
'unknown'
|
||||
@ -32,7 +37,11 @@ export class CombinedBranch {
|
||||
|
||||
get displayName(): string {
|
||||
return (
|
||||
this.pr?.sourceBranch || this.remoteBranch?.displayName || this.vbranch?.name || 'unknown'
|
||||
this.pr?.sourceBranch ||
|
||||
this.remoteBranch?.displayName ||
|
||||
this.localBranch?.displayName ||
|
||||
this.vbranch?.name ||
|
||||
'unknown'
|
||||
);
|
||||
}
|
||||
|
||||
@ -41,9 +50,9 @@ export class CombinedBranch {
|
||||
if (this.pr?.author) {
|
||||
authors.push(this.pr.author);
|
||||
}
|
||||
if (this.remoteBranch) {
|
||||
if (this.remoteBranch.lastCommitAuthor) {
|
||||
authors.push({ name: this.remoteBranch.lastCommitAuthor });
|
||||
if (this.branch) {
|
||||
if (this.branch.lastCommitAuthor) {
|
||||
authors.push({ name: this.branch.lastCommitAuthor });
|
||||
}
|
||||
}
|
||||
if (this.vbranch) {
|
||||
@ -59,7 +68,14 @@ export class CombinedBranch {
|
||||
return this.authors[0];
|
||||
}
|
||||
|
||||
get icon(): 'remote-branch' | 'virtual-branch' | 'pr' | 'pr-draft' | 'pr-closed' | undefined {
|
||||
get icon():
|
||||
| 'remote-branch'
|
||||
| 'local-branch'
|
||||
| 'virtual-branch'
|
||||
| 'pr'
|
||||
| 'pr-draft'
|
||||
| 'pr-closed'
|
||||
| undefined {
|
||||
return this.currentState();
|
||||
}
|
||||
|
||||
@ -73,9 +89,9 @@ export class CombinedBranch {
|
||||
|
||||
get modifiedAt(): Date | undefined {
|
||||
if (this.vbranch) return this.vbranch.updatedAt;
|
||||
if (this.remoteBranch) {
|
||||
return this.remoteBranch.lastCommitTimestampMs
|
||||
? new Date(this.remoteBranch.lastCommitTimestampMs)
|
||||
if (this.branch) {
|
||||
return this.branch.lastCommitTimestampMs
|
||||
? new Date(this.branch.lastCommitTimestampMs)
|
||||
: undefined;
|
||||
}
|
||||
if (this.pr) {
|
||||
@ -90,6 +106,8 @@ export class CombinedBranch {
|
||||
return 'Virtual branch';
|
||||
case BranchState.RemoteBranch:
|
||||
return 'Remote branch';
|
||||
case BranchState.LocalBranch:
|
||||
return 'Local branch';
|
||||
case BranchState.PR:
|
||||
return 'Pull Request';
|
||||
case BranchState.PRClosed:
|
||||
@ -109,9 +127,9 @@ export class CombinedBranch {
|
||||
this.pr.author?.email && identifiers.push(this.pr.author.email);
|
||||
this.pr.author?.name && identifiers.push(this.pr.author.name);
|
||||
}
|
||||
if (this.remoteBranch) {
|
||||
identifiers.push(this.remoteBranch.displayName);
|
||||
this.remoteBranch.lastCommitAuthor && identifiers.push(this.remoteBranch.lastCommitAuthor);
|
||||
if (this.branch) {
|
||||
identifiers.push(this.branch.displayName);
|
||||
this.branch.lastCommitAuthor && identifiers.push(this.branch.lastCommitAuthor);
|
||||
}
|
||||
|
||||
return identifiers.map((identifier) => identifier.toLowerCase());
|
||||
@ -120,13 +138,21 @@ export class CombinedBranch {
|
||||
currentState(): BranchState | undefined {
|
||||
if (this.pr) return BranchState.PR;
|
||||
if (this.remoteBranch) return BranchState.RemoteBranch;
|
||||
if (this.localBranch) return BranchState.LocalBranch;
|
||||
if (this.vbranch) return BranchState.VirtualBranch;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get branch() {
|
||||
// Prefer the local branch over the remote branch
|
||||
// We should always have at least one branch
|
||||
return this.localBranch || this.remoteBranch;
|
||||
}
|
||||
}
|
||||
|
||||
enum BranchState {
|
||||
RemoteBranch = 'remote-branch',
|
||||
LocalBranch = 'local-branch',
|
||||
VirtualBranch = 'virtual-branch',
|
||||
PR = 'pr',
|
||||
PRDraft = 'pr-draft',
|
||||
|
231
app/src/lib/components/BranchPreview.svelte
Normal file
231
app/src/lib/components/BranchPreview.svelte
Normal file
@ -0,0 +1,231 @@
|
||||
<script lang="ts">
|
||||
import BranchPreviewHeader from '../branch/BranchPreviewHeader.svelte';
|
||||
import Resizer from '../shared/Resizer.svelte';
|
||||
import ScrollableContainer from '../shared/ScrollableContainer.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
import CommitCard from '$lib/commit/CommitCard.svelte';
|
||||
import { transformAnyCommit } from '$lib/commitLines/transformers';
|
||||
import FileCard from '$lib/file/FileCard.svelte';
|
||||
import { SETTINGS, type Settings } from '$lib/settings/userSettings';
|
||||
import { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
import { getContext, getContextStore, getContextStoreBySymbol } from '$lib/utils/context';
|
||||
import { getMarkdownRenderer } from '$lib/utils/markdown';
|
||||
import { FileIdSelection } from '$lib/vbranches/fileIdSelection';
|
||||
import { BranchData, type Branch } from '$lib/vbranches/types';
|
||||
import LineGroup from '@gitbutler/ui/CommitLines/LineGroup.svelte';
|
||||
import { LineManagerFactory } from '@gitbutler/ui/CommitLines/lineManager';
|
||||
import lscache from 'lscache';
|
||||
import { marked } from 'marked';
|
||||
import { onMount, setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { PullRequest } from '$lib/gitHost/interface/types';
|
||||
|
||||
export let localBranch: Branch | undefined;
|
||||
export let remoteBranch: Branch | undefined;
|
||||
export let pr: PullRequest | undefined;
|
||||
|
||||
const project = getContext(Project);
|
||||
const remoteBranchService = getContext(RemoteBranchService);
|
||||
const baseBranch = getContextStore(BaseBranch);
|
||||
|
||||
const fileIdSelection = new FileIdSelection(project.id, writable([]));
|
||||
setContext(FileIdSelection, fileIdSelection);
|
||||
|
||||
$: selectedFile = fileIdSelection.selectedFile;
|
||||
|
||||
const defaultBranchWidthRem = 30;
|
||||
const laneWidthKey = 'branchPreviewLaneWidth';
|
||||
const userSettings = getContextStoreBySymbol<Settings>(SETTINGS);
|
||||
const lineManagerFactory = getContext(LineManagerFactory);
|
||||
|
||||
let localBranchData: BranchData | undefined;
|
||||
let remoteBranchData: BranchData | undefined;
|
||||
|
||||
$: console.log({ localBranch, localBranchData, remoteBranch, remoteBranchData });
|
||||
|
||||
// The remote branch service (which needs to be renamed) is responsible for
|
||||
// fetching local and remote branches.
|
||||
// We must manually set the branch data to undefined as the component
|
||||
// doesn't get completely re-rendered on a page change.
|
||||
$: if (localBranch) {
|
||||
remoteBranchService
|
||||
.getRemoteBranchData(localBranch.name)
|
||||
.then((branchData) => (localBranchData = branchData));
|
||||
} else {
|
||||
localBranchData = undefined;
|
||||
}
|
||||
|
||||
$: if (remoteBranch) {
|
||||
remoteBranchService
|
||||
.getRemoteBranchData(remoteBranch.name)
|
||||
.then((branchData) => (remoteBranchData = branchData));
|
||||
} else {
|
||||
localBranchData = undefined;
|
||||
}
|
||||
|
||||
$: remoteCommitShas = new Set(remoteBranchData?.commits.map((commit) => commit.id) || []);
|
||||
|
||||
// Find commits common in the local and remote
|
||||
$: localAndRemoteCommits =
|
||||
localBranchData?.commits.filter((commit) => remoteCommitShas.has(commit.id)) || [];
|
||||
|
||||
$: localAndRemoteCommitShas = new Set(localAndRemoteCommits.map((commit) => commit.id));
|
||||
|
||||
// Find the local and remote commits that are not shared
|
||||
$: localCommits =
|
||||
localBranchData?.commits.filter((commit) => !localAndRemoteCommitShas.has(commit.id)) || [];
|
||||
$: remoteCommits =
|
||||
remoteBranchData?.commits.filter((commit) => !localAndRemoteCommitShas.has(commit.id)) || [];
|
||||
|
||||
$: lineManager = lineManagerFactory.build(
|
||||
{
|
||||
remoteCommits: remoteCommits.map(transformAnyCommit),
|
||||
localCommits: localCommits.map(transformAnyCommit),
|
||||
localAndRemoteCommits: localAndRemoteCommits.map(transformAnyCommit),
|
||||
integratedCommits: []
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
let rsViewport: HTMLDivElement;
|
||||
let laneWidth: number;
|
||||
|
||||
onMount(() => {
|
||||
laneWidth = lscache.get(laneWidthKey);
|
||||
});
|
||||
|
||||
const renderer = getMarkdownRenderer();
|
||||
</script>
|
||||
|
||||
{#if remoteBranch || localBranch}
|
||||
<div class="base">
|
||||
<div
|
||||
class="base__left"
|
||||
bind:this={rsViewport}
|
||||
style:width={`${laneWidth || defaultBranchWidthRem}rem`}
|
||||
>
|
||||
<ScrollableContainer wide>
|
||||
<div class="branch-preview">
|
||||
<BranchPreviewHeader base={$baseBranch} {localBranch} {remoteBranch} {pr} />
|
||||
{#if pr}
|
||||
<div class="card">
|
||||
<div class="card__header text-base-body-14 text-semibold">{pr.title}</div>
|
||||
{#if pr.body}
|
||||
<div class="markdown card__content text-base-body-13">
|
||||
{@html marked.parse(pr.body, { renderer })}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if remoteCommits}
|
||||
{#each remoteCommits as commit, index (commit.id)}
|
||||
<CommitCard
|
||||
first={index === 0}
|
||||
last={index === remoteCommits.length - 1}
|
||||
{commit}
|
||||
commitUrl={$baseBranch?.commitUrl(commit.id)}
|
||||
type="remote"
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if localCommits}
|
||||
{#each localCommits as commit, index (commit.id)}
|
||||
<CommitCard
|
||||
first={index === 0}
|
||||
last={index === localCommits.length - 1}
|
||||
{commit}
|
||||
commitUrl={$baseBranch?.commitUrl(commit.id)}
|
||||
type="local"
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if localAndRemoteCommits}
|
||||
{#each localAndRemoteCommits as commit, index (commit.id)}
|
||||
<CommitCard
|
||||
first={index === 0}
|
||||
last={index === localAndRemoteCommits.length - 1}
|
||||
{commit}
|
||||
commitUrl={$baseBranch?.commitUrl(commit.id)}
|
||||
type="localAndRemote"
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<Resizer
|
||||
viewport={rsViewport}
|
||||
direction="right"
|
||||
minWidth={320}
|
||||
on:width={(e) => {
|
||||
laneWidth = e.detail / (16 * $userSettings.zoom);
|
||||
lscache.set(laneWidthKey, laneWidth, 7 * 1440); // 7 day ttl
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="base__right">
|
||||
{#await $selectedFile then selected}
|
||||
{#if selected}
|
||||
<FileCard
|
||||
conflicted={selected.conflicted}
|
||||
file={selected}
|
||||
isUnapplied={false}
|
||||
readonly={true}
|
||||
on:close={() => {
|
||||
fileIdSelection.clear();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p>No local or remote branch found</p>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.base {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.base__left {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.base__right {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
align-items: flex-start;
|
||||
padding: 12px 12px 12px 6px;
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
.branch-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: 12px 6px 12px 12px;
|
||||
}
|
||||
|
||||
.card__content {
|
||||
color: var(--clr-scale-ntrl-30);
|
||||
}
|
||||
</style>
|
@ -1,141 +0,0 @@
|
||||
<script lang="ts">
|
||||
import BranchPreviewHeader from '../branch/BranchPreviewHeader.svelte';
|
||||
import Resizer from '../shared/Resizer.svelte';
|
||||
import ScrollableContainer from '../shared/ScrollableContainer.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
import CommitCard from '$lib/commit/CommitCard.svelte';
|
||||
import FileCard from '$lib/file/FileCard.svelte';
|
||||
import { SETTINGS, type Settings } from '$lib/settings/userSettings';
|
||||
import { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
import { getContext, getContextStore, getContextStoreBySymbol } from '$lib/utils/context';
|
||||
import { getMarkdownRenderer } from '$lib/utils/markdown';
|
||||
import { FileIdSelection } from '$lib/vbranches/fileIdSelection';
|
||||
import { type Branch } from '$lib/vbranches/types';
|
||||
import lscache from 'lscache';
|
||||
import { marked } from 'marked';
|
||||
import { onMount, setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { PullRequest } from '$lib/gitHost/interface/types';
|
||||
|
||||
export let branch: Branch;
|
||||
export let pr: PullRequest | undefined;
|
||||
|
||||
const project = getContext(Project);
|
||||
const remoteBranchService = getContext(RemoteBranchService);
|
||||
const baseBranch = getContextStore(BaseBranch);
|
||||
|
||||
const fileIdSelection = new FileIdSelection(project.id, writable([]));
|
||||
setContext(FileIdSelection, fileIdSelection);
|
||||
|
||||
$: selectedFile = fileIdSelection.selectedFile;
|
||||
|
||||
const defaultBranchWidthRem = 30;
|
||||
const laneWidthKey = 'branchPreviewLaneWidth';
|
||||
const userSettings = getContextStoreBySymbol<Settings>(SETTINGS);
|
||||
|
||||
let rsViewport: HTMLDivElement;
|
||||
let laneWidth: number;
|
||||
|
||||
onMount(() => {
|
||||
laneWidth = lscache.get(laneWidthKey);
|
||||
});
|
||||
|
||||
const renderer = getMarkdownRenderer();
|
||||
</script>
|
||||
|
||||
<div class="base">
|
||||
<div
|
||||
class="base__left"
|
||||
bind:this={rsViewport}
|
||||
style:width={`${laneWidth || defaultBranchWidthRem}rem`}
|
||||
>
|
||||
<ScrollableContainer wide>
|
||||
<div class="branch-preview">
|
||||
<BranchPreviewHeader base={$baseBranch} {branch} {pr} />
|
||||
{#if pr}
|
||||
<div class="card">
|
||||
<div class="card__header text-base-body-14 text-semibold">{pr.title}</div>
|
||||
{#if pr.body}
|
||||
<div class="markdown card__content text-base-body-13">
|
||||
{@html marked.parse(pr.body, { renderer })}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#await remoteBranchService.getRemoteBranchData(branch.name) then branchData}
|
||||
{#if branchData.commits && branchData.commits.length > 0}
|
||||
<div>
|
||||
{#each branchData.commits as commit, index (commit.id)}
|
||||
<CommitCard
|
||||
first={index === 0}
|
||||
last={index === branchData.commits.length - 1}
|
||||
{commit}
|
||||
commitUrl={$baseBranch?.commitUrl(commit.id)}
|
||||
type="localAndRemote"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<Resizer
|
||||
viewport={rsViewport}
|
||||
direction="right"
|
||||
minWidth={320}
|
||||
on:width={(e) => {
|
||||
laneWidth = e.detail / (16 * $userSettings.zoom);
|
||||
lscache.set(laneWidthKey, laneWidth, 7 * 1440); // 7 day ttl
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="base__right">
|
||||
{#await $selectedFile then selected}
|
||||
{#if selected}
|
||||
<FileCard
|
||||
conflicted={selected.conflicted}
|
||||
file={selected}
|
||||
isUnapplied={false}
|
||||
readonly={true}
|
||||
on:close={() => {
|
||||
fileIdSelection.clear();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.base {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.base__left {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.base__right {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
align-items: flex-start;
|
||||
padding: 12px 12px 12px 6px;
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
.branch-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: 12px 6px 12px 12px;
|
||||
}
|
||||
|
||||
.card__content {
|
||||
color: var(--clr-scale-ntrl-30);
|
||||
}
|
||||
</style>
|
@ -10,7 +10,10 @@
|
||||
|
||||
function getBranchLink(b: CombinedBranch): string | undefined {
|
||||
if (b.vbranch) return `/${projectId}/board/`;
|
||||
if (b.remoteBranch) return `/${projectId}/remote/${branch?.remoteBranch?.displayName}`;
|
||||
// Here we specifically want to prefer looking at the remote branch as
|
||||
// the there may be multiple remotes that share the same local branch.
|
||||
if (b.branch)
|
||||
return `/${projectId}/branch/${branch?.remoteBranch?.name || branch?.localBranch?.name}`;
|
||||
if (b.pr) return `/${projectId}/pull/${b.pr.number}`;
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@
|
||||
if (b.pr) return false;
|
||||
}
|
||||
|
||||
if (params.includeRemote && b.remoteBranch) return true;
|
||||
if (params.includeRemote && b.branch) return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
@ -315,14 +315,11 @@ export class Branch {
|
||||
lastCommitTimestampMs?: number | undefined;
|
||||
lastCommitAuthor?: string | undefined;
|
||||
givenName!: string;
|
||||
isRemote!: boolean;
|
||||
|
||||
get displayName(): string {
|
||||
return this.name.replace('refs/remotes/', '').replace('refs/heads/', '');
|
||||
}
|
||||
|
||||
get isRemote() {
|
||||
return !!this.upstream;
|
||||
}
|
||||
}
|
||||
|
||||
export class BranchData {
|
||||
|
@ -4,6 +4,7 @@
|
||||
import { BaseBranch, NoDefaultTarget } from '$lib/baseBranch/baseBranch';
|
||||
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
|
||||
import { BranchDragActionsFactory } from '$lib/branches/dragActions';
|
||||
import { getNameNormalizationServiceContext } from '$lib/branches/nameNormalizationService';
|
||||
import { BranchService, createBranchServiceStore } from '$lib/branches/service';
|
||||
import { CommitDragActionsFactory } from '$lib/commits/dragActions';
|
||||
import NoBaseBranch from '$lib/components/NoBaseBranch.svelte';
|
||||
@ -33,6 +34,8 @@
|
||||
|
||||
const { data, children }: { data: LayoutData; children: Snippet } = $props();
|
||||
|
||||
const nameNormalizationService = getNameNormalizationServiceContext();
|
||||
|
||||
const {
|
||||
vbranchService,
|
||||
project,
|
||||
@ -104,7 +107,14 @@
|
||||
|
||||
listServiceStore.set(ghListService);
|
||||
githubRepoServiceStore.set(gitHost);
|
||||
branchServiceStore.set(new BranchService(vbranchService, remoteBranchService, ghListService));
|
||||
branchServiceStore.set(
|
||||
new BranchService(
|
||||
vbranchService,
|
||||
remoteBranchService,
|
||||
ghListService,
|
||||
nameNormalizationService
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// Once on load and every time the project id changes
|
||||
|
@ -4,15 +4,18 @@
|
||||
// - And it does NOT have a cooresponding vbranch
|
||||
// It may also display details about a cooresponding pr if they exist
|
||||
import { getBranchServiceStore } from '$lib/branches/service';
|
||||
import RemoteBranchPreview from '$lib/components/BranchPreview.svelte';
|
||||
import FullviewLoading from '$lib/components/FullviewLoading.svelte';
|
||||
import RemoteBranchPreview from '$lib/components/RemoteBranchPreview.svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
const branchService = getBranchServiceStore();
|
||||
const branches = $derived($branchService?.branches);
|
||||
const error = $derived($branchService?.error);
|
||||
// Search for remote branch first as there may be multiple combined branches
|
||||
// which have the same local branch
|
||||
const branch = $derived(
|
||||
$branches?.find((cb) => cb.remoteBranch?.displayName === $page.params.name)
|
||||
$branches?.find((cb) => cb.remoteBranch?.name === $page.params.name) ||
|
||||
$branches?.find((cb) => cb.localBranch?.name === $page.params.name)
|
||||
);
|
||||
// $: branch = $branches?.find((b) => b.displayName === $page.params.name);
|
||||
</script>
|
||||
@ -21,8 +24,12 @@
|
||||
<p>Error...</p>
|
||||
{:else if !$branches}
|
||||
<FullviewLoading />
|
||||
{:else if branch?.remoteBranch}
|
||||
<RemoteBranchPreview branch={branch.remoteBranch} pr={branch.pr} />
|
||||
{:else if branch?.remoteBranch || branch?.localBranch}
|
||||
<RemoteBranchPreview
|
||||
localBranch={branch?.localBranch}
|
||||
remoteBranch={branch?.remoteBranch}
|
||||
pr={branch.pr}
|
||||
/>
|
||||
{:else}
|
||||
<p>Branch doesn't seem to exist</p>
|
||||
{/if}
|
@ -30,6 +30,7 @@ pub struct RemoteBranch {
|
||||
pub given_name: String,
|
||||
pub last_commit_timestamp_ms: Option<u128>,
|
||||
pub last_commit_author: Option<String>,
|
||||
pub is_remote: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
@ -139,6 +140,7 @@ pub(crate) fn branch_to_remote_branch(
|
||||
.map(|t: u128| t * 1000)
|
||||
.ok(),
|
||||
last_commit_author: commit.author().name().map(std::string::ToString::to_string),
|
||||
is_remote: branch.get().is_remote(),
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user