Skip to content

Commit 7e1130a

Browse files
authored
Merge pull request #10534 from gitbutlerapp/upstream-integration-empty-commit-tests
Upstream integration: Respect empty commits
2 parents 4db3c35 + 2bb7a77 commit 7e1130a

File tree

5 files changed

+142
-80
lines changed

5 files changed

+142
-80
lines changed

apps/desktop/src/components/BranchHeaderContextMenu.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@
237237
<ContextMenuItem
238238
label="Add empty commit"
239239
icon="new-empty-commit"
240+
testId={TestId.BranchHeaderContextMenu_AddEmptyCommit}
240241
onclick={async () => {
241242
await insertBlankCommitInBranch({
242243
projectId,

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

Lines changed: 29 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::{r#virtual::IsCommitIntegrated, BranchManagerExt, VirtualBranchesExt as _};
1+
use std::collections::HashMap;
2+
3+
use crate::{BranchManagerExt, VirtualBranchesExt as _};
24
use anyhow::{anyhow, bail, Context, Result};
35
use bstr::ByteSlice;
46
use but_core::Reference;
@@ -264,30 +266,11 @@ fn stack_details(
264266
/// Takes both a gix and git2 repository. The git2 repository can't be in
265267
/// memory as the gix repository needs to be able to access those commits
266268
fn get_stack_status(
267-
repo: &git2::Repository,
268269
gix_repo: &gix::Repository,
269-
target: Target,
270270
new_target_commit_id: gix::ObjectId,
271271
stack_id: Option<StackId>,
272272
ctx: &CommandContext,
273273
) -> Result<StackStatus> {
274-
let cache = gix_repo.commit_graph_if_enabled()?;
275-
let mut graph = gix_repo.revision_graph(cache.as_ref());
276-
let upstream_commit_oids = repo.l(
277-
gix_to_git2_oid(new_target_commit_id),
278-
LogUntil::Commit(target.sha),
279-
true,
280-
)?;
281-
let new_target_tree_id = gix_repo.find_commit(new_target_commit_id)?.tree_id()?;
282-
let mut check_commit = IsCommitIntegrated::new_basic(
283-
gix_repo,
284-
repo,
285-
&mut graph,
286-
git2_to_gix_object_id(target.sha),
287-
new_target_tree_id.detach(),
288-
upstream_commit_oids,
289-
);
290-
291274
let mut unintegrated_branch_found = false;
292275

293276
let mut last_head: git2::Oid = gix_to_git2_oid(new_target_commit_id);
@@ -298,10 +281,25 @@ fn get_stack_status(
298281

299282
let branches = details.branch_details;
300283
for branch in &branches {
301-
let branch_head = repo.find_commit(branch.tip.to_git2())?;
284+
let local_commits = &branch.commits;
285+
286+
let Some(branch_head) = local_commits.first() else {
287+
branch_statuses.push(NameAndStatus {
288+
name: branch.name.to_string(),
289+
status: BranchStatus::Empty,
290+
});
291+
292+
continue;
293+
};
294+
302295
// If an integrated branch has been found, there is no need to bother
303296
// with subsequent branches.
304-
if !unintegrated_branch_found && check_commit.is_integrated(&branch_head)? {
297+
if !unintegrated_branch_found
298+
&& matches!(
299+
branch_head.state,
300+
but_workspace::ui::CommitState::Integrated
301+
)
302+
{
305303
branch_statuses.push(NameAndStatus {
306304
name: branch.name.to_string(),
307305
status: BranchStatus::Integrated,
@@ -317,17 +315,6 @@ fn get_stack_status(
317315
// mergable is rebasable.
318316
// Doing both would be preferable, but we don't communicate that
319317
// to the frontend at the minute.
320-
let local_commits = &branch.commits;
321-
322-
if local_commits.is_empty() {
323-
branch_statuses.push(NameAndStatus {
324-
name: branch.name.to_string(),
325-
status: BranchStatus::Empty,
326-
});
327-
328-
continue;
329-
}
330-
331318
let local_commit_ids = local_commits
332319
.iter()
333320
.map(|commit| commit.id)
@@ -455,9 +442,7 @@ pub fn upstream_integration_statuses(
455442
Ok((
456443
stack.id,
457444
get_stack_status(
458-
repo,
459445
&gix_repo_in_memory,
460-
target.clone(),
461446
git2_to_gix_object_id(new_target.id()),
462447
stack.id,
463448
context.ctx,
@@ -756,20 +741,6 @@ fn compute_resolutions(
756741
))
757742
}
758743
ResolutionApproach::Rebase => {
759-
let gix_repo = gitbutler_command_context::gix_repo_for_merging(repo.path())?;
760-
let cache = gix_repo.commit_graph_if_enabled()?;
761-
let mut graph = gix_repo.revision_graph(cache.as_ref());
762-
let upstream_commit_oids =
763-
repo.l(new_target.id(), LogUntil::Commit(target.sha), true)?;
764-
let mut check_commit = IsCommitIntegrated::new_basic(
765-
&gix_repo,
766-
repo,
767-
&mut graph,
768-
git2_to_gix_object_id(target.sha),
769-
git2_to_gix_object_id(new_target.tree_id()),
770-
upstream_commit_oids,
771-
);
772-
773744
// Rebase the commits, then try rebasing the tree. If
774745
// the tree ends up conflicted, commit the tree.
775746

@@ -782,6 +753,12 @@ fn compute_resolutions(
782753
};
783754

784755
let details = stack_details(context.ctx, stack.id)?;
756+
let mut commit_map = HashMap::new();
757+
for branch in &details.branch_details {
758+
for commit in &branch.commits {
759+
commit_map.insert(commit.id, commit.clone());
760+
}
761+
}
785762

786763
let all_steps = details.as_rebase_steps(context.gix_repo)?;
787764
let branches_before = as_buckets(all_steps.clone());
@@ -794,7 +771,9 @@ fn compute_resolutions(
794771
new_message: _,
795772
} => {
796773
let commit = repo.find_commit(commit_id.to_git2()).ok()?;
797-
let is_integrated = check_commit.is_integrated(&commit).ok()?;
774+
let is_integrated = commit_map.get(&commit_id).is_some_and(|c| {
775+
matches!(c.state, but_workspace::ui::CommitState::Integrated)
776+
});
798777
let forced = forced_integrated(
799778
&resolution.force_integrated_branches,
800779
&branches_before,

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

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -326,36 +326,6 @@ impl<'repo, 'cache, 'graph> IsCommitIntegrated<'repo, 'cache, 'graph> {
326326
upstream_change_ids,
327327
})
328328
}
329-
330-
/// Used to construct [`IsCommitIntegrated`] without a [`CommandContext`]. If
331-
/// you have a `CommandContext` available, use [`Self::new`] instead.
332-
pub(crate) fn new_basic(
333-
gix_repo: &'repo gix::Repository,
334-
repo: &'repo git2::Repository,
335-
graph: &'graph mut MergeBaseCommitGraph<'repo, 'cache>,
336-
target_commit_id: gix::ObjectId,
337-
upstream_tree_id: gix::ObjectId,
338-
mut upstream_commits: Vec<git2::Oid>,
339-
) -> Self {
340-
// Ensure upstream commits are sorted for binary search
341-
upstream_commits.sort();
342-
let upstream_change_ids = upstream_commits
343-
.iter()
344-
.filter_map(|oid| {
345-
let commit = repo.find_commit(*oid).ok()?;
346-
commit.change_id()
347-
})
348-
.sorted()
349-
.collect();
350-
Self {
351-
gix_repo,
352-
graph,
353-
target_commit_id,
354-
upstream_tree_id,
355-
upstream_commits,
356-
upstream_change_ids,
357-
}
358-
}
359329
}
360330

361331
impl IsCommitIntegrated<'_, '_, '_> {

e2e/playwright/tests/upstreamIntegration.spec.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,114 @@ test('should handle the update of the workspace with multiple stacks gracefully'
114114
stacks = getByTestId(page, 'stack');
115115
await expect(stacks).toHaveCount(1);
116116
});
117+
118+
test('should handle the update of an empty branch gracefully', async ({
119+
page,
120+
context
121+
}, testInfo) => {
122+
const workdir = testInfo.outputPath('workdir');
123+
const configdir = testInfo.outputPath('config');
124+
gitbutler = await startGitButler(workdir, configdir, context);
125+
126+
await gitbutler.runScript('project-with-stacks.sh');
127+
128+
await page.goto('/');
129+
130+
// Should load the workspace
131+
await waitForTestId(page, 'workspace-view');
132+
133+
// There should be no stacks
134+
let stacks = getByTestId(page, 'stack');
135+
await expect(stacks).toHaveCount(0);
136+
137+
// Create a new branch
138+
await clickByTestId(page, 'create-stack-button');
139+
const modal = await waitForTestId(page, 'create-new-branch-modal');
140+
141+
const input = modal.locator('#new-branch-name-input');
142+
await input.fill('new-branch');
143+
await clickByTestId(page, 'confirm-submit');
144+
145+
// There should be no stacks
146+
stacks = getByTestId(page, 'stack');
147+
await expect(stacks).toHaveCount(1);
148+
149+
// Branch one was merged in the forge
150+
await gitbutler.runScript('merge-upstream-branch-to-base.sh', ['branch1']);
151+
152+
// Click the sync button
153+
await clickByTestId(page, 'sync-button');
154+
155+
// Update the workspace
156+
await clickByTestId(page, 'integrate-upstream-commits-button');
157+
await clickByTestId(page, 'integrate-upstream-action-button');
158+
159+
// There should be one stack left
160+
stacks = getByTestId(page, 'stack');
161+
await expect(stacks).toHaveCount(1);
162+
});
163+
164+
test('should handle the update of a branch with an empty commit', async ({
165+
page,
166+
context
167+
}, testInfo) => {
168+
const workdir = testInfo.outputPath('workdir');
169+
const configdir = testInfo.outputPath('config');
170+
gitbutler = await startGitButler(workdir, configdir, context);
171+
172+
await gitbutler.runScript('project-with-stacks.sh');
173+
174+
await page.goto('/');
175+
176+
// Should load the workspace
177+
await waitForTestId(page, 'workspace-view');
178+
179+
// There should be no stacks
180+
let stacks = getByTestId(page, 'stack');
181+
await expect(stacks).toHaveCount(0);
182+
183+
// Create a new branch
184+
await clickByTestId(page, 'create-stack-button');
185+
const modal = await waitForTestId(page, 'create-new-branch-modal');
186+
187+
const input = modal.locator('#new-branch-name-input');
188+
await input.fill('new-branch');
189+
await clickByTestId(page, 'confirm-submit');
190+
191+
// There should be one stack
192+
stacks = getByTestId(page, 'stack');
193+
await expect(stacks).toHaveCount(1);
194+
195+
const branchCard = getByTestId(page, 'branch-card');
196+
await branchCard.isVisible();
197+
await branchCard.click({
198+
button: 'right'
199+
});
200+
201+
// Add an empty commit
202+
await waitForTestId(page, 'branch-header-context-menu');
203+
await clickByTestId(page, 'branch-header-context-menu-add-empty-commit');
204+
205+
// There should be one commit
206+
let commits = getByTestId(page, 'commit-row');
207+
await expect(commits).toHaveCount(1);
208+
209+
// Branch one was merged in the forge
210+
await gitbutler.runScript('merge-upstream-branch-to-base.sh', ['branch1']);
211+
212+
// Click the sync button
213+
await clickByTestId(page, 'sync-button');
214+
215+
// Update the workspace
216+
await clickByTestId(page, 'integrate-upstream-commits-button');
217+
await clickByTestId(page, 'integrate-upstream-action-button');
218+
219+
await waitForTestIdToNotExist(page, 'integrate-upstream-action-button');
220+
221+
// There should be one stack left
222+
stacks = getByTestId(page, 'stack');
223+
await expect(stacks).toHaveCount(1);
224+
// There should be one commit
225+
commits = getByTestId(page, 'commit-row');
226+
await expect(commits).toHaveCount(1);
227+
});

packages/ui/src/lib/utils/testIds.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export enum TestId {
2424
BranchHeaderAddDependanttBranchModal = 'branch-header-add-dependent-branch-modal',
2525
BranchHeaderAddDependanttBranchModal_ActionButton = 'branch-header-add-dependent-branch-modal-action-button',
2626
BranchHeaderContextMenu_SquashAllCommits = 'branch-header-context-menu-squash-all-commits',
27+
BranchHeaderContextMenu_AddEmptyCommit = 'branch-header-context-menu-add-empty-commit',
2728
EditCommitMessageBox = 'edit-commit-message-box',
2829
CommitDrawer = 'commit-drawer',
2930
CommitDrawerActionUncommit = 'commit-drawer-action-uncommit',

0 commit comments

Comments
 (0)