Skip to content

Commit 7b748ac

Browse files
authored
Merge pull request #11150 from gitbutlerapp/branches-stale-selection
Clean-up stale branch selection
2 parents ae01e61 + 7c3b2c0 commit 7b748ac

File tree

11 files changed

+128
-37
lines changed

11 files changed

+128
-37
lines changed

apps/desktop/cypress/e2e/support/mock/upstreamIntegration.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,5 @@ export const MOCK_BRANCH_STATUSES_RESPONSE: BranchStatusesResponse = {
55
};
66

77
export const MOCK_INTEGRATION_OUTCOME: IntegrationOutcome = {
8-
archivedBranches: [],
9-
reviewIdsToClose: []
8+
deletedBranches: []
109
};

apps/desktop/src/components/BranchesView.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@
192192
current.stackId || (current.branchName && !isTargetBranch)}
193193
{@const isNonLocalPr = !isStackOrNormalBranchPreview && current.prNumber !== undefined}
194194

195-
<div class="branches-view">
195+
<div class="branches-view" data-testid={TestId.BranchesView}>
196196
<div class="relative overflow-hidden radius-ml">
197197
<div
198198
bind:this={branchViewLeftEl}

apps/desktop/src/lib/bootstrap/deps.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export function initDependencies(args: {
262262
const upstreamIntegrationService = new UpstreamIntegrationService(
263263
clientState,
264264
stackService,
265-
projectsService
265+
uiState
266266
);
267267

268268
// ============================================================================

apps/desktop/src/lib/state/uiState.svelte.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,3 +527,15 @@ export function updateStalePrSelection(uiState: UiState, projectId: string, prs:
527527
projectState.branchesSelection.set({});
528528
}
529529
}
530+
531+
export function updateStaleStackSelectionInBranchesView(
532+
uiState: UiState,
533+
projectId: string,
534+
deletedBranches: string[]
535+
) {
536+
const projectState = uiState.project(projectId);
537+
const selection = projectState.branchesSelection.current;
538+
if (selection.branchName && deletedBranches.includes(selection.branchName)) {
539+
projectState.branchesSelection.set({});
540+
}
541+
}

apps/desktop/src/lib/upstream/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ export type BaseBranchResolution = {
6363
};
6464

6565
export type IntegrationOutcome = {
66-
archivedBranches: string[];
67-
reviewIdsToClose: string[];
66+
deletedBranches: string[];
6867
};
6968

7069
export function getBaseBranchResolution(

apps/desktop/src/lib/upstream/upstreamIntegrationService.svelte.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { ProjectsService } from '$lib/project/projectsService';
21
import { invalidatesList, providesList, ReduxTag } from '$lib/state/tags';
2+
import { updateStaleStackSelectionInBranchesView, type UiState } from '$lib/state/uiState.svelte';
33
import { InjectionToken } from '@gitbutler/core/context';
44
import { isDefined } from '@gitbutler/ui/utils/typeguards';
55
import type { StackService } from '$lib/stacks/stackService.svelte';
@@ -23,9 +23,9 @@ export class UpstreamIntegrationService {
2323
constructor(
2424
state: ClientState,
2525
private stackService: StackService,
26-
private projectsService: ProjectsService
26+
uiState: UiState
2727
) {
28-
this.api = injectEndpoints(state.backendApi);
28+
this.api = injectEndpoints(state.backendApi, uiState);
2929
}
3030

3131
async upstreamStatuses(
@@ -72,7 +72,7 @@ export class UpstreamIntegrationService {
7272
}
7373
}
7474

75-
function injectEndpoints(api: ClientState['backendApi']) {
75+
function injectEndpoints(api: ClientState['backendApi'], uiState: UiState) {
7676
return api.injectEndpoints({
7777
endpoints: (build) => ({
7878
upstreamIntegrationStatuses: build.query<
@@ -99,8 +99,13 @@ function injectEndpoints(api: ClientState['backendApi']) {
9999
invalidatesTags: [
100100
invalidatesList(ReduxTag.UpstreamIntegrationStatus),
101101
invalidatesList(ReduxTag.Stacks),
102-
invalidatesList(ReduxTag.StackDetails)
103-
]
102+
invalidatesList(ReduxTag.StackDetails),
103+
invalidatesList(ReduxTag.BranchListing)
104+
],
105+
transformResponse: (response: IntegrationOutcome, _, { projectId }) => {
106+
updateStaleStackSelectionInBranchesView(uiState, projectId, response.deletedBranches);
107+
return response;
108+
}
104109
}),
105110
resolveUpstreamIntegration: build.mutation<
106111
string,

crates/gitbutler-branch-actions/src/upstream_integration.rs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,8 @@ pub struct BaseBranchResolution {
9898
#[derive(Serialize, Deserialize, PartialEq, Debug)]
9999
#[serde(rename_all = "camelCase")]
100100
pub struct IntegrationOutcome {
101-
/// This is the list of branch names that have become archived as a result of the upstream integration
102-
archived_branches: Vec<String>,
103-
/// This is the list of review ids that have been closed as a result of the upstream integration
104-
review_ids_to_close: Vec<String>,
101+
/// The list of branches that have been deleted as a result of the upstream integration
102+
deleted_branches: Vec<String>,
105103
}
106104

107105
impl StackStatus {
@@ -468,8 +466,7 @@ pub(crate) fn integrate_upstream(
468466
let virtual_branches_state = VirtualBranchesHandle::new(ctx.project().gb_dir());
469467
let default_target = virtual_branches_state.get_default_target()?;
470468

471-
let mut newly_archived_branches = vec![];
472-
let mut to_be_closed_review_ids = vec![];
469+
let mut deleted_branches = vec![];
473470

474471
// Ensure resolutions match current statuses
475472
{
@@ -544,7 +541,15 @@ pub(crate) fn integrate_upstream(
544541

545542
if delete_local_refs {
546543
for head in &stack.heads {
547-
head.delete_reference(&gix_repo).ok();
544+
let branch_name = head.name.to_str().context("Invalid branch name")?;
545+
match head.delete_reference(&gix_repo) {
546+
Ok(_) => {
547+
deleted_branches.push(branch_name.to_string());
548+
}
549+
_ => {
550+
// Fail silently because interrupting this is worse
551+
}
552+
}
548553
}
549554
}
550555
}
@@ -611,10 +616,9 @@ pub(crate) fn integrate_upstream(
611616
.map(|r| r.delete_integrated_branches)
612617
.unwrap_or(false);
613618

614-
let (mut archived_branches, mut review_ids_to_close) =
619+
let stack_branches_deleted =
615620
stack.archive_integrated_heads(ctx, &gix_repo, for_archival, delete_local_refs)?;
616-
newly_archived_branches.append(&mut archived_branches);
617-
to_be_closed_review_ids.append(&mut review_ids_to_close);
621+
deleted_branches.extend(stack_branches_deleted);
618622
}
619623

620624
{
@@ -625,10 +629,10 @@ pub(crate) fn integrate_upstream(
625629
crate::integration::update_workspace_commit(&virtual_branches_state, ctx, false)?;
626630
}
627631

628-
Ok(IntegrationOutcome {
629-
archived_branches: newly_archived_branches,
630-
review_ids_to_close: to_be_closed_review_ids,
631-
})
632+
deleted_branches.sort();
633+
deleted_branches.dedup();
634+
635+
Ok(IntegrationOutcome { deleted_branches })
632636
}
633637

634638
pub(crate) fn resolve_upstream_integration(

crates/gitbutler-stack/src/stack.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -663,11 +663,10 @@ impl Stack {
663663
repo: &gix::Repository,
664664
for_archival: &[Reference],
665665
delete_local_refs: bool,
666-
) -> Result<(Vec<String>, Vec<String>)> {
666+
) -> Result<Vec<String>> {
667667
self.ensure_initialized()?;
668668

669-
let mut newly_archived_branches = vec![];
670-
let mut review_ids_to_close = vec![];
669+
let mut deleted_branches = vec![];
671670

672671
self.updated_timestamp_ms = gitbutler_time::time::now_ms();
673672
let state = branch_state(ctx);
@@ -678,13 +677,16 @@ impl Stack {
678677
Reference::Virtual(r) => r == head.name(),
679678
}) {
680679
head.archived = true;
681-
newly_archived_branches.push(head.name().clone());
682-
if let Some(review_id) = head.review_id.clone() {
683-
review_ids_to_close.push(review_id);
684-
}
685680

686681
if delete_local_refs {
687-
head.delete_reference(repo).ok(); // Fail silently because interrupting this is worse
682+
match head.delete_reference(repo) {
683+
Ok(_) => {
684+
deleted_branches.push(head.name().clone());
685+
}
686+
Err(_) => {
687+
// Ignore errors when deleting references
688+
}
689+
}
688690
}
689691
}
690692
}
@@ -701,7 +703,7 @@ impl Stack {
701703

702704
state.set_stack(self.clone())?;
703705

704-
Ok((newly_archived_branches, review_ids_to_close))
706+
Ok(deleted_branches)
705707
}
706708

707709
/// Prepares push details according to the series to be pushed (picking out the correct sha and remote refname)

e2e/playwright/scripts/merge-upstream-branch-to-base.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
echo "GIT CONFIG $GIT_CONFIG_GLOBAL"
44
echo "DATA DIR $GITBUTLER_CLI_DATA_DIR"
55
echo "BUT_TESTING $BUT_TESTING"
6-
echo "BRANCH TO APPLY: $1"
7-
# Apply remote branch to the workspace.
6+
echo "BRANCH TO MERGE: $1"
7+
8+
# Merge the upstream branch into master and delete the upstream branch
89
pushd remote-project
910
git checkout master
1011
git merge --no-ff -m "Merging upstream branch $1 into base" "$1"
12+
git branch -d "$1"
1113
popd

e2e/playwright/tests/branches.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,3 +574,70 @@ test('should handle gracefully applying two conflicting branches', async ({
574574
// The modal explaining this should be visible
575575
await waitForTestId(page, 'stacks-unapplied-toast');
576576
});
577+
578+
test('should update the stale selection of an unexisting branch', async ({
579+
page,
580+
context
581+
}, testInfo) => {
582+
const workdir = testInfo.outputPath('workdir');
583+
const configdir = testInfo.outputPath('config');
584+
gitbutler = await startGitButler(workdir, configdir, context);
585+
586+
await gitbutler.runScript('project-with-remote-branches.sh');
587+
// Apply branch1
588+
await gitbutler.runScript('apply-upstream-branch.sh', ['branch1', 'local-clone']);
589+
590+
await page.goto('/');
591+
592+
// Should load the workspace
593+
await waitForTestId(page, 'workspace-view');
594+
595+
// Navigate to branches page
596+
await clickByTestId(page, 'navigation-branches-button');
597+
let header = await waitForTestId(page, 'target-commit-list-header');
598+
599+
await expect(header).toContainText('origin/master');
600+
601+
let branchListCards = getByTestId(page, 'branch-list-card');
602+
await expect(branchListCards).toHaveCount(3);
603+
604+
// Select the branch1
605+
let firstBranchCard = branchListCards.filter({ hasText: 'branch1' });
606+
await expect(firstBranchCard).toBeVisible();
607+
await firstBranchCard.click();
608+
609+
// Go back to the workspace
610+
await clickByTestId(page, 'navigation-workspace-button');
611+
await waitForTestId(page, 'workspace-view');
612+
613+
// There should be one stack applied
614+
const stacks = getByTestId(page, 'stack');
615+
await expect(stacks).toHaveCount(1);
616+
617+
// Branch one was merged in the forge
618+
await gitbutler.runScript('merge-upstream-branch-to-base.sh', ['branch1']);
619+
620+
// Click the sync button
621+
await clickByTestId(page, 'sync-button');
622+
623+
// Update the workspace
624+
await clickByTestId(page, 'integrate-upstream-commits-button');
625+
await clickByTestId(page, 'integrate-upstream-action-button');
626+
await waitForTestIdToNotExist(page, 'integrate-upstream-action-button');
627+
628+
// There should be no stacks
629+
await waitForTestIdToNotExist(page, 'stack');
630+
631+
// Navigate to branches page
632+
await clickByTestId(page, 'navigation-branches-button');
633+
await waitForTestId(page, 'branches-view');
634+
635+
header = await waitForTestId(page, 'target-commit-list-header');
636+
637+
await expect(header).toContainText('origin/master');
638+
// The previously selected branch1 should not be selected anymore
639+
branchListCards = getByTestId(page, 'branch-list-card');
640+
await expect(branchListCards).toHaveCount(2);
641+
firstBranchCard = branchListCards.filter({ hasText: 'branch1' });
642+
await expect(firstBranchCard).not.toBeVisible();
643+
});

0 commit comments

Comments
 (0)