cargo/sources/registry/git_remote.rs
1//! Access to a Git index based registry. See [`GitRegistry`] for details.
2
3use crate::sources::git;
4use crate::sources::git::fetch::RemoteKind;
5use crate::sources::git::resolve_ref;
6use crate::sources::registry::MaybeLock;
7use crate::sources::registry::download;
8use crate::sources::registry::{LoadResponse, RegistryConfig, RegistryData};
9use crate::util::cache_lock::CacheLockMode;
10use crate::util::errors::CargoResult;
11use crate::util::interning::InternedString;
12use crate::util::{Filesystem, GlobalContext};
13use crate::workspace::global_cache_tracker;
14use crate::workspace::{GitReference, PackageId, SourceId};
15use anyhow::Context as _;
16use cargo_util::paths;
17use std::cell::{Cell, Ref, RefCell};
18use std::fs::File;
19use std::mem;
20use std::path::Path;
21use std::str;
22use tracing::{debug, trace};
23
24/// A remote registry is a registry that lives at a remote URL (such as
25/// crates.io). The git index is cloned locally, and `.crate` files are
26/// downloaded as needed and cached locally.
27///
28/// This type is primarily accessed through the [`RegistryData`] trait.
29///
30/// See the [module-level documentation](super) for the index format and layout.
31///
32/// ## History of Git-based index registry
33///
34/// Using Git to host this index used to be quite efficient. The full index can
35/// be stored efficiently locally on disk, and once it is downloaded, all
36/// queries of a registry can happen locally and needn't touch the network.
37/// Git-based index was a reasonable design choice at the time when HTTP/2
38/// was just introduced.
39///
40/// However, the full index keeps growing as crates.io grows. It becomes
41/// relatively big and slows down the first use of Cargo. Git (specifically
42/// libgit2) is not efficient at handling huge amounts of small files either.
43/// On the other hand, newer protocols like HTTP/2 are prevalent and capable to
44/// serve a bunch of tiny files. Today, it is encouraged to use [`HttpRegistry`],
45/// which is the default from 1.70.0. That being said, Cargo will continue
46/// supporting Git-based index for a pretty long while.
47///
48/// [`HttpRegistry`]: super::http_remote::HttpRegistry
49pub struct GitRegistry<'gctx> {
50 /// The name of this source, a unique string (across all sources) used as
51 /// the directory name where its cached content is stored.
52 name: InternedString,
53 /// Path to the registry index (`$CARGO_HOME/registry/index/$REG-HASH`).
54 index_path: Filesystem,
55 /// Path to the cache of `.crate` files (`$CARGO_HOME/registry/cache/$REG-HASH`).
56 cache_path: Filesystem,
57 /// The unique identifier of this registry source.
58 source_id: SourceId,
59 /// This reference is stored so that when a registry needs update, it knows
60 /// where to fetch from.
61 index_git_ref: GitReference,
62 gctx: &'gctx GlobalContext,
63 /// A Git [tree object] to help this registry find crate metadata from the
64 /// underlying Git repository.
65 ///
66 /// This is stored here to prevent Git from repeatedly creating a tree object
67 /// during each call into `load()`.
68 ///
69 /// [tree object]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects#_tree_objects
70 tree: RefCell<Option<git2::Tree<'static>>>,
71 /// A Git repository that contains the actual index we want.
72 repo: RefCell<Option<git2::Repository>>,
73 /// The current HEAD commit of the underlying Git repository.
74 head: Cell<Option<git2::Oid>>,
75 /// This stores sha value of the current HEAD commit for convenience.
76 current_sha: Cell<Option<InternedString>>,
77 /// Whether this registry needs to update package information.
78 ///
79 /// See [`GitRegistry::mark_updated`] on how to make sure a registry
80 /// index is updated only once per session.
81 needs_update: Cell<bool>,
82 /// Disables status messages.
83 quiet: bool,
84}
85
86impl<'gctx> GitRegistry<'gctx> {
87 /// Creates a Git-rebased remote registry for `source_id`.
88 ///
89 /// * `name` --- Name of a path segment where `.crate` tarballs and the
90 /// registry index are stored. Expect to be unique.
91 pub fn new(source_id: SourceId, gctx: &'gctx GlobalContext, name: &str) -> GitRegistry<'gctx> {
92 GitRegistry {
93 name: name.into(),
94 index_path: gctx.registry_index_path().join(name),
95 cache_path: gctx.registry_cache_path().join(name),
96 source_id,
97 gctx,
98 index_git_ref: GitReference::DefaultBranch,
99 tree: RefCell::new(None),
100 repo: RefCell::new(None),
101 head: Cell::new(None),
102 current_sha: Cell::new(None),
103 needs_update: Cell::new(false),
104 quiet: false,
105 }
106 }
107
108 /// Creates intermediate dirs and initialize the repository.
109 fn repo(&self) -> CargoResult<Ref<'_, Option<git2::Repository>>> {
110 if self.repo.borrow().is_none() {
111 trace!("acquiring registry index lock");
112 let path = self
113 .gctx
114 .assert_package_cache_locked(CacheLockMode::DownloadExclusive, &self.index_path);
115
116 self.repo.replace(Some(match git2::Repository::open(&path) {
117 Ok(repo) => repo,
118 Err(_) => {
119 drop(paths::remove_dir_all(&path));
120 paths::create_dir_all(&path)?;
121
122 // Note that we'd actually prefer to use a bare repository
123 // here as we're not actually going to check anything out.
124 // All versions of Cargo, though, share the same CARGO_HOME,
125 // so for compatibility with older Cargo which *does* do
126 // checkouts we make sure to initialize a new full
127 // repository (not a bare one).
128 //
129 // We should change this to `init_bare` whenever we feel
130 // like enough time has passed or if we change the directory
131 // that the folder is located in, such as by changing the
132 // hash at the end of the directory.
133 //
134 // Note that in the meantime we also skip `init.templatedir`
135 // as it can be misconfigured sometimes or otherwise add
136 // things that we don't want.
137 let mut opts = git2::RepositoryInitOptions::new();
138 opts.external_template(false);
139 git2::Repository::init_opts(&path, &opts).with_context(|| {
140 format!("failed to initialize index git repository (in {:?})", path)
141 })?
142 }
143 }));
144 }
145
146 Ok(self.repo.borrow())
147 }
148
149 /// Get the object ID of the HEAD commit from the underlying Git repository.
150 fn head(&self) -> CargoResult<git2::Oid> {
151 if self.head.get().is_none() {
152 let repo = self.repo()?;
153 let repo = repo.as_ref().unwrap();
154 let oid = resolve_ref(&self.index_git_ref, repo)?;
155 self.head.set(Some(oid));
156 }
157 Ok(self.head.get().unwrap())
158 }
159
160 /// Returns a [`git2::Tree`] object of the current HEAD commit of the
161 /// underlying Git repository.
162 fn tree(&self) -> CargoResult<Ref<'_, git2::Tree<'_>>> {
163 {
164 let tree = self.tree.borrow();
165 if tree.is_some() {
166 return Ok(Ref::map(tree, |s| s.as_ref().unwrap()));
167 }
168 }
169 let repo = self.repo()?;
170 let repo = repo.as_ref().unwrap();
171 let commit = repo.find_commit(self.head()?)?;
172 let tree = commit.tree()?;
173
174 // SAFETY:
175 // Unfortunately in libgit2 the tree objects look like they've got a
176 // reference to the repository object which means that a tree cannot
177 // outlive the repository that it came from. Here we want to cache this
178 // tree, though, so to accomplish this we transmute it to a static
179 // lifetime.
180 //
181 // Note that we don't actually hand out the static lifetime, instead we
182 // only return a scoped one from this function. Additionally the repo
183 // we loaded from (above) lives as long as this object
184 // (`GitRegistry`) so we then just need to ensure that the tree is
185 // destroyed first in the destructor, hence the destructor on
186 // `GitRegistry` below.
187 let tree = unsafe { mem::transmute::<git2::Tree<'_>, git2::Tree<'static>>(tree) };
188 *self.tree.borrow_mut() = Some(tree);
189 Ok(Ref::map(self.tree.borrow(), |s| s.as_ref().unwrap()))
190 }
191
192 /// Gets the current version of the registry index.
193 ///
194 /// It is usually sha of the HEAD commit from the underlying Git repository.
195 fn current_version(&self) -> Option<InternedString> {
196 if let Some(sha) = self.current_sha.get() {
197 return Some(sha);
198 }
199 let sha = self.head().ok()?.to_string().into();
200 self.current_sha.set(Some(sha));
201 Some(sha)
202 }
203
204 /// Whether the registry is up-to-date. See [`Self::mark_updated`] for more.
205 fn is_updated(&self) -> bool {
206 self.gctx.updated_sources().contains(&self.source_id)
207 }
208
209 /// Marks this registry as up-to-date.
210 ///
211 /// This makes sure the index is only updated once per session since it is
212 /// an expensive operation. This generally only happens when the resolver
213 /// is run multiple times, such as during `cargo publish`.
214 fn mark_updated(&self) {
215 self.gctx.updated_sources().insert(self.source_id);
216 }
217
218 fn update(&self) -> CargoResult<()> {
219 if !self.needs_update.get() {
220 return Ok(());
221 }
222
223 self.needs_update.set(false);
224
225 if self.is_updated() {
226 return Ok(());
227 }
228 self.mark_updated();
229
230 if !self.gctx.network_allowed() {
231 return Ok(());
232 }
233 if self.gctx.cli_unstable().no_index_update {
234 return Ok(());
235 }
236
237 debug!("updating the index");
238
239 // Ensure that we'll actually be able to acquire an HTTP handle later on
240 // once we start trying to download crates. This will weed out any
241 // problems with `.cargo/config` configuration related to HTTP.
242 //
243 // This way if there's a problem the error gets printed before we even
244 // hit the index, which may not actually read this configuration.
245 self.gctx.http()?;
246
247 self.prepare()?;
248 self.head.set(None);
249 *self.tree.borrow_mut() = None;
250 self.current_sha.set(None);
251 let _path = self
252 .gctx
253 .assert_package_cache_locked(CacheLockMode::DownloadExclusive, &self.index_path);
254 if !self.quiet {
255 self.gctx
256 .shell()
257 .status("Updating", self.source_id.display_index())?;
258 }
259
260 // Fetch the latest version of our `index_git_ref` into the index
261 // checkout.
262 let url = self.source_id.url();
263 let mut repo = self.repo.borrow_mut();
264 let repo = repo.as_mut().unwrap();
265 git::fetch(
266 repo,
267 url.as_str(),
268 &self.index_git_ref,
269 &self.index_git_ref,
270 self.gctx,
271 RemoteKind::Registry,
272 )
273 .with_context(|| format!("failed to fetch `{}`", url))?;
274
275 Ok(())
276 }
277}
278
279#[async_trait::async_trait(?Send)]
280impl<'gctx> RegistryData for GitRegistry<'gctx> {
281 fn prepare(&self) -> CargoResult<()> {
282 self.repo()?;
283 self.gctx
284 .deferred_global_last_use()?
285 .mark_registry_index_used(global_cache_tracker::RegistryIndex {
286 encoded_registry_name: self.name,
287 });
288 Ok(())
289 }
290
291 fn index_path(&self) -> &Filesystem {
292 &self.index_path
293 }
294
295 fn cache_path(&self) -> &Filesystem {
296 &self.cache_path
297 }
298
299 fn assert_index_locked<'a>(&self, path: &'a Filesystem) -> &'a Path {
300 self.gctx
301 .assert_package_cache_locked(CacheLockMode::DownloadExclusive, path)
302 }
303
304 /// Read the general concept for `load()` on [`RegistryData::load`].
305 ///
306 /// `index_version` is a string representing the version of the file used
307 /// to construct the cached copy.
308 ///
309 /// Older versions of Cargo used the single value of the hash of the HEAD
310 /// commit as a `index_version`. This is technically correct but a little
311 /// too conservative. If a new commit is fetched all cached files need to
312 /// be regenerated even if a particular file was not changed.
313 ///
314 /// However if an old cargo has written such a file we still know how to
315 /// read it, as long as we check for that hash value.
316 ///
317 /// Cargo now uses a hash of the file's contents as provided by git.
318 async fn load(
319 &self,
320 _root: &Path,
321 path: &Path,
322 index_version: Option<&str>,
323 ) -> CargoResult<LoadResponse> {
324 if self.needs_update.get() {
325 self.update()?;
326 }
327 // Check if the cache is valid.
328 let git_commit_hash = self.current_version();
329 if index_version.is_some() && index_version == git_commit_hash.as_deref() {
330 // This file was written by an old version of cargo, but it is
331 // still up-to-date.
332 return Ok(LoadResponse::CacheValid);
333 }
334 // Note that the index calls this method and the filesystem is locked
335 // in the index, so we don't need to worry about an `update_index`
336 // happening in a different process.
337 fn load_helper(
338 registry: &GitRegistry<'_>,
339 path: &Path,
340 index_version: Option<&str>,
341 ) -> CargoResult<LoadResponse> {
342 let repo = registry.repo()?;
343 let repo = repo.as_ref().unwrap();
344 let tree = registry.tree()?;
345 let entry = tree.get_path(path);
346 let entry = entry?;
347 let git_file_hash = Some(entry.id().to_string());
348
349 // Check if the cache is valid.
350 if index_version.is_some() && index_version == git_file_hash.as_deref() {
351 return Ok(LoadResponse::CacheValid);
352 }
353
354 let object = entry.to_object(repo)?;
355 let Some(blob) = object.as_blob() else {
356 anyhow::bail!("path `{}` is not a blob in the git repo", path.display())
357 };
358
359 Ok(LoadResponse::Data {
360 raw_data: blob.content().to_vec(),
361 index_version: git_file_hash,
362 })
363 }
364
365 loop {
366 return match load_helper(&self, path, index_version) {
367 Ok(result) => Ok(result),
368 Err(_) if !self.is_updated() => {
369 // If git returns an error and we haven't updated the repo,
370 // return pending to allow an update to try again.
371 self.needs_update.set(true);
372 self.update()?;
373 continue;
374 }
375 Err(e)
376 if e.downcast_ref::<git2::Error>()
377 .map(|e| e.code() == git2::ErrorCode::NotFound)
378 .unwrap_or_default() =>
379 {
380 // The repo has been updated and the file does not exist.
381 Ok(LoadResponse::NotFound)
382 }
383 Err(e) => Err(e),
384 };
385 }
386 }
387
388 async fn config(&self) -> CargoResult<Option<RegistryConfig>> {
389 debug!("loading config");
390 self.prepare()?;
391 self.gctx
392 .assert_package_cache_locked(CacheLockMode::DownloadExclusive, &self.index_path);
393 match self
394 .load(Path::new(""), Path::new(RegistryConfig::NAME), None)
395 .await?
396 {
397 LoadResponse::Data { raw_data, .. } => {
398 trace!("config loaded");
399 let cfg: RegistryConfig = serde_json::from_slice(&raw_data)?;
400 Ok(Some(cfg))
401 }
402 _ => Ok(None),
403 }
404 }
405
406 /// Read the general concept for `invalidate_cache()` on
407 /// [`RegistryData::invalidate_cache`].
408 ///
409 /// To fully invalidate, undo [`GitRegistry::mark_updated`]'s work.
410 fn invalidate_cache(&self) {
411 self.needs_update.set(true);
412 }
413
414 fn set_quiet(&mut self, quiet: bool) {
415 self.quiet = quiet;
416 }
417
418 fn is_updated(&self) -> bool {
419 self.is_updated()
420 }
421
422 async fn download(&self, pkg: PackageId, checksum: &str) -> CargoResult<MaybeLock> {
423 let registry_config = self.config().await?.unwrap();
424
425 download::download(
426 &self.cache_path,
427 &self.gctx,
428 self.name,
429 pkg,
430 checksum,
431 registry_config,
432 )
433 }
434
435 async fn finish_download(
436 &self,
437 pkg: PackageId,
438 checksum: &str,
439 data: &[u8],
440 ) -> CargoResult<File> {
441 download::finish_download(
442 &self.cache_path,
443 &self.gctx,
444 self.name.clone(),
445 pkg,
446 checksum,
447 data,
448 )
449 }
450
451 fn is_crate_downloaded(&self, pkg: PackageId) -> bool {
452 download::is_crate_downloaded(&self.cache_path, &self.gctx, pkg)
453 }
454}
455
456/// Implemented to just be sure to drop `tree` field before our other fields.
457/// See SAFETY inside [`GitRegistry::tree()`] for more.
458impl<'gctx> Drop for GitRegistry<'gctx> {
459 fn drop(&mut self) {
460 self.tree.borrow_mut().take();
461 }
462}