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