|
| 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