Skip to content

Commit 39470f3

Browse files
committed
Ability to make an existing branch dependent and visible in the workspace
Ambiguous branches aren't displayed, but they will be once they are listed in the workspace metadata.
1 parent bbfacac commit 39470f3

File tree

13 files changed

+813
-90
lines changed

13 files changed

+813
-90
lines changed

crates/but-core/src/ref_metadata.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::Id;
2+
use gix::refs::FullNameRef;
23

34
/// Metadata about workspaces, associated with references that are designated to a workspace,
45
/// i.e. `refs/heads/gitbutler/workspaces/<name>`.
@@ -35,6 +36,50 @@ pub struct Workspace {
3536
pub push_remote: Option<String>,
3637
}
3738

39+
/// Mutations
40+
impl Workspace {
41+
/// Insert `branch` as new stack if it's not yet contained in the workspace, and return
42+
/// Returns `true` if the ref was newly added, or false if it already existed.
43+
pub fn add_new_stack_if_not_present(&mut self, branch: &FullNameRef) -> bool {
44+
if self.contains_ref(branch) {
45+
return false;
46+
};
47+
48+
self.stacks.push(WorkspaceStack {
49+
id: StackId::generate(),
50+
branches: vec![WorkspaceStackBranch {
51+
ref_name: branch.to_owned(),
52+
archived: false,
53+
}],
54+
});
55+
true
56+
}
57+
58+
/// Return `true` if the branch with `name` is the workspace target or the targets local tracking branch,
59+
/// using `repo` for the lookup of the local tracking branch.
60+
pub fn is_branch_the_target_or_its_local_tracking_branch(
61+
&self,
62+
name: &gix::refs::FullNameRef,
63+
repo: &gix::Repository,
64+
) -> anyhow::Result<bool> {
65+
let Some(target_ref) = self.target_ref.as_ref() else {
66+
return Ok(false);
67+
};
68+
69+
if target_ref.as_ref() == name {
70+
Ok(true)
71+
} else {
72+
let Some((local_tracking_branch, _remote_name)) =
73+
repo.upstream_branch_and_remote_for_tracking_branch(target_ref.as_ref())?
74+
else {
75+
return Ok(false);
76+
};
77+
Ok(local_tracking_branch.as_ref() == name)
78+
}
79+
}
80+
}
81+
82+
/// Access
3883
impl Workspace {
3984
/// Return `true` if `name` is a reference mentioned in our [stacks](Workspace::stacks).
4085
pub fn contains_ref(&self, name: &gix::refs::FullNameRef) -> bool {
@@ -92,6 +137,18 @@ pub struct Branch {
92137
pub review: Review,
93138
}
94139

140+
/// Mutations
141+
impl Branch {
142+
/// Claim that we now updated the branch in some way, and possibly also set the created time
143+
/// if `is_new_ref` is `true`
144+
pub fn update_times(&mut self, is_new_ref: bool) {
145+
self.ref_info.set_updated_to_now();
146+
if is_new_ref {
147+
self.ref_info.set_created_to_now();
148+
}
149+
}
150+
}
151+
95152
impl std::fmt::Debug for Branch {
96153
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97154
const DEFAULT_IN_TESTSUITE: gix::date::Time = gix::date::Time {

crates/but-core/tests/core/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ mod cmd;
22
mod commit;
33
mod diff;
44
mod json_samples;
5+
mod ref_metadata;
56
mod settings;
67
mod unified_diff;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
mod workspace {
2+
use but_core::ref_metadata::Workspace;
3+
4+
#[test]
5+
fn add_new_stack_if_not_present() {
6+
let mut ws = Workspace::default();
7+
assert_eq!(ws.stacks.len(), 0);
8+
9+
let a_ref = r("refs/heads/A");
10+
assert!(ws.add_new_stack_if_not_present(a_ref));
11+
assert!(!ws.add_new_stack_if_not_present(a_ref));
12+
assert_eq!(ws.stacks.len(), 1);
13+
14+
let b_ref = r("refs/heads/B");
15+
assert!(ws.add_new_stack_if_not_present(b_ref));
16+
assert_eq!(ws.stacks.len(), 2);
17+
}
18+
19+
fn r(name: &str) -> &gix::refs::FullNameRef {
20+
name.try_into().expect("statically known ref")
21+
}
22+
}

crates/but-graph/src/api.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@ impl Graph {
171171
self.hard_limit_hit = true;
172172
}
173173

174+
/// Lookup the segment of `sidx` and then find its sibling segment, if it has one.
175+
pub fn lookup_sibling_segment(&self, sidx: SegmentIndex) -> Option<&Segment> {
176+
self.inner
177+
.node_weight(self.inner.node_weight(sidx)?.sibling_segment_id?)
178+
}
179+
174180
/// Return the entry-point of the graph as configured during traversal.
175181
/// It's useful for when one wants to know which commit was used to discover the entire graph.
176182
///

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub(crate) type Entrypoint = Option<(gix::ObjectId, Option<gix::refs::FullName>)
2525

2626
/// A way to define information to be served from memory, instead of from the underlying data source, when
2727
/// [initializing](Graph::from_commit_traversal()) the graph.
28-
#[derive(Debug, Default)]
28+
#[derive(Debug, Default, Clone)]
2929
pub struct Overlay {
3030
entrypoint: Entrypoint,
3131
nonoverriding_references: Vec<gix::refs::Reference>,

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ impl Stack {
2727
.and_then(|s| s.commits.first().map(|c| c.id))
2828
}
2929

30+
/// Return the name of the first segment of the stack.
31+
pub fn ref_name(&self) -> Option<&gix::refs::FullNameRef> {
32+
self.segments
33+
.first()
34+
.and_then(|s| s.ref_name.as_ref().map(|rn| rn.as_ref()))
35+
}
36+
3037
/// Return the first commit of the first non-empty segment, or `None` this stack is completely empty, or has only empty segments.
3138
pub fn tip_skip_empty(&self) -> Option<gix::ObjectId> {
3239
self.segments.iter().find_map(|s| {
@@ -36,6 +43,7 @@ impl Stack {
3643
s.commits.first().map(|c| c.id)
3744
})
3845
}
46+
3947
/// The [base](StackSegment::base) of the last of our segments.
4048
pub fn base(&self) -> Option<gix::ObjectId> {
4149
self.segments.last().and_then(|s| s.base)

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

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,23 @@ impl Workspace<'_> {
7070
.all(|s| s.segments.iter().all(|s| !s.is_entrypoint))
7171
}
7272

73+
/// Return `true` if the branch with `name` is the workspace target or the targets local tracking branch.
74+
pub fn is_branch_the_target_or_its_local_tracking_branch(
75+
&self,
76+
name: &gix::refs::FullNameRef,
77+
) -> bool {
78+
let Some(t) = self.target.as_ref() else {
79+
return false;
80+
};
81+
82+
t.ref_name.as_ref() == name
83+
|| self
84+
.graph
85+
.lookup_sibling_segment(t.segment_index)
86+
.and_then(|local_tracking_segment| local_tracking_segment.ref_name.as_ref())
87+
.is_some_and(|local_tracking_ref| local_tracking_ref.as_ref() == name)
88+
}
89+
7390
/// Return the `commit` at the tip of the workspace itself, and do so by following empty segments along the
7491
/// first parent until the first commit is found.
7592
/// This importantly is different from the [`Graph::lookup_entrypoint()`] `commit`, as the entrypoint could be anywhere
@@ -264,6 +281,24 @@ pub enum WorkspaceKind {
264281
AdHoc,
265282
}
266283

284+
impl WorkspaceKind {
285+
/// Return `true` if this workspace has a managed reference, meaning we control certain aspects of it
286+
/// by means of workspace metadata that is associated with that ref.
287+
/// If `false`, we are more conservative and may not support all features.
288+
pub fn has_managed_ref(&self) -> bool {
289+
matches!(
290+
self,
291+
WorkspaceKind::Managed { .. } | WorkspaceKind::ManagedMissingWorkspaceCommit { .. }
292+
)
293+
}
294+
295+
/// Return `true` if we have a workspace commit, a commit that merges all stacks together.
296+
/// Implies `has_managed_ref() == true`.
297+
pub fn has_managed_commit(&self) -> bool {
298+
matches!(self, WorkspaceKind::Managed { .. })
299+
}
300+
}
301+
267302
impl WorkspaceKind {
268303
fn managed(ref_name: &Option<gix::refs::FullName>) -> anyhow::Result<Self> {
269304
Ok(WorkspaceKind::Managed {
@@ -279,7 +314,7 @@ impl WorkspaceKind {
279314
#[derive(Debug, Clone)]
280315
pub struct Target {
281316
/// The name of the target branch, i.e. the branch that all [Stacks](Stack) want to get merged into.
282-
/// Typically, this is `origin/main`.
317+
/// Typically, this is `refs/remotes/origin/main`.
283318
pub ref_name: gix::refs::FullName,
284319
/// The index to the respective segment in the graph.
285320
pub segment_index: SegmentIndex,
@@ -438,7 +473,7 @@ impl Graph {
438473
lower_bound: None,
439474
};
440475

441-
let ws_lower_bound = if ws.has_managed_ref() {
476+
let ws_lower_bound = if ws.kind.has_managed_ref() {
442477
self.compute_lowest_base(ws.id, ws.target.as_ref(), self.extra_target)
443478
.or_else(|| {
444479
// target not available? Try the base of the workspace itself
@@ -501,7 +536,7 @@ impl Graph {
501536
*lower_bound_segment_id = None;
502537
}
503538

504-
if ws.has_managed_ref() && self[ws.id].commits.is_empty() {
539+
if ws.kind.has_managed_ref() && self[ws.id].commits.is_empty() {
505540
ws.kind = WorkspaceKind::ManagedMissingWorkspaceCommit {
506541
ref_name: ws_tip_segment
507542
.ref_name
@@ -516,7 +551,7 @@ impl Graph {
516551
.is_some_and(|rn| rn.as_bstr().starts_with_str("refs/heads/gitbutler/"))
517552
}
518553

519-
if ws.has_managed_ref() {
554+
if ws.kind.has_managed_ref() {
520555
let (lowest_base, lowest_base_sidx) =
521556
ws_lower_bound.map_or((None, None), |(base, sidx)| (Some(base), Some(sidx)));
522557
for stack_top_sidx in self
@@ -1070,15 +1105,6 @@ impl Workspace<'_> {
10701105

10711106
/// Query
10721107
impl<'graph> Workspace<'graph> {
1073-
/// Return `true` if this workspace is managed, meaning we control certain aspects of it.
1074-
/// If `false`, we are more conservative and may not support all features.
1075-
pub fn has_managed_ref(&self) -> bool {
1076-
matches!(
1077-
self.kind,
1078-
WorkspaceKind::Managed { .. } | WorkspaceKind::ManagedMissingWorkspaceCommit { .. }
1079-
)
1080-
}
1081-
10821108
/// Return `true` if the workspace has workspace metadata associated with it.
10831109
/// This is relevant when creating references for example.
10841110
pub fn has_metadata(&self) -> bool {

0 commit comments

Comments
 (0)