Skip to main content

jj_lib/
commit_builder.rs

1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![expect(missing_docs)]
16
17use std::sync::Arc;
18
19use pollster::FutureExt as _;
20
21use crate::backend;
22use crate::backend::BackendError;
23use crate::backend::BackendResult;
24use crate::backend::ChangeId;
25use crate::backend::CommitId;
26use crate::backend::Signature;
27use crate::backend::TreeId;
28use crate::commit::Commit;
29use crate::commit::is_backend_commit_empty;
30use crate::conflict_labels::ConflictLabels;
31use crate::merge::Merge;
32use crate::merged_tree::MergedTree;
33use crate::repo::MutableRepo;
34use crate::repo::Repo;
35use crate::settings::JJRng;
36use crate::settings::SignBehavior;
37use crate::settings::SignSettings;
38use crate::settings::UserSettings;
39use crate::store::Store;
40
41#[must_use]
42pub struct CommitBuilder<'repo> {
43    mut_repo: &'repo mut MutableRepo,
44    inner: DetachedCommitBuilder,
45}
46
47impl CommitBuilder<'_> {
48    /// Detaches from `&'repo mut` lifetime. The returned builder can be used in
49    /// order to obtain a temporary commit object.
50    pub fn detach(self) -> DetachedCommitBuilder {
51        self.inner
52    }
53
54    /// Clears the source commit to not record new commit as rewritten from it.
55    ///
56    /// The caller should also assign new change id to not create divergence.
57    pub fn clear_rewrite_source(mut self) -> Self {
58        self.inner.clear_rewrite_source();
59        self
60    }
61
62    pub fn parents(&self) -> &[CommitId] {
63        self.inner.parents()
64    }
65
66    pub fn set_parents(mut self, parents: Vec<CommitId>) -> Self {
67        self.inner.set_parents(parents);
68        self
69    }
70
71    pub fn predecessors(&self) -> &[CommitId] {
72        self.inner.predecessors()
73    }
74
75    pub fn set_predecessors(mut self, predecessors: Vec<CommitId>) -> Self {
76        self.inner.set_predecessors(predecessors);
77        self
78    }
79
80    pub fn tree(&self) -> MergedTree {
81        self.inner.tree()
82    }
83
84    pub fn tree_ids(&self) -> &Merge<TreeId> {
85        self.inner.tree_ids()
86    }
87
88    pub fn set_tree(mut self, tree: MergedTree) -> Self {
89        self.inner.set_tree(tree);
90        self
91    }
92
93    /// [`Commit::is_empty()`] for the new commit.
94    pub async fn is_empty(&self) -> BackendResult<bool> {
95        self.inner.is_empty(self.mut_repo).await
96    }
97
98    pub fn change_id(&self) -> &ChangeId {
99        self.inner.change_id()
100    }
101
102    pub fn set_change_id(mut self, change_id: ChangeId) -> Self {
103        self.inner.set_change_id(change_id);
104        self
105    }
106
107    pub fn generate_new_change_id(mut self) -> Self {
108        self.inner.generate_new_change_id();
109        self
110    }
111
112    pub fn description(&self) -> &str {
113        self.inner.description()
114    }
115
116    pub fn set_description(mut self, description: impl Into<String>) -> Self {
117        self.inner.set_description(description);
118        self
119    }
120
121    pub fn author(&self) -> &Signature {
122        self.inner.author()
123    }
124
125    pub fn set_author(mut self, author: Signature) -> Self {
126        self.inner.set_author(author);
127        self
128    }
129
130    pub fn committer(&self) -> &Signature {
131        self.inner.committer()
132    }
133
134    pub fn set_committer(mut self, committer: Signature) -> Self {
135        self.inner.set_committer(committer);
136        self
137    }
138
139    /// [`Commit::is_discardable()`] for the new commit.
140    pub async fn is_discardable(&self) -> BackendResult<bool> {
141        self.inner.is_discardable(self.mut_repo).await
142    }
143
144    pub fn sign_settings(&self) -> &SignSettings {
145        self.inner.sign_settings()
146    }
147
148    pub fn set_sign_behavior(mut self, sign_behavior: SignBehavior) -> Self {
149        self.inner.set_sign_behavior(sign_behavior);
150        self
151    }
152
153    pub fn set_sign_key_override(mut self, sign_key: String) -> Self {
154        self.inner.set_sign_key_override(sign_key);
155        self
156    }
157
158    pub fn clear_sign_key_override(mut self) -> Self {
159        self.inner.clear_sign_key_override();
160        self
161    }
162
163    pub async fn write(self) -> BackendResult<Commit> {
164        self.inner.write(self.mut_repo).await
165    }
166
167    /// Records the old commit as abandoned instead of writing new commit. This
168    /// is noop for the builder created by [`MutableRepo::new_commit()`].
169    pub fn abandon(self) {
170        self.inner.abandon(self.mut_repo);
171    }
172}
173
174/// Like `CommitBuilder`, but doesn't mutably borrow `MutableRepo`.
175#[derive(Debug)]
176pub struct DetachedCommitBuilder {
177    store: Arc<Store>,
178    rng: Arc<JJRng>,
179    commit: backend::Commit,
180    predecessors: Vec<CommitId>,
181    rewrite_source: Option<Commit>,
182    sign_key_override: Option<String>,
183    sign_settings: SignSettings,
184    record_predecessors_in_commit: bool,
185}
186
187impl DetachedCommitBuilder {
188    /// Only called from [`MutableRepo::new_commit`]. Use that function instead.
189    pub(crate) fn for_new_commit(
190        repo: &dyn Repo,
191        settings: &UserSettings,
192        parents: Vec<CommitId>,
193        tree: MergedTree,
194    ) -> Self {
195        let store = repo.store().clone();
196        let signature = settings.signature();
197        assert!(!parents.is_empty());
198        let rng = settings.get_rng();
199        let (root_tree, conflict_labels) = tree.into_tree_ids_and_labels();
200        let change_id = rng.new_change_id(store.change_id_length());
201        let commit = backend::Commit {
202            parents,
203            predecessors: vec![],
204            root_tree,
205            conflict_labels: conflict_labels.into_merge(),
206            change_id,
207            description: String::new(),
208            author: signature.clone(),
209            committer: signature,
210            secure_sig: None,
211        };
212        let record_predecessors_in_commit = settings
213            .get_bool("experimental.record-predecessors-in-commit")
214            .unwrap();
215        Self {
216            store,
217            rng,
218            commit,
219            rewrite_source: None,
220            predecessors: vec![],
221            sign_key_override: None,
222            sign_settings: settings.sign_settings(),
223            record_predecessors_in_commit,
224        }
225    }
226
227    /// Only called from [`MutableRepo::rewrite_commit`]. Use that function
228    /// instead.
229    pub(crate) fn for_rewrite_from(
230        repo: &dyn Repo,
231        settings: &UserSettings,
232        predecessor: &Commit,
233    ) -> Self {
234        let store = repo.store().clone();
235        let mut commit = backend::Commit::clone(predecessor.store_commit());
236        commit.predecessors = vec![];
237        commit.committer = settings.signature();
238        // If the user had not configured a name and email before but now they have,
239        // update the author fields with the new information.
240        if commit.author.name.is_empty() {
241            commit.author.name.clone_from(&commit.committer.name);
242        }
243        if commit.author.email.is_empty() {
244            commit.author.email.clone_from(&commit.committer.email);
245        }
246
247        // Reset author timestamp on discardable commits if the author is the
248        // committer. While it's unlikely we'll have somebody else's commit
249        // with no description in our repo, we'd like to be extra safe.
250        if commit.author.name == commit.committer.name
251            && commit.author.email == commit.committer.email
252            && predecessor
253                .is_discardable(repo)
254                .block_on()
255                .unwrap_or_default()
256        {
257            commit.author.timestamp = commit.committer.timestamp;
258        }
259
260        let record_predecessors_in_commit = settings
261            .get_bool("experimental.record-predecessors-in-commit")
262            .unwrap();
263        Self {
264            store,
265            commit,
266            rng: settings.get_rng(),
267            rewrite_source: Some(predecessor.clone()),
268            predecessors: vec![predecessor.id().clone()],
269            sign_key_override: None,
270            sign_settings: settings.sign_settings(),
271            record_predecessors_in_commit,
272        }
273    }
274
275    /// Attaches the underlying `mut_repo`.
276    pub fn attach(self, mut_repo: &mut MutableRepo) -> CommitBuilder<'_> {
277        assert!(Arc::ptr_eq(&self.store, mut_repo.store()));
278        CommitBuilder {
279            mut_repo,
280            inner: self,
281        }
282    }
283
284    /// Clears the source commit to not record new commit as rewritten from it.
285    ///
286    /// The caller should also assign new change id to not create divergence.
287    pub fn clear_rewrite_source(&mut self) {
288        self.rewrite_source = None;
289    }
290
291    pub fn parents(&self) -> &[CommitId] {
292        &self.commit.parents
293    }
294
295    pub fn set_parents(&mut self, parents: Vec<CommitId>) -> &mut Self {
296        assert!(!parents.is_empty());
297        self.commit.parents = parents;
298        self
299    }
300
301    pub fn predecessors(&self) -> &[CommitId] {
302        &self.predecessors
303    }
304
305    pub fn set_predecessors(&mut self, predecessors: Vec<CommitId>) -> &mut Self {
306        self.predecessors = predecessors;
307        self
308    }
309
310    pub fn tree(&self) -> MergedTree {
311        MergedTree::new(
312            self.store.clone(),
313            self.commit.root_tree.clone(),
314            ConflictLabels::from_merge(self.commit.conflict_labels.clone()),
315        )
316    }
317
318    pub fn tree_ids(&self) -> &Merge<TreeId> {
319        &self.commit.root_tree
320    }
321
322    pub fn set_tree(&mut self, tree: MergedTree) -> &mut Self {
323        assert!(Arc::ptr_eq(tree.store(), &self.store));
324        let (root_tree, conflict_labels) = tree.into_tree_ids_and_labels();
325        self.commit.root_tree = root_tree;
326        self.commit.conflict_labels = conflict_labels.into_merge();
327        self
328    }
329
330    /// [`Commit::is_empty()`] for the new commit.
331    pub async fn is_empty(&self, repo: &dyn Repo) -> BackendResult<bool> {
332        is_backend_commit_empty(repo, &self.store, &self.commit).await
333    }
334
335    pub fn change_id(&self) -> &ChangeId {
336        &self.commit.change_id
337    }
338
339    pub fn set_change_id(&mut self, change_id: ChangeId) -> &mut Self {
340        self.commit.change_id = change_id;
341        self
342    }
343
344    pub fn generate_new_change_id(&mut self) -> &mut Self {
345        self.commit.change_id = self.rng.new_change_id(self.store.change_id_length());
346        self
347    }
348
349    pub fn description(&self) -> &str {
350        &self.commit.description
351    }
352
353    pub fn set_description(&mut self, description: impl Into<String>) -> &mut Self {
354        self.commit.description = description.into();
355        self
356    }
357
358    pub fn author(&self) -> &Signature {
359        &self.commit.author
360    }
361
362    pub fn set_author(&mut self, author: Signature) -> &mut Self {
363        self.commit.author = author;
364        self
365    }
366
367    pub fn committer(&self) -> &Signature {
368        &self.commit.committer
369    }
370
371    pub fn set_committer(&mut self, committer: Signature) -> &mut Self {
372        self.commit.committer = committer;
373        self
374    }
375
376    /// [`Commit::is_discardable()`] for the new commit.
377    pub async fn is_discardable(&self, repo: &dyn Repo) -> BackendResult<bool> {
378        Ok(self.description().is_empty() && self.is_empty(repo).await?)
379    }
380
381    pub fn sign_settings(&self) -> &SignSettings {
382        &self.sign_settings
383    }
384
385    pub fn set_sign_behavior(&mut self, sign_behavior: SignBehavior) -> &mut Self {
386        self.sign_settings.behavior = sign_behavior;
387        self
388    }
389
390    pub fn set_sign_key_override(&mut self, sign_key: String) -> &mut Self {
391        self.sign_key_override = Some(sign_key);
392        self
393    }
394
395    pub fn clear_sign_key_override(&mut self) -> &mut Self {
396        self.sign_key_override = None;
397        self
398    }
399
400    /// Writes new commit and makes it visible in the `mut_repo`.
401    pub async fn write(mut self, mut_repo: &mut MutableRepo) -> BackendResult<Commit> {
402        if self.record_predecessors_in_commit {
403            self.commit.predecessors = self.predecessors.clone();
404        }
405        let commit = write_to_store(
406            &self.store,
407            self.commit,
408            self.sign_key_override.as_deref(),
409            &self.sign_settings,
410        )
411        .await?;
412        // FIXME: Google's index.has_id() always returns true.
413        if mut_repo.is_backed_by_default_index()
414            && mut_repo
415                .index()
416                .has_id(commit.id())
417                .await
418                // TODO: indexing error shouldn't be a "BackendError"
419                .map_err(|err| BackendError::Other(err.into()))?
420        {
421            // Recording existing commit as new would create cycle in
422            // predecessors/parent mappings within the current transaction, and
423            // in predecessors graph globally.
424            return Err(BackendError::Other(
425                format!("Newly-created commit {id} already exists", id = commit.id()).into(),
426            ));
427        }
428        mut_repo.add_head(&commit).await?;
429        mut_repo.set_predecessors(commit.id().clone(), self.predecessors);
430        if let Some(rewrite_source) = self.rewrite_source {
431            mut_repo.set_rewritten_commit(rewrite_source.id().clone(), commit.id().clone());
432        }
433        Ok(commit)
434    }
435
436    /// Writes new commit without making it visible in the repo.
437    ///
438    /// This does not consume the builder, so you can reuse the current
439    /// configuration to create another commit later.
440    pub async fn write_hidden(&self) -> BackendResult<Commit> {
441        let mut commit = self.commit.clone();
442        if self.record_predecessors_in_commit {
443            commit.predecessors = self.predecessors.clone();
444        }
445        write_to_store(
446            &self.store,
447            commit,
448            self.sign_key_override.as_deref(),
449            &self.sign_settings,
450        )
451        .await
452    }
453
454    /// Records the old commit as abandoned in the `mut_repo`.
455    ///
456    /// This is noop if there's no old commit that would be rewritten to the new
457    /// commit by `write()`.
458    pub fn abandon(self, mut_repo: &mut MutableRepo) {
459        let commit = self.commit;
460        if let Some(rewrite_source) = &self.rewrite_source {
461            mut_repo
462                .record_abandoned_commit_with_parents(rewrite_source.id().clone(), commit.parents);
463        }
464    }
465}
466
467async fn write_to_store(
468    store: &Arc<Store>,
469    mut commit: backend::Commit,
470    sign_key: Option<&str>,
471    sign_settings: &SignSettings,
472) -> BackendResult<Commit> {
473    let should_sign = store.signer().can_sign() && sign_settings.should_sign(&commit);
474    let sign_fn = |data: &[u8]| store.signer().sign(data, sign_key);
475
476    // Commit backend doesn't use secure_sig for writing and enforces it with an
477    // assert, but sign_settings.should_sign check above will want to know
478    // if we're rewriting a signed commit
479    commit.secure_sig = None;
480
481    store
482        .write_commit(commit, should_sign.then_some(&mut &sign_fn))
483        .await
484}