Skip to content

Commit b47bc92

Browse files
committed
A first shot at producing stacks from a graph
1 parent 091661f commit b47bc92

File tree

6 files changed

+179
-4
lines changed

6 files changed

+179
-4
lines changed

crates/but-graph/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub use segment::{Commit, CommitDetails, CommitFlags, Segment, SegmentMetadata};
4343
mod api;
4444
/// Produce a graph from a Git repository.
4545
pub mod init;
46+
pub mod projection;
4647

4748
mod ref_metadata_legacy;
4849
pub use ref_metadata_legacy::{VirtualBranchesTomlMetadata, is_workspace_ref_name};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//! A way to represent the graph in a simplified (but more usable) form.
2+
//!
3+
//! This is the current default way of GitButler to perceive its world, but most inexpensively generated to stay
4+
//! close to the source of truth, [The Graph](crate::Graph).
5+
//!
6+
//! These types are not for direct consumption, but should be processed further for consumption by the user.
7+
8+
/// Types related to the stack representation for graphs.
9+
///
10+
/// Note that these are always a simplification, degenerating information, while maintaining a link back to the graph.
11+
mod stack;
12+
pub use stack::{Stack, StackCommit, StackCommitFlags, StackSegment};
13+
14+
mod workspace;
15+
pub use workspace::{HeadLocation, Target, Workspace};
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use crate::SegmentIndex;
2+
use bitflags::bitflags;
3+
use bstr::BString;
4+
use but_core::ref_metadata;
5+
6+
/// A list of segments that together represent a list of dependent branches, stacked on top of each other.
7+
#[derive(Debug, Clone)]
8+
pub struct Stack {
9+
/// If there is an integration branch, we know a base commit shared with the integration branch from
10+
/// which we branched off.
11+
/// It is `None` if this is a stack derived from a branch without relation to any other branch.
12+
pub base: Option<gix::ObjectId>,
13+
/// The branch-name denoted segments of the stack from its tip to the point of reference, typically a merge-base.
14+
/// This array is never empty.
15+
pub segments: Vec<StackSegment>,
16+
}
17+
18+
/// A typically named set of linearized commits, obtained by first-parent-only traversal.
19+
///
20+
/// Note that this maybe an aggregation of multiple [graph segments](crate::Segment).
21+
#[derive(Debug, Clone)]
22+
pub struct StackSegment {
23+
/// The unambiguous or disambiguated name of the branch at the tip of the segment, i.e. at the first commit.
24+
///
25+
/// It is `None` if this branch is the top-most stack segment and the `ref_name` wasn't pointing to
26+
/// a commit anymore that was reached by our rev-walk.
27+
/// This can happen if the ref is deleted, or if it was advanced by other means.
28+
/// Alternatively, the naming could have been ambiguous while this is the first segment in the stack.
29+
/// named segment.
30+
pub ref_name: Option<gix::refs::FullName>,
31+
/// An ID which uniquely identifies the first [graph segment](crate::Segment) that is contained
32+
/// in this instance.
33+
/// Note that it's not suitable to permanently identify the segment, so should not be persisted.
34+
pub id: SegmentIndex,
35+
/// The name of the remote tracking branch of this segment, if present, i.e. `refs/remotes/origin/main`.
36+
/// Its presence means [`commits_unique_in_remote_tracking_branch`] are possibly available.
37+
pub remote_tracking_ref_name: Option<gix::refs::FullName>,
38+
/// The portion of commits that can be reached from the tip of the *branch* downwards to the next [StackSegment],
39+
/// so that they are unique for this stack segment and not included in any other stack or stack segment.
40+
///
41+
/// The list could be empty for when this is a dedicated empty segment as insertion position of commits.
42+
pub commits: Vec<StackCommit>,
43+
/// Commits that are reachable from the remote-tracking branch that is associated with this branch,
44+
/// but are not reachable from this branch or duplicated by a commit in it if comparing their hash-identity.
45+
///
46+
/// No further processing was done to deduplicate these.
47+
pub commits_unique_in_remote_tracking_branch: Vec<StackCommit>,
48+
/// Read-only branch metadata with additional information, or `None` if nothing was present.
49+
pub metadata: Option<ref_metadata::Branch>,
50+
}
51+
52+
/// A combination of [Commits](crate::Commit) and [CommitDetails](crate::CommitDetails).
53+
#[derive(Debug, Clone, Eq, PartialEq)]
54+
pub struct StackCommit {
55+
/// The hash of the commit.
56+
pub id: gix::ObjectId,
57+
/// The IDs of the parent commits, but may be empty if this is the first commit.
58+
pub parent_ids: Vec<gix::ObjectId>,
59+
/// Additional properties to help classify this commit.
60+
pub flags: StackCommitFlags,
61+
/// The references pointing to this commit, even after dereferencing tag objects.
62+
/// These can be names of tags and branches.
63+
pub refs: Vec<gix::refs::FullName>,
64+
/// The complete message, verbatim.
65+
pub message: BString,
66+
/// The signature at which the commit was authored.
67+
pub author: gix::actor::Signature,
68+
}
69+
70+
bitflags! {
71+
/// Provide more information about a commit, as gathered during traversal and as member of the stack.
72+
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
73+
pub struct StackCommitFlags: u8 {
74+
/// If `true`, the commit was pushed and is included in a [remote tracking branch](StackSegment::remote_tracking_ref_name).
75+
///
76+
/// These commits should be considered frozen and not be manipulated casually.
77+
const ReachableByRemote = 1 << 0;
78+
/// Following the graph upward will lead to at least one tip that is a workspace.
79+
///
80+
/// Note that if this flag isn't present, this means the commit isn't reachable
81+
/// from a workspace.
82+
const InWorkspace = 1 << 1;
83+
/// The commit is reachable from either the target branch (usually `refs/remotes/origin/main`).
84+
/// Note that when multiple workspaces are included in the traversal, this flag is set by
85+
/// any of many target branches.
86+
const Integrated = 1 << 2;
87+
/// Whether the commit is in a conflicted state, a GitButler concept.
88+
/// GitButler will perform rebasing/reordering etc. without interruptions and flag commits as conflicted if needed.
89+
/// Conflicts are resolved via the Edit Mode mechanism.
90+
///
91+
/// Note that even though GitButler won't push branches with conflicts, the user can still push such branches at will.
92+
const has_conflicts = 1 << 3;
93+
}
94+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use crate::projection::Stack;
2+
use crate::{Graph, SegmentIndex};
3+
use but_core::ref_metadata;
4+
5+
/// A workspace is a list of [Stacks](Stack).
6+
#[derive(Debug, Clone)]
7+
pub struct Workspace {
8+
/// Where `HEAD` is currently pointing to.
9+
pub head: HeadLocation,
10+
/// One or more stacks that live in the workspace.
11+
pub stacks: Vec<Stack>,
12+
/// The target to integrate workspace stacks into.
13+
///
14+
/// If `None`, this is a local workspace that doesn't know when possibly pushed branches are considered integrated.
15+
pub target: Option<Target>,
16+
/// Read-only workspace metadata with additional information, or `None` if nothing was present.
17+
pub metadata: Option<ref_metadata::Workspace>,
18+
}
19+
20+
/// Learn where the current `HEAD` is pointing to.
21+
#[derive(Debug, Clone)]
22+
pub enum HeadLocation {
23+
/// The `HEAD` is pointing to the workspace reference, like `refs/heads/gitbutler/workspace`.
24+
Workspace {
25+
/// The name of the reference pointing to the workspace commit. Useful for deriving the workspace name.
26+
ref_name: gix::refs::FullName,
27+
},
28+
/// A segment is checked out directly.
29+
///
30+
/// It can be inside or outside of a workspace.
31+
/// If the respective segment is not named, this means the `HEAD` id detached.
32+
/// The commit that the working tree is at is always implied to be the first commit of the [`StackSegment`].
33+
Segment {
34+
/// The id of the segment to be found in any stack of the [workspace stacks](Workspace::stacks).
35+
segment_index: SegmentIndex,
36+
},
37+
}
38+
39+
/// Information about the target reference.
40+
#[derive(Debug, Clone)]
41+
pub struct Target {
42+
/// The name of the target branch, i.e. the branch that all [Stacks](Stack) want to get merged into.
43+
pub ref_name: gix::refs::FullName,
44+
/// The amount of commits that aren't reachable by any segment in the workspace, they are in its future.
45+
pub commits_ahead: usize,
46+
}
47+
48+
impl Graph {
49+
/// Analyse the current graph starting at its [entrypoint](Self::lookup_entrypoint()).
50+
///
51+
/// No matter what, each location of `HEAD1`, which corresponds to the entrypoint, can be represented as workspace.
52+
/// Further, the most expensive operations we perform to query additional commit information by reading it, but we
53+
/// only do so on the ones that the user can interact with.
54+
pub fn to_workspace(&self) -> anyhow::Result<Workspace> {
55+
todo!()
56+
}
57+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ fn unborn() -> anyhow::Result<()> {
3434
hard_limit_hit: false,
3535
}
3636
"#);
37+
38+
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"");
3739
Ok(())
3840
}
3941

@@ -486,6 +488,7 @@ fn with_limits() -> anyhow::Result<()> {
486488
mod with_workspace;
487489

488490
mod utils;
491+
use crate::utils::graph_workspace;
489492
pub use utils::{
490493
StackState, add_stack_with_segments, add_workspace, id_at, id_by_rev,
491494
read_only_in_memory_scenario, standard_options,

crates/but-graph/tests/graph/utils.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@ use but_graph::{EntryPoint, Graph, SegmentIndex};
22
use std::collections::{BTreeMap, BTreeSet};
33
use termtree::Tree;
44

5-
type SegmentTree = Tree<String>;
5+
type StringTree = Tree<String>;
66

77
/// Visualize `graph` as a tree.
8-
pub fn graph_tree(graph: &but_graph::Graph) -> SegmentTree {
8+
pub fn graph_workspace(_graph: &but_graph::projection::Workspace) -> StringTree {
9+
todo!()
10+
}
11+
12+
/// Visualize `graph` as a tree.
13+
pub fn graph_tree(graph: &Graph) -> StringTree {
914
fn tree_for_commit(
1015
commit: &but_graph::Commit,
1116
has_conflicts: bool,
1217
is_entrypoint: bool,
1318
is_early_end: bool,
1419
hard_limit_hit: bool,
15-
) -> SegmentTree {
20+
) -> StringTree {
1621
Graph::commit_debug_string(
1722
commit,
1823
has_conflicts,
@@ -27,7 +32,7 @@ pub fn graph_tree(graph: &but_graph::Graph) -> SegmentTree {
2732
graph: &but_graph::Graph,
2833
sidx: SegmentIndex,
2934
seen: &mut BTreeSet<SegmentIndex>,
30-
) -> SegmentTree {
35+
) -> StringTree {
3136
if seen.contains(&sidx) {
3237
return format!(
3338
"→:{sidx}:{name}",

0 commit comments

Comments
 (0)