Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dist-ssr

.turbo
build
!crates/*/src/build/
.svelte-kit
package
!.env.example
Expand Down
8 changes: 3 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,6 @@ bitflags = "2.11.1"
notify = "8.2.0"
snapbox = { version = "0.6.23", features = ["json"] }
url = "2.5.7"
petgraph = { version = "0.8.3", default-features = false, features = [
"stable_graph",
"std",
] }

schemars = { version = "1.2.0", default-features = false, features = [
"std",
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export default defineConfig({
fs: {
strict: false,
},
watch: process.env.VITE_DISABLE_WATCH ? null : undefined,
hmr: process.env.VITE_DISABLE_WATCH ? false : undefined,
},
optimizeDeps: {
// Exclude local packages from pre-bundling
Expand Down
6 changes: 4 additions & 2 deletions crates/WORKSPACE_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,13 @@ Use it for state/query questions such as:
- which commits belong under a ref or stack-like UI grouping?
- what branch/ref relationships exist before deciding what to display or mutate?

Construction: production code asks for the projection directly — `but_graph::Workspace::from_head()`, `Workspace::from_tip()`, `workspace.redo()`, or `SuccessfulRebase::overlayed_workspace()` after an editor rebase. The graph rides along as `Workspace::graph`; only tests and debug tooling build a bare `Graph`.

Caveats:

- The graph is segment/bucket based because older UI concerns influenced it.
- It can encode ordering information Git itself does not represent, especially around refs.
- Merge parent order may not always be reliable; be careful with first-parent traversal or UI that assumes the first parent is the mainline.
- Within `but_graph`, parent order is preserved and authoritative: parent arrays are ordered, and a segment's outgoing connections follow them. Whether the *first* parent is the user's "mainline" is still a workflow assumption — question it at the product level, not the graph level.

## Workspace projection and refinfo

Expand Down Expand Up @@ -194,7 +196,7 @@ Ask this for both read/query code and mutation code:

## Examples / starting points

- Graph construction and workspace projection: `crates/but-graph/tests/graph/init/with_workspace.rs`, especially `workspace_with_stack_and_local_target()` and `workspace_projection_with_advanced_stack_tip()`, shows `Graph::from_head()`, `validated()`, `into_workspace()`, and snapshot-backed graph/projection expectations.
- Graph construction and workspace projection: `crates/but-graph/tests/graph/walk/with_workspace.rs`, especially `workspace_with_stack_and_local_target()` and `workspace_projection_with_advanced_stack_tip()`, shows `Graph::from_head()`, `validated()`, `into_workspace()`, and snapshot-backed graph/projection expectations.
- Target ref and target commit semantics: `crates/but-graph/tests/graph/workspace/resolved_target_commit_id.rs`, especially `prefers_target_commit_over_target_ref()` and `returns_none_with_only_extra_target()`, shows cases where target commit metadata, target refs, and extra traversal targets intentionally differ.
- Graph editor mutation patterns: `crates/but-rebase/tests/rebase/graph_rebase/replace.rs` and `crates/but-rebase/tests/rebase/graph_rebase/insert.rs` show selecting commits, replacing/inserting steps, checking `overlayed_graph()`, and materializing once.
- Workspace mutation call sites layered over the graph editor: `crates/but-workspace/tests/workspace/commit/move_commit.rs` shows creating an editor, calling `but_workspace::commit::move_commit`, materializing, refreshing workspace state, and asserting ref movement.
Expand Down
2 changes: 1 addition & 1 deletion crates/but-action/src/reword.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub fn commit(
bail!("commit message cannot be empty");
}

let editor = Editor::create(ws, meta, repo)?;
let editor = Editor::create(ws.commit_graph(), ws.project_meta(), meta, repo)?;
let (rebase, edited_commit_selector) =
but_workspace::commit::reword(editor, input.commit_id, message.as_bytes().as_bstr())?;
let new_commit_id = rebase.lookup_pick(edited_commit_selector)?;
Expand Down
5 changes: 2 additions & 3 deletions crates/but-action/src/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ use anyhow::{Context as _, anyhow};
use but_core::{DiffSpec, RefMetadata, ref_metadata::StackId, sync::RepoExclusive};
use but_db::DbHandle;
use but_rebase::graph_rebase::{
Editor, LookupStep as _,
mutate::{InsertSide, RelativeToRef},
Editor, LookupStep as _, mutate::InsertSide, selector::RelativeToRef,
};

use crate::Outcome;
Expand Down Expand Up @@ -114,7 +113,7 @@ pub(crate) fn handle_changes(
let full_ref_name: gix::refs::FullName =
format!("refs/heads/{stack_branch_name}").try_into()?;

let editor = Editor::create(ws, meta, repo)?;
let editor = Editor::create(ws.commit_graph(), ws.project_meta(), meta, repo)?;
let outcome = but_workspace::commit::commit_create(
editor,
diff_specs,
Expand Down
53 changes: 21 additions & 32 deletions crates/but-api/src/branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -933,11 +933,7 @@ pub fn branch_remove_with_perm(
};
let deleted_meta = meta.remove(ref_name.as_ref())?;
if deleted_ref || deleted_meta {
let new_ws = ws
.graph
.redo_traversal_with_overlay(&repo, &meta, Default::default())?
.into_workspace()?;
*ws = new_ws;
*ws = ws.redo(&repo, &meta, Default::default())?;
true
} else {
false
Expand Down Expand Up @@ -1454,12 +1450,11 @@ pub fn get_initial_branch_integration(
) -> anyhow::Result<InitialBranchIntegration> {
let mut meta = ctx.meta()?;
let (_guard, repo, ws, _) = ctx.workspace_and_db()?;
let mut ws = ws.clone();
let strategy = strategy
.map(BranchIntegrationStrategy::from)
.unwrap_or_default();
but_workspace::branch::integrate_branch_upstream::get_initial_integration_steps_for_branch(
branch, strategy, &mut ws, &mut meta, &repo,
branch, strategy, &ws, &mut meta, &repo,
)
}

Expand Down Expand Up @@ -1507,14 +1502,14 @@ pub fn apply_branch_integration_with_perm(
let rebase = but_workspace::branch::integrate_branch_with_steps(
branch,
integration,
&mut ws,
&ws,
&mut meta,
&repo,
)?;

Ok(IntegrateBranchResult {
workspace: WorkspaceState::from_successful_rebase_with_db(
rebase, &repo, dry_run, &db,
&mut ws, rebase, &repo, dry_run, &db,
)?,
})
},
Expand Down Expand Up @@ -1570,16 +1565,17 @@ pub fn move_branch_with_perm(
|ctx, perm| {
let mut meta = ctx.meta()?;
let (repo, mut ws, db) = ctx.workspace_mut_and_db_with_perm(perm)?;
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
let editor = Editor::create(ws.commit_graph(), ws.project_meta(), &mut meta, &repo)?;
let but_workspace::branch::move_branch::Outcome {
rebase,
ws_meta,
new_tip,
branch_stack_order,
} = but_workspace::branch::move_branch(editor, subject_branch, target_branch)?;
} = but_workspace::branch::move_branch(editor, &ws, subject_branch, target_branch)?;

let result = MoveBranchResult {
workspace: branch_workspace_from_rebase(
&mut ws,
rebase,
ws_meta,
new_tip.as_ref(),
Expand Down Expand Up @@ -1650,14 +1646,14 @@ pub fn tear_off_branch_with_perm(
|ctx, perm| {
let mut meta = ctx.meta()?;
let (repo, mut ws, db) = ctx.workspace_mut_and_db_with_perm(perm)?;
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
let editor = Editor::create(ws.commit_graph(), ws.project_meta(), &mut meta, &repo)?;
let but_workspace::branch::move_branch::Outcome {
rebase, ws_meta, ..
} = but_workspace::branch::tear_off_branch(editor, subject_branch, None)?;
} = but_workspace::branch::tear_off_branch(editor, &ws, subject_branch, None)?;

Ok(MoveBranchResult {
workspace: branch_workspace_from_rebase(
rebase, ws_meta, None, None, &repo, dry_run, &db,
&mut ws, rebase, ws_meta, None, None, &repo, dry_run, &db,
)?,
})
},
Expand Down Expand Up @@ -1691,8 +1687,10 @@ where
result
}

#[allow(clippy::too_many_arguments)]
fn branch_workspace_from_rebase<M: but_core::RefMetadata>(
mut rebase: SuccessfulRebase<'_, '_, M>,
workspace: &mut but_graph::Workspace,
mut rebase: SuccessfulRebase<'_, M>,
ws_meta: Option<but_core::ref_metadata::Workspace>,
new_tip: Option<&gix::refs::FullName>,
branch_stack_order: Option<&[gix::refs::FullName]>,
Expand All @@ -1707,36 +1705,27 @@ fn branch_workspace_from_rebase<M: but_core::RefMetadata>(
})
.transpose()?;
let replaced_commits = rebase.history.commit_mappings();
let workspace = rebase
.overlayed_graph_with_workspace_overrides(entrypoint, branch_stack_order)?
.into_workspace()?;
let overlay =
rebase.rebase_overlay_with_workspace_overrides(entrypoint, branch_stack_order)?;
let (repo, meta) = rebase.repo_and_meta_mut();
return WorkspaceState::from_workspace_with_db(
&workspace,
meta,
repo,
replaced_commits,
db,
);
let preview = workspace.redo(repo, meta, overlay)?;
return WorkspaceState::from_workspace_with_db(&preview, meta, repo, replaced_commits, db);
}

let materialized = rebase.materialize()?;
if let Some(order) = branch_stack_order {
materialized.meta.set_branch_stack_order(order)?;
let project_meta = materialized.workspace.graph.project_meta.clone();
materialized
.workspace
.refresh_from_head(repo, &*materialized.meta, project_meta)?;
}
if let Some((ws_meta, ref_name)) = ws_meta.zip(materialized.workspace.ref_name()) {
workspace.refresh_from_commit_graph(materialized.arena().clone(), repo, materialized.meta)?;
if let Some((ws_meta, ref_name)) = ws_meta.zip(workspace.ref_name()) {
let mut md = materialized.meta.workspace(ref_name)?;
*md = ws_meta;
md.set_project_meta(materialized.workspace.graph.project_meta.clone());
md.set_project_meta(workspace.project_meta().clone());
materialized.meta.set_workspace(&md)?;
}

WorkspaceState::from_workspace_with_db(
materialized.workspace,
workspace,
materialized.meta,
repo,
materialized.history.commit_mappings(),
Expand Down
5 changes: 3 additions & 2 deletions crates/but-api/src/commit/amend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub(crate) fn commit_amend_only_impl(
) -> anyhow::Result<CommitCreateResult> {
let mut meta = ctx.meta()?;
let (repo, mut ws, db) = ctx.workspace_mut_and_db_with_perm(perm)?;
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
let editor = Editor::create(ws.commit_graph(), ws.project_meta(), &mut meta, &repo)?;

let but_workspace::commit::CommitAmendOutcome {
rebase,
Expand All @@ -53,7 +53,8 @@ pub(crate) fn commit_amend_only_impl(
let new_commit = commit_selector
.map(|commit_selector| rebase.lookup_pick(commit_selector))
.transpose()?;
let workspace = WorkspaceState::from_successful_rebase_with_db(rebase, &repo, dry_run, &db)?;
let workspace =
WorkspaceState::from_successful_rebase_with_db(&mut ws, rebase, &repo, dry_run, &db)?;

Ok(CommitCreateResult {
new_commit,
Expand Down
10 changes: 4 additions & 6 deletions crates/but-api/src/commit/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ use crate::WorkspaceState;
use but_api_macros::but_api;
use but_core::{DiffSpec, DryRun, sync::RepoExclusive};
use but_oplog::legacy::{OperationKind, SnapshotDetails};
use but_rebase::graph_rebase::{
Editor, LookupStep as _,
mutate::{InsertSide, RelativeTo},
};
use but_rebase::graph_rebase::{Editor, LookupStep as _, mutate::InsertSide, selector::RelativeTo};
use tracing::instrument;

use super::types::CommitCreateResult;
Expand Down Expand Up @@ -59,7 +56,7 @@ pub(crate) fn commit_create_only_impl(
) -> anyhow::Result<CommitCreateResult> {
let mut meta = ctx.meta()?;
let (repo, mut ws, db) = ctx.workspace_mut_and_db_with_perm(perm)?;
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
let editor = Editor::create(ws.commit_graph(), ws.project_meta(), &mut meta, &repo)?;

let but_workspace::commit::CommitCreateOutcome {
rebase,
Expand All @@ -77,7 +74,8 @@ pub(crate) fn commit_create_only_impl(
let new_commit = commit_selector
.map(|commit_selector| rebase.lookup_pick(commit_selector))
.transpose()?;
let workspace = WorkspaceState::from_successful_rebase_with_db(rebase, &repo, dry_run, &db)?;
let workspace =
WorkspaceState::from_successful_rebase_with_db(&mut ws, rebase, &repo, dry_run, &db)?;

Ok(CommitCreateResult {
new_commit,
Expand Down
16 changes: 11 additions & 5 deletions crates/but-api/src/commit/discard_commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ pub fn commit_discard_only_with_perm(
) -> anyhow::Result<CommitDiscardResult> {
let mut meta = ctx.meta()?;
let (repo, mut ws, db) = ctx.workspace_mut_and_db_with_perm(perm)?;
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
let editor = Editor::create(ws.commit_graph(), ws.project_meta(), &mut meta, &repo)?;

let rebase = but_workspace::commit::discard_commits(editor, [subject_commit_id])?;

let workspace = WorkspaceState::from_successful_rebase_with_db(rebase, &repo, dry_run, &db)?;
let workspace =
WorkspaceState::from_successful_rebase_with_db(&mut ws, rebase, &repo, dry_run, &db)?;

Ok(CommitDiscardResult {
discarded_commit: subject_commit_id,
Expand Down Expand Up @@ -142,12 +143,17 @@ pub fn commit_discard_changes_only_with_perm(
let context_lines = ctx.settings.context_lines;
let mut meta = ctx.meta()?;
let (repo, mut ws, db) = ctx.workspace_mut_and_db_with_perm(perm)?;
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
let editor = Editor::create(ws.commit_graph(), ws.project_meta(), &mut meta, &repo)?;

let outcome =
but_workspace::commit::uncommit_changes(editor, commit_id, changes, context_lines)?;
let workspace =
WorkspaceState::from_successful_rebase_with_db(outcome.rebase, &repo, dry_run, &db)?;
let workspace = WorkspaceState::from_successful_rebase_with_db(
&mut ws,
outcome.rebase,
&repo,
dry_run,
&db,
)?;

Ok(MoveChangesResult { workspace })
}
Expand Down
10 changes: 4 additions & 6 deletions crates/but-api/src/commit/insert_blank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ use crate::WorkspaceState;
use but_api_macros::but_api;
use but_core::{DryRun, sync::RepoExclusive};
use but_oplog::legacy::{OperationKind, SnapshotDetails};
use but_rebase::graph_rebase::{
Editor, LookupStep as _,
mutate::{InsertSide, RelativeTo},
};
use but_rebase::graph_rebase::{Editor, LookupStep as _, mutate::InsertSide, selector::RelativeTo};
use tracing::instrument;

use super::types::CommitInsertBlankResult;
Expand Down Expand Up @@ -41,12 +38,13 @@ pub(crate) fn commit_insert_blank_only_impl(
) -> anyhow::Result<CommitInsertBlankResult> {
let mut meta = ctx.meta()?;
let (repo, mut ws, db) = ctx.workspace_mut_and_db_with_perm(perm)?;
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
let editor = Editor::create(ws.commit_graph(), ws.project_meta(), &mut meta, &repo)?;

let (rebase, blank_commit_selector) =
but_workspace::commit::insert_blank_commit(editor, side, relative_to)?;
let new_commit = rebase.lookup_pick(blank_commit_selector)?;
let workspace = WorkspaceState::from_successful_rebase_with_db(rebase, &repo, dry_run, &db)?;
let workspace =
WorkspaceState::from_successful_rebase_with_db(&mut ws, rebase, &repo, dry_run, &db)?;

Ok(CommitInsertBlankResult {
new_commit,
Expand Down
2 changes: 1 addition & 1 deletion crates/but-api/src/commit/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ pub enum RelativeTo {
#[cfg(feature = "export-schema")]
but_schemars::register_sdk_type!(RelativeTo);

impl From<RelativeTo> for but_rebase::graph_rebase::mutate::RelativeTo {
impl From<RelativeTo> for but_rebase::graph_rebase::selector::RelativeTo {
fn from(value: RelativeTo) -> Self {
match value {
RelativeTo::Commit(commit) => Self::Commit(commit),
Expand Down
Loading
Loading