| 1 | //! Landing a change onto a bookmark (decided §13.2). |
| 2 | //! |
| 3 | //! > **§13.2 merge semantics** — server-side fast-forward + merge commit. |
| 4 | //! > Rebase-and-merge deferred. |
| 5 | //! |
| 6 | //! Two outcomes, chosen by the graph rather than by the caller: |
| 7 | //! |
| 8 | //! * The bookmark is an ancestor of the change → **fast-forward**. No commit is |
| 9 | //! written; the ref moves. This is the case that matters for jj users, whose |
| 10 | //! changes are usually already on top of the target. |
| 11 | //! * Otherwise → a three-way **merge commit**, with the bookmark as first parent |
| 12 | //! and the change as second, exactly as `git merge --no-ff` would order them. |
| 13 | //! |
| 14 | //! A textual conflict refuses the merge rather than writing conflict markers |
| 15 | //! into the target. Spec §4 is explicit that conflict resolution happens in the |
| 16 | //! user's working copy, and a server that resolves by writing `<<<<<<<` into |
| 17 | //! `main` would be doing the opposite of that. |
| 18 | //! |
| 19 | //! The ref update is a **compare-and-swap** against the bookmark's expected |
| 20 | //! value. Two merges racing on one bookmark must not silently lose one of them, |
| 21 | //! and the pre-receive hook is not involved here because this write does not go |
| 22 | //! through the Git wire protocol. |
| 23 | |
| 24 | use gix::refs::transaction::{Change, PreviousValue, RefEdit}; |
| 25 | use gix::refs::{FullName, Target}; |
| 26 | |
| 27 | use crate::{MergeOutcome, Result, RevId, Signature, StoreError}; |
| 28 | |
| 29 | /// Merge `rev` into `bookmark`. |
| 30 | pub fn merge( |
| 31 | repo: &gix::Repository, |
| 32 | bookmark: &str, |
| 33 | rev: &RevId, |
| 34 | message: &str, |
| 35 | author: &Signature, |
| 36 | ) -> Result<MergeOutcome> { |
| 37 | let full = format!("refs/heads/{bookmark}"); |
| 38 | let name: FullName = full |
| 39 | .as_str() |
| 40 | .try_into() |
| 41 | .map_err(|_| StoreError::Path(crate::path::PathError::Control))?; |
| 42 | |
| 43 | let target_ref = repo |
| 44 | .find_reference(&full) |
| 45 | .map_err(|_| StoreError::NoSuchRevision)?; |
| 46 | let target_id = target_ref |
| 47 | .try_id() |
| 48 | .ok_or_else(|| StoreError::Other(anyhow::anyhow!("{bookmark} is a symbolic reference")))? |
| 49 | .detach(); |
| 50 | |
| 51 | let source_id = super::convert::parse_oid(repo, rev)?; |
| 52 | |
| 53 | // Already landed. Reported rather than treated as an error: two people |
| 54 | // clicking merge is a race, not a mistake, and the second one should be told |
| 55 | // it is done, not shown a failure. |
| 56 | if target_id == source_id |
| 57 | || is_ancestor(repo, source_id, target_id)? |
| 58 | { |
| 59 | return Ok(MergeOutcome::AlreadyMerged); |
| 60 | } |
| 61 | |
| 62 | // ── fast-forward ───────────────────────────────────────────────────────── |
| 63 | if is_ancestor(repo, target_id, source_id)? { |
| 64 | set_ref(repo, name, target_id, source_id, "merge: fast-forward")?; |
| 65 | return Ok(MergeOutcome::FastForward { |
| 66 | new_target: RevId::from_stored(source_id.to_string()), |
| 67 | }); |
| 68 | } |
| 69 | |
| 70 | // ── three-way merge ────────────────────────────────────────────────────── |
| 71 | let ours = tree_of(repo, target_id)?; |
| 72 | let theirs = tree_of(repo, source_id)?; |
| 73 | |
| 74 | // No merge base means unrelated histories. Git needs a flag for that case |
| 75 | // and so do we — silently merging two unrelated trees is how a repository |
| 76 | // ends up with somebody else's files in it. |
| 77 | let base = repo |
| 78 | .merge_base(target_id, source_id) |
| 79 | .map_err(|_| StoreError::Other(anyhow::anyhow!("no common ancestor")))? |
| 80 | .detach(); |
| 81 | let base_tree = tree_of(repo, base)?; |
| 82 | |
| 83 | let options = repo |
| 84 | .tree_merge_options() |
| 85 | .map_err(|e| StoreError::Other(anyhow::anyhow!("merge options: {e}")))?; |
| 86 | |
| 87 | let labels = gix::merge::blob::builtin_driver::text::Labels { |
| 88 | ancestor: Some("base".into()), |
| 89 | current: Some(bookmark.into()), |
| 90 | other: Some("change".into()), |
| 91 | }; |
| 92 | |
| 93 | let mut outcome = repo |
| 94 | .merge_trees(base_tree, ours, theirs, labels, options) |
| 95 | .map_err(|e| StoreError::Other(anyhow::anyhow!("merging trees: {e}")))?; |
| 96 | |
| 97 | // `ConflictMarkers` is the strictest reading: anything that would have |
| 98 | // needed a marker written into the file counts as unresolved. A rename |
| 99 | // conflict that gix can auto-resolve is allowed through, exactly as a |
| 100 | // client-side `git merge` would. |
| 101 | if outcome.has_unresolved_conflicts(gix::merge::tree::TreatAsUnresolved::ConflictMarkers) { |
| 102 | return Ok(MergeOutcome::Conflicted); |
| 103 | } |
| 104 | |
| 105 | let merged_tree = outcome |
| 106 | .tree |
| 107 | .write() |
| 108 | .map_err(|e| StoreError::Other(anyhow::anyhow!("writing merged tree: {e}")))? |
| 109 | .detach(); |
| 110 | |
| 111 | let sig = gix::actor::Signature { |
| 112 | name: author.name.clone().into(), |
| 113 | email: author.email.clone().into(), |
| 114 | time: gix::date::Time::new(author.when.timestamp(), 0), |
| 115 | }; |
| 116 | |
| 117 | // `commit_as` writes the commit *and* moves the reference in one call, with |
| 118 | // the reference update constrained to the value we read above. |
| 119 | let sig_ref = sig.to_ref(); |
| 120 | let new_commit = repo |
| 121 | .commit_as( |
| 122 | sig_ref, |
| 123 | sig_ref, |
| 124 | name.as_ref().as_bstr(), |
| 125 | message, |
| 126 | merged_tree, |
| 127 | // Bookmark first, change second — the ordering `git log --first-parent` |
| 128 | // relies on to follow the target branch's own history. |
| 129 | [target_id, source_id], |
| 130 | ) |
| 131 | .map_err(|e| StoreError::Other(anyhow::anyhow!("writing merge commit: {e}")))? |
| 132 | .detach(); |
| 133 | |
| 134 | Ok(MergeOutcome::Merged { |
| 135 | new_target: RevId::from_stored(new_commit.to_string()), |
| 136 | }) |
| 137 | } |
| 138 | |
| 139 | fn tree_of(repo: &gix::Repository, commit: gix::ObjectId) -> Result<gix::ObjectId> { |
| 140 | Ok(repo |
| 141 | .find_object(commit) |
| 142 | .map_err(|_| StoreError::NoSuchRevision)? |
| 143 | .try_into_commit() |
| 144 | .map_err(|_| StoreError::NoSuchRevision)? |
| 145 | .tree_id() |
| 146 | .map_err(|e| StoreError::Other(anyhow::anyhow!("reading tree: {e}")))? |
| 147 | .detach()) |
| 148 | } |
| 149 | |
| 150 | fn is_ancestor( |
| 151 | repo: &gix::Repository, |
| 152 | ancestor: gix::ObjectId, |
| 153 | descendant: gix::ObjectId, |
| 154 | ) -> Result<bool> { |
| 155 | if ancestor == descendant { |
| 156 | return Ok(true); |
| 157 | } |
| 158 | // `merge_base(a, b) == a` is the definition of "a is an ancestor of b". |
| 159 | Ok(repo |
| 160 | .merge_base(ancestor, descendant) |
| 161 | .map(|b| b.detach() == ancestor) |
| 162 | .unwrap_or(false)) |
| 163 | } |
| 164 | |
| 165 | /// Move a reference, refusing if it has changed since we read it. |
| 166 | fn set_ref( |
| 167 | repo: &gix::Repository, |
| 168 | name: FullName, |
| 169 | expected: gix::ObjectId, |
| 170 | new: gix::ObjectId, |
| 171 | reason: &str, |
| 172 | ) -> Result<()> { |
| 173 | repo.edit_reference(RefEdit { |
| 174 | change: Change::Update { |
| 175 | log: gix::refs::transaction::LogChange { |
| 176 | mode: gix::refs::transaction::RefLog::AndReference, |
| 177 | force_create_reflog: false, |
| 178 | message: reason.into(), |
| 179 | }, |
| 180 | // The compare-and-swap. If another push moved the bookmark between |
| 181 | // the read and this write, the transaction fails and the caller is |
| 182 | // told to retry rather than silently discarding that push. |
| 183 | expected: PreviousValue::MustExistAndMatch(Target::Object(expected)), |
| 184 | new: Target::Object(new), |
| 185 | }, |
| 186 | name, |
| 187 | deref: false, |
| 188 | }) |
| 189 | .map_err(|e| { |
| 190 | StoreError::Other(anyhow::anyhow!( |
| 191 | "the bookmark moved while merging; nothing was changed ({e})" |
| 192 | )) |
| 193 | })?; |
| 194 | Ok(()) |
| 195 | } |
195 lines · Rust