Skip to main content

cli/client/
local_sync.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Local repository synchronization.
3//!
4//! Direct access to local repositories without network protocol overhead.
5
6use std::{collections::HashSet, path::Path};
7
8use anyhow::{Result, anyhow};
9use objects::object::{ChangeId, ContentHash};
10use repo::Repository;
11
12/// Synchronize objects from a local source repository to a target repository.
13pub struct LocalSync {
14    source: Repository,
15}
16
17impl LocalSync {
18    /// Open a local repository for synchronization.
19    pub fn open(path: &Path) -> Result<Self> {
20        let source = Repository::open(path)?;
21        Ok(Self { source })
22    }
23
24    /// Get the source repository.
25    pub fn source(&self) -> &Repository {
26        &self.source
27    }
28
29    /// List all threads in the source repository.
30    pub fn list_threads(&self) -> Result<Vec<(String, ChangeId)>> {
31        let mut threads = Vec::new();
32        for thread in self.source.refs().list_threads()? {
33            if let Some(state_id) = self.source.refs().get_thread(&thread)? {
34                threads.push((thread, state_id));
35            }
36        }
37        Ok(threads)
38    }
39
40    /// List all markers in the source repository.
41    pub fn list_markers(&self) -> Result<Vec<(String, ChangeId)>> {
42        let mut markers = Vec::new();
43        for marker in self.source.refs().list_markers()? {
44            if let Some(state_id) = self.source.refs().get_marker(&marker)? {
45                markers.push((marker, state_id));
46            }
47        }
48        Ok(markers)
49    }
50
51    /// Fetch a state and all its dependencies from source to target.
52    pub fn fetch_state(&self, target: &Repository, state_id: &ChangeId) -> Result<usize> {
53        let mut copied = 0;
54        let mut visited = HashSet::new();
55        self.copy_state_recursive(target, state_id, &mut visited, &mut copied, None)?;
56        Ok(copied)
57    }
58
59    /// Fetch a state with limited depth (shallow clone).
60    ///
61    /// Depth 1 means the target state and its immediate parents.
62    /// A depth of 0 should be treated by callers as "full history".
63    pub fn fetch_state_with_depth(
64        &self,
65        target: &Repository,
66        state_id: &ChangeId,
67        depth: u32,
68    ) -> Result<usize> {
69        let mut copied = 0;
70        let mut visited = HashSet::new();
71        self.copy_state_recursive(target, state_id, &mut visited, &mut copied, Some(depth))?;
72        Ok(copied)
73    }
74
75    fn copy_state_recursive(
76        &self,
77        target: &Repository,
78        state_id: &ChangeId,
79        visited: &mut HashSet<ChangeId>,
80        copied: &mut usize,
81        max_depth: Option<u32>,
82    ) -> Result<()> {
83        if visited.contains(state_id) {
84            return Ok(());
85        }
86        visited.insert(*state_id);
87
88        // Check if target already has this state
89        if target.store().has_state(state_id)? {
90            return Ok(());
91        }
92
93        // Get the state from source
94        let state = self
95            .source
96            .store()
97            .get_state(state_id)?
98            .ok_or_else(|| anyhow!("State {} not found in source", state_id))?;
99
100        // Copy tree recursively
101        self.copy_tree_recursive(target, &state.tree, copied)?;
102        if let Some(provenance_root) = state.provenance {
103            self.copy_tree_recursive(target, &provenance_root, copied)?;
104        }
105        if let Some(context_root) = state.context {
106            self.copy_tree_recursive(target, &context_root, copied)?;
107        }
108
109        // Copy parent states recursively (if depth allows)
110        if let Some(depth) = max_depth {
111            if depth > 0 {
112                for parent in &state.parents {
113                    self.copy_state_recursive(target, parent, visited, copied, Some(depth - 1))?;
114                }
115            } else {
116                // Shallow state - mark parents as grafted
117                target.set_shallow(state_id, &state.parents)?;
118            }
119        } else {
120            for parent in &state.parents {
121                self.copy_state_recursive(target, parent, visited, copied, None)?;
122            }
123        }
124
125        // Store the state in target
126        target.store().put_state(&state)?;
127        *copied += 1;
128
129        Ok(())
130    }
131
132    fn copy_tree_recursive(
133        &self,
134        target: &Repository,
135        tree_hash: &ContentHash,
136        copied: &mut usize,
137    ) -> Result<()> {
138        // Check if target already has this tree
139        if target.store().has_tree(tree_hash)? {
140            return Ok(());
141        }
142
143        // Get the tree from source
144        let tree = self
145            .source
146            .store()
147            .get_tree(tree_hash)?
148            .ok_or_else(|| anyhow!("Tree {} not found in source", tree_hash))?;
149
150        // Copy all blobs and sub-trees
151        for entry in tree.entries() {
152            match entry.entry_type {
153                objects::object::EntryType::Blob => {
154                    if !target.store().has_blob(&entry.hash)? {
155                        let blob = self.source.require_blob(&entry.hash)?;
156                        target.store().put_blob(&blob)?;
157                        *copied += 1;
158                    }
159                }
160                objects::object::EntryType::Tree => {
161                    self.copy_tree_recursive(target, &entry.hash, copied)?;
162                }
163                objects::object::EntryType::Symlink => {
164                    if !target.store().has_blob(&entry.hash)? {
165                        let blob = self.source.require_blob(&entry.hash)?;
166                        target.store().put_blob(&blob)?;
167                        *copied += 1;
168                    }
169                }
170            }
171        }
172
173        // Store the tree in target
174        target.store().put_tree(&tree)?;
175        *copied += 1;
176
177        Ok(())
178    }
179
180    /// Copy a specific blob from source to target.
181    pub fn copy_blob(&self, target: &Repository, hash: &ContentHash) -> Result<bool> {
182        if target.store().has_blob(hash)? {
183            return Ok(false);
184        }
185
186        let blob = self.source.require_blob(hash)?;
187
188        target.store().put_blob(&blob)?;
189        Ok(true)
190    }
191}