Skip to content

Commit d284092

Browse files
committed
Basic apply and unapply
The idea is to develop both at the same time so it's easier to test that 'unapply' truly goes back to the (mostly) original state.
1 parent 22e3330 commit d284092

File tree

10 files changed

+298
-9
lines changed

10 files changed

+298
-9
lines changed

crates/but-graph/src/projection/workspace.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ pub enum WorkspaceKind {
189189
/// The name of the reference pointing to the workspace commit. Useful for deriving the workspace name.
190190
ref_name: gix::refs::FullName,
191191
},
192-
/// Information for when a workspace reference was advanced by hand and does not point to a
193-
/// managed workspace commit anymore.
194-
/// That commit, however, is reachable by following the first parent from the workspace reference.
192+
/// Information for when a workspace reference was *possibly* advanced by hand and does not point to a
193+
/// managed workspace commit (anymore).
194+
/// That workspace commit, may be reachable by following the first parent from the workspace reference.
195195
///
196196
/// Note that the stacks that follow *will* be in unusable if the workspace commit is in a segment below,
197197
/// but typically is usable if there is just a single real stack, or any amount of virtual stacks below
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/// Returned by [function::apply()].
2+
pub struct Outcome {
3+
/// The newly created graph, useful to project a workspace and see how the workspace looks like with the branch applied.
4+
pub graph: but_graph::Graph,
5+
/// `true` if we created the given workspace ref as it didn't exist yet.
6+
pub workspace_ref_created: bool,
7+
}
8+
9+
impl std::fmt::Debug for Outcome {
10+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11+
f.debug_struct("Outcome")
12+
.field("workspace_ref_created", &self.workspace_ref_created)
13+
.finish()
14+
}
15+
}
16+
17+
/// How the newly applied branch should be integrated into the workspace.
18+
#[derive(Default, Debug, Copy, Clone)]
19+
pub enum IntegrationMode {
20+
/// Do nothing but to merge it into the workspace commit.
21+
#[default]
22+
Merge,
23+
}
24+
25+
/// What to do if the applied branch conflicts?
26+
#[derive(Default, Debug, Copy, Clone)]
27+
pub enum OnWorkspaceConflict {
28+
/// Provide additional information about the stack that conflicted and the files involved in it,
29+
/// and don't perform the operation.
30+
#[default]
31+
AbortAndReportConflictingStack,
32+
}
33+
34+
/// Decide how a newly created workspace reference should be named.
35+
#[derive(Default, Debug, Clone)]
36+
pub enum WorkspaceReferenceNaming {
37+
/// Create a default workspace branch
38+
#[default]
39+
Default,
40+
/// Create a workspace with the given name instead.
41+
Given(gix::refs::FullName),
42+
}
43+
44+
/// Options for [function::apply()].
45+
#[derive(Default, Debug, Clone)]
46+
pub struct Options {
47+
/// how the branch should be brought into the workspace.
48+
pub integration_mode: IntegrationMode,
49+
/// Decide how to deal with conflicts.
50+
pub on_workspace_conflict: OnWorkspaceConflict,
51+
/// How the workspace reference should be named should it be created.
52+
/// The creation is always needed if there are more than one branch applied.
53+
pub workspace_reference_naming: WorkspaceReferenceNaming,
54+
}
55+
56+
pub(crate) mod function {
57+
use super::{Options, Outcome, WorkspaceReferenceNaming};
58+
use anyhow::bail;
59+
use but_core::RefMetadata;
60+
use but_graph::projection::WorkspaceKind;
61+
62+
/// Apply `branch` to the given `workspace`, and possibly create the workspace reference in `repo`
63+
/// along with its `meta`-data if it doesn't exist yet.
64+
/// Otherwise, add it to the existing `workspace`, and update its metadata accordingly.
65+
/// Note that `workspace` is expected to match the state in `repo` as it's used instead of querying `repo` directly
66+
/// where possible.
67+
/// Note that we will create a managed workspace reference as needed if necessary, and a workspace commit if there is more than
68+
/// one reference in the workspace afterward.
69+
///
70+
/// On `error`, neither `repo` nor `meta` will have been changed.
71+
/// Otherwise, objects will have been persisted, and references and metadata will have been updated.
72+
pub fn apply<T: RefMetadata>(
73+
branch: &gix::refs::FullNameRef,
74+
workspace: &but_graph::projection::Workspace,
75+
repo: &mut gix::Repository,
76+
meta: &mut T,
77+
Options {
78+
integration_mode,
79+
on_workspace_conflict,
80+
workspace_reference_naming,
81+
}: Options,
82+
) -> anyhow::Result<Outcome> {
83+
if workspace
84+
.find_segment_and_stack_by_refname(branch)
85+
.is_some()
86+
{
87+
return Ok(Outcome {
88+
graph: workspace.graph.clone(),
89+
workspace_ref_created: false,
90+
});
91+
}
92+
93+
if let Some(ws_ref_name) = workspace.ref_name() {
94+
if repo.try_find_reference(ws_ref_name)?.is_none() {
95+
bail!(
96+
"Cannot create reference on unborn branch '{}'",
97+
ws_ref_name.shorten()
98+
);
99+
}
100+
}
101+
let workspace_ref_name = match &workspace.kind {
102+
WorkspaceKind::Managed { ref_name }
103+
| WorkspaceKind::ManagedMissingWorkspaceCommit { ref_name } => Ok(ref_name.clone()),
104+
WorkspaceKind::AdHoc => Err(match workspace_reference_naming {
105+
WorkspaceReferenceNaming::Default => {
106+
gix::refs::FullName::try_from("refs/heads/gitbutler/workspace")
107+
.expect("known statically")
108+
}
109+
WorkspaceReferenceNaming::Given(name) => name,
110+
}),
111+
};
112+
todo!()
113+
}
114+
}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,14 @@ pub(super) mod function {
218218
ref_name.shorten()
219219
);
220220
};
221-
position.resolve_commit(segment.commits.first().context(
222-
"BUG: empty segments aren't possible without workspace metadata",
223-
)?.into(), ws_base)?
221+
position.resolve_commit(
222+
segment
223+
.commits
224+
.first()
225+
.context("Cannot create reference on unborn branch")?
226+
.into(),
227+
ws_base,
228+
)?
224229
};
225230
(
226231
validate_id,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,10 @@ impl Stack {
456456
}
457457
}
458458

459+
/// Functions and types related to applying a workspace branch.
460+
pub mod apply;
461+
pub use apply::function::apply;
462+
459463
/// related types for removing a workspace reference.
460464
pub mod remove_reference;
461465
pub use remove_reference::function::remove_reference;

crates/but-workspace/tests/fixtures/generated-archives/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@
2929
/with-conflict.tar
3030
/journey*.tar
3131
/single-branch*.tar
32-
/index*.tar
32+
/index*.tar
33+
/detached-*.tar
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
3+
set -eu -o pipefail
4+
source "${BASH_SOURCE[0]%/*}/shared.sh"
5+
6+
### Description
7+
# The HEAD is detached, and there are multiple branches.
8+
9+
git init
10+
commit M1
11+
git branch B
12+
git branch C
13+
git checkout -b A
14+
commit A1
15+
git checkout B
16+
commit B1
17+
git checkout C
18+
commit C1
19+
git checkout --detach HEAD
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
3+
set -eu -o pipefail
4+
5+
source "${BASH_SOURCE[0]%/*}/shared.sh"
6+
7+
### Description
8+
# A newly initialized git repository, but with a known remote that has an object.
9+
10+
git init remote
11+
(cd remote
12+
commit "M1"
13+
)
14+
15+
git init unborn
16+
(cd unborn
17+
git remote add orphan ../remote
18+
git fetch orphan
19+
)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use crate::ref_info::with_workspace_commit::utils::{
2+
named_read_only_in_memory_scenario, named_writable_scenario_with_description_and_graph,
3+
};
4+
use crate::utils::r;
5+
use but_graph::init::Options;
6+
use but_testsupport::{graph_workspace, visualize_commit_graph_all};
7+
8+
#[test]
9+
#[ignore = "TBD: idempotent"]
10+
fn unapply_branch_not_in_workspace() -> anyhow::Result<()> {
11+
Ok(())
12+
}
13+
14+
#[test]
15+
fn detached_head_journey() -> anyhow::Result<()> {
16+
let (_tmp, graph, mut repo, mut meta, _description) =
17+
named_writable_scenario_with_description_and_graph(
18+
"detached-with-multiple-branches",
19+
|_meta| {},
20+
)?;
21+
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
22+
* 49d4b34 (A) A1
23+
| * f57c528 (B) B1
24+
|/
25+
| * aaa195b (HEAD, C) C1
26+
|/
27+
* 3183e43 (main) M1
28+
");
29+
let ws = graph.to_workspace()?;
30+
insta::assert_snapshot!(graph_workspace(&ws), @r"
31+
⌂:0:DETACHED <> ✓!
32+
└── ≡:0:anon:
33+
├── :0:anon:
34+
│ └── ·aaa195b ►C
35+
└── :1:main
36+
└── ·3183e43
37+
");
38+
39+
let out = but_workspace::branch::apply(
40+
r("refs/heads/A"),
41+
&ws,
42+
&mut repo,
43+
&mut meta,
44+
Default::default(),
45+
)?;
46+
insta::assert_debug_snapshot!(out, @r"
47+
Outcome {
48+
workspace_ref_created: false,
49+
}
50+
");
51+
Ok(())
52+
}
53+
54+
#[test]
55+
fn unborn_apply_needs_base() -> anyhow::Result<()> {
56+
let (mut repo, mut meta) =
57+
named_read_only_in_memory_scenario("unborn-empty-detached-remote", "unborn")?;
58+
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @"* 3183e43 (orphan/main) M1");
59+
let graph = but_graph::Graph::from_head(&repo, &*meta, Options::limited())?;
60+
let ws = graph.to_workspace()?;
61+
insta::assert_snapshot!(graph_workspace(&ws), @r"
62+
⌂:0:main <> ✓!
63+
└── ≡:0:main
64+
└── :0:main
65+
");
66+
67+
// Idempotency
68+
let out = but_workspace::branch::apply(
69+
r("refs/heads/main"),
70+
&ws,
71+
&mut repo,
72+
&mut *meta,
73+
Default::default(),
74+
)?;
75+
insta::assert_debug_snapshot!(out, @r"
76+
Outcome {
77+
workspace_ref_created: false,
78+
}
79+
");
80+
81+
// Cannot apply branch without a base.
82+
let err = but_workspace::branch::apply(
83+
r("refs/remotes/orphan/main"),
84+
&ws,
85+
&mut repo,
86+
&mut *meta,
87+
Default::default(),
88+
)
89+
.unwrap_err();
90+
assert_eq!(
91+
err.to_string(),
92+
"Cannot create reference on unborn branch 'main'"
93+
);
94+
Ok(())
95+
}
96+
97+
#[test]
98+
#[ignore = "TBD"]
99+
fn apply_branch_resting_on_base() -> anyhow::Result<()> {
100+
// THis can't work, but should fail gracefully.
101+
Ok(())
102+
}

crates/but-workspace/tests/workspace/branch/create_reference.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use but_core::ref_metadata::ValueInfo;
77
use but_graph::init::Options;
88
use but_testsupport::{graph_workspace, id_at, id_by_rev, visualize_commit_graph_all};
99
use but_workspace::branch::create_reference::{Anchor, Position::*};
10+
use std::borrow::Cow;
1011

1112
mod with_workspace {
1213
use crate::ref_info::with_workspace_commit::utils::{
@@ -1066,6 +1067,30 @@ mod with_workspace {
10661067

10671068
#[test]
10681069
fn errors() -> anyhow::Result<()> {
1070+
let (repo, mut meta) = named_read_only_in_memory_scenario("unborn-empty", "")?;
1071+
let graph = but_graph::Graph::from_head(&repo, &*meta, Options::limited())?;
1072+
let ws = graph.to_workspace()?;
1073+
insta::assert_snapshot!(graph_workspace(&ws), @r"
1074+
⌂:0:main <> ✓!
1075+
└── ≡:0:main
1076+
└── :0:main
1077+
");
1078+
1079+
// Below first in history
1080+
let new_name = r("refs/heads/does-not-matter");
1081+
let err = but_workspace::branch::create_reference(
1082+
new_name,
1083+
Anchor::AtSegment {
1084+
ref_name: Cow::Borrowed(r("refs/heads/main")),
1085+
position: Above,
1086+
},
1087+
&repo,
1088+
&ws,
1089+
&mut *meta,
1090+
)
1091+
.unwrap_err();
1092+
assert_eq!(err.to_string(), "Cannot create reference on unborn branch");
1093+
10691094
let (repo, mut meta) =
10701095
named_read_only_in_memory_scenario("with-remotes-no-workspace", "remote")?;
10711096
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
@@ -1085,7 +1110,6 @@ fn errors() -> anyhow::Result<()> {
10851110
");
10861111

10871112
let (id, ref_name) = id_at(&repo, "main");
1088-
let new_name = r("refs/heads/does-not-matter");
10891113
for anchor in [
10901114
Anchor::at_id(id, Below),
10911115
Anchor::at_segment(ref_name.as_ref(), Below),
@@ -1252,7 +1276,7 @@ fn errors() -> anyhow::Result<()> {
12521276
}
12531277

12541278
#[test]
1255-
fn journey() -> anyhow::Result<()> {
1279+
fn journey_with_commits() -> anyhow::Result<()> {
12561280
let (_tmp, repo, mut meta) = named_writable_scenario("single-branch-with-3-commits")?;
12571281
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
12581282
* 281da94 (HEAD -> main) 3
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
mod apply_unapply;
12
mod create_reference;
23
mod remove_reference;

0 commit comments

Comments
 (0)