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