Merge pull request #4489 from gitbutlerapp/Apply-from-local-branches

Apply from local branches
This commit is contained in:
Caleb Owens 2024-07-25 15:30:59 +02:00 committed by GitHub
commit 9e2f065109
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 482 additions and 259 deletions

View File

@ -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"

View File

@ -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"
@ -79,7 +89,11 @@
on:click={async () => {
isApplying = true;
try {
await branchController.createvBranchFromBranch(branch.name);
if (localBranch) {
await branchController.createvBranchFromBranch(localBranch.name, remoteBranch?.name);
} else {
await branchController.createvBranchFromBranch(remoteBranch!.name);
}
goto(`/${project.id}/board`);
} catch (e) {
const err = 'Failed to apply branch';

View File

@ -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;
}

View File

@ -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',

View File

@ -0,0 +1,229 @@
<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;
// 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 {
remoteBranchData = 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>

View File

@ -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>

View File

@ -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}`;
}

View File

@ -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;
});
}

View File

@ -276,11 +276,18 @@ You can find them in the 'Branches' sidebar in order to resolve conflicts.`;
}
}
async createvBranchFromBranch(branch: string) {
/**
*
* @param branch The branch you want to create a virtual branch for. If you
* have a local branch, this should be the branch.
* @param remote Optionally sets another branch as the upstream.
*/
async createvBranchFromBranch(branch: string, remote: string | undefined = undefined) {
try {
await invoke<string>('create_virtual_branch_from_branch', {
projectId: this.projectId,
branch
branch,
remote
});
} catch (err) {
showError('Failed to create virtual branch', err);

View File

@ -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 {

View File

@ -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

View File

@ -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}

View File

@ -470,12 +470,13 @@ impl VirtualBranchActions {
&self,
project: &Project,
branch: &Refname,
remote: Option<RemoteRefname>,
) -> Result<BranchId> {
let project_repository = open_with_verify(project)?;
let branch_manager = project_repository.branch_manager();
let mut guard = project.exclusive_worktree_access();
branch_manager
.create_virtual_branch_from_branch(branch, guard.write_permission())
.create_virtual_branch_from_branch(branch, remote, guard.write_permission())
.map_err(Into::into)
}
}

View File

@ -14,7 +14,7 @@ use gitbutler_commit::commit_headers::HasCommitHeaders;
use gitbutler_error::error::Marker;
use gitbutler_oplog::SnapshotExt;
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_reference::Refname;
use gitbutler_reference::{Refname, RemoteRefname};
use gitbutler_repo::{rebase::cherry_rebase, RepoActionsExt, RepositoryExt};
use gitbutler_time::time::now_since_unix_epoch_ms;
use std::borrow::Cow;
@ -36,7 +36,7 @@ impl BranchManager<'_> {
let tree = commit
.tree()
.context("failed to find defaut target commit tree")?;
.context("failed to find default target commit tree")?;
let mut all_virtual_branches = vb_state
.list_branches_in_workspace()
@ -126,20 +126,26 @@ impl BranchManager<'_> {
pub fn create_virtual_branch_from_branch(
&self,
upstream: &Refname,
target: &Refname,
upstream_branch: Option<RemoteRefname>,
perm: &mut WorktreeWritePermission,
) -> Result<BranchId> {
// only set upstream if it's not the default target
let upstream_branch = match upstream {
Refname::Other(_) | Refname::Virtual(_) => {
// we only support local or remote branches
bail!("branch {upstream} must be a local or remote branch");
let upstream_branch = match upstream_branch {
Some(upstream_branch) => Some(upstream_branch),
None => {
match target {
Refname::Other(_) | Refname::Virtual(_) => {
// we only support local or remote branches
bail!("branch {target} must be a local or remote branch");
}
Refname::Remote(remote) => Some(remote.clone()),
Refname::Local(local) => local.remote().cloned(),
}
}
Refname::Remote(remote) => Some(remote.clone()),
Refname::Local(local) => local.remote().cloned(),
};
let branch_name = upstream
let branch_name = target
.branch()
.expect("always a branch reference")
.to_string();
@ -153,21 +159,21 @@ impl BranchManager<'_> {
let default_target = vb_state.get_default_target()?;
if let Refname::Remote(remote_upstream) = upstream {
if let Refname::Remote(remote_upstream) = target {
if default_target.branch == *remote_upstream {
bail!("cannot create a branch from default target")
}
}
let repo = self.project_repository.repo();
let head_reference =
repo.find_reference(&upstream.to_string())
.map_err(|err| match err {
err if err.code() == git2::ErrorCode::NotFound => {
anyhow!("branch {upstream} was not found")
}
err => err.into(),
})?;
let head_reference = repo
.find_reference(&target.to_string())
.map_err(|err| match err {
err if err.code() == git2::ErrorCode::NotFound => {
anyhow!("branch {target} was not found")
}
err => err.into(),
})?;
let head_commit = head_reference
.peel_to_commit()
.context("failed to peel to commit")?;
@ -220,7 +226,7 @@ impl BranchManager<'_> {
);
let branch = if let Ok(Some(mut branch)) =
vb_state.find_by_source_refname_where_not_in_workspace(upstream)
vb_state.find_by_source_refname_where_not_in_workspace(target)
{
branch.upstream_head = upstream_branch.is_some().then_some(head_commit.id());
branch.upstream = upstream_branch;
@ -239,7 +245,7 @@ impl BranchManager<'_> {
id: BranchId::generate(),
name: branch_name.clone(),
notes: String::new(),
source_refname: Some(upstream.clone()),
source_refname: Some(target.clone()),
upstream_head: upstream_branch.is_some().then_some(head_commit.id()),
upstream: upstream_branch,
tree: head_commit_tree.id(),

View File

@ -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(),
})
}

View File

@ -1177,6 +1177,7 @@ fn unapply_branch() -> Result<()> {
let branch_manager = project_repository.branch_manager();
let branch1_id = branch_manager.create_virtual_branch_from_branch(
&Refname::from_str(&real_branch)?,
None,
guard.write_permission(),
)?;
let contents = std::fs::read(Path::new(&project.path).join(file_path))?;
@ -1271,6 +1272,7 @@ fn apply_unapply_added_deleted_files() -> Result<()> {
branch_manager
.create_virtual_branch_from_branch(
&Refname::from_str(&real_branch_2).unwrap(),
None,
guard.write_permission(),
)
.unwrap();
@ -1281,6 +1283,7 @@ fn apply_unapply_added_deleted_files() -> Result<()> {
branch_manager
.create_virtual_branch_from_branch(
&Refname::from_str(&real_branch_3).unwrap(),
None,
guard.write_permission(),
)
.unwrap();

View File

@ -94,7 +94,7 @@ async fn rebase_commit() {
{
// apply first vbranch again
branch1_id = controller
.create_virtual_branch_from_branch(project, &unapplied_branch)
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
.await
.unwrap();
@ -192,7 +192,7 @@ async fn rebase_work() {
{
// apply first vbranch again
branch1_id = controller
.create_virtual_branch_from_branch(project, &unapplied_branch)
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
.await
.unwrap();

View File

@ -92,7 +92,7 @@ async fn conflicting() {
let branch_id = {
// apply branch, it should conflict
let branch_id = controller
.create_virtual_branch_from_branch(project, &unapplied_branch)
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
.await
.unwrap();

View File

@ -56,7 +56,7 @@ async fn integration() {
// checkout a existing remote branch
let branch_id = controller
.create_virtual_branch_from_branch(project, &branch_name)
.create_virtual_branch_from_branch(project, &branch_name, None)
.await
.unwrap();
@ -151,7 +151,11 @@ async fn no_conflicts() {
assert!(branches.is_empty());
let branch_id = controller
.create_virtual_branch_from_branch(project, &"refs/remotes/origin/branch".parse().unwrap())
.create_virtual_branch_from_branch(
project,
&"refs/remotes/origin/branch".parse().unwrap(),
None,
)
.await
.unwrap();
@ -197,7 +201,11 @@ async fn conflicts_with_uncommited() {
// branch should be created unapplied, because of the conflict
let new_branch_id = controller
.create_virtual_branch_from_branch(project, &"refs/remotes/origin/branch".parse().unwrap())
.create_virtual_branch_from_branch(
project,
&"refs/remotes/origin/branch".parse().unwrap(),
None,
)
.await
.unwrap();
let new_branch = controller
@ -253,7 +261,11 @@ async fn conflicts_with_commited() {
// branch should be created unapplied, because of the conflict
let new_branch_id = controller
.create_virtual_branch_from_branch(project, &"refs/remotes/origin/branch".parse().unwrap())
.create_virtual_branch_from_branch(
project,
&"refs/remotes/origin/branch".parse().unwrap(),
None,
)
.await
.unwrap();
let new_branch = controller
@ -289,6 +301,7 @@ async fn from_default_target() {
.create_virtual_branch_from_branch(
project,
&"refs/remotes/origin/master".parse().unwrap(),
None
)
.await
.unwrap_err()
@ -317,6 +330,7 @@ async fn from_non_existent_branch() {
.create_virtual_branch_from_branch(
project,
&"refs/remotes/origin/branch".parse().unwrap(),
None
)
.await
.unwrap_err()
@ -355,7 +369,11 @@ async fn from_state_remote_branch() {
.unwrap();
let branch_id = controller
.create_virtual_branch_from_branch(project, &"refs/remotes/origin/branch".parse().unwrap())
.create_virtual_branch_from_branch(
project,
&"refs/remotes/origin/branch".parse().unwrap(),
None,
)
.await
.unwrap();

View File

@ -136,7 +136,7 @@ async fn resolve_conflict_flow() {
let branch1_id = {
// when we apply conflicted branch, it has conflict
let branch1_id = controller
.create_virtual_branch_from_branch(project, &unapplied_branch)
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
.await
.unwrap();

View File

@ -376,7 +376,7 @@ async fn applying_first_branch() {
.unwrap();
let unapplied_branch = Refname::from_str(&unapplied_branch).unwrap();
controller
.create_virtual_branch_from_branch(project, &unapplied_branch)
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
.await
.unwrap();

View File

@ -54,7 +54,7 @@ mod applied_branch {
{
// applying the branch should produce conflict markers
controller
.create_virtual_branch_from_branch(project, &unapplied_branch)
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
.await
.unwrap();
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
@ -124,7 +124,7 @@ mod applied_branch {
{
// applying the branch should produce conflict markers
controller
.create_virtual_branch_from_branch(project, &unapplied_branch)
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
.await
.unwrap();
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
@ -200,7 +200,7 @@ mod applied_branch {
{
// applying the branch should produce conflict markers
controller
.create_virtual_branch_from_branch(project, &unapplied_branch)
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
.await
.unwrap();
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
@ -273,7 +273,7 @@ mod applied_branch {
{
// applying the branch should produce conflict markers
controller
.create_virtual_branch_from_branch(project, &unapplied_branch)
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
.await
.unwrap();
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
@ -346,7 +346,7 @@ mod applied_branch {
{
// applying the branch should produce conflict markers
controller
.create_virtual_branch_from_branch(project, &unapplied_branch)
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
.await
.unwrap();
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
@ -767,6 +767,7 @@ mod applied_branch {
.create_virtual_branch_from_branch(
project,
&Refname::from_str(unapplied_refname.as_str()).unwrap(),
None,
)
.await
.unwrap();

View File

@ -83,10 +83,11 @@ pub mod commands {
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch: Refname,
remote: Option<RemoteRefname>,
) -> Result<BranchId, Error> {
let project = projects.get(project_id)?;
let branch_id = VirtualBranchActions
.create_virtual_branch_from_branch(&project, &branch)
.create_virtual_branch_from_branch(&project, &branch, remote)
.await?;
emit_vbranches(&windows, project_id).await;
Ok(branch_id)