Skip to content

Commit 6e6e730

Browse files
authored
Merge pull request #10338 from Byron/next
V3 apply/unapply: spread safe checkout
2 parents e47f937 + c678a00 commit 6e6e730

File tree

24 files changed

+276
-122
lines changed

24 files changed

+276
-122
lines changed

Cargo.lock

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ incremental = false
114114

115115
# Assure that `gix` is always fast so debug builds aren't unnecessarily slow.
116116
[profile.dev.package]
117+
gix = { opt-level = 3 }
117118
gix-object = { opt-level = 3 }
118119
gix-ref = { opt-level = 3 }
119120
gix-pack = { opt-level = 3 }

apps/desktop/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,11 @@ This builds the **web** target, points it to the but-server on `http://localhost
3636
#### 3. Go to the browser
3737

3838
Open Chrome (let's not kid ourselves) and got to `http://localhost:1420` and enjoy
39+
40+
### Development
41+
42+
#### Auto-build the server on Rust changes
43+
44+
```bash
45+
watchexec -w crates -r -- cargo run -p but-server
46+
```

crates/but-api/src/commands/workspace.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ pub fn uncommit_changes(
487487

488488
/// This API allows the user to quickly "stash" a bunch of uncommitted changes - getting them out of the worktree.
489489
/// Unlike the regular stash, the user specifies a new branch where those changes will be 'saved'/committed.
490-
/// Immediatelly after the changes are committed, the branch is unapplied from the workspace, and the "stash" branch can be re-applied at a later time
490+
/// Immediately after the changes are committed, the branch is unapplied from the workspace, and the "stash" branch can be re-applied at a later time
491491
/// In theory it should be possible to specify an existing "dumping" branch for this, but currently this endpoint expects a new branch.
492492
#[api_cmd]
493493
#[tauri::command(async)]
@@ -542,7 +542,13 @@ pub fn stash_into_branch(
542542
gitbutler_branch_actions::update_workspace_commit(&vb_state, &ctx)
543543
.context("failed to update gitbutler workspace")?;
544544

545-
branch_manager.unapply(stack.id, perm, false, Vec::new())?;
545+
branch_manager.unapply(
546+
stack.id,
547+
perm,
548+
false,
549+
Vec::new(),
550+
ctx.app_settings().feature_flags.cv3,
551+
)?;
546552

547553
let outcome = outcome?;
548554
Ok(outcome.into())

crates/but-server/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ but-broadcaster.workspace = true
3535
but-path.workspace = true
3636
but-settings.workspace = true
3737
but-feedback.workspace = true
38-
gitbutler-user.workspace = true
3938
gitbutler-project.workspace = true
4039
gitbutler-watcher.workspace = true
40+
tracing.workspace = true
41+
tracing-subscriber.workspace = true
42+
tracing-forest = { version = "0.2.0" }
43+
gitbutler-secret.workspace = true

crates/but-server/src/main.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,32 @@
11
#[tokio::main]
2-
async fn main() {
2+
async fn main() -> anyhow::Result<()> {
3+
trace::init()?;
4+
// On macOS, in dev mode with debug assertions, we encounter popups each time
5+
// the binary is rebuilt. To counter that, use a git-credential based implementation.
6+
// This isn't an issue for actual release build (i.e. nightly, production),
7+
// hence the specific condition.
8+
if cfg!(debug_assertions) && cfg!(target_os = "macos") {
9+
gitbutler_secret::secret::git_credentials::setup().ok();
10+
}
311
but_server::run().await;
12+
Ok(())
13+
}
14+
15+
mod trace {
16+
use tracing::metadata::LevelFilter;
17+
use tracing_subscriber::Layer;
18+
use tracing_subscriber::layer::SubscriberExt;
19+
use tracing_subscriber::util::SubscriberInitExt;
20+
21+
pub fn init() -> anyhow::Result<()> {
22+
tracing_subscriber::registry()
23+
.with(
24+
tracing_forest::ForestLayer::from(
25+
tracing_forest::printer::PrettyPrinter::new().writer(std::io::stderr),
26+
)
27+
.with_filter(LevelFilter::DEBUG),
28+
)
29+
.init();
30+
Ok(())
31+
}
432
}

crates/but-workspace/src/branch/checkout/function.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,22 @@ use gix::refs::transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog};
1111
use std::collections::BTreeSet;
1212
use tracing::instrument;
1313

14+
/// Like [`safe_checkout()`], but the current tree will always be fetched from
15+
pub fn safe_checkout_from_head(
16+
new_head_id: gix::ObjectId,
17+
repo: &gix::Repository,
18+
opts: Options,
19+
) -> anyhow::Result<Outcome> {
20+
safe_checkout(
21+
repo.head_tree_id_or_empty()?.detach(),
22+
new_head_id,
23+
repo,
24+
opts,
25+
)
26+
}
1427
/// Given the `current_head_id^{tree}` for the tree that matches what `HEAD` points to, perform all file operations necessary
1528
/// to turn the *worktree* of `repo` into `new_head_id^{tree}`. Note that the current *worktree* is assumed to be at the state of
16-
/// `current_head_tree_id` along with arbitrary uncommitted user changes.
29+
/// `current_head_id` along with arbitrary uncommitted user changes.
1730
///
1831
/// Note that we don't care if the worktree actually matches the `new_head_id^{tree}`, we only care about the operations from
1932
/// `current_head_id^{tree}` to be performed, and if there are none, we will do nothing.

crates/but-workspace/src/branch/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ impl Stack {
458458

459459
/// Functions related to workspace checkouts.
460460
pub mod checkout;
461-
pub use checkout::function::safe_checkout;
461+
pub use checkout::function::{safe_checkout, safe_checkout_from_head};
462462

463463
/// Functions and types related to applying a workspace branch.
464464
pub mod apply;

crates/but-workspace/src/head.rs

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
use anyhow::{Context, Result};
33
use gitbutler_cherry_pick::RepositoryExt as _;
44
use gitbutler_command_context::CommandContext;
5-
use gitbutler_oxidize::{
6-
GixRepositoryExt, ObjectIdExt, OidExt, RepoExt, git2_to_gix_object_id, gix_to_git2_oid,
7-
};
5+
use gitbutler_oxidize::{GixRepositoryExt, ObjectIdExt, OidExt};
86
use gitbutler_repo::{RepositoryExt, SignaturePurpose};
97
use gitbutler_stack::{Stack, VirtualBranchesHandle};
108
use gix::merge::tree::TreatAsUnresolved;
@@ -24,7 +22,7 @@ pub fn merge_worktree_with_workspace<'a>(
2422

2523
// The tree of where the gitbutler workspace is at
2624
let workspace_tree = gix_repo
27-
.find_commit(super::head(ctx)?.to_gix())?
25+
.find_commit(super::remerged_workspace_commit_v2(ctx)?.to_gix())?
2826
.tree_id()?
2927
.detach();
3028

@@ -42,36 +40,32 @@ pub fn merge_worktree_with_workspace<'a>(
4240
Ok((outcome, conflict_kind))
4341
}
4442

45-
/// Creates and returns a merge commit of all active branch heads.
46-
///
47-
/// This is the base against which we diff the working directory to understand
48-
/// what files have been modified.
49-
///
50-
/// This should be used to update the `gitbutler/workspace` ref with, which is usually
51-
/// done from [`update_workspace_commit()`], after any of its input changes.
52-
/// This is namely the conflicting state, or any head of the virtual branches.
53-
#[instrument(level = tracing::Level::DEBUG, skip(ctx))]
54-
pub fn head(ctx: &CommandContext) -> Result<git2::Oid> {
43+
/// Merge all currently stored stacks together into a new tree and return `(merged_tree, stacks, target_commit)` id accordingly.
44+
/// `gix_repo` should be optimised for merging.
45+
pub fn remerged_workspace_tree_v2<'git2_repo>(
46+
ctx: &'git2_repo CommandContext,
47+
gix_repo: &gix::Repository,
48+
) -> Result<(git2::Oid, Vec<Stack>, git2::Commit<'git2_repo>)> {
5549
let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir());
5650
let target = vb_state
5751
.get_default_target()
5852
.context("failed to get target")?;
59-
let repo: &git2::Repository = ctx.repo();
60-
53+
let repo = ctx.repo();
6154
let mut stacks: Vec<Stack> = vb_state.list_stacks_in_workspace()?;
6255

6356
let target_commit = repo.find_commit(target.sha)?;
64-
let mut workspace_tree = repo.find_real_tree(&target_commit, Default::default())?;
65-
let mut workspace_tree_id = git2_to_gix_object_id(workspace_tree.id());
57+
let workspace_tree = repo.find_real_tree(&target_commit, Default::default())?;
58+
let mut workspace_tree_id = workspace_tree.id().to_gix();
6659

67-
let gix_repo = ctx.gix_repo_for_merging()?;
6860
let (merge_options_fail_fast, conflict_kind) = gix_repo.merge_options_fail_fast()?;
69-
let merge_tree_id = git2_to_gix_object_id(repo.find_commit(target.sha)?.tree_id());
61+
let merge_tree_id = repo.find_commit(target.sha)?.tree_id().to_gix();
7062
for stack in stacks.iter_mut() {
71-
stack.migrate_change_ids(ctx).ok(); // If it fails thats ok - best effort migration
72-
let branch_head = repo.find_commit(stack.head_oid(&gix_repo)?.to_git2())?;
73-
let branch_tree_id =
74-
git2_to_gix_object_id(repo.find_real_tree(&branch_head, Default::default())?.id());
63+
stack.migrate_change_ids(ctx).ok(); // If it fails that's ok - best effort migration
64+
let branch_head = repo.find_commit(stack.head_oid(gix_repo)?.to_git2())?;
65+
let branch_tree_id = repo
66+
.find_real_tree(&branch_head, Default::default())?
67+
.id()
68+
.to_gix();
7569

7670
let mut merge = gix_repo.merge_trees(
7771
merge_tree_id,
@@ -90,11 +84,26 @@ pub fn head(ctx: &CommandContext) -> Result<git2::Oid> {
9084
vb_state.set_stack(stack.clone())?;
9185
}
9286
}
93-
workspace_tree = repo.find_tree(gix_to_git2_oid(workspace_tree_id))?;
87+
Ok((workspace_tree_id.to_git2(), stacks, target_commit))
88+
}
89+
90+
/// Creates and returns a merge commit of all active branch heads.
91+
///
92+
/// This is the base against which we diff the working directory to understand
93+
/// what files have been modified.
94+
///
95+
/// This should be used to update the `gitbutler/workspace` ref with, which is usually
96+
/// done from [`update_workspace_commit()`], after any of its input changes.
97+
/// This is namely the conflicting state, or any head of the virtual branches.
98+
#[instrument(level = tracing::Level::DEBUG, skip(ctx))]
99+
pub fn remerged_workspace_commit_v2(ctx: &CommandContext) -> Result<git2::Oid> {
100+
let repo = ctx.repo();
101+
let gix_repo = ctx.gix_repo_for_merging()?;
102+
let (workspace_tree_id, stacks, target_commit) = remerged_workspace_tree_v2(ctx, &gix_repo)?;
103+
let workspace_tree = repo.find_tree(workspace_tree_id)?;
94104

95105
let committer = gitbutler_repo::signature(SignaturePurpose::Committer)?;
96106
let author = gitbutler_repo::signature(SignaturePurpose::Author)?;
97-
let gix_repo = repo.to_gix()?;
98107
let mut heads: Vec<git2::Commit<'_>> = stacks
99108
.iter()
100109
.filter_map(|stack| stack.head_oid(&gix_repo).ok())

crates/but-workspace/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ pub use tree_manipulation::{
4949
split_commit::{CommitFiles, CommmitSplitOutcome, split_commit},
5050
};
5151
pub mod head;
52-
pub use head::{head, merge_worktree_with_workspace};
52+
pub use head::{
53+
merge_worktree_with_workspace, remerged_workspace_commit_v2, remerged_workspace_tree_v2,
54+
};
5355

5456
/// 🚧utilities for applying and unapplying branches 🚧.
5557
/// Ignore the name of this module; it's just a place to put code by now.

0 commit comments

Comments
 (0)