Skip to main content

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