Skip to content

Commit c33f8f9

Browse files
committed
Handle commiting to workspace correctly
1 parent 5f69ab0 commit c33f8f9

File tree

6 files changed

+172
-77
lines changed

6 files changed

+172
-77
lines changed

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

+134-74
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
1+
use std::cmp::Ordering;
2+
use std::collections::HashMap;
13
use std::{path::PathBuf, vec};
24

35
use anyhow::{anyhow, Context, Result};
4-
use bstr::ByteSlice;
56
use gitbutler_branch::BranchCreateRequest;
67
use gitbutler_branch::{self, GITBUTLER_WORKSPACE_REFERENCE};
78
use gitbutler_cherry_pick::RepositoryExt as _;
89
use gitbutler_command_context::CommandContext;
910
use gitbutler_commit::commit_ext::CommitExt;
11+
use gitbutler_diff::diff_files_into_hunks;
1012
use gitbutler_error::error::Marker;
11-
use gitbutler_operating_modes::OPEN_WORKSPACE_REFS;
13+
use gitbutler_operating_modes::{OPEN_WORKSPACE_REFS, WORKSPACE_BRANCH_REF};
1214
use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt};
13-
use gitbutler_project::access::WorktreeWritePermission;
15+
use gitbutler_project::access::{WorktreeReadPermission, WorktreeWritePermission};
1416
use gitbutler_repo::logging::{LogUntil, RepositoryExt as _};
15-
use gitbutler_repo::RepositoryExt;
17+
use gitbutler_repo::rebase::cherry_rebase_group;
1618
use gitbutler_repo::SignaturePurpose;
17-
use gitbutler_stack::{Stack, VirtualBranchesHandle};
19+
use gitbutler_stack::{Stack, StackId, VirtualBranchesHandle};
1820
use tracing::instrument;
1921

22+
use crate::compute_workspace_dependencies;
2023
use crate::{branch_manager::BranchManagerExt, conflicts, VirtualBranchesExt};
2124

2225
const WORKSPACE_HEAD: &str = "Workspace Head";
@@ -322,28 +325,29 @@ fn verify_current_branch_name(ctx: &CommandContext) -> Result<&CommandContext> {
322325
}
323326
}
324327

325-
// TODO(ST): Probably there should not be an implicit vbranch creation here.
326-
fn verify_head_is_clean(ctx: &CommandContext, perm: &mut WorktreeWritePermission) -> Result<()> {
327-
let head_commit = ctx
328-
.repo()
329-
.head()
330-
.context("failed to get head")?
331-
.peel_to_commit()
332-
.context("failed to peel to commit")?;
328+
#[derive(Debug)]
329+
pub enum WorkspaceState {
330+
OffWorkspaceCommit {
331+
workspace_commit: git2::Oid,
332+
extra_commits: Vec<git2::Oid>,
333+
},
334+
OnWorkspaceCommit,
335+
}
333336

337+
pub fn workspace_state(
338+
ctx: &CommandContext,
339+
_perm: &WorktreeReadPermission,
340+
) -> Result<WorkspaceState> {
341+
let repository = ctx.repo();
334342
let vb_handle = VirtualBranchesHandle::new(ctx.project().gb_dir());
335-
let default_target = vb_handle
336-
.get_default_target()
337-
.context("failed to get default target")?;
343+
let default_target = vb_handle.get_default_target()?;
338344

339-
let commits = ctx
340-
.repo()
341-
.log(
342-
head_commit.id(),
343-
LogUntil::Commit(default_target.sha),
344-
false,
345-
)
346-
.context("failed to get log")?;
345+
let head_commit = repository.head()?.peel_to_commit()?;
346+
let commits = repository.log(
347+
head_commit.id(),
348+
LogUntil::Commit(default_target.sha),
349+
false,
350+
)?;
347351

348352
let workspace_index = commits
349353
.iter()
@@ -353,69 +357,125 @@ fn verify_head_is_clean(ctx: &CommandContext, perm: &mut WorktreeWritePermission
353357
|| message.starts_with(GITBUTLER_INTEGRATION_COMMIT_TITLE)
354358
})
355359
})
356-
.context("GitButler workspace commit not found")?;
360+
.context("")?;
357361
let workspace_commit = &commits[workspace_index];
358-
let mut extra_commits = commits[..workspace_index].to_vec();
359-
extra_commits.reverse();
362+
let extra_commits = commits[..workspace_index].to_vec();
360363

361364
if extra_commits.is_empty() {
362365
// no extra commits found, so we're good
363-
return Ok(());
366+
return Ok(WorkspaceState::OnWorkspaceCommit);
364367
}
365368

366-
ctx.repo()
367-
.reset(workspace_commit.as_object(), git2::ResetType::Soft, None)
368-
.context("failed to reset to workspace commit")?;
369-
370-
let branch_manager = ctx.branch_manager();
371-
let mut new_branch = branch_manager
372-
.create_virtual_branch(
373-
&BranchCreateRequest {
374-
name: extra_commits
375-
.last()
376-
.map(|commit| commit.message_bstr().to_string()),
377-
..Default::default()
378-
},
379-
perm,
380-
)
381-
.context("failed to create virtual branch")?;
382-
383-
// rebasing the extra commits onto the new branch
384-
let mut head = new_branch.head();
385-
for commit in extra_commits {
386-
let new_branch_head = ctx
387-
.repo()
388-
.find_commit(head)
389-
.context("failed to find new branch head")?;
390-
391-
let rebased_commit_oid = ctx
392-
.repo()
393-
.commit_with_signature(
394-
None,
395-
&commit.author(),
396-
&commit.committer(),
397-
&commit.message_bstr().to_str_lossy(),
398-
&commit.tree().unwrap(),
399-
&[&new_branch_head],
400-
None,
401-
)
402-
.context(format!(
403-
"failed to rebase commit {} onto new branch",
404-
commit.id()
405-
))?;
369+
Ok(WorkspaceState::OffWorkspaceCommit {
370+
workspace_commit: workspace_commit.id(),
371+
extra_commits: extra_commits
372+
.iter()
373+
.map(git2::Commit::id)
374+
.collect::<Vec<_>>(),
375+
})
376+
}
377+
378+
// TODO(ST): Probably there should not be an implicit vbranch creation here.
379+
fn verify_head_is_clean(ctx: &CommandContext, perm: &mut WorktreeWritePermission) -> Result<()> {
380+
let repository = ctx.repo();
381+
let head_commit = repository.head()?.peel_to_commit()?;
382+
383+
let WorkspaceState::OffWorkspaceCommit {
384+
workspace_commit,
385+
extra_commits,
386+
} = dbg!(workspace_state(ctx, perm.read_permission())?)
387+
else {
388+
return Ok(());
389+
};
406390

407-
let rebased_commit = ctx.repo().find_commit(rebased_commit_oid).context(format!(
408-
"failed to find rebased commit {}",
409-
rebased_commit_oid
410-
))?;
391+
let best_stack_id = find_best_stack_for_changes(ctx, perm, head_commit.id(), workspace_commit)?;
411392

412-
new_branch.set_stack_head(ctx, rebased_commit.id(), Some(rebased_commit.tree_id()))?;
393+
if let Some(best_stack_id) = best_stack_id {
394+
let vb_handle = VirtualBranchesHandle::new(ctx.project().gb_dir());
395+
let mut stack = vb_handle.get_stack_in_workspace(best_stack_id)?;
413396

414-
head = rebased_commit.id();
397+
let new_head = cherry_rebase_group(repository, stack.head(), &extra_commits, false)?;
398+
399+
stack.set_stack_head(
400+
ctx,
401+
new_head,
402+
Some(repository.find_commit(new_head)?.tree_id()),
403+
)?;
404+
405+
update_workspace_commit(&vb_handle, ctx)?;
406+
} else {
407+
// There is no stack which can hold the commits so we should just unroll those changes
408+
repository.reference(WORKSPACE_BRANCH_REF, workspace_commit, true, "")?;
409+
repository.set_head(WORKSPACE_BRANCH_REF)?;
415410
}
411+
416412
Ok(())
417413
}
418414

415+
fn find_best_stack_for_changes(
416+
ctx: &CommandContext,
417+
perm: &mut WorktreeWritePermission,
418+
head_commit: git2::Oid,
419+
workspace_commit: git2::Oid,
420+
) -> Result<Option<StackId>> {
421+
let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir());
422+
let default_target = vb_state.get_default_target()?;
423+
let repository = ctx.repo();
424+
let stacks = vb_state.list_stacks_in_workspace()?;
425+
426+
let head_commit = repository.find_commit(head_commit)?;
427+
428+
let diffs = gitbutler_diff::trees(
429+
ctx.repo(),
430+
&repository.find_commit(workspace_commit)?.tree()?,
431+
&head_commit.tree()?,
432+
true,
433+
)?;
434+
let base_diffs: HashMap<_, _> = diff_files_into_hunks(&diffs).collect();
435+
let workspace_dependencies =
436+
compute_workspace_dependencies(ctx, &default_target.sha, &base_diffs, &stacks)?;
437+
438+
match workspace_dependencies.commit_dependent_diffs.len().cmp(&1) {
439+
Ordering::Greater => {
440+
// The commits are locked to multiple stacks. We can't correctly assign it
441+
// to any one stack, so the commits should be undone.
442+
Ok(None)
443+
}
444+
Ordering::Equal => {
445+
// There is one stack which the commits are locked to, so the commits
446+
// should be added to that particular stack.
447+
let stack_id = workspace_dependencies
448+
.commit_dependent_diffs
449+
.keys()
450+
.next()
451+
.expect("Values was asserted length 1 above");
452+
Ok(Some(*stack_id))
453+
}
454+
Ordering::Less => {
455+
// We should return the branch selected for changes, or create a new default branch.
456+
let mut stacks = vb_state.list_stacks_in_workspace()?;
457+
stacks.sort_by_key(|stack| stack.selected_for_changes.unwrap_or(0));
458+
459+
if let Some(stack) = stacks.last() {
460+
return Ok(Some(stack.id));
461+
}
462+
463+
let branch_manager = ctx.branch_manager();
464+
let new_stack = branch_manager
465+
.create_virtual_branch(
466+
&BranchCreateRequest {
467+
name: Some(head_commit.message_bstr().to_string()),
468+
..Default::default()
469+
},
470+
perm,
471+
)
472+
.context("failed to create virtual branch")?;
473+
474+
Ok(Some(new_stack.id))
475+
}
476+
}
477+
}
478+
419479
fn invalid_head_err(head_name: &str) -> anyhow::Error {
420480
anyhow!(
421481
"project is on {head_name}. Please checkout {} to continue",

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub use dependencies::compute_workspace_dependencies;
3838
pub mod upstream_integration;
3939

4040
mod integration;
41-
pub use integration::{update_workspace_commit, verify_branch};
41+
pub use integration::{update_workspace_commit, verify_branch, workspace_state, WorkspaceState};
4242

4343
mod file;
4444
pub use file::{Get, RemoteBranchFile};

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use crate::{
88
remote::branch_to_remote_branch,
99
stack::stack_series,
1010
status::{get_applied_status, get_applied_status_cached},
11-
Get, RemoteBranchData, VirtualBranchHunkRange, VirtualBranchHunkRangeMap, VirtualBranchesExt,
11+
verify_branch, Get, RemoteBranchData, VirtualBranchHunkRange, VirtualBranchHunkRangeMap,
12+
VirtualBranchesExt,
1213
};
1314
use anyhow::{anyhow, bail, Context, Result};
1415
use bstr::{BString, ByteSlice};
@@ -326,6 +327,9 @@ pub fn list_virtual_branches_cached(
326327
) -> Result<StackListResult> {
327328
assure_open_workspace_mode(ctx)
328329
.context("Listing virtual branches requires open workspace mode")?;
330+
// Make sure that the workspace commit is the head of the branch before listing.
331+
verify_branch(ctx, perm)?;
332+
329333
let mut branches: Vec<VirtualBranch> = Vec::new();
330334

331335
let vb_state = ctx.project().virtual_branches();

crates/gitbutler-branch-actions/tests/virtual_branches/create_virtual_branch_from_branch.rs

+2
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,8 @@ mod conflict_cases {
382382
let branch_refname =
383383
gitbutler_branch_actions::save_and_unapply_virutal_branch(ctx, branch.id).unwrap();
384384

385+
gitbutler_branch_actions::list_virtual_branches(ctx).unwrap();
386+
385387
// Make X and set base branch to X
386388
let mut tree_builder = git_repository
387389
.treebuilder(Some(

crates/gitbutler-stack/src/stack.rs

+3
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,9 @@ impl Stack {
511511
self.updated_timestamp_ms = gitbutler_time::time::now_ms();
512512
#[allow(deprecated)] // this is the only place where this is allowed
513513
self.set_head(commit_id);
514+
// TODO(CTO): Determine whether this does anything. If we're not
515+
// calling `checkout_branch_trees` right after this, then it will
516+
// likly get overridden by the next `list_virtual_branches`.
514517
if let Some(tree) = tree {
515518
self.tree = tree;
516519
}

crates/gitbutler-watcher/src/handler.rs

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use std::{path::PathBuf, sync::Arc};
22

33
use anyhow::{Context, Result};
4-
use gitbutler_branch_actions::{internal::StackListResult, VirtualBranches};
4+
use gitbutler_branch_actions::{
5+
internal::StackListResult, verify_branch, workspace_state, VirtualBranches, WorkspaceState,
6+
};
57
use gitbutler_command_context::CommandContext;
68
use gitbutler_diff::DiffByPathMap;
79
use gitbutler_error::error::Marker;
@@ -196,6 +198,29 @@ impl Handler {
196198
Ok(())
197199
}
198200

201+
fn handle_commited_workspace(&self, ctx: &CommandContext) -> Result<()> {
202+
// Don't worry about non-workspace settings (yet)
203+
if !in_open_workspace_mode(ctx) {
204+
return Ok(());
205+
}
206+
207+
let workspace_state = {
208+
let guard = ctx.project().exclusive_worktree_access();
209+
let permission = guard.read_permission();
210+
workspace_state(ctx, permission)?
211+
};
212+
if matches!(workspace_state, WorkspaceState::OffWorkspaceCommit { .. }) {
213+
{
214+
let mut guard = ctx.project().exclusive_worktree_access();
215+
let permission = guard.write_permission();
216+
verify_branch(ctx, permission)?;
217+
}
218+
self.calculate_virtual_branches(ctx, None)?;
219+
}
220+
221+
Ok(())
222+
}
223+
199224
pub fn git_files_change(&self, paths: Vec<PathBuf>, ctx: &CommandContext) -> Result<()> {
200225
for path in paths {
201226
let Some(file_name) = path.to_str() else {
@@ -206,6 +231,7 @@ impl Handler {
206231
self.emit_app_event(Change::GitFetch(ctx.project().id))?;
207232
}
208233
"logs/HEAD" => {
234+
self.handle_commited_workspace(ctx)?;
209235
self.emit_app_event(Change::GitActivity(ctx.project().id))?;
210236
}
211237
"index" => {

0 commit comments

Comments
 (0)