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