Skip to content

Commit 337c131

Browse files
committed
Use the projected workspace to drive ref-info
That way it will be possible for the graph to first show up in the UI.
1 parent 280c503 commit 337c131

File tree

11 files changed

+674
-257
lines changed

11 files changed

+674
-257
lines changed

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{CommitFlags, CommitIndex, Edge};
22
use crate::{Graph, Segment, SegmentIndex, SegmentMetadata};
3-
use anyhow::bail;
3+
use anyhow::{Context, bail};
44
use but_core::RefMetadata;
55
use gix::hashtable::hash_map::Entry;
66
use gix::prelude::{ObjectIdExt, ReferenceExt};
@@ -98,6 +98,7 @@ impl Graph {
9898
options: Options,
9999
) -> anyhow::Result<Self> {
100100
let head = repo.head()?;
101+
let mut is_detached = false;
101102
let (tip, maybe_name) = match head.kind {
102103
gix::head::Kind::Unborn(ref_name) => {
103104
let mut graph = Graph::default();
@@ -109,6 +110,7 @@ impl Graph {
109110
return Ok(graph);
110111
}
111112
gix::head::Kind::Detached { target, peeled } => {
113+
is_detached = true;
112114
(peeled.unwrap_or(target).attach(repo), None)
113115
}
114116
gix::head::Kind::Symbolic(existing_reference) => {
@@ -117,7 +119,23 @@ impl Graph {
117119
(tip, Some(existing_reference.inner.name))
118120
}
119121
};
120-
Self::from_commit_traversal(tip, maybe_name, meta, options)
122+
let mut graph = Self::from_commit_traversal(tip, maybe_name, meta, options)?;
123+
if is_detached {
124+
// graph is eagerly naming segments, which we undo to show it's detached.
125+
let sidx = graph
126+
.entrypoint
127+
.context("BUG: entrypoint is set after first traversal")?
128+
.0;
129+
let s = &mut graph[sidx];
130+
if let Some((rn, first_commit)) = s
131+
.commits
132+
.first_mut()
133+
.and_then(|first_commit| s.ref_name.take().map(|rn| (rn, first_commit)))
134+
{
135+
first_commit.refs.push(rn);
136+
}
137+
};
138+
Ok(graph)
121139
}
122140
/// Produce a minimal-effort representation of the commit-graph reachable from the commit at `tip` such the returned instance
123141
/// can represent everything that's observed, without loosing information.

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::{CommitFlags, Graph, SegmentIndex, SegmentMetadata};
22
use anyhow::{Context, bail};
33
use bitflags::bitflags;
44
use but_core::ref_metadata;
5+
use gix::reference::Category;
56
use petgraph::Direction;
67
use std::fmt::Formatter;
78

@@ -80,9 +81,12 @@ pub struct StackSegment {
8081
///
8182
/// The list could be empty for when this is a dedicated empty segment as insertion position of commits.
8283
pub commits: Vec<StackCommit>,
83-
/// Commits that are *only* reachable from the remote-tracking branch that is associated with this branch.
84+
/// Commits that are *only* reachable from the tip of the remote-tracking branch that is associated with this branch,
85+
/// down to the first (and possibly unrelated) non-remote commit.
8486
/// Note that these commits may not have an actual commit-graph connection to the local
8587
/// `commits` available above.
88+
/// Further, despite being in a simple list, their order is based on a simple topological walk, so
89+
/// this form doesn't imply a linear history.
8690
pub commits_on_remote: Vec<StackCommit>,
8791
/// Read-only branch metadata with additional information, or `None` if nothing was present.
8892
pub metadata: Option<ref_metadata::Branch>,
@@ -127,6 +131,7 @@ impl StackSegment {
127131

128132
// TODO: copy the ReachableByMatchingRemote down to all segments from the first commit that has them,
129133
// as they are only detected (and set) on named commits, not their anonymous 'splits'.
134+
// Is this not sufficiently done below? I think so - needs test.
130135
let mut commits_by_segment = Vec::new();
131136
for s in segments {
132137
let mut stack_commits = Vec::new();
@@ -153,7 +158,12 @@ impl StackSegment {
153158

154159
let mut v = Vec::new();
155160
graph.visit_all_segments_until(remote_sidx, Direction::Outgoing, |s| {
156-
let prune = !s.commits.iter().all(|c| c.flags.is_remote());
161+
let prune = !s.commits.iter().all(|c| c.flags.is_remote())
162+
// Do not 'steal' commits from other known remote segments while they are officially connected.
163+
|| (s.id != remote_sidx
164+
&& s.ref_name
165+
.as_ref()
166+
.is_some_and(|orn| orn.category() == Some(Category::RemoteBranch)));
157167
if prune {
158168
// See if this segment links to a commit we know as local, and mark it accordingly.
159169
// `s` may be in `segments`, let's find it and mark all its commits (and all that follow).

crates/but-graph/tests/fixtures/scenarios.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ git init detached
9999
(cd detached
100100
commit init && git branch other
101101
commit first && git tag release/v1 && git tag -am "tag object" annotated
102+
git checkout -f @
102103
)
103104

104105
# A top-down split that is highly unusual, but good to assure we can handle it.

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,24 @@ fn unborn() -> anyhow::Result<()> {
4949
fn detached() -> anyhow::Result<()> {
5050
let (repo, meta) = read_only_in_memory_scenario("detached")?;
5151
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
52-
* 541396b (HEAD -> main, tag: release/v1, tag: annotated) first
52+
* 541396b (HEAD, tag: release/v1, tag: annotated, main) first
5353
* fafd9d0 (other) init
5454
");
5555

56+
// Detached branches are forcefully made anonymous, and it's something
57+
// we only know by examining `HEAD`.
5658
let graph = Graph::from_head(&repo, &*meta, standard_options())?;
5759
insta::assert_snapshot!(graph_tree(&graph), @r"
58-
└── 👉►:0:main
59-
└── ·541396b (⌂|1) ►tags/annotated, ►tags/release/v1
60+
└── ►:0:anon:
61+
└── 👉·541396b (⌂|1) ►tags/annotated, ►tags/release/v1, ►main
6062
└── ►:1:other
6163
└── ·fafd9d0 (⌂|1)
6264
");
6365
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"
64-
⌂:0:main <> ✓!
65-
└── ≡:0:main
66-
├── :0:main
67-
│ └── ·541396b ►tags/annotated, ►tags/release/v1
66+
⌂:0:DETACHED <> ✓!
67+
└── ≡:0:<anon>
68+
├── :0:<anon>
69+
│ └── ·541396b ►tags/annotated, ►tags/release/v1, ►main
6870
└── :1:other
6971
└── ·fafd9d0
7072
");
@@ -78,7 +80,7 @@ fn detached() -> anyhow::Result<()> {
7880
node weights: {
7981
0: StackSegment {
8082
id: NodeIndex(0),
81-
ref_name: "refs/heads/main",
83+
ref_name: "None",
8284
remote_tracking_ref_name: "None",
8385
commits: [
8486
Commit(541396b, ⌂|1),
@@ -283,10 +285,9 @@ fn stacked_rebased_remotes() -> anyhow::Result<()> {
283285
// 'main' is frozen because it connects to a 'foreign' remote, the commit was pushed.
284286
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"
285287
⌂:0:B <> ✓!
286-
└── ≡:0:B <> origin/B⇡1⇣2
287-
├── :0:B <> origin/B⇡1⇣2
288+
└── ≡:0:B <> origin/B⇡1⇣1
289+
├── :0:B <> origin/B⇡1⇣1
288290
│ ├── 🟣682be32
289-
│ ├── 🟣e29c23d
290291
│ └── ·312f819
291292
├── :2:A <> origin/A⇡1⇣1
292293
│ ├── 🟣e29c23d

crates/but-graph/tests/graph/init/with_workspace.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -839,14 +839,14 @@ fn stacked_rebased_remotes() -> anyhow::Result<()> {
839839
└── 🟣e29c23d
840840
└── →:1: (origin/main)
841841
");
842-
// It's worth noting that we can list remote commits multiple times, and this is because
842+
// It's worth noting that we avoid double-listing remote commits that are also
843+
// directly owned by another remote segment.
843844
// they have to be considered as something relevant to the branch history.
844845
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"
845846
📕🏘️:0:gitbutler/workspace <> ✓refs/remotes/origin/main
846-
└── ≡:2:B <> origin/B⇡1⇣2
847-
├── :2:B <> origin/B⇡1⇣2
847+
└── ≡:2:B <> origin/B⇡1⇣1
848+
├── :2:B <> origin/B⇡1⇣1
848849
│ ├── 🟣682be32
849-
│ ├── 🟣e29c23d
850850
│ └── ·312f819 (🏘️)
851851
└── :4:A <> origin/A⇡1⇣1
852852
├── 🟣e29c23d
@@ -873,10 +873,9 @@ fn stacked_rebased_remotes() -> anyhow::Result<()> {
873873
");
874874
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"
875875
📕🏘️:1:gitbutler/workspace <> ✓refs/remotes/origin/main
876-
└── ≡:4:B <> origin/B⇡1⇣2
877-
├── :4:B <> origin/B⇡1⇣2
876+
└── ≡:4:B <> origin/B⇡1⇣1
877+
├── :4:B <> origin/B⇡1⇣1
878878
│ ├── 🟣682be32
879-
│ ├── 🟣e29c23d
880879
│ └── ·312f819 (🏘️)
881880
└── 👉:0:A <> origin/A⇡1⇣1
882881
├── 🟣e29c23d

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ pub fn ref_info(args: &super::Args, ref_name: Option<&str>, expensive: bool) ->
483483
let opts = but_workspace::ref_info::Options {
484484
stack_commit_limit: 0,
485485
expensive_commit_info: expensive,
486+
traversal: Default::default(),
486487
};
487488

488489
let project = project.with_context(|| {

crates/but-workspace/src/lib.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ mod commit;
7373
///
7474
/// Note that many of these types should eventually end up in the crate root.
7575
pub mod ref_info;
76-
pub use ref_info::function::{head_info, ref_info};
76+
pub use ref_info::function::{head_info, head_info2, ref_info, ref_info2};
7777

7878
/// High level Stack funtions that use primitives from this crate (`but-workspace`)
7979
pub mod stack_ext;
@@ -162,19 +162,25 @@ impl HunkHeader {
162162
///
163163
/// We always try to deduce a set of stacks that are currently applied to a workspace,
164164
/// even though it's possible to look at refs that are outside a workspace as well.
165+
/// TODO: this should become the UI version of [`but_graph::projection::Workspace`].
166+
/// This should also include base-branch data, see `get_base_branch_data()`.
165167
#[derive(Debug, Clone, Eq, PartialEq)]
166168
pub struct RefInfo {
167-
/// The name of the ref that points to a workspace commit.
168-
///
169-
/// Such a commit always combines one or more branches and stacks.
169+
/// The name of the ref that points to a workspace commit,
170+
/// *or* the name of the first stack segment.
170171
pub workspace_ref_name: Option<gix::refs::FullName>,
171172
/// The stacks visible in the current workspace.
172173
///
173174
/// This is an empty array if the `HEAD` is detached.
174175
/// Otherwise, there is one or more stacks.
175176
pub stacks: Vec<branch::Stack>,
176177
/// The full name to the target reference that we should integrate with, if present.
178+
/// It's never present in single-branch mode.
177179
pub target_ref: Option<gix::refs::FullName>,
180+
/// The `workspace_ref_name` is `Some(_)` and belongs to GitButler, because it had metadata attached.
181+
pub is_managed: bool,
182+
/// The workspace represents what `HEAD` is pointing to.
183+
pub is_entrypoint: bool,
178184
}
179185

180186
/// A representation of the commit that is the tip of the workspace i.e., usually what `HEAD` points to,

0 commit comments

Comments
 (0)