Skip to content

Commit 5bbb034

Browse files
committed
Improve the documentation of Graphs and their workspace projections.
This should help to onboard people.
1 parent 0e68c51 commit 5bbb034

File tree

3 files changed

+168
-0
lines changed

3 files changed

+168
-0
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ impl Options {
6767

6868
/// Set a hard limit for the amount of commits to traverse. Even though it may be off by a couple, it's not dependent
6969
/// on any additional logic.
70+
///
71+
/// ### Warning
72+
///
73+
/// This stops traversal early despite not having discovered all desired graph partitions, possibly leading to
74+
/// incorrect results. Ideally, this is not used.
7075
pub fn with_hard_limit(mut self, limit: usize) -> Self {
7176
self.hard_limit = Some(limit);
7277
self

crates/but-graph/src/lib.rs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,167 @@
3434
//!
3535
//! All this makes the Graph the **new core data-structure** that is the world of GitButler and upon which visualisations and
3636
//! mutation operations are based.
37+
//!
38+
//! ### New Workspace Concepts
39+
//!
40+
//! The workspace is merely a projection of *The Graph*, and as such is mostly useful for display and user interaction.
41+
//! In the end it boils down to passing commit-hashes, or [segment-ids](SegmentIndex) at most.
42+
//!
43+
//! The workspace has been redesigned from the ground up for flexibility, enabling new user-experiences. To help thinking
44+
//! about these, a few new concepts will be good to know about.
45+
//!
46+
//! #### Entrypoint
47+
//!
48+
//! *The Graph* knows where its traversal started as *Entrypoint*, even though it may extend beyond the entrypoint as it
49+
//! needs to discover possible surrounding workspaces and the target branches that come with them.
50+
//! In practice, the entrypoint relates to the position of the Git `HEAD` reference, and with that it relates to what
51+
//! the user currently sees in their worktree.
52+
//!
53+
//! #### Early End of Traversal
54+
//!
55+
//! During traversal there are mandatory goals, but when reached the traversal usually obeys a limit, if configured.
56+
//! This is particularly relevant in open-ended traversals outside of workspaces, they can go on until the end of history,
57+
//! literally.
58+
//!
59+
//! For that reason, whenever a commit isn't the end of the graph, but the end traversal as a [limit was hit](init::Options::with_limit_hint),
60+
//! it will be flagged as such.
61+
//!
62+
//! This way one can visualize such Early Ends, and allow the user to extend the traversal selectively the next time it
63+
//! is performed.
64+
//!
65+
//! Despite that, one has to learn how to deal with possible huge graphs, and possible workspaces with a lot of commits,
66+
//! and [a hard limit](init::Options::with_hard_limit()) as long as downstream cannot deal with this on their own.
67+
//!
68+
//! #### Managed Workspaces, and unmanaged ones
69+
//!
70+
//! A Workspace is considered managed if it [has workspace metadata](projection::Workspace::metadata). This is typically
71+
//! only the case for workspaces that have been created by GitButler.
72+
//!
73+
//! Workspaces without such metadata can be anything, and are usually just made up to allow GitButler to work with it based
74+
//! on any `HEAD` position. These should be treated with care, and multi-lane workflows should generally be avoided - these
75+
//! are reserved to managed Workspaces with the managed merge commit that comes with them.
76+
//!
77+
//! #### Optional Targets
78+
//!
79+
//! Even on *Managed Workspaces*, target references are now optional. This makes it possible to have a workspace that doesn't
80+
//! know if it's integrated or not. These are the reason a [soft limit](init::Options::with_limit_hint()) must always be set
81+
//! to assure the traversal doesn't fetch the entire Git history.
82+
//!
83+
//! This, however, also means that the workspace creation doesn't have to be interrupted by a "what's your target" prompt anymore.
84+
//! Instead, this can be prompted once an action first requires it.
85+
//!
86+
//! #### Commit Flags and Segment Flags
87+
//!
88+
//! For convenience, various boolean parameters have been aggregated into [bitflags](Commit::flags). Thanks to the way *The Graph*
89+
//! is traversed, we know that the first commit of any [graph segment](Segment) will always bear the flags that are also used by every other commit
90+
//! contained within it. Thus, [segment flags](Segment::non_empty_flags_of_first_commit()) are equivalent to the flags of
91+
//! their first commit.
92+
//!
93+
//! The same is *not* true for [stack segments](projection::StackSegment), i.e. segments within a [workspace projection](projection::Workspace).
94+
//! The reason for this is that they are first-parent aggregations of one *or more* [graph segments](Segment), and thus have multiple
95+
//! sets of flags, possibly one per [segment](Segment).
96+
//!
97+
//! #### The 'frozen' Commit-Flag
98+
//!
99+
//! Commits now have a new state that tells for each if it is reachable by *any* remote, and further, if it's reachable
100+
//! by the remote configured for *their segment*.
101+
//!
102+
//! This additional partitioning could be leveraged for enhanced user experiences.
103+
//!
104+
//! ### The Graph - Traversal and more
105+
//!
106+
//! There are three distinct steps to processing the git commit-graph into more usable forms.
107+
//!
108+
//! * **traversal**
109+
//! - walk the git commit graph to produce a segmented graph, which assigns commits to segments,
110+
//! but also splits segments on incoming and multiple outgoing connections.
111+
//! * **reconciliation**
112+
//! - a post-processing step which adds workspace metadata into the segmented graph, as such information
113+
//! can't be held in the commit-graph itself.
114+
//! * **projection**
115+
//! - transform the segmented and reconciled graph into a view that is application-specific, i.e. see
116+
//! stacks of first-parent traversed named segments.
117+
//!
118+
//! #### Commits are owned by Segments
119+
//!
120+
//! A commit can only be owned by a single segment. Thus, there are empty *named* segments which point at other segments,
121+
//! effectively representing a reference.
122+
//! Which of these references gets to own a commit depends on the traversal logic, or can be the result of *Reconciliation*.
123+
//!
124+
//! #### Reconciliation
125+
//!
126+
//! *The Graph* is created from traversing the Git commit graph. Thus, information that is not contained in it has to be
127+
//! reconciled with *what was actually traversed*.
128+
//!
129+
//! Nonetheless, we can create *stacks* as independent branches and dependent branches inside of them without having
130+
//! a single commit to differentiate their respective branches from each other.
131+
//!
132+
//! Imagine a repository with a single commit `73a30f8` with the following Git references pointing to it: `gitbutler/workspace`,
133+
//! `stack1-segment1`, `stack1-segment2`, `stack2-segment1`, and `refs/remotes/origin/main`.
134+
//!
135+
//! Right after traversal, a Graph would look like this:
136+
//!
137+
//! ```text
138+
//! ┌────────────────────┐
139+
//! │ origin/main │
140+
//! └────────────────────┘
141+
//! │
142+
//! ▼
143+
//! ┌────────────────────────┐
144+
//! │gitbutler/workspace │
145+
//! │------------------------│
146+
//! │73a30f8 ►stack1-segment1│
147+
//! │ ►stack1-segment2│
148+
//! │ ►stack2-segment1│
149+
//! │ ►main │
150+
//! └────────────────────────┘
151+
//! ```
152+
//!
153+
//! This is due to `gitbutler/workspace` finding `73a30f8` first, with `origin/main` arriving later, pointing to the
154+
//! first commit in `gitbutler/workspace` effectively. The other references aren't participating in the traversal.
155+
//!
156+
//! The tip that finds the commit first is dependent on various factors, and it could also happen that `origin/main` finds
157+
//! it first. In any case, this needs to be adjusted after traversal in the process called *reconiliation*, so the graph
158+
//! matches what our [workspace metadata](but_core::ref_metadata::Workspace::stacks) says it should be.
159+
//!
160+
//! After reconciling, the graph would become this:
161+
//!
162+
//! ```text
163+
//! ┌────────────────────┐
164+
//! │ origin/main │
165+
//! └────────────────────┘
166+
//! │ ┌────────────────────┐
167+
//! │ │gitbutler/workspace │
168+
//! │ └────────────────────┘
169+
//! │ │
170+
//! │ ┌─────────┴─────────┐
171+
//! │ │ │
172+
//! │ ▼ │
173+
//! │ ┌───────────────┐ │
174+
//! │ │stack1-segment1│ ▼
175+
//! │ └───────────────┘ ┌───────────────┐
176+
//! │ │ │stack2-segment1│
177+
//! │ ▼ └───────────────┘
178+
//! │ ┌───────────────┐ │
179+
//! │ │stack1-segment2│ │
180+
//! │ └───────────────┘ │
181+
//! │ │ │
182+
//! │ └─────────┬─────────┘
183+
//! │ │
184+
//! │ ▼
185+
//! │ ┌─────────┐
186+
//! │ │ main │
187+
//! └─────────────────▶│ ------- │
188+
//! │ 73a30f8 │
189+
//! └─────────┘
190+
//! ```
191+
//!
192+
//! #### Projection
193+
//!
194+
//! A projection is a mapping of the segmented graph to any shape an application needs, and for any purpose.
195+
//! It cannot be stressed enough that the source of truth for all commit-graph manipulation must be the segmented graph,
196+
//! as projections are inherently lossy.
197+
//! Thus, it's useful create projects with links back to the segments that the information was extracted from.
37198
#![forbid(unsafe_code)]
38199
#![deny(missing_docs, rust_2018_idioms)]
39200

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ impl StackSegment {
125125
.next()
126126
.context("BUG: need one or more segments")?;
127127

128+
// TODO: copy the ReachableByMatchingRemote down to all segments from the first commit that has them,
129+
// as they are only detected (and set) on named commits, not their anonymous 'splits'.
128130
let mut commits_by_segment = Vec::new();
129131
for s in segments {
130132
let mut stack_commits = Vec::new();

0 commit comments

Comments
 (0)