Skip to content

Commit d10252b

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

File tree

13 files changed

+601
-180
lines changed

13 files changed

+601
-180
lines changed

crates/but-graph/src/api.rs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -94,20 +94,25 @@ impl Graph {
9494
/// aren't fully defined as traversal stopped due to some abort condition being met.
9595
/// Valid partial segments always have at least one commit.
9696
pub fn partial_segments(&self) -> impl Iterator<Item = SegmentIndex> {
97-
self.base_segments().filter(|s| {
98-
let has_outgoing = self
99-
.inner
100-
.edges_directed(*s, Direction::Outgoing)
101-
.next()
102-
.is_some();
103-
if has_outgoing {
104-
return false;
105-
}
106-
self[*s]
107-
.commits
108-
.first()
109-
.is_none_or(|c| !c.parent_ids.is_empty())
110-
})
97+
self.base_segments().filter(|s| self.is_partial_segment(*s))
98+
}
99+
100+
/// Return `true` if the segment behind `sidx`
101+
/// isn't fully defined as traversal stopped due to some abort condition.
102+
/// Valid partial segments always have at least one commit.
103+
pub fn is_partial_segment(&self, sidx: SegmentIndex) -> bool {
104+
let has_outgoing = self
105+
.inner
106+
.edges_directed(sidx, Direction::Outgoing)
107+
.next()
108+
.is_some();
109+
if has_outgoing {
110+
return false;
111+
}
112+
self[sidx]
113+
.commits
114+
.first()
115+
.is_none_or(|c| !c.parent_ids.is_empty())
111116
}
112117

113118
/// Return all segments that sit on top of the `sidx` segment as `(source_commit_index(of sidx), destination_segment_index)`,

crates/but-graph/src/debug.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::init::PetGraph;
2-
use crate::{CommitFlags, Edge, Graph, Segment, SegmentIndex};
2+
use crate::{Edge, Graph, Segment, SegmentIndex};
33
use bstr::ByteSlice;
44
use gix::reference::Category;
55
use petgraph::graph::EdgeReference;
@@ -24,10 +24,10 @@ impl Graph {
2424
} else {
2525
""
2626
},
27-
kind = if commit.flags.contains(CommitFlags::NotInRemote) {
28-
"·"
29-
} else {
27+
kind = if commit.flags.is_remote() {
3028
"🟣"
29+
} else {
30+
"·"
3131
},
3232
conflict = if has_conflicts { "💥" } else { "" },
3333
flags = if !commit.flags.is_empty() {

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: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
use crate::{Graph, SegmentIndex, SegmentMetadata};
2+
use anyhow::{Context, bail};
3+
use bitflags::bitflags;
4+
use bstr::BString;
5+
use but_core::ref_metadata;
6+
7+
/// A list of segments that together represent a list of dependent branches, stacked on top of each other.
8+
#[derive(Debug, Clone)]
9+
pub struct Stack {
10+
/// If there is an integration branch, we know a base commit shared with the integration branch from
11+
/// which we branched off.
12+
/// It is `None` if this is a stack derived from a branch without relation to any other branch.
13+
pub base: Option<gix::ObjectId>,
14+
/// The branch-name denoted segments of the stack from its tip to the point of reference, typically a merge-base.
15+
/// This array is never empty.
16+
pub segments: Vec<StackSegment>,
17+
}
18+
19+
/// A typically named set of linearized commits, obtained by first-parent-only traversal.
20+
///
21+
/// Note that this maybe an aggregation of multiple [graph segments](crate::Segment).
22+
#[derive(Debug, Clone)]
23+
pub struct StackSegment {
24+
/// The unambiguous or disambiguated name of the branch at the tip of the segment, i.e. at the first commit.
25+
///
26+
/// It is `None` if this branch is the top-most stack segment and the `ref_name` wasn't pointing to
27+
/// a commit anymore that was reached by our rev-walk.
28+
/// This can happen if the ref is deleted, or if it was advanced by other means.
29+
/// Alternatively, the naming could have been ambiguous while this is the first segment in the stack.
30+
/// named segment.
31+
pub ref_name: Option<gix::refs::FullName>,
32+
/// An ID which uniquely identifies the first [graph segment](crate::Segment) that is contained
33+
/// in this instance.
34+
/// Note that it's not suitable to permanently identify the segment, so should not be persisted.
35+
pub id: SegmentIndex,
36+
/// The name of the remote tracking branch of this segment, if present, i.e. `refs/remotes/origin/main`.
37+
/// Its presence means [`commits_unique_in_remote_tracking_branch`] are possibly available.
38+
pub remote_tracking_ref_name: Option<gix::refs::FullName>,
39+
/// The portion of commits that can be reached from the tip of the *branch* downwards to the next [StackSegment],
40+
/// so that they are unique for this stack segment and not included in any other stack or stack segment.
41+
///
42+
/// The list could be empty for when this is a dedicated empty segment as insertion position of commits.
43+
pub commits: Vec<StackCommit>,
44+
/// Commits that are reachable from the remote-tracking branch that is associated with this branch,
45+
/// but are not reachable from this branch or duplicated by a commit in it if comparing their hash-identity.
46+
///
47+
/// No further processing was done to deduplicate these.
48+
pub commits_unique_in_remote_tracking_branch: Vec<StackCommit>,
49+
/// Read-only branch metadata with additional information, or `None` if nothing was present.
50+
pub metadata: Option<ref_metadata::Branch>,
51+
}
52+
53+
impl StackSegment {
54+
/// Given a list of graph `segments` to aggregate, produce a stack segment that is like the combination
55+
/// of a remote segment and a local ones, along with more detailed commits.
56+
///
57+
/// `graph` is used to look up the remote segment and find its commits.
58+
pub fn from_graph_segments(
59+
segments: &[&crate::Segment],
60+
graph: &Graph,
61+
repo: &gix::Repository,
62+
) -> anyhow::Result<Self> {
63+
let mut segments = segments.into_iter();
64+
let crate::Segment {
65+
id,
66+
ref_name,
67+
remote_tracking_ref_name,
68+
commits,
69+
metadata,
70+
} = segments.next().context("BUG: need one or more segments")?;
71+
72+
Ok(StackSegment {
73+
ref_name: ref_name.as_ref().map(|rn| rn.clone()),
74+
id: *id,
75+
remote_tracking_ref_name: remote_tracking_ref_name.as_ref().map(|rn| rn.clone()),
76+
commits: commits
77+
.iter()
78+
.chain(segments.flat_map(|s| s.commits.iter()))
79+
.map(|c| StackCommit::from_graph_commit(c, repo))
80+
.collect::<Result<_, _>>()?,
81+
// TODO: collect remotes
82+
commits_unique_in_remote_tracking_branch: vec![],
83+
metadata: metadata
84+
.as_ref()
85+
.map(|md| match md {
86+
SegmentMetadata::Branch(md) => Ok(md.clone()),
87+
SegmentMetadata::Workspace(_) => {
88+
bail!("BUG: Should always stop stacks at workspaces")
89+
}
90+
})
91+
.transpose()?,
92+
})
93+
}
94+
}
95+
96+
/// A combination of [Commits](crate::Commit) and [CommitDetails](crate::CommitDetails).
97+
#[derive(Debug, Clone, Eq, PartialEq)]
98+
pub struct StackCommit {
99+
/// The hash of the commit.
100+
pub id: gix::ObjectId,
101+
/// The IDs of the parent commits, but may be empty if this is the first commit.
102+
pub parent_ids: Vec<gix::ObjectId>,
103+
/// Additional properties to help classify this commit.
104+
pub flags: StackCommitFlags,
105+
/// The references pointing to this commit, even after dereferencing tag objects.
106+
/// These can be names of tags and branches.
107+
pub refs: Vec<gix::refs::FullName>,
108+
/// The complete message, verbatim.
109+
pub message: BString,
110+
/// The signature at which the commit was authored.
111+
pub author: gix::actor::Signature,
112+
}
113+
114+
impl StackCommit {
115+
/// Collect additional information on `commit` using `repo`.
116+
pub fn from_graph_commit(
117+
commit: &crate::Commit,
118+
repo: &gix::Repository,
119+
) -> anyhow::Result<Self> {
120+
todo!()
121+
}
122+
}
123+
124+
bitflags! {
125+
/// Provide more information about a commit, as gathered during traversal and as member of the stack.
126+
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
127+
pub struct StackCommitFlags: u8 {
128+
/// If `true`, the commit was pushed and is included in a [remote tracking branch](StackSegment::remote_tracking_ref_name).
129+
///
130+
/// These commits should be considered frozen and not be manipulated casually.
131+
const ReachableByRemote = 1 << 0;
132+
/// Following the graph upward will lead to at least one tip that is a workspace.
133+
///
134+
/// Note that if this flag isn't present, this means the commit isn't reachable
135+
/// from a workspace.
136+
const InWorkspace = 1 << 1;
137+
/// The commit is reachable from either the target branch (usually `refs/remotes/origin/main`).
138+
/// Note that when multiple workspaces are included in the traversal, this flag is set by
139+
/// any of many target branches.
140+
const Integrated = 1 << 2;
141+
/// Whether the commit is in a conflicted state, a GitButler concept.
142+
/// GitButler will perform rebasing/reordering etc. without interruptions and flag commits as conflicted if needed.
143+
/// Conflicts are resolved via the Edit Mode mechanism.
144+
///
145+
/// Note that even though GitButler won't push branches with conflicts, the user can still push such branches at will.
146+
const has_conflicts = 1 << 3;
147+
}
148+
}

0 commit comments

Comments
 (0)