Skip to main content

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