Skip to content

Commit 7ac5ec5

Browse files
authored
Merge pull request #9682 from Byron/next
V3 of remove_branch
2 parents 0b20139 + e8b6a1c commit 7ac5ec5

File tree

26 files changed

+2412
-1564
lines changed

26 files changed

+2412
-1564
lines changed

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

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1+
use crate::{App, error::Error};
12
use anyhow::{Context, anyhow};
23
use but_workspace::branch::{ReferenceAnchor, ReferencePosition};
34
use gitbutler_branch_actions::internal::PushResult;
45
use gitbutler_branch_actions::stack::CreateSeriesRequest;
56
use gitbutler_command_context::CommandContext;
7+
use gitbutler_oplog::SnapshotExt;
68
use gitbutler_project::ProjectId;
79
use gitbutler_stack::StackId;
810
use gitbutler_user::User;
911
use gix::refs::Category;
1012
use serde::Deserialize;
11-
12-
use crate::{App, error::Error};
13+
use std::borrow::Cow;
1314

1415
#[derive(Deserialize, Debug)]
1516
#[serde(rename_all = "camelCase")]
@@ -21,9 +22,8 @@ pub struct CreateBranchParams {
2122

2223
pub fn create_branch(app: &App, params: CreateBranchParams) -> Result<(), Error> {
2324
let project = gitbutler_project::get(params.project_id)?;
24-
let ws3_enabled = app.app_settings.get()?.feature_flags.ws3;
2525
let ctx = CommandContext::open(&project, app.app_settings.get()?.clone())?;
26-
if ws3_enabled {
26+
if app.app_settings.get()?.feature_flags.ws3 {
2727
use ReferencePosition::Above;
2828
let (repo, mut meta, graph) = ctx.graph_and_meta_and_repo()?;
2929
let ws = graph.to_workspace()?;
@@ -38,16 +38,18 @@ pub fn create_branch(app: &App, params: CreateBranchParams) -> Result<(), Error>
3838
.into());
3939
}
4040

41-
let _guard = project.exclusive_worktree_access();
42-
but_workspace::branch::create_reference(
41+
let mut guard = project.exclusive_worktree_access();
42+
ctx.snapshot_create_dependent_branch(&params.request.name, guard.write_permission())
43+
.ok();
44+
_ = but_workspace::branch::create_reference(
4345
new_ref.as_ref(),
4446
{
4547
let segment = stack.segments.first().context("BUG: no empty stacks")?;
4648
segment
4749
.ref_name
4850
.as_ref()
4951
.map(|rn| ReferenceAnchor::AtSegment {
50-
ref_name: rn.as_ref(),
52+
ref_name: Cow::Borrowed(rn.as_ref()),
5153
position: Above,
5254
})
5355
.or_else(|| {
@@ -85,7 +87,31 @@ pub struct RemoveBranchParams {
8587
pub fn remove_branch(app: &App, params: RemoveBranchParams) -> Result<(), Error> {
8688
let project = gitbutler_project::get(params.project_id)?;
8789
let ctx = CommandContext::open(&project, app.app_settings.get()?.clone())?;
88-
gitbutler_branch_actions::stack::remove_branch(&ctx, params.stack_id, params.branch_name)?;
90+
if app.app_settings.get()?.feature_flags.ws3 {
91+
let (repo, mut meta, graph) = ctx.graph_and_meta_and_repo()?;
92+
let ws = graph.to_workspace()?;
93+
let ref_name = Category::LocalBranch
94+
.to_full_name(params.branch_name.as_str())
95+
.map_err(anyhow::Error::from)?;
96+
let mut guard = project.exclusive_worktree_access();
97+
ctx.snapshot_remove_dependent_branch(&params.branch_name, guard.write_permission())
98+
.ok();
99+
but_workspace::branch::remove_reference(
100+
ref_name.as_ref(),
101+
&repo,
102+
&ws,
103+
&mut meta,
104+
but_workspace::branch::remove_reference::Options {
105+
avoid_anonymous_stacks: true,
106+
// The UI kind of keeps it, but we can't do that somehow
107+
// the object id is null, and stuff breaks. Fine for now.
108+
// Delete is delete.
109+
keep_metadata: false,
110+
},
111+
)?;
112+
} else {
113+
gitbutler_branch_actions::stack::remove_branch(&ctx, params.stack_id, params.branch_name)?;
114+
}
89115
Ok(())
90116
}
91117

crates/but-graph/src/init/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,18 @@ impl Graph {
646646
let ref_name = self[tip_sidx].ref_name.clone();
647647
Graph::from_commit_traversal_inner(tip, &repo, ref_name, &meta, self.options.clone())
648648
}
649+
650+
/// Like [`Self::redo_traversal_with_overlay()`], but replaces this instance, without overlay, and returns
651+
/// a newly computed workspace for it.
652+
pub fn workspace_of_redone_traversal(
653+
&mut self,
654+
repo: &gix::Repository,
655+
meta: &impl RefMetadata,
656+
) -> anyhow::Result<crate::projection::Workspace<'_>> {
657+
let new = self.redo_traversal_with_overlay(repo, meta, Default::default())?;
658+
*self = new;
659+
self.to_workspace()
660+
}
649661
}
650662

651663
impl Graph {

crates/but-graph/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ pub type CommitIndex = usize;
225225

226226
/// A graph of connected segments that represent a section of the actual commit-graph.
227227
#[derive(Default, Debug, Clone)]
228+
#[must_use]
228229
pub struct Graph {
229230
inner: init::PetGraph,
230231
/// From where the graph was created. This is useful if one wants to focus on a subset of the graph.

crates/but-graph/src/ref_metadata_legacy.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::virtual_branches_legacy_types::{Stack, StackBranch, VirtualBranches};
1+
use crate::virtual_branches_legacy_types::{CommitOrChangeId, Stack, StackBranch, VirtualBranches};
22
use anyhow::{Context, bail};
33
use but_core::RefMetadata;
44
use but_core::ref_metadata::{
@@ -48,6 +48,17 @@ impl Snapshot {
4848
/// Instead of trying to maintain this, let's just fix it before writing.
4949
fn to_consistent_data(&self) -> VirtualBranches {
5050
let mut data = self.content.clone();
51+
// EVIL HACK: assure we fill-in the CommitIDs of heads or else everything breaks.
52+
// this probably won't be needed once no old code is running, and by then
53+
// we should move away from this anyway and have a DB backed implementation.
54+
let repo = gix::discover(self.path.parent().expect("at least a file"))
55+
.ok()
56+
.map(|repo| {
57+
(
58+
repo,
59+
CommitOrChangeId::CommitId(gix::hash::Kind::Sha1.null().to_string()),
60+
)
61+
});
5162
for stack in data.branches.values_mut() {
5263
if stack.name.is_empty() {
5364
stack.name = stack
@@ -58,6 +69,18 @@ impl Snapshot {
5869
.unwrap_or_default()
5970
.to_string();
6071
}
72+
if let Some((repo, null_id)) = repo.as_ref() {
73+
for segment in &mut stack.heads {
74+
if &segment.head == null_id {
75+
let Ok(mut r) = repo.find_reference(&segment.name) else {
76+
continue;
77+
};
78+
if let Ok(id) = r.peel_to_id_in_place() {
79+
segment.head = CommitOrChangeId::CommitId(id.to_string());
80+
}
81+
}
82+
}
83+
}
6184
}
6285
data
6386
}

crates/but-settings/assets/defaults.jsonc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
"v3": false,
2323
/// Enable the usage of V3 workspace APIs.
2424
"ws3": false,
25+
/// Enable undo/redo support.
26+
"undo": false,
2527
/// Enable the usage of GitButler Acitions.
2628
"actions": false,
2729
/// Enable the usage of the butbot chat.

crates/but-settings/src/app_settings.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ pub struct FeatureFlags {
3232
/// Enable the usage of V3 workspace APIs.
3333
#[serde(default = "default_true")]
3434
pub ws3: bool,
35+
/// Enable undo/redo support.
36+
///
37+
/// ### Progression for implementation
38+
///
39+
/// * use snapshot system in undo/redo queue
40+
/// - consider not referring to these objects by reference to `git gc` will catch them,
41+
/// or even purge them on shutdown. Alternatively, keep them in-memory with in-memory objects.
42+
/// * add user-control to snapshot system to purge now, or purge after time X. That way data isn't stored forever.
43+
/// * Finally, consider implementing undo/redo with invasive primitives that are undoable/redoable themselves for
44+
/// the most efficient solution, inherently in memory, i.e.
45+
/// - CRUD reference
46+
/// - CRUD metadata
47+
/// - CRUD workspace
48+
/// - CRUD files
49+
pub undo: bool,
3550
/// Enable the usage of GitButler Acitions.
3651
pub actions: bool,
3752
/// Enable the usage of the butbot chat.

crates/but-testing/src/args.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,30 @@ pub enum Subcommands {
205205
#[clap(long, short = 'd')]
206206
description: Option<String>,
207207
},
208+
/// Create a reference at the given position (dependent and independent)
209+
CreateReference {
210+
/// Create the branch above the given commit or branch short name.
211+
#[arg(long, short = 'a', conflicts_with = "below")]
212+
above: Option<String>,
213+
/// Create the branch below the given commit or branch short name.
214+
#[arg(long, short = 'b', conflicts_with = "above")]
215+
below: Option<String>,
216+
/// the short-name of the new branch.
217+
short_name: String,
218+
},
219+
/// Delete the given workspace reference.
220+
RemoveReference {
221+
/// Allow stacks to be empty.
222+
#[arg(long)]
223+
permit_empty_stacks: bool,
224+
/// Do not delete the branch metadata associated with the deleted reference.
225+
///
226+
/// Useful if it might be recreated, afterwards.
227+
#[arg(long)]
228+
keep_metadata: bool,
229+
/// the short-name of the reference to delete.
230+
short_name: String,
231+
},
208232
}
209233

210234
#[cfg(test)]

crates/but-testing/src/command/mod.rs

Lines changed: 100 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ use but_core::UnifiedDiff;
33
use but_db::poll::ItemKind;
44
use but_graph::VirtualBranchesTomlMetadata;
55
use but_settings::AppSettings;
6+
use but_workspace::branch::{ReferenceAnchor, ReferencePosition};
67
use but_workspace::{DiffSpec, HunkHeader};
78
use gitbutler_project::{Project, ProjectId};
89
use gix::bstr::{BString, ByteSlice};
910
use gix::odb::store::RefreshMode;
11+
use gix::refs::Category;
12+
use std::borrow::Cow;
1013
use std::io::{Write, stdout};
1114
use std::mem::ManuallyDrop;
1215
use std::path::Path;
@@ -72,6 +75,21 @@ pub fn repo_and_maybe_project(
7275
Ok(res)
7376
}
7477

78+
pub fn repo_and_maybe_project_and_graph(
79+
args: &super::Args,
80+
mode: RepositoryOpenMode,
81+
) -> anyhow::Result<(
82+
gix::Repository,
83+
Option<Project>,
84+
but_graph::Graph,
85+
ManuallyDrop<VirtualBranchesTomlMetadata>,
86+
)> {
87+
let (repo, project) = repo_and_maybe_project(args, mode)?;
88+
let meta = meta_from_maybe_project(project.as_ref())?;
89+
let graph = but_graph::Graph::from_head(&repo, &*meta, meta.graph_options())?;
90+
Ok((repo, project, graph, meta))
91+
}
92+
7593
fn debug_print(this: impl std::fmt::Debug) -> anyhow::Result<()> {
7694
println!("{:#?}", this);
7795
Ok(())
@@ -318,14 +336,15 @@ pub mod stacks {
318336
description: Option<&str>,
319337
current_dir: &Path,
320338
use_json: bool,
339+
ws3: bool,
321340
) -> anyhow::Result<()> {
322341
let project = project_from_path(current_dir)?;
323342
// Enable v3 feature flags for the command context
324343
let app_settings = AppSettings {
325344
feature_flags: but_settings::app_settings::FeatureFlags {
326345
v3: true,
327-
// Keep this off until it caught up at least.
328-
ws3: false,
346+
ws3,
347+
undo: false,
329348
actions: false,
330349
butbot: false,
331350
rules: false,
@@ -493,26 +512,14 @@ pub fn graph(
493512
.collect(),
494513
};
495514

496-
let meta_with_drop;
497-
let meta_without_drop;
498-
let meta = match project {
499-
None => {
500-
meta_without_drop = ManuallyDrop::new(VirtualBranchesTomlMetadata::from_path(
501-
"should-never-be-written-back.toml",
502-
)?);
503-
&meta_without_drop
504-
}
505-
Some(project) => {
506-
meta_with_drop = ref_metadata_toml(&project)?;
507-
&meta_with_drop
508-
}
509-
};
515+
// Never drop - this is read-only.
516+
let meta = meta_from_maybe_project(project.as_ref())?;
510517
let graph = match ref_name {
511-
None => but_graph::Graph::from_head(&repo, meta, opts),
518+
None => but_graph::Graph::from_head(&repo, &*meta, opts),
512519
Some(ref_name) => {
513520
let mut reference = repo.find_reference(ref_name)?;
514521
let id = reference.peel_to_id_in_place()?;
515-
but_graph::Graph::from_commit_traversal(id, reference.name().to_owned(), meta, opts)
522+
but_graph::Graph::from_commit_traversal(id, reference.name().to_owned(), &*meta, opts)
516523
}
517524
}?;
518525

@@ -556,6 +563,81 @@ pub fn graph(
556563
Ok(())
557564
}
558565

566+
/// NOTE: THis is a special case while vb.toml is used and while projects are somewhat special.
567+
fn meta_from_maybe_project(
568+
project: Option<&Project>,
569+
) -> anyhow::Result<ManuallyDrop<VirtualBranchesTomlMetadata>> {
570+
let meta = ManuallyDrop::new(match project {
571+
None => VirtualBranchesTomlMetadata::from_path("should-never-be-written-back.toml")?,
572+
Some(project) => ref_metadata_toml(project)?,
573+
});
574+
Ok(meta)
575+
}
576+
577+
pub fn remove_reference(
578+
args: &super::Args,
579+
short_name: &str,
580+
opts: but_workspace::branch::remove_reference::Options,
581+
) -> anyhow::Result<()> {
582+
let (repo, _project, graph, mut meta) =
583+
repo_and_maybe_project_and_graph(args, RepositoryOpenMode::General)?;
584+
585+
let ref_name = Category::LocalBranch.to_full_name(short_name)?;
586+
let deleted = but_workspace::branch::remove_reference(
587+
ref_name.as_ref(),
588+
&repo,
589+
&graph.to_workspace()?,
590+
&mut *meta,
591+
opts,
592+
)?
593+
.is_some();
594+
if deleted {
595+
eprintln!("Deleted");
596+
} else {
597+
eprintln!("Nothing deleted");
598+
}
599+
// write metadata if there are projects - this is a special case while we use vb.toml.
600+
ManuallyDrop::into_inner(meta);
601+
Ok(())
602+
}
603+
604+
pub fn create_reference(
605+
args: &super::Args,
606+
short_name: &str,
607+
above: Option<&str>,
608+
below: Option<&str>,
609+
) -> anyhow::Result<()> {
610+
let (repo, project, graph, mut meta) =
611+
repo_and_maybe_project_and_graph(args, RepositoryOpenMode::General)?;
612+
let resolve =
613+
|spec: &str, position: ReferencePosition| -> anyhow::Result<ReferenceAnchor<'_>> {
614+
Ok(match repo.try_find_reference(spec)? {
615+
None => ReferenceAnchor::AtCommit {
616+
commit_id: repo.rev_parse_single(spec)?.detach(),
617+
position,
618+
},
619+
Some(rn) => ReferenceAnchor::AtSegment {
620+
ref_name: Cow::Owned(rn.inner.name),
621+
position,
622+
},
623+
})
624+
};
625+
let anchor = above
626+
.map(|spec| resolve(spec, ReferencePosition::Above))
627+
.or_else(|| below.map(|spec| resolve(spec, ReferencePosition::Below)))
628+
.transpose()?;
629+
630+
let new_ref = Category::LocalBranch.to_full_name(short_name)?;
631+
let ws = graph.to_workspace()?;
632+
_ = but_workspace::branch::create_reference(new_ref.as_ref(), anchor, &repo, &ws, &mut *meta)?;
633+
634+
if project.is_some() {
635+
// write metadata if there are projects - this is a special case while we use vb.toml.
636+
ManuallyDrop::into_inner(meta);
637+
}
638+
Ok(())
639+
}
640+
559641
fn indices_or_headers_to_hunk_headers(
560642
repo: &gix::Repository,
561643
indices_or_headers: Option<IndicesOrHeaders<'_>>,

0 commit comments

Comments
 (0)