Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve the "update" button #4782

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

183 changes: 161 additions & 22 deletions apps/desktop/src/lib/components/UpdateBaseButton.svelte
Original file line number Diff line number Diff line change
@@ -1,36 +1,175 @@
<script lang="ts">
import { showInfo, showError } from '$lib/notifications/toasts';
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
import CommitCard from '$lib/commit/CommitCard.svelte';
import { getGitHost } from '$lib/gitHost/interface/gitHost';
import Select from '$lib/select/Select.svelte';
import SelectItem from '$lib/select/SelectItem.svelte';
import { getContext } from '$lib/utils/context';
import { BranchController } from '$lib/vbranches/branchController';
import {
UpstreamIntegrationService,
type BranchStatusesWithBranches
} from '$lib/vbranches/upstreamIntegrationService';
import Button from '@gitbutler/ui/Button.svelte';
import Modal from '@gitbutler/ui/Modal.svelte';
import type { Readable } from 'svelte/store';

const branchController = getContext(BranchController);
const upstreamIntegrationService = getContext(UpstreamIntegrationService);
const baseBranchService = getContext(BaseBranchService);
const gitHost = getGitHost();

let loading = false;
const base = baseBranchService.base;

let modal = $state<Modal>();

let modalOpeningState = $state<'inert' | 'loading' | 'completed'>('inert');
let branchStatuses = $state<Readable<BranchStatusesWithBranches | undefined>>();

const statuses = $derived.by(() => {
if ($branchStatuses?.type !== 'UpdatesRequired') return [];

const statuses = [...$branchStatuses.subject];
statuses.sort((a, b) => {
if (
(a.status.type !== 'FullyIntegrated' && b.status.type !== 'FullyIntegrated') ||
(a.status.type === 'FullyIntegrated' && b.status.type === 'FullyIntegrated')
) {
return (a.branch?.name || 'Unknown').localeCompare(b.branch?.name || 'Unknown');
}

if (a.status.type === 'FullyIntegrated') {
return 1;
} else {
return -1;
}
});

return statuses;
});

$effect(() => {
if ($branchStatuses && modalOpeningState === 'loading') {
modalOpeningState = 'completed';
modal?.show();
console.log(modalOpeningState);
}
});

function openModal() {
modalOpeningState = 'loading';
branchStatuses = upstreamIntegrationService.upstreamStatuses();
}

function onClose() {
modalOpeningState = 'inert';
}

$inspect($branchStatuses);
</script>

<Modal bind:this={modal} title="Integrate upstream changes" {onClose} width="small">
{#if $base}
<div class="upstream-commits">
{#each $base.upstreamCommits as commit, index}
<CommitCard
{commit}
first={index === 0}
last={index === $base.upstreamCommits.length - 1}
isUnapplied={true}
commitUrl={$gitHost?.commitUrl(commit.id)}
type="remote"
/>
{/each}
</div>
{/if}
<div class="statuses">
{#each statuses as { branch, status }}
<div class="branch-status" class:integrated={status.type === 'FullyIntegrated'}>
<div class="description">
<h5 class="text-16">{branch?.name || 'Unknown'}</h5>
{#if status.type === 'Conflicted'}
<p>Conflicted</p>
{:else if status.type === 'SaflyUpdatable' || status.type === 'Empty'}
<p>No Conflicts</p>
{:else if status.type === 'FullyIntegrated'}
<p>Integrated</p>
{/if}
</div>

<div class="action" class:action--centered={status.type === 'FullyIntegrated'}>
{#if status.type === 'FullyIntegrated'}
<p>Changes included in base branch</p>
{:else}
<Select
value="rebase"
options={[
{ label: 'Rebase', value: 'rebase' },
{ label: 'Merge', value: 'merge' },
{ label: 'Stash', value: 'stash' }
]}
>
{#snippet itemSnippet({ item, highlighted })}
<SelectItem selected={highlighted} {highlighted}>
{item.label}
</SelectItem>
{/snippet}
</Select>
{/if}
</div>
</div>
{/each}
</div>

{#snippet controls()}
<Button onclick={() => modal?.close()}>Cancel</Button>
<Button onclick={() => modal?.close()} style="pop" kind="solid">Integrate</Button>
{/snippet}
</Modal>

<Button
size="tag"
style="error"
kind="solid"
tooltip="Merge upstream into common base"
onclick={async () => {
loading = true;
try {
let infoText = await branchController.updateBaseBranch();
if (infoText) {
showInfo('Stashed conflicting branches', infoText);
}
} catch (err) {
showError('Failed update workspace', err);
} finally {
loading = false;
}
}}
onclick={openModal}
loading={modalOpeningState === 'loading'}
>
{#if loading}
busy...
{:else}
Update
{/if}
Update
</Button>

<style>
.upstream-commits {
text-align: left;

margin-top: -10px;
margin-bottom: 8px;
}

.branch-status {
display: flex;
justify-content: space-between;

padding: 14px;

&.integrated {
background-color: var(--clr-bg-2);
}

& .description {
display: flex;
flex-direction: column;

gap: 8px;
text-align: left;
}

& .action {
width: 144px;

&.action--centered {
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>
4 changes: 1 addition & 3 deletions apps/desktop/src/lib/layout/SettingsPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@
{title}
</h1>
{/if}
{#if children}
{@render children()}
{/if}
{@render children()}
</div>
</div>
</ScrollableContainer>
Expand Down
5 changes: 1 addition & 4 deletions apps/desktop/src/lib/navigation/WorkspaceButton.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
<script lang="ts">
import DomainButton from './DomainButton.svelte';
import UpdateBaseButton from '../components/UpdateBaseButton.svelte';
import { BaseBranch } from '$lib/baseBranch/baseBranch';
import { getContextStore } from '$lib/utils/context';
import { goto } from '$app/navigation';
import { page } from '$app/stores';

Expand All @@ -13,7 +11,6 @@

const { href, isNavCollapsed }: Props = $props();

const baseBranch = getContextStore(BaseBranch);
const label = 'Workspace';
</script>

Expand All @@ -27,7 +24,7 @@

{#if !isNavCollapsed}
<span class="text-14 text-semibold" class:collapsed-txt={isNavCollapsed}>{label}</span>
{#if ($baseBranch?.behind || 0) > 0 && !isNavCollapsed}
{#if !isNavCollapsed}
<UpdateBaseButton />
{/if}
{/if}
Expand Down
71 changes: 71 additions & 0 deletions apps/desktop/src/lib/vbranches/upstreamIntegrationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { invoke } from '$lib/backend/ipc';
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
import { derived, readable, type Readable } from 'svelte/store';
import type { Project } from '$lib/backend/projects';
import type { VirtualBranch } from '$lib/vbranches/types';

type BranchStatus =
| {
type: 'Empty' | 'FullyIntegrated' | 'SaflyUpdatable';
}
| {
type: 'Conflicted';
subject: {
potentiallyConflictedUncommitedChanges: boolean;
};
};

type BranchStatuses =
| {
type: 'UpToDate';
}
| {
type: 'UpdatesRequired';
subject: [string, BranchStatus][];
};

export type BranchStatusesWithBranches =
| {
type: 'UpToDate';
}
| {
type: 'UpdatesRequired';
subject: { branch: VirtualBranch | undefined; status: BranchStatus }[];
};

export class UpstreamIntegrationService {
constructor(
private project: Project,
private virtual_branch_service: VirtualBranchService
) {}

upstreamStatuses(): Readable<BranchStatusesWithBranches | undefined> {
const branchStatuses = readable<BranchStatuses | undefined>(undefined, (set) => {
invoke<BranchStatuses>('upstream_integration_statuses', { projectId: this.project.id }).then(
set
);
});

const branchStatusesWithBranches = derived(
[branchStatuses, this.virtual_branch_service.branches],
([branchStatuses, branches]): BranchStatusesWithBranches | undefined => {
if (!branchStatuses || !branches) return;
if (branchStatuses.type === 'UpToDate') return branchStatuses;

return {
type: 'UpdatesRequired',
subject: branchStatuses.subject.map((status) => {
const branch = branches.find((appliedBranch) => appliedBranch.id === status[0]);

return {
branch,
status: status[1]
};
})
};
}
);

return branchStatusesWithBranches;
}
}
2 changes: 2 additions & 0 deletions apps/desktop/src/routes/[projectId]/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import { parseRemoteUrl } from '$lib/url/gitUrl';
import { debounce } from '$lib/utils/debounce';
import { BranchController } from '$lib/vbranches/branchController';
import { UpstreamIntegrationService } from '$lib/vbranches/upstreamIntegrationService';
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
import { onDestroy, setContext, type Snippet } from 'svelte';
import type { LayoutData } from './$types';
Expand Down Expand Up @@ -77,6 +78,7 @@
setContext(BranchListingService, data.branchListingService);
setContext(ModeService, data.modeService);
setContext(UncommitedFilesWatcher, data.uncommitedFileWatcher);
setContext(UpstreamIntegrationService, data.upstreamIntegrationService);
});

let intervalId: any;
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/routes/[projectId]/+layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ModeService } from '$lib/modes/service';
import { RemoteBranchService } from '$lib/stores/remoteBranches';
import { UncommitedFilesWatcher } from '$lib/uncommitedFiles/watcher';
import { BranchController } from '$lib/vbranches/branchController';
import { UpstreamIntegrationService } from '$lib/vbranches/upstreamIntegrationService';
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
import { error } from '@sveltejs/kit';
import type { Project } from '$lib/backend/projects';
Expand Down Expand Up @@ -80,6 +81,7 @@ export const load: LayoutLoad = async ({ params, parent }) => {
const reorderDropzoneManagerFactory = new ReorderDropzoneManagerFactory(branchController);

const uncommitedFileWatcher = new UncommitedFilesWatcher(project);
const upstreamIntegrationService = new UpstreamIntegrationService(project, vbranchService);

return {
authService,
Expand All @@ -93,6 +95,7 @@ export const load: LayoutLoad = async ({ params, parent }) => {
projectMetrics,
modeService,
fetchSignal,
upstreamIntegrationService,

// These observables are provided for convenience
branchDragActionsFactory,
Expand Down
1 change: 1 addition & 0 deletions crates/gitbutler-branch-actions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ glob = "0.3.1"
serial_test = "3.1.1"
tempfile = "3.10"
criterion = "0.5.1"
uuid.workspace = true

[features]
## Only enabled when benchmark runs are performed.
Expand Down
16 changes: 13 additions & 3 deletions crates/gitbutler-branch-actions/src/actions.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::r#virtual as vbranch;
use crate::branch;
use super::r#virtual as branch;
use crate::upstream_integration::{self, BranchStatuses, UpstreamIntegrationContext};
use crate::{
base,
base::BaseBranch,
Expand Down Expand Up @@ -517,15 +518,24 @@ pub fn create_virtual_branch_from_branch(
pub fn get_uncommited_files(project: &Project) -> Result<Vec<RemoteBranchFile>> {
let context = CommandContext::open(project)?;
let guard = project.exclusive_worktree_access();
branch::get_uncommited_files(&context, guard.read_permission())
crate::branch::get_uncommited_files(&context, guard.read_permission())
}

/// Like [`get_uncommited_files()`], but returns a type that can be re-used with
/// [`crate::list_virtual_branches()`].
pub fn get_uncommited_files_reusable(project: &Project) -> Result<DiffByPathMap> {
let context = CommandContext::open(project)?;
let guard = project.exclusive_worktree_access();
branch::get_uncommited_files_raw(&context, guard.read_permission())
crate::branch::get_uncommited_files_raw(&context, guard.read_permission())
}

pub fn upstream_integration_statuses(project: &Project) -> Result<BranchStatuses> {
let command_context = CommandContext::open(project)?;
let mut guard = project.exclusive_worktree_access();

let context = UpstreamIntegrationContext::open(&command_context, guard.write_permission())?;

upstream_integration::upstream_integration_statuses(&context)
}

fn open_with_verify(project: &Project) -> Result<CommandContext> {
Expand Down
Loading
Loading