|
| 1 | +use std::borrow::Cow; |
| 2 | + |
| 3 | +/// Returned by [function::apply()]. |
| 4 | +pub struct Outcome<'graph> { |
| 5 | + /// The newly created graph, if owned, useful to project a workspace and see how the workspace looks like with the branch applied. |
| 6 | + /// If borrowed, the graph already contains the desired branch and nothing had to be applied. |
| 7 | + pub graph: Cow<'graph, but_graph::Graph>, |
| 8 | + /// `true` if we created the given workspace ref as it didn't exist yet. |
| 9 | + pub workspace_ref_created: bool, |
| 10 | +} |
| 11 | + |
| 12 | +impl Outcome<'_> { |
| 13 | + /// Return `true` if a new graph traversal was performed, which always is a sign for an operation which changed the workspace. |
| 14 | + /// This is `false` if the to be applied branch was already contained in the current workspace. |
| 15 | + pub fn workspace_changed(&self) -> bool { |
| 16 | + matches!(self.graph, Cow::Owned(_)) |
| 17 | + } |
| 18 | +} |
| 19 | + |
| 20 | +impl std::fmt::Debug for Outcome<'_> { |
| 21 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 22 | + f.debug_struct("Outcome") |
| 23 | + .field("workspace_changed", &self.workspace_changed()) |
| 24 | + .field("workspace_ref_created", &self.workspace_ref_created) |
| 25 | + .finish() |
| 26 | + } |
| 27 | +} |
| 28 | + |
| 29 | +/// How the newly applied branch should be integrated into the workspace. |
| 30 | +#[derive(Default, Debug, Copy, Clone)] |
| 31 | +pub enum IntegrationMode { |
| 32 | + /// Do nothing but to merge it into the workspace commit, *even* if it's not needed as the workspace reference |
| 33 | + /// can connect directly with the *one* workspace base. |
| 34 | + #[default] |
| 35 | + AlwaysMerge, |
| 36 | + /// Only create a merge commit if a new commit is effectively merged in. This avoids *unnecessary* merge commits, |
| 37 | + /// but also requires support for this when creating commits (which may then have to create a merge-commit themselves). |
| 38 | + // TODO: make this the default |
| 39 | + MergeIfNeeded, |
| 40 | +} |
| 41 | + |
| 42 | +/// What to do if the applied branch conflicts? |
| 43 | +#[derive(Default, Debug, Copy, Clone)] |
| 44 | +pub enum OnWorkspaceConflict { |
| 45 | + /// Provide additional information about the stack that conflicted and the files involved in it, |
| 46 | + /// and don't perform the operation. |
| 47 | + #[default] |
| 48 | + AbortAndReportConflictingStack, |
| 49 | +} |
| 50 | + |
| 51 | +/// Decide how a newly created workspace reference should be named. |
| 52 | +#[derive(Default, Debug, Clone)] |
| 53 | +pub enum WorkspaceReferenceNaming { |
| 54 | + /// Create a default workspace branch |
| 55 | + #[default] |
| 56 | + Default, |
| 57 | + /// Create a workspace with the given name instead. |
| 58 | + Given(gix::refs::FullName), |
| 59 | +} |
| 60 | + |
| 61 | +/// Options for [function::apply()]. |
| 62 | +#[derive(Default, Debug, Clone)] |
| 63 | +pub struct Options { |
| 64 | + /// how the branch should be brought into the workspace. |
| 65 | + pub integration_mode: IntegrationMode, |
| 66 | + /// Decide how to deal with conflicts. |
| 67 | + pub on_workspace_conflict: OnWorkspaceConflict, |
| 68 | + /// How the workspace reference should be named should it be created. |
| 69 | + /// The creation is always needed if there are more than one branch applied. |
| 70 | + pub workspace_reference_naming: WorkspaceReferenceNaming, |
| 71 | +} |
| 72 | + |
| 73 | +pub(crate) mod function { |
| 74 | + use super::{Options, Outcome, WorkspaceReferenceNaming}; |
| 75 | + use crate::ref_info::WorkspaceExt; |
| 76 | + use anyhow::{Context, bail}; |
| 77 | + use but_core::ref_metadata::{StackId, ValueInfo, WorkspaceStack, WorkspaceStackBranch}; |
| 78 | + use but_core::{RefMetadata, ref_metadata}; |
| 79 | + use but_graph::init::Overlay; |
| 80 | + use but_graph::projection::WorkspaceKind; |
| 81 | + use std::borrow::Cow; |
| 82 | + |
| 83 | + /// Apply `branch` to the given `workspace`, and possibly create the workspace reference in `repo` |
| 84 | + /// along with its `meta`-data if it doesn't exist yet. |
| 85 | + /// Otherwise, add it to the existing `workspace`, and update its metadata accordingly. |
| 86 | + /// **This means that the contents of `branch` is observable from the new state of `repo`**. |
| 87 | + /// |
| 88 | + /// Note that `workspace` is expected to match the state in `repo` as it's used instead of querying `repo` directly |
| 89 | + /// where possible. |
| 90 | + /// |
| 91 | + /// Also note that we will create a managed workspace reference as needed if necessary, and a workspace commit if there is more than |
| 92 | + /// one reference in the workspace afterward. |
| 93 | + /// |
| 94 | + /// On `error`, neither `repo` nor `meta` will have been changed, but `repo` may contain in-memory objects. |
| 95 | + /// Otherwise, objects will have been persisted, and references and metadata will have been updated. |
| 96 | + pub fn apply<'graph, T: RefMetadata>( |
| 97 | + branch: &gix::refs::FullNameRef, |
| 98 | + workspace: &but_graph::projection::Workspace<'graph>, |
| 99 | + repo: &mut gix::Repository, |
| 100 | + meta: &mut T, |
| 101 | + Options { |
| 102 | + integration_mode: _, |
| 103 | + on_workspace_conflict: _, |
| 104 | + workspace_reference_naming, |
| 105 | + }: Options, |
| 106 | + ) -> anyhow::Result<Outcome<'graph>> { |
| 107 | + if workspace.refname_is_segment(branch) { |
| 108 | + return Ok(Outcome { |
| 109 | + graph: Cow::Borrowed(workspace.graph), |
| 110 | + workspace_ref_created: false, |
| 111 | + }); |
| 112 | + } |
| 113 | + |
| 114 | + if let Some(ws_ref_name) = workspace.ref_name() { |
| 115 | + if repo.try_find_reference(ws_ref_name)?.is_none() { |
| 116 | + // The workspace is the probably ad-hoc, and doesn't exist, *assume* unborn. |
| 117 | + bail!( |
| 118 | + "Cannot create reference on unborn branch '{}'", |
| 119 | + ws_ref_name.shorten() |
| 120 | + ); |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + if workspace.has_workspace_commit_in_ancestry(repo) { |
| 125 | + bail!("Refusing to work on workspace whose workspace commit isn't at the top"); |
| 126 | + } |
| 127 | + |
| 128 | + let (workspace_ref_name_to_update, ws_ref_metadata, branch_to_apply_metadata, graph) = |
| 129 | + match &workspace.kind { |
| 130 | + WorkspaceKind::Managed { ref_name } |
| 131 | + | WorkspaceKind::ManagedMissingWorkspaceCommit { ref_name } => ( |
| 132 | + ref_name.clone(), |
| 133 | + meta.workspace(ref_name.as_ref())?, |
| 134 | + None, |
| 135 | + Cow::Borrowed(workspace.graph), |
| 136 | + ), |
| 137 | + WorkspaceKind::AdHoc => { |
| 138 | + // We need to switch over to a possibly existing workspace. |
| 139 | + // We know that the current branch is *not* reachable from the workspace or isn't naturally included, |
| 140 | + // so it needs to be added as well. |
| 141 | + let next_ws_ref_name = match workspace_reference_naming { |
| 142 | + WorkspaceReferenceNaming::Default => { |
| 143 | + gix::refs::FullName::try_from("refs/heads/gitbutler/workspace") |
| 144 | + .expect("known statically") |
| 145 | + } |
| 146 | + WorkspaceReferenceNaming::Given(name) => name, |
| 147 | + }; |
| 148 | + let ws_ref_id = match repo.try_find_reference(next_ws_ref_name.as_ref())? { |
| 149 | + None => { |
| 150 | + // Create a workspace reference later at the current AdHoc workspace id |
| 151 | + let ws_id = workspace |
| 152 | + .stacks |
| 153 | + .first() |
| 154 | + .and_then(|s| s.segments.first()) |
| 155 | + .and_then(|s| s.commits.first().map(|c| c.id)) |
| 156 | + .context("BUG: how can an empty ad-hoc workspace exist? Should have at least one stack-segment with commit")?; |
| 157 | + ws_id |
| 158 | + } |
| 159 | + Some(mut existing_workspace_reference) => { |
| 160 | + let id = existing_workspace_reference.peel_to_id_in_place()?; |
| 161 | + id.detach() |
| 162 | + } |
| 163 | + }; |
| 164 | + |
| 165 | + // Get as close as possible to what would happen if the branch was already part of it (without merging), |
| 166 | + // and branch-metadata can disambiguate. Having this data also isn't harmful. |
| 167 | + // We want to see if the branch is naturally included in workspace already. |
| 168 | + let branch_md = meta.branch(branch)?; |
| 169 | + let (branch_md_override, branch_md_to_set) = if branch_md.is_default() { |
| 170 | + ( |
| 171 | + Some((branch.to_owned(), (*branch_md).clone())), |
| 172 | + Some(branch_md), |
| 173 | + ) |
| 174 | + } else { |
| 175 | + (None, None) |
| 176 | + }; |
| 177 | + let mut ws_md = meta.workspace(next_ws_ref_name.as_ref())?; |
| 178 | + { |
| 179 | + let ws_mut: &mut ref_metadata::Workspace = &mut *ws_md; |
| 180 | + ws_mut.stacks.push(WorkspaceStack { |
| 181 | + id: StackId::generate(), |
| 182 | + branches: vec![WorkspaceStackBranch { |
| 183 | + ref_name: branch.to_owned(), |
| 184 | + archived: false, |
| 185 | + }], |
| 186 | + }) |
| 187 | + } |
| 188 | + let ws_md_override = Some((next_ws_ref_name.clone(), (*ws_md).clone())); |
| 189 | + |
| 190 | + let graph = workspace.graph.redo_traversal_with_overlay( |
| 191 | + repo, |
| 192 | + meta, |
| 193 | + Overlay::default() |
| 194 | + .with_entrypoint(ws_ref_id, Some(next_ws_ref_name)) |
| 195 | + .with_branch_metadata_override(branch_md_override) |
| 196 | + .with_workspace_metadata_override(ws_md_override), |
| 197 | + )?; |
| 198 | + |
| 199 | + let ws = graph.to_workspace()?; |
| 200 | + if ws.refname_is_segment(branch) { |
| 201 | + todo!( |
| 202 | + "no need to add this branch, but we need to add the current ad-hoc branch instead" |
| 203 | + ); |
| 204 | + } |
| 205 | + |
| 206 | + todo!("put current ad-hoc stack into ") |
| 207 | + } |
| 208 | + }; |
| 209 | + |
| 210 | + // Everything worked? Assure the ref exists now that (nearly nothing) can go wrong anymore. |
| 211 | + let workspace_ref_created = false; // TODO: use rval of reference update to know if it existed. |
| 212 | + |
| 213 | + if let Some(branch_md) = branch_to_apply_metadata { |
| 214 | + meta.set_branch(branch_md)?; |
| 215 | + } |
| 216 | + |
| 217 | + Ok(Outcome { |
| 218 | + graph, |
| 219 | + workspace_ref_created, |
| 220 | + }) |
| 221 | + } |
| 222 | +} |
0 commit comments