jj_lib/
op_store.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::any::Any;
18use std::collections::BTreeMap;
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::fmt::Debug;
22use std::iter;
23use std::sync::LazyLock;
24use std::time::SystemTime;
25
26use async_trait::async_trait;
27use itertools::Itertools as _;
28use thiserror::Error;
29
30use crate::backend::CommitId;
31use crate::backend::MillisSinceEpoch;
32use crate::backend::Timestamp;
33use crate::content_hash::ContentHash;
34use crate::merge::Merge;
35use crate::object_id::HexPrefix;
36use crate::object_id::ObjectId as _;
37use crate::object_id::PrefixResolution;
38use crate::object_id::id_type;
39use crate::ref_name::GitRefNameBuf;
40use crate::ref_name::RefName;
41use crate::ref_name::RefNameBuf;
42use crate::ref_name::RemoteName;
43use crate::ref_name::RemoteNameBuf;
44use crate::ref_name::RemoteRefSymbol;
45use crate::ref_name::WorkspaceNameBuf;
46
47id_type!(pub ViewId { hex() });
48id_type!(pub OperationId { hex() });
49
50#[derive(ContentHash, PartialEq, Eq, Hash, Clone, Debug, serde::Serialize)]
51#[serde(transparent)]
52pub struct RefTarget {
53    merge: Merge<Option<CommitId>>,
54}
55
56impl Default for RefTarget {
57    fn default() -> Self {
58        Self::absent()
59    }
60}
61
62impl RefTarget {
63    /// Creates non-conflicting target pointing to no commit.
64    pub fn absent() -> Self {
65        Self::from_merge(Merge::absent())
66    }
67
68    /// Returns non-conflicting target pointing to no commit.
69    ///
70    /// This will typically be used in place of `None` returned by map lookup.
71    pub fn absent_ref() -> &'static Self {
72        static TARGET: LazyLock<RefTarget> = LazyLock::new(RefTarget::absent);
73        &TARGET
74    }
75
76    /// Creates non-conflicting target that optionally points to a commit.
77    pub fn resolved(maybe_id: Option<CommitId>) -> Self {
78        Self::from_merge(Merge::resolved(maybe_id))
79    }
80
81    /// Creates non-conflicting target pointing to a commit.
82    pub fn normal(id: CommitId) -> Self {
83        Self::from_merge(Merge::normal(id))
84    }
85
86    /// Creates target from removed/added ids.
87    pub fn from_legacy_form(
88        removed_ids: impl IntoIterator<Item = CommitId>,
89        added_ids: impl IntoIterator<Item = CommitId>,
90    ) -> Self {
91        Self::from_merge(Merge::from_legacy_form(removed_ids, added_ids))
92    }
93
94    pub fn from_merge(merge: Merge<Option<CommitId>>) -> Self {
95        Self { merge }
96    }
97
98    /// Returns the underlying value if this target is non-conflicting.
99    pub fn as_resolved(&self) -> Option<&Option<CommitId>> {
100        self.merge.as_resolved()
101    }
102
103    /// Returns id if this target is non-conflicting and points to a commit.
104    pub fn as_normal(&self) -> Option<&CommitId> {
105        self.merge.as_normal()
106    }
107
108    /// Returns true if this target points to no commit.
109    pub fn is_absent(&self) -> bool {
110        self.merge.is_absent()
111    }
112
113    /// Returns true if this target points to any commit. Conflicting target is
114    /// always "present" as it should have at least one commit id.
115    pub fn is_present(&self) -> bool {
116        self.merge.is_present()
117    }
118
119    /// Whether this target has conflicts.
120    pub fn has_conflict(&self) -> bool {
121        !self.merge.is_resolved()
122    }
123
124    pub fn removed_ids(&self) -> impl Iterator<Item = &CommitId> {
125        self.merge.removes().flatten()
126    }
127
128    pub fn added_ids(&self) -> impl Iterator<Item = &CommitId> {
129        self.merge.adds().flatten()
130    }
131
132    pub fn as_merge(&self) -> &Merge<Option<CommitId>> {
133        &self.merge
134    }
135}
136
137/// Remote bookmark or tag.
138#[derive(ContentHash, Clone, Debug, Eq, Hash, PartialEq)]
139pub struct RemoteRef {
140    pub target: RefTarget,
141    pub state: RemoteRefState,
142}
143
144impl RemoteRef {
145    /// Creates remote ref pointing to no commit.
146    pub fn absent() -> Self {
147        Self {
148            target: RefTarget::absent(),
149            state: RemoteRefState::New,
150        }
151    }
152
153    /// Returns remote ref pointing to no commit.
154    ///
155    /// This will typically be used in place of `None` returned by map lookup.
156    pub fn absent_ref() -> &'static Self {
157        static TARGET: LazyLock<RemoteRef> = LazyLock::new(RemoteRef::absent);
158        &TARGET
159    }
160
161    /// Returns true if the target points to no commit.
162    pub fn is_absent(&self) -> bool {
163        self.target.is_absent()
164    }
165
166    /// Returns true if the target points to any commit.
167    pub fn is_present(&self) -> bool {
168        self.target.is_present()
169    }
170
171    /// Returns true if the ref is supposed to be merged in to the local ref.
172    pub fn is_tracked(&self) -> bool {
173        self.state == RemoteRefState::Tracked
174    }
175
176    /// Target that should have been merged in to the local ref.
177    ///
178    /// Use this as the base or known target when merging new remote ref in to
179    /// local or pushing local ref to remote.
180    pub fn tracked_target(&self) -> &RefTarget {
181        if self.is_tracked() {
182            &self.target
183        } else {
184            RefTarget::absent_ref()
185        }
186    }
187}
188
189/// Whether the ref is tracked or not.
190#[derive(ContentHash, Clone, Copy, Debug, Eq, Hash, PartialEq)]
191pub enum RemoteRefState {
192    /// Remote ref is not merged in to the local ref.
193    New,
194    /// Remote ref has been merged in to the local ref. Incoming ref will be
195    /// merged, too.
196    Tracked,
197}
198
199/// Helper to strip redundant `Option<T>` from `RefTarget` lookup result.
200pub trait RefTargetOptionExt {
201    type Value;
202
203    fn flatten(self) -> Self::Value;
204}
205
206impl RefTargetOptionExt for Option<RefTarget> {
207    type Value = RefTarget;
208
209    fn flatten(self) -> Self::Value {
210        self.unwrap_or_else(RefTarget::absent)
211    }
212}
213
214impl<'a> RefTargetOptionExt for Option<&'a RefTarget> {
215    type Value = &'a RefTarget;
216
217    fn flatten(self) -> Self::Value {
218        self.unwrap_or_else(|| RefTarget::absent_ref())
219    }
220}
221
222impl RefTargetOptionExt for Option<RemoteRef> {
223    type Value = RemoteRef;
224
225    fn flatten(self) -> Self::Value {
226        self.unwrap_or_else(RemoteRef::absent)
227    }
228}
229
230impl<'a> RefTargetOptionExt for Option<&'a RemoteRef> {
231    type Value = &'a RemoteRef;
232
233    fn flatten(self) -> Self::Value {
234        self.unwrap_or_else(|| RemoteRef::absent_ref())
235    }
236}
237
238/// Local and remote refs of the same name.
239#[derive(PartialEq, Eq, Clone, Debug)]
240pub struct LocalRemoteRefTarget<'a> {
241    /// The commit the ref points to locally.
242    pub local_target: &'a RefTarget,
243    /// `(remote_name, remote_ref)` pairs in lexicographical order.
244    pub remote_refs: Vec<(&'a RemoteName, &'a RemoteRef)>,
245}
246
247/// Represents the way the repo looks at a given time, just like how a Tree
248/// object represents how the file system looks at a given time.
249#[derive(ContentHash, PartialEq, Eq, Clone, Debug)]
250pub struct View {
251    /// All head commits. There should be at least one head commit.
252    pub head_ids: HashSet<CommitId>,
253    pub local_bookmarks: BTreeMap<RefNameBuf, RefTarget>,
254    pub local_tags: BTreeMap<RefNameBuf, RefTarget>,
255    pub remote_views: BTreeMap<RemoteNameBuf, RemoteView>,
256    pub git_refs: BTreeMap<GitRefNameBuf, RefTarget>,
257    /// The commit the Git HEAD points to.
258    // TODO: Support multiple Git worktrees?
259    // TODO: Do we want to store the current bookmark name too?
260    pub git_head: RefTarget,
261    // The commit that *should be* checked out in the workspace. Note that the working copy
262    // (.jj/working_copy/) has the source of truth about which commit *is* checked out (to be
263    // precise: the commit to which we most recently completed an update to).
264    pub wc_commit_ids: BTreeMap<WorkspaceNameBuf, CommitId>,
265}
266
267impl View {
268    /// Creates new (mostly empty) view containing the given commit as the head.
269    pub fn make_root(root_commit_id: CommitId) -> Self {
270        Self {
271            head_ids: HashSet::from([root_commit_id]),
272            local_bookmarks: BTreeMap::new(),
273            local_tags: BTreeMap::new(),
274            remote_views: BTreeMap::new(),
275            git_refs: BTreeMap::new(),
276            git_head: RefTarget::absent(),
277            wc_commit_ids: BTreeMap::new(),
278        }
279    }
280}
281
282/// Represents the state of the remote repo.
283#[derive(ContentHash, Clone, Debug, Default, Eq, PartialEq)]
284pub struct RemoteView {
285    // TODO: Do we need to support tombstones for remote bookmarks? For example, if the bookmark
286    // has been deleted locally and you pull from a remote, maybe it should make a difference
287    // whether the bookmark is known to have existed on the remote. We may not want to resurrect
288    // the bookmark if the bookmark's state on the remote was just not known.
289    pub bookmarks: BTreeMap<RefNameBuf, RemoteRef>,
290    pub tags: BTreeMap<RefNameBuf, RemoteRef>,
291}
292
293/// Iterates pair of local and remote refs by name.
294pub(crate) fn merge_join_ref_views<'a>(
295    local_refs: &'a BTreeMap<RefNameBuf, RefTarget>,
296    remote_views: &'a BTreeMap<RemoteNameBuf, RemoteView>,
297    get_remote_refs: impl FnMut(&RemoteView) -> &BTreeMap<RefNameBuf, RemoteRef>,
298) -> impl Iterator<Item = (&'a RefName, LocalRemoteRefTarget<'a>)> {
299    let mut local_refs_iter = local_refs
300        .iter()
301        .map(|(name, target)| (&**name, target))
302        .peekable();
303    let mut remote_refs_iter = flatten_remote_refs(remote_views, get_remote_refs).peekable();
304
305    iter::from_fn(move || {
306        // Pick earlier bookmark name
307        let (name, local_target) = if let Some((symbol, _)) = remote_refs_iter.peek() {
308            local_refs_iter
309                .next_if(|&(local_name, _)| local_name <= symbol.name)
310                .unwrap_or((symbol.name, RefTarget::absent_ref()))
311        } else {
312            local_refs_iter.next()?
313        };
314        let remote_refs = remote_refs_iter
315            .peeking_take_while(|(symbol, _)| symbol.name == name)
316            .map(|(symbol, remote_ref)| (symbol.remote, remote_ref))
317            .collect();
318        let local_remote_target = LocalRemoteRefTarget {
319            local_target,
320            remote_refs,
321        };
322        Some((name, local_remote_target))
323    })
324}
325
326/// Iterates `(symbol, remote_ref)`s in lexicographical order.
327pub(crate) fn flatten_remote_refs(
328    remote_views: &BTreeMap<RemoteNameBuf, RemoteView>,
329    mut get_remote_refs: impl FnMut(&RemoteView) -> &BTreeMap<RefNameBuf, RemoteRef>,
330) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
331    remote_views
332        .iter()
333        .map(|(remote, remote_view)| {
334            get_remote_refs(remote_view)
335                .iter()
336                .map(move |(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
337        })
338        .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
339}
340
341#[derive(Clone, ContentHash, Debug, Eq, PartialEq, serde::Serialize)]
342pub struct TimestampRange {
343    // Could be aliased to Range<Timestamp> if needed.
344    pub start: Timestamp,
345    pub end: Timestamp,
346}
347
348/// Represents an operation (transaction) on the repo view, just like how a
349/// Commit object represents an operation on the tree.
350///
351/// Operations and views are not meant to be exchanged between repos or users;
352/// they represent local state and history.
353///
354/// The operation history will almost always be linear. It will only have
355/// forks when parallel operations occurred. The parent is determined when
356/// the transaction starts. When the transaction commits, a lock will be
357/// taken and it will be checked that the current head of the operation
358/// graph is unchanged. If the current head has changed, there has been
359/// concurrent operation.
360#[derive(ContentHash, PartialEq, Eq, Clone, Debug, serde::Serialize)]
361pub struct Operation {
362    #[serde(skip)] // TODO: should be exposed?
363    pub view_id: ViewId,
364    pub parents: Vec<OperationId>,
365    #[serde(flatten)]
366    pub metadata: OperationMetadata,
367    /// Mapping from new commit to its predecessors, or `None` if predecessors
368    /// weren't recorded when the operation was written.
369    ///
370    /// * `commit_id: []` if the commit was newly created.
371    /// * `commit_id: [predecessor_id, ..]` if the commit was rewritten.
372    ///
373    /// This mapping preserves all transitive predecessors if a commit was
374    /// rewritten multiple times within the same transaction. For example, if
375    /// `X` was rewritten as `Y`, then rebased as `Z`, these modifications are
376    /// recorded as `{Y: [X], Z: [Y]}`.
377    ///
378    /// Existing commits (including commits imported from Git) aren't tracked
379    /// even if they became visible at this operation.
380    // BTreeMap for ease of deterministic serialization. If the deserialization
381    // cost matters, maybe this can be changed to sorted Vec.
382    #[serde(skip)] // TODO: should be exposed?
383    pub commit_predecessors: Option<BTreeMap<CommitId, Vec<CommitId>>>,
384}
385
386impl Operation {
387    pub fn make_root(root_view_id: ViewId) -> Self {
388        let timestamp = Timestamp {
389            timestamp: MillisSinceEpoch(0),
390            tz_offset: 0,
391        };
392        let metadata = OperationMetadata {
393            time: TimestampRange {
394                start: timestamp,
395                end: timestamp,
396            },
397            description: "".to_string(),
398            hostname: "".to_string(),
399            username: "".to_string(),
400            is_snapshot: false,
401            tags: HashMap::new(),
402        };
403        Self {
404            view_id: root_view_id,
405            parents: vec![],
406            metadata,
407            // The root operation is guaranteed to have no new commits. The root
408            // commit could be considered born at the root operation, but there
409            // may be other commits created within the abandoned operations.
410            // They don't have any predecessors records as well.
411            commit_predecessors: Some(BTreeMap::new()),
412        }
413    }
414}
415
416#[derive(ContentHash, PartialEq, Eq, Clone, Debug, serde::Serialize)]
417pub struct OperationMetadata {
418    pub time: TimestampRange,
419    // Whatever is useful to the user, such as exact command line call
420    pub description: String,
421    pub hostname: String,
422    pub username: String,
423    /// Whether this operation represents a pure snapshotting of the working
424    /// copy.
425    pub is_snapshot: bool,
426    pub tags: HashMap<String, String>,
427}
428
429/// Data to be loaded into the root operation/view.
430#[derive(Clone, Debug)]
431pub struct RootOperationData {
432    /// The root commit ID, which should exist in the root view.
433    pub root_commit_id: CommitId,
434}
435
436#[derive(Debug, Error)]
437pub enum OpStoreError {
438    #[error("Object {hash} of type {object_type} not found")]
439    ObjectNotFound {
440        object_type: String,
441        hash: String,
442        source: Box<dyn std::error::Error + Send + Sync>,
443    },
444    #[error("Error when reading object {hash} of type {object_type}")]
445    ReadObject {
446        object_type: String,
447        hash: String,
448        source: Box<dyn std::error::Error + Send + Sync>,
449    },
450    #[error("Could not write object of type {object_type}")]
451    WriteObject {
452        object_type: &'static str,
453        source: Box<dyn std::error::Error + Send + Sync>,
454    },
455    #[error(transparent)]
456    Other(Box<dyn std::error::Error + Send + Sync>),
457}
458
459pub type OpStoreResult<T> = Result<T, OpStoreError>;
460
461#[async_trait]
462pub trait OpStore: Any + Send + Sync + Debug {
463    fn name(&self) -> &str;
464
465    fn root_operation_id(&self) -> &OperationId;
466
467    async fn read_view(&self, id: &ViewId) -> OpStoreResult<View>;
468
469    async fn write_view(&self, contents: &View) -> OpStoreResult<ViewId>;
470
471    async fn read_operation(&self, id: &OperationId) -> OpStoreResult<Operation>;
472
473    async fn write_operation(&self, contents: &Operation) -> OpStoreResult<OperationId>;
474
475    /// Resolves an unambiguous operation ID prefix.
476    async fn resolve_operation_id_prefix(
477        &self,
478        prefix: &HexPrefix,
479    ) -> OpStoreResult<PrefixResolution<OperationId>>;
480
481    /// Prunes unreachable operations and views.
482    ///
483    /// All operations and views reachable from the `head_ids` won't be
484    /// removed. In addition to that, objects created after `keep_newer` will be
485    /// preserved. This mitigates a risk of deleting new heads created
486    /// concurrently by another process.
487    // TODO: return stats?
488    fn gc(&self, head_ids: &[OperationId], keep_newer: SystemTime) -> OpStoreResult<()>;
489}
490
491impl dyn OpStore {
492    /// Returns reference of the implementation type.
493    pub fn downcast_ref<T: OpStore>(&self) -> Option<&T> {
494        (self as &dyn Any).downcast_ref()
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use maplit::btreemap;
501
502    use super::*;
503
504    #[test]
505    fn test_merge_join_bookmark_views() {
506        let remote_ref = |target: &RefTarget| RemoteRef {
507            target: target.clone(),
508            state: RemoteRefState::Tracked, // doesn't matter
509        };
510        let local_bookmark1_target = RefTarget::normal(CommitId::from_hex("111111"));
511        let local_bookmark2_target = RefTarget::normal(CommitId::from_hex("222222"));
512        let git_bookmark1_remote_ref = remote_ref(&RefTarget::normal(CommitId::from_hex("333333")));
513        let git_bookmark2_remote_ref = remote_ref(&RefTarget::normal(CommitId::from_hex("444444")));
514        let remote1_bookmark1_remote_ref =
515            remote_ref(&RefTarget::normal(CommitId::from_hex("555555")));
516        let remote2_bookmark2_remote_ref =
517            remote_ref(&RefTarget::normal(CommitId::from_hex("666666")));
518
519        let local_bookmarks = btreemap! {
520            "bookmark1".into() => local_bookmark1_target.clone(),
521            "bookmark2".into() => local_bookmark2_target.clone(),
522        };
523        let remote_views = btreemap! {
524            "git".into() => RemoteView {
525                bookmarks: btreemap! {
526                    "bookmark1".into() => git_bookmark1_remote_ref.clone(),
527                    "bookmark2".into() => git_bookmark2_remote_ref.clone(),
528                },
529                tags: btreemap! {},
530            },
531            "remote1".into() => RemoteView {
532                bookmarks: btreemap! {
533                    "bookmark1".into() => remote1_bookmark1_remote_ref.clone(),
534                },
535                tags: btreemap! {},
536            },
537            "remote2".into() => RemoteView {
538                bookmarks: btreemap! {
539                    "bookmark2".into() => remote2_bookmark2_remote_ref.clone(),
540                },
541                tags: btreemap! {},
542            },
543        };
544        assert_eq!(
545            merge_join_ref_views(&local_bookmarks, &remote_views, |view| &view.bookmarks)
546                .collect_vec(),
547            vec![
548                (
549                    "bookmark1".as_ref(),
550                    LocalRemoteRefTarget {
551                        local_target: &local_bookmark1_target,
552                        remote_refs: vec![
553                            ("git".as_ref(), &git_bookmark1_remote_ref),
554                            ("remote1".as_ref(), &remote1_bookmark1_remote_ref),
555                        ],
556                    },
557                ),
558                (
559                    "bookmark2".as_ref(),
560                    LocalRemoteRefTarget {
561                        local_target: &local_bookmark2_target.clone(),
562                        remote_refs: vec![
563                            ("git".as_ref(), &git_bookmark2_remote_ref),
564                            ("remote2".as_ref(), &remote2_bookmark2_remote_ref),
565                        ],
566                    },
567                ),
568            ],
569        );
570
571        // Local only
572        let local_bookmarks = btreemap! {
573            "bookmark1".into() => local_bookmark1_target.clone(),
574        };
575        let remote_views = btreemap! {};
576        assert_eq!(
577            merge_join_ref_views(&local_bookmarks, &remote_views, |view| &view.bookmarks)
578                .collect_vec(),
579            vec![(
580                "bookmark1".as_ref(),
581                LocalRemoteRefTarget {
582                    local_target: &local_bookmark1_target,
583                    remote_refs: vec![]
584                },
585            )],
586        );
587
588        // Remote only
589        let local_bookmarks = btreemap! {};
590        let remote_views = btreemap! {
591            "remote1".into() => RemoteView {
592                bookmarks: btreemap! {
593                    "bookmark1".into() => remote1_bookmark1_remote_ref.clone(),
594                },
595                tags: btreemap! {},
596            },
597        };
598        assert_eq!(
599            merge_join_ref_views(&local_bookmarks, &remote_views, |view| &view.bookmarks)
600                .collect_vec(),
601            vec![(
602                "bookmark1".as_ref(),
603                LocalRemoteRefTarget {
604                    local_target: RefTarget::absent_ref(),
605                    remote_refs: vec![("remote1".as_ref(), &remote1_bookmark1_remote_ref)],
606                },
607            )],
608        );
609    }
610}